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.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>
+10
View File
@@ -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) }
}