diff --git a/app/build.gradle b/app/build.gradle index 5de4a648..397b9648 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -191,6 +191,8 @@ dependencies { kapt "androidx.hilt:hilt-compiler:1.0.0" implementation 'androidx.hilt:hilt-common:1.0.0' + kaptTest "androidx.hilt:hilt-compiler:1.0.0" + testImplementation 'androidx.hilt:hilt-common:1.0.0' // Support libs implementation 'androidx.core:core-ktx:1.7.0' 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 e1b02657..84728592 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 @@ -2,6 +2,8 @@ package eu.darken.capod.pods.core import android.content.Context import androidx.annotation.DrawableRes +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import eu.darken.capod.R import eu.darken.capod.common.bluetooth.BleScanResult import java.time.Instant @@ -12,6 +14,8 @@ interface PodDevice { val identifier: Id + val model: Model + val lastSeenAt: Instant val scanResult: BleScanResult @@ -31,12 +35,60 @@ interface PodDevice { val rawDataHex: String get() = rawData.joinToString(separator = " ") { String.format("%02X", it) } - fun getLabel(context: Context): String + fun getLabel(context: Context): String = model.label @get:DrawableRes val iconRes: Int - get() = R.drawable.ic_device_generic_earbuds + get() = model.iconRes @JvmInline value class Id(private val id: UUID = UUID.randomUUID()) + + @JsonClass(generateAdapter = false) + enum class Model( + val label: String, + @DrawableRes val iconRes: Int = R.drawable.ic_device_generic_earbuds, + ) { + @Json(name = "airpods.gen1") AIRPODS_GEN1( + label = "AirPods (Gen 1)", + iconRes = R.drawable.ic_device_airpods_gen1, + ), + @Json(name = "airpods.gen2") AIRPODS_GEN2( + "AirPods (Gen 2)", + R.drawable.ic_device_airpods_gen2, + ), + @Json(name = "airpods.gen3") AIRPODS_GEN3( + "AirPods (Gen 3)", + R.drawable.ic_device_airpods_gen2, + ), + @Json(name = "airpods.pro") AIRPODS_PRO( + "AirPods Pro", + R.drawable.ic_device_airpods_gen2 + ), + @Json(name = "airpods.max") AIRPODS_MAX( + "AirPods Max", + R.drawable.ic_device_generic_headphones + ), + @Json(name = "beats.flex") BEATS_FLEX( + "Beats Flex" + ), + @Json(name = "beats.solo.3") BEATS_SOLO_3( + "Beats Solo 3" + ), + @Json(name = "beats.studio.3") BEATS_STUDIO_3( + "Beats Studio 3" + ), + @Json(name = "beats.x") BEATS_X( + "Beats X" + ), + @Json(name = "beats.powerbeats.3") POWERBEATS_3( + "Power Beats 3" + ), + @Json(name = "beats.powerbeats.pro") POWERBEATS_PRO( + "Power Beats Pro" + ), + @Json(name = "unknown") UNKNOWN( + "Unknown" + ); + } } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/AppleFactory.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/AppleFactory.kt index b127f15f..1641dfd6 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/AppleFactory.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/AppleFactory.kt @@ -1,13 +1,11 @@ package eu.darken.capod.pods.core.apple -import eu.darken.capod.common.SystemClockWrap import eu.darken.capod.common.bluetooth.BleScanResult import eu.darken.capod.common.debug.Bugs import eu.darken.capod.common.debug.logging.Logging.Priority.* 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.pods.core.HasCase import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.apple.airpods.* import eu.darken.capod.pods.core.apple.beats.* @@ -15,7 +13,6 @@ import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import java.time.Duration import java.util.* import javax.inject.Inject import javax.inject.Singleton @@ -24,38 +21,13 @@ import javax.inject.Singleton class AppleFactory @Inject constructor( private val continuityProtocolDecoder: ContinuityProtocol.Decoder, private val proximityPairingDecoder: ProximityPairing.Decoder, + private val podFactories: @JvmSuppressWildcards Set, + private val unknownAppleFactory: UnknownAppleDevice.Factory, ) { - data class KnownDevice( - val identifier: PodDevice.Id, - val scanResult: BleScanResult, - val message: ProximityPairing.Message, - ) { - val address: String - get() = scanResult.address - - val rssi: Int - get() = scanResult.rssi - - val timestampNanos: Duration - get() = Duration.ofNanos(scanResult.generatedAtNanos) - - fun isOlderThan(age: Duration): Boolean { - val now = Duration.ofNanos(SystemClockWrap.elapsedRealtimeNanos) - return now - timestampNanos > age - } - } - private val lock = Mutex() - private val knownDevs = mutableMapOf() - data class ValueCache( - val caseBatteryPercentage: Float? - ) - - private val cachedValues = mutableMapOf() - - private suspend fun getMessage(scanResult: BleScanResult): ProximityPairing.Message? { + private fun getMessage(scanResult: BleScanResult): ProximityPairing.Message? { val messages = try { continuityProtocolDecoder.decode(scanResult) } catch (e: Exception) { @@ -80,162 +52,29 @@ class AppleFactory @Inject constructor( return proximityMessage } - private suspend fun recognizeDevice(scanResult: BleScanResult, message: ProximityPairing.Message): PodDevice.Id { - val address = scanResult.address - - var identifier: PodDevice.Id? = null - - knownDevs.values.firstOrNull { it.address == address }?.let { - log(TAG, VERBOSE) { "recognizeDevice: Recovered previous ID via address: $it" } - knownDevs[it.identifier] = it.copy( - scanResult = scanResult, - message = message, - ) - identifier = it.identifier - } - - if (identifier == null) { - val currentMarkers = message.getRecogMarkers() - knownDevs.values - .firstOrNull { it.message.getRecogMarkers() == currentMarkers } - ?.let { - log(TAG, DEBUG) { "recognizeDevice: Close match based similarity markers." } - log(TAG, DEBUG) { "recognizeDevice: Old marker: ${it.message.getRecogMarkers()}" } - log(TAG, DEBUG) { "recognizeDevice: New marker: $currentMarkers" } - knownDevs[it.identifier] = it.copy( - scanResult = scanResult, - message = message, - ) - identifier = it.identifier - } - } - - if (identifier == null) { - log(TAG, VERBOSE) { "recognizeDevice: Mapping as new device" } - identifier = PodDevice.Id() - } - - knownDevs[identifier!!] = KnownDevice( - identifier = identifier!!, - scanResult = scanResult, - message = message - ) - - knownDevs.values.toList().forEach { knownDevice -> - if (knownDevice.isOlderThan(Duration.ofSeconds(20))) { - log(TAG, VERBOSE) { "recognizeDevice: Removing stale known device: $knownDevice" } - knownDevs.remove(knownDevice.identifier) - } - } - - return identifier!! - } - suspend fun create(scanResult: BleScanResult): PodDevice? = lock.withLock { val pm = getMessage(scanResult) ?: return@withLock null - val identifier = recognizeDevice(scanResult, pm) - log(TAG, INFO) { "Decoding $scanResult" } - return createSpecificDevice(scanResult, pm, identifier) - } + var factory = podFactories.firstOrNull { it.isResponsible(pm) } - private fun createSpecificDevice( - scanResult: BleScanResult, - pm: ProximityPairing.Message, - identifier: PodDevice.Id - ): ApplePods { - - val cachedCaseBattery = cachedValues[identifier]?.caseBatteryPercentage - - val dm = (((pm.data[1].toInt() and 255) shl 8) or (pm.data[2].toInt() and 255)).toUShort() - val dmDirty = pm.data[1] - - val specificDevice = when { - dm == AirPodsGen1.DEVICE_CODE -> AirPodsGen1( - identifier = identifier, - scanResult = scanResult, - proximityMessage = pm, - cachedBatteryPercentage = cachedCaseBattery, + if (factory == null) { + Bugs.report( + tag = TAG, + message = "Unknown proximity message type", + exception = IllegalArgumentException("Unknown ProximityMessage: $pm") ) - dm == AirPodsGen2.DEVICE_CODE -> AirPodsGen2( - identifier = identifier, - scanResult = scanResult, - proximityMessage = pm, - cachedBatteryPercentage = cachedCaseBattery, - ) - dm == AirPodsGen3.DEVICE_CODE -> AirPodsGen3( - identifier = identifier, - scanResult = scanResult, - proximityMessage = pm, - cachedBatteryPercentage = cachedCaseBattery, - ) - dm == AirPodsPro.DEVICE_CODE -> AirPodsPro( - identifier = identifier, - scanResult = scanResult, - proximityMessage = pm, - cachedBatteryPercentage = cachedCaseBattery, - ) - dmDirty == PowerBeatsPro.DEVICE_CODE_DIRTY -> PowerBeatsPro( - identifier = identifier, - scanResult = scanResult, - proximityMessage = pm, - cachedBatteryPercentage = cachedCaseBattery, - ) - dmDirty == AirPodsMax.DEVICE_CODE_DIRTY -> AirPodsMax( - identifier = identifier, - scanResult = scanResult, - proximityMessage = pm, - ) - dm == PowerBeats3.DEVICE_CODE -> PowerBeats3( - identifier = identifier, - scanResult = scanResult, - proximityMessage = pm - ) - dm == BeatsSolo3.DEVICE_CODE -> BeatsSolo3( - identifier = identifier, - scanResult = scanResult, - proximityMessage = pm - ) - dmDirty == BeatsStudio3.DEVICE_CODE_DIRTY -> BeatsStudio3( - identifier = identifier, - scanResult = scanResult, - proximityMessage = pm - ) - dm == BeatsX.DEVICE_CODE -> BeatsX( - identifier = identifier, - scanResult = scanResult, - proximityMessage = pm - ) - dm == BeatsFlex.DEVICE_CODE -> BeatsFlex( - identifier = identifier, - scanResult = scanResult, - proximityMessage = pm - ) - - else -> { - Bugs.report( - tag = TAG, - message = "Unknown proximity message type", - exception = IllegalArgumentException("Unknown ProximityMessage: $pm") - ) - UnknownAppleDevice( - identifier = identifier, - scanResult = scanResult, - proximityMessage = pm - ) - } + factory = unknownAppleFactory } - cachedValues[identifier] = ValueCache( - caseBatteryPercentage = (specificDevice as? HasCase)?.batteryCasePercent + return factory.create( + scanResult = scanResult, + proximityMessage = pm, ) - - return specificDevice } companion object { - private val TAG = logTag("Pod", "AirPods", "Factory") + private val TAG = logTag("Pod", "Apple", "Factory") } } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/AppleFactoryModule.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/AppleFactoryModule.kt new file mode 100644 index 00000000..d9793d23 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/AppleFactoryModule.kt @@ -0,0 +1,28 @@ +package eu.darken.capod.pods.core.apple + +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntoSet +import eu.darken.capod.pods.core.apple.airpods.* +import eu.darken.capod.pods.core.apple.beats.* + +@InstallIn(SingletonComponent::class) +@Module +abstract class AppleFactoryModule { + + @Binds @IntoSet abstract fun airPodsGen1(factory: AirPodsGen1.Factory): ApplePods.Factory + @Binds @IntoSet abstract fun airPodsGen2(factory: AirPodsGen2.Factory): ApplePods.Factory + @Binds @IntoSet abstract fun airPodsGen3(factory: AirPodsGen3.Factory): ApplePods.Factory + + @Binds @IntoSet abstract fun airPodsPro(factory: AirPodsPro.Factory): ApplePods.Factory + @Binds @IntoSet abstract fun airPodsMax(factory: AirPodsMax.Factory): ApplePods.Factory + + @Binds @IntoSet abstract fun beatsFlex(factory: BeatsFlex.Factory): ApplePods.Factory + @Binds @IntoSet abstract fun beatsSolo3(factory: BeatsSolo3.Factory): ApplePods.Factory + @Binds @IntoSet abstract fun beatsStudio3(factory: BeatsStudio3.Factory): ApplePods.Factory + @Binds @IntoSet abstract fun beatsX(factory: BeatsX.Factory): ApplePods.Factory + @Binds @IntoSet abstract fun powerBeats3(factory: PowerBeats3.Factory): ApplePods.Factory + @Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePods.Factory +} \ No newline at end of file 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 a045aa6d..1e711f8c 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 @@ -1,8 +1,14 @@ package eu.darken.capod.pods.core.apple +import eu.darken.capod.common.SystemClockWrap +import eu.darken.capod.common.bluetooth.BleScanResult +import eu.darken.capod.common.debug.logging.Logging.Priority.DEBUG +import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE +import eu.darken.capod.common.debug.logging.log 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 +import java.time.Duration interface ApplePods : PodDevice { @@ -35,4 +41,104 @@ interface ApplePods : PodDevice { val rawSuffix: UByte get() = proximityMessage.data[8] + + + abstract class Factory(private val tag: String) { + + internal data class ValueCache( + val caseBatteryPercentage: Float? + ) + + internal val cachedValues = mutableMapOf() + + internal data class KnownDevice( + val identifier: PodDevice.Id, + val scanResult: BleScanResult, + val message: ProximityPairing.Message, + ) { + val address: String + get() = scanResult.address + + val rssi: Int + get() = scanResult.rssi + + val timestampNanos: Duration + get() = Duration.ofNanos(scanResult.generatedAtNanos) + + fun isOlderThan(age: Duration): Boolean { + val now = Duration.ofNanos(SystemClockWrap.elapsedRealtimeNanos) + return now - timestampNanos > age + } + } + + private val knownDevs = mutableMapOf() + + internal fun recognizeDevice(scanResult: BleScanResult, message: ProximityPairing.Message): PodDevice.Id { + val address = scanResult.address + + var identifier: PodDevice.Id? = null + + knownDevs.values.firstOrNull { it.address == address }?.let { + log(tag, VERBOSE) { "recognizeDevice: Recovered previous ID via address: $it" } + knownDevs[it.identifier] = it.copy( + scanResult = scanResult, + message = message, + ) + identifier = it.identifier + } + + if (identifier == null) { + val currentMarkers = message.getRecogMarkers() + knownDevs.values + .firstOrNull { it.message.getRecogMarkers() == currentMarkers } + ?.let { + log(tag, DEBUG) { "recognizeDevice: Close match based similarity markers." } + log(tag, DEBUG) { "recognizeDevice: Old marker: ${it.message.getRecogMarkers()}" } + log(tag, DEBUG) { "recognizeDevice: New marker: $currentMarkers" } + knownDevs[it.identifier] = it.copy( + scanResult = scanResult, + message = message, + ) + identifier = it.identifier + } + } + + if (identifier == null) { + log(tag, VERBOSE) { "recognizeDevice: Mapping as new device" } + identifier = PodDevice.Id() + } + + knownDevs[identifier!!] = KnownDevice( + identifier = identifier!!, + scanResult = scanResult, + message = message + ) + + knownDevs.values.toList().forEach { knownDevice -> + if (knownDevice.isOlderThan(Duration.ofSeconds(20))) { + log(tag, VERBOSE) { "recognizeDevice: Removing stale known device: $knownDevice" } + knownDevs.remove(knownDevice.identifier) + } + } + + return identifier!! + } + + data class ModelInfo( + val full: UShort, + val dirty: UByte, + ) + + fun ProximityPairing.Message.getModelInfo(): ModelInfo = ModelInfo( + full = (((data[1].toInt() and 255) shl 8) or (data[2].toInt() and 255)).toUShort(), + dirty = data[1] + ) + + abstract fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean + + abstract fun create( + scanResult: BleScanResult, + proximityMessage: ProximityPairing.Message, + ): ApplePods + } } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/UnknownAppleDevice.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/UnknownAppleDevice.kt index 77ad1e60..8dcfc8fb 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/UnknownAppleDevice.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/UnknownAppleDevice.kt @@ -3,9 +3,11 @@ package eu.darken.capod.pods.core.apple import android.content.Context import eu.darken.capod.R import eu.darken.capod.common.bluetooth.BleScanResult +import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import java.time.Instant +import javax.inject.Inject data class UnknownAppleDevice( override val identifier: PodDevice.Id = PodDevice.Id(), @@ -14,8 +16,28 @@ data class UnknownAppleDevice( override val proximityMessage: ProximityPairing.Message ) : ApplePods { - override fun getLabel(context: Context): String { - return context.getString(R.string.pods_unknown_label) + override val model: PodDevice.Model = PodDevice.Model.UNKNOWN + + override fun getLabel(context: Context): String = context.getString(R.string.pods_unknown_label) + + class Factory @Inject constructor() : ApplePods.Factory(TAG) { + override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean = true + + override fun create( + scanResult: BleScanResult, + proximityMessage: ProximityPairing.Message, + ): ApplePods { + val identifier = recognizeDevice(scanResult, proximityMessage) + + return UnknownAppleDevice( + identifier = identifier, + scanResult = scanResult, + proximityMessage = proximityMessage, + ) + } } + companion object { + private val TAG = logTag("PodDevice", "Apple", "Unknown") + } } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen1.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen1.kt index 424e9f43..4874d806 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen1.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen1.kt @@ -1,12 +1,13 @@ package eu.darken.capod.pods.core.apple.airpods -import android.content.Context -import eu.darken.capod.R import eu.darken.capod.common.bluetooth.BleScanResult +import eu.darken.capod.common.debug.logging.logTag 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.ProximityPairing import java.time.Instant +import javax.inject.Inject data class AirPodsGen1 constructor( override val identifier: PodDevice.Id = PodDevice.Id(), @@ -16,15 +17,36 @@ data class AirPodsGen1 constructor( private val cachedBatteryPercentage: Float? ) : DualApplePods { - override fun getLabel(context: Context): String = "AirPods (Gen 1)" - - override val iconRes: Int - get() = R.drawable.ic_device_airpods_gen1 + override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN1 override val batteryCasePercent: Float? get() = super.batteryCasePercent ?: cachedBatteryPercentage + class Factory @Inject constructor() : ApplePods.Factory(TAG) { + + override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean = + proximityMessage.getModelInfo().full == DEVICE_CODE + + override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods { + val identifier = recognizeDevice(scanResult, proximityMessage) + + val device = AirPodsGen1( + identifier = identifier, + scanResult = scanResult, + proximityMessage = proximityMessage, + cachedBatteryPercentage = cachedValues[identifier]?.caseBatteryPercentage + ) + + cachedValues[identifier] = ValueCache( + caseBatteryPercentage = device.batteryCasePercent + ) + + return device + } + } + companion object { - val DEVICE_CODE = 0x0220.toUShort() + private val DEVICE_CODE = 0x0220.toUShort() + private val TAG = logTag("PodDevice", "Apple", "AirPods", "Gen1") } } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen2.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen2.kt index 87133228..e914aa7a 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen2.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen2.kt @@ -1,12 +1,13 @@ package eu.darken.capod.pods.core.apple.airpods -import android.content.Context -import eu.darken.capod.R import eu.darken.capod.common.bluetooth.BleScanResult +import eu.darken.capod.common.debug.logging.logTag 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.ProximityPairing import java.time.Instant +import javax.inject.Inject data class AirPodsGen2 constructor( override val identifier: PodDevice.Id = PodDevice.Id(), @@ -16,15 +17,36 @@ data class AirPodsGen2 constructor( private val cachedBatteryPercentage: Float?, ) : DualApplePods { - override fun getLabel(context: Context): String = "AirPods (Gen 2)" - - override val iconRes: Int - get() = R.drawable.ic_device_airpods_gen2 + override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN2 override val batteryCasePercent: Float? get() = super.batteryCasePercent ?: cachedBatteryPercentage + class Factory @Inject constructor() : ApplePods.Factory(TAG) { + + override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean = + proximityMessage.getModelInfo().full == DEVICE_CODE + + override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods { + val identifier = recognizeDevice(scanResult, proximityMessage) + + val device = AirPodsGen2( + identifier = identifier, + scanResult = scanResult, + proximityMessage = proximityMessage, + cachedBatteryPercentage = cachedValues[identifier]?.caseBatteryPercentage + ) + + cachedValues[identifier] = ValueCache( + caseBatteryPercentage = device.batteryCasePercent + ) + + return device + } + } + companion object { - val DEVICE_CODE = 0x0F20.toUShort() + private val DEVICE_CODE = 0x0F20.toUShort() + private val TAG = logTag("PodDevice", "Apple", "AirPods", "Gen2") } } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen3.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen3.kt index 70b3233d..73c87ed9 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen3.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsGen3.kt @@ -1,12 +1,13 @@ package eu.darken.capod.pods.core.apple.airpods -import android.content.Context -import eu.darken.capod.R import eu.darken.capod.common.bluetooth.BleScanResult +import eu.darken.capod.common.debug.logging.logTag 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.ProximityPairing import java.time.Instant +import javax.inject.Inject data class AirPodsGen3 constructor( override val identifier: PodDevice.Id = PodDevice.Id(), @@ -15,16 +16,37 @@ data class AirPodsGen3 constructor( override val proximityMessage: ProximityPairing.Message, private val cachedBatteryPercentage: Float?, ) : DualApplePods { - - override fun getLabel(context: Context): String = "AirPods (Gen 3)" - - override val iconRes: Int - get() = R.drawable.ic_device_airpods_gen2 + override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN3 override val batteryCasePercent: Float? get() = super.batteryCasePercent ?: cachedBatteryPercentage + class Factory @Inject constructor() : ApplePods.Factory(TAG) { + + override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean = + proximityMessage.getModelInfo().full == DEVICE_CODE + + override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods { + val identifier = recognizeDevice(scanResult, proximityMessage) + + val device = AirPodsGen3( + identifier = identifier, + scanResult = scanResult, + proximityMessage = proximityMessage, + cachedBatteryPercentage = cachedValues[identifier]?.caseBatteryPercentage + ) + + cachedValues[identifier] = ValueCache( + caseBatteryPercentage = device.batteryCasePercent + ) + + return device + } + + } + companion object { - val DEVICE_CODE = 0x1320.toUShort() + private val DEVICE_CODE = 0x1320.toUShort() + private val TAG = logTag("PodDevice", "Apple", "AirPods", "Gen3") } } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMax.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMax.kt index 7ec66c9e..de88c6fe 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMax.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/airpods/AirPodsMax.kt @@ -1,12 +1,13 @@ package eu.darken.capod.pods.core.apple.airpods -import android.content.Context -import eu.darken.capod.R import eu.darken.capod.common.bluetooth.BleScanResult +import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.pods.core.PodDevice +import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.SingleApplePods import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import java.time.Instant +import javax.inject.Inject data class AirPodsMax( override val identifier: PodDevice.Id = PodDevice.Id(), @@ -15,12 +16,27 @@ data class AirPodsMax( override val proximityMessage: ProximityPairing.Message, ) : SingleApplePods { - override fun getLabel(context: Context): String = "AirPods Max" + override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX - override val iconRes: Int - get() = R.drawable.ic_device_generic_headphones + class Factory @Inject constructor() : ApplePods.Factory(TAG) { + + override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean = + proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY + + override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods { + val identifier = recognizeDevice(scanResult, proximityMessage) + + return AirPodsMax( + identifier = identifier, + scanResult = scanResult, + proximityMessage = proximityMessage, + ) + } + + } companion object { - val DEVICE_CODE_DIRTY = 10.toUByte() + private val DEVICE_CODE_DIRTY = 10.toUByte() + private val TAG = logTag("PodDevice", "Apple", "AirPods", "Max") } } \ No newline at end of file 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 a80bff84..35713722 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 @@ -1,12 +1,13 @@ package eu.darken.capod.pods.core.apple.airpods -import android.content.Context -import eu.darken.capod.R import eu.darken.capod.common.bluetooth.BleScanResult +import eu.darken.capod.common.debug.logging.logTag 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.ProximityPairing import java.time.Instant +import javax.inject.Inject data class AirPodsPro( override val identifier: PodDevice.Id = PodDevice.Id(), @@ -16,17 +17,37 @@ data class AirPodsPro( private val cachedBatteryPercentage: Float?, ) : DualApplePods { - override fun getLabel(context: Context): String { - return "AirPods Pro" - } - - override val iconRes: Int - get() = R.drawable.ic_device_airpods_gen2 + override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO override val batteryCasePercent: Float? get() = super.batteryCasePercent ?: cachedBatteryPercentage + class Factory @Inject constructor() : ApplePods.Factory(TAG) { + + override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean = + proximityMessage.getModelInfo().full == DEVICE_CODE + + override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods { + val identifier = recognizeDevice(scanResult, proximityMessage) + + val device = AirPodsPro( + identifier = identifier, + scanResult = scanResult, + proximityMessage = proximityMessage, + cachedBatteryPercentage = cachedValues[identifier]?.caseBatteryPercentage + ) + + cachedValues[identifier] = ValueCache( + caseBatteryPercentage = device.batteryCasePercent + ) + + return device + } + + } + companion object { - val DEVICE_CODE = 0x0e20.toUShort() + private val DEVICE_CODE = 0x0e20.toUShort() + private val TAG = logTag("PodDevice", "Apple", "AirPods", "Pro") } } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsFlex.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsFlex.kt index 8563e26c..53756911 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsFlex.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsFlex.kt @@ -1,12 +1,13 @@ package eu.darken.capod.pods.core.apple.beats -import android.content.Context -import eu.darken.capod.R import eu.darken.capod.common.bluetooth.BleScanResult +import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.pods.core.PodDevice +import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.BasicSingleApplePods import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import java.time.Instant +import javax.inject.Inject data class BeatsFlex( override val identifier: PodDevice.Id = PodDevice.Id(), @@ -15,12 +16,27 @@ data class BeatsFlex( override val proximityMessage: ProximityPairing.Message ) : BasicSingleApplePods { - override fun getLabel(context: Context): String = "Beats Flex" + override val model: PodDevice.Model = PodDevice.Model.BEATS_FLEX - override val iconRes: Int - get() = R.drawable.ic_device_generic_earbuds + class Factory @Inject constructor() : ApplePods.Factory(TAG) { + + override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean = + proximityMessage.getModelInfo().full == DEVICE_CODE + + override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods { + val identifier = recognizeDevice(scanResult, proximityMessage) + + return BeatsFlex( + identifier = identifier, + scanResult = scanResult, + proximityMessage = proximityMessage, + ) + } + + } companion object { - val DEVICE_CODE = 0x1020.toUShort() + private val DEVICE_CODE = 0x1020.toUShort() + private val TAG = logTag("PodDevice", "Beats", "Flex") } } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsSolo3.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsSolo3.kt index da790de9..29c5b6d8 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsSolo3.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsSolo3.kt @@ -1,12 +1,13 @@ package eu.darken.capod.pods.core.apple.beats -import android.content.Context -import eu.darken.capod.R import eu.darken.capod.common.bluetooth.BleScanResult +import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.pods.core.PodDevice +import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.BasicSingleApplePods import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import java.time.Instant +import javax.inject.Inject data class BeatsSolo3( override val identifier: PodDevice.Id = PodDevice.Id(), @@ -15,12 +16,27 @@ data class BeatsSolo3( override val proximityMessage: ProximityPairing.Message ) : BasicSingleApplePods { - override fun getLabel(context: Context): String = "Beats Solo 3" + override val model: PodDevice.Model = PodDevice.Model.BEATS_SOLO_3 - override val iconRes: Int - get() = R.drawable.ic_device_generic_earbuds + class Factory @Inject constructor() : ApplePods.Factory(TAG) { + + override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean = + proximityMessage.getModelInfo().full == DEVICE_CODE + + override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods { + val identifier = recognizeDevice(scanResult, proximityMessage) + + return BeatsSolo3( + identifier = identifier, + scanResult = scanResult, + proximityMessage = proximityMessage, + ) + } + + } companion object { - val DEVICE_CODE = 0x0620.toUShort() + private val DEVICE_CODE = 0x0620.toUShort() + private val TAG = logTag("PodDevice", "Beats", "Solo", "3") } } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsStudio3.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsStudio3.kt index 523570a6..99e53b83 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsStudio3.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsStudio3.kt @@ -1,12 +1,13 @@ package eu.darken.capod.pods.core.apple.beats -import android.content.Context -import eu.darken.capod.R import eu.darken.capod.common.bluetooth.BleScanResult +import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.pods.core.PodDevice +import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.BasicSingleApplePods import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import java.time.Instant +import javax.inject.Inject data class BeatsStudio3( override val identifier: PodDevice.Id = PodDevice.Id(), @@ -15,12 +16,27 @@ data class BeatsStudio3( override val proximityMessage: ProximityPairing.Message ) : BasicSingleApplePods { - override fun getLabel(context: Context): String = "Beats Studio 3" + override val model: PodDevice.Model = PodDevice.Model.BEATS_STUDIO_3 - override val iconRes: Int - get() = R.drawable.ic_device_generic_earbuds + class Factory @Inject constructor() : ApplePods.Factory(TAG) { + + override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean = + proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY + + override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods { + val identifier = recognizeDevice(scanResult, proximityMessage) + + return BeatsStudio3( + identifier = identifier, + scanResult = scanResult, + proximityMessage = proximityMessage, + ) + } + + } companion object { - val DEVICE_CODE_DIRTY = 9.toUByte() + private val DEVICE_CODE_DIRTY = 9.toUByte() + private val TAG = logTag("PodDevice", "Beats", "Studio", "3") } } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsX.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsX.kt index c19a019d..57c13d3f 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsX.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/BeatsX.kt @@ -1,12 +1,13 @@ package eu.darken.capod.pods.core.apple.beats -import android.content.Context -import eu.darken.capod.R import eu.darken.capod.common.bluetooth.BleScanResult +import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.pods.core.PodDevice +import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.BasicSingleApplePods import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import java.time.Instant +import javax.inject.Inject data class BeatsX( override val identifier: PodDevice.Id = PodDevice.Id(), @@ -15,12 +16,27 @@ data class BeatsX( override val proximityMessage: ProximityPairing.Message ) : BasicSingleApplePods { - override fun getLabel(context: Context): String = "Beats X" + override val model: PodDevice.Model = PodDevice.Model.BEATS_X - override val iconRes: Int - get() = R.drawable.ic_device_generic_earbuds + class Factory @Inject constructor() : ApplePods.Factory(TAG) { + + override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean = + proximityMessage.getModelInfo().full == DEVICE_CODE + + override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods { + val identifier = recognizeDevice(scanResult, proximityMessage) + + return BeatsX( + identifier = identifier, + scanResult = scanResult, + proximityMessage = proximityMessage, + ) + } + + } companion object { - val DEVICE_CODE = 0x0520.toUShort() + private val DEVICE_CODE = 0x0520.toUShort() + private val TAG = logTag("PodDevice", "Beats", "X") } } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeats3.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeats3.kt index 2c818cb9..16787800 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeats3.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeats3.kt @@ -1,12 +1,13 @@ package eu.darken.capod.pods.core.apple.beats -import android.content.Context -import eu.darken.capod.R import eu.darken.capod.common.bluetooth.BleScanResult +import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.pods.core.PodDevice +import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.BasicSingleApplePods import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import java.time.Instant +import javax.inject.Inject data class PowerBeats3( override val identifier: PodDevice.Id = PodDevice.Id(), @@ -14,13 +15,26 @@ data class PowerBeats3( override val scanResult: BleScanResult, override val proximityMessage: ProximityPairing.Message ) : BasicSingleApplePods { + override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_3 - override fun getLabel(context: Context): String = "Power Beats 3" + class Factory @Inject constructor() : ApplePods.Factory(TAG) { - override val iconRes: Int - get() = R.drawable.ic_device_generic_earbuds + override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean = + proximityMessage.getModelInfo().full == DEVICE_CODE + + override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods { + val identifier = recognizeDevice(scanResult, proximityMessage) + + return PowerBeats3( + identifier = identifier, + scanResult = scanResult, + proximityMessage = proximityMessage, + ) + } + } companion object { - val DEVICE_CODE = 0x0320.toUShort() + private val DEVICE_CODE = 0x0320.toUShort() + private val TAG = logTag("PodDevice", "Beats", "PowerBeats", "3") } } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeatsPro.kt b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeatsPro.kt index 6892310b..07fb9ea4 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeatsPro.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/apple/beats/PowerBeatsPro.kt @@ -1,12 +1,13 @@ package eu.darken.capod.pods.core.apple.beats -import android.content.Context -import eu.darken.capod.R import eu.darken.capod.common.bluetooth.BleScanResult +import eu.darken.capod.common.debug.logging.logTag 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.ProximityPairing import java.time.Instant +import javax.inject.Inject data class PowerBeatsPro( override val identifier: PodDevice.Id = PodDevice.Id(), @@ -16,15 +17,37 @@ data class PowerBeatsPro( private val cachedBatteryPercentage: Float?, ) : DualApplePods { - override fun getLabel(context: Context): String = "Power Beats Pro" - - override val iconRes: Int - get() = R.drawable.ic_device_generic_earbuds + override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_PRO override val batteryCasePercent: Float? get() = super.batteryCasePercent ?: cachedBatteryPercentage + class Factory @Inject constructor() : ApplePods.Factory(TAG) { + + override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean = + proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY + + override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods { + val identifier = recognizeDevice(scanResult, proximityMessage) + + val device = PowerBeatsPro( + identifier = identifier, + scanResult = scanResult, + proximityMessage = proximityMessage, + cachedBatteryPercentage = cachedValues[identifier]?.caseBatteryPercentage + ) + + cachedValues[identifier] = ValueCache( + caseBatteryPercentage = device.batteryCasePercent + ) + + return device + } + + } + companion object { - val DEVICE_CODE_DIRTY = 11.toUByte() + private val DEVICE_CODE_DIRTY = 11.toUByte() + private val TAG = logTag("PodDevice", "Beats", "PowerBeats", "Pro") } } \ No newline at end of file diff --git a/app/src/test/java/eu/darken/capod/pods/core/apple/BaseAirPodsTest.kt b/app/src/test/java/eu/darken/capod/pods/core/apple/BaseAirPodsTest.kt index bc309026..4461486a 100644 --- a/app/src/test/java/eu/darken/capod/pods/core/apple/BaseAirPodsTest.kt +++ b/app/src/test/java/eu/darken/capod/pods/core/apple/BaseAirPodsTest.kt @@ -1,17 +1,29 @@ package eu.darken.capod.pods.core.apple +import dagger.Component import eu.darken.capod.common.SystemClockWrap import eu.darken.capod.common.bluetooth.BleScanResult 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 import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.mockkObject import org.junit.jupiter.api.BeforeEach import testhelper.BaseTest +import javax.inject.Singleton abstract class BaseAirPodsTest : BaseTest() { + @Singleton + @Component(modules = [AppleFactoryModule::class]) + interface AppleFactoryTestComponent { + + val appleFactory: AppleFactory + + @Component.Factory + interface Factory { + fun create(): AppleFactoryTestComponent + } + } private val baseBleScanResult = BleScanResult( address = "77:49:4C:D8:25:0C", @@ -20,10 +32,7 @@ abstract class BaseAirPodsTest : BaseTest() { manufacturerSpecificData = emptyMap() ) - val factory = AppleFactory( - proximityPairingDecoder = ProximityPairing.Decoder(), - continuityProtocolDecoder = ContinuityProtocol.Decoder(), - ) + val factory: AppleFactory = DaggerBaseAirPodsTest_AppleFactoryTestComponent.factory().create().appleFactory @BeforeEach fun setup() {