Reorganize Searchable data types

This commit is contained in:
MM20 2022-10-12 20:36:39 +02:00
parent a01b0aa03d
commit bcda89c211
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
98 changed files with 817 additions and 738 deletions

View File

@ -45,7 +45,6 @@ dependencies {
implementation(libs.commons.text)
implementation(project(":search"))
implementation(project(":base"))
implementation(project(":preferences"))
implementation(project(":ktx"))

View File

@ -12,17 +12,18 @@ import android.os.Process
import android.os.UserHandle
import android.util.Log
import de.mm20.launcher2.ktx.normalize
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.SearchableRepository
import de.mm20.launcher2.search.data.LauncherApp
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext
import org.apache.commons.text.similarity.FuzzyScore
import java.util.*
interface AppRepository {
fun search(query: String): Flow<List<Application>>
fun getAllInstalledApps(): Flow<List<Application>>
interface AppRepository: SearchableRepository<LauncherApp> {
fun getAllInstalledApps(): Flow<List<LauncherApp>>
fun getSuspendedPackages(): Flow<List<String>>
}
@ -140,11 +141,11 @@ internal class AppRepositoryImpl(
return LauncherApp(context, launcherActivityInfo)
}
override fun search(query: String): Flow<List<Application>> = channelFlow {
override fun search(query: String): Flow<ImmutableList<LauncherApp>> = channelFlow {
installedApps.collectLatest { apps ->
withContext(Dispatchers.Default) {
val appResults = mutableListOf<Application>()
val appResults = mutableListOf<LauncherApp>()
if (query.isEmpty()) {
appResults.addAll(apps)
} else {
@ -158,12 +159,12 @@ internal class AppRepositoryImpl(
appResults.sort()
send(appResults)
send(appResults.toImmutableList())
}
}
}
override fun getAllInstalledApps(): Flow<List<Application>> {
override fun getAllInstalledApps(): Flow<List<LauncherApp>> {
return installedApps
}
@ -174,7 +175,7 @@ internal class AppRepositoryImpl(
fuzzyScore.fuzzyScore(normalizedLabel, query.normalize()) >= query.length * 1.5
}
private fun getActivityByComponentName(componentName: ComponentName?): Application? {
private fun getActivityByComponentName(componentName: ComponentName?): LauncherApp? {
componentName ?: return null
val intent = Intent().setComponent(componentName)
val lai = launcherApps.resolveActivity(intent, Process.myUserHandle())

View File

@ -1,73 +0,0 @@
package de.mm20.launcher2.search.data
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.graphics.drawable.BitmapDrawable
import androidx.core.content.ContextCompat
import de.mm20.launcher2.applications.R
import de.mm20.launcher2.icons.*
class AppInstallation(
val session: PackageInstaller.SessionInfo
) : Application(
label = session.appLabel?.toString() ?: "",
`package` = session.appPackageName ?: "",
activity = "",
flags = 0,
version = null
) {
override val key: String
get() = "installer://${session.installerPackageName}:${session.appPackageName}"
override fun getLaunchIntent(context: Context): Intent? {
return session.createDetailsIntent()
}
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
return StaticLauncherIcon(
foregroundLayer = TintedIconLayer(
icon = ContextCompat.getDrawable(context, R.drawable.ic_file_android)!!,
scale = 0.5f,
color = 0xFF757575.toInt()
),
backgroundLayer = ColorLayer(0xFF757575.toInt())
)
}
override suspend fun loadIcon(
context: Context,
size: Int,
themed: Boolean,
): LauncherIcon {
val icon = session.appIcon ?: return getPlaceholderIcon(context)
val foreground = BitmapDrawable(context.resources, icon)
foreground.colorFilter = ColorMatrixColorFilter(ColorMatrix().apply {
setSaturation(0f)
})
return StaticLauncherIcon(
foregroundLayer = StaticIconLayer(
icon = foreground,
),
backgroundLayer = ColorLayer(0xFF757575.toInt())
)
}
override fun getStoreDetails(context: Context): StoreLink? {
return getStoreLinkForInstaller(session.installerPackageName, `package`)
}
companion object {
fun search(context: Context): List<AppInstallation> {
val installer = context.packageManager.packageInstaller
val sessions = installer.allSessions
val results = sessions.mapNotNull {
if (it.appLabel != null && it.isActive) AppInstallation(it) else null
}
return results
}
}
}

View File

@ -4,20 +4,17 @@ 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.ktx.jsonObjectOf
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer
import org.json.JSONObject
import org.koin.core.component.KoinComponent
class LauncherAppSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String {
override fun serialize(searchable: PinnableSearchable): String {
searchable as LauncherApp
val json = JSONObject()
json.put("package", searchable.`package`)
@ -31,7 +28,7 @@ class LauncherAppSerializer : SearchableSerializer {
}
class LauncherAppDeserializer(val context: Context) : SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? {
override fun deserialize(serialized: String): PinnableSearchable? {
val json = JSONObject(serialized)
val launcherApps = context.getSystemService<LauncherApps>()!!
val userManager = context.getSystemService<UserManager>()!!

View File

@ -1,96 +0,0 @@
package de.mm20.launcher2.search.data
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import androidx.core.content.ContextCompat
import de.mm20.launcher2.applications.R
import de.mm20.launcher2.compat.PackageManagerCompat
import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TintedIconLayer
import org.json.JSONObject
abstract class Application(
override val label: String,
val `package`: String,
val activity: String,
val flags: Int,
val version: String?,
) : Searchable() {
override fun serialize(): String {
val json = JSONObject()
json.put("package", `package`)
json.put("activity", activity)
return json.toString()
}
override fun getLaunchIntent(context: Context): Intent? {
val intent = Intent()
intent.component = ComponentName(`package`, activity)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
return intent
}
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
return StaticLauncherIcon(
foregroundLayer = TintedIconLayer(
icon = ContextCompat.getDrawable(context, R.drawable.ic_file_android)!!,
scale = 0.5f,
color = 0xff3dda84.toInt(),
),
backgroundLayer = ColorLayer(0xff3dda84.toInt())
)
}
open fun getStoreDetails(context: Context): StoreLink? {
val pm = context.packageManager
return try {
val installSourceInfo = PackageManagerCompat.getInstallSource(pm, `package`)
getStoreLinkForInstaller(installSourceInfo.initiatingPackageName, `package`)
} catch (e: PackageManager.NameNotFoundException) {
null
}
}
override val key: String
get() = "app://$`package`:$activity"
companion object {
internal fun getStoreLinkForInstaller(
installerPackage: String?,
packageName: String?
): StoreLink? {
if (packageName == null) return null
return when (installerPackage) {
"de.amazon.mShop.android", "com.amazon.venezia" -> {
StoreLink(
"Amazon App Shop",
"http://www.amazon.com/gp/mas/dl/android?p=${packageName}"
)
}
"com.android.vending" -> {
StoreLink(
"Google Play Store",
"https://play.google.com/store/apps/details?id=${packageName}"
)
}
"org.fdroid.fdroid", "com.aurora.adroid" -> {
StoreLink(
"F-Droid",
"https://f-droid.org/packages/${packageName}"
)
}
else -> null
}
}
}
}
data class StoreLink(
val label: String,
val url: String
)

View File

@ -10,37 +10,65 @@ import android.graphics.drawable.AdaptiveIconDrawable
import android.os.Bundle
import android.os.Process
import android.os.UserHandle
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService
import de.mm20.launcher2.applications.R
import de.mm20.launcher2.compat.PackageManagerCompat
import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.getSerialNumber
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.search.PinnableSearchable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* An [Application] based on an [android.content.pm.LauncherActivityInfo]
*/
class LauncherApp(
context: Context,
val launcherActivityInfo: LauncherActivityInfo
) : Application(
label = launcherActivityInfo.label.toString(),
`package` = launcherActivityInfo.applicationInfo.packageName,
activity = launcherActivityInfo.name,
flags = launcherActivityInfo.applicationInfo.flags,
version = getPackageVersionName(context, launcherActivityInfo.applicationInfo.packageName),
) {
data class LauncherApp(
val launcherActivityInfo: LauncherActivityInfo,
override val label: String,
val `package`: String,
val activity: String,
val flags: Int,
val version: String?,
internal val userSerialNumber: Long,
override val labelOverride: String? = null,
) : PinnableSearchable {
constructor(context: Context, launcherActivityInfo: LauncherActivityInfo): this(
launcherActivityInfo,
label = launcherActivityInfo.label.toString(),
`package` = launcherActivityInfo.applicationInfo.packageName,
activity = launcherActivityInfo.name,
flags = launcherActivityInfo.applicationInfo.flags,
version = getPackageVersionName(context, launcherActivityInfo.applicationInfo.packageName),
userSerialNumber = launcherActivityInfo.user.getSerialNumber(context)
)
internal val userSerialNumber: Long = launcherActivityInfo.user.getSerialNumber(context)
val isMainProfile = launcherActivityInfo.user == Process.myUserHandle()
override val domain: String = Domain
override val preferDetailsOverLaunch: Boolean = false
override fun overrideLabel(label: String): LauncherApp {
return this.copy(labelOverride = label)
}
override val key: String
get() = if (isMainProfile) "app://$`package`:$activity" else "app://$`package`:$activity:${userSerialNumber}"
get() = if (isMainProfile) "${domain}://$`package`:$activity" else "${domain}://$`package`:$activity:${userSerialNumber}"
fun getUser(): UserHandle? {
return launcherActivityInfo.user
}
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
return StaticLauncherIcon(
foregroundLayer = TintedIconLayer(
icon = ContextCompat.getDrawable(context, R.drawable.ic_file_android)!!,
scale = 0.5f,
color = 0xff3dda84.toInt(),
),
backgroundLayer = ColorLayer(0xff3dda84.toInt())
)
}
override suspend fun loadIcon(
context: Context,
size: Int,
@ -107,7 +135,46 @@ class LauncherApp(
return true
}
fun getStoreDetails(context: Context): StoreLink? {
val pm = context.packageManager
return try {
val installSourceInfo = PackageManagerCompat.getInstallSource(pm, `package`)
getStoreLinkForInstaller(installSourceInfo.initiatingPackageName, `package`)
} catch (e: PackageManager.NameNotFoundException) {
null
}
}
companion object {
private fun getStoreLinkForInstaller(
installerPackage: String?,
packageName: String?
): StoreLink? {
if (packageName == null) return null
return when (installerPackage) {
"de.amazon.mShop.android", "com.amazon.venezia" -> {
StoreLink(
"Amazon App Shop",
"http://www.amazon.com/gp/mas/dl/android?p=${packageName}"
)
}
"com.android.vending" -> {
StoreLink(
"Google Play Store",
"https://play.google.com/store/apps/details?id=${packageName}"
)
}
"org.fdroid.fdroid", "com.aurora.adroid" -> {
StoreLink(
"F-Droid",
"https://f-droid.org/packages/${packageName}"
)
}
else -> null
}
}
fun getPackageVersionName(context: Context, packageName: String): String? {
return try {
@ -116,5 +183,12 @@ class LauncherApp(
null
}
}
const val Domain = "app"
}
}
}
data class StoreLink(
val label: String,
val url: String
)

View File

@ -44,7 +44,6 @@ dependencies {
implementation(libs.commons.text)
implementation(project(":applications"))
implementation(project(":search"))
implementation(project(":permissions"))
implementation(project(":base"))
implementation(project(":preferences"))

View File

@ -15,9 +15,13 @@ import de.mm20.launcher2.ktx.normalize
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.SearchableRepository
import de.mm20.launcher2.search.data.AppShortcut
import de.mm20.launcher2.search.data.LauncherApp
import de.mm20.launcher2.search.data.LauncherShortcut
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -27,7 +31,7 @@ import kotlinx.coroutines.withContext
import org.apache.commons.text.similarity.FuzzyScore
import java.util.*
interface AppShortcutRepository {
interface AppShortcutRepository: SearchableRepository<AppShortcut> {
suspend fun getShortcutsForActivity(
launcherActivityInfo: LauncherActivityInfo,
count: Int = 5
@ -35,8 +39,6 @@ interface AppShortcutRepository {
suspend fun getShortcutsConfigActivities(): List<LauncherApp>
fun search(query: String): Flow<List<AppShortcut>>
fun removePinnedShortcut(shortcut: LauncherShortcut)
}
@ -72,32 +74,31 @@ internal class AppShortcutRepositoryImpl(
LauncherShortcut(
context,
it,
launcherActivityInfo.label.toString()
)
} ?: emptyList())
appShortcuts
}
override fun search(query: String) = channelFlow<List<AppShortcut>> {
override fun search(query: String) = channelFlow<ImmutableList<AppShortcut>> {
if (query.length < 3) {
send(emptyList())
send(persistentListOf())
return@channelFlow
}
withContext(Dispatchers.IO) {
if (!permissionsManager.checkPermissionOnce(PermissionGroup.AppShortcuts)) {
send(emptyList())
send(persistentListOf())
return@withContext
}
dataStore.data.map { it.appShortcutSearch.enabled }.collectLatest { enabled ->
if (!enabled) {
send(emptyList())
send(persistentListOf())
return@collectLatest
}
shortcutChangeEmitter.collectLatest {
val launcherApps =
context.getSystemService<LauncherApps>() ?: return@collectLatest send(
emptyList()
persistentListOf()
)
val shortcutQuery = LauncherApps.ShortcutQuery()
@ -124,17 +125,11 @@ internal class AppShortcutRepositoryImpl(
send(
shortcuts.mapNotNull {
val label = try {
pm.getApplicationInfo(it.`package`, 0).loadLabel(pm).toString()
} catch (e: PackageManager.NameNotFoundException) {
""
}
LauncherShortcut(
context,
it,
label
it
)
}
}.toImmutableList()
)
}
}

View File

@ -5,22 +5,22 @@ import android.content.Intent
import android.content.Intent.ShortcutIconResource
import android.content.pm.LauncherApps
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Process
import android.os.UserManager
import androidx.core.content.getSystemService
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.search.data.LauncherShortcut
import de.mm20.launcher2.search.data.LegacyShortcut
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.Searchable
import org.json.JSONObject
import org.koin.core.component.KoinComponent
class LauncherShortcutSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String {
override fun serialize(searchable: PinnableSearchable): String {
searchable as LauncherShortcut
return jsonObjectOf(
"packagename" to searchable.launcherShortcut.`package`,
@ -38,7 +38,7 @@ class LauncherShortcutDeserializer(
val context: Context
) : SearchableDeserializer, KoinComponent {
override fun deserialize(serialized: String): Searchable? {
override fun deserialize(serialized: String): PinnableSearchable? {
val launcherApps = context.getSystemService(Context.LAUNCHER_APPS_SERVICE) as LauncherApps
if (!launcherApps.hasShortcutHostPermission()) return null
else {
@ -75,7 +75,6 @@ class LauncherShortcutDeserializer(
return LauncherShortcut(
context = context,
launcherShortcut = shortcuts[0],
appName = appName
)
}
}
@ -83,7 +82,7 @@ class LauncherShortcutDeserializer(
}
class LegacyShortcutSerializer: SearchableSerializer {
override fun serialize(searchable: Searchable): String? {
override fun serialize(searchable: PinnableSearchable): String {
searchable as LegacyShortcut
return jsonObjectOf(
"label" to searchable.label,
@ -104,7 +103,7 @@ class LegacyShortcutSerializer: SearchableSerializer {
class LegacyShortcutDeserializer(
val context: Context
): SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? {
override fun deserialize(serialized: String): PinnableSearchable {
val json = JSONObject(serialized)
val label = json.getString("label")
val intent = Intent.parseUri(json.getString("intent"), 0)

View File

@ -7,10 +7,15 @@ import de.mm20.launcher2.appshortcuts.R
import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TintedIconLayer
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
interface AppShortcut: PinnableSearchable {
abstract class AppShortcut(
val appName: String?
) : Searchable() {
override val preferDetailsOverLaunch: Boolean
get() = false
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
return StaticLauncherIcon(

View File

@ -4,6 +4,7 @@ import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.LauncherApps
import android.content.pm.PackageManager
import android.content.pm.ShortcutInfo
import android.graphics.drawable.AdaptiveIconDrawable
import android.os.Bundle
@ -14,36 +15,55 @@ import de.mm20.launcher2.appshortcuts.R
import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.getSerialNumber
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.search.PinnableSearchable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* Represents a modern (Android O+) launcher shortcut
*/
class LauncherShortcut(
context: Context,
data class LauncherShortcut(
val launcherShortcut: ShortcutInfo,
appName: String
) : AppShortcut(appName) {
override val appName: String?,
internal val userSerialNumber: Long,
override val labelOverride: String? = null,
) : AppShortcut {
override val domain: String = Domain
constructor(
context: Context,
launcherShortcut: ShortcutInfo,
): this(
launcherShortcut = launcherShortcut,
appName = try {
context.packageManager.getApplicationInfo(launcherShortcut.`package`, 0)
.loadLabel(context.packageManager).toString()
} catch (e: PackageManager.NameNotFoundException) {
null
},
userSerialNumber = launcherShortcut.userHandle.getSerialNumber(context)
)
override val label: String
get() = launcherShortcut.shortLabel?.toString() ?: ""
override fun overrideLabel(label: String): LauncherShortcut {
return this.copy(labelOverride = label)
}
override val preferDetailsOverLaunch: Boolean = false
internal val userSerialNumber: Long = launcherShortcut.userHandle.getSerialNumber(context)
val isMainProfile = launcherShortcut.userHandle == Process.myUserHandle()
override val key: String
get() = if (isMainProfile) {
"shortcut://${launcherShortcut.`package`}/${launcherShortcut.id}"
"$domain://${launcherShortcut.`package`}/${launcherShortcut.id}"
} else {
"shortcut://${launcherShortcut.`package`}/${launcherShortcut.id}:${userSerialNumber}"
"$domain://${launcherShortcut.`package`}/${launcherShortcut.id}:${userSerialNumber}"
}
override fun getLaunchIntent(context: Context): Intent? {
return launcherShortcut.intent
}
override fun launch(context: Context, options: Bundle?): Boolean {
val launcherApps = context.getSystemService<LauncherApps>()!!
try {
@ -123,10 +143,10 @@ class LauncherShortcut(
return LauncherShortcut(
context,
shortcutInfo,
context.packageManager.getApplicationInfo(shortcutInfo.`package`, 0)
.loadLabel(context.packageManager).toString()
)
}
const val Domain = "shortcut"
}
}

View File

@ -4,22 +4,32 @@ import android.content.Context
import android.content.Intent
import android.content.Intent.ShortcutIconResource
import android.graphics.drawable.AdaptiveIconDrawable
import android.os.Bundle
import android.util.Log
import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.getDrawableOrNull
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.PinnableSearchable
class LegacyShortcut(
data class LegacyShortcut(
val intent: Intent,
override val label: String,
appName: String?,
override val appName: String?,
val iconResource: ShortcutIconResource?,
) : AppShortcut(appName) {
override val key: String
get() = "legacyshortcut://${intent.toUri(0)}"
override val labelOverride: String? = null,
) : AppShortcut {
override fun getLaunchIntent(context: Context): Intent {
return intent
override val domain = Domain
override val key: String = "$domain://${intent.toUri(0)}"
override fun overrideLabel(label: String): LegacyShortcut {
return this.copy(labelOverride = label)
}
override fun launch(context: Context, options: Bundle?): Boolean {
return context.tryStartActivity(intent, options)
}
val packageName: String?
@ -67,6 +77,9 @@ class LegacyShortcut(
}
companion object {
const val Domain = "legacyshortcut"
fun fromPinRequestIntent(context: Context, data: Intent): LegacyShortcut? {
val intent: Intent? = data.extras?.getParcelable(Intent.EXTRA_SHORTCUT_INTENT)
val name: String? = data.extras?.getString(Intent.EXTRA_SHORTCUT_NAME)

View File

@ -41,8 +41,8 @@ dependencies {
implementation(libs.koin.android)
implementation(project(":favorites"))
implementation(project(":search"))
implementation(project(":widgets"))
implementation(project(":search"))
implementation(project(":preferences"))
implementation(project(":ktx"))
implementation(project(":customattrs"))

View File

@ -14,8 +14,6 @@ import kotlinx.coroutines.*
import java.io.File
import java.io.InputStream
import java.io.OutputStream
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream

View File

@ -49,6 +49,5 @@ dependencies {
implementation(project(":notifications"))
implementation(project(":preferences"))
implementation(project(":base"))
implementation(project(":search"))
implementation(project(":files"))
}

View File

@ -3,7 +3,7 @@ package de.mm20.launcher2.badges
import android.content.Context
import de.mm20.launcher2.badges.providers.*
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.Searchable
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import org.koin.core.component.KoinComponent

View File

@ -6,7 +6,7 @@ import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.graphics.BadgeDrawable
import de.mm20.launcher2.search.data.LauncherShortcut
import de.mm20.launcher2.search.data.LegacyShortcut
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.Searchable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow

View File

@ -1,7 +1,7 @@
package de.mm20.launcher2.badges.providers
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.Searchable
import kotlinx.coroutines.flow.Flow
interface BadgeProvider {

View File

@ -2,7 +2,7 @@ package de.mm20.launcher2.badges.providers
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.search.data.File
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.Searchable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow

View File

@ -3,9 +3,8 @@ package de.mm20.launcher2.badges.providers
import android.app.Notification
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.notifications.NotificationRepository
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.data.LauncherApp
import de.mm20.launcher2.search.data.Searchable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
@ -17,7 +16,7 @@ class NotificationBadgeProvider : BadgeProvider, KoinComponent {
private val notificationRepository: NotificationRepository by inject()
override fun getBadge(searchable: Searchable): Flow<Badge?> = channelFlow {
if (searchable is Application) {
if (searchable is LauncherApp) {
val packageName = searchable.`package`
notificationRepository.notifications.map {
it.filter { it.packageName == packageName }

View File

@ -3,8 +3,8 @@ package de.mm20.launcher2.badges.providers
import de.mm20.launcher2.applications.AppRepository
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.R
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.data.LauncherApp
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
@ -15,7 +15,7 @@ class SuspendedAppsBadgeProvider : BadgeProvider, KoinComponent {
private val appRepository: AppRepository by inject()
override fun getBadge(searchable: Searchable): Flow<Badge?> = channelFlow {
if (searchable is Application) {
if (searchable is LauncherApp) {
val packageName = searchable.`package`
appRepository.getSuspendedPackages().collectLatest {
if (it.contains(packageName)) {

View File

@ -2,10 +2,9 @@ package de.mm20.launcher2.badges.providers
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.R
import de.mm20.launcher2.search.data.AppShortcut
import de.mm20.launcher2.search.data.LauncherApp
import de.mm20.launcher2.search.data.LauncherShortcut
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.Searchable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow

View File

@ -0,0 +1,41 @@
package de.mm20.launcher2.search
import android.content.Context
import android.os.Bundle
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.ktx.romanize
import java.text.Collator
interface PinnableSearchable : Searchable, Comparable<PinnableSearchable> {
val label: String
val labelOverride: String?
get() = null
fun overrideLabel(label: String): PinnableSearchable
fun launch(context: Context, options: Bundle?): Boolean
/**
* If this is true, tapping the item will open the details popup instead of launching it
*/
val preferDetailsOverLaunch: Boolean
fun getPlaceholderIcon(context: Context): StaticLauncherIcon
suspend fun loadIcon(
context: Context,
size: Int,
themed: Boolean
): LauncherIcon? = null
override fun compareTo(other: PinnableSearchable): Int {
val label1 = labelOverride ?: label
val label2 = other.labelOverride ?: other.label
return Collator.getInstance().apply { strength = Collator.SECONDARY }
.compare(label1.romanize(), label2.romanize())
}
}

View File

@ -0,0 +1,6 @@
package de.mm20.launcher2.search
interface Searchable {
val domain: String
val key: String
}

View File

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

View File

@ -0,0 +1,8 @@
package de.mm20.launcher2.search
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.Flow
interface SearchableRepository<T: Searchable> {
fun search(query: String): Flow<ImmutableList<T>>
}

View File

@ -1,14 +1,12 @@
package de.mm20.launcher2.search
import de.mm20.launcher2.search.data.Searchable
interface SearchableSerializer {
fun serialize(searchable: Searchable): String?
fun serialize(searchable: PinnableSearchable): String?
val typePrefix: String
}
class NullSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String? {
override fun serialize(searchable: PinnableSearchable): String? {
return null
}

View File

@ -46,6 +46,6 @@ dependencies {
implementation(libs.koin.android)
implementation(project(":preferences"))
implementation(project(":search"))
implementation(project(":base"))
}

View File

@ -1,14 +1,21 @@
package de.mm20.launcher2.search.data
import de.mm20.launcher2.search.Searchable
import java.text.DecimalFormat
import java.util.*
import kotlin.math.abs
import kotlin.math.roundToInt
class Calculator(
data class Calculator(
val term: String,
val solution: Double
) {
): Searchable {
override val domain: String
get() = "calculator"
override val key: String
get() = "calculator://$term"
val formattedString: String
val formattedBinaryString: String

View File

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

View File

@ -7,8 +7,12 @@ import androidx.core.database.getStringOrNull
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.SearchableRepository
import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.search.data.UserCalendar
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext
@ -16,9 +20,7 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.util.*
interface CalendarRepository {
fun search(query: String): Flow<List<CalendarEvent>>
interface CalendarRepository: SearchableRepository<CalendarEvent> {
fun getUpcomingEvents(): Flow<List<CalendarEvent>>
suspend fun getCalendars(): List<UserCalendar>
@ -31,10 +33,10 @@ internal class CalendarRepositoryImpl(
private val dataStore: LauncherDataStore by inject()
private val permissionsManager: PermissionsManager by inject()
override fun search(query: String): Flow<List<CalendarEvent>> {
override fun search(query: String): Flow<ImmutableList<CalendarEvent>> {
if (query.isBlank() || query.length < 3) {
return flow {
emit(emptyList())
emit(persistentListOf())
}
}
@ -49,9 +51,9 @@ internal class CalendarRepositoryImpl(
query,
intervalStart = now,
intervalEnd = now + 14 * 24 * 60 * 60 * 1000L,
)
).toImmutableList()
} else {
emptyList()
persistentListOf()
}
}

View File

@ -7,15 +7,16 @@ import android.content.pm.PackageManager
import android.provider.CalendarContract
import androidx.core.content.ContextCompat
import androidx.core.database.getStringOrNull
import de.mm20.launcher2.search.PinnableSearchable
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 de.mm20.launcher2.search.Searchable
import org.json.JSONObject
import java.util.*
class CalendarEventSerializer: SearchableSerializer {
override fun serialize(searchable: Searchable): String {
override fun serialize(searchable: PinnableSearchable): String {
searchable as CalendarEvent
val json = JSONObject()
json.put("id", searchable.id)
@ -27,7 +28,7 @@ class CalendarEventSerializer: SearchableSerializer {
}
class CalendarEventDeserializer(val context: Context): SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? {
override fun deserialize(serialized: String): PinnableSearchable? {
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CALENDAR) != PackageManager.PERMISSION_GRANTED) return null
val json = JSONObject(serialized)
val id = json.getLong("id")

View File

@ -3,13 +3,17 @@ package de.mm20.launcher2.search.data
import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.provider.CalendarContract
import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TextLayer
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
import java.text.SimpleDateFormat
class CalendarEvent(
data class CalendarEvent(
override val label: String,
val id: Long,
val color: Int,
@ -19,12 +23,20 @@ class CalendarEvent(
val location: String,
val attendees: List<String>,
val description: String,
val calendar: Long
) : Searchable() {
val calendar: Long,
override val labelOverride: String? = null,
) : PinnableSearchable {
override val domain: String = Domain
override val key: String
get() = "calendar://$id"
get() = "$domain://$id"
override val preferDetailsOverLaunch: Boolean = true
override fun overrideLabel(label: String): CalendarEvent {
return this.copy(labelOverride = label)
}
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
val df = SimpleDateFormat("dd")
@ -37,10 +49,18 @@ class CalendarEvent(
)
}
override fun getLaunchIntent(context: Context): Intent {
private fun getLaunchIntent(): Intent {
val uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id)
return Intent(Intent.ACTION_VIEW).setData(uri).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
override fun launch(context: Context, options: Bundle?): Boolean {
return context.tryStartActivity(getLaunchIntent(), options)
}
companion object {
const val Domain = "calendar"
}
}
data class UserCalendar(

View File

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

View File

@ -5,16 +5,18 @@ import android.provider.ContactsContract
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.SearchableRepository
import de.mm20.launcher2.search.data.Contact
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
interface ContactRepository {
fun search(query: String): Flow<List<Contact>>
}
interface ContactRepository: SearchableRepository<Contact>
internal class ContactRepositoryImpl(
private val context: Context,
@ -23,13 +25,13 @@ internal class ContactRepositoryImpl(
private val permissionsManager: PermissionsManager by inject()
private val dataStore: LauncherDataStore by inject()
override fun search(query: String): Flow<List<Contact>> {
override fun search(query: String): Flow<ImmutableList<Contact>> {
val searchContacts = dataStore.data.map { it.contactsSearch.enabled }
val hasPermission = permissionsManager.hasPermission(PermissionGroup.Contacts)
if (query.length < 3) {
return flow {
emit(emptyList())
emit(persistentListOf())
}
}
@ -39,12 +41,12 @@ internal class ContactRepositoryImpl(
if (it) {
queryContacts(query)
} else {
emptyList()
persistentListOf()
}
}
}
private suspend fun queryContacts(query: String): List<Contact> {
private suspend fun queryContacts(query: String): ImmutableList<Contact> {
val results = withContext(Dispatchers.IO) {
val proj = arrayOf(
ContactsContract.RawContacts.CONTACT_ID,
@ -67,6 +69,6 @@ internal class ContactRepositoryImpl(
}
results
}
return results
return results.toImmutableList()
}
}

View File

@ -6,14 +6,15 @@ 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.PinnableSearchable
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 de.mm20.launcher2.search.Searchable
import org.json.JSONObject
class ContactSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String {
override fun serialize(searchable: PinnableSearchable): String {
searchable as Contact
return jsonObjectOf(
"id" to searchable.id
@ -25,7 +26,7 @@ class ContactSerializer : SearchableSerializer {
}
class ContactDeserializer(val context: Context) : SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? {
override fun deserialize(serialized: String): PinnableSearchable? {
if (ContextCompat.checkSelfPermission(
context,
Manifest.permission.READ_CONTACTS

View File

@ -3,20 +3,21 @@ package de.mm20.launcher2.search.data
import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.provider.ContactsContract
import androidx.core.content.ContextCompat
import androidx.core.database.getStringOrNull
import androidx.core.graphics.drawable.toDrawable
import de.mm20.launcher2.contacts.R
import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.asBitmap
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.net.URLEncoder
class Contact(
data class Contact(
val id: Long,
val firstName: String,
val lastName: String,
@ -27,12 +28,21 @@ class Contact(
val telegram: Set<ContactInfo>,
val whatsapp: Set<ContactInfo>,
val postals: Set<ContactInfo>,
) : Searchable() {
override val labelOverride: String? = null
) : Searchable, PinnableSearchable {
override val domain: String = Domain
override val key: String
get() = "contact://$id"
get() = "${Domain}://$id"
override val label: String
get() = "$firstName $lastName"
override fun overrideLabel(label: String): Contact {
return this.copy(labelOverride = label)
}
override val preferDetailsOverLaunch: Boolean = true
val summary: String
get() {
return phones.union(emails).joinToString(separator = ", ") { it.label }
@ -69,7 +79,12 @@ class Contact(
)
}
override fun getLaunchIntent(context: Context): Intent {
override fun launch(context: Context, options: Bundle?): Boolean {
val intent = getLaunchIntent()
return context.tryStartActivity(intent, options)
}
private fun getLaunchIntent(): Intent {
val uri =
ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, id)
return Intent(Intent.ACTION_VIEW).setData(uri).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
@ -202,7 +217,10 @@ class Contact(
)
}
const val Domain = "contact"
}
}
data class ContactInfo(

View File

@ -41,7 +41,7 @@ dependencies {
implementation(libs.koin.android)
implementation(project(":database"))
implementation(project(":search"))
implementation(project(":base"))
implementation(project(":ktx"))
implementation(project(":crashreporter"))
implementation(project(":favorites"))

View File

@ -5,7 +5,7 @@ import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.database.entities.CustomAttributeEntity
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.PinnableSearchable
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
@ -15,24 +15,24 @@ import org.json.JSONException
import java.io.File
interface CustomAttributesRepository {
fun getCustomIcon(searchable: Searchable): Flow<CustomIcon?>
fun setCustomIcon(searchable: Searchable, icon: CustomIcon?)
fun getCustomIcon(searchable: PinnableSearchable): Flow<CustomIcon?>
fun setCustomIcon(searchable: PinnableSearchable, icon: CustomIcon?)
fun getCustomLabels(items: List<Searchable>): Flow<List<CustomLabel>>
fun setCustomLabel(searchable: Searchable, label: String)
fun clearCustomLabel(searchable: Searchable)
fun getCustomLabels(items: List<PinnableSearchable>): Flow<List<CustomLabel>>
fun setCustomLabel(searchable: PinnableSearchable, label: String)
fun clearCustomLabel(searchable: PinnableSearchable)
fun setTags(searchable: Searchable, tags: List<String>)
fun getTags(searchable: Searchable): Flow<List<String>>
fun setTags(searchable: PinnableSearchable, tags: List<String>)
fun getTags(searchable: PinnableSearchable): Flow<List<String>>
suspend fun search(query: String): Flow<List<Searchable>>
suspend fun search(query: String): Flow<List<PinnableSearchable>>
suspend fun export(toDir: File)
suspend fun import(fromDir: File)
suspend fun getAllTags(startsWith: String? = null): List<String>
fun getItemsForTag(tag: String): Flow<List<Searchable>>
fun addTag(item: Searchable, tag: String)
fun getItemsForTag(tag: String): Flow<List<PinnableSearchable>>
fun addTag(item: PinnableSearchable, tag: String)
suspend fun cleanupDatabase(): Int
}
@ -42,7 +42,7 @@ internal class CustomAttributesRepositoryImpl(
) : CustomAttributesRepository {
private val scope = CoroutineScope(Job() + Dispatchers.Default)
override fun getCustomIcon(searchable: Searchable): Flow<CustomIcon?> {
override fun getCustomIcon(searchable: PinnableSearchable): Flow<CustomIcon?> {
val dao = appDatabase.customAttrsDao()
return dao.getCustomAttribute(searchable.key, CustomAttributeType.Icon.value)
.map {
@ -50,7 +50,7 @@ internal class CustomAttributesRepositoryImpl(
}
}
override fun setCustomIcon(searchable: Searchable, icon: CustomIcon?) {
override fun setCustomIcon(searchable: PinnableSearchable, icon: CustomIcon?) {
val dao = appDatabase.customAttrsDao()
scope.launch {
dao.clearCustomAttribute(searchable.key, CustomAttributeType.Icon.value)
@ -60,7 +60,7 @@ internal class CustomAttributesRepositoryImpl(
}
}
override fun getCustomLabels(items: List<Searchable>): Flow<List<CustomLabel>> {
override fun getCustomLabels(items: List<PinnableSearchable>): Flow<List<CustomLabel>> {
val dao = appDatabase.customAttrsDao()
return dao.getCustomAttributes(items.map { it.key }, CustomAttributeType.Label.value)
.map { list ->
@ -68,7 +68,7 @@ internal class CustomAttributesRepositoryImpl(
}
}
override fun setCustomLabel(searchable: Searchable, label: String) {
override fun setCustomLabel(searchable: PinnableSearchable, label: String) {
val dao = appDatabase.customAttrsDao()
scope.launch {
favoritesRepository.save(searchable)
@ -84,14 +84,14 @@ internal class CustomAttributesRepositoryImpl(
}
}
override fun clearCustomLabel(searchable: Searchable) {
override fun clearCustomLabel(searchable: PinnableSearchable) {
val dao = appDatabase.customAttrsDao()
scope.launch {
dao.clearCustomAttribute(searchable.key, CustomAttributeType.Label.value)
}
}
override fun setTags(searchable: Searchable, tags: List<String>) {
override fun setTags(searchable: PinnableSearchable, tags: List<String>) {
val dao = appDatabase.customAttrsDao()
scope.launch {
favoritesRepository.save(searchable)
@ -101,7 +101,7 @@ internal class CustomAttributesRepositoryImpl(
}
}
override fun getTags(searchable: Searchable): Flow<List<String>> {
override fun getTags(searchable: PinnableSearchable): Flow<List<String>> {
val dao = appDatabase.customAttrsDao()
return dao.getCustomAttributes(listOf(searchable.key), CustomAttributeType.Tag.value).map {
it.map { it.value }
@ -117,21 +117,21 @@ internal class CustomAttributesRepositoryImpl(
}
}
override fun getItemsForTag(tag: String): Flow<List<Searchable>> {
override fun getItemsForTag(tag: String): Flow<List<PinnableSearchable>> {
val dao = appDatabase.customAttrsDao()
return dao.getItemsWithTag(tag).map {
favoritesRepository.getFromKeys(it)
}
}
override fun addTag(item: Searchable, tag: String) {
override fun addTag(item: PinnableSearchable, tag: String) {
val dao = appDatabase.customAttrsDao()
scope.launch {
dao.addTag(item.key, tag)
}
}
override suspend fun search(query: String): Flow<List<Searchable>> {
override suspend fun search(query: String): Flow<List<PinnableSearchable>> {
if (query.isBlank()) {
return flow {
emit(emptyList())

View File

@ -43,7 +43,6 @@ dependencies {
implementation(libs.koin.android)
implementation(project(":base"))
implementation(project(":search"))
implementation(project(":calendar"))
implementation(project(":database"))
implementation(project(":preferences"))

View File

@ -1,15 +1,16 @@
package de.mm20.launcher2.favorites
import de.mm20.launcher2.database.entities.FavoritesItemEntity
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.Searchable
data class FavoritesItem(
val key: String,
/**
* null if searchable could not be deserialized (i.e. the app has been uninstalled)
*/
val searchable: Searchable?,
val searchable: PinnableSearchable?,
var launchCount: Int,
var pinPosition: Int,
var hidden: Boolean

View File

@ -5,11 +5,10 @@ import android.util.Log
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.database.entities.FavoritesItemEntity
import de.mm20.launcher2.ktx.ceilToInt
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.search.data.Searchable
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import org.json.JSONArray
@ -34,47 +33,47 @@ interface FavoritesRepository {
automaticallySorted: Boolean = false,
frequentlyUsed: Boolean = false,
limit: Int = 100
): Flow<List<Searchable>>
): Flow<List<PinnableSearchable>>
fun getPinnedCalendarEvents(): Flow<List<Searchable>>
fun getPinnedCalendarEvents(): Flow<List<PinnableSearchable>>
fun getHiddenCalendarEventKeys(): Flow<List<String>>
fun isPinned(searchable: Searchable): Flow<Boolean>
fun pinItem(searchable: Searchable)
fun unpinItem(searchable: Searchable)
fun isHidden(searchable: Searchable): Flow<Boolean>
fun hideItem(searchable: Searchable)
fun unhideItem(searchable: Searchable)
fun incrementLaunchCounter(searchable: Searchable)
fun isPinned(searchable: PinnableSearchable): Flow<Boolean>
fun pinItem(searchable: PinnableSearchable)
fun unpinItem(searchable: PinnableSearchable)
fun isHidden(searchable: PinnableSearchable): Flow<Boolean>
fun hideItem(searchable: PinnableSearchable)
fun unhideItem(searchable: PinnableSearchable)
fun incrementLaunchCounter(searchable: PinnableSearchable)
fun updateFavorites(
manuallySorted: List<Searchable>,
automaticallySorted: List<Searchable>,
manuallySorted: List<PinnableSearchable>,
automaticallySorted: List<PinnableSearchable>,
)
fun getHiddenItems(): Flow<List<Searchable>>
fun getHiddenItems(): Flow<List<PinnableSearchable>>
fun getHiddenItemKeys(): Flow<List<String>>
/**
* Remove this item from the Searchable database
*/
fun remove(searchable: Searchable)
fun remove(searchable: PinnableSearchable)
/**
* Remove this item from favorites and reset launch counter
*/
fun removeFromFavorites(searchable: Searchable)
fun removeFromFavorites(searchable: PinnableSearchable)
/**
* Ensure that this searchable exists in the Favorites table.
* If it doesn't exist, insert it with 0 launch count, not pinned and not hidden
*/
fun save(searchable: Searchable)
fun save(searchable: PinnableSearchable)
/**
* Get items with the given keys from the favorites database.
* Items that don't exist in the database will not be returned.
*/
suspend fun getFromKeys(keys: List<String>): List<Searchable>
suspend fun getFromKeys(keys: List<String>): List<PinnableSearchable>
suspend fun export(toDir: File)
suspend fun import(fromDir: File)
@ -101,7 +100,7 @@ internal class FavoritesRepositoryImpl(
automaticallySorted: Boolean,
frequentlyUsed: Boolean,
limit: Int
): Flow<List<Searchable>> {
): Flow<List<PinnableSearchable>> {
val dao = database.searchDao()
val entities = when {
includeTypes == null && excludeTypes == null -> dao.getFavorites(
@ -150,11 +149,11 @@ internal class FavoritesRepositoryImpl(
return database.searchDao().getHiddenCalendarEventKeys()
}
override fun isPinned(searchable: Searchable): Flow<Boolean> {
override fun isPinned(searchable: PinnableSearchable): Flow<Boolean> {
return AppDatabase.getInstance(context).searchDao().isPinned(searchable.key)
}
override fun pinItem(searchable: Searchable) {
override fun pinItem(searchable: PinnableSearchable) {
scope.launch {
withContext(Dispatchers.IO) {
val dao = AppDatabase.getInstance(context).searchDao()
@ -171,7 +170,7 @@ internal class FavoritesRepositoryImpl(
}
}
override fun unpinItem(searchable: Searchable) {
override fun unpinItem(searchable: PinnableSearchable) {
scope.launch {
withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).searchDao().unpinFavorite(searchable.key)
@ -179,11 +178,11 @@ internal class FavoritesRepositoryImpl(
}
}
override fun isHidden(searchable: Searchable): Flow<Boolean> {
override fun isHidden(searchable: PinnableSearchable): Flow<Boolean> {
return AppDatabase.getInstance(context).searchDao().isHidden(searchable.key)
}
override fun hideItem(searchable: Searchable) {
override fun hideItem(searchable: PinnableSearchable) {
scope.launch {
withContext(Dispatchers.IO) {
val dao = AppDatabase.getInstance(context).searchDao()
@ -200,7 +199,7 @@ internal class FavoritesRepositoryImpl(
}
}
override fun unhideItem(searchable: Searchable) {
override fun unhideItem(searchable: PinnableSearchable) {
scope.launch {
withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).searchDao().unhideItem(searchable.key)
@ -208,7 +207,7 @@ internal class FavoritesRepositoryImpl(
}
}
override fun incrementLaunchCounter(searchable: Searchable) {
override fun incrementLaunchCounter(searchable: PinnableSearchable) {
scope.launch {
withContext(Dispatchers.IO) {
val item = FavoritesItem(searchable.key, searchable, 0, 0, false)
@ -220,7 +219,7 @@ internal class FavoritesRepositoryImpl(
}
}
override fun getHiddenItems(): Flow<List<Searchable>> {
override fun getHiddenItems(): Flow<List<PinnableSearchable>> {
return database.searchDao().getHiddenItems().map {
it.mapNotNull { fromDatabaseEntity(it).searchable }
}
@ -230,7 +229,7 @@ internal class FavoritesRepositoryImpl(
return database.searchDao().getHiddenItemKeys()
}
override fun remove(searchable: Searchable) {
override fun remove(searchable: PinnableSearchable) {
scope.launch {
withContext(Dispatchers.IO) {
database.searchDao().deleteByKey(searchable.key)
@ -238,13 +237,13 @@ internal class FavoritesRepositoryImpl(
}
}
override fun removeFromFavorites(searchable: Searchable) {
override fun removeFromFavorites(searchable: PinnableSearchable) {
scope.launch {
database.searchDao().resetPinStatusAndLaunchCounter(searchable.key)
}
}
override fun save(searchable: Searchable) {
override fun save(searchable: PinnableSearchable) {
scope.launch {
withContext(Dispatchers.IO) {
val entity = FavoritesItem(
@ -260,8 +259,8 @@ internal class FavoritesRepositoryImpl(
}
override fun updateFavorites(
manuallySorted: List<Searchable>,
automaticallySorted: List<Searchable>
manuallySorted: List<PinnableSearchable>,
automaticallySorted: List<PinnableSearchable>
) {
val dao = database.searchDao()
scope.launch {
@ -321,7 +320,7 @@ internal class FavoritesRepositoryImpl(
}
}
override suspend fun getFromKeys(keys: List<String>): List<Searchable> {
override suspend fun getFromKeys(keys: List<String>): List<PinnableSearchable> {
val dao = database.searchDao()
return dao.getFromKeys(keys)
.mapNotNull { fromDatabaseEntity(it).searchable }

View File

@ -12,6 +12,7 @@ import de.mm20.launcher2.contacts.ContactSerializer
import de.mm20.launcher2.files.*
import de.mm20.launcher2.search.NullDeserializer
import de.mm20.launcher2.search.NullSerializer
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.search.data.*

View File

@ -1,14 +1,14 @@
package de.mm20.launcher2.favorites
import android.content.Context
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.data.Tag
import org.json.JSONObject
class TagSerializer: SearchableSerializer {
override fun serialize(searchable: Searchable): String {
override fun serialize(searchable: PinnableSearchable): String {
searchable as Tag
val json = JSONObject()
json.put("tag", searchable.tag)
@ -20,7 +20,7 @@ class TagSerializer: SearchableSerializer {
}
class TagDeserializer: SearchableDeserializer {
override fun deserialize(serialized: String): Searchable {
override fun deserialize(serialized: String): PinnableSearchable {
val json = JSONObject(serialized)
return Tag(json.getString("tag"))

View File

@ -1,20 +1,40 @@
package de.mm20.launcher2.search.data
import android.content.Context
import android.os.Bundle
import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TextLayer
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
class Tag(
data class Tag(
val tag: String,
): Searchable() {
override val key: String = "tag://$tag"
override val labelOverride: String? = null
): PinnableSearchable {
override val domain: String = Domain
override val key: String = "$domain://$tag"
override val label: String = tag
override val preferDetailsOverLaunch: Boolean = true
override fun launch(context: Context, options: Bundle?): Boolean {
return false
}
override fun overrideLabel(label: String): PinnableSearchable {
return this.copy(labelOverride = label)
}
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
return StaticLauncherIcon(
foregroundLayer = TextLayer("#"),
backgroundLayer = ColorLayer()
)
}
companion object {
const val Domain = "tag"
}
}

View File

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

View File

@ -1,12 +1,13 @@
package de.mm20.launcher2.files
import android.content.Context
import android.provider.DocumentsContract
import android.provider.MediaStore
import androidx.core.database.getStringOrNull
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.search.data.*
@ -15,7 +16,7 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.get
class LocalFileSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String {
override fun serialize(searchable: PinnableSearchable): String {
searchable as LocalFile
return jsonObjectOf(
"id" to searchable.id
@ -29,7 +30,7 @@ class LocalFileSerializer : SearchableSerializer {
class LocalFileDeserializer(
val context: Context
) : SearchableDeserializer, KoinComponent {
override fun deserialize(serialized: String): Searchable? {
override fun deserialize(serialized: String): PinnableSearchable? {
val permissionsManager: PermissionsManager = get()
if (!permissionsManager.checkPermissionOnce(
PermissionGroup.ExternalStorage
@ -74,7 +75,7 @@ class LocalFileDeserializer(
}
class GDriveFileSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String {
override fun serialize(searchable: PinnableSearchable): String {
searchable as GDriveFile
return jsonObjectOf(
"id" to searchable.fileId,
@ -103,7 +104,7 @@ class GDriveFileSerializer : SearchableSerializer {
}
class GDriveFileDeserializer : SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? {
override fun deserialize(serialized: String): PinnableSearchable {
val json = JSONObject(serialized)
val id = json.getString("id")
val label = json.getString("label")
@ -134,7 +135,7 @@ class GDriveFileDeserializer : SearchableDeserializer {
}
class OneDriveFileSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String {
override fun serialize(searchable: PinnableSearchable): String {
searchable as OneDriveFile
return jsonObjectOf(
"id" to searchable.fileId,
@ -161,7 +162,7 @@ class OneDriveFileSerializer : SearchableSerializer {
}
class OneDriveFileDeserializer : SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? {
override fun deserialize(serialized: String): PinnableSearchable {
val json = JSONObject(serialized)
val fileId = json.getString("id")
val label = json.getString("label")
@ -189,10 +190,10 @@ class OneDriveFileDeserializer : SearchableDeserializer {
}
class NextcloudFileSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String {
override fun serialize(searchable: PinnableSearchable): String {
searchable as NextcloudFile
return jsonObjectOf(
"id" to searchable.id,
"id" to searchable.fileId,
"label" to searchable.label,
"path" to searchable.path,
"mimeType" to searchable.mimeType,
@ -216,7 +217,7 @@ class NextcloudFileSerializer : SearchableSerializer {
}
class NextcloudFileDeserializer : SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? {
override fun deserialize(serialized: String): PinnableSearchable {
val json = JSONObject(serialized)
val id = json.getLong("id")
val label = json.getString("label")
@ -242,10 +243,10 @@ class NextcloudFileDeserializer : SearchableDeserializer {
}
class OwncloudFileSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String {
override fun serialize(searchable: PinnableSearchable): String {
searchable as OwncloudFile
return jsonObjectOf(
"id" to searchable.id,
"id" to searchable.fileId,
"label" to searchable.label,
"path" to searchable.path,
"mimeType" to searchable.mimeType,
@ -269,7 +270,7 @@ class OwncloudFileSerializer : SearchableSerializer {
}
class OwncloudFileDeserializer : SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? {
override fun deserialize(serialized: String): PinnableSearchable {
val json = JSONObject(serialized)
val id = json.getLong("id")
val label = json.getString("label")

View File

@ -6,15 +6,18 @@ import de.mm20.launcher2.nextcloud.NextcloudApiHelper
import de.mm20.launcher2.owncloud.OwncloudClient
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.SearchableRepository
import de.mm20.launcher2.search.data.File
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
interface FileRepository {
fun search(query: String): Flow<List<File>>
interface FileRepository: SearchableRepository<File> {
fun deleteFile(file: File)
}
@ -60,21 +63,21 @@ internal class FileRepositoryImpl(
}
}
override fun search(query: String): Flow<List<File>> = channelFlow {
override fun search(query: String): Flow<ImmutableList<File>> = channelFlow {
if (query.isBlank()) {
send(emptyList())
send(persistentListOf())
return@channelFlow
}
providers.collectLatest { providers ->
if (providers.isEmpty()) {
send(emptyList())
send(persistentListOf())
return@collectLatest
}
val results = mutableListOf<File>()
for (provider in providers) {
results.addAll(provider.search(query))
send(results.toList())
send(results.toImmutableList())
}
}
}

View File

@ -1,25 +1,29 @@
package de.mm20.launcher2.search.data
import android.content.Context
import android.graphics.Color
import androidx.core.content.ContextCompat
import de.mm20.launcher2.files.R
import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TintedIconLayer
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
import java.util.*
abstract class File(
val id: Long,
val path: String,
val mimeType: String,
val size: Long,
val isDirectory: Boolean,
interface File : PinnableSearchable {
val path: String
val mimeType: String
val size: Long
val isDirectory: Boolean
val metaData: List<Pair<Int, String>>
) : Searchable() {
abstract val isStoredInCloud: Boolean
open val providerIconRes: Int? = null
val isStoredInCloud: Boolean
override val preferDetailsOverLaunch: Boolean
get() = false
open val providerIconRes: Int?
get() = null
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
val (resId, bgColor) = when {
@ -124,7 +128,8 @@ abstract class File(
return context.getString(resource)
}
open val isDeletable: Boolean = false
open suspend fun delete(context: Context) {}
val isDeletable: Boolean
get() = false
suspend fun delete(context: Context) {}
}

View File

@ -3,30 +3,47 @@ package de.mm20.launcher2.search.data
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import de.mm20.launcher2.files.R
import de.mm20.launcher2.ktx.tryStartActivity
class GDriveFile(
data class GDriveFile(
val fileId: String,
override val label: String,
path: String,
mimeType: String,
size: Long,
isDirectory: Boolean,
metaData: List<Pair<Int, String>>,
override val path: String,
override val mimeType: String,
override val size: Long,
override val isDirectory: Boolean,
override val metaData: List<Pair<Int, String>>,
val directoryColor: String?,
val viewUri: String
) : File(0, path, mimeType, size, isDirectory, metaData) {
val viewUri: String,
override val labelOverride: String? = null,
) : File {
override val key: String = "gdrive://$fileId"
override fun overrideLabel(label: String): GDriveFile {
return this.copy(labelOverride = label)
}
override val domain: String = Domain
override val key: String = "$domain://$fileId"
override val isStoredInCloud = true
override val providerIconRes = R.drawable.ic_badge_gdrive
override fun getLaunchIntent(context: Context): Intent? {
private fun getLaunchIntent(): Intent {
return Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(viewUri)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
}
override fun launch(context: Context, options: Bundle?): Boolean {
return context.tryStartActivity(getLaunchIntent(), options)
}
companion object {
const val Domain = "gdrive"
}
}

View File

@ -9,6 +9,7 @@ import android.location.Geocoder
import android.media.MediaMetadataRetriever
import android.media.ThumbnailUtils
import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore
import android.text.format.DateUtils
import android.util.Size
@ -17,25 +18,34 @@ import androidx.exifinterface.media.ExifInterface
import de.mm20.launcher2.files.R
import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.formatToString
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.media.ThumbnailUtilsCompat
import de.mm20.launcher2.search.PinnableSearchable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import java.io.IOException
import java.io.File as JavaIOFile
open class LocalFile(
id: Long,
path: String,
mimeType: String,
size: Long,
isDirectory: Boolean,
metaData: List<Pair<Int, String>>
) : File(id, path, mimeType, size, isDirectory, metaData) {
data class LocalFile(
val id: Long,
override val path: String,
override val mimeType: String,
override val size: Long,
override val isDirectory: Boolean,
override val metaData: List<Pair<Int, String>>,
override val labelOverride: String? = null
) : File {
override val label = path.substringAfterLast('/')
override val key = "file://$path"
override fun overrideLabel(label: String): LocalFile {
return this.copy(labelOverride = label)
}
override val domain: String = Domain
override val key = "$domain://$path"
override val isStoredInCloud = false
@ -148,7 +158,7 @@ open class LocalFile(
}
override fun getLaunchIntent(context: Context): Intent? {
private fun getLaunchIntent(context: Context): Intent {
val uri = if (isDirectory) {
Uri.parse(path)
} else {
@ -162,6 +172,10 @@ open class LocalFile(
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
override fun launch(context: Context, options: Bundle?): Boolean {
return context.tryStartActivity(getLaunchIntent(context), options)
}
override val isDeletable: Boolean
get() {
val file = java.io.File(path)
@ -186,6 +200,9 @@ open class LocalFile(
companion object {
const val Domain = "file"
internal fun getMimetypeByFileExtension(extension: String): String {
return when (extension) {
"apk" -> "application/vnd.android.package-archive"

View File

@ -4,34 +4,50 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import de.mm20.launcher2.files.R
import de.mm20.launcher2.ktx.tryStartActivity
class NextcloudFile(
fileId: Long,
data class NextcloudFile(
val fileId: Long,
override val label: String,
path: String,
mimeType: String,
size: Long,
isDirectory: Boolean,
override val path: String,
override val mimeType: String,
override val size: Long,
override val isDirectory: Boolean,
val server: String,
metaData: List<Pair<Int, String>>
) : File(fileId, path, mimeType, size, isDirectory, metaData) {
override val key: String = "nextcloud://$server/$fileId"
override val metaData: List<Pair<Int, String>>,
override val labelOverride: String? = null,
) : File {
override fun overrideLabel(label: String): NextcloudFile {
return this.copy(labelOverride = label)
}
override val domain: String = Domain
override val key: String = "$domain://$server/$fileId"
override val isStoredInCloud: Boolean
get() = true
override val providerIconRes = R.drawable.ic_badge_nextcloud
override fun getLaunchIntent(context: Context): Intent? {
private fun getLaunchIntent(context: Context): Intent {
return Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse("$server/f/$id")
data = Uri.parse("$server/f/$fileId")
flags = Intent.FLAG_ACTIVITY_NEW_TASK
`package` = getNextcloudAppPackage(context)
}
}
override fun launch(context: Context, options: Bundle?): Boolean {
return context.tryStartActivity(getLaunchIntent(context), options)
}
companion object {
const val Domain = "nextcloud"
private fun getNextcloudAppPackage(context: Context): String? {
val candidates = listOf("com.nextcloud.client", "com.nextcloud.android.beta")

View File

@ -3,29 +3,46 @@ package de.mm20.launcher2.search.data
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import de.mm20.launcher2.files.R
import de.mm20.launcher2.ktx.tryStartActivity
class OneDriveFile(
data class OneDriveFile(
val fileId: String,
override val label: String,
path: String,
mimeType: String,
size: Long,
isDirectory: Boolean,
metaData: List<Pair<Int, String>>,
val webUrl: String
) : File(0, path, mimeType, size, isDirectory, metaData) {
override val path: String,
override val mimeType: String,
override val size: Long,
override val isDirectory: Boolean,
override val metaData: List<Pair<Int, String>>,
val webUrl: String,
override val labelOverride: String? = null,
) : File {
override val key: String = "onedrive://$fileId"
override fun overrideLabel(label: String): OneDriveFile {
return this.copy(labelOverride = label)
}
override val domain: String = Domain
override val key: String = "$domain://$fileId"
override val providerIconRes = R.drawable.ic_badge_onedrive
override val isStoredInCloud = true
override fun getLaunchIntent(context: Context): Intent? {
private fun getLaunchIntent(): Intent {
return Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(webUrl)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
}
override fun launch(context: Context, options: Bundle?): Boolean {
return context.tryStartActivity(getLaunchIntent(), options)
}
companion object {
const val Domain = "onedrive"
}
}

View File

@ -3,29 +3,46 @@ package de.mm20.launcher2.search.data
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import de.mm20.launcher2.files.R
import de.mm20.launcher2.ktx.tryStartActivity
class OwncloudFile(
fileId: Long,
data class OwncloudFile(
val fileId: Long,
override val label: String,
path: String,
mimeType: String,
size: Long,
isDirectory: Boolean,
override val path: String,
override val mimeType: String,
override val size: Long,
override val isDirectory: Boolean,
val server: String,
metaData: List<Pair<Int, String>>
) : File(fileId, path, mimeType, size, isDirectory, metaData) {
override val key: String = "owncloud://$server/$fileId"
override val metaData: List<Pair<Int, String>>,
override val labelOverride: String? = null,
) : File {
override fun overrideLabel(label: String): OwncloudFile {
return this.copy(labelOverride = label)
}
override val domain: String = Domain
override val key: String = "$domain://$server/$fileId"
override val isStoredInCloud: Boolean
get() = true
override val providerIconRes = R.drawable.ic_badge_owncloud
override fun getLaunchIntent(context: Context): Intent? {
private fun getLaunchIntent(): Intent {
return Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse("$server/f/$id")
data = Uri.parse("$server/f/$fileId")
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
}
override fun launch(context: Context, options: Bundle?): Boolean {
return context.tryStartActivity(getLaunchIntent(), options)
}
companion object {
const val Domain = "owncloud"
}
}

View File

@ -50,7 +50,6 @@ dependencies {
implementation(project(":preferences"))
implementation(project(":ktx"))
implementation(project(":base"))
implementation(project(":search"))
implementation(project(":applications"))
implementation(project(":crashreporter"))
api(project(":customattrs"))

View File

@ -13,8 +13,9 @@ import de.mm20.launcher2.icons.transformations.LauncherIconTransformation
import de.mm20.launcher2.icons.transformations.LegacyToAdaptiveTransformation
import de.mm20.launcher2.icons.transformations.transform
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.data.LauncherApp
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.Searchable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -98,7 +99,7 @@ class IconRepository(
}
fun getIcon(searchable: Searchable, size: Int): Flow<LauncherIcon> = channelFlow {
fun getIcon(searchable: PinnableSearchable, size: Int): Flow<LauncherIcon> = channelFlow {
iconProviders.collectLatest { providers ->
transformations.collectLatest { transformations ->
customAttributesRepository.getCustomIcon(searchable).collectLatest { customIcon ->
@ -189,7 +190,7 @@ class IconRepository(
}
suspend fun getCustomIconSuggestions(
searchable: Searchable,
searchable: PinnableSearchable,
size: Int
): List<CustomIconWithPreview> {
val suggestions = mutableListOf<CustomIconWithPreview>()
@ -301,7 +302,7 @@ class IconRepository(
}
suspend fun getUncustomizedDefaultIcon(searchable: Searchable, size: Int): CustomIconWithPreview? {
suspend fun getUncustomizedDefaultIcon(searchable: PinnableSearchable, size: Int): CustomIconWithPreview? {
val icon = iconProviders.first().getFirstIcon(searchable, size)
?.transform(transformations.first()) ?: return null
return CustomIconWithPreview(
@ -338,7 +339,7 @@ class IconRepository(
return iconPackIcons + themedIcons
}
fun setCustomIcon(searchable: Searchable, icon: CustomIcon?) {
fun setCustomIcon(searchable: PinnableSearchable, icon: CustomIcon?) {
customAttributesRepository.setCustomIcon(searchable, icon)
}

View File

@ -3,16 +3,16 @@ package de.mm20.launcher2.icons.providers
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.drawable.ColorDrawable
import de.mm20.launcher2.icons.DynamicCalendarIcon
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.obtainTypedArrayOrNull
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.data.LauncherApp
class CalendarIconProvider(val context: Context): IconProvider {
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? {
if(searchable !is Application) return null
override suspend fun getIcon(searchable: PinnableSearchable, size: Int): LauncherIcon? {
if(searchable !is LauncherApp) return null
val component = ComponentName(searchable.`package`, searchable.activity)
val pm = context.packageManager
val ai = try {

View File

@ -4,14 +4,14 @@ import android.content.ComponentName
import de.mm20.launcher2.customattrs.CustomIconPackIcon
import de.mm20.launcher2.icons.IconPackManager
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.data.LauncherApp
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
class CustomIconPackIconProvider(
private val customIcon: CustomIconPackIcon,
private val iconPackManager: IconPackManager,
) : IconProvider {
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? {
override suspend fun getIcon(searchable: PinnableSearchable, size: Int): LauncherIcon? {
return iconPackManager.getIcon(
customIcon.iconPackPackage,
ComponentName.unflattenFromString(customIcon.iconComponentName) ?: return null

View File

@ -3,13 +3,14 @@ package de.mm20.launcher2.icons.providers
import de.mm20.launcher2.customattrs.CustomThemedIcon
import de.mm20.launcher2.icons.IconPackManager
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
class CustomThemedIconProvider(
private val customIcon: CustomThemedIcon,
private val iconPackManager: IconPackManager,
): IconProvider {
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? {
override suspend fun getIcon(searchable: PinnableSearchable, size: Int): LauncherIcon? {
return iconPackManager.getThemedIcon(customIcon.iconPackageName)
}
}

View File

@ -8,13 +8,13 @@ import android.graphics.drawable.LayerDrawable
import android.graphics.drawable.RotateDrawable
import androidx.core.content.res.ResourcesCompat
import de.mm20.launcher2.icons.*
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.data.Searchable
import kotlin.math.roundToInt
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.data.LauncherApp
class GoogleClockIconProvider(val context: Context) : IconProvider {
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? {
if (searchable !is Application) return null
override suspend fun getIcon(searchable: PinnableSearchable, size: Int): LauncherIcon? {
if (searchable !is LauncherApp) return null
if (searchable.`package` != "com.google.android.deskclock") return null
val pm = context.packageManager
val appInfo = try {

View File

@ -3,8 +3,9 @@ package de.mm20.launcher2.icons.providers
import android.content.ComponentName
import android.content.Context
import de.mm20.launcher2.icons.*
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.data.LauncherApp
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.Searchable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -13,7 +14,7 @@ class IconPackIconProvider(
private val iconPack: String,
private val iconPackManager: IconPackManager,
): IconProvider {
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? {
override suspend fun getIcon(searchable: PinnableSearchable, size: Int): LauncherIcon? {
if (searchable !is LauncherApp) return null
val component = ComponentName(searchable.`package`, searchable.activity)

View File

@ -1,14 +1,15 @@
package de.mm20.launcher2.icons.providers
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
interface IconProvider {
suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon?
suspend fun getIcon(searchable: PinnableSearchable, size: Int): LauncherIcon?
}
internal suspend fun Iterable<IconProvider>.getFirstIcon(
searchable: Searchable,
searchable: PinnableSearchable,
size: Int
): LauncherIcon? {
for (provider in this) {

View File

@ -2,10 +2,11 @@ package de.mm20.launcher2.icons.providers
import android.content.Context
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
class PlaceholderIconProvider(val context: Context) : IconProvider {
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon {
override suspend fun getIcon(searchable: PinnableSearchable, size: Int): LauncherIcon {
return searchable.getPlaceholderIcon(context)
}
}

View File

@ -2,13 +2,14 @@ package de.mm20.launcher2.icons.providers
import android.content.Context
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
class SystemIconProvider(
private val context: Context,
private val themedIcons: Boolean,
) : IconProvider {
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? {
override suspend fun getIcon(searchable: PinnableSearchable, size: Int): LauncherIcon? {
return searchable.loadIcon(context, size, themedIcons)
}
}

View File

@ -1,25 +1,16 @@
package de.mm20.launcher2.icons.providers
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Resources
import android.graphics.drawable.LayerDrawable
import android.graphics.drawable.RotateDrawable
import androidx.core.content.res.ResourcesCompat
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.obtainTypedArrayOrNull
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.data.LauncherApp
internal class ThemedIconProvider(
private val iconPackManager: IconPackManager,
) : IconProvider {
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? {
if (searchable !is Application) return null
override suspend fun getIcon(searchable: PinnableSearchable, size: Int): LauncherIcon? {
if (searchable !is LauncherApp) return null
return iconPackManager.getThemedIcon(searchable.`package`)
}
}

View File

@ -2,13 +2,14 @@ package de.mm20.launcher2.icons.providers
import android.content.Context
import de.mm20.launcher2.icons.*
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
internal class ThemedPlaceholderIconProvider(
private val context: Context,
) : IconProvider {
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon {
override suspend fun getIcon(searchable: PinnableSearchable, size: Int): LauncherIcon {
val icon = searchable.getPlaceholderIcon(context)
return StaticLauncherIcon(

View File

@ -43,6 +43,5 @@ dependencies {
implementation(project(":ktx"))
implementation(project(":base"))
implementation(project(":search"))
implementation(project(":crashreporter"))
}

View File

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

View File

@ -1,68 +0,0 @@
package de.mm20.launcher2.search.data
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.ktx.romanize
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.R
import java.text.Collator
abstract class Searchable : Comparable<Searchable> {
abstract val key: String
abstract val label: String
var labelOverride: String? = null
open fun serialize(): String = ""
open fun getLaunchIntent(context: Context): Intent? = null
open fun launch(context: Context, options: Bundle?): Boolean {
val intent = getLaunchIntent(context) ?: return false
return if (context.tryStartActivity(intent, options)) {
true
} else {
Toast.makeText(
context,
context.getString(R.string.error_activity_not_found, label),
Toast.LENGTH_SHORT
).show()
false
}
}
open suspend fun loadIcon(
context: Context,
size: Int,
themed: Boolean,
): LauncherIcon? = null
abstract fun getPlaceholderIcon(context: Context): StaticLauncherIcon
override fun compareTo(other: Searchable): Int {
val label1 = labelOverride ?: label
val label2 = other.labelOverride ?: other.label
return Collator.getInstance().apply { strength = Collator.SECONDARY }
.compare(label1.romanize(), label2.romanize())
}
override fun equals(other: Any?): Boolean {
return if (other is Searchable) key == other.key && label == other.label && labelOverride == other.labelOverride
else super.equals(other)
}
override fun hashCode(): Int {
var result = key.hashCode()
result = 31 * result + label.hashCode()
return result
}
override fun toString(): String {
return "$label ($key)"
}
}

View File

@ -5,7 +5,8 @@ import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.customattrs.CustomAttributesRepository
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.data.Tag
import de.mm20.launcher2.ui.utils.withCustomLabels
import de.mm20.launcher2.widgets.WidgetRepository
@ -32,7 +33,7 @@ open class FavoritesVM : ViewModel(), KoinComponent {
it.filterIsInstance<Tag>()
}
val favorites: Flow<List<Searchable>> = selectedTag.flatMapLatest { tag ->
val favorites: Flow<List<PinnableSearchable>> = selectedTag.flatMapLatest { tag ->
if (tag == null) {
val columns = dataStore.data.map { it.grid.columnCount }
val excludeCalendar = widgetRepository.isCalendarWidgetEnabled()

View File

@ -71,7 +71,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.drawOutline
@ -90,7 +89,8 @@ import androidx.compose.ui.unit.toSize
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.MissingPermissionBanner
@ -739,7 +739,7 @@ fun ShortcutPicker(viewModel: EditFavoritesSheetVM) {
}
sealed interface FavoritesSheetGridItem {
class Favorite(val item: Searchable) : FavoritesSheetGridItem
class Favorite(val item: PinnableSearchable) : FavoritesSheetGridItem
class Divider(val section: FavoritesSheetSection) : FavoritesSheetGridItem
class Spacer(val span: Int = 1) : FavoritesSheetGridItem
object EmptySection : FavoritesSheetGridItem

View File

@ -17,12 +17,12 @@ import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.normalize
import de.mm20.launcher2.ktx.romanize
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.data.AppShortcut
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.data.Tag
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
@ -48,9 +48,9 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent {
val createShortcutTarget = MutableLiveData<FavoritesSheetSection?>(null)
private var manuallySorted: MutableList<Searchable> = mutableListOf()
private var automaticallySorted: MutableList<Searchable> = mutableListOf()
private var frequentlyUsed: MutableList<Searchable> = mutableListOf()
private var manuallySorted: MutableList<PinnableSearchable> = mutableListOf()
private var automaticallySorted: MutableList<PinnableSearchable> = mutableListOf()
private var frequentlyUsed: MutableList<PinnableSearchable> = mutableListOf()
val pinnedTags = MutableLiveData<List<Tag>>(emptyList())
val availableTags = MutableLiveData<List<Tag>>(emptyList())
@ -179,7 +179,7 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent {
)
}
fun getIcon(searchable: Searchable, size: Int): Flow<LauncherIcon?> {
fun getIcon(searchable: PinnableSearchable, size: Int): Flow<LauncherIcon?> {
return iconRepository.getIcon(searchable, size)
}

View File

@ -16,14 +16,15 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
@Composable
fun HiddenItemsSheet(
items: List<Searchable>,
items: List<PinnableSearchable>,
onDismiss: () -> Unit
) {
val viewModel: HiddenItemsSheetVM = viewModel()

View File

@ -43,7 +43,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.LauncherCard
import de.mm20.launcher2.ui.component.MissingPermissionBanner
@ -381,7 +382,7 @@ fun SearchColumn(
}
fun LazyListScope.GridResults(
items: ImmutableList<Searchable>,
items: ImmutableList<PinnableSearchable>,
columns: Int,
reverse: Boolean,
showLabels: Boolean,
@ -446,7 +447,7 @@ fun LazyListScope.GridResults(
@Composable
fun GridRow(
modifier: Modifier = Modifier,
items: ImmutableList<Searchable>,
items: ImmutableList<PinnableSearchable>,
columns: Int,
showLabels: Boolean,
) {
@ -470,7 +471,7 @@ fun GridRow(
}
fun LazyListScope.ListResults(
items: ImmutableList<Searchable>,
items: ImmutableList<PinnableSearchable>,
reverse: Boolean,
key: String,
before: (@Composable () -> Unit)? = null,
@ -523,7 +524,7 @@ fun LazyListScope.ListResults(
@Composable
fun ListRow(
modifier: Modifier = Modifier,
item: Searchable,
item: PinnableSearchable,
) {
Box(
modifier = modifier.padding(

View File

@ -16,12 +16,13 @@ import de.mm20.launcher2.files.FileRepository
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.WebsearchRepository
import de.mm20.launcher2.search.data.*
import de.mm20.launcher2.ui.utils.withCustomLabels
import de.mm20.launcher2.unitconverter.UnitConverterRepository
import de.mm20.launcher2.websites.WebsiteRepository
import de.mm20.launcher2.widgets.WidgetRepository
import de.mm20.launcher2.wikipedia.WikipediaRepository
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
@ -52,8 +53,8 @@ class SearchVM : ViewModel(), KoinComponent {
val showLabels = dataStore.data.map { it.grid.showLabels }.asLiveData()
val appResults = MutableLiveData<List<Application>>(emptyList())
val workAppResults = MutableLiveData<List<Application>>(emptyList())
val appResults = MutableLiveData<List<LauncherApp>>(emptyList())
val workAppResults = MutableLiveData<List<LauncherApp>>(emptyList())
val appShortcutResults = MutableLiveData<List<AppShortcut>>(emptyList())
val fileResults = MutableLiveData<List<File>>(emptyList())
val contactResults = MutableLiveData<List<Contact>>(emptyList())
@ -64,7 +65,7 @@ class SearchVM : ViewModel(), KoinComponent {
val unitConverterResult = MutableLiveData<UnitConverter?>(null)
val websearchResults = MutableLiveData<List<Websearch>>(emptyList())
val hiddenResults = MutableLiveData<List<Searchable>>(emptyList())
val hiddenResults = MutableLiveData<List<PinnableSearchable>>(emptyList())
val favoritesEnabled = dataStore.data.map { it.favorites.enabled }
val hideFavorites = MutableLiveData(false)
@ -95,7 +96,7 @@ class SearchVM : ViewModel(), KoinComponent {
val customAttrResults = customAttributesRepository.search(query)
.combine(dataStore.data) { items, settings ->
items.filter {
it is Application
it is LauncherApp
|| it is Contact && settings.contactsSearch.enabled
|| it is CalendarEvent && settings.calendarSearch.enabled
|| it is AppShortcut && settings.appShortcutSearch.enabled
@ -285,15 +286,15 @@ class SearchVM : ViewModel(), KoinComponent {
}
private inline fun <reified T : Searchable> Flow<List<T>>.withCustomAttributeResults(
customAttributeResults: Flow<List<Searchable>>
private inline fun <reified T : PinnableSearchable> Flow<List<T>>.withCustomAttributeResults(
customAttributeResults: Flow<List<PinnableSearchable>>
): Flow<List<T>> {
return this.combine(customAttributeResults) { items, items2 ->
(items + items2.filterIsInstance<T>()).distinctBy { it.key }
}
}
private suspend fun <T : Searchable> Flow<List<T>>.collectWithHiddenItems(
private suspend fun <T : PinnableSearchable> Flow<List<T>>.collectWithHiddenItems(
hiddenItemKeys: Flow<List<String>>,
action: (items: List<T>, hidden: List<T>) -> Unit
) {
@ -305,18 +306,18 @@ class SearchVM : ViewModel(), KoinComponent {
}
}
private fun <T : Searchable> Flow<List<T>>.sorted(): Flow<List<T>> = this.map { it.sorted() }
private fun <T : PinnableSearchable> Flow<List<T>>.sorted(): Flow<List<T>> = this.map { it.sorted() }
}
private data class HiddenItemResults(
val apps: List<Application> = emptyList(),
val apps: List<LauncherApp> = emptyList(),
val contacts: List<Contact> = emptyList(),
val calendarEvents: List<CalendarEvent> = emptyList(),
val files: List<File> = emptyList(),
val appShortcuts: List<AppShortcut> = emptyList(),
) {
fun joinToList(): List<Searchable> {
fun joinToList(): List<PinnableSearchable> {
return apps + contacts + calendarEvents + files + appShortcuts
}
}

View File

@ -23,7 +23,7 @@ import androidx.core.app.NotificationCompat
import androidx.lifecycle.lifecycleScope
import coil.compose.rememberAsyncImagePainter
import com.google.accompanist.flowlayout.FlowRow
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.data.LauncherApp
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.*
import de.mm20.launcher2.ui.ktx.toDp
@ -40,7 +40,7 @@ import kotlin.math.roundToInt
@Composable
fun AppItem(
modifier: Modifier = Modifier,
app: Application,
app: LauncherApp,
onBack: () -> Unit
) {
val viewModel = remember { AppItemVM(app) }
@ -336,7 +336,7 @@ fun AppItem(
@Composable
fun AppItemGridPopup(
app: Application,
app: LauncherApp,
show: Boolean,
animationProgress: Float,
origin: Rect,

View File

@ -8,16 +8,13 @@ import android.content.pm.*
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Process
import android.provider.Settings
import android.service.notification.StatusBarNotification
import androidx.core.content.FileProvider
import androidx.core.content.getSystemService
import de.mm20.launcher2.appshortcuts.AppShortcutRepository
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.notifications.NotificationRepository
import de.mm20.launcher2.search.data.AppShortcut
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.data.LauncherApp
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
import kotlinx.coroutines.Dispatchers
@ -28,7 +25,7 @@ import kotlinx.coroutines.withContext
import org.koin.core.component.inject
class AppItemVM(
private val app: Application
private val app: LauncherApp
) : SearchableItemVM(app) {
private val notificationRepository: NotificationRepository by inject()
private val appShortcutRepository: AppShortcutRepository by inject()
@ -44,21 +41,12 @@ class AppItemVM(
fun openAppInfo(context: Context) {
val launcherApps = context.getSystemService<LauncherApps>()!!
if (app is LauncherApp) {
launcherApps.startAppDetailsActivity(
ComponentName(app.`package`, app.activity),
app.getUser(),
null,
null
)
} else {
context.tryStartActivity(
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.parse("package:${app.`package`}")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
)
}
launcherApps.startAppDetailsActivity(
ComponentName(app.`package`, app.activity),
app.getUser(),
null,
null
)
}
suspend fun shareApkFile(context: Context) {
@ -128,9 +116,7 @@ class AppItemVM(
}
val shortcuts = flow {
if (app is LauncherApp) {
emit(appShortcutRepository.getShortcutsForActivity(app.launcherActivityInfo, 5))
}
emit(appShortcutRepository.getShortcutsForActivity(app.launcherActivityInfo, 5))
}
fun isShortcutPinned(shortcut: AppShortcut): Flow<Boolean> {

View File

@ -10,15 +10,15 @@ import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.data.AppShortcut
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.data.LauncherApp
import kotlinx.coroutines.flow.Flow
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
abstract class SearchableItemVM(
private val searchable: Searchable
private val searchable: PinnableSearchable
) : KoinComponent {
protected val favoritesRepository: FavoritesRepository by inject()
protected val badgeRepository: BadgeRepository by inject()
@ -74,7 +74,7 @@ abstract class SearchableItemVM(
if (searchable.launch(context, bundle)) {
favoritesRepository.incrementLaunchCounter(searchable)
return true
} else if (searchable is Application || searchable is AppShortcut) {
} else if (searchable is LauncherApp || searchable is AppShortcut) {
favoritesRepository.remove(searchable)
}
return false

View File

@ -23,7 +23,8 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.icons.CustomIconWithPreview
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
@ -35,7 +36,7 @@ import kotlinx.coroutines.launch
@Composable
fun CustomizeSearchableSheet(
searchable: Searchable,
searchable: PinnableSearchable,
onDismiss: () -> Unit,
) {
val viewModel: CustomizeSearchableSheetVM =

View File

@ -4,11 +4,11 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.liveData
import de.mm20.launcher2.customattrs.CustomAttributesRepository
import de.mm20.launcher2.customattrs.CustomIcon
import de.mm20.launcher2.customattrs.customAttrsModule
import de.mm20.launcher2.icons.CustomIconWithPreview
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import org.koin.core.component.KoinComponent
@ -16,7 +16,7 @@ import org.koin.core.component.inject
import kotlin.coroutines.coroutineContext
class CustomizeSearchableSheetVM(
private val searchable: Searchable
private val searchable: PinnableSearchable
) : KoinComponent {
private val iconRepository: IconRepository by inject()
private val customAttributesRepository: CustomAttributesRepository by inject()

View File

@ -2,19 +2,15 @@ package de.mm20.launcher2.ui.launcher.search.common.grid
import android.content.ComponentName
import androidx.activity.compose.BackHandler
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
@ -26,6 +22,8 @@ import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.data.*
import de.mm20.launcher2.ui.component.LauncherCard
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
@ -47,7 +45,7 @@ import kotlinx.coroutines.delay
@Composable
fun GridItem(modifier: Modifier = Modifier, item: Searchable, showLabels: Boolean = true) {
fun GridItem(modifier: Modifier = Modifier, item: PinnableSearchable, showLabels: Boolean = true) {
val viewModel = remember(item.key) { GridItemVM(item) }
val context = LocalContext.current
@ -59,13 +57,11 @@ fun GridItem(modifier: Modifier = Modifier, item: Searchable, showLabels: Boolea
val iconSize = LocalGridIconSize.current.toPixels()
val icon by remember(item.key) { viewModel.getIcon(iconSize.toInt()) }.collectAsState(null)
// If item is one of these types, try to launch them on click; show details otherwise
val launchOnPress =
item is File || item is Application || item is AppShortcut || item is Website || item is Wikipedia
val launchOnPress = !item.preferDetailsOverLaunch
val windowSize = LocalWindowSize.current
if (item is Application) {
if (item is LauncherApp) {
HandleHomeTransition {
val cn = ComponentName(item.`package`, item.activity)
if (
@ -164,7 +160,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
.wrapContentSize()
) {
when (searchable) {
is Application -> {
is LauncherApp -> {
AppItemGridPopup(
app = searchable,
show = show,

View File

@ -1,8 +1,9 @@
package de.mm20.launcher2.ui.launcher.search.common.grid
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
class GridItemVM(
searchable: Searchable
searchable: PinnableSearchable
): SearchableItemVM(searchable)

View File

@ -5,14 +5,15 @@ import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.ui.layout.BottomReversed
import de.mm20.launcher2.ui.locals.LocalGridColumns
import kotlin.math.ceil
@Composable
fun SearchResultGrid(
items: List<Searchable>,
items: List<PinnableSearchable>,
modifier: Modifier = Modifier,
showLabels: Boolean = true,
columns: Int = LocalGridColumns.current,

View File

@ -2,13 +2,14 @@ package de.mm20.launcher2.ui.launcher.search.common.list
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.data.*
import de.mm20.launcher2.ui.component.InnerCard
import de.mm20.launcher2.ui.launcher.search.calendar.CalendarItem
@ -17,7 +18,7 @@ import de.mm20.launcher2.ui.launcher.search.files.FileItem
import de.mm20.launcher2.ui.launcher.search.shortcut.AppShortcutItem
@Composable
fun ListItem(modifier: Modifier = Modifier, item: Searchable) {
fun ListItem(modifier: Modifier = Modifier, item: PinnableSearchable) {
var showDetails by remember { mutableStateOf(false) }
val context = LocalContext.current

View File

@ -1,8 +1,9 @@
package de.mm20.launcher2.ui.launcher.search.common.list
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
class ListItemVM(
searchable: Searchable
searchable: PinnableSearchable
): SearchableItemVM(searchable)

View File

@ -8,12 +8,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.ui.layout.BottomReversed
@Composable
fun SearchResultList(
items: List<Searchable>,
items: List<PinnableSearchable>,
modifier: Modifier = Modifier,
reverse: Boolean = false
) {

View File

@ -57,7 +57,7 @@ fun WebsiteItem(
modifier = Modifier.padding(16.dp),
) {
Text(
text = website.label,
text = website.labelOverride ?: website.label,
style = MaterialTheme.typography.titleLarge
)
val tags by remember(viewModel) { viewModel.getTags() }.collectAsState(emptyList())

View File

@ -1,17 +1,5 @@
package de.mm20.launcher2.ui.launcher.widgets.favorites
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.ui.common.FavoritesVM
import de.mm20.launcher2.widgets.WidgetRepository
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class FavoritesWidgetVM: FavoritesVM()

View File

@ -1,6 +1,5 @@
package de.mm20.launcher2.ui.settings.hiddenitems
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
@ -21,7 +20,6 @@ import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen

View File

@ -2,11 +2,8 @@ package de.mm20.launcher2.ui.settings.hiddenitems
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.LauncherApps
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import androidx.core.content.getSystemService
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
@ -17,10 +14,9 @@ import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.data.LauncherApp
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.Searchable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
@ -37,18 +33,18 @@ class HiddenItemsSettingsScreenVM : ViewModel(), KoinComponent {
val allApps = appRepository.getAllInstalledApps().map {
withContext(Dispatchers.Default) { it.sorted() }
}.asLiveData()
val hiddenItems: LiveData<List<Searchable>> = liveData {
val hiddenItems: LiveData<List<PinnableSearchable>> = liveData {
val hidden = withContext(Dispatchers.Default) {
favoritesRepository.getHiddenItems().first().filter { it !is Application }.sorted()
favoritesRepository.getHiddenItems().first().filter { it !is LauncherApp }.sorted()
}
emit(hidden)
}
fun isHidden(searchable: Searchable): Flow<Boolean> {
fun isHidden(searchable: PinnableSearchable): Flow<Boolean> {
return favoritesRepository.isHidden(searchable)
}
fun setHidden(searchable: Searchable, hidden: Boolean) {
fun setHidden(searchable: PinnableSearchable, hidden: Boolean) {
if(hidden) {
favoritesRepository.hideItem(searchable)
} else {
@ -56,11 +52,11 @@ class HiddenItemsSettingsScreenVM : ViewModel(), KoinComponent {
}
}
fun getIcon(searchable: Searchable, size: Int): Flow<LauncherIcon> {
fun getIcon(searchable: PinnableSearchable, size: Int): Flow<LauncherIcon> {
return iconRepository.getIcon(searchable, size)
}
fun launch(context: Context, searchable: Searchable) {
fun launch(context: Context, searchable: PinnableSearchable) {
val bundle = Bundle()
if (isAtLeastApiLevel(31)) {
bundle.putInt("android.activity.splashScreenStyle", 1)
@ -68,23 +64,14 @@ class HiddenItemsSettingsScreenVM : ViewModel(), KoinComponent {
searchable.launch(context, bundle)
}
fun openAppInfo(context: Context, app: Application) {
fun openAppInfo(context: Context, app: LauncherApp) {
val launcherApps = context.getSystemService<LauncherApps>()!!
if (app is LauncherApp) {
launcherApps.startAppDetailsActivity(
ComponentName(app.`package`, app.activity),
app.getUser(),
null,
null
)
} else {
context.tryStartActivity(
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.parse("package:${app.`package`}")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
)
}
launcherApps.startAppDetailsActivity(
ComponentName(app.`package`, app.activity),
app.getUser(),
null,
null
)
}
}

View File

@ -1,23 +1,26 @@
package de.mm20.launcher2.ui.utils
import de.mm20.launcher2.customattrs.CustomAttributesRepository
import de.mm20.launcher2.customattrs.CustomLabel
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
fun <T : Searchable> Flow<List<T>>.withCustomLabels(
fun <T : PinnableSearchable> Flow<List<T>>.withCustomLabels(
customAttributesRepository: CustomAttributesRepository,
): Flow<List<T>> = channelFlow {
this@withCustomLabels.collectLatest { items ->
val customLabels = customAttributesRepository.getCustomLabels(items)
customLabels.collectLatest { labels ->
for (item in items) {
send(items.map { item ->
val customLabel = labels.find { it.key == item.key }
item.labelOverride = customLabel?.label
}
send(items)
if (customLabel != null) {
item.overrideLabel(customLabel.label) as T
} else {
item
}
})
}
}
}

View File

@ -45,7 +45,7 @@ dependencies {
implementation(project(":preferences"))
implementation(project(":currencies"))
implementation(project(":search"))
implementation(project(":base"))
implementation(project(":i18n"))
}

View File

@ -51,7 +51,6 @@ dependencies {
implementation(libs.coil.core)
implementation(project(":preferences"))
implementation(project(":search"))
implementation(project(":base"))
implementation(project(":ktx"))

View File

@ -2,26 +2,38 @@ package de.mm20.launcher2.search.data
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Bundle
import androidx.core.content.ContextCompat
import coil.imageLoader
import coil.request.ImageRequest
import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.websites.R
import java.util.concurrent.ExecutionException
class Website(
data class Website(
override val label: String,
val url: String,
val description: String,
val image: String,
val favicon: String,
val color: Int
) : Searchable() {
val color: Int,
override val labelOverride: String? = null,
) : PinnableSearchable {
override val domain: String = Domain
override val key = "$domain://$url"
override val preferDetailsOverLaunch: Boolean = false
override fun overrideLabel(label: String): Website {
return this.copy(labelOverride = label)
}
override val key = "web://$url"
override suspend fun loadIcon(
context: Context,
size: Int,
@ -68,10 +80,18 @@ class Website(
)
}
override fun getLaunchIntent(context: Context): Intent? {
private fun getLaunchIntent(): Intent {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
return intent
}
override fun launch(context: Context, options: Bundle?): Boolean {
return context.tryStartActivity(getLaunchIntent(), options)
}
companion object {
const val Domain = "web"
}
}

View File

@ -1,14 +1,15 @@
package de.mm20.launcher2.websites
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.data.Website
import org.json.JSONObject
class WebsiteSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String {
override fun serialize(searchable: PinnableSearchable): String {
searchable as Website
return jsonObjectOf(
"label" to searchable.label,
@ -25,7 +26,7 @@ class WebsiteSerializer : SearchableSerializer {
}
class WebsiteDeserializer: SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? {
override fun deserialize(serialized: String): PinnableSearchable? {
val json = JSONObject(serialized)
return Website(
label = json.getString("label"),

View File

@ -48,8 +48,8 @@ dependencies {
implementation(libs.koin.android)
implementation(project(":preferences"))
implementation(project(":search"))
implementation(project(":base"))
implementation(project(":ktx"))
implementation(project(":crashreporter"))
}

View File

@ -4,21 +4,35 @@ import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.ContextCompat
import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TintedIconLayer
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.wikipedia.R
class Wikipedia(
data class Wikipedia(
override val label: String,
val id: Long,
val text: String,
val image: String?,
val wikipediaUrl: String,
) : Searchable() {
override val key = "wikipedia://$wikipediaUrl:$id"
override val labelOverride: String? = null,
) : PinnableSearchable {
override val domain: String = Domain
override val preferDetailsOverLaunch: Boolean = false
override fun overrideLabel(label: String): Wikipedia {
return this.copy(labelOverride = label)
}
override val key = "$domain://$wikipediaUrl:$id"
override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
return StaticLauncherIcon(
@ -31,7 +45,7 @@ class Wikipedia(
)
}
override fun getLaunchIntent(context: Context): Intent? {
private fun getLaunchIntent(): Intent {
val intent = CustomTabsIntent
.Builder()
.setToolbarColor(Color.BLACK)
@ -42,4 +56,12 @@ class Wikipedia(
intent.intent.data = Uri.parse(uri)
return intent.intent
}
override fun launch(context: Context, options: Bundle?): Boolean {
return context.tryStartActivity(getLaunchIntent(), options)
}
companion object {
const val Domain = "wikipedia"
}
}

View File

@ -1,14 +1,15 @@
package de.mm20.launcher2.wikipedia
import android.content.Context
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.search.data.Searchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.data.Wikipedia
import org.json.JSONObject
class WikipediaSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String {
override fun serialize(searchable: PinnableSearchable): String {
searchable as Wikipedia
val json = JSONObject()
json.put("label", searchable.label)
@ -24,7 +25,7 @@ class WikipediaSerializer : SearchableSerializer {
}
class WikipediaDeserializer(val context: Context) : SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? {
override fun deserialize(serialized: String): PinnableSearchable? {
val json = JSONObject(serialized)
return Wikipedia(
label = json.getString("label"),