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:
darken
2025-09-29 13:37:01 +02:00
parent b6027186c0
commit 7b41f83a53
38 changed files with 174 additions and 93 deletions
@@ -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()
}
@@ -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)
}
}
}