Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide abstraction over storage system #6

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 16 additions & 32 deletions library/src/main/java/de/Maxr1998/modernpreferences/Preferences.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,13 +25,13 @@ 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.Storage
import java.util.concurrent.atomic.AtomicBoolean

abstract class AbstractPreference internal constructor(val key: String) {
Expand Down Expand Up @@ -117,7 +116,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
Expand All @@ -137,7 +136,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()
}
Expand Down Expand Up @@ -259,36 +258,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, " +
Expand All @@ -299,12 +292,10 @@ open class Preference(key: String) : AbstractPreference(key) {
fun getString(defaultValue: String): String = throw UnsupportedOperationException("Not implemented")

fun commitStringSet(values: Set<String>) {
prefs?.edit {
putStringSet(key, values)
}
storage?.setStringSet(key, values)
}

fun getStringSet(): Set<String>? = prefs?.getStringSet(key, null)
fun getStringSet(): Set<String>? = storage?.getStringSet(key, null)

/**
* Can be set to [Preference.preBindListener]
Expand Down Expand Up @@ -365,7 +356,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<String, Preference> = builder.keyMap
private val preferences: List<Preference> = builder.preferences
internal val collapseIcon: Boolean = builder.collapseIcon
Expand Down Expand Up @@ -438,21 +429,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<String, Preference>()
internal val preferences = ArrayList<Preference>()

/**
* 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
*/
Expand Down Expand Up @@ -489,7 +474,6 @@ class PreferenceScreen private constructor(builder: Builder) : Preference(builde
}

fun build(): PreferenceScreen {
prefs = context?.getSharedPreferences(preferenceFileName, Context.MODE_PRIVATE)
context = null
return PreferenceScreen(this)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
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
Expand All @@ -22,7 +22,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 }
}
Expand All @@ -33,11 +33,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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,17 @@ 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
@Retention(AnnotationRetention.SOURCE)
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) {} }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package de.Maxr1998.modernpreferences.storage

import android.content.Context
import android.content.SharedPreferences

class SharedPreferencesStorage(
private val prefs: SharedPreferences,
private val mode: Mode = Mode.Apply
) : Storage {

enum class Mode {
Commit,
Apply
}

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)
}

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<String>) = edit {
putStringSet(key, values)
}

override fun getStringSet(key: String, defaultValue: Set<String>?): Set<String>? =
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()
}
}

}
Original file line number Diff line number Diff line change
@@ -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<String>)
fun getStringSet(key: String, defaultValue: Set<String>?): Set<String>?
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -142,7 +146,7 @@ class PreferencesTests {
}
check(dependent, dependency)

screen(contextMock) {
screen(contextMock, storageMock) {
val dependencyKey = uniqueKeySequence.next()
subScreen {
dependent = pref(uniqueKeySequence.next()) {
Expand All @@ -155,7 +159,7 @@ class PreferencesTests {
}
check(dependent, dependency)

screen(contextMock) {
screen(contextMock, storageMock) {
val dependencyKey = uniqueKeySequence.next()
dependency = switch(dependencyKey) {
this.disableDependents = disableDependents
Expand All @@ -168,7 +172,7 @@ class PreferencesTests {
}
check(dependent, dependency)

screen(contextMock) {
screen(contextMock, storageMock) {
val dependencyKey = uniqueKeySequence.next()
subScreen {
dependency = switch(dependencyKey) {
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
}

Expand All @@ -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)
Expand Down