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.draglinearlayout)
|
||||||
implementation(libs.viewpropertyobjectanimator)
|
implementation(libs.viewpropertyobjectanimator)
|
||||||
|
|
||||||
implementation(libs.bundles.koin)
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
implementation(project(":applications"))
|
implementation(project(":applications"))
|
||||||
implementation(project(":appsearch"))
|
implementation(project(":appsearch"))
|
||||||
@ -126,6 +126,7 @@ dependencies {
|
|||||||
implementation(project(":calendar"))
|
implementation(project(":calendar"))
|
||||||
implementation(project(":contacts"))
|
implementation(project(":contacts"))
|
||||||
implementation(project(":crashreporter"))
|
implementation(project(":crashreporter"))
|
||||||
|
implementation(project(":currencies"))
|
||||||
implementation(project(":favorites"))
|
implementation(project(":favorites"))
|
||||||
implementation(project(":files"))
|
implementation(project(":files"))
|
||||||
implementation(project(":g-services"))
|
implementation(project(":g-services"))
|
||||||
|
|||||||
@ -7,12 +7,29 @@ import android.content.Intent
|
|||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import de.mm20.launcher2.applications.applicationsModule
|
||||||
|
import de.mm20.launcher2.badges.badgesModule
|
||||||
|
import de.mm20.launcher2.calculator.calculatorModule
|
||||||
|
import de.mm20.launcher2.calendar.calendarModule
|
||||||
|
import de.mm20.launcher2.contacts.contactsModule
|
||||||
import de.mm20.launcher2.debug.Debug
|
import de.mm20.launcher2.debug.Debug
|
||||||
import de.mm20.launcher2.icons.IconRepository
|
import de.mm20.launcher2.favorites.favoritesModule
|
||||||
|
import de.mm20.launcher2.files.filesModule
|
||||||
|
import de.mm20.launcher2.hiddenitems.hiddenItemsModule
|
||||||
|
import de.mm20.launcher2.icons.iconsModule
|
||||||
|
import de.mm20.launcher2.music.musicModule
|
||||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||||
import de.mm20.launcher2.preferences.Themes
|
import de.mm20.launcher2.preferences.Themes
|
||||||
|
import de.mm20.launcher2.search.searchModule
|
||||||
import de.mm20.launcher2.ui.legacy.helper.WallpaperBlur
|
import de.mm20.launcher2.ui.legacy.helper.WallpaperBlur
|
||||||
|
import de.mm20.launcher2.unitconverter.unitConverterModule
|
||||||
|
import de.mm20.launcher2.websites.websitesModule
|
||||||
|
import de.mm20.launcher2.widgets.widgetsModule
|
||||||
|
import de.mm20.launcher2.wikipedia.wikipediaModule
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import org.koin.android.ext.koin.androidContext
|
||||||
|
import org.koin.android.ext.koin.androidLogger
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
import java.text.Collator
|
import java.text.Collator
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
@ -23,29 +40,12 @@ class LauncherApplication : Application(), CoroutineScope {
|
|||||||
|
|
||||||
var blurredWallpaper: Bitmap? = null
|
var blurredWallpaper: Bitmap? = null
|
||||||
|
|
||||||
|
|
||||||
private val appReceiver = object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
|
||||||
IconRepository.getInstance(this@LauncherApplication).requestIconPackListUpdate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
Debug()
|
Debug()
|
||||||
instance = this
|
instance = this
|
||||||
LauncherPreferences.initialize(this)
|
LauncherPreferences.initialize(this)
|
||||||
IconRepository.getInstance(this).requestIconPackListUpdate()
|
|
||||||
|
|
||||||
registerReceiver(appReceiver, IntentFilter().apply {
|
|
||||||
addAction(Intent.ACTION_PACKAGE_REPLACED)
|
|
||||||
addAction(Intent.ACTION_PACKAGE_ADDED)
|
|
||||||
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
|
||||||
addAction(Intent.ACTION_MY_PACKAGE_REPLACED)
|
|
||||||
addAction(Intent.ACTION_PACKAGE_CHANGED)
|
|
||||||
addDataScheme("package")
|
|
||||||
})
|
|
||||||
val theme = LauncherPreferences.instance.theme
|
val theme = LauncherPreferences.instance.theme
|
||||||
AppCompatDelegate.setDefaultNightMode(
|
AppCompatDelegate.setDefaultNightMode(
|
||||||
when (theme) {
|
when (theme) {
|
||||||
@ -58,6 +58,30 @@ class LauncherApplication : Application(), CoroutineScope {
|
|||||||
WallpaperBlur.requestBlur(this)
|
WallpaperBlur.requestBlur(this)
|
||||||
@Suppress("DEPRECATION") // We need to access the wallpaper directly to blur it
|
@Suppress("DEPRECATION") // We need to access the wallpaper directly to blur it
|
||||||
registerReceiver(WallpaperReceiver(), IntentFilter(Intent.ACTION_WALLPAPER_CHANGED))
|
registerReceiver(WallpaperReceiver(), IntentFilter(Intent.ACTION_WALLPAPER_CHANGED))
|
||||||
|
|
||||||
|
startKoin {
|
||||||
|
androidLogger()
|
||||||
|
androidContext(this@LauncherApplication)
|
||||||
|
modules(
|
||||||
|
listOf(
|
||||||
|
applicationsModule,
|
||||||
|
calculatorModule,
|
||||||
|
badgesModule,
|
||||||
|
calendarModule,
|
||||||
|
contactsModule,
|
||||||
|
favoritesModule,
|
||||||
|
filesModule,
|
||||||
|
hiddenItemsModule,
|
||||||
|
iconsModule,
|
||||||
|
musicModule,
|
||||||
|
searchModule,
|
||||||
|
unitConverterModule,
|
||||||
|
websitesModule,
|
||||||
|
widgetsModule,
|
||||||
|
wikipediaModule
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@ -7,9 +7,12 @@ import android.os.Build
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import de.mm20.launcher2.favorites.FavoritesRepository
|
import de.mm20.launcher2.favorites.FavoritesRepository
|
||||||
import de.mm20.launcher2.search.data.AppShortcut
|
import de.mm20.launcher2.search.data.AppShortcut
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
class AddItemActivity : Activity() {
|
class AddItemActivity : Activity() {
|
||||||
|
|
||||||
|
val favoritesRepository: FavoritesRepository by inject()
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
@ -20,7 +23,7 @@ class AddItemActivity : Activity() {
|
|||||||
packageManager.getApplicationInfo(shortcutInfo.`package`, 0)
|
packageManager.getApplicationInfo(shortcutInfo.`package`, 0)
|
||||||
.loadLabel(packageManager).toString())
|
.loadLabel(packageManager).toString())
|
||||||
if (pinRequest.accept()) {
|
if (pinRequest.accept()) {
|
||||||
FavoritesRepository.getInstance(this).pinItem(shortcut)
|
favoritesRepository.pinItem(shortcut)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finish()
|
finish()
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import de.mm20.launcher2.preferences.LauncherPreferences
|
|||||||
import de.mm20.launcher2.preferences.Themes
|
import de.mm20.launcher2.preferences.Themes
|
||||||
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
|
import de.mm20.launcher2.ui.legacy.view.LauncherIconView
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
class PreferencesAppearanceFragment : PreferenceFragmentCompat() {
|
class PreferencesAppearanceFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
@ -74,9 +75,10 @@ class PreferencesAppearanceFragment : PreferenceFragmentCompat() {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
val manager = IconPackManager.getInstance(requireContext())
|
val iconPackManager: IconPackManager by inject()
|
||||||
|
val iconRepository: IconRepository by inject()
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val packs = manager.getInstalledIconPacks()
|
val packs = iconPackManager.getInstalledIconPacks()
|
||||||
findPreference<ListPreference>("icon_pack")?.apply {
|
findPreference<ListPreference>("icon_pack")?.apply {
|
||||||
entries = packs.map { it.name }.toMutableList().apply { add(0, "System") }.toTypedArray()
|
entries = packs.map { it.name }.toMutableList().apply { add(0, "System") }.toTypedArray()
|
||||||
entryValues = (-1 until packs.size).map { it.toString() }.toTypedArray()
|
entryValues = (-1 until packs.size).map { it.toString() }.toTypedArray()
|
||||||
@ -86,14 +88,14 @@ class PreferencesAppearanceFragment : PreferenceFragmentCompat() {
|
|||||||
} else {
|
} else {
|
||||||
isEnabled = true
|
isEnabled = true
|
||||||
summary = "%s"
|
summary = "%s"
|
||||||
value = packs.indexOfFirst { it.packageName == manager.selectedIconPack }.toString()
|
value = packs.indexOfFirst { it.packageName == iconPackManager.selectedIconPack }.toString()
|
||||||
}
|
}
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
val index = (newValue as String).toInt()
|
val index = (newValue as String).toInt()
|
||||||
IconRepository.getInstance(requireContext()).clearCache()
|
iconRepository.clearCache()
|
||||||
if (index == -1) manager.selectIconPack("")
|
if (index == -1) iconPackManager.selectIconPack("")
|
||||||
else {
|
else {
|
||||||
manager.selectIconPack(packs[index].packageName)
|
iconPackManager.selectIconPack(packs[index].packageName)
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@ -101,7 +103,7 @@ class PreferencesAppearanceFragment : PreferenceFragmentCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
findPreference<Preference>("legacy_icon_bg")?.setOnPreferenceChangeListener { _, _ ->
|
findPreference<Preference>("legacy_icon_bg")?.setOnPreferenceChangeListener { _, _ ->
|
||||||
IconRepository.getInstance(requireContext()).clearCache()
|
iconRepository.clearCache()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package de.mm20.launcher2.fragment
|
package de.mm20.launcher2.fragment
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
@ -7,32 +7,35 @@ import androidx.preference.PreferenceFragmentCompat
|
|||||||
import de.mm20.launcher2.R
|
import de.mm20.launcher2.R
|
||||||
import de.mm20.launcher2.badges.BadgeProvider
|
import de.mm20.launcher2.badges.BadgeProvider
|
||||||
import de.mm20.launcher2.notifications.NotificationService
|
import de.mm20.launcher2.notifications.NotificationService
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
class PreferencesBadgesFragment : PreferenceFragmentCompat() {
|
class PreferencesBadgesFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
|
private val badgesProvider: BadgeProvider by inject()
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
addPreferencesFromResource(R.xml.preferences_badges)
|
addPreferencesFromResource(R.xml.preferences_badges)
|
||||||
findPreference<Preference>("notification_badges")?.setOnPreferenceChangeListener { _, newValue ->
|
findPreference<Preference>("notification_badges")?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
if (newValue as Boolean) {
|
if (newValue as Boolean) {
|
||||||
de.mm20.launcher2.notifications.NotificationService.getInstance()?.generateBadges()
|
NotificationService.getInstance()?.generateBadges()
|
||||||
} else {
|
} else {
|
||||||
BadgeProvider.getInstance(requireContext()).removeNotificationBadges()
|
badgesProvider.removeNotificationBadges()
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
findPreference<Preference>("suspended_badges")?.setOnPreferenceChangeListener { _, newValue ->
|
findPreference<Preference>("suspended_badges")?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
if (newValue as Boolean) {
|
if (newValue as Boolean) {
|
||||||
BadgeProvider.getInstance(requireContext()).addSuspendBadges()
|
badgesProvider.addSuspendBadges()
|
||||||
} else {
|
} else {
|
||||||
BadgeProvider.getInstance(requireContext()).removeSuspendBadges()
|
badgesProvider.removeSuspendBadges()
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
findPreference<Preference>("cloud_badges")?.setOnPreferenceChangeListener { _, newValue ->
|
findPreference<Preference>("cloud_badges")?.setOnPreferenceChangeListener { _, newValue ->
|
||||||
if (newValue as Boolean) {
|
if (newValue as Boolean) {
|
||||||
BadgeProvider.getInstance(requireContext()).addCloudBadges()
|
badgesProvider.addCloudBadges()
|
||||||
} else {
|
} else {
|
||||||
BadgeProvider.getInstance(requireContext()).removeCloudBadges()
|
badgesProvider.removeCloudBadges()
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
@ -42,6 +45,6 @@ class PreferencesBadgesFragment : PreferenceFragmentCompat() {
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
(activity as AppCompatActivity).supportActionBar
|
(activity as AppCompatActivity).supportActionBar
|
||||||
?.setTitle(R.string.preference_screen_badges)
|
?.setTitle(R.string.preference_screen_badges)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -19,9 +19,12 @@ import de.mm20.launcher2.nextcloud.NextcloudApiHelper
|
|||||||
import de.mm20.launcher2.owncloud.OwncloudClient
|
import de.mm20.launcher2.owncloud.OwncloudClient
|
||||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
class PreferencesSearchFragment : PreferenceFragmentCompat() {
|
class PreferencesSearchFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
|
private val googleApiHelper: GoogleApiHelper by inject()
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
super.onViewCreated(view, savedInstanceState)
|
super.onViewCreated(view, savedInstanceState)
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
|
|||||||
@ -17,7 +17,6 @@ import android.widget.ImageView
|
|||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.graphics.scale
|
import androidx.core.graphics.scale
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
@ -29,9 +28,9 @@ import com.bumptech.glide.request.target.SimpleTarget
|
|||||||
import com.bumptech.glide.request.transition.Transition
|
import com.bumptech.glide.request.transition.Transition
|
||||||
import de.mm20.launcher2.R
|
import de.mm20.launcher2.R
|
||||||
import de.mm20.launcher2.ktx.dp
|
import de.mm20.launcher2.ktx.dp
|
||||||
import de.mm20.launcher2.search.SearchViewModel
|
|
||||||
import de.mm20.launcher2.search.WebsearchViewModel
|
import de.mm20.launcher2.search.WebsearchViewModel
|
||||||
import de.mm20.launcher2.search.data.Websearch
|
import de.mm20.launcher2.search.data.Websearch
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
@ -42,9 +41,7 @@ class PreferencesWebSearchesFragment : PreferenceFragmentCompat() {
|
|||||||
|
|
||||||
private var sheetIcon: WeakReference<ImageView>? = null
|
private var sheetIcon: WeakReference<ImageView>? = null
|
||||||
|
|
||||||
private val viewModel by lazy {
|
private val viewModel: WebsearchViewModel by viewModel()
|
||||||
ViewModelProvider(context as AppCompatActivity)[WebsearchViewModel::class.java]
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
preferenceScreen = preferenceManager.createPreferenceScreen(activity)
|
preferenceScreen = preferenceManager.createPreferenceScreen(activity)
|
||||||
@ -61,19 +58,23 @@ class PreferencesWebSearchesFragment : PreferenceFragmentCompat() {
|
|||||||
val pref = Preference(context)
|
val pref = Preference(context)
|
||||||
pref.title = search.label
|
pref.title = search.label
|
||||||
if (search.icon == null) {
|
if (search.icon == null) {
|
||||||
val drawable = resources.getDrawable(R.drawable.ic_search, requireActivity().theme).mutate()
|
val drawable =
|
||||||
|
resources.getDrawable(R.drawable.ic_search, requireActivity().theme).mutate()
|
||||||
drawable.setTintMode(PorterDuff.Mode.SRC_ATOP)
|
drawable.setTintMode(PorterDuff.Mode.SRC_ATOP)
|
||||||
drawable.setTint(search.color)
|
drawable.setTint(search.color)
|
||||||
pref.icon = drawable
|
pref.icon = drawable
|
||||||
} else {
|
} else {
|
||||||
Glide.with(requireContext())
|
Glide.with(requireContext())
|
||||||
.asDrawable()
|
.asDrawable()
|
||||||
.load(search.icon)
|
.load(search.icon)
|
||||||
.into(object : SimpleTarget<Drawable>() {
|
.into(object : SimpleTarget<Drawable>() {
|
||||||
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
override fun onResourceReady(
|
||||||
pref.icon = resource
|
resource: Drawable,
|
||||||
}
|
transition: Transition<in Drawable>?
|
||||||
})
|
) {
|
||||||
|
pref.icon = resource
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
pref.setOnPreferenceClickListener {
|
pref.setOnPreferenceClickListener {
|
||||||
editSearch(search)
|
editSearch(search)
|
||||||
@ -109,23 +110,23 @@ class PreferencesWebSearchesFragment : PreferenceFragmentCompat() {
|
|||||||
imageTintList = ColorStateList.valueOf(websearch.color)
|
imageTintList = ColorStateList.valueOf(websearch.color)
|
||||||
} else {
|
} else {
|
||||||
Glide.with(this)
|
Glide.with(this)
|
||||||
.load(websearch.icon)
|
.load(websearch.icon)
|
||||||
.into(this)
|
.into(this)
|
||||||
}
|
}
|
||||||
sheetIcon = WeakReference(this)
|
sheetIcon = WeakReference(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
val sheet = MaterialDialog(requireContext(), BottomSheet())
|
val sheet = MaterialDialog(requireContext(), BottomSheet())
|
||||||
.cornerRadius(8f)
|
.cornerRadius(8f)
|
||||||
.customView(view = dialogView)
|
.customView(view = dialogView)
|
||||||
|
|
||||||
val radius = 8 * dialogView.dp
|
val radius = 8 * dialogView.dp
|
||||||
dialogView.background = GradientDrawable().apply {
|
dialogView.background = GradientDrawable().apply {
|
||||||
cornerRadii = floatArrayOf(
|
cornerRadii = floatArrayOf(
|
||||||
radius, radius, // top left
|
radius, radius, // top left
|
||||||
radius, radius, // top right
|
radius, radius, // top right
|
||||||
0f, 0f, // bottom left
|
0f, 0f, // bottom left
|
||||||
0f, 0f // bottom right
|
0f, 0f // bottom right
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,30 +135,31 @@ class PreferencesWebSearchesFragment : PreferenceFragmentCompat() {
|
|||||||
|
|
||||||
|
|
||||||
sheet.noAutoDismiss()
|
sheet.noAutoDismiss()
|
||||||
.positiveButton(android.R.string.ok) {
|
.positiveButton(android.R.string.ok) {
|
||||||
val newUrl = urlEdit.text.toString()
|
val newUrl = urlEdit.text.toString()
|
||||||
val newName = nameEdit.text.toString()
|
val newName = nameEdit.text.toString()
|
||||||
if (!newUrl.contains("\${1}")) {
|
if (!newUrl.contains("\${1}")) {
|
||||||
urlEdit.error = getString(R.string.websearch_dialog_url_error)
|
urlEdit.error = getString(R.string.websearch_dialog_url_error)
|
||||||
return@positiveButton
|
return@positiveButton
|
||||||
|
}
|
||||||
|
File(requireContext().cacheDir, "websearch-tmp").takeIf { it.exists() }?.let {
|
||||||
|
websearch.icon?.let { File(it).takeIf { it.exists() }?.delete() }
|
||||||
|
val newFile =
|
||||||
|
File(requireContext().filesDir, "websearch-${System.currentTimeMillis()}")
|
||||||
|
it.copyTo(newFile, true)
|
||||||
|
it.delete()
|
||||||
|
newIcon = newFile.absolutePath
|
||||||
|
}
|
||||||
|
if (newIcon == null) {
|
||||||
|
websearch.icon?.let { File(it).takeIf { it.exists() }?.delete() }
|
||||||
|
}
|
||||||
|
websearch.urlTemplate = newUrl
|
||||||
|
websearch.label = newName
|
||||||
|
websearch.icon = newIcon
|
||||||
|
websearch.color = newColor
|
||||||
|
viewModel.insertWebsearch(websearch)
|
||||||
|
sheet.dismiss()
|
||||||
}
|
}
|
||||||
File(requireContext().cacheDir, "websearch-tmp").takeIf { it.exists() }?.let {
|
|
||||||
websearch.icon?.let { File(it).takeIf { it.exists() }?.delete() }
|
|
||||||
val newFile = File(requireContext().filesDir, "websearch-${System.currentTimeMillis()}")
|
|
||||||
it.copyTo(newFile, true)
|
|
||||||
it.delete()
|
|
||||||
newIcon = newFile.absolutePath
|
|
||||||
}
|
|
||||||
if (newIcon == null) {
|
|
||||||
websearch.icon?.let { File(it).takeIf { it.exists() }?.delete() }
|
|
||||||
}
|
|
||||||
websearch.urlTemplate = newUrl
|
|
||||||
websearch.label = newName
|
|
||||||
websearch.icon = newIcon
|
|
||||||
websearch.color = newColor
|
|
||||||
viewModel.insertWebsearch(websearch)
|
|
||||||
sheet.dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
sheet.negativeButton(android.R.string.cancel) {
|
sheet.negativeButton(android.R.string.cancel) {
|
||||||
sheet.cancel()
|
sheet.cancel()
|
||||||
@ -188,15 +190,16 @@ class PreferencesWebSearchesFragment : PreferenceFragmentCompat() {
|
|||||||
}
|
}
|
||||||
title(R.string.websearch_dialog_choose_icon_color)
|
title(R.string.websearch_dialog_choose_icon_color)
|
||||||
colorChooser(
|
colorChooser(
|
||||||
colors = context.resources.getIntArray(R.array.color_chooser_presets),
|
colors = context.resources.getIntArray(R.array.color_chooser_presets),
|
||||||
allowCustomArgb = true,
|
allowCustomArgb = true,
|
||||||
showAlphaSelector = false
|
showAlphaSelector = false
|
||||||
) { _, color ->
|
) { _, color ->
|
||||||
iconView.setImageResource(R.drawable.ic_search)
|
iconView.setImageResource(R.drawable.ic_search)
|
||||||
iconView.imageTintList = ColorStateList.valueOf(color)
|
iconView.imageTintList = ColorStateList.valueOf(color)
|
||||||
newColor = color
|
newColor = color
|
||||||
newIcon = null
|
newIcon = null
|
||||||
File(requireContext().cacheDir, "websearch-tmp").takeIf { it.exists() }?.delete()
|
File(requireContext().cacheDir, "websearch-tmp").takeIf { it.exists() }
|
||||||
|
?.delete()
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -207,7 +210,7 @@ class PreferencesWebSearchesFragment : PreferenceFragmentCompat() {
|
|||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
(activity as AppCompatActivity).supportActionBar
|
(activity as AppCompatActivity).supportActionBar
|
||||||
?.setTitle(R.string.preference_search_edit_websearch)
|
?.setTitle(R.string.preference_search_edit_websearch)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
@ -216,7 +219,8 @@ class PreferencesWebSearchesFragment : PreferenceFragmentCompat() {
|
|||||||
if (requestCode == 24 && resultCode == Activity.RESULT_OK && dataUri != null) {
|
if (requestCode == 24 && resultCode == Activity.RESULT_OK && dataUri != null) {
|
||||||
val stream = requireActivity().contentResolver.openInputStream(dataUri)
|
val stream = requireActivity().contentResolver.openInputStream(dataUri)
|
||||||
val icon = BitmapFactory.decodeStream(stream)
|
val icon = BitmapFactory.decodeStream(stream)
|
||||||
val scaledIcon = icon.scale((32 * requireContext().dp).toInt(), (32 * requireContext().dp).toInt())
|
val scaledIcon =
|
||||||
|
icon.scale((32 * requireContext().dp).toInt(), (32 * requireContext().dp).toInt())
|
||||||
val out = FileOutputStream(File(requireContext().cacheDir, "websearch-tmp"))
|
val out = FileOutputStream(File(requireContext().cacheDir, "websearch-tmp"))
|
||||||
scaledIcon.compress(Bitmap.CompressFormat.PNG, 100, out)
|
scaledIcon.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||||
out.close()
|
out.close()
|
||||||
|
|||||||
@ -42,6 +42,8 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.bundles.androidx.lifecycle)
|
implementation(libs.bundles.androidx.lifecycle)
|
||||||
|
|
||||||
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
implementation(project(":search"))
|
implementation(project(":search"))
|
||||||
implementation(project(":base"))
|
implementation(project(":base"))
|
||||||
implementation(project(":icons"))
|
implementation(project(":icons"))
|
||||||
|
|||||||
@ -25,7 +25,12 @@ import de.mm20.launcher2.search.data.LauncherApp
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class AppRepository private constructor(val context: Context) : BaseSearchableRepository() {
|
class AppRepository(
|
||||||
|
val context: Context,
|
||||||
|
val iconRepository: IconRepository,
|
||||||
|
hiddenItemsRepository: HiddenItemsRepository,
|
||||||
|
badgeProvider: BadgeProvider
|
||||||
|
) : BaseSearchableRepository() {
|
||||||
|
|
||||||
private val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
private val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
||||||
|
|
||||||
@ -34,7 +39,7 @@ class AppRepository private constructor(val context: Context) : BaseSearchableRe
|
|||||||
|
|
||||||
private val installedApps = MutableLiveData<List<Application>>(emptyList())
|
private val installedApps = MutableLiveData<List<Application>>(emptyList())
|
||||||
private val installations = MutableLiveData<MutableList<AppInstallation>>(mutableListOf())
|
private val installations = MutableLiveData<MutableList<AppInstallation>>(mutableListOf())
|
||||||
private val hiddenItemKeys = HiddenItemsRepository.getInstance(context).hiddenItemsKeys
|
private val hiddenItemKeys = hiddenItemsRepository.hiddenItemsKeys
|
||||||
|
|
||||||
private val installingPackages = mutableMapOf<Int, String>()
|
private val installingPackages = mutableMapOf<Int, String>()
|
||||||
|
|
||||||
@ -97,14 +102,14 @@ class AppRepository private constructor(val context: Context) : BaseSearchableRe
|
|||||||
override fun onPackagesSuspended(packageNames: Array<out String>?, user: UserHandle?) {
|
override fun onPackagesSuspended(packageNames: Array<out String>?, user: UserHandle?) {
|
||||||
super.onPackagesSuspended(packageNames, user)
|
super.onPackagesSuspended(packageNames, user)
|
||||||
packageNames?.forEach {
|
packageNames?.forEach {
|
||||||
BadgeProvider.getInstance(context).setBadge("app://$it", Badge(iconRes = R.drawable.ic_badge_suspended))
|
badgeProvider.setBadge("app://$it", Badge(iconRes = R.drawable.ic_badge_suspended))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPackagesUnsuspended(packageNames: Array<out String>?, user: UserHandle?) {
|
override fun onPackagesUnsuspended(packageNames: Array<out String>?, user: UserHandle?) {
|
||||||
super.onPackagesUnsuspended(packageNames, user)
|
super.onPackagesUnsuspended(packageNames, user)
|
||||||
packageNames?.forEach {
|
packageNames?.forEach {
|
||||||
BadgeProvider.getInstance(context).removeBadge("app://$it")
|
badgeProvider.removeBadge("app://$it")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,7 +122,7 @@ class AppRepository private constructor(val context: Context) : BaseSearchableRe
|
|||||||
override fun onProgressChanged(sessionId: Int, progress: Float) {
|
override fun onProgressChanged(sessionId: Int, progress: Float) {
|
||||||
val session = packageInstaller.getSessionInfo(sessionId) ?: return
|
val session = packageInstaller.getSessionInfo(sessionId) ?: return
|
||||||
val pkg = session.appPackageName ?: return
|
val pkg = session.appPackageName ?: return
|
||||||
BadgeProvider.getInstance(context).updateBadge("app://$pkg", Badge(progress = progress))
|
badgeProvider.updateBadge("app://$pkg", Badge(progress = progress))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActiveChanged(sessionId: Int, active: Boolean) {
|
override fun onActiveChanged(sessionId: Int, active: Boolean) {
|
||||||
@ -129,9 +134,9 @@ class AppRepository private constructor(val context: Context) : BaseSearchableRe
|
|||||||
val pkg = installingPackages[sessionId]
|
val pkg = installingPackages[sessionId]
|
||||||
installingPackages.remove(sessionId)
|
installingPackages.remove(sessionId)
|
||||||
val key = "app://$pkg"
|
val key = "app://$pkg"
|
||||||
val badge = BadgeProvider.getInstance(context).getBadge(key)?.apply { progress = null }
|
val badge = badgeProvider.getBadge(key)?.apply { progress = null }
|
||||||
?: Badge()
|
?: Badge()
|
||||||
BadgeProvider.getInstance(context).setBadge(key, badge)
|
badgeProvider.setBadge(key, badge)
|
||||||
val inst = installations.value ?: return
|
val inst = installations.value ?: return
|
||||||
inst.removeAll {
|
inst.removeAll {
|
||||||
it.session.sessionId == sessionId
|
it.session.sessionId == sessionId
|
||||||
@ -144,7 +149,7 @@ class AppRepository private constructor(val context: Context) : BaseSearchableRe
|
|||||||
val inst = installations.value ?: mutableListOf()
|
val inst = installations.value ?: mutableListOf()
|
||||||
inst.removeAll {
|
inst.removeAll {
|
||||||
if (it.session.sessionId == sessionId) {
|
if (it.session.sessionId == sessionId) {
|
||||||
IconRepository.getInstance(context).removeIconFromCache(it)
|
iconRepository.removeIconFromCache(it)
|
||||||
true
|
true
|
||||||
} else false
|
} else false
|
||||||
}
|
}
|
||||||
@ -173,7 +178,7 @@ class AppRepository private constructor(val context: Context) : BaseSearchableRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateAppsForDisplay() {
|
private suspend fun updateAppsForDisplay() {
|
||||||
val query = SearchRepository.getInstance().currentQuery.value ?: ""
|
val query = searchRepository.currentQuery.value ?: ""
|
||||||
|
|
||||||
val componentName = ComponentName.unflattenFromString(query)
|
val componentName = ComponentName.unflattenFromString(query)
|
||||||
|
|
||||||
@ -216,12 +221,4 @@ class AppRepository private constructor(val context: Context) : BaseSearchableRe
|
|||||||
|
|
||||||
return profiles.map { p -> launcherApps.getActivityList(packageName, p).mapNotNull { getApplication(it, p) } }.flatten()
|
return profiles.map { p -> launcherApps.getActivityList(packageName, p).mapNotNull { getApplication(it, p) } }.flatten()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private lateinit var instance: AppRepository
|
|
||||||
fun getInstance(context: Context): AppRepository {
|
|
||||||
if (!::instance.isInitialized) instance = AppRepository(context.applicationContext)
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,13 +1,12 @@
|
|||||||
package de.mm20.launcher2.applications
|
package de.mm20.launcher2.applications
|
||||||
|
|
||||||
import android.app.Application as AndroidApp
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import de.mm20.launcher2.applications.AppRepository
|
import androidx.lifecycle.ViewModel
|
||||||
import de.mm20.launcher2.search.data.Application
|
import de.mm20.launcher2.search.data.Application
|
||||||
|
|
||||||
class AppViewModel(app: AndroidApp): AndroidViewModel(app) {
|
class AppViewModel(
|
||||||
private val repository = AppRepository.getInstance(app)
|
appRepository: AppRepository
|
||||||
val applications: LiveData<List<Application>> = repository.applications
|
): ViewModel() {
|
||||||
|
val applications: LiveData<List<Application>> = appRepository.applications
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
private val isMainProfile = launcherShortcut.userHandle == Process.myUserHandle()
|
||||||
|
|
||||||
override val key: String
|
override val key: String
|
||||||
@ -58,15 +58,6 @@ class AppShortcut(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun serialize(): String {
|
|
||||||
return jsonObjectOf(
|
|
||||||
"packagename" to launcherShortcut.`package`,
|
|
||||||
"id" to launcherShortcut.id,
|
|
||||||
"user" to userSerialNumber,
|
|
||||||
).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun getLaunchIntent(context: Context): Intent? {
|
override fun getLaunchIntent(context: Context): Intent? {
|
||||||
return launcherShortcut.intent
|
return launcherShortcut.intent
|
||||||
}
|
}
|
||||||
@ -106,56 +97,4 @@ class AppShortcut(
|
|||||||
autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt()
|
autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun deserialize(context: Context, serialized: String): AppShortcut? {
|
|
||||||
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
|
|
||||||
if (!launcherApps.hasShortcutHostPermission()) return null
|
|
||||||
else {
|
|
||||||
val json = JSONObject(serialized)
|
|
||||||
val packageName = json.getString("packagename")
|
|
||||||
val id = json.getString("id")
|
|
||||||
val userSerial = json.optLong("user")
|
|
||||||
val query = LauncherApps.ShortcutQuery()
|
|
||||||
query.setPackage(packageName)
|
|
||||||
query.setQueryFlags(LauncherApps.ShortcutQuery.FLAG_MATCH_DYNAMIC or
|
|
||||||
LauncherApps.ShortcutQuery.FLAG_MATCH_MANIFEST or
|
|
||||||
LauncherApps.ShortcutQuery.FLAG_MATCH_PINNED)
|
|
||||||
query.setShortcutIds(mutableListOf(id))
|
|
||||||
val userManager = context.getSystemService<UserManager>()!!
|
|
||||||
val user = userManager.getUserForSerialNumber(userSerial) ?: Process.myUserHandle()
|
|
||||||
val shortcuts = try {
|
|
||||||
launcherApps.getShortcuts(query, user)
|
|
||||||
} catch (e: IllegalStateException) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val pm = context.packageManager
|
|
||||||
val appName = try {
|
|
||||||
pm.getApplicationInfo(packageName, 0).loadLabel(pm).toString()
|
|
||||||
} catch (e: PackageManager.NameNotFoundException) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
if (shortcuts == null || shortcuts.isEmpty()) return null else {
|
|
||||||
GlobalScope.launch {
|
|
||||||
val activity = shortcuts[0].activity
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val icon = try {
|
|
||||||
context.packageManager.getActivityIcon(activity
|
|
||||||
?: return@withContext)
|
|
||||||
} catch (e: PackageManager.NameNotFoundException) {
|
|
||||||
return@withContext
|
|
||||||
}
|
|
||||||
val badge = Badge(icon = BadgeDrawable(context, icon))
|
|
||||||
BadgeProvider.getInstance(context).setBadge("shortcut://${activity.flattenToShortString()}", badge)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return AppShortcut(
|
|
||||||
context = context,
|
|
||||||
launcherShortcut = shortcuts[0],
|
|
||||||
appName = appName
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -15,6 +15,8 @@ import de.mm20.launcher2.ktx.getSerialNumber
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An [Application] based on an [android.content.pm.LauncherActivityInfo]
|
* An [Application] based on an [android.content.pm.LauncherActivityInfo]
|
||||||
@ -46,9 +48,9 @@ class LauncherApp(
|
|||||||
}
|
}
|
||||||
appShortcuts
|
appShortcuts
|
||||||
}
|
}
|
||||||
) {
|
), KoinComponent {
|
||||||
|
|
||||||
private val userSerialNumber: Long = launcherActivityInfo.user.getSerialNumber(context)
|
internal val userSerialNumber: Long = launcherActivityInfo.user.getSerialNumber(context)
|
||||||
private val isMainProfile = launcherActivityInfo.user == Process.myUserHandle()
|
private val isMainProfile = launcherActivityInfo.user == Process.myUserHandle()
|
||||||
|
|
||||||
override val badgeKey: String = if (isMainProfile) "app://${`package`}" else "profile://$userSerialNumber"
|
override val badgeKey: String = if (isMainProfile) "app://${`package`}" else "profile://$userSerialNumber"
|
||||||
@ -56,21 +58,14 @@ class LauncherApp(
|
|||||||
override val key: String
|
override val key: String
|
||||||
get() = if (isMainProfile) "app://$`package`:$activity" else "app://$`package`:$activity:${userSerialNumber}"
|
get() = if (isMainProfile) "app://$`package`:$activity" else "app://$`package`:$activity:${userSerialNumber}"
|
||||||
|
|
||||||
override fun serialize(): String {
|
|
||||||
val json = JSONObject()
|
|
||||||
json.put("package", `package`)
|
|
||||||
json.put("activity", activity)
|
|
||||||
json.put("user", userSerialNumber)
|
|
||||||
return json.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getUser(): UserHandle? {
|
fun getUser(): UserHandle? {
|
||||||
return launcherActivityInfo.user
|
return launcherActivityInfo.user
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun loadIconAsync(context: Context, size: Int): LauncherIcon? {
|
override suspend fun loadIconAsync(context: Context, size: Int): LauncherIcon? {
|
||||||
|
val iconPackManager: IconPackManager by inject()
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
IconPackManager.getInstance(context).getIcon(context, launcherActivityInfo, size)
|
iconPackManager.getIcon(context, launcherActivityInfo, size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,20 +93,6 @@ class LauncherApp(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
fun deserialize(context: Context, serialized: String): LauncherApp? {
|
|
||||||
val json = JSONObject(serialized)
|
|
||||||
val launcherApps = context.getSystemService<LauncherApps>()!!
|
|
||||||
val userManager = context.getSystemService<UserManager>()!!
|
|
||||||
val userSerial = json.optLong("user")
|
|
||||||
val user = userManager.getUserForSerialNumber(userSerial) ?: Process.myUserHandle()
|
|
||||||
val pkg = json.getString("package")
|
|
||||||
val intent = Intent().also {
|
|
||||||
it.component = ComponentName(pkg, json.getString("activity"))
|
|
||||||
}
|
|
||||||
val launcherActivityInfo = launcherApps.resolveActivity(intent, user) ?: return null
|
|
||||||
return LauncherApp(context, launcherActivityInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPackageVersionName(context: Context, packageName: String): String? {
|
fun getPackageVersionName(context: Context, packageName: String): String? {
|
||||||
return try {
|
return try {
|
||||||
context.packageManager.getPackageInfo(packageName, 0).versionName
|
context.packageManager.getPackageInfo(packageName, 0).versionName
|
||||||
|
|||||||
@ -48,6 +48,8 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.guava)
|
implementation(libs.guava)
|
||||||
|
|
||||||
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
implementation(project(":search"))
|
implementation(project(":search"))
|
||||||
implementation(project(":base"))
|
implementation(project(":base"))
|
||||||
implementation(project(":icons"))
|
implementation(project(":icons"))
|
||||||
|
|||||||
@ -16,14 +16,17 @@ import kotlinx.coroutines.withContext
|
|||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
class AppSearchRepository private constructor(val context: Context) : BaseSearchableRepository() {
|
class AppSearchRepository(
|
||||||
|
val context: Context,
|
||||||
|
hiddenItemsRepository: HiddenItemsRepository
|
||||||
|
) : BaseSearchableRepository() {
|
||||||
|
|
||||||
private var session: GlobalSearchSession? = null
|
private var session: GlobalSearchSession? = null
|
||||||
|
|
||||||
val appSearchResults = MediatorLiveData<List<AppSearchResult>?>()
|
val appSearchResults = MediatorLiveData<List<AppSearchResult>?>()
|
||||||
|
|
||||||
private val allAppSearchResults = MutableLiveData<List<AppSearchResult>?>(emptyList())
|
private val allAppSearchResults = MutableLiveData<List<AppSearchResult>?>(emptyList())
|
||||||
private val hiddenItemKeys = HiddenItemsRepository.getInstance(context).hiddenItemsKeys
|
private val hiddenItemKeys = hiddenItemsRepository.hiddenItemsKeys
|
||||||
|
|
||||||
init {
|
init {
|
||||||
appSearchResults.addSource(hiddenItemKeys) { keys ->
|
appSearchResults.addSource(hiddenItemKeys) { keys ->
|
||||||
@ -57,13 +60,4 @@ class AppSearchRepository private constructor(val context: Context) : BaseSearch
|
|||||||
}
|
}
|
||||||
allAppSearchResults.value = results
|
allAppSearchResults.value = results
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private lateinit var instance: AppSearchRepository
|
|
||||||
fun getInstance(context: Context): AppSearchRepository {
|
|
||||||
if (!::instance.isInitialized) instance = AppSearchRepository(context.applicationContext)
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -3,9 +3,11 @@ package de.mm20.launcher2.appsearch
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
import de.mm20.launcher2.search.data.AppSearchResult
|
import de.mm20.launcher2.search.data.AppSearchResult
|
||||||
|
|
||||||
class AppSearchViewModel(app: Application) : AndroidViewModel(app) {
|
class AppSearchViewModel(
|
||||||
private val repository = AppSearchRepository.getInstance(app)
|
appSearchRepository: AppSearchRepository
|
||||||
private val appSearch: LiveData<List<AppSearchResult>?> = repository.appSearchResults
|
) : ViewModel() {
|
||||||
|
private val appSearch: LiveData<List<AppSearchResult>?> = appSearchRepository.appSearchResults
|
||||||
}
|
}
|
||||||
@ -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.bundles.androidx.lifecycle)
|
||||||
|
|
||||||
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
implementation(project(":ktx"))
|
implementation(project(":ktx"))
|
||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
|
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
|||||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
class BadgeProvider private constructor(val context: Context) {
|
class BadgeProvider(val context: Context) {
|
||||||
|
|
||||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
|
||||||
@ -138,13 +138,4 @@ class BadgeProvider private constructor(val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private lateinit var instance: BadgeProvider
|
|
||||||
|
|
||||||
fun getInstance(context: Context): BadgeProvider {
|
|
||||||
if (!::instance.isInitialized) instance = BadgeProvider(context.applicationContext)
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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.mathparser)
|
||||||
|
|
||||||
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
implementation(project(":search"))
|
implementation(project(":search"))
|
||||||
|
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import de.mm20.launcher2.search.BaseSearchableRepository
|
|||||||
import de.mm20.launcher2.search.data.Calculator
|
import de.mm20.launcher2.search.data.Calculator
|
||||||
import org.mariuszgromada.math.mxparser.Expression
|
import org.mariuszgromada.math.mxparser.Expression
|
||||||
|
|
||||||
class CalculatorRepository private constructor() : BaseSearchableRepository() {
|
class CalculatorRepository : BaseSearchableRepository() {
|
||||||
|
|
||||||
val calculator = MutableLiveData<Calculator?>()
|
val calculator = MutableLiveData<Calculator?>()
|
||||||
|
|
||||||
@ -52,13 +52,4 @@ class CalculatorRepository private constructor() : BaseSearchableRepository() {
|
|||||||
}
|
}
|
||||||
calculator.value = calc
|
calculator.value = calc
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private lateinit var instance: CalculatorRepository
|
|
||||||
|
|
||||||
fun getInstance(): CalculatorRepository {
|
|
||||||
if (!::instance.isInitialized) instance = CalculatorRepository()
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -2,6 +2,8 @@ package de.mm20.launcher2.calculator
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
|
||||||
class CalculatorViewModel: ViewModel() {
|
class CalculatorViewModel(
|
||||||
val calculator = CalculatorRepository.getInstance().calculator
|
calculatorRepository: CalculatorRepository
|
||||||
|
): ViewModel() {
|
||||||
|
val calculator = calculatorRepository.calculator
|
||||||
}
|
}
|
||||||
@ -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.textdrawable)
|
||||||
|
|
||||||
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
api(project(":search"))
|
api(project(":search"))
|
||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
implementation(project(":ktx"))
|
implementation(project(":ktx"))
|
||||||
|
|||||||
@ -10,13 +10,16 @@ import de.mm20.launcher2.search.data.CalendarEvent
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class CalendarRepository private constructor(val context: Context) : BaseSearchableRepository() {
|
class CalendarRepository(
|
||||||
|
val context: Context,
|
||||||
|
hiddenItemsRepository: HiddenItemsRepository
|
||||||
|
) : BaseSearchableRepository() {
|
||||||
|
|
||||||
val calendarEvents = MediatorLiveData<List<CalendarEvent>?>()
|
val calendarEvents = MediatorLiveData<List<CalendarEvent>?>()
|
||||||
val upcomingCalendarEvents = MutableLiveData<List<CalendarEvent>>(emptyList())
|
val upcomingCalendarEvents = MutableLiveData<List<CalendarEvent>>(emptyList())
|
||||||
|
|
||||||
private val allEvents = MutableLiveData<List<CalendarEvent>?>(emptyList())
|
private val allEvents = MutableLiveData<List<CalendarEvent>?>(emptyList())
|
||||||
private val hiddenItemKeys = HiddenItemsRepository.getInstance(context).hiddenItemsKeys
|
private val hiddenItemKeys = hiddenItemsRepository.hiddenItemsKeys
|
||||||
|
|
||||||
init {
|
init {
|
||||||
calendarEvents.addSource(hiddenItemKeys) { keys ->
|
calendarEvents.addSource(hiddenItemKeys) { keys ->
|
||||||
@ -40,17 +43,17 @@ class CalendarRepository private constructor(val context: Context) : BaseSearcha
|
|||||||
val end = now + 14 * 24 * 60 * 60 * 1000L
|
val end = now + 14 * 24 * 60 * 60 * 1000L
|
||||||
val events = withContext(Dispatchers.IO) {
|
val events = withContext(Dispatchers.IO) {
|
||||||
CalendarEvent.search(
|
CalendarEvent.search(
|
||||||
context = context,
|
context = context,
|
||||||
query = "",
|
query = "",
|
||||||
intervalStart = now,
|
intervalStart = now,
|
||||||
intervalEnd = end,
|
intervalEnd = end,
|
||||||
limit = 700,
|
limit = 700,
|
||||||
hideAllDayEvents = hideAlldayEvents,
|
hideAllDayEvents = hideAlldayEvents,
|
||||||
unselectedCalendars = unselectedCalendars,
|
unselectedCalendars = unselectedCalendars,
|
||||||
hiddenEvents = hiddenItemKeys.value?.mapNotNull {
|
hiddenEvents = hiddenItemKeys.value?.mapNotNull {
|
||||||
if (it.startsWith("calendar")) it.substringAfterLast("/").toLong()
|
if (it.startsWith("calendar")) it.substringAfterLast("/").toLong()
|
||||||
else null
|
else null
|
||||||
} ?: emptyList()
|
} ?: emptyList()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
upcomingCalendarEvents.value = events
|
upcomingCalendarEvents.value = events
|
||||||
@ -69,13 +72,4 @@ class CalendarRepository private constructor(val context: Context) : BaseSearcha
|
|||||||
}
|
}
|
||||||
allEvents.value = events
|
allEvents.value = events
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private lateinit var instance: CalendarRepository
|
|
||||||
fun getInstance(context: Context): CalendarRepository {
|
|
||||||
if (!::instance.isInitialized) instance = CalendarRepository(context.applicationContext)
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -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
|
package de.mm20.launcher2.calendar
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
import de.mm20.launcher2.search.data.CalendarEvent
|
import de.mm20.launcher2.search.data.CalendarEvent
|
||||||
|
|
||||||
class CalendarViewModel(app:Application): AndroidViewModel(app) {
|
class CalendarViewModel(
|
||||||
val calendarEvents: LiveData<List<CalendarEvent>?> = CalendarRepository.getInstance(app).calendarEvents
|
calendarRepository: CalendarRepository
|
||||||
val upcomingCalendarEvents: LiveData<List<CalendarEvent>> = CalendarRepository.getInstance(app).upcomingCalendarEvents
|
): ViewModel() {
|
||||||
|
val calendarEvents: LiveData<List<CalendarEvent>?> = calendarRepository.calendarEvents
|
||||||
|
val upcomingCalendarEvents: LiveData<List<CalendarEvent>> = calendarRepository.upcomingCalendarEvents
|
||||||
}
|
}
|
||||||
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
|
val calendar: Long
|
||||||
) : Searchable() {
|
) : Searchable() {
|
||||||
|
|
||||||
override fun serialize(): String {
|
|
||||||
val json = JSONObject()
|
|
||||||
json.put("id", id)
|
|
||||||
return json.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
override val key: String
|
override val key: String
|
||||||
get() = "calendar://$id"
|
get() = "calendar://$id"
|
||||||
|
|
||||||
@ -175,79 +169,6 @@ class CalendarEvent(
|
|||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deserialize(context: Context, serialized: String): CalendarEvent? {
|
|
||||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) return null
|
|
||||||
val json = JSONObject(serialized)
|
|
||||||
val id = json.getLong("id")
|
|
||||||
val builder = CalendarContract.Instances.CONTENT_URI.buildUpon()
|
|
||||||
ContentUris.appendId(builder, System.currentTimeMillis())
|
|
||||||
ContentUris.appendId(builder, System.currentTimeMillis() + 63072000000L)
|
|
||||||
val uri = builder.build()
|
|
||||||
val projection = arrayOf(
|
|
||||||
CalendarContract.Instances.EVENT_ID,
|
|
||||||
CalendarContract.Instances.TITLE,
|
|
||||||
CalendarContract.Instances.BEGIN,
|
|
||||||
CalendarContract.Instances.END,
|
|
||||||
CalendarContract.Instances.ALL_DAY,
|
|
||||||
CalendarContract.Instances.DISPLAY_COLOR,
|
|
||||||
CalendarContract.Instances.EVENT_LOCATION,
|
|
||||||
CalendarContract.Instances.CALENDAR_ID,
|
|
||||||
CalendarContract.Instances.DESCRIPTION
|
|
||||||
)
|
|
||||||
val selection = CalendarContract.Instances.EVENT_ID + " = ?"
|
|
||||||
val selArgs = arrayOf(id.toString())
|
|
||||||
val cursor = context.contentResolver.query(uri, projection, selection, selArgs, null)
|
|
||||||
?: return null
|
|
||||||
if (cursor.moveToNext()) {
|
|
||||||
val title = cursor.getString(1)
|
|
||||||
val begin = cursor.getLong(2)
|
|
||||||
val end = cursor.getLong(3)
|
|
||||||
val allday = cursor.getInt(4) != 0
|
|
||||||
val color = cursor.getInt(5)
|
|
||||||
val location = cursor.getString(6)
|
|
||||||
val calendar = cursor.getLong(7)
|
|
||||||
val description = cursor.getStringOrNull(8)
|
|
||||||
?: ""
|
|
||||||
cursor.close()
|
|
||||||
val proj = arrayOf(
|
|
||||||
CalendarContract.Attendees.EVENT_ID,
|
|
||||||
CalendarContract.Attendees.ATTENDEE_NAME,
|
|
||||||
CalendarContract.Attendees.ATTENDEE_EMAIL
|
|
||||||
)
|
|
||||||
val sel = "${CalendarContract.Attendees.EVENT_ID} = $id"
|
|
||||||
val s = "${CalendarContract.Attendees.ATTENDEE_NAME} COLLATE NOCASE ASC"
|
|
||||||
val cur = context.contentResolver.query(
|
|
||||||
CalendarContract.Attendees.CONTENT_URI,
|
|
||||||
proj, sel, null, s
|
|
||||||
) ?: return null
|
|
||||||
val attendees = mutableListOf<String>()
|
|
||||||
while (cur.moveToNext()) {
|
|
||||||
attendees.add(cur.getString(1).takeUnless { it.isNullOrBlank() }
|
|
||||||
?: cur.getString(2))
|
|
||||||
}
|
|
||||||
cur.close()
|
|
||||||
val tzOffset = if (allday) {
|
|
||||||
Calendar.getInstance().timeZone.getOffset(begin)
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
return CalendarEvent(
|
|
||||||
label = title,
|
|
||||||
id = id,
|
|
||||||
color = color,
|
|
||||||
startTime = begin - tzOffset,
|
|
||||||
endTime = end - tzOffset - if (allday) 1 else 0,
|
|
||||||
allDay = allday,
|
|
||||||
location = location ?: "",
|
|
||||||
attendees = attendees,
|
|
||||||
description = description,
|
|
||||||
calendar = calendar
|
|
||||||
)
|
|
||||||
}
|
|
||||||
cursor.close()
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCalendars(context: Context): List<UserCalendar> {
|
fun getCalendars(context: Context): List<UserCalendar> {
|
||||||
val calendars = mutableListOf<UserCalendar>()
|
val calendars = mutableListOf<UserCalendar>()
|
||||||
val uri = CalendarContract.Calendars.CONTENT_URI
|
val uri = CalendarContract.Calendars.CONTENT_URI
|
||||||
|
|||||||
@ -42,6 +42,8 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.textdrawable)
|
implementation(libs.textdrawable)
|
||||||
|
|
||||||
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
implementation(project(":search"))
|
implementation(project(":search"))
|
||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
implementation(project(":ktx"))
|
implementation(project(":ktx"))
|
||||||
|
|||||||
@ -9,12 +9,15 @@ import de.mm20.launcher2.search.data.Contact
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class ContactRepository private constructor(val context: Context) : BaseSearchableRepository() {
|
class ContactRepository(
|
||||||
|
val context: Context,
|
||||||
|
hiddenItemsRepository: HiddenItemsRepository
|
||||||
|
) : BaseSearchableRepository() {
|
||||||
|
|
||||||
val contacts = MediatorLiveData<List<Contact>?>()
|
val contacts = MediatorLiveData<List<Contact>?>()
|
||||||
|
|
||||||
private val allContacts = MutableLiveData<List<Contact>?>(emptyList())
|
private val allContacts = MutableLiveData<List<Contact>?>(emptyList())
|
||||||
private val hiddenItemKeys = HiddenItemsRepository.getInstance(context).hiddenItemsKeys
|
private val hiddenItemKeys = hiddenItemsRepository.hiddenItemsKeys
|
||||||
|
|
||||||
init {
|
init {
|
||||||
contacts.addSource(hiddenItemKeys) { keys ->
|
contacts.addSource(hiddenItemKeys) { keys ->
|
||||||
@ -35,13 +38,4 @@ class ContactRepository private constructor(val context: Context) : BaseSearchab
|
|||||||
}
|
}
|
||||||
allContacts.value = results
|
allContacts.value = results
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private lateinit var instance: ContactRepository
|
|
||||||
|
|
||||||
fun getInstance(context: Context): ContactRepository {
|
|
||||||
if (!::instance.isInitialized) instance = ContactRepository(context.applicationContext)
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -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
|
package de.mm20.launcher2.contacts
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
import de.mm20.launcher2.search.data.Contact
|
import de.mm20.launcher2.search.data.Contact
|
||||||
|
|
||||||
class ContactViewModel(app: Application) : AndroidViewModel(app) {
|
class ContactViewModel(
|
||||||
val contacts: LiveData<List<Contact>?> = ContactRepository.getInstance(app).contacts
|
contactRepository: ContactRepository
|
||||||
|
) : ViewModel() {
|
||||||
|
val contacts: LiveData<List<Contact>?> = contactRepository.contacts
|
||||||
}
|
}
|
||||||
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 = ", ")
|
return phones.union(emails).joinToString(separator = ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun serialize(): String {
|
|
||||||
return jsonObjectOf(
|
|
||||||
"id" to id
|
|
||||||
).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPlaceholderIcon(context: Context): LauncherIcon {
|
override fun getPlaceholderIcon(context: Context): LauncherIcon {
|
||||||
val iconText = if (firstName.isNotEmpty()) firstName[0].toString() else "" + if (lastName.isNotEmpty()) lastName[0].toString() else ""
|
val iconText = if (firstName.isNotEmpty()) firstName[0].toString() else "" + if (lastName.isNotEmpty()) lastName[0].toString() else ""
|
||||||
return LauncherIcon(
|
return LauncherIcon(
|
||||||
@ -96,7 +90,7 @@ class Contact(
|
|||||||
return results.sortedBy { it }
|
return results.sortedBy { it }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun contactById(context: Context, id: Long, rawIds: Set<Long>): Contact? {
|
internal fun contactById(context: Context, id: Long, rawIds: Set<Long>): Contact? {
|
||||||
val s = "(" + rawIds.joinToString(separator = " OR ",
|
val s = "(" + rawIds.joinToString(separator = " OR ",
|
||||||
transform = { "${ContactsContract.Data.RAW_CONTACT_ID} = $it" }) + ")" +
|
transform = { "${ContactsContract.Data.RAW_CONTACT_ID} = $it" }) + ")" +
|
||||||
" AND (${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE}\"" +
|
" AND (${ContactsContract.Data.MIMETYPE} = \"${ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE}\"" +
|
||||||
@ -187,24 +181,5 @@ class Contact(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deserialize(context: Context, serialized: String): Contact? {
|
|
||||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) return null
|
|
||||||
val id = JSONObject(serialized).getLong("id")
|
|
||||||
val rawContactsCursor = context.contentResolver.query(
|
|
||||||
ContactsContract.RawContacts.CONTENT_URI,
|
|
||||||
arrayOf(ContactsContract.RawContacts._ID),
|
|
||||||
"${ContactsContract.RawContacts.CONTACT_ID} = ?",
|
|
||||||
arrayOf(id.toString()),
|
|
||||||
null
|
|
||||||
) ?: return null
|
|
||||||
val rawContacts = mutableSetOf<Long>()
|
|
||||||
while (rawContactsCursor.moveToNext()) {
|
|
||||||
rawContacts.add(rawContactsCursor.getLong(0))
|
|
||||||
}
|
|
||||||
rawContactsCursor.close()
|
|
||||||
if (rawContacts.isEmpty()) return null
|
|
||||||
|
|
||||||
return contactById(context, id, rawContacts)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -98,12 +98,4 @@ class CurrencyRepository(val context: Context) {
|
|||||||
AppDatabase.getInstance(context).currencyDao().getLastUpdate(symbol)
|
AppDatabase.getInstance(context).currencyDao().getLastUpdate(symbol)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private lateinit var instance: CurrencyRepository
|
|
||||||
fun getInstance(context: Context): CurrencyRepository {
|
|
||||||
if (!::instance.isInitialized) instance = CurrencyRepository(context.applicationContext)
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -40,6 +40,8 @@ dependencies {
|
|||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
|
|
||||||
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
implementation(project(":search"))
|
implementation(project(":search"))
|
||||||
implementation(project(":calendar"))
|
implementation(project(":calendar"))
|
||||||
implementation(project(":database"))
|
implementation(project(":database"))
|
||||||
|
|||||||
@ -2,34 +2,38 @@ package de.mm20.launcher2.favorites
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import de.mm20.launcher2.database.entities.FavoritesItemEntity
|
import de.mm20.launcher2.database.entities.FavoritesItemEntity
|
||||||
|
import de.mm20.launcher2.search.SearchableSerializer
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.data.Searchable
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
import org.koin.core.parameter.parametersOf
|
||||||
|
|
||||||
data class FavoritesItem(
|
data class FavoritesItem(
|
||||||
val key: String,
|
val key: String,
|
||||||
/**
|
/**
|
||||||
* null if searchable could not be deserialized (i.e. the app has been uninstalled)
|
* null if searchable could not be deserialized (i.e. the app has been uninstalled)
|
||||||
*/
|
*/
|
||||||
val searchable: Searchable?,
|
val searchable: Searchable?,
|
||||||
var launchCount: Int,
|
var launchCount: Int,
|
||||||
var pinPosition: Int,
|
var pinPosition: Int,
|
||||||
var hidden: Boolean
|
var hidden: Boolean
|
||||||
){
|
) : KoinComponent {
|
||||||
constructor(context: Context, entity: FavoritesItemEntity) : this(
|
private val serializer: SearchableSerializer by inject { parametersOf(searchable) }
|
||||||
key = entity.key,
|
|
||||||
searchable = SearchableDeserializer(context).deserialize(entity.serializedSearchable),
|
|
||||||
launchCount = entity.launchCount,
|
|
||||||
pinPosition = entity.pinPosition,
|
|
||||||
hidden = entity.hidden
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
fun toDatabaseEntity(): FavoritesItemEntity {
|
fun toDatabaseEntity(): FavoritesItemEntity {
|
||||||
|
|
||||||
return FavoritesItemEntity(
|
return FavoritesItemEntity(
|
||||||
key = key,
|
key = key,
|
||||||
serializedSearchable = searchable?.let { "${SearchableDeserializer.getTypePrefix(it)}#${it.serialize()}" } ?: "",
|
serializedSearchable = searchable?.let {
|
||||||
hidden = hidden,
|
"${serializer.typePrefix}#${
|
||||||
pinPosition = pinPosition,
|
serializer.serialize(
|
||||||
launchCount = launchCount
|
it
|
||||||
|
)
|
||||||
|
}"
|
||||||
|
} ?: "",
|
||||||
|
hidden = hidden,
|
||||||
|
pinPosition = pinPosition,
|
||||||
|
launchCount = launchCount
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -10,13 +10,16 @@ import de.mm20.launcher2.database.entities.FavoritesItemEntity
|
|||||||
import de.mm20.launcher2.ktx.ceilToInt
|
import de.mm20.launcher2.ktx.ceilToInt
|
||||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||||
import de.mm20.launcher2.search.BaseSearchableRepository
|
import de.mm20.launcher2.search.BaseSearchableRepository
|
||||||
|
import de.mm20.launcher2.search.SearchableDeserializer
|
||||||
import de.mm20.launcher2.search.data.CalendarEvent
|
import de.mm20.launcher2.search.data.CalendarEvent
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.data.Searchable
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
import org.koin.core.parameter.parametersOf
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
class FavoritesRepository private constructor(private val context: Context) : BaseSearchableRepository() {
|
class FavoritesRepository(private val context: Context) : BaseSearchableRepository() {
|
||||||
|
|
||||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
|
||||||
@ -30,6 +33,17 @@ class FavoritesRepository private constructor(private val context: Context) : Ba
|
|||||||
|
|
||||||
val pinnedCalendarEvents = MediatorLiveData<List<CalendarEvent>>()
|
val pinnedCalendarEvents = MediatorLiveData<List<CalendarEvent>>()
|
||||||
|
|
||||||
|
private fun fromDatabaseEntity(entity: FavoritesItemEntity): FavoritesItem {
|
||||||
|
val deserializer: SearchableDeserializer by inject { parametersOf(entity.serializedSearchable) }
|
||||||
|
return FavoritesItem(
|
||||||
|
key = entity.key,
|
||||||
|
searchable = deserializer.deserialize(entity.serializedSearchable.substringAfter("#")),
|
||||||
|
launchCount = entity.launchCount,
|
||||||
|
pinPosition = entity.pinPosition,
|
||||||
|
hidden = entity.hidden
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private val reloadFavorites: (String) -> Unit = {
|
private val reloadFavorites: (String) -> Unit = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
if(!LauncherPreferences.instance.searchShowFavorites) {
|
if(!LauncherPreferences.instance.searchShowFavorites) {
|
||||||
@ -41,7 +55,7 @@ class FavoritesRepository private constructor(private val context: Context) : Ba
|
|||||||
val dao = AppDatabase.getInstance(context).searchDao()
|
val dao = AppDatabase.getInstance(context).searchDao()
|
||||||
val favItems = pinnedFavorites.value ?: emptyList()
|
val favItems = pinnedFavorites.value ?: emptyList()
|
||||||
favs.addAll(favItems.mapNotNull {
|
favs.addAll(favItems.mapNotNull {
|
||||||
val item = FavoritesItem(context, it)
|
val item = fromDatabaseEntity(it)
|
||||||
if (item.searchable == null) {
|
if (item.searchable == null) {
|
||||||
dao.deleteByKey(item.key)
|
dao.deleteByKey(item.key)
|
||||||
}
|
}
|
||||||
@ -52,7 +66,7 @@ class FavoritesRepository private constructor(private val context: Context) : Ba
|
|||||||
if(favItems.size < columns) favCount += columns
|
if(favItems.size < columns) favCount += columns
|
||||||
val autoFavs = dao.getAutoFavorites(favCount - favs.size)
|
val autoFavs = dao.getAutoFavorites(favCount - favs.size)
|
||||||
favs.addAll(autoFavs.mapNotNull {
|
favs.addAll(autoFavs.mapNotNull {
|
||||||
val item = FavoritesItem(context, it)
|
val item = fromDatabaseEntity(it)
|
||||||
if (item.searchable == null) {
|
if (item.searchable == null) {
|
||||||
dao.deleteByKey(item.key)
|
dao.deleteByKey(item.key)
|
||||||
}
|
}
|
||||||
@ -68,7 +82,7 @@ class FavoritesRepository private constructor(private val context: Context) : Ba
|
|||||||
init {
|
init {
|
||||||
val hidden = AppDatabase.getInstance(context).searchDao().getHiddenItems()
|
val hidden = AppDatabase.getInstance(context).searchDao().getHiddenItems()
|
||||||
hiddenItems.addSource(hidden) { h ->
|
hiddenItems.addSource(hidden) { h ->
|
||||||
hiddenItems.value = h.mapNotNull { FavoritesItem(context, it).searchable }
|
hiddenItems.value = h.mapNotNull { fromDatabaseEntity(it).searchable }
|
||||||
}
|
}
|
||||||
favorites.addSource(pinnedFavorites) {
|
favorites.addSource(pinnedFavorites) {
|
||||||
reloadFavorites("")
|
reloadFavorites("")
|
||||||
@ -77,7 +91,7 @@ class FavoritesRepository private constructor(private val context: Context) : Ba
|
|||||||
scope.launch {
|
scope.launch {
|
||||||
val dao = AppDatabase.getInstance(context).searchDao()
|
val dao = AppDatabase.getInstance(context).searchDao()
|
||||||
pinnedCalendarEvents.value = it.filter { it.key.startsWith("calendar://") }.mapNotNull {
|
pinnedCalendarEvents.value = it.filter { it.key.startsWith("calendar://") }.mapNotNull {
|
||||||
val item = FavoritesItem(context, it)
|
val item = fromDatabaseEntity(it)
|
||||||
if (item.searchable == null) {
|
if (item.searchable == null) {
|
||||||
withContext(Dispatchers.IO) { dao.deleteByKey(item.key) }
|
withContext(Dispatchers.IO) { dao.deleteByKey(item.key) }
|
||||||
}
|
}
|
||||||
@ -187,7 +201,7 @@ class FavoritesRepository private constructor(private val context: Context) : Ba
|
|||||||
suspend fun getAllFavoriteItems(): List<FavoritesItem> {
|
suspend fun getAllFavoriteItems(): List<FavoritesItem> {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
AppDatabase.getInstance(context).searchDao().getAllFavoriteItems().mapNotNull {
|
AppDatabase.getInstance(context).searchDao().getAllFavoriteItems().mapNotNull {
|
||||||
FavoritesItem(context, it).takeIf { it.searchable != null }
|
fromDatabaseEntity(it).takeIf { it.searchable != null }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -208,11 +222,4 @@ class FavoritesRepository private constructor(private val context: Context) : Ba
|
|||||||
return favs
|
return favs
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private lateinit var instance: FavoritesRepository
|
|
||||||
fun getInstance(context: Context): FavoritesRepository {
|
|
||||||
if (!::instance.isInitialized) instance = FavoritesRepository(context.applicationContext)
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,58 +1,54 @@
|
|||||||
package de.mm20.launcher2.favorites
|
package de.mm20.launcher2.favorites
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MediatorLiveData
|
import androidx.lifecycle.ViewModel
|
||||||
import de.mm20.launcher2.search.data.CalendarEvent
|
import de.mm20.launcher2.search.data.CalendarEvent
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.data.Searchable
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class FavoritesViewModel(app: Application) : AndroidViewModel(app) {
|
class FavoritesViewModel(
|
||||||
|
private val favoritesRepository: FavoritesRepository
|
||||||
val repository = FavoritesRepository.getInstance(app)
|
) : ViewModel() {
|
||||||
|
|
||||||
fun getTopFavorites(count: Int): LiveData<List<Searchable>> {
|
fun getTopFavorites(count: Int): LiveData<List<Searchable>> {
|
||||||
return repository.getTopFavorites(count)
|
return favoritesRepository.getTopFavorites(count)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getFavorites(columns: Int): LiveData<List<Searchable>> {
|
fun getFavorites(columns: Int): LiveData<List<Searchable>> {
|
||||||
return repository.getFavorites(columns)
|
return favoritesRepository.getFavorites(columns)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pinItem(searchable: Searchable) {
|
fun pinItem(searchable: Searchable) {
|
||||||
repository.pinItem(searchable)
|
favoritesRepository.pinItem(searchable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unpinItem(searchable: Searchable) {
|
fun unpinItem(searchable: Searchable) {
|
||||||
repository.unpinItem(searchable)
|
favoritesRepository.unpinItem(searchable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isPinned(searchable: Searchable): LiveData<Boolean> {
|
fun isPinned(searchable: Searchable): LiveData<Boolean> {
|
||||||
return repository.isPinned(searchable)
|
return favoritesRepository.isPinned(searchable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isHidden(searchable: Searchable): LiveData<Boolean> {
|
fun isHidden(searchable: Searchable): LiveData<Boolean> {
|
||||||
return repository.isHidden(searchable)
|
return favoritesRepository.isHidden(searchable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hideItem(searchable: Searchable) {
|
fun hideItem(searchable: Searchable) {
|
||||||
repository.hideItem(searchable)
|
favoritesRepository.hideItem(searchable)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unhideItem(searchable: Searchable) {
|
fun unhideItem(searchable: Searchable) {
|
||||||
repository.unhideItem(searchable)
|
favoritesRepository.unhideItem(searchable)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getAllFavoriteItems(): List<FavoritesItem> {
|
suspend fun getAllFavoriteItems(): List<FavoritesItem> {
|
||||||
return repository.getAllFavoriteItems()
|
return favoritesRepository.getAllFavoriteItems()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveFavorites(favorites: MutableList<FavoritesItem>) {
|
fun saveFavorites(favorites: MutableList<FavoritesItem>) {
|
||||||
repository.saveFavorites(favorites)
|
favoritesRepository.saveFavorites(favorites)
|
||||||
}
|
}
|
||||||
|
|
||||||
val hiddenItems: LiveData<List<Searchable>> = repository.hiddenItems
|
val hiddenItems: LiveData<List<Searchable>> = this.favoritesRepository.hiddenItems
|
||||||
val pinnedCalendarEvents: LiveData<List<CalendarEvent>> = repository.pinnedCalendarEvents
|
val pinnedCalendarEvents: LiveData<List<CalendarEvent>> = this.favoritesRepository.pinnedCalendarEvents
|
||||||
}
|
}
|
||||||
@ -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.bundles.androidx.lifecycle)
|
||||||
|
|
||||||
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
implementation(project(":search"))
|
implementation(project(":search"))
|
||||||
implementation(project(":hiddenitems"))
|
implementation(project(":hiddenitems"))
|
||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
|
|||||||
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 de.mm20.launcher2.search.data.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
class FilesRepository private constructor(val context: Context) : BaseSearchableRepository() {
|
class FilesRepository(
|
||||||
|
val context: Context,
|
||||||
|
hiddenItemsRepository: HiddenItemsRepository
|
||||||
|
) : BaseSearchableRepository() {
|
||||||
|
|
||||||
val files = MediatorLiveData<List<File>?>()
|
val files = MediatorLiveData<List<File>?>()
|
||||||
|
|
||||||
private val allFiles = MutableLiveData<List<File>?>(emptyList())
|
private val allFiles = MutableLiveData<List<File>?>(emptyList())
|
||||||
private val hiddenItemKeys = HiddenItemsRepository.getInstance(context).hiddenItemsKeys
|
private val hiddenItemKeys = hiddenItemsRepository.hiddenItemsKeys
|
||||||
|
|
||||||
private val nextcloudClient by lazy {
|
private val nextcloudClient by lazy {
|
||||||
NextcloudApiHelper(context)
|
NextcloudApiHelper(context)
|
||||||
@ -46,10 +49,10 @@ class FilesRepository private constructor(val context: Context) : BaseSearchable
|
|||||||
val cloudFiles = withContext(Dispatchers.IO) {
|
val cloudFiles = withContext(Dispatchers.IO) {
|
||||||
delay(300)
|
delay(300)
|
||||||
listOf(
|
listOf(
|
||||||
async { OneDriveFile.search(context, query) },
|
async { OneDriveFile.search(context, query) },
|
||||||
async { GDriveFile.search(context, query) },
|
async { GDriveFile.search(context, query) },
|
||||||
async { NextcloudFile.search(context, query, nextcloudClient) },
|
async { NextcloudFile.search(context, query, nextcloudClient) },
|
||||||
async { OwncloudFile.search(context, query, owncloudClient) }
|
async { OwncloudFile.search(context, query, owncloudClient) }
|
||||||
).awaitAll().flatten()
|
).awaitAll().flatten()
|
||||||
}
|
}
|
||||||
yield()
|
yield()
|
||||||
@ -59,12 +62,4 @@ class FilesRepository private constructor(val context: Context) : BaseSearchable
|
|||||||
fun removeFile(file: File) {
|
fun removeFile(file: File) {
|
||||||
allFiles.value = allFiles.value?.filter { it != file }
|
allFiles.value = allFiles.value?.filter { it != file }
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private lateinit var instance: FilesRepository
|
|
||||||
fun getInstance(context: Context): FilesRepository {
|
|
||||||
if (!::instance.isInitialized) instance = FilesRepository(context.applicationContext)
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,16 +1,16 @@
|
|||||||
package de.mm20.launcher2.files
|
package de.mm20.launcher2.files
|
||||||
|
|
||||||
import android.app.Application
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import de.mm20.launcher2.search.data.File
|
import de.mm20.launcher2.search.data.File
|
||||||
|
|
||||||
class FilesViewModel(app: Application): AndroidViewModel(app) {
|
class FilesViewModel(
|
||||||
|
private val filesRepository: FilesRepository
|
||||||
|
): ViewModel() {
|
||||||
|
|
||||||
|
|
||||||
private val repository = FilesRepository.getInstance(app)
|
val files = filesRepository.files
|
||||||
val files = repository.files
|
|
||||||
|
|
||||||
fun removeFile(file: File) {
|
fun removeFile(file: File) {
|
||||||
repository.removeFile(file)
|
filesRepository.removeFile(file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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)
|
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun serialize(): String {
|
|
||||||
return jsonObjectOf(
|
|
||||||
"id" to id
|
|
||||||
).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getFileType(context: Context): String {
|
fun getFileType(context: Context): String {
|
||||||
if (isDirectory) return context.getString(R.string.file_type_directory)
|
if (isDirectory) return context.getString(R.string.file_type_directory)
|
||||||
val resource = when (mimeType) {
|
val resource = when (mimeType) {
|
||||||
@ -261,7 +255,7 @@ open class File(
|
|||||||
return results.sortedBy { it }
|
return results.sortedBy { it }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getMimetypeByFileExtension(extension: String): String {
|
internal fun getMimetypeByFileExtension(extension: String): String {
|
||||||
return when (extension) {
|
return when (extension) {
|
||||||
"apk" -> "application/vnd.android.package-archive"
|
"apk" -> "application/vnd.android.package-archive"
|
||||||
"zip" -> "application/zip"
|
"zip" -> "application/zip"
|
||||||
@ -287,7 +281,7 @@ open class File(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun getMetaData(context: Context, mimeType: String, path: String): List<Pair<Int, String>> {
|
internal fun getMetaData(context: Context, mimeType: String, path: String): List<Pair<Int, String>> {
|
||||||
val metaData = mutableListOf<Pair<Int, String>>()
|
val metaData = mutableListOf<Pair<Int, String>>()
|
||||||
when {
|
when {
|
||||||
mimeType.startsWith("audio/") -> {
|
mimeType.startsWith("audio/") -> {
|
||||||
@ -365,37 +359,5 @@ open class File(
|
|||||||
}
|
}
|
||||||
return metaData
|
return metaData
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deserialize(context: Context, serialized: String): File? {
|
|
||||||
if (!PermissionsManager.checkPermission(context, PermissionsManager.EXTERNAL_STORAGE)) return null
|
|
||||||
val json = JSONObject(serialized)
|
|
||||||
val uri = MediaStore.Files.getContentUri("external")
|
|
||||||
val proj = arrayOf(MediaStore.Files.FileColumns._ID,
|
|
||||||
MediaStore.Files.FileColumns.SIZE,
|
|
||||||
MediaStore.Files.FileColumns.DATA,
|
|
||||||
MediaStore.Files.FileColumns.MIME_TYPE)
|
|
||||||
val sel = "${MediaStore.Files.FileColumns._ID} = ?"
|
|
||||||
val selArgs = arrayOf(json.getLong("id").toString())
|
|
||||||
val cursor = context.contentResolver.query(uri, proj, sel, selArgs, null) ?: return null
|
|
||||||
if (cursor.moveToNext()) {
|
|
||||||
val path = cursor.getString(2)
|
|
||||||
if (!JavaIOFile(path).exists()) return null
|
|
||||||
val directory = JavaIOFile(path).isDirectory
|
|
||||||
val id = cursor.getLong(0)
|
|
||||||
val mimeType = cursor.getStringOrNull(3)
|
|
||||||
?: if (directory) "inode/directory" else getMimetypeByFileExtension(path.substringAfterLast('.'))
|
|
||||||
val size = cursor.getLong(1)
|
|
||||||
cursor.close()
|
|
||||||
return File(
|
|
||||||
path = path,
|
|
||||||
mimeType = mimeType,
|
|
||||||
size = size,
|
|
||||||
isDirectory = directory,
|
|
||||||
id = id,
|
|
||||||
metaData = getMetaData(context, mimeType, path))
|
|
||||||
}
|
|
||||||
cursor.close()
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -8,20 +8,18 @@ import de.mm20.launcher2.gservices.DriveFileMeta
|
|||||||
import de.mm20.launcher2.gservices.GoogleApiHelper
|
import de.mm20.launcher2.gservices.GoogleApiHelper
|
||||||
import de.mm20.launcher2.helper.NetworkUtils
|
import de.mm20.launcher2.helper.NetworkUtils
|
||||||
import de.mm20.launcher2.icons.LauncherIcon
|
import de.mm20.launcher2.icons.LauncherIcon
|
||||||
import de.mm20.launcher2.ktx.jsonObjectOf
|
|
||||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||||
import org.json.JSONObject
|
|
||||||
|
|
||||||
class GDriveFile(
|
class GDriveFile(
|
||||||
val fileId: String,
|
val fileId: String,
|
||||||
override val label: String,
|
override val label: String,
|
||||||
path: String,
|
path: String,
|
||||||
mimeType: String,
|
mimeType: String,
|
||||||
size: Long,
|
size: Long,
|
||||||
isDirectory: Boolean,
|
isDirectory: Boolean,
|
||||||
metaData: List<Pair<Int, String>>,
|
metaData: List<Pair<Int, String>>,
|
||||||
val directoryColor: String?,
|
val directoryColor: String?,
|
||||||
val viewUri: String
|
val viewUri: String
|
||||||
) : File(0, path, mimeType, size, isDirectory, metaData) {
|
) : File(0, path, mimeType, size, isDirectory, metaData) {
|
||||||
|
|
||||||
override val key: String = "gdrive://$fileId"
|
override val key: String = "gdrive://$fileId"
|
||||||
@ -29,27 +27,6 @@ class GDriveFile(
|
|||||||
override val badgeKey: String
|
override val badgeKey: String
|
||||||
get() = "gdrive://"
|
get() = "gdrive://"
|
||||||
|
|
||||||
override fun serialize(): String {
|
|
||||||
return jsonObjectOf(
|
|
||||||
"id" to fileId,
|
|
||||||
"label" to label,
|
|
||||||
"path" to path,
|
|
||||||
"mimeType" to mimeType,
|
|
||||||
"size" to size,
|
|
||||||
"directory" to isDirectory,
|
|
||||||
"color" to directoryColor,
|
|
||||||
"uri" to viewUri
|
|
||||||
).apply {
|
|
||||||
for ((k, v) in metaData) {
|
|
||||||
put(when (k) {
|
|
||||||
R.string.file_meta_owner -> "owner"
|
|
||||||
R.string.file_meta_dimensions -> "dimensions"
|
|
||||||
else -> "other"
|
|
||||||
}, v)
|
|
||||||
}
|
|
||||||
}.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
override val isStoredInCloud = true
|
override val isStoredInCloud = true
|
||||||
|
|
||||||
override fun getLaunchIntent(context: Context): Intent? {
|
override fun getLaunchIntent(context: Context): Intent? {
|
||||||
@ -72,15 +49,15 @@ class GDriveFile(
|
|||||||
val driveFiles = GoogleApiHelper.getInstance(context).queryGDriveFiles(query)
|
val driveFiles = GoogleApiHelper.getInstance(context).queryGDriveFiles(query)
|
||||||
return driveFiles.map {
|
return driveFiles.map {
|
||||||
GDriveFile(
|
GDriveFile(
|
||||||
fileId = it.fileId,
|
fileId = it.fileId,
|
||||||
label = it.label,
|
label = it.label,
|
||||||
size = it.size,
|
size = it.size,
|
||||||
mimeType = it.mimeType,
|
mimeType = it.mimeType,
|
||||||
isDirectory = it.isDirectory,
|
isDirectory = it.isDirectory,
|
||||||
path = "",
|
path = "",
|
||||||
directoryColor = it.directoryColor,
|
directoryColor = it.directoryColor,
|
||||||
viewUri = it.viewUri,
|
viewUri = it.viewUri,
|
||||||
metaData = getMetadata(it.metadata)
|
metaData = getMetadata(it.metadata)
|
||||||
)
|
)
|
||||||
}.sorted()
|
}.sorted()
|
||||||
}
|
}
|
||||||
@ -94,33 +71,5 @@ class GDriveFile(
|
|||||||
if (width != null && height != null) metaData.add(R.string.file_meta_dimensions to "${width}x$height")
|
if (width != null && height != null) metaData.add(R.string.file_meta_dimensions to "${width}x$height")
|
||||||
return metaData
|
return metaData
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deserialize(serialized: String): GDriveFile? {
|
|
||||||
val json = JSONObject(serialized)
|
|
||||||
val id = json.getString("id")
|
|
||||||
val label = json.getString("label")
|
|
||||||
val path = json.getString("path")
|
|
||||||
val mimeType = json.getString("mimeType")
|
|
||||||
val size = json.getLong("size")
|
|
||||||
val directory = json.getBoolean("directory")
|
|
||||||
val color = json.optString("color")
|
|
||||||
val uri = json.getString("uri")
|
|
||||||
val owner = json.optString("owner")
|
|
||||||
val dimensions = json.optString("dimensions")
|
|
||||||
val metaData = mutableListOf<Pair<Int, String>>()
|
|
||||||
owner.takeIf { it.isNotEmpty() }?.let { metaData.add(R.string.file_meta_owner to it) }
|
|
||||||
dimensions.takeIf { it.isNotEmpty() }?.let { metaData.add(R.string.file_meta_dimensions to it) }
|
|
||||||
return GDriveFile(
|
|
||||||
fileId = id,
|
|
||||||
label = label,
|
|
||||||
path = path,
|
|
||||||
mimeType = mimeType,
|
|
||||||
size = size,
|
|
||||||
directoryColor = color,
|
|
||||||
isDirectory = directory,
|
|
||||||
viewUri = uri,
|
|
||||||
metaData = metaData
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -34,25 +34,6 @@ class NextcloudFile(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun serialize(): String {
|
|
||||||
return jsonObjectOf(
|
|
||||||
"id" to id,
|
|
||||||
"label" to label,
|
|
||||||
"path" to path,
|
|
||||||
"mimeType" to mimeType,
|
|
||||||
"size" to size,
|
|
||||||
"isDirectory" to isDirectory,
|
|
||||||
"server" to server
|
|
||||||
).apply {
|
|
||||||
for ((k, v) in metaData) {
|
|
||||||
put(when (k) {
|
|
||||||
R.string.file_meta_owner -> "owner"
|
|
||||||
else -> "other"
|
|
||||||
}, v)
|
|
||||||
}
|
|
||||||
}.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
suspend fun search(context: Context, query: String, nextcloudClient: NextcloudApiHelper) : List<NextcloudFile> {
|
suspend fun search(context: Context, query: String, nextcloudClient: NextcloudApiHelper) : List<NextcloudFile> {
|
||||||
if (!LauncherPreferences.instance.searchNextcloud) return emptyList()
|
if (!LauncherPreferences.instance.searchNextcloud) return emptyList()
|
||||||
@ -73,29 +54,5 @@ class NextcloudFile(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deserialize(serialized: String): NextcloudFile? {
|
|
||||||
val json = JSONObject(serialized)
|
|
||||||
val id = json.getLong("id")
|
|
||||||
val label = json.getString("label")
|
|
||||||
val path = json.getString("path")
|
|
||||||
val mimeType = json.getString("mimeType")
|
|
||||||
val size = json.getLong("size")
|
|
||||||
val isDirectory = json.getBoolean("isDirectory")
|
|
||||||
val server = json.getString("server")
|
|
||||||
val owner = json.optString("owner").takeIf { it.isNotEmpty() }
|
|
||||||
|
|
||||||
return NextcloudFile(
|
|
||||||
fileId = id,
|
|
||||||
label = label,
|
|
||||||
path = path,
|
|
||||||
mimeType = mimeType,
|
|
||||||
size = size,
|
|
||||||
isDirectory = isDirectory,
|
|
||||||
server = server,
|
|
||||||
metaData = owner?.let { listOf(R.string.file_meta_owner to it) } ?: emptyList()
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -6,10 +6,8 @@ import android.net.Uri
|
|||||||
import de.mm20.launcher2.msservices.DriveItem
|
import de.mm20.launcher2.msservices.DriveItem
|
||||||
import de.mm20.launcher2.files.R
|
import de.mm20.launcher2.files.R
|
||||||
import de.mm20.launcher2.msservices.MicrosoftGraphApiHelper
|
import de.mm20.launcher2.msservices.MicrosoftGraphApiHelper
|
||||||
import de.mm20.launcher2.ktx.jsonObjectOf
|
|
||||||
import de.mm20.launcher2.icons.LauncherIcon
|
import de.mm20.launcher2.icons.LauncherIcon
|
||||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||||
import org.json.JSONObject
|
|
||||||
|
|
||||||
class OneDriveFile(
|
class OneDriveFile(
|
||||||
val fileId: String,
|
val fileId: String,
|
||||||
@ -39,25 +37,6 @@ class OneDriveFile(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun serialize(): String {
|
|
||||||
return jsonObjectOf(
|
|
||||||
"id" to fileId,
|
|
||||||
"label" to label,
|
|
||||||
"mimeType" to mimeType,
|
|
||||||
"size" to size,
|
|
||||||
"directory" to isDirectory,
|
|
||||||
"webUrl" to webUrl
|
|
||||||
).apply {
|
|
||||||
for ((k, v) in metaData) {
|
|
||||||
put(when (k) {
|
|
||||||
R.string.file_meta_owner -> "owner"
|
|
||||||
R.string.file_meta_dimensions -> "dimensions"
|
|
||||||
else -> "other"
|
|
||||||
}, v)
|
|
||||||
}
|
|
||||||
}.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
suspend fun search(context: Context, query: String): List<File> {
|
suspend fun search(context: Context, query: String): List<File> {
|
||||||
if (query.length < 4) return emptyList()
|
if (query.length < 4) return emptyList()
|
||||||
@ -79,31 +58,6 @@ class OneDriveFile(
|
|||||||
return files.sorted()
|
return files.sorted()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deserialize(serialized: String): OneDriveFile? {
|
|
||||||
val json = JSONObject(serialized)
|
|
||||||
val fileId = json.getString("id")
|
|
||||||
val label = json.getString("label")
|
|
||||||
val mimeType = json.getString("mimeType")
|
|
||||||
val size = json.getLong("size")
|
|
||||||
val isDirectory = json.getBoolean("directory")
|
|
||||||
val webUrl = json.getString("webUrl")
|
|
||||||
val owner = json.optString("owner")
|
|
||||||
val dimensions = json.optString("dimensions")
|
|
||||||
val metaData = mutableListOf<Pair<Int, String>>()
|
|
||||||
owner.takeIf { it.isNotEmpty() }?.let { metaData.add(R.string.file_meta_owner to it) }
|
|
||||||
dimensions.takeIf { it.isNotEmpty() }?.let { metaData.add(R.string.file_meta_dimensions to it) }
|
|
||||||
return OneDriveFile(
|
|
||||||
fileId = fileId,
|
|
||||||
label = label,
|
|
||||||
path = "",
|
|
||||||
mimeType = mimeType,
|
|
||||||
size = size,
|
|
||||||
isDirectory = isDirectory,
|
|
||||||
metaData = metaData,
|
|
||||||
webUrl = webUrl
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getMetaData(driveItem: DriveItem): List<Pair<Int, String>> {
|
private fun getMetaData(driveItem: DriveItem): List<Pair<Int, String>> {
|
||||||
val metaData = mutableListOf<Pair<Int, String>>()
|
val metaData = mutableListOf<Pair<Int, String>>()
|
||||||
driveItem.meta.owner?.let {
|
driveItem.meta.owner?.let {
|
||||||
|
|||||||
@ -34,25 +34,6 @@ class OwncloudFile(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun serialize(): String {
|
|
||||||
return jsonObjectOf(
|
|
||||||
"id" to id,
|
|
||||||
"label" to label,
|
|
||||||
"path" to path,
|
|
||||||
"mimeType" to mimeType,
|
|
||||||
"size" to size,
|
|
||||||
"isDirectory" to isDirectory,
|
|
||||||
"server" to server
|
|
||||||
).apply {
|
|
||||||
for ((k, v) in metaData) {
|
|
||||||
put(when (k) {
|
|
||||||
R.string.file_meta_owner -> "owner"
|
|
||||||
else -> "other"
|
|
||||||
}, v)
|
|
||||||
}
|
|
||||||
}.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
suspend fun search(context: Context, query: String, owncloudClient: OwncloudClient) : List<OwncloudFile> {
|
suspend fun search(context: Context, query: String, owncloudClient: OwncloudClient) : List<OwncloudFile> {
|
||||||
if (!LauncherPreferences.instance.searchOwncloud) return emptyList()
|
if (!LauncherPreferences.instance.searchOwncloud) return emptyList()
|
||||||
@ -73,29 +54,5 @@ class OwncloudFile(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deserialize(serialized: String): OwncloudFile? {
|
|
||||||
val json = JSONObject(serialized)
|
|
||||||
val id = json.getLong("id")
|
|
||||||
val label = json.getString("label")
|
|
||||||
val path = json.getString("path")
|
|
||||||
val mimeType = json.getString("mimeType")
|
|
||||||
val size = json.getLong("size")
|
|
||||||
val isDirectory = json.getBoolean("isDirectory")
|
|
||||||
val server = json.getString("server")
|
|
||||||
val owner = json.optString("owner").takeIf { it.isNotEmpty() }
|
|
||||||
|
|
||||||
return OwncloudFile(
|
|
||||||
fileId = id,
|
|
||||||
label = label,
|
|
||||||
path = path,
|
|
||||||
mimeType = mimeType,
|
|
||||||
size = size,
|
|
||||||
isDirectory = isDirectory,
|
|
||||||
server = server,
|
|
||||||
metaData = owner?.let { listOf(R.string.file_meta_owner to it) } ?: emptyList()
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -40,6 +40,8 @@ dependencies {
|
|||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
|
|
||||||
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
implementation(project(":database"))
|
implementation(project(":database"))
|
||||||
implementation(project(":search"))
|
implementation(project(":search"))
|
||||||
}
|
}
|
||||||
@ -10,20 +10,11 @@ import de.mm20.launcher2.search.data.Searchable
|
|||||||
* A low level repository for hidden items. This can only be used to retrieve keys and to check
|
* A low level repository for hidden items. This can only be used to retrieve keys and to check
|
||||||
* whether an item is hidden. To retrieve actual Searchable objects, use FavoritesRepository.
|
* whether an item is hidden. To retrieve actual Searchable objects, use FavoritesRepository.
|
||||||
*/
|
*/
|
||||||
class HiddenItemsRepository private constructor(val context: Context) {
|
class HiddenItemsRepository(val context: Context) {
|
||||||
|
|
||||||
val hiddenItemsKeys : LiveData<List<String>> = AppDatabase.getInstance(context).searchDao().getHiddenItemKeys()
|
val hiddenItemsKeys : LiveData<List<String>> = AppDatabase.getInstance(context).searchDao().getHiddenItemKeys()
|
||||||
|
|
||||||
fun isHidden(item: Searchable): LiveData<Boolean> {
|
fun isHidden(item: Searchable): LiveData<Boolean> {
|
||||||
return AppDatabase.getInstance(context).searchDao().isHidden(item.key)
|
return AppDatabase.getInstance(context).searchDao().isHidden(item.key)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private lateinit var instance: HiddenItemsRepository
|
|
||||||
|
|
||||||
fun getInstance(context: Context): HiddenItemsRepository {
|
|
||||||
if(!Companion::instance.isInitialized) instance = HiddenItemsRepository(context.applicationContext)
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,9 +1,10 @@
|
|||||||
package de.mm20.launcher2.hiddenitems
|
package de.mm20.launcher2.hiddenitems
|
||||||
|
|
||||||
import android.app.Application
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
|
|
||||||
class HiddenItemsViewModel(app: Application): AndroidViewModel(app) {
|
class HiddenItemsViewModel(
|
||||||
val hiddenItemsKeys = HiddenItemsRepository.getInstance(app).hiddenItemsKeys
|
hiddenItemsRepository: HiddenItemsRepository
|
||||||
|
): ViewModel() {
|
||||||
|
val hiddenItemsKeys = hiddenItemsRepository.hiddenItemsKeys
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -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.bundles.androidx.lifecycle)
|
||||||
|
|
||||||
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
implementation(project(":database"))
|
implementation(project(":database"))
|
||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
implementation(project(":ktx"))
|
implementation(project(":ktx"))
|
||||||
|
|||||||
@ -30,11 +30,6 @@ class CalendarDynamicLauncherIcon(
|
|||||||
null
|
null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
init {
|
|
||||||
DynamicIconController.getInstance(context).registerIcon(this)
|
|
||||||
update(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
var currentDay = 0
|
var currentDay = 0
|
||||||
override fun update(context: Context) {
|
override fun update(context: Context) {
|
||||||
val calendar = Calendar.getInstance()
|
val calendar = Calendar.getInstance()
|
||||||
|
|||||||
@ -7,6 +7,8 @@ import android.graphics.drawable.LayerDrawable
|
|||||||
import android.graphics.drawable.RotateDrawable
|
import android.graphics.drawable.RotateDrawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@ -32,6 +34,7 @@ class ClockDynamicLauncherIcon(
|
|||||||
null
|
null
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
foreground.also {
|
foreground.also {
|
||||||
it.setDrawable(secondLayer, ColorDrawable(0))
|
it.setDrawable(secondLayer, ColorDrawable(0))
|
||||||
@ -40,8 +43,6 @@ class ClockDynamicLauncherIcon(
|
|||||||
(it.getDrawable(minuteLayer) as? RotateDrawable)?.fromDegrees = 0f
|
(it.getDrawable(minuteLayer) as? RotateDrawable)?.fromDegrees = 0f
|
||||||
(it.getDrawable(minuteLayer) as? RotateDrawable)?.toDegrees = 360f
|
(it.getDrawable(minuteLayer) as? RotateDrawable)?.toDegrees = 360f
|
||||||
}
|
}
|
||||||
DynamicIconController.getInstance(context).registerIcon(this)
|
|
||||||
update(context)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun update(context: Context) {
|
override fun update(context: Context) {
|
||||||
|
|||||||
@ -52,15 +52,7 @@ class DynamicIconController(val context: Context): LifecycleObserver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun registerIcon(icon: DynamicLauncherIcon) {
|
fun registerIcon(icon: DynamicLauncherIcon) {
|
||||||
|
icon.update(context)
|
||||||
registeredIcons.add(WeakReference(icon))
|
registeredIcons.add(WeakReference(icon))
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private lateinit var instance: DynamicIconController
|
|
||||||
|
|
||||||
fun getInstance(context: Context): DynamicIconController {
|
|
||||||
if(!::instance.isInitialized) instance = DynamicIconController(context.applicationContext)
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -2,6 +2,8 @@ package de.mm20.launcher2.icons
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
abstract class DynamicLauncherIcon(
|
abstract class DynamicLauncherIcon(
|
||||||
foreground: Drawable,
|
foreground: Drawable,
|
||||||
@ -18,5 +20,6 @@ abstract class DynamicLauncherIcon(
|
|||||||
backgroundScale,
|
backgroundScale,
|
||||||
autoGenerateBackgroundMode
|
autoGenerateBackgroundMode
|
||||||
) {
|
) {
|
||||||
|
|
||||||
abstract fun update(context: Context)
|
abstract fun update(context: Context)
|
||||||
}
|
}
|
||||||
@ -14,14 +14,11 @@ import android.graphics.drawable.BitmapDrawable
|
|||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.graphics.drawable.LayerDrawable
|
import android.graphics.drawable.LayerDrawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.UserHandle
|
|
||||||
import android.util.DisplayMetrics
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||||
import de.mm20.launcher2.database.AppDatabase
|
import de.mm20.launcher2.database.AppDatabase
|
||||||
import de.mm20.launcher2.ktx.dp
|
|
||||||
import de.mm20.launcher2.ktx.obtainTypedArrayOrNull
|
import de.mm20.launcher2.ktx.obtainTypedArrayOrNull
|
||||||
import de.mm20.launcher2.ktx.randomElementOrNull
|
import de.mm20.launcher2.ktx.randomElementOrNull
|
||||||
import de.mm20.launcher2.preferences.IconShape
|
import de.mm20.launcher2.preferences.IconShape
|
||||||
@ -35,18 +32,21 @@ import java.io.InputStreamReader
|
|||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
|
||||||
class IconPackManager private constructor(val context: Context) {
|
class IconPackManager(
|
||||||
|
val context: Context,
|
||||||
|
val dynamicIconController: DynamicIconController
|
||||||
|
) {
|
||||||
var selectedIconPack: String
|
var selectedIconPack: String
|
||||||
get() {
|
get() {
|
||||||
return context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)
|
return context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)
|
||||||
.getString(KEY_ICON_PACK, "")!!
|
.getString(KEY_ICON_PACK, "")!!
|
||||||
}
|
}
|
||||||
set(value) {
|
set(value) {
|
||||||
Log.d("MM20", "Selected icon pack: $value")
|
Log.d("MM20", "Selected icon pack: $value")
|
||||||
context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)
|
context.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE)
|
||||||
.edit()
|
.edit()
|
||||||
.putString(KEY_ICON_PACK, value)
|
.putString(KEY_ICON_PACK, value)
|
||||||
.apply()
|
.apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -59,7 +59,11 @@ class IconPackManager private constructor(val context: Context) {
|
|||||||
return getFromPack(context, activity, size) ?: generateIcon(context, activity, size)
|
return getFromPack(context, activity, size) ?: generateIcon(context, activity, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFromPack(context: Context, activity: LauncherActivityInfo, size: Int): LauncherIcon? {
|
private fun getFromPack(
|
||||||
|
context: Context,
|
||||||
|
activity: LauncherActivityInfo,
|
||||||
|
size: Int
|
||||||
|
): LauncherIcon? {
|
||||||
val res = try {
|
val res = try {
|
||||||
context.packageManager.getResourcesForApplication(selectedIconPack)
|
context.packageManager.getResourcesForApplication(selectedIconPack)
|
||||||
} catch (e: PackageManager.NameNotFoundException) {
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
@ -69,36 +73,42 @@ class IconPackManager private constructor(val context: Context) {
|
|||||||
val iconDao = AppDatabase.getInstance(context).iconDao()
|
val iconDao = AppDatabase.getInstance(context).iconDao()
|
||||||
val component = ComponentName(activity.applicationInfo.packageName, activity.name)
|
val component = ComponentName(activity.applicationInfo.packageName, activity.name)
|
||||||
val icon = iconDao.getIcon(component.flattenToString(), selectedIconPack)
|
val icon = iconDao.getIcon(component.flattenToString(), selectedIconPack)
|
||||||
?: return generateIcon(context, activity, size)
|
?: return generateIcon(context, activity, size)
|
||||||
|
|
||||||
if (icon.type == "calendar") {
|
if (icon.type == "calendar") {
|
||||||
return getIconPackCalendarIcon(context, icon.iconPack, icon.drawable ?: return null)
|
return getIconPackCalendarIcon(context, icon.iconPack, icon.drawable ?: return null)?.also {
|
||||||
|
dynamicIconController.registerIcon(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val drawableName = icon.drawable
|
val drawableName = icon.drawable
|
||||||
val resId = res.getIdentifier(drawableName, "drawable", selectedIconPack).takeIf { it != 0 }
|
val resId = res.getIdentifier(drawableName, "drawable", selectedIconPack).takeIf { it != 0 }
|
||||||
?: return generateIcon(context, activity, size)
|
?: return generateIcon(context, activity, size)
|
||||||
val drawable = ResourcesCompat.getDrawable(res, resId, context.theme) ?: return null
|
val drawable = ResourcesCompat.getDrawable(res, resId, context.theme) ?: return null
|
||||||
return when {
|
return when {
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && drawable is AdaptiveIconDrawable -> {
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && drawable is AdaptiveIconDrawable -> {
|
||||||
LauncherIcon(
|
LauncherIcon(
|
||||||
foreground = drawable.foreground,
|
foreground = drawable.foreground,
|
||||||
background = drawable.background,
|
background = drawable.background,
|
||||||
foregroundScale = 1.5f,
|
foregroundScale = 1.5f,
|
||||||
backgroundScale = 1.5f
|
backgroundScale = 1.5f
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
LauncherIcon(
|
LauncherIcon(
|
||||||
foreground = drawable,
|
foreground = drawable,
|
||||||
foregroundScale = getScale(),
|
foregroundScale = getScale(),
|
||||||
autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt()
|
autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun generateIcon(context: Context, activity: LauncherActivityInfo, size: Int): LauncherIcon? {
|
private fun generateIcon(
|
||||||
|
context: Context,
|
||||||
|
activity: LauncherActivityInfo,
|
||||||
|
size: Int
|
||||||
|
): LauncherIcon? {
|
||||||
val back = getIconBack()
|
val back = getIconBack()
|
||||||
val upon = getIconUpon()
|
val upon = getIconUpon()
|
||||||
val mask = getIconMask()
|
val mask = getIconMask()
|
||||||
@ -124,10 +134,12 @@ class IconPackManager private constructor(val context: Context) {
|
|||||||
val icon = drawable.toBitmap(width = size, height = size)
|
val icon = drawable.toBitmap(width = size, height = size)
|
||||||
|
|
||||||
inBounds = Rect(0, 0, icon.width, icon.height)
|
inBounds = Rect(0, 0, icon.width, icon.height)
|
||||||
outBounds = Rect((bitmap.width * (1 - scale) * 0.5).roundToInt(),
|
outBounds = Rect(
|
||||||
(bitmap.height * (1 - scale) * 0.5).roundToInt(),
|
(bitmap.width * (1 - scale) * 0.5).roundToInt(),
|
||||||
(bitmap.width - bitmap.width * (1 - scale) * 0.5).roundToInt(),
|
(bitmap.height * (1 - scale) * 0.5).roundToInt(),
|
||||||
(bitmap.height - bitmap.height * (1 - scale) * 0.5).roundToInt())
|
(bitmap.width - bitmap.width * (1 - scale) * 0.5).roundToInt(),
|
||||||
|
(bitmap.height - bitmap.height * (1 - scale) * 0.5).roundToInt()
|
||||||
|
)
|
||||||
canvas.drawBitmap(icon, inBounds, outBounds, paint)
|
canvas.drawBitmap(icon, inBounds, outBounds, paint)
|
||||||
|
|
||||||
val pack = selectedIconPack
|
val pack = selectedIconPack
|
||||||
@ -170,33 +182,39 @@ class IconPackManager private constructor(val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return LauncherIcon(
|
return LauncherIcon(
|
||||||
foreground = BitmapDrawable(context.resources, bitmap),
|
foreground = BitmapDrawable(context.resources, bitmap),
|
||||||
foregroundScale = getScale(),
|
foregroundScale = getScale(),
|
||||||
autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt()
|
autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDefaultIcon(context: Context, activity: LauncherActivityInfo): LauncherIcon? {
|
private fun getDefaultIcon(context: Context, activity: LauncherActivityInfo): LauncherIcon? {
|
||||||
if (activity.applicationInfo.packageName == GOOGLE_DESK_CLOCK_PACKAGE_NAME) {
|
if (activity.applicationInfo.packageName == GOOGLE_DESK_CLOCK_PACKAGE_NAME) {
|
||||||
getGoogleDeskClockIcon(context)?.let { return it }
|
getGoogleDeskClockIcon(context)?.let {
|
||||||
|
dynamicIconController.registerIcon(it)
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getCalendarIcon(context, activity)?.let {
|
||||||
|
dynamicIconController.registerIcon(it)
|
||||||
|
return it
|
||||||
}
|
}
|
||||||
getCalendarIcon(context, activity)?.let { return it }
|
|
||||||
try {
|
try {
|
||||||
val icon = activity.getIcon(context.resources.displayMetrics.densityDpi) ?: return null
|
val icon = activity.getIcon(context.resources.displayMetrics.densityDpi) ?: return null
|
||||||
when {
|
when {
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && icon is AdaptiveIconDrawable -> {
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && icon is AdaptiveIconDrawable -> {
|
||||||
return LauncherIcon(
|
return LauncherIcon(
|
||||||
foreground = icon.foreground ?: return null,
|
foreground = icon.foreground ?: return null,
|
||||||
background = icon.background,
|
background = icon.background,
|
||||||
foregroundScale = 1.5f,
|
foregroundScale = 1.5f,
|
||||||
backgroundScale = 1.5f
|
backgroundScale = 1.5f
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
return LauncherIcon(
|
return LauncherIcon(
|
||||||
foreground = icon,
|
foreground = icon,
|
||||||
foregroundScale = getScale(),
|
foregroundScale = getScale(),
|
||||||
autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt()
|
autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -205,7 +223,11 @@ class IconPackManager private constructor(val context: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getIconPackCalendarIcon(context: Context, iconPack: String, baseIconName: String): CalendarDynamicLauncherIcon? {
|
private fun getIconPackCalendarIcon(
|
||||||
|
context: Context,
|
||||||
|
iconPack: String,
|
||||||
|
baseIconName: String
|
||||||
|
): CalendarDynamicLauncherIcon? {
|
||||||
val resources = try {
|
val resources = try {
|
||||||
context.packageManager.getResourcesForApplication(iconPack)
|
context.packageManager.getResourcesForApplication(iconPack)
|
||||||
} catch (e: PackageManager.NameNotFoundException) {
|
} catch (e: PackageManager.NameNotFoundException) {
|
||||||
@ -218,18 +240,21 @@ class IconPackManager private constructor(val context: Context) {
|
|||||||
id
|
id
|
||||||
}.toIntArray()
|
}.toIntArray()
|
||||||
return CalendarDynamicLauncherIcon(
|
return CalendarDynamicLauncherIcon(
|
||||||
context = context,
|
context = context,
|
||||||
background = ColorDrawable(0),
|
background = ColorDrawable(0),
|
||||||
foreground = ColorDrawable(0),
|
foreground = ColorDrawable(0),
|
||||||
foregroundScale = 1.5f,
|
foregroundScale = 1.5f,
|
||||||
backgroundScale = 1.5f,
|
backgroundScale = 1.5f,
|
||||||
packageName = iconPack,
|
packageName = iconPack,
|
||||||
drawableIds = drawableIds,
|
drawableIds = drawableIds,
|
||||||
autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt()
|
autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCalendarIcon(context: Context, activity: LauncherActivityInfo): CalendarDynamicLauncherIcon? {
|
private fun getCalendarIcon(
|
||||||
|
context: Context,
|
||||||
|
activity: LauncherActivityInfo
|
||||||
|
): CalendarDynamicLauncherIcon? {
|
||||||
val component = ComponentName(activity.applicationInfo.packageName, activity.name)
|
val component = ComponentName(activity.applicationInfo.packageName, activity.name)
|
||||||
val pm = context.packageManager
|
val pm = context.packageManager
|
||||||
val ai = try {
|
val ai = try {
|
||||||
@ -240,7 +265,7 @@ class IconPackManager private constructor(val context: Context) {
|
|||||||
val resources = pm.getResourcesForActivity(component)
|
val resources = pm.getResourcesForActivity(component)
|
||||||
var arrayId = ai.metaData?.getInt("com.teslacoilsw.launcher.calendarIconArray") ?: 0
|
var arrayId = ai.metaData?.getInt("com.teslacoilsw.launcher.calendarIconArray") ?: 0
|
||||||
if (arrayId == 0) arrayId = ai.metaData?.getInt("com.google.android.calendar.dynamic_icons")
|
if (arrayId == 0) arrayId = ai.metaData?.getInt("com.google.android.calendar.dynamic_icons")
|
||||||
?: return null
|
?: return null
|
||||||
if (arrayId == 0) return null
|
if (arrayId == 0) return null
|
||||||
val typedArray = resources.obtainTypedArrayOrNull(arrayId) ?: return null
|
val typedArray = resources.obtainTypedArrayOrNull(arrayId) ?: return null
|
||||||
if (typedArray.length() != 31) {
|
if (typedArray.length() != 31) {
|
||||||
@ -253,43 +278,49 @@ class IconPackManager private constructor(val context: Context) {
|
|||||||
}
|
}
|
||||||
typedArray.recycle()
|
typedArray.recycle()
|
||||||
return CalendarDynamicLauncherIcon(
|
return CalendarDynamicLauncherIcon(
|
||||||
context = context,
|
context = context,
|
||||||
background = ColorDrawable(0),
|
background = ColorDrawable(0),
|
||||||
foreground = ColorDrawable(0),
|
foreground = ColorDrawable(0),
|
||||||
foregroundScale = 1.5f,
|
foregroundScale = 1.5f,
|
||||||
backgroundScale = 1.5f,
|
backgroundScale = 1.5f,
|
||||||
packageName = component.packageName,
|
packageName = component.packageName,
|
||||||
drawableIds = drawableIds,
|
drawableIds = drawableIds,
|
||||||
autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt()
|
autoGenerateBackgroundMode = LauncherPreferences.instance.legacyIconBg.toInt()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getGoogleDeskClockIcon(context: Context): ClockDynamicLauncherIcon? {
|
private fun getGoogleDeskClockIcon(context: Context): ClockDynamicLauncherIcon? {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return null
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return null
|
||||||
val pm = context.packageManager
|
val pm = context.packageManager
|
||||||
val appInfo = pm.getApplicationInfo(GOOGLE_DESK_CLOCK_PACKAGE_NAME, PackageManager.GET_META_DATA)
|
val appInfo =
|
||||||
|
pm.getApplicationInfo(GOOGLE_DESK_CLOCK_PACKAGE_NAME, PackageManager.GET_META_DATA)
|
||||||
?: return null
|
?: return null
|
||||||
val drawable = appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.LEVEL_PER_TICK_ICON_ROUND")
|
val drawable =
|
||||||
|
appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.LEVEL_PER_TICK_ICON_ROUND")
|
||||||
val resources = pm.getResourcesForApplication(appInfo)
|
val resources = pm.getResourcesForApplication(appInfo)
|
||||||
val baseIcon = try {
|
val baseIcon = try {
|
||||||
ResourcesCompat.getDrawable(resources, drawable, null) as? AdaptiveIconDrawable ?: return null
|
ResourcesCompat.getDrawable(resources, drawable, null) as? AdaptiveIconDrawable
|
||||||
|
?: return null
|
||||||
} catch (e: Resources.NotFoundException) {
|
} catch (e: Resources.NotFoundException) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
val foreground = baseIcon.foreground as? LayerDrawable ?: return null
|
val foreground = baseIcon.foreground as? LayerDrawable ?: return null
|
||||||
val hourLayer = appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.HOUR_LAYER_INDEX")
|
val hourLayer =
|
||||||
val minuteLayer = appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.MINUTE_LAYER_INDEX")
|
appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.HOUR_LAYER_INDEX")
|
||||||
val secondLayer = appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.SECOND_LAYER_INDEX")
|
val minuteLayer =
|
||||||
|
appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.MINUTE_LAYER_INDEX")
|
||||||
|
val secondLayer =
|
||||||
|
appInfo.metaData.getInt("com.google.android.apps.nexuslauncher.SECOND_LAYER_INDEX")
|
||||||
return ClockDynamicLauncherIcon(
|
return ClockDynamicLauncherIcon(
|
||||||
context = context,
|
context = context,
|
||||||
background = baseIcon.background,
|
background = baseIcon.background,
|
||||||
backgroundScale = 1.5f,
|
backgroundScale = 1.5f,
|
||||||
foreground = foreground,
|
foreground = foreground,
|
||||||
foregroundScale = 1.5f,
|
foregroundScale = 1.5f,
|
||||||
badgeNumber = 0f,
|
badgeNumber = 0f,
|
||||||
hourLayer = hourLayer,
|
hourLayer = hourLayer,
|
||||||
minuteLayer = minuteLayer,
|
minuteLayer = minuteLayer,
|
||||||
secondLayer = secondLayer
|
secondLayer = secondLayer
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -335,12 +366,6 @@ class IconPackManager private constructor(val context: Context) {
|
|||||||
companion object {
|
companion object {
|
||||||
const val GOOGLE_DESK_CLOCK_PACKAGE_NAME = "com.google.android.deskclock"
|
const val GOOGLE_DESK_CLOCK_PACKAGE_NAME = "com.google.android.deskclock"
|
||||||
const val GOOGLE_CALENDAR_PACKAGE_NAME = "com.google.android.calendar"
|
const val GOOGLE_CALENDAR_PACKAGE_NAME = "com.google.android.calendar"
|
||||||
|
|
||||||
private lateinit var instance: IconPackManager
|
|
||||||
fun getInstance(context: Context): IconPackManager {
|
|
||||||
if (!::instance.isInitialized) instance = IconPackManager(context.applicationContext)
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@ -363,9 +388,9 @@ class UpdateIconPacksWorker(val context: Context) {
|
|||||||
try {
|
try {
|
||||||
val packInfo = context.packageManager.getPackageInfo(pack, 0)
|
val packInfo = context.packageManager.getPackageInfo(pack, 0)
|
||||||
val iconPack = IconPack(
|
val iconPack = IconPack(
|
||||||
name = packInfo.applicationInfo.loadLabel(context.packageManager).toString(),
|
name = packInfo.applicationInfo.loadLabel(context.packageManager).toString(),
|
||||||
packageName = pack,
|
packageName = pack,
|
||||||
version = packInfo.versionName
|
version = packInfo.versionName
|
||||||
)
|
)
|
||||||
//if (iconDao.isInstalled(iconPack)) continue
|
//if (iconDao.isInstalled(iconPack)) continue
|
||||||
installIconPack(iconPack)
|
installIconPack(iconPack)
|
||||||
@ -384,7 +409,9 @@ class UpdateIconPacksWorker(val context: Context) {
|
|||||||
intent = Intent("com.novalauncher.THEME")
|
intent = Intent("com.novalauncher.THEME")
|
||||||
val novaPacks = pm.queryIntentActivities(intent, 0)
|
val novaPacks = pm.queryIntentActivities(intent, 0)
|
||||||
novaPacks.forEach {
|
novaPacks.forEach {
|
||||||
if (packs.none { p -> p.activityInfo.packageName == it.activityInfo.packageName }) packs.add(it)
|
if (packs.none { p -> p.activityInfo.packageName == it.activityInfo.packageName }) packs.add(
|
||||||
|
it
|
||||||
|
)
|
||||||
}
|
}
|
||||||
packs.sortWith(ResolveInfo.DisplayNameComparator(pm))
|
packs.sortWith(ResolveInfo.DisplayNameComparator(pm))
|
||||||
return packs
|
return packs
|
||||||
@ -401,7 +428,10 @@ class UpdateIconPacksWorker(val context: Context) {
|
|||||||
else {
|
else {
|
||||||
val rawId = res.getIdentifier("appfilter", "raw", pkgName)
|
val rawId = res.getIdentifier("appfilter", "raw", pkgName)
|
||||||
if (rawId == 0) {
|
if (rawId == 0) {
|
||||||
Log.e("MM20", "Icon pack $pkgName has no appfilter.xml, neither in xml nor in raw")
|
Log.e(
|
||||||
|
"MM20",
|
||||||
|
"Icon pack $pkgName has no appfilter.xml, neither in xml nor in raw"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
parser = XmlPullParserFactory.newInstance().newPullParser()
|
parser = XmlPullParserFactory.newInstance().newPullParser()
|
||||||
@ -417,33 +447,43 @@ class UpdateIconPacksWorker(val context: Context) {
|
|||||||
when (parser.name) {
|
when (parser.name) {
|
||||||
"item" -> {
|
"item" -> {
|
||||||
val component = parser.getAttributeValue(null, "component")
|
val component = parser.getAttributeValue(null, "component")
|
||||||
?: continue@loop
|
?: continue@loop
|
||||||
val drawable = parser.getAttributeValue(null, "drawable")
|
val drawable = parser.getAttributeValue(null, "drawable")
|
||||||
?: continue@loop
|
?: continue@loop
|
||||||
if (component.length <= 14) continue@loop
|
if (component.length <= 14) continue@loop
|
||||||
val componentName = ComponentName.unflattenFromString(component.substring(14, component.lastIndex))
|
val componentName = ComponentName.unflattenFromString(
|
||||||
?: continue@loop
|
component.substring(
|
||||||
|
14,
|
||||||
|
component.lastIndex
|
||||||
|
)
|
||||||
|
)
|
||||||
|
?: continue@loop
|
||||||
val icon = Icon(
|
val icon = Icon(
|
||||||
componentName = componentName,
|
componentName = componentName,
|
||||||
drawable = drawable,
|
drawable = drawable,
|
||||||
iconPack = pkgName,
|
iconPack = pkgName,
|
||||||
type = "app"
|
type = "app"
|
||||||
)
|
)
|
||||||
icons.add(icon)
|
icons.add(icon)
|
||||||
}
|
}
|
||||||
"calendar" -> {
|
"calendar" -> {
|
||||||
val component = parser.getAttributeValue(null, "component")
|
val component = parser.getAttributeValue(null, "component")
|
||||||
?: continue@loop
|
?: continue@loop
|
||||||
val drawable = parser.getAttributeValue(null, "prefix") ?: continue@loop
|
val drawable = parser.getAttributeValue(null, "prefix") ?: continue@loop
|
||||||
if (component.length < 14) continue@loop
|
if (component.length < 14) continue@loop
|
||||||
val componentName = ComponentName.unflattenFromString(component.substring(14, component.lastIndex))
|
val componentName = ComponentName.unflattenFromString(
|
||||||
?: continue@loop
|
component.substring(
|
||||||
|
14,
|
||||||
|
component.lastIndex
|
||||||
|
)
|
||||||
|
)
|
||||||
|
?: continue@loop
|
||||||
|
|
||||||
val icon = Icon(
|
val icon = Icon(
|
||||||
componentName = componentName,
|
componentName = componentName,
|
||||||
drawable = drawable,
|
drawable = drawable,
|
||||||
iconPack = pkgName,
|
iconPack = pkgName,
|
||||||
type = "calendar"
|
type = "calendar"
|
||||||
)
|
)
|
||||||
icons.add(icon)
|
icons.add(icon)
|
||||||
}
|
}
|
||||||
@ -452,10 +492,10 @@ class UpdateIconPacksWorker(val context: Context) {
|
|||||||
if (parser.getAttributeName(i).startsWith("img")) {
|
if (parser.getAttributeName(i).startsWith("img")) {
|
||||||
val drawable = parser.getAttributeValue(i)
|
val drawable = parser.getAttributeValue(i)
|
||||||
val icon = Icon(
|
val icon = Icon(
|
||||||
componentName = null,
|
componentName = null,
|
||||||
drawable = drawable,
|
drawable = drawable,
|
||||||
iconPack = pkgName,
|
iconPack = pkgName,
|
||||||
type = "iconback"
|
type = "iconback"
|
||||||
)
|
)
|
||||||
icons.add(icon)
|
icons.add(icon)
|
||||||
}
|
}
|
||||||
@ -466,10 +506,10 @@ class UpdateIconPacksWorker(val context: Context) {
|
|||||||
if (parser.getAttributeName(i).startsWith("img")) {
|
if (parser.getAttributeName(i).startsWith("img")) {
|
||||||
val drawable = parser.getAttributeValue(i)
|
val drawable = parser.getAttributeValue(i)
|
||||||
val icon = Icon(
|
val icon = Icon(
|
||||||
componentName = null,
|
componentName = null,
|
||||||
drawable = drawable,
|
drawable = drawable,
|
||||||
iconPack = pkgName,
|
iconPack = pkgName,
|
||||||
type = "iconupon"
|
type = "iconupon"
|
||||||
)
|
)
|
||||||
icons.add(icon)
|
icons.add(icon)
|
||||||
}
|
}
|
||||||
@ -480,10 +520,10 @@ class UpdateIconPacksWorker(val context: Context) {
|
|||||||
if (parser.getAttributeName(i).startsWith("img")) {
|
if (parser.getAttributeName(i).startsWith("img")) {
|
||||||
val drawable = parser.getAttributeValue(i)
|
val drawable = parser.getAttributeValue(i)
|
||||||
val icon = Icon(
|
val icon = Icon(
|
||||||
componentName = null,
|
componentName = null,
|
||||||
drawable = drawable,
|
drawable = drawable,
|
||||||
iconPack = pkgName,
|
iconPack = pkgName,
|
||||||
type = "iconmask"
|
type = "iconmask"
|
||||||
)
|
)
|
||||||
icons.add(icon)
|
icons.add(icon)
|
||||||
}
|
}
|
||||||
@ -491,13 +531,15 @@ class UpdateIconPacksWorker(val context: Context) {
|
|||||||
}
|
}
|
||||||
"scale" -> {
|
"scale" -> {
|
||||||
val scale = parser.getAttributeValue(null, "factor")?.toFloatOrNull()
|
val scale = parser.getAttributeValue(null, "factor")?.toFloatOrNull()
|
||||||
?: continue@loop
|
?: continue@loop
|
||||||
iconPack.scale = scale
|
iconPack.scale = scale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
iconDao.installIconPack(iconPack.toDatabaseEntity(), icons.map { it.toDatabaseEntity() })
|
iconDao.installIconPack(
|
||||||
|
iconPack.toDatabaseEntity(),
|
||||||
|
icons.map { it.toDatabaseEntity() })
|
||||||
|
|
||||||
(parser as? XmlResourceParser)?.close()
|
(parser as? XmlResourceParser)?.close()
|
||||||
inStream?.close()
|
inStream?.close()
|
||||||
|
|||||||
@ -1,17 +1,41 @@
|
|||||||
package de.mm20.launcher2.icons
|
package de.mm20.launcher2.icons
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
import android.util.LruCache
|
import android.util.LruCache
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.data.Searchable
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
|
|
||||||
class IconRepository private constructor(val context: Context) {
|
class IconRepository(
|
||||||
|
val context: Context,
|
||||||
|
val iconPackManager: IconPackManager
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val appReceiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
requestIconPackListUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
|
||||||
|
|
||||||
|
init {
|
||||||
|
requestIconPackListUpdate()
|
||||||
|
context.registerReceiver(appReceiver, IntentFilter().apply {
|
||||||
|
addAction(Intent.ACTION_PACKAGE_REPLACED)
|
||||||
|
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||||
|
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||||
|
addAction(Intent.ACTION_MY_PACKAGE_REPLACED)
|
||||||
|
addAction(Intent.ACTION_PACKAGE_CHANGED)
|
||||||
|
addDataScheme("package")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private val cache = LruCache<String, LauncherIcon>(200)
|
private val cache = LruCache<String, LauncherIcon>(200)
|
||||||
|
|
||||||
fun getIcon(searchable: Searchable, size: Int): Flow<LauncherIcon> = flow {
|
fun getIcon(searchable: Searchable, size: Int): Flow<LauncherIcon> = flow {
|
||||||
@ -43,7 +67,7 @@ class IconRepository private constructor(val context: Context) {
|
|||||||
|
|
||||||
fun requestIconPackListUpdate() {
|
fun requestIconPackListUpdate() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
IconPackManager.getInstance(context).updateIconPacks()
|
iconPackManager.updateIconPacks()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,14 +78,4 @@ class IconRepository private constructor(val context: Context) {
|
|||||||
fun clearCache() {
|
fun clearCache() {
|
||||||
cache.evictAll()
|
cache.evictAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private lateinit var instance: IconRepository
|
|
||||||
|
|
||||||
fun getInstance(context: Context): IconRepository {
|
|
||||||
if (!::instance.isInitialized) instance = IconRepository(context.applicationContext)
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
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.appcompat)
|
||||||
implementation(libs.androidx.media2)
|
implementation(libs.androidx.media2)
|
||||||
|
|
||||||
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
implementation(project(":ktx"))
|
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.io.File
|
||||||
import java.util.concurrent.Executors
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
class MusicRepository private constructor(val context: Context) {
|
class MusicRepository(val context: Context) {
|
||||||
|
|
||||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
|
||||||
@ -245,12 +245,6 @@ class MusicRepository private constructor(val context: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private lateinit var instance: MusicRepository
|
|
||||||
|
|
||||||
fun getInstance(context: Context): MusicRepository {
|
|
||||||
if (!::instance.isInitialized) instance = MusicRepository(context.applicationContext)
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
|
|
||||||
private const val PREFS = "music"
|
private const val PREFS = "music"
|
||||||
private const val PREFS_KEY_TITLE = "title"
|
private const val PREFS_KEY_TITLE = "title"
|
||||||
|
|||||||
@ -1,16 +1,14 @@
|
|||||||
package de.mm20.launcher2.music
|
package de.mm20.launcher2.music
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.ViewModel
|
||||||
|
|
||||||
class MusicViewModel(app: Application) : AndroidViewModel(app) {
|
class MusicViewModel(
|
||||||
|
val musicRepository: MusicRepository
|
||||||
val musicRepository = MusicRepository.getInstance(app)
|
) : ViewModel() {
|
||||||
|
|
||||||
val title: LiveData<String?> = musicRepository.title
|
val title: LiveData<String?> = musicRepository.title
|
||||||
val artist: LiveData<String?> = musicRepository.artist
|
val artist: LiveData<String?> = musicRepository.artist
|
||||||
|
|||||||
@ -43,6 +43,8 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.bundles.androidx.lifecycle)
|
implementation(libs.bundles.androidx.lifecycle)
|
||||||
|
|
||||||
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
implementation(project(":music"))
|
implementation(project(":music"))
|
||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
implementation(project(":badges"))
|
implementation(project(":badges"))
|
||||||
|
|||||||
@ -13,10 +13,15 @@ import de.mm20.launcher2.badges.Badge
|
|||||||
import de.mm20.launcher2.badges.BadgeProvider
|
import de.mm20.launcher2.badges.BadgeProvider
|
||||||
import de.mm20.launcher2.music.MusicRepository
|
import de.mm20.launcher2.music.MusicRepository
|
||||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
class NotificationService : NotificationListenerService() {
|
class NotificationService : NotificationListenerService() {
|
||||||
|
|
||||||
|
private val musicRepository: MusicRepository by inject()
|
||||||
|
|
||||||
|
private val badgeProvider: BadgeProvider by inject()
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
return Service.START_STICKY
|
return Service.START_STICKY
|
||||||
}
|
}
|
||||||
@ -30,7 +35,7 @@ class NotificationService : NotificationListenerService() {
|
|||||||
if (packageManager.queryIntentActivities(intent, 0).none { it.activityInfo.packageName == n.packageName }) continue
|
if (packageManager.queryIntentActivities(intent, 0).none { it.activityInfo.packageName == n.packageName }) continue
|
||||||
val token = n.notification.extras[NotificationCompat.EXTRA_MEDIA_SESSION] as? MediaSession.Token
|
val token = n.notification.extras[NotificationCompat.EXTRA_MEDIA_SESSION] as? MediaSession.Token
|
||||||
?: continue
|
?: continue
|
||||||
MusicRepository.getInstance(this).setMediaSession(MediaSessionCompat.Token.fromToken(token))
|
musicRepository.setMediaSession(MediaSessionCompat.Token.fromToken(token))
|
||||||
}
|
}
|
||||||
if (LauncherPreferences.instance.notificationBadges) {
|
if (LauncherPreferences.instance.notificationBadges) {
|
||||||
generateBadges()
|
generateBadges()
|
||||||
@ -46,16 +51,16 @@ class NotificationService : NotificationListenerService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun generateBadges() {
|
fun generateBadges() {
|
||||||
BadgeProvider.getInstance(this).removeNotificationBadges()
|
badgeProvider.removeNotificationBadges()
|
||||||
getNotifications().forEach {
|
getNotifications().forEach {
|
||||||
val pkg = it.packageName
|
val pkg = it.packageName
|
||||||
val badge = BadgeProvider.getInstance(this).getBadge("app://$pkg") ?: Badge()
|
val badge = badgeProvider.getBadge("app://$pkg") ?: Badge()
|
||||||
badge.number = activeNotifications.filter {
|
badge.number = activeNotifications.filter {
|
||||||
it.packageName == pkg
|
it.packageName == pkg
|
||||||
}.sumBy {
|
}.sumBy {
|
||||||
it.notification.number
|
it.notification.number
|
||||||
}
|
}
|
||||||
BadgeProvider.getInstance(this).setBadge("app://$pkg", badge)
|
badgeProvider.setBadge("app://$pkg", badge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,15 +78,15 @@ class NotificationService : NotificationListenerService() {
|
|||||||
if (packageManager.queryIntentActivities(intent, 0).none { it.activityInfo.packageName == sbn.packageName }) return
|
if (packageManager.queryIntentActivities(intent, 0).none { it.activityInfo.packageName == sbn.packageName }) return
|
||||||
val token = sbn.notification.extras[NotificationCompat.EXTRA_MEDIA_SESSION] as? MediaSession.Token
|
val token = sbn.notification.extras[NotificationCompat.EXTRA_MEDIA_SESSION] as? MediaSession.Token
|
||||||
?: return
|
?: return
|
||||||
MusicRepository.getInstance(this).setMediaSession(MediaSessionCompat.Token.fromToken(token))
|
musicRepository.setMediaSession(MediaSessionCompat.Token.fromToken(token))
|
||||||
}
|
}
|
||||||
if (LauncherPreferences.instance.notificationBadges) {
|
if (LauncherPreferences.instance.notificationBadges) {
|
||||||
val pkg = sbn.packageName
|
val pkg = sbn.packageName
|
||||||
val badge = BadgeProvider.getInstance(this).getBadge("app://$pkg") ?: Badge()
|
val badge = badgeProvider.getBadge("app://$pkg") ?: Badge()
|
||||||
badge.number = activeNotifications.filter { it.packageName == pkg }.sumBy {
|
badge.number = activeNotifications.filter { it.packageName == pkg }.sumBy {
|
||||||
it.notification.number
|
it.notification.number
|
||||||
}
|
}
|
||||||
BadgeProvider.getInstance(this).setBadge("app://$pkg", badge)
|
badgeProvider.setBadge("app://$pkg", badge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,20 +96,20 @@ class NotificationService : NotificationListenerService() {
|
|||||||
if (LauncherPreferences.instance.notificationBadges) {
|
if (LauncherPreferences.instance.notificationBadges) {
|
||||||
val pkg = sbn.packageName
|
val pkg = sbn.packageName
|
||||||
if (getNotifications().any { it.packageName == pkg && it.id != sbn.id }) {
|
if (getNotifications().any { it.packageName == pkg && it.id != sbn.id }) {
|
||||||
val badge = BadgeProvider.getInstance(this).getBadge("app://$pkg") ?: Badge()
|
val badge = badgeProvider.getBadge("app://$pkg") ?: Badge()
|
||||||
badge.number = activeNotifications.filter { it.packageName == pkg }.sumBy {
|
badge.number = activeNotifications.filter { it.packageName == pkg }.sumBy {
|
||||||
it.notification.number
|
it.notification.number
|
||||||
}
|
}
|
||||||
BadgeProvider.getInstance(this).setBadge("app://$pkg", badge)
|
badgeProvider.setBadge("app://$pkg", badge)
|
||||||
} else {
|
} else {
|
||||||
BadgeProvider.getInstance(this).removeBadge("app://$pkg")
|
badgeProvider.removeBadge("app://$pkg")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onListenerDisconnected() {
|
override fun onListenerDisconnected() {
|
||||||
super.onListenerDisconnected()
|
super.onListenerDisconnected()
|
||||||
BadgeProvider.getInstance(this).removeNotificationBadges()
|
badgeProvider.removeNotificationBadges()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@ -42,6 +42,8 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.bundles.androidx.lifecycle)
|
implementation(libs.bundles.androidx.lifecycle)
|
||||||
|
|
||||||
|
implementation(libs.koin.android)
|
||||||
|
|
||||||
implementation(project(":base"))
|
implementation(project(":base"))
|
||||||
implementation(project(":database"))
|
implementation(project(":database"))
|
||||||
}
|
}
|
||||||
@ -1,13 +1,16 @@
|
|||||||
package de.mm20.launcher2.search
|
package de.mm20.launcher2.search
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
import kotlin.coroutines.EmptyCoroutineContext
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
abstract class BaseSearchableRepository {
|
abstract class BaseSearchableRepository: KoinComponent {
|
||||||
|
|
||||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
private val searchQuery = SearchRepository.getInstance().currentQuery
|
val searchRepository: SearchRepository by inject()
|
||||||
|
private val searchQuery = searchRepository.currentQuery
|
||||||
|
|
||||||
init {
|
init {
|
||||||
searchQuery.observeForever {
|
searchQuery.observeForever {
|
||||||
@ -33,10 +36,10 @@ abstract class BaseSearchableRepository {
|
|||||||
onCancel()
|
onCancel()
|
||||||
searchJob?.takeIf { !it.isCompleted || !it.isCancelled }?.cancelAndJoin()
|
searchJob?.takeIf { !it.isCompleted || !it.isCancelled }?.cancelAndJoin()
|
||||||
searchJob = scope.launch {
|
searchJob = scope.launch {
|
||||||
SearchRepository.getInstance().startSearch()
|
searchRepository.startSearch()
|
||||||
search(query)
|
search(query)
|
||||||
}.also {
|
}.also {
|
||||||
it.invokeOnCompletion { SearchRepository.getInstance().endSearch() }
|
it.invokeOnCompletion { searchRepository.endSearch() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
|
||||||
class SearchRepository private constructor() {
|
class SearchRepository {
|
||||||
|
|
||||||
val isSearching = MutableLiveData<Boolean>(false)
|
val isSearching = MutableLiveData<Boolean>(false)
|
||||||
val currentQuery = MutableLiveData<String>()
|
val currentQuery = MutableLiveData<String>()
|
||||||
@ -27,12 +27,4 @@ class SearchRepository private constructor() {
|
|||||||
runningSearches--
|
runningSearches--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private lateinit var instance: SearchRepository
|
|
||||||
fun getInstance(): SearchRepository {
|
|
||||||
if (!::instance.isInitialized) instance = SearchRepository()
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,16 +1,16 @@
|
|||||||
package de.mm20.launcher2.search
|
package de.mm20.launcher2.search
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
|
||||||
class SearchViewModel(app: Application) : AndroidViewModel(app) {
|
class SearchViewModel(
|
||||||
private val repository = SearchRepository.getInstance()
|
private val searchRepository: SearchRepository
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
val isSearching: LiveData<Boolean> = repository.isSearching
|
val isSearching: LiveData<Boolean> = searchRepository.isSearching
|
||||||
|
|
||||||
fun search(query: String) {
|
fun search(query: String) {
|
||||||
repository.currentQuery.value = query
|
searchRepository.currentQuery.value = query
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -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.delay
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class WebsearchRepository private constructor(val context: Context) : BaseSearchableRepository() {
|
class WebsearchRepository(val context: Context) : BaseSearchableRepository() {
|
||||||
|
|
||||||
val websearches = MutableLiveData<List<Websearch>>(emptyList())
|
val websearches = MutableLiveData<List<Websearch>>(emptyList())
|
||||||
|
|
||||||
@ -52,13 +52,4 @@ class WebsearchRepository private constructor(val context: Context) : BaseSearch
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
private lateinit var instance: WebsearchRepository
|
|
||||||
|
|
||||||
fun getInstance(context: Context): WebsearchRepository {
|
|
||||||
if (!::instance.isInitialized) instance = WebsearchRepository(context.applicationContext)
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -1,22 +1,22 @@
|
|||||||
package de.mm20.launcher2.search
|
package de.mm20.launcher2.search
|
||||||
|
|
||||||
import android.app.Application
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import de.mm20.launcher2.search.data.Websearch
|
import de.mm20.launcher2.search.data.Websearch
|
||||||
|
|
||||||
class WebsearchViewModel(app:Application): AndroidViewModel(app) {
|
class WebsearchViewModel(
|
||||||
|
private val websearchRepository: WebsearchRepository
|
||||||
|
): ViewModel() {
|
||||||
|
|
||||||
private val repository = WebsearchRepository.getInstance(app)
|
|
||||||
|
|
||||||
fun insertWebsearch(websearch: Websearch) {
|
fun insertWebsearch(websearch: Websearch) {
|
||||||
return repository.insertWebsearch(websearch)
|
return websearchRepository.insertWebsearch(websearch)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteWebsearch(websearch: Websearch) {
|
fun deleteWebsearch(websearch: Websearch) {
|
||||||
repository.deleteWebsearch(websearch)
|
websearchRepository.deleteWebsearch(websearch)
|
||||||
}
|
}
|
||||||
|
|
||||||
val websearches = repository.websearches
|
val websearches = websearchRepository.websearches
|
||||||
val allWebsearches = repository.allWebsearches
|
val allWebsearches = websearchRepository.allWebsearches
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -391,15 +391,9 @@ dependencyResolutionManagement {
|
|||||||
alias("koin.android")
|
alias("koin.android")
|
||||||
.to("io.insert-koin", "koin-android")
|
.to("io.insert-koin", "koin-android")
|
||||||
.versionRef("koin")
|
.versionRef("koin")
|
||||||
alias("koin.androidviewmodel")
|
alias("koin.androidxcompose")
|
||||||
.to("io.insert-koin", "koin-android-viewmodel")
|
.to("io.insert-koin", "koin-androidx-compose")
|
||||||
.versionRef("koin")
|
.versionRef("koin")
|
||||||
bundle(
|
|
||||||
"koin", listOf(
|
|
||||||
"koin.android",
|
|
||||||
"koin.androidviewmodel"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -93,6 +93,9 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.jsoup)
|
implementation(libs.jsoup)
|
||||||
|
|
||||||
|
implementation(libs.koin.android)
|
||||||
|
implementation(libs.koin.androidxcompose)
|
||||||
|
|
||||||
implementation(project(":base"))
|
implementation(project(":base"))
|
||||||
implementation(project(":i18n"))
|
implementation(project(":i18n"))
|
||||||
implementation(project(":compat"))
|
implementation(project(":compat"))
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import de.mm20.launcher2.favorites.FavoritesViewModel
|
|||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.data.Searchable
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.theme.divider
|
import de.mm20.launcher2.ui.theme.divider
|
||||||
|
import org.koin.androidx.compose.getViewModel
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalAnimationApi::class)
|
@OptIn(ExperimentalMaterialApi::class, ExperimentalAnimationApi::class)
|
||||||
@ -39,7 +40,7 @@ fun DefaultSwipeActions(
|
|||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
content: @Composable RowScope.() -> Unit
|
content: @Composable RowScope.() -> Unit
|
||||||
) {
|
) {
|
||||||
val viewModel: FavoritesViewModel = viewModel()
|
val viewModel: FavoritesViewModel = getViewModel()
|
||||||
|
|
||||||
val isPinned by viewModel.isPinned(item).observeAsState()
|
val isPinned by viewModel.isPinned(item).observeAsState()
|
||||||
val isHidden by viewModel.isHidden(item).observeAsState()
|
val isHidden by viewModel.isHidden(item).observeAsState()
|
||||||
|
|||||||
@ -19,16 +19,15 @@ import androidx.compose.ui.graphics.SolidColor
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
|
||||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
import com.google.accompanist.pager.PagerState
|
import com.google.accompanist.pager.PagerState
|
||||||
import de.mm20.launcher2.search.SearchViewModel
|
import de.mm20.launcher2.search.SearchViewModel
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.locals.LocalNavController
|
import de.mm20.launcher2.ui.locals.LocalNavController
|
||||||
import de.mm20.launcher2.ui.locals.LocalWindowSize
|
import de.mm20.launcher2.ui.locals.LocalWindowSize
|
||||||
|
import org.koin.androidx.compose.getViewModel
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search bar
|
* Search bar
|
||||||
@ -47,7 +46,7 @@ fun SearchBar(
|
|||||||
) {
|
) {
|
||||||
var searchQuery by remember { mutableStateOf("") }
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
|
|
||||||
val viewModel: SearchViewModel = viewModel()
|
val viewModel: SearchViewModel = getViewModel()
|
||||||
|
|
||||||
LaunchedEffect(searchQuery) {
|
LaunchedEffect(searchQuery) {
|
||||||
viewModel.search(searchQuery)
|
viewModel.search(searchQuery)
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
|
|||||||
import de.mm20.launcher2.favorites.FavoritesViewModel
|
import de.mm20.launcher2.favorites.FavoritesViewModel
|
||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.data.Searchable
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
|
import org.koin.androidx.compose.getViewModel
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -162,7 +163,7 @@ data class ToggleToolbarAction(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun favoritesToolbarAction(item: Searchable): ToggleToolbarAction {
|
fun favoritesToolbarAction(item: Searchable): ToggleToolbarAction {
|
||||||
val viewModel = viewModel<FavoritesViewModel>()
|
val viewModel: FavoritesViewModel = getViewModel()
|
||||||
val isPinned by viewModel.isPinned(item).observeAsState(false)
|
val isPinned by viewModel.isPinned(item).observeAsState(false)
|
||||||
|
|
||||||
return ToggleToolbarAction(
|
return ToggleToolbarAction(
|
||||||
@ -183,7 +184,7 @@ fun favoritesToolbarAction(item: Searchable): ToggleToolbarAction {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun hideToolbarAction(item: Searchable): ToggleToolbarAction {
|
fun hideToolbarAction(item: Searchable): ToggleToolbarAction {
|
||||||
val viewModel = viewModel<FavoritesViewModel>()
|
val viewModel: FavoritesViewModel = getViewModel()
|
||||||
val isHidden by viewModel.isHidden(item).observeAsState(false)
|
val isHidden by viewModel.isHidden(item).observeAsState(false)
|
||||||
|
|
||||||
return ToggleToolbarAction(
|
return ToggleToolbarAction(
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import de.mm20.launcher2.ui.locals.LocalWindowSize
|
|||||||
import de.mm20.launcher2.ui.widget.WidgetCard
|
import de.mm20.launcher2.ui.widget.WidgetCard
|
||||||
import de.mm20.launcher2.widgets.Widget
|
import de.mm20.launcher2.widgets.Widget
|
||||||
import de.mm20.launcher2.widgets.WidgetViewModel
|
import de.mm20.launcher2.widgets.WidgetViewModel
|
||||||
|
import org.koin.androidx.compose.getViewModel
|
||||||
|
|
||||||
@OptIn(ExperimentalAnimationApi::class, ExperimentalComposeUiApi::class, ExperimentalAnimationGraphicsApi::class
|
@OptIn(ExperimentalAnimationApi::class, ExperimentalComposeUiApi::class, ExperimentalAnimationGraphicsApi::class
|
||||||
)
|
)
|
||||||
@ -39,7 +40,7 @@ fun WidgetColumn(
|
|||||||
|
|
||||||
var widgets by remember { mutableStateOf(listOf<Widget>()) }
|
var widgets by remember { mutableStateOf(listOf<Widget>()) }
|
||||||
|
|
||||||
val viewModel: WidgetViewModel = viewModel()
|
val viewModel: WidgetViewModel = getViewModel()
|
||||||
|
|
||||||
var editMode by remember { mutableStateOf(false) }
|
var editMode by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
|||||||
@ -68,6 +68,8 @@ import de.mm20.launcher2.widgets.WidgetType
|
|||||||
import de.mm20.launcher2.widgets.WidgetViewModel
|
import de.mm20.launcher2.widgets.WidgetViewModel
|
||||||
import kotlinx.android.synthetic.main.activity_launcher.*
|
import kotlinx.android.synthetic.main.activity_launcher.*
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@ -84,8 +86,9 @@ class LauncherActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private lateinit var overlayView: ViewGroupOverlay
|
private lateinit var overlayView: ViewGroupOverlay
|
||||||
|
|
||||||
private lateinit var searchViewModel: SearchViewModel
|
private val searchViewModel: SearchViewModel by viewModel()
|
||||||
private lateinit var widgetViewModel: WidgetViewModel
|
private val widgetViewModel: WidgetViewModel by viewModel()
|
||||||
|
private val favoritesViewModel: FavoritesViewModel by viewModel()
|
||||||
|
|
||||||
private val preferences = LauncherPreferences.instance
|
private val preferences = LauncherPreferences.instance
|
||||||
|
|
||||||
@ -205,8 +208,6 @@ class LauncherActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
overlayView = rootView.overlay
|
overlayView = rootView.overlay
|
||||||
|
|
||||||
searchViewModel = ViewModelProvider(this)[SearchViewModel::class.java]
|
|
||||||
widgetViewModel = ViewModelProvider(this)[WidgetViewModel::class.java]
|
|
||||||
|
|
||||||
|
|
||||||
scrollContainer.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
scrollContainer.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||||
@ -302,8 +303,7 @@ class LauncherActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
hiddenItemsGrid.columnCount =
|
hiddenItemsGrid.columnCount =
|
||||||
resources.getInteger(R.integer.config_columnCount)
|
resources.getInteger(R.integer.config_columnCount)
|
||||||
val hiddenItems =
|
val hiddenItems = favoritesViewModel.hiddenItems
|
||||||
ViewModelProvider(this)[FavoritesViewModel::class.java].hiddenItems
|
|
||||||
hiddenItems.observe(this) {
|
hiddenItems.observe(this) {
|
||||||
hiddenItemsGrid.submitItems(it)
|
hiddenItemsGrid.submitItems(it)
|
||||||
}
|
}
|
||||||
@ -359,7 +359,9 @@ class LauncherActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lifecycle.addObserver(DynamicIconController.getInstance(this))
|
val dynamicIconController: DynamicIconController by inject()
|
||||||
|
|
||||||
|
lifecycle.addObserver(dynamicIconController)
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
widgets.addAll(widgetViewModel.getWidgets())
|
widgets.addAll(widgetViewModel.getWidgets())
|
||||||
@ -399,10 +401,9 @@ class LauncherActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun addWidget() {
|
private fun addWidget() {
|
||||||
val viewModel = ViewModelProvider(this)[WidgetViewModel::class.java]
|
|
||||||
val usedWidgets = widgets.filter { it.type == WidgetType.INTERNAL }.map { it.data }
|
val usedWidgets = widgets.filter { it.type == WidgetType.INTERNAL }.map { it.data }
|
||||||
val internalWidgets =
|
val internalWidgets =
|
||||||
viewModel.getInternalWidgets().filter { !usedWidgets.contains(it.data) }
|
widgetViewModel.getInternalWidgets().filter { !usedWidgets.contains(it.data) }
|
||||||
if (internalWidgets.isNotEmpty()) {
|
if (internalWidgets.isNotEmpty()) {
|
||||||
MaterialDialog(this).show {
|
MaterialDialog(this).show {
|
||||||
val widgetList =
|
val widgetList =
|
||||||
@ -652,11 +653,11 @@ class LauncherActivity : AppCompatActivity() {
|
|||||||
ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(this)
|
ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(this)
|
||||||
}
|
}
|
||||||
PermissionsManager.CALENDAR -> {
|
PermissionsManager.CALENDAR -> {
|
||||||
ViewModelProvider(this)[WidgetViewModel::class.java].requestCalendarUpdate()
|
widgetViewModel.requestCalendarUpdate()
|
||||||
}
|
}
|
||||||
PermissionsManager.ALL -> {
|
PermissionsManager.ALL -> {
|
||||||
ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(this)
|
ViewModelProvider(this).get(WeatherViewModel::class.java).requestUpdate(this)
|
||||||
ViewModelProvider(this)[WidgetViewModel::class.java].requestCalendarUpdate()
|
widgetViewModel.requestCalendarUpdate()
|
||||||
search(searchBar.getSearchQuery())
|
search(searchBar.getSearchQuery())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import de.mm20.launcher2.applications.AppViewModel
|
|||||||
import de.mm20.launcher2.search.data.Application
|
import de.mm20.launcher2.search.data.Application
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import kotlinx.android.synthetic.main.view_application.view.*
|
import kotlinx.android.synthetic.main.view_application.view.*
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
class ApplicationView : FrameLayout {
|
class ApplicationView : FrameLayout {
|
||||||
|
|
||||||
@ -25,7 +26,8 @@ class ApplicationView : FrameLayout {
|
|||||||
layoutTransition = LayoutTransition()
|
layoutTransition = LayoutTransition()
|
||||||
layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||||
applicationCard.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
applicationCard.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||||
applications = ViewModelProvider(context as AppCompatActivity).get(AppViewModel::class.java).applications
|
val viewModel: AppViewModel by (context as AppCompatActivity).viewModel()
|
||||||
|
applications = viewModel.applications
|
||||||
applications.observe(context as AppCompatActivity, Observer<List<Application>> {
|
applications.observe(context as AppCompatActivity, Observer<List<Application>> {
|
||||||
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE
|
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE
|
||||||
applicationGrid.submitItems(it)
|
applicationGrid.submitItems(it)
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import de.mm20.launcher2.ui.R
|
|||||||
import de.mm20.launcher2.calculator.CalculatorViewModel
|
import de.mm20.launcher2.calculator.CalculatorViewModel
|
||||||
import de.mm20.launcher2.search.data.Calculator
|
import de.mm20.launcher2.search.data.Calculator
|
||||||
import kotlinx.android.synthetic.main.view_calculator.view.*
|
import kotlinx.android.synthetic.main.view_calculator.view.*
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import kotlin.math.round
|
import kotlin.math.round
|
||||||
|
|
||||||
class CalculatorView : FrameLayout {
|
class CalculatorView : FrameLayout {
|
||||||
@ -24,7 +25,8 @@ class CalculatorView : FrameLayout {
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
View.inflate(context, R.layout.view_calculator, this)
|
View.inflate(context, R.layout.view_calculator, this)
|
||||||
calculator = ViewModelProvider(context as AppCompatActivity).get(CalculatorViewModel::class.java).calculator
|
val viewModel: CalculatorViewModel by (context as AppCompatActivity).viewModel()
|
||||||
|
calculator = viewModel.calculator
|
||||||
calculator.observe(context as AppCompatActivity, Observer {
|
calculator.observe(context as AppCompatActivity, Observer {
|
||||||
if (it == null) visibility = View.GONE
|
if (it == null) visibility = View.GONE
|
||||||
else {
|
else {
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import de.mm20.launcher2.preferences.LauncherPreferences
|
|||||||
import de.mm20.launcher2.search.data.CalendarEvent
|
import de.mm20.launcher2.search.data.CalendarEvent
|
||||||
import de.mm20.launcher2.search.data.MissingPermission
|
import de.mm20.launcher2.search.data.MissingPermission
|
||||||
import de.mm20.launcher2.ui.legacy.search.SearchListView
|
import de.mm20.launcher2.ui.legacy.search.SearchListView
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
class CalendarView : FrameLayout {
|
class CalendarView : FrameLayout {
|
||||||
|
|
||||||
@ -32,7 +33,8 @@ class CalendarView : FrameLayout {
|
|||||||
val card = findViewById<ViewGroup>(R.id.card)
|
val card = findViewById<ViewGroup>(R.id.card)
|
||||||
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||||
val list = findViewById<SearchListView>(R.id.list)
|
val list = findViewById<SearchListView>(R.id.list)
|
||||||
calendarEvents = ViewModelProvider(context as AppCompatActivity).get(CalendarViewModel::class.java).calendarEvents
|
val viewModel: CalendarViewModel by (context as AppCompatActivity).viewModel()
|
||||||
|
calendarEvents = viewModel.calendarEvents
|
||||||
calendarEvents.observe(context as AppCompatActivity, {
|
calendarEvents.observe(context as AppCompatActivity, {
|
||||||
if (it == null) {
|
if (it == null) {
|
||||||
visibility = View.GONE
|
visibility = View.GONE
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import de.mm20.launcher2.search.data.Contact
|
|||||||
import de.mm20.launcher2.search.data.MissingPermission
|
import de.mm20.launcher2.search.data.MissingPermission
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.legacy.search.SearchListView
|
import de.mm20.launcher2.ui.legacy.search.SearchListView
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
class ContactView : FrameLayout {
|
class ContactView : FrameLayout {
|
||||||
private val contacts: LiveData<List<Contact>?>
|
private val contacts: LiveData<List<Contact>?>
|
||||||
@ -30,7 +31,8 @@ class ContactView : FrameLayout {
|
|||||||
layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||||
val card = findViewById<ViewGroup>(R.id.card)
|
val card = findViewById<ViewGroup>(R.id.card)
|
||||||
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||||
contacts = ViewModelProvider(context as AppCompatActivity).get(ContactViewModel::class.java).contacts
|
val viewModel: ContactViewModel by (context as AppCompatActivity).viewModel()
|
||||||
|
contacts = viewModel.contacts
|
||||||
val list = findViewById<SearchListView>(R.id.list)
|
val list = findViewById<SearchListView>(R.id.list)
|
||||||
contacts.observe(context as AppCompatActivity, {
|
contacts.observe(context as AppCompatActivity, {
|
||||||
if (it == null) {
|
if (it == null) {
|
||||||
|
|||||||
@ -12,15 +12,20 @@ import de.mm20.launcher2.ui.R
|
|||||||
import kotlinx.android.synthetic.main.edit_favorites_row.view.*
|
import kotlinx.android.synthetic.main.edit_favorites_row.view.*
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
class EditFavoritesRow @JvmOverloads constructor(
|
class EditFavoritesRow @JvmOverloads constructor(
|
||||||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, val favoritesItem: FavoritesItem
|
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, val favoritesItem: FavoritesItem
|
||||||
) : LinearLayout(context, attrs, defStyleAttr) {
|
) : LinearLayout(context, attrs, defStyleAttr), KoinComponent {
|
||||||
|
|
||||||
|
val iconRepository: IconRepository by inject()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
View.inflate(context, R.layout.edit_favorites_row, this)
|
View.inflate(context, R.layout.edit_favorites_row, this)
|
||||||
label.text = favoritesItem.searchable?.label
|
label.text = favoritesItem.searchable?.label
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
IconRepository.getInstance(context).getIcon(favoritesItem.searchable!!, (48*dp).toInt()).collect{
|
iconRepository.getIcon(favoritesItem.searchable!!, (48*dp).toInt()).collect{
|
||||||
icon.icon = it
|
icon.icon = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,10 +21,14 @@ import kotlinx.android.synthetic.main.dialog_edit_favorites.view.*
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
class EditFavoritesView @JvmOverloads constructor(
|
class EditFavoritesView @JvmOverloads constructor(
|
||||||
context: Context, attrs: AttributeSet? = null
|
context: Context, attrs: AttributeSet? = null
|
||||||
) : FrameLayout(context, attrs) {
|
) : FrameLayout(context, attrs) {
|
||||||
|
|
||||||
|
val viewModel : FavoritesViewModel by (context as AppCompatActivity).viewModel()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
View.inflate(context, R.layout.dialog_edit_favorites, this)
|
View.inflate(context, R.layout.dialog_edit_favorites, this)
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
@ -35,7 +39,6 @@ class EditFavoritesView @JvmOverloads constructor(
|
|||||||
private lateinit var favorites: MutableList<FavoritesItem>
|
private lateinit var favorites: MutableList<FavoritesItem>
|
||||||
|
|
||||||
suspend fun initView() {
|
suspend fun initView() {
|
||||||
val viewModel = ViewModelProvider(context as AppCompatActivity)[FavoritesViewModel::class.java]
|
|
||||||
favorites = withContext(Dispatchers.IO) {
|
favorites = withContext(Dispatchers.IO) {
|
||||||
viewModel.getAllFavoriteItems().toMutableList()
|
viewModel.getAllFavoriteItems().toMutableList()
|
||||||
}
|
}
|
||||||
@ -117,7 +120,6 @@ class EditFavoritesView @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun save() {
|
fun save() {
|
||||||
val viewModel = ViewModelProvider(context as AppCompatActivity)[FavoritesViewModel::class.java]
|
|
||||||
viewModel.saveFavorites(favorites)
|
viewModel.saveFavorites(favorites)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import de.mm20.launcher2.favorites.FavoritesViewModel
|
|||||||
import de.mm20.launcher2.search.data.Searchable
|
import de.mm20.launcher2.search.data.Searchable
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import kotlinx.android.synthetic.main.view_favorites.view.*
|
import kotlinx.android.synthetic.main.view_favorites.view.*
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
class FavoritesView : FrameLayout {
|
class FavoritesView : FrameLayout {
|
||||||
|
|
||||||
@ -23,7 +24,7 @@ class FavoritesView : FrameLayout {
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
View.inflate(context, R.layout.view_favorites, this)
|
View.inflate(context, R.layout.view_favorites, this)
|
||||||
val viewModel = ViewModelProvider(context as AppCompatActivity)[FavoritesViewModel::class.java]
|
val viewModel: FavoritesViewModel by (context as AppCompatActivity).viewModel()
|
||||||
favorites = viewModel.getFavorites(context.resources.getInteger(R.integer.config_columnCount))
|
favorites = viewModel.getFavorites(context.resources.getInteger(R.integer.config_columnCount))
|
||||||
favorites.observe(context as AppCompatActivity, Observer {
|
favorites.observe(context as AppCompatActivity, Observer {
|
||||||
visibility = if (it?.isEmpty() == true) View.GONE else View.VISIBLE
|
visibility = if (it?.isEmpty() == true) View.GONE else View.VISIBLE
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import de.mm20.launcher2.search.data.File
|
|||||||
import de.mm20.launcher2.search.data.MissingPermission
|
import de.mm20.launcher2.search.data.MissingPermission
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.legacy.search.SearchListView
|
import de.mm20.launcher2.ui.legacy.search.SearchListView
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
class FileView : FrameLayout {
|
class FileView : FrameLayout {
|
||||||
private val files: LiveData<List<File>?>
|
private val files: LiveData<List<File>?>
|
||||||
@ -30,7 +31,8 @@ class FileView : FrameLayout {
|
|||||||
val card = findViewById<ViewGroup>(R.id.card)
|
val card = findViewById<ViewGroup>(R.id.card)
|
||||||
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||||
val list = findViewById<SearchListView>(R.id.list)
|
val list = findViewById<SearchListView>(R.id.list)
|
||||||
files = ViewModelProvider(context as AppCompatActivity).get(FilesViewModel::class.java).files
|
val viewModel: FilesViewModel by (context as AppCompatActivity).viewModel()
|
||||||
|
files = viewModel.files
|
||||||
files.observe(context as AppCompatActivity, {
|
files.observe(context as AppCompatActivity, {
|
||||||
if (it == null) {
|
if (it == null) {
|
||||||
visibility = View.GONE
|
visibility = View.GONE
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import de.mm20.launcher2.transition.ChangingLayoutTransition
|
|||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.legacy.view.LauncherCardView
|
import de.mm20.launcher2.ui.legacy.view.LauncherCardView
|
||||||
import kotlinx.android.synthetic.main.view_search_bar.view.*
|
import kotlinx.android.synthetic.main.view_search_bar.view.*
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
class SearchBar @JvmOverloads constructor(
|
class SearchBar @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
@ -70,7 +71,9 @@ class SearchBar @JvmOverloads constructor(
|
|||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
ViewModelProvider(context as AppCompatActivity)[SearchViewModel::class.java].isSearching.observe(context, Observer {
|
val viewModel = (context as AppCompatActivity).viewModel<SearchViewModel>().value
|
||||||
|
|
||||||
|
viewModel.isSearching.observe(context, Observer {
|
||||||
searchProgressBar.visibility = if (it) View.VISIBLE else View.GONE
|
searchProgressBar.visibility = if (it) View.VISIBLE else View.GONE
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import androidx.browser.customtabs.CustomTabsIntent
|
|||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
@ -31,6 +32,7 @@ import de.mm20.launcher2.ui.R
|
|||||||
import de.mm20.launcher2.unitconverter.UnitConverterViewModel
|
import de.mm20.launcher2.unitconverter.UnitConverterViewModel
|
||||||
import de.mm20.launcher2.unitconverter.UnitValue
|
import de.mm20.launcher2.unitconverter.UnitValue
|
||||||
import kotlinx.android.synthetic.main.view_unitconverter.view.*
|
import kotlinx.android.synthetic.main.view_unitconverter.view.*
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
@ -46,7 +48,8 @@ class UnitConverterView : FrameLayout {
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
View.inflate(context, R.layout.view_unitconverter, this)
|
View.inflate(context, R.layout.view_unitconverter, this)
|
||||||
unitConverter = ViewModelProvider(context as AppCompatActivity).get(UnitConverterViewModel::class.java).unitConverter
|
val unitConverterViewModel by (context as AppCompatActivity).viewModel<UnitConverterViewModel>()
|
||||||
|
unitConverter = unitConverterViewModel.unitConverter
|
||||||
unitConverter.observe(context as AppCompatActivity, Observer {
|
unitConverter.observe(context as AppCompatActivity, Observer {
|
||||||
if (it == null) visibility = View.GONE
|
if (it == null) visibility = View.GONE
|
||||||
else {
|
else {
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import de.mm20.launcher2.search.WebsearchViewModel
|
|||||||
import de.mm20.launcher2.search.data.Websearch
|
import de.mm20.launcher2.search.data.Websearch
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import kotlinx.android.synthetic.main.view_websearch.view.*
|
import kotlinx.android.synthetic.main.view_websearch.view.*
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
class WebSearchView : FrameLayout {
|
class WebSearchView : FrameLayout {
|
||||||
constructor(context: Context) : super(context)
|
constructor(context: Context) : super(context)
|
||||||
@ -31,7 +32,7 @@ class WebSearchView : FrameLayout {
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
View.inflate(context, R.layout.view_websearch, this)
|
View.inflate(context, R.layout.view_websearch, this)
|
||||||
val viewModel = ViewModelProvider(context as AppCompatActivity)[WebsearchViewModel::class.java]
|
val viewModel: WebsearchViewModel by (context as AppCompatActivity).viewModel()
|
||||||
websearches = viewModel.websearches
|
websearches = viewModel.websearches
|
||||||
websearches.observe(context as AppCompatActivity, Observer {
|
websearches.observe(context as AppCompatActivity, Observer {
|
||||||
updateWebsearches(it)
|
updateWebsearches(it)
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import de.mm20.launcher2.search.data.Website
|
|||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
|
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
|
||||||
import de.mm20.launcher2.websites.WebsiteViewModel
|
import de.mm20.launcher2.websites.WebsiteViewModel
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
|
|
||||||
class WebsiteView : FrameLayout {
|
class WebsiteView : FrameLayout {
|
||||||
|
|
||||||
@ -30,7 +31,8 @@ class WebsiteView : FrameLayout {
|
|||||||
val card = findViewById<ViewGroup>(R.id.card)
|
val card = findViewById<ViewGroup>(R.id.card)
|
||||||
websiteView.layoutParams = params
|
websiteView.layoutParams = params
|
||||||
card.addView(websiteView)
|
card.addView(websiteView)
|
||||||
website = ViewModelProvider(context as AppCompatActivity)[WebsiteViewModel::class.java].website
|
val viewModel: WebsiteViewModel by (context as AppCompatActivity).viewModel()
|
||||||
|
website = viewModel.website
|
||||||
website.observe(context as AppCompatActivity, Observer {
|
website.observe(context as AppCompatActivity, Observer {
|
||||||
visibility = if (it == null) View.GONE else View.VISIBLE
|
visibility = if (it == null) View.GONE else View.VISIBLE
|
||||||
card.setOnClickListener { _ ->
|
card.setOnClickListener { _ ->
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user