Initial commit
This commit is contained in:
@@ -0,0 +1 @@
|
||||
<manifest package="de.mm20.launcher2.search" />
|
||||
@@ -0,0 +1,50 @@
|
||||
package de.mm20.launcher2.search
|
||||
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
abstract class BaseSearchableRepository {
|
||||
|
||||
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||
private val searchQuery = SearchRepository.getInstance().currentQuery
|
||||
|
||||
init {
|
||||
searchQuery.observeForever {
|
||||
onQueryChange(it)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun onCancel() {
|
||||
|
||||
}
|
||||
|
||||
protected fun launch(
|
||||
context: CoroutineContext = EmptyCoroutineContext,
|
||||
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||
block: suspend CoroutineScope.() -> Unit
|
||||
) {
|
||||
scope.launch(context, start, block)
|
||||
}
|
||||
|
||||
private var searchJob: Job? = null
|
||||
private fun onQueryChange(query: String) {
|
||||
scope.launch {
|
||||
onCancel()
|
||||
searchJob?.takeIf { !it.isCompleted || !it.isCancelled }?.cancelAndJoin()
|
||||
searchJob = scope.launch {
|
||||
SearchRepository.getInstance().startSearch()
|
||||
search(query)
|
||||
}.also {
|
||||
it.invokeOnCompletion { SearchRepository.getInstance().endSearch() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the query string changes. This method should change the current data presented
|
||||
* by this SearchableRepository.
|
||||
*/
|
||||
protected abstract suspend fun search(query: String)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package de.mm20.launcher2.search
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
|
||||
class SearchRepository private constructor() {
|
||||
|
||||
val isSearching = MutableLiveData<Boolean>(false)
|
||||
val currentQuery = MutableLiveData<String>()
|
||||
|
||||
private var runningSearches = 0
|
||||
set(value) {
|
||||
synchronized(runningSearches) {
|
||||
field = value
|
||||
isSearching.value = value > 0
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun startSearch() {
|
||||
synchronized(runningSearches) {
|
||||
runningSearches++
|
||||
}
|
||||
}
|
||||
|
||||
fun endSearch() {
|
||||
synchronized(runningSearches) {
|
||||
runningSearches--
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private lateinit var instance: SearchRepository
|
||||
fun getInstance(): SearchRepository {
|
||||
if (!::instance.isInitialized) instance = SearchRepository()
|
||||
return instance
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package de.mm20.launcher2.search
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
|
||||
class SearchViewModel(app: Application) : AndroidViewModel(app) {
|
||||
private val repository = SearchRepository.getInstance()
|
||||
|
||||
val isSearching: LiveData<Boolean> = repository.isSearching
|
||||
|
||||
fun search(query: String) {
|
||||
repository.currentQuery.value = query
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package de.mm20.launcher2.search
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import de.mm20.launcher2.database.AppDatabase
|
||||
import de.mm20.launcher2.search.data.Websearch
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class WebsearchRepository private constructor(val context: Context) : BaseSearchableRepository() {
|
||||
|
||||
val websearches = MutableLiveData<List<Websearch>>(emptyList())
|
||||
|
||||
val allWebsearches = MediatorLiveData<List<Websearch>>()
|
||||
|
||||
init {
|
||||
val databaseWebsearches = AppDatabase.getInstance(context).searchDao().getWebSearchesLiveData()
|
||||
allWebsearches.addSource(databaseWebsearches) {
|
||||
allWebsearches.value = it.map { Websearch(it) }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun search(query: String) {
|
||||
if (query.isEmpty()) {
|
||||
websearches.value = emptyList()
|
||||
return
|
||||
}
|
||||
val searches = withContext(Dispatchers.IO) {
|
||||
AppDatabase.getInstance(context).searchDao().getWebSearches().map {
|
||||
Websearch(it, query)
|
||||
}
|
||||
}
|
||||
websearches.value = searches
|
||||
}
|
||||
|
||||
fun insertWebsearch(websearch: Websearch) {
|
||||
launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
AppDatabase.getInstance(context).searchDao().insertWebsearch(websearch.toDatabaseEntity())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteWebsearch(websearch: Websearch) {
|
||||
launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
AppDatabase.getInstance(context).searchDao().deleteWebsearch(websearch.toDatabaseEntity())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private lateinit var instance: WebsearchRepository
|
||||
|
||||
fun getInstance(context: Context): WebsearchRepository {
|
||||
if (!::instance.isInitialized) instance = WebsearchRepository(context.applicationContext)
|
||||
return instance
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package de.mm20.launcher2.search
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import de.mm20.launcher2.search.data.Websearch
|
||||
|
||||
class WebsearchViewModel(app:Application): AndroidViewModel(app) {
|
||||
|
||||
private val repository = WebsearchRepository.getInstance(app)
|
||||
|
||||
fun insertWebsearch(websearch: Websearch) {
|
||||
return repository.insertWebsearch(websearch)
|
||||
}
|
||||
|
||||
fun deleteWebsearch(websearch: Websearch) {
|
||||
repository.deleteWebsearch(websearch)
|
||||
}
|
||||
|
||||
val websearches = repository.websearches
|
||||
val allWebsearches = repository.allWebsearches
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package de.mm20.launcher2.search.data
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
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.search.R
|
||||
import java.text.Collator
|
||||
|
||||
abstract class Searchable : Comparable<Searchable> {
|
||||
|
||||
abstract val key: String
|
||||
abstract val label: String
|
||||
|
||||
open val badgeKey
|
||||
get() = key
|
||||
|
||||
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 try {
|
||||
context.startActivity(intent, options)
|
||||
true
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(context, context.getString(R.string.activity_not_found_searchable, label), Toast.LENGTH_SHORT).show()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
open suspend fun loadIconAsync(context: Context, size: Int): LauncherIcon? = null
|
||||
abstract fun getPlaceholderIcon(context: Context): LauncherIcon
|
||||
|
||||
override fun compareTo(other: Searchable): Int {
|
||||
return Collator.getInstance().apply { strength = Collator.SECONDARY }.compare(label, other.label)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return if (other is Searchable) key == other.key
|
||||
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)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package de.mm20.launcher2.search.data
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import de.mm20.launcher2.database.entities.WebsearchEntity
|
||||
import java.net.URLEncoder
|
||||
|
||||
class Websearch(
|
||||
var urlTemplate: String,
|
||||
var label: String,
|
||||
var color: Int,
|
||||
var icon: String?,
|
||||
var id: Long? = null,
|
||||
val query: String? = null
|
||||
) {
|
||||
|
||||
constructor(entity: WebsearchEntity, query: String? = null) : this(
|
||||
urlTemplate = entity.urlTemplate,
|
||||
label = entity.label,
|
||||
icon = entity.icon,
|
||||
color = entity.color,
|
||||
id = entity.id,
|
||||
query = query
|
||||
)
|
||||
|
||||
fun toDatabaseEntity(): WebsearchEntity {
|
||||
return WebsearchEntity(
|
||||
urlTemplate = urlTemplate,
|
||||
color = color,
|
||||
icon = icon,
|
||||
label = label,
|
||||
id = id
|
||||
)
|
||||
}
|
||||
|
||||
fun getLaunchIntent(): Intent? {
|
||||
if(query == null) return null
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
val encodedQuery = URLEncoder.encode(query, "utf8")
|
||||
val url = urlTemplate.replace("\${1}", encodedQuery)
|
||||
intent.data = Uri.parse(url)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user