diff --git a/app/src/main/java/gq/kirmanak/mealient/App.kt b/app/src/main/java/gq/kirmanak/mealient/App.kt index 090b6c6..df1cc2d 100644 --- a/app/src/main/java/gq/kirmanak/mealient/App.kt +++ b/app/src/main/java/gq/kirmanak/mealient/App.kt @@ -4,7 +4,12 @@ import android.app.Application import com.google.android.material.color.DynamicColors import dagger.hilt.android.HiltAndroidApp import gq.kirmanak.mealient.architecture.configuration.BuildConfiguration +import gq.kirmanak.mealient.data.migration.MigrationDetector import gq.kirmanak.mealient.logging.Logger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import javax.inject.Inject @HiltAndroidApp @@ -16,9 +21,15 @@ class App : Application() { @Inject lateinit var buildConfiguration: BuildConfiguration + @Inject + lateinit var migrationDetector: MigrationDetector + + private val appCoroutineScope = CoroutineScope(Dispatchers.Main + Job()) + override fun onCreate() { super.onCreate() logger.v { "onCreate() called" } DynamicColors.applyToActivitiesIfAvailable(this) + appCoroutineScope.launch { migrationDetector.executeMigrations() } } } diff --git a/app/src/main/java/gq/kirmanak/mealient/data/configuration/BuildConfigurationImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/configuration/BuildConfigurationImpl.kt new file mode 100644 index 0000000..beb9ac5 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/configuration/BuildConfigurationImpl.kt @@ -0,0 +1,19 @@ +package gq.kirmanak.mealient.data.configuration + +import gq.kirmanak.mealient.BuildConfig +import gq.kirmanak.mealient.architecture.configuration.BuildConfiguration +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class BuildConfigurationImpl @Inject constructor() : BuildConfiguration { + + @get:JvmName("_isDebug") + private val isDebug by lazy { BuildConfig.DEBUG } + + private val versionCode by lazy { BuildConfig.VERSION_CODE } + + override fun isDebug(): Boolean = isDebug + + override fun versionCode(): Int = versionCode +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/migration/From24AuthMigrationExecutor.kt b/app/src/main/java/gq/kirmanak/mealient/data/migration/From24AuthMigrationExecutor.kt new file mode 100644 index 0000000..638c8bd --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/migration/From24AuthMigrationExecutor.kt @@ -0,0 +1,41 @@ +package gq.kirmanak.mealient.data.migration + +import android.content.SharedPreferences +import androidx.core.content.edit +import gq.kirmanak.mealient.data.auth.AuthRepo +import gq.kirmanak.mealient.datasource.runCatchingExceptCancel +import gq.kirmanak.mealient.datastore.DataStoreModule.Companion.ENCRYPTED +import gq.kirmanak.mealient.logging.Logger +import javax.inject.Inject +import javax.inject.Named +import javax.inject.Singleton + +@Singleton +class From24AuthMigrationExecutor @Inject constructor( + @Named(ENCRYPTED) private val sharedPreferences: SharedPreferences, + private val authRepo: AuthRepo, + private val logger: Logger, +) : MigrationExecutor { + + override val migratingFrom: Int = 24 + + override suspend fun executeMigration() { + logger.v { "executeMigration() was called" } + val email = sharedPreferences.getString(EMAIL_KEY, null) + val password = sharedPreferences.getString(PASSWORD_KEY, null) + if (email != null && password != null) { + runCatchingExceptCancel { authRepo.authenticate(email, password) } + .onFailure { logger.e(it) { "API token creation failed" } } + .onSuccess { logger.i { "Created API token during migration" } } + } + sharedPreferences.edit { + remove(EMAIL_KEY) + remove(PASSWORD_KEY) + } + } + + companion object { + private const val EMAIL_KEY = "email" + private const val PASSWORD_KEY = "password" + } +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/migration/MigrationDetector.kt b/app/src/main/java/gq/kirmanak/mealient/data/migration/MigrationDetector.kt new file mode 100644 index 0000000..ee9ec4a --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/migration/MigrationDetector.kt @@ -0,0 +1,6 @@ +package gq.kirmanak.mealient.data.migration + +interface MigrationDetector { + + suspend fun executeMigrations() +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/migration/MigrationDetectorImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/migration/MigrationDetectorImpl.kt new file mode 100644 index 0000000..dd6ec66 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/migration/MigrationDetectorImpl.kt @@ -0,0 +1,43 @@ +package gq.kirmanak.mealient.data.migration + +import gq.kirmanak.mealient.architecture.configuration.BuildConfiguration +import gq.kirmanak.mealient.data.storage.PreferencesStorage +import gq.kirmanak.mealient.datasource.runCatchingExceptCancel +import gq.kirmanak.mealient.logging.Logger +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class MigrationDetectorImpl @Inject constructor( + private val preferencesStorage: PreferencesStorage, + private val migrationExecutors: Set<@JvmSuppressWildcards MigrationExecutor>, + private val buildConfiguration: BuildConfiguration, + private val logger: Logger, +) : MigrationDetector { + + + override suspend fun executeMigrations() { + val key = preferencesStorage.lastExecutedMigrationVersionKey + + val lastVersion = preferencesStorage.getValue(key) ?: VERSION_BEFORE_MIGRATION_IMPLEMENTED + val currentVersion = buildConfiguration.versionCode() + logger.i { "Last migration version is $lastVersion, current is $currentVersion" } + + if (lastVersion != currentVersion) { + migrationExecutors + .filter { it.migratingFrom <= lastVersion } + .forEach { + runCatchingExceptCancel { it.executeMigration() } + .onFailure { logger.e(it) { "Migration executor failed: $it" } } + .onSuccess { logger.i { "Migration executor succeeded: $it" } } + } + } + + preferencesStorage.storeValues(Pair(key, currentVersion)) + } + + + companion object { + private const val VERSION_BEFORE_MIGRATION_IMPLEMENTED = 24 + } +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/migration/MigrationExecutor.kt b/app/src/main/java/gq/kirmanak/mealient/data/migration/MigrationExecutor.kt new file mode 100644 index 0000000..aeb9249 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/migration/MigrationExecutor.kt @@ -0,0 +1,8 @@ +package gq.kirmanak.mealient.data.migration + +interface MigrationExecutor { + + val migratingFrom: Int + + suspend fun executeMigration() +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/storage/PreferencesStorage.kt b/app/src/main/java/gq/kirmanak/mealient/data/storage/PreferencesStorage.kt index df88285..403dfd7 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/storage/PreferencesStorage.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/storage/PreferencesStorage.kt @@ -11,6 +11,8 @@ interface PreferencesStorage { val isDisclaimerAcceptedKey: Preferences.Key + val lastExecutedMigrationVersionKey: Preferences.Key + suspend fun getValue(key: Preferences.Key): T? suspend fun requireValue(key: Preferences.Key): T diff --git a/app/src/main/java/gq/kirmanak/mealient/data/storage/PreferencesStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/storage/PreferencesStorageImpl.kt index f9e7bfa..6cd6077 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/storage/PreferencesStorageImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/storage/PreferencesStorageImpl.kt @@ -4,9 +4,15 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import gq.kirmanak.mealient.logging.Logger -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach import javax.inject.Inject import javax.inject.Singleton @@ -22,6 +28,9 @@ class PreferencesStorageImpl @Inject constructor( override val isDisclaimerAcceptedKey = booleanPreferencesKey("isDisclaimedAccepted") + override val lastExecutedMigrationVersionKey: Preferences.Key = + intPreferencesKey("lastExecutedMigrationVersion") + override suspend fun getValue(key: Preferences.Key): T? { val value = dataStore.data.first()[key] logger.v { "getValue() returned: $value for $key" } diff --git a/architecture/src/main/kotlin/gq/kirmanak/mealient/architecture/ArchitectureModule.kt b/app/src/main/java/gq/kirmanak/mealient/di/ArchitectureModule.kt similarity index 78% rename from architecture/src/main/kotlin/gq/kirmanak/mealient/architecture/ArchitectureModule.kt rename to app/src/main/java/gq/kirmanak/mealient/di/ArchitectureModule.kt index 59a979e..c322301 100644 --- a/architecture/src/main/kotlin/gq/kirmanak/mealient/architecture/ArchitectureModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/ArchitectureModule.kt @@ -1,11 +1,11 @@ -package gq.kirmanak.mealient.architecture +package gq.kirmanak.mealient.di import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import gq.kirmanak.mealient.architecture.configuration.BuildConfiguration -import gq.kirmanak.mealient.architecture.configuration.BuildConfigurationImpl +import gq.kirmanak.mealient.data.configuration.BuildConfigurationImpl import javax.inject.Singleton @Module diff --git a/app/src/main/java/gq/kirmanak/mealient/di/MigrationModule.kt b/app/src/main/java/gq/kirmanak/mealient/di/MigrationModule.kt new file mode 100644 index 0000000..2ffd796 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/di/MigrationModule.kt @@ -0,0 +1,26 @@ +package gq.kirmanak.mealient.di + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet +import gq.kirmanak.mealient.data.migration.From24AuthMigrationExecutor +import gq.kirmanak.mealient.data.migration.MigrationDetector +import gq.kirmanak.mealient.data.migration.MigrationDetectorImpl +import gq.kirmanak.mealient.data.migration.MigrationExecutor +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +interface MigrationModule { + + @Binds + @Singleton + @IntoSet + fun bindFrom24AuthMigrationExecutor(from24AuthMigrationExecutor: From24AuthMigrationExecutor): MigrationExecutor + + @Binds + @Singleton + fun bindMigrationDetector(migrationDetectorImpl: MigrationDetectorImpl): MigrationDetector +} \ No newline at end of file diff --git a/architecture/src/main/kotlin/gq/kirmanak/mealient/architecture/configuration/BuildConfiguration.kt b/architecture/src/main/kotlin/gq/kirmanak/mealient/architecture/configuration/BuildConfiguration.kt index e076468..a34ab9e 100644 --- a/architecture/src/main/kotlin/gq/kirmanak/mealient/architecture/configuration/BuildConfiguration.kt +++ b/architecture/src/main/kotlin/gq/kirmanak/mealient/architecture/configuration/BuildConfiguration.kt @@ -3,4 +3,6 @@ package gq.kirmanak.mealient.architecture.configuration interface BuildConfiguration { fun isDebug(): Boolean + + fun versionCode(): Int } \ No newline at end of file diff --git a/architecture/src/main/kotlin/gq/kirmanak/mealient/architecture/configuration/BuildConfigurationImpl.kt b/architecture/src/main/kotlin/gq/kirmanak/mealient/architecture/configuration/BuildConfigurationImpl.kt deleted file mode 100644 index b660cd9..0000000 --- a/architecture/src/main/kotlin/gq/kirmanak/mealient/architecture/configuration/BuildConfigurationImpl.kt +++ /dev/null @@ -1,14 +0,0 @@ -package gq.kirmanak.mealient.architecture.configuration - -import gq.kirmanak.mealient.architecture.BuildConfig -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class BuildConfigurationImpl @Inject constructor() : BuildConfiguration { - - @get:JvmName("_isDebug") - private val isDebug by lazy { BuildConfig.DEBUG } - - override fun isDebug(): Boolean = isDebug -} \ No newline at end of file