diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b93798e2d..a91e2aa55 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -64,6 +64,7 @@ dependencies { implementation(libs.androidx.lifecycle.viewmodel.ktx) implementation(libs.androidslidinguppanel) implementation(libs.androidx.preference.ktx) + implementation(libs.androidx.datastore) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) diff --git a/app/src/main/java/com/darkempire78/opencalculator/MyPreferences.kt b/app/src/main/java/com/darkempire78/opencalculator/MyPreferences.kt index 7f8239994..9b2366f54 100644 --- a/app/src/main/java/com/darkempire78/opencalculator/MyPreferences.kt +++ b/app/src/main/java/com/darkempire78/opencalculator/MyPreferences.kt @@ -2,92 +2,161 @@ package com.darkempire78.opencalculator import android.content.Context import androidx.appcompat.app.AppCompatDelegate.* -import androidx.preference.PreferenceManager +import androidx.datastore.migrations.SharedPreferencesMigration +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 androidx.datastore.preferences.preferencesDataStore import com.darkempire78.opencalculator.history.History +import com.darkempire78.opencalculator.util.ScientificModeTypes import com.google.gson.Gson +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking -class MyPreferences(context: Context) { +private object PreferenceKeys { + // https://proandroiddev.com/dark-mode-on-android-app-with-kotlin-dc759fc5f0e1 + val THEME = intPreferencesKey("darkempire78.opencalculator.THEME") + val FORCE_DAY_NIGHT = intPreferencesKey("darkempire78.opencalculator.FORCE_DAY_NIGHT") + + val VIBRATION_STATUS = booleanPreferencesKey("darkempire78.opencalculator.KEY_VIBRATION_STATUS") + val HISTORY = stringPreferencesKey("darkempire78.opencalculator.HISTORY_ELEMENTS") + val PREVENT_PHONE_FROM_SLEEPING = booleanPreferencesKey("darkempire78.opencalculator.PREVENT_PHONE_FROM_SLEEPING") + val HISTORY_SIZE = stringPreferencesKey("darkempire78.opencalculator.HISTORY_SIZE") + val SCIENTIFIC_MODE_ENABLED_BY_DEFAULT = intPreferencesKey("darkempire78.opencalculator.SCIENTIFIC_MODE_ENABLED_BY_DEFAULT") + val RADIANS_INSTEAD_OF_DEGREES_BY_DEFAULT = booleanPreferencesKey("darkempire78.opencalculator.RADIANS_INSTEAD_OF_DEGREES_BY_DEFAULT") + val NUMBER_PRECISION = stringPreferencesKey("darkempire78.opencalculator.NUMBER_PRECISION") + val WRITE_NUMBER_INTO_SCIENTIFIC_NOTATION = booleanPreferencesKey("darkempire78.opencalculator.WRITE_NUMBER_INTO_SCIENTIC_NOTATION") // key typo + val LONG_CLICK_TO_COPY_VALUE = booleanPreferencesKey("darkempire78.opencalculator.LONG_CLICK_TO_COPY_VALUE") + val ADD_MODULO_BUTTON = booleanPreferencesKey("darkempire78.opencalculator.ADD_MODULO_BUTTON") + val SPLIT_PARENTHESIS_BUTTON = booleanPreferencesKey("darkempire78.opencalculator.SPLIT_PARENTHESIS_BUTTON") + val DELETE_HISTORY_ON_SWIPE = booleanPreferencesKey("darkempire78.opencalculator.DELETE_HISTORY_ELEMENT_ON_SWIPE") + val AUTO_SAVE_CALCULATION_WITHOUT_EQUAL_BUTTON = booleanPreferencesKey("darkempire78.opencalculator.AUTO_SAVE_CALCULATION_WITHOUT_EQUAL_BUTTON") + val MOVE_BACK_BUTTON_LEFT = booleanPreferencesKey("darkempire78.opencalculator.MOVE_BACK_BUTTON_LEFT") + val NUMBERING_SYSTEM = intPreferencesKey("darkempire78.opencalculator.NUMBERING_SYSTEM") + val SHOW_ON_LOCK_SCREEN = booleanPreferencesKey("darkempire78.opencalculator.KEY_SHOW_ON_LOCK_SCREEN") +} + +private val Context.dataStore by preferencesDataStore( + name = "datastore", + produceMigrations = { context -> + listOf( + SharedPreferencesMigration(context, context.packageName + "_preferences") { sharedPrefs, currentData -> + val mutablePreferences = currentData.toMutablePreferences() + // Migrate the scientific mode + // The migrated ordinal value according to these rules: + // - true → ScientificModeTypes.ACTIVE.ordinal (1) + // - false → ScientificModeTypes.NOT_ACTIVE.ordinal (0) + // - Invalid/unknown types → ScientificModeTypes.OFF.ordinal (2) + val oldScientificMode = sharedPrefs.getAll()[PreferenceKeys.SCIENTIFIC_MODE_ENABLED_BY_DEFAULT.name] + val migratedValue: Int = when (oldScientificMode) { + is Boolean -> { + if (oldScientificMode) { + ScientificModeTypes.ACTIVE.ordinal + } else { + ScientificModeTypes.NOT_ACTIVE.ordinal + } + } + is Int -> { + if (oldScientificMode in ScientificModeTypes.entries.toTypedArray().indices){ + oldScientificMode + } else { + ScientificModeTypes.OFF.ordinal + } + } + else -> ScientificModeTypes.OFF.ordinal + } + // set the migrated value + mutablePreferences[PreferenceKeys.SCIENTIFIC_MODE_ENABLED_BY_DEFAULT] = migratedValue + + mutablePreferences + } + ) + } +) - var ctx = context +class MyPreferences(private val context: Context) { - // https://proandroiddev.com/dark-mode-on-android-app-with-kotlin-dc759fc5f0e1 - companion object { - private const val THEME = "darkempire78.opencalculator.THEME" - private const val FORCE_DAY_NIGHT = "darkempire78.opencalculator.FORCE_DAY_NIGHT" - - private const val KEY_VIBRATION_STATUS = "darkempire78.opencalculator.KEY_VIBRATION_STATUS" - private const val KEY_HISTORY = "darkempire78.opencalculator.HISTORY_ELEMENTS" - private const val KEY_PREVENT_PHONE_FROM_SLEEPING = "darkempire78.opencalculator.PREVENT_PHONE_FROM_SLEEPING" - private const val KEY_HISTORY_SIZE = "darkempire78.opencalculator.HISTORY_SIZE" - private const val KEY_SCIENTIFIC_MODE_ENABLED_BY_DEFAULT = "darkempire78.opencalculator.SCIENTIFIC_MODE_ENABLED_BY_DEFAULT" - private const val KEY_RADIANS_INSTEAD_OF_DEGREES_BY_DEFAULT = "darkempire78.opencalculator.RADIANS_INSTEAD_OF_DEGREES_BY_DEFAULT" - private const val KEY_NUMBER_PRECISION = "darkempire78.opencalculator.NUMBER_PRECISION" - private const val KEY_WRITE_NUMBER_INTO_SCIENTIC_NOTATION = "darkempire78.opencalculator.WRITE_NUMBER_INTO_SCIENTIC_NOTATION" - private const val KEY_LONG_CLICK_TO_COPY_VALUE = "darkempire78.opencalculator.LONG_CLICK_TO_COPY_VALUE" - private const val KEY_ADD_MODULO_BUTTON = "darkempire78.opencalculator.ADD_MODULO_BUTTON" - private const val KEY_SPLIT_PARENTHESIS_BUTTON = "darkempire78.opencalculator.SPLIT_PARENTHESIS_BUTTON" - private const val KEY_DELETE_HISTORY_ON_SWIPE = "darkempire78.opencalculator.DELETE_HISTORY_ELEMENT_ON_SWIPE" - private const val KEY_AUTO_SAVE_CALCULATION_WITHOUT_EQUAL_BUTTON = "darkempire78.opencalculator.AUTO_SAVE_CALCULATION_WITHOUT_EQUAL_BUTTON" - private const val KEY_MOVE_BACK_BUTTON_LEFT = "darkempire78.opencalculator.MOVE_BACK_BUTTON_LEFT" - private const val KEY_NUMBERING_SYSTEM = "darkempire78.opencalculator.NUMBERING_SYSTEM" - private const val KEY_SHOW_ON_LOCK_SCREEN = "darkempire78.opencalculator.KEY_SHOW_ON_LOCK_SCREEN" + private fun readSync(key: Preferences.Key, default: T): T = runBlocking { + context.dataStore.data.first()[key] ?: default } - private val preferences = PreferenceManager.getDefaultSharedPreferences(context) - - var theme = preferences.getInt(THEME, -1) - set(value) = preferences.edit().putInt(THEME, value).apply() - var forceDayNight = preferences.getInt(FORCE_DAY_NIGHT, MODE_NIGHT_UNSPECIFIED) - set(value) = preferences.edit().putInt(FORCE_DAY_NIGHT, value).apply() - - var vibrationMode = preferences.getBoolean(KEY_VIBRATION_STATUS, true) - set(value) = preferences.edit().putBoolean(KEY_VIBRATION_STATUS, value).apply() - private val currentScientificModeTypes= MyPreferenceMigrator.migrateScientificMode(preferences, KEY_SCIENTIFIC_MODE_ENABLED_BY_DEFAULT) - var scientificMode = preferences.getInt(KEY_SCIENTIFIC_MODE_ENABLED_BY_DEFAULT, currentScientificModeTypes) - set(value) = preferences.edit().putInt(KEY_SCIENTIFIC_MODE_ENABLED_BY_DEFAULT, value).apply() - var useRadiansByDefault = preferences.getBoolean(KEY_RADIANS_INSTEAD_OF_DEGREES_BY_DEFAULT, false) - set(value) = preferences.edit().putBoolean(KEY_RADIANS_INSTEAD_OF_DEGREES_BY_DEFAULT, value).apply() - private var history = preferences.getString(KEY_HISTORY, null) - set(value) = preferences.edit().putString(KEY_HISTORY, value).apply() - var preventPhoneFromSleepingMode = preferences.getBoolean(KEY_PREVENT_PHONE_FROM_SLEEPING, false) - set(value) = preferences.edit().putBoolean(KEY_PREVENT_PHONE_FROM_SLEEPING, value).apply() - var historySize = preferences.getString(KEY_HISTORY_SIZE, "50") - set(value) = preferences.edit().putString(KEY_HISTORY_SIZE, value).apply() - var numberPrecision = preferences.getString(KEY_NUMBER_PRECISION, "10") - set(value) = preferences.edit().putString(KEY_NUMBER_PRECISION, value).apply() - var numberIntoScientificNotation = preferences.getBoolean(KEY_WRITE_NUMBER_INTO_SCIENTIC_NOTATION, false) - set(value) = preferences.edit().putBoolean(KEY_WRITE_NUMBER_INTO_SCIENTIC_NOTATION, value).apply() - var longClickToCopyValue = preferences.getBoolean(KEY_LONG_CLICK_TO_COPY_VALUE, true) - set(value) = preferences.edit().putBoolean(KEY_LONG_CLICK_TO_COPY_VALUE, value).apply() - var addModuloButton = preferences.getBoolean(KEY_ADD_MODULO_BUTTON, true) - set(value) = preferences.edit().putBoolean(KEY_ADD_MODULO_BUTTON, value).apply() - var splitParenthesisButton = preferences.getBoolean(KEY_SPLIT_PARENTHESIS_BUTTON, false) - set(value) = preferences.edit().putBoolean(KEY_SPLIT_PARENTHESIS_BUTTON, value).apply() - var deleteHistoryOnSwipe = preferences.getBoolean(KEY_DELETE_HISTORY_ON_SWIPE, true) - set(value) = preferences.edit().putBoolean(KEY_DELETE_HISTORY_ON_SWIPE, value).apply() - - var autoSaveCalculationWithoutEqualButton = preferences.getBoolean(KEY_AUTO_SAVE_CALCULATION_WITHOUT_EQUAL_BUTTON, true) - set(value) = preferences.edit().putBoolean(KEY_AUTO_SAVE_CALCULATION_WITHOUT_EQUAL_BUTTON, value).apply() - - var moveBackButtonLeft = preferences.getBoolean(KEY_MOVE_BACK_BUTTON_LEFT, false) - set(value) = preferences.edit().putBoolean(KEY_MOVE_BACK_BUTTON_LEFT, value).apply() - - var numberingSystem = preferences.getInt(KEY_NUMBERING_SYSTEM, 0) - set(value) = preferences.edit().putInt(KEY_NUMBERING_SYSTEM, value).apply() - - var showOnLockScreen = preferences.getBoolean(KEY_SHOW_ON_LOCK_SCREEN, true) - set(value) = preferences.edit().putBoolean(KEY_SHOW_ON_LOCK_SCREEN, value).apply() + private fun writeSync(key: Preferences.Key, value: T) = runBlocking { + context.dataStore.edit { prefs -> prefs[key] = value } + return@runBlocking + } + var theme: Int + get() = readSync(PreferenceKeys.THEME, -1) + set(value) = writeSync(PreferenceKeys.THEME, value) + + var forceDayNight: Int + get() = readSync(PreferenceKeys.FORCE_DAY_NIGHT, MODE_NIGHT_UNSPECIFIED) + set(value) = writeSync(PreferenceKeys.FORCE_DAY_NIGHT, value) + + var vibrationMode: Boolean + get() = readSync(PreferenceKeys.VIBRATION_STATUS, true) + set(value) = writeSync(PreferenceKeys.VIBRATION_STATUS, value) + + var scientificMode:Int + get() = readSync(PreferenceKeys.SCIENTIFIC_MODE_ENABLED_BY_DEFAULT, ScientificModeTypes.OFF.ordinal) + set(value) = writeSync(PreferenceKeys.SCIENTIFIC_MODE_ENABLED_BY_DEFAULT, value) + + var useRadiansByDefault: Boolean + get() = readSync(PreferenceKeys.RADIANS_INSTEAD_OF_DEGREES_BY_DEFAULT, false) + set(value) = writeSync(PreferenceKeys.RADIANS_INSTEAD_OF_DEGREES_BY_DEFAULT, value) + + private var historyJson: String + get() = readSync(PreferenceKeys.HISTORY, "") + set(value) = writeSync(PreferenceKeys.HISTORY, value) + + var preventPhoneFromSleepingMode: Boolean + get() = readSync(PreferenceKeys.PREVENT_PHONE_FROM_SLEEPING, false) + set(value) = writeSync(PreferenceKeys.PREVENT_PHONE_FROM_SLEEPING, value) + var historySize: String + get() = readSync(PreferenceKeys.HISTORY_SIZE, "50") + set(value) = writeSync(PreferenceKeys.HISTORY_SIZE, value) + var numberPrecision: String + get() = readSync(PreferenceKeys.NUMBER_PRECISION, "10") + set(value) = writeSync(PreferenceKeys.NUMBER_PRECISION, value) + var numberIntoScientificNotation: Boolean + get() = readSync(PreferenceKeys.WRITE_NUMBER_INTO_SCIENTIFIC_NOTATION, false) + set(value) = writeSync(PreferenceKeys.WRITE_NUMBER_INTO_SCIENTIFIC_NOTATION, value) + var longClickToCopyValue: Boolean + get() = readSync(PreferenceKeys.LONG_CLICK_TO_COPY_VALUE, true) + set(value) = writeSync(PreferenceKeys.LONG_CLICK_TO_COPY_VALUE, value) + var addModuloButton: Boolean + get() = readSync(PreferenceKeys.ADD_MODULO_BUTTON, true) + set(value) = writeSync(PreferenceKeys.ADD_MODULO_BUTTON, value) + var splitParenthesisButton: Boolean + get() = readSync(PreferenceKeys.SPLIT_PARENTHESIS_BUTTON, false) + set(value) = writeSync(PreferenceKeys.SPLIT_PARENTHESIS_BUTTON, value) + var deleteHistoryOnSwipe: Boolean + get() = readSync(PreferenceKeys.DELETE_HISTORY_ON_SWIPE, false) + set(value) = writeSync(PreferenceKeys.DELETE_HISTORY_ON_SWIPE, value) + + var autoSaveCalculationWithoutEqualButton: Boolean + get() = readSync(PreferenceKeys.AUTO_SAVE_CALCULATION_WITHOUT_EQUAL_BUTTON, true) + set(value) = writeSync(PreferenceKeys.AUTO_SAVE_CALCULATION_WITHOUT_EQUAL_BUTTON, value) + + var moveBackButtonLeft: Boolean + get() = readSync(PreferenceKeys.MOVE_BACK_BUTTON_LEFT, false) + set(value) = writeSync(PreferenceKeys.MOVE_BACK_BUTTON_LEFT, value) + + var numberingSystem: Int + get() = readSync(PreferenceKeys.NUMBERING_SYSTEM, 0) + set(value) = writeSync(PreferenceKeys.NUMBERING_SYSTEM, value) + + var showOnLockScreen: Boolean + get() = readSync(PreferenceKeys.SHOW_ON_LOCK_SCREEN, true) + set(value) = writeSync(PreferenceKeys.SHOW_ON_LOCK_SCREEN, value) fun getHistory(): MutableList { val gson = Gson() - - val historyJson = preferences.getString(KEY_HISTORY, null) - - return if (historyJson != null) { + return if (historyJson.isNotEmpty()) { try { - val list = gson.fromJson(historyJson, Array::class.java).asList().toMutableList() - list + gson.fromJson(historyJson, Array::class.java).asList().toMutableList() } catch (e: Exception) { mutableListOf() } @@ -96,15 +165,13 @@ class MyPreferences(context: Context) { } } - - fun saveHistory(history: List){ val gson = Gson() val history2 = history.toMutableList() - while (historySize!!.toInt() > 0 && history2.size > historySize!!.toInt()) { + while (historySize.toInt() > 0 && history2.size > historySize.toInt()) { history2.removeAt(0) } - MyPreferences(ctx).history = gson.toJson(history2) // Convert to json + historyJson = gson.toJson(history2) // Convert to json } fun getHistoryElementById(id: String): History? { diff --git a/app/src/main/java/com/darkempire78/opencalculator/util/MyPreferenceMigrator.kt b/app/src/main/java/com/darkempire78/opencalculator/util/MyPreferenceMigrator.kt deleted file mode 100644 index 754600302..000000000 --- a/app/src/main/java/com/darkempire78/opencalculator/util/MyPreferenceMigrator.kt +++ /dev/null @@ -1,102 +0,0 @@ -import android.content.SharedPreferences -import com.darkempire78.opencalculator.util.ScientificModeTypes - -/** - * Handles migration of SharedPreferences values between different data types. - * Primarily used for converting boolean flags to enum ordinal representations. - */ -object MyPreferenceMigrator { - - - /** - * Migrates a boolean preference value to its corresponding enum ordinal representation. - * - * @param sharedPreferences The SharedPreferences instance containing the value to migrate - * @param key The preference key to migrate - * @return The migrated ordinal value according to these rules: - * - true → ScientificModeTypes.ACTIVE.ordinal (1) - * - false → ScientificModeTypes.NOT_ACTIVE.ordinal (0) - * - Invalid/unknown types → ScientificModeTypes.OFF.ordinal (2) - * @throws IllegalArgumentException if sharedPreferences is null - */ - fun migrateScientificMode(sharedPreferences: SharedPreferences, key: String): Int { - - return when (val value = sharedPreferences.all[key]) { - // Boolean case - convert to corresponding ordinal - is Boolean -> { - val modeOrdinal = when { - value -> ScientificModeTypes.ACTIVE.ordinal - else -> ScientificModeTypes.NOT_ACTIVE.ordinal - } - saveMigratedValue(sharedPreferences, key, modeOrdinal) - } - - // Already migrated case (stored as Int) - is Int -> { - if (value in ScientificModeTypes.entries.toTypedArray().indices) { - value // Return existing valid ordinal - } else { - resetToDefault(sharedPreferences, key) - } - } - // All other cases reset to OFF - else -> resetToDefault(sharedPreferences, key) - } - } - - /** - * Safely saves a migrated value to SharedPreferences. - * - * @param sharedPreferences The SharedPreferences instance to modify - * @param key The preference key to update - * @param value The ordinal value to save - * @return The saved ordinal value - * @throws IllegalArgumentException if value is not a valid ordinal - */ - private fun saveMigratedValue( - sharedPreferences: SharedPreferences, - key: String, - value: Int - ): Int { - require(value in ScientificModeTypes.entries.toTypedArray().indices) { - "Invalid ordinal value $value for ScientificModeTypes" - } - - sharedPreferences.edit() - .remove(key) // Remove old value first - .putInt(key, value) - .apply() - return value - } - - /** - * Resets a preference to the default OFF state. - * - * @param sharedPreferences The SharedPreferences instance to modify - * @param key The preference key to reset - * @return The default ordinal value (ScientificModeTypes.OFF.ordinal) - */ - private fun resetToDefault(sharedPreferences: SharedPreferences, key: String): Int { - return saveMigratedValue( - sharedPreferences, - key, - ScientificModeTypes.OFF.ordinal - ) - } - - /** - * Helper function to safely retrieve the current scientific mode. - * - * @param sharedPreferences The SharedPreferences instance to read from - * @param key The preference key to read - * @return The current ScientificModeTypes enum value - */ - fun getCurrentMode(sharedPreferences: SharedPreferences, key: String): ScientificModeTypes { - return try { - val ordinal = sharedPreferences.getInt(key, ScientificModeTypes.OFF.ordinal) - ScientificModeTypes.entries.getOrNull(ordinal) ?: ScientificModeTypes.OFF - } catch (e: Exception) { - ScientificModeTypes.OFF - } - } -} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c5238c5e7..4932195ce 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ material = "1.12.0" preferenceKtx = "1.2.1" kotlin = "2.0.0" runtime = "1.6.8" +datastore = "1.1.7" [libraries] androidslidinguppanel = { module = "com.github.hannesa2:AndroidSlidingUpPanel", version.ref = "androidslidinguppanel" } @@ -27,6 +28,7 @@ androidx-runtime = { module = "androidx.compose.runtime:runtime", version.ref = gson = { module = "com.google.code.gson:gson", version.ref = "gson" } junit = { module = "junit:junit", version.ref = "junit" } material = { module = "com.google.android.material:material", version.ref = "material" } +androidx-datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore"} [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" }