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 @@
/build
+54
View File
@@ -0,0 +1,54 @@
plugins {
id("com.android.library")
id("kotlin-android")
id("kotlin-android-extensions")
}
android {
compileSdk = sdk.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = sdk.versions.minSdk.get().toInt()
targetSdk = sdk.versions.targetSdk.get().toInt()
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
}
dependencies {
implementation(libs.bundles.kotlin)
implementation(libs.androidx.core)
implementation(libs.androidx.appcompat)
implementation(project(":search"))
implementation(project(":calendar"))
implementation(project(":database"))
implementation(project(":preferences"))
implementation(project(":applications"))
implementation(project(":contacts"))
implementation(project(":ktx"))
implementation(project(":files"))
implementation(project(":websites"))
implementation(project(":wikipedia"))
}
View File
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
+1
View File
@@ -0,0 +1 @@
<manifest package="de.mm20.launcher2.favorites" />
@@ -0,0 +1,35 @@
package de.mm20.launcher2.favorites
import android.content.Context
import de.mm20.launcher2.database.entities.FavoritesItemEntity
import de.mm20.launcher2.search.data.Searchable
data class FavoritesItem(
val key: String,
/**
* null if searchable could not be deserialized (i.e. the app has been uninstalled)
*/
val searchable: Searchable?,
var launchCount: Int,
var pinPosition: Int,
var hidden: Boolean
){
constructor(context: Context, entity: FavoritesItemEntity) : this(
key = entity.key,
searchable = SearchableDeserializer(context).deserialize(entity.serializedSearchable),
launchCount = entity.launchCount,
pinPosition = entity.pinPosition,
hidden = entity.hidden
)
fun toDatabaseEntity(): FavoritesItemEntity {
return FavoritesItemEntity(
key = key,
serializedSearchable = searchable?.let { "${SearchableDeserializer.getTypePrefix(it)}#${it.serialize()}" } ?: "",
hidden = hidden,
pinPosition = pinPosition,
launchCount = launchCount
)
}
}
@@ -0,0 +1,218 @@
package de.mm20.launcher2.favorites
import android.content.Context
import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.database.entities.FavoritesItemEntity
import de.mm20.launcher2.ktx.ceilToInt
import de.mm20.launcher2.preferences.LauncherPreferences
import de.mm20.launcher2.search.BaseSearchableRepository
import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.search.data.Searchable
import kotlinx.coroutines.*
import kotlin.math.max
import kotlin.math.min
class FavoritesRepository private constructor(private val context: Context) : BaseSearchableRepository() {
private val scope = CoroutineScope(Job() + Dispatchers.Main)
private val favorites = MediatorLiveData<List<Searchable>>()
private val favoriteItems: LiveData<List<FavoritesItemEntity>> = MutableLiveData()
val hiddenItems = MediatorLiveData<List<Searchable>>()
private val pinnedFavorites = AppDatabase.getInstance(context).searchDao().getFavorites()
val pinnedCalendarEvents = MediatorLiveData<List<CalendarEvent>>()
private val reloadFavorites: (String) -> Unit = {
scope.launch {
if(!LauncherPreferences.instance.searchShowFavorites) {
favorites.value = emptyList()
return@launch
}
val favs = mutableListOf<Searchable>()
withContext(Dispatchers.IO) {
val dao = AppDatabase.getInstance(context).searchDao()
val favItems = pinnedFavorites.value ?: emptyList()
favs.addAll(favItems.mapNotNull {
val item = FavoritesItem(context, it)
if (item.searchable == null) {
dao.deleteByKey(item.key)
}
if (item.searchable is CalendarEvent) return@mapNotNull null
item.searchable
})
var favCount = (favs.size.toDouble() / columns).ceilToInt() * columns
if(favItems.size < columns) favCount += columns
val autoFavs = dao.getAutoFavorites(favCount - favs.size)
favs.addAll(autoFavs.mapNotNull {
val item = FavoritesItem(context, it)
if (item.searchable == null) {
dao.deleteByKey(item.key)
}
item.searchable
})
}
favorites.value = favs
}
}
private var columns = 1
init {
val hidden = AppDatabase.getInstance(context).searchDao().getHiddenItems()
hiddenItems.addSource(hidden) { h ->
hiddenItems.value = h.mapNotNull { FavoritesItem(context, it).searchable }
}
favorites.addSource(pinnedFavorites) {
reloadFavorites("")
}
pinnedCalendarEvents.addSource(pinnedFavorites) {
scope.launch {
val dao = AppDatabase.getInstance(context).searchDao()
pinnedCalendarEvents.value = it.filter { it.key.startsWith("calendar://") }.mapNotNull {
val item = FavoritesItem(context, it)
if (item.searchable == null) {
withContext(Dispatchers.IO) { dao.deleteByKey(item.key) }
}
item.searchable as? CalendarEvent
}
}
}
LauncherPreferences.instance.doOnPreferenceChange(
"search_show_favorites",
"search_auto_add_favorites",
action = reloadFavorites
)
}
fun isHidden(searchable: Searchable): LiveData<Boolean> {
return AppDatabase.getInstance(context).searchDao().isHidden(searchable.key)
}
fun getFavorites(columns: Int): LiveData<List<Searchable>> {
if (columns != this.columns) {
this.columns = columns
reloadFavorites("")
}
return favorites
}
override suspend fun search(query: String) {
if (query.isEmpty()) {
reloadFavorites("")
} else {
favorites.value = emptyList()
}
}
fun isPinned(searchable: Searchable): LiveData<Boolean> {
return AppDatabase.getInstance(context).searchDao().isPinned(searchable.key)
}
fun pinItem(searchable: Searchable) {
scope.launch {
withContext(Dispatchers.IO) {
val dao = AppDatabase.getInstance(context).searchDao()
val databaseItem = dao.getFavorite(searchable.key)
val favoritesItem = FavoritesItem(
key = searchable.key,
searchable = searchable,
launchCount = databaseItem?.launchCount ?: 0,
pinPosition = 1,
hidden = false
)
dao.insertReplaceExisting(favoritesItem.toDatabaseEntity())
}
}
}
fun unpinItem(searchable: Searchable) {
scope.launch {
withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).searchDao().unpinFavorite(searchable.key)
}
}
}
fun hideItem(searchable: Searchable) {
scope.launch {
withContext(Dispatchers.IO) {
val dao = AppDatabase.getInstance(context).searchDao()
val databaseItem = dao.getFavorite(searchable.key)
val favoritesItem = FavoritesItem(
key = searchable.key,
searchable = searchable,
launchCount = databaseItem?.launchCount ?: 0,
pinPosition = 0,
hidden = true
)
dao.insertReplaceExisting(favoritesItem.toDatabaseEntity())
}
}
}
fun unhideItem(searchable: Searchable) {
scope.launch {
withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).searchDao().unhideItem(searchable.key)
}
}
}
fun deleteItem(key: String) {
scope.launch {
withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).searchDao().deleteByKey(key)
}
}
}
fun incrementLaunchCount(searchable: Searchable) {
scope.launch {
withContext(Dispatchers.IO) {
val item = FavoritesItem(searchable.key, searchable, 0, 0, false)
AppDatabase.getInstance(context).searchDao().incrementLaunchCount(item.toDatabaseEntity())
}
}
}
suspend fun getAllFavoriteItems(): List<FavoritesItem> {
return withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).searchDao().getAllFavoriteItems().mapNotNull {
FavoritesItem(context, it).takeIf { it.searchable != null }
}
}
}
fun saveFavorites(favorites: MutableList<FavoritesItem>) {
scope.launch {
withContext(Dispatchers.IO) {
AppDatabase.getInstance(context).searchDao().saveFavorites(favorites.map { it.toDatabaseEntity() })
}
}
}
fun getTopFavorites(count: Int): LiveData<List<Searchable>> {
val favs = MediatorLiveData<List<Searchable>>()
favs.addSource(favorites) {
favs.value = it.subList(0, min(count, it.size))
}
return favs
}
companion object {
private lateinit var instance: FavoritesRepository
fun getInstance(context: Context): FavoritesRepository {
if (!::instance.isInitialized) instance = FavoritesRepository(context.applicationContext)
return instance
}
}
}
@@ -0,0 +1,58 @@
package de.mm20.launcher2.favorites
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import de.mm20.launcher2.search.data.CalendarEvent
import de.mm20.launcher2.search.data.Searchable
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class FavoritesViewModel(app: Application) : AndroidViewModel(app) {
val repository = FavoritesRepository.getInstance(app)
fun getTopFavorites(count: Int): LiveData<List<Searchable>> {
return repository.getTopFavorites(count)
}
fun getFavorites(columns: Int): LiveData<List<Searchable>> {
return repository.getFavorites(columns)
}
fun pinItem(searchable: Searchable) {
repository.pinItem(searchable)
}
fun unpinItem(searchable: Searchable) {
repository.unpinItem(searchable)
}
fun isPinned(searchable: Searchable): LiveData<Boolean> {
return repository.isPinned(searchable)
}
fun isHidden(searchable: Searchable): LiveData<Boolean> {
return repository.isHidden(searchable)
}
fun hideItem(searchable: Searchable) {
repository.hideItem(searchable)
}
fun unhideItem(searchable: Searchable) {
repository.unhideItem(searchable)
}
suspend fun getAllFavoriteItems(): List<FavoritesItem> {
return repository.getAllFavoriteItems()
}
fun saveFavorites(favorites: MutableList<FavoritesItem>) {
repository.saveFavorites(favorites)
}
val hiddenItems: LiveData<List<Searchable>> = repository.hiddenItems
val pinnedCalendarEvents: LiveData<List<CalendarEvent>> = repository.pinnedCalendarEvents
}
@@ -0,0 +1,47 @@
package de.mm20.launcher2.favorites
import android.content.Context
import android.util.Log
import de.mm20.launcher2.search.data.*
class SearchableDeserializer(val context: Context) {
fun deserialize(serialized: String?): Searchable? {
val type = serialized?.substringBefore("#") ?: return null
val data = serialized.substringAfter("#")
return when (type) {
"app" -> LauncherApp.deserialize(context, data)
"shortcut" -> AppShortcut.deserialize(context, data)
"calculator" -> null
"calendar" -> CalendarEvent.deserialize(context, data)
"contact" -> Contact.deserialize(context, data)
"gdrive" -> GDriveFile.deserialize(data)
"owncloud" -> OwncloudFile.deserialize(data)
"nextcloud" -> NextcloudFile.deserialize(data)
"file" -> File.deserialize(context, data)
"onedrive" -> OneDriveFile.deserialize(data)
"websearch" -> null
"website" -> Website.deserialize(data)
"wikipedia" -> Wikipedia.deserialize(data)
else -> null
}
}
companion object {
fun getTypePrefix(searchable: Searchable): String {
return when(searchable) {
is Application -> "app"
is AppShortcut -> "shortcut"
is CalendarEvent -> "calendar"
is Contact -> "contact"
is GDriveFile -> "gdrive"
is OneDriveFile -> "onedrive"
is NextcloudFile -> "nextcloud"
is OwncloudFile -> "owncloud"
is File -> "file"
is Website -> "website"
is Wikipedia -> "wikipedia"
else -> ""
}
}
}
}