+
+
+
+ %SUBJECT% | Wulkanowy
+
+
+
+%SUBJECT%
+
+
+ %INFO%
+
+
+
+
+
Treść wiadomości
+ %CONTENT%
+
+
+
diff --git a/app/src/main/assets/wulkanowy-logo-black.svg b/app/src/main/assets/wulkanowy-logo-black.svg
new file mode 100644
index 00000000..9bfbe2c0
--- /dev/null
+++ b/app/src/main/assets/wulkanowy-logo-black.svg
@@ -0,0 +1,74 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt b/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt
index 96ec7cb8..223224e2 100644
--- a/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt
+++ b/app/src/main/java/io/github/wulkanowy/WulkanowyApp.kt
@@ -1,37 +1,33 @@
package io.github.wulkanowy
+import android.app.Application
import android.content.Context
import android.util.Log.DEBUG
import android.util.Log.INFO
import android.util.Log.VERBOSE
+import android.webkit.WebView
+import androidx.hilt.work.HiltWorkerFactory
import androidx.multidex.MultiDex
import androidx.work.Configuration
-import com.jakewharton.threetenabp.AndroidThreeTen
import com.yariksoffice.lingver.Lingver
-import dagger.android.AndroidInjector
-import dagger.android.support.DaggerApplication
-import eu.davidea.flexibleadapter.FlexibleAdapter
-import eu.davidea.flexibleadapter.utils.Log
+import dagger.hilt.android.HiltAndroidApp
import fr.bipi.tressence.file.FileLoggerTree
-import io.github.wulkanowy.di.DaggerAppComponent
-import io.github.wulkanowy.services.sync.SyncWorkerFactory
+import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.ui.base.ThemeManager
import io.github.wulkanowy.utils.ActivityLifecycleLogger
+import io.github.wulkanowy.utils.AnalyticsHelper
import io.github.wulkanowy.utils.AppInfo
-import io.github.wulkanowy.utils.CrashlyticsExceptionTree
-import io.github.wulkanowy.utils.CrashlyticsTree
+import io.github.wulkanowy.utils.CrashLogExceptionTree
+import io.github.wulkanowy.utils.CrashLogTree
import io.github.wulkanowy.utils.DebugLogTree
-import io.github.wulkanowy.utils.initCrashlytics
-import io.reactivex.exceptions.UndeliverableException
-import io.reactivex.plugins.RxJavaPlugins
import timber.log.Timber
-import java.io.IOException
import javax.inject.Inject
-class WulkanowyApp : DaggerApplication(), Configuration.Provider {
+@HiltAndroidApp
+class WulkanowyApp : Application(), Configuration.Provider {
@Inject
- lateinit var workerFactory: SyncWorkerFactory
+ lateinit var workerFactory: HiltWorkerFactory
@Inject
lateinit var themeManager: ThemeManager
@@ -39,6 +35,12 @@ class WulkanowyApp : DaggerApplication(), Configuration.Provider {
@Inject
lateinit var appInfo: AppInfo
+ @Inject
+ lateinit var preferencesRepository: PreferencesRepository
+
+ @Inject
+ lateinit var analyticsHelper: AnalyticsHelper
+
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
MultiDex.install(this)
@@ -46,43 +48,49 @@ class WulkanowyApp : DaggerApplication(), Configuration.Provider {
override fun onCreate() {
super.onCreate()
- AndroidThreeTen.init(this)
- RxJavaPlugins.setErrorHandler(::onError)
- Lingver.init(this)
- themeManager.applyDefaultTheme()
+ initializeAppLanguage()
+ themeManager.applyDefaultTheme()
initLogging()
- initCrashlytics(this, appInfo)
+ fixWebViewLocale()
}
private fun initLogging() {
if (appInfo.isDebug) {
- FlexibleAdapter.enableLogs(Log.Level.DEBUG)
Timber.plant(DebugLogTree())
- Timber.plant(FileLoggerTree.Builder()
- .withFileName("wulkanowy.%g.log")
- .withDirName(applicationContext.filesDir.absolutePath)
- .withFileLimit(10)
- .withMinPriority(DEBUG)
- .build()
+ Timber.plant(
+ FileLoggerTree.Builder()
+ .withFileName("wulkanowy.%g.log")
+ .withDirName(applicationContext.filesDir.absolutePath)
+ .withFileLimit(10)
+ .withMinPriority(DEBUG)
+ .build()
)
} else {
- Timber.plant(CrashlyticsExceptionTree())
- Timber.plant(CrashlyticsTree())
+ Timber.plant(CrashLogExceptionTree())
+ Timber.plant(CrashLogTree())
}
registerActivityLifecycleCallbacks(ActivityLifecycleLogger())
}
- private fun onError(error: Throwable) {
- //RxJava's too deep stack traces may cause SOE on older android devices
- val cause = error.cause
- if (error is UndeliverableException && cause is IOException || cause is InterruptedException || cause is StackOverflowError) {
- Timber.e(cause, "An undeliverable error occurred")
- } else throw error
+ private fun initializeAppLanguage() {
+ Lingver.init(this)
+
+ if (preferencesRepository.appLanguage == "system") {
+ Lingver.getInstance().setFollowSystemLocale(this)
+ analyticsHelper.logEvent("language", "startup" to appInfo.systemLanguage)
+ } else {
+ analyticsHelper.logEvent("language", "startup" to preferencesRepository.appLanguage)
+ }
}
- override fun applicationInjector(): AndroidInjector {
- return DaggerAppComponent.factory().create(this)
+ private fun fixWebViewLocale() {
+ //https://stackoverflow.com/questions/40398528/android-webview-language-changes-abruptly-on-android-7-0-and-above
+ try {
+ WebView(this).destroy()
+ } catch (e: Exception) {
+ //Ignore exceptions
+ }
}
override fun getWorkManagerConfiguration() = Configuration.Builder()
diff --git a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt b/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt
index 43c27c52..8b850659 100644
--- a/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/RepositoryModule.kt
@@ -5,47 +5,49 @@ import android.content.SharedPreferences
import android.content.res.AssetManager
import android.content.res.Resources
import androidx.preference.PreferenceManager
-import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
-import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.strategy.WalledGardenInternetObservingStrategy
import com.chuckerteam.chucker.api.ChuckerCollector
import com.chuckerteam.chucker.api.ChuckerInterceptor
import com.chuckerteam.chucker.api.RetentionManager
import dagger.Module
import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
import io.github.wulkanowy.data.db.AppDatabase
import io.github.wulkanowy.data.db.SharedPrefProvider
-import io.github.wulkanowy.data.repositories.preferences.PreferencesRepository
+import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.sdk.Sdk
import timber.log.Timber
import javax.inject.Singleton
@Module
+@InstallIn(SingletonComponent::class)
internal class RepositoryModule {
@Singleton
@Provides
- fun provideInternetObservingSettings(): InternetObservingSettings {
- return InternetObservingSettings.builder()
- .strategy(WalledGardenInternetObservingStrategy())
- .build()
- }
-
- @Singleton
- @Provides
- fun provideSdk(chuckerCollector: ChuckerCollector, context: Context): Sdk {
+ fun provideSdk(chuckerCollector: ChuckerCollector, @ApplicationContext context: Context): Sdk {
return Sdk().apply {
androidVersion = android.os.Build.VERSION.RELEASE
buildTag = android.os.Build.MODEL
setSimpleHttpLogger { Timber.d(it) }
// for debug only
- addInterceptor(ChuckerInterceptor(context, chuckerCollector), true)
+ addInterceptor(
+ ChuckerInterceptor.Builder(context)
+ .collector(chuckerCollector)
+ .alwaysReadResponseBody(true)
+ .build(), network = true
+ )
}
}
@Singleton
@Provides
- fun provideChuckerCollector(context: Context, prefRepository: PreferencesRepository): ChuckerCollector {
+ fun provideChuckerCollector(
+ @ApplicationContext context: Context,
+ prefRepository: PreferencesRepository
+ ): ChuckerCollector {
return ChuckerCollector(
context = context,
showNotification = prefRepository.isDebugNotificationEnable,
@@ -55,19 +57,23 @@ internal class RepositoryModule {
@Singleton
@Provides
- fun provideDatabase(context: Context, sharedPrefProvider: SharedPrefProvider) = AppDatabase.newInstance(context, sharedPrefProvider)
+ fun provideDatabase(
+ @ApplicationContext context: Context,
+ sharedPrefProvider: SharedPrefProvider,
+ ) = AppDatabase.newInstance(context, sharedPrefProvider)
@Singleton
@Provides
- fun provideResources(context: Context): Resources = context.resources
+ fun provideResources(@ApplicationContext context: Context): Resources = context.resources
@Singleton
@Provides
- fun provideAssets(context: Context): AssetManager = context.assets
+ fun provideAssets(@ApplicationContext context: Context): AssetManager = context.assets
@Singleton
@Provides
- fun provideSharedPref(context: Context): SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
+ fun provideSharedPref(@ApplicationContext context: Context): SharedPreferences =
+ PreferenceManager.getDefaultSharedPreferences(context)
@Singleton
@Provides
@@ -87,11 +93,16 @@ internal class RepositoryModule {
@Singleton
@Provides
- fun provideGradeStatisticsDao(database: AppDatabase) = database.gradeStatistics
+ fun provideGradePartialStatisticsDao(database: AppDatabase) = database.gradePartialStatisticsDao
@Singleton
@Provides
- fun provideGradePointsStatisticsDao(database: AppDatabase) = database.gradePointsStatistics
+ fun provideGradeSemesterStatisticsDao(database: AppDatabase) =
+ database.gradeSemesterStatisticsDao
+
+ @Singleton
+ @Provides
+ fun provideGradePointsStatisticsDao(database: AppDatabase) = database.gradePointsStatisticsDao
@Singleton
@Provides
@@ -156,4 +167,16 @@ internal class RepositoryModule {
@Singleton
@Provides
fun provideSchoolInfoDao(database: AppDatabase) = database.schoolDao
+
+ @Singleton
+ @Provides
+ fun provideConferenceDao(database: AppDatabase) = database.conferenceDao
+
+ @Singleton
+ @Provides
+ fun provideTimetableAdditionalDao(database: AppDatabase) = database.timetableAdditionalDao
+
+ @Singleton
+ @Provides
+ fun provideStudentInfoDao(database: AppDatabase) = database.studentInfoDao
}
diff --git a/app/src/main/java/io/github/wulkanowy/data/Resource.kt b/app/src/main/java/io/github/wulkanowy/data/Resource.kt
new file mode 100644
index 00000000..406440c8
--- /dev/null
+++ b/app/src/main/java/io/github/wulkanowy/data/Resource.kt
@@ -0,0 +1,23 @@
+package io.github.wulkanowy.data
+
+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)
+ }
+
+ fun error(error: Throwable?, data: T? = null): Resource {
+ return Resource(Status.ERROR, data, error)
+ }
+
+ fun loading(data: T? = null): Resource {
+ return Resource(Status.LOADING, data, null)
+ }
+ }
+}
+
+enum class Status {
+ LOADING,
+ SUCCESS,
+ ERROR
+}
diff --git a/app/src/main/java/io/github/wulkanowy/data/SdkHelper.kt b/app/src/main/java/io/github/wulkanowy/data/SdkHelper.kt
deleted file mode 100644
index 90171259..00000000
--- a/app/src/main/java/io/github/wulkanowy/data/SdkHelper.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package io.github.wulkanowy.data
-
-import io.github.wulkanowy.data.db.entities.Student
-import io.github.wulkanowy.sdk.Sdk
-import javax.inject.Inject
-
-class SdkHelper @Inject constructor(private val sdk: Sdk) {
-
- fun init(student: Student) {
- sdk.apply {
- email = student.email
- password = student.password
- symbol = student.symbol
- schoolSymbol = student.schoolSymbol
- studentId = student.studentId
- classId = student.classId
-
- if (Sdk.Mode.valueOf(student.loginMode) != Sdk.Mode.API) {
- scrapperBaseUrl = student.scrapperBaseUrl
- loginType = Sdk.ScrapperLoginType.valueOf(student.loginType)
- }
- loginId = student.userLoginId
-
- mode = Sdk.Mode.valueOf(student.loginMode)
- mobileBaseUrl = student.mobileBaseUrl
- certKey = student.certificateKey
- privateKey = student.privateKey
- }
- }
-}
diff --git a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt
index 762c52f8..ab89b84c 100644
--- a/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/db/AppDatabase.kt
@@ -10,10 +10,12 @@ import androidx.room.migration.Migration
import io.github.wulkanowy.data.db.dao.AttendanceDao
import io.github.wulkanowy.data.db.dao.AttendanceSummaryDao
import io.github.wulkanowy.data.db.dao.CompletedLessonsDao
+import io.github.wulkanowy.data.db.dao.ConferenceDao
import io.github.wulkanowy.data.db.dao.ExamDao
import io.github.wulkanowy.data.db.dao.GradeDao
+import io.github.wulkanowy.data.db.dao.GradePartialStatisticsDao
import io.github.wulkanowy.data.db.dao.GradePointsStatisticsDao
-import io.github.wulkanowy.data.db.dao.GradeStatisticsDao
+import io.github.wulkanowy.data.db.dao.GradeSemesterStatisticsDao
import io.github.wulkanowy.data.db.dao.GradeSummaryDao
import io.github.wulkanowy.data.db.dao.HomeworkDao
import io.github.wulkanowy.data.db.dao.LuckyNumberDao
@@ -26,16 +28,20 @@ import io.github.wulkanowy.data.db.dao.ReportingUnitDao
import io.github.wulkanowy.data.db.dao.SchoolDao
import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
+import io.github.wulkanowy.data.db.dao.StudentInfoDao
import io.github.wulkanowy.data.db.dao.SubjectDao
import io.github.wulkanowy.data.db.dao.TeacherDao
+import io.github.wulkanowy.data.db.dao.TimetableAdditionalDao
import io.github.wulkanowy.data.db.dao.TimetableDao
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.AttendanceSummary
import io.github.wulkanowy.data.db.entities.CompletedLesson
+import io.github.wulkanowy.data.db.entities.Conference
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.db.entities.Grade
+import io.github.wulkanowy.data.db.entities.GradePartialStatistics
import io.github.wulkanowy.data.db.entities.GradePointsStatistics
-import io.github.wulkanowy.data.db.entities.GradeStatistics
+import io.github.wulkanowy.data.db.entities.GradeSemesterStatistics
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Homework
import io.github.wulkanowy.data.db.entities.LuckyNumber
@@ -48,9 +54,11 @@ import io.github.wulkanowy.data.db.entities.ReportingUnit
import io.github.wulkanowy.data.db.entities.School
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
+import io.github.wulkanowy.data.db.entities.StudentInfo
import io.github.wulkanowy.data.db.entities.Subject
import io.github.wulkanowy.data.db.entities.Teacher
import io.github.wulkanowy.data.db.entities.Timetable
+import io.github.wulkanowy.data.db.entities.TimetableAdditional
import io.github.wulkanowy.data.db.migrations.Migration10
import io.github.wulkanowy.data.db.migrations.Migration11
import io.github.wulkanowy.data.db.migrations.Migration12
@@ -68,7 +76,16 @@ import io.github.wulkanowy.data.db.migrations.Migration22
import io.github.wulkanowy.data.db.migrations.Migration23
import io.github.wulkanowy.data.db.migrations.Migration24
import io.github.wulkanowy.data.db.migrations.Migration25
+import io.github.wulkanowy.data.db.migrations.Migration26
+import io.github.wulkanowy.data.db.migrations.Migration27
+import io.github.wulkanowy.data.db.migrations.Migration28
+import io.github.wulkanowy.data.db.migrations.Migration29
import io.github.wulkanowy.data.db.migrations.Migration3
+import io.github.wulkanowy.data.db.migrations.Migration30
+import io.github.wulkanowy.data.db.migrations.Migration31
+import io.github.wulkanowy.data.db.migrations.Migration32
+import io.github.wulkanowy.data.db.migrations.Migration33
+import io.github.wulkanowy.data.db.migrations.Migration34
import io.github.wulkanowy.data.db.migrations.Migration4
import io.github.wulkanowy.data.db.migrations.Migration5
import io.github.wulkanowy.data.db.migrations.Migration6
@@ -88,8 +105,9 @@ import javax.inject.Singleton
AttendanceSummary::class,
Grade::class,
GradeSummary::class,
- GradeStatistics::class,
+ GradePartialStatistics::class,
GradePointsStatistics::class,
+ GradeSemesterStatistics::class,
Message::class,
MessageAttachment::class,
Note::class,
@@ -101,7 +119,10 @@ import javax.inject.Singleton
Recipient::class,
MobileDevice::class,
Teacher::class,
- School::class
+ School::class,
+ Conference::class,
+ TimetableAdditional::class,
+ StudentInfo::class,
],
version = AppDatabase.VERSION_SCHEMA,
exportSchema = true
@@ -110,7 +131,7 @@ import javax.inject.Singleton
abstract class AppDatabase : RoomDatabase() {
companion object {
- const val VERSION_SCHEMA = 25
+ const val VERSION_SCHEMA = 34
fun getMigrations(sharedPrefProvider: SharedPrefProvider): Array {
return arrayOf(
@@ -137,7 +158,16 @@ abstract class AppDatabase : RoomDatabase() {
Migration22(),
Migration23(),
Migration24(),
- Migration25()
+ Migration25(),
+ Migration26(),
+ Migration27(),
+ Migration28(),
+ Migration29(),
+ Migration30(),
+ Migration31(),
+ Migration32(),
+ Migration33(),
+ Migration34()
)
}
@@ -167,9 +197,11 @@ abstract class AppDatabase : RoomDatabase() {
abstract val gradeSummaryDao: GradeSummaryDao
- abstract val gradeStatistics: GradeStatisticsDao
+ abstract val gradePartialStatisticsDao: GradePartialStatisticsDao
- abstract val gradePointsStatistics: GradePointsStatisticsDao
+ abstract val gradePointsStatisticsDao: GradePointsStatisticsDao
+
+ abstract val gradeSemesterStatisticsDao: GradeSemesterStatisticsDao
abstract val messagesDao: MessagesDao
@@ -194,4 +226,10 @@ abstract class AppDatabase : RoomDatabase() {
abstract val teacherDao: TeacherDao
abstract val schoolDao: SchoolDao
+
+ abstract val conferenceDao: ConferenceDao
+
+ abstract val timetableAdditionalDao: TimetableAdditionalDao
+
+ abstract val studentInfoDao: StudentInfoDao
}
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 294f73d3..def0b371 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,21 +1,31 @@
package io.github.wulkanowy.data.db
import androidx.room.TypeConverter
-import com.google.gson.Gson
-import com.google.gson.reflect.TypeToken
-import org.threeten.bp.DateTimeUtils
-import org.threeten.bp.Instant
-import org.threeten.bp.LocalDate
-import org.threeten.bp.LocalDateTime
-import org.threeten.bp.Month
-import org.threeten.bp.ZoneOffset
+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
+import java.time.Month
+import java.time.ZoneOffset
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 {
- DateTimeUtils.toInstant(Date(value)).atZone(ZoneOffset.UTC).toLocalDate()
+ Date(value).toInstant().atZone(ZoneOffset.UTC).toLocalDate()
}
@TypeConverter
@@ -40,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/SharedPrefProvider.kt b/app/src/main/java/io/github/wulkanowy/data/db/SharedPrefProvider.kt
index 4a4aaf74..9301d5fa 100644
--- a/app/src/main/java/io/github/wulkanowy/data/db/SharedPrefProvider.kt
+++ b/app/src/main/java/io/github/wulkanowy/data/db/SharedPrefProvider.kt
@@ -6,7 +6,9 @@ import javax.inject.Inject
import javax.inject.Singleton
@Singleton
-class SharedPrefProvider @Inject constructor(private val sharedPref: SharedPreferences) {
+class SharedPrefProvider @Inject constructor(
+ private val sharedPref: SharedPreferences
+) {
companion object {
const val APP_VERSION_CODE_KEY = "app_version_code"
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 00000000..4a9b168d
--- /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