Make search action order customizable

This commit is contained in:
MM20
2022-11-05 17:59:24 +01:00
parent 960c3b70f5
commit 3463b3b800
29 changed files with 526 additions and 253 deletions
@@ -5,10 +5,21 @@ import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.database.entities.SearchActionEntity
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.searchactions.builders.CallActionBuilder
import de.mm20.launcher2.searchactions.builders.CreateContactActionBuilder
import de.mm20.launcher2.searchactions.builders.EmailActionBuilder
import de.mm20.launcher2.searchactions.builders.MessageActionBuilder
import de.mm20.launcher2.searchactions.builders.OpenUrlActionBuilder
import de.mm20.launcher2.searchactions.builders.ScheduleEventActionBuilder
import de.mm20.launcher2.searchactions.builders.SearchActionBuilder
import de.mm20.launcher2.searchactions.builders.SetAlarmActionBuilder
import de.mm20.launcher2.searchactions.builders.TimerActionBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONException
@@ -17,6 +28,9 @@ import java.util.UUID
interface SearchActionRepository {
fun getSearchActionBuilders(): Flow<List<SearchActionBuilder>>
fun getBuiltinSearchActionBuilders(): List<SearchActionBuilder>
fun saveSearchActionBuilders(builders: List<SearchActionBuilder>)
suspend fun export(toDir: File)
suspend fun import(fromDir: File)
@@ -26,9 +40,35 @@ internal class SearchActionRepositoryImpl(
private val context: Context,
private val database: AppDatabase
): SearchActionRepository {
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
override fun getSearchActionBuilders(): Flow<List<SearchActionBuilder>> {
val dao = database.searchActionDao()
return dao.getSearchActions().map { it.mapNotNull { SearchActionBuilder.from(it) } }
return dao.getSearchActions().map { it.mapNotNull { SearchActionBuilder.from(context, it) } }
}
override fun getBuiltinSearchActionBuilders(): List<SearchActionBuilder> {
val allActions = listOf(
CallActionBuilder(context),
MessageActionBuilder(context),
CreateContactActionBuilder(context),
EmailActionBuilder(context),
ScheduleEventActionBuilder(context),
SetAlarmActionBuilder(context),
TimerActionBuilder(context),
OpenUrlActionBuilder(context),
)
return allActions
}
override fun saveSearchActionBuilders(builders: List<SearchActionBuilder>) {
scope.launch {
val dao = database.searchActionDao()
dao.replaceAll(
builders.mapIndexed { i, it -> SearchActionBuilder.toDatabaseEntity(it, i) }
)
}
}
override suspend fun export(toDir: File) = withContext(Dispatchers.IO) {
@@ -2,13 +2,9 @@ package de.mm20.launcher2.searchactions
import android.content.Context
import android.content.Intent
import android.content.pm.LauncherActivityInfo
import android.content.pm.LauncherApps
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.graphics.Bitmap
import android.net.Uri
import android.os.UserHandle
import android.util.Log
import android.util.Xml
import androidx.core.graphics.drawable.toBitmap
@@ -16,20 +12,10 @@ import coil.imageLoader
import coil.request.ImageRequest
import coil.size.Scale
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.entities.WebsearchEntity
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.preferences.Settings.SearchActionSettings
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
import de.mm20.launcher2.searchactions.builders.CallActionBuilder
import de.mm20.launcher2.searchactions.builders.CreateContactActionBuilder
import de.mm20.launcher2.searchactions.builders.EmailActionBuilder
import de.mm20.launcher2.searchactions.builders.MessageActionBuilder
import de.mm20.launcher2.searchactions.builders.OpenUrlActionBuilder
import de.mm20.launcher2.searchactions.builders.ScheduleEventActionBuilder
import de.mm20.launcher2.searchactions.builders.SearchActionBuilder
import de.mm20.launcher2.searchactions.builders.SetAlarmActionBuilder
import de.mm20.launcher2.searchactions.builders.TimerActionBuilder
import de.mm20.launcher2.searchactions.builders.WebsearchActionBuilder
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@@ -42,8 +28,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONArray
import org.json.JSONException
import org.jsoup.Jsoup
import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException
@@ -54,12 +38,18 @@ import java.net.URL
import java.util.UUID
interface SearchActionService {
fun search(settings: SearchActionSettings, query: String): Flow<ImmutableList<SearchAction>>
fun search(query: String): Flow<ImmutableList<SearchAction>>
fun getSearchActionBuilders(): Flow<List<SearchActionBuilder>>
fun getDisabledActionBuilders(): Flow<List<SearchActionBuilder>>
fun saveSearchActionBuilders(builders: List<SearchActionBuilder>)
suspend fun importWebsearch(url: String, iconSize: Int): WebsearchActionBuilder?
suspend fun createIcon(uri: Uri, size: Int): String?
suspend fun getSearchActivities(): List<ResolveInfo>
suspend fun createIcon(uri: Uri, size: Int): String?
}
internal class SearchActionServiceImpl(
@@ -68,7 +58,6 @@ internal class SearchActionServiceImpl(
private val textClassifier: TextClassifier,
) : SearchActionService {
override fun search(
settings: SearchActionSettings,
query: String
): Flow<ImmutableList<SearchAction>> = flow {
if (query.isBlank()) {
@@ -76,28 +65,33 @@ internal class SearchActionServiceImpl(
return@flow
}
val builders = mutableListOf<SearchActionBuilder>()
if (settings.call) builders.add(CallActionBuilder)
if (settings.message) builders.add(MessageActionBuilder)
if (settings.contact) builders.add(CreateContactActionBuilder)
if (settings.email) builders.add(EmailActionBuilder)
if (settings.openUrl) builders.add(OpenUrlActionBuilder)
if (settings.scheduleEvent) builders.add(ScheduleEventActionBuilder)
if (settings.setAlarm) builders.add(SetAlarmActionBuilder)
if (settings.startTimer) builders.add(TimerActionBuilder)
val classificationResult = textClassifier.classify(context, query)
val other = repository.getSearchActionBuilders()
val builders = repository.getSearchActionBuilders()
emitAll(
other.map {
(builders + it).mapNotNull { it.build(context, classificationResult) }.toImmutableList()
builders.map {
it.mapNotNull { it.build(context, classificationResult) }.toImmutableList()
}
)
}
override fun getSearchActionBuilders(): Flow<List<SearchActionBuilder>> {
return repository.getSearchActionBuilders()
}
override fun getDisabledActionBuilders(): Flow<List<SearchActionBuilder>> {
val allActions = repository.getBuiltinSearchActionBuilders()
return getSearchActionBuilders().map { enabled ->
allActions.filter { action -> !enabled.any { it.key == action.key } }
}
}
override fun saveSearchActionBuilders(builders: List<SearchActionBuilder>) {
repository.saveSearchActionBuilders(builders)
}
override suspend fun importWebsearch(url: String, iconSize: Int): WebsearchActionBuilder? =
withContext(Dispatchers.IO) {
try {
@@ -1,21 +1,27 @@
package de.mm20.launcher2.searchactions.builders
import android.content.ComponentName
import android.content.Context
import android.content.pm.LauncherActivityInfo
import de.mm20.launcher2.searchactions.TextClassificationResult
import de.mm20.launcher2.searchactions.TextType
import de.mm20.launcher2.searchactions.actions.AppSearchAction
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
class AppSearchActionBuilder(
val label: String,
val activity: LauncherActivityInfo,
val filter: TextType? = null,
) : SearchActionBuilder {
override val label: String,
val componentName: ComponentName,
override val icon: SearchActionIcon = SearchActionIcon.Search,
override val iconColor: Int = 0,
override val customIcon: String? = null,
) : CustomizableSearchActionBuilder {
override val key: String
get() = "app://${componentName.flattenToShortString()}"
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
return AppSearchAction(
label = label,
componentName = activity.componentName,
componentName = componentName,
query = classifiedQuery.text,
)
}
@@ -5,8 +5,18 @@ import de.mm20.launcher2.searchactions.R
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.searchactions.TextClassificationResult
import de.mm20.launcher2.searchactions.actions.CallAction
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
object CallActionBuilder: SearchActionBuilder {
class CallActionBuilder(
override val label: String
): SearchActionBuilder {
constructor(context: Context): this(context.getString(R.string.search_action_call))
override val key: String
get() = "call"
override val icon: SearchActionIcon = SearchActionIcon.Phone
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
if (classifiedQuery.phoneNumber != null) {
@@ -5,8 +5,18 @@ import de.mm20.launcher2.searchactions.R
import de.mm20.launcher2.searchactions.TextClassificationResult
import de.mm20.launcher2.searchactions.actions.CreateContactAction
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
object CreateContactActionBuilder : SearchActionBuilder {
class CreateContactActionBuilder(
override val label: String
) : SearchActionBuilder {
constructor(context: Context) : this(context.getString(R.string.search_action_contact))
override val key: String
get() = "contact"
override val icon: SearchActionIcon = SearchActionIcon.Contact
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
if (classifiedQuery.phoneNumber != null || classifiedQuery.email != null) {
@@ -0,0 +1,3 @@
package de.mm20.launcher2.searchactions.builders
sealed interface CustomizableSearchActionBuilder: SearchActionBuilder
@@ -5,8 +5,18 @@ import de.mm20.launcher2.searchactions.R
import de.mm20.launcher2.searchactions.TextClassificationResult
import de.mm20.launcher2.searchactions.actions.EmailAction
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
object EmailActionBuilder: SearchActionBuilder {
class EmailActionBuilder(
override val label: String
): SearchActionBuilder {
constructor(context: Context) : this(context.getString(R.string.search_action_email))
override val key: String
get() = "email"
override val icon: SearchActionIcon = SearchActionIcon.Email
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
if (classifiedQuery.email != null) {
@@ -5,8 +5,18 @@ import de.mm20.launcher2.searchactions.R
import de.mm20.launcher2.searchactions.TextClassificationResult
import de.mm20.launcher2.searchactions.actions.MessageAction
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
object MessageActionBuilder: SearchActionBuilder {
class MessageActionBuilder(
override val label: String
): SearchActionBuilder {
constructor(context: Context) : this(context.getString(R.string.search_action_message))
override val key: String
get() = "message"
override val icon: SearchActionIcon = SearchActionIcon.Message
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
if (classifiedQuery.phoneNumber != null) {
@@ -6,8 +6,18 @@ import de.mm20.launcher2.searchactions.TextClassificationResult
import de.mm20.launcher2.searchactions.actions.MessageAction
import de.mm20.launcher2.searchactions.actions.OpenUrlAction
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
object OpenUrlActionBuilder : SearchActionBuilder {
class OpenUrlActionBuilder(
override val label: String
) : SearchActionBuilder {
constructor(context: Context) : this(context.getString(R.string.search_action_open_url))
override val key: String
get() = "website"
override val icon: SearchActionIcon = SearchActionIcon.Website
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
if (classifiedQuery.url != null) {
@@ -6,9 +6,19 @@ import de.mm20.launcher2.searchactions.TextClassificationResult
import de.mm20.launcher2.searchactions.actions.MessageAction
import de.mm20.launcher2.searchactions.actions.ScheduleEventAction
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
import java.time.LocalDateTime
object ScheduleEventActionBuilder : SearchActionBuilder {
class ScheduleEventActionBuilder(
override val label: String
) : SearchActionBuilder {
constructor(context: Context) : this(context.getString(R.string.search_action_event))
override val key: String
get() = "calendar"
override val icon: SearchActionIcon = SearchActionIcon.Calendar
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
if (classifiedQuery.date != null) {
@@ -1,7 +1,9 @@
package de.mm20.launcher2.searchactions.builders
import android.content.Context
import android.media.metrics.Event
import de.mm20.launcher2.database.entities.SearchActionEntity
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.searchactions.TextClassificationResult
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
@@ -9,10 +11,18 @@ import org.json.JSONException
import org.json.JSONObject
interface SearchActionBuilder {
val label: String
val icon: SearchActionIcon
val iconColor: Int
get() = 0
val customIcon: String?
get() = null
val key: String
fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction?
companion object {
fun from(entity: SearchActionEntity): SearchActionBuilder? {
internal fun from(context: Context, entity: SearchActionEntity): SearchActionBuilder? {
val options = entity.options?.let {
try {
JSONObject(it)
@@ -24,8 +34,8 @@ interface SearchActionBuilder {
"url" -> {
return WebsearchActionBuilder(
label = entity.label ?: "",
urlTemplate = entity.data,
color = entity.color,
urlTemplate = entity.data ?: return null,
iconColor = entity.color ?: 0,
icon = SearchActionIcon.fromInt(entity.icon),
customIcon = entity.customIcon,
encoding = WebsearchActionBuilder.QueryEncoding.fromInt(options?.optInt("encoding"))
@@ -34,9 +44,38 @@ interface SearchActionBuilder {
"app" -> {
return null
}
"call" -> return CallActionBuilder(context)
"message" -> return MessageActionBuilder(context)
"email" -> return EmailActionBuilder(context)
"contact" -> return CreateContactActionBuilder(context)
"alarm" -> return SetAlarmActionBuilder(context)
"timer" -> return TimerActionBuilder(context)
"calendar" -> return ScheduleEventActionBuilder(context)
"website" -> return OpenUrlActionBuilder(context)
else -> return null
}
}
}
internal fun toDatabaseEntity(builder: SearchActionBuilder, position: Int): SearchActionEntity {
return when(builder) {
is WebsearchActionBuilder -> SearchActionEntity(
position = position,
type = "url",
label = builder.label,
data = builder.urlTemplate,
color = builder.iconColor,
icon = builder.icon.toInt(),
customIcon = builder.customIcon,
options = jsonObjectOf(
"encoding" to builder.encoding.toInt()
).toString()
)
//is AppSearchActionBuilder -> null
else -> SearchActionEntity(
position = position,
type = builder.key,
)
}
}
}
}
@@ -4,10 +4,20 @@ import android.content.Context
import de.mm20.launcher2.searchactions.R
import de.mm20.launcher2.searchactions.TextClassificationResult
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
import de.mm20.launcher2.searchactions.actions.SetAlarmAction
import java.time.LocalDate
object SetAlarmActionBuilder : SearchActionBuilder {
class SetAlarmActionBuilder(
override val label: String
) : SearchActionBuilder {
constructor(context: Context) : this(context.getString(R.string.search_action_alarm))
override val key: String
get() = "alarm"
override val icon: SearchActionIcon = SearchActionIcon.Alarm
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
if (classifiedQuery.time != null) {
@@ -3,10 +3,19 @@ package de.mm20.launcher2.searchactions.builders
import android.content.Context
import de.mm20.launcher2.searchactions.R
import de.mm20.launcher2.searchactions.TextClassificationResult
import de.mm20.launcher2.searchactions.actions.TimerAction
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
import de.mm20.launcher2.searchactions.actions.TimerAction
object TimerActionBuilder : SearchActionBuilder {
class TimerActionBuilder(
override val label: String,
) : SearchActionBuilder {
constructor(context: Context) : this(context.getString(R.string.search_action_timer))
override val key: String
get() = "timer"
override val icon: SearchActionIcon = SearchActionIcon.Timer
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
if (classifiedQuery.timespan != null && classifiedQuery.timespan.seconds <= 86400) {
@@ -9,13 +9,16 @@ import de.mm20.launcher2.searchactions.actions.SearchActionIcon
import java.net.URLEncoder
class WebsearchActionBuilder(
val label: String,
override val label: String,
val urlTemplate: String,
val icon: SearchActionIcon = SearchActionIcon.Search,
val color: Int = 0,
val customIcon: String? = null,
override val icon: SearchActionIcon = SearchActionIcon.Search,
override val iconColor: Int = 0,
override val customIcon: String? = null,
val encoding: QueryEncoding = QueryEncoding.UrlEncode,
) : SearchActionBuilder {
) : CustomizableSearchActionBuilder {
override val key: String
get() = "web://$urlTemplate"
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction {
val url = urlTemplate.replace("\${1}", encodeQuery(classifiedQuery.text, encoding))
@@ -24,7 +27,7 @@ class WebsearchActionBuilder(
url = url,
icon = icon,
customIcon = customIcon,
iconColor = color,
iconColor = iconColor,
)
}