Reorganize and group modules
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package de.mm20.launcher2.searchactions
|
||||
|
||||
import de.mm20.launcher2.searchactions.builders.WebsearchActionBuilder
|
||||
|
||||
fun knownWebsearchByHostname(hostname: String): WebsearchActionBuilder? {
|
||||
// List of popular web search engines that do not implement the OpenSearch standard
|
||||
return when(hostname) {
|
||||
"google.com" -> WebsearchActionBuilder(label = "Google", urlTemplate = "https://google.com/search?q=\${1}")
|
||||
"bing.com" -> WebsearchActionBuilder(label = "Google", urlTemplate = "https://bing.com/search?q=\${1}")
|
||||
"amazon.com" -> WebsearchActionBuilder(label = "Amazon", urlTemplate = "https://www.amazon.com/s?k=\${1}")
|
||||
"amazon.de" -> WebsearchActionBuilder(label = "Amazon DE", urlTemplate = "https://www.amazon.de/s?k=\${1}")
|
||||
"amazon.co.uk" -> WebsearchActionBuilder(label = "Amazon UK", urlTemplate = "https://www.amazon.co.uk/s?k=\${1}")
|
||||
"amazon.fr" -> WebsearchActionBuilder(label = "Amazon FR", urlTemplate = "https://www.amazon.fr/s?k=\${1}")
|
||||
"amazon.co.jp" -> WebsearchActionBuilder(label = "Amazon JP", urlTemplate = "https://www.amazon.co.jp/s?k=\${1}")
|
||||
"amazon.ca" -> WebsearchActionBuilder(label = "Amazon CA", urlTemplate = "https://www.amazon.ca/s?k=\${1}")
|
||||
"amazon.cn" -> WebsearchActionBuilder(label = "Amazon CN", urlTemplate = "https://www.amazon.cn/s?k=\${1}")
|
||||
"duckduckgo.com" -> WebsearchActionBuilder(label = "DuckDuckGo", urlTemplate = "https://duckduckgo.com/?q=\${1}")
|
||||
"yahoo.com" -> WebsearchActionBuilder(label = "DuckDuckGo", urlTemplate = "https://search.yahoo.com/search?p=\${1}")
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.mm20.launcher2.searchactions
|
||||
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.dsl.module
|
||||
|
||||
val searchActionsModule = module {
|
||||
single<SearchActionRepository> { SearchActionRepositoryImpl(androidContext(), get()) }
|
||||
single<SearchActionService> { SearchActionServiceImpl(androidContext(), get(), TextClassifierImpl()) }
|
||||
}
|
||||
+165
@@ -0,0 +1,165 @@
|
||||
package de.mm20.launcher2.searchactions
|
||||
|
||||
import android.content.Context
|
||||
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
|
||||
import java.io.File
|
||||
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)
|
||||
}
|
||||
|
||||
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(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) {
|
||||
val dao = database.backupDao()
|
||||
var page = 0
|
||||
var iconCounter = 0
|
||||
do {
|
||||
val websearches = dao.exportSearchActions(limit = 100, offset = page * 100)
|
||||
val jsonArray = JSONArray()
|
||||
for (websearch in websearches) {
|
||||
var customIcon = websearch.customIcon
|
||||
if (customIcon != null) {
|
||||
val fileName = "asset.searchaction.${iconCounter.toString().padStart(4, '0')}"
|
||||
val iconAssetFile = File(toDir, fileName)
|
||||
File(customIcon).inputStream().use { inStream ->
|
||||
iconAssetFile.outputStream().use { outStream ->
|
||||
inStream.copyTo(outStream)
|
||||
}
|
||||
}
|
||||
customIcon = fileName
|
||||
|
||||
iconCounter++
|
||||
}
|
||||
jsonArray.put(
|
||||
jsonObjectOf(
|
||||
"color" to websearch.color,
|
||||
"label" to websearch.label,
|
||||
"data" to websearch.data,
|
||||
"icon" to websearch.icon,
|
||||
"customIcon" to customIcon,
|
||||
"options" to websearch.options,
|
||||
"position" to websearch.position,
|
||||
"type" to websearch.type,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val file = File(toDir, "searchactions.${page.toString().padStart(4, '0')}")
|
||||
file.bufferedWriter().use {
|
||||
it.write(jsonArray.toString())
|
||||
}
|
||||
page++
|
||||
} while (websearches.size == 100)
|
||||
}
|
||||
|
||||
override suspend fun import(fromDir: File) = withContext(Dispatchers.IO) {
|
||||
val dao = database.backupDao()
|
||||
dao.wipeSearchActions()
|
||||
|
||||
val files = fromDir.listFiles { _, name -> name.startsWith("searchactions.") } ?: return@withContext
|
||||
|
||||
for (file in files) {
|
||||
val searchActions = mutableListOf<SearchActionEntity>()
|
||||
try {
|
||||
val jsonArray = JSONArray(file.inputStream().reader().readText())
|
||||
|
||||
for (i in 0 until jsonArray.length()) {
|
||||
val json = jsonArray.getJSONObject(i)
|
||||
|
||||
val customIcon = json.optString("customIcon").takeIf { it.isNotEmpty() }
|
||||
|
||||
var iconFile: File? = null
|
||||
|
||||
if (customIcon != null) {
|
||||
val asset = File(fromDir, customIcon)
|
||||
iconFile = File(context.filesDir, UUID.randomUUID().toString())
|
||||
asset.inputStream().use { inStream ->
|
||||
iconFile.outputStream().use { outStream ->
|
||||
inStream.copyTo(outStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val entity = SearchActionEntity(
|
||||
position = json.getInt("position"),
|
||||
data = json.getString("data"),
|
||||
color = json.optInt("color", 0),
|
||||
label = json.getString("label"),
|
||||
icon = json.optInt("icon", 0),
|
||||
customIcon = iconFile?.absolutePath,
|
||||
options = json.optString("options").takeIf { it.isNotEmpty() },
|
||||
type = json.getString("type"),
|
||||
)
|
||||
searchActions.add(entity)
|
||||
}
|
||||
|
||||
dao.importSearchActions(searchActions)
|
||||
|
||||
} catch (e: JSONException) {
|
||||
CrashReporter.logException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
package de.mm20.launcher2.searchactions
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.util.Xml
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.size.Scale
|
||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
||||
import de.mm20.launcher2.searchactions.builders.SearchActionBuilder
|
||||
import de.mm20.launcher2.searchactions.builders.WebsearchActionBuilder
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.jsoup.Jsoup
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import java.util.UUID
|
||||
|
||||
interface SearchActionService {
|
||||
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 getSearchActivities(): List<ComponentName>
|
||||
|
||||
suspend fun createIcon(uri: Uri, size: Int): String?
|
||||
}
|
||||
|
||||
internal class SearchActionServiceImpl(
|
||||
private val context: Context,
|
||||
private val repository: SearchActionRepository,
|
||||
private val textClassifier: TextClassifier,
|
||||
) : SearchActionService {
|
||||
override fun search(
|
||||
query: String
|
||||
): Flow<ImmutableList<SearchAction>> = flow {
|
||||
if (query.isBlank()) {
|
||||
emit(persistentListOf())
|
||||
return@flow
|
||||
}
|
||||
|
||||
val classificationResult = textClassifier.classify(context, query)
|
||||
|
||||
val builders = repository.getSearchActionBuilders()
|
||||
|
||||
emitAll(
|
||||
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 {
|
||||
val u = if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||
url
|
||||
} else {
|
||||
"https://$url"
|
||||
}
|
||||
|
||||
if (u.contains("${1}")) {
|
||||
return@withContext WebsearchActionBuilder(
|
||||
urlTemplate = u,
|
||||
label = "",
|
||||
iconColor = 0,
|
||||
icon = SearchActionIcon.Search,
|
||||
)
|
||||
}
|
||||
|
||||
val document = Jsoup.parse(URL(u), 5000)
|
||||
val metaElements =
|
||||
document.select("link[rel=\"search\"][href][type=\"application/opensearchdescription+xml\"]")
|
||||
val openSearchHref = metaElements
|
||||
.getOrNull(0)
|
||||
?.absUrl("href")
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
|
||||
val action = openSearchHref?.let {
|
||||
importOpenSearch(it, iconSize)
|
||||
}
|
||||
|
||||
if (action != null) {
|
||||
return@withContext action
|
||||
}
|
||||
|
||||
val host = URL(u).host ?: return@withContext null
|
||||
return@withContext knownWebsearchByHostname(host)
|
||||
|
||||
} catch (e: IOException) {
|
||||
CrashReporter.logException(e)
|
||||
} catch (e: XmlPullParserException) {
|
||||
CrashReporter.logException(e)
|
||||
}
|
||||
return@withContext null
|
||||
}
|
||||
|
||||
private suspend fun importOpenSearch(openSearchHref: String, iconSize: Int): WebsearchActionBuilder? {
|
||||
try {
|
||||
val httpClient = OkHttpClient()
|
||||
val request = Request.Builder()
|
||||
.url(openSearchHref)
|
||||
.build()
|
||||
val response = httpClient.newCall(request).execute()
|
||||
val inputStream = response.body?.byteStream() ?: return null
|
||||
|
||||
var label: String? = null
|
||||
var urlTemplate: String? = null
|
||||
var icon: String? = null
|
||||
var largestIconSize: Int = 0
|
||||
var largestIcon: String? = null
|
||||
|
||||
inputStream.use {
|
||||
val parser = Xml.newPullParser()
|
||||
parser.setInput(inputStream.reader())
|
||||
while (parser.next() != XmlPullParser.END_DOCUMENT) {
|
||||
if (parser.eventType == XmlPullParser.START_TAG) {
|
||||
when (parser.name) {
|
||||
"ShortName" -> {
|
||||
parser.next()
|
||||
if (parser.eventType == XmlPullParser.TEXT) {
|
||||
label = parser.text
|
||||
}
|
||||
}
|
||||
|
||||
"LongName" -> {
|
||||
parser.next()
|
||||
if (parser.eventType == XmlPullParser.TEXT) {
|
||||
if (label != null) label = parser.text
|
||||
}
|
||||
}
|
||||
|
||||
"Image" -> {
|
||||
val width =
|
||||
parser.getAttributeValue(null, "width")?.toIntOrNull() ?: 0
|
||||
if (width > largestIconSize || largestIcon == null) {
|
||||
parser.next()
|
||||
if (parser.eventType == XmlPullParser.TEXT) {
|
||||
largestIcon = parser.text
|
||||
largestIconSize = width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"Url" -> {
|
||||
if (parser.getAttributeValue(null, "type") == "text/html") {
|
||||
val rel = parser.getAttributeValue(null, "rel")
|
||||
if (rel == null || rel == "results") {
|
||||
val template =
|
||||
parser.getAttributeValue(null, "template")
|
||||
?.takeIf { it.isNotEmpty() } ?: continue
|
||||
urlTemplate = template
|
||||
.replace("{searchTerms}", "\${1}")
|
||||
.replace("{startPage?}", "1")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val localIconUrl = largestIcon?.let {
|
||||
val uri = Uri.parse(it)
|
||||
createIcon(uri, iconSize)
|
||||
}
|
||||
|
||||
return WebsearchActionBuilder(
|
||||
label = label ?: "",
|
||||
icon = if (localIconUrl == null) SearchActionIcon.Search else SearchActionIcon.Custom,
|
||||
customIcon = localIconUrl,
|
||||
iconColor = if (localIconUrl == null) 0 else 1,
|
||||
urlTemplate = urlTemplate ?: ""
|
||||
)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
|
||||
} catch (e: XmlPullParserException) {}
|
||||
return null
|
||||
}
|
||||
|
||||
override suspend fun createIcon(uri: Uri, size: Int): String? = withContext(
|
||||
Dispatchers.IO
|
||||
) {
|
||||
val file = File(context.filesDir, UUID.randomUUID().toString())
|
||||
val imageRequest = ImageRequest.Builder(context)
|
||||
.data(uri)
|
||||
.size(size)
|
||||
.scale(Scale.FIT)
|
||||
.build()
|
||||
val drawable =
|
||||
context.imageLoader.execute(imageRequest).drawable ?: return@withContext null
|
||||
val scaledIcon = drawable.toBitmap()
|
||||
val out = FileOutputStream(file)
|
||||
scaledIcon.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||
out.close()
|
||||
return@withContext file.absolutePath
|
||||
}
|
||||
|
||||
override suspend fun getSearchActivities(): List<ComponentName> {
|
||||
return withContext(Dispatchers.Default) {
|
||||
val resolveInfos = context.packageManager.queryIntentActivities(
|
||||
Intent(Intent.ACTION_SEARCH).addCategory(Intent.CATEGORY_DEFAULT), PackageManager.GET_META_DATA,
|
||||
)
|
||||
resolveInfos.mapNotNull {
|
||||
if (!it.activityInfo.exported || !it.activityInfo.enabled) return@mapNotNull null
|
||||
if (it.activityInfo.permission != null && context.checkSelfPermission(it.activityInfo.permission) != PackageManager.PERMISSION_GRANTED) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
val componentName = ComponentName(it.activityInfo.packageName, it.activityInfo.name)
|
||||
componentName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package de.mm20.launcher2.searchactions
|
||||
|
||||
import android.content.Context
|
||||
import android.icu.text.SimpleDateFormat
|
||||
import android.text.format.DateFormat
|
||||
import java.text.ParseException
|
||||
import java.time.Duration
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
import java.time.ZoneId
|
||||
import java.util.Locale
|
||||
|
||||
internal interface TextClassifier {
|
||||
suspend fun classify(context: Context, query: String): TextClassificationResult
|
||||
}
|
||||
|
||||
internal class TextClassifierImpl : TextClassifier {
|
||||
override suspend fun classify(context: Context, query: String): TextClassificationResult {
|
||||
return when {
|
||||
query.matches(Regex("^\\S+@\\S+$")) -> TextClassificationResult(
|
||||
type = TextType.Email,
|
||||
text = query,
|
||||
email = query
|
||||
)
|
||||
|
||||
query.matches(Regex("^\\+?[0-9- ]{4,18}$")) -> TextClassificationResult(
|
||||
type = TextType.PhoneNumber,
|
||||
text = query,
|
||||
phoneNumber = query
|
||||
)
|
||||
|
||||
query.matches(Regex("^(http(s)?://.)?(www\\.)?[-a-zA-Z0-9@:%._+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_+.~#?&/=]*)$")) -> TextClassificationResult(
|
||||
type = TextType.Url,
|
||||
text = query,
|
||||
url = query
|
||||
)
|
||||
|
||||
else -> {
|
||||
parseDate(context, query)?.let { return it }
|
||||
TextClassificationResult(type = TextType.Text, text = query)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseDate(context: Context, query: String): TextClassificationResult? {
|
||||
val dateTimeFormat = SimpleDateFormat(
|
||||
DateFormat.getBestDateTimePattern(
|
||||
Locale.getDefault(),
|
||||
"yyyy-MM-dd, HH:mm"
|
||||
)
|
||||
)
|
||||
try {
|
||||
dateTimeFormat.parse(query)?.let {
|
||||
val dateTime = LocalDateTime.ofInstant(it.toInstant(), ZoneId.systemDefault())
|
||||
return TextClassificationResult(
|
||||
type = TextType.DateTime,
|
||||
text = query,
|
||||
time = dateTime.toLocalTime(),
|
||||
date = dateTime.toLocalDate(),
|
||||
)
|
||||
}
|
||||
} catch (_: ParseException) {
|
||||
// Not a datetime
|
||||
}
|
||||
val dateFormat = DateFormat.getDateFormat(context)
|
||||
try {
|
||||
dateFormat.parse(query)?.let {
|
||||
return TextClassificationResult(
|
||||
type = TextType.Date,
|
||||
text = query,
|
||||
date = LocalDateTime.ofInstant(it.toInstant(), ZoneId.systemDefault())
|
||||
.toLocalDate()
|
||||
)
|
||||
}
|
||||
} catch (_: ParseException) {
|
||||
// Not a date either
|
||||
}
|
||||
val timeFormat = DateFormat.getTimeFormat(context)
|
||||
try {
|
||||
timeFormat.parse(query)?.let {
|
||||
return TextClassificationResult(
|
||||
type = TextType.Time,
|
||||
text = query,
|
||||
time = LocalDateTime.ofInstant(it.toInstant(), ZoneId.systemDefault())
|
||||
.toLocalTime(),
|
||||
)
|
||||
}
|
||||
} catch (_: ParseException) {
|
||||
// Nope, not a time
|
||||
}
|
||||
|
||||
val seconds = context.getString(R.string.unit_second_symbol)
|
||||
if (query.matches(Regex("^[0-9]+ ${seconds}$"))) {
|
||||
val value = query.substringBefore(" ").toLong()
|
||||
return TextClassificationResult(
|
||||
type = TextType.Timespan,
|
||||
text = query,
|
||||
timespan = Duration.ofSeconds(value)
|
||||
)
|
||||
}
|
||||
|
||||
val days = context.getString(R.string.unit_day_symbol)
|
||||
if (query.matches(Regex("^[0-9]+ ${days}$"))) {
|
||||
val value = query.substringBefore(" ").toLong()
|
||||
return TextClassificationResult(
|
||||
type = TextType.Timespan,
|
||||
text = query,
|
||||
timespan = Duration.ofDays(value)
|
||||
)
|
||||
}
|
||||
val minutes = context.getString(R.string.unit_minute_symbol)
|
||||
if (query.matches(Regex("^[0-9]+ ${minutes}$"))) {
|
||||
val value = query.substringBefore(" ").toLong()
|
||||
val then = LocalDateTime.now().plusMinutes(value)
|
||||
return TextClassificationResult(
|
||||
type = TextType.Timespan,
|
||||
text = query,
|
||||
timespan = Duration.ofMinutes(value)
|
||||
)
|
||||
}
|
||||
val hours = context.getString(R.string.unit_hour_symbol)
|
||||
if (query.matches(Regex("^[0-9]+ ${hours}$"))) {
|
||||
val value = query.substringBefore(" ").toLong()
|
||||
return TextClassificationResult(
|
||||
type = TextType.Timespan,
|
||||
text = query,
|
||||
timespan = Duration.ofHours(value)
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
data class TextClassificationResult(
|
||||
val type: TextType,
|
||||
val text: String,
|
||||
val email: String? = null,
|
||||
val phoneNumber: String? = null,
|
||||
val time: LocalTime? = null,
|
||||
val date: LocalDate? = null,
|
||||
val timespan: Duration? = null,
|
||||
val url: String? = null,
|
||||
)
|
||||
|
||||
enum class TextType {
|
||||
Text,
|
||||
Email,
|
||||
Url,
|
||||
PhoneNumber,
|
||||
DateTime,
|
||||
Date,
|
||||
Time,
|
||||
Timespan,
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package de.mm20.launcher2.searchactions.actions
|
||||
|
||||
import android.app.SearchManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
|
||||
data class AppSearchAction(
|
||||
override val label: String,
|
||||
val baseIntent: Intent,
|
||||
val query: String,
|
||||
override val icon: SearchActionIcon = SearchActionIcon.Custom,
|
||||
override val iconColor: Int = 1,
|
||||
override val customIcon: String? = null,
|
||||
): SearchAction {
|
||||
|
||||
override fun start(context: Context) {
|
||||
val intent = Intent(baseIntent).apply {
|
||||
action = Intent.ACTION_SEARCH
|
||||
putExtra(SearchManager.QUERY, query)
|
||||
putExtra(SearchManager.USER_QUERY, query)
|
||||
}
|
||||
context.tryStartActivity(intent)
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package de.mm20.launcher2.searchactions.actions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.searchactions.R
|
||||
|
||||
data class CallAction(
|
||||
override val label: String,
|
||||
val number: String,
|
||||
): SearchAction {
|
||||
|
||||
override val icon: SearchActionIcon = SearchActionIcon.Phone
|
||||
override val iconColor: Int = 0
|
||||
override val customIcon: String? = null
|
||||
|
||||
override fun start(context: Context) {
|
||||
val intent = Intent(Intent.ACTION_DIAL).apply {
|
||||
data = Uri.parse("tel:$number")
|
||||
}
|
||||
context.tryStartActivity(intent)
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package de.mm20.launcher2.searchactions.actions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.provider.ContactsContract
|
||||
import android.provider.ContactsContract.Intents.Insert
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
|
||||
class CreateContactAction(
|
||||
override val label: String,
|
||||
val phone: String? = null,
|
||||
val email: String? = null,
|
||||
) : SearchAction {
|
||||
override val icon: SearchActionIcon = SearchActionIcon.Contact
|
||||
override val iconColor: Int = 0
|
||||
override val customIcon: String? = null
|
||||
|
||||
override fun start(context: Context) {
|
||||
val intent = Intent(Intent.ACTION_INSERT).apply {
|
||||
type = ContactsContract.Contacts.CONTENT_TYPE
|
||||
if (email != null) putExtra(Insert.EMAIL, email)
|
||||
if (phone != null) putExtra(Insert.PHONE, phone)
|
||||
}
|
||||
context.tryStartActivity(intent)
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package de.mm20.launcher2.searchactions.actions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
|
||||
class CustomIntentAction(
|
||||
override val label: String,
|
||||
val query: String,
|
||||
private val queryKey: String,
|
||||
private val baseIntent: Intent,
|
||||
override val icon: SearchActionIcon = SearchActionIcon.Custom,
|
||||
override val iconColor: Int = 1,
|
||||
override val customIcon: String? = null,
|
||||
) : SearchAction {
|
||||
override fun start(context: Context) {
|
||||
val intent = Intent(baseIntent).also {
|
||||
it.putExtra(queryKey, query)
|
||||
}
|
||||
context.tryStartActivity(intent)
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package de.mm20.launcher2.searchactions.actions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
|
||||
data class EmailAction(
|
||||
override val label: String,
|
||||
val email: String,
|
||||
) : SearchAction {
|
||||
override val icon: SearchActionIcon = SearchActionIcon.Email
|
||||
override val iconColor: Int = 0
|
||||
override val customIcon: String? = null
|
||||
override fun start(context: Context) {
|
||||
val intent = Intent(Intent.ACTION_SENDTO).apply {
|
||||
type = "*/*"
|
||||
data = Uri.parse("mailto:$email")
|
||||
}
|
||||
context.tryStartActivity(intent)
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package de.mm20.launcher2.searchactions.actions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
|
||||
data class MessageAction(
|
||||
override val label: String,
|
||||
val number: String,
|
||||
): SearchAction {
|
||||
override val icon: SearchActionIcon = SearchActionIcon.Message
|
||||
override val iconColor: Int = 0
|
||||
override val customIcon: String? = null
|
||||
override fun start(context: Context) {
|
||||
val intent = Intent(Intent.ACTION_SENDTO).apply {
|
||||
data = Uri.parse("sms:$number")
|
||||
}
|
||||
context.tryStartActivity(intent)
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package de.mm20.launcher2.searchactions.actions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
|
||||
data class OpenUrlAction(
|
||||
override val label: String,
|
||||
val url: String,
|
||||
override val icon: SearchActionIcon = SearchActionIcon.Website,
|
||||
override val iconColor: Int = 0,
|
||||
override val customIcon: String? = null,
|
||||
) : SearchAction {
|
||||
|
||||
|
||||
override fun start(context: Context) {
|
||||
val url =
|
||||
if (url.startsWith("https://") || url.startsWith("http://")) url else "https://$url"
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
data = Uri.parse(url)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
}
|
||||
context.tryStartActivity(intent)
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package de.mm20.launcher2.searchactions.actions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.provider.CalendarContract
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.LocalTime
|
||||
import java.time.ZoneId
|
||||
|
||||
data class ScheduleEventAction(
|
||||
override val label: String,
|
||||
val date: LocalDate,
|
||||
val time: LocalTime?,
|
||||
) : SearchAction {
|
||||
override val icon: SearchActionIcon = SearchActionIcon.Calendar
|
||||
override val iconColor: Int = 0
|
||||
override val customIcon: String? = null
|
||||
override fun start(context: Context) {
|
||||
|
||||
val startTime = date.let {
|
||||
if (time != null) it.atTime(time)
|
||||
else it.atTime(0, 0)
|
||||
}.atZone(ZoneId.systemDefault()).toEpochSecond() * 1000L
|
||||
|
||||
val intent = Intent(Intent.ACTION_INSERT).apply {
|
||||
type = "vnd.android.cursor.dir/event"
|
||||
putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, startTime)
|
||||
if (time == null) putExtra(CalendarContract.EXTRA_EVENT_ALL_DAY, true)
|
||||
}
|
||||
context.tryStartActivity(intent)
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package de.mm20.launcher2.searchactions.actions
|
||||
|
||||
import android.content.Context
|
||||
import de.mm20.launcher2.search.Searchable
|
||||
|
||||
interface SearchAction : Searchable {
|
||||
val label: String
|
||||
val icon: SearchActionIcon
|
||||
val iconColor: Int
|
||||
val customIcon: String?
|
||||
fun start(context: Context)
|
||||
}
|
||||
|
||||
enum class SearchActionIcon(private val value: Int) {
|
||||
Search(0),
|
||||
Custom(1),
|
||||
Website(2),
|
||||
Alarm(3),
|
||||
Timer(4),
|
||||
Contact(5),
|
||||
Phone(6),
|
||||
Email(7),
|
||||
Message(8),
|
||||
Calendar(9),
|
||||
Translate(10),
|
||||
WebSearch(11),
|
||||
PersonSearch(12),
|
||||
StatsSearch(13),
|
||||
SearchPage(14),
|
||||
SearchList(15),
|
||||
ImageSearch(16),
|
||||
Location(17),
|
||||
Movie(18),
|
||||
Music(19),
|
||||
Game(20),
|
||||
Note(21);
|
||||
fun toInt(): Int {
|
||||
return value
|
||||
}
|
||||
companion object {
|
||||
fun fromInt(value: Int?): SearchActionIcon {
|
||||
return values().firstOrNull { it.value == value } ?: Search
|
||||
}
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package de.mm20.launcher2.searchactions.actions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.provider.AlarmClock
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import java.time.LocalTime
|
||||
|
||||
data class SetAlarmAction(
|
||||
override val label: String,
|
||||
val time: LocalTime
|
||||
) : SearchAction {
|
||||
override val icon: SearchActionIcon = SearchActionIcon.Alarm
|
||||
override val iconColor: Int = 0
|
||||
override val customIcon: String? = null
|
||||
override fun start(context: Context) {
|
||||
val intent = Intent(AlarmClock.ACTION_SET_ALARM).apply {
|
||||
putExtra(AlarmClock.EXTRA_HOUR, time.hour)
|
||||
putExtra(AlarmClock.EXTRA_MINUTES, time.minute)
|
||||
}
|
||||
context.tryStartActivity(intent)
|
||||
}
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
package de.mm20.launcher2.searchactions.actions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.provider.AlarmClock
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import java.time.Duration
|
||||
|
||||
data class TimerAction(
|
||||
override val label: String,
|
||||
val length: Duration
|
||||
): SearchAction {
|
||||
|
||||
override val icon: SearchActionIcon = SearchActionIcon.Timer
|
||||
override val iconColor: Int = 0
|
||||
override val customIcon: String? = null
|
||||
override fun start(context: Context) {
|
||||
val intent = Intent(AlarmClock.ACTION_SET_TIMER).apply {
|
||||
putExtra(AlarmClock.EXTRA_LENGTH, length.seconds.toInt())
|
||||
}
|
||||
context.tryStartActivity(intent)
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package de.mm20.launcher2.searchactions.builders
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
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
|
||||
|
||||
data class AppSearchActionBuilder(
|
||||
override val label: String,
|
||||
val baseIntent: Intent,
|
||||
override val icon: SearchActionIcon = SearchActionIcon.Custom,
|
||||
override val iconColor: Int = 0,
|
||||
override val customIcon: String? = null,
|
||||
) : CustomizableSearchActionBuilder {
|
||||
|
||||
override val key: String
|
||||
get() = "app://${baseIntent.toUri(0)}"
|
||||
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction {
|
||||
return AppSearchAction(
|
||||
label = label,
|
||||
baseIntent = baseIntent,
|
||||
query = classifiedQuery.text,
|
||||
icon = icon,
|
||||
iconColor = iconColor,
|
||||
customIcon = customIcon,
|
||||
)
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package de.mm20.launcher2.searchactions.builders
|
||||
|
||||
import android.content.Context
|
||||
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
|
||||
|
||||
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) {
|
||||
return CallAction(
|
||||
context.getString(R.string.search_action_call), classifiedQuery.phoneNumber
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
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.CreateContactAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
||||
|
||||
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) {
|
||||
return CreateContactAction(
|
||||
context.getString(R.string.search_action_contact),
|
||||
phone = classifiedQuery.phoneNumber,
|
||||
email = classifiedQuery.email,
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package de.mm20.launcher2.searchactions.builders
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import de.mm20.launcher2.searchactions.TextClassificationResult
|
||||
import de.mm20.launcher2.searchactions.actions.CustomIntentAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
||||
|
||||
data class CustomIntentActionBuilder(
|
||||
override val label: String,
|
||||
val queryKey: String,
|
||||
val baseIntent: Intent,
|
||||
override val icon: SearchActionIcon = SearchActionIcon.Custom,
|
||||
override val iconColor: Int = 1,
|
||||
override val customIcon: String? = null,
|
||||
) : CustomizableSearchActionBuilder {
|
||||
override val key: String
|
||||
get() = "intent://${baseIntent.toUri(0)}"
|
||||
|
||||
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction {
|
||||
return CustomIntentAction(
|
||||
label, classifiedQuery.text, queryKey, baseIntent, icon, iconColor, customIcon
|
||||
)
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
package de.mm20.launcher2.searchactions.builders
|
||||
|
||||
sealed interface CustomizableSearchActionBuilder: SearchActionBuilder
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
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.EmailAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
||||
|
||||
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) {
|
||||
return EmailAction(
|
||||
context.getString(R.string.search_action_email), classifiedQuery.email
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
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.MessageAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
||||
|
||||
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) {
|
||||
return MessageAction(
|
||||
context.getString(R.string.search_action_message), classifiedQuery.phoneNumber
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
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.MessageAction
|
||||
import de.mm20.launcher2.searchactions.actions.OpenUrlAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
||||
|
||||
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) {
|
||||
return OpenUrlAction(
|
||||
context.getString(R.string.search_action_open_url), classifiedQuery.url
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
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.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
|
||||
|
||||
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) {
|
||||
return ScheduleEventAction(
|
||||
context.getString(R.string.search_action_event),
|
||||
date = classifiedQuery.date,
|
||||
time = classifiedQuery.time
|
||||
)
|
||||
}
|
||||
if (classifiedQuery.timespan != null && classifiedQuery.timespan.seconds > 86400) {
|
||||
val datetime = LocalDateTime.now().plus(classifiedQuery.timespan)
|
||||
return ScheduleEventAction(
|
||||
context.getString(R.string.search_action_event),
|
||||
date = datetime.toLocalDate(),
|
||||
time = datetime.toLocalTime(),
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package de.mm20.launcher2.searchactions.builders
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
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
|
||||
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 {
|
||||
internal fun from(context: Context, entity: SearchActionEntity): SearchActionBuilder? {
|
||||
val options = entity.options?.let {
|
||||
try {
|
||||
JSONObject(it)
|
||||
} catch (_: JSONException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
when (entity.type) {
|
||||
"url" -> {
|
||||
return WebsearchActionBuilder(
|
||||
label = entity.label ?: "",
|
||||
urlTemplate = entity.data ?: return null,
|
||||
iconColor = entity.color ?: 0,
|
||||
icon = SearchActionIcon.fromInt(entity.icon),
|
||||
customIcon = entity.customIcon,
|
||||
encoding = WebsearchActionBuilder.QueryEncoding.fromInt(options?.optInt("encoding"))
|
||||
)
|
||||
}
|
||||
"app" -> {
|
||||
return AppSearchActionBuilder(
|
||||
label = entity.label ?: "",
|
||||
baseIntent = Intent.parseUri(entity.data, 0),
|
||||
iconColor = entity.color ?: 0,
|
||||
icon = SearchActionIcon.fromInt(entity.icon),
|
||||
customIcon = entity.customIcon,
|
||||
)
|
||||
}
|
||||
"intent" -> {
|
||||
return CustomIntentActionBuilder(
|
||||
entity.label ?: "",
|
||||
baseIntent = Intent.parseUri(entity.data, 0),
|
||||
iconColor = entity.color ?: 0,
|
||||
icon = SearchActionIcon.fromInt(entity.icon),
|
||||
customIcon = entity.customIcon,
|
||||
queryKey = options?.getString("extra")?.takeIf { it.isNotEmpty() } ?: 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 -> SearchActionEntity(
|
||||
position = position,
|
||||
type = "app",
|
||||
label = builder.label,
|
||||
data = builder.baseIntent.toUri(0),
|
||||
color = builder.iconColor,
|
||||
icon = builder.icon.toInt(),
|
||||
customIcon = builder.customIcon,
|
||||
options = null
|
||||
)
|
||||
is CustomIntentActionBuilder -> SearchActionEntity(
|
||||
position = position,
|
||||
type = "intent",
|
||||
label = builder.label,
|
||||
data = builder.baseIntent.toUri(0),
|
||||
color = builder.iconColor,
|
||||
icon = builder.icon.toInt(),
|
||||
customIcon = builder.customIcon,
|
||||
options = jsonObjectOf(
|
||||
"extra" to builder.queryKey
|
||||
).toString()
|
||||
)
|
||||
else -> SearchActionEntity(
|
||||
position = position,
|
||||
type = builder.key,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
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.SearchAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
||||
import de.mm20.launcher2.searchactions.actions.SetAlarmAction
|
||||
import java.time.LocalDate
|
||||
|
||||
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) {
|
||||
return SetAlarmAction(
|
||||
context.getString(R.string.search_action_alarm), classifiedQuery.time
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
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.SearchAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
||||
import de.mm20.launcher2.searchactions.actions.TimerAction
|
||||
|
||||
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) {
|
||||
return TimerAction(
|
||||
context.getString(R.string.search_action_timer), classifiedQuery.timespan
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package de.mm20.launcher2.searchactions.builders
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import de.mm20.launcher2.searchactions.TextClassificationResult
|
||||
import de.mm20.launcher2.searchactions.actions.OpenUrlAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
||||
import java.net.URLEncoder
|
||||
|
||||
data class WebsearchActionBuilder(
|
||||
override val label: String,
|
||||
val urlTemplate: String,
|
||||
override val icon: SearchActionIcon = SearchActionIcon.Search,
|
||||
override val iconColor: Int = 0,
|
||||
override val customIcon: String? = null,
|
||||
val encoding: QueryEncoding = QueryEncoding.UrlEncode,
|
||||
) : 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))
|
||||
return OpenUrlAction(
|
||||
label = label,
|
||||
url = url,
|
||||
icon = icon,
|
||||
customIcon = customIcon,
|
||||
iconColor = iconColor,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private fun encodeQuery(query: String, encoding: QueryEncoding): String {
|
||||
return when (encoding) {
|
||||
QueryEncoding.UrlEncode -> Uri.encode(query)
|
||||
QueryEncoding.FormData -> URLEncoder.encode(query, "UTF-8")
|
||||
QueryEncoding.None -> query
|
||||
}
|
||||
}
|
||||
|
||||
enum class QueryEncoding {
|
||||
UrlEncode,
|
||||
FormData,
|
||||
None;
|
||||
|
||||
fun toInt(): Int {
|
||||
return when (this) {
|
||||
UrlEncode -> 0
|
||||
FormData -> 1
|
||||
None -> 2
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromInt(value: Int?): QueryEncoding {
|
||||
return when (value) {
|
||||
1 -> FormData
|
||||
2 -> None
|
||||
else -> UrlEncode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user