Skip to content

Commit

Permalink
[RFR-769] Migrate to Proto DataStore (#59)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
hb0 authored Sep 18, 2023
1 parent 72f0b0f commit 970a4ee
Show file tree
Hide file tree
Showing 11 changed files with 440 additions and 54 deletions.
22 changes: 12 additions & 10 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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"

Expand All @@ -92,8 +99,3 @@ allprojects {
}

apply plugin: 'android-reporting'

// Auto-generated by Android Studio
task clean(type: Delete) {
delete rootProject.buildDir
}
27 changes: 26 additions & 1 deletion energy_settings/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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'
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019 Cyface GmbH
* Copyright 2019-2023 Cyface GmbH
*
* This file is part of the Cyface Energy Settings for Android.
*
Expand All @@ -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
Expand All @@ -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
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand All @@ -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 {
Expand All @@ -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)
Expand Down Expand Up @@ -165,25 +176,37 @@ 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)
dialog.title(titleRes, null)

// 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,38 @@ 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

/**
* Holds the API for this energy setting library.
*
* 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*.
*
Expand Down Expand Up @@ -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
Expand All @@ -336,24 +361,30 @@ 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
@Suppress("MemberVisibilityCanBePrivate") // Used by implementing app
fun showProblematicManufacturerDialog(
activity: Activity?,
force: Boolean,
recipientEmail: String
recipientEmail: String,
scope: LifecycleCoroutineScope
): Boolean {

if (activity == null || activity.isFinishing) {
Log.w(Constants.TAG, "showProblematicManufacturerDialog: aborted, activity is null")
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
Expand Down
Loading

0 comments on commit 970a4ee

Please sign in to comment.