Refactor/add settings

This commit is contained in:
darken
2022-01-04 18:51:05 +01:00
parent 033162bf2b
commit bfe6dc65fd
23 changed files with 652 additions and 125 deletions
+3
View File
@@ -154,6 +154,9 @@ dependencies {
implementation ('com.bugsnag:bugsnag-android:5.9.2')
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
implementation "com.google.dagger:dagger:${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.SharedPreferences
import androidx.preference.PreferenceDataStore
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 javax.inject.Inject
import javax.inject.Singleton
@@ -13,26 +10,12 @@ import javax.inject.Singleton
@Singleton
class DebugSettings @Inject constructor(
@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 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(
private val preferences: SharedPreferences,
val key: String,
private val reader: SharedPreferences.(key: String) -> T,
private val writer: SharedPreferences.Editor.(key: String, value: T) -> Unit
val rawReader: (Any?) -> T,
val rawWriter: (T) -> Any?
) {
private val flowInternal = MutableStateFlow(internalValue)
private val flowInternal = MutableStateFlow(value)
val flow: Flow<T> = flowInternal
private val preferenceChangeListener =
SharedPreferences.OnSharedPreferenceChangeListener { changedPrefs, changedKey ->
if (changedKey != key) return@OnSharedPreferenceChangeListener
val newValue = reader(changedPrefs, changedKey)
val currentvalue = flowInternal.value
if (currentvalue != newValue && flowInternal.compareAndSet(currentvalue, newValue)) {
val newValue = rawReader(changedPrefs.all[key])
val currentValue = flowInternal.value
if (currentValue != newValue && flowInternal.compareAndSet(currentValue, newValue)) {
log(VERBOSE) { "$changedPrefs:$changedKey changed to $newValue" }
}
}
@@ -32,29 +33,16 @@ class FlowPreference<T> constructor(
preferences.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
}
private var internalValue: T
get() = reader(preferences, key)
set(newValue) {
preferences.edit {
writer(key, newValue)
}
flowInternal.value = internalValue
var value: T
get() = rawReader(valueRaw)
set(newVal) {
valueRaw = rawWriter(newVal)
}
val value: T
get() = internalValue
fun update(update: (T) -> T) {
internalValue = update(internalValue)
}
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 ->
var valueRaw: Any?
get() = preferences.all[key] ?: rawWriter(rawReader(null))
set(value) {
preferences.edit {
when (value) {
is Boolean -> putBoolean(key, value)
is String -> putString(key, value)
@@ -64,27 +52,12 @@ class FlowPreference<T> constructor(
null -> remove(key)
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
abstract class PreferenceStoreMapper : PreferenceDataStore() {
open class PreferenceStoreMapper(
private vararg val flowPreferences: FlowPreference<*>
) : PreferenceDataStore() {
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) {
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? {
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?) {
throw NotImplementedError("putString(key=$key, defValue=$value)")
}
override fun putInt(key: String?, value: Int) {
throw NotImplementedError("putInt(key=$key, defValue=$value)")
flowPreferences.singleOrNull { it.key == key }?.let { flowPref ->
flowPref.valueRaw = value
} ?: throw NotImplementedError("putString(key=$key, defValue=$value)")
}
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) {
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>?) {
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> {
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.SharedPreferences
import androidx.preference.PreferenceDataStore
import com.squareup.moshi.Moshi
import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.androidstarter.common.preferences.Settings
import eu.darken.capod.common.debug.autoreport.DebugSettings
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.preferences.PreferenceStoreMapper
import eu.darken.capod.common.preferences.createFlowPreference
import javax.inject.Inject
import javax.inject.Singleton
@@ -15,20 +17,29 @@ import javax.inject.Singleton
class GeneralSettings @Inject constructor(
@ApplicationContext private val context: Context,
private val debugSettings: DebugSettings,
private val moshi: Moshi,
) : Settings() {
override val preferences: SharedPreferences = context.getSharedPreferences("settings_general", Context.MODE_PRIVATE)
override val preferenceDataStore: PreferenceDataStore = object : PreferenceStoreMapper() {
override fun getBoolean(key: String, defValue: Boolean): Boolean = when (key) {
else -> debugSettings.preferenceDataStore.getBoolean(key, defValue)
}
val monitorMode = preferences.createFlowPreference(
"core.monitor.mode",
MonitorMode.AUTOMATIC,
moshi
)
override fun putBoolean(key: String, value: Boolean) = when (key) {
else -> debugSettings.preferenceDataStore.putBoolean(key, value)
}
}
val scannerMode = preferences.createFlowPreference(
"core.scanner.mode",
ScannerMode.BALANCED,
moshi
)
override val preferenceDataStore: PreferenceDataStore = PreferenceStoreMapper(
monitorMode,
scannerMode,
debugSettings.isDebugModeEnabled,
debugSettings.isAutoReportEnabled
)
companion object {
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.fragment.app.viewModels
import androidx.preference.ListPreference
import dagger.hilt.android.AndroidEntryPoint
import eu.darken.capod.R
import eu.darken.capod.common.uix.PreferenceFragment2
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
@Keep
@@ -21,5 +24,18 @@ class GeneralSettingsFragment : PreferenceFragment2() {
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
import android.bluetooth.le.ScanSettings
import eu.darken.capod.common.bluetooth.BleScanner
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.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.PodFactory
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import javax.inject.Inject
@@ -16,9 +20,19 @@ import javax.inject.Singleton
class PodMonitor @Inject constructor(
private val bleScanner: BleScanner,
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 ->
// For each address we only want the newest result, upstream may batch data
result.groupBy { it.device.address }
@@ -31,9 +45,9 @@ class PodMonitor @Inject constructor(
}
.onStart { emptyList<PodDevice>() }
.map { scanResults ->
val pods = scanResults
.sortedByDescending { it.rssi }
.mapNotNull { podFactory.createPod(it) }
val pods = scanResults
.sortedByDescending { it.rssi }
.mapNotNull { podFactory.createPod(it) }
// if (BuildConfigWrap.DEBUG && scanResults.isNotEmpty()) {
// 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.permissions.Permission
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.MonitorCoroutineScope
import eu.darken.capod.monitor.core.MonitorSettings
import eu.darken.capod.monitor.ui.MonitorNotifications
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
import kotlinx.coroutines.cancel
@@ -34,7 +35,7 @@ class MonitorWorker @AssistedInject constructor(
monitorComponentBuilder: MonitorComponent.Builder,
private val monitorNotifications: MonitorNotifications,
private val notificationManager: NotificationManager,
private val monitorSettings: MonitorSettings,
private val generalSettings: GeneralSettings,
) : CoroutineWorker(context, params) {
private val workerScope = MonitorCoroutineScope()
@@ -81,11 +82,15 @@ class MonitorWorker @AssistedInject constructor(
}
.setupCommonEventHandlers(TAG) { "ConnectedDevices" }
.flatMapLatest { arePodsConnected ->
log(TAG) { "Monitor mode: ${monitorSettings.mode}" }
when (monitorSettings.mode) {
MonitorSettings.Mode.MANUAL -> emptyFlow()
MonitorSettings.Mode.ALWAYS -> emptyFlow()
MonitorSettings.Mode.AUTOMATIC -> flow<Unit> {
val mode = generalSettings.monitorMode.value
log(TAG) { "Monitor mode: $mode" }
when (mode) {
MonitorMode.MANUAL -> flow<Unit> {
// Cancel worker, ui scans manually
workerScope.coroutineContext.cancelChildren()
}
MonitorMode.ALWAYS -> emptyFlow()
MonitorMode.AUTOMATIC -> flow<Unit> {
if (arePodsConnected) {
log(TAG) { "Pods are connected, aborting any timeout." }
} 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>
+10
View File
@@ -85,4 +85,14 @@
<string name="pods_single_headphones_label">Headphones</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_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>
@@ -1,6 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<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">
<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
}
}
@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) }
}