diff --git a/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt b/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt index 7800b007..76de1497 100644 --- a/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt +++ b/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt @@ -26,7 +26,7 @@ class BleScanner @Inject constructor( // TODO check Bluetooth enabled fun scan( filter: Set = ProximityPairing.getBleScanFilter(), - mode: Int = ScanSettings.SCAN_MODE_LOW_LATENCY, + mode: Int = ScanSettings.SCAN_MODE_LOW_POWER, delay: Long = 1, ): Flow> = callbackFlow { val scanner = bluetoothManager.adapter.bluetoothLeScanner diff --git a/app/src/main/java/eu/darken/capod/common/debug/autoreport/DebugSettings.kt b/app/src/main/java/eu/darken/capod/common/debug/autoreport/DebugSettings.kt index 0b9ba142..3e6fc4fb 100644 --- a/app/src/main/java/eu/darken/capod/common/debug/autoreport/DebugSettings.kt +++ b/app/src/main/java/eu/darken/capod/common/debug/autoreport/DebugSettings.kt @@ -1,7 +1,11 @@ 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 @@ -9,12 +13,26 @@ import javax.inject.Singleton @Singleton class DebugSettings @Inject constructor( @ApplicationContext private val context: Context, -) { +) : Settings() { - private val prefs by lazy { - context.getSharedPreferences("settings_debug", Context.MODE_PRIVATE) + override 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) + } } - val isAutoReportEnabled = prefs.createFlowPreference("bugreport.automatic.enabled", true) - } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/common/lists/BindableVH.kt b/app/src/main/java/eu/darken/capod/common/lists/BindableVH.kt index 54af2a23..b5465dd9 100644 --- a/app/src/main/java/eu/darken/capod/common/lists/BindableVH.kt +++ b/app/src/main/java/eu/darken/capod/common/lists/BindableVH.kt @@ -11,4 +11,16 @@ interface BindableVH { fun bind(item: ItemT, payloads: MutableList = mutableListOf()) = with(viewBinding.value) { onBindData(item, payloads) } +} + +@Suppress("unused") +inline fun BindableVH.binding( + payload: Boolean = true, + crossinline block: ViewBindingT.(ItemT) -> Unit, +): ViewBindingT.(item: ItemT, payloads: List) -> Unit = { item: ItemT, payloads: List -> + val newestItem = when (payload) { + true -> payloads.filterIsInstance().lastOrNull() ?: item + false -> item + } + block(newestItem) } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/common/preferences/FlowPreference.kt b/app/src/main/java/eu/darken/capod/common/preferences/FlowPreference.kt index 23c32b43..1a8a99e2 100644 --- a/app/src/main/java/eu/darken/capod/common/preferences/FlowPreference.kt +++ b/app/src/main/java/eu/darken/capod/common/preferences/FlowPreference.kt @@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.MutableStateFlow class FlowPreference constructor( private val preferences: SharedPreferences, - private val key: String, + val key: String, private val reader: SharedPreferences.(key: String) -> T, private val writer: SharedPreferences.Editor.(key: String, value: T) -> Unit ) { diff --git a/app/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt b/app/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt index 2d3e74d7..dd6712fb 100644 --- a/app/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt +++ b/app/src/main/java/eu/darken/capod/main/core/GeneralSettings.kt @@ -17,22 +17,20 @@ class GeneralSettings @Inject constructor( private val debugSettings: DebugSettings, ) : 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) { - PK_BUGTRACKING_ENABLED -> debugSettings.isAutoReportEnabled.value - else -> super.getBoolean(key, defValue) + else -> debugSettings.preferenceDataStore.getBoolean(key, defValue) } override fun putBoolean(key: String, value: Boolean) = when (key) { - PK_BUGTRACKING_ENABLED -> debugSettings.isAutoReportEnabled.update { value } - else -> super.putBoolean(key, value) + else -> debugSettings.preferenceDataStore.putBoolean(key, value) } } - override val preferences: SharedPreferences = context.getSharedPreferences("settings_general", Context.MODE_PRIVATE) companion object { internal val TAG = logTag("Core", "Settings") - private const val PK_BUGTRACKING_ENABLED = "core.bugtracking.enabled" } } \ No newline at end of file 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 f9af53b0..42a15d03 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,7 +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.PermissionCardVH +import eu.darken.capod.main.ui.overview.cards.pods.BasicSingleApplePodsCardVH import eu.darken.capod.main.ui.overview.cards.pods.DualApplePodsCardVH +import eu.darken.capod.main.ui.overview.cards.pods.SingleApplePodsCardVH import eu.darken.capod.main.ui.overview.cards.pods.UnknownPodDeviceCardVH import javax.inject.Inject @@ -26,6 +28,8 @@ class OverviewAdapter @Inject constructor() : modules.add(DataBinderMod(data)) modules.add(TypedVHCreatorMod({ data[it] is PermissionCardVH.Item }) { PermissionCardVH(it) }) modules.add(TypedVHCreatorMod({ data[it] is DualApplePodsCardVH.Item }) { DualApplePodsCardVH(it) }) + modules.add(TypedVHCreatorMod({ data[it] is SingleApplePodsCardVH.Item }) { SingleApplePodsCardVH(it) }) + modules.add(TypedVHCreatorMod({ data[it] is BasicSingleApplePodsCardVH.Item }) { BasicSingleApplePodsCardVH(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 85b03d90..ad4bd7d6 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 @@ -6,6 +6,7 @@ import androidx.lifecycle.SavedStateHandle import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import eu.darken.capod.common.coroutine.DispatcherProvider +import eu.darken.capod.common.debug.autoreport.DebugSettings import eu.darken.capod.common.debug.recording.core.RecorderModule import eu.darken.capod.common.livedata.SingleLiveEvent import eu.darken.capod.common.navigation.navVia @@ -13,14 +14,20 @@ import eu.darken.capod.common.permissions.Permission import eu.darken.capod.common.permissions.isGrantedOrNotRequired import eu.darken.capod.common.uix.ViewModel3 import eu.darken.capod.main.ui.overview.cards.PermissionCardVH +import eu.darken.capod.main.ui.overview.cards.pods.BasicSingleApplePodsCardVH import eu.darken.capod.main.ui.overview.cards.pods.DualApplePodsCardVH +import eu.darken.capod.main.ui.overview.cards.pods.SingleApplePodsCardVH import eu.darken.capod.main.ui.overview.cards.pods.UnknownPodDeviceCardVH import eu.darken.capod.monitor.core.PodMonitor import eu.darken.capod.monitor.core.worker.MonitorControl import eu.darken.capod.pods.core.PodDevice +import eu.darken.capod.pods.core.apple.BasicSingleApplePods import eu.darken.capod.pods.core.apple.DualApplePods +import eu.darken.capod.pods.core.apple.SingleApplePods import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* +import kotlinx.coroutines.isActive import java.util.* import javax.inject.Inject @@ -32,12 +39,19 @@ class OverviewFragmentVM @Inject constructor( private val recorderModule: RecorderModule, private val monitorControl: MonitorControl, private val podMonitor: PodMonitor, + private val debugSettings: DebugSettings, ) : ViewModel3(dispatcherProvider = dispatcherProvider) { private val enabledState: Flow = flow { emit(false) } + private val updateTicker = channelFlow { + while (isActive) { + trySend(Unit) + delay(1000) + } + } private val permissionCheckTrigger = MutableStateFlow(UUID.randomUUID()) private val requiredPermissions: Flow> = permissionCheckTrigger .map { @@ -64,10 +78,12 @@ class OverviewFragmentVM @Inject constructor( } val listItems: LiveData> = combine( + updateTicker, enabledState, requiredPermissions, - pods - ) { state, permissions, pods -> + pods, + debugSettings.isDebugModeEnabled.flow + ) { tick, state, permissions, pods, isDebugMode -> val items = mutableListOf() permissions @@ -82,7 +98,9 @@ class OverviewFragmentVM @Inject constructor( pods .map { when (it) { - is DualApplePods -> DualApplePodsCardVH.Item(it) + is DualApplePods -> DualApplePodsCardVH.Item(it, showDebug = isDebugMode) + is SingleApplePods -> SingleApplePodsCardVH.Item(it, showDebug = isDebugMode) + is BasicSingleApplePods -> BasicSingleApplePodsCardVH.Item(it, showDebug = isDebugMode) else -> UnknownPodDeviceCardVH.Item(it) } } diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/BasicSingleApplePodsCardVH.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/BasicSingleApplePodsCardVH.kt new file mode 100644 index 00000000..3eda1b5c --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/BasicSingleApplePodsCardVH.kt @@ -0,0 +1,60 @@ +package eu.darken.capod.main.ui.overview.cards.pods + +import android.icu.text.RelativeDateTimeFormatter +import android.view.ViewGroup +import androidx.core.view.isGone +import eu.darken.capod.R +import eu.darken.capod.common.lists.binding +import eu.darken.capod.databinding.OverviewPodsAppleSingleBasicItemBinding +import eu.darken.capod.pods.core.apple.BasicSingleApplePods +import eu.darken.capod.pods.core.getBatteryLevelHeadset +import java.time.Duration +import java.time.Instant + +class BasicSingleApplePodsCardVH(parent: ViewGroup) : + PodDeviceVH( + R.layout.overview_pods_apple_single_basic_item, + parent + ) { + + override val viewBinding = lazy { OverviewPodsAppleSingleBasicItemBinding.bind(itemView) } + + private val lastSeenFormatter = RelativeDateTimeFormatter.getInstance() + + override val onBindData = binding(payload = true) { item: Item -> + val device = item.device + + name.text = device.getLabel(context) + deviceIcon.setImageResource(device.iconRes) + + val duration = Duration.between(device.lastSeenAt, Instant.now()) + lastSeen.text = lastSeenFormatter.format( + duration.seconds.toDouble(), + RelativeDateTimeFormatter.Direction.LAST, + RelativeDateTimeFormatter.RelativeUnit.SECONDS + ) + + reception.text = device.getSignalQuality(context) + + headphones.apply { + val sb = StringBuilder(context.getString(R.string.pods_single_headphones_label)) + sb.append("\n").append(device.getBatteryLevelHeadset(context)) + text = sb + } + + status.apply { + val sb = StringBuilder() + if (item.showDebug) { + sb.append("--- Debug ---") + sb.append("\n").append(device.rawDataHex) + } + text = sb + isGone = !item.showDebug + } + } + + data class Item( + override val device: BasicSingleApplePods, + override val showDebug: Boolean, + ) : PodDeviceVH.Item +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/DualApplePodsCardVH.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/DualApplePodsCardVH.kt index bf21eaf9..3cf5ede9 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/DualApplePodsCardVH.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/DualApplePodsCardVH.kt @@ -3,15 +3,15 @@ package eu.darken.capod.main.ui.overview.cards.pods import android.icu.text.RelativeDateTimeFormatter import android.view.ViewGroup import eu.darken.capod.R -import eu.darken.capod.common.BuildConfigWrap +import eu.darken.capod.common.lists.binding import eu.darken.capod.databinding.OverviewPodsAppleDualItemBinding -import eu.darken.capod.pods.core.DualPods +import eu.darken.capod.pods.core.DualPodDevice.Pod import eu.darken.capod.pods.core.apple.DualApplePods import eu.darken.capod.pods.core.apple.DualApplePods.DeviceColor import eu.darken.capod.pods.core.apple.DualApplePods.LidState -import eu.darken.capod.pods.core.getBatteryCase -import eu.darken.capod.pods.core.getBatteryLeftPod -import eu.darken.capod.pods.core.getBatteryRightPod +import eu.darken.capod.pods.core.getBatteryLevelCase +import eu.darken.capod.pods.core.getBatteryLevelLeftPod +import eu.darken.capod.pods.core.getBatteryLevelRightPod import java.time.Duration import java.time.Instant @@ -21,19 +21,13 @@ class DualApplePodsCardVH(parent: ViewGroup) : parent ) { - override val viewBinding = lazy { - OverviewPodsAppleDualItemBinding.bind(itemView) - } + override val viewBinding = lazy { OverviewPodsAppleDualItemBinding.bind(itemView) } private val lastSeenFormatter = RelativeDateTimeFormatter.getInstance() - override val onBindData: OverviewPodsAppleDualItemBinding.( - item: Item, - payloads: List - ) -> Unit = { item, _ -> + override val onBindData = binding(payload = true) { item: Item -> val device = item.device - name.apply { val sb = StringBuilder(device.getLabel(context)) if (device.deviceColor != DeviceColor.UNKNOWN) { @@ -54,7 +48,7 @@ class DualApplePodsCardVH(parent: ViewGroup) : podLeft.apply { val sb = StringBuilder(context.getString(R.string.pods_dual_left_label)) - sb.append("\n").append(device.getBatteryLeftPod(context)) + sb.append("\n").append(device.getBatteryLevelLeftPod(context)) when { device.isLeftPodCharging -> sb.append("\n").append("Charging") device.isLeftPodInEar -> sb.append("\n").append("In ear") @@ -65,7 +59,7 @@ class DualApplePodsCardVH(parent: ViewGroup) : podRight.apply { val sb = StringBuilder("Right pod") - sb.append("\n").append(device.getBatteryRightPod(context)) + sb.append("\n").append(device.getBatteryLevelRightPod(context)) when { device.isRightPodCharging -> sb.append("\n").append("Charging") device.isRightPodInEar -> sb.append("\n").append("In ear") @@ -75,13 +69,13 @@ class DualApplePodsCardVH(parent: ViewGroup) : } when (device.microPhonePod) { - DualPods.Pod.LEFT -> podLeft.append("\n(Microphone)") - DualPods.Pod.RIGHT -> podRight.append("\n(Microphone)") + Pod.LEFT -> podLeft.append("\n(Microphone)") + Pod.RIGHT -> podRight.append("\n(Microphone)") } podCase.apply { val sb = StringBuilder("Case") - sb.append("\n").append(device.getBatteryCase(context)) + sb.append("\n").append(device.getBatteryLevelCase(context)) if (device.isCaseCharging) sb.append("\n").append("Charging") when (device.caseLidState) { LidState.OPEN -> sb.append("\n").append(context.getString(R.string.pods_case_status_open_label)) @@ -93,14 +87,18 @@ class DualApplePodsCardVH(parent: ViewGroup) : text = sb } - status.text = getString(R.string.pods_status_x_label, device.getConnectionStateLabel(context)) - if (BuildConfigWrap.DEBUG) { - status.append("\n") - status.append(device.identifier.toString()) + status.apply { + val sb = StringBuilder(getString(R.string.pods_status_x_label, device.getConnectionStateLabel(context))) + if (item.showDebug) { + sb.append("\n\n").append("---Debug---") + sb.append("\n").append(device.rawDataHex) + } + text = sb } } data class Item( - override val device: DualApplePods + override val device: DualApplePods, + override val showDebug: Boolean, ) : PodDeviceVH.Item } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/PodDeviceVH.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/PodDeviceVH.kt index 7be9fdc1..98e67316 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/PodDeviceVH.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/PodDeviceVH.kt @@ -18,6 +18,8 @@ abstract class PodDeviceVH( val device: PodDevice + val showDebug: Boolean + override val stableId: Long get() = device.identifier.hashCode().toLong() override val payloadProvider: ((DifferItem, DifferItem) -> DifferItem?)? diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/SingleApplePodsCardVH.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/SingleApplePodsCardVH.kt new file mode 100644 index 00000000..077741c5 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/SingleApplePodsCardVH.kt @@ -0,0 +1,65 @@ +package eu.darken.capod.main.ui.overview.cards.pods + +import android.icu.text.RelativeDateTimeFormatter +import android.view.ViewGroup +import androidx.core.view.isGone +import eu.darken.capod.R +import eu.darken.capod.common.lists.binding +import eu.darken.capod.databinding.OverviewPodsAppleSingleItemBinding +import eu.darken.capod.pods.core.apple.SingleApplePods +import eu.darken.capod.pods.core.getBatteryLevelHeadset +import java.time.Duration +import java.time.Instant + +class SingleApplePodsCardVH(parent: ViewGroup) : + PodDeviceVH( + R.layout.overview_pods_apple_single_item, + parent + ) { + + override val viewBinding = lazy { OverviewPodsAppleSingleItemBinding.bind(itemView) } + + private val lastSeenFormatter = RelativeDateTimeFormatter.getInstance() + + override val onBindData = binding(payload = true) { item: Item -> + val device = item.device + + name.text = device.getLabel(context) + deviceIcon.setImageResource(device.iconRes) + + val duration = Duration.between(device.lastSeenAt, Instant.now()) + lastSeen.text = lastSeenFormatter.format( + duration.seconds.toDouble(), + RelativeDateTimeFormatter.Direction.LAST, + RelativeDateTimeFormatter.RelativeUnit.SECONDS + ) + + reception.text = device.getSignalQuality(context) + + headphones.apply { + val sb = StringBuilder(context.getString(R.string.pods_single_headphones_label)) + sb.append("\n").append(device.getBatteryLevelHeadset(context)) + when { + device.isHeadsetBeingCharged -> sb.append("\n").append("Charging") + device.isHeadsetBeingWorn -> sb.append("\n").append("In ear") + else -> {} + } + text = sb + } + + status.apply { + val sb = StringBuilder() + if (item.showDebug) { + sb.append("--- Debug ---") + sb.append("\n").append(device.rawDataHex) + } + text = sb + isGone = !item.showDebug + } + } + + data class Item( + override val device: SingleApplePods, + override val showDebug: Boolean, + ) : PodDeviceVH.Item +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/UnknownPodDeviceCardVH.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/UnknownPodDeviceCardVH.kt index 32edb697..532e66a7 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/UnknownPodDeviceCardVH.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/pods/UnknownPodDeviceCardVH.kt @@ -3,6 +3,7 @@ package eu.darken.capod.main.ui.overview.cards.pods import android.icu.text.RelativeDateTimeFormatter import android.view.ViewGroup import eu.darken.capod.R +import eu.darken.capod.common.lists.binding import eu.darken.capod.databinding.OverviewPodsUnknownItemBinding import eu.darken.capod.pods.core.PodDevice import java.time.Duration @@ -20,10 +21,7 @@ class UnknownPodDeviceCardVH(parent: ViewGroup) : private val lastSeenFormatter = RelativeDateTimeFormatter.getInstance() - override val onBindData: OverviewPodsUnknownItemBinding.( - item: Item, - payloads: List - ) -> Unit = { item, _ -> + override val onBindData = binding(payload = true) { item -> val device = item.device name.text = device.getLabel(context) @@ -33,9 +31,12 @@ class UnknownPodDeviceCardVH(parent: ViewGroup) : RelativeDateTimeFormatter.Direction.LAST, RelativeDateTimeFormatter.RelativeUnit.SECONDS ) + + rawdata.text = device.rawDataHex } data class Item( - override val device: PodDevice + override val device: PodDevice, + override val showDebug: Boolean = false ) : PodDeviceVH.Item } \ 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 f5407618..ec249323 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 @@ -31,9 +31,39 @@ class PodMonitor @Inject constructor( } .onStart { emptyList() } .map { scanResults -> - 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( +// identifier = UUID.fromString("63436171-5433-4506-8bf5-faf6c35ffa8a"), +// scanResult = scanResults.first(), +// proximityMessage = ProximityPairing.Decoder().decode( +// message = ContinuityProtocol.Message( +// type = 0x07.toUByte(), +// length = 25, +// data = arrayOf(1, 10, 32, 98, 4, -128, 1, 15, 64, 13, 112, 80, 22, -14, 64, -125, 22, -65, 16, 22, 52, -101, 116, -124, -24).toByteArray().toUByteArray() +// ) +// )!! +// ) +// val fake2 = BeatsFlex( +// identifier = UUID.fromString("e563098d-ea24-4136-a169-5a59344317c3"), +// scanResult = scanResults.first(), +// proximityMessage = ProximityPairing.Decoder().decode( +// message = ContinuityProtocol.Message( +// type = 0x07.toUByte(), +// length = 25, +// data = arrayOf(1, 16, 32, 10, -12, -113, 0, 1, 0, -60, 113, -97, -100, -17, -94, -29, -70, 102, -2, 29, 69, -97, -55, 47, -96).toByteArray().toUByteArray() +// ) +// )!! +// ) +// pods.plus(fake1).plus(fake2) +// } else { +// pods +// } + + pods } companion object { diff --git a/app/src/main/java/eu/darken/capod/monitor/ui/MonitorNotifications.kt b/app/src/main/java/eu/darken/capod/monitor/ui/MonitorNotifications.kt index a344a429..ee609a2d 100644 --- a/app/src/main/java/eu/darken/capod/monitor/ui/MonitorNotifications.kt +++ b/app/src/main/java/eu/darken/capod/monitor/ui/MonitorNotifications.kt @@ -21,9 +21,9 @@ import eu.darken.capod.common.notifications.PendingIntentCompat import eu.darken.capod.main.ui.MainActivity import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.apple.DualApplePods -import eu.darken.capod.pods.core.getBatteryCase -import eu.darken.capod.pods.core.getBatteryLeftPod -import eu.darken.capod.pods.core.getBatteryRightPod +import eu.darken.capod.pods.core.getBatteryLevelCase +import eu.darken.capod.pods.core.getBatteryLevelLeftPod +import eu.darken.capod.pods.core.getBatteryLevelRightPod import javax.inject.Inject @@ -70,11 +70,11 @@ class MonitorNotifications @Inject constructor( val infoText = when (device) { is DualApplePods -> { val sb = StringBuilder() - sb.append("L: ${device.getBatteryLeftPod(context)}") + sb.append("L: ${device.getBatteryLevelLeftPod(context)}") sb.append(" | ") - sb.append("C: ${device.getBatteryCase(context)}") + sb.append("C: ${device.getBatteryLevelCase(context)}") sb.append(" | ") - sb.append("R: ${device.getBatteryRightPod(context)}") + sb.append("R: ${device.getBatteryLevelRightPod(context)}") } else -> { "Unknown device" diff --git a/app/src/main/java/eu/darken/capod/pods/core/DualPods.kt b/app/src/main/java/eu/darken/capod/pods/core/DualPodDevice.kt similarity index 86% rename from app/src/main/java/eu/darken/capod/pods/core/DualPods.kt rename to app/src/main/java/eu/darken/capod/pods/core/DualPodDevice.kt index 4e3c662f..1d634b0b 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/DualPods.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/DualPodDevice.kt @@ -1,8 +1,6 @@ package eu.darken.capod.pods.core -interface DualPods : PodDevice { - - val microPhonePod: Pod +interface DualPodDevice : PodDevice { enum class Pod { LEFT, diff --git a/app/src/main/java/eu/darken/capod/pods/core/DualPodsExtensions.kt b/app/src/main/java/eu/darken/capod/pods/core/DualPodsExtensions.kt index f952ab59..cb569e13 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/DualPodsExtensions.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/DualPodsExtensions.kt @@ -4,14 +4,14 @@ import android.content.Context import eu.darken.capod.R import kotlin.math.roundToInt -fun DualPods.getBatteryLeftPod(context: Context): String = +fun DualPodDevice.getBatteryLevelLeftPod(context: Context): String = batteryLeftPodPercent?.let { "${(it * 100).roundToInt()}%" } ?: context.getString(R.string.general_value_not_available_label) -fun DualPods.getBatteryRightPod(context: Context): String = +fun DualPodDevice.getBatteryLevelRightPod(context: Context): String = batteryRightPodPercent?.let { "${(it * 100).roundToInt()}%" } ?: context.getString(R.string.general_value_not_available_label) -fun DualPods.getBatteryCase(context: Context): String = +fun DualPodDevice.getBatteryLevelCase(context: Context): String = batteryCasePercent?.let { "${(it * 100).roundToInt()}%" } ?: context.getString(R.string.general_value_not_available_label) diff --git a/app/src/main/java/eu/darken/capod/pods/core/PodDevice.kt b/app/src/main/java/eu/darken/capod/pods/core/PodDevice.kt index 66a02da8..d00118ee 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/PodDevice.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/PodDevice.kt @@ -12,13 +12,18 @@ interface PodDevice { val identifier: UUID - val scanResult: ScanResult - val lastSeenAt: Instant + val scanResult: ScanResult + val rssi: Int get() = scanResult.rssi + val rawData: ByteArray + + val rawDataHex: String + get() = rawData.joinToString(separator = " ") { String.format("%02X", it) } + fun getSignalQuality(context: Context): String { val percentage = (100 - abs(rssi)) return "~$percentage%" diff --git a/app/src/main/java/eu/darken/capod/pods/core/SinglePodDevice.kt b/app/src/main/java/eu/darken/capod/pods/core/SinglePodDevice.kt new file mode 100644 index 00000000..d00a1176 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/pods/core/SinglePodDevice.kt @@ -0,0 +1,6 @@ +package eu.darken.capod.pods.core + +interface SinglePodDevice : PodDevice { + + val batteryHeadsetPercent: Float? +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/SinglePodsExtensions.kt b/app/src/main/java/eu/darken/capod/pods/core/SinglePodsExtensions.kt new file mode 100644 index 00000000..d22e3a2e --- /dev/null +++ b/app/src/main/java/eu/darken/capod/pods/core/SinglePodsExtensions.kt @@ -0,0 +1,9 @@ +package eu.darken.capod.pods.core + +import android.content.Context +import eu.darken.capod.R +import kotlin.math.roundToInt + +fun SinglePodDevice.getBatteryLevelHeadset(context: Context): String = + batteryHeadsetPercent?.let { "${(it * 100).roundToInt()}%" } + ?: context.getString(R.string.general_value_not_available_label) diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/ApplePods.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/ApplePods.kt index b762ffc1..1b61105a 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/ApplePods.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/ApplePods.kt @@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.pods.core.PodDevice +import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol import eu.darken.capod.pods.core.apple.protocol.ProximityPairing interface ApplePods : PodDevice { @@ -10,6 +11,9 @@ interface ApplePods : PodDevice { val proximityMessage: ProximityPairing.Message + override val rawData: ByteArray + get() = scanResult.scanRecord!!.getManufacturerSpecificData(ContinuityProtocol.APPLE_COMPANY_IDENTIFIER)!! + // We start counting at the airpods prefix byte val rawPrefix: UByte get() = proximityMessage.data[0] diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/BasicSingleApplePods.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/BasicSingleApplePods.kt index 699aa98b..305d939d 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/BasicSingleApplePods.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/BasicSingleApplePods.kt @@ -2,10 +2,11 @@ package eu.darken.capod.pods.core.apple import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.lowerNibble +import eu.darken.capod.pods.core.SinglePodDevice -interface BasicSingleApplePods : ApplePods { +interface BasicSingleApplePods : ApplePods, SinglePodDevice { - val batteryHeadsetPercent: Float? + override val batteryHeadsetPercent: Float? get() = when (val value = rawPodsBattery.lowerNibble.toInt()) { 15 -> null else -> if (value > 10) { diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/DualApplePods.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/DualApplePods.kt index 5268f8b5..00f5c239 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/DualApplePods.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/DualApplePods.kt @@ -7,12 +7,12 @@ import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.isBitSet import eu.darken.capod.common.lowerNibble import eu.darken.capod.common.upperNibble -import eu.darken.capod.pods.core.DualPods -import eu.darken.capod.pods.core.DualPods.Pod +import eu.darken.capod.pods.core.DualPodDevice +import eu.darken.capod.pods.core.DualPodDevice.Pod -interface DualApplePods : ApplePods, DualPods { +interface DualApplePods : ApplePods, DualPodDevice { - override val microPhonePod: Pod + val microPhonePod: Pod get() = when (rawStatus.isBitSet(5)) { true -> Pod.LEFT false -> Pod.RIGHT diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsPro.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsPro.kt index 21b6c6d6..03b7694b 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsPro.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsPro.kt @@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple.airpods import android.bluetooth.le.ScanResult import android.content.Context +import eu.darken.capod.R import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.pods.core.apple.DualApplePods import eu.darken.capod.pods.core.apple.protocol.ProximityPairing @@ -19,5 +20,8 @@ data class AirPodsPro constructor( return "AirPods Pro" } + override val iconRes: Int + get() = R.drawable.ic_device_airpods_gen2 + override val tag: String = logTag("Pod", "Apple", "AirPods", "Pro") } \ No newline at end of file diff --git a/app/src/main/res/layout/overview_pods_apple_dual_item.xml b/app/src/main/res/layout/overview_pods_apple_dual_item.xml index 2fae122a..5a298082 100644 --- a/app/src/main/res/layout/overview_pods_apple_dual_item.xml +++ b/app/src/main/res/layout/overview_pods_apple_dual_item.xml @@ -33,31 +33,13 @@ android:layout_marginStart="4dp" android:layout_marginTop="16dp" android:layout_marginEnd="8dp" - android:layout_marginBottom="8dp" android:gravity="center_vertical" - app:layout_constraintBottom_toTopOf="@id/barrier_top" + app:layout_constraintBottom_toTopOf="@+id/last_seen" app:layout_constraintEnd_toStartOf="@id/reception" app:layout_constraintStart_toEndOf="@id/device_icon" app:layout_constraintTop_toTopOf="parent" - app:layout_constraintVertical_bias="0.0" tools:text="Apple AirPods Pro" /> - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/overview_pods_apple_single_item.xml b/app/src/main/res/layout/overview_pods_apple_single_item.xml new file mode 100644 index 00000000..053d9463 --- /dev/null +++ b/app/src/main/res/layout/overview_pods_apple_single_item.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/overview_pods_unknown_item.xml b/app/src/main/res/layout/overview_pods_unknown_item.xml index 41466fe2..20cc4193 100644 --- a/app/src/main/res/layout/overview_pods_unknown_item.xml +++ b/app/src/main/res/layout/overview_pods_unknown_item.xml @@ -18,23 +18,22 @@ style="@style/TextAppearance.MaterialComponents.Body2" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginHorizontal="16dp" - android:layout_marginStart="16dp" android:layout_marginTop="16dp" + android:layout_marginStart="16dp" android:layout_marginEnd="8dp" - android:layout_marginBottom="8dp" + android:gravity="center_vertical" + app:layout_constraintBottom_toTopOf="@+id/last_seen" app:layout_constraintEnd_toStartOf="@id/reception" - app:layout_constraintHorizontal_bias="0.49" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - tools:text="Unknown Device" /> + tools:text="Apple AirPods Max" /> + app:layout_constraintTop_toTopOf="@+id/name" + tools:text="Yours (RSSI -61)" /> + + Call Ringing Hanging up - Status %s + Status: %s Settings @@ -82,4 +82,7 @@ Automatic bug reports Automatically reports issues, e.g. details on an app crash so I can figure out how to fix it. Unknown device + Headphones + Debug mode + Show additional information to troubleshoot issues. \ No newline at end of file diff --git a/app/src/main/res/xml/preferences_general.xml b/app/src/main/res/xml/preferences_general.xml index 63eda712..ca6cea1e 100644 --- a/app/src/main/res/xml/preferences_general.xml +++ b/app/src/main/res/xml/preferences_general.xml @@ -5,10 +5,16 @@ + + \ No newline at end of file diff --git a/app/src/test/java/eu/darken/capod/pods/core/apple/AirPodsFactoryTest.kt b/app/src/test/java/eu/darken/capod/pods/core/apple/AirPodsFactoryTest.kt index 133464b0..cb1e861d 100644 --- a/app/src/test/java/eu/darken/capod/pods/core/apple/AirPodsFactoryTest.kt +++ b/app/src/test/java/eu/darken/capod/pods/core/apple/AirPodsFactoryTest.kt @@ -1,6 +1,6 @@ package eu.darken.capod.pods.core.apple -import eu.darken.capod.pods.core.DualPods +import eu.darken.capod.pods.core.DualPodDevice import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.apple.airpods.AirPodsGen1 import eu.darken.capod.pods.core.apple.airpods.AirPodsPro @@ -16,12 +16,12 @@ class AirPodsFactoryTest : BaseAirPodsTest() { create("07 19 01 0E 20 >2B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { // 00101011 // --^----- - microPhonePod shouldBe DualPods.Pod.LEFT + microPhonePod shouldBe DualPodDevice.Pod.LEFT } create("07 19 01 0E 20 >0B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { // 00001011 // --^----- - microPhonePod shouldBe DualPods.Pod.RIGHT + microPhonePod shouldBe DualPodDevice.Pod.RIGHT } } diff --git a/app/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsProTest.kt b/app/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsProTest.kt index f26ec535..23d48b1f 100644 --- a/app/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsProTest.kt +++ b/app/src/test/java/eu/darken/capod/pods/core/apple/airpods/AirPodsProTest.kt @@ -1,6 +1,6 @@ package eu.darken.capod.pods.core.apple.airpods -import eu.darken.capod.pods.core.DualPods +import eu.darken.capod.pods.core.DualPodDevice import eu.darken.capod.pods.core.apple.BaseAirPodsTest import eu.darken.capod.pods.core.apple.DualApplePods import io.kotest.matchers.shouldBe @@ -22,7 +22,7 @@ class AirPodsProTest : BaseAirPodsTest() { rawDeviceColor shouldBe 0x00.toUByte() rawSuffix shouldBe 0x00.toUByte() - microPhonePod shouldBe DualPods.Pod.RIGHT + microPhonePod shouldBe DualPodDevice.Pod.RIGHT batteryLeftPodPercent shouldBe 1.0f batteryRightPodPercent shouldBe 1.0f @@ -39,7 +39,7 @@ class AirPodsProTest : BaseAirPodsTest() { @Test fun `test AirPods from my downstairs neighbour`() = runBlockingTest { create("07 19 01 0E 20 00 F3 8F 02 00 04 79 C6 3F F9 C3 15 D9 11 A1 3C B1 58 66 B9 8B 67") { - microPhonePod shouldBe DualPods.Pod.RIGHT + microPhonePod shouldBe DualPodDevice.Pod.RIGHT batteryLeftPodPercent shouldBe null batteryRightPodPercent shouldBe 0.3f diff --git a/app/src/test/java/eu/darken/capod/pods/core/apple/beats/PowerBeatsProTest.kt b/app/src/test/java/eu/darken/capod/pods/core/apple/beats/PowerBeatsProTest.kt index 340ca068..27143611 100644 --- a/app/src/test/java/eu/darken/capod/pods/core/apple/beats/PowerBeatsProTest.kt +++ b/app/src/test/java/eu/darken/capod/pods/core/apple/beats/PowerBeatsProTest.kt @@ -1,6 +1,6 @@ package eu.darken.capod.pods.core.apple.beats -import eu.darken.capod.pods.core.DualPods +import eu.darken.capod.pods.core.DualPodDevice import eu.darken.capod.pods.core.apple.BaseAirPodsTest import eu.darken.capod.pods.core.apple.DualApplePods import io.kotest.matchers.shouldBe @@ -22,7 +22,7 @@ class PowerBeatsProTest : BaseAirPodsTest() { rawDeviceColor shouldBe 0x00.toUByte() rawSuffix shouldBe 0x00.toUByte() - microPhonePod shouldBe DualPods.Pod.RIGHT + microPhonePod shouldBe DualPodDevice.Pod.RIGHT batteryLeftPodPercent shouldBe 1.0f batteryRightPodPercent shouldBe 1.0f