mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Refactor device profiles and settings migration
This commit introduces a new device profile system, moving away from a single "main device" configuration in `GeneralSettings`. Key changes: - `DeviceProfilesRepo` now manages a list of `DeviceProfile` objects, allowing for multiple device configurations. - On first launch with this update, existing "main device" settings from `GeneralSettings` (address, model, keys, signal quality) are migrated into a new default `AppleDeviceProfile`. - The `MonitorWorker`, `PopUpReaction`, `AutoConnect`, and `PodHistoryRepo` have been updated to use the new `DeviceProfilesRepo` instead of the old `GeneralSettings` for device-specific information. - `AppleFactory` now uses `DeviceProfilesRepo` to find matching profiles based on IRK. - `TroubleShooterFragmentVM` now interacts with `DeviceProfilesRepo` for profile management during troubleshooting. - The `create` method in `ApplePodsFactory` and its implementations are now `suspend` functions to allow for asynchronous operations like fetching profiles from the repository. - Old "main device" preference keys in `GeneralSettings` have been renamed with an "old" prefix and will be removed in a future update. - A new `currentProfiles()` extension function provides a convenient way to get the current list of profiles.
This commit is contained in:
@@ -35,16 +35,19 @@ class GeneralSettings @Inject constructor(
|
||||
|
||||
val scannerMode = preferences.createFlowPreference("core.scanner.mode", ScannerMode.BALANCED, moshi)
|
||||
|
||||
// TODO migrate these to new profiles
|
||||
val minimumSignalQuality = preferences.createFlowPreference("core.signal.minimum", 0.20f)
|
||||
val mainDeviceAddress = preferences.createFlowPreference<BluetoothAddress?>("core.maindevice.address", null)
|
||||
val mainDeviceModel = preferences.createFlowPreference("core.maindevice.model", PodDevice.Model.UNKNOWN, moshi)
|
||||
val mainDeviceIdentityKey = preferences.createFlowPreference<IdentityResolvingKey?>(
|
||||
val oldMinimumSignalQuality = preferences.createFlowPreference("core.signal.minimum", 0.20f)
|
||||
|
||||
val oldMainDeviceAddress = preferences.createFlowPreference<BluetoothAddress?>("core.maindevice.address", null)
|
||||
|
||||
val oldMainDeviceModel = preferences.createFlowPreference("core.maindevice.model", PodDevice.Model.UNKNOWN, moshi)
|
||||
|
||||
val oldMainDeviceIdentityKey = preferences.createFlowPreference<IdentityResolvingKey?>(
|
||||
"core.maindevice.identitykey",
|
||||
null,
|
||||
moshi
|
||||
)
|
||||
val mainDeviceEncryptionKey = preferences.createFlowPreference<ProximityEncryptionKey?>(
|
||||
|
||||
val oldMainDeviceEncryptionKey = preferences.createFlowPreference<ProximityEncryptionKey?>(
|
||||
"core.maindevice.encryptionkey",
|
||||
null,
|
||||
moshi
|
||||
@@ -64,8 +67,6 @@ class GeneralSettings @Inject constructor(
|
||||
useExtraMonitorNotification,
|
||||
keepConnectedNotificationAfterDisconnect,
|
||||
scannerMode,
|
||||
minimumSignalQuality,
|
||||
mainDeviceAddress,
|
||||
isOffloadedFilteringDisabled,
|
||||
isOffloadedBatchingDisabled,
|
||||
useIndirectScanResultCallback,
|
||||
|
||||
@@ -14,17 +14,17 @@ import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.flow.replayingShare
|
||||
import eu.darken.capod.common.flow.throttleLatest
|
||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
import eu.darken.capod.main.core.PermissionTool
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.PodFactory
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||
import eu.darken.capod.profiles.core.currentProfiles
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
@@ -85,7 +85,7 @@ class PodMonitor @Inject constructor(
|
||||
|
||||
private suspend fun sortPodsToInterest(devices: Collection<PodDevice>): List<PodDevice> {
|
||||
val now = Instant.now()
|
||||
val profiles = profilesRepo.profiles.firstOrNull() ?: emptyList()
|
||||
val profiles = profilesRepo.currentProfiles() ?: emptyList()
|
||||
|
||||
return devices.sortedWith(
|
||||
compareBy<PodDevice> { device ->
|
||||
@@ -185,7 +185,7 @@ class PodMonitor @Inject constructor(
|
||||
val currentMain = devices.firstOrNull()?.firstOrNull()
|
||||
log(TAG) { "Live mainDevice is $currentMain" }
|
||||
|
||||
return currentMain ?: profilesRepo.profiles.first().firstOrNull()
|
||||
return currentMain ?: profilesRepo.currentProfiles().firstOrNull()
|
||||
?.let { podDeviceCache.load(it.id) }
|
||||
?.let { podFactory.createPod(it)?.device }
|
||||
.also { log(TAG) { "Cached mainDevice is $it" } }
|
||||
|
||||
@@ -8,7 +8,6 @@ import androidx.work.ForegroundInfo
|
||||
import androidx.work.WorkerParameters
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import eu.darken.capod.common.bluetooth.BluetoothAddress
|
||||
import eu.darken.capod.common.bluetooth.BluetoothDevice2
|
||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
@@ -27,6 +26,8 @@ import eu.darken.capod.monitor.core.MonitorCoroutineScope
|
||||
import eu.darken.capod.monitor.core.PodMonitor
|
||||
import eu.darken.capod.monitor.core.primaryDevice
|
||||
import eu.darken.capod.monitor.ui.MonitorNotifications
|
||||
import eu.darken.capod.profiles.core.DeviceProfile
|
||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||
import eu.darken.capod.reaction.core.autoconnect.AutoConnect
|
||||
import eu.darken.capod.reaction.core.playpause.PlayPause
|
||||
import eu.darken.capod.reaction.core.popup.PopUpReaction
|
||||
@@ -62,6 +63,7 @@ class MonitorWorker @AssistedInject constructor(
|
||||
private val autoConnect: AutoConnect,
|
||||
private val popUpReaction: PopUpReaction,
|
||||
private val popUpWindow: PopUpWindow,
|
||||
private val profilesRepo: DeviceProfilesRepo,
|
||||
) : CoroutineWorker(context, params) {
|
||||
|
||||
private val workerScope = MonitorCoroutineScope()
|
||||
@@ -145,22 +147,29 @@ class MonitorWorker @AssistedInject constructor(
|
||||
} else {
|
||||
combine(
|
||||
generalSettings.monitorMode.flow,
|
||||
generalSettings.mainDeviceAddress.flow,
|
||||
profilesRepo.profiles,
|
||||
bluetoothManager.connectedDevices(),
|
||||
) { monitorMode, mainAddress, connectedDevices ->
|
||||
listOf(monitorMode, mainAddress, connectedDevices)
|
||||
) { monitorMode, profiles, connectedDevices ->
|
||||
listOf(monitorMode, profiles, connectedDevices)
|
||||
}
|
||||
}
|
||||
}
|
||||
.setupCommonEventHandlers(TAG) { "MonitorMode" }
|
||||
.flatMapLatest { arguments ->
|
||||
val monitorMode = arguments[0] as MonitorMode
|
||||
val mainAddress = arguments[1] as BluetoothAddress?
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val profiles = arguments[1] as List<DeviceProfile>
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val devices = arguments[2] as Collection<BluetoothDevice2>
|
||||
|
||||
|
||||
val connectedAddresses = devices.map { it.address }.toSet()
|
||||
val knownAddresses = profiles.mapNotNull { it.address }.toSet()
|
||||
log(TAG) { "Monitor mode: $monitorMode" }
|
||||
log(TAG) { "connectedAddresses: $connectedAddresses" }
|
||||
log(TAG) { "knownAddresses: $knownAddresses" }
|
||||
|
||||
when (monitorMode) {
|
||||
MonitorMode.MANUAL -> flow<Unit> {
|
||||
// Cancel worker, ui scans manually
|
||||
@@ -170,12 +179,12 @@ class MonitorWorker @AssistedInject constructor(
|
||||
MonitorMode.ALWAYS -> emptyFlow()
|
||||
MonitorMode.AUTOMATIC -> flow {
|
||||
when {
|
||||
mainAddress == null && devices.isNotEmpty() -> {
|
||||
profiles.isEmpty() && devices.isNotEmpty() -> {
|
||||
log(TAG, WARN) { "Main device address not set, staying alive while any is connected" }
|
||||
}
|
||||
|
||||
devices.any { it.address == mainAddress } -> {
|
||||
log(TAG) { "Main device is connected ($mainAddress), aborting any timeout." }
|
||||
knownAddresses.any { it in connectedAddresses } -> {
|
||||
log(TAG) { "A device is connected, aborting any timeout." }
|
||||
}
|
||||
|
||||
else -> {
|
||||
|
||||
@@ -15,7 +15,7 @@ import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
|
||||
import eu.darken.capod.pods.core.apple.protocol.RPAChecker
|
||||
import eu.darken.capod.profiles.core.AppleDeviceProfile
|
||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||
import kotlinx.coroutines.flow.first
|
||||
import eu.darken.capod.profiles.core.currentProfiles
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import javax.inject.Inject
|
||||
@@ -63,7 +63,7 @@ class AppleFactory @Inject constructor(
|
||||
val proximityMessage = getMessage(scanResult) ?: return@withLock null
|
||||
val factory = podFactories.firstOrNull { it.isResponsible(proximityMessage) } ?: unknownAppleFactory
|
||||
|
||||
val profiles = profilesRepo.profiles.first().filterIsInstance<AppleDeviceProfile>()
|
||||
val profiles = profilesRepo.currentProfiles().filterIsInstance<AppleDeviceProfile>()
|
||||
var profile = profiles.firstOrNull {
|
||||
it.identityKey != null && rpaChecker.verify(scanResult.address, it.identityKey)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.history.KnownDevice
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityMessage
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
|
||||
|
||||
interface ApplePodsFactory {
|
||||
fun isResponsible(message: ProximityMessage): Boolean
|
||||
fun create(scanResult: BleScanResult, payload: ProximityPayload, meta: ApplePods.AppleMeta): ApplePods
|
||||
suspend fun create(scanResult: BleScanResult, payload: ProximityPayload, meta: ApplePods.AppleMeta): ApplePods
|
||||
|
||||
data class ModelInfo(
|
||||
val full: UShort,
|
||||
|
||||
@@ -47,7 +47,7 @@ data class AirPodsGen1(
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
override suspend fun create(
|
||||
scanResult: BleScanResult,
|
||||
payload: ProximityPayload,
|
||||
meta: ApplePods.AppleMeta
|
||||
|
||||
@@ -47,7 +47,7 @@ data class AirPodsGen2(
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
override suspend fun create(
|
||||
scanResult: BleScanResult,
|
||||
payload: ProximityPayload,
|
||||
meta: ApplePods.AppleMeta
|
||||
|
||||
@@ -47,7 +47,7 @@ data class AirPodsGen3(
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
override suspend fun create(
|
||||
scanResult: BleScanResult,
|
||||
payload: ProximityPayload,
|
||||
meta: ApplePods.AppleMeta
|
||||
|
||||
@@ -47,7 +47,7 @@ data class AirPodsGen4(
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
override suspend fun create(
|
||||
scanResult: BleScanResult,
|
||||
payload: ProximityPayload,
|
||||
meta: ApplePods.AppleMeta
|
||||
|
||||
@@ -45,7 +45,7 @@ data class AirPodsGen4Anc(
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
override suspend fun create(
|
||||
scanResult: BleScanResult,
|
||||
payload: ProximityPayload,
|
||||
meta: ApplePods.AppleMeta
|
||||
|
||||
@@ -52,7 +52,7 @@ data class AirPodsMax(
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
override suspend fun create(
|
||||
scanResult: BleScanResult,
|
||||
payload: ProximityPayload,
|
||||
meta: ApplePods.AppleMeta
|
||||
|
||||
@@ -52,7 +52,7 @@ data class AirPodsMaxUsbc(
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
override suspend fun create(
|
||||
scanResult: BleScanResult,
|
||||
payload: ProximityPayload,
|
||||
meta: ApplePods.AppleMeta
|
||||
|
||||
@@ -64,7 +64,7 @@ data class AirPodsPro(
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
override suspend fun create(
|
||||
scanResult: BleScanResult,
|
||||
payload: ProximityPayload,
|
||||
meta: ApplePods.AppleMeta
|
||||
|
||||
@@ -64,7 +64,7 @@ data class AirPodsPro2(
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
override suspend fun create(
|
||||
scanResult: BleScanResult,
|
||||
payload: ProximityPayload,
|
||||
meta: ApplePods.AppleMeta
|
||||
|
||||
@@ -64,7 +64,7 @@ data class AirPodsPro2Usbc(
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
override suspend fun create(
|
||||
scanResult: BleScanResult,
|
||||
payload: ProximityPayload,
|
||||
meta: ApplePods.AppleMeta
|
||||
|
||||
@@ -45,7 +45,7 @@ data class BeatsFitPro(
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
override suspend fun create(
|
||||
scanResult: BleScanResult,
|
||||
payload: ProximityPayload,
|
||||
meta: ApplePods.AppleMeta
|
||||
|
||||
@@ -37,7 +37,7 @@ data class BeatsFlex(
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
override suspend fun create(
|
||||
scanResult: BleScanResult,
|
||||
payload: ProximityPayload,
|
||||
meta: ApplePods.AppleMeta
|
||||
|
||||
@@ -36,7 +36,7 @@ data class BeatsSolo3(
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
override suspend fun create(
|
||||
scanResult: BleScanResult,
|
||||
payload: ProximityPayload,
|
||||
meta: ApplePods.AppleMeta
|
||||
|
||||
@@ -34,7 +34,7 @@ data class BeatsStudio3(
|
||||
getModelInfo().dirty == DEVICE_CODE_DIRTY && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
override suspend fun create(
|
||||
scanResult: BleScanResult,
|
||||
payload: ProximityPayload,
|
||||
meta: ApplePods.AppleMeta
|
||||
|
||||
@@ -37,7 +37,7 @@ data class BeatsX(
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
override suspend fun create(
|
||||
scanResult: BleScanResult,
|
||||
payload: ProximityPayload,
|
||||
meta: ApplePods.AppleMeta
|
||||
|
||||
@@ -37,7 +37,7 @@ data class PowerBeats3(
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
override suspend fun create(
|
||||
scanResult: BleScanResult,
|
||||
payload: ProximityPayload,
|
||||
meta: ApplePods.AppleMeta
|
||||
|
||||
@@ -38,7 +38,7 @@ data class PowerBeats4(
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
override suspend fun create(
|
||||
scanResult: BleScanResult,
|
||||
payload: ProximityPayload,
|
||||
meta: ApplePods.AppleMeta
|
||||
|
||||
@@ -45,7 +45,7 @@ data class PowerBeatsPro(
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
override suspend fun create(
|
||||
scanResult: BleScanResult,
|
||||
payload: ProximityPayload,
|
||||
meta: ApplePods.AppleMeta
|
||||
|
||||
@@ -45,7 +45,7 @@ data class PowerBeatsPro2(
|
||||
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||
}
|
||||
|
||||
override fun create(
|
||||
override suspend fun create(
|
||||
scanResult: BleScanResult,
|
||||
payload: ProximityPayload,
|
||||
meta: ApplePods.AppleMeta
|
||||
|
||||
@@ -7,20 +7,22 @@ import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.common.upperNibble
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
import eu.darken.capod.pods.core.HasCase
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.ApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPayload
|
||||
import eu.darken.capod.pods.core.apple.protocol.RPAChecker
|
||||
import eu.darken.capod.profiles.core.AppleDeviceProfile
|
||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||
import eu.darken.capod.profiles.core.currentProfiles
|
||||
import java.time.Duration
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class PodHistoryRepo @Inject constructor(
|
||||
private val generalSettings: GeneralSettings,
|
||||
private val profilesRepo: DeviceProfilesRepo,
|
||||
private val rpaChecker: RPAChecker,
|
||||
) {
|
||||
|
||||
@@ -66,7 +68,7 @@ class PodHistoryRepo @Inject constructor(
|
||||
val model: PodDevice.Model,
|
||||
)
|
||||
|
||||
fun search(current: ApplePods): KnownDevice? {
|
||||
suspend fun search(current: ApplePods): KnownDevice? {
|
||||
val basicResult = baseSearch(current)
|
||||
|
||||
val caseIgnored = if (current is DualApplePods) {
|
||||
@@ -97,7 +99,7 @@ class PodHistoryRepo @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun baseSearch(current: ApplePods): KnownDevice? {
|
||||
private suspend fun baseSearch(current: ApplePods): KnownDevice? {
|
||||
val scanResult = current.scanResult
|
||||
val payload = current.payload
|
||||
|
||||
@@ -120,14 +122,14 @@ class PodHistoryRepo @Inject constructor(
|
||||
?.also { log(TAG, VERBOSE) { "search1: Recovered previous ID via address: $it" } }
|
||||
|
||||
if (recognizedDevice == null) {
|
||||
generalSettings.mainDeviceIdentityKey.value?.let { key ->
|
||||
if (!rpaChecker.verify(current.address, key)) {
|
||||
log(TAG, VERBOSE) { "search1: Current device does not match IRK" }
|
||||
return@let
|
||||
}
|
||||
val profile = profilesRepo.currentProfiles()
|
||||
.filterIsInstance<AppleDeviceProfile>()
|
||||
.filter { it.identityKey != null }
|
||||
.firstOrNull { rpaChecker.verify(current.address, it.identityKey!!) }
|
||||
|
||||
if (profile != null) {
|
||||
recognizedDevice = knownDevices.values
|
||||
.firstOrNull { rpaChecker.verify(it.lastAddress, key) }
|
||||
.firstOrNull { rpaChecker.verify(it.lastAddress, profile.identityKey!!) }
|
||||
.also { log(TAG, VERBOSE) { "search1: Recovered previous ID via IRK: $it" } }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ data class FakeAirPodsGen1(
|
||||
getModelInfo().full == DEVICE_CODE && length == 19
|
||||
}
|
||||
|
||||
override fun create(
|
||||
override suspend fun create(
|
||||
scanResult: BleScanResult,
|
||||
payload: ProximityPayload,
|
||||
meta: ApplePods.AppleMeta
|
||||
|
||||
@@ -47,7 +47,7 @@ data class FakeAirPodsGen2(
|
||||
getModelInfo().full == DEVICE_CODE && length == 19
|
||||
}
|
||||
|
||||
override fun create(
|
||||
override suspend fun create(
|
||||
scanResult: BleScanResult,
|
||||
payload: ProximityPayload,
|
||||
meta: ApplePods.AppleMeta
|
||||
|
||||
@@ -49,7 +49,7 @@ data class FakeAirPodsGen3(
|
||||
getModelInfo().full == DEVICE_CODE && length == 19
|
||||
}
|
||||
|
||||
override fun create(
|
||||
override suspend fun create(
|
||||
scanResult: BleScanResult,
|
||||
payload: ProximityPayload,
|
||||
meta: ApplePods.AppleMeta
|
||||
|
||||
@@ -67,7 +67,7 @@ data class FakeAirPodsPro(
|
||||
getModelInfo().full == DEVICE_CODE && length == 19
|
||||
}
|
||||
|
||||
override fun create(
|
||||
override suspend fun create(
|
||||
scanResult: BleScanResult,
|
||||
payload: ProximityPayload,
|
||||
meta: ApplePods.AppleMeta
|
||||
|
||||
@@ -71,7 +71,7 @@ data class FakeAirPodsPro2(
|
||||
getModelInfo().full == DEVICE_CODE && length == 19
|
||||
}
|
||||
|
||||
override fun create(
|
||||
override suspend fun create(
|
||||
scanResult: BleScanResult,
|
||||
payload: ProximityPayload,
|
||||
meta: ApplePods.AppleMeta
|
||||
|
||||
@@ -37,7 +37,7 @@ data class UnknownAppleDevice(
|
||||
) : ApplePodsFactory {
|
||||
override fun isResponsible(message: ProximityMessage): Boolean = true
|
||||
|
||||
override fun create(
|
||||
override suspend fun create(
|
||||
scanResult: BleScanResult,
|
||||
payload: ProximityPayload,
|
||||
meta: ApplePods.AppleMeta,
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package eu.darken.capod.profiles.core
|
||||
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
suspend fun DeviceProfilesRepo.currentProfiles(): List<DeviceProfile> = profiles.first()
|
||||
@@ -1,44 +1,94 @@
|
||||
package eu.darken.capod.profiles.core
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.coroutine.AppScope
|
||||
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 kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class DeviceProfilesRepo @Inject constructor(
|
||||
@AppScope private val scope: CoroutineScope,
|
||||
@ApplicationContext private val context: Context,
|
||||
private val generalSettings: GeneralSettings,
|
||||
private val settings: DeviceProfilesSettings,
|
||||
) {
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
||||
init {
|
||||
scope.launch {
|
||||
if (settings.defaultProfileCreated.value) return@launch
|
||||
|
||||
|
||||
log(TAG) { "Creating default profile" }
|
||||
var defaultProfile = AppleDeviceProfile(
|
||||
label = context.getString(R.string.profiles_name_default),
|
||||
)
|
||||
if (!settings.singleToMultiMigrationDone.value) {
|
||||
log(TAG) { "Migrating settings default profile" }
|
||||
defaultProfile = defaultProfile.copy(
|
||||
minimumSignalQuality = generalSettings.oldMinimumSignalQuality.value,
|
||||
model = generalSettings.oldMainDeviceModel.value,
|
||||
address = generalSettings.oldMainDeviceAddress.value,
|
||||
identityKey = generalSettings.oldMainDeviceIdentityKey.value,
|
||||
encryptionKey = generalSettings.oldMainDeviceEncryptionKey.value,
|
||||
)
|
||||
settings.singleToMultiMigrationDone.value = true
|
||||
}
|
||||
|
||||
log(TAG) { "Default profile: $defaultProfile" }
|
||||
addProfile(defaultProfile)
|
||||
settings.defaultProfileCreated.value = true
|
||||
}
|
||||
}
|
||||
|
||||
val profiles: Flow<List<DeviceProfile>> = settings.profiles.flow.map { it.profiles }
|
||||
|
||||
fun addProfile(profile: DeviceProfile) {
|
||||
suspend fun addProfile(profile: DeviceProfile) = mutex.withLock {
|
||||
val currentContainer = settings.profiles.value
|
||||
val updatedProfiles = currentContainer.profiles + profile
|
||||
settings.profiles.value = DeviceProfilesContainer(updatedProfiles)
|
||||
log(VERBOSE) { "Added device profile: ${profile.label}" }
|
||||
}
|
||||
|
||||
fun updateProfile(profile: DeviceProfile) {
|
||||
suspend fun updateProfile(profile: DeviceProfile) = mutex.withLock {
|
||||
val currentContainer = settings.profiles.value
|
||||
val updatedProfiles = currentContainer.profiles.map {
|
||||
if (it.id == profile.id) profile else it
|
||||
val updatedProfiles = currentContainer.profiles.map {
|
||||
if (it.id == profile.id) profile else it
|
||||
}
|
||||
settings.profiles.value = DeviceProfilesContainer(updatedProfiles)
|
||||
log(VERBOSE) { "Updated device profile: ${profile.label}" }
|
||||
}
|
||||
|
||||
fun removeProfile(profileId: String) {
|
||||
suspend fun removeProfile(profileId: String) = mutex.withLock {
|
||||
val currentContainer = settings.profiles.value
|
||||
val updatedProfiles = currentContainer.profiles.filter { it.id != profileId }
|
||||
settings.profiles.value = DeviceProfilesContainer(updatedProfiles)
|
||||
log(VERBOSE) { "Removed device profile with ID: $profileId" }
|
||||
}
|
||||
|
||||
fun reorderProfiles(profiles: List<DeviceProfile>) {
|
||||
suspend fun reorderProfiles(profiles: List<DeviceProfile>) = mutex.withLock {
|
||||
settings.profiles.value = DeviceProfilesContainer(profiles.toList())
|
||||
log(VERBOSE) { "Reordered ${profiles.size} device profiles" }
|
||||
}
|
||||
|
||||
suspend fun clear() {
|
||||
settings.profiles.value = DeviceProfilesContainer(emptyList())
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = logTag("Profiles", "Repo")
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceDataStore
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.common.preferences.PreferenceStoreMapper
|
||||
import eu.darken.capod.common.preferences.Settings
|
||||
@@ -21,10 +20,12 @@ class DeviceProfilesSettings @Inject constructor(
|
||||
override val preferences: SharedPreferences = context.getSharedPreferences("device_profiles", Context.MODE_PRIVATE)
|
||||
|
||||
val profiles = preferences.createFlowPreference<DeviceProfilesContainer>(
|
||||
"profiles",
|
||||
"profiles.data",
|
||||
DeviceProfilesContainer(),
|
||||
moshi
|
||||
)
|
||||
val singleToMultiMigrationDone = preferences.createFlowPreference("profiles.migration.v2.done", false)
|
||||
val defaultProfileCreated = preferences.createFlowPreference("profiles.default.v2.created", false)
|
||||
|
||||
override val preferenceDataStore: PreferenceDataStore = PreferenceStoreMapper()
|
||||
}
|
||||
+5
-4
@@ -6,20 +6,21 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.asLiveData
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.bluetooth.BluetoothDevice2
|
||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.uix.ViewModel3
|
||||
import eu.darken.capod.profiles.core.AppleDeviceProfile
|
||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.protocol.IdentityResolvingKey
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityEncryptionKey
|
||||
import eu.darken.capod.profiles.core.AppleDeviceProfile
|
||||
import eu.darken.capod.profiles.core.DeviceProfile
|
||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||
import eu.darken.capod.profiles.core.ProfileId
|
||||
import eu.darken.capod.profiles.core.currentProfiles
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@@ -188,7 +189,7 @@ class DeviceProfileCreationFragmentVM @Inject constructor(
|
||||
private fun loadProfile(profileId: String) {
|
||||
launch {
|
||||
try {
|
||||
val profiles = deviceProfilesRepo.profiles.first()
|
||||
val profiles = deviceProfilesRepo.currentProfiles()
|
||||
val profile = profiles.find { it.id == profileId }
|
||||
if (profile != null) {
|
||||
// Set current values
|
||||
|
||||
@@ -12,6 +12,7 @@ import eu.darken.capod.monitor.core.primaryDevice
|
||||
import eu.darken.capod.pods.core.HasEarDetection
|
||||
import eu.darken.capod.pods.core.HasEarDetectionDual
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||
import eu.darken.capod.reaction.core.ReactionSettings
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@@ -30,7 +31,8 @@ class AutoConnect @Inject constructor(
|
||||
private val bluetoothManager: BluetoothManager2,
|
||||
private val podMonitor: PodMonitor,
|
||||
private val generalSettings: GeneralSettings,
|
||||
private val reactionSettings: ReactionSettings
|
||||
private val reactionSettings: ReactionSettings,
|
||||
private val deviceProfilesRepo: DeviceProfilesRepo,
|
||||
) {
|
||||
|
||||
fun monitor(): Flow<Unit> = reactionSettings.autoConnect.flow
|
||||
@@ -49,7 +51,7 @@ class AutoConnect @Inject constructor(
|
||||
.map { (connectedDevices, mainDevice) ->
|
||||
log(TAG, VERBOSE) { "mainPodDevice is $mainDevice" }
|
||||
|
||||
val mainDeviceAddr = generalSettings.mainDeviceAddress.value
|
||||
val mainDeviceAddr = mainDevice.meta.profile?.address
|
||||
if (mainDeviceAddr.isNullOrEmpty()) {
|
||||
log(TAG, WARN) { "mainDeviceAddress is null" }
|
||||
return@map
|
||||
|
||||
@@ -9,7 +9,6 @@ import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||
import eu.darken.capod.common.flow.withPrevious
|
||||
import eu.darken.capod.main.core.GeneralSettings
|
||||
import eu.darken.capod.monitor.core.PodMonitor
|
||||
import eu.darken.capod.monitor.core.primaryDevice
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
@@ -32,7 +31,6 @@ import javax.inject.Singleton
|
||||
class PopUpReaction @Inject constructor(
|
||||
private val podMonitor: PodMonitor,
|
||||
private val reactionSettings: ReactionSettings,
|
||||
private val generalSettings: GeneralSettings,
|
||||
private val bluetoothManager: BluetoothManager2,
|
||||
) {
|
||||
|
||||
@@ -118,17 +116,17 @@ class PopUpReaction @Inject constructor(
|
||||
if (!isEnabled) return@flatMapLatest emptyFlow()
|
||||
|
||||
combine(
|
||||
generalSettings.mainDeviceAddress.flow,
|
||||
bluetoothManager.connectedDevices().distinctUntilChanged(),
|
||||
podMonitor.primaryDevice().distinctUntilChangedBy { it?.rawDataHex },
|
||||
) { targetAddress, devices, broadcast ->
|
||||
log(TAG) { "$targetAddress $broadcast $devices " }
|
||||
val direct = devices.singleOrNull { it.address == targetAddress }.also {
|
||||
) { devices, broadcast ->
|
||||
log(TAG) { "$broadcast $devices " }
|
||||
val primaryAddr = broadcast?.meta?.profile?.address
|
||||
val direct = devices.singleOrNull { it.address == primaryAddr }.also {
|
||||
log(TAG, VERBOSE) { "Connected main device is $it" }
|
||||
}
|
||||
if (direct == null) {
|
||||
connectionCoolDowns.remove(targetAddress).also {
|
||||
if (it != null) log(TAG) { "Cleared connection cooldown for $targetAddress due to disconect" }
|
||||
connectionCoolDowns.remove(primaryAddr).also {
|
||||
if (it != null) log(TAG) { "Cleared connection cooldown for $primaryAddr due to disconect" }
|
||||
}
|
||||
}
|
||||
if (direct != null && broadcast != null) direct to broadcast else null
|
||||
|
||||
@@ -13,7 +13,16 @@ import eu.darken.capod.monitor.core.PodMonitor
|
||||
import eu.darken.capod.monitor.core.primaryDevice
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.unknown.UnknownDevice
|
||||
import kotlinx.coroutines.flow.*
|
||||
import eu.darken.capod.profiles.core.AppleDeviceProfile
|
||||
import eu.darken.capod.profiles.core.DeviceProfilesRepo
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.flow.takeWhile
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -22,6 +31,7 @@ class TroubleShooterFragmentVM @Inject constructor(
|
||||
@Suppress("UNUSED_PARAMETER") handle: SavedStateHandle,
|
||||
private val dispatcherProvider: DispatcherProvider,
|
||||
private val generalSettings: GeneralSettings,
|
||||
private val profilesRepo: DeviceProfilesRepo,
|
||||
private val podMonitor: PodMonitor,
|
||||
private val debugSettings: DebugSettings,
|
||||
) : ViewModel3(dispatcherProvider = dispatcherProvider) {
|
||||
@@ -192,25 +202,28 @@ class TroubleShooterFragmentVM @Inject constructor(
|
||||
|
||||
progress("Headphones found nearby, but not detected as yours. Resetting filters.\n")
|
||||
|
||||
generalSettings.mainDeviceModel.value = PodDevice.Model.UNKNOWN
|
||||
generalSettings.mainDeviceAddress.value = null
|
||||
generalSettings.minimumSignalQuality.value = 0.25f
|
||||
profilesRepo.clear()
|
||||
|
||||
progress("Setting headphone with strongest signal as yours.")
|
||||
|
||||
generalSettings.mainDeviceModel.value = otherDevices.maxBy { it.signalQuality }.model
|
||||
profilesRepo.addProfile(
|
||||
AppleDeviceProfile(
|
||||
label = "Test",
|
||||
model = otherDevices.maxBy { it.signalQuality }.model,
|
||||
)
|
||||
)
|
||||
|
||||
val mainDevice = withTimeoutOrNull(STEP_TIME) {
|
||||
podMonitor.primaryDevice().filterNotNull().firstOrNull()
|
||||
}
|
||||
if (mainDevice != null) {
|
||||
generalSettings.mainDeviceModel.value = mainDevice.model
|
||||
generalSettings.scannerMode.value = ScannerMode.BALANCED
|
||||
success("Success! Detected your headphones.")
|
||||
return@launch
|
||||
}
|
||||
|
||||
failure("No headphones detected near your device.", BleState.Result.Failure.Type.HEADPHONES)
|
||||
generalSettings.scannerMode.value = ScannerMode.BALANCED
|
||||
|
||||
if (mainDevice != null) {
|
||||
success("Success! Detected your headphones.")
|
||||
} else {
|
||||
failure("No headphones detected near your device.", BleState.Result.Failure.Type.HEADPHONES)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user