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(libs.commons.text)
implementation(project(":search"))
implementation(project(":base")) implementation(project(":base"))
implementation(project(":preferences")) implementation(project(":preferences"))
implementation(project(":ktx")) implementation(project(":ktx"))

View File

@ -12,17 +12,18 @@ import android.os.Process
import android.os.UserHandle import android.os.UserHandle
import android.util.Log import android.util.Log
import de.mm20.launcher2.ktx.normalize 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 de.mm20.launcher2.search.data.LauncherApp
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.apache.commons.text.similarity.FuzzyScore import org.apache.commons.text.similarity.FuzzyScore
import java.util.* import java.util.*
interface AppRepository { interface AppRepository: SearchableRepository<LauncherApp> {
fun search(query: String): Flow<List<Application>> fun getAllInstalledApps(): Flow<List<LauncherApp>>
fun getAllInstalledApps(): Flow<List<Application>>
fun getSuspendedPackages(): Flow<List<String>> fun getSuspendedPackages(): Flow<List<String>>
} }
@ -140,11 +141,11 @@ internal class AppRepositoryImpl(
return LauncherApp(context, launcherActivityInfo) 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 -> installedApps.collectLatest { apps ->
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
val appResults = mutableListOf<Application>() val appResults = mutableListOf<LauncherApp>()
if (query.isEmpty()) { if (query.isEmpty()) {
appResults.addAll(apps) appResults.addAll(apps)
} else { } else {
@ -158,12 +159,12 @@ internal class AppRepositoryImpl(
appResults.sort() appResults.sort()
send(appResults) send(appResults.toImmutableList())
} }
} }
} }
override fun getAllInstalledApps(): Flow<List<Application>> { override fun getAllInstalledApps(): Flow<List<LauncherApp>> {
return installedApps return installedApps
} }
@ -174,7 +175,7 @@ internal class AppRepositoryImpl(
fuzzyScore.fuzzyScore(normalizedLabel, query.normalize()) >= query.length * 1.5 fuzzyScore.fuzzyScore(normalizedLabel, query.normalize()) >= query.length * 1.5
} }
private fun getActivityByComponentName(componentName: ComponentName?): Application? { private fun getActivityByComponentName(componentName: ComponentName?): LauncherApp? {
componentName ?: return null componentName ?: return null
val intent = Intent().setComponent(componentName) val intent = Intent().setComponent(componentName)
val lai = launcherApps.resolveActivity(intent, Process.myUserHandle()) 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.Context
import android.content.Intent import android.content.Intent
import android.content.pm.LauncherApps import android.content.pm.LauncherApps
import android.content.pm.PackageManager
import android.os.Build
import android.os.Process import android.os.Process
import android.os.UserManager import android.os.UserManager
import androidx.annotation.RequiresApi
import androidx.core.content.getSystemService 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.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer import de.mm20.launcher2.search.SearchableSerializer
import org.json.JSONObject import org.json.JSONObject
import org.koin.core.component.KoinComponent
class LauncherAppSerializer : SearchableSerializer { class LauncherAppSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String { override fun serialize(searchable: PinnableSearchable): String {
searchable as LauncherApp searchable as LauncherApp
val json = JSONObject() val json = JSONObject()
json.put("package", searchable.`package`) json.put("package", searchable.`package`)
@ -31,7 +28,7 @@ class LauncherAppSerializer : SearchableSerializer {
} }
class LauncherAppDeserializer(val context: Context) : SearchableDeserializer { class LauncherAppDeserializer(val context: Context) : SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? { override fun deserialize(serialized: String): PinnableSearchable? {
val json = JSONObject(serialized) val json = JSONObject(serialized)
val launcherApps = context.getSystemService<LauncherApps>()!! val launcherApps = context.getSystemService<LauncherApps>()!!
val userManager = context.getSystemService<UserManager>()!! 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.Bundle
import android.os.Process import android.os.Process
import android.os.UserHandle import android.os.UserHandle
import androidx.core.content.ContextCompat
import androidx.core.content.getSystemService 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.icons.*
import de.mm20.launcher2.ktx.getSerialNumber import de.mm20.launcher2.ktx.getSerialNumber
import de.mm20.launcher2.ktx.isAtLeastApiLevel import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.search.PinnableSearchable
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
/** data class LauncherApp(
* An [Application] based on an [android.content.pm.LauncherActivityInfo] val launcherActivityInfo: LauncherActivityInfo,
*/ override val label: String,
class LauncherApp( val `package`: String,
context: Context, val activity: String,
val launcherActivityInfo: LauncherActivityInfo val flags: Int,
) : Application( val version: String?,
label = launcherActivityInfo.label.toString(), internal val userSerialNumber: Long,
`package` = launcherActivityInfo.applicationInfo.packageName, override val labelOverride: String? = null,
activity = launcherActivityInfo.name, ) : PinnableSearchable {
flags = launcherActivityInfo.applicationInfo.flags,
version = getPackageVersionName(context, launcherActivityInfo.applicationInfo.packageName), 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() 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 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? { fun getUser(): UserHandle? {
return launcherActivityInfo.user 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( override suspend fun loadIcon(
context: Context, context: Context,
size: Int, size: Int,
@ -107,7 +135,46 @@ class LauncherApp(
return true 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 { 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? { fun getPackageVersionName(context: Context, packageName: String): String? {
return try { return try {
@ -116,5 +183,12 @@ class LauncherApp(
null 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(libs.commons.text)
implementation(project(":applications")) implementation(project(":applications"))
implementation(project(":search"))
implementation(project(":permissions")) implementation(project(":permissions"))
implementation(project(":base")) implementation(project(":base"))
implementation(project(":preferences")) 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.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore 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.AppShortcut
import de.mm20.launcher2.search.data.LauncherApp import de.mm20.launcher2.search.data.LauncherApp
import de.mm20.launcher2.search.data.LauncherShortcut 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -27,7 +31,7 @@ import kotlinx.coroutines.withContext
import org.apache.commons.text.similarity.FuzzyScore import org.apache.commons.text.similarity.FuzzyScore
import java.util.* import java.util.*
interface AppShortcutRepository { interface AppShortcutRepository: SearchableRepository<AppShortcut> {
suspend fun getShortcutsForActivity( suspend fun getShortcutsForActivity(
launcherActivityInfo: LauncherActivityInfo, launcherActivityInfo: LauncherActivityInfo,
count: Int = 5 count: Int = 5
@ -35,8 +39,6 @@ interface AppShortcutRepository {
suspend fun getShortcutsConfigActivities(): List<LauncherApp> suspend fun getShortcutsConfigActivities(): List<LauncherApp>
fun search(query: String): Flow<List<AppShortcut>>
fun removePinnedShortcut(shortcut: LauncherShortcut) fun removePinnedShortcut(shortcut: LauncherShortcut)
} }
@ -72,32 +74,31 @@ internal class AppShortcutRepositoryImpl(
LauncherShortcut( LauncherShortcut(
context, context,
it, it,
launcherActivityInfo.label.toString()
) )
} ?: emptyList()) } ?: emptyList())
appShortcuts appShortcuts
} }
override fun search(query: String) = channelFlow<List<AppShortcut>> { override fun search(query: String) = channelFlow<ImmutableList<AppShortcut>> {
if (query.length < 3) { if (query.length < 3) {
send(emptyList()) send(persistentListOf())
return@channelFlow return@channelFlow
} }
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if (!permissionsManager.checkPermissionOnce(PermissionGroup.AppShortcuts)) { if (!permissionsManager.checkPermissionOnce(PermissionGroup.AppShortcuts)) {
send(emptyList()) send(persistentListOf())
return@withContext return@withContext
} }
dataStore.data.map { it.appShortcutSearch.enabled }.collectLatest { enabled -> dataStore.data.map { it.appShortcutSearch.enabled }.collectLatest { enabled ->
if (!enabled) { if (!enabled) {
send(emptyList()) send(persistentListOf())
return@collectLatest return@collectLatest
} }
shortcutChangeEmitter.collectLatest { shortcutChangeEmitter.collectLatest {
val launcherApps = val launcherApps =
context.getSystemService<LauncherApps>() ?: return@collectLatest send( context.getSystemService<LauncherApps>() ?: return@collectLatest send(
emptyList() persistentListOf()
) )
val shortcutQuery = LauncherApps.ShortcutQuery() val shortcutQuery = LauncherApps.ShortcutQuery()
@ -124,17 +125,11 @@ internal class AppShortcutRepositoryImpl(
send( send(
shortcuts.mapNotNull { shortcuts.mapNotNull {
val label = try {
pm.getApplicationInfo(it.`package`, 0).loadLabel(pm).toString()
} catch (e: PackageManager.NameNotFoundException) {
""
}
LauncherShortcut( LauncherShortcut(
context, context,
it, it
label
) )
} }.toImmutableList()
) )
} }
} }

View File

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

View File

@ -4,6 +4,7 @@ import android.content.ActivityNotFoundException
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.LauncherApps import android.content.pm.LauncherApps
import android.content.pm.PackageManager
import android.content.pm.ShortcutInfo import android.content.pm.ShortcutInfo
import android.graphics.drawable.AdaptiveIconDrawable import android.graphics.drawable.AdaptiveIconDrawable
import android.os.Bundle import android.os.Bundle
@ -14,36 +15,55 @@ import de.mm20.launcher2.appshortcuts.R
import de.mm20.launcher2.icons.* import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.getSerialNumber import de.mm20.launcher2.ktx.getSerialNumber
import de.mm20.launcher2.ktx.isAtLeastApiLevel import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.search.PinnableSearchable
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
/** /**
* Represents a modern (Android O+) launcher shortcut * Represents a modern (Android O+) launcher shortcut
*/ */
class LauncherShortcut( data class LauncherShortcut(
context: Context,
val launcherShortcut: ShortcutInfo, val launcherShortcut: ShortcutInfo,
appName: String override val appName: String?,
) : AppShortcut(appName) { 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 override val label: String
get() = launcherShortcut.shortLabel?.toString() ?: "" 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() val isMainProfile = launcherShortcut.userHandle == Process.myUserHandle()
override val key: String override val key: String
get() = if (isMainProfile) { get() = if (isMainProfile) {
"shortcut://${launcherShortcut.`package`}/${launcherShortcut.id}" "$domain://${launcherShortcut.`package`}/${launcherShortcut.id}"
} else { } 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 { override fun launch(context: Context, options: Bundle?): Boolean {
val launcherApps = context.getSystemService<LauncherApps>()!! val launcherApps = context.getSystemService<LauncherApps>()!!
try { try {
@ -123,10 +143,10 @@ class LauncherShortcut(
return LauncherShortcut( return LauncherShortcut(
context, context,
shortcutInfo, 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
import android.content.Intent.ShortcutIconResource import android.content.Intent.ShortcutIconResource
import android.graphics.drawable.AdaptiveIconDrawable import android.graphics.drawable.AdaptiveIconDrawable
import android.os.Bundle
import android.util.Log import android.util.Log
import de.mm20.launcher2.icons.* import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.getDrawableOrNull import de.mm20.launcher2.ktx.getDrawableOrNull
import de.mm20.launcher2.ktx.isAtLeastApiLevel 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, val intent: Intent,
override val label: String, override val label: String,
appName: String?, override val appName: String?,
val iconResource: ShortcutIconResource?, val iconResource: ShortcutIconResource?,
) : AppShortcut(appName) { override val labelOverride: String? = null,
override val key: String ) : AppShortcut {
get() = "legacyshortcut://${intent.toUri(0)}"
override fun getLaunchIntent(context: Context): Intent { override val domain = Domain
return intent 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? val packageName: String?
@ -67,6 +77,9 @@ class LegacyShortcut(
} }
companion object { companion object {
const val Domain = "legacyshortcut"
fun fromPinRequestIntent(context: Context, data: Intent): LegacyShortcut? { fun fromPinRequestIntent(context: Context, data: Intent): LegacyShortcut? {
val intent: Intent? = data.extras?.getParcelable(Intent.EXTRA_SHORTCUT_INTENT) val intent: Intent? = data.extras?.getParcelable(Intent.EXTRA_SHORTCUT_INTENT)
val name: String? = data.extras?.getString(Intent.EXTRA_SHORTCUT_NAME) val name: String? = data.extras?.getString(Intent.EXTRA_SHORTCUT_NAME)

View File

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

View File

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

View File

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

View File

@ -3,7 +3,7 @@ package de.mm20.launcher2.badges
import android.content.Context import android.content.Context
import de.mm20.launcher2.badges.providers.* import de.mm20.launcher2.badges.providers.*
import de.mm20.launcher2.preferences.LauncherDataStore 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.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import org.koin.core.component.KoinComponent 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.graphics.BadgeDrawable
import de.mm20.launcher2.search.data.LauncherShortcut import de.mm20.launcher2.search.data.LauncherShortcut
import de.mm20.launcher2.search.data.LegacyShortcut 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.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow

View File

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

View File

@ -2,7 +2,7 @@ package de.mm20.launcher2.badges.providers
import de.mm20.launcher2.badges.Badge import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.search.data.File 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
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 android.app.Notification
import de.mm20.launcher2.badges.Badge import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.notifications.NotificationRepository 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.LauncherApp
import de.mm20.launcher2.search.data.Searchable
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@ -17,7 +16,7 @@ class NotificationBadgeProvider : BadgeProvider, KoinComponent {
private val notificationRepository: NotificationRepository by inject() private val notificationRepository: NotificationRepository by inject()
override fun getBadge(searchable: Searchable): Flow<Badge?> = channelFlow { override fun getBadge(searchable: Searchable): Flow<Badge?> = channelFlow {
if (searchable is Application) { if (searchable is LauncherApp) {
val packageName = searchable.`package` val packageName = searchable.`package`
notificationRepository.notifications.map { notificationRepository.notifications.map {
it.filter { it.packageName == packageName } 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.applications.AppRepository
import de.mm20.launcher2.badges.Badge import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.R import de.mm20.launcher2.badges.R
import de.mm20.launcher2.search.data.Application import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.search.data.LauncherApp
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@ -15,7 +15,7 @@ class SuspendedAppsBadgeProvider : BadgeProvider, KoinComponent {
private val appRepository: AppRepository by inject() private val appRepository: AppRepository by inject()
override fun getBadge(searchable: Searchable): Flow<Badge?> = channelFlow { override fun getBadge(searchable: Searchable): Flow<Badge?> = channelFlow {
if (searchable is Application) { if (searchable is LauncherApp) {
val packageName = searchable.`package` val packageName = searchable.`package`
appRepository.getSuspendedPackages().collectLatest { appRepository.getSuspendedPackages().collectLatest {
if (it.contains(packageName)) { 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.Badge
import de.mm20.launcher2.badges.R 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.LauncherApp
import de.mm20.launcher2.search.data.LauncherShortcut 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
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 package de.mm20.launcher2.search
import de.mm20.launcher2.search.data.Searchable
interface SearchableSerializer { interface SearchableSerializer {
fun serialize(searchable: Searchable): String? fun serialize(searchable: PinnableSearchable): String?
val typePrefix: String val typePrefix: String
} }
class NullSerializer : SearchableSerializer { class NullSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String? { override fun serialize(searchable: PinnableSearchable): String? {
return null return null
} }

View File

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

View File

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

View File

@ -42,7 +42,6 @@ dependencies {
implementation(libs.koin.android) implementation(libs.koin.android)
api(project(":search"))
implementation(project(":preferences")) implementation(project(":preferences"))
implementation(project(":ktx")) implementation(project(":ktx"))
implementation(project(":base")) 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.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore 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.CalendarEvent
import de.mm20.launcher2.search.data.UserCalendar 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.Dispatchers
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -16,9 +20,7 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import java.util.* import java.util.*
interface CalendarRepository { interface CalendarRepository: SearchableRepository<CalendarEvent> {
fun search(query: String): Flow<List<CalendarEvent>>
fun getUpcomingEvents(): Flow<List<CalendarEvent>> fun getUpcomingEvents(): Flow<List<CalendarEvent>>
suspend fun getCalendars(): List<UserCalendar> suspend fun getCalendars(): List<UserCalendar>
@ -31,10 +33,10 @@ internal class CalendarRepositoryImpl(
private val dataStore: LauncherDataStore by inject() private val dataStore: LauncherDataStore by inject()
private val permissionsManager: PermissionsManager 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) { if (query.isBlank() || query.length < 3) {
return flow { return flow {
emit(emptyList()) emit(persistentListOf())
} }
} }
@ -49,9 +51,9 @@ internal class CalendarRepositoryImpl(
query, query,
intervalStart = now, intervalStart = now,
intervalEnd = now + 14 * 24 * 60 * 60 * 1000L, intervalEnd = now + 14 * 24 * 60 * 60 * 1000L,
) ).toImmutableList()
} else { } else {
emptyList() persistentListOf()
} }
} }

View File

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

View File

@ -3,13 +3,17 @@ package de.mm20.launcher2.search.data
import android.content.ContentUris import android.content.ContentUris
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle
import android.provider.CalendarContract import android.provider.CalendarContract
import de.mm20.launcher2.icons.ColorLayer import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TextLayer 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 import java.text.SimpleDateFormat
class CalendarEvent( data class CalendarEvent(
override val label: String, override val label: String,
val id: Long, val id: Long,
val color: Int, val color: Int,
@ -19,12 +23,20 @@ class CalendarEvent(
val location: String, val location: String,
val attendees: List<String>, val attendees: List<String>,
val description: String, val description: String,
val calendar: Long val calendar: Long,
) : Searchable() { override val labelOverride: String? = null,
) : PinnableSearchable {
override val domain: String = Domain
override val key: String 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 { override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
val df = SimpleDateFormat("dd") 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) val uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, id)
return Intent(Intent.ACTION_VIEW).setData(uri).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 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( data class UserCalendar(

View File

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

View File

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

View File

@ -6,14 +6,15 @@ import android.content.pm.PackageManager
import android.provider.ContactsContract import android.provider.ContactsContract
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import de.mm20.launcher2.ktx.jsonObjectOf import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.search.data.Contact import de.mm20.launcher2.search.data.Contact
import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.search.Searchable
import org.json.JSONObject import org.json.JSONObject
class ContactSerializer : SearchableSerializer { class ContactSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String { override fun serialize(searchable: PinnableSearchable): String {
searchable as Contact searchable as Contact
return jsonObjectOf( return jsonObjectOf(
"id" to searchable.id "id" to searchable.id
@ -25,7 +26,7 @@ class ContactSerializer : SearchableSerializer {
} }
class ContactDeserializer(val context: Context) : SearchableDeserializer { class ContactDeserializer(val context: Context) : SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? { override fun deserialize(serialized: String): PinnableSearchable? {
if (ContextCompat.checkSelfPermission( if (ContextCompat.checkSelfPermission(
context, context,
Manifest.permission.READ_CONTACTS Manifest.permission.READ_CONTACTS

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,20 +1,40 @@
package de.mm20.launcher2.search.data package de.mm20.launcher2.search.data
import android.content.Context import android.content.Context
import android.os.Bundle
import de.mm20.launcher2.icons.ColorLayer import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TextLayer 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, val tag: String,
): Searchable() { override val labelOverride: String? = null
override val key: String = "tag://$tag" ): PinnableSearchable {
override val domain: String = Domain
override val key: String = "$domain://$tag"
override val label: String = 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 { override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
return StaticLauncherIcon( return StaticLauncherIcon(
foregroundLayer = TextLayer("#"), foregroundLayer = TextLayer("#"),
backgroundLayer = ColorLayer() backgroundLayer = ColorLayer()
) )
} }
companion object {
const val Domain = "tag"
}
} }

View File

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

View File

@ -1,12 +1,13 @@
package de.mm20.launcher2.files package de.mm20.launcher2.files
import android.content.Context import android.content.Context
import android.provider.DocumentsContract
import android.provider.MediaStore import android.provider.MediaStore
import androidx.core.database.getStringOrNull import androidx.core.database.getStringOrNull
import de.mm20.launcher2.ktx.jsonObjectOf import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager 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.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer import de.mm20.launcher2.search.SearchableSerializer
import de.mm20.launcher2.search.data.* import de.mm20.launcher2.search.data.*
@ -15,7 +16,7 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
class LocalFileSerializer : SearchableSerializer { class LocalFileSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String { override fun serialize(searchable: PinnableSearchable): String {
searchable as LocalFile searchable as LocalFile
return jsonObjectOf( return jsonObjectOf(
"id" to searchable.id "id" to searchable.id
@ -29,7 +30,7 @@ class LocalFileSerializer : SearchableSerializer {
class LocalFileDeserializer( class LocalFileDeserializer(
val context: Context val context: Context
) : SearchableDeserializer, KoinComponent { ) : SearchableDeserializer, KoinComponent {
override fun deserialize(serialized: String): Searchable? { override fun deserialize(serialized: String): PinnableSearchable? {
val permissionsManager: PermissionsManager = get() val permissionsManager: PermissionsManager = get()
if (!permissionsManager.checkPermissionOnce( if (!permissionsManager.checkPermissionOnce(
PermissionGroup.ExternalStorage PermissionGroup.ExternalStorage
@ -74,7 +75,7 @@ class LocalFileDeserializer(
} }
class GDriveFileSerializer : SearchableSerializer { class GDriveFileSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String { override fun serialize(searchable: PinnableSearchable): String {
searchable as GDriveFile searchable as GDriveFile
return jsonObjectOf( return jsonObjectOf(
"id" to searchable.fileId, "id" to searchable.fileId,
@ -103,7 +104,7 @@ class GDriveFileSerializer : SearchableSerializer {
} }
class GDriveFileDeserializer : SearchableDeserializer { class GDriveFileDeserializer : SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? { override fun deserialize(serialized: String): PinnableSearchable {
val json = JSONObject(serialized) val json = JSONObject(serialized)
val id = json.getString("id") val id = json.getString("id")
val label = json.getString("label") val label = json.getString("label")
@ -134,7 +135,7 @@ class GDriveFileDeserializer : SearchableDeserializer {
} }
class OneDriveFileSerializer : SearchableSerializer { class OneDriveFileSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String { override fun serialize(searchable: PinnableSearchable): String {
searchable as OneDriveFile searchable as OneDriveFile
return jsonObjectOf( return jsonObjectOf(
"id" to searchable.fileId, "id" to searchable.fileId,
@ -161,7 +162,7 @@ class OneDriveFileSerializer : SearchableSerializer {
} }
class OneDriveFileDeserializer : SearchableDeserializer { class OneDriveFileDeserializer : SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? { override fun deserialize(serialized: String): PinnableSearchable {
val json = JSONObject(serialized) val json = JSONObject(serialized)
val fileId = json.getString("id") val fileId = json.getString("id")
val label = json.getString("label") val label = json.getString("label")
@ -189,10 +190,10 @@ class OneDriveFileDeserializer : SearchableDeserializer {
} }
class NextcloudFileSerializer : SearchableSerializer { class NextcloudFileSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String { override fun serialize(searchable: PinnableSearchable): String {
searchable as NextcloudFile searchable as NextcloudFile
return jsonObjectOf( return jsonObjectOf(
"id" to searchable.id, "id" to searchable.fileId,
"label" to searchable.label, "label" to searchable.label,
"path" to searchable.path, "path" to searchable.path,
"mimeType" to searchable.mimeType, "mimeType" to searchable.mimeType,
@ -216,7 +217,7 @@ class NextcloudFileSerializer : SearchableSerializer {
} }
class NextcloudFileDeserializer : SearchableDeserializer { class NextcloudFileDeserializer : SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? { override fun deserialize(serialized: String): PinnableSearchable {
val json = JSONObject(serialized) val json = JSONObject(serialized)
val id = json.getLong("id") val id = json.getLong("id")
val label = json.getString("label") val label = json.getString("label")
@ -242,10 +243,10 @@ class NextcloudFileDeserializer : SearchableDeserializer {
} }
class OwncloudFileSerializer : SearchableSerializer { class OwncloudFileSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String { override fun serialize(searchable: PinnableSearchable): String {
searchable as OwncloudFile searchable as OwncloudFile
return jsonObjectOf( return jsonObjectOf(
"id" to searchable.id, "id" to searchable.fileId,
"label" to searchable.label, "label" to searchable.label,
"path" to searchable.path, "path" to searchable.path,
"mimeType" to searchable.mimeType, "mimeType" to searchable.mimeType,
@ -269,7 +270,7 @@ class OwncloudFileSerializer : SearchableSerializer {
} }
class OwncloudFileDeserializer : SearchableDeserializer { class OwncloudFileDeserializer : SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? { override fun deserialize(serialized: String): PinnableSearchable {
val json = JSONObject(serialized) val json = JSONObject(serialized)
val id = json.getLong("id") val id = json.getLong("id")
val label = json.getString("label") 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.owncloud.OwncloudClient
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.search.SearchableRepository
import de.mm20.launcher2.search.data.File 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.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
interface FileRepository { interface FileRepository: SearchableRepository<File> {
fun search(query: String): Flow<List<File>>
fun deleteFile(file: 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()) { if (query.isBlank()) {
send(emptyList()) send(persistentListOf())
return@channelFlow return@channelFlow
} }
providers.collectLatest { providers -> providers.collectLatest { providers ->
if (providers.isEmpty()) { if (providers.isEmpty()) {
send(emptyList()) send(persistentListOf())
return@collectLatest return@collectLatest
} }
val results = mutableListOf<File>() val results = mutableListOf<File>()
for (provider in providers) { for (provider in providers) {
results.addAll(provider.search(query)) results.addAll(provider.search(query))
send(results.toList()) send(results.toImmutableList())
} }
} }
} }

View File

@ -1,25 +1,29 @@
package de.mm20.launcher2.search.data package de.mm20.launcher2.search.data
import android.content.Context import android.content.Context
import android.graphics.Color
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import de.mm20.launcher2.files.R import de.mm20.launcher2.files.R
import de.mm20.launcher2.icons.ColorLayer import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TintedIconLayer import de.mm20.launcher2.icons.TintedIconLayer
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.Searchable
import java.util.* import java.util.*
abstract class File( interface File : PinnableSearchable {
val id: Long, val path: String
val path: String, val mimeType: String
val mimeType: String, val size: Long
val size: Long, val isDirectory: Boolean
val isDirectory: Boolean,
val metaData: List<Pair<Int, String>> 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 { override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
val (resId, bgColor) = when { val (resId, bgColor) = when {
@ -124,7 +128,8 @@ abstract class File(
return context.getString(resource) return context.getString(resource)
} }
open val isDeletable: Boolean = false val isDeletable: Boolean
open suspend fun delete(context: Context) {} 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.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle
import de.mm20.launcher2.files.R import de.mm20.launcher2.files.R
import de.mm20.launcher2.ktx.tryStartActivity
class GDriveFile( data class GDriveFile(
val fileId: String, val fileId: String,
override val label: String, override val label: String,
path: String, override val path: String,
mimeType: String, override val mimeType: String,
size: Long, override val size: Long,
isDirectory: Boolean, override val isDirectory: Boolean,
metaData: List<Pair<Int, String>>, override val metaData: List<Pair<Int, String>>,
val directoryColor: String?, val directoryColor: String?,
val viewUri: String val viewUri: String,
) : File(0, path, mimeType, size, isDirectory, metaData) { 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 isStoredInCloud = true
override val providerIconRes = R.drawable.ic_badge_gdrive override val providerIconRes = R.drawable.ic_badge_gdrive
override fun getLaunchIntent(context: Context): Intent? { private fun getLaunchIntent(): Intent {
return Intent(Intent.ACTION_VIEW).apply { return Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(viewUri) data = Uri.parse(viewUri)
flags = Intent.FLAG_ACTIVITY_NEW_TASK 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.MediaMetadataRetriever
import android.media.ThumbnailUtils import android.media.ThumbnailUtils
import android.net.Uri import android.net.Uri
import android.os.Bundle
import android.provider.MediaStore import android.provider.MediaStore
import android.text.format.DateUtils import android.text.format.DateUtils
import android.util.Size import android.util.Size
@ -17,25 +18,34 @@ import androidx.exifinterface.media.ExifInterface
import de.mm20.launcher2.files.R import de.mm20.launcher2.files.R
import de.mm20.launcher2.icons.* import de.mm20.launcher2.icons.*
import de.mm20.launcher2.ktx.formatToString import de.mm20.launcher2.ktx.formatToString
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.media.ThumbnailUtilsCompat import de.mm20.launcher2.media.ThumbnailUtilsCompat
import de.mm20.launcher2.search.PinnableSearchable
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import java.io.IOException import java.io.IOException
import java.io.File as JavaIOFile import java.io.File as JavaIOFile
open class LocalFile( data class LocalFile(
id: Long, val id: Long,
path: String, override val path: String,
mimeType: String, override val mimeType: String,
size: Long, override val size: Long,
isDirectory: Boolean, override val isDirectory: Boolean,
metaData: List<Pair<Int, String>> override val metaData: List<Pair<Int, String>>,
) : File(id, path, mimeType, size, isDirectory, metaData) { override val labelOverride: String? = null
) : File {
override val label = path.substringAfterLast('/') 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 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) { val uri = if (isDirectory) {
Uri.parse(path) Uri.parse(path)
} else { } else {
@ -162,6 +172,10 @@ open class LocalFile(
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION) .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION)
} }
override fun launch(context: Context, options: Bundle?): Boolean {
return context.tryStartActivity(getLaunchIntent(context), options)
}
override val isDeletable: Boolean override val isDeletable: Boolean
get() { get() {
val file = java.io.File(path) val file = java.io.File(path)
@ -186,6 +200,9 @@ open class LocalFile(
companion object { companion object {
const val Domain = "file"
internal fun getMimetypeByFileExtension(extension: String): String { internal fun getMimetypeByFileExtension(extension: String): String {
return when (extension) { return when (extension) {
"apk" -> "application/vnd.android.package-archive" "apk" -> "application/vnd.android.package-archive"

View File

@ -4,34 +4,50 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri import android.net.Uri
import android.os.Bundle
import de.mm20.launcher2.files.R import de.mm20.launcher2.files.R
import de.mm20.launcher2.ktx.tryStartActivity
class NextcloudFile( data class NextcloudFile(
fileId: Long, val fileId: Long,
override val label: String, override val label: String,
path: String, override val path: String,
mimeType: String, override val mimeType: String,
size: Long, override val size: Long,
isDirectory: Boolean, override val isDirectory: Boolean,
val server: String, val server: String,
metaData: List<Pair<Int, String>> override val metaData: List<Pair<Int, String>>,
) : File(fileId, path, mimeType, size, isDirectory, metaData) { override val labelOverride: String? = null,
override val key: String = "nextcloud://$server/$fileId" ) : 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 override val isStoredInCloud: Boolean
get() = true get() = true
override val providerIconRes = R.drawable.ic_badge_nextcloud 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 { return Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse("$server/f/$id") data = Uri.parse("$server/f/$fileId")
flags = Intent.FLAG_ACTIVITY_NEW_TASK flags = Intent.FLAG_ACTIVITY_NEW_TASK
`package` = getNextcloudAppPackage(context) `package` = getNextcloudAppPackage(context)
} }
} }
override fun launch(context: Context, options: Bundle?): Boolean {
return context.tryStartActivity(getLaunchIntent(context), options)
}
companion object { companion object {
const val Domain = "nextcloud"
private fun getNextcloudAppPackage(context: Context): String? { private fun getNextcloudAppPackage(context: Context): String? {
val candidates = listOf("com.nextcloud.client", "com.nextcloud.android.beta") 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.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle
import de.mm20.launcher2.files.R import de.mm20.launcher2.files.R
import de.mm20.launcher2.ktx.tryStartActivity
class OneDriveFile( data class OneDriveFile(
val fileId: String, val fileId: String,
override val label: String, override val label: String,
path: String, override val path: String,
mimeType: String, override val mimeType: String,
size: Long, override val size: Long,
isDirectory: Boolean, override val isDirectory: Boolean,
metaData: List<Pair<Int, String>>, override val metaData: List<Pair<Int, String>>,
val webUrl: String val webUrl: String,
) : File(0, path, mimeType, size, isDirectory, metaData) { 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 providerIconRes = R.drawable.ic_badge_onedrive
override val isStoredInCloud = true override val isStoredInCloud = true
override fun getLaunchIntent(context: Context): Intent? { private fun getLaunchIntent(): Intent {
return Intent(Intent.ACTION_VIEW).apply { return Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(webUrl) data = Uri.parse(webUrl)
flags = Intent.FLAG_ACTIVITY_NEW_TASK 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.Context
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle
import de.mm20.launcher2.files.R import de.mm20.launcher2.files.R
import de.mm20.launcher2.ktx.tryStartActivity
class OwncloudFile( data class OwncloudFile(
fileId: Long, val fileId: Long,
override val label: String, override val label: String,
path: String, override val path: String,
mimeType: String, override val mimeType: String,
size: Long, override val size: Long,
isDirectory: Boolean, override val isDirectory: Boolean,
val server: String, val server: String,
metaData: List<Pair<Int, String>> override val metaData: List<Pair<Int, String>>,
) : File(fileId, path, mimeType, size, isDirectory, metaData) { override val labelOverride: String? = null,
override val key: String = "owncloud://$server/$fileId" ) : 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 override val isStoredInCloud: Boolean
get() = true get() = true
override val providerIconRes = R.drawable.ic_badge_owncloud override val providerIconRes = R.drawable.ic_badge_owncloud
override fun getLaunchIntent(context: Context): Intent? { private fun getLaunchIntent(): Intent {
return Intent(Intent.ACTION_VIEW).apply { return Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse("$server/f/$id") data = Uri.parse("$server/f/$fileId")
flags = Intent.FLAG_ACTIVITY_NEW_TASK 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(":preferences"))
implementation(project(":ktx")) implementation(project(":ktx"))
implementation(project(":base")) implementation(project(":base"))
implementation(project(":search"))
implementation(project(":applications")) implementation(project(":applications"))
implementation(project(":crashreporter")) implementation(project(":crashreporter"))
api(project(":customattrs")) 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.LegacyToAdaptiveTransformation
import de.mm20.launcher2.icons.transformations.transform import de.mm20.launcher2.icons.transformations.transform
import de.mm20.launcher2.preferences.LauncherDataStore 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.LauncherApp
import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.search.Searchable
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job 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 -> iconProviders.collectLatest { providers ->
transformations.collectLatest { transformations -> transformations.collectLatest { transformations ->
customAttributesRepository.getCustomIcon(searchable).collectLatest { customIcon -> customAttributesRepository.getCustomIcon(searchable).collectLatest { customIcon ->
@ -189,7 +190,7 @@ class IconRepository(
} }
suspend fun getCustomIconSuggestions( suspend fun getCustomIconSuggestions(
searchable: Searchable, searchable: PinnableSearchable,
size: Int size: Int
): List<CustomIconWithPreview> { ): List<CustomIconWithPreview> {
val suggestions = mutableListOf<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) val icon = iconProviders.first().getFirstIcon(searchable, size)
?.transform(transformations.first()) ?: return null ?.transform(transformations.first()) ?: return null
return CustomIconWithPreview( return CustomIconWithPreview(
@ -338,7 +339,7 @@ class IconRepository(
return iconPackIcons + themedIcons return iconPackIcons + themedIcons
} }
fun setCustomIcon(searchable: Searchable, icon: CustomIcon?) { fun setCustomIcon(searchable: PinnableSearchable, icon: CustomIcon?) {
customAttributesRepository.setCustomIcon(searchable, icon) customAttributesRepository.setCustomIcon(searchable, icon)
} }

View File

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

View File

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

View File

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

View File

@ -3,8 +3,9 @@ package de.mm20.launcher2.icons.providers
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import de.mm20.launcher2.icons.* import de.mm20.launcher2.icons.*
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.data.LauncherApp 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.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -13,7 +14,7 @@ class IconPackIconProvider(
private val iconPack: String, private val iconPack: String,
private val iconPackManager: IconPackManager, private val iconPackManager: IconPackManager,
): IconProvider { ): 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 if (searchable !is LauncherApp) return null
val component = ComponentName(searchable.`package`, searchable.activity) val component = ComponentName(searchable.`package`, searchable.activity)

View File

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

View File

@ -2,10 +2,11 @@ package de.mm20.launcher2.icons.providers
import android.content.Context import android.content.Context
import de.mm20.launcher2.icons.LauncherIcon 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 { 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) return searchable.getPlaceholderIcon(context)
} }
} }

View File

@ -2,13 +2,14 @@ package de.mm20.launcher2.icons.providers
import android.content.Context import android.content.Context
import de.mm20.launcher2.icons.LauncherIcon 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( class SystemIconProvider(
private val context: Context, private val context: Context,
private val themedIcons: Boolean, private val themedIcons: Boolean,
) : IconProvider { ) : 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) return searchable.loadIcon(context, size, themedIcons)
} }
} }

View File

@ -1,25 +1,16 @@
package de.mm20.launcher2.icons.providers 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.icons.*
import de.mm20.launcher2.ktx.obtainTypedArrayOrNull import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.data.Application import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.search.data.LauncherApp
internal class ThemedIconProvider( internal class ThemedIconProvider(
private val iconPackManager: IconPackManager, private val iconPackManager: IconPackManager,
) : IconProvider { ) : IconProvider {
override suspend fun getIcon(searchable: Searchable, size: Int): LauncherIcon? { override suspend fun getIcon(searchable: PinnableSearchable, size: Int): LauncherIcon? {
if (searchable !is Application) return null if (searchable !is LauncherApp) return null
return iconPackManager.getThemedIcon(searchable.`package`) return iconPackManager.getThemedIcon(searchable.`package`)
} }
} }

View File

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

View File

@ -43,6 +43,5 @@ dependencies {
implementation(project(":ktx")) implementation(project(":ktx"))
implementation(project(":base")) implementation(project(":base"))
implementation(project(":search"))
implementation(project(":crashreporter")) 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.customattrs.CustomAttributesRepository
import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.preferences.LauncherDataStore 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.search.data.Tag
import de.mm20.launcher2.ui.utils.withCustomLabels import de.mm20.launcher2.ui.utils.withCustomLabels
import de.mm20.launcher2.widgets.WidgetRepository import de.mm20.launcher2.widgets.WidgetRepository
@ -32,7 +33,7 @@ open class FavoritesVM : ViewModel(), KoinComponent {
it.filterIsInstance<Tag>() it.filterIsInstance<Tag>()
} }
val favorites: Flow<List<Searchable>> = selectedTag.flatMapLatest { tag -> val favorites: Flow<List<PinnableSearchable>> = selectedTag.flatMapLatest { tag ->
if (tag == null) { if (tag == null) {
val columns = dataStore.data.map { it.grid.columnCount } val columns = dataStore.data.map { it.grid.columnCount }
val excludeCalendar = widgetRepository.isCalendarWidgetEnabled() val excludeCalendar = widgetRepository.isCalendarWidgetEnabled()

View File

@ -71,7 +71,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.PathEffect import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.drawOutline import androidx.compose.ui.graphics.drawOutline
@ -90,7 +89,8 @@ import androidx.compose.ui.unit.toSize
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.badges.Badge import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.icons.LauncherIcon 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.R
import de.mm20.launcher2.ui.component.BottomSheetDialog import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.MissingPermissionBanner import de.mm20.launcher2.ui.component.MissingPermissionBanner
@ -739,7 +739,7 @@ fun ShortcutPicker(viewModel: EditFavoritesSheetVM) {
} }
sealed interface FavoritesSheetGridItem { sealed interface FavoritesSheetGridItem {
class Favorite(val item: Searchable) : FavoritesSheetGridItem class Favorite(val item: PinnableSearchable) : FavoritesSheetGridItem
class Divider(val section: FavoritesSheetSection) : FavoritesSheetGridItem class Divider(val section: FavoritesSheetSection) : FavoritesSheetGridItem
class Spacer(val span: Int = 1) : FavoritesSheetGridItem class Spacer(val span: Int = 1) : FavoritesSheetGridItem
object EmptySection : 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.IconRepository
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.normalize import de.mm20.launcher2.ktx.normalize
import de.mm20.launcher2.ktx.romanize
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore 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.AppShortcut
import de.mm20.launcher2.search.data.Searchable import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.data.Tag import de.mm20.launcher2.search.data.Tag
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -48,9 +48,9 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent {
val createShortcutTarget = MutableLiveData<FavoritesSheetSection?>(null) val createShortcutTarget = MutableLiveData<FavoritesSheetSection?>(null)
private var manuallySorted: MutableList<Searchable> = mutableListOf() private var manuallySorted: MutableList<PinnableSearchable> = mutableListOf()
private var automaticallySorted: MutableList<Searchable> = mutableListOf() private var automaticallySorted: MutableList<PinnableSearchable> = mutableListOf()
private var frequentlyUsed: MutableList<Searchable> = mutableListOf() private var frequentlyUsed: MutableList<PinnableSearchable> = mutableListOf()
val pinnedTags = MutableLiveData<List<Tag>>(emptyList()) val pinnedTags = MutableLiveData<List<Tag>>(emptyList())
val availableTags = 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) 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.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel 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.R
import de.mm20.launcher2.ui.component.BottomSheetDialog import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
@Composable @Composable
fun HiddenItemsSheet( fun HiddenItemsSheet(
items: List<Searchable>, items: List<PinnableSearchable>,
onDismiss: () -> Unit onDismiss: () -> Unit
) { ) {
val viewModel: HiddenItemsSheetVM = viewModel() 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.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel 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.R
import de.mm20.launcher2.ui.component.LauncherCard import de.mm20.launcher2.ui.component.LauncherCard
import de.mm20.launcher2.ui.component.MissingPermissionBanner import de.mm20.launcher2.ui.component.MissingPermissionBanner
@ -381,7 +382,7 @@ fun SearchColumn(
} }
fun LazyListScope.GridResults( fun LazyListScope.GridResults(
items: ImmutableList<Searchable>, items: ImmutableList<PinnableSearchable>,
columns: Int, columns: Int,
reverse: Boolean, reverse: Boolean,
showLabels: Boolean, showLabels: Boolean,
@ -446,7 +447,7 @@ fun LazyListScope.GridResults(
@Composable @Composable
fun GridRow( fun GridRow(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
items: ImmutableList<Searchable>, items: ImmutableList<PinnableSearchable>,
columns: Int, columns: Int,
showLabels: Boolean, showLabels: Boolean,
) { ) {
@ -470,7 +471,7 @@ fun GridRow(
} }
fun LazyListScope.ListResults( fun LazyListScope.ListResults(
items: ImmutableList<Searchable>, items: ImmutableList<PinnableSearchable>,
reverse: Boolean, reverse: Boolean,
key: String, key: String,
before: (@Composable () -> Unit)? = null, before: (@Composable () -> Unit)? = null,
@ -523,7 +524,7 @@ fun LazyListScope.ListResults(
@Composable @Composable
fun ListRow( fun ListRow(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
item: Searchable, item: PinnableSearchable,
) { ) {
Box( Box(
modifier = modifier.padding( 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.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.preferences.LauncherDataStore 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.WebsearchRepository
import de.mm20.launcher2.search.data.* import de.mm20.launcher2.search.data.*
import de.mm20.launcher2.ui.utils.withCustomLabels import de.mm20.launcher2.ui.utils.withCustomLabels
import de.mm20.launcher2.unitconverter.UnitConverterRepository import de.mm20.launcher2.unitconverter.UnitConverterRepository
import de.mm20.launcher2.websites.WebsiteRepository import de.mm20.launcher2.websites.WebsiteRepository
import de.mm20.launcher2.widgets.WidgetRepository
import de.mm20.launcher2.wikipedia.WikipediaRepository import de.mm20.launcher2.wikipedia.WikipediaRepository
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
@ -52,8 +53,8 @@ class SearchVM : ViewModel(), KoinComponent {
val showLabels = dataStore.data.map { it.grid.showLabels }.asLiveData() val showLabels = dataStore.data.map { it.grid.showLabels }.asLiveData()
val appResults = MutableLiveData<List<Application>>(emptyList()) val appResults = MutableLiveData<List<LauncherApp>>(emptyList())
val workAppResults = MutableLiveData<List<Application>>(emptyList()) val workAppResults = MutableLiveData<List<LauncherApp>>(emptyList())
val appShortcutResults = MutableLiveData<List<AppShortcut>>(emptyList()) val appShortcutResults = MutableLiveData<List<AppShortcut>>(emptyList())
val fileResults = MutableLiveData<List<File>>(emptyList()) val fileResults = MutableLiveData<List<File>>(emptyList())
val contactResults = MutableLiveData<List<Contact>>(emptyList()) val contactResults = MutableLiveData<List<Contact>>(emptyList())
@ -64,7 +65,7 @@ class SearchVM : ViewModel(), KoinComponent {
val unitConverterResult = MutableLiveData<UnitConverter?>(null) val unitConverterResult = MutableLiveData<UnitConverter?>(null)
val websearchResults = MutableLiveData<List<Websearch>>(emptyList()) 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 favoritesEnabled = dataStore.data.map { it.favorites.enabled }
val hideFavorites = MutableLiveData(false) val hideFavorites = MutableLiveData(false)
@ -95,7 +96,7 @@ class SearchVM : ViewModel(), KoinComponent {
val customAttrResults = customAttributesRepository.search(query) val customAttrResults = customAttributesRepository.search(query)
.combine(dataStore.data) { items, settings -> .combine(dataStore.data) { items, settings ->
items.filter { items.filter {
it is Application it is LauncherApp
|| it is Contact && settings.contactsSearch.enabled || it is Contact && settings.contactsSearch.enabled
|| it is CalendarEvent && settings.calendarSearch.enabled || it is CalendarEvent && settings.calendarSearch.enabled
|| it is AppShortcut && settings.appShortcutSearch.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( private inline fun <reified T : PinnableSearchable> Flow<List<T>>.withCustomAttributeResults(
customAttributeResults: Flow<List<Searchable>> customAttributeResults: Flow<List<PinnableSearchable>>
): Flow<List<T>> { ): Flow<List<T>> {
return this.combine(customAttributeResults) { items, items2 -> return this.combine(customAttributeResults) { items, items2 ->
(items + items2.filterIsInstance<T>()).distinctBy { it.key } (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>>, hiddenItemKeys: Flow<List<String>>,
action: (items: List<T>, hidden: List<T>) -> Unit 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( private data class HiddenItemResults(
val apps: List<Application> = emptyList(), val apps: List<LauncherApp> = emptyList(),
val contacts: List<Contact> = emptyList(), val contacts: List<Contact> = emptyList(),
val calendarEvents: List<CalendarEvent> = emptyList(), val calendarEvents: List<CalendarEvent> = emptyList(),
val files: List<File> = emptyList(), val files: List<File> = emptyList(),
val appShortcuts: List<AppShortcut> = emptyList(), val appShortcuts: List<AppShortcut> = emptyList(),
) { ) {
fun joinToList(): List<Searchable> { fun joinToList(): List<PinnableSearchable> {
return apps + contacts + calendarEvents + files + appShortcuts return apps + contacts + calendarEvents + files + appShortcuts
} }
} }

View File

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

View File

@ -8,16 +8,13 @@ import android.content.pm.*
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.os.Process import android.os.Process
import android.provider.Settings
import android.service.notification.StatusBarNotification import android.service.notification.StatusBarNotification
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import de.mm20.launcher2.appshortcuts.AppShortcutRepository import de.mm20.launcher2.appshortcuts.AppShortcutRepository
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.notifications.NotificationRepository import de.mm20.launcher2.notifications.NotificationRepository
import de.mm20.launcher2.search.data.AppShortcut 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.search.data.LauncherApp
import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -28,7 +25,7 @@ import kotlinx.coroutines.withContext
import org.koin.core.component.inject import org.koin.core.component.inject
class AppItemVM( class AppItemVM(
private val app: Application private val app: LauncherApp
) : SearchableItemVM(app) { ) : SearchableItemVM(app) {
private val notificationRepository: NotificationRepository by inject() private val notificationRepository: NotificationRepository by inject()
private val appShortcutRepository: AppShortcutRepository by inject() private val appShortcutRepository: AppShortcutRepository by inject()
@ -44,21 +41,12 @@ class AppItemVM(
fun openAppInfo(context: Context) { fun openAppInfo(context: Context) {
val launcherApps = context.getSystemService<LauncherApps>()!! val launcherApps = context.getSystemService<LauncherApps>()!!
if (app is LauncherApp) { launcherApps.startAppDetailsActivity(
launcherApps.startAppDetailsActivity( ComponentName(app.`package`, app.activity),
ComponentName(app.`package`, app.activity), app.getUser(),
app.getUser(), null,
null, null
null )
)
} else {
context.tryStartActivity(
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.parse("package:${app.`package`}")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
)
}
} }
suspend fun shareApkFile(context: Context) { suspend fun shareApkFile(context: Context) {
@ -128,9 +116,7 @@ class AppItemVM(
} }
val shortcuts = flow { 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> { 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.IconRepository
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.isAtLeastApiLevel 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.AppShortcut
import de.mm20.launcher2.search.data.Application import de.mm20.launcher2.search.data.LauncherApp
import de.mm20.launcher2.search.data.Searchable
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
abstract class SearchableItemVM( abstract class SearchableItemVM(
private val searchable: Searchable private val searchable: PinnableSearchable
) : KoinComponent { ) : KoinComponent {
protected val favoritesRepository: FavoritesRepository by inject() protected val favoritesRepository: FavoritesRepository by inject()
protected val badgeRepository: BadgeRepository by inject() protected val badgeRepository: BadgeRepository by inject()
@ -74,7 +74,7 @@ abstract class SearchableItemVM(
if (searchable.launch(context, bundle)) { if (searchable.launch(context, bundle)) {
favoritesRepository.incrementLaunchCounter(searchable) favoritesRepository.incrementLaunchCounter(searchable)
return true return true
} else if (searchable is Application || searchable is AppShortcut) { } else if (searchable is LauncherApp || searchable is AppShortcut) {
favoritesRepository.remove(searchable) favoritesRepository.remove(searchable)
} }
return false return false

View File

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

View File

@ -4,11 +4,11 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.liveData import androidx.lifecycle.liveData
import de.mm20.launcher2.customattrs.CustomAttributesRepository import de.mm20.launcher2.customattrs.CustomAttributesRepository
import de.mm20.launcher2.customattrs.CustomIcon import de.mm20.launcher2.customattrs.CustomIcon
import de.mm20.launcher2.customattrs.customAttrsModule
import de.mm20.launcher2.icons.CustomIconWithPreview import de.mm20.launcher2.icons.CustomIconWithPreview
import de.mm20.launcher2.icons.IconRepository import de.mm20.launcher2.icons.IconRepository
import de.mm20.launcher2.icons.LauncherIcon 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.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
@ -16,7 +16,7 @@ import org.koin.core.component.inject
import kotlin.coroutines.coroutineContext import kotlin.coroutines.coroutineContext
class CustomizeSearchableSheetVM( class CustomizeSearchableSheetVM(
private val searchable: Searchable private val searchable: PinnableSearchable
) : KoinComponent { ) : KoinComponent {
private val iconRepository: IconRepository by inject() private val iconRepository: IconRepository by inject()
private val customAttributesRepository: CustomAttributesRepository 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 android.content.ComponentName
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned 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.unit.dp
import androidx.compose.ui.window.Popup import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupProperties 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.search.data.*
import de.mm20.launcher2.ui.component.LauncherCard import de.mm20.launcher2.ui.component.LauncherCard
import de.mm20.launcher2.ui.component.ShapedLauncherIcon import de.mm20.launcher2.ui.component.ShapedLauncherIcon
@ -47,7 +45,7 @@ import kotlinx.coroutines.delay
@Composable @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 viewModel = remember(item.key) { GridItemVM(item) }
val context = LocalContext.current val context = LocalContext.current
@ -59,13 +57,11 @@ fun GridItem(modifier: Modifier = Modifier, item: Searchable, showLabels: Boolea
val iconSize = LocalGridIconSize.current.toPixels() val iconSize = LocalGridIconSize.current.toPixels()
val icon by remember(item.key) { viewModel.getIcon(iconSize.toInt()) }.collectAsState(null) 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.preferDetailsOverLaunch
val launchOnPress =
item is File || item is Application || item is AppShortcut || item is Website || item is Wikipedia
val windowSize = LocalWindowSize.current val windowSize = LocalWindowSize.current
if (item is Application) { if (item is LauncherApp) {
HandleHomeTransition { HandleHomeTransition {
val cn = ComponentName(item.`package`, item.activity) val cn = ComponentName(item.`package`, item.activity)
if ( if (
@ -164,7 +160,7 @@ fun ItemPopup(origin: Rect, searchable: Searchable, onDismissRequest: () -> Unit
.wrapContentSize() .wrapContentSize()
) { ) {
when (searchable) { when (searchable) {
is Application -> { is LauncherApp -> {
AppItemGridPopup( AppItemGridPopup(
app = searchable, app = searchable,
show = show, show = show,

View File

@ -1,8 +1,9 @@
package de.mm20.launcher2.ui.launcher.search.common.grid 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 import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
class GridItemVM( class GridItemVM(
searchable: Searchable searchable: PinnableSearchable
): SearchableItemVM(searchable) ): SearchableItemVM(searchable)

View File

@ -5,14 +5,15 @@ import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp 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.layout.BottomReversed
import de.mm20.launcher2.ui.locals.LocalGridColumns import de.mm20.launcher2.ui.locals.LocalGridColumns
import kotlin.math.ceil import kotlin.math.ceil
@Composable @Composable
fun SearchResultGrid( fun SearchResultGrid(
items: List<Searchable>, items: List<PinnableSearchable>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
showLabels: Boolean = true, showLabels: Boolean = true,
columns: Int = LocalGridColumns.current, 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.combinedClickable
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Rect import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext 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.search.data.*
import de.mm20.launcher2.ui.component.InnerCard import de.mm20.launcher2.ui.component.InnerCard
import de.mm20.launcher2.ui.launcher.search.calendar.CalendarItem 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 import de.mm20.launcher2.ui.launcher.search.shortcut.AppShortcutItem
@Composable @Composable
fun ListItem(modifier: Modifier = Modifier, item: Searchable) { fun ListItem(modifier: Modifier = Modifier, item: PinnableSearchable) {
var showDetails by remember { mutableStateOf(false) } var showDetails by remember { mutableStateOf(false) }
val context = LocalContext.current val context = LocalContext.current

View File

@ -1,8 +1,9 @@
package de.mm20.launcher2.ui.launcher.search.common.list 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 import de.mm20.launcher2.ui.launcher.search.common.SearchableItemVM
class ListItemVM( class ListItemVM(
searchable: Searchable searchable: PinnableSearchable
): SearchableItemVM(searchable) ): SearchableItemVM(searchable)

View File

@ -8,12 +8,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.key import androidx.compose.runtime.key
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp 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.layout.BottomReversed
@Composable @Composable
fun SearchResultList( fun SearchResultList(
items: List<Searchable>, items: List<PinnableSearchable>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
reverse: Boolean = false reverse: Boolean = false
) { ) {

View File

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

View File

@ -1,17 +1,5 @@
package de.mm20.launcher2.ui.launcher.widgets.favorites 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.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() class FavoritesWidgetVM: FavoritesVM()

View File

@ -1,6 +1,5 @@
package de.mm20.launcher2.ui.settings.hiddenitems package de.mm20.launcher2.ui.settings.hiddenitems
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@ -21,7 +20,6 @@ import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.ShapedLauncherIcon import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen 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.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.pm.LauncherApps import android.content.pm.LauncherApps
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel 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.IconRepository
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.ktx.isAtLeastApiLevel import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.ktx.tryStartActivity import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.data.Application
import de.mm20.launcher2.search.data.LauncherApp 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.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -37,18 +33,18 @@ class HiddenItemsSettingsScreenVM : ViewModel(), KoinComponent {
val allApps = appRepository.getAllInstalledApps().map { val allApps = appRepository.getAllInstalledApps().map {
withContext(Dispatchers.Default) { it.sorted() } withContext(Dispatchers.Default) { it.sorted() }
}.asLiveData() }.asLiveData()
val hiddenItems: LiveData<List<Searchable>> = liveData { val hiddenItems: LiveData<List<PinnableSearchable>> = liveData {
val hidden = withContext(Dispatchers.Default) { val hidden = withContext(Dispatchers.Default) {
favoritesRepository.getHiddenItems().first().filter { it !is Application }.sorted() favoritesRepository.getHiddenItems().first().filter { it !is LauncherApp }.sorted()
} }
emit(hidden) emit(hidden)
} }
fun isHidden(searchable: Searchable): Flow<Boolean> { fun isHidden(searchable: PinnableSearchable): Flow<Boolean> {
return favoritesRepository.isHidden(searchable) return favoritesRepository.isHidden(searchable)
} }
fun setHidden(searchable: Searchable, hidden: Boolean) { fun setHidden(searchable: PinnableSearchable, hidden: Boolean) {
if(hidden) { if(hidden) {
favoritesRepository.hideItem(searchable) favoritesRepository.hideItem(searchable)
} else { } 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) return iconRepository.getIcon(searchable, size)
} }
fun launch(context: Context, searchable: Searchable) { fun launch(context: Context, searchable: PinnableSearchable) {
val bundle = Bundle() val bundle = Bundle()
if (isAtLeastApiLevel(31)) { if (isAtLeastApiLevel(31)) {
bundle.putInt("android.activity.splashScreenStyle", 1) bundle.putInt("android.activity.splashScreenStyle", 1)
@ -68,23 +64,14 @@ class HiddenItemsSettingsScreenVM : ViewModel(), KoinComponent {
searchable.launch(context, bundle) searchable.launch(context, bundle)
} }
fun openAppInfo(context: Context, app: Application) { fun openAppInfo(context: Context, app: LauncherApp) {
val launcherApps = context.getSystemService<LauncherApps>()!! val launcherApps = context.getSystemService<LauncherApps>()!!
if (app is LauncherApp) { launcherApps.startAppDetailsActivity(
launcherApps.startAppDetailsActivity( ComponentName(app.`package`, app.activity),
ComponentName(app.`package`, app.activity), app.getUser(),
app.getUser(), null,
null, null
null )
)
} else {
context.tryStartActivity(
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.parse("package:${app.`package`}")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
)
}
} }
} }

View File

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

View File

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

View File

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

View File

@ -2,26 +2,38 @@ package de.mm20.launcher2.search.data
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri import android.net.Uri
import android.os.Bundle
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import coil.imageLoader import coil.imageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import de.mm20.launcher2.icons.* 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 de.mm20.launcher2.websites.R
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
class Website( data class Website(
override val label: String, override val label: String,
val url: String, val url: String,
val description: String, val description: String,
val image: String, val image: String,
val favicon: String, val favicon: String,
val color: Int val color: Int,
) : Searchable() { 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( override suspend fun loadIcon(
context: Context, context: Context,
size: Int, size: Int,
@ -68,10 +80,18 @@ class Website(
) )
} }
override fun getLaunchIntent(context: Context): Intent? { private fun getLaunchIntent(): Intent {
val intent = Intent(Intent.ACTION_VIEW) val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse(url) intent.data = Uri.parse(url)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
return intent 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 package de.mm20.launcher2.websites
import de.mm20.launcher2.ktx.jsonObjectOf import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer 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 de.mm20.launcher2.search.data.Website
import org.json.JSONObject import org.json.JSONObject
class WebsiteSerializer : SearchableSerializer { class WebsiteSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String { override fun serialize(searchable: PinnableSearchable): String {
searchable as Website searchable as Website
return jsonObjectOf( return jsonObjectOf(
"label" to searchable.label, "label" to searchable.label,
@ -25,7 +26,7 @@ class WebsiteSerializer : SearchableSerializer {
} }
class WebsiteDeserializer: SearchableDeserializer { class WebsiteDeserializer: SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? { override fun deserialize(serialized: String): PinnableSearchable? {
val json = JSONObject(serialized) val json = JSONObject(serialized)
return Website( return Website(
label = json.getString("label"), label = json.getString("label"),

View File

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

View File

@ -4,21 +4,35 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Color import android.graphics.Color
import android.net.Uri import android.net.Uri
import android.os.Bundle
import androidx.browser.customtabs.CustomTabsIntent import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import de.mm20.launcher2.icons.ColorLayer import de.mm20.launcher2.icons.ColorLayer
import de.mm20.launcher2.icons.StaticLauncherIcon import de.mm20.launcher2.icons.StaticLauncherIcon
import de.mm20.launcher2.icons.TintedIconLayer 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 import de.mm20.launcher2.wikipedia.R
class Wikipedia( data class Wikipedia(
override val label: String, override val label: String,
val id: Long, val id: Long,
val text: String, val text: String,
val image: String?, val image: String?,
val wikipediaUrl: String, val wikipediaUrl: String,
) : Searchable() { override val labelOverride: String? = null,
override val key = "wikipedia://$wikipediaUrl:$id" ) : 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 { override fun getPlaceholderIcon(context: Context): StaticLauncherIcon {
return StaticLauncherIcon( return StaticLauncherIcon(
@ -31,7 +45,7 @@ class Wikipedia(
) )
} }
override fun getLaunchIntent(context: Context): Intent? { private fun getLaunchIntent(): Intent {
val intent = CustomTabsIntent val intent = CustomTabsIntent
.Builder() .Builder()
.setToolbarColor(Color.BLACK) .setToolbarColor(Color.BLACK)
@ -42,4 +56,12 @@ class Wikipedia(
intent.intent.data = Uri.parse(uri) intent.intent.data = Uri.parse(uri)
return intent.intent 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 package de.mm20.launcher2.wikipedia
import android.content.Context import android.content.Context
import de.mm20.launcher2.search.PinnableSearchable
import de.mm20.launcher2.search.SearchableDeserializer import de.mm20.launcher2.search.SearchableDeserializer
import de.mm20.launcher2.search.SearchableSerializer 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 de.mm20.launcher2.search.data.Wikipedia
import org.json.JSONObject import org.json.JSONObject
class WikipediaSerializer : SearchableSerializer { class WikipediaSerializer : SearchableSerializer {
override fun serialize(searchable: Searchable): String { override fun serialize(searchable: PinnableSearchable): String {
searchable as Wikipedia searchable as Wikipedia
val json = JSONObject() val json = JSONObject()
json.put("label", searchable.label) json.put("label", searchable.label)
@ -24,7 +25,7 @@ class WikipediaSerializer : SearchableSerializer {
} }
class WikipediaDeserializer(val context: Context) : SearchableDeserializer { class WikipediaDeserializer(val context: Context) : SearchableDeserializer {
override fun deserialize(serialized: String): Searchable? { override fun deserialize(serialized: String): PinnableSearchable? {
val json = JSONObject(serialized) val json = JSONObject(serialized)
return Wikipedia( return Wikipedia(
label = json.getString("label"), label = json.getString("label"),