Reorganize and group modules
This commit is contained in:
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -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"))
|
||||
}
|
||||
Vendored
+21
@@ -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()
|
||||
}
|
||||
}
|
||||
+13
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
+225
@@ -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
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user