Initial commit

This commit is contained in:
MM20
2021-09-18 23:37:52 +02:00
commit 749e4e3073
938 changed files with 50475 additions and 0 deletions
+1
View File
@@ -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
}
}