From 1689153070b2325aee33bb7de2173f8410c54666 Mon Sep 17 00:00:00 2001 From: darken Date: Thu, 4 Sep 2025 16:45:28 +0200 Subject: [PATCH] Implement profile-based device separation in overview UI - Add UnmatchedDevicesCard to show/hide devices without profiles - Separate devices with profiles from unmatched devices in overview - Add priority-based sorting (profile.priority with 0 = highest) - Add compiler args for experimental unsigned types and annotation targets - Add string resources for unmatched devices UI - Refactor overview to handle both profiled and non-profiled devices The UI now shows: 1. Devices with configured profiles first (sorted by priority) 2. Collapsible section for unmatched devices with toggle button 3. Session-persistent show/hide state for unmatched devices --- app/build.gradle.kts | 4 +- .../common/bluetooth/BleScannerExtensions.kt | 2 + .../NameBasedPolyJsonAdapterFactory.kt | 110 ++++++++++++++++++ .../capod/devices/core/AppleDeviceProfile.kt | 3 + .../capod/devices/core/DeviceProfileId.kt | 2 + .../capod/main/ui/overview/OverviewAdapter.kt | 6 +- .../main/ui/overview/OverviewFragmentVM.kt | 90 +++++++++++--- ...MissingMainDeviceVH.kt => NoProfilesVH.kt} | 0 .../overview/cards/UnmatchedDevicesCardVH.kt | 50 ++++++++ .../darken/capod/monitor/core/PodMonitor.kt | 95 ++++----------- .../monitor/core/PodMonitorExtensions.kt | 11 ++ ..._item.xml => overview_noprofiles_item.xml} | 0 .../overview_unmatched_devices_item.xml | 56 +++++++++ app/src/main/res/values/strings.xml | 5 + 14 files changed, 341 insertions(+), 93 deletions(-) create mode 100644 app/src/main/java/eu/darken/capod/common/bluetooth/BleScannerExtensions.kt create mode 100644 app/src/main/java/eu/darken/capod/common/serialization/NameBasedPolyJsonAdapterFactory.kt create mode 100644 app/src/main/java/eu/darken/capod/devices/core/AppleDeviceProfile.kt create mode 100644 app/src/main/java/eu/darken/capod/devices/core/DeviceProfileId.kt rename app/src/main/java/eu/darken/capod/main/ui/overview/cards/{MissingMainDeviceVH.kt => NoProfilesVH.kt} (100%) create mode 100644 app/src/main/java/eu/darken/capod/main/ui/overview/cards/UnmatchedDevicesCardVH.kt create mode 100644 app/src/main/java/eu/darken/capod/monitor/core/PodMonitorExtensions.kt rename app/src/main/res/layout/{overview_nomaindevice_item.xml => overview_noprofiles_item.xml} (100%) create mode 100644 app/src/main/res/layout/overview_unmatched_devices_item.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 239ecb43..cca23e61 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -114,7 +114,9 @@ android { "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.FlowPreview", "-opt-in=kotlin.time.ExperimentalTime", - "-opt-in=kotlin.RequiresOptIn" + "-opt-in=kotlin.RequiresOptIn", + "-opt-in=kotlin.ExperimentalUnsignedTypes", + "-Xannotation-default-target=param-property" ) } diff --git a/app/src/main/java/eu/darken/capod/common/bluetooth/BleScannerExtensions.kt b/app/src/main/java/eu/darken/capod/common/bluetooth/BleScannerExtensions.kt new file mode 100644 index 00000000..324e5d17 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/common/bluetooth/BleScannerExtensions.kt @@ -0,0 +1,2 @@ +package eu.darken.capod.common.bluetooth + diff --git a/app/src/main/java/eu/darken/capod/common/serialization/NameBasedPolyJsonAdapterFactory.kt b/app/src/main/java/eu/darken/capod/common/serialization/NameBasedPolyJsonAdapterFactory.kt new file mode 100644 index 00000000..1f958f81 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/common/serialization/NameBasedPolyJsonAdapterFactory.kt @@ -0,0 +1,110 @@ +@file:Suppress("MemberVisibilityCanBePrivate") + +package eu.darken.sdmse.common.serialization + +import com.squareup.moshi.* +import java.lang.reflect.Type +import java.util.* +import javax.annotation.CheckReturnValue + +class NameBasedPolyJsonAdapterFactory internal constructor( + val baseType: Class, + val keyLabels: List = emptyList(), + val subtypes: List = emptyList(), +) : JsonAdapter.Factory { + + fun withSubtype(subtype: Class, label: String): NameBasedPolyJsonAdapterFactory { + require(!keyLabels.contains(label)) { "Labels must be unique." } + return NameBasedPolyJsonAdapterFactory( + baseType = baseType, + keyLabels = keyLabels + label, + subtypes = subtypes + subtype, + ) + } + + override fun create(type: Type, annotations: Set, moshi: Moshi): JsonAdapter<*>? { + if (Types.getRawType(type) != baseType || annotations.isNotEmpty()) { + return null + } + + val jsonAdapters = ArrayList>(subtypes.size) + var i = 0 + val size = subtypes.size + while (i < size) { + jsonAdapters.add(moshi.adapter(subtypes[i])) + i++ + } + + return PolymorphicJsonAdapter( + nameLabels = keyLabels, + subTypes = subtypes, + jsonAdapters = jsonAdapters, + ).nullSafe() + } + + internal class PolymorphicJsonAdapter( + val nameLabels: List, + val subTypes: List, + val jsonAdapters: List>, + ) : JsonAdapter() { + + private val nameOptions: JsonReader.Options = JsonReader.Options.of(*nameLabels.toTypedArray()) + + override fun fromJson(reader: JsonReader): Any? { + val peeked = reader.peekJson().apply { + setFailOnUnknown(false) + } + val labelIndex = peeked.use(::labelIndex) + + if (labelIndex == -1) { + throw JsonDataException("No matching Field names for $nameLabels") + } + + return jsonAdapters[labelIndex].fromJson(reader) + + } + + private fun labelIndex(reader: JsonReader): Int { + reader.beginObject() + while (reader.hasNext()) { + val labelIndex = reader.selectName(nameOptions) + if (labelIndex == -1) { + reader.skipName() + reader.skipValue() + continue + } + return labelIndex + } + + return -1 +// throw JsonDataException("Missing label for $labelKey") + } + + override fun toJson(writer: JsonWriter, value: Any?) { + val type = value!!.javaClass + val typeIndex = subTypes.indexOf(type) + + if (typeIndex == -1) { + throw JsonDataException("No matching name label for $value. Valid labels are $nameLabels") + } + + val adapter = jsonAdapters[typeIndex] + + writer.beginObject() + val flattenToken = writer.beginFlatten() + adapter.toJson(writer, value) + writer.endFlatten(flattenToken) + writer.endObject() + } + + override fun toString(): String = "KeyBasedPolyJsonAdapterFactory($nameLabels)" + } + + companion object { + + @CheckReturnValue + fun of(baseType: Class): NameBasedPolyJsonAdapterFactory = NameBasedPolyJsonAdapterFactory( + baseType + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/devices/core/AppleDeviceProfile.kt b/app/src/main/java/eu/darken/capod/devices/core/AppleDeviceProfile.kt new file mode 100644 index 00000000..020ab5dd --- /dev/null +++ b/app/src/main/java/eu/darken/capod/devices/core/AppleDeviceProfile.kt @@ -0,0 +1,3 @@ +package eu.darken.capod.devices.core + +data class AppleDeviceProfile() diff --git a/app/src/main/java/eu/darken/capod/devices/core/DeviceProfileId.kt b/app/src/main/java/eu/darken/capod/devices/core/DeviceProfileId.kt new file mode 100644 index 00000000..99153add --- /dev/null +++ b/app/src/main/java/eu/darken/capod/devices/core/DeviceProfileId.kt @@ -0,0 +1,2 @@ +package eu.darken.capod.devices.core + diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewAdapter.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewAdapter.kt index 15d4464f..ee9b6f3e 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewAdapter.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewAdapter.kt @@ -12,8 +12,9 @@ import eu.darken.capod.common.lists.modular.ModularAdapter import eu.darken.capod.common.lists.modular.mods.DataBinderMod import eu.darken.capod.common.lists.modular.mods.TypedVHCreatorMod import eu.darken.capod.main.ui.overview.cards.BluetoothDisabledVH -import eu.darken.capod.main.ui.overview.cards.MissingMainDeviceVH +import eu.darken.capod.main.ui.overview.cards.NoProfilesVH import eu.darken.capod.main.ui.overview.cards.PermissionCardVH +import eu.darken.capod.main.ui.overview.cards.UnmatchedDevicesCardVH import eu.darken.capod.main.ui.overview.cards.pods.DualPodsCardVH import eu.darken.capod.main.ui.overview.cards.pods.SinglePodsCardVH import eu.darken.capod.main.ui.overview.cards.pods.UnknownPodDeviceCardVH @@ -30,8 +31,9 @@ class OverviewAdapter @Inject constructor() : modules.add(TypedVHCreatorMod({ data[it] is PermissionCardVH.Item }) { PermissionCardVH(it) }) modules.add(TypedVHCreatorMod({ data[it] is DualPodsCardVH.Item }) { DualPodsCardVH(it) }) modules.add(TypedVHCreatorMod({ data[it] is SinglePodsCardVH.Item }) { SinglePodsCardVH(it) }) - modules.add(TypedVHCreatorMod({ data[it] is MissingMainDeviceVH.Item }) { MissingMainDeviceVH(it) }) + modules.add(TypedVHCreatorMod({ data[it] is NoProfilesVH.Item }) { NoProfilesVH(it) }) modules.add(TypedVHCreatorMod({ data[it] is BluetoothDisabledVH.Item }) { BluetoothDisabledVH(it) }) + modules.add(TypedVHCreatorMod({ data[it] is UnmatchedDevicesCardVH.Item }) { UnmatchedDevicesCardVH(it) }) modules.add(TypedVHCreatorMod({ data[it] is UnknownPodDeviceCardVH.Item }) { UnknownPodDeviceCardVH(it) }) } diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt index 7287371c..4ffad19e 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt @@ -8,18 +8,21 @@ import eu.darken.capod.common.bluetooth.BluetoothManager2 import eu.darken.capod.common.coroutine.DispatcherProvider import eu.darken.capod.common.debug.DebugSettings import eu.darken.capod.common.debug.logging.log +import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.flow.combine import eu.darken.capod.common.flow.throttleLatest import eu.darken.capod.common.livedata.SingleLiveEvent import eu.darken.capod.common.permissions.Permission import eu.darken.capod.common.uix.ViewModel3 import eu.darken.capod.common.upgrade.UpgradeRepo +import eu.darken.capod.devices.core.DeviceProfilesRepo import eu.darken.capod.main.core.GeneralSettings import eu.darken.capod.main.core.MonitorMode import eu.darken.capod.main.core.PermissionTool import eu.darken.capod.main.ui.overview.cards.BluetoothDisabledVH -import eu.darken.capod.main.ui.overview.cards.MissingMainDeviceVH +import eu.darken.capod.main.ui.overview.cards.NoProfilesVH import eu.darken.capod.main.ui.overview.cards.PermissionCardVH +import eu.darken.capod.main.ui.overview.cards.UnmatchedDevicesCardVH import eu.darken.capod.main.ui.overview.cards.pods.DualPodsCardVH import eu.darken.capod.main.ui.overview.cards.pods.SinglePodsCardVH import eu.darken.capod.main.ui.overview.cards.pods.UnknownPodDeviceCardVH @@ -38,8 +41,10 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.isActive +import java.time.Duration import java.time.Instant import javax.inject.Inject +import kotlin.collections.sortedWith @HiltViewModel class OverviewFragmentVM @Inject constructor( @@ -52,6 +57,7 @@ class OverviewFragmentVM @Inject constructor( debugSettings: DebugSettings, private val upgradeRepo: UpgradeRepo, private val bluetoothManager: BluetoothManager2, + private val profilesRepo: DeviceProfilesRepo, ) : ViewModel3(dispatcherProvider = dispatcherProvider) { init { @@ -98,6 +104,8 @@ class OverviewFragmentVM @Inject constructor( val requestPermissionEvent = SingleLiveEvent() + private var showUnmatchedDevices = false + private val pods: Flow> = permissionTool.missingPermissions .flatMapLatest { permissions -> if (permissions.isNotEmpty()) { @@ -115,8 +123,8 @@ class OverviewFragmentVM @Inject constructor( pods, debugSettings.isDebugModeEnabled.flow, bluetoothManager.isBluetoothEnabled, - podMonitor.mainDevice, - ) { _, permissions, pods, isDebugMode, isBluetoothEnabled, mainPod -> + profilesRepo.profiles, + ) { _, permissions, devices, isDebugMode, isBluetoothEnabled, profiles -> val items = mutableListOf() permissions @@ -131,42 +139,79 @@ class OverviewFragmentVM @Inject constructor( if (permissions.isEmpty()) { if (!isBluetoothEnabled) { items.add(0, BluetoothDisabledVH.Item) - } else if (mainPod == null) { - items.add(0, MissingMainDeviceVH.Item( - onManageDevices = { - OverviewFragmentDirections.actionOverviewFragmentToDeviceManagerFragment().navigate() - } - )) + } else if (profiles.isEmpty()) { + items.add( + 0, NoProfilesVH.Item( + onManageDevices = { + OverviewFragmentDirections.actionOverviewFragmentToDeviceManagerFragment().navigate() + } + )) } } if (permissions.isEmpty() && isBluetoothEnabled) { - pods.map { - val now = Instant.now() - val isMainPod = it.identifier == mainPod?.identifier - when (it) { + val now = Instant.now() + + // Split devices into profiled and unmatched + val profiledDevices = devices.filter { it.meta.profile != null } + val unmatchedDevices = devices.filter { it.meta.profile == null } + + // Add profiled devices first + profiledDevices.map { device -> + when (device) { is DualPodDevice -> DualPodsCardVH.Item( now = now, - device = it, + device = device, showDebug = isDebugMode, - isMainPod = isMainPod, ) is SinglePodDevice -> SinglePodsCardVH.Item( now = now, - device = it, + device = device, showDebug = isDebugMode, - isMainPod = isMainPod, ) else -> UnknownPodDeviceCardVH.Item( now = now, - device = it, + device = device, showDebug = isDebugMode, - isMainPod = isMainPod, ) } }.run { items.addAll(this) } + + // Add unmatched devices section if any exist + if (unmatchedDevices.isNotEmpty()) { + items.add(UnmatchedDevicesCardVH.Item( + count = unmatchedDevices.size, + isExpanded = showUnmatchedDevices, + onToggle = { toggleUnmatchedDevices() } + )) + + // Show unmatched devices if expanded + if (showUnmatchedDevices) { + unmatchedDevices.map { device -> + when (device) { + is DualPodDevice -> DualPodsCardVH.Item( + now = now, + device = device, + showDebug = isDebugMode, + ) + + is SinglePodDevice -> SinglePodsCardVH.Item( + now = now, + device = device, + showDebug = isDebugMode, + ) + + else -> UnknownPodDeviceCardVH.Item( + now = now, + device = device, + showDebug = isDebugMode, + ) + } + }.run { items.addAll(this) } + } + } } items @@ -193,4 +238,11 @@ class OverviewFragmentVM @Inject constructor( launchUpgradeFlow.postValue(call) } + private fun toggleUnmatchedDevices() { + showUnmatchedDevices = !showUnmatchedDevices + } + + companion object { + private val TAG = logTag("Overview", "OverviewFragmentVM") + } } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/MissingMainDeviceVH.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/NoProfilesVH.kt similarity index 100% rename from app/src/main/java/eu/darken/capod/main/ui/overview/cards/MissingMainDeviceVH.kt rename to app/src/main/java/eu/darken/capod/main/ui/overview/cards/NoProfilesVH.kt diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/UnmatchedDevicesCardVH.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/UnmatchedDevicesCardVH.kt new file mode 100644 index 00000000..4defee06 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/UnmatchedDevicesCardVH.kt @@ -0,0 +1,50 @@ +package eu.darken.capod.main.ui.overview.cards + +import android.view.ViewGroup +import eu.darken.capod.R +import eu.darken.capod.common.lists.binding +import eu.darken.capod.common.lists.differ.DifferItem +import eu.darken.capod.databinding.OverviewUnmatchedDevicesItemBinding +import eu.darken.capod.main.ui.overview.OverviewAdapter + +class UnmatchedDevicesCardVH(parent: ViewGroup) : + OverviewAdapter.BaseVH( + R.layout.overview_unmatched_devices_item, + parent + ) { + + override val viewBinding = lazy { + OverviewUnmatchedDevicesItemBinding.bind(itemView) + } + + override val onBindData: OverviewUnmatchedDevicesItemBinding.( + item: Item, + payloads: List + ) -> Unit = binding(payload = true) { item -> + val countText = when (item.count) { + 1 -> context.getString(R.string.overview_unmatched_devices_count_single) + else -> context.getString(R.string.overview_unmatched_devices_count_plural, item.count) + } + unmatchedCount.text = countText + + val toggleText = if (item.isExpanded) { + context.getString(R.string.general_hide_action) + } else { + context.getString(R.string.general_show_action) + } + toggleAction.text = toggleText + + toggleAction.setOnClickListener { item.onToggle() } + } + + data class Item( + val count: Int, + val isExpanded: Boolean, + val onToggle: () -> Unit, + ) : OverviewAdapter.Item { + override val stableId: Long = Item::class.hashCode().toLong() + + override val payloadProvider: ((DifferItem, DifferItem) -> DifferItem?) + get() = { old, new -> if (new::class.isInstance(old)) new else null } + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt b/app/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt index dc92fa5e..85a60ea5 100644 --- a/app/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt +++ b/app/src/main/java/eu/darken/capod/monitor/core/PodMonitor.kt @@ -1,10 +1,10 @@ package eu.darken.capod.monitor.core import android.bluetooth.le.ScanFilter -import eu.darken.capod.common.bluetooth.BleScanResult import eu.darken.capod.common.bluetooth.BleScanner import eu.darken.capod.common.bluetooth.BluetoothManager2 import eu.darken.capod.common.bluetooth.ScannerMode +import eu.darken.capod.common.bluetooth.onlyNewAndUnique import eu.darken.capod.common.coroutine.AppScope import eu.darken.capod.common.debug.DebugSettings import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE @@ -13,18 +13,18 @@ import eu.darken.capod.common.debug.logging.asLog 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.setupCommonEventHandlers import eu.darken.capod.common.flow.throttleLatest +import eu.darken.capod.devices.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.ApplePods import eu.darken.capod.pods.core.apple.protocol.ProximityPairing 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 @@ -48,6 +48,7 @@ class PodMonitor @Inject constructor( private val debugSettings: DebugSettings, private val podDeviceCache: PodDeviceCache, permissionTool: PermissionTool, + private val profilesRepo: DeviceProfilesRepo, ) { private val deviceCache = mutableMapOf() @@ -69,17 +70,9 @@ class PodMonitor @Inject constructor( createBleScanner() } } - .map { newPods -> - val pods = processWithCache(newPods) - - val presorted = sortPodsToInterest(pods.values) - val main = determineMainDevice(presorted) - newPods?.firstOrNull { it.device.identifier == main?.identifier }?.let { - podDeviceCache.saveMainDevice(it.scanResult) - } - - presorted.sortedByDescending { it == main } - } + .map { results -> results?.mapNotNull { podFactory.createPod(it) } } + .map { processWithCache(it).values } + .map { sortPodsToInterest(it) } .retryWhen { cause, attempt -> log(TAG, WARN) { "PodMonitor failed (attempt=$attempt), will retry: ${cause.asLog()}" } delay(3000) @@ -88,10 +81,18 @@ class PodMonitor @Inject constructor( .onStart { emit(emptyList()) } .replayingShare(appScope) - val mainDevice: Flow = devices - .map { determineMainDevice(it) } - .setupCommonEventHandlers(TAG) { "mainDevice" } - .replayingShare(appScope) + private fun sortPodsToInterest(devices: Collection): List { + val now = Instant.now() + return devices.sortedWith( + compareBy { it.meta.profile?.priority ?: Int.MAX_VALUE } + .thenBy { + val age = Duration.between(it.seenLastAt, now).seconds + if (age < 5) 0L else (age / 3L) + } + .thenByDescending { it.signalQuality } + .thenByDescending { (it.seenCounter / 10) } + ) + } private data class ScannerOptions( val scannerMode: ScannerMode, @@ -139,8 +140,9 @@ class PodMonitor @Inject constructor( disableOffloadFiltering = options.offloadedFilteringDisabled, disableOffloadBatching = options.offloadedBatchingDisabled, disableDirectScanCallback = options.disableDirectCallback, - ).map { preFilterAndMap(it) } + ) } + .map { it.onlyNewAndUnique() } private suspend fun processWithCache( newPods: List? @@ -170,61 +172,12 @@ class PodMonitor @Inject constructor( return pods } - private suspend fun preFilterAndMap(rawResults: Collection): List = rawResults - .groupBy { it.address } - .values - .map { sameAdrDevs -> - // For each address we only want the newest result, upstream may batch data - val newest = sameAdrDevs.maxByOrNull { it.generatedAtNanos }!! - sameAdrDevs.minus(newest).let { - if (it.isNotEmpty()) log(TAG, VERBOSE) { "Discarding stale results: $it" } - } - newest - } - .mapNotNull { podFactory.createPod(it) } - - private fun sortPodsToInterest(pods: Collection): List { - val now = Instant.now() - - return pods.sortedWith( - compareByDescending { true } - .thenBy { - val age = Duration.between(it.seenLastAt, now).seconds - if (age < 5) 0L else (age / 3L) - } - .thenByDescending { it.signalQuality } - .thenByDescending { (it.seenCounter / 10) } - ) - } - - private fun determineMainDevice(pods: List): PodDevice? { - val identityKey = generalSettings.mainDeviceIdentityKey.value?.takeIf { it.isNotEmpty() } - if (identityKey != null) { - val irkHit = pods.filterIsInstance().firstOrNull { it.flags.isIRKMatch } - log(TAG) { "IRK is configured, main device irkHit=$irkHit" } - return irkHit - } - - val mainDeviceModel = generalSettings.mainDeviceModel.value - val presorted = sortPodsToInterest(pods).sortedByDescending { - it.model == mainDeviceModel && it.model != PodDevice.Model.UNKNOWN - } - - return presorted.firstOrNull()?.let { candidate -> - when { - candidate.model == PodDevice.Model.UNKNOWN -> null - mainDeviceModel != PodDevice.Model.UNKNOWN && candidate.model != mainDeviceModel -> null - candidate.signalQuality <= generalSettings.minimumSignalQuality.value -> null - else -> candidate - } - } - } - suspend fun latestMainDevice(): PodDevice? { - val currentMain = mainDevice.firstOrNull() + val currentMain = devices.firstOrNull()?.firstOrNull() log(TAG) { "Live mainDevice is $currentMain" } - return currentMain ?: podDeviceCache.loadMainDevice() + return currentMain ?: profilesRepo.profiles.first().firstOrNull() + ?.let { podDeviceCache.load(it.id) } ?.let { podFactory.createPod(it)?.device } .also { log(TAG) { "Cached mainDevice is $it" } } } diff --git a/app/src/main/java/eu/darken/capod/monitor/core/PodMonitorExtensions.kt b/app/src/main/java/eu/darken/capod/monitor/core/PodMonitorExtensions.kt new file mode 100644 index 00000000..684dc0eb --- /dev/null +++ b/app/src/main/java/eu/darken/capod/monitor/core/PodMonitorExtensions.kt @@ -0,0 +1,11 @@ +package eu.darken.capod.monitor.core + +import eu.darken.capod.pods.core.PodDevice +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlin.collections.firstOrNull + +fun PodMonitor.devicesWithProfiles(): Flow> = devices + .map { devices -> devices.filter { it.profile != null } } + +fun PodMonitor.primaryDevice(): Flow = devicesWithProfiles().map { it.firstOrNull() } \ No newline at end of file diff --git a/app/src/main/res/layout/overview_nomaindevice_item.xml b/app/src/main/res/layout/overview_noprofiles_item.xml similarity index 100% rename from app/src/main/res/layout/overview_nomaindevice_item.xml rename to app/src/main/res/layout/overview_noprofiles_item.xml diff --git a/app/src/main/res/layout/overview_unmatched_devices_item.xml b/app/src/main/res/layout/overview_unmatched_devices_item.xml new file mode 100644 index 00000000..d76f6359 --- /dev/null +++ b/app/src/main/res/layout/overview_unmatched_devices_item.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 78ea8150..a699cad4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -10,6 +10,8 @@ Save Guide Continue + Show + Hide E.g.: %s @@ -163,6 +165,9 @@ Configure your device to start monitoring battery levels and enable additional features. Bluetooth is disabled Bluetooth is disabled, enable it ;) + Unmatched devices + 1 device without matching profile + %d devices without matching profile Bluetooth connect This app requires the \"Bluetooth connect\" permission to interact with paired devices and initiate connections.