From 970a4ee71247a8a2169786024a0d4a8fc4543a0f Mon Sep 17 00:00:00 2001 From: Armin Date: Mon, 18 Sep 2023 10:36:56 +0200 Subject: [PATCH] [RFR-769] Migrate to Proto DataStore (#59) * Upgrade robolectric to 4.10.2 * Upgrade kotlin to 1.9.0 (IDE upgrade required) * Add Proto DataStore dependencies * [RFR-780] Add synchronous DataStore implementation * Fix start * [RFR-787] Add migration for sharedprefrences * [RFR-789] Set default values in migration code * Rename classes * Add documentation * Upgrade utils to 4.0.0 * Upgrade compileSdkVersion to 34 * Cleanup --- build.gradle | 22 ++--- energy_settings/build.gradle | 27 +++++- .../de/cyface/energy_settings/Constants.kt | 8 +- .../energy_settings/CustomPreferences.kt | 20 ----- .../ProblematicManufacturerWarningDialog.kt | 39 ++++++-- .../energy_settings/TrackingSettings.kt | 49 ++++++++-- .../settings/EnergySettings.kt | 90 +++++++++++++++++++ .../settings/PreferencesMigration.kt | 68 ++++++++++++++ .../settings/SettingsSerializer.kt | 52 +++++++++++ .../settings/StoreMigration.kt | 77 ++++++++++++++++ .../src/main/proto/energy_settings.proto | 42 +++++++++ 11 files changed, 440 insertions(+), 54 deletions(-) delete mode 100644 energy_settings/src/main/kotlin/de/cyface/energy_settings/CustomPreferences.kt create mode 100644 energy_settings/src/main/kotlin/de/cyface/energy_settings/settings/EnergySettings.kt create mode 100644 energy_settings/src/main/kotlin/de/cyface/energy_settings/settings/PreferencesMigration.kt create mode 100644 energy_settings/src/main/kotlin/de/cyface/energy_settings/settings/SettingsSerializer.kt create mode 100644 energy_settings/src/main/kotlin/de/cyface/energy_settings/settings/StoreMigration.kt create mode 100644 energy_settings/src/main/proto/energy_settings.proto diff --git a/build.gradle b/build.gradle index d255157..9fe0164 100644 --- a/build.gradle +++ b/build.gradle @@ -20,13 +20,13 @@ * Top-level build file where you can add configuration options common to all sub-projects/modules. * * @author Armin Schnabel - * @version 3.2.2 + * @version 3.3.0 * @since 1.0.0 */ buildscript { ext.gradle_version = "7.4.2" - ext.kotlin_version = "1.8.20" + ext.kotlin_version = "1.9.0" repositories { google() @@ -39,33 +39,40 @@ buildscript { } } +plugins { + // To generate Proto DataStore + id 'com.google.protobuf' version '0.8.19' apply false // Maybe keep in sync with other usages +} + ext { // This libraries version cyfaceEnergySettingsVersion = "0.0.0" // Automatically overwritten by CI // Cyface dependencies - cyfaceUtilsVersion = "3.5.1" + cyfaceUtilsVersion = "4.0.0" // Android SDK versions minSdkVersion = 21 // device support targetSdkVersion = 33 // behavioral changes, follow migration guide & test the app against this - compileSdkVersion = 33 // allows newest APIs to be used and to see deprecations, use latest + compileSdkVersion = 34 // allows newest APIs to be used and to see deprecations, use latest buildToolsVersion = '34.0.0' // optional, if defined, use latest (SDK Manager > SDK Tools) // Android dependencies androidxAnnotationVersion = "1.6.0" androidxAppCompatVersion = "1.6.1" androidPreferencesVersion = '1.2.1' + datastoreVersion = "1.1.0-alpha05" // only 1.1.0 supports multi-process datastore // Other dependencies materialDialogsVersion = '3.3.0' + protobufVersion = '3.22.2' // For Proto Datastore. Maybe keep in sync with other versions. // Testing junitVersion = "1.1.5" mockitoVersion = '5.2.0' hamcrestVersion = "1.3" rulesVersion = "1.5.0" - robolectricVersion = "4.8.2" + robolectricVersion = "4.10.2" androidxTestCoreVersion = "1.4.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" @@ -92,8 +99,3 @@ allprojects { } apply plugin: 'android-reporting' - -// Auto-generated by Android Studio -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/energy_settings/build.gradle b/energy_settings/build.gradle index 486ecd1..43e0b2a 100644 --- a/energy_settings/build.gradle +++ b/energy_settings/build.gradle @@ -20,12 +20,13 @@ * Gradle's build file for this module. * * @author Armin Schnabel - * @version 1.3.0 + * @version 1.4.0 * @since 1.0.0 */ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' +apply plugin: 'com.google.protobuf' // For Proto DataStore android { namespace "de.cyface.energy_settings" @@ -70,6 +71,10 @@ android { } dependencies { + // Proto DataStore with SingleProcess support to store settings + implementation "androidx.datastore:datastore:${datastoreVersion}" + implementation "com.google.protobuf:protobuf-javalite:${protobufVersion}" + // Other Android libraries implementation "androidx.appcompat:appcompat:$rootProject.ext.androidxAppCompatVersion" implementation "androidx.preference:preference-ktx:$rootProject.ext.androidPreferencesVersion" @@ -94,5 +99,25 @@ dependencies { testImplementation "org.robolectric:robolectric:$rootProject.ext.robolectricVersion" } +// Required for Proto DataStore +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:$protobufVersion" + } + + // Generates the java Protobuf-lite code for the Protobuf files in this project. See + // https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation + // for more information. + generateProtoTasks { + all().each { task -> + task.builtins { + java { + option 'lite' + } + } + } + } +} + // The gradle publish tasks is defined in a separate file: apply from: 'publish.gradle' diff --git a/energy_settings/src/main/kotlin/de/cyface/energy_settings/Constants.kt b/energy_settings/src/main/kotlin/de/cyface/energy_settings/Constants.kt index 2a11dd1..3676f4f 100644 --- a/energy_settings/src/main/kotlin/de/cyface/energy_settings/Constants.kt +++ b/energy_settings/src/main/kotlin/de/cyface/energy_settings/Constants.kt @@ -1,5 +1,5 @@ /* - * Copyright 2019 Cyface GmbH + * Copyright 2019-2023 Cyface GmbH * * This file is part of the Cyface Energy Settings for Android. * @@ -22,13 +22,12 @@ package de.cyface.energy_settings * Holds constants required by multiple classes. * * @author Armin Schnabel - * @version 1.0.1 + * @version 1.1.0 * @since 1.0.0 */ @Suppress("SpellCheckingInspection") object Constants { const val TAG = "de.cyface.es" - private const val PACKAGE = "de.cyface.energy_settings" // Dialog codes to identify the different dialogs const val DIALOG_ENERGY_SAFER_WARNING_CODE = 2019071101 @@ -37,9 +36,6 @@ object Constants { const val DIALOG_GPS_DISABLED_WARNING_CODE = 2019071104 const val DIALOG_NO_GUIDANCE_NEEDED_DIALOG_CODE = 2019071105 - // Preference key for shared preferences - const val PREFERENCES_MANUFACTURER_WARNING_SHOWN_KEY = "$PACKAGE.manufacturer_warning_shown" - // Manufacturer names const val MANUFACTURER_HUAWEI = "huawei" const val MANUFACTURER_HONOR = "honor" // sub brand of huawei diff --git a/energy_settings/src/main/kotlin/de/cyface/energy_settings/CustomPreferences.kt b/energy_settings/src/main/kotlin/de/cyface/energy_settings/CustomPreferences.kt deleted file mode 100644 index 1a65029..0000000 --- a/energy_settings/src/main/kotlin/de/cyface/energy_settings/CustomPreferences.kt +++ /dev/null @@ -1,20 +0,0 @@ -package de.cyface.energy_settings - -import android.content.Context -import androidx.core.content.edit -import de.cyface.energy_settings.Constants.PREFERENCES_MANUFACTURER_WARNING_SHOWN_KEY -import de.cyface.utils.AppPreferences - -class CustomPreferences(context: Context): AppPreferences(context) { - - fun saveWarningShown(warningShown: Boolean) { - preferences.edit { - putBoolean(PREFERENCES_MANUFACTURER_WARNING_SHOWN_KEY, warningShown) - apply() - } - } - - fun getWarningShown(): Boolean { - return preferences.getBoolean(PREFERENCES_MANUFACTURER_WARNING_SHOWN_KEY, false) - } -} \ No newline at end of file diff --git a/energy_settings/src/main/kotlin/de/cyface/energy_settings/ProblematicManufacturerWarningDialog.kt b/energy_settings/src/main/kotlin/de/cyface/energy_settings/ProblematicManufacturerWarningDialog.kt index e1301a6..6ca5485 100644 --- a/energy_settings/src/main/kotlin/de/cyface/energy_settings/ProblematicManufacturerWarningDialog.kt +++ b/energy_settings/src/main/kotlin/de/cyface/energy_settings/ProblematicManufacturerWarningDialog.kt @@ -28,10 +28,13 @@ import android.content.pm.PackageManager import android.os.Build import android.os.Bundle import android.util.Log +import androidx.lifecycle.LifecycleCoroutineScope import com.afollestad.materialdialogs.MaterialDialog import de.cyface.energy_settings.Constants.TAG import de.cyface.energy_settings.GnssDisabledWarningDialog.Companion.create import de.cyface.energy_settings.ProblematicManufacturerWarningDialog.Companion.create +import de.cyface.energy_settings.settings.EnergySettings +import kotlinx.coroutines.launch import java.util.Locale /** @@ -51,12 +54,18 @@ import java.util.Locale * 2. As [MaterialDialog]. Use the static [create] method which returns the dialog. * * @author Armin Schnabel - * @version 2.1.0 + * @version 3.0.0 * @since 1.0.0 * - * @param recipientEmail The e-mail address to which the feedback email should be addressed to in the generated template. + * @property recipientEmail The e-mail address to which the feedback email should be addressed to in the generated template. + * @property scope The scope to execute async code in. + * @property settings The settings which contain the user preferences. */ -internal class ProblematicManufacturerWarningDialog(private val recipientEmail: String) : +internal class ProblematicManufacturerWarningDialog( + private val recipientEmail: String, + private val scope: LifecycleCoroutineScope, + private val settings: EnergySettings +) : EnergySettingDialog() { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { @@ -67,7 +76,9 @@ internal class ProblematicManufacturerWarningDialog(private val recipientEmail: // Allow the user to express its preference to disable auto-popup of this dialog builder.setNegativeButton(negativeButtonRes) { _, _ -> - onNegativeButtonCall(context) + scope.launch { + onNegativeButtonCall(settings) + } } // Show Sony STAMINA specific dialog (no manufacturer specific intent name known yet) @@ -165,17 +176,27 @@ internal class ProblematicManufacturerWarningDialog(private val recipientEmail: /** * Saves the user's preference to disable auto-popup of this dialog + * + * @param settings The settings which contain the user preferences. */ - private fun onNegativeButtonCall(context: Context?) { - CustomPreferences(context!!).saveWarningShown(true) + private suspend fun onNegativeButtonCall(settings: EnergySettings) { + settings.setManufacturerWarningShown(true) } /** * Alternative, `FragmentManager`-less implementation. * * @param activity Required to show the dialog + * @param recipientEmail The e-mail address to which the feedback email should be addressed to in the generated template. + * @param settings The settings which contain the user preferences. + * @param scope The scope to execute async code in. */ - fun create(activity: Activity, recipientEmail: String): MaterialDialog { + fun create( + activity: Activity, + recipientEmail: String, + settings: EnergySettings, + scope: LifecycleCoroutineScope + ): MaterialDialog { // Generate dialog val dialog = MaterialDialog(activity, DIALOG_BEHAVIOUR) @@ -183,7 +204,9 @@ internal class ProblematicManufacturerWarningDialog(private val recipientEmail: // Allow the user to express its preference to disable auto-popup of this dialog dialog.negativeButton(negativeButtonRes) { - onNegativeButtonCall(activity.applicationContext) + scope.launch { + onNegativeButtonCall(settings) + } } // Show Sony STAMINA specific dialog (no manufacturer specific intent name known yet) diff --git a/energy_settings/src/main/kotlin/de/cyface/energy_settings/TrackingSettings.kt b/energy_settings/src/main/kotlin/de/cyface/energy_settings/TrackingSettings.kt index e19b892..19eb481 100644 --- a/energy_settings/src/main/kotlin/de/cyface/energy_settings/TrackingSettings.kt +++ b/energy_settings/src/main/kotlin/de/cyface/energy_settings/TrackingSettings.kt @@ -30,7 +30,13 @@ import android.util.Log import androidx.annotation.RequiresApi import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager +import androidx.lifecycle.LifecycleCoroutineScope +import androidx.lifecycle.lifecycleScope +import de.cyface.energy_settings.TrackingSettings.initialize +import de.cyface.energy_settings.settings.EnergySettings import de.cyface.utils.Validate +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking import java.util.Locale /** @@ -38,12 +44,24 @@ import java.util.Locale * * Offers checks and dialogs for energy settings required for background tracking. * + * Attention: You need to call [initialize] before you use this object, e.g. in Activity.onCreate. + * * @author Armin Schnabel - * @version 2.0.3 + * @version 2.1.0 * @since 1.0.0 */ object TrackingSettings { + /** + * Custom settings used by this library. + */ + private lateinit var settings: EnergySettings + + @JvmStatic + fun initialize(context: Context) { + settings = EnergySettings(context) + } + /** * Checks whether the energy safer mode is active *at this moment*. * @@ -311,12 +329,19 @@ object TrackingSettings { force: Boolean, recipientEmail: String ): Boolean { - - val preferences = CustomPreferences(context) - if (isProblematicManufacturer && (force || !preferences.getWarningShown())) { + // TODO: consider async-preloading data at least, see: + // https://developer.android.com/topic/libraries/architecture/datastore#synchronous + val warningShown = runBlocking { + settings.manufacturerWarningShownFlow.first() + } + if (isProblematicManufacturer && (force || !warningShown)) { val fragmentManager = fragment.fragmentManager Validate.notNull(fragmentManager) - val dialog = ProblematicManufacturerWarningDialog(recipientEmail) + val dialog = ProblematicManufacturerWarningDialog( + recipientEmail, + fragment.lifecycleScope, + settings + ) dialog.setTargetFragment( fragment, Constants.DIALOG_PROBLEMATIC_MANUFACTURER_WARNING_CODE @@ -336,6 +361,7 @@ object TrackingSettings { * @param recipientEmail The e-mail address to which the feedback email should be addressed to in the generated * template. * @param force `True` if the dialog should be shown no matter of the preferences state + * @param scope The scope to execute async code in. * @return `True` if the dialog is shown */ @JvmStatic @@ -343,7 +369,8 @@ object TrackingSettings { fun showProblematicManufacturerDialog( activity: Activity?, force: Boolean, - recipientEmail: String + recipientEmail: String, + scope: LifecycleCoroutineScope ): Boolean { if (activity == null || activity.isFinishing) { @@ -351,9 +378,13 @@ object TrackingSettings { return false } - val preferences = CustomPreferences(activity.applicationContext) - if (isProblematicManufacturer && (force || !preferences.getWarningShown())) { - ProblematicManufacturerWarningDialog.create(activity, recipientEmail).show() + val warningShown = runBlocking { + settings.manufacturerWarningShownFlow.first() + } + + if (isProblematicManufacturer && (force || !warningShown)) { + ProblematicManufacturerWarningDialog.create(activity, recipientEmail, settings, scope) + .show() return true } return false diff --git a/energy_settings/src/main/kotlin/de/cyface/energy_settings/settings/EnergySettings.kt b/energy_settings/src/main/kotlin/de/cyface/energy_settings/settings/EnergySettings.kt new file mode 100644 index 0000000..10bd718 --- /dev/null +++ b/energy_settings/src/main/kotlin/de/cyface/energy_settings/settings/EnergySettings.kt @@ -0,0 +1,90 @@ +/* + * Copyright 2023 Cyface GmbH + * + * This file is part of the Cyface Energy Settings for Android. + * + * The Cyface Energy Settings for Android is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Cyface Energy Settings for Android is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with the Cyface Energy Settings for Android. If not, see . + */ +package de.cyface.energy_settings.settings + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import de.cyface.energy_settings.Settings +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +/** + * Settings used by this library. + * + * We currently don't use a repository to abstract the interface of the data types from the data + * source. The reason for this is the class is very simple and we don't plan multiple data sources. + * If this changes, consider using the standard Android Architecture, see `MeasurementRepository`. + * + * @author Armin Schnabel + * @version 2.0.0 + * @since 3.3.4 + * @param context The context to access the settings from. + */ +class EnergySettings(context: Context) { + + /** + * This avoids leaking the context when this object outlives the Activity of Fragment. + */ + private val appContext = context.applicationContext + + /** + * The data store with single-process support. + * + * We don't use multi-process support as this should usually only run in the ui process. + * I.e. there is no need for that overhead. + * + * Attention: + * - Never mix SingleProcessDataStore with MultiProcessDataStore for the same file. + * - We use SingleProcessDataStore, so don't access preferences from multiple processes. + * - Only create one instance of `DataStore` per file in the same process. + * - We use ProtoBuf to ensure type safety. Rebuild after changing the .proto file. + */ + private var dataStore: DataStore = DataStoreFactory.create( + serializer = SettingsSerializer, + produceFile = { appContext.dataStoreFile("energy_settings.pb") }, + migrations = listOf( + PreferencesMigrationFactory.create(appContext), + StoreMigration() + ) + ) + + /** + * Saves whether the user marked the manufacturer-specific warning as "don't show again". + * + * @param value The boolean value to save. + */ + @Suppress("unused") // Part of the API + suspend fun setManufacturerWarningShown(value: Boolean) { + dataStore.updateData { currentSettings -> + currentSettings.toBuilder() + .setManufacturerWarningShown(value) + .build() + } + } + + /** + * @return Whether user marked the manufacturer-specific warning as "don't show again". + */ + val manufacturerWarningShownFlow: Flow = dataStore.data + .map { settings -> + settings.manufacturerWarningShown + } +} \ No newline at end of file diff --git a/energy_settings/src/main/kotlin/de/cyface/energy_settings/settings/PreferencesMigration.kt b/energy_settings/src/main/kotlin/de/cyface/energy_settings/settings/PreferencesMigration.kt new file mode 100644 index 0000000..32e7897 --- /dev/null +++ b/energy_settings/src/main/kotlin/de/cyface/energy_settings/settings/PreferencesMigration.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2023 Cyface GmbH + * + * This file is part of the Cyface Energy Settings for Android. + * + * The Cyface Energy Settings for Android is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Cyface Energy Settings for Android is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with the Cyface Energy Settings for Android. If not, see . + */ +package de.cyface.energy_settings.settings + +import android.content.Context +import androidx.datastore.migrations.SharedPreferencesMigration +import androidx.datastore.migrations.SharedPreferencesView +import de.cyface.energy_settings.Settings + +/** + * Factory for the migration which imports preferences from the previously used SharedPreferences. + * + * @author Armin Schnabel + * @since 4.3.0 + */ +object PreferencesMigrationFactory { + + /** + * The filename, keys and defaults of the preferences, historically. + * + * *Don't change this, this is migration code!* + */ + private const val PREFERENCES_NAME = "AppPreferences" + private const val MANUFACTURER_WARNING_KEY = + "de.cyface.energy_settings.manufacturer_warning_shown" + + /** + * @param context The context to search and access the old SharedPreferences from. + * @return The migration code which imports preferences from the SharedPreferences if found. + */ + fun create(context: Context): SharedPreferencesMigration { + return SharedPreferencesMigration( + context, + PREFERENCES_NAME, + migrate = ::migratePreferences + ) + } + + private fun migratePreferences( + preferences: SharedPreferencesView, + settings: Settings + ): Settings { + return settings.toBuilder() + // Setting version to 1 as it would else default to Protobuf default of 0 which would + // trigger the StoreMigration from 0 -> 1 which ignores previous settings. + // This way the last supported version of SharedPreferences is hard-coded here and + // then the migration steps in StoreMigration starting at version 1 continues from here. + .setVersion(1) + .setManufacturerWarningShown(preferences.getBoolean(MANUFACTURER_WARNING_KEY, false)) + .build() + } +} \ No newline at end of file diff --git a/energy_settings/src/main/kotlin/de/cyface/energy_settings/settings/SettingsSerializer.kt b/energy_settings/src/main/kotlin/de/cyface/energy_settings/settings/SettingsSerializer.kt new file mode 100644 index 0000000..9779b45 --- /dev/null +++ b/energy_settings/src/main/kotlin/de/cyface/energy_settings/settings/SettingsSerializer.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2023 Cyface GmbH + * + * This file is part of the Cyface Energy Settings for Android. + * + * The Cyface Energy Settings for Android is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Cyface Energy Settings for Android is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with the Cyface Energy Settings for Android. If not, see . + */ +package de.cyface.energy_settings.settings + +import androidx.datastore.core.CorruptionException +import androidx.datastore.core.Serializer +import com.google.protobuf.InvalidProtocolBufferException +import de.cyface.energy_settings.Settings +import java.io.InputStream +import java.io.OutputStream + +/** + * The serializer for the Proto DataStore of the settings stored in the [Settings] file. + * + * For details: https://developer.android.com/topic/libraries/architecture/datastore#proto-datastore + * + * @author Armin Schnabel + * @since 3.4.0 + * @version 1.0.0 + */ +object SettingsSerializer : Serializer { + override val defaultValue: Settings = Settings.getDefaultInstance() + + override suspend fun readFrom(input: InputStream): Settings { + try { + return Settings.parseFrom(input) + } catch (exception: InvalidProtocolBufferException) { + throw CorruptionException("Cannot read proto.", exception) + } + } + + override suspend fun writeTo( + t: Settings, + output: OutputStream + ) = t.writeTo(output) +} \ No newline at end of file diff --git a/energy_settings/src/main/kotlin/de/cyface/energy_settings/settings/StoreMigration.kt b/energy_settings/src/main/kotlin/de/cyface/energy_settings/settings/StoreMigration.kt new file mode 100644 index 0000000..a8e1ea1 --- /dev/null +++ b/energy_settings/src/main/kotlin/de/cyface/energy_settings/settings/StoreMigration.kt @@ -0,0 +1,77 @@ +/* + * Copyright 2023 Cyface GmbH + * + * This file is part of the Cyface Energy Settings for Android. + * + * The Cyface Energy Settings for Android is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Cyface Energy Settings for Android is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with the Cyface Energy Settings for Android. If not, see . + */ +package de.cyface.energy_settings.settings + +import android.util.Log +import androidx.datastore.core.DataMigration +import de.cyface.energy_settings.Constants.TAG +import de.cyface.energy_settings.Settings +import de.cyface.utils.settings.MigrationException + +/** + * Migration which ensures DataStore files from all versions are compatible. + * + * @author Armin Schnabel + * @version 1.0.0 + * @since 3.4.0 + */ +class StoreMigration : DataMigration { + + /** + * The current version of the datastore schema. Increase this when migration is necessary, + * e.g. when you add a new field, to ensure a correct default value is set instead of the + * Protobuf default value for that data type like "" or 0, 0.0, etc. + */ + private val currentVersion = 1 + + override suspend fun shouldMigrate(currentData: Settings): Boolean { + // When no previous datastore file exists, the version starts at 0, i.e. this ensures + // that the correct default values are set instead of the Protobuf default value for that + // data type like "" or 0, 0.0, etc. + return currentData.version < currentVersion + } + + override suspend fun migrate(currentData: Settings): Settings { + val currentVersion = currentData.version + val targetVersion = currentVersion + 1 + val logTemplate = + "Migrating ${Settings::class.java.name} from $currentVersion to $targetVersion" + Log.i(TAG, String.format(logTemplate, currentVersion, targetVersion)) + + val builder = currentData.toBuilder() + when (currentVersion) { + 0 -> { + @Suppress("UsePropertyAccessSyntax") + builder + .setVersion(targetVersion) + .setManufacturerWarningShown(false) + } + + else -> { + throw MigrationException("No migration code for version ${currentVersion}.") + } + } + return builder.build() + } + + override suspend fun cleanUp() { + // Is called when migration was successful + // Currently, there is no cleanup to do, yet + } +} diff --git a/energy_settings/src/main/proto/energy_settings.proto b/energy_settings/src/main/proto/energy_settings.proto new file mode 100644 index 0000000..304e0f8 --- /dev/null +++ b/energy_settings/src/main/proto/energy_settings.proto @@ -0,0 +1,42 @@ +/* + * Copyright 2023 Cyface GmbH + * + * This file is part of the Cyface Energy Settings for Android. + * + * The Cyface Energy Settings for Android is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * The Cyface Energy Settings for Android is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with the Cyface Energy Settings for Android. If not, see . + */ +syntax = "proto3"; + +option java_package = "de.cyface.energy_settings"; +option java_multiple_files = true; + +/** + * The data types for the values stored in the `CustomSettings`. + * + * See https://protobuf.dev/programming-guides/proto3/ + * + * Attention: The classes are generated from the file at compile time. Don't forget to rebuild. + * + * @author: Armin Schnabel + * @since: 3.4.0 + * @version: 1.0.0 + */ +message Settings { + // Version of this datastore. It helps to support default values and to migrate data. When the + // datastore is newly created, it's 0, after default values are set it's 1, then migration happens. + uint32 version = 1; + + // Whether the user marked the manufacturer-specific warning as "don't show again". + bool manufacturer_warning_shown = 2; +} \ No newline at end of file