mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-16 19:26:12 -04:00
Refactor/add settings
This commit is contained in:
@@ -154,6 +154,9 @@ dependencies {
|
|||||||
implementation ('com.bugsnag:bugsnag-android:5.9.2')
|
implementation ('com.bugsnag:bugsnag-android:5.9.2')
|
||||||
implementation 'com.getkeepsafe.relinker:relinker:1.4.3'
|
implementation 'com.getkeepsafe.relinker:relinker:1.4.3'
|
||||||
|
|
||||||
|
implementation("com.squareup.moshi:moshi:1.13.0")
|
||||||
|
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.13.0")
|
||||||
|
|
||||||
// DI
|
// DI
|
||||||
implementation "com.google.dagger:dagger:${versions.dagger.core}"
|
implementation "com.google.dagger:dagger:${versions.dagger.core}"
|
||||||
implementation "com.google.dagger:dagger-android:${versions.dagger.core}"
|
implementation "com.google.dagger:dagger-android:${versions.dagger.core}"
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ package eu.darken.capod.common.debug.autoreport
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.preference.PreferenceDataStore
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import eu.darken.androidstarter.common.preferences.Settings
|
|
||||||
import eu.darken.capod.common.preferences.PreferenceStoreMapper
|
|
||||||
import eu.darken.capod.common.preferences.createFlowPreference
|
import eu.darken.capod.common.preferences.createFlowPreference
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
@@ -13,26 +10,12 @@ import javax.inject.Singleton
|
|||||||
@Singleton
|
@Singleton
|
||||||
class DebugSettings @Inject constructor(
|
class DebugSettings @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
) : Settings() {
|
) {
|
||||||
|
|
||||||
override val preferences: SharedPreferences = context.getSharedPreferences("settings_debug", Context.MODE_PRIVATE)
|
private val preferences: SharedPreferences = context.getSharedPreferences("settings_debug", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
val isAutoReportEnabled = preferences.createFlowPreference("debug.bugreport.automatic.enabled", true)
|
val isAutoReportEnabled = preferences.createFlowPreference("debug.bugreport.automatic.enabled", true)
|
||||||
|
|
||||||
val isDebugModeEnabled = preferences.createFlowPreference("debug.mode.enabled", false)
|
val isDebugModeEnabled = preferences.createFlowPreference("debug.mode.enabled", false)
|
||||||
|
|
||||||
override val preferenceDataStore: PreferenceDataStore = object : PreferenceStoreMapper() {
|
|
||||||
override fun getBoolean(key: String, defValue: Boolean): Boolean = when (key) {
|
|
||||||
isAutoReportEnabled.key -> isAutoReportEnabled.value
|
|
||||||
isDebugModeEnabled.key -> isDebugModeEnabled.value
|
|
||||||
else -> super.getBoolean(key, defValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun putBoolean(key: String, value: Boolean) = when (key) {
|
|
||||||
isAutoReportEnabled.key -> isAutoReportEnabled.update { value }
|
|
||||||
isDebugModeEnabled.key -> isDebugModeEnabled.update { value }
|
|
||||||
else -> super.putBoolean(key, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -10,20 +10,21 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
|||||||
class FlowPreference<T> constructor(
|
class FlowPreference<T> constructor(
|
||||||
private val preferences: SharedPreferences,
|
private val preferences: SharedPreferences,
|
||||||
val key: String,
|
val key: String,
|
||||||
private val reader: SharedPreferences.(key: String) -> T,
|
val rawReader: (Any?) -> T,
|
||||||
private val writer: SharedPreferences.Editor.(key: String, value: T) -> Unit
|
val rawWriter: (T) -> Any?
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val flowInternal = MutableStateFlow(internalValue)
|
private val flowInternal = MutableStateFlow(value)
|
||||||
val flow: Flow<T> = flowInternal
|
val flow: Flow<T> = flowInternal
|
||||||
|
|
||||||
private val preferenceChangeListener =
|
private val preferenceChangeListener =
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener { changedPrefs, changedKey ->
|
SharedPreferences.OnSharedPreferenceChangeListener { changedPrefs, changedKey ->
|
||||||
if (changedKey != key) return@OnSharedPreferenceChangeListener
|
if (changedKey != key) return@OnSharedPreferenceChangeListener
|
||||||
|
|
||||||
val newValue = reader(changedPrefs, changedKey)
|
val newValue = rawReader(changedPrefs.all[key])
|
||||||
val currentvalue = flowInternal.value
|
|
||||||
if (currentvalue != newValue && flowInternal.compareAndSet(currentvalue, newValue)) {
|
val currentValue = flowInternal.value
|
||||||
|
if (currentValue != newValue && flowInternal.compareAndSet(currentValue, newValue)) {
|
||||||
log(VERBOSE) { "$changedPrefs:$changedKey changed to $newValue" }
|
log(VERBOSE) { "$changedPrefs:$changedKey changed to $newValue" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,29 +33,16 @@ class FlowPreference<T> constructor(
|
|||||||
preferences.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
|
preferences.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var internalValue: T
|
var value: T
|
||||||
get() = reader(preferences, key)
|
get() = rawReader(valueRaw)
|
||||||
set(newValue) {
|
set(newVal) {
|
||||||
preferences.edit {
|
valueRaw = rawWriter(newVal)
|
||||||
writer(key, newValue)
|
|
||||||
}
|
|
||||||
flowInternal.value = internalValue
|
|
||||||
}
|
}
|
||||||
val value: T
|
|
||||||
get() = internalValue
|
|
||||||
|
|
||||||
fun update(update: (T) -> T) {
|
var valueRaw: Any?
|
||||||
internalValue = update(internalValue)
|
get() = preferences.all[key] ?: rawWriter(rawReader(null))
|
||||||
}
|
set(value) {
|
||||||
|
preferences.edit {
|
||||||
companion object {
|
|
||||||
inline fun <reified T> basicReader(defaultValue: T): SharedPreferences.(key: String) -> T =
|
|
||||||
{ key ->
|
|
||||||
(this.all[key] ?: defaultValue) as T
|
|
||||||
}
|
|
||||||
|
|
||||||
inline fun <reified T> basicWriter(): SharedPreferences.Editor.(key: String, value: T) -> Unit =
|
|
||||||
{ key, value ->
|
|
||||||
when (value) {
|
when (value) {
|
||||||
is Boolean -> putBoolean(key, value)
|
is Boolean -> putBoolean(key, value)
|
||||||
is String -> putString(key, value)
|
is String -> putString(key, value)
|
||||||
@@ -64,27 +52,12 @@ class FlowPreference<T> constructor(
|
|||||||
null -> remove(key)
|
null -> remove(key)
|
||||||
else -> throw NotImplementedError()
|
else -> throw NotImplementedError()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
flowInternal.value = rawReader(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(update: (T) -> T) {
|
||||||
|
value = update(value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified T : Any?> SharedPreferences.createFlowPreference(
|
|
||||||
key: String,
|
|
||||||
defaultValue: T = null as T
|
|
||||||
) = FlowPreference(
|
|
||||||
preferences = this,
|
|
||||||
key = key,
|
|
||||||
reader = FlowPreference.basicReader(defaultValue),
|
|
||||||
writer = FlowPreference.basicWriter()
|
|
||||||
)
|
|
||||||
|
|
||||||
inline fun <reified T : Any?> SharedPreferences.createFlowPreference(
|
|
||||||
key: String,
|
|
||||||
noinline reader: SharedPreferences.(key: String) -> T,
|
|
||||||
noinline writer: SharedPreferences.Editor.(key: String, value: T) -> Unit
|
|
||||||
) = FlowPreference(
|
|
||||||
preferences = this,
|
|
||||||
key = key,
|
|
||||||
reader = reader,
|
|
||||||
writer = writer
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package eu.darken.capod.common.preferences
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
|
||||||
|
|
||||||
|
inline fun <reified T> basicReader(defaultValue: T): (rawValue: Any?) -> T =
|
||||||
|
{ rawValue ->
|
||||||
|
(rawValue ?: defaultValue) as T
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T> basicWriter(): (T) -> Any? =
|
||||||
|
{ value ->
|
||||||
|
when (value) {
|
||||||
|
is Boolean -> value
|
||||||
|
is String -> value
|
||||||
|
is Int -> value
|
||||||
|
is Long -> value
|
||||||
|
is Float -> value
|
||||||
|
null -> null
|
||||||
|
else -> throw NotImplementedError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T : Any?> SharedPreferences.createFlowPreference(
|
||||||
|
key: String,
|
||||||
|
defaultValue: T = null as T
|
||||||
|
) = FlowPreference(
|
||||||
|
preferences = this,
|
||||||
|
key = key,
|
||||||
|
rawReader = basicReader(defaultValue),
|
||||||
|
rawWriter = basicWriter()
|
||||||
|
)
|
||||||
|
|
||||||
|
inline fun <reified T : Any?> SharedPreferences.createFlowPreference(
|
||||||
|
key: String,
|
||||||
|
noinline reader: (rawValue: Any?) -> T,
|
||||||
|
noinline writer: (value: T) -> Any?
|
||||||
|
) = FlowPreference(
|
||||||
|
preferences = this,
|
||||||
|
key = key,
|
||||||
|
rawReader = reader,
|
||||||
|
rawWriter = writer
|
||||||
|
)
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package eu.darken.capod.common.preferences
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
|
|
||||||
|
inline fun <reified T> moshiReader(
|
||||||
|
moshi: Moshi,
|
||||||
|
defaultValue: T,
|
||||||
|
): (Any?) -> T {
|
||||||
|
val adapter = moshi.adapter(T::class.java)
|
||||||
|
return { rawValue ->
|
||||||
|
rawValue as String?
|
||||||
|
rawValue?.let { adapter.fromJson(it) } ?: defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T> moshiWriter(
|
||||||
|
moshi: Moshi,
|
||||||
|
): (T) -> Any? {
|
||||||
|
val adapter = moshi.adapter(T::class.java)
|
||||||
|
return { newValue: T ->
|
||||||
|
newValue?.let { adapter.toJson(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun <reified T : Any?> SharedPreferences.createFlowPreference(
|
||||||
|
key: String,
|
||||||
|
defaultValue: T = null as T,
|
||||||
|
moshi: Moshi,
|
||||||
|
) = FlowPreference(
|
||||||
|
preferences = this,
|
||||||
|
key = key,
|
||||||
|
rawReader = moshiReader(moshi, defaultValue),
|
||||||
|
rawWriter = moshiWriter(moshi)
|
||||||
|
)
|
||||||
@@ -2,51 +2,74 @@ package eu.darken.capod.common.preferences
|
|||||||
|
|
||||||
import androidx.preference.PreferenceDataStore
|
import androidx.preference.PreferenceDataStore
|
||||||
|
|
||||||
abstract class PreferenceStoreMapper : PreferenceDataStore() {
|
open class PreferenceStoreMapper(
|
||||||
|
private vararg val flowPreferences: FlowPreference<*>
|
||||||
|
) : PreferenceDataStore() {
|
||||||
|
|
||||||
override fun getBoolean(key: String, defValue: Boolean): Boolean {
|
override fun getBoolean(key: String, defValue: Boolean): Boolean {
|
||||||
throw NotImplementedError("getBoolean(key=$key, defValue=$defValue)")
|
return flowPreferences.singleOrNull { it.key == key }?.let { flowPref ->
|
||||||
|
flowPref.valueRaw as Boolean
|
||||||
|
} ?: throw NotImplementedError("getBoolean(key=$key, defValue=$defValue)")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun putBoolean(key: String, value: Boolean) {
|
override fun putBoolean(key: String, value: Boolean) {
|
||||||
throw NotImplementedError("putBoolean(key=$key, defValue=$value)")
|
flowPreferences.singleOrNull { it.key == key }?.let { flowPref ->
|
||||||
|
flowPref.valueRaw = value
|
||||||
|
} ?: throw NotImplementedError("putBoolean(key=$key, defValue=$value)")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getString(key: String, defValue: String?): String? {
|
override fun getString(key: String, defValue: String?): String? {
|
||||||
throw NotImplementedError("getString(key=$key, defValue=$defValue)")
|
return flowPreferences.singleOrNull { it.key == key }?.let { flowPref ->
|
||||||
|
flowPref.valueRaw as String?
|
||||||
|
} ?: throw NotImplementedError("getString(key=$key, defValue=$defValue)")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun putString(key: String, value: String?) {
|
override fun putString(key: String, value: String?) {
|
||||||
throw NotImplementedError("putString(key=$key, defValue=$value)")
|
flowPreferences.singleOrNull { it.key == key }?.let { flowPref ->
|
||||||
}
|
flowPref.valueRaw = value
|
||||||
|
} ?: throw NotImplementedError("putString(key=$key, defValue=$value)")
|
||||||
override fun putInt(key: String?, value: Int) {
|
|
||||||
throw NotImplementedError("putInt(key=$key, defValue=$value)")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getInt(key: String?, defValue: Int): Int {
|
override fun getInt(key: String?, defValue: Int): Int {
|
||||||
throw NotImplementedError("getInt(key=$key, defValue=$defValue)")
|
return flowPreferences.singleOrNull { it.key == key }?.let { flowPref ->
|
||||||
|
flowPref.valueRaw as Int
|
||||||
|
} ?: throw NotImplementedError("getInt(key=$key, defValue=$defValue)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putInt(key: String?, value: Int) {
|
||||||
|
flowPreferences.singleOrNull { it.key == key }?.let { flowPref ->
|
||||||
|
flowPref.valueRaw = value
|
||||||
|
} ?: throw NotImplementedError("putInt(key=$key, defValue=$value)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getLong(key: String?, defValue: Long): Long {
|
||||||
|
return flowPreferences.singleOrNull { it.key == key }?.let { flowPref ->
|
||||||
|
flowPref.valueRaw as Long
|
||||||
|
} ?: throw NotImplementedError("getLong(key=$key, defValue=$defValue)")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun putLong(key: String?, value: Long) {
|
override fun putLong(key: String?, value: Long) {
|
||||||
throw NotImplementedError("putLong(key=$key, defValue=$value)")
|
flowPreferences.singleOrNull { it.key == key }?.let { flowPref ->
|
||||||
|
flowPref.valueRaw = value
|
||||||
|
} ?: throw NotImplementedError("putLong(key=$key, defValue=$value)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFloat(key: String?, defValue: Float): Float {
|
||||||
|
return flowPreferences.singleOrNull { it.key == key }?.let { flowPref ->
|
||||||
|
flowPref.valueRaw as Float
|
||||||
|
} ?: throw NotImplementedError("getFloat(key=$key, defValue=$defValue)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun putFloat(key: String?, value: Float) {
|
||||||
|
flowPreferences.singleOrNull { it.key == key }?.let { flowPref ->
|
||||||
|
flowPref.valueRaw = value
|
||||||
|
} ?: throw NotImplementedError("putFloat(key=$key, defValue=$value)")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun putStringSet(key: String?, values: MutableSet<String>?) {
|
override fun putStringSet(key: String?, values: MutableSet<String>?) {
|
||||||
throw NotImplementedError("putStringSet(key=$key, defValue=$values)")
|
throw NotImplementedError("putStringSet(key=$key, defValue=$values)")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLong(key: String?, defValue: Long): Long {
|
|
||||||
throw NotImplementedError("getLong(key=$key, defValue=$defValue)")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getFloat(key: String?, defValue: Float): Float {
|
|
||||||
throw NotImplementedError("getFloat(key=$key, defValue=$defValue)")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun putFloat(key: String?, value: Float) {
|
|
||||||
throw NotImplementedError("putFloat(key=$key, defValue=$value)")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getStringSet(key: String?, defValues: MutableSet<String>?): MutableSet<String> {
|
override fun getStringSet(key: String?, defValues: MutableSet<String>?): MutableSet<String> {
|
||||||
throw NotImplementedError("getStringSet(key=$key, defValue=$defValues)")
|
throw NotImplementedError("getStringSet(key=$key, defValue=$defValues)")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package eu.darken.capod.common.serialization
|
||||||
|
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
@Module
|
||||||
|
class SerializationModule {
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun moshi(): Moshi = Moshi.Builder().build()
|
||||||
|
|
||||||
|
}
|
||||||
@@ -3,11 +3,13 @@ package eu.darken.capod.main.core
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import androidx.preference.PreferenceDataStore
|
import androidx.preference.PreferenceDataStore
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import eu.darken.androidstarter.common.preferences.Settings
|
import eu.darken.androidstarter.common.preferences.Settings
|
||||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.common.preferences.PreferenceStoreMapper
|
import eu.darken.capod.common.preferences.PreferenceStoreMapper
|
||||||
|
import eu.darken.capod.common.preferences.createFlowPreference
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -15,20 +17,29 @@ import javax.inject.Singleton
|
|||||||
class GeneralSettings @Inject constructor(
|
class GeneralSettings @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val debugSettings: DebugSettings,
|
private val debugSettings: DebugSettings,
|
||||||
|
private val moshi: Moshi,
|
||||||
) : Settings() {
|
) : Settings() {
|
||||||
|
|
||||||
override val preferences: SharedPreferences = context.getSharedPreferences("settings_general", Context.MODE_PRIVATE)
|
override val preferences: SharedPreferences = context.getSharedPreferences("settings_general", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
override val preferenceDataStore: PreferenceDataStore = object : PreferenceStoreMapper() {
|
val monitorMode = preferences.createFlowPreference(
|
||||||
override fun getBoolean(key: String, defValue: Boolean): Boolean = when (key) {
|
"core.monitor.mode",
|
||||||
else -> debugSettings.preferenceDataStore.getBoolean(key, defValue)
|
MonitorMode.AUTOMATIC,
|
||||||
}
|
moshi
|
||||||
|
)
|
||||||
|
|
||||||
override fun putBoolean(key: String, value: Boolean) = when (key) {
|
val scannerMode = preferences.createFlowPreference(
|
||||||
else -> debugSettings.preferenceDataStore.putBoolean(key, value)
|
"core.scanner.mode",
|
||||||
}
|
ScannerMode.BALANCED,
|
||||||
}
|
moshi
|
||||||
|
)
|
||||||
|
|
||||||
|
override val preferenceDataStore: PreferenceDataStore = PreferenceStoreMapper(
|
||||||
|
monitorMode,
|
||||||
|
scannerMode,
|
||||||
|
debugSettings.isDebugModeEnabled,
|
||||||
|
debugSettings.isAutoReportEnabled
|
||||||
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
internal val TAG = logTag("Core", "Settings")
|
internal val TAG = logTag("Core", "Settings")
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package eu.darken.capod.main.core
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import eu.darken.capod.R
|
||||||
|
|
||||||
|
enum class MonitorMode(
|
||||||
|
val identifier: String,
|
||||||
|
@StringRes val labelRes: Int
|
||||||
|
) {
|
||||||
|
@Json(name = "monitor.mode.manual") MANUAL(
|
||||||
|
"monitor.mode.manual",
|
||||||
|
R.string.settings_monitor_mode_manual_label
|
||||||
|
),
|
||||||
|
@Json(name = "monitor.mode.automatic") AUTOMATIC(
|
||||||
|
"monitor.mode.automatic",
|
||||||
|
R.string.settings_monitor_mode_automatic_label
|
||||||
|
),
|
||||||
|
@Json(name = "monitor.mode.always") ALWAYS(
|
||||||
|
"monitor.mode.always",
|
||||||
|
R.string.settings_monitor_mode_always_label
|
||||||
|
),
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package eu.darken.capod.main.core
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import com.squareup.moshi.Json
|
||||||
|
import eu.darken.capod.R
|
||||||
|
|
||||||
|
enum class ScannerMode(
|
||||||
|
val identifier: String,
|
||||||
|
@StringRes val labelRes: Int
|
||||||
|
) {
|
||||||
|
@Json(name = "scanner.mode.lowpower") LOW_POWER(
|
||||||
|
"scanner.mode.lowpower",
|
||||||
|
R.string.settings_scanner_mode_lowpower_label
|
||||||
|
),
|
||||||
|
@Json(name = "scanner.mode.balanced") BALANCED(
|
||||||
|
"scanner.mode.balanced",
|
||||||
|
R.string.settings_scanner_mode_balanced_label
|
||||||
|
),
|
||||||
|
@Json(name = "scanner.mode.lowlatency") LOW_LATENCY(
|
||||||
|
"scanner.mode.lowlatency",
|
||||||
|
R.string.settings_scanner_mode_lowlatency_label
|
||||||
|
),
|
||||||
|
}
|
||||||
@@ -2,10 +2,13 @@ package eu.darken.capod.main.ui.settings.general
|
|||||||
|
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.preference.ListPreference
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.uix.PreferenceFragment2
|
import eu.darken.capod.common.uix.PreferenceFragment2
|
||||||
import eu.darken.capod.main.core.GeneralSettings
|
import eu.darken.capod.main.core.GeneralSettings
|
||||||
|
import eu.darken.capod.main.core.MonitorMode
|
||||||
|
import eu.darken.capod.main.core.ScannerMode
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@@ -21,5 +24,18 @@ class GeneralSettingsFragment : PreferenceFragment2() {
|
|||||||
|
|
||||||
override val preferenceFile: Int = R.xml.preferences_general
|
override val preferenceFile: Int = R.xml.preferences_general
|
||||||
|
|
||||||
|
private val monitorModePref by lazy { findPreference<ListPreference>(generalSettings.monitorMode.key)!! }
|
||||||
|
private val scanModePref by lazy { findPreference<ListPreference>(generalSettings.scannerMode.key)!! }
|
||||||
|
|
||||||
|
override fun onPreferencesCreated() {
|
||||||
|
monitorModePref.apply {
|
||||||
|
entries = MonitorMode.values().map { getString(it.labelRes) }.toTypedArray()
|
||||||
|
entryValues = MonitorMode.values().map { settings.monitorMode.rawWriter(it) as String }.toTypedArray()
|
||||||
|
}
|
||||||
|
scanModePref.apply {
|
||||||
|
entries = ScannerMode.values().map { getString(it.labelRes) }.toTypedArray()
|
||||||
|
entryValues = ScannerMode.values().map { settings.scannerMode.rawWriter(it) as String }.toTypedArray()
|
||||||
|
}
|
||||||
|
super.onPreferencesCreated()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package eu.darken.capod.monitor.core
|
|
||||||
|
|
||||||
import javax.inject.Inject
|
|
||||||
import javax.inject.Singleton
|
|
||||||
|
|
||||||
@Singleton
|
|
||||||
class MonitorSettings @Inject constructor() {
|
|
||||||
|
|
||||||
val mode: Mode = Mode.ALWAYS
|
|
||||||
|
|
||||||
enum class Mode(val raw: String) {
|
|
||||||
MANUAL("monitor.mode.manual"),
|
|
||||||
AUTOMATIC("monitor.mode.automatic"),
|
|
||||||
ALWAYS("monitor.mode.always")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
package eu.darken.capod.monitor.core
|
package eu.darken.capod.monitor.core
|
||||||
|
|
||||||
|
import android.bluetooth.le.ScanSettings
|
||||||
import eu.darken.capod.common.bluetooth.BleScanner
|
import eu.darken.capod.common.bluetooth.BleScanner
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||||
import eu.darken.capod.common.debug.logging.log
|
import eu.darken.capod.common.debug.logging.log
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
|
import eu.darken.capod.main.core.GeneralSettings
|
||||||
|
import eu.darken.capod.main.core.ScannerMode
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.PodFactory
|
import eu.darken.capod.pods.core.PodFactory
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.onStart
|
import kotlinx.coroutines.flow.onStart
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -16,9 +20,19 @@ import javax.inject.Singleton
|
|||||||
class PodMonitor @Inject constructor(
|
class PodMonitor @Inject constructor(
|
||||||
private val bleScanner: BleScanner,
|
private val bleScanner: BleScanner,
|
||||||
private val podFactory: PodFactory,
|
private val podFactory: PodFactory,
|
||||||
|
private val generalSettings: GeneralSettings,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val pods: Flow<List<PodDevice>> = bleScanner.scan()
|
val pods: Flow<List<PodDevice>> = generalSettings.scannerMode.flow
|
||||||
|
.flatMapLatest {
|
||||||
|
bleScanner.scan(
|
||||||
|
mode = when (it) {
|
||||||
|
ScannerMode.LOW_POWER -> ScanSettings.SCAN_MODE_LOW_POWER
|
||||||
|
ScannerMode.BALANCED -> ScanSettings.SCAN_MODE_BALANCED
|
||||||
|
ScannerMode.LOW_LATENCY -> ScanSettings.SCAN_MODE_LOW_LATENCY
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
.map { result ->
|
.map { result ->
|
||||||
// For each address we only want the newest result, upstream may batch data
|
// For each address we only want the newest result, upstream may batch data
|
||||||
result.groupBy { it.device.address }
|
result.groupBy { it.device.address }
|
||||||
@@ -31,9 +45,9 @@ class PodMonitor @Inject constructor(
|
|||||||
}
|
}
|
||||||
.onStart { emptyList<PodDevice>() }
|
.onStart { emptyList<PodDevice>() }
|
||||||
.map { scanResults ->
|
.map { scanResults ->
|
||||||
val pods = scanResults
|
val pods = scanResults
|
||||||
.sortedByDescending { it.rssi }
|
.sortedByDescending { it.rssi }
|
||||||
.mapNotNull { podFactory.createPod(it) }
|
.mapNotNull { podFactory.createPod(it) }
|
||||||
|
|
||||||
// if (BuildConfigWrap.DEBUG && scanResults.isNotEmpty()) {
|
// if (BuildConfigWrap.DEBUG && scanResults.isNotEmpty()) {
|
||||||
// val fake1 = AirPodsMax(
|
// val fake1 = AirPodsMax(
|
||||||
|
|||||||
@@ -16,9 +16,10 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||||
import eu.darken.capod.common.permissions.Permission
|
import eu.darken.capod.common.permissions.Permission
|
||||||
import eu.darken.capod.common.permissions.isGrantedOrNotRequired
|
import eu.darken.capod.common.permissions.isGrantedOrNotRequired
|
||||||
|
import eu.darken.capod.main.core.GeneralSettings
|
||||||
|
import eu.darken.capod.main.core.MonitorMode
|
||||||
import eu.darken.capod.monitor.core.MonitorComponent
|
import eu.darken.capod.monitor.core.MonitorComponent
|
||||||
import eu.darken.capod.monitor.core.MonitorCoroutineScope
|
import eu.darken.capod.monitor.core.MonitorCoroutineScope
|
||||||
import eu.darken.capod.monitor.core.MonitorSettings
|
|
||||||
import eu.darken.capod.monitor.ui.MonitorNotifications
|
import eu.darken.capod.monitor.ui.MonitorNotifications
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
@@ -34,7 +35,7 @@ class MonitorWorker @AssistedInject constructor(
|
|||||||
monitorComponentBuilder: MonitorComponent.Builder,
|
monitorComponentBuilder: MonitorComponent.Builder,
|
||||||
private val monitorNotifications: MonitorNotifications,
|
private val monitorNotifications: MonitorNotifications,
|
||||||
private val notificationManager: NotificationManager,
|
private val notificationManager: NotificationManager,
|
||||||
private val monitorSettings: MonitorSettings,
|
private val generalSettings: GeneralSettings,
|
||||||
) : CoroutineWorker(context, params) {
|
) : CoroutineWorker(context, params) {
|
||||||
|
|
||||||
private val workerScope = MonitorCoroutineScope()
|
private val workerScope = MonitorCoroutineScope()
|
||||||
@@ -81,11 +82,15 @@ class MonitorWorker @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
.setupCommonEventHandlers(TAG) { "ConnectedDevices" }
|
.setupCommonEventHandlers(TAG) { "ConnectedDevices" }
|
||||||
.flatMapLatest { arePodsConnected ->
|
.flatMapLatest { arePodsConnected ->
|
||||||
log(TAG) { "Monitor mode: ${monitorSettings.mode}" }
|
val mode = generalSettings.monitorMode.value
|
||||||
when (monitorSettings.mode) {
|
log(TAG) { "Monitor mode: $mode" }
|
||||||
MonitorSettings.Mode.MANUAL -> emptyFlow()
|
when (mode) {
|
||||||
MonitorSettings.Mode.ALWAYS -> emptyFlow()
|
MonitorMode.MANUAL -> flow<Unit> {
|
||||||
MonitorSettings.Mode.AUTOMATIC -> flow<Unit> {
|
// Cancel worker, ui scans manually
|
||||||
|
workerScope.coroutineContext.cancelChildren()
|
||||||
|
}
|
||||||
|
MonitorMode.ALWAYS -> emptyFlow()
|
||||||
|
MonitorMode.AUTOMATIC -> flow<Unit> {
|
||||||
if (arePodsConnected) {
|
if (arePodsConnected) {
|
||||||
log(TAG) { "Pods are connected, aborting any timeout." }
|
log(TAG) { "Pods are connected, aborting any timeout." }
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M21.99,12.34C22,12.23 22,12.11 22,12c0,-5.52 -4.48,-10 -10,-10S2,6.48 2,12c0,5.17 3.93,9.43 8.96,9.95c-0.93,-0.73 -1.72,-1.64 -2.32,-2.68C5.9,18 4,15.22 4,12c0,-1.85 0.63,-3.55 1.69,-4.9l5.66,5.66c0.56,-0.4 1.17,-0.73 1.82,-1L7.1,5.69C8.45,4.63 10.15,4 12,4c4.24,0 7.7,3.29 7.98,7.45C20.69,11.67 21.37,11.97 21.99,12.34zM17,13c-3.18,0 -5.9,1.87 -7,4.5c1.1,2.63 3.82,4.5 7,4.5s5.9,-1.87 7,-4.5C22.9,14.87 20.18,13 17,13zM17,20c-1.38,0 -2.5,-1.12 -2.5,-2.5c0,-1.38 1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5C19.5,18.88 18.38,20 17,20zM18.5,17.5c0,0.83 -0.67,1.5 -1.5,1.5s-1.5,-0.67 -1.5,-1.5c0,-0.83 0.67,-1.5 1.5,-1.5S18.5,16.67 18.5,17.5z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M20,3L4,3c-1.1,0 -2,0.9 -2,2v11c0,1.1 0.9,2 2,2h3l-1,1v2h12v-2l-1,-1h3c1.1,0 2,-0.9 2,-2L22,5c0,-1.1 -0.9,-2 -2,-2zM20,16L4,16L4,5h16v11z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M11,24h2v-2h-2v2zM7,24h2v-2L7,22v2zM15,24h2v-2h-2v2zM17.71,5.71L12,0h-1v7.59L6.41,3 5,4.41 10.59,10 5,15.59 6.41,17 11,12.41L11,20h1l5.71,-5.71 -4.3,-4.29 4.3,-4.29zM13,3.83l1.88,1.88L13,7.59L13,3.83zM14.88,14.29L13,16.17v-3.76l1.88,1.88z" />
|
||||||
|
</vector>
|
||||||
@@ -85,4 +85,14 @@
|
|||||||
<string name="pods_single_headphones_label">Headphones</string>
|
<string name="pods_single_headphones_label">Headphones</string>
|
||||||
<string name="settings_debug_mode_label">Debug mode</string>
|
<string name="settings_debug_mode_label">Debug mode</string>
|
||||||
<string name="settings_debug_mode_description">Show additional information to troubleshoot issues.</string>
|
<string name="settings_debug_mode_description">Show additional information to troubleshoot issues.</string>
|
||||||
|
<string name="settings_monitor_mode_label">Monitor mode</string>
|
||||||
|
<string name="settings_monitor_mode_description">Under which circumstances this app monitors Bluetooth data.</string>
|
||||||
|
<string name="settings_scanner_mode_label">Scanner mode</string>
|
||||||
|
<string name="settings_scanner_mode_description">Should the Bluetooth Low Energy scanner prioritize performance or conserve energy?</string>
|
||||||
|
<string name="settings_monitor_mode_manual_label">When app is open</string>
|
||||||
|
<string name="settings_monitor_mode_automatic_label">When device is connected</string>
|
||||||
|
<string name="settings_monitor_mode_always_label">Always</string>
|
||||||
|
<string name="settings_scanner_mode_lowpower_label">Low power</string>
|
||||||
|
<string name="settings_scanner_mode_balanced_label">Balanced</string>
|
||||||
|
<string name="settings_scanner_mode_lowlatency_label">Low latency</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -1,6 +1,18 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<ListPreference
|
||||||
|
android:icon="@drawable/ic_baseline_disabled_visible_24"
|
||||||
|
android:key="core.monitor.mode"
|
||||||
|
android:summary="@string/settings_monitor_mode_description"
|
||||||
|
android:title="@string/settings_monitor_mode_label" />
|
||||||
|
|
||||||
|
<ListPreference
|
||||||
|
android:icon="@drawable/ic_baseline_settings_bluetooth_24"
|
||||||
|
android:key="core.scanner.mode"
|
||||||
|
android:summary="@string/settings_scanner_mode_description"
|
||||||
|
android:title="@string/settings_scanner_mode_label" />
|
||||||
|
|
||||||
<PreferenceCategory android:title="@string/settings_category_other_label">
|
<PreferenceCategory android:title="@string/settings_category_other_label">
|
||||||
|
|
||||||
<CheckBoxPreference
|
<CheckBoxPreference
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package eu.darken.capod.common.preferences
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonClass
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
|
import eu.darken.capod.main.core.MonitorMode
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.test.runBlockingTest
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import testhelper.BaseTest
|
||||||
|
import testhelper.json.toComparableJson
|
||||||
|
import testhelpers.preferences.MockSharedPreferences
|
||||||
|
|
||||||
|
class FlowPreferenceMoshiTest : BaseTest() {
|
||||||
|
|
||||||
|
private val mockPreferences = MockSharedPreferences()
|
||||||
|
|
||||||
|
@JsonClass(generateAdapter = true)
|
||||||
|
data class TestGson(
|
||||||
|
val string: String = "",
|
||||||
|
val boolean: Boolean = true,
|
||||||
|
val float: Float = 1.0f,
|
||||||
|
val int: Int = 1,
|
||||||
|
val long: Long = 1L
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `reading and writing using manual reader and writer`() = runBlockingTest {
|
||||||
|
val testData1 = TestGson(string = "teststring")
|
||||||
|
val testData2 = TestGson(string = "update")
|
||||||
|
val moshi = Moshi.Builder().build()
|
||||||
|
FlowPreference<TestGson?>(
|
||||||
|
preferences = mockPreferences,
|
||||||
|
key = "testKey",
|
||||||
|
rawReader = moshiReader(moshi, testData1),
|
||||||
|
rawWriter = moshiWriter(moshi)
|
||||||
|
).apply {
|
||||||
|
value shouldBe testData1
|
||||||
|
flow.first() shouldBe testData1
|
||||||
|
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||||
|
|
||||||
|
update {
|
||||||
|
it shouldBe testData1
|
||||||
|
it!!.copy(string = "update")
|
||||||
|
}
|
||||||
|
|
||||||
|
value shouldBe testData2
|
||||||
|
flow.first() shouldBe testData2
|
||||||
|
(mockPreferences.dataMapPeek.values.first() as String).toComparableJson() shouldBe """
|
||||||
|
{
|
||||||
|
"string":"update",
|
||||||
|
"boolean":true,
|
||||||
|
"float":1.0,
|
||||||
|
"int":1,
|
||||||
|
"long":1
|
||||||
|
}
|
||||||
|
""".toComparableJson()
|
||||||
|
|
||||||
|
update {
|
||||||
|
it shouldBe testData2
|
||||||
|
null
|
||||||
|
}
|
||||||
|
value shouldBe testData1
|
||||||
|
flow.first() shouldBe testData1
|
||||||
|
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `reading and writing using autocreated reader and writer`() = runBlockingTest {
|
||||||
|
val testData1 = TestGson(string = "teststring")
|
||||||
|
val testData2 = TestGson(string = "update")
|
||||||
|
val moshi = Moshi.Builder().build()
|
||||||
|
|
||||||
|
mockPreferences.createFlowPreference<TestGson?>(
|
||||||
|
key = "testKey",
|
||||||
|
defaultValue = testData1,
|
||||||
|
moshi = moshi
|
||||||
|
).apply {
|
||||||
|
value shouldBe testData1
|
||||||
|
flow.first() shouldBe testData1
|
||||||
|
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||||
|
|
||||||
|
update {
|
||||||
|
it shouldBe testData1
|
||||||
|
it!!.copy(string = "update")
|
||||||
|
}
|
||||||
|
|
||||||
|
value shouldBe testData2
|
||||||
|
flow.first() shouldBe testData2
|
||||||
|
(mockPreferences.dataMapPeek.values.first() as String).toComparableJson() shouldBe """
|
||||||
|
{
|
||||||
|
"string":"update",
|
||||||
|
"boolean":true,
|
||||||
|
"float":1.0,
|
||||||
|
"int":1,
|
||||||
|
"long":1
|
||||||
|
}
|
||||||
|
""".toComparableJson()
|
||||||
|
|
||||||
|
update {
|
||||||
|
it shouldBe testData2
|
||||||
|
null
|
||||||
|
}
|
||||||
|
value shouldBe testData1
|
||||||
|
flow.first() shouldBe testData1
|
||||||
|
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `enum serialization`() = runBlockingTest {
|
||||||
|
val moshi = Moshi.Builder().build()
|
||||||
|
val monitorMode = mockPreferences.createFlowPreference(
|
||||||
|
"core.monitor.mode",
|
||||||
|
MonitorMode.AUTOMATIC,
|
||||||
|
moshi
|
||||||
|
)
|
||||||
|
|
||||||
|
monitorMode.value shouldBe MonitorMode.AUTOMATIC
|
||||||
|
monitorMode.update { MonitorMode.MANUAL }
|
||||||
|
monitorMode.value shouldBe MonitorMode.MANUAL
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
package eu.darken.capod.common.preferences
|
||||||
|
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.test.runBlockingTest
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import testhelper.BaseTest
|
||||||
|
import testhelpers.preferences.MockSharedPreferences
|
||||||
|
|
||||||
|
class FlowPreferenceTest : BaseTest() {
|
||||||
|
|
||||||
|
private val mockPreferences = MockSharedPreferences()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `reading and writing strings`() = runBlockingTest {
|
||||||
|
mockPreferences.createFlowPreference<String?>(
|
||||||
|
key = "testKey",
|
||||||
|
defaultValue = "default"
|
||||||
|
).apply {
|
||||||
|
value shouldBe "default"
|
||||||
|
flow.first() shouldBe "default"
|
||||||
|
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||||
|
|
||||||
|
update {
|
||||||
|
it shouldBe "default"
|
||||||
|
"newvalue"
|
||||||
|
}
|
||||||
|
|
||||||
|
value shouldBe "newvalue"
|
||||||
|
flow.first() shouldBe "newvalue"
|
||||||
|
mockPreferences.dataMapPeek.values.first() shouldBe "newvalue"
|
||||||
|
|
||||||
|
update {
|
||||||
|
it shouldBe "newvalue"
|
||||||
|
null
|
||||||
|
}
|
||||||
|
value shouldBe "default"
|
||||||
|
flow.first() shouldBe "default"
|
||||||
|
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `reading and writing boolean`() = runBlockingTest {
|
||||||
|
mockPreferences.createFlowPreference<Boolean?>(
|
||||||
|
key = "testKey",
|
||||||
|
defaultValue = true
|
||||||
|
).apply {
|
||||||
|
value shouldBe true
|
||||||
|
flow.first() shouldBe true
|
||||||
|
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||||
|
|
||||||
|
update {
|
||||||
|
it shouldBe true
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
value shouldBe false
|
||||||
|
flow.first() shouldBe false
|
||||||
|
mockPreferences.dataMapPeek.values.first() shouldBe false
|
||||||
|
|
||||||
|
update {
|
||||||
|
it shouldBe false
|
||||||
|
null
|
||||||
|
}
|
||||||
|
value shouldBe true
|
||||||
|
flow.first() shouldBe true
|
||||||
|
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `reading and writing long`() = runBlockingTest {
|
||||||
|
mockPreferences.createFlowPreference<Long?>(
|
||||||
|
key = "testKey",
|
||||||
|
defaultValue = 9000L
|
||||||
|
).apply {
|
||||||
|
value shouldBe 9000L
|
||||||
|
flow.first() shouldBe 9000L
|
||||||
|
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||||
|
|
||||||
|
update {
|
||||||
|
it shouldBe 9000L
|
||||||
|
9001L
|
||||||
|
}
|
||||||
|
|
||||||
|
value shouldBe 9001L
|
||||||
|
flow.first() shouldBe 9001L
|
||||||
|
mockPreferences.dataMapPeek.values.first() shouldBe 9001L
|
||||||
|
|
||||||
|
update {
|
||||||
|
it shouldBe 9001L
|
||||||
|
null
|
||||||
|
}
|
||||||
|
value shouldBe 9000L
|
||||||
|
flow.first() shouldBe 9000L
|
||||||
|
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `reading and writing integer`() = runBlockingTest {
|
||||||
|
mockPreferences.createFlowPreference<Long?>(
|
||||||
|
key = "testKey",
|
||||||
|
defaultValue = 123
|
||||||
|
).apply {
|
||||||
|
value shouldBe 123
|
||||||
|
flow.first() shouldBe 123
|
||||||
|
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||||
|
|
||||||
|
update {
|
||||||
|
it shouldBe 123
|
||||||
|
44
|
||||||
|
}
|
||||||
|
|
||||||
|
value shouldBe 44
|
||||||
|
flow.first() shouldBe 44
|
||||||
|
mockPreferences.dataMapPeek.values.first() shouldBe 44
|
||||||
|
|
||||||
|
update {
|
||||||
|
it shouldBe 44
|
||||||
|
null
|
||||||
|
}
|
||||||
|
value shouldBe 123
|
||||||
|
flow.first() shouldBe 123
|
||||||
|
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `reading and writing float`() = runBlockingTest {
|
||||||
|
mockPreferences.createFlowPreference<Float?>(
|
||||||
|
key = "testKey",
|
||||||
|
defaultValue = 3.6f
|
||||||
|
).apply {
|
||||||
|
value shouldBe 3.6f
|
||||||
|
flow.first() shouldBe 3.6f
|
||||||
|
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||||
|
|
||||||
|
update {
|
||||||
|
it shouldBe 3.6f
|
||||||
|
15000f
|
||||||
|
}
|
||||||
|
|
||||||
|
value shouldBe 15000f
|
||||||
|
flow.first() shouldBe 15000f
|
||||||
|
mockPreferences.dataMapPeek.values.first() shouldBe 15000f
|
||||||
|
|
||||||
|
update {
|
||||||
|
it shouldBe 15000f
|
||||||
|
null
|
||||||
|
}
|
||||||
|
value shouldBe 3.6f
|
||||||
|
flow.first() shouldBe 3.6f
|
||||||
|
mockPreferences.dataMapPeek.values.isEmpty() shouldBe true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -23,4 +23,13 @@ class BeatsFlexText : BaseAirPodsTest() {
|
|||||||
batteryHeadsetPercent shouldBe 0.4f
|
batteryHeadsetPercent shouldBe 0.4f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `random neighbour`() = runBlockingTest {
|
||||||
|
create<BeatsFlex>("07 19 01 10 20 0A F6 8F 02 4F 00 95 68 94 9E 99 D6 90 F4 5E 68 3C 58 21 68 9F 0D") {
|
||||||
|
|
||||||
|
batteryHeadsetPercent shouldBe 0.6f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package testhelper.json
|
||||||
|
|
||||||
|
import com.squareup.moshi.JsonReader
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
|
import okio.Buffer
|
||||||
|
import okio.ByteString.Companion.encode
|
||||||
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
|
||||||
|
fun String.toComparableJson(): String {
|
||||||
|
val value = Buffer().use {
|
||||||
|
it.writeUtf8(this)
|
||||||
|
val reader = JsonReader.of(it)
|
||||||
|
reader.readJsonValue()
|
||||||
|
}
|
||||||
|
|
||||||
|
val adapter = Moshi.Builder().build().adapter(Any::class.java).indent(" ")
|
||||||
|
|
||||||
|
return adapter.toJson(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.writeToFile(file: File) = encode().let { text ->
|
||||||
|
require(!file.exists())
|
||||||
|
file.parentFile?.mkdirs()
|
||||||
|
file.createNewFile()
|
||||||
|
file.sink().buffer().use { it.write(text) }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user