From 9b49909a6287e17cdb42294e02a04d397a2f6546 Mon Sep 17 00:00:00 2001 From: MFlisar Date: Thu, 10 Dec 2020 09:34:06 +0100 Subject: [PATCH 1/4] add storage interface to support arbitrary storage implementations --- .../Maxr1998/modernpreferences/Preferences.kt | 47 +++++-------- .../helpers/DependencyManager.kt | 7 +- .../helpers/PreferencesDsl.kt | 6 +- .../storage/SharedPreferencesStorage.kt | 68 +++++++++++++++++++ .../modernpreferences/storage/Storage.kt | 16 +++++ 5 files changed, 109 insertions(+), 35 deletions(-) create mode 100644 library/src/main/java/de/Maxr1998/modernpreferences/storage/SharedPreferencesStorage.kt create mode 100644 library/src/main/java/de/Maxr1998/modernpreferences/storage/Storage.kt diff --git a/library/src/main/java/de/Maxr1998/modernpreferences/Preferences.kt b/library/src/main/java/de/Maxr1998/modernpreferences/Preferences.kt index d71fbe02..43b3ed5c 100644 --- a/library/src/main/java/de/Maxr1998/modernpreferences/Preferences.kt +++ b/library/src/main/java/de/Maxr1998/modernpreferences/Preferences.kt @@ -33,6 +33,8 @@ import de.Maxr1998.modernpreferences.helpers.KEY_ROOT_SCREEN import de.Maxr1998.modernpreferences.helpers.PreferenceMarker import de.Maxr1998.modernpreferences.preferences.CollapsePreference import de.Maxr1998.modernpreferences.preferences.SeekBarPreference +import de.Maxr1998.modernpreferences.storage.SharedPreferencesStorage +import de.Maxr1998.modernpreferences.storage.Storage import java.util.concurrent.atomic.AtomicBoolean abstract class AbstractPreference internal constructor(val key: String) { @@ -117,7 +119,7 @@ open class Preference(key: String) : AbstractPreference(key) { var screenPosition: Int = 0 private set - private var prefs: SharedPreferences? = null + private var storage: Storage? = null /** * Whether or not to persist changes to this preference to the attached [SharedPreferences] instance @@ -137,7 +139,7 @@ open class Preference(key: String) : AbstractPreference(key) { check(parent == null) { "Preference was already attached to a screen!" } parent = screen screenPosition = position - prefs = if (persistent) screen.prefs else null + storage = if (persistent) screen.storage else null DependencyManager.register(this) onAttach() } @@ -259,36 +261,30 @@ open class Preference(key: String) : AbstractPreference(key) { * Save an int for this [Preference]s' [key] to the [SharedPreferences] of the attached [PreferenceScreen] */ fun commitInt(value: Int) { - prefs?.edit { - putInt(key, value) - } + storage?.setInt(key, value) } fun getInt(defaultValue: Int): Int = - prefs?.getInt(key, defaultValue) ?: defaultValue + storage?.getInt(key, defaultValue) ?: defaultValue /** * Save a boolean for this [Preference]s' [key] to the [SharedPreferences] of the attached [PreferenceScreen] */ fun commitBoolean(value: Boolean) { - prefs?.edit { - putBoolean(key, value) - } + storage?.setBoolean(key, value) } fun getBoolean(defaultValue: Boolean): Boolean = - prefs?.getBoolean(key, defaultValue) ?: defaultValue + storage?.getBoolean(key, defaultValue) ?: defaultValue /** * Save a String for this [Preference]s' [key] to the [SharedPreferences] of the attached [PreferenceScreen] */ fun commitString(value: String) { - prefs?.edit { - putString(key, value) - } + storage?.setString(key, value) } - fun getString(): String? = prefs?.getString(key, null) + fun getString(): String? = storage?.getString(key, null) @Deprecated( "Passing a default value is not supported anymore, " + @@ -299,12 +295,10 @@ open class Preference(key: String) : AbstractPreference(key) { fun getString(defaultValue: String): String = throw UnsupportedOperationException("Not implemented") fun commitStringSet(values: Set) { - prefs?.edit { - putStringSet(key, values) - } + storage?.setStringSet(key, values) } - fun getStringSet(): Set? = prefs?.getStringSet(key, null) + fun getStringSet(): Set? = storage?.getStringSet(key, null) /** * Can be set to [Preference.preBindListener] @@ -365,7 +359,7 @@ open class Preference(key: String) : AbstractPreference(key) { */ @Suppress("unused", "MemberVisibilityCanBePrivate") class PreferenceScreen private constructor(builder: Builder) : Preference(builder.key) { - internal val prefs = builder.prefs + internal val storage = builder.storage private val keyMap: Map = builder.keyMap private val preferences: List = builder.preferences internal val collapseIcon: Boolean = builder.collapseIcon @@ -438,21 +432,15 @@ class PreferenceScreen private constructor(builder: Builder) : Preference(builde override fun hashCode() = (31 * key.hashCode()) + preferences.hashCode() @PreferenceMarker - class Builder private constructor(private var context: Context?, key: String) : AbstractPreference(key), Appendable { - constructor(context: Context?) : this(context, KEY_ROOT_SCREEN) - constructor(builder: Builder, key: String = "") : this(builder.context, key) - constructor(collapse: CollapsePreference, key: String = "") : this(collapse.screen?.context, key) + class Builder private constructor(private var context: Context?, internal val storage: Storage?, key: String) : AbstractPreference(key), Appendable { + constructor(context: Context?, storage: Storage?) : this(context, storage, KEY_ROOT_SCREEN) + constructor(builder: Builder, key: String = "") : this(builder.context, builder.storage, key) + constructor(collapse: CollapsePreference, key: String = "") : this(collapse.screen?.context, collapse.screen?.storage, key) // Internal structures - internal var prefs: SharedPreferences? = null internal val keyMap = HashMap() internal val preferences = ArrayList() - /** - * The filename to use for the [SharedPreferences] of this [PreferenceScreen] - */ - var preferenceFileName: String = (context?.packageName ?: "package") + "_preferences" - /** * If true, the preference items in this screen will have a smaller left padding when they have no icon */ @@ -489,7 +477,6 @@ class PreferenceScreen private constructor(builder: Builder) : Preference(builde } fun build(): PreferenceScreen { - prefs = context?.getSharedPreferences(preferenceFileName, Context.MODE_PRIVATE) context = null return PreferenceScreen(this) } diff --git a/library/src/main/java/de/Maxr1998/modernpreferences/helpers/DependencyManager.kt b/library/src/main/java/de/Maxr1998/modernpreferences/helpers/DependencyManager.kt index 17341419..ace3c2c3 100644 --- a/library/src/main/java/de/Maxr1998/modernpreferences/helpers/DependencyManager.kt +++ b/library/src/main/java/de/Maxr1998/modernpreferences/helpers/DependencyManager.kt @@ -3,6 +3,7 @@ package de.Maxr1998.modernpreferences.helpers import android.content.SharedPreferences import de.Maxr1998.modernpreferences.Preference import de.Maxr1998.modernpreferences.preferences.StatefulPreference +import de.Maxr1998.modernpreferences.storage.Storage import java.lang.ref.WeakReference import java.util.* import kotlin.collections.HashMap @@ -22,7 +23,7 @@ internal object DependencyManager { val screen = preference.parent check(screen != null) { "Preference must be attached to a screen first" } val dependency = preference.dependency ?: return - val key = PreferenceKey(screen.prefs, dependency) + val key = PreferenceKey(screen.storage, dependency) preferences.getOrPut(key) { LinkedList() }.add(WeakReference(preference)) stateCache[key]?.let { state -> preference.enabled = state } } @@ -33,11 +34,11 @@ internal object DependencyManager { fun publishState(preference: StatefulPreference) { val screen = preference.parent check(screen != null) { "Preference must be attached to a screen first" } - val key = PreferenceKey(screen.prefs, preference.key) + val key = PreferenceKey(screen.storage, preference.key) val state = preference.state // Cache state so that every dependent gets the same value stateCache[key] = state preferences[key]?.forEach { it.get()?.enabled = state } } - private data class PreferenceKey(val preferenceStore: SharedPreferences?, val key: String) + private data class PreferenceKey(val storage: Storage?, val key: String) } \ No newline at end of file diff --git a/library/src/main/java/de/Maxr1998/modernpreferences/helpers/PreferencesDsl.kt b/library/src/main/java/de/Maxr1998/modernpreferences/helpers/PreferencesDsl.kt index ef652bbd..8269af11 100644 --- a/library/src/main/java/de/Maxr1998/modernpreferences/helpers/PreferencesDsl.kt +++ b/library/src/main/java/de/Maxr1998/modernpreferences/helpers/PreferencesDsl.kt @@ -26,6 +26,8 @@ import de.Maxr1998.modernpreferences.preferences.* import de.Maxr1998.modernpreferences.preferences.choice.MultiChoiceDialogPreference import de.Maxr1998.modernpreferences.preferences.choice.SelectionItem import de.Maxr1998.modernpreferences.preferences.choice.SingleChoiceDialogPreference +import de.Maxr1998.modernpreferences.storage.SharedPreferencesStorage +import de.Maxr1998.modernpreferences.storage.Storage // DSL marker @DslMarker @@ -33,8 +35,8 @@ import de.Maxr1998.modernpreferences.preferences.choice.SingleChoiceDialogPrefer annotation class PreferenceMarker // PreferenceScreen DSL functions -inline fun screen(context: Context?, block: PreferenceScreen.Builder.() -> Unit): PreferenceScreen { - return PreferenceScreen.Builder(context).apply(block).build() +inline fun screen(context: Context?, storage: Storage? = context?.let { SharedPreferencesStorage(it) }, block: PreferenceScreen.Builder.() -> Unit): PreferenceScreen { + return PreferenceScreen.Builder(context, storage).apply(block).build() } val emptyScreen: PreferenceScreen by lazy { screen(null) {} } diff --git a/library/src/main/java/de/Maxr1998/modernpreferences/storage/SharedPreferencesStorage.kt b/library/src/main/java/de/Maxr1998/modernpreferences/storage/SharedPreferencesStorage.kt new file mode 100644 index 00000000..9d328e66 --- /dev/null +++ b/library/src/main/java/de/Maxr1998/modernpreferences/storage/SharedPreferencesStorage.kt @@ -0,0 +1,68 @@ +package de.Maxr1998.modernpreferences.storage + +import android.content.Context +import android.content.SharedPreferences +import de.Maxr1998.modernpreferences.PreferenceScreen + +class SharedPreferencesStorage( + context: Context, + private val mode: Mode = Mode.Apply +) : Storage { + + enum class Mode { + Commit, + Apply + } + + private val prefs: SharedPreferences + + /** + * The filename to use for the [SharedPreferences] of this [PreferenceScreen] + */ + var preferenceFileName: String = (context.packageName ?: "package") + "_preferences" + + init { + prefs = context.getSharedPreferences(preferenceFileName, Context.MODE_PRIVATE) + } + + override fun setInt(key: String, value: Int) = edit { + putInt(key, value) + } + + override fun getInt(key: String, defaultValue: Int): Int = + prefs.getInt(key, defaultValue) + + override fun setBoolean(key: String, value: Boolean) = edit { + putBoolean(key, value) + } + + override fun getBoolean(key: String, defaultValue: Boolean): Boolean = + prefs.getBoolean(key, defaultValue) + + override fun setString(key: String, value: String) = edit { + putString(key, value) + } + + override fun getString(key: String, defaultValue: String?): String? = + prefs.getString(key, defaultValue) + + override fun setStringSet(key: String, values: Set) = edit { + putStringSet(key, values) + } + + override fun getStringSet(key: String, defaultValue: Set?): Set? = + prefs.getStringSet(key, defaultValue) + + private fun edit( + action: SharedPreferences.Editor.() -> Unit + ) { + val editor = prefs.edit() + action(editor) + if (mode == Mode.Commit) { + editor.commit() + } else { + editor.apply() + } + } + +} \ No newline at end of file diff --git a/library/src/main/java/de/Maxr1998/modernpreferences/storage/Storage.kt b/library/src/main/java/de/Maxr1998/modernpreferences/storage/Storage.kt new file mode 100644 index 00000000..72244f17 --- /dev/null +++ b/library/src/main/java/de/Maxr1998/modernpreferences/storage/Storage.kt @@ -0,0 +1,16 @@ +package de.Maxr1998.modernpreferences.storage + +interface Storage { + + fun setInt(key: String, value: Int) + fun getInt(key: String, defaultValue: Int): Int + + fun setBoolean(key: String, value: Boolean) + fun getBoolean(key: String, defaultValue: Boolean): Boolean + + fun setString(key: String, value: String) + fun getString(key: String, defaultValue: String?): String? + + fun setStringSet(key: String, values: Set) + fun getStringSet(key: String, defaultValue: Set?): Set? +} \ No newline at end of file From 344dbf1ff49c33d5d36357ae5b880452617f2fc0 Mon Sep 17 00:00:00 2001 From: MFlisar Date: Thu, 10 Dec 2020 09:45:42 +0100 Subject: [PATCH 2/4] cleaned imports --- .../src/main/java/de/Maxr1998/modernpreferences/Preferences.kt | 3 --- .../de/Maxr1998/modernpreferences/helpers/DependencyManager.kt | 1 - 2 files changed, 4 deletions(-) diff --git a/library/src/main/java/de/Maxr1998/modernpreferences/Preferences.kt b/library/src/main/java/de/Maxr1998/modernpreferences/Preferences.kt index 43b3ed5c..ba3c0462 100644 --- a/library/src/main/java/de/Maxr1998/modernpreferences/Preferences.kt +++ b/library/src/main/java/de/Maxr1998/modernpreferences/Preferences.kt @@ -17,7 +17,6 @@ package de.Maxr1998.modernpreferences import android.content.Context -import android.content.SharedPreferences import android.graphics.drawable.Drawable import android.view.Gravity import android.view.ViewGroup @@ -26,14 +25,12 @@ import androidx.annotation.CallSuper import androidx.annotation.DrawableRes import androidx.annotation.LayoutRes import androidx.annotation.StringRes -import androidx.core.content.edit import androidx.core.view.isVisible import de.Maxr1998.modernpreferences.helpers.DependencyManager import de.Maxr1998.modernpreferences.helpers.KEY_ROOT_SCREEN import de.Maxr1998.modernpreferences.helpers.PreferenceMarker import de.Maxr1998.modernpreferences.preferences.CollapsePreference import de.Maxr1998.modernpreferences.preferences.SeekBarPreference -import de.Maxr1998.modernpreferences.storage.SharedPreferencesStorage import de.Maxr1998.modernpreferences.storage.Storage import java.util.concurrent.atomic.AtomicBoolean diff --git a/library/src/main/java/de/Maxr1998/modernpreferences/helpers/DependencyManager.kt b/library/src/main/java/de/Maxr1998/modernpreferences/helpers/DependencyManager.kt index ace3c2c3..e6903e28 100644 --- a/library/src/main/java/de/Maxr1998/modernpreferences/helpers/DependencyManager.kt +++ b/library/src/main/java/de/Maxr1998/modernpreferences/helpers/DependencyManager.kt @@ -1,6 +1,5 @@ package de.Maxr1998.modernpreferences.helpers -import android.content.SharedPreferences import de.Maxr1998.modernpreferences.Preference import de.Maxr1998.modernpreferences.preferences.StatefulPreference import de.Maxr1998.modernpreferences.storage.Storage From fe4a49cc27ebf080f66db90c48b62193cf6b450c Mon Sep 17 00:00:00 2001 From: MFlisar Date: Thu, 10 Dec 2020 09:47:30 +0100 Subject: [PATCH 3/4] moved SharedPreferencesStorage filename definition into constructor --- .../storage/SharedPreferencesStorage.kt | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/library/src/main/java/de/Maxr1998/modernpreferences/storage/SharedPreferencesStorage.kt b/library/src/main/java/de/Maxr1998/modernpreferences/storage/SharedPreferencesStorage.kt index 9d328e66..c3ecc9c9 100644 --- a/library/src/main/java/de/Maxr1998/modernpreferences/storage/SharedPreferencesStorage.kt +++ b/library/src/main/java/de/Maxr1998/modernpreferences/storage/SharedPreferencesStorage.kt @@ -2,10 +2,10 @@ package de.Maxr1998.modernpreferences.storage import android.content.Context import android.content.SharedPreferences -import de.Maxr1998.modernpreferences.PreferenceScreen class SharedPreferencesStorage( context: Context, + preferenceFileName: String = (context.packageName ?: "package") + "_preferences", private val mode: Mode = Mode.Apply ) : Storage { @@ -14,16 +14,8 @@ class SharedPreferencesStorage( Apply } - private val prefs: SharedPreferences - - /** - * The filename to use for the [SharedPreferences] of this [PreferenceScreen] - */ - var preferenceFileName: String = (context.packageName ?: "package") + "_preferences" - - init { - prefs = context.getSharedPreferences(preferenceFileName, Context.MODE_PRIVATE) - } + private val prefs: SharedPreferences = + context.getSharedPreferences(preferenceFileName, Context.MODE_PRIVATE) override fun setInt(key: String, value: Int) = edit { putInt(key, value) From 443037ced2aa0cbd05c930dd473ac5f4943514f9 Mon Sep 17 00:00:00 2001 From: MFlisar Date: Thu, 10 Dec 2020 12:21:40 +0100 Subject: [PATCH 4/4] adjustments for tests --- .../storage/SharedPreferencesStorage.kt | 10 ++++--- .../testing/PreferencesTests.kt | 26 +++++++++++-------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/library/src/main/java/de/Maxr1998/modernpreferences/storage/SharedPreferencesStorage.kt b/library/src/main/java/de/Maxr1998/modernpreferences/storage/SharedPreferencesStorage.kt index c3ecc9c9..972a39a7 100644 --- a/library/src/main/java/de/Maxr1998/modernpreferences/storage/SharedPreferencesStorage.kt +++ b/library/src/main/java/de/Maxr1998/modernpreferences/storage/SharedPreferencesStorage.kt @@ -4,8 +4,7 @@ import android.content.Context import android.content.SharedPreferences class SharedPreferencesStorage( - context: Context, - preferenceFileName: String = (context.packageName ?: "package") + "_preferences", + private val prefs: SharedPreferences, private val mode: Mode = Mode.Apply ) : Storage { @@ -14,8 +13,11 @@ class SharedPreferencesStorage( Apply } - private val prefs: SharedPreferences = - context.getSharedPreferences(preferenceFileName, Context.MODE_PRIVATE) + constructor( + context: Context, + preferenceFileName: String = (context.packageName ?: "package") + "_preferences", + mode: Mode = Mode.Apply + ) : this(context.getSharedPreferences(preferenceFileName, Context.MODE_PRIVATE), mode) override fun setInt(key: String, value: Int) = edit { putInt(key, value) diff --git a/library/src/test/java/de/Maxr1998/modernpreferences/testing/PreferencesTests.kt b/library/src/test/java/de/Maxr1998/modernpreferences/testing/PreferencesTests.kt index 01f9e5b1..ac9ebda1 100644 --- a/library/src/test/java/de/Maxr1998/modernpreferences/testing/PreferencesTests.kt +++ b/library/src/test/java/de/Maxr1998/modernpreferences/testing/PreferencesTests.kt @@ -11,6 +11,8 @@ import de.Maxr1998.modernpreferences.helpers.subScreen import de.Maxr1998.modernpreferences.helpers.switch import de.Maxr1998.modernpreferences.preferences.SwitchPreference import de.Maxr1998.modernpreferences.preferences.TwoStatePreference +import de.Maxr1998.modernpreferences.storage.SharedPreferencesStorage +import de.Maxr1998.modernpreferences.storage.Storage import io.kotest.assertions.throwables.shouldThrow import io.kotest.data.blocking.forAll import io.kotest.data.row @@ -32,11 +34,13 @@ import org.junit.jupiter.api.TestInstance class PreferencesTests { private val contextMock: Context = mockk() + private lateinit var storageMock: Storage @BeforeAll fun setup() { // Setup mocks for SharedPreferences val sharedPreferences = SPMockBuilder().createSharedPreferences() + storageMock = SharedPreferencesStorage(sharedPreferences) every { contextMock.packageName } returns "package" every { contextMock.getSharedPreferences(any(), any()) } returns sharedPreferences } @@ -76,7 +80,7 @@ class PreferencesTests { row(a = true, b = true, c = false) ) { checked: Boolean, disableDependents: Boolean, state: Boolean -> lateinit var pref: TwoStatePreference - screen(contextMock) { + screen(contextMock, storageMock) { pref = switch(uniqueKeySequence.next()) { this.disableDependents = disableDependents } @@ -105,7 +109,7 @@ class PreferencesTests { runBlocking { checkAll(2, Exhaustive.boolean()) { disableDependents -> - screen(contextMock) { + screen(contextMock, storageMock) { val dependencyKey = uniqueKeySequence.next() dependent = pref(uniqueKeySequence.next()) { this.dependency = dependencyKey @@ -117,7 +121,7 @@ class PreferencesTests { check(dependent, dependency) // With inverted order of dependent and dependency - screen(contextMock) { + screen(contextMock, storageMock) { val dependencyKey = uniqueKeySequence.next() dependency = switch(dependencyKey) { this.disableDependents = disableDependents @@ -129,7 +133,7 @@ class PreferencesTests { check(dependent, dependency) // With sub-screens - screen(contextMock) { + screen(contextMock, storageMock) { val dependencyKey = uniqueKeySequence.next() dependent = pref(uniqueKeySequence.next()) { this.dependency = dependencyKey @@ -142,7 +146,7 @@ class PreferencesTests { } check(dependent, dependency) - screen(contextMock) { + screen(contextMock, storageMock) { val dependencyKey = uniqueKeySequence.next() subScreen { dependent = pref(uniqueKeySequence.next()) { @@ -155,7 +159,7 @@ class PreferencesTests { } check(dependent, dependency) - screen(contextMock) { + screen(contextMock, storageMock) { val dependencyKey = uniqueKeySequence.next() dependency = switch(dependencyKey) { this.disableDependents = disableDependents @@ -168,7 +172,7 @@ class PreferencesTests { } check(dependent, dependency) - screen(contextMock) { + screen(contextMock, storageMock) { val dependencyKey = uniqueKeySequence.next() subScreen { dependency = switch(dependencyKey) { @@ -190,7 +194,7 @@ class PreferencesTests { // Setup screens lateinit var subScreen: PreferenceScreen - val rootScreen = screen(contextMock) { + val rootScreen = screen(contextMock, storageMock) { subScreen = +PreferenceScreen.Builder(this, "").build() } adapter.setRootScreen(rootScreen) @@ -224,7 +228,7 @@ class PreferencesTests { // Setup screens lateinit var subScreen: PreferenceScreen - val rootScreen = screen(contextMock) { + val rootScreen = screen(contextMock, storageMock) { subScreen = +PreferenceScreen.Builder(this, "").build() } adapter.setRootScreen(rootScreen) @@ -257,7 +261,7 @@ class PreferencesTests { @Test fun `Saved state should be empty on root screen`() { val adapter = createPreferenceAdapter() - adapter.setRootScreen(screen(contextMock) {}) + adapter.setRootScreen(screen(contextMock, storageMock) {}) adapter.getSavedState().screenPath.size shouldBe 0 } @@ -267,7 +271,7 @@ class PreferencesTests { // Setup screens lateinit var subScreen: PreferenceScreen - val rootScreen = screen(contextMock) { + val rootScreen = screen(contextMock, storageMock) { subScreen = +PreferenceScreen.Builder(this, "").build() } adapter.setRootScreen(rootScreen)