mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-15 02:36: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.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>
|
||||
@@ -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) }
|
||||
}
|
||||
Reference in New Issue
Block a user