v0.6.4
This is a test if updates work
This commit is contained in:
1
core/database/.gitignore
vendored
Normal file
1
core/database/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
17
core/database/build.gradle.kts
Normal file
17
core/database/build.gradle.kts
Normal file
@@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
alias(libs.plugins.looker.android.library)
|
||||
alias(libs.plugins.looker.room)
|
||||
alias(libs.plugins.looker.hilt)
|
||||
alias(libs.plugins.looker.serialization)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.looker.core.database"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
modules(Modules.coreCommon, Modules.coreDomain)
|
||||
|
||||
implementation(libs.kotlinx.coroutines.android)
|
||||
implementation(libs.androidx.core.ktx)
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 1,
|
||||
"identityHash": "b01a8fae755b8b96d36459e885dea04b",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "apps",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `repoId` INTEGER NOT NULL, `categories` BLOB NOT NULL, `summary` TEXT NOT NULL, `description` TEXT NOT NULL, `changelog` TEXT NOT NULL, `translation` TEXT NOT NULL, `issueTracker` TEXT NOT NULL, `sourceCode` TEXT NOT NULL, `binaries` TEXT NOT NULL, `name` TEXT NOT NULL, `authorName` TEXT NOT NULL, `authorEmail` TEXT NOT NULL, `authorWebSite` TEXT NOT NULL, `donate` TEXT NOT NULL, `liberapayID` TEXT NOT NULL, `liberapay` TEXT NOT NULL, `openCollective` TEXT NOT NULL, `bitcoin` TEXT NOT NULL, `litecoin` TEXT NOT NULL, `flattrID` TEXT NOT NULL, `suggestedVersionName` TEXT NOT NULL, `suggestedVersionCode` INTEGER NOT NULL, `license` TEXT NOT NULL, `webSite` TEXT NOT NULL, `added` INTEGER NOT NULL, `icon` TEXT NOT NULL, `phoneScreenshots` TEXT NOT NULL, `sevenInchScreenshots` TEXT NOT NULL, `tenInchScreenshots` TEXT NOT NULL, `wearScreenshots` TEXT NOT NULL, `tvScreenshots` TEXT NOT NULL, `featureGraphic` TEXT NOT NULL, `promoGraphic` TEXT NOT NULL, `tvBanner` TEXT NOT NULL, `video` TEXT NOT NULL, `lastUpdated` INTEGER NOT NULL, `packages` TEXT NOT NULL, PRIMARY KEY(`repoId`, `packageName`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "packageName",
|
||||
"columnName": "packageName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "repoId",
|
||||
"columnName": "repoId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "categories",
|
||||
"columnName": "categories",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "summary",
|
||||
"columnName": "summary",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "changelog",
|
||||
"columnName": "changelog",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "translation",
|
||||
"columnName": "translation",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "issueTracker",
|
||||
"columnName": "issueTracker",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sourceCode",
|
||||
"columnName": "sourceCode",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "binaries",
|
||||
"columnName": "binaries",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "authorName",
|
||||
"columnName": "authorName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "authorEmail",
|
||||
"columnName": "authorEmail",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "authorWebSite",
|
||||
"columnName": "authorWebSite",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "donate",
|
||||
"columnName": "donate",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "liberapayID",
|
||||
"columnName": "liberapayID",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "liberapay",
|
||||
"columnName": "liberapay",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "openCollective",
|
||||
"columnName": "openCollective",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "bitcoin",
|
||||
"columnName": "bitcoin",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "litecoin",
|
||||
"columnName": "litecoin",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "flattrID",
|
||||
"columnName": "flattrID",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "suggestedVersionName",
|
||||
"columnName": "suggestedVersionName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "suggestedVersionCode",
|
||||
"columnName": "suggestedVersionCode",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "license",
|
||||
"columnName": "license",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "webSite",
|
||||
"columnName": "webSite",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "added",
|
||||
"columnName": "added",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "icon",
|
||||
"columnName": "icon",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "phoneScreenshots",
|
||||
"columnName": "phoneScreenshots",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "sevenInchScreenshots",
|
||||
"columnName": "sevenInchScreenshots",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tenInchScreenshots",
|
||||
"columnName": "tenInchScreenshots",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "wearScreenshots",
|
||||
"columnName": "wearScreenshots",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tvScreenshots",
|
||||
"columnName": "tvScreenshots",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "featureGraphic",
|
||||
"columnName": "featureGraphic",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "promoGraphic",
|
||||
"columnName": "promoGraphic",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "tvBanner",
|
||||
"columnName": "tvBanner",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "video",
|
||||
"columnName": "video",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastUpdated",
|
||||
"columnName": "lastUpdated",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "packages",
|
||||
"columnName": "packages",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"repoId",
|
||||
"packageName"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "repos",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `enabled` INTEGER NOT NULL, `fingerprint` TEXT NOT NULL, `etag` TEXT NOT NULL, `username` TEXT NOT NULL, `password` TEXT NOT NULL, `address` TEXT NOT NULL, `mirrors` BLOB NOT NULL, `name` TEXT NOT NULL, `description` TEXT NOT NULL, `antiFeatures` TEXT NOT NULL, `categories` TEXT NOT NULL, `timestamp` INTEGER NOT NULL)",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "enabled",
|
||||
"columnName": "enabled",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "fingerprint",
|
||||
"columnName": "fingerprint",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "etag",
|
||||
"columnName": "etag",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "username",
|
||||
"columnName": "username",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "password",
|
||||
"columnName": "password",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "address",
|
||||
"columnName": "address",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "mirrors",
|
||||
"columnName": "mirrors",
|
||||
"affinity": "BLOB",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "description",
|
||||
"columnName": "description",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "antiFeatures",
|
||||
"columnName": "antiFeatures",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "categories",
|
||||
"columnName": "categories",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "timestamp",
|
||||
"columnName": "timestamp",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": true,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
},
|
||||
{
|
||||
"tableName": "InstalledEntity",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `versionCode` INTEGER NOT NULL, `signature` TEXT NOT NULL, PRIMARY KEY(`packageName`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "packageName",
|
||||
"columnName": "packageName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "versionCode",
|
||||
"columnName": "versionCode",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "signature",
|
||||
"columnName": "signature",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"packageName"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b01a8fae755b8b96d36459e885dea04b')"
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
core/database/src/main/assets/repo.db
Normal file
BIN
core/database/src/main/assets/repo.db
Normal file
Binary file not shown.
@@ -0,0 +1,94 @@
|
||||
package com.looker.core.database
|
||||
|
||||
import androidx.room.TypeConverter
|
||||
import com.looker.core.database.model.AntiFeatureEntity
|
||||
import com.looker.core.database.model.CategoryEntity
|
||||
import com.looker.core.database.model.LocalizedList
|
||||
import com.looker.core.database.model.LocalizedString
|
||||
import com.looker.core.database.model.PackageEntity
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.builtins.MapSerializer
|
||||
import kotlinx.serialization.builtins.serializer
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
}
|
||||
|
||||
internal const val STRING_DELIMITER = "!@#$%^&*"
|
||||
private val stringListSerializer = ListSerializer(String.serializer())
|
||||
private val localizedStringSerializer = MapSerializer(String.serializer(), String.serializer())
|
||||
private val localizedListSerializer = MapSerializer(String.serializer(), stringListSerializer)
|
||||
private val antiFeatureSerializer =
|
||||
MapSerializer(String.serializer(), AntiFeatureEntity.serializer())
|
||||
private val categorySerializer = MapSerializer(String.serializer(), CategoryEntity.serializer())
|
||||
private val packageListSerializer = ListSerializer(PackageEntity.serializer())
|
||||
|
||||
class CollectionConverter {
|
||||
|
||||
@TypeConverter
|
||||
fun listToString(list: List<String>): ByteArray =
|
||||
list.joinToString(STRING_DELIMITER).toByteArray()
|
||||
|
||||
@TypeConverter
|
||||
fun stringToList(byteArray: ByteArray): List<String> = String(byteArray).split(STRING_DELIMITER)
|
||||
}
|
||||
|
||||
class LocalizedConverter {
|
||||
|
||||
@TypeConverter
|
||||
fun localizedStringToJson(localizedEntity: LocalizedString): String =
|
||||
json.encodeToString(localizedStringSerializer, localizedEntity)
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToLocalizedString(jsonObject: String): LocalizedString =
|
||||
json.decodeFromString(localizedStringSerializer, jsonObject)
|
||||
|
||||
@TypeConverter
|
||||
fun localizedListToJson(localizedEntity: LocalizedList): String =
|
||||
json.encodeToString(localizedListSerializer, localizedEntity)
|
||||
|
||||
@TypeConverter
|
||||
fun jsonToLocalizedList(jsonObject: String): LocalizedList =
|
||||
json.decodeFromString(localizedListSerializer, jsonObject)
|
||||
}
|
||||
|
||||
class PackageEntityConverter {
|
||||
|
||||
@TypeConverter
|
||||
fun entityToString(packageEntity: PackageEntity): String =
|
||||
json.encodeToString(packageEntity)
|
||||
|
||||
@TypeConverter
|
||||
fun stringToPackage(jsonString: String): PackageEntity =
|
||||
json.decodeFromString(jsonString)
|
||||
|
||||
@TypeConverter
|
||||
fun entityListToString(packageEntity: List<PackageEntity>): String =
|
||||
json.encodeToString(packageListSerializer, packageEntity)
|
||||
|
||||
@TypeConverter
|
||||
fun stringToPackageList(jsonString: String): List<PackageEntity> =
|
||||
json.decodeFromString(packageListSerializer, jsonString)
|
||||
}
|
||||
|
||||
class RepoConverter {
|
||||
|
||||
@TypeConverter
|
||||
fun antiFeaturesToString(map: Map<String, AntiFeatureEntity>): String =
|
||||
json.encodeToString(antiFeatureSerializer, map)
|
||||
|
||||
@TypeConverter
|
||||
fun stringToAntiFeatures(string: String): Map<String, AntiFeatureEntity> =
|
||||
json.decodeFromString(antiFeatureSerializer, string)
|
||||
|
||||
@TypeConverter
|
||||
fun categoryToString(map: Map<String, CategoryEntity>): String =
|
||||
json.encodeToString(categorySerializer, map)
|
||||
|
||||
@TypeConverter
|
||||
fun stringToCategory(string: String): Map<String, CategoryEntity> =
|
||||
json.decodeFromString(categorySerializer, string)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.looker.core.database
|
||||
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import com.looker.core.database.dao.AppDao
|
||||
import com.looker.core.database.dao.InstalledDao
|
||||
import com.looker.core.database.dao.RepoDao
|
||||
import com.looker.core.database.model.AppEntity
|
||||
import com.looker.core.database.model.InstalledEntity
|
||||
import com.looker.core.database.model.RepoEntity
|
||||
|
||||
@Database(
|
||||
version = 1,
|
||||
entities = [
|
||||
AppEntity::class,
|
||||
RepoEntity::class,
|
||||
InstalledEntity::class
|
||||
]
|
||||
)
|
||||
@TypeConverters(
|
||||
CollectionConverter::class,
|
||||
LocalizedConverter::class,
|
||||
PackageEntityConverter::class,
|
||||
RepoConverter::class
|
||||
)
|
||||
abstract class DroidifyDatabase : RoomDatabase() {
|
||||
|
||||
abstract fun appDao(): AppDao
|
||||
|
||||
abstract fun repoDao(): RepoDao
|
||||
|
||||
abstract fun installedDao(): InstalledDao
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.looker.core.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import com.looker.core.database.model.AppEntity
|
||||
import com.looker.core.database.model.PackageEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface AppDao {
|
||||
|
||||
@Query(value = "SELECT * FROM apps")
|
||||
fun getAppStream(): Flow<List<AppEntity>>
|
||||
|
||||
@Query(
|
||||
value = """
|
||||
SELECT * FROM apps
|
||||
WHERE authorName = :authorName
|
||||
"""
|
||||
)
|
||||
fun getAppsFromAuthor(authorName: String): Flow<List<AppEntity>>
|
||||
|
||||
@Query(value = "SELECT * FROM apps WHERE packageName = :packageName")
|
||||
fun getApp(packageName: String): Flow<List<AppEntity>>
|
||||
|
||||
@Query(
|
||||
value = """
|
||||
SELECT packages FROM apps
|
||||
WHERE packageName = :packageName
|
||||
"""
|
||||
)
|
||||
fun getPackages(packageName: String): Flow<List<PackageEntity>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
suspend fun insertOrIgnore(apps: List<AppEntity>)
|
||||
|
||||
@Upsert
|
||||
suspend fun upsertApps(apps: List<AppEntity>)
|
||||
|
||||
@Query(
|
||||
value = """
|
||||
DELETE FROM apps
|
||||
WHERE repoId = :repoId
|
||||
"""
|
||||
)
|
||||
suspend fun deleteApps(repoId: Long)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.looker.core.database.dao
|
||||
|
||||
import androidx.room.*
|
||||
import com.looker.core.database.model.InstalledEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface InstalledDao {
|
||||
|
||||
@Query("SELECT * FROM installedentity")
|
||||
fun getInstalledStream(): Flow<List<InstalledEntity>>
|
||||
|
||||
@Upsert
|
||||
suspend fun upsertInstalled(installedEntity: InstalledEntity)
|
||||
|
||||
@Delete
|
||||
suspend fun deleteInstalled(installedEntity: InstalledEntity)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.looker.core.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Upsert
|
||||
import com.looker.core.database.model.RepoEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface RepoDao {
|
||||
|
||||
@Query(value = "SELECT * FROM repos")
|
||||
fun getRepoStream(): Flow<List<RepoEntity>>
|
||||
|
||||
@Query(value = "SELECT * FROM repos WHERE id = :id")
|
||||
suspend fun getRepoById(id: Long): RepoEntity
|
||||
|
||||
@Upsert
|
||||
suspend fun upsertRepo(repoEntity: RepoEntity)
|
||||
|
||||
@Query(
|
||||
value = """
|
||||
DELETE FROM repos
|
||||
WHERE id = :id
|
||||
"""
|
||||
)
|
||||
suspend fun deleteRepo(id: Long)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.looker.core.database.di
|
||||
|
||||
import com.looker.core.database.DroidifyDatabase
|
||||
import com.looker.core.database.dao.AppDao
|
||||
import com.looker.core.database.dao.InstalledDao
|
||||
import com.looker.core.database.dao.RepoDao
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DaoModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRepoDao(
|
||||
database: DroidifyDatabase
|
||||
): RepoDao = database.repoDao()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAppDao(
|
||||
database: DroidifyDatabase
|
||||
): AppDao = database.appDao()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideInstalledDao(
|
||||
database: DroidifyDatabase
|
||||
): InstalledDao = database.installedDao()
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.looker.core.database.di
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Room
|
||||
import com.looker.core.database.DroidifyDatabase
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DatabaseModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDroidifyDatabase(
|
||||
@ApplicationContext context: Context
|
||||
): DroidifyDatabase = Room.databaseBuilder(
|
||||
context,
|
||||
DroidifyDatabase::class.java,
|
||||
"droidify-database"
|
||||
).createFromAsset("repo.db").build()
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package com.looker.core.database.model
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import com.looker.core.common.nullIfEmpty
|
||||
import com.looker.core.domain.model.toPackageName
|
||||
import com.looker.core.database.utils.localizedValue
|
||||
import com.looker.core.domain.model.App
|
||||
import com.looker.core.domain.model.Author
|
||||
import com.looker.core.domain.model.Donation
|
||||
import com.looker.core.domain.model.Graphics
|
||||
import com.looker.core.domain.model.Links
|
||||
import com.looker.core.domain.model.Metadata
|
||||
import com.looker.core.domain.model.Screenshots
|
||||
|
||||
internal typealias LocalizedString = Map<String, String>
|
||||
internal typealias LocalizedList = Map<String, List<String>>
|
||||
|
||||
@Entity(tableName = "apps", primaryKeys = ["repoId", "packageName"])
|
||||
data class AppEntity(
|
||||
@ColumnInfo(name = "packageName")
|
||||
val packageName: String,
|
||||
@ColumnInfo(name = "repoId")
|
||||
val repoId: Long,
|
||||
val categories: List<String>,
|
||||
val summary: LocalizedString,
|
||||
val description: LocalizedString,
|
||||
val changelog: String,
|
||||
val translation: String,
|
||||
val issueTracker: String,
|
||||
val sourceCode: String,
|
||||
val binaries: String,
|
||||
val name: LocalizedString,
|
||||
val authorName: String,
|
||||
val authorEmail: String,
|
||||
val authorWebSite: String,
|
||||
val donate: String,
|
||||
val liberapayID: String,
|
||||
val liberapay: String,
|
||||
val openCollective: String,
|
||||
val bitcoin: String,
|
||||
val litecoin: String,
|
||||
val flattrID: String,
|
||||
val suggestedVersionName: String,
|
||||
val suggestedVersionCode: Long,
|
||||
val license: String,
|
||||
val webSite: String,
|
||||
val added: Long,
|
||||
val icon: LocalizedString,
|
||||
val phoneScreenshots: LocalizedList,
|
||||
val sevenInchScreenshots: LocalizedList,
|
||||
val tenInchScreenshots: LocalizedList,
|
||||
val wearScreenshots: LocalizedList,
|
||||
val tvScreenshots: LocalizedList,
|
||||
val featureGraphic: LocalizedString,
|
||||
val promoGraphic: LocalizedString,
|
||||
val tvBanner: LocalizedString,
|
||||
val video: LocalizedString,
|
||||
val lastUpdated: Long,
|
||||
val packages: List<PackageEntity>
|
||||
)
|
||||
|
||||
fun AppEntity.toExternal(locale: String, installed: PackageEntity? = null): App = App(
|
||||
repoId = repoId,
|
||||
categories = categories,
|
||||
links = links(),
|
||||
metadata = metadata(locale),
|
||||
screenshots = screenshots(locale),
|
||||
graphics = graphics(locale),
|
||||
author = author(),
|
||||
donation = donations(),
|
||||
packages = packages.toExternal(locale) { it == installed }
|
||||
)
|
||||
|
||||
fun List<AppEntity>.toExternal(
|
||||
locale: String,
|
||||
isInstalled: (AppEntity) -> PackageEntity?
|
||||
): List<App> = map {
|
||||
it.toExternal(locale, isInstalled(it))
|
||||
}
|
||||
|
||||
private fun AppEntity.author(): Author = Author(
|
||||
name = authorName,
|
||||
email = authorEmail,
|
||||
web = authorWebSite
|
||||
)
|
||||
|
||||
private fun AppEntity.donations(): Donation = Donation(
|
||||
regularUrl = donate.nullIfEmpty(),
|
||||
bitcoinAddress = bitcoin.nullIfEmpty(),
|
||||
flattrId = flattrID.nullIfEmpty(),
|
||||
liteCoinAddress = litecoin.nullIfEmpty(),
|
||||
openCollectiveId = openCollective.nullIfEmpty(),
|
||||
librePayId = liberapayID.nullIfEmpty(),
|
||||
librePayAddress = liberapay.nullIfEmpty()
|
||||
)
|
||||
|
||||
private fun AppEntity.graphics(locale: String): Graphics = Graphics(
|
||||
featureGraphic = featureGraphic.localizedValue(locale) ?: "",
|
||||
promoGraphic = promoGraphic.localizedValue(locale) ?: "",
|
||||
tvBanner = tvBanner.localizedValue(locale) ?: "",
|
||||
video = video.localizedValue(locale) ?: ""
|
||||
)
|
||||
|
||||
private fun AppEntity.links(): Links = Links(
|
||||
changelog = changelog,
|
||||
issueTracker = issueTracker,
|
||||
sourceCode = sourceCode,
|
||||
translation = translation,
|
||||
webSite = webSite
|
||||
)
|
||||
|
||||
private fun AppEntity.metadata(locale: String): Metadata = Metadata(
|
||||
name = name.localizedValue(locale) ?: "",
|
||||
packageName = packageName.toPackageName(),
|
||||
added = added,
|
||||
description = description.localizedValue(locale) ?: "",
|
||||
icon = icon.localizedValue(locale) ?: "",
|
||||
lastUpdated = lastUpdated,
|
||||
license = license,
|
||||
suggestedVersionCode = suggestedVersionCode,
|
||||
suggestedVersionName = suggestedVersionName,
|
||||
summary = summary.localizedValue(locale) ?: ""
|
||||
)
|
||||
|
||||
private fun AppEntity.screenshots(locale: String): Screenshots = Screenshots(
|
||||
phone = phoneScreenshots.localizedValue(locale) ?: emptyList(),
|
||||
sevenInch = sevenInchScreenshots.localizedValue(locale) ?: emptyList(),
|
||||
tenInch = tenInchScreenshots.localizedValue(locale) ?: emptyList(),
|
||||
tv = tvScreenshots.localizedValue(locale) ?: emptyList(),
|
||||
wear = wearScreenshots.localizedValue(locale) ?: emptyList()
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.looker.core.database.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity
|
||||
data class InstalledEntity(
|
||||
@PrimaryKey
|
||||
val packageName: String,
|
||||
val versionCode: Long,
|
||||
val signature: String
|
||||
)
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.looker.core.database.model
|
||||
|
||||
import com.looker.core.database.utils.localizedValue
|
||||
import com.looker.core.domain.model.ApkFile
|
||||
import com.looker.core.domain.model.Manifest
|
||||
import com.looker.core.domain.model.Package
|
||||
import com.looker.core.domain.model.Permission
|
||||
import com.looker.core.domain.model.Platforms
|
||||
import com.looker.core.domain.model.SDKs
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class PackageEntity(
|
||||
val added: Long,
|
||||
val apkName: String,
|
||||
val hash: String,
|
||||
val hashType: String,
|
||||
val minSdkVersion: Int,
|
||||
val maxSdkVersion: Int,
|
||||
val targetSdkVersion: Int,
|
||||
val sig: String,
|
||||
val signer: String,
|
||||
val size: Long,
|
||||
val srcName: String,
|
||||
val usesPermission: List<PermissionEntity>,
|
||||
val versionCode: Long,
|
||||
val versionName: String,
|
||||
val nativeCode: List<String>,
|
||||
val features: List<String>,
|
||||
val antiFeatures: List<String>,
|
||||
val whatsNew: LocalizedString
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PermissionEntity(
|
||||
val name: String,
|
||||
val minSdk: Int? = null,
|
||||
val maxSdk: Int? = null
|
||||
)
|
||||
|
||||
fun PackageEntity.toExternal(locale: String, installed: Boolean): Package = Package(
|
||||
installed = installed,
|
||||
added = added,
|
||||
apk = ApkFile(
|
||||
name = apkName,
|
||||
hash = hash,
|
||||
size = size
|
||||
),
|
||||
manifest = Manifest(
|
||||
versionCode = versionCode,
|
||||
versionName = versionName,
|
||||
usesSDKs = SDKs(minSdkVersion, targetSdkVersion),
|
||||
signer = setOf(signer),
|
||||
permissions = usesPermission.map(PermissionEntity::toExternalModel)
|
||||
),
|
||||
platforms = Platforms(nativeCode),
|
||||
features = features,
|
||||
antiFeatures = antiFeatures,
|
||||
whatsNew = whatsNew.localizedValue(locale) ?: ""
|
||||
)
|
||||
|
||||
fun List<PackageEntity>.toExternal(
|
||||
locale: String,
|
||||
installed: (PackageEntity) -> Boolean
|
||||
): List<Package> = map { it.toExternal(locale, installed(it)) }
|
||||
|
||||
fun PermissionEntity.toExternalModel(): Permission = Permission(
|
||||
name = name,
|
||||
sdKs = SDKs(min = minSdk ?: -1, max = maxSdk ?: -1)
|
||||
)
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.looker.core.database.model
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.looker.core.database.utils.localizedValue
|
||||
import com.looker.core.domain.model.AntiFeature
|
||||
import com.looker.core.domain.model.Authentication
|
||||
import com.looker.core.domain.model.Category
|
||||
import com.looker.core.domain.model.Fingerprint
|
||||
import com.looker.core.domain.model.Repo
|
||||
import com.looker.core.domain.model.VersionInfo
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Entity(tableName = "repos")
|
||||
data class RepoEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
val id: Long? = null,
|
||||
val enabled: Boolean,
|
||||
val fingerprint: String,
|
||||
val etag: String,
|
||||
val username: String,
|
||||
val password: String,
|
||||
val address: String,
|
||||
val mirrors: List<String>,
|
||||
val name: LocalizedString,
|
||||
val description: LocalizedString,
|
||||
val antiFeatures: Map<String, AntiFeatureEntity>,
|
||||
val categories: Map<String, CategoryEntity>,
|
||||
val timestamp: Long
|
||||
)
|
||||
|
||||
fun RepoEntity.update(repo: Repo) = copy(
|
||||
username = repo.authentication.username,
|
||||
password = repo.authentication.password,
|
||||
timestamp = repo.versionInfo.timestamp,
|
||||
enabled = repo.enabled,
|
||||
mirrors = repo.mirrors,
|
||||
fingerprint = repo.fingerprint?.value ?: ""
|
||||
)
|
||||
|
||||
fun RepoEntity.toExternal(locale: String): Repo = Repo(
|
||||
id = id!!,
|
||||
enabled = enabled,
|
||||
address = address,
|
||||
name = name.localizedValue(locale) ?: "",
|
||||
description = description.localizedValue(locale) ?: "",
|
||||
fingerprint = if (fingerprint.isBlank()) null else Fingerprint(fingerprint),
|
||||
authentication = Authentication(username, password),
|
||||
versionInfo = VersionInfo(timestamp = timestamp, etag = etag),
|
||||
mirrors = mirrors,
|
||||
categories = categories.values.toCategoryList(locale),
|
||||
antiFeatures = antiFeatures.values.toAntiFeatureList(locale)
|
||||
)
|
||||
|
||||
fun List<RepoEntity>.toExternal(locale: String): List<Repo> =
|
||||
map { it.toExternal(locale) }
|
||||
|
||||
@Serializable
|
||||
data class CategoryEntity(
|
||||
val icon: LocalizedString,
|
||||
val name: LocalizedString,
|
||||
val description: LocalizedString
|
||||
)
|
||||
|
||||
private fun CategoryEntity.toCategory(locale: String) =
|
||||
Category(
|
||||
name = name.localizedValue(locale) ?: "",
|
||||
icon = icon.localizedValue(locale) ?: "",
|
||||
description = description.localizedValue(locale) ?: ""
|
||||
)
|
||||
|
||||
fun Collection<CategoryEntity>.toCategoryList(locale: String): List<Category> =
|
||||
map { it.toCategory(locale) }
|
||||
|
||||
@Serializable
|
||||
data class AntiFeatureEntity(
|
||||
val icon: LocalizedString,
|
||||
val name: LocalizedString,
|
||||
val description: LocalizedString
|
||||
)
|
||||
|
||||
private fun AntiFeatureEntity.toAntiFeature(locale: String) =
|
||||
AntiFeature(
|
||||
name = name.localizedValue(locale) ?: "",
|
||||
icon = icon.localizedValue(locale) ?: "",
|
||||
description = description.localizedValue(locale) ?: ""
|
||||
)
|
||||
|
||||
fun Collection<AntiFeatureEntity>.toAntiFeatureList(locale: String): List<AntiFeature> =
|
||||
map { it.toAntiFeature(locale) }
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.looker.core.database.utils
|
||||
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import com.looker.core.common.stripBetween
|
||||
import java.util.Locale
|
||||
|
||||
internal fun localeListCompat(tag: String): LocaleListCompat =
|
||||
LocaleListCompat.forLanguageTags(tag)
|
||||
|
||||
/**
|
||||
* Find the Localized value from [Map<String,T>] using [locale]
|
||||
*
|
||||
* Returns null if none matches or map or [locale] is empty
|
||||
*/
|
||||
fun <T> Map<String, T>?.localizedValue(locale: String): T? {
|
||||
val localeList = localeListCompat(locale)
|
||||
if (isNullOrEmpty() || localeList.isEmpty) return null
|
||||
val suitableLocale = localeList.suitableLocale(keys)
|
||||
return get(suitableLocale)
|
||||
?: get("en_US")
|
||||
?: get("en-US")
|
||||
?: get("en")
|
||||
?: values.firstOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the most suitable Locale from the [keys] using [LocaleListCompat]
|
||||
*
|
||||
* Returns null if none found
|
||||
*/
|
||||
internal fun LocaleListCompat.suitableLocale(keys: Set<String>): String? = (0..<size())
|
||||
.asSequence()
|
||||
.mapNotNull { get(it).suitableTag(keys) }
|
||||
.firstOrNull()
|
||||
|
||||
/**
|
||||
* Get the suitable tag for [Locale] from [keys]
|
||||
*
|
||||
* Returns null if [keys] are empty or [Locale] in null
|
||||
*/
|
||||
internal fun Locale?.suitableTag(keys: Set<String>): String? {
|
||||
if (keys.isEmpty()) return null
|
||||
val currentLocale = this ?: return null
|
||||
val tag = currentLocale.toLanguageTag()
|
||||
val soloTag = currentLocale.language
|
||||
val strippedTag = tag.stripBetween("-")
|
||||
|
||||
return if (tag in keys) tag
|
||||
else if (strippedTag in keys) strippedTag
|
||||
else if (soloTag in keys) soloTag
|
||||
// try children of the language
|
||||
else keys.find { it.startsWith(soloTag) }
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package com.looker.core.database
|
||||
|
||||
import androidx.core.os.LocaleListCompat
|
||||
import androidx.core.os.LocaleListCompat.getEmptyLocaleList
|
||||
import com.looker.core.database.utils.localeListCompat
|
||||
import com.looker.core.database.utils.localizedValue
|
||||
import com.looker.core.database.utils.suitableLocale
|
||||
import com.looker.core.database.utils.suitableTag
|
||||
import org.junit.Test
|
||||
import java.util.Locale
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
/**
|
||||
*
|
||||
* This code is copyrighted to (F-Droid.org), I merely rewrote it.
|
||||
* Tests based on F-Droid's BestLocaleTest [https://gitlab.com/fdroid/fdroidclient/-/blob/680a1154cf3806390c2e4a9e95a7c6d6107b470f/libs/index/src/androidAndroidTest/kotlin/org/fdroid/BestLocaleTest.kt]
|
||||
*
|
||||
* https://developer.android.com/guide/topics/resources/multilingual-support#resource-resolution-examples
|
||||
*/
|
||||
class LocalizationTest {
|
||||
|
||||
@Test
|
||||
fun `Get correct localeList`() {
|
||||
assertEquals(
|
||||
LocaleListCompat.create(Locale.ENGLISH, Locale.US),
|
||||
localeListCompat("en,en-US")
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Return empty locale on none match`() {
|
||||
assertNull(emptyMap<String, String>().localizedValue("en-US,de-DE"))
|
||||
assertNull(getMap("en-US", "de-DE").localizedValue(""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Fallback to english`() {
|
||||
assertEquals(
|
||||
"en",
|
||||
getMap("de-AT", "de-DE", "en").localizedValue("fr-FR")
|
||||
)
|
||||
assertEquals(
|
||||
"en-US",
|
||||
getMap("en", "en-US").localizedValue("zh-Hant-TW,zh-Hans-CN")
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Use the first selected locale, en_US`() {
|
||||
assertEquals(
|
||||
"en-US",
|
||||
getMap("de-AT", "de-DE", "en-US").localizedValue("en-US,de-DE")
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Use the first en translation`() {
|
||||
assertEquals(
|
||||
"en-US",
|
||||
getMap("de-AT", "de-DE", "en-US").localizedValue("en-SE,de-DE")
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Use the first full match against a non-default locale`() {
|
||||
assertEquals(
|
||||
"de-AT",
|
||||
getMap(
|
||||
"de-AT",
|
||||
"de-DE",
|
||||
"en-GB",
|
||||
"en-US"
|
||||
).localizedValue("de-AT,de-DE")
|
||||
)
|
||||
assertEquals(
|
||||
"de",
|
||||
getMap("de-AT", "de", "en-GB", "en-US").localizedValue("de-CH,en-US")
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Stripped locale tag`() {
|
||||
assertEquals(
|
||||
"zh-TW",
|
||||
getMap(
|
||||
"en-US",
|
||||
"zh-CN",
|
||||
"zh-HK",
|
||||
"zh-TW"
|
||||
).localizedValue("zh-Hant-TW,zh-Hans-CN")
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Google specified test`() {
|
||||
assertEquals(
|
||||
"fr-FR",
|
||||
getMap("en-US", "de-DE", "es-ES", "fr-FR", "it-IT")
|
||||
.localizedValue("fr-CH")
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
"it-IT",
|
||||
getMap("en-US", "de-DE", "es-ES", "it-IT")
|
||||
.localizedValue("fr-CH,it-CH")
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Check null for suitable locale from list`() {
|
||||
assertNull(localeListCompat("en-US").suitableLocale(keys("de-DE", "es-ES")))
|
||||
assertNull(localeListCompat("en-US").suitableLocale(keys()))
|
||||
assertNull(getEmptyLocaleList().suitableLocale(keys("de-DE", "es-ES")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Find suitable locale from wrong list`() {
|
||||
assertNull(localeListCompat("en-US").suitableLocale(keys("de-DE", "es-ES")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Find suitable locale from list without modification`() {
|
||||
assertEquals(
|
||||
"en-US",
|
||||
localeListCompat("en-US").suitableLocale(keys("en", "en-US", "en-UK"))
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Find suitable locale from list only with language`() {
|
||||
assertEquals(
|
||||
"en",
|
||||
localeListCompat("en-US").suitableLocale(keys("de-DE", "fr-FR", "en-UK", "en"))
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Find stripped locale from the list`() {
|
||||
assertEquals(
|
||||
"zh-TW",
|
||||
localeListCompat("zh-Hant-TW").suitableLocale(
|
||||
keys(
|
||||
"en",
|
||||
"de-DE",
|
||||
"fr-FR",
|
||||
"zh-TW",
|
||||
"zh"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Check null for suitable locale`() {
|
||||
val locale: Locale? = null
|
||||
assertNull(locale.suitableTag(keys("en-US", "de-DE", "es-ES", "it-IT")))
|
||||
assertNull(Locale.ENGLISH.suitableTag(keys()))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Find suitable locale from wrong keys`() {
|
||||
assertNull(Locale.ENGLISH.suitableTag(keys("de-DE", "es-ES")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Get suitable locale without modification`() {
|
||||
assertEquals("en-US", Locale("en", "US").suitableTag(keys("en", "en-US", "en-UK")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Get suitable locale with only language`() {
|
||||
assertEquals("en", Locale("en", "US").suitableTag(keys("en", "de-DE", "fr-FR")))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Get suitable locale with stripped parts`() {
|
||||
assertEquals(
|
||||
"zh-TW",
|
||||
localeListCompat("zh-Hant-TW")[0].suitableTag(
|
||||
keys(
|
||||
"en",
|
||||
"de-DE",
|
||||
"fr-FR",
|
||||
"zh-TW",
|
||||
"zh"
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private fun keys(vararg tag: String): Set<String> = tag.toSet()
|
||||
|
||||
private fun getMap(vararg locales: String): Map<String, String> = locales.associateWith { it }
|
||||
}
|
||||
Reference in New Issue
Block a user