mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-15 02:36:12 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4d9316665 | ||
|
|
a234ac0dd2 | ||
|
|
3fc18bc40b | ||
|
|
0f924ecb82 | ||
|
|
6bd350ce42 | ||
|
|
e1b4ab14e4 |
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-5
@@ -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))
|
||||||
|
|||||||
+6
-4
@@ -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
|
||||||
|
|||||||
+1
-5
@@ -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))
|
||||||
|
|||||||
+1
-5
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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,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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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
Reference in New Issue
Block a user