Reorganize and group modules

This commit is contained in:
MM20
2022-12-13 17:37:26 +01:00
parent bac24baad2
commit 3f8880a90a
995 changed files with 501 additions and 298 deletions
+1
View File
@@ -0,0 +1 @@
/build
+48
View File
@@ -0,0 +1,48 @@
plugins {
id("com.android.library")
id("kotlin-android")
}
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 {
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
namespace = "de.mm20.launcher2.data.customattrs"
}
dependencies {
implementation(libs.bundles.kotlin)
implementation(libs.androidx.core)
implementation(libs.koin.android)
implementation(project(":core:database"))
implementation(project(":core:base"))
implementation(project(":core:ktx"))
implementation(project(":core:crashreporter"))
implementation(project(":data:favorites"))
}
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.
#
# 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
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
@@ -0,0 +1,188 @@
package de.mm20.launcher2.data.customattrs
import android.util.Log
import de.mm20.launcher2.database.entities.CustomAttributeEntity
import de.mm20.launcher2.ktx.jsonObjectOf
import org.json.JSONObject
sealed interface CustomAttribute {
fun toDatabaseEntity(key: String): CustomAttributeEntity
companion object {
internal fun fromDatabaseEntity(entity: CustomAttributeEntity?): CustomAttribute? {
if (entity == null) return null
return when (entity.type) {
CustomAttributeType.Label.value -> CustomLabel(
label = entity.value,
key = entity.key
)
CustomAttributeType.Tag.value -> CustomTag(
tagName = entity.value
)
CustomAttributeType.Icon.value -> CustomIcon.fromDatabaseEntity(entity)
else -> {
Log.e("MM20", "Invalid custom attribute type: ${entity.type}")
null
}
}
}
}
}
class CustomLabel(
val key: String,
val label: String,
) : CustomAttribute {
override fun toDatabaseEntity(key: String): CustomAttributeEntity {
return CustomAttributeEntity(
key = key,
type = CustomAttributeType.Label.value,
value = label,
)
}
}
class CustomTag(
val tagName: String
): CustomAttribute {
override fun toDatabaseEntity(key: String): CustomAttributeEntity {
return CustomAttributeEntity(
key = key,
type = CustomAttributeType.Tag.value,
value = tagName,
)
}
}
sealed class CustomIcon : CustomAttribute {
override fun toDatabaseEntity(key: String): CustomAttributeEntity {
return CustomAttributeEntity(
key = key,
type = CustomAttributeType.Icon.value,
value = this.toDatabaseValue()
)
}
internal abstract fun toDatabaseValue(): String
companion object {
internal fun fromDatabaseEntity(entity: CustomAttributeEntity): CustomIcon? {
val payload = JSONObject(entity.value)
val type = payload.getString("type")
return when (type) {
"custom_icon_pack_icon" -> {
CustomIconPackIcon(
iconComponentName = payload.getString("icon"),
iconPackPackage = payload.getString("icon_pack")
)
}
"custom_themed_icon" -> {
CustomThemedIcon(
iconPackageName = payload.getString("icon"),
)
}
"default_icon" -> {
UnmodifiedSystemDefaultIcon
}
"adaptified_legacy_icon" -> {
AdaptifiedLegacyIcon(
fgScale = payload.getDouble("fg_scale").toFloat(),
bgColor = payload.getInt("bg_color")
)
}
"force_themed_icon" -> ForceThemedIcon
"default_placeholder_icon" -> DefaultPlaceholderIcon
else -> null
}
}
}
}
data class CustomIconPackIcon(
val iconPackPackage: String,
val iconComponentName: String,
) : CustomIcon() {
override fun toDatabaseValue(): String {
return jsonObjectOf(
"type" to "custom_icon_pack_icon",
"icon" to iconComponentName,
"icon_pack" to iconPackPackage,
).toString()
}
}
data class AdaptifiedLegacyIcon(
val fgScale: Float,
/**
* The background color in ARGB format or [UnspecifiedColor] or [ThemeColor]
*/
val bgColor: Int = UnspecifiedColor,
): CustomIcon() {
override fun toDatabaseValue(): String {
return jsonObjectOf(
"type" to "adaptified_legacy_icon",
"fg_scale" to fgScale,
"bg_color" to bgColor,
).toString()
}
companion object {
/**
* Extract color from foreground icon
*/
const val UnspecifiedColor = 1
/**
* Use color from theme
*/
const val ThemeColor = 0
}
}
data class CustomThemedIcon(
val iconPackageName: String,
) : CustomIcon() {
override fun toDatabaseValue(): String {
return jsonObjectOf(
"type" to "custom_themed_icon",
"icon" to iconPackageName,
).toString()
}
}
object ForceThemedIcon : CustomIcon() {
override fun toDatabaseValue(): String {
return jsonObjectOf(
"type" to "force_themed_icon"
).toString()
}
}
/**
* Use default icon, ignore any icon pack, themed icon or force adaptive settings.
*/
object UnmodifiedSystemDefaultIcon: CustomIcon() {
override fun toDatabaseValue(): String {
return jsonObjectOf(
"type" to "default_icon"
).toString()
}
}
/**
* Use the default placeholder icon
*/
object DefaultPlaceholderIcon: CustomIcon() {
override fun toDatabaseValue(): String {
return jsonObjectOf(
"type" to "default_placeholder_icon"
).toString()
}
}
@@ -0,0 +1,13 @@
package de.mm20.launcher2.data.customattrs
enum class CustomAttributeType(val value: String) {
Icon("icon"),
Label("label"),
Tag("tag");
companion object {
internal fun fromValue(value: String): CustomAttributeType {
return values().first { it.value == value }
}
}
}
@@ -0,0 +1,225 @@
package de.mm20.launcher2.data.customattrs
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase
import de.mm20.launcher2.database.entities.CustomAttributeEntity
import de.mm20.launcher2.favorites.FavoritesRepository
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.search.SavableSearchable
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import org.json.JSONArray
import org.json.JSONException
import java.io.File
interface CustomAttributesRepository {
fun search(query: String): Flow<ImmutableList<SavableSearchable>>
fun getCustomIcon(searchable: SavableSearchable): Flow<CustomIcon?>
fun setCustomIcon(searchable: SavableSearchable, icon: CustomIcon?)
fun getCustomLabels(items: List<SavableSearchable>): Flow<List<CustomLabel>>
fun setCustomLabel(searchable: SavableSearchable, label: String)
fun clearCustomLabel(searchable: SavableSearchable)
fun setTags(searchable: SavableSearchable, tags: List<String>)
fun getTags(searchable: SavableSearchable): Flow<List<String>>
suspend fun export(toDir: File)
suspend fun import(fromDir: File)
suspend fun getAllTags(startsWith: String? = null): List<String>
fun getItemsForTag(tag: String): Flow<List<SavableSearchable>>
fun addTag(item: SavableSearchable, tag: String)
fun renameTag(oldName: String, newName: String)
suspend fun cleanupDatabase(): Int
}
internal class CustomAttributesRepositoryImpl(
private val appDatabase: AppDatabase,
private val favoritesRepository: FavoritesRepository
) : CustomAttributesRepository {
private val scope = CoroutineScope(Job() + Dispatchers.Default)
override fun getCustomIcon(searchable: SavableSearchable): Flow<CustomIcon?> {
val dao = appDatabase.customAttrsDao()
return dao.getCustomAttribute(searchable.key, CustomAttributeType.Icon.value)
.map {
CustomAttribute.fromDatabaseEntity(it) as? CustomIcon
}
}
override fun setCustomIcon(searchable: SavableSearchable, icon: CustomIcon?) {
val dao = appDatabase.customAttrsDao()
scope.launch {
dao.clearCustomAttribute(searchable.key, CustomAttributeType.Icon.value)
if (icon != null) {
dao.setCustomAttribute(icon.toDatabaseEntity(searchable.key))
}
}
}
override fun getCustomLabels(items: List<SavableSearchable>): Flow<List<CustomLabel>> {
val dao = appDatabase.customAttrsDao()
return dao.getCustomAttributes(items.map { it.key }, CustomAttributeType.Label.value)
.map { list ->
list.mapNotNull { CustomAttribute.fromDatabaseEntity(it) as? CustomLabel }
}
}
override fun setCustomLabel(searchable: SavableSearchable, label: String) {
val dao = appDatabase.customAttrsDao()
scope.launch {
favoritesRepository.save(searchable)
appDatabase.runInTransaction {
dao.clearCustomAttribute(searchable.key, CustomAttributeType.Label.value)
dao.setCustomAttribute(
CustomLabel(
key = searchable.key,
label = label,
).toDatabaseEntity(searchable.key)
)
}
}
}
override fun clearCustomLabel(searchable: SavableSearchable) {
val dao = appDatabase.customAttrsDao()
scope.launch {
dao.clearCustomAttribute(searchable.key, CustomAttributeType.Label.value)
}
}
override fun setTags(searchable: SavableSearchable, tags: List<String>) {
val dao = appDatabase.customAttrsDao()
scope.launch {
favoritesRepository.save(searchable)
dao.setTags(searchable.key, tags.map {
CustomTag(it).toDatabaseEntity(searchable.key)
})
}
}
override fun getTags(searchable: SavableSearchable): Flow<List<String>> {
val dao = appDatabase.customAttrsDao()
return dao.getCustomAttributes(listOf(searchable.key), CustomAttributeType.Tag.value).map {
it.map { it.value }
}
}
override suspend fun getAllTags(startsWith: String?): List<String> {
val dao = appDatabase.customAttrsDao()
return if (startsWith != null) {
dao.getAllTagsLike("$startsWith%")
} else {
dao.getAllTags()
}
}
override fun getItemsForTag(tag: String): Flow<List<SavableSearchable>> {
val dao = appDatabase.customAttrsDao()
return dao.getItemsWithTag(tag).map {
favoritesRepository.getFromKeys(it)
}
}
override fun addTag(item: SavableSearchable, tag: String) {
val dao = appDatabase.customAttrsDao()
scope.launch {
dao.addTag(item.key, tag)
}
}
override fun renameTag(oldName: String, newName: String) {
val dao = appDatabase.customAttrsDao()
scope.launch {
dao.renameTag(oldName, newName)
}
}
override fun search(query: String): Flow<ImmutableList<SavableSearchable>> {
if (query.isBlank()) {
return flow {
emit(persistentListOf())
}
}
val dao = appDatabase.customAttrsDao()
return dao.search("%$query%").map {
favoritesRepository.getFromKeys(it).toImmutableList()
}
}
override suspend fun export(toDir: File) = withContext(Dispatchers.IO) {
val dao = appDatabase.backupDao()
var page = 0
do {
val customAttrs = dao.exportCustomAttributes(limit = 100, offset = page * 100)
val jsonArray = JSONArray()
for (customAttr in customAttrs) {
jsonArray.put(
jsonObjectOf(
"key" to customAttr.key,
"value" to customAttr.value,
"type" to customAttr.type,
)
)
}
val file = File(toDir, "customizations.${page.toString().padStart(4, '0')}")
file.bufferedWriter().use {
it.write(jsonArray.toString())
}
page++
} while (customAttrs.size == 100)
}
override suspend fun import(fromDir: File) = withContext(Dispatchers.IO) {
val dao = appDatabase.backupDao()
dao.wipeCustomAttributes()
val files =
fromDir.listFiles { _, name -> name.startsWith("customizations.") }
?: return@withContext
for (file in files) {
val customAttrs = mutableListOf<CustomAttributeEntity>()
try {
val jsonArray = JSONArray(file.inputStream().reader().readText())
for (i in 0 until jsonArray.length()) {
val json = jsonArray.getJSONObject(i)
val entity = CustomAttributeEntity(
id = null,
type = json.getString("type"),
value = json.optString("value"),
key = json.optString("key"),
)
customAttrs.add(entity)
}
dao.importCustomAttributes(customAttrs)
} catch (e: JSONException) {
CrashReporter.logException(e)
}
}
}
override suspend fun cleanupDatabase(): Int {
val dao = appDatabase.backupDao()
var removed = 0
val job = scope.launch {
removed = dao.cleanUp()
}
job.join()
return removed
}
}
@@ -0,0 +1,7 @@
package de.mm20.launcher2.data.customattrs
import org.koin.dsl.module
val customAttrsModule = module {
single<CustomAttributesRepository> { CustomAttributesRepositoryImpl(get(), get()) }
}
@@ -0,0 +1,25 @@
package de.mm20.launcher2.data.customattrs.utils
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
import de.mm20.launcher2.search.SavableSearchable
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.flow.collectLatest
fun <T: SavableSearchable>Flow<List<T>>.withCustomLabels(
customAttributesRepository: CustomAttributesRepository,
): Flow<List<T>> = channelFlow {
this@withCustomLabels.collectLatest { items ->
val customLabels = customAttributesRepository.getCustomLabels(items)
customLabels.collectLatest { labels ->
send(items.map { item ->
val customLabel = labels.find { it.key == item.key }
if (customLabel != null) {
item.overrideLabel(customLabel.label) as T
} else {
item
}
})
}
}
}