Compare commits

...
9 Commits
85 changed files with 1201 additions and 947 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
.idea .idea
.gradle .gradle
build/ build/
/fastlane/report.xml
+3
View File
@@ -0,0 +1,3 @@
source "https://rubygems.org"
gem "fastlane"
+218
View File
@@ -0,0 +1,218 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.5)
rexml
addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
artifactory (3.0.15)
atomos (0.1.3)
aws-eventstream (1.2.0)
aws-partitions (1.554.0)
aws-sdk-core (3.126.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
jmespath (~> 1.0)
aws-sdk-kms (1.54.0)
aws-sdk-core (~> 3, >= 3.126.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.112.0)
aws-sdk-core (~> 3, >= 3.126.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
aws-sigv4 (1.4.0)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
declarative (0.0.20)
digest-crc (0.6.4)
rake (>= 12.0.0, < 14.0.0)
domain_name (0.5.20190701)
unf (>= 0.0.5, < 1.0.0)
dotenv (2.7.6)
emoji_regex (3.2.3)
excon (0.91.0)
faraday (1.9.3)
faraday-em_http (~> 1.0)
faraday-em_synchrony (~> 1.0)
faraday-excon (~> 1.1)
faraday-httpclient (~> 1.0)
faraday-multipart (~> 1.0)
faraday-net_http (~> 1.0)
faraday-net_http_persistent (~> 1.0)
faraday-patron (~> 1.0)
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
faraday-cookie_jar (0.0.7)
faraday (>= 0.8.0)
http-cookie (~> 1.0.0)
faraday-em_http (1.0.0)
faraday-em_synchrony (1.0.0)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
faraday-multipart (1.0.3)
multipart-post (>= 1.2, < 3)
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
faraday-rack (1.0.0)
faraday-retry (1.0.3)
faraday_middleware (1.2.0)
faraday (~> 1.0)
fastimage (2.2.6)
fastlane (2.204.3)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
bundler (>= 1.12.0, < 3.0.0)
colored
commander (~> 4.6)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
faraday (~> 1.0)
faraday-cookie_jar (~> 0.0.6)
faraday_middleware (~> 1.0)
fastimage (>= 2.1.0, < 3.0.0)
gh_inspector (>= 1.1.2, < 2.0.0)
google-apis-androidpublisher_v3 (~> 0.3)
google-apis-playcustomapp_v1 (~> 0.1)
google-cloud-storage (~> 1.31)
highline (~> 2.0)
json (< 3.0.0)
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (~> 2.0.0)
naturally (~> 2.2)
optparse (~> 0.1.1)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
security (= 0.1.3)
simctl (~> 1.6.3)
terminal-notifier (>= 2.0.0, < 3.0.0)
terminal-table (>= 1.4.5, < 2.0.0)
tty-screen (>= 0.6.3, < 1.0.0)
tty-spinner (>= 0.8.0, < 1.0.0)
word_wrap (~> 1.0.0)
xcodeproj (>= 1.13.0, < 2.0.0)
xcpretty (~> 0.3.0)
xcpretty-travis-formatter (>= 0.0.3)
gh_inspector (1.1.3)
google-apis-androidpublisher_v3 (0.16.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-core (0.4.2)
addressable (~> 2.5, >= 2.5.1)
googleauth (>= 0.16.2, < 2.a)
httpclient (>= 2.8.1, < 3.a)
mini_mime (~> 1.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.a)
rexml
webrick
google-apis-iamcredentials_v1 (0.10.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-playcustomapp_v1 (0.7.0)
google-apis-core (>= 0.4, < 2.a)
google-apis-storage_v1 (0.11.0)
google-apis-core (>= 0.4, < 2.a)
google-cloud-core (1.6.0)
google-cloud-env (~> 1.0)
google-cloud-errors (~> 1.0)
google-cloud-env (1.5.0)
faraday (>= 0.17.3, < 2.0)
google-cloud-errors (1.2.0)
google-cloud-storage (1.36.1)
addressable (~> 2.8)
digest-crc (~> 0.4)
google-apis-iamcredentials_v1 (~> 0.1)
google-apis-storage_v1 (~> 0.1)
google-cloud-core (~> 1.6)
googleauth (>= 0.16.2, < 2.a)
mini_mime (~> 1.0)
googleauth (1.1.0)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.4, < 3.0)
memoist (~> 0.16)
multi_json (~> 1.11)
os (>= 0.9, < 2.0)
signet (>= 0.16, < 2.a)
highline (2.0.3)
http-cookie (1.0.4)
domain_name (~> 0.5)
httpclient (2.8.3)
jmespath (1.5.0)
json (2.6.1)
jwt (2.3.0)
memoist (0.16.2)
mini_magick (4.11.0)
mini_mime (1.1.2)
multi_json (1.15.0)
multipart-post (2.0.0)
nanaimo (0.3.0)
naturally (2.2.1)
optparse (0.1.1)
os (1.1.4)
plist (3.6.0)
public_suffix (4.0.6)
rake (13.0.6)
representable (3.1.1)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
rexml (3.2.5)
rouge (2.0.7)
ruby2_keywords (0.0.5)
rubyzip (2.3.2)
security (0.1.3)
signet (0.16.0)
addressable (~> 2.8)
faraday (>= 0.17.3, < 2.0)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.8)
CFPropertyList
naturally
terminal-notifier (2.0.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
trailblazer-option (0.1.2)
tty-cursor (0.7.1)
tty-screen (0.8.1)
tty-spinner (0.9.3)
tty-cursor (~> 0.7)
uber (0.1.0)
unf (0.1.4)
unf_ext
unf_ext (0.0.8)
unicode-display_width (1.8.0)
webrick (1.7.0)
word_wrap (1.0.0)
xcodeproj (1.21.0)
CFPropertyList (>= 2.3.3, < 4.0)
atomos (~> 0.1.3)
claide (>= 1.0.2, < 2.0)
colored2 (~> 3.1)
nanaimo (~> 0.3.0)
rexml (~> 3.2.4)
xcpretty (0.3.0)
rouge (~> 2.0.7)
xcpretty-travis-formatter (1.0.1)
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
x86_64-linux
DEPENDENCIES
fastlane
BUNDLED WITH
2.2.8
@@ -14,9 +14,8 @@ 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.BluetoothDisabledVH
import eu.darken.capod.main.ui.overview.cards.MissingMainDeviceVH import eu.darken.capod.main.ui.overview.cards.MissingMainDeviceVH
import eu.darken.capod.main.ui.overview.cards.PermissionCardVH 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.DualPodsCardVH
import eu.darken.capod.main.ui.overview.cards.pods.DualApplePodsCardVH import eu.darken.capod.main.ui.overview.cards.pods.SinglePodsCardVH
import eu.darken.capod.main.ui.overview.cards.pods.SingleApplePodsCardVH
import eu.darken.capod.main.ui.overview.cards.pods.UnknownPodDeviceCardVH import eu.darken.capod.main.ui.overview.cards.pods.UnknownPodDeviceCardVH
import javax.inject.Inject import javax.inject.Inject
@@ -29,9 +28,8 @@ class OverviewAdapter @Inject constructor() :
init { init {
modules.add(DataBinderMod(data)) modules.add(DataBinderMod(data))
modules.add(TypedVHCreatorMod({ data[it] is PermissionCardVH.Item }) { PermissionCardVH(it) }) 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 DualPodsCardVH.Item }) { DualPodsCardVH(it) })
modules.add(TypedVHCreatorMod({ data[it] is SingleApplePodsCardVH.Item }) { SingleApplePodsCardVH(it) }) modules.add(TypedVHCreatorMod({ data[it] is SinglePodsCardVH.Item }) { SinglePodsCardVH(it) })
modules.add(TypedVHCreatorMod({ data[it] is BasicSingleApplePodsCardVH.Item }) { BasicSingleApplePodsCardVH(it) })
modules.add(TypedVHCreatorMod({ data[it] is MissingMainDeviceVH.Item }) { MissingMainDeviceVH(it) }) modules.add(TypedVHCreatorMod({ data[it] is MissingMainDeviceVH.Item }) { MissingMainDeviceVH(it) })
modules.add(TypedVHCreatorMod({ data[it] is BluetoothDisabledVH.Item }) { BluetoothDisabledVH(it) }) modules.add(TypedVHCreatorMod({ data[it] is BluetoothDisabledVH.Item }) { BluetoothDisabledVH(it) })
modules.add(TypedVHCreatorMod({ data[it] is UnknownPodDeviceCardVH.Item }) { UnknownPodDeviceCardVH(it) }) modules.add(TypedVHCreatorMod({ data[it] is UnknownPodDeviceCardVH.Item }) { UnknownPodDeviceCardVH(it) })
@@ -20,23 +20,23 @@ import eu.darken.capod.main.core.PermissionTool
import eu.darken.capod.main.ui.overview.cards.BluetoothDisabledVH 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.MissingMainDeviceVH
import eu.darken.capod.main.ui.overview.cards.PermissionCardVH import eu.darken.capod.main.ui.overview.cards.PermissionCardVH
import eu.darken.capod.main.ui.overview.cards.pods.* 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
import eu.darken.capod.monitor.core.PodMonitor import eu.darken.capod.monitor.core.PodMonitor
import eu.darken.capod.monitor.core.worker.MonitorControl import eu.darken.capod.monitor.core.worker.MonitorControl
import eu.darken.capod.pods.core.DualPodDevice
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.BasicSingleApplePods import eu.darken.capod.pods.core.SinglePodDevice
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.SingleApplePods
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import java.time.Instant import java.time.Instant
import java.util.*
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
class OverviewFragmentVM @Inject constructor( class OverviewFragmentVM @Inject constructor(
handle: SavedStateHandle, @Suppress("UNUSED_PARAMETER") handle: SavedStateHandle,
dispatcherProvider: DispatcherProvider, dispatcherProvider: DispatcherProvider,
private val monitorControl: MonitorControl, private val monitorControl: MonitorControl,
private val podMonitor: PodMonitor, private val podMonitor: PodMonitor,
@@ -74,7 +74,7 @@ class OverviewFragmentVM @Inject constructor(
monitorControl.startMonitor() monitorControl.startMonitor()
} }
} }
.map { Unit } .map { }
.asLiveData2() .asLiveData2()
val requestPermissionEvent = SingleLiveEvent<Permission>() val requestPermissionEvent = SingleLiveEvent<Permission>()
@@ -129,19 +129,13 @@ class OverviewFragmentVM @Inject constructor(
pods.map { pods.map {
val now = Instant.now() val now = Instant.now()
when (it) { when (it) {
is DualApplePods -> DualApplePodsCardVH.Item( is DualPodDevice -> DualPodsCardVH.Item(
now = now, now = now,
device = it, device = it,
showDebug = isDebugMode, showDebug = isDebugMode,
isMainPod = it == mainPod, isMainPod = it == mainPod,
) )
is SingleApplePods -> SingleApplePodsCardVH.Item( is SinglePodDevice -> SinglePodsCardVH.Item(
now = now,
device = it,
showDebug = isDebugMode,
isMainPod = it == mainPod,
)
is BasicSingleApplePods -> BasicSingleApplePodsCardVH.Item(
now = now, now = now,
device = it, device = it,
showDebug = isDebugMode, showDebug = isDebugMode,
@@ -1,58 +0,0 @@
package eu.darken.capod.main.ui.overview.cards.pods
import android.graphics.Typeface
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.getBatteryDrawable
import eu.darken.capod.pods.core.getBatteryLevelHeadset
import eu.darken.capod.pods.core.lastSeenFormatted
import java.time.Instant
class BasicSingleApplePodsCardVH(parent: ViewGroup) :
PodDeviceVH<BasicSingleApplePodsCardVH.Item, OverviewPodsAppleSingleBasicItemBinding>(
R.layout.overview_pods_apple_single_basic_item,
parent
) {
override val viewBinding = lazy { OverviewPodsAppleSingleBasicItemBinding.bind(itemView) }
override val onBindData = binding(payload = true) { item: Item ->
val device = item.device
name.apply {
text = device.getLabel(context)
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
else setTypeface(typeface, Typeface.NORMAL)
}
deviceIcon.setImageResource(device.iconRes)
lastSeen.text = device.lastSeenFormatted(item.now)
reception.text = item.getReceptionText()
batteryLabel.text = device.getBatteryLevelHeadset(context)
batteryIcon.setImageResource(getBatteryDrawable(device.batteryHeadsetPercent))
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 now: Instant,
override val device: BasicSingleApplePods,
override val showDebug: Boolean,
override val isMainPod: Boolean,
) : PodDeviceVH.Item
}
@@ -1,112 +0,0 @@
package eu.darken.capod.main.ui.overview.cards.pods
import android.graphics.Typeface
import android.view.ViewGroup
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import eu.darken.capod.R
import eu.darken.capod.common.lists.binding
import eu.darken.capod.databinding.OverviewPodsAppleDualItemBinding
import eu.darken.capod.pods.core.*
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 java.time.Duration
import java.time.Instant
class DualApplePodsCardVH(parent: ViewGroup) :
PodDeviceVH<DualApplePodsCardVH.Item, OverviewPodsAppleDualItemBinding>(
R.layout.overview_pods_apple_dual_item,
parent
) {
override val viewBinding = lazy { OverviewPodsAppleDualItemBinding.bind(itemView) }
override val onBindData = binding(payload = true) { item: Item ->
val device = item.device
name.apply {
val sb = StringBuilder(device.getLabel(context))
if (!listOf(DeviceColor.WHITE, DeviceColor.UNKNOWN).contains(device.deviceColor)) {
sb.append(" (${device.deviceColor.name})")
}
text = sb
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
else setTypeface(typeface, Typeface.NORMAL)
if (item.showDebug) {
append(" [${device.primaryPod.name}]")
}
}
deviceIcon.setImageResource(device.iconRes)
lastSeen.text = context.getString(R.string.last_seen_x, device.lastSeenFormatted(item.now))
firstSeen.text = context.getString(R.string.first_seen_x, device.firstSeenFormatted(item.now))
firstSeen.isGone = Duration.between(device.seenFirstAt, device.seenLastAt).toMinutes() < 1
reception.text = item.getReceptionText()
// Left Pod
device.apply {
podLeftBatteryIcon.setImageResource(getBatteryDrawable(batteryLeftPodPercent))
podLeftBatteryLabel.text = getBatteryLevelLeftPod(context)
podLeftChargingIcon.isInvisible = !isLeftPodCharging
podLeftChargingLabel.isInvisible = !isLeftPodCharging
podLeftMicrophoneIcon.isInvisible = !isLeftPodMicrophone
podLeftMicrophoneLabel.isInvisible = !isLeftPodMicrophone
podLeftWearIcon.isInvisible = !isLeftPodInEar
podLeftWearLabel.isInvisible = !isLeftPodInEar
}
// Right Pod
device.apply {
podRightBatteryIcon.setImageResource(getBatteryDrawable(batteryRightPodPercent))
podRightBatteryLabel.text = getBatteryLevelRightPod(context)
podRightChargingIcon.isInvisible = !isRightPodCharging
podRightChargingLabel.isInvisible = !isRightPodCharging
podRightMicrophoneIcon.isInvisible = !isRightPodMicrophone
podRightMicrophoneLabel.isInvisible = !isRightPodMicrophone
podRightWearIcon.isInvisible = !isRightPodInEar
podRightWearLabel.isInvisible = !isRightPodInEar
}
// Case
device.apply {
podCaseBatteryIcon.setImageResource(getBatteryDrawable(batteryCasePercent))
podCaseBatteryLabel.text = getBatteryLevelCase(context)
podCaseChargingIcon.isInvisible = !isCaseCharging
podCaseChargingLabel.isInvisible = !isCaseCharging
podCaseLidLabel.text = when (caseLidState) {
LidState.OPEN -> context.getString(R.string.pods_case_status_open_label)
LidState.CLOSED -> context.getString(R.string.pods_case_status_closed_label)
else -> context.getString(R.string.pods_case_unknown_state)
}
val hideInfo = !listOf(LidState.OPEN, LidState.CLOSED).contains(caseLidState)
podCaseLidIcon.isInvisible = hideInfo
podCaseLidLabel.isInvisible = hideInfo
}
status.apply {
val sb = StringBuilder(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 now: Instant,
override val device: DualApplePods,
override val showDebug: Boolean,
override val isMainPod: Boolean,
) : PodDeviceVH.Item
}
@@ -0,0 +1,166 @@
package eu.darken.capod.main.ui.overview.cards.pods
import android.graphics.Typeface
import android.view.ViewGroup
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import eu.darken.capod.R
import eu.darken.capod.common.lists.binding
import eu.darken.capod.databinding.OverviewPodsDualItemBinding
import eu.darken.capod.pods.core.*
import eu.darken.capod.pods.core.apple.DualAirPods
import eu.darken.capod.pods.core.apple.DualAirPods.LidState
import java.time.Duration
import java.time.Instant
class DualPodsCardVH(parent: ViewGroup) :
PodDeviceVH<DualPodsCardVH.Item, OverviewPodsDualItemBinding>(
R.layout.overview_pods_dual_item,
parent
) {
override val viewBinding = lazy { OverviewPodsDualItemBinding.bind(itemView) }
override val onBindData = binding(payload = true) { item: Item ->
val device = item.device
name.apply {
val sb = StringBuilder(device.getLabel(context))
if (device is HasPodStyle && item.showDebug) {
val style = device.podStyle
sb.append(" (${style.getColor(context)})")
}
text = sb
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
else setTypeface(typeface, Typeface.NORMAL)
if (device is DualAirPods && item.showDebug) {
append(" [${device.primaryPod.name}]")
}
}
deviceIcon.setImageResource(device.iconRes)
lastSeen.text = context.getString(R.string.last_seen_x, device.lastSeenFormatted(item.now))
firstSeen.text = context.getString(R.string.first_seen_x, device.firstSeenFormatted(item.now))
firstSeen.isGone = Duration.between(device.seenFirstAt, device.seenLastAt).toMinutes() < 1
reception.text = item.getReceptionText()
// Pods battery state
device.apply {
podLeftBatteryIcon.setImageResource(getBatteryDrawable(batteryLeftPodPercent))
podLeftBatteryLabel.text = getBatteryLevelLeftPod(context)
podRightBatteryIcon.setImageResource(getBatteryDrawable(batteryRightPodPercent))
podRightBatteryLabel.text = getBatteryLevelRightPod(context)
}
// Pods charging state
device.apply {
if (this is HasChargeDetectionDual) {
podLeftChargingIcon.isInvisible = !isLeftPodCharging
podLeftChargingLabel.isInvisible = !isLeftPodCharging
podRightChargingIcon.isInvisible = !isRightPodCharging
podRightChargingLabel.isInvisible = !isRightPodCharging
} else {
podLeftChargingIcon.isGone = true
podLeftChargingLabel.isGone = true
podRightChargingIcon.isGone = true
podRightChargingLabel.isGone = true
}
}
// Microphone state
device.apply {
if (this is HasDualMicrophone) {
podLeftMicrophoneIcon.isInvisible = !isLeftPodMicrophone
podLeftMicrophoneLabel.isInvisible = !isLeftPodMicrophone
podRightMicrophoneIcon.isInvisible = !isRightPodMicrophone
podRightMicrophoneLabel.isInvisible = !isRightPodMicrophone
} else {
podLeftMicrophoneIcon.isGone = true
podLeftMicrophoneLabel.isGone = true
podRightMicrophoneIcon.isGone = true
podRightMicrophoneLabel.isGone = true
}
}
// Pods wear state
device.apply {
if (this is HasEarDetectionDual) {
podLeftWearIcon.isInvisible = !isLeftPodInEar
podLeftWearLabel.isInvisible = !isLeftPodInEar
podRightWearIcon.isInvisible = !isRightPodInEar
podRightWearLabel.isInvisible = !isRightPodInEar
} else {
podLeftWearIcon.isGone = true
podLeftWearLabel.isGone = true
podRightWearIcon.isGone = true
podRightWearLabel.isGone = true
}
}
// Case charge state
device.apply {
if (this is HasCase) {
podCaseBatteryIcon.isGone = false
podCaseBatteryIcon.setImageResource(getBatteryDrawable(batteryCasePercent))
podCaseBatteryLabel.text = getBatteryLevelCase(context)
podCaseChargingIcon.isInvisible = !isCaseCharging
podCaseChargingLabel.isInvisible = !isCaseCharging
} else {
podCaseBatteryIcon.isGone = true
podCaseBatteryLabel.isGone = true
podCaseChargingIcon.isGone = true
podCaseChargingLabel.isGone = true
}
}
// Case lid state
device.apply {
if (this is DualAirPods) {
podCaseLidLabel.text = when (caseLidState) {
LidState.OPEN -> context.getString(R.string.pods_case_status_open_label)
LidState.CLOSED -> context.getString(R.string.pods_case_status_closed_label)
else -> context.getString(R.string.pods_case_unknown_state)
}
val hideInfo = !listOf(LidState.OPEN, LidState.CLOSED).contains(caseLidState)
podCaseLidIcon.isInvisible = hideInfo
podCaseLidLabel.isInvisible = hideInfo
} else {
podCaseLidIcon.isGone = true
podCaseLidLabel.isGone = true
}
}
// Connection state
device.apply {
val sb = StringBuilder()
if (this is HasStateDetection) {
sb.append(state.getLabel(context))
}
if (item.showDebug) {
sb.append("\n\n").append("---Debug---")
sb.append("\n").append(rawDataHex)
}
status.text = sb
status.isGone = sb.isEmpty()
}
}
data class Item(
override val now: Instant,
override val device: DualPodDevice,
override val showDebug: Boolean,
override val isMainPod: Boolean,
) : PodDeviceVH.Item
}
@@ -1,65 +0,0 @@
package eu.darken.capod.main.ui.overview.cards.pods
import android.graphics.Typeface
import android.view.ViewGroup
import androidx.core.view.isGone
import androidx.core.view.isInvisible
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.getBatteryDrawable
import eu.darken.capod.pods.core.getBatteryLevelHeadset
import eu.darken.capod.pods.core.lastSeenFormatted
import java.time.Instant
class SingleApplePodsCardVH(parent: ViewGroup) :
PodDeviceVH<SingleApplePodsCardVH.Item, OverviewPodsAppleSingleItemBinding>(
R.layout.overview_pods_apple_single_item,
parent
) {
override val viewBinding = lazy { OverviewPodsAppleSingleItemBinding.bind(itemView) }
override val onBindData = binding(payload = true) { item: Item ->
val device = item.device
name.apply {
text = device.getLabel(context)
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
else setTypeface(typeface, Typeface.NORMAL)
}
deviceIcon.setImageResource(device.iconRes)
lastSeen.text = device.lastSeenFormatted(item.now)
reception.text = item.getReceptionText()
batteryLabel.text = device.getBatteryLevelHeadset(context)
batteryIcon.setImageResource(getBatteryDrawable(device.batteryHeadsetPercent))
chargingIcon.isInvisible = device.isHeadsetBeingCharged
chargingLabel.isInvisible = device.isHeadsetBeingCharged
wearIcon.isInvisible = device.isHeadphonesBeingWorn
wearLabel.isInvisible = device.isHeadphonesBeingWorn
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 now: Instant,
override val device: SingleApplePods,
override val showDebug: Boolean,
override val isMainPod: Boolean,
) : PodDeviceVH.Item
}
@@ -0,0 +1,81 @@
package eu.darken.capod.main.ui.overview.cards.pods
import android.graphics.Typeface
import android.view.ViewGroup
import androidx.core.view.isGone
import androidx.core.view.isInvisible
import eu.darken.capod.R
import eu.darken.capod.common.lists.binding
import eu.darken.capod.databinding.OverviewPodsSingleItemBinding
import eu.darken.capod.pods.core.*
import java.time.Instant
class SinglePodsCardVH(parent: ViewGroup) :
PodDeviceVH<SinglePodsCardVH.Item, OverviewPodsSingleItemBinding>(
R.layout.overview_pods_single_item,
parent
) {
override val viewBinding = lazy { OverviewPodsSingleItemBinding.bind(itemView) }
override val onBindData = binding(payload = true) { item: Item ->
val device = item.device
name.apply {
text = device.getLabel(context)
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
else setTypeface(typeface, Typeface.NORMAL)
}
deviceIcon.setImageResource(device.iconRes)
lastSeen.text = device.lastSeenFormatted(item.now)
reception.text = item.getReceptionText()
// Battery level
device.apply {
batteryLabel.text = getBatteryLevelHeadset(context)
batteryIcon.setImageResource(getBatteryDrawable(batteryHeadsetPercent))
}
// Charge state
device.apply {
if (this is HasChargeDetection) {
chargingIcon.isInvisible = isHeadsetBeingCharged
chargingLabel.isInvisible = isHeadsetBeingCharged
} else {
chargingIcon.isGone = true
chargingLabel.isGone = true
}
}
// Has ear detection
device.apply {
if (this is HasEarDetection) {
wearIcon.isInvisible = isBeingWorn
wearLabel.isInvisible = isBeingWorn
} else {
wearIcon.isGone = true
wearLabel.isGone = true
}
}
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 now: Instant,
override val device: SinglePodDevice,
override val showDebug: Boolean,
override val isMainPod: Boolean,
) : PodDeviceVH.Item
}
@@ -6,8 +6,7 @@ import android.widget.RemoteViews
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.R import eu.darken.capod.R
import eu.darken.capod.pods.core.* import eu.darken.capod.pods.core.*
import eu.darken.capod.pods.core.apple.BasicSingleApplePods import eu.darken.capod.pods.core.apple.DualAirPods
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.SingleApplePods import eu.darken.capod.pods.core.apple.SingleApplePods
import javax.inject.Inject import javax.inject.Inject
@@ -17,13 +16,12 @@ class MonitorNotificationViewFactory @Inject constructor(
) { ) {
fun createContentView(device: PodDevice): RemoteViews = when (device) { fun createContentView(device: PodDevice): RemoteViews = when (device) {
is DualApplePods -> createDualApplePods(device) is DualAirPods -> createDualApplePods(device)
is SingleApplePods -> createSingleApplePods(device) is SingleApplePods -> createSingleApplePods(device)
is BasicSingleApplePods -> createSingleBasicApplePods(device)
else -> createUnknownDevice(device) else -> createUnknownDevice(device)
} }
private fun createDualApplePods(device: DualApplePods): RemoteViews = RemoteViews( private fun createDualApplePods(device: DualAirPods): RemoteViews = RemoteViews(
context.packageName, context.packageName,
R.layout.monitor_notification_dual_pods_small R.layout.monitor_notification_dual_pods_small
).apply { ).apply {
@@ -52,19 +50,12 @@ class MonitorNotificationViewFactory @Inject constructor(
setTextViewText(R.id.headphones_label, getLabel(context)) setTextViewText(R.id.headphones_label, getLabel(context))
setImageViewResource(R.id.headphones_battery_icon, getBatteryDrawable(batteryHeadsetPercent)) setImageViewResource(R.id.headphones_battery_icon, getBatteryDrawable(batteryHeadsetPercent))
setTextViewText(R.id.headphones_battery_label, getBatteryLevelHeadset(context)) setTextViewText(R.id.headphones_battery_label, getBatteryLevelHeadset(context))
setViewVisibility(R.id.headphones_charging, if (isHeadsetBeingCharged) View.VISIBLE else View.GONE) if (this is HasEarDetection) {
setViewVisibility(R.id.headphones_worn, if (isHeadphonesBeingWorn) View.VISIBLE else View.GONE) setViewVisibility(R.id.headphones_worn, if (isBeingWorn) View.VISIBLE else View.GONE)
} }
} if (this is HasChargeDetectionDual) {
setViewVisibility(R.id.headphones_charging, if (isHeadsetBeingCharged) View.VISIBLE else View.GONE)
private fun createSingleBasicApplePods(device: BasicSingleApplePods): RemoteViews = RemoteViews( }
context.packageName,
R.layout.monitor_notification_single_pods_basic_small
).apply {
device.apply {
setTextViewText(R.id.headphones_label, getLabel(context))
setImageViewResource(R.id.headphones_battery_icon, getBatteryDrawable(batteryHeadsetPercent))
setTextViewText(R.id.headphones_battery_label, getBatteryLevelHeadset(context))
} }
} }
@@ -1,6 +1,6 @@
package eu.darken.capod.pods.core package eu.darken.capod.pods.core
interface HasDualPods { interface DualPodDevice : PodDevice {
enum class Pod { enum class Pod {
LEFT, LEFT,
@@ -0,0 +1,7 @@
package eu.darken.capod.pods.core
interface HasChargeDetection {
val isHeadsetBeingCharged: Boolean
}
@@ -0,0 +1,14 @@
package eu.darken.capod.pods.core
interface HasChargeDetectionDual : HasChargeDetection {
val isLeftPodCharging: Boolean
val isRightPodCharging: Boolean
val isEitherPodCharging: Boolean
get() = isLeftPodCharging || isRightPodCharging
override val isHeadsetBeingCharged: Boolean
get() = isEitherPodCharging
}
@@ -0,0 +1,9 @@
package eu.darken.capod.pods.core
interface HasDualMicrophone {
val isLeftPodMicrophone: Boolean
val isRightPodMicrophone: Boolean
}
@@ -1,6 +1,6 @@
package eu.darken.capod.pods.core package eu.darken.capod.pods.core
interface HasEarDetection : PodDevice { interface HasEarDetection {
val isBeingWorn: Boolean val isBeingWorn: Boolean
@@ -1,6 +1,6 @@
package eu.darken.capod.pods.core package eu.darken.capod.pods.core
interface HasDualEarDetection : HasEarDetection { interface HasEarDetectionDual : HasEarDetection {
val isLeftPodInEar: Boolean val isLeftPodInEar: Boolean
@@ -0,0 +1,19 @@
package eu.darken.capod.pods.core
import android.content.Context
import androidx.annotation.ColorRes
interface HasPodStyle {
val podStyle: PodStyle
interface PodStyle {
fun getLabel(context: Context): String
@ColorRes
fun getColor(context: Context): Int
val identifier: String
}
}
@@ -0,0 +1,12 @@
package eu.darken.capod.pods.core
import android.content.Context
interface HasStateDetection {
val state: State
interface State {
fun getLabel(context: Context): String
}
}
@@ -103,6 +103,9 @@ interface PodDevice {
@Json(name = "fakes.tws.i99999") TWS_I99999( @Json(name = "fakes.tws.i99999") TWS_I99999(
"TWS i99999" "TWS i99999"
), ),
@Json(name = "fakes.varunr.airpodspro") VARUNR_AIRPODS_PRO(
"Varunr AirPods Pro"
),
@Json(name = "unknown") UNKNOWN( @Json(name = "unknown") UNKNOWN(
"Unknown" "Unknown"
); );
@@ -8,11 +8,11 @@ import java.time.Duration
import java.time.Instant import java.time.Instant
import kotlin.math.roundToInt import kotlin.math.roundToInt
fun HasDualPods.getBatteryLevelLeftPod(context: Context): String = fun DualPodDevice.getBatteryLevelLeftPod(context: Context): String =
batteryLeftPodPercent?.let { "${(it * 100).roundToInt()}%" } batteryLeftPodPercent?.let { "${(it * 100).roundToInt()}%" }
?: context.getString(R.string.general_value_not_available_label) ?: context.getString(R.string.general_value_not_available_label)
fun HasDualPods.getBatteryLevelRightPod(context: Context): String = fun DualPodDevice.getBatteryLevelRightPod(context: Context): String =
batteryRightPodPercent?.let { "${(it * 100).roundToInt()}%" } batteryRightPodPercent?.let { "${(it * 100).roundToInt()}%" }
?: context.getString(R.string.general_value_not_available_label) ?: context.getString(R.string.general_value_not_available_label)
@@ -20,7 +20,7 @@ fun HasCase.getBatteryLevelCase(context: Context): String =
batteryCasePercent?.let { "${(it * 100).roundToInt()}%" } batteryCasePercent?.let { "${(it * 100).roundToInt()}%" }
?: context.getString(R.string.general_value_not_available_label) ?: context.getString(R.string.general_value_not_available_label)
fun HasSinglePod.getBatteryLevelHeadset(context: Context): String = fun SinglePodDevice.getBatteryLevelHeadset(context: Context): String =
batteryHeadsetPercent?.let { "${(it * 100).roundToInt()}%" } batteryHeadsetPercent?.let { "${(it * 100).roundToInt()}%" }
?: context.getString(R.string.general_value_not_available_label) ?: context.getString(R.string.general_value_not_available_label)
@@ -1,6 +1,6 @@
package eu.darken.capod.pods.core package eu.darken.capod.pods.core
interface HasSinglePod { interface SinglePodDevice : PodDevice {
val batteryHeadsetPercent: Float? val batteryHeadsetPercent: Float?
} }
@@ -6,6 +6,7 @@ import eu.darken.capod.common.debug.logging.asLog
import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.misc.UnknownAppleDevice
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
@@ -7,7 +7,8 @@ import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet import dagger.multibindings.IntoSet
import eu.darken.capod.pods.core.apple.airpods.* import eu.darken.capod.pods.core.apple.airpods.*
import eu.darken.capod.pods.core.apple.beats.* import eu.darken.capod.pods.core.apple.beats.*
import eu.darken.capod.pods.core.apple.fakes.Twsi99999 import eu.darken.capod.pods.core.apple.misc.Twsi99999
import eu.darken.capod.pods.core.apple.misc.VarunrAirPodsPro
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
@Module @Module
@@ -28,4 +29,6 @@ abstract class AppleFactoryModule {
@Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePodsFactory<out ApplePods> @Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet abstract fun fakesTwsi999999(factory: Twsi99999.Factory): ApplePodsFactory<out ApplePods> @Binds @IntoSet abstract fun fakesTwsi999999(factory: Twsi99999.Factory): ApplePodsFactory<out ApplePods>
@Binds @IntoSet
abstract fun fakesVarunrAirPodsPro(factory: VarunrAirPodsPro.Factory): ApplePodsFactory<out ApplePods>
} }
@@ -5,7 +5,9 @@ import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.lowerNibble import eu.darken.capod.common.lowerNibble
import eu.darken.capod.common.upperNibble import eu.darken.capod.common.upperNibble
import eu.darken.capod.pods.core.HasCase
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
@@ -21,7 +23,7 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
val deviceColor: UByte, val deviceColor: UByte,
) )
fun ProximityPairing.Message.getApplePodsMarkings(): Markings = Markings( private fun ProximityPairing.Message.getApplePodsMarkings(): Markings = Markings(
vendor = ProximityPairing.CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING, vendor = ProximityPairing.CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING,
length = ProximityPairing.PAIRING_MESSAGE_LENGTH.toUByte(), length = ProximityPairing.PAIRING_MESSAGE_LENGTH.toUByte(),
device = (((data[1].toInt() and 255) shl 8) or (data[2].toInt() and 255)).toUShort(), device = (((data[1].toInt() and 255) shl 8) or (data[2].toInt() and 255)).toUShort(),
@@ -70,6 +72,22 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
internal val knownDevices = mutableMapOf<PodDevice.Id, KnownDevice>() internal val knownDevices = mutableMapOf<PodDevice.Id, KnownDevice>()
fun KnownDevice.getLatestCaseBattery(): Float? = history
.filterIsInstance<HasCase>()
.mapNotNull { it.batteryCasePercent }
.lastOrNull()
fun KnownDevice.getLatestCaseLidState(basic: DualAirPods): DualAirPods.LidState? {
val definitive = setOf(DualAirPods.LidState.OPEN, DualAirPods.LidState.CLOSED)
if (definitive.contains(basic.caseLidState)) return null
return history
.filterIsInstance<AirPodsPro>()
.lastOrNull { it.caseLidState != DualAirPods.LidState.NOT_IN_CASE }
?.caseLidState
}
internal open fun searchHistory(current: PodType): KnownDevice? { internal open fun searchHistory(current: PodType): KnownDevice? {
val scanResult = current.scanResult val scanResult = current.scanResult
val message = current.proximityMessage val message = current.proximityMessage
@@ -1,19 +0,0 @@
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.HasSinglePod
interface BasicSingleApplePods : ApplePods, HasSinglePod {
override val batteryHeadsetPercent: Float?
get() = when (val value = rawPodsBattery.lowerNibble.toInt()) {
15 -> null
else -> if (value > 10) {
log { "Left pod: Above 100% battery: $value" }
1.0f
} else {
(value / 10f)
}
}
}
@@ -1,3 +0,0 @@
package eu.darken.capod.pods.core.apple
abstract class BasicSingleApplePodsFactory(private val tag: String) : ApplePodsFactory<BasicSingleApplePods>(tag)
@@ -7,12 +7,11 @@ import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.isBitSet import eu.darken.capod.common.isBitSet
import eu.darken.capod.common.lowerNibble import eu.darken.capod.common.lowerNibble
import eu.darken.capod.common.upperNibble import eu.darken.capod.common.upperNibble
import eu.darken.capod.pods.core.HasCase import eu.darken.capod.pods.core.*
import eu.darken.capod.pods.core.HasDualEarDetection import eu.darken.capod.pods.core.DualPodDevice.Pod
import eu.darken.capod.pods.core.HasDualPods
import eu.darken.capod.pods.core.HasDualPods.Pod
interface DualApplePods : ApplePods, HasDualPods, HasDualEarDetection, HasCase { interface DualAirPods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase,
HasStateDetection, HasDualMicrophone, HasAppleColor {
val primaryPod: Pod val primaryPod: Pod
get() = when (rawStatus.isBitSet(5)) { get() = when (rawStatus.isBitSet(5)) {
@@ -86,23 +85,23 @@ interface DualApplePods : ApplePods, HasDualPods, HasDualEarDetection, HasCase {
* The data flip bit is set if the left pod is primary. * The data flip bit is set if the left pod is primary.
* For the pod that is in the case, this is flipped again though. * For the pod that is in the case, this is flipped again though.
*/ */
val isLeftPodMicrophone: Boolean override val isLeftPodMicrophone: Boolean
get() = rawStatus.isBitSet(5) xor isThisPodInThecase get() = rawStatus.isBitSet(5) xor isThisPodInThecase
/** /**
* The data flip bit is UNset if the right pod is primary. * The data flip bit is UNset if the right pod is primary.
* For the pod that is in the case, this is flipped again though. * For the pod that is in the case, this is flipped again though.
*/ */
val isRightPodMicrophone: Boolean override val isRightPodMicrophone: Boolean
get() = !rawStatus.isBitSet(5) xor isThisPodInThecase get() = !rawStatus.isBitSet(5) xor isThisPodInThecase
val isLeftPodCharging: Boolean override val isLeftPodCharging: Boolean
get() = when (areValuesFlipped) { get() = when (areValuesFlipped) {
false -> rawFlags.isBitSet(0) false -> rawFlags.isBitSet(0)
true -> rawFlags.isBitSet(1) true -> rawFlags.isBitSet(1)
} }
val isRightPodCharging: Boolean override val isRightPodCharging: Boolean
get() = when (areValuesFlipped) { get() = when (areValuesFlipped) {
false -> rawFlags.isBitSet(1) false -> rawFlags.isBitSet(1)
true -> rawFlags.isBitSet(0) true -> rawFlags.isBitSet(0)
@@ -143,35 +142,10 @@ interface DualApplePods : ApplePods, HasDualPods, HasDualEarDetection, HasCase {
UNKNOWN(0xFF..0xFF); UNKNOWN(0xFF..0xFF);
} }
val deviceColor: DeviceColor override val state: ConnectionState
get() = DeviceColor.values().firstOrNull { it.raw == rawDeviceColor } ?: DeviceColor.UNKNOWN
enum class DeviceColor(val raw: UByte?) {
WHITE(0x00),
BLACK(0x01),
RED(0x02),
BLUE(0x03),
PINK(0x04),
GRAY(0x05),
SILVER(0x06),
GOLD(0x07),
ROSE_GOLD(0x08),
SPACE_GRAY(0x09),
DARK_BLUE(0x0a),
LIGHT_BLUE(0x0b),
YELLOW(0x0c),
UNKNOWN(null);
constructor(raw: Int) : this(raw.toUByte())
}
val connectionState: ConnectionState
get() = ConnectionState.values().firstOrNull { rawSuffix == it.raw } ?: ConnectionState.UNKNOWN get() = ConnectionState.values().firstOrNull { rawSuffix == it.raw } ?: ConnectionState.UNKNOWN
fun getConnectionStateLabel(context: Context): String = context.getString(connectionState.labelRes) enum class ConnectionState(val raw: UByte?, @StringRes val labelRes: Int) : HasStateDetection.State {
enum class ConnectionState(val raw: UByte?, @StringRes val labelRes: Int) {
DISCONNECTED(0x00, R.string.pods_connection_state_disconnected_label), DISCONNECTED(0x00, R.string.pods_connection_state_disconnected_label),
IDLE(0x04, R.string.pods_connection_state_idle_label), IDLE(0x04, R.string.pods_connection_state_idle_label),
MUSIC(0x05, R.string.pods_connection_state_music_label), MUSIC(0x05, R.string.pods_connection_state_music_label),
@@ -180,6 +154,8 @@ interface DualApplePods : ApplePods, HasDualPods, HasDualEarDetection, HasCase {
HANGING_UP(0x09, R.string.pods_connection_state_hanging_up_label), HANGING_UP(0x09, R.string.pods_connection_state_hanging_up_label),
UNKNOWN(null, R.string.pods_connection_state_unknown_label); UNKNOWN(null, R.string.pods_connection_state_unknown_label);
override fun getLabel(context: Context): String = context.getString(labelRes)
constructor(raw: Int, @StringRes labelRes: Int) : this(raw.toUByte(), labelRes) constructor(raw: Int, @StringRes labelRes: Int) : this(raw.toUByte(), labelRes)
} }
@@ -3,18 +3,17 @@ package eu.darken.capod.pods.core.apple
import eu.darken.capod.common.debug.logging.Logging.Priority.DEBUG import eu.darken.capod.common.debug.logging.Logging.Priority.DEBUG
import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro
abstract class DualApplePodsFactory(private val tag: String) : ApplePodsFactory<DualApplePods>(tag) { abstract class DualApplePodsFactory(private val tag: String) : ApplePodsFactory<DualAirPods>(tag) {
fun DualApplePods.getCaseMatchMarkings() = SplitPodsMarkings( fun DualAirPods.getCaseMatchMarkings() = SplitPodsMarkings(
leftPodBattery = batteryLeftPodPercent, leftPodBattery = batteryLeftPodPercent,
rightPodBattery = batteryRightPodPercent, rightPodBattery = batteryRightPodPercent,
microPhoneLeft = isLeftPodMicrophone, microPhoneLeft = isLeftPodMicrophone,
microPhoneRight = isRightPodMicrophone, microPhoneRight = isRightPodMicrophone,
chargingLeft = isLeftPodCharging, chargingLeft = isLeftPodCharging,
chargingRight = isRightPodCharging, chargingRight = isRightPodCharging,
color = deviceColor, color = rawDeviceColor,
model = model model = model
) )
@@ -28,36 +27,21 @@ abstract class DualApplePodsFactory(private val tag: String) : ApplePodsFactory<
val microPhoneRight: Boolean, val microPhoneRight: Boolean,
val chargingLeft: Boolean, val chargingLeft: Boolean,
val chargingRight: Boolean, val chargingRight: Boolean,
val color: DualApplePods.DeviceColor, val color: UByte,
val model: PodDevice.Model, val model: PodDevice.Model,
) )
private fun Collection<KnownDevice>.findSplitPodsMatch(device: DualApplePods): Collection<KnownDevice> { private fun Collection<KnownDevice>.findSplitPodsMatch(device: DualAirPods): Collection<KnownDevice> {
val target = device.getCaseMatchMarkings() val target = device.getCaseMatchMarkings()
return filter { known -> return filter { known ->
known.history known.history
.filterIsInstance<DualApplePods>() .filterIsInstance<DualAirPods>()
.any { it.getCaseMatchMarkings() == target } .any { it.getCaseMatchMarkings() == target }
} }
} }
fun KnownDevice.getLatestCaseBattery(): Float? = history override fun searchHistory(current: DualAirPods): KnownDevice? {
.filterIsInstance<DualApplePods>()
.mapNotNull { it.batteryCasePercent }
.lastOrNull()
fun KnownDevice.getLatestCaseLidState(basic: DualApplePods): DualApplePods.LidState? {
val definitive = setOf(DualApplePods.LidState.OPEN, DualApplePods.LidState.CLOSED)
if (definitive.contains(basic.caseLidState)) return null
return history
.filterIsInstance<AirPodsPro>()
.lastOrNull { it.caseLidState != DualApplePods.LidState.NOT_IN_CASE }
?.caseLidState
}
override fun searchHistory(current: DualApplePods): KnownDevice? {
val basicResult = super.searchHistory(current) val basicResult = super.searchHistory(current)
val caseIgnored = knownDevices.values.findSplitPodsMatch(current) val caseIgnored = knownDevices.values.findSplitPodsMatch(current)
@@ -0,0 +1,38 @@
package eu.darken.capod.pods.core.apple
import android.content.Context
import eu.darken.capod.pods.core.HasPodStyle
interface HasAppleColor : ApplePods, HasPodStyle {
override val podStyle: HasPodStyle.PodStyle
get() = DeviceColor.values()
.firstOrNull { it.raw == rawDeviceColor }
?: DeviceColor.UNKNOWN
enum class DeviceColor(val raw: UByte?) : HasPodStyle.PodStyle {
WHITE(0x00),
BLACK(0x01),
RED(0x02),
BLUE(0x03),
PINK(0x04),
GRAY(0x05),
SILVER(0x06),
GOLD(0x07),
ROSE_GOLD(0x08),
SPACE_GRAY(0x09),
DARK_BLUE(0x0a),
LIGHT_BLUE(0x0b),
YELLOW(0x0c),
UNKNOWN(null);
override fun getLabel(context: Context): String = this.name
override fun getColor(context: Context): Int = android.R.color.white
override val identifier: String
get() = name
constructor(raw: Int) : this(raw.toUByte())
}
}
@@ -1,17 +1,22 @@
package eu.darken.capod.pods.core.apple package eu.darken.capod.pods.core.apple
import eu.darken.capod.common.isBitSet import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.pods.core.HasEarDetection import eu.darken.capod.common.lowerNibble
import eu.darken.capod.pods.core.HasSinglePod import eu.darken.capod.pods.core.SinglePodDevice
interface SingleApplePods : BasicSingleApplePods, HasEarDetection, HasSinglePod { /**
* Devices that only present a single charge level, e.g. most Beats devices
*/
interface SingleApplePods : ApplePods, SinglePodDevice, HasAppleColor {
val isHeadphonesBeingWorn: Boolean override val batteryHeadsetPercent: Float?
get() = rawStatus.isBitSet(1) get() = when (val value = rawPodsBattery.lowerNibble.toInt()) {
15 -> null
val isHeadsetBeingCharged: Boolean else -> if (value > 10) {
get() = rawFlags.isBitSet(0) log { "Headset above 100% battery: $value" }
1.0f
override val isBeingWorn: Boolean } else {
get() = isHeadphonesBeingWorn (value / 10f)
}
}
} }
@@ -4,7 +4,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.DualApplePods import eu.darken.capod.pods.core.apple.DualAirPods
import eu.darken.capod.pods.core.apple.DualApplePodsFactory import eu.darken.capod.pods.core.apple.DualApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant import java.time.Instant
@@ -20,15 +20,15 @@ data class AirPodsGen1 constructor(
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null, private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null, private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: DualApplePods.LidState? = null private val cachedCaseState: DualAirPods.LidState? = null
) : DualApplePods { ) : DualAirPods {
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN1 override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN1
override val batteryCasePercent: Float? override val batteryCasePercent: Float?
get() = super.batteryCasePercent ?: cachedBatteryPercentage get() = super.batteryCasePercent ?: cachedBatteryPercentage
override val caseLidState: DualApplePods.LidState override val caseLidState: DualAirPods.LidState
get() = cachedCaseState ?: super.caseLidState get() = cachedCaseState ?: super.caseLidState
override val rssi: Int override val rssi: Int
@@ -4,7 +4,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.DualApplePods import eu.darken.capod.pods.core.apple.DualAirPods
import eu.darken.capod.pods.core.apple.DualApplePodsFactory import eu.darken.capod.pods.core.apple.DualApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant import java.time.Instant
@@ -20,15 +20,15 @@ data class AirPodsGen2 constructor(
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null, private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null, private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: DualApplePods.LidState? = null private val cachedCaseState: DualAirPods.LidState? = null
) : DualApplePods { ) : DualAirPods {
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN2 override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN2
override val batteryCasePercent: Float? override val batteryCasePercent: Float?
get() = super.batteryCasePercent ?: cachedBatteryPercentage get() = super.batteryCasePercent ?: cachedBatteryPercentage
override val caseLidState: DualApplePods.LidState override val caseLidState: DualAirPods.LidState
get() = cachedCaseState ?: super.caseLidState get() = cachedCaseState ?: super.caseLidState
override val rssi: Int override val rssi: Int
@@ -4,7 +4,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.DualApplePods import eu.darken.capod.pods.core.apple.DualAirPods
import eu.darken.capod.pods.core.apple.DualApplePodsFactory import eu.darken.capod.pods.core.apple.DualApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant import java.time.Instant
@@ -20,14 +20,14 @@ data class AirPodsGen3 constructor(
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null, private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null, private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: DualApplePods.LidState? = null private val cachedCaseState: DualAirPods.LidState? = null
) : DualApplePods { ) : DualAirPods {
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN3 override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN3
override val batteryCasePercent: Float? override val batteryCasePercent: Float?
get() = super.batteryCasePercent ?: cachedBatteryPercentage get() = super.batteryCasePercent ?: cachedBatteryPercentage
override val caseLidState: DualApplePods.LidState override val caseLidState: DualAirPods.LidState
get() = cachedCaseState ?: super.caseLidState get() = cachedCaseState ?: super.caseLidState
override val rssi: Int override val rssi: Int
@@ -2,6 +2,8 @@ package eu.darken.capod.pods.core.apple.airpods
import eu.darken.capod.common.bluetooth.BleScanResult import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.isBitSet
import eu.darken.capod.pods.core.HasEarDetection
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.SingleApplePods import eu.darken.capod.pods.core.apple.SingleApplePods
@@ -19,13 +21,22 @@ data class AirPodsMax(
override val proximityMessage: ProximityPairing.Message, override val proximityMessage: ProximityPairing.Message,
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null, private val rssiAverage: Int? = null,
) : SingleApplePods { ) : SingleApplePods, HasEarDetection {
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX
override val rssi: Int override val rssi: Int
get() = rssiAverage ?: super.rssi get() = rssiAverage ?: super.rssi
val isHeadphonesBeingWorn: Boolean
get() = rawStatus.isBitSet(1)
val isHeadsetBeingCharged: Boolean
get() = rawFlags.isBitSet(0)
override val isBeingWorn: Boolean
get() = isHeadphonesBeingWorn
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) { class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run { override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
@@ -49,7 +60,6 @@ data class AirPodsMax(
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.averageRssi(basic.rssi),
) )
} }
} }
companion object { companion object {
@@ -4,8 +4,8 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.DualApplePods import eu.darken.capod.pods.core.apple.DualAirPods
import eu.darken.capod.pods.core.apple.DualApplePods.LidState import eu.darken.capod.pods.core.apple.DualAirPods.LidState
import eu.darken.capod.pods.core.apple.DualApplePodsFactory import eu.darken.capod.pods.core.apple.DualApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant import java.time.Instant
@@ -22,7 +22,7 @@ data class AirPodsPro(
private val rssiAverage: Int? = null, private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null, private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: LidState? = null private val cachedCaseState: LidState? = null
) : DualApplePods { ) : DualAirPods {
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO
@@ -4,8 +4,8 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.BasicSingleApplePods import eu.darken.capod.pods.core.apple.SingleApplePods
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant import java.time.Instant
import javax.inject.Inject import javax.inject.Inject
@@ -19,14 +19,14 @@ data class BeatsFlex(
override val proximityMessage: ProximityPairing.Message, override val proximityMessage: ProximityPairing.Message,
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null, private val rssiAverage: Int? = null,
) : BasicSingleApplePods { ) : SingleApplePods {
override val model: PodDevice.Model = PodDevice.Model.BEATS_FLEX override val model: PodDevice.Model = PodDevice.Model.BEATS_FLEX
override val rssi: Int override val rssi: Int
get() = rssiAverage ?: super.rssi get() = rssiAverage ?: super.rssi
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) { class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run { override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
@@ -4,8 +4,8 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.BasicSingleApplePods import eu.darken.capod.pods.core.apple.SingleApplePods
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant import java.time.Instant
import javax.inject.Inject import javax.inject.Inject
@@ -19,13 +19,13 @@ data class BeatsSolo3(
override val proximityMessage: ProximityPairing.Message, override val proximityMessage: ProximityPairing.Message,
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null, private val rssiAverage: Int? = null,
) : BasicSingleApplePods { ) : SingleApplePods {
override val model: PodDevice.Model = PodDevice.Model.BEATS_SOLO_3 override val model: PodDevice.Model = PodDevice.Model.BEATS_SOLO_3
override val rssi: Int override val rssi: Int
get() = rssiAverage ?: super.rssi get() = rssiAverage ?: super.rssi
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) { class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run { override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
@@ -4,8 +4,8 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.BasicSingleApplePods import eu.darken.capod.pods.core.apple.SingleApplePods
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant import java.time.Instant
import javax.inject.Inject import javax.inject.Inject
@@ -19,11 +19,11 @@ data class BeatsStudio3(
override val proximityMessage: ProximityPairing.Message, override val proximityMessage: ProximityPairing.Message,
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null, private val rssiAverage: Int? = null,
) : BasicSingleApplePods { ) : SingleApplePods {
override val model: PodDevice.Model = PodDevice.Model.BEATS_STUDIO_3 override val model: PodDevice.Model = PodDevice.Model.BEATS_STUDIO_3
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) { class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run { override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
getModelInfo().dirty == DEVICE_CODE_DIRTY && length == ProximityPairing.PAIRING_MESSAGE_LENGTH getModelInfo().dirty == DEVICE_CODE_DIRTY && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
@@ -46,7 +46,6 @@ data class BeatsStudio3(
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.averageRssi(basic.rssi),
) )
} }
} }
companion object { companion object {
@@ -4,8 +4,8 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.BasicSingleApplePods import eu.darken.capod.pods.core.apple.SingleApplePods
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant import java.time.Instant
import javax.inject.Inject import javax.inject.Inject
@@ -19,14 +19,14 @@ data class BeatsX(
override val proximityMessage: ProximityPairing.Message, override val proximityMessage: ProximityPairing.Message,
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null, private val rssiAverage: Int? = null,
) : BasicSingleApplePods { ) : SingleApplePods {
override val model: PodDevice.Model = PodDevice.Model.BEATS_X override val model: PodDevice.Model = PodDevice.Model.BEATS_X
override val rssi: Int override val rssi: Int
get() = rssiAverage ?: super.rssi get() = rssiAverage ?: super.rssi
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) { class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run { override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
@@ -4,8 +4,8 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.BasicSingleApplePods import eu.darken.capod.pods.core.apple.SingleApplePods
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant import java.time.Instant
import javax.inject.Inject import javax.inject.Inject
@@ -19,13 +19,14 @@ data class PowerBeats3(
override val proximityMessage: ProximityPairing.Message, override val proximityMessage: ProximityPairing.Message,
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null, private val rssiAverage: Int? = null,
) : BasicSingleApplePods { ) : SingleApplePods {
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_3 override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_3
override val rssi: Int override val rssi: Int
get() = rssiAverage ?: super.rssi get() = rssiAverage ?: super.rssi
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) { class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run { override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
@@ -4,7 +4,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.DualApplePods import eu.darken.capod.pods.core.apple.DualAirPods
import eu.darken.capod.pods.core.apple.DualApplePodsFactory import eu.darken.capod.pods.core.apple.DualApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant import java.time.Instant
@@ -20,15 +20,15 @@ data class PowerBeatsPro(
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null, private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null, private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: DualApplePods.LidState? = null private val cachedCaseState: DualAirPods.LidState? = null
) : DualApplePods { ) : DualAirPods {
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_PRO override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_PRO
override val batteryCasePercent: Float? override val batteryCasePercent: Float?
get() = super.batteryCasePercent ?: cachedBatteryPercentage get() = super.batteryCasePercent ?: cachedBatteryPercentage
override val caseLidState: DualApplePods.LidState override val caseLidState: DualAirPods.LidState
get() = cachedCaseState ?: super.caseLidState get() = cachedCaseState ?: super.caseLidState
override val rssi: Int override val rssi: Int
@@ -1,72 +0,0 @@
package eu.darken.capod.pods.core.apple.fakes
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.DualApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant
import javax.inject.Inject
/**
* Basically an AirPods GEN1 clone
*/
data class Twsi99999 constructor(
override val identifier: PodDevice.Id = PodDevice.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: DualApplePods.LidState? = null
) : DualApplePods {
override val model: PodDevice.Model = PodDevice.Model.TWS_I99999
override val batteryCasePercent: Float?
get() = super.batteryCasePercent ?: cachedBatteryPercentage
override val caseLidState: DualApplePods.LidState
get() = cachedCaseState ?: super.caseLidState
override val rssi: Int
get() = rssiAverage ?: super.rssi
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
getModelInfo().full == DEVICE_CODE && length == 19
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = Twsi99999(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
if (result == null) return basic
return basic.copy(
identifier = result.id,
seenFirstAt = result.seenFirstAt,
seenCounter = result.seenCounter,
confidence = result.confidence,
cachedBatteryPercentage = result.getLatestCaseBattery(),
rssiAverage = result.averageRssi(basic.rssi),
cachedCaseState = result.getLatestCaseLidState(basic)
)
}
}
companion object {
private val DEVICE_CODE = 0x0220.toUShort()
private val TAG = logTag("PodDevice", "Apple", "TWS", "i99999")
}
}
@@ -0,0 +1,143 @@
package eu.darken.capod.pods.core.apple.misc
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.isBitSet
import eu.darken.capod.common.lowerNibble
import eu.darken.capod.common.upperNibble
import eu.darken.capod.pods.core.DualPodDevice
import eu.darken.capod.pods.core.HasCase
import eu.darken.capod.pods.core.HasDualMicrophone
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant
import javax.inject.Inject
/**
* Basically an AirPods GEN1 clone
* Similar data structure but a lot of placeholder values or hardcoded values
*/
data class Twsi99999 constructor(
override val identifier: PodDevice.Id = PodDevice.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null,
) : ApplePods, DualPodDevice, HasDualMicrophone, HasCase {
override val model: PodDevice.Model = PodDevice.Model.TWS_I99999
override val rssi: Int
get() = rssiAverage ?: super<ApplePods>.rssi
/**
* Normally values for the left pod are in the lower nibbles, if the left pod is primary (microphone)
* If the right pod is the primary, the values are flipped.
*/
val areValuesFlipped: Boolean
get() = !rawStatus.isBitSet(5)
override val batteryLeftPodPercent: Float?
get() {
val value = when (areValuesFlipped) {
true -> rawPodsBattery.upperNibble.toInt()
false -> rawPodsBattery.lowerNibble.toInt()
}
return when (value) {
15 -> null
else -> if (value > 10) {
log { "Left pod: Above 100% battery: $value" }
1.0f
} else {
(value / 10f)
}
}
}
override val batteryRightPodPercent: Float?
get() {
val value = when (areValuesFlipped) {
true -> rawPodsBattery.lowerNibble.toInt()
false -> rawPodsBattery.upperNibble.toInt()
}
return when (value) {
15 -> null
else -> if (value > 10) {
log { "Right pod: Above 100% battery: $value" }
1.0f
} else {
value / 10f
}
}
}
val isThisPodInThecase: Boolean
get() = rawStatus.isBitSet(6)
/**
* The data flip bit is set if the left pod is primary.
* For the pod that is in the case, this is flipped again though.
*/
override val isLeftPodMicrophone: Boolean
get() = rawStatus.isBitSet(5) xor isThisPodInThecase
/**
* The data flip bit is UNset if the right pod is primary.
* For the pod that is in the case, this is flipped again though.
*/
override val isRightPodMicrophone: Boolean
get() = !rawStatus.isBitSet(5) xor isThisPodInThecase
override val batteryCasePercent: Float?
get() = when (val value = rawCaseBattery.toInt()) {
15 -> cachedBatteryPercentage
else -> if (value > 10) {
log { "Case: Above 100% battery: $value" }
1.0f
} else {
value / 10f
}
}
override val isCaseCharging: Boolean
get() = rawFlags.isBitSet(2)
class Factory @Inject constructor() : ApplePodsFactory<Twsi99999>(TAG) {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
getModelInfo().full == DEVICE_CODE && length == 19
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = Twsi99999(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
if (result == null) return basic
return basic.copy(
identifier = result.id,
seenFirstAt = result.seenFirstAt,
seenCounter = result.seenCounter,
confidence = result.confidence,
cachedBatteryPercentage = result.getLatestCaseBattery(),
rssiAverage = result.averageRssi(basic.rssi),
)
}
}
companion object {
private val DEVICE_CODE = 0x0220.toUShort()
private val TAG = logTag("PodDevice", "Apple", "TWS", "i99999")
}
}
@@ -1,10 +1,12 @@
package eu.darken.capod.pods.core.apple package eu.darken.capod.pods.core.apple.misc
import android.content.Context import android.content.Context
import eu.darken.capod.R import eu.darken.capod.R
import eu.darken.capod.common.bluetooth.BleScanResult import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant import java.time.Instant
import javax.inject.Inject import javax.inject.Inject
@@ -0,0 +1,143 @@
package eu.darken.capod.pods.core.apple.misc
import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.log
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.isBitSet
import eu.darken.capod.common.lowerNibble
import eu.darken.capod.common.upperNibble
import eu.darken.capod.pods.core.DualPodDevice
import eu.darken.capod.pods.core.HasCase
import eu.darken.capod.pods.core.HasDualMicrophone
import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.ApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant
import javax.inject.Inject
/**
* AirPods Pro clone similar to Twsi999999.
* Shorter data structure.
*/
data class VarunrAirPodsPro constructor(
override val identifier: PodDevice.Id = PodDevice.Id(),
override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message,
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null,
) : ApplePods, DualPodDevice, HasDualMicrophone, HasCase {
override val model: PodDevice.Model = PodDevice.Model.VARUNR_AIRPODS_PRO
override val rssi: Int
get() = rssiAverage ?: super<ApplePods>.rssi
/**
* Normally values for the left pod are in the lower nibbles, if the left pod is primary (microphone)
* If the right pod is the primary, the values are flipped.
*/
val areValuesFlipped: Boolean
get() = !rawStatus.isBitSet(5)
override val batteryLeftPodPercent: Float?
get() {
val value = when (areValuesFlipped) {
true -> rawPodsBattery.upperNibble.toInt()
false -> rawPodsBattery.lowerNibble.toInt()
}
return when (value) {
15 -> null
else -> if (value > 10) {
log { "Left pod: Above 100% battery: $value" }
1.0f
} else {
(value / 10f)
}
}
}
override val batteryRightPodPercent: Float?
get() {
val value = when (areValuesFlipped) {
true -> rawPodsBattery.lowerNibble.toInt()
false -> rawPodsBattery.upperNibble.toInt()
}
return when (value) {
15 -> null
else -> if (value > 10) {
log { "Right pod: Above 100% battery: $value" }
1.0f
} else {
value / 10f
}
}
}
val isThisPodInThecase: Boolean
get() = rawStatus.isBitSet(6)
/**
* The data flip bit is set if the left pod is primary.
* For the pod that is in the case, this is flipped again though.
*/
override val isLeftPodMicrophone: Boolean
get() = rawStatus.isBitSet(5) xor isThisPodInThecase
/**
* The data flip bit is UNset if the right pod is primary.
* For the pod that is in the case, this is flipped again though.
*/
override val isRightPodMicrophone: Boolean
get() = !rawStatus.isBitSet(5) xor isThisPodInThecase
override val batteryCasePercent: Float?
get() = when (val value = rawCaseBattery.toInt()) {
15 -> cachedBatteryPercentage
else -> if (value > 10) {
log { "Case: Above 100% battery: $value" }
1.0f
} else {
value / 10f
}
}
override val isCaseCharging: Boolean
get() = rawFlags.isBitSet(2)
class Factory @Inject constructor() : ApplePodsFactory<VarunrAirPodsPro>(TAG) {
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
getModelInfo().full == DEVICE_CODE && length == 19
}
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
var basic = VarunrAirPodsPro(scanResult = scanResult, proximityMessage = message)
val result = searchHistory(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic)
if (result == null) return basic
return basic.copy(
identifier = result.id,
seenFirstAt = result.seenFirstAt,
seenCounter = result.seenCounter,
confidence = result.confidence,
cachedBatteryPercentage = result.getLatestCaseBattery(),
rssiAverage = result.averageRssi(basic.rssi),
)
}
}
companion object {
private val DEVICE_CODE = 0x0E20.toUShort()
private val TAG = logTag("PodDevice", "Apple", "Varunr", "AirPodsPro")
}
}
@@ -8,9 +8,9 @@ import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.setupCommonEventHandlers import eu.darken.capod.common.flow.setupCommonEventHandlers
import eu.darken.capod.main.core.GeneralSettings import eu.darken.capod.main.core.GeneralSettings
import eu.darken.capod.monitor.core.PodMonitor import eu.darken.capod.monitor.core.PodMonitor
import eu.darken.capod.pods.core.HasDualEarDetection
import eu.darken.capod.pods.core.HasEarDetection import eu.darken.capod.pods.core.HasEarDetection
import eu.darken.capod.pods.core.apple.DualApplePods import eu.darken.capod.pods.core.HasEarDetectionDual
import eu.darken.capod.pods.core.apple.DualAirPods
import eu.darken.capod.reaction.settings.ReactionSettings import eu.darken.capod.reaction.settings.ReactionSettings
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import javax.inject.Inject import javax.inject.Inject
@@ -68,12 +68,12 @@ class AutoConnect @Inject constructor(
val conditionFulfilled = when (condition) { val conditionFulfilled = when (condition) {
AutoConnectCondition.WHEN_SEEN -> true AutoConnectCondition.WHEN_SEEN -> true
AutoConnectCondition.CASE_OPEN -> when (mainDevice) { AutoConnectCondition.CASE_OPEN -> when (mainDevice) {
is DualApplePods -> mainDevice.caseLidState == DualApplePods.LidState.OPEN is DualAirPods -> mainDevice.caseLidState == DualAirPods.LidState.OPEN
else -> true else -> true
} }
AutoConnectCondition.IN_EAR -> when (mainDevice) { AutoConnectCondition.IN_EAR -> when (mainDevice) {
is HasEarDetection -> { is HasEarDetection -> {
if (mainDevice is HasDualEarDetection && reactionSettings.onePodMode.value) { if (mainDevice is HasEarDetectionDual && reactionSettings.onePodMode.value) {
mainDevice.isEitherPodInEar mainDevice.isEitherPodInEar
} else { } else {
mainDevice.isBeingWorn mainDevice.isBeingWorn
@@ -9,8 +9,8 @@ import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.flow.setupCommonEventHandlers import eu.darken.capod.common.flow.setupCommonEventHandlers
import eu.darken.capod.common.flow.withPrevious import eu.darken.capod.common.flow.withPrevious
import eu.darken.capod.monitor.core.PodMonitor import eu.darken.capod.monitor.core.PodMonitor
import eu.darken.capod.pods.core.HasDualEarDetection
import eu.darken.capod.pods.core.HasEarDetection import eu.darken.capod.pods.core.HasEarDetection
import eu.darken.capod.pods.core.HasEarDetectionDual
import eu.darken.capod.reaction.settings.ReactionSettings import eu.darken.capod.reaction.settings.ReactionSettings
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import javax.inject.Inject import javax.inject.Inject
@@ -48,7 +48,7 @@ class PlayPause @Inject constructor(
return@onEach return@onEach
} }
if (previous is HasDualEarDetection && current is HasDualEarDetection && reactionSettings.onePodMode.value) { if (previous is HasEarDetectionDual && current is HasEarDetectionDual && reactionSettings.onePodMode.value) {
log(TAG) { "Ear status changed for dual pod device in single pod mode." } log(TAG) { "Ear status changed for dual pod device in single pod mode." }
val previousWorn = previous.isEitherPodInEar val previousWorn = previous.isEitherPodInEar
val currentWorn = current.isEitherPodInEar val currentWorn = current.isEitherPodInEar
@@ -8,7 +8,7 @@ import eu.darken.capod.common.flow.setupCommonEventHandlers
import eu.darken.capod.common.flow.withPrevious import eu.darken.capod.common.flow.withPrevious
import eu.darken.capod.monitor.core.PodMonitor import eu.darken.capod.monitor.core.PodMonitor
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.DualApplePods import eu.darken.capod.pods.core.apple.DualAirPods
import eu.darken.capod.reaction.popup.ui.PopUpWindow import eu.darken.capod.reaction.popup.ui.PopUpWindow
import eu.darken.capod.reaction.settings.ReactionSettings import eu.darken.capod.reaction.settings.ReactionSettings
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
@@ -39,7 +39,7 @@ class PopUpReaction @Inject constructor(
.withPrevious() .withPrevious()
.setupCommonEventHandlers(TAG) { "monitor" } .setupCommonEventHandlers(TAG) { "monitor" }
.map { (previous, current) -> .map { (previous, current) ->
if (previous is DualApplePods? && current is DualApplePods) { if (previous is DualAirPods? && current is DualAirPods) {
log(TAG, VERBOSE) { log(TAG, VERBOSE) {
val prev = previous?.rawCaseLidState?.let { String.format("%02X", it.toByte()) } val prev = previous?.rawCaseLidState?.let { String.format("%02X", it.toByte()) }
val cur = current.rawCaseLidState.let { String.format("%02X", it.toByte()) } val cur = current.rawCaseLidState.let { String.format("%02X", it.toByte()) }
@@ -60,8 +60,8 @@ class PopUpReaction @Inject constructor(
} }
} }
private suspend fun tryPopWindow(current: DualApplePods) { private suspend fun tryPopWindow(current: DualAirPods) {
if (current.caseLidState == DualApplePods.LidState.OPEN) { if (current.caseLidState == DualAirPods.LidState.OPEN) {
log(TAG, INFO) { "Show popup" } log(TAG, INFO) { "Show popup" }
val now = Instant.now() val now = Instant.now()
@@ -78,9 +78,9 @@ class PopUpReaction @Inject constructor(
withContext(dispatcherProvider.Main) { withContext(dispatcherProvider.Main) {
popupWindow.show(current) popupWindow.show(current)
} }
} else if (current.caseLidState != DualApplePods.LidState.OPEN) { } else if (current.caseLidState != DualAirPods.LidState.OPEN) {
when (current.caseLidState) { when (current.caseLidState) {
DualApplePods.LidState.CLOSED -> { DualAirPods.LidState.CLOSED -> {
log(TAG, INFO) { "Lid was actively closed, resetting cooldown." } log(TAG, INFO) { "Lid was actively closed, resetting cooldown." }
coolDowns.remove(current.identifier) coolDowns.remove(current.identifier)
} }
@@ -10,11 +10,9 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.R import eu.darken.capod.R
import eu.darken.capod.common.debug.autoreport.DebugSettings import eu.darken.capod.common.debug.autoreport.DebugSettings
import eu.darken.capod.databinding.PopupNotificationDualPodsBinding import eu.darken.capod.databinding.PopupNotificationDualPodsBinding
import eu.darken.capod.databinding.PopupNotificationSinglePodsBasicBinding
import eu.darken.capod.databinding.PopupNotificationSinglePodsBinding import eu.darken.capod.databinding.PopupNotificationSinglePodsBinding
import eu.darken.capod.pods.core.* import eu.darken.capod.pods.core.*
import eu.darken.capod.pods.core.apple.BasicSingleApplePods import eu.darken.capod.pods.core.apple.DualAirPods
import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.SingleApplePods import eu.darken.capod.pods.core.apple.SingleApplePods
import javax.inject.Inject import javax.inject.Inject
@@ -28,16 +26,13 @@ class PopUpPodViewFactory @Inject constructor(
private val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater private val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
fun createContentView(parent: ViewGroup, device: PodDevice): View = when (device) { fun createContentView(parent: ViewGroup, device: PodDevice): View = when (device) {
is DualApplePods -> createDualApplePods(parent, device) is DualAirPods -> createDualApplePods(parent, device)
is SingleApplePods -> createSingleApplePods(parent, device) // Unused, has no case to trigger reaction? // Unused, has no case to trigger reaction?
is BasicSingleApplePods -> createSingleBasicApplePods( is SingleApplePods -> createSingleApplePods(parent, device)
parent,
device
) // Unused, has no case to trigger reaction?
else -> throw IllegalArgumentException("Unexpected device: $device") else -> throw IllegalArgumentException("Unexpected device: $device")
} }
private fun createDualApplePods(parent: ViewGroup, device: DualApplePods): View = private fun createDualApplePods(parent: ViewGroup, device: DualAirPods): View =
PopupNotificationDualPodsBinding.inflate(layoutInflater, parent, false).apply { PopupNotificationDualPodsBinding.inflate(layoutInflater, parent, false).apply {
device.apply { device.apply {
podIcon.setImageResource(iconRes) podIcon.setImageResource(iconRes)
@@ -72,17 +67,4 @@ class PopUpPodViewFactory @Inject constructor(
} }
}.root }.root
private fun createSingleBasicApplePods(parent: ViewGroup, device: BasicSingleApplePods): View =
PopupNotificationSinglePodsBasicBinding.inflate(layoutInflater, parent, false).apply {
device.apply {
headphonesIcon.setImageResource(iconRes)
headphonesLabel.text = getLabel(context)
signal.text = getSignalQuality(context)
signal.isInvisible = debugSettings.isDebugModeEnabled.value
headphonesBatteryIcon.setImageResource(getBatteryDrawable(batteryHeadsetPercent))
headphonesBatteryLabel.text = getBatteryLevelHeadset(context)
}
}.root
} }
+1 -1
View File
@@ -22,7 +22,7 @@
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
tools:listitem="@layout/overview_pods_apple_dual_item" tools:listitem="@layout/overview_pods_dual_item"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar" /> app:layout_constraintTop_toBottomOf="@id/toolbar" />
@@ -1,37 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal">
<TextView
android:id="@+id/headphones_label"
style="@style/TextAppearance.Compat.Notification.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
tools:text="Beats Solo 3" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:orientation="horizontal">
<ImageView
android:id="@+id/headphones_battery_icon"
style="@style/PodInfoItemIcon"
android:src="@drawable/ic_baseline_battery_unknown_24" />
<TextView
android:id="@+id/headphones_battery_label"
style="@style/TextAppearance.Compat.Notification.Title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginHorizontal="8dp"
tools:text="100%" />
</LinearLayout>
</LinearLayout>
@@ -1,152 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/card"
style="@style/MyCardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
tools:context=".main.ui.MainActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/card_content"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/device_icon"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
app:layout_constraintBottom_toBottomOf="@id/name"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/name"
app:srcCompat="@drawable/ic_device_generic_headphones" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/name"
style="@style/TextAppearance.MaterialComponents.Body2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginStart="4dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:gravity="center_vertical"
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"
tools:text="Power Beats 3" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/last_seen"
style="@style/TextAppearance.MaterialComponents.Caption"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toTopOf="@id/barrier_top"
app:layout_constraintEnd_toEndOf="@id/name"
app:layout_constraintStart_toStartOf="@id/name"
app:layout_constraintTop_toBottomOf="@id/name"
tools:text="3s ago" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/reception"
style="@style/TextAppearance.MaterialComponents.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginStart="8dp"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="@id/name"
app:layout_constraintEnd_toStartOf="@id/reception_icon"
app:layout_constraintStart_toEndOf="@id/name"
app:layout_constraintTop_toTopOf="@+id/name"
tools:text="Yours (RSSI -61)" />
<ImageView
android:id="@+id/reception_icon"
style="@style/TextAppearance.MaterialComponents.Caption"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginHorizontal="16dp"
android:layout_marginStart="4dp"
android:layout_marginEnd="16dp"
android:gravity="center"
app:layout_constraintBottom_toBottomOf="@+id/reception"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/reception"
app:layout_constraintTop_toTopOf="@+id/reception"
app:srcCompat="@drawable/ic_baseline_settings_input_antenna_24"
tools:text="Yours (RSSI -61)" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="top"
app:constraint_referenced_ids="headphones"
tools:layout_editor_absoluteY="60dp" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/headphones"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier_top"
app:layout_constraintVertical_bias="0.0"
app:layout_goneMarginBottom="16dp">
<ImageView
android:id="@+id/battery_icon"
style="@style/PodInfoItemIcon"
android:src="@drawable/ic_baseline_battery_unknown_24"
app:layout_constraintBottom_toBottomOf="@id/battery_label"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/battery_label" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/battery_label"
style="@style/PodInfoItemText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="2dp"
android:text="@string/general_value_not_available_label"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/battery_icon"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="headphones" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/status"
style="@style/TextAppearance.MaterialComponents.Body2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:gravity="center_vertical"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier_bottom"
tools:text="Music Active, Case Closed" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
@@ -12,7 +12,8 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/card_content" android:id="@+id/card_content"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:paddingBottom="16dp">
<ImageView <ImageView
android:id="@+id/device_icon" android:id="@+id/device_icon"
@@ -107,7 +108,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="4dp" android:layout_marginEnd="4dp"
android:minHeight="108dp"
app:layout_constraintBottom_toTopOf="@id/barrier_bottom" app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
app:layout_constraintEnd_toStartOf="@id/pod_case_container" app:layout_constraintEnd_toStartOf="@id/pod_case_container"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@@ -117,6 +117,7 @@
<ImageView <ImageView
android:id="@+id/pod_left_icon" android:id="@+id/pod_left_icon"
style="@style/PodInfoItemIcon" style="@style/PodInfoItemIcon"
android:contentDescription="@string/pods_dual_left_label"
android:src="@drawable/ic_airpod_left_24" android:src="@drawable/ic_airpod_left_24"
app:layout_constraintBottom_toBottomOf="@id/pod_left_label" app:layout_constraintBottom_toBottomOf="@id/pod_left_label"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@@ -322,6 +323,7 @@
<ImageView <ImageView
android:id="@+id/pod_right_icon" android:id="@+id/pod_right_icon"
style="@style/PodInfoItemIcon" style="@style/PodInfoItemIcon"
android:contentDescription="@string/pods_dual_right_label"
android:src="@drawable/ic_airpod_right_24" android:src="@drawable/ic_airpod_right_24"
app:layout_constraintBottom_toBottomOf="@id/pod_right_label" app:layout_constraintBottom_toBottomOf="@id/pod_right_label"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@@ -420,7 +422,6 @@
app:layout_constraintTop_toBottomOf="@id/pod_right_microphone_label" /> app:layout_constraintTop_toBottomOf="@id/pod_right_microphone_label" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<androidx.constraintlayout.widget.Barrier <androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_bottom" android:id="@+id/barrier_bottom"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -435,7 +436,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp" android:layout_marginHorizontal="16dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:gravity="center_vertical" android:gravity="center_vertical"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@@ -12,7 +12,8 @@
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/card_content" android:id="@+id/card_content"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:paddingBottom="16dp">
<ImageView <ImageView
android:id="@+id/device_icon" android:id="@+id/device_icon"
@@ -96,13 +97,11 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:minHeight="64dp"
app:layout_constraintBottom_toTopOf="@id/barrier_bottom" app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier_top" app:layout_constraintTop_toBottomOf="@id/barrier_top"
app:layout_constraintVertical_bias="0.0" app:layout_constraintVertical_bias="0.0">
app:layout_goneMarginBottom="16dp">
<ImageView <ImageView
android:id="@+id/battery_icon" android:id="@+id/battery_icon"
@@ -180,7 +179,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp" android:layout_marginHorizontal="16dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginBottom="16dp"
android:gravity="center_vertical" android:gravity="center_vertical"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@@ -1,81 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/headphones_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_device_generic_earbuds"
app:layout_constraintBottom_toBottomOf="@id/headphones_label"
app:layout_constraintEnd_toStartOf="@id/headphones_label"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/headphones_label" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/headphones_label"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginHorizontal="8dp"
android:layout_marginTop="4dp"
app:layout_constraintEnd_toStartOf="@id/signal"
app:layout_constraintStart_toEndOf="@id/headphones_icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="Beats Solo 3" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/signal"
style="@style/TextAppearance.MaterialComponents.Caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawableEnd="@drawable/ic_baseline_signal_cellular_alt_24"
android:gravity="center"
android:minWidth="44dp"
app:layout_constraintBottom_toBottomOf="@+id/headphones_label"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/headphones_label"
tools:text="-90%" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/headphones_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/headphones_label">
<ImageView
android:id="@id/headphones_battery_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_baseline_battery_unknown_24"
app:layout_constraintBottom_toBottomOf="@id/headphones_battery_label"
app:layout_constraintEnd_toStartOf="@id/headphones_battery_label"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/headphones_battery_label" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/headphones_battery_label"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/headphones_battery_icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="100%" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
+7
View File
@@ -81,6 +81,7 @@
<string name="settings_monitor_mode_label">Mode surveillance</string> <string name="settings_monitor_mode_label">Mode surveillance</string>
<string name="settings_monitor_mode_description">Les circonstances de surveillance des données Bluetooth par lappli.</string> <string name="settings_monitor_mode_description">Les circonstances de surveillance des données Bluetooth par lappli.</string>
<string name="settings_scanner_mode_label">Mode analyse</string> <string name="settings_scanner_mode_label">Mode analyse</string>
<string name="settings_scanner_mode_description">Lanalyseur des données « basse énergie » de Bluetooth devrait-il prioriser les performances ou préserver l’énergie?</string>
<string name="settings_monitor_mode_manual_label">Si lappli est en fonction</string> <string name="settings_monitor_mode_manual_label">Si lappli est en fonction</string>
<string name="settings_monitor_mode_automatic_label">Si lappareil est connecté</string> <string name="settings_monitor_mode_automatic_label">Si lappareil est connecté</string>
<string name="settings_monitor_mode_always_label">Toujours</string> <string name="settings_monitor_mode_always_label">Toujours</string>
@@ -88,10 +89,13 @@
<string name="settings_scanner_mode_balanced_label">Équilibré</string> <string name="settings_scanner_mode_balanced_label">Équilibré</string>
<string name="settings_scanner_mode_lowlatency_label">Faible latence</string> <string name="settings_scanner_mode_lowlatency_label">Faible latence</string>
<string name="settings_autopause_label">Pause automatique</string> <string name="settings_autopause_label">Pause automatique</string>
<string name="settings_autopause_description">Mettre le son en pause si vous ôtez lappareil de votre oreille.</string>
<string name="settings_showall_label">Afficher tous les appareils</string> <string name="settings_showall_label">Afficher tous les appareils</string>
<string name="settings_showall_description">Afficher les appareils dautres personnes à proximité.</string> <string name="settings_showall_description">Afficher les appareils dautres personnes à proximité.</string>
<string name="settings_autopplay_label">Lecture automatique</string> <string name="settings_autopplay_label">Lecture automatique</string>
<string name="settings_autoplay_description">Démarrer la lecture du son quand lappareil est porté.</string>
<string name="settings_fake_data_label">Fausses données</string> <string name="settings_fake_data_label">Fausses données</string>
<string name="settings_fake_data_description">Afficher les fausses données, c.-à-d. simuler les appareils qui nexistent pas.</string>
<string name="settings_debug_label">Paramètres de débogage</string> <string name="settings_debug_label">Paramètres de débogage</string>
<string name="settings_debug_description">Autres paramètres pour aider à diagnostiquer les problèmes de lappli.</string> <string name="settings_debug_description">Autres paramètres pour aider à diagnostiquer les problèmes de lappli.</string>
<string name="settings_signal_minimum_label">Qualité minimale du signal</string> <string name="settings_signal_minimum_label">Qualité minimale du signal</string>
@@ -118,6 +122,7 @@
<string name="overview_bluetooth_disabled_label">Le Bluetooth est désactivé</string> <string name="overview_bluetooth_disabled_label">Le Bluetooth est désactivé</string>
<string name="overview_bluetooth_disabled_description">Le Bluetooth est désactivé, activez-le ;)</string> <string name="overview_bluetooth_disabled_description">Le Bluetooth est désactivé, activez-le ;)</string>
<string name="help_translate_label">Traduction</string> <string name="help_translate_label">Traduction</string>
<string name="help_translate_description">Aider à traduire cette appli dans votre langue préférée.</string>
<string name="settings_onepod_mode_label">Mode à un seul AirPod</string> <string name="settings_onepod_mode_label">Mode à un seul AirPod</string>
<string name="settings_onepod_mode_description">Il nest pas nécessaire de porter deux AirPods, en porter un seul suffit à déclencher les réactions.</string> <string name="settings_onepod_mode_description">Il nest pas nécessaire de porter deux AirPods, en porter un seul suffit à déclencher les réactions.</string>
<string name="permission_system_alert_window_label">Fenêtre dalerte système</string> <string name="permission_system_alert_window_label">Fenêtre dalerte système</string>
@@ -127,4 +132,6 @@
<string name="settings_blescanner_unfiltered_label">Données BÉB non filtrées</string> <string name="settings_blescanner_unfiltered_label">Données BÉB non filtrées</string>
<string name="settings_blescanner_unfiltered_description">Supprimer tous les filtres de lanalyseur BÉB pour afficher toutes les données BÉB diffusées. Utile pour prendre en charge les nouveaux types de casques-micros.</string> <string name="settings_blescanner_unfiltered_description">Supprimer tous les filtres de lanalyseur BÉB pour afficher toutes les données BÉB diffusées. Utile pour prendre en charge les nouveaux types de casques-micros.</string>
<string name="permission_required_title">Lautorisation suivante est nécessaire :</string> <string name="permission_required_title">Lautorisation suivante est nécessaire :</string>
<string name="settings_compatibility_mode_label">Mode de compatibilité</string>
<string name="settings_compatibility_mode_description">Désactiver les optimisations pour améliorer la compatibilité. À essayer si vous ne voyez aucune donnée.</string>
</resources> </resources>
+7
View File
@@ -81,6 +81,7 @@
<string name="settings_monitor_mode_label">Mode monitor</string> <string name="settings_monitor_mode_label">Mode monitor</string>
<string name="settings_monitor_mode_description">Dalam keadaan apa aplikasi ini memonitor data Bluetooth.</string> <string name="settings_monitor_mode_description">Dalam keadaan apa aplikasi ini memonitor data Bluetooth.</string>
<string name="settings_scanner_mode_label">Modus memindai</string> <string name="settings_scanner_mode_label">Modus memindai</string>
<string name="settings_scanner_mode_description">Haruskah pemindai data Bluetooth Hemat Energi memprioritaskan kinerja atau menghemat energi?</string>
<string name="settings_monitor_mode_manual_label">Saat aplikasi terbuka</string> <string name="settings_monitor_mode_manual_label">Saat aplikasi terbuka</string>
<string name="settings_monitor_mode_automatic_label">Saat perangkat terhubung</string> <string name="settings_monitor_mode_automatic_label">Saat perangkat terhubung</string>
<string name="settings_monitor_mode_always_label">Selalu</string> <string name="settings_monitor_mode_always_label">Selalu</string>
@@ -88,10 +89,13 @@
<string name="settings_scanner_mode_balanced_label">Seimbang</string> <string name="settings_scanner_mode_balanced_label">Seimbang</string>
<string name="settings_scanner_mode_lowlatency_label">Latemsi rendah</string> <string name="settings_scanner_mode_lowlatency_label">Latemsi rendah</string>
<string name="settings_autopause_label">Jeda otomatis</string> <string name="settings_autopause_label">Jeda otomatis</string>
<string name="settings_autopause_description">Jeda audio saat melepaskan perangkat dari telinga anda.</string>
<string name="settings_showall_label">Tampilkan semua perangkat</string> <string name="settings_showall_label">Tampilkan semua perangkat</string>
<string name="settings_showall_description">Tampilkan perangkat orang lain yang ada di dekat anda.</string> <string name="settings_showall_description">Tampilkan perangkat orang lain yang ada di dekat anda.</string>
<string name="settings_autopplay_label">Putar otomatis</string> <string name="settings_autopplay_label">Putar otomatis</string>
<string name="settings_autoplay_description">Mulai pemutaran audio saat perangkat dipakai.</string>
<string name="settings_fake_data_label">Data palsu</string> <string name="settings_fake_data_label">Data palsu</string>
<string name="settings_fake_data_description">Tampilkan data palsu, yaitu, mensimulasikan perangkat yang tidak ada.</string>
<string name="settings_debug_label">Pengaturan debug</string> <string name="settings_debug_label">Pengaturan debug</string>
<string name="settings_debug_description">Pengaturan tambahan untuk membantu memecahkan masalah dengan aplikasi.</string> <string name="settings_debug_description">Pengaturan tambahan untuk membantu memecahkan masalah dengan aplikasi.</string>
<string name="settings_signal_minimum_label">Kualitas sinyal minimum</string> <string name="settings_signal_minimum_label">Kualitas sinyal minimum</string>
@@ -118,6 +122,7 @@
<string name="overview_bluetooth_disabled_label">Bluetooth dinonaktifkan</string> <string name="overview_bluetooth_disabled_label">Bluetooth dinonaktifkan</string>
<string name="overview_bluetooth_disabled_description">Bluetooth dinonaktifkan, aktifkan terlebih dahulu ;)</string> <string name="overview_bluetooth_disabled_description">Bluetooth dinonaktifkan, aktifkan terlebih dahulu ;)</string>
<string name="help_translate_label">Terjemahan</string> <string name="help_translate_label">Terjemahan</string>
<string name="help_translate_description">Bantu terjemahkan applikasi ini ke bahasa favorit anda.</string>
<string name="settings_onepod_mode_label">Mode satu pod</string> <string name="settings_onepod_mode_label">Mode satu pod</string>
<string name="settings_onepod_mode_description">Menggunakan kedua pod tidak diperlukan, memakai satu pod sudah cukup untuk memicu reaksi.</string> <string name="settings_onepod_mode_description">Menggunakan kedua pod tidak diperlukan, memakai satu pod sudah cukup untuk memicu reaksi.</string>
<string name="permission_system_alert_window_label">Jendela Peringatan Sistem</string> <string name="permission_system_alert_window_label">Jendela Peringatan Sistem</string>
@@ -127,4 +132,6 @@
<string name="settings_blescanner_unfiltered_label">Data BLE yang tidak difilter</string> <string name="settings_blescanner_unfiltered_label">Data BLE yang tidak difilter</string>
<string name="settings_blescanner_unfiltered_description">Hapus semua filter dari pemindai BLE untuk menampilkan semua data BLE yang disiarkan. Berguna untuk menambahkan dukungan pada jenis headphone baru.</string> <string name="settings_blescanner_unfiltered_description">Hapus semua filter dari pemindai BLE untuk menampilkan semua data BLE yang disiarkan. Berguna untuk menambahkan dukungan pada jenis headphone baru.</string>
<string name="permission_required_title">Izin berikut diperlukan:</string> <string name="permission_required_title">Izin berikut diperlukan:</string>
<string name="settings_compatibility_mode_label">Mode kompatibilitas</string>
<string name="settings_compatibility_mode_description">Nonaktifkan pengoptimalan untuk meningkatkan kompatibilitas. Coba ini jika anda tidak melihat data apa pun.</string>
</resources> </resources>
+7
View File
@@ -81,6 +81,7 @@
<string name="settings_monitor_mode_label">Monitor modus</string> <string name="settings_monitor_mode_label">Monitor modus</string>
<string name="settings_monitor_mode_description">Onder welke omstandigheden houdt deze app Bluetooth-gegevens bij.</string> <string name="settings_monitor_mode_description">Onder welke omstandigheden houdt deze app Bluetooth-gegevens bij.</string>
<string name="settings_scanner_mode_label">Scannermodus</string> <string name="settings_scanner_mode_label">Scannermodus</string>
<string name="settings_scanner_mode_description">Moet de Bluetooth Low Energy-gegevensscanner prioriteit geven aan prestaties of energie besparen?</string>
<string name="settings_monitor_mode_manual_label">Wanneer app is geopend</string> <string name="settings_monitor_mode_manual_label">Wanneer app is geopend</string>
<string name="settings_monitor_mode_automatic_label">Wanneer het apparaat is aangesloten</string> <string name="settings_monitor_mode_automatic_label">Wanneer het apparaat is aangesloten</string>
<string name="settings_monitor_mode_always_label">Altijd</string> <string name="settings_monitor_mode_always_label">Altijd</string>
@@ -88,10 +89,13 @@
<string name="settings_scanner_mode_balanced_label">Gebalanceerd</string> <string name="settings_scanner_mode_balanced_label">Gebalanceerd</string>
<string name="settings_scanner_mode_lowlatency_label">Lage latentie</string> <string name="settings_scanner_mode_lowlatency_label">Lage latentie</string>
<string name="settings_autopause_label">Auto pauze</string> <string name="settings_autopause_label">Auto pauze</string>
<string name="settings_autopause_description">Pauzeer de muziek wanneer u het apparaat van uw oor haalt.</string>
<string name="settings_showall_label">Alle apparaten weergeven</string> <string name="settings_showall_label">Alle apparaten weergeven</string>
<string name="settings_showall_description">Toon apparaten van andere mensen die bij u in de buurt zijn.</string> <string name="settings_showall_description">Toon apparaten van andere mensen die bij u in de buurt zijn.</string>
<string name="settings_autopplay_label">Automatisch afspelen</string> <string name="settings_autopplay_label">Automatisch afspelen</string>
<string name="settings_autoplay_description">Start audioweergave wanneer het apparaat wordt gedragen.</string>
<string name="settings_fake_data_label">Nep gegevens</string> <string name="settings_fake_data_label">Nep gegevens</string>
<string name="settings_fake_data_description">Toon nepgegevens, d.w.z. simuleer apparaten die niet bestaan.</string>
<string name="settings_debug_label">Foutopsporingsinstellingen</string> <string name="settings_debug_label">Foutopsporingsinstellingen</string>
<string name="settings_debug_description">Aanvullende instellingen voor het oplossen van problemen met de app.</string> <string name="settings_debug_description">Aanvullende instellingen voor het oplossen van problemen met de app.</string>
<string name="settings_signal_minimum_label">Minimale signaalkwaliteit</string> <string name="settings_signal_minimum_label">Minimale signaalkwaliteit</string>
@@ -118,6 +122,7 @@
<string name="overview_bluetooth_disabled_label">Bluetooth is uitgeschakeld</string> <string name="overview_bluetooth_disabled_label">Bluetooth is uitgeschakeld</string>
<string name="overview_bluetooth_disabled_description">Bluetooth is uitgeschakeld, activeer het ;)</string> <string name="overview_bluetooth_disabled_description">Bluetooth is uitgeschakeld, activeer het ;)</string>
<string name="help_translate_label">Vertaling</string> <string name="help_translate_label">Vertaling</string>
<string name="help_translate_description">Help deze app te vertalen in uw favoriete taal.</string>
<string name="settings_onepod_mode_label">Eén pod-modus</string> <string name="settings_onepod_mode_label">Eén pod-modus</string>
<string name="settings_onepod_mode_description">Het dragen van beide pods is niet vereist, het dragen van een enkele pod is voldoende om reacties uit te lokken.</string> <string name="settings_onepod_mode_description">Het dragen van beide pods is niet vereist, het dragen van een enkele pod is voldoende om reacties uit te lokken.</string>
<string name="permission_system_alert_window_label">Systeemwaarschuwingsvenster</string> <string name="permission_system_alert_window_label">Systeemwaarschuwingsvenster</string>
@@ -127,4 +132,6 @@
<string name="settings_blescanner_unfiltered_label">Ongefilterde BLE-gegevens</string> <string name="settings_blescanner_unfiltered_label">Ongefilterde BLE-gegevens</string>
<string name="settings_blescanner_unfiltered_description">Verwijder eventuele filters van de BLE-scanner om alle uitgezonden BLE-gegevens weer te geven. Handig om ondersteuning toe te voegen voor nieuwe typen hoofdtelefoons.</string> <string name="settings_blescanner_unfiltered_description">Verwijder eventuele filters van de BLE-scanner om alle uitgezonden BLE-gegevens weer te geven. Handig om ondersteuning toe te voegen voor nieuwe typen hoofdtelefoons.</string>
<string name="permission_required_title">De volgende toestemming is vereist:</string> <string name="permission_required_title">De volgende toestemming is vereist:</string>
<string name="settings_compatibility_mode_label">Compatibiliteitsmodus</string>
<string name="settings_compatibility_mode_description">Schakel optimalisaties uit om de compatibiliteit te verbeteren. Probeer dit als u geen gegevens ziet.</string>
</resources> </resources>
+3 -3
View File
@@ -26,7 +26,7 @@
<string name="permission_bluetooth_label">蓝牙</string> <string name="permission_bluetooth_label">蓝牙</string>
<string name="permission_bluetooth_description">本应用需要“蓝牙”权限才能连接已配对的蓝牙设备</string> <string name="permission_bluetooth_description">本应用需要“蓝牙”权限才能连接已配对的蓝牙设备</string>
<string name="permission_access_fine_location_label">访问精准位置</string> <string name="permission_access_fine_location_label">访问精准位置</string>
<string name="permission_access_fine_location_description">CAPod 使用“精准定位”权限来接收低功耗蓝牙数据。您的耳机使用低功耗蓝牙技术来播其状态,本应用「不会」通过蓝牙数据来给您定位</string> <string name="permission_access_fine_location_description">CAPod 使用“精准定位”权限来接收低功耗蓝牙数据。您的耳机使用低功耗蓝牙技术来广播其状态,本应用「不会」通过蓝牙数据测定您的位置</string>
<string name="permission_background_location_label">在后台访问位置</string> <string name="permission_background_location_label">在后台访问位置</string>
<string name="permission_background_location_description">CAPod 使用“在后台访问位置”权限以在本应用被关闭时继续提供如“显示弹窗”、“自动连接”等功能。此权限允许本应用在后台运行时继续接收蓝牙低功耗数据。本应用并不使用蓝牙数据来窥探您的位置。</string> <string name="permission_background_location_description">CAPod 使用“在后台访问位置”权限以在本应用被关闭时继续提供如“显示弹窗”、“自动连接”等功能。此权限允许本应用在后台运行时继续接收蓝牙低功耗数据。本应用并不使用蓝牙数据来窥探您的位置。</string>
<string name="permission_ignore_battery_optimizations_label">禁用电池优化</string> <string name="permission_ignore_battery_optimizations_label">禁用电池优化</string>
@@ -79,7 +79,7 @@
<string name="settings_debug_mode_label">调试模式</string> <string name="settings_debug_mode_label">调试模式</string>
<string name="settings_debug_mode_description">显示额外信息以帮助调试故障。</string> <string name="settings_debug_mode_description">显示额外信息以帮助调试故障。</string>
<string name="settings_monitor_mode_label">监控模式</string> <string name="settings_monitor_mode_label">监控模式</string>
<string name="settings_monitor_mode_description">此模式下本应用将监视蓝牙数据。</string> <string name="settings_monitor_mode_description">决定本应用在何时收听蓝牙数据。</string>
<string name="settings_scanner_mode_label">扫描模式</string> <string name="settings_scanner_mode_label">扫描模式</string>
<string name="settings_scanner_mode_description">低功耗蓝牙数据扫描器应着重性能还是省电?</string> <string name="settings_scanner_mode_description">低功耗蓝牙数据扫描器应着重性能还是省电?</string>
<string name="settings_monitor_mode_manual_label">应用打开时</string> <string name="settings_monitor_mode_manual_label">应用打开时</string>
@@ -120,7 +120,7 @@
<string name="settings_popup_caseopen_label">显示弹窗</string> <string name="settings_popup_caseopen_label">显示弹窗</string>
<string name="settings_popup_caseopen_description">设备充电仓打开时显示弹窗(实验性)</string> <string name="settings_popup_caseopen_description">设备充电仓打开时显示弹窗(实验性)</string>
<string name="overview_bluetooth_disabled_label">蓝牙已禁用</string> <string name="overview_bluetooth_disabled_label">蓝牙已禁用</string>
<string name="overview_bluetooth_disabled_description">蓝牙已关闭,请启用之 ;)</string> <string name="overview_bluetooth_disabled_description">蓝牙已关闭,请启 ;)</string>
<string name="help_translate_label">翻译</string> <string name="help_translate_label">翻译</string>
<string name="help_translate_description">帮助翻译本应用</string> <string name="help_translate_description">帮助翻译本应用</string>
<string name="settings_onepod_mode_label">单耳模式</string> <string name="settings_onepod_mode_label">单耳模式</string>
+3 -3
View File
@@ -68,7 +68,7 @@
<string name="settings_support_installid_label">安裝 ID</string> <string name="settings_support_installid_label">安裝 ID</string>
<string name="settings_support_installid_desc">自動傳送的錯誤報告是匿名的。如果開發人員需要找出你的錯誤報告將分享你的安裝 ID。</string> <string name="settings_support_installid_desc">自動傳送的錯誤報告是匿名的。如果開發人員需要找出你的錯誤報告將分享你的安裝 ID。</string>
<string name="settings_support_label">支援</string> <string name="settings_support_label">支援</string>
<string name="settings_support_description">如果你需要助。</string> <string name="settings_support_description">如果你需要助。</string>
<string name="issue_tracker_label">問題追蹤器</string> <string name="issue_tracker_label">問題追蹤器</string>
<string name="issue_tracker_description">一個用於錯誤回報和功能需求的公用問題追蹤器 (僅英文)。</string> <string name="issue_tracker_description">一個用於錯誤回報和功能需求的公用問題追蹤器 (僅英文)。</string>
<string name="discord_label">Discord</string> <string name="discord_label">Discord</string>
@@ -97,7 +97,7 @@
<string name="settings_fake_data_label">假資料</string> <string name="settings_fake_data_label">假資料</string>
<string name="settings_fake_data_description">顯示假資料,模擬不存在的裝置。</string> <string name="settings_fake_data_description">顯示假資料,模擬不存在的裝置。</string>
<string name="settings_debug_label">偵錯設定</string> <string name="settings_debug_label">偵錯設定</string>
<string name="settings_debug_description">一個能助這個應用程式故障檢修的附加設定。</string> <string name="settings_debug_description">一個能助這個應用程式故障檢修的附加設定。</string>
<string name="settings_signal_minimum_label">最低訊號質量</string> <string name="settings_signal_minimum_label">最低訊號質量</string>
<string name="settings_signal_minimum_description">可以被認為是你的裝置的最低訊號質量。</string> <string name="settings_signal_minimum_description">可以被認為是你的裝置的最低訊號質量。</string>
<string name="settings_autoconnect_label">自動連線</string> <string name="settings_autoconnect_label">自動連線</string>
@@ -111,7 +111,7 @@
<string name="settings_maindevice_address_description">你的已配對裝置的位址。這個應用程式用該位址來確定何時連線到你的手機。</string> <string name="settings_maindevice_address_description">你的已配對裝置的位址。這個應用程式用該位址來確定何時連線到你的手機。</string>
<string name="settings_maindevice_address_none"></string> <string name="settings_maindevice_address_none"></string>
<string name="settings_maindevice_model_label">你的裝置型號</string> <string name="settings_maindevice_model_label">你的裝置型號</string>
<string name="settings_maindevice_model_description">你的主要裝置型號。這能助這個應用程式在未連線到你的手機時辨識你的裝置。</string> <string name="settings_maindevice_model_description">你的主要裝置型號。這能助這個應用程式在未連線到你的手機時辨識你的裝置。</string>
<string name="settings_reaction_autoconnect_whenseen_label">看到時</string> <string name="settings_reaction_autoconnect_whenseen_label">看到時</string>
<string name="settings_reaction_autoconnect_caseopen_label">充電盒開啟</string> <string name="settings_reaction_autoconnect_caseopen_label">充電盒開啟</string>
<string name="settings_reaction_autoconnect_inear_label">在耳中</string> <string name="settings_reaction_autoconnect_inear_label">在耳中</string>
+2
View File
@@ -144,4 +144,6 @@
<string name="permission_required_title">The following permission is required:</string> <string name="permission_required_title">The following permission is required:</string>
<string name="settings_compatibility_mode_label">Compatibility mode</string> <string name="settings_compatibility_mode_label">Compatibility mode</string>
<string name="settings_compatibility_mode_description">Disable optimizations to improve compatibility. Try this if you are not seeing any data.</string> <string name="settings_compatibility_mode_description">Disable optimizations to improve compatibility. Try this if you are not seeing any data.</string>
<string name="translators_thanks_title">Translators</string>
<string name="translators_thanks_description">darken</string>
</resources> </resources>
@@ -4,6 +4,15 @@
<PreferenceCategory <PreferenceCategory
android:title="@string/general_thank_you_label" android:title="@string/general_thank_you_label"
app:iconSpaceReserved="false"> app:iconSpaceReserved="false">
<Preference
android:key="thanks.translators"
android:summary="@string/translators_thanks_description"
android:title="@string/translators_thanks_title"
app:iconSpaceReserved="false">
<intent
android:action="android.intent.action.VIEW"
android:data="https://crowdin.com/project/capod/activity-stream" />
</Preference>
<Preference <Preference
android:key="thanks.maxpatchs" android:key="thanks.maxpatchs"
android:summary="Thanks for the lovely icons." android:summary="Thanks for the lovely icons."
@@ -3,6 +3,7 @@ package eu.darken.capod.pods.core.apple
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.airpods.AirPodsGen1 import eu.darken.capod.pods.core.apple.airpods.AirPodsGen1
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro import eu.darken.capod.pods.core.apple.airpods.AirPodsPro
import eu.darken.capod.pods.core.apple.misc.UnknownAppleDevice
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import io.kotest.matchers.types.instanceOf import io.kotest.matchers.types.instanceOf
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runBlockingTest
@@ -12,14 +13,14 @@ class AirPodsFactoryTest : BaseAirPodsTest() {
@Test @Test
fun `create AirPodsGen1`() = runBlockingTest { fun `create AirPodsGen1`() = runBlockingTest {
create<DualApplePods>("07 19 01 02 20 55 AA 56 31 00 00 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88") { create<DualAirPods>("07 19 01 02 20 55 AA 56 31 00 00 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88") {
this shouldBe instanceOf<AirPodsGen1>() this shouldBe instanceOf<AirPodsGen1>()
} }
} }
@Test @Test
fun `create AirPodsPro`() = runBlockingTest { fun `create AirPodsPro`() = runBlockingTest {
create<DualApplePods>("07 19 01 0E 20 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") { create<DualAirPods>("07 19 01 0E 20 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") {
this shouldBe instanceOf<AirPodsPro>() this shouldBe instanceOf<AirPodsPro>()
} }
} }
@@ -8,7 +8,7 @@ class BasicSingleApplePodsTest : BaseAirPodsTest() {
@Test @Test
fun `test mapping`() = runBlockingTest { fun `test mapping`() = runBlockingTest {
create<BasicSingleApplePods>("07 19 01 05 20 00 F5 0F 01 01 00 6D CE C0 04 22 0A 85 31 2D 82 6B 42 80 01 20 1A") { create<SingleApplePods>("07 19 01 05 20 00 F5 0F 01 01 00 6D CE C0 04 22 0A 85 31 2D 82 6B 42 80 01 20 1A") {
rawPrefix shouldBe 0x01.toUByte() rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0520.toUShort() rawDeviceModel shouldBe 0x0520.toUShort()
rawStatus shouldBe 0x00.toUByte() rawStatus shouldBe 0x00.toUByte()
@@ -23,7 +23,7 @@ class BasicSingleApplePodsTest : BaseAirPodsTest() {
@Test @Test
fun `test battery headset percent`() = runBlockingTest { fun `test battery headset percent`() = runBlockingTest {
create<BasicSingleApplePods>("07 19 01 05 20 00 F5 0F 01 01 00 6D CE C0 04 22 0A 85 31 2D 82 6B 42 80 01 20 1A") { create<SingleApplePods>("07 19 01 05 20 00 F5 0F 01 01 00 6D CE C0 04 22 0A 85 31 2D 82 6B 42 80 01 20 1A") {
batteryHeadsetPercent shouldBe 0.5f batteryHeadsetPercent shouldBe 0.5f
} }
} }
@@ -8,7 +8,7 @@ class DualApplePodsTest : BaseAirPodsTest() {
@Test @Test
fun `test bit mapping`() = runBlockingTest { fun `test bit mapping`() = runBlockingTest {
create<DualApplePods>("07 19 01 0E 20 54 AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { create<DualAirPods>("07 19 01 0E 20 54 AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
rawPrefix shouldBe 0x01.toUByte() rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0e20.toUShort() rawDeviceModel shouldBe 0x0e20.toUShort()
@@ -24,13 +24,13 @@ class DualApplePodsTest : BaseAirPodsTest() {
@Test @Test
fun `test AirPodDevice - active microphone`() = runBlockingTest { fun `test AirPodDevice - active microphone`() = runBlockingTest {
create<DualApplePods>("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") { create<DualAirPods>("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 // 00101011
// --^----- // --^-----
isLeftPodMicrophone shouldBe true isLeftPodMicrophone shouldBe true
isRightPodMicrophone shouldBe false isRightPodMicrophone shouldBe false
} }
create<DualApplePods>("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") { create<DualAirPods>("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 // 00001011
// --^----- // --^-----
isLeftPodMicrophone shouldBe false isLeftPodMicrophone shouldBe false
@@ -41,24 +41,24 @@ class DualApplePodsTest : BaseAirPodsTest() {
@Test @Test
fun `test AirPodDevice - left pod ear status`() = runBlockingTest { fun `test AirPodDevice - left pod ear status`() = runBlockingTest {
// Left Pod primary // Left Pod primary
create<DualApplePods>("07 19 01 0E 20 >22< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { create<DualAirPods>("07 19 01 0E 20 >22< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00100010 // 00100010
// 765432¹0 // 765432¹0
isLeftPodInEar shouldBe true isLeftPodInEar shouldBe true
} }
create<DualApplePods>("07 19 01 0E 20 >20< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { create<DualAirPods>("07 19 01 0E 20 >20< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00100000 // 00100000
// 765432¹0 // 765432¹0
isLeftPodInEar shouldBe false isLeftPodInEar shouldBe false
} }
// Right Pod is primary // Right Pod is primary
create<DualApplePods>("07 19 01 0E 20 >09< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { create<DualAirPods>("07 19 01 0E 20 >09< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00001001 // 00001001
// 7654³210 // 7654³210
isLeftPodInEar shouldBe true isLeftPodInEar shouldBe true
} }
create<DualApplePods>("07 19 01 0E 20 >20< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { create<DualAirPods>("07 19 01 0E 20 >20< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00000001 // 00000001
// 7654³210 // 7654³210
isLeftPodInEar shouldBe false isLeftPodInEar shouldBe false
@@ -68,24 +68,24 @@ class DualApplePodsTest : BaseAirPodsTest() {
@Test @Test
fun `test AirPodDevice - right pod ear status`() = runBlockingTest { fun `test AirPodDevice - right pod ear status`() = runBlockingTest {
// Left Pod primary // Left Pod primary
create<DualApplePods>("07 19 01 0E 20 >29< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { create<DualAirPods>("07 19 01 0E 20 >29< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00101001 // 00101001
// 7654³210 // 7654³210
isRightPodInEar shouldBe true isRightPodInEar shouldBe true
} }
create<DualApplePods>("07 19 01 0E 20 >21< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { create<DualAirPods>("07 19 01 0E 20 >21< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00100001 // 00100001
// 7654³210 // 7654³210
isRightPodInEar shouldBe false isRightPodInEar shouldBe false
} }
// Right Pod is primary // Right Pod is primary
create<DualApplePods>("07 19 01 0E 20 >03< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { create<DualAirPods>("07 19 01 0E 20 >03< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00000011 // 00000011
// 765432¹0 // 765432¹0
isRightPodInEar shouldBe true isRightPodInEar shouldBe true
} }
create<DualApplePods>("07 19 01 0E 20 >01< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { create<DualAirPods>("07 19 01 0E 20 >01< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00000001 // 00000001
// 765432¹0 // 765432¹0
isRightPodInEar shouldBe false isRightPodInEar shouldBe false
@@ -95,13 +95,13 @@ class DualApplePodsTest : BaseAirPodsTest() {
@Test @Test
fun `test AirPodDevice - battery status`() = runBlockingTest { fun `test AirPodDevice - battery status`() = runBlockingTest {
// Right Pod is primary // Right Pod is primary
create<DualApplePods>("07 19 01 0E 20 0B >98< 94 52 00 05 09 73 3C 3D F9 2C 3E B3 DD 76 02 DD 4E 16 FD FB") { create<DualAirPods>("07 19 01 0E 20 0B >98< 94 52 00 05 09 73 3C 3D F9 2C 3E B3 DD 76 02 DD 4E 16 FD FB") {
// 88 10001000 // 88 10001000
batteryLeftPodPercent shouldBe 0.9f batteryLeftPodPercent shouldBe 0.9f
batteryRightPodPercent shouldBe 0.8f batteryRightPodPercent shouldBe 0.8f
} }
// Left Pod primary // Left Pod primary
create<DualApplePods>("07 19 01 0E 20 2B >89< 94 52 00 05 09 73 3C 3D F9 2C 3E B3 DD 76 02 DD 4E 16 FD FB") { create<DualAirPods>("07 19 01 0E 20 2B >89< 94 52 00 05 09 73 3C 3D F9 2C 3E B3 DD 76 02 DD 4E 16 FD FB") {
// F8 11111000 // F8 11111000
batteryLeftPodPercent shouldBe 0.9f batteryLeftPodPercent shouldBe 0.9f
batteryRightPodPercent shouldBe 0.8f batteryRightPodPercent shouldBe 0.8f
@@ -114,13 +114,13 @@ class DualApplePodsTest : BaseAirPodsTest() {
* Right pod is charging * Right pod is charging
*/ */
// This is the left // This is the left
create<DualApplePods>("07 19 01 0E 20 51 89 >94< 52 00 00 F4 89 82 6D 3E 27 7F 26 62 57 D0 E2 A6 49 E9 35") { create<DualAirPods>("07 19 01 0E 20 51 89 >94< 52 00 00 F4 89 82 6D 3E 27 7F 26 62 57 D0 E2 A6 49 E9 35") {
// 1001 0100 // 1001 0100
isLeftPodCharging shouldBe false isLeftPodCharging shouldBe false
isRightPodCharging shouldBe true isRightPodCharging shouldBe true
} }
// This is the right // This is the right
create<DualApplePods>("07 19 01 0E 20 31 98 >A4< 01 00 00 31 B9 A0 C4 80 CD D1 CF B9 3A 9A 6D 48 31 08 EB") { create<DualAirPods>("07 19 01 0E 20 31 98 >A4< 01 00 00 31 B9 A0 C4 80 CD D1 CF B9 3A 9A 6D 48 31 08 EB") {
// 1010 0100 // 1010 0100
isLeftPodCharging shouldBe false isLeftPodCharging shouldBe false
isRightPodCharging shouldBe true isRightPodCharging shouldBe true
@@ -130,26 +130,26 @@ class DualApplePodsTest : BaseAirPodsTest() {
* Left pod is charging * Left pod is charging
*/ */
// This is the left // This is the left
create<DualApplePods>("07 19 01 0E 20 71 98 >94< 52 00 05 A5 37 31 B2 BD 42 68 0C 64 FD 00 99 4A E5 3E F4") { create<DualAirPods>("07 19 01 0E 20 71 98 >94< 52 00 05 A5 37 31 B2 BD 42 68 0C 64 FD 00 99 4A E5 3E F4") {
// 1001 0100 // 1001 0100
isLeftPodCharging shouldBe true isLeftPodCharging shouldBe true
isRightPodCharging shouldBe false isRightPodCharging shouldBe false
} }
// This is the right // This is the right
create<DualApplePods>("07 19 01 0E 20 11 89 >A4< 04 00 04 BA 79 1B C0 65 69 C6 9F 19 6E 37 7D 6D 86 8D D9") { create<DualAirPods>("07 19 01 0E 20 11 89 >A4< 04 00 04 BA 79 1B C0 65 69 C6 9F 19 6E 37 7D 6D 86 8D D9") {
// 1010 0100 // 1010 0100
isLeftPodCharging shouldBe true isLeftPodCharging shouldBe true
isRightPodCharging shouldBe false isRightPodCharging shouldBe false
} }
// Both charging // Both charging
create<DualApplePods>("07 19 01 0E 20 55 88 >B4< 59 00 05 4B FC DF 68 28 A5 45 52 65 9C FE 51 86 3A B5 DB") { create<DualAirPods>("07 19 01 0E 20 55 88 >B4< 59 00 05 4B FC DF 68 28 A5 45 52 65 9C FE 51 86 3A B5 DB") {
// 1011 0100 // 1011 0100
isLeftPodCharging shouldBe true isLeftPodCharging shouldBe true
isRightPodCharging shouldBe true isRightPodCharging shouldBe true
} }
// Both not charging // Both not charging
create<DualApplePods>("07 19 01 0E 20 00 F8 >8F< 03 00 05 4C 0F A0 C4 05 24 DD EB AF 92 99 FD 54 B1 06 48") { create<DualAirPods>("07 19 01 0E 20 00 F8 >8F< 03 00 05 4C 0F A0 C4 05 24 DD EB AF 92 99 FD 54 B1 06 48") {
// 1000 1111 // 1000 1111
isLeftPodCharging shouldBe false isLeftPodCharging shouldBe false
isRightPodCharging shouldBe false isRightPodCharging shouldBe false
@@ -158,11 +158,11 @@ class DualApplePodsTest : BaseAirPodsTest() {
@Test @Test
fun `test AirPodDevice - case charging`() = runBlockingTest { fun `test AirPodDevice - case charging`() = runBlockingTest {
create<DualApplePods>("07 19 01 0E 20 75 99 >B4< 31 00 05 77 C8 BA 0C 4E 1F BE AD 70 C5 40 71 D2 E9 17 A2") { create<DualAirPods>("07 19 01 0E 20 75 99 >B4< 31 00 05 77 C8 BA 0C 4E 1F BE AD 70 C5 40 71 D2 E9 17 A2") {
// 0011 0011 // 0011 0011
isCaseCharging shouldBe false isCaseCharging shouldBe false
} }
create<DualApplePods>("07 19 01 0E 20 75 A9 >F4< 51 00 05 A0 37 92 35 49 79 CC DC 27 94 8E FB 72 12 94 52") { create<DualAirPods>("07 19 01 0E 20 75 A9 >F4< 51 00 05 A0 37 92 35 49 79 CC DC 27 94 8E FB 72 12 94 52") {
// 0101 0011 // 0101 0011
isCaseCharging shouldBe true isCaseCharging shouldBe true
} }
@@ -171,75 +171,75 @@ class DualApplePodsTest : BaseAirPodsTest() {
@Test @Test
fun `test AirPodDevice - case lid test`() = runBlockingTest { fun `test AirPodDevice - case lid test`() = runBlockingTest {
// Lid open // Lid open
create<DualApplePods>("07 19 01 0E 20 55 AA B4 >31< 00 00 A1 D0 BD 82 D3 52 86 CA FC 11 62 DC 42 C6 92 8E") { create<DualAirPods>("07 19 01 0E 20 55 AA B4 >31< 00 00 A1 D0 BD 82 D3 52 86 CA FC 11 62 DC 42 C6 92 8E") {
// 31 0011 0001 // 31 0011 0001
caseLidState shouldBe DualApplePods.LidState.OPEN caseLidState shouldBe DualAirPods.LidState.OPEN
} }
// Lid open, left pod in case // Lid open, left pod in case
create<DualApplePods>("07 19 01 0E 20 51 9A 93 >31< 00 00 95 D0 A5 D7 E3 F4 F1 38 38 99 61 3B 57 95 37 B7") { create<DualAirPods>("07 19 01 0E 20 51 9A 93 >31< 00 00 95 D0 A5 D7 E3 F4 F1 38 38 99 61 3B 57 95 37 B7") {
// 31 0011 0001 // 31 0011 0001
caseLidState shouldBe DualApplePods.LidState.OPEN caseLidState shouldBe DualAirPods.LidState.OPEN
} }
// Lid open, left pod in case // Lid open, left pod in case
create<DualApplePods>("07 19 01 0E 20 71 A9 92 31 00 00 DB 48 7F 32 8C CE 80 6F D9 27 98 D6 76 45 9D 62") { create<DualAirPods>("07 19 01 0E 20 71 A9 92 31 00 00 DB 48 7F 32 8C CE 80 6F D9 27 98 D6 76 45 9D 62") {
// 31 0011 0001 // 31 0011 0001
caseLidState shouldBe DualApplePods.LidState.OPEN caseLidState shouldBe DualAirPods.LidState.OPEN
} }
// Lid just closed // Lid just closed
create<DualApplePods>("07 19 01 0E 20 55 AA B4 >39< 00 00 08 A6 DB 99 E0 5E 14 85 E5 C2 0B 68 D7 FF C3 A1") { create<DualAirPods>("07 19 01 0E 20 55 AA B4 >39< 00 00 08 A6 DB 99 E0 5E 14 85 E5 C2 0B 68 D7 FF C3 A1") {
// 39 0011 1001 // 39 0011 1001
caseLidState shouldBe DualApplePods.LidState.CLOSED caseLidState shouldBe DualAirPods.LidState.CLOSED
} }
// Lid closed // Lid closed
create<DualApplePods>("07 19 01 0E 20 55 AA B4 38 00 00 F3 F7 08 3B 98 09 C0 DD E4 BD BD 84 55 56 8B 81") { create<DualAirPods>("07 19 01 0E 20 55 AA B4 38 00 00 F3 F7 08 3B 98 09 C0 DD E4 BD BD 84 55 56 8B 81") {
// 38 0011 1000 // 38 0011 1000
caseLidState shouldBe DualApplePods.LidState.CLOSED caseLidState shouldBe DualAirPods.LidState.CLOSED
} }
// Lid closed, right pod in case // Lid closed, right pod in case
create<DualApplePods>("07 19 01 0E 20 51 9A 93 >38< 00 00 3A D8 85 76 B0 91 48 31 DA FF 6C 4A 2B C2 67 F4") { create<DualAirPods>("07 19 01 0E 20 51 9A 93 >38< 00 00 3A D8 85 76 B0 91 48 31 DA FF 6C 4A 2B C2 67 F4") {
// 38 0011 1000 // 38 0011 1000
caseLidState shouldBe DualApplePods.LidState.CLOSED caseLidState shouldBe DualAirPods.LidState.CLOSED
} }
// Lid closed, left pod in case // Lid closed, left pod in case
create<DualApplePods>("07 19 01 0E 20 71 A9 92 38 00 00 44 91 C4 8B 85 98 DD 55 4E 6A CA BC B5 CA 8D 37") { create<DualAirPods>("07 19 01 0E 20 71 A9 92 38 00 00 44 91 C4 8B 85 98 DD 55 4E 6A CA BC B5 CA 8D 37") {
// 38 0011 1000 // 38 0011 1000
caseLidState shouldBe DualApplePods.LidState.CLOSED caseLidState shouldBe DualAirPods.LidState.CLOSED
} }
} }
@Test @Test
fun `test AirPodDevice - connection state`() = runBlockingTest { fun `test AirPodDevice - connection state`() = runBlockingTest {
// Disconnected // Disconnected
create<DualApplePods>("07 19 01 0E 20 2B AA 8F 01 00 >00< 62 D4 BB F1 A7 F8 64 98 D2 C8 BD 7B 3A EF 2E 15") { create<DualAirPods>("07 19 01 0E 20 2B AA 8F 01 00 >00< 62 D4 BB F1 A7 F8 64 98 D2 C8 BD 7B 3A EF 2E 15") {
// 31 0011 0001 // 31 0011 0001
connectionState shouldBe DualApplePods.ConnectionState.DISCONNECTED state shouldBe DualAirPods.ConnectionState.DISCONNECTED
} }
// Connected idle // Connected idle
create<DualApplePods>("07 19 01 0E 20 2B AA 8F 01 00 >04< 1D 69 69 9C C2 51 F3 1F BF 6E 45 DA 90 4A A3 E3") { create<DualAirPods>("07 19 01 0E 20 2B AA 8F 01 00 >04< 1D 69 69 9C C2 51 F3 1F BF 6E 45 DA 90 4A A3 E3") {
// 39 0011 1001 // 39 0011 1001
connectionState shouldBe DualApplePods.ConnectionState.IDLE state shouldBe DualAirPods.ConnectionState.IDLE
} }
// Connected and playing music // Connected and playing music
create<DualApplePods>("07 19 01 0E 20 2B A9 8F 01 00 >05< 14 F7 CB 49 9F D3 B3 22 77 D2 22 F1 74 8C AC A6") { create<DualAirPods>("07 19 01 0E 20 2B A9 8F 01 00 >05< 14 F7 CB 49 9F D3 B3 22 77 D2 22 F1 74 8C AC A6") {
// 38 0011 1000 // 38 0011 1000
connectionState shouldBe DualApplePods.ConnectionState.MUSIC state shouldBe DualAirPods.ConnectionState.MUSIC
} }
// Connected and call active // Connected and call active
create<DualApplePods>("07 19 01 0E 20 2B 99 8F 01 00 >06< 0F 4B 43 25 E0 4A 73 63 14 22 C2 3C 89 13 BD 97") { create<DualAirPods>("07 19 01 0E 20 2B 99 8F 01 00 >06< 0F 4B 43 25 E0 4A 73 63 14 22 C2 3C 89 13 BD 97") {
// 38 0011 1000 // 38 0011 1000
connectionState shouldBe DualApplePods.ConnectionState.CALL state shouldBe DualAirPods.ConnectionState.CALL
} }
// Connected and call active // Connected and call active
create<DualApplePods>("07 19 01 0E 20 2B 99 8F 01 00 >07< E7 DF 76 44 85 B5 30 F4 95 14 02 DC A1 A4 8A 09") { create<DualAirPods>("07 19 01 0E 20 2B 99 8F 01 00 >07< E7 DF 76 44 85 B5 30 F4 95 14 02 DC A1 A4 8A 09") {
// 38 0011 1000 // 38 0011 1000
connectionState shouldBe DualApplePods.ConnectionState.RINGING state shouldBe DualAirPods.ConnectionState.RINGING
} }
// Switching? // Switching?
create<DualApplePods>("07 19 01 0E 20 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") { create<DualAirPods>("07 19 01 0E 20 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") {
// 38 0011 1000 // 38 0011 1000
connectionState shouldBe DualApplePods.ConnectionState.HANGING_UP state shouldBe DualAirPods.ConnectionState.HANGING_UP
} }
} }
@@ -1,7 +1,8 @@
package eu.darken.capod.pods.core.apple.airpods package eu.darken.capod.pods.core.apple.airpods
import eu.darken.capod.pods.core.apple.BaseAirPodsTest import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.DualApplePods import eu.darken.capod.pods.core.apple.DualAirPods
import eu.darken.capod.pods.core.apple.HasAppleColor
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runBlockingTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@@ -33,11 +34,11 @@ class AirPodsGen1Test : BaseAirPodsTest() {
isRightPodInEar shouldBe false isRightPodInEar shouldBe false
batteryCasePercent shouldBe 0.6f batteryCasePercent shouldBe 0.6f
caseLidState shouldBe DualApplePods.LidState.OPEN caseLidState shouldBe DualAirPods.LidState.OPEN
connectionState shouldBe DualApplePods.ConnectionState.DISCONNECTED state shouldBe DualAirPods.ConnectionState.DISCONNECTED
deviceColor shouldBe DualApplePods.DeviceColor.WHITE podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
} }
} }
} }
@@ -1,7 +1,8 @@
package eu.darken.capod.pods.core.apple.airpods package eu.darken.capod.pods.core.apple.airpods
import eu.darken.capod.pods.core.apple.BaseAirPodsTest import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.DualApplePods import eu.darken.capod.pods.core.apple.DualAirPods
import eu.darken.capod.pods.core.apple.HasAppleColor
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runBlockingTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@@ -32,11 +33,11 @@ class AirPodsGen2Test : BaseAirPodsTest() {
isRightPodInEar shouldBe true isRightPodInEar shouldBe true
batteryCasePercent shouldBe null batteryCasePercent shouldBe null
caseLidState shouldBe DualApplePods.LidState.NOT_IN_CASE caseLidState shouldBe DualAirPods.LidState.NOT_IN_CASE
connectionState shouldBe DualApplePods.ConnectionState.MUSIC state shouldBe DualAirPods.ConnectionState.MUSIC
deviceColor shouldBe DualApplePods.DeviceColor.WHITE podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
} }
} }
} }
@@ -1,7 +1,8 @@
package eu.darken.capod.pods.core.apple.airpods package eu.darken.capod.pods.core.apple.airpods
import eu.darken.capod.pods.core.apple.BaseAirPodsTest import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.DualApplePods import eu.darken.capod.pods.core.apple.DualAirPods
import eu.darken.capod.pods.core.apple.HasAppleColor
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runBlockingTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@@ -32,11 +33,11 @@ class AirPodsGen3Test : BaseAirPodsTest() {
isRightPodInEar shouldBe false isRightPodInEar shouldBe false
batteryCasePercent shouldBe 0.9f batteryCasePercent shouldBe 0.9f
caseLidState shouldBe DualApplePods.LidState.OPEN caseLidState shouldBe DualAirPods.LidState.OPEN
connectionState shouldBe DualApplePods.ConnectionState.IDLE state shouldBe DualAirPods.ConnectionState.IDLE
deviceColor shouldBe DualApplePods.DeviceColor.WHITE podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
} }
} }
@@ -54,11 +55,11 @@ class AirPodsGen3Test : BaseAirPodsTest() {
isRightPodInEar shouldBe true isRightPodInEar shouldBe true
batteryCasePercent shouldBe null batteryCasePercent shouldBe null
caseLidState shouldBe DualApplePods.LidState.NOT_IN_CASE caseLidState shouldBe DualAirPods.LidState.NOT_IN_CASE
connectionState shouldBe DualApplePods.ConnectionState.UNKNOWN state shouldBe DualAirPods.ConnectionState.UNKNOWN
deviceColor shouldBe DualApplePods.DeviceColor.WHITE podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
} }
} }
} }
@@ -1,7 +1,7 @@
package eu.darken.capod.pods.core.apple.airpods package eu.darken.capod.pods.core.apple.airpods
import eu.darken.capod.pods.core.apple.BaseAirPodsTest import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.DualApplePods import eu.darken.capod.pods.core.apple.HasAppleColor
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runBlockingTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@@ -33,7 +33,7 @@ class AirPodsProTest : BaseAirPodsTest() {
isLeftPodCharging shouldBe true isLeftPodCharging shouldBe true
batteryCasePercent shouldBe 0.5f batteryCasePercent shouldBe 0.5f
deviceColor shouldBe DualApplePods.DeviceColor.WHITE podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
} }
} }
@@ -50,8 +50,6 @@ class AirPodsProTest : BaseAirPodsTest() {
isRightPodCharging shouldBe false isRightPodCharging shouldBe false
isLeftPodCharging shouldBe false isLeftPodCharging shouldBe false
batteryCasePercent shouldBe null batteryCasePercent shouldBe null
deviceColor shouldBe DualApplePods.DeviceColor.WHITE
} }
} }
@@ -66,8 +64,6 @@ class AirPodsProTest : BaseAirPodsTest() {
isRightPodCharging shouldBe false isRightPodCharging shouldBe false
isLeftPodCharging shouldBe false isLeftPodCharging shouldBe false
batteryCasePercent shouldBe null batteryCasePercent shouldBe null
deviceColor shouldBe DualApplePods.DeviceColor.WHITE
} }
create<AirPodsPro>("0719010e202b668f01000500000000000000000000000000000000") { create<AirPodsPro>("0719010e202b668f01000500000000000000000000000000000000") {
@@ -78,8 +74,6 @@ class AirPodsProTest : BaseAirPodsTest() {
isRightPodCharging shouldBe false isRightPodCharging shouldBe false
isLeftPodCharging shouldBe false isLeftPodCharging shouldBe false
batteryCasePercent shouldBe null batteryCasePercent shouldBe null
deviceColor shouldBe DualApplePods.DeviceColor.WHITE
} }
create<AirPodsPro>("0719010e202b668f01000400000000000000000000000000000000") { create<AirPodsPro>("0719010e202b668f01000400000000000000000000000000000000") {
@@ -90,8 +84,6 @@ class AirPodsProTest : BaseAirPodsTest() {
isRightPodCharging shouldBe false isRightPodCharging shouldBe false
isLeftPodCharging shouldBe false isLeftPodCharging shouldBe false
batteryCasePercent shouldBe null batteryCasePercent shouldBe null
deviceColor shouldBe DualApplePods.DeviceColor.WHITE
} }
create<AirPodsPro>("0719010e200b668f01000500000000000000000000000000000000") { create<AirPodsPro>("0719010e200b668f01000500000000000000000000000000000000") {
@@ -102,8 +94,6 @@ class AirPodsProTest : BaseAirPodsTest() {
isRightPodCharging shouldBe false isRightPodCharging shouldBe false
isLeftPodCharging shouldBe false isLeftPodCharging shouldBe false
batteryCasePercent shouldBe null batteryCasePercent shouldBe null
deviceColor shouldBe DualApplePods.DeviceColor.WHITE
} }
create<AirPodsPro>("0719010e2003668f01000500000000000000000000000000000000") { create<AirPodsPro>("0719010e2003668f01000500000000000000000000000000000000") {
@@ -114,8 +104,6 @@ class AirPodsProTest : BaseAirPodsTest() {
isRightPodCharging shouldBe false isRightPodCharging shouldBe false
isLeftPodCharging shouldBe false isLeftPodCharging shouldBe false
batteryCasePercent shouldBe null batteryCasePercent shouldBe null
deviceColor shouldBe DualApplePods.DeviceColor.WHITE
} }
create<AirPodsPro>("0719010e2001668f01000500000000000000000000000000000000") { create<AirPodsPro>("0719010e2001668f01000500000000000000000000000000000000") {
@@ -126,8 +114,6 @@ class AirPodsProTest : BaseAirPodsTest() {
isRightPodCharging shouldBe false isRightPodCharging shouldBe false
isLeftPodCharging shouldBe false isLeftPodCharging shouldBe false
batteryCasePercent shouldBe null batteryCasePercent shouldBe null
deviceColor shouldBe DualApplePods.DeviceColor.WHITE
} }
create<AirPodsPro>("0719010e2009668f01000500000000000000000000000000000000") { create<AirPodsPro>("0719010e2009668f01000500000000000000000000000000000000") {
@@ -138,8 +124,6 @@ class AirPodsProTest : BaseAirPodsTest() {
isRightPodCharging shouldBe false isRightPodCharging shouldBe false
isLeftPodCharging shouldBe false isLeftPodCharging shouldBe false
batteryCasePercent shouldBe null batteryCasePercent shouldBe null
deviceColor shouldBe DualApplePods.DeviceColor.WHITE
} }
create<AirPodsPro>("0719010e2053669653000500000000000000000000000000000000") { create<AirPodsPro>("0719010e2053669653000500000000000000000000000000000000") {
@@ -150,8 +134,6 @@ class AirPodsProTest : BaseAirPodsTest() {
isRightPodCharging shouldBe true isRightPodCharging shouldBe true
isLeftPodCharging shouldBe false isLeftPodCharging shouldBe false
batteryCasePercent shouldBe 0.6f batteryCasePercent shouldBe 0.6f
deviceColor shouldBe DualApplePods.DeviceColor.WHITE
} }
create<AirPodsPro>("0719010e203366a602000500000000000000000000000000000000") { create<AirPodsPro>("0719010e203366a602000500000000000000000000000000000000") {
@@ -162,8 +144,6 @@ class AirPodsProTest : BaseAirPodsTest() {
isRightPodCharging shouldBe true isRightPodCharging shouldBe true
isLeftPodCharging shouldBe false isLeftPodCharging shouldBe false
batteryCasePercent shouldBe 0.6f batteryCasePercent shouldBe 0.6f
deviceColor shouldBe DualApplePods.DeviceColor.WHITE
} }
create<AirPodsPro>("0719010e202b768f02000500000000000000000000000000000000") { create<AirPodsPro>("0719010e202b768f02000500000000000000000000000000000000") {
@@ -174,8 +154,6 @@ class AirPodsProTest : BaseAirPodsTest() {
isRightPodCharging shouldBe false isRightPodCharging shouldBe false
isLeftPodCharging shouldBe false isLeftPodCharging shouldBe false
batteryCasePercent shouldBe 0.6f batteryCasePercent shouldBe 0.6f
deviceColor shouldBe DualApplePods.DeviceColor.WHITE
} }
} }
@@ -190,8 +168,6 @@ class AirPodsProTest : BaseAirPodsTest() {
isRightPodCharging shouldBe true isRightPodCharging shouldBe true
isLeftPodCharging shouldBe false isLeftPodCharging shouldBe false
batteryCasePercent shouldBe 0.6f batteryCasePercent shouldBe 0.6f
deviceColor shouldBe DualApplePods.DeviceColor.WHITE
} }
} }
} }
@@ -1,7 +1,7 @@
package eu.darken.capod.pods.core.apple.beats package eu.darken.capod.pods.core.apple.beats
import eu.darken.capod.pods.core.apple.BaseAirPodsTest import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.DualApplePods import eu.darken.capod.pods.core.apple.HasAppleColor
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runBlockingTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@@ -33,7 +33,7 @@ class PowerBeatsProTest : BaseAirPodsTest() {
isLeftPodCharging shouldBe true isLeftPodCharging shouldBe true
batteryCasePercent shouldBe 0.5f batteryCasePercent shouldBe 0.5f
deviceColor shouldBe DualApplePods.DeviceColor.WHITE podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
} }
} }
} }
@@ -1,7 +1,6 @@
package eu.darken.capod.pods.core.apple.fakes package eu.darken.capod.pods.core.apple.misc
import eu.darken.capod.pods.core.apple.BaseAirPodsTest import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import eu.darken.capod.pods.core.apple.DualApplePods
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.test.runBlockingTest
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
@@ -25,18 +24,8 @@ class Twsi99999Test : BaseAirPodsTest() {
batteryRightPodPercent shouldBe 1.0f batteryRightPodPercent shouldBe 1.0f
isCaseCharging shouldBe false isCaseCharging shouldBe false
isLeftPodCharging shouldBe true
isRightPodCharging shouldBe true
isLeftPodInEar shouldBe false
isRightPodInEar shouldBe false
batteryCasePercent shouldBe 0.7f batteryCasePercent shouldBe 0.7f
caseLidState shouldBe DualApplePods.LidState.OPEN
connectionState shouldBe DualApplePods.ConnectionState.UNKNOWN
deviceColor shouldBe DualApplePods.DeviceColor.WHITE
} }
} }
} }
@@ -0,0 +1,31 @@
package eu.darken.capod.pods.core.apple.misc
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.test.runBlockingTest
import org.junit.jupiter.api.Test
class VarunrAirPodsProTest : BaseAirPodsTest() {
@Test
fun `guessed data`() = runBlockingTest {
create<VarunrAirPodsPro>("07 13 01 0E 20 71 AA 37 36 00 10 00 FF 64 FF 00 00 00 00 00 00") {
rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0E20.toUShort()
rawStatus shouldBe 0x71.toUByte()
rawPodsBattery shouldBe 0xAA.toUByte()
rawFlags shouldBe 0x3.toUShort()
rawCaseBattery shouldBe 0x7.toUShort()
rawCaseLidState shouldBe 0x36.toUByte()
rawDeviceColor shouldBe 0x00.toUByte()
rawSuffix shouldBe 0x10.toUByte()
batteryLeftPodPercent shouldBe 1.0f
batteryRightPodPercent shouldBe 1.0f
isCaseCharging shouldBe false
batteryCasePercent shouldBe 0.7f
}
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ buildscript {
'version' : [ 'version' : [
'major': 1, 'major': 1,
'minor': 3, 'minor': 3,
'patch': 6, 'patch': 8,
'build': 0, 'build': 0,
], ],
] ]
+1 -1
View File
@@ -11,7 +11,7 @@
# This is the minimum version number required. # This is the minimum version number required.
# Update this, if you use features of a newer version # Update this, if you use features of a newer version
fastlane_version "2.137.0" fastlane_version "2.204.3"
default_platform :android default_platform :android
+16 -18
View File
@@ -1,21 +1,15 @@
fastlane documentation fastlane documentation
================ ----
# Installation # Installation
Make sure you have the latest version of the Xcode command line tools installed: Make sure you have the latest version of the Xcode command line tools installed:
``` ```sh
xcode-select --install xcode-select --install
``` ```
Install _fastlane_ using For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
```
[sudo] gem install fastlane -NV
```
or alternatively using `brew cask install fastlane`
# Available Actions # Available Actions
@@ -23,24 +17,28 @@ or alternatively using `brew cask install fastlane`
### android beta ### android beta
```sh
[bundle exec] fastlane android beta
``` ```
fastlane android beta
```
### android production ### android production
``` ```sh
fastlane android production [bundle exec] fastlane android production
``` ```
### android listing_only ### android listing_only
``` ```sh
fastlane android listing_only [bundle exec] fastlane android listing_only
``` ```
---- ----
This README.md is auto-generated and will be re-generated every time [fastlane](https://fastlane.tools) is run. More This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
information about fastlane can be found on [fastlane.tools](https://fastlane.tools). The documentation of fastlane can
be found on [docs.fastlane.tools](https://docs.fastlane.tools). More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
+1 -1
View File
@@ -1 +1 @@
CAPod - Acompanyant per als AirPods CAPod - Assistent pels AirPods
+1 -1
View File
@@ -1 +1 @@
CAPod - Aplicación complementaria para AirPods CAPod - Asistente para AirPods
+1 -1
View File
@@ -1 +1 @@
A CAPod az AirPods kísérőalkalmazása CAPod - Companion for AirPods
+1 -1
View File
@@ -1 +1 @@
CAPod - Pendamping untuk AirPods CAPod - Pendamping AirPods
+1 -1
View File
@@ -1 +1 @@
CAPod - Companion app per le Airpods CAPod - Companion for AirPods
+1 -1
View File
@@ -1 +1 @@
CAPod - Complemento para AirPods CAPod - Companion for AirPods
+1 -1
View File
@@ -1 +1 @@
CAPod - Bạn đồng hành cho AirPods CAPod - Companion for AirPods
@@ -6,7 +6,7 @@ CAPod 是为 AirPods 提供配套功能的一个应用。
* 有关连接、麦克风和充电仓的附加信息。 * 有关连接、麦克风和充电仓的附加信息。
* 能够接收和显示位于附近的全部设备。 * 能够接收和显示位于附近的全部设备。
* 佩戴/取下时自动开始/暂停音乐播放。 * 佩戴/取下时自动开始/暂停音乐播放。
* Automatically connect phone and AirPods. * 自动连接手机与 AirPods
* 充电仓打开时显示弹窗。 * 充电仓打开时显示弹窗。
CAPod 是一款免费共享软件。 某些功能需在应用内购买。 CAPod 是一款免费共享软件。 某些功能需在应用内购买。
+4
View File
@@ -7,5 +7,9 @@ rm -rv ./metadata/android/sq-AL
rm -rv ./metadata/android/uz rm -rv ./metadata/android/uz
rm -rv ./metadata/android/pcm-NG rm -rv ./metadata/android/pcm-NG
rm -rv ./metadata/android/tl-PH rm -rv ./metadata/android/tl-PH
rm -rv ./metadata/android/ku-TR
rm -rv ./metadata/android/kmr-TR
rm -rv ./metadata/android/ur-IN
rm -rv ./metadata/android/zu
find ./metadata/android -empty -type d -delete find ./metadata/android -empty -type d -delete
exit 0 exit 0