Compare commits

..
6 Commits
Author SHA1 Message Date
darken b4d9316665 Add translation scalfolding. 2022-01-29 20:26:15 +01:00
darken a234ac0dd2 Add crowdin scripts 2022-01-29 20:23:06 +01:00
darken 3fc18bc40b Improve sorting and heuristics. 2022-01-29 20:00:04 +01:00
darken 0f924ecb82 Track first time a device is seen and display it. 2022-01-29 18:00:13 +01:00
darken 6bd350ce42 Refactor fake data into extra class 2022-01-29 16:41:50 +01:00
darken e1b4ab14e4 Version bump 2022-01-27 23:01:01 +01:00
331 changed files with 2064 additions and 143 deletions
@@ -6,8 +6,6 @@ import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings import android.bluetooth.le.ScanSettings
import android.content.Context import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import eu.darken.capod.common.SystemClockWrap
import eu.darken.capod.common.debug.autoreport.DebugSettings
import eu.darken.capod.common.debug.logging.Logging.Priority.* import eu.darken.capod.common.debug.logging.Logging.Priority.*
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
@@ -27,7 +25,7 @@ import javax.inject.Singleton
class BleScanner @Inject constructor( class BleScanner @Inject constructor(
@ApplicationContext private val context: Context, @ApplicationContext private val context: Context,
private val bluetoothManager: BluetoothManager2, private val bluetoothManager: BluetoothManager2,
private val debugSettings: DebugSettings, private val fakeBleData: FakeBleData,
) { ) {
fun scan( fun scan(
@@ -108,58 +106,7 @@ class BleScanner @Inject constructor(
log(TAG, INFO) { "BleScanner stopped" } log(TAG, INFO) { "BleScanner stopped" }
} }
} }
.map { origs -> .map { fakeBleData.maybeAddfakeData(it) }
val fakeDevices = mutableListOf<BleScanResult>()
if (debugSettings.showFakeData.value) {
// AirPods Pro
BleScanResult(
address = "78:73:AF:B4:85:5E",
rssi = -48,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
manufacturerSpecificData = mapOf(76 to "07 19 01 0E 20 75 AA B6 31 00 05 9C 5A A4 5D C0 2C A0 B4 6F B9 ED 8E CE 03 97 CA".hexToByteArray())
).run { fakeDevices.add(this) }
// AirPods Gen1
BleScanResult(
address = "4E:9E:D1:49:D2:6D",
rssi = -55,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 200,
manufacturerSpecificData = mapOf(76 to "07 19 01 02 20 55 AF 56 31 00 06 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88".hexToByteArray())
).run { fakeDevices.add(this) }
// AirPods Max
BleScanResult(
address = "7E:E5:C7:65:D2:B5",
rssi = -30,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 300,
manufacturerSpecificData = mapOf(76 to "07 19 01 0A 20 02 05 80 04 0F 44 A7 60 9B F8 3C FD B1 D8 1C 61 EA 82 60 A3 2C 4E".hexToByteArray())
).run { fakeDevices.add(this) }
// BeatsFlex
BleScanResult(
address = "5E:9E:D1:49:D2:6D",
rssi = -59,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
manufacturerSpecificData = mapOf(76 to "07 19 01 10 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0".hexToByteArray())
).run { fakeDevices.add(this) }
// Unknown Device
BleScanResult(
address = "6E:9E:D1:49:D2:6D",
rssi = -60,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 500,
manufacturerSpecificData = mapOf(76 to "07 19 01 FF 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0".hexToByteArray())
).run { fakeDevices.add(this) }
}
origs + fakeDevices
}
fun String.hexToByteArray(): ByteArray {
val trimmed = this
.replace(" ", "")
.replace(">", "")
.replace("<", "")
require(trimmed.length % 2 == 0) { "Not a HEX string" }
return trimmed.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
}
companion object { companion object {
private val TAG = logTag("Bluetooth", "BleScanner") private val TAG = logTag("Bluetooth", "BleScanner")
@@ -0,0 +1,103 @@
package eu.darken.capod.common.bluetooth
import dagger.Reusable
import eu.darken.capod.common.SystemClockWrap
import eu.darken.capod.common.debug.autoreport.DebugSettings
import javax.inject.Inject
import kotlin.random.Random
@Reusable
class FakeBleData @Inject constructor(
private val debugSettings: DebugSettings,
) {
fun maybeAddfakeData(originals: List<BleScanResult>): List<BleScanResult> {
if (!debugSettings.showFakeData.value) return originals
return originals + getFakeData()
}
fun getFakeData(): Collection<BleScanResult> {
val fakeDevices = mutableListOf<BleScanResult>()
// AirPods Gen1
BleScanResult(
address = "78:73:AF:B4:85:22",
rssi = Random.nextInt(100) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
manufacturerSpecificData = mapOf(76 to "07 19 01 02 20 75 AA B6 31 00 05 9C 5A A4 5D C0 2C A0 B4 6F B9 ED 8E CE 03 97 CA".hexToByteArray())
).run {
if (Random.nextBoolean()) {
fakeDevices.add(this)
}
}
// AirPods Gen2
BleScanResult(
address = "78:73:FF:B4:85:5E",
rssi = Random.nextInt(100) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
manufacturerSpecificData = mapOf(76 to "07 19 01 0F 20 75 AA B6 31 00 05 9C 5A A4 5D C0 2C A0 B4 6F B9 ED 8E CE 03 97 CA".hexToByteArray())
).run {
if (Random.nextBoolean()) {
fakeDevices.add(this)
}
}
// AirPods Gen3
BleScanResult(
address = "4E:9E:D1:49:D2:6D",
rssi = Random.nextInt(15, 75) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 200,
manufacturerSpecificData = mapOf(76 to "07 19 01 13 20 55 AF 56 31 00 06 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88".hexToByteArray())
).run {
if (Random.nextBoolean()) {
fakeDevices.add(this)
}
}
// AirPods Max
BleScanResult(
address = "7E:E5:C7:65:D2:B5",
rssi = Random.nextInt(15, 75) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 300,
manufacturerSpecificData = mapOf(76 to "07 19 01 0A 20 02 05 80 04 0F 44 A7 60 9B F8 3C FD B1 D8 1C 61 EA 82 60 A3 2C 4E".hexToByteArray())
).run {
if (Random.nextBoolean()) {
fakeDevices.add(this)
}
}
// BeatsFlex
BleScanResult(
address = "5E:9E:D1:49:D2:6D",
rssi = Random.nextInt(15, 75) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
manufacturerSpecificData = mapOf(76 to "07 19 01 10 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0".hexToByteArray())
).run {
if (Random.nextBoolean()) {
fakeDevices.add(this)
}
}
// Unknown Device
BleScanResult(
address = "6E:9E:D1:49:D2:6D",
rssi = Random.nextInt(15, 75) * -1,
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 500,
manufacturerSpecificData = mapOf(76 to "07 19 01 FF 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0".hexToByteArray())
).run {
if (Random.nextBoolean()) {
fakeDevices.add(this)
}
}
return fakeDevices
}
private fun String.hexToByteArray(): ByteArray {
val trimmed = this
.replace(" ", "")
.replace(">", "")
.replace("<", "")
require(trimmed.length % 2 == 0) { "Not a HEX string" }
return trimmed.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
}
}
@@ -70,7 +70,9 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) {
} }
} }
vm.listItems.observe2(ui) { adapter.update(it) } vm.listItems.observe2(ui) {
adapter.update(it)
}
vm.workerAutolaunch.observe2 { vm.workerAutolaunch.observe2 {
// While UI is active, subscribe to the autolaunch routine // While UI is active, subscribe to the autolaunch routine
@@ -4,7 +4,6 @@ import android.app.Activity
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import eu.darken.capod.common.WebpageTool
import eu.darken.capod.common.bluetooth.BluetoothManager2 import eu.darken.capod.common.bluetooth.BluetoothManager2
import eu.darken.capod.common.coroutine.DispatcherProvider import eu.darken.capod.common.coroutine.DispatcherProvider
import eu.darken.capod.common.debug.autoreport.DebugSettings import eu.darken.capod.common.debug.autoreport.DebugSettings
@@ -46,7 +45,6 @@ class OverviewFragmentVM @Inject constructor(
debugSettings: DebugSettings, debugSettings: DebugSettings,
private val upgradeRepo: UpgradeRepo, private val upgradeRepo: UpgradeRepo,
private val bluetoothManager: BluetoothManager2, private val bluetoothManager: BluetoothManager2,
private val webpageTool: WebpageTool,
) : ViewModel3(dispatcherProvider = dispatcherProvider) { ) : ViewModel3(dispatcherProvider = dispatcherProvider) {
val upgradeState = upgradeRepo.upgradeInfo.asLiveData2() val upgradeState = upgradeRepo.upgradeInfo.asLiveData2()
@@ -55,7 +53,7 @@ class OverviewFragmentVM @Inject constructor(
private val updateTicker = channelFlow<Unit> { private val updateTicker = channelFlow<Unit> {
while (isActive) { while (isActive) {
trySend(Unit) trySend(Unit)
delay(1000) delay(3000)
} }
} }
@@ -9,7 +9,6 @@ import eu.darken.capod.databinding.OverviewPodsAppleSingleBasicItemBinding
import eu.darken.capod.pods.core.apple.BasicSingleApplePods import eu.darken.capod.pods.core.apple.BasicSingleApplePods
import eu.darken.capod.pods.core.getBatteryDrawable import eu.darken.capod.pods.core.getBatteryDrawable
import eu.darken.capod.pods.core.getBatteryLevelHeadset import eu.darken.capod.pods.core.getBatteryLevelHeadset
import eu.darken.capod.pods.core.getSignalQuality
import eu.darken.capod.pods.core.lastSeenFormatted import eu.darken.capod.pods.core.lastSeenFormatted
import java.time.Instant import java.time.Instant
@@ -34,10 +33,7 @@ class BasicSingleApplePodsCardVH(parent: ViewGroup) :
lastSeen.text = device.lastSeenFormatted(item.now) lastSeen.text = device.lastSeenFormatted(item.now)
reception.text = device.getSignalQuality(context) reception.text = item.getReceptionText()
if (item.isMainPod) {
reception.append("\n(${getString(R.string.pods_yours)})")
}
batteryLabel.text = device.getBatteryLevelHeadset(context) batteryLabel.text = device.getBatteryLevelHeadset(context)
batteryIcon.setImageResource(getBatteryDrawable(device.batteryHeadsetPercent)) batteryIcon.setImageResource(getBatteryDrawable(device.batteryHeadsetPercent))
@@ -2,6 +2,7 @@ package eu.darken.capod.main.ui.overview.cards.pods
import android.graphics.Typeface import android.graphics.Typeface
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isGone
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import eu.darken.capod.R import eu.darken.capod.R
import eu.darken.capod.common.lists.binding import eu.darken.capod.common.lists.binding
@@ -10,6 +11,7 @@ import eu.darken.capod.pods.core.*
import eu.darken.capod.pods.core.apple.DualApplePods 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.DeviceColor
import eu.darken.capod.pods.core.apple.DualApplePods.LidState import eu.darken.capod.pods.core.apple.DualApplePods.LidState
import java.time.Duration
import java.time.Instant import java.time.Instant
class DualApplePodsCardVH(parent: ViewGroup) : class DualApplePodsCardVH(parent: ViewGroup) :
@@ -36,11 +38,11 @@ class DualApplePodsCardVH(parent: ViewGroup) :
} }
deviceIcon.setImageResource(device.iconRes) deviceIcon.setImageResource(device.iconRes)
lastSeen.text = device.lastSeenFormatted(item.now) 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 = device.getSignalQuality(context).let { reception.text = item.getReceptionText()
if (item.isMainPod) "$it\n(${getString(R.string.pods_yours)})" else it
}
// Left Pod // Left Pod
device.apply { device.apply {
@@ -3,11 +3,13 @@ package eu.darken.capod.main.ui.overview.cards.pods
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import eu.darken.capod.R
import eu.darken.capod.common.lists.BindableVH import eu.darken.capod.common.lists.BindableVH
import eu.darken.capod.common.lists.differ.DifferItem import eu.darken.capod.common.lists.differ.DifferItem
import eu.darken.capod.common.lists.modular.ModularAdapter import eu.darken.capod.common.lists.modular.ModularAdapter
import eu.darken.capod.main.ui.overview.OverviewAdapter import eu.darken.capod.main.ui.overview.OverviewAdapter
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.getSignalQuality
import java.time.Instant import java.time.Instant
abstract class PodDeviceVH<D : PodDeviceVH.Item, B : ViewBinding>( abstract class PodDeviceVH<D : PodDeviceVH.Item, B : ViewBinding>(
@@ -15,6 +17,10 @@ abstract class PodDeviceVH<D : PodDeviceVH.Item, B : ViewBinding>(
parent: ViewGroup parent: ViewGroup
) : ModularAdapter.VH(layoutId, parent), BindableVH<D, B> { ) : ModularAdapter.VH(layoutId, parent), BindableVH<D, B> {
fun Item.getReceptionText(): String = device.getSignalQuality(context)
.let { if (showDebug) "$it ${device.seenCounter}" else it }
.let { if (isMainPod) "$it\n(${getString(R.string.pods_yours)})" else it }
interface Item : OverviewAdapter.Item { interface Item : OverviewAdapter.Item {
val now: Instant val now: Instant
@@ -10,7 +10,6 @@ import eu.darken.capod.databinding.OverviewPodsAppleSingleItemBinding
import eu.darken.capod.pods.core.apple.SingleApplePods import eu.darken.capod.pods.core.apple.SingleApplePods
import eu.darken.capod.pods.core.getBatteryDrawable import eu.darken.capod.pods.core.getBatteryDrawable
import eu.darken.capod.pods.core.getBatteryLevelHeadset import eu.darken.capod.pods.core.getBatteryLevelHeadset
import eu.darken.capod.pods.core.getSignalQuality
import eu.darken.capod.pods.core.lastSeenFormatted import eu.darken.capod.pods.core.lastSeenFormatted
import java.time.Instant import java.time.Instant
@@ -35,10 +34,7 @@ class SingleApplePodsCardVH(parent: ViewGroup) :
lastSeen.text = device.lastSeenFormatted(item.now) lastSeen.text = device.lastSeenFormatted(item.now)
reception.text = device.getSignalQuality(context) reception.text = item.getReceptionText()
if (item.isMainPod) {
reception.append("\n(${getString(R.string.pods_yours)})")
}
batteryLabel.text = device.getBatteryLevelHeadset(context) batteryLabel.text = device.getBatteryLevelHeadset(context)
batteryIcon.setImageResource(getBatteryDrawable(device.batteryHeadsetPercent)) batteryIcon.setImageResource(getBatteryDrawable(device.batteryHeadsetPercent))
@@ -6,7 +6,6 @@ import eu.darken.capod.R
import eu.darken.capod.common.lists.binding import eu.darken.capod.common.lists.binding
import eu.darken.capod.databinding.OverviewPodsUnknownItemBinding import eu.darken.capod.databinding.OverviewPodsUnknownItemBinding
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.getSignalQuality
import eu.darken.capod.pods.core.lastSeenFormatted import eu.darken.capod.pods.core.lastSeenFormatted
import java.time.Instant import java.time.Instant
@@ -30,10 +29,7 @@ class UnknownPodDeviceCardVH(parent: ViewGroup) :
lastSeen.text = device.lastSeenFormatted(item.now) lastSeen.text = device.lastSeenFormatted(item.now)
reception.text = device.getSignalQuality(context) reception.text = item.getReceptionText()
if (item.isMainPod) {
reception.append("\n(${getString(R.string.pods_yours)})")
}
rawdata.text = device.rawDataHex rawdata.text = device.rawDataHex
} }
@@ -75,7 +75,7 @@ class PodMonitor @Inject constructor(
val now = Instant.now() val now = Instant.now()
deviceCache.toList().forEach { (key, value) -> deviceCache.toList().forEach { (key, value) ->
if (Duration.between(value.lastSeenAt, now) > Duration.ofSeconds(20)) { if (Duration.between(value.seenLastAt, now) > Duration.ofSeconds(20)) {
log(TAG, VERBOSE) { "Removing stale device from cache: $value" } log(TAG, VERBOSE) { "Removing stale device from cache: $value" }
deviceCache.remove(key) deviceCache.remove(key)
} }
@@ -89,28 +89,9 @@ class PodMonitor @Inject constructor(
} }
} }
val now = Instant.now() val presorted = pods.values.sortPodsToInterest()
val minimumSignalQuality = generalSettings.minimumSignalQuality.value
val mainDeviceModel = generalSettings.mainDeviceModel.value
val aboveTresholdSorted = pods.values
.filter { it.signalQuality > minimumSignalQuality }
.sortedWith(
compareByDescending<PodDevice> { it.model == mainDeviceModel && it.model != PodDevice.Model.UNKNOWN }
.thenBy {
val age = Duration.between(it.lastSeenAt, now).seconds
if (age < 3) 0L else age
}
.thenByDescending { it.signalQuality }
)
val belowThresholdSorted = pods.values
.filter { it.signalQuality <= minimumSignalQuality }
.sortedWith(
compareBy<PodDevice> { Duration.between(it.lastSeenAt, now) }.thenByDescending { it.signalQuality }
)
val presorted = (aboveTresholdSorted + belowThresholdSorted)
val main = presorted.determineMainDevice() val main = presorted.determineMainDevice()
presorted.sortedByDescending { it == main } presorted.sortedByDescending { it == main }
} }
.onStart { emit(emptyList()) } .onStart { emit(emptyList()) }
@@ -126,16 +107,40 @@ class PodMonitor @Inject constructor(
.map { it.determineMainDevice() } .map { it.determineMainDevice() }
.replayingShare(appScope) .replayingShare(appScope)
private fun List<PodDevice>.determineMainDevice(): PodDevice? = this.firstOrNull()?.let { private fun Collection<PodDevice>.sortPodsToInterest(): List<PodDevice> = this.let { devices ->
val mainDeviceModel = generalSettings.mainDeviceModel.value val now = Instant.now()
when {
it.model == PodDevice.Model.UNKNOWN -> null return@let devices.sortedWith(
mainDeviceModel != PodDevice.Model.UNKNOWN && it.model != mainDeviceModel -> null compareByDescending<PodDevice> { true }
it.signalQuality <= generalSettings.minimumSignalQuality.value -> null .thenBy {
else -> it val age = Duration.between(it.seenLastAt, now).seconds
} if (age < 5) 0L else (age / 3L).toLong()
}
.thenByDescending { it.signalQuality }
.thenByDescending { (it.seenCounter / 10) }
)
} }
private fun List<PodDevice>.determineMainDevice(): PodDevice? = this
.sortPodsToInterest()
.let { devices ->
val mainDeviceModel = generalSettings.mainDeviceModel.value
val presorted = devices.sortedByDescending {
it.model == mainDeviceModel && it.model != PodDevice.Model.UNKNOWN
}
return@let presorted.firstOrNull()?.let { candidate ->
when {
candidate.model == PodDevice.Model.UNKNOWN -> null
mainDeviceModel != PodDevice.Model.UNKNOWN && candidate.model != mainDeviceModel -> null
candidate.signalQuality <= generalSettings.minimumSignalQuality.value -> null
else -> candidate
}
}
}
companion object { companion object {
private val TAG = logTag("Monitor", "PodMonitor") private val TAG = logTag("Monitor", "PodMonitor")
} }
@@ -20,7 +20,11 @@ interface PodDevice {
val address: String val address: String
get() = scanResult.address get() = scanResult.address
val lastSeenAt: Instant val seenLastAt: Instant
val seenFirstAt: Instant
val seenCounter: Int
val scanResult: BleScanResult val scanResult: BleScanResult
@@ -45,10 +45,19 @@ fun getBatteryDrawable(percent: Float?): Int = when {
private val lastSeenFormatter = RelativeDateTimeFormatter.getInstance() private val lastSeenFormatter = RelativeDateTimeFormatter.getInstance()
fun PodDevice.lastSeenFormatted(now: Instant): String { fun PodDevice.lastSeenFormatted(now: Instant): String {
val duration = Duration.between(lastSeenAt, now) val duration = Duration.between(seenLastAt, now)
return lastSeenFormatter.format( return lastSeenFormatter.format(
duration.seconds.toDouble(), duration.seconds.toDouble(),
RelativeDateTimeFormatter.Direction.LAST, RelativeDateTimeFormatter.Direction.LAST,
RelativeDateTimeFormatter.RelativeUnit.SECONDS RelativeDateTimeFormatter.RelativeUnit.SECONDS
) )
}
fun PodDevice.firstSeenFormatted(now: Instant): String {
val duration = Duration.between(seenFirstAt, now)
return lastSeenFormatter.format(
duration.toMinutes().toDouble(),
RelativeDateTimeFormatter.Direction.LAST,
RelativeDateTimeFormatter.RelativeUnit.MINUTES
)
} }
@@ -1,6 +1,5 @@
package eu.darken.capod.pods.core.apple package eu.darken.capod.pods.core.apple
import eu.darken.capod.common.SystemClockWrap
import eu.darken.capod.common.bluetooth.BleScanResult import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE 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
@@ -9,6 +8,7 @@ import eu.darken.capod.common.upperNibble
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
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
abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) { abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
@@ -33,6 +33,8 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
data class KnownDevice( data class KnownDevice(
val id: PodDevice.Id, val id: PodDevice.Id,
val seenFirstAt: Instant,
val seenCounter: Int,
val history: List<ApplePods> val history: List<ApplePods>
) { ) {
val lastMessage: ProximityPairing.Message val lastMessage: ProximityPairing.Message
@@ -45,14 +47,18 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
get() = 0.75f + (history.size / (MAX_HISTORY * 4f)) get() = 0.75f + (history.size / (MAX_HISTORY * 4f))
fun averageRssi(latest: Int): Int = fun averageRssi(latest: Int): Int =
history.map { it.rssi }.plus(latest).takeLast(10).average().toInt() history.map { it.rssi }.plus(latest).takeLast(10).median()
val timestampNanos: Duration
get() = Duration.ofNanos(history.last().scanResult.generatedAtNanos)
fun isOlderThan(age: Duration): Boolean { fun isOlderThan(age: Duration): Boolean {
val now = Duration.ofNanos(SystemClockWrap.elapsedRealtimeNanos) val now = Instant.now()
return now - timestampNanos > age return Duration.between(history.last().seenLastAt, now) > age
}
private fun List<Int>.median(): Int = this.sorted().let {
if (it.size % 2 == 0)
(it[it.size / 2] + it[(it.size - 1) / 2]) / 2
else
it[it.size / 2]
} }
override fun toString(): String = "KnownDevice(history=${history.size}, last=${history.last()})" override fun toString(): String = "KnownDevice(history=${history.size}, last=${history.last()})"
@@ -105,12 +111,17 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
knownDevices[device.identifier] = when { knownDevices[device.identifier] = when {
existing != null -> { existing != null -> {
existing.copy(history = existing.history.plus(device)) existing.copy(
seenCounter = existing.seenCounter + 1,
history = existing.history.plus(device)
)
} }
else -> { else -> {
log(tag) { "searchHistory1: Creating new history for $device" } log(tag) { "searchHistory1: Creating new history for $device" }
KnownDevice( KnownDevice(
id = device.identifier, id = device.identifier,
seenFirstAt = device.seenFirstAt,
seenCounter = 1,
history = listOf(device) history = listOf(device)
) )
} }
@@ -11,7 +11,9 @@ import javax.inject.Inject
data class UnknownAppleDevice( data class UnknownAppleDevice(
override val identifier: PodDevice.Id = PodDevice.Id(), override val identifier: PodDevice.Id = PodDevice.Id(),
override val lastSeenAt: Instant = Instant.now(), override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, override val proximityMessage: ProximityPairing.Message,
override val confidence: Float = 0f, override val confidence: Float = 0f,
@@ -32,15 +34,18 @@ data class UnknownAppleDevice(
scanResult: BleScanResult, scanResult: BleScanResult,
proximityMessage: ProximityPairing.Message, proximityMessage: ProximityPairing.Message,
): ApplePods { ): ApplePods {
val basic = UnknownAppleDevice(scanResult = scanResult, proximityMessage = proximityMessage) var basic = UnknownAppleDevice(scanResult = scanResult, proximityMessage = proximityMessage)
val result = searchHistory(basic) val result = searchHistory(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic) updateHistory(basic)
if (result == null) return basic if (result == null) return basic
return basic.copy( return basic.copy(
identifier = result.id, identifier = result.id,
seenFirstAt = result.seenFirstAt,
seenCounter = result.seenCounter,
confidence = result.confidence, confidence = result.confidence,
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.averageRssi(basic.rssi),
) )
@@ -12,7 +12,9 @@ import javax.inject.Inject
data class AirPodsGen1 constructor( data class AirPodsGen1 constructor(
override val identifier: PodDevice.Id = PodDevice.Id(), override val identifier: PodDevice.Id = PodDevice.Id(),
override val lastSeenAt: Instant = Instant.now(), override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, override val proximityMessage: ProximityPairing.Message,
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
@@ -38,15 +40,18 @@ data class AirPodsGen1 constructor(
proximityMessage.getModelInfo().full == DEVICE_CODE proximityMessage.getModelInfo().full == DEVICE_CODE
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods { override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
val basic = AirPodsGen1(scanResult = scanResult, proximityMessage = proximityMessage) var basic = AirPodsGen1(scanResult = scanResult, proximityMessage = proximityMessage)
val result = searchHistory(basic) val result = searchHistory(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic) updateHistory(basic)
if (result == null) return basic if (result == null) return basic
return basic.copy( return basic.copy(
identifier = result.id, identifier = result.id,
seenFirstAt = result.seenFirstAt,
seenCounter = result.seenCounter,
confidence = result.confidence, confidence = result.confidence,
cachedBatteryPercentage = result.getLatestCaseBattery(), cachedBatteryPercentage = result.getLatestCaseBattery(),
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.averageRssi(basic.rssi),
@@ -12,7 +12,9 @@ import javax.inject.Inject
data class AirPodsGen2 constructor( data class AirPodsGen2 constructor(
override val identifier: PodDevice.Id = PodDevice.Id(), override val identifier: PodDevice.Id = PodDevice.Id(),
override val lastSeenAt: Instant = Instant.now(), override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, override val proximityMessage: ProximityPairing.Message,
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
@@ -38,15 +40,18 @@ data class AirPodsGen2 constructor(
proximityMessage.getModelInfo().full == DEVICE_CODE proximityMessage.getModelInfo().full == DEVICE_CODE
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods { override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
val basic = AirPodsGen2(scanResult = scanResult, proximityMessage = proximityMessage) var basic = AirPodsGen2(scanResult = scanResult, proximityMessage = proximityMessage)
val result = searchHistory(basic) val result = searchHistory(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic) updateHistory(basic)
if (result == null) return basic if (result == null) return basic
return basic.copy( return basic.copy(
identifier = result.id, identifier = result.id,
seenFirstAt = result.seenFirstAt,
seenCounter = result.seenCounter,
confidence = result.confidence, confidence = result.confidence,
cachedBatteryPercentage = result.getLatestCaseBattery(), cachedBatteryPercentage = result.getLatestCaseBattery(),
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.averageRssi(basic.rssi),
@@ -12,7 +12,9 @@ import javax.inject.Inject
data class AirPodsGen3 constructor( data class AirPodsGen3 constructor(
override val identifier: PodDevice.Id = PodDevice.Id(), override val identifier: PodDevice.Id = PodDevice.Id(),
override val lastSeenAt: Instant = Instant.now(), override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, override val proximityMessage: ProximityPairing.Message,
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
@@ -37,15 +39,18 @@ data class AirPodsGen3 constructor(
proximityMessage.getModelInfo().full == DEVICE_CODE proximityMessage.getModelInfo().full == DEVICE_CODE
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods { override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
val basic = AirPodsGen3(scanResult = scanResult, proximityMessage = proximityMessage) var basic = AirPodsGen3(scanResult = scanResult, proximityMessage = proximityMessage)
val result = searchHistory(basic) val result = searchHistory(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic) updateHistory(basic)
if (result == null) return basic if (result == null) return basic
return basic.copy( return basic.copy(
identifier = result.id, identifier = result.id,
seenFirstAt = result.seenFirstAt,
seenCounter = result.seenCounter,
confidence = result.confidence, confidence = result.confidence,
cachedBatteryPercentage = result.getLatestCaseBattery(), cachedBatteryPercentage = result.getLatestCaseBattery(),
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.averageRssi(basic.rssi),
@@ -12,7 +12,9 @@ import javax.inject.Inject
data class AirPodsMax( data class AirPodsMax(
override val identifier: PodDevice.Id = PodDevice.Id(), override val identifier: PodDevice.Id = PodDevice.Id(),
override val lastSeenAt: Instant = Instant.now(), override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, override val proximityMessage: ProximityPairing.Message,
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
@@ -30,15 +32,18 @@ data class AirPodsMax(
proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods { override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
val basic = AirPodsMax(scanResult = scanResult, proximityMessage = proximityMessage) var basic = AirPodsMax(scanResult = scanResult, proximityMessage = proximityMessage)
val result = searchHistory(basic) val result = searchHistory(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic) updateHistory(basic)
if (result == null) return basic if (result == null) return basic
return basic.copy( return basic.copy(
identifier = result.id, identifier = result.id,
seenFirstAt = result.seenFirstAt,
seenCounter = result.seenCounter,
confidence = result.confidence, confidence = result.confidence,
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.averageRssi(basic.rssi),
) )
@@ -13,7 +13,9 @@ import javax.inject.Inject
data class AirPodsPro( data class AirPodsPro(
override val identifier: PodDevice.Id = PodDevice.Id(), override val identifier: PodDevice.Id = PodDevice.Id(),
override val lastSeenAt: Instant = Instant.now(), override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, override val proximityMessage: ProximityPairing.Message,
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
@@ -49,6 +51,8 @@ data class AirPodsPro(
return basic.copy( return basic.copy(
identifier = result.id, identifier = result.id,
seenFirstAt = result.seenFirstAt,
seenCounter = result.seenCounter,
confidence = result.confidence, confidence = result.confidence,
cachedBatteryPercentage = result.getLatestCaseBattery(), cachedBatteryPercentage = result.getLatestCaseBattery(),
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.averageRssi(basic.rssi),
@@ -12,7 +12,9 @@ import javax.inject.Inject
data class BeatsFlex( data class BeatsFlex(
override val identifier: PodDevice.Id = PodDevice.Id(), override val identifier: PodDevice.Id = PodDevice.Id(),
override val lastSeenAt: Instant = Instant.now(), override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, override val proximityMessage: ProximityPairing.Message,
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
@@ -30,15 +32,18 @@ data class BeatsFlex(
proximityMessage.getModelInfo().full == DEVICE_CODE proximityMessage.getModelInfo().full == DEVICE_CODE
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods { override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
val basic = BeatsFlex(scanResult = scanResult, proximityMessage = proximityMessage) var basic = BeatsFlex(scanResult = scanResult, proximityMessage = proximityMessage)
val result = searchHistory(basic) val result = searchHistory(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic) updateHistory(basic)
if (result == null) return basic if (result == null) return basic
return basic.copy( return basic.copy(
identifier = result.id, identifier = result.id,
seenFirstAt = result.seenFirstAt,
seenCounter = result.seenCounter,
confidence = result.confidence, confidence = result.confidence,
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.averageRssi(basic.rssi),
) )
@@ -12,7 +12,9 @@ import javax.inject.Inject
data class BeatsSolo3( data class BeatsSolo3(
override val identifier: PodDevice.Id = PodDevice.Id(), override val identifier: PodDevice.Id = PodDevice.Id(),
override val lastSeenAt: Instant = Instant.now(), override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, override val proximityMessage: ProximityPairing.Message,
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
@@ -29,15 +31,18 @@ data class BeatsSolo3(
proximityMessage.getModelInfo().full == DEVICE_CODE proximityMessage.getModelInfo().full == DEVICE_CODE
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods { override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
val basic = BeatsSolo3(scanResult = scanResult, proximityMessage = proximityMessage) var basic = BeatsSolo3(scanResult = scanResult, proximityMessage = proximityMessage)
val result = searchHistory(basic) val result = searchHistory(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic) updateHistory(basic)
if (result == null) return basic if (result == null) return basic
return basic.copy( return basic.copy(
identifier = result.id, identifier = result.id,
seenFirstAt = result.seenFirstAt,
seenCounter = result.seenCounter,
confidence = result.confidence, confidence = result.confidence,
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.averageRssi(basic.rssi),
) )
@@ -12,7 +12,9 @@ import javax.inject.Inject
data class BeatsStudio3( data class BeatsStudio3(
override val identifier: PodDevice.Id = PodDevice.Id(), override val identifier: PodDevice.Id = PodDevice.Id(),
override val lastSeenAt: Instant = Instant.now(), override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, override val proximityMessage: ProximityPairing.Message,
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
@@ -27,15 +29,18 @@ data class BeatsStudio3(
proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods { override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
val basic = BeatsStudio3(scanResult = scanResult, proximityMessage = proximityMessage) var basic = BeatsStudio3(scanResult = scanResult, proximityMessage = proximityMessage)
val result = searchHistory(basic) val result = searchHistory(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic) updateHistory(basic)
if (result == null) return basic if (result == null) return basic
return basic.copy( return basic.copy(
identifier = result.id, identifier = result.id,
seenFirstAt = result.seenFirstAt,
seenCounter = result.seenCounter,
confidence = result.confidence, confidence = result.confidence,
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.averageRssi(basic.rssi),
) )
@@ -12,7 +12,9 @@ import javax.inject.Inject
data class BeatsX( data class BeatsX(
override val identifier: PodDevice.Id = PodDevice.Id(), override val identifier: PodDevice.Id = PodDevice.Id(),
override val lastSeenAt: Instant = Instant.now(), override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, override val proximityMessage: ProximityPairing.Message,
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
@@ -30,15 +32,18 @@ data class BeatsX(
proximityMessage.getModelInfo().full == DEVICE_CODE proximityMessage.getModelInfo().full == DEVICE_CODE
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods { override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
val basic = BeatsX(scanResult = scanResult, proximityMessage = proximityMessage) var basic = BeatsX(scanResult = scanResult, proximityMessage = proximityMessage)
val result = searchHistory(basic) val result = searchHistory(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic) updateHistory(basic)
if (result == null) return basic if (result == null) return basic
return basic.copy( return basic.copy(
identifier = result.id, identifier = result.id,
seenFirstAt = result.seenFirstAt,
seenCounter = result.seenCounter,
confidence = result.confidence, confidence = result.confidence,
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.averageRssi(basic.rssi),
) )
@@ -12,7 +12,9 @@ import javax.inject.Inject
data class PowerBeats3( data class PowerBeats3(
override val identifier: PodDevice.Id = PodDevice.Id(), override val identifier: PodDevice.Id = PodDevice.Id(),
override val lastSeenAt: Instant = Instant.now(), override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, override val proximityMessage: ProximityPairing.Message,
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
@@ -29,15 +31,18 @@ data class PowerBeats3(
proximityMessage.getModelInfo().full == DEVICE_CODE proximityMessage.getModelInfo().full == DEVICE_CODE
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods { override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
val basic = PowerBeats3(scanResult = scanResult, proximityMessage = proximityMessage) var basic = PowerBeats3(scanResult = scanResult, proximityMessage = proximityMessage)
val result = searchHistory(basic) val result = searchHistory(basic)
if (result != null) basic = basic.copy(identifier = result.id)
updateHistory(basic) updateHistory(basic)
if (result == null) return basic if (result == null) return basic
return basic.copy( return basic.copy(
identifier = result.id, identifier = result.id,
seenFirstAt = result.seenFirstAt,
seenCounter = result.seenCounter,
confidence = result.confidence, confidence = result.confidence,
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.averageRssi(basic.rssi),
) )
@@ -12,7 +12,9 @@ import javax.inject.Inject
data class PowerBeatsPro( data class PowerBeatsPro(
override val identifier: PodDevice.Id = PodDevice.Id(), override val identifier: PodDevice.Id = PodDevice.Id(),
override val lastSeenAt: Instant = Instant.now(), override val seenLastAt: Instant = Instant.now(),
override val seenFirstAt: Instant = Instant.now(),
override val seenCounter: Int = 1,
override val scanResult: BleScanResult, override val scanResult: BleScanResult,
override val proximityMessage: ProximityPairing.Message, override val proximityMessage: ProximityPairing.Message,
override val confidence: Float = PodDevice.BASE_CONFIDENCE, override val confidence: Float = PodDevice.BASE_CONFIDENCE,
@@ -48,6 +50,8 @@ data class PowerBeatsPro(
return basic.copy( return basic.copy(
identifier = result.id, identifier = result.id,
seenFirstAt = result.seenFirstAt,
seenCounter = result.seenCounter,
confidence = result.confidence, confidence = result.confidence,
cachedBatteryPercentage = result.getLatestCaseBattery(), cachedBatteryPercentage = result.getLatestCaseBattery(),
rssiAverage = result.averageRssi(basic.rssi), rssiAverage = result.averageRssi(basic.rssi),
@@ -45,12 +45,24 @@
style="@style/TextAppearance.MaterialComponents.Caption" style="@style/TextAppearance.MaterialComponents.Caption"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_constraintBottom_toTopOf="@id/first_seen"
app:layout_constraintEnd_toEndOf="@id/name"
app:layout_constraintStart_toStartOf="@id/name"
app:layout_constraintTop_toBottomOf="@id/name"
app:layout_goneMarginBottom="8dp"
tools:text="Last seen: 3s ago" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/first_seen"
style="@style/TextAppearance.MaterialComponents.Caption"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
app:layout_constraintBottom_toTopOf="@id/barrier_top" app:layout_constraintBottom_toTopOf="@id/barrier_top"
app:layout_constraintEnd_toEndOf="@id/name" app:layout_constraintEnd_toEndOf="@id/name"
app:layout_constraintStart_toStartOf="@id/name" app:layout_constraintStart_toStartOf="@id/name"
app:layout_constraintTop_toBottomOf="@id/name" app:layout_constraintTop_toBottomOf="@id/last_seen"
tools:text="3s ago" /> tools:text="First seen: 3s ago" />
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/reception" android:id="@+id/reception"
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">CAPod</string>
<string name="app_name_pro">CAPod Pro</string>
<string name="app_name_foss">CAPod FOSS</string>
<string name="notification_channel_device_status_label">Geräte Status</string>
<string name="general_error_label">Fehler</string>
<string name="general_share_action">Teilen</string>
<string name="general_done_action">Fertig</string>
<string name="general_copy_action">Kopieren</string>
<string name="general_value_unknown_label">Unbekannt</string>
<string name="general_thank_you_label">Danke</string>
<string name="general_value_not_available_label">k.A.</string>
<string name="general_grant_permission_action">Berechtigung erteilen</string>
</resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
+2
View File
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
@@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

Some files were not shown because too many files have changed in this diff Show More