diff --git a/.gitignore b/.gitignore
index d3fb6e4e9..5daeb6b97 100644
--- a/.gitignore
+++ b/.gitignore
@@ -113,3 +113,6 @@ Thumbs.db
!/gradle/wrapper/gradle-wrapper.jar
.idea/jarRepositories.xml
+
+
+app/src/release/agconnect-services.json
diff --git a/.travis.yml b/.travis.yml
index e8366be2a..26abd2025 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,8 +3,8 @@ jdk: oraclejdk8
env:
global:
- - ANDROID_API_LEVEL=29
- - ANDROID_BUILD_TOOLS_VERSION=29.0.3
+ - ANDROID_API_LEVEL=30
+ - ANDROID_BUILD_TOOLS_VERSION=30.0.2
cache:
directories:
@@ -14,7 +14,7 @@ cache:
branches:
only:
- develop
- - 0.20.1
+ - 0.22.2
android:
licenses:
@@ -28,32 +28,37 @@ android:
- build-tools-$ANDROID_BUILD_TOOLS_VERSION
# The SDK version used to compile your project
- android-$ANDROID_API_LEVEL
- # Additional components
+ # Additional components
- extra-google-google_play_services
- extra-google-m2repository
- extra-android-m2repository
- addon-google_apis-google-$ANDROID_API_LEVEL
- # Android emulator
+ # Android emulator
- android-22
- sys-img-armeabi-v7a-android-22
+before_install:
+ - yes | sdkmanager "platforms;android-30"
+ - yes | sdkmanager "build-tools;30.0.2"
+
before_script:
- # Launch emulator before the execution
- - echo no | android create avd --force -n test -t android-22 --abi armeabi-v7a
- - emulator -avd test -no-audio -no-window &
- - android-wait-for-emulator
- - adb shell input keyevent 82 &
- - "curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | sudo bash"
+ # Launch emulator before the execution
+ - echo no | android create avd --force -n test -t android-22 --abi armeabi-v7a
+ - emulator -avd test -no-audio -no-window &
+ - android-wait-for-emulator
+ - adb shell input keyevent 82 &
+ - "curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | sudo bash"
script:
- ./gradlew dependencies --stacktrace --daemon
- fossa --no-ansi || true
- - ./gradlew -Pcoverage testPlayDebugUnitTest --stacktrace --daemon
- - ./gradlew -Pcoverage createFdroidDebugCoverageReport --stacktrace --daemon
+ - ./gradlew -Pcoverage testFdroidDebugUnitTest --stacktrace --daemon
+ - ./gradlew -Pcoverage connectedFdroidDebugAndroidTest --stacktrace --daemon
- ./gradlew -Pcoverage jacocoTestReport --stacktrace --daemon
- |
if [ $TRAVIS_TAG ]; then
gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/google-services.json.gpg;
+ gpg --yes --batch --passphrase=$SERVICES_ENCRYPT_KEY ./app/src/release/agconnect-services.json.gpg;
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/key.p12.gpg;
gpg --yes --batch --passphrase=$ENCRYPT_KEY ./app/upload-key.jks.gpg;
./gradlew publishPlayRelease -PenableFirebase --stacktrace;
diff --git a/README.en.md b/README.en.md
index 28cce1c34..4c5e53da8 100644
--- a/README.en.md
+++ b/README.en.md
@@ -32,14 +32,17 @@ Unofficial android VULCAN UONET+ register client for both students and their par
## Download
-You can download the current beta version from the Google Play or the F-Droid store
+You can download the current beta version from the Google Play, F-Droid or Huawei AppGallery store
[
](https://play.google.com/store/apps/details?id=io.github.wulkanowy)
+ alt="Get it on Google Play"
+ height="80">](https://play.google.com/store/apps/details?id=io.github.wulkanowy)
[
](https://f-droid.org/packages/io.github.wulkanowy/)
+[
](https://appgallery.cloud.huawei.com/ag/n/app/C101440411?channelId=Badge&id=1b3f7fbb700849a9be0dba6b520b2282&s=EB1D3BF9ED9D1564D869B7B94B18016D3CABFCA5AEFB8E29F675FA04E0DC131D&detailType=0&v=)
You can also download a [development version](https://wulkanowy.github.io/#download) that includes new features being prepared for the next release
diff --git a/README.md b/README.md
index 02e1900c8..9e29cdb6c 100644
--- a/README.md
+++ b/README.md
@@ -32,14 +32,17 @@ Nieoficjalny klient dziennika VULCAN UONET+ dla ucznia i rodzica
## Pobierz
-Aktualną wersję beta możesz pobrać ze sklepu Google Play lub F-Droid
+Aktualną wersję beta możesz pobrać ze sklepu Google Play, F-Droid lub Huawei AppGallery
[
](https://play.google.com/store/apps/details?id=io.github.wulkanowy)
+ alt="Pobierz z Google Play"
+ height="80">](https://play.google.com/store/apps/details?id=io.github.wulkanowy)
[
](https://f-droid.org/packages/io.github.wulkanowy/)
+[
](https://appgallery.cloud.huawei.com/ag/n/app/C101440411?channelId=Badge&id=1b3f7fbb700849a9be0dba6b520b2282&s=EB1D3BF9ED9D1564D869B7B94B18016D3CABFCA5AEFB8E29F675FA04E0DC131D&detailType=0&v=)
Możesz także pobrać [wersję rozwojową](https://wulkanowy.github.io/#download), która zawiera nowe funkcje przygotowywane do następnego wydania
diff --git a/app/build.gradle b/app/build.gradle
index eb9329787..541aff36c 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -10,16 +10,16 @@ apply from: 'sonarqube.gradle'
apply from: 'hooks.gradle'
android {
- compileSdkVersion 29
- buildToolsVersion '29.0.3'
+ compileSdkVersion 30
+ buildToolsVersion '30.0.2'
defaultConfig {
applicationId "io.github.wulkanowy"
testApplicationId "io.github.tests.wulkanowy"
minSdkVersion 17
- targetSdkVersion 29
- versionCode 64
- versionName "0.20.0"
+ targetSdkVersion 30
+ versionCode 75
+ versionName "0.22.2"
multiDexEnabled true
resValue "string", "app_name", "Wulkanowy"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -69,12 +69,26 @@ android {
flavorDimensions "platform"
productFlavors {
+ hms {
+ dimension "platform"
+ minSdkVersion 19
+ manifestPlaceholders = [
+ install_channel: "AppGallery"
+ ]
+ }
+
play {
dimension "platform"
+ manifestPlaceholders = [
+ install_channel: "Google Play"
+ ]
}
fdroid {
dimension "platform"
+ manifestPlaceholders = [
+ install_channel: "F-Droid"
+ ]
}
}
@@ -112,13 +126,15 @@ play {
serviceAccountCredentials = file('key.p12')
defaultToAppBundles = false
track = 'alpha'
+ updatePriority = 1
}
ext {
work_manager = "2.4.0"
room = "2.2.5"
- chucker = "3.2.0"
- mockk = "1.10.0"
+ chucker = "3.3.0"
+ mockk = "1.10.2"
+ moshi = "1.11.0"
}
configurations.all {
@@ -126,14 +142,14 @@ configurations.all {
}
dependencies {
- implementation "io.github.wulkanowy:sdk:0.20.1"
+ implementation "io.github.wulkanowy:sdk:0.22.2"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.0.10'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
- implementation "androidx.core:core-ktx:1.3.1"
+ implementation "androidx.core:core-ktx:1.3.2"
implementation "androidx.activity:activity-ktx:1.1.0"
implementation "androidx.appcompat:appcompat:1.2.0"
implementation "androidx.appcompat:appcompat-resources:1.2.0"
@@ -147,7 +163,7 @@ dependencies {
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation "androidx.constraintlayout:constraintlayout:2.0.1"
implementation "androidx.coordinatorlayout:coordinatorlayout:1.1.0"
- implementation "com.google.android.material:material:1.2.0"
+ implementation "com.google.android.material:material:1.2.1"
implementation "com.github.wulkanowy:material-chips-input:2.1.1"
implementation "com.github.PhilJay:MPAndroidChart:v3.1.0"
implementation "me.zhanghai.android.materialprogressbar:library:1.6.1"
@@ -170,29 +186,35 @@ dependencies {
implementation "com.ncapdevi:frag-nav:3.3.0"
implementation "com.github.YarikSOffice:lingver:1.2.2"
- implementation "com.google.code.gson:gson:2.8.6"
+ implementation "com.squareup.moshi:moshi:$moshi"
+ implementation "com.squareup.moshi:moshi-adapters:$moshi"
+ kapt "com.squareup.moshi:moshi-kotlin-codegen:$moshi"
implementation "com.jakewharton.timber:timber:4.7.1"
implementation "at.favre.lib:slf4j-timber:1.0.1"
implementation "fr.bipi.treessence:treessence:0.3.2"
implementation "com.mikepenz:aboutlibraries-core:$about_libraries"
implementation 'com.wdullaer:materialdatetimepicker:4.2.3'
- implementation "io.coil-kt:coil:1.0.0-rc1"
+ implementation "io.coil-kt:coil:1.0.0-rc3"
implementation "io.github.wulkanowy:AppKillerManager:3.0.0"
implementation 'me.xdrop:fuzzywuzzy:1.3.1'
- playImplementation 'com.google.firebase:firebase-analytics:17.5.0'
+ playImplementation 'com.google.firebase:firebase-analytics:17.6.0'
playImplementation 'com.google.firebase:firebase-inappmessaging-display-ktx:19.1.1'
playImplementation "com.google.firebase:firebase-inappmessaging-ktx:19.1.1"
- playImplementation 'com.google.firebase:firebase-messaging:20.2.4'
- playImplementation 'com.google.firebase:firebase-crashlytics:17.2.1'
+ playImplementation 'com.google.firebase:firebase-messaging:20.3.0'
+ playImplementation 'com.google.firebase:firebase-crashlytics:17.2.2'
+ playImplementation 'com.google.android.play:core-ktx:1.8.1'
playImplementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
+ hmsImplementation 'com.huawei.hms:hianalytics:5.0.4.301'
+ hmsImplementation 'com.huawei.agconnect:agconnect-crash:1.4.1.300'
+
releaseImplementation "com.github.ChuckerTeam.Chucker:library-no-op:$chucker"
debugImplementation "com.github.ChuckerTeam.Chucker:library:$chucker"
debugImplementation "com.amitshekhar.android:debug-db:1.0.6"
- testImplementation "junit:junit:4.13"
+ testImplementation "junit:junit:4.13.1"
testImplementation "io.mockk:mockk:$mockk"
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.3.9'
@@ -205,3 +227,4 @@ dependencies {
}
apply plugin: 'com.google.gms.google-services'
+apply plugin: 'com.huawei.agconnect'
diff --git a/app/jacoco.gradle b/app/jacoco.gradle
index e9abfb613..a5cf84e63 100644
--- a/app/jacoco.gradle
+++ b/app/jacoco.gradle
@@ -35,13 +35,13 @@ task jacocoTestReport(type: JacocoReport) {
dir: "$buildDir/intermediates/classes/debug",
excludes: excludes
) + fileTree(
- dir: "$buildDir/tmp/kotlin-classes/playDebug",
+ dir: "$buildDir/tmp/kotlin-classes/fdroidDebug",
excludes: excludes
))
sourceDirectories.setFrom(files([
"src/main/java",
- "src/play/java"
+ "src/fdroid/java"
]))
executionData.setFrom(fileTree(
dir: project.projectDir,
diff --git a/app/src/debug/agconnect-services.json b/app/src/debug/agconnect-services.json
new file mode 100644
index 000000000..48192df01
--- /dev/null
+++ b/app/src/debug/agconnect-services.json
@@ -0,0 +1,33 @@
+{
+ "agcgw":{
+ "backurl":"connect-dre.dbankcloud.cn",
+ "url":"connect-dre.hispace.hicloud.com"
+ },
+ "client":{
+ "cp_id":"890048000024105546",
+ "product_id":"",
+ "client_id":"",
+ "client_secret":"",
+ "app_id":"101440411",
+ "package_name":"io.github.wulkanowy.dev",
+ "api_key":""
+ },
+ "service":{
+ "analytics":{
+ "collector_url":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn",
+ "resource_id":"p1",
+ "channel_id":""
+ },
+ "search":{
+ "url":"https://search-dre.cloud.huawei.com"
+ },
+ "cloudstorage":{
+ "storage_url":"https://ops-dre.agcstorage.link"
+ },
+ "ml":{
+ "mlservice_url":"ml-api-dre.ai.dbankcloud.com,ml-api-dre.ai.dbankcloud.cn"
+ }
+ },
+ "region":"DE",
+ "configuration_version":"1.0"
+}
diff --git a/app/src/fdroid/java/io/github/wulkanowy/utils/FirebaseAnalyticsHelper.kt b/app/src/fdroid/java/io/github/wulkanowy/utils/AnalyticsHelper.kt
similarity index 86%
rename from app/src/fdroid/java/io/github/wulkanowy/utils/FirebaseAnalyticsHelper.kt
rename to app/src/fdroid/java/io/github/wulkanowy/utils/AnalyticsHelper.kt
index f23645bc3..0cd9a52e4 100644
--- a/app/src/fdroid/java/io/github/wulkanowy/utils/FirebaseAnalyticsHelper.kt
+++ b/app/src/fdroid/java/io/github/wulkanowy/utils/AnalyticsHelper.kt
@@ -6,7 +6,7 @@ import javax.inject.Singleton
@Singleton
@Suppress("UNUSED_PARAMETER")
-class FirebaseAnalyticsHelper @Inject constructor() {
+class AnalyticsHelper @Inject constructor() {
fun logEvent(name: String, vararg params: Pair) {
// do nothing
diff --git a/app/src/fdroid/java/io/github/wulkanowy/utils/CrashlyticsUtils.kt b/app/src/fdroid/java/io/github/wulkanowy/utils/CrashLogUtils.kt
similarity index 71%
rename from app/src/fdroid/java/io/github/wulkanowy/utils/CrashlyticsUtils.kt
rename to app/src/fdroid/java/io/github/wulkanowy/utils/CrashLogUtils.kt
index d03a319a2..5d58270d4 100644
--- a/app/src/fdroid/java/io/github/wulkanowy/utils/CrashlyticsUtils.kt
+++ b/app/src/fdroid/java/io/github/wulkanowy/utils/CrashLogUtils.kt
@@ -8,6 +8,6 @@ open class TimberTreeNoOp : Timber.Tree() {
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {}
}
-class CrashlyticsTree : TimberTreeNoOp()
+class CrashLogTree : TimberTreeNoOp()
-class CrashlyticsExceptionTree : TimberTreeNoOp()
+class CrashLogExceptionTree : TimberTreeNoOp()
diff --git a/app/src/fdroid/java/io/github/wulkanowy/utils/UpdateHelper.kt b/app/src/fdroid/java/io/github/wulkanowy/utils/UpdateHelper.kt
new file mode 100644
index 000000000..3abab9629
--- /dev/null
+++ b/app/src/fdroid/java/io/github/wulkanowy/utils/UpdateHelper.kt
@@ -0,0 +1,17 @@
+package io.github.wulkanowy.utils
+
+import android.app.Activity
+import android.view.View
+import javax.inject.Inject
+
+@Suppress("UNUSED_PARAMETER")
+class UpdateHelper @Inject constructor() {
+
+ lateinit var messageContainer: View
+
+ fun checkAndInstallUpdates(activity: Activity) {}
+
+ fun onActivityResult(requestCode: Int, resultCode: Int) {}
+
+ fun onResume(activity: Activity) {}
+}
diff --git a/app/src/hms/java/io/github/wulkanowy/utils/AnalyticsHelper.kt b/app/src/hms/java/io/github/wulkanowy/utils/AnalyticsHelper.kt
new file mode 100644
index 000000000..b3cecf243
--- /dev/null
+++ b/app/src/hms/java/io/github/wulkanowy/utils/AnalyticsHelper.kt
@@ -0,0 +1,38 @@
+package io.github.wulkanowy.utils
+
+import android.app.Activity
+import android.content.Context
+import android.os.Bundle
+import com.huawei.hms.analytics.HiAnalytics
+import dagger.hilt.android.qualifiers.ApplicationContext
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class AnalyticsHelper @Inject constructor(
+ @ApplicationContext private val context: Context
+) {
+
+ private val analytics by lazy { HiAnalytics.getInstance(context) }
+
+ fun logEvent(name: String, vararg params: Pair) {
+ Bundle().apply {
+ params.forEach {
+ if (it.second == null) return@forEach
+ when (it.second) {
+ is String, is String? -> putString(it.first, it.second as String)
+ is Int, is Int? -> putInt(it.first, it.second as Int)
+ is Boolean, is Boolean? -> putBoolean(it.first, it.second as Boolean)
+ }
+ }
+ analytics.onEvent(name, this)
+ }
+ }
+
+ fun setCurrentScreen(activity: Activity, name: String?) {
+ analytics.onEvent("screen_view", Bundle().apply {
+ putString("screen_name", name)
+ putString("screen_class", activity::class.simpleName)
+ })
+ }
+}
diff --git a/app/src/hms/java/io/github/wulkanowy/utils/CrashLogUtils.kt b/app/src/hms/java/io/github/wulkanowy/utils/CrashLogUtils.kt
new file mode 100644
index 000000000..7f4bedae4
--- /dev/null
+++ b/app/src/hms/java/io/github/wulkanowy/utils/CrashLogUtils.kt
@@ -0,0 +1,52 @@
+package io.github.wulkanowy.utils
+
+import android.util.Log
+import com.huawei.agconnect.crash.AGConnectCrash
+import fr.bipi.tressence.base.FormatterPriorityTree
+import fr.bipi.tressence.common.StackTraceRecorder
+import io.github.wulkanowy.sdk.exception.FeatureNotAvailableException
+import io.github.wulkanowy.sdk.scrapper.exception.FeatureDisabledException
+import java.io.InterruptedIOException
+import java.net.SocketTimeoutException
+import java.net.UnknownHostException
+
+class CrashLogTree : FormatterPriorityTree(Log.VERBOSE) {
+
+ private val connectCrash by lazy { AGConnectCrash.getInstance() }
+
+ override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
+ if (skipLog(priority, tag, message, t)) return
+
+ connectCrash.log(format(priority, tag, message))
+ }
+}
+
+class CrashLogExceptionTree : FormatterPriorityTree(Log.ERROR) {
+
+ private val connectCrash by lazy { AGConnectCrash.getInstance() }
+
+ override fun skipLog(priority: Int, tag: String?, message: String, t: Throwable?): Boolean {
+ return when (t) {
+ is FeatureDisabledException,
+ is FeatureNotAvailableException,
+ is UnknownHostException,
+ is SocketTimeoutException,
+ is InterruptedIOException -> true
+ else -> super.skipLog(priority, tag, message, t)
+ }
+ }
+
+ override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
+ if (skipLog(priority, tag, message, t)) return
+
+ connectCrash.setCustomKey("priority", priority)
+ connectCrash.setCustomKey("tag", tag.orEmpty())
+ connectCrash.setCustomKey("message", message)
+ connectCrash.log(priority, t?.stackTraceToString())
+ if (t != null) {
+ connectCrash.log(priority, t.stackTraceToString())
+ } else {
+ connectCrash.log(priority, StackTraceRecorder(format(priority, tag, message)).stackTraceToString())
+ }
+ }
+}
diff --git a/app/src/hms/java/io/github/wulkanowy/utils/UpdateHelper.kt b/app/src/hms/java/io/github/wulkanowy/utils/UpdateHelper.kt
new file mode 100644
index 000000000..3abab9629
--- /dev/null
+++ b/app/src/hms/java/io/github/wulkanowy/utils/UpdateHelper.kt
@@ -0,0 +1,17 @@
+package io.github.wulkanowy.utils
+
+import android.app.Activity
+import android.view.View
+import javax.inject.Inject
+
+@Suppress("UNUSED_PARAMETER")
+class UpdateHelper @Inject constructor() {
+
+ lateinit var messageContainer: View
+
+ fun checkAndInstallUpdates(activity: Activity) {}
+
+ fun onActivityResult(requestCode: Int, resultCode: Int) {}
+
+ fun onResume(activity: Activity) {}
+}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4ec2f7816..a8d2b49e3 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -110,6 +110,11 @@
android:resource="@xml/provider_paths" />
+
+
+
(val status: Status, val data: T?, val error: Throwable?) {
+data class Resource(val status: Status, val data: T?, val error: Throwable?) {
companion object {
fun success(data: T?): Resource {
return Resource(Status.SUCCESS, data, null)
diff --git a/app/src/main/java/io/github/wulkanowy/data/db/Converters.kt b/app/src/main/java/io/github/wulkanowy/data/db/Converters.kt
index b21c4834f..def0b3715 100644
--- a/app/src/main/java/io/github/wulkanowy/data/db/Converters.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/db/Converters.kt
@@ -1,8 +1,9 @@
package io.github.wulkanowy.data.db
import androidx.room.TypeConverter
-import com.google.gson.Gson
-import com.google.gson.reflect.TypeToken
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.Types
+import io.github.wulkanowy.data.db.adapters.PairAdapterFactory
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
@@ -12,6 +13,16 @@ import java.util.Date
class Converters {
+ private val moshi by lazy { Moshi.Builder().add(PairAdapterFactory).build() }
+
+ private val integerListAdapter by lazy {
+ moshi.adapter>(Types.newParameterizedType(List::class.java, Integer::class.java))
+ }
+
+ private val stringListPairAdapter by lazy {
+ moshi.adapter>>(Types.newParameterizedType(List::class.java, Pair::class.java, String::class.java, String::class.java))
+ }
+
@TypeConverter
fun timestampToDate(value: Long?): LocalDate? = value?.run {
Date(value).toInstant().atZone(ZoneOffset.UTC).toLocalDate()
@@ -39,22 +50,22 @@ class Converters {
fun intToMonth(value: Int?) = value?.let { Month.of(it) }
@TypeConverter
- fun intListToGson(list: List): String {
- return Gson().toJson(list)
+ fun intListToJson(list: List): String {
+ return integerListAdapter.toJson(list)
}
@TypeConverter
- fun gsonToIntList(value: String): List {
- return Gson().fromJson(value, object : TypeToken>() {}.type)
+ fun jsonToIntList(value: String): List {
+ return integerListAdapter.fromJson(value).orEmpty()
}
@TypeConverter
- fun stringPairListToGson(list: List>): String {
- return Gson().toJson(list)
+ fun stringPairListToJson(list: List>): String {
+ return stringListPairAdapter.toJson(list)
}
@TypeConverter
- fun gsonToStringPairList(value: String): List> {
- return Gson().fromJson(value, object : TypeToken>>() {}.type)
+ fun jsonToStringPairList(value: String): List> {
+ return stringListPairAdapter.fromJson(value).orEmpty()
}
}
diff --git a/app/src/main/java/io/github/wulkanowy/data/db/adapters/PairAdapterFactory.kt b/app/src/main/java/io/github/wulkanowy/data/db/adapters/PairAdapterFactory.kt
new file mode 100644
index 000000000..4a9b168dd
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/data/db/adapters/PairAdapterFactory.kt
@@ -0,0 +1,68 @@
+package io.github.wulkanowy.data.db.adapters
+
+import com.squareup.moshi.JsonAdapter
+import com.squareup.moshi.JsonReader
+import com.squareup.moshi.JsonWriter
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.Types
+import java.lang.reflect.ParameterizedType
+import java.lang.reflect.Type
+
+object PairAdapterFactory : JsonAdapter.Factory {
+
+ override fun create(type: Type, annotations: MutableSet, moshi: Moshi): JsonAdapter<*>? {
+ if (type !is ParameterizedType || List::class.java != type.rawType) return null
+ if (type.actualTypeArguments[0] != Pair::class.java) return null
+
+ val listType = Types.newParameterizedType(List::class.java, Map::class.java, String::class.java)
+ val listAdapter = moshi.adapter>>(listType)
+
+ val mapType = Types.newParameterizedType(MutableMap::class.java, String::class.java, String::class.java)
+ val mapAdapter = moshi.adapter