mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4d9316665 | ||
|
|
a234ac0dd2 | ||
|
|
3fc18bc40b | ||
|
|
0f924ecb82 | ||
|
|
6bd350ce42 | ||
|
|
e1b4ab14e4 | ||
|
|
adef0b39e9 | ||
|
|
af12a0e5cb | ||
|
|
90fc2fac24 | ||
|
|
3be7d8b43f | ||
|
|
a35d8c57b4 | ||
|
|
f792431710 | ||
|
|
7ff20acfc0 | ||
|
|
8e20e0e108 | ||
|
|
a6fda04640 | ||
|
|
0bbcfb5e88 | ||
|
|
e79ba6bfe5 | ||
|
|
369b0207c4 | ||
|
|
ce737b45b9 | ||
|
|
cde3afdcd9 | ||
|
|
07b3f4320e | ||
|
|
e1c71cf6b6 | ||
|
|
c233cd2445 | ||
|
|
8f609ca989 | ||
|
|
2de2369600 | ||
|
|
d5d2ae784c | ||
|
|
56bedb64c4 | ||
|
|
3b48ca90d3 | ||
|
|
272ecc006f | ||
|
|
ddacfa6841 | ||
|
|
c5b1c417a4 | ||
|
|
38bc3be5b7 | ||
|
|
57cd823b63 | ||
|
|
6cd799130e | ||
|
|
a1c58e0a32 | ||
|
|
9cd4cea63d | ||
|
|
1d5e13cc24 | ||
|
|
6d2c4ba8ca | ||
|
|
357baa12e6 | ||
|
|
909a9ba194 | ||
|
|
79f68c3a0d |
-3
@@ -109,9 +109,6 @@ class BillingClientConnectionProvider @Inject constructor(
|
|||||||
delay(3000 * attempt)
|
delay(3000 * attempt)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
.catch {
|
|
||||||
log(TAG, ERROR) { "Unable to provide client connection:\n${it.asLog()}" }
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG: String = logTag("Upgrade", "Gplay", "Billing", "Client", "ConnectionProvider")
|
val TAG: String = logTag("Upgrade", "Gplay", "Billing", "Client", "ConnectionProvider")
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class BillingDataRepo @Inject constructor(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
private val connectionProvider = billingClientConnectionProvider.connection
|
private val connectionProvider = billingClientConnectionProvider.connection
|
||||||
|
.catch { log(TAG, ERROR) { "Unable to provide client connection:\n${it.asLog()}" } }
|
||||||
.replayingShare(scope)
|
.replayingShare(scope)
|
||||||
|
|
||||||
val billingData: Flow<BillingData> = connectionProvider
|
val billingData: Flow<BillingData> = connectionProvider
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
android:usesPermissionFlags="neverForLocation" />
|
android:usesPermissionFlags="neverForLocation" />
|
||||||
|
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.bluetooth_le"
|
android:name="android.hardware.bluetooth_le"
|
||||||
android:required="true" />
|
android:required="true" />
|
||||||
|
|||||||
@@ -16,5 +16,6 @@ object BuildConfigWrap {
|
|||||||
val VERSION_NAME: String = BuildConfig.VERSION_NAME
|
val VERSION_NAME: String = BuildConfig.VERSION_NAME
|
||||||
val GIT_SHA: String = BuildConfig.GITSHA
|
val GIT_SHA: String = BuildConfig.GITSHA
|
||||||
|
|
||||||
val VERSION_DESCRIPTION: String = "v$VERSION_NAME ($VERSION_CODE) [$GIT_SHA] ${FLAVOR}_$BUILD_TYPE"
|
val VERSION_DESCRIPTION_LONG: String = "v$VERSION_NAME ($VERSION_CODE) [$GIT_SHA] ${FLAVOR}_$BUILD_TYPE"
|
||||||
|
val VERSION_DESCRIPTION_SHORT: String = "v$VERSION_NAME [$GIT_SHA] $FLAVOR"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,16 @@ import java.util.*
|
|||||||
fun Byte.toHex(): String = String.format("%02X", this)
|
fun Byte.toHex(): String = String.format("%02X", this)
|
||||||
fun UByte.toHex(): String = this.toByte().toHex()
|
fun UByte.toHex(): String = this.toByte().toHex()
|
||||||
|
|
||||||
val Byte.upperNibble get() = (this.toInt() shr 4 and 0b1111).toByte()
|
val Byte.upperNibble get() = (this.toInt() shr 4 and 0b1111).toShort()
|
||||||
val Byte.lowerNibble get() = (this.toInt() and 0b1111).toByte()
|
val Byte.lowerNibble get() = (this.toInt() and 0b1111).toShort()
|
||||||
val UByte.upperNibble get() = (this.toInt() shr 4 and 0b1111).toUByte()
|
val UByte.upperNibble get() = (this.toInt() shr 4 and 0b1111).toUShort()
|
||||||
val UByte.lowerNibble get() = (this.toInt() and 0b1111).toUByte()
|
val UByte.lowerNibble get() = (this.toInt() and 0b1111).toUShort()
|
||||||
|
|
||||||
fun Byte.isBitSet(pos: Int): Boolean = BitSet.valueOf(arrayOf(this).toByteArray()).get(pos)
|
fun Byte.isBitSet(pos: Int): Boolean = BitSet.valueOf(arrayOf(this).toByteArray()).get(pos)
|
||||||
fun UByte.isBitSet(pos: Int): Boolean = this.toByte().isBitSet(pos)
|
fun UByte.isBitSet(pos: Int): Boolean = this.toByte().isBitSet(pos)
|
||||||
|
|
||||||
|
fun Short.isBitSet(pos: Int): Boolean = this.toByte().isBitSet(pos)
|
||||||
|
fun UShort.isBitSet(pos: Int): Boolean = this.toShort().isBitSet(pos)
|
||||||
|
|
||||||
|
fun UShort.toBinaryString(): String = Integer.toBinaryString(this.toInt()).padStart(4, '0')
|
||||||
|
fun UByte.toBinaryString(): String = Integer.toBinaryString(this.toInt()).padStart(8, '0')
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package eu.darken.capod.common
|
||||||
|
|
||||||
|
object PrivacyPolicy {
|
||||||
|
const val URL = "https://raw.githubusercontent.com/d4rken/capod-public/main/PRIVACY_POLICY.md"
|
||||||
|
}
|
||||||
@@ -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(
|
||||||
@@ -72,8 +70,8 @@ class BleScanner @Inject constructor(
|
|||||||
)
|
)
|
||||||
if (adapter.isOffloadedScanBatchingSupported) {
|
if (adapter.isOffloadedScanBatchingSupported) {
|
||||||
when (scannerMode) {
|
when (scannerMode) {
|
||||||
ScannerMode.LOW_POWER -> setReportDelay(5000)
|
ScannerMode.LOW_POWER -> setReportDelay(2000)
|
||||||
ScannerMode.BALANCED -> setReportDelay(2500)
|
ScannerMode.BALANCED -> setReportDelay(1000)
|
||||||
ScannerMode.LOW_LATENCY -> setReportDelay(500)
|
ScannerMode.LOW_LATENCY -> setReportDelay(500)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -87,7 +85,7 @@ class BleScanner @Inject constructor(
|
|||||||
adapter.bluetoothLeScanner.flushPendingScanResults(callback)
|
adapter.bluetoothLeScanner.flushPendingScanResults(callback)
|
||||||
when (scannerMode) {
|
when (scannerMode) {
|
||||||
ScannerMode.LOW_POWER -> break
|
ScannerMode.LOW_POWER -> break
|
||||||
ScannerMode.BALANCED -> delay(2500)
|
ScannerMode.BALANCED -> delay(1000)
|
||||||
ScannerMode.LOW_LATENCY -> delay(500)
|
ScannerMode.LOW_LATENCY -> delay(500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -58,7 +58,7 @@ class RecorderModule @Inject constructor(
|
|||||||
context.startServiceCompat(Intent(context, RecorderService::class.java))
|
context.startServiceCompat(Intent(context, RecorderService::class.java))
|
||||||
|
|
||||||
log(TAG, INFO) { "Build.Fingerprint: ${Build.FINGERPRINT}" }
|
log(TAG, INFO) { "Build.Fingerprint: ${Build.FINGERPRINT}" }
|
||||||
log(TAG, INFO) { "BuildConfig.Versions: ${BuildConfigWrap.VERSION_DESCRIPTION}" }
|
log(TAG, INFO) { "BuildConfig.Versions: ${BuildConfigWrap.VERSION_DESCRIPTION_LONG}" }
|
||||||
|
|
||||||
copy(
|
copy(
|
||||||
recorder = newRecorder
|
recorder = newRecorder
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ class RecorderActivityVM @Inject constructor(
|
|||||||
type = "application/zip"
|
type = "application/zip"
|
||||||
|
|
||||||
addCategory(Intent.CATEGORY_DEFAULT)
|
addCategory(Intent.CATEGORY_DEFAULT)
|
||||||
putExtra(Intent.EXTRA_SUBJECT, "CAPod DebugLog - ${BuildConfigWrap.VERSION_DESCRIPTION})")
|
putExtra(Intent.EXTRA_SUBJECT, "CAPod DebugLog - ${BuildConfigWrap.VERSION_DESCRIPTION_LONG})")
|
||||||
putExtra(Intent.EXTRA_TEXT, "Your text here.")
|
putExtra(Intent.EXTRA_TEXT, "Your text here.")
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ fun <T : Any> Flow<T>.shareLatest(
|
|||||||
)
|
)
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
|
|
||||||
fun <T : Any> Flow<T>.replayingShare(scope: CoroutineScope) = this.shareIn(
|
fun <T : Any?> Flow<T>.replayingShare(scope: CoroutineScope) = this.shareIn(
|
||||||
scope = scope,
|
scope = scope,
|
||||||
replay = 1,
|
replay = 1,
|
||||||
started = SharingStarted.WhileSubscribed(replayExpiration = Duration.ZERO)
|
started = SharingStarted.WhileSubscribed(replayExpiration = Duration.ZERO)
|
||||||
|
|||||||
@@ -62,6 +62,15 @@ enum class Permission(
|
|||||||
val pwm = it.getSystemService(Context.POWER_SERVICE) as PowerManager
|
val pwm = it.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||||
pwm.isIgnoringBatteryOptimizations(BuildConfigWrap.APPLICATION_ID)
|
pwm.isIgnoringBatteryOptimizations(BuildConfigWrap.APPLICATION_ID)
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
SYSTEM_ALERT_WINDOW(
|
||||||
|
minApiLevel = Build.VERSION_CODES.BASE,
|
||||||
|
labelRes = R.string.permission_system_alert_window_label,
|
||||||
|
descriptionRes = R.string.permission_system_alert_window_description,
|
||||||
|
permissionId = "android.permission.SYSTEM_ALERT_WINDOW",
|
||||||
|
isGranted = {
|
||||||
|
android.provider.Settings.canDrawOverlays(it)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class GeneralSettings @Inject constructor(
|
|||||||
|
|
||||||
val scannerMode = preferences.createFlowPreference(
|
val scannerMode = preferences.createFlowPreference(
|
||||||
"core.scanner.mode",
|
"core.scanner.mode",
|
||||||
ScannerMode.BALANCED,
|
ScannerMode.LOW_LATENCY,
|
||||||
moshi
|
moshi
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import eu.darken.capod.common.debug.logging.log
|
|||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.common.permissions.Permission
|
import eu.darken.capod.common.permissions.Permission
|
||||||
import eu.darken.capod.common.permissions.isRequired
|
import eu.darken.capod.common.permissions.isRequired
|
||||||
|
import eu.darken.capod.reaction.settings.ReactionSettings
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
@@ -18,6 +19,7 @@ import javax.inject.Singleton
|
|||||||
class PermissionTool @Inject constructor(
|
class PermissionTool @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val generalSettings: GeneralSettings,
|
private val generalSettings: GeneralSettings,
|
||||||
|
private val reactionSettings: ReactionSettings,
|
||||||
) {
|
) {
|
||||||
private val permissionCheckTrigger = MutableStateFlow(UUID.randomUUID())
|
private val permissionCheckTrigger = MutableStateFlow(UUID.randomUUID())
|
||||||
|
|
||||||
@@ -29,9 +31,12 @@ class PermissionTool @Inject constructor(
|
|||||||
val missingPermissions: Flow<Set<Permission>> = combine(
|
val missingPermissions: Flow<Set<Permission>> = combine(
|
||||||
permissionCheckTrigger,
|
permissionCheckTrigger,
|
||||||
generalSettings.monitorMode.flow,
|
generalSettings.monitorMode.flow,
|
||||||
) { _, monitorMode ->
|
reactionSettings.showPopUpOnCaseOpen.flow
|
||||||
|
) { _, monitorMode, showPopUp ->
|
||||||
Permission.values()
|
Permission.values()
|
||||||
.filter { it != Permission.IGNORE_BATTERY_OPTIMIZATION || monitorMode == MonitorMode.ALWAYS }
|
.filter { it != Permission.IGNORE_BATTERY_OPTIMIZATION || monitorMode == MonitorMode.ALWAYS }
|
||||||
|
.filter { it != Permission.ACCESS_BACKGROUND_LOCATION || monitorMode == MonitorMode.ALWAYS }
|
||||||
|
.filter { it != Permission.SYSTEM_ALERT_WINDOW || showPopUp }
|
||||||
.filter { it.isRequired(context) }
|
.filter { it.isRequired(context) }
|
||||||
.toSet()
|
.toSet()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,11 +33,11 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) {
|
|||||||
lateinit var adapter: OverviewAdapter
|
lateinit var adapter: OverviewAdapter
|
||||||
|
|
||||||
lateinit var permissionLauncher: ActivityResultLauncher<String>
|
lateinit var permissionLauncher: ActivityResultLauncher<String>
|
||||||
var awaitingIgnoreBatteryOptimization = false
|
var awaitingPermission = false
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
awaitingIgnoreBatteryOptimization = savedInstanceState?.getBoolean("awaitingIgnoreBatteryOptimization") ?: false
|
awaitingPermission = savedInstanceState?.getBoolean("awaitingPermission") ?: false
|
||||||
|
|
||||||
permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||||
log { "Request for $id was granted=$granted" }
|
log { "Request for $id was granted=$granted" }
|
||||||
@@ -70,23 +70,37 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
vm.requestPermissionEvent.observe2(ui) {
|
vm.requestPermissionEvent.observe2(ui) {
|
||||||
if (it == Permission.IGNORE_BATTERY_OPTIMIZATION) {
|
when (it) {
|
||||||
awaitingIgnoreBatteryOptimization = true
|
Permission.IGNORE_BATTERY_OPTIMIZATION -> {
|
||||||
startActivity(
|
awaitingPermission = true
|
||||||
Intent(
|
startActivity(
|
||||||
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
|
Intent(
|
||||||
Uri.parse("package:${requireContext().packageName}")
|
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
|
||||||
|
Uri.parse("package:${requireContext().packageName}")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
} else {
|
Permission.SYSTEM_ALERT_WINDOW -> {
|
||||||
permissionLauncher.launch(it.permissionId)
|
awaitingPermission = true
|
||||||
|
startActivity(
|
||||||
|
Intent(
|
||||||
|
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||||
|
Uri.parse("package:${requireContext().packageName}")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
permissionLauncher.launch(it.permissionId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,15 +149,15 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
outState.putBoolean("awaitingIgnoreBatteryOptimization", awaitingIgnoreBatteryOptimization)
|
outState.putBoolean("awaitingPermission", awaitingPermission)
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
if (awaitingIgnoreBatteryOptimization) {
|
if (awaitingPermission) {
|
||||||
awaitingIgnoreBatteryOptimization = false
|
awaitingPermission = false
|
||||||
log { "awaitingIgnoreBatteryOptimization=true" }
|
log { "awaitingPermission=true" }
|
||||||
vm.onPermissionResult(true)
|
vm.onPermissionResult(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,16 +104,15 @@ class OverviewFragmentVM @Inject constructor(
|
|||||||
debugSettings.isDebugModeEnabled.flow,
|
debugSettings.isDebugModeEnabled.flow,
|
||||||
generalSettings.showAll.flow,
|
generalSettings.showAll.flow,
|
||||||
bluetoothManager.isBluetoothEnabled,
|
bluetoothManager.isBluetoothEnabled,
|
||||||
) { _, permissions, pods, isDebugMode, showAll, isBluetoothEnabled ->
|
podMonitor.mainDevice,
|
||||||
|
) { _, permissions, pods, isDebugMode, showAll, isBluetoothEnabled, mainPod ->
|
||||||
val items = mutableListOf<OverviewAdapter.Item>()
|
val items = mutableListOf<OverviewAdapter.Item>()
|
||||||
|
|
||||||
val mainPod = podMonitor.mainDevice.first()
|
|
||||||
|
|
||||||
permissions
|
permissions
|
||||||
.map { perm ->
|
.map { perm ->
|
||||||
PermissionCardVH.Item(
|
PermissionCardVH.Item(
|
||||||
permission = perm,
|
permission = perm,
|
||||||
onRequest = { requestPermissionEvent.postValue(it) }
|
onRequest = { requestPermissionEvent.postValue(it) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.run { items.addAll(this) }
|
.run { items.addAll(this) }
|
||||||
@@ -127,37 +126,35 @@ class OverviewFragmentVM @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (permissions.isEmpty() && isBluetoothEnabled) {
|
if (permissions.isEmpty() && isBluetoothEnabled) {
|
||||||
pods
|
pods.map {
|
||||||
.map {
|
val now = Instant.now()
|
||||||
val now = Instant.now()
|
when (it) {
|
||||||
when (it) {
|
is DualApplePods -> DualApplePodsCardVH.Item(
|
||||||
is DualApplePods -> DualApplePodsCardVH.Item(
|
now = now,
|
||||||
now = now,
|
device = it,
|
||||||
device = it,
|
showDebug = isDebugMode,
|
||||||
showDebug = isDebugMode,
|
isMainPod = it == mainPod,
|
||||||
isMainPod = it == mainPod,
|
)
|
||||||
)
|
is SingleApplePods -> SingleApplePodsCardVH.Item(
|
||||||
is SingleApplePods -> SingleApplePodsCardVH.Item(
|
now = now,
|
||||||
now = now,
|
device = it,
|
||||||
device = it,
|
showDebug = isDebugMode,
|
||||||
showDebug = isDebugMode,
|
isMainPod = it == mainPod,
|
||||||
isMainPod = it == mainPod,
|
)
|
||||||
)
|
is BasicSingleApplePods -> BasicSingleApplePodsCardVH.Item(
|
||||||
is BasicSingleApplePods -> BasicSingleApplePodsCardVH.Item(
|
now = now,
|
||||||
now = now,
|
device = it,
|
||||||
device = it,
|
showDebug = isDebugMode,
|
||||||
showDebug = isDebugMode,
|
isMainPod = it == mainPod,
|
||||||
isMainPod = it == mainPod,
|
)
|
||||||
)
|
else -> UnknownPodDeviceCardVH.Item(
|
||||||
else -> UnknownPodDeviceCardVH.Item(
|
now = now,
|
||||||
now = now,
|
device = it,
|
||||||
device = it,
|
showDebug = isDebugMode,
|
||||||
showDebug = isDebugMode,
|
isMainPod = it == mainPod,
|
||||||
isMainPod = it == mainPod,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.run { items.addAll(this) }
|
}.run { items.addAll(this) }
|
||||||
}
|
}
|
||||||
|
|
||||||
items
|
items
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package eu.darken.capod.main.ui.overview.cards
|
package eu.darken.capod.main.ui.overview.cards
|
||||||
|
|
||||||
|
import android.text.Html
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
|
import eu.darken.capod.common.PrivacyPolicy
|
||||||
import eu.darken.capod.common.lists.binding
|
import eu.darken.capod.common.lists.binding
|
||||||
import eu.darken.capod.common.lists.differ.DifferItem
|
import eu.darken.capod.common.lists.differ.DifferItem
|
||||||
import eu.darken.capod.common.permissions.Permission
|
import eu.darken.capod.common.permissions.Permission
|
||||||
@@ -25,6 +28,12 @@ class PermissionCardVH(parent: ViewGroup) :
|
|||||||
permissionLabel.setText(item.permission.labelRes)
|
permissionLabel.setText(item.permission.labelRes)
|
||||||
permissionDescription.setText(item.permission.descriptionRes)
|
permissionDescription.setText(item.permission.descriptionRes)
|
||||||
grantAction.setOnClickListener { item.onRequest(item.permission) }
|
grantAction.setOnClickListener { item.onRequest(item.permission) }
|
||||||
|
privacyPolicy.apply {
|
||||||
|
movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
val ppText = getString(R.string.settings_privacy_policy_label)
|
||||||
|
val ppLink = PrivacyPolicy.URL
|
||||||
|
text = Html.fromHtml("<html><a href=\"$ppLink\">$ppText</a></html>", 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Item(
|
data class Item(
|
||||||
|
|||||||
+4
-10
@@ -7,8 +7,8 @@ import eu.darken.capod.R
|
|||||||
import eu.darken.capod.common.lists.binding
|
import eu.darken.capod.common.lists.binding
|
||||||
import eu.darken.capod.databinding.OverviewPodsAppleSingleBasicItemBinding
|
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.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
|
||||||
|
|
||||||
@@ -33,16 +33,10 @@ 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)})")
|
|
||||||
}
|
|
||||||
|
|
||||||
headphones.apply {
|
batteryLabel.text = device.getBatteryLevelHeadset(context)
|
||||||
val sb = StringBuilder(context.getString(R.string.pods_single_headphones_label))
|
batteryIcon.setImageResource(getBatteryDrawable(device.batteryHeadsetPercent))
|
||||||
sb.append("\n").append(device.getBatteryLevelHeadset(context))
|
|
||||||
text = sb
|
|
||||||
}
|
|
||||||
|
|
||||||
status.apply {
|
status.apply {
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
|
|||||||
+56
-44
@@ -2,14 +2,16 @@ 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 eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.lists.binding
|
import eu.darken.capod.common.lists.binding
|
||||||
import eu.darken.capod.databinding.OverviewPodsAppleDualItemBinding
|
import eu.darken.capod.databinding.OverviewPodsAppleDualItemBinding
|
||||||
import eu.darken.capod.pods.core.*
|
import eu.darken.capod.pods.core.*
|
||||||
import eu.darken.capod.pods.core.HasDualPods.Pod
|
|
||||||
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) :
|
||||||
@@ -24,61 +26,71 @@ class DualApplePodsCardVH(parent: ViewGroup) :
|
|||||||
val device = item.device
|
val device = item.device
|
||||||
name.apply {
|
name.apply {
|
||||||
val sb = StringBuilder(device.getLabel(context))
|
val sb = StringBuilder(device.getLabel(context))
|
||||||
if (device.deviceColor != DeviceColor.UNKNOWN) {
|
if (!listOf(DeviceColor.WHITE, DeviceColor.UNKNOWN).contains(device.deviceColor)) {
|
||||||
sb.append(" (${device.getDeviceColorLabel(context)})")
|
sb.append(" (${device.deviceColor.name})")
|
||||||
}
|
}
|
||||||
text = sb
|
text = sb
|
||||||
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
|
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
|
||||||
else setTypeface(typeface, Typeface.NORMAL)
|
else setTypeface(typeface, Typeface.NORMAL)
|
||||||
|
if (item.showDebug) {
|
||||||
|
append(" [${device.primaryPod.name}]")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
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)
|
reception.text = item.getReceptionText()
|
||||||
if (item.isMainPod) {
|
|
||||||
reception.append("\n(${getString(R.string.pods_yours)})")
|
// Left Pod
|
||||||
|
device.apply {
|
||||||
|
podLeftBatteryIcon.setImageResource(getBatteryDrawable(batteryLeftPodPercent))
|
||||||
|
podLeftBatteryLabel.text = getBatteryLevelLeftPod(context)
|
||||||
|
|
||||||
|
podLeftChargingIcon.isInvisible = !isLeftPodCharging
|
||||||
|
podLeftChargingLabel.isInvisible = !isLeftPodCharging
|
||||||
|
|
||||||
|
podLeftMicrophoneIcon.isInvisible = !isLeftPodMicrophone
|
||||||
|
podLeftMicrophoneLabel.isInvisible = !isLeftPodMicrophone
|
||||||
|
|
||||||
|
podLeftWearIcon.isInvisible = !isLeftPodInEar
|
||||||
|
podLeftWearLabel.isInvisible = !isLeftPodInEar
|
||||||
}
|
}
|
||||||
|
|
||||||
podLeft.apply {
|
// Right Pod
|
||||||
val sb = StringBuilder(context.getString(R.string.pods_dual_left_label))
|
device.apply {
|
||||||
sb.append("\n").append(device.getBatteryLevelLeftPod(context))
|
podRightBatteryIcon.setImageResource(getBatteryDrawable(batteryRightPodPercent))
|
||||||
when {
|
podRightBatteryLabel.text = getBatteryLevelRightPod(context)
|
||||||
device.isLeftPodCharging -> sb.append("\n").append(getString(R.string.pods_charging_label))
|
|
||||||
device.isLeftPodInEar -> sb.append("\n").append(getString(R.string.pods_inear_label))
|
podRightChargingIcon.isInvisible = !isRightPodCharging
|
||||||
else -> {}
|
podRightChargingLabel.isInvisible = !isRightPodCharging
|
||||||
|
|
||||||
|
podRightMicrophoneIcon.isInvisible = !isRightPodMicrophone
|
||||||
|
podRightMicrophoneLabel.isInvisible = !isRightPodMicrophone
|
||||||
|
|
||||||
|
podRightWearIcon.isInvisible = !isRightPodInEar
|
||||||
|
podRightWearLabel.isInvisible = !isRightPodInEar
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case
|
||||||
|
device.apply {
|
||||||
|
podCaseBatteryIcon.setImageResource(getBatteryDrawable(batteryCasePercent))
|
||||||
|
podCaseBatteryLabel.text = getBatteryLevelCase(context)
|
||||||
|
|
||||||
|
podCaseChargingIcon.isInvisible = !isCaseCharging
|
||||||
|
podCaseChargingLabel.isInvisible = !isCaseCharging
|
||||||
|
|
||||||
|
podCaseLidLabel.text = when (caseLidState) {
|
||||||
|
LidState.OPEN -> context.getString(R.string.pods_case_status_open_label)
|
||||||
|
LidState.CLOSED -> context.getString(R.string.pods_case_status_closed_label)
|
||||||
|
else -> context.getString(R.string.general_value_unknown_label)
|
||||||
}
|
}
|
||||||
text = sb
|
|
||||||
}
|
|
||||||
|
|
||||||
podRight.apply {
|
val hideInfo = !listOf(LidState.OPEN, LidState.CLOSED).contains(caseLidState)
|
||||||
val sb = StringBuilder(getString(R.string.pods_dual_right_label))
|
podCaseLidIcon.isInvisible = hideInfo
|
||||||
sb.append("\n").append(device.getBatteryLevelRightPod(context))
|
podCaseLidLabel.isInvisible = hideInfo
|
||||||
when {
|
|
||||||
device.isRightPodCharging -> sb.append("\n").append(getString(R.string.pods_charging_label))
|
|
||||||
device.isRightPodInEar -> sb.append("\n").append(getString(R.string.pods_inear_label))
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
text = sb
|
|
||||||
}
|
|
||||||
|
|
||||||
when (device.microPhonePod) {
|
|
||||||
Pod.LEFT -> podLeft.append("\n(${context.getString(R.string.pods_microphone_label)})")
|
|
||||||
Pod.RIGHT -> podRight.append("\n(${context.getString(R.string.pods_microphone_label)})")
|
|
||||||
}
|
|
||||||
|
|
||||||
podCase.apply {
|
|
||||||
val sb = StringBuilder(getString(R.string.pods_case_label))
|
|
||||||
sb.append("\n").append(device.getBatteryLevelCase(context))
|
|
||||||
if (device.isCaseCharging) sb.append("\n").append(getString(R.string.pods_charging_label))
|
|
||||||
when (device.caseLidState) {
|
|
||||||
LidState.OPEN -> sb.append("\n").append(context.getString(R.string.pods_case_status_open_label))
|
|
||||||
LidState.CLOSED -> sb.append("\n").append(context.getString(R.string.pods_case_status_closed_label))
|
|
||||||
LidState.NOT_IN_CASE,
|
|
||||||
LidState.UNKNOWN -> {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
text = sb
|
|
||||||
}
|
}
|
||||||
|
|
||||||
status.apply {
|
status.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
|
||||||
|
|||||||
+11
-15
@@ -3,12 +3,13 @@ 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.isGone
|
||||||
|
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
|
||||||
import eu.darken.capod.databinding.OverviewPodsAppleSingleItemBinding
|
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.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
|
||||||
|
|
||||||
@@ -33,21 +34,16 @@ 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)})")
|
|
||||||
}
|
|
||||||
|
|
||||||
headphones.apply {
|
batteryLabel.text = device.getBatteryLevelHeadset(context)
|
||||||
val sb = StringBuilder(context.getString(R.string.pods_single_headphones_label))
|
batteryIcon.setImageResource(getBatteryDrawable(device.batteryHeadsetPercent))
|
||||||
sb.append("\n").append(device.getBatteryLevelHeadset(context))
|
|
||||||
when {
|
chargingIcon.isInvisible = device.isHeadsetBeingCharged
|
||||||
device.isHeadsetBeingCharged -> sb.append("\n").append("Charging")
|
chargingLabel.isInvisible = device.isHeadsetBeingCharged
|
||||||
device.isHeadphonesBeingWorn -> sb.append("\n").append("In ear")
|
|
||||||
else -> {}
|
wearIcon.isInvisible = device.isHeadphonesBeingWorn
|
||||||
}
|
wearLabel.isInvisible = device.isHeadphonesBeingWorn
|
||||||
text = sb
|
|
||||||
}
|
|
||||||
|
|
||||||
status.apply {
|
status.apply {
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
|
|||||||
+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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class SettingsFragment : Fragment2(R.layout.settings_fragment),
|
|||||||
}
|
}
|
||||||
|
|
||||||
ui.toolbar.apply {
|
ui.toolbar.apply {
|
||||||
subtitle = BuildConfigWrap.VERSION_DESCRIPTION
|
subtitle = BuildConfigWrap.VERSION_DESCRIPTION_SHORT
|
||||||
setNavigationOnClickListener { requireActivity().onBackPressed() }
|
setNavigationOnClickListener { requireActivity().onBackPressed() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||||||
import eu.darken.androidstarter.common.preferences.Settings
|
import eu.darken.androidstarter.common.preferences.Settings
|
||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.BuildConfigWrap
|
import eu.darken.capod.common.BuildConfigWrap
|
||||||
|
import eu.darken.capod.common.PrivacyPolicy
|
||||||
import eu.darken.capod.common.WebpageTool
|
import eu.darken.capod.common.WebpageTool
|
||||||
import eu.darken.capod.common.uix.PreferenceFragment2
|
import eu.darken.capod.common.uix.PreferenceFragment2
|
||||||
import eu.darken.capod.main.core.GeneralSettings
|
import eu.darken.capod.main.core.GeneralSettings
|
||||||
@@ -34,7 +35,11 @@ class SettingsIndexFragment : PreferenceFragment2() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPreferencesCreated() {
|
override fun onPreferencesCreated() {
|
||||||
findPreference<Preference>("core.changelog")!!.summary = BuildConfigWrap.VERSION_DESCRIPTION
|
findPreference<Preference>("core.changelog")!!.summary = BuildConfigWrap.VERSION_DESCRIPTION_LONG
|
||||||
|
findPreference<Preference>("core.privacy")!!.setOnPreferenceClickListener {
|
||||||
|
webpageTool.open(PrivacyPolicy.URL)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
super.onPreferencesCreated()
|
super.onPreferencesCreated()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class SupportFragmentVM @Inject constructor(
|
|||||||
|
|
||||||
bodyInfo.append("--- Infos for the developer ---\n")
|
bodyInfo.append("--- Infos for the developer ---\n")
|
||||||
|
|
||||||
bodyInfo.append("App version: ").append(BuildConfigWrap.VERSION_DESCRIPTION).append("\n")
|
bodyInfo.append("App version: ").append(BuildConfigWrap.VERSION_DESCRIPTION_LONG).append("\n")
|
||||||
|
|
||||||
bodyInfo.append("Device: ").append(Build.FINGERPRINT).append("\n")
|
bodyInfo.append("Device: ").append(Build.FINGERPRINT).append("\n")
|
||||||
bodyInfo.append("Install ID: ").append(installId.id).append("\n")
|
bodyInfo.append("Install ID: ").append(installId.id).append("\n")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.darken.capod.monitor.core
|
package eu.darken.capod.monitor.core
|
||||||
|
|
||||||
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.bluetooth.BleScanner
|
import eu.darken.capod.common.bluetooth.BleScanner
|
||||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||||
import eu.darken.capod.common.coroutine.AppScope
|
import eu.darken.capod.common.coroutine.AppScope
|
||||||
@@ -9,9 +10,11 @@ import eu.darken.capod.common.debug.logging.log
|
|||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.common.flow.replayingShare
|
import eu.darken.capod.common.flow.replayingShare
|
||||||
import eu.darken.capod.main.core.GeneralSettings
|
import eu.darken.capod.main.core.GeneralSettings
|
||||||
|
import eu.darken.capod.main.core.ScannerMode
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.PodFactory
|
import eu.darken.capod.pods.core.PodFactory
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
@@ -32,38 +35,47 @@ class PodMonitor @Inject constructor(
|
|||||||
private val deviceCache = mutableMapOf<PodDevice.Id, PodDevice>()
|
private val deviceCache = mutableMapOf<PodDevice.Id, PodDevice>()
|
||||||
private val cacheLock = Mutex()
|
private val cacheLock = Mutex()
|
||||||
|
|
||||||
|
private suspend fun List<BleScanResult>.preFilterAndMap(
|
||||||
|
scannerMode: ScannerMode
|
||||||
|
): List<PodDevice> = this
|
||||||
|
.groupBy { it.address }
|
||||||
|
.values
|
||||||
|
.map { sameAdrDevs ->
|
||||||
|
// For each address we only want the newest result, upstream may batch data
|
||||||
|
val newest = sameAdrDevs.maxByOrNull { it.generatedAtNanos }!!
|
||||||
|
sameAdrDevs.minus(newest).let {
|
||||||
|
if (it.isNotEmpty()) log(TAG, VERBOSE) { "Discarding stale results: $it" }
|
||||||
|
}
|
||||||
|
newest
|
||||||
|
}
|
||||||
|
.mapNotNull { podFactory.createPod(it) }
|
||||||
|
|
||||||
val devices: Flow<List<PodDevice>> = bluetoothManager.isBluetoothEnabled
|
val devices: Flow<List<PodDevice>> = bluetoothManager.isBluetoothEnabled
|
||||||
.flatMapLatest { isBluetoothEnabled ->
|
.flatMapLatest { isBluetoothEnabled ->
|
||||||
if (isBluetoothEnabled) {
|
if (isBluetoothEnabled) {
|
||||||
|
log(TAG) { "Bluetooth is enabled" }
|
||||||
generalSettings.scannerMode.flow
|
generalSettings.scannerMode.flow
|
||||||
|
.flatMapLatest { mode ->
|
||||||
|
bleScanner.scan(scannerMode = mode).map { it.preFilterAndMap(mode) }
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log(TAG, WARN) { "Bluetooth is currently disabled" }
|
log(TAG, WARN) { "Bluetooth is currently disabled" }
|
||||||
emptyFlow()
|
flowOf(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.flatMapLatest { bleScanner.scan(scannerMode = it) }
|
.map { newPods ->
|
||||||
.map { result ->
|
|
||||||
// For each address we only want the newest result, upstream may batch data
|
|
||||||
result.groupBy { it.address }
|
|
||||||
.values
|
|
||||||
.map { sameAdrDevs ->
|
|
||||||
val newest = sameAdrDevs.maxByOrNull { it.generatedAtNanos }!!
|
|
||||||
sameAdrDevs.minus(newest).let {
|
|
||||||
if (it.isNotEmpty()) log(TAG, VERBOSE) { "Discarding stale results: $it" }
|
|
||||||
}
|
|
||||||
newest
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.map { scanResults ->
|
|
||||||
val newPods = scanResults
|
|
||||||
.mapNotNull { podFactory.createPod(it) }
|
|
||||||
|
|
||||||
val pods = mutableMapOf<PodDevice.Id, PodDevice>()
|
val pods = mutableMapOf<PodDevice.Id, PodDevice>()
|
||||||
|
|
||||||
cacheLock.withLock {
|
cacheLock.withLock {
|
||||||
|
if (newPods == null) {
|
||||||
|
log(TAG) { "Null result, Bluetooth is disabled." }
|
||||||
|
deviceCache.clear()
|
||||||
|
return@map emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
@@ -77,45 +89,55 @@ class PodMonitor @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val presorted = pods.values.sortPodsToInterest()
|
||||||
|
val main = presorted.determineMainDevice()
|
||||||
|
|
||||||
val aboveThreshold = mutableListOf<PodDevice>()
|
presorted.sortedByDescending { it == main }
|
||||||
val belowThreshold = mutableListOf<PodDevice>()
|
|
||||||
val minimumSignalQuality = generalSettings.minimumSignalQuality.value
|
|
||||||
pods.values.forEach { device ->
|
|
||||||
if (device.signalQuality > minimumSignalQuality) aboveThreshold.add(device)
|
|
||||||
else belowThreshold.add(device)
|
|
||||||
}
|
|
||||||
val now = Instant.now()
|
|
||||||
aboveThreshold.sortedWith(compareBy<PodDevice> {
|
|
||||||
Duration.between(
|
|
||||||
it.lastSeenAt,
|
|
||||||
now
|
|
||||||
)
|
|
||||||
}.thenByDescending { it.rssi })
|
|
||||||
.plus(belowThreshold.sortedWith(compareBy<PodDevice> {
|
|
||||||
Duration.between(
|
|
||||||
it.lastSeenAt,
|
|
||||||
now
|
|
||||||
)
|
|
||||||
}.thenByDescending { it.rssi }))
|
|
||||||
// pods.values.sortedByDescending { it.rssi }
|
|
||||||
}
|
}
|
||||||
.onStart { emit(emptyList()) }
|
.onStart { emit(emptyList()) }
|
||||||
.catch {
|
.retryWhen { cause, attempt ->
|
||||||
log(TAG, ERROR) { "PodMonitor failed:\n${it.asLog()}" }
|
log(TAG, WARN) { "PodMonitor failed (attempt=$attempt), will retry: ${cause.asLog()}" }
|
||||||
|
delay(3000)
|
||||||
|
true
|
||||||
}
|
}
|
||||||
.replayingShare(appScope)
|
.replayingShare(appScope)
|
||||||
|
|
||||||
val mainDevice: Flow<PodDevice?>
|
val mainDevice: Flow<PodDevice?>
|
||||||
get() = devices.map { devices ->
|
get() = devices
|
||||||
devices.maxByOrNull { it.rssi }?.let filter@{ device ->
|
.map { it.determineMainDevice() }
|
||||||
val minimumSignalQuality = generalSettings.minimumSignalQuality.value
|
.replayingShare(appScope)
|
||||||
|
|
||||||
generalSettings.mainDeviceModel.value.let {
|
private fun Collection<PodDevice>.sortPodsToInterest(): List<PodDevice> = this.let { devices ->
|
||||||
if (device.model != it && it != PodDevice.Model.UNKNOWN) return@filter null
|
val now = Instant.now()
|
||||||
|
|
||||||
|
return@let devices.sortedWith(
|
||||||
|
compareByDescending<PodDevice> { true }
|
||||||
|
.thenBy {
|
||||||
|
val age = Duration.between(it.seenLastAt, now).seconds
|
||||||
|
if (age < 5) 0L else (age / 3L).toLong()
|
||||||
}
|
}
|
||||||
|
.thenByDescending { it.signalQuality }
|
||||||
|
.thenByDescending { (it.seenCounter / 10) }
|
||||||
|
|
||||||
if (device.signalQuality > minimumSignalQuality) device else null
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package eu.darken.capod.monitor.core
|
||||||
|
|
||||||
|
import dagger.Reusable
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@Reusable
|
||||||
|
class PodSorter @Inject constructor(
|
||||||
|
|
||||||
|
)
|
||||||
@@ -21,6 +21,7 @@ import eu.darken.capod.main.core.PermissionTool
|
|||||||
import eu.darken.capod.monitor.core.*
|
import eu.darken.capod.monitor.core.*
|
||||||
import eu.darken.capod.monitor.ui.MonitorNotifications
|
import eu.darken.capod.monitor.ui.MonitorNotifications
|
||||||
import eu.darken.capod.reaction.ReactionHub
|
import eu.darken.capod.reaction.ReactionHub
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.cancelChildren
|
import kotlinx.coroutines.cancelChildren
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -68,9 +69,13 @@ class MonitorWorker @AssistedInject constructor(
|
|||||||
|
|
||||||
Result.success(inputData)
|
Result.success(inputData)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Bugs.report(tag = TAG, "Execution failed", exception = e)
|
if (e !is CancellationException) {
|
||||||
finishedWithError = true
|
Bugs.report(tag = TAG, "Execution failed", exception = e)
|
||||||
Result.failure(inputData)
|
finishedWithError = true
|
||||||
|
Result.failure(inputData)
|
||||||
|
} else {
|
||||||
|
Result.success()
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.workerScope.cancel("Worker finished (withError?=$finishedWithError).")
|
this.workerScope.cancel("Worker finished (withError?=$finishedWithError).")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package eu.darken.capod.monitor.ui
|
package eu.darken.capod.monitor.ui
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
@@ -26,38 +27,45 @@ class MonitorNotificationViewFactory @Inject constructor(
|
|||||||
context.packageName,
|
context.packageName,
|
||||||
R.layout.monitor_notification_dual_pods_small
|
R.layout.monitor_notification_dual_pods_small
|
||||||
).apply {
|
).apply {
|
||||||
device.getBatteryLevelLeftPod(context)
|
device.apply {
|
||||||
.let { if (device.isLeftPodCharging) "$it⚡" else it }
|
// Left
|
||||||
.run { setTextViewText(R.id.pod_left, this) }
|
setTextViewText(R.id.pod_left_label, getBatteryLevelLeftPod(context))
|
||||||
|
setViewVisibility(R.id.pod_left_charging, if (isLeftPodCharging) View.VISIBLE else View.GONE)
|
||||||
|
setViewVisibility(R.id.pod_left_ear, if (isLeftPodInEar) View.VISIBLE else View.GONE)
|
||||||
|
|
||||||
device.getBatteryLevelCase(context)
|
// Case
|
||||||
.let { if (device.isCaseCharging) "$it⚡" else it }
|
setTextViewText(R.id.pod_case_label, getBatteryLevelCase(context))
|
||||||
.run { setTextViewText(R.id.pod_case, this) }
|
setViewVisibility(R.id.pod_case_charging, if (isCaseCharging) View.VISIBLE else View.GONE)
|
||||||
|
|
||||||
device.getBatteryLevelRightPod(context)
|
// Right
|
||||||
.let { if (device.isRightPodCharging) "$it⚡" else it }
|
setTextViewText(R.id.pod_right_label, getBatteryLevelRightPod(context))
|
||||||
.run { setTextViewText(R.id.pod_right, this) }
|
setViewVisibility(R.id.pod_right_charging, if (isRightPodCharging) View.VISIBLE else View.GONE)
|
||||||
|
setViewVisibility(R.id.pod_right_ear, if (isRightPodInEar) View.VISIBLE else View.GONE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSingleApplePods(device: SingleApplePods): RemoteViews = RemoteViews(
|
private fun createSingleApplePods(device: SingleApplePods): RemoteViews = RemoteViews(
|
||||||
context.packageName,
|
context.packageName,
|
||||||
R.layout.monitor_notification_single_pods_small
|
R.layout.monitor_notification_single_pods_small
|
||||||
).apply {
|
).apply {
|
||||||
setTextViewText(R.id.headphones_label, device.getLabel(context))
|
device.apply {
|
||||||
|
setTextViewText(R.id.headphones_label, getLabel(context))
|
||||||
device.getBatteryLevelHeadset(context)
|
setImageViewResource(R.id.headphones_battery_icon, getBatteryDrawable(batteryHeadsetPercent))
|
||||||
.let { if (!device.isHeadsetBeingCharged) "$it⚡" else it }
|
setTextViewText(R.id.headphones_battery_label, getBatteryLevelHeadset(context))
|
||||||
.run { setTextViewText(R.id.headphones, this) }
|
setViewVisibility(R.id.headphones_charging, if (isHeadsetBeingCharged) View.VISIBLE else View.GONE)
|
||||||
|
setViewVisibility(R.id.headphones_worn, if (isHeadphonesBeingWorn) View.VISIBLE else View.GONE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSingleBasicApplePods(device: BasicSingleApplePods): RemoteViews = RemoteViews(
|
private fun createSingleBasicApplePods(device: BasicSingleApplePods): RemoteViews = RemoteViews(
|
||||||
context.packageName,
|
context.packageName,
|
||||||
R.layout.monitor_notification_single_pods_basic_small
|
R.layout.monitor_notification_single_pods_basic_small
|
||||||
).apply {
|
).apply {
|
||||||
setTextViewText(R.id.headphones_label, device.getLabel(context))
|
device.apply {
|
||||||
|
setTextViewText(R.id.headphones_label, getLabel(context))
|
||||||
device.getBatteryLevelHeadset(context)
|
setImageViewResource(R.id.headphones_battery_icon, getBatteryDrawable(batteryHeadsetPercent))
|
||||||
.run { setTextViewText(R.id.headphones, this) }
|
setTextViewText(R.id.headphones_battery_label, getBatteryLevelHeadset(context))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createUnknownDevice(device: PodDevice): RemoteViews = RemoteViews(
|
private fun createUnknownDevice(device: PodDevice): RemoteViews = RemoteViews(
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package eu.darken.capod.pods.core
|
||||||
|
|
||||||
|
interface HasDualEarDetection : HasEarDetection {
|
||||||
|
|
||||||
|
val isLeftPodInEar: Boolean
|
||||||
|
|
||||||
|
val isRightPodInEar: Boolean
|
||||||
|
|
||||||
|
val isEitherPodInEar: Boolean
|
||||||
|
get() = isLeftPodInEar || isRightPodInEar
|
||||||
|
|
||||||
|
override val isBeingWorn: Boolean
|
||||||
|
get() = isLeftPodInEar && isRightPodInEar
|
||||||
|
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
|
|||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
interface PodDevice {
|
interface PodDevice {
|
||||||
|
|
||||||
@@ -16,19 +17,28 @@ interface PodDevice {
|
|||||||
|
|
||||||
val model: Model
|
val model: Model
|
||||||
|
|
||||||
val lastSeenAt: Instant
|
val address: String
|
||||||
|
get() = scanResult.address
|
||||||
|
|
||||||
|
val seenLastAt: Instant
|
||||||
|
|
||||||
|
val seenFirstAt: Instant
|
||||||
|
|
||||||
|
val seenCounter: Int
|
||||||
|
|
||||||
val scanResult: BleScanResult
|
val scanResult: BleScanResult
|
||||||
|
|
||||||
val rssi: Int
|
val rssi: Int
|
||||||
get() = scanResult.rssi
|
get() = scanResult.rssi
|
||||||
|
|
||||||
|
val confidence: Float
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is not correct but it works ¯\_(ツ)_/¯
|
* This is not correct but it works ¯\_(ツ)_/¯
|
||||||
* The range of the RSSI is device specific (ROMs).
|
* The range of the RSSI is device specific (ROMs).
|
||||||
*/
|
*/
|
||||||
val signalQuality: Float
|
val signalQuality: Float
|
||||||
get() = (100 - abs(rssi)) / 100f
|
get() = ((100 - abs(rssi)) / 100f) * (max(BASE_CONFIDENCE, confidence))
|
||||||
|
|
||||||
val rawData: ByteArray
|
val rawData: ByteArray
|
||||||
|
|
||||||
@@ -91,4 +101,8 @@ interface PodDevice {
|
|||||||
"Unknown"
|
"Unknown"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val BASE_CONFIDENCE = 0.5f
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.icu.text.RelativeDateTimeFormatter
|
import android.icu.text.RelativeDateTimeFormatter
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -28,13 +29,35 @@ fun PodDevice.getSignalQuality(context: Context): String {
|
|||||||
return "~${percentage.roundToInt()}%"
|
return "~${percentage.roundToInt()}%"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DrawableRes
|
||||||
|
fun getBatteryDrawable(percent: Float?): Int = when {
|
||||||
|
percent == null -> R.drawable.ic_baseline_battery_unknown_24
|
||||||
|
percent > 0.95f -> R.drawable.ic_baseline_battery_full_24
|
||||||
|
percent > 0.80f -> R.drawable.ic_baseline_battery_6_bar_24
|
||||||
|
percent > 0.65f -> R.drawable.ic_baseline_battery_5_bar_24
|
||||||
|
percent > 0.50f -> R.drawable.ic_baseline_battery_4_bar_24
|
||||||
|
percent > 0.35f -> R.drawable.ic_baseline_battery_3_bar_24
|
||||||
|
percent > 0.20f -> R.drawable.ic_baseline_battery_2_bar_24
|
||||||
|
percent > 0.05f -> R.drawable.ic_baseline_battery_1_bar_24
|
||||||
|
else -> R.drawable.ic_baseline_battery_0_bar_24
|
||||||
|
}
|
||||||
|
|
||||||
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
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@ import javax.inject.Singleton
|
|||||||
class AppleFactory @Inject constructor(
|
class AppleFactory @Inject constructor(
|
||||||
private val continuityProtocolDecoder: ContinuityProtocol.Decoder,
|
private val continuityProtocolDecoder: ContinuityProtocol.Decoder,
|
||||||
private val proximityPairingDecoder: ProximityPairing.Decoder,
|
private val proximityPairingDecoder: ProximityPairing.Decoder,
|
||||||
private val podFactories: @JvmSuppressWildcards Set<ApplePods.Factory>,
|
private val podFactories: @JvmSuppressWildcards Set<ApplePodsFactory<out ApplePods>>,
|
||||||
private val unknownAppleFactory: UnknownAppleDevice.Factory,
|
private val unknownAppleFactory: UnknownAppleDevice.Factory,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -57,24 +57,27 @@ class AppleFactory @Inject constructor(
|
|||||||
|
|
||||||
log(TAG, INFO) { "Decoding $scanResult" }
|
log(TAG, INFO) { "Decoding $scanResult" }
|
||||||
|
|
||||||
var factory = podFactories.firstOrNull { it.isResponsible(pm) }
|
val factory = podFactories.firstOrNull { it.isResponsible(pm) }
|
||||||
|
|
||||||
if (factory == null) {
|
val device = (factory ?: unknownAppleFactory).create(
|
||||||
|
scanResult = scanResult,
|
||||||
|
proximityMessage = pm,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (factory == null && !SILENCED_PMS.contains(device.rawDeviceModel) && scanResult.address != "6E:9E:D1:49:D2:6D") {
|
||||||
|
SILENCED_PMS.add(device.rawDeviceModel)
|
||||||
Bugs.report(
|
Bugs.report(
|
||||||
tag = TAG,
|
tag = TAG,
|
||||||
message = "Unknown proximity message type",
|
message = "Unknown proximity message type",
|
||||||
exception = IllegalArgumentException("Unknown ProximityMessage: $pm")
|
exception = IllegalArgumentException("Unknown ProximityMessage: $pm")
|
||||||
)
|
)
|
||||||
factory = unknownAppleFactory
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return factory.create(
|
return@withLock device
|
||||||
scanResult = scanResult,
|
|
||||||
proximityMessage = pm,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private val SILENCED_PMS = mutableSetOf<UShort>()
|
||||||
private val TAG = logTag("Pod", "Apple", "Factory")
|
private val TAG = logTag("Pod", "Apple", "Factory")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,17 +12,17 @@ import eu.darken.capod.pods.core.apple.beats.*
|
|||||||
@Module
|
@Module
|
||||||
abstract class AppleFactoryModule {
|
abstract class AppleFactoryModule {
|
||||||
|
|
||||||
@Binds @IntoSet abstract fun airPodsGen1(factory: AirPodsGen1.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun airPodsGen1(factory: AirPodsGen1.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun airPodsGen2(factory: AirPodsGen2.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun airPodsGen2(factory: AirPodsGen2.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun airPodsGen3(factory: AirPodsGen3.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun airPodsGen3(factory: AirPodsGen3.Factory): ApplePodsFactory<out ApplePods>
|
||||||
|
|
||||||
@Binds @IntoSet abstract fun airPodsPro(factory: AirPodsPro.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun airPodsPro(factory: AirPodsPro.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun airPodsMax(factory: AirPodsMax.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun airPodsMax(factory: AirPodsMax.Factory): ApplePodsFactory<out ApplePods>
|
||||||
|
|
||||||
@Binds @IntoSet abstract fun beatsFlex(factory: BeatsFlex.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun beatsFlex(factory: BeatsFlex.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun beatsSolo3(factory: BeatsSolo3.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun beatsSolo3(factory: BeatsSolo3.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun beatsStudio3(factory: BeatsStudio3.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun beatsStudio3(factory: BeatsStudio3.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun beatsX(factory: BeatsX.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun beatsX(factory: BeatsX.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun powerBeats3(factory: PowerBeats3.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun powerBeats3(factory: PowerBeats3.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePodsFactory<out ApplePods>
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
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.lowerNibble
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.toHex
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.DEBUG
|
import eu.darken.capod.common.upperNibble
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
|
||||||
import eu.darken.capod.common.debug.logging.log
|
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Duration
|
|
||||||
|
|
||||||
interface ApplePods : PodDevice {
|
interface ApplePods : PodDevice {
|
||||||
|
|
||||||
@@ -27,11 +24,20 @@ interface ApplePods : PodDevice {
|
|||||||
val rawStatus: UByte
|
val rawStatus: UByte
|
||||||
get() = proximityMessage.data[3]
|
get() = proximityMessage.data[3]
|
||||||
|
|
||||||
|
val rawStatusHex: String
|
||||||
|
get() = rawStatus.toHex()
|
||||||
|
|
||||||
val rawPodsBattery: UByte
|
val rawPodsBattery: UByte
|
||||||
get() = proximityMessage.data[4]
|
get() = proximityMessage.data[4]
|
||||||
|
|
||||||
val rawCaseBattery: UByte
|
val rawPodsBatteryHex: String
|
||||||
get() = proximityMessage.data[5]
|
get() = rawPodsBattery.toHex()
|
||||||
|
|
||||||
|
val rawFlags: UShort
|
||||||
|
get() = proximityMessage.data[5].upperNibble
|
||||||
|
|
||||||
|
val rawCaseBattery: UShort
|
||||||
|
get() = proximityMessage.data[5].lowerNibble
|
||||||
|
|
||||||
val rawCaseLidState: UByte
|
val rawCaseLidState: UByte
|
||||||
get() = proximityMessage.data[6]
|
get() = proximityMessage.data[6]
|
||||||
@@ -43,102 +49,4 @@ interface ApplePods : PodDevice {
|
|||||||
get() = proximityMessage.data[8]
|
get() = proximityMessage.data[8]
|
||||||
|
|
||||||
|
|
||||||
abstract class Factory(private val tag: String) {
|
|
||||||
|
|
||||||
internal data class ValueCache(
|
|
||||||
val caseBatteryPercentage: Float?
|
|
||||||
)
|
|
||||||
|
|
||||||
internal val cachedValues = mutableMapOf<PodDevice.Id, ValueCache>()
|
|
||||||
|
|
||||||
internal data class KnownDevice(
|
|
||||||
val identifier: PodDevice.Id,
|
|
||||||
val scanResult: BleScanResult,
|
|
||||||
val message: ProximityPairing.Message,
|
|
||||||
) {
|
|
||||||
val address: String
|
|
||||||
get() = scanResult.address
|
|
||||||
|
|
||||||
val rssi: Int
|
|
||||||
get() = scanResult.rssi
|
|
||||||
|
|
||||||
val timestampNanos: Duration
|
|
||||||
get() = Duration.ofNanos(scanResult.generatedAtNanos)
|
|
||||||
|
|
||||||
fun isOlderThan(age: Duration): Boolean {
|
|
||||||
val now = Duration.ofNanos(SystemClockWrap.elapsedRealtimeNanos)
|
|
||||||
return now - timestampNanos > age
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val knownDevs = mutableMapOf<PodDevice.Id, KnownDevice>()
|
|
||||||
|
|
||||||
internal fun recognizeDevice(scanResult: BleScanResult, message: ProximityPairing.Message): PodDevice.Id {
|
|
||||||
val address = scanResult.address
|
|
||||||
|
|
||||||
var identifier: PodDevice.Id? = null
|
|
||||||
|
|
||||||
knownDevs.values.firstOrNull { it.address == address }?.let {
|
|
||||||
log(tag, VERBOSE) { "recognizeDevice: Recovered previous ID via address: $it" }
|
|
||||||
knownDevs[it.identifier] = it.copy(
|
|
||||||
scanResult = scanResult,
|
|
||||||
message = message,
|
|
||||||
)
|
|
||||||
identifier = it.identifier
|
|
||||||
}
|
|
||||||
|
|
||||||
if (identifier == null) {
|
|
||||||
val currentMarkers = message.getRecogMarkers()
|
|
||||||
knownDevs.values
|
|
||||||
.firstOrNull { it.message.getRecogMarkers() == currentMarkers }
|
|
||||||
?.let {
|
|
||||||
log(tag, DEBUG) { "recognizeDevice: Close match based similarity markers." }
|
|
||||||
log(tag, DEBUG) { "recognizeDevice: Old marker: ${it.message.getRecogMarkers()}" }
|
|
||||||
log(tag, DEBUG) { "recognizeDevice: New marker: $currentMarkers" }
|
|
||||||
knownDevs[it.identifier] = it.copy(
|
|
||||||
scanResult = scanResult,
|
|
||||||
message = message,
|
|
||||||
)
|
|
||||||
identifier = it.identifier
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (identifier == null) {
|
|
||||||
log(tag, VERBOSE) { "recognizeDevice: Mapping as new device" }
|
|
||||||
identifier = PodDevice.Id()
|
|
||||||
}
|
|
||||||
|
|
||||||
knownDevs[identifier!!] = KnownDevice(
|
|
||||||
identifier = identifier!!,
|
|
||||||
scanResult = scanResult,
|
|
||||||
message = message
|
|
||||||
)
|
|
||||||
|
|
||||||
knownDevs.values.toList().forEach { knownDevice ->
|
|
||||||
if (knownDevice.isOlderThan(Duration.ofSeconds(20))) {
|
|
||||||
log(tag, VERBOSE) { "recognizeDevice: Removing stale known device: $knownDevice" }
|
|
||||||
knownDevs.remove(knownDevice.identifier)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return identifier!!
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ModelInfo(
|
|
||||||
val full: UShort,
|
|
||||||
val dirty: UByte,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun ProximityPairing.Message.getModelInfo(): ModelInfo = ModelInfo(
|
|
||||||
full = (((data[1].toInt() and 255) shl 8) or (data[2].toInt() and 255)).toUShort(),
|
|
||||||
dirty = data[1]
|
|
||||||
)
|
|
||||||
|
|
||||||
abstract fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean
|
|
||||||
|
|
||||||
abstract fun create(
|
|
||||||
scanResult: BleScanResult,
|
|
||||||
proximityMessage: ProximityPairing.Message,
|
|
||||||
): ApplePods
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
|
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
|
import eu.darken.capod.common.lowerNibble
|
||||||
|
import eu.darken.capod.common.upperNibble
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
||||||
|
|
||||||
|
data class Markings(
|
||||||
|
val vendor: UByte,
|
||||||
|
val length: UByte,
|
||||||
|
val device: UShort,
|
||||||
|
val podBatteryData: Set<UShort>,
|
||||||
|
val caseBatteryData: UShort,
|
||||||
|
val deviceColor: UByte,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun ProximityPairing.Message.getApplePodsMarkings(): Markings = Markings(
|
||||||
|
vendor = ProximityPairing.CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING,
|
||||||
|
length = ProximityPairing.PROXIMITY_PAIRING_MESSAGE_LENGTH.toUByte(),
|
||||||
|
device = (((data[1].toInt() and 255) shl 8) or (data[2].toInt() and 255)).toUShort(),
|
||||||
|
// Make comparison order independent
|
||||||
|
podBatteryData = setOf(data[4].upperNibble, data[4].lowerNibble),
|
||||||
|
caseBatteryData = data[5].lowerNibble,
|
||||||
|
deviceColor = data[7]
|
||||||
|
)
|
||||||
|
|
||||||
|
data class KnownDevice(
|
||||||
|
val id: PodDevice.Id,
|
||||||
|
val seenFirstAt: Instant,
|
||||||
|
val seenCounter: Int,
|
||||||
|
val history: List<ApplePods>
|
||||||
|
) {
|
||||||
|
val lastMessage: ProximityPairing.Message
|
||||||
|
get() = history.last().proximityMessage
|
||||||
|
|
||||||
|
val lastAddress: String
|
||||||
|
get() = history.last().address
|
||||||
|
|
||||||
|
val confidence: Float
|
||||||
|
get() = 0.75f + (history.size / (MAX_HISTORY * 4f))
|
||||||
|
|
||||||
|
fun averageRssi(latest: Int): Int =
|
||||||
|
history.map { it.rssi }.plus(latest).takeLast(10).median()
|
||||||
|
|
||||||
|
fun isOlderThan(age: Duration): Boolean {
|
||||||
|
val now = Instant.now()
|
||||||
|
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()})"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val MAX_HISTORY = 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val knownDevices = mutableMapOf<PodDevice.Id, KnownDevice>()
|
||||||
|
|
||||||
|
internal open fun searchHistory(current: PodType): KnownDevice? {
|
||||||
|
val scanResult = current.scanResult
|
||||||
|
val message = current.proximityMessage
|
||||||
|
|
||||||
|
knownDevices.values.toList().forEach { knownDevice ->
|
||||||
|
if (knownDevice.isOlderThan(Duration.ofSeconds(20))) {
|
||||||
|
log(tag, VERBOSE) { "searchHistory1: Removing stale known device: $knownDevice" }
|
||||||
|
knownDevices.remove(knownDevice.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
knownDevices.values
|
||||||
|
.filter { it.history.size > KnownDevice.MAX_HISTORY }
|
||||||
|
.toList()
|
||||||
|
.forEach {
|
||||||
|
knownDevices[it.id] = it.copy(history = it.history.takeLast(KnownDevice.MAX_HISTORY))
|
||||||
|
}
|
||||||
|
|
||||||
|
var recognizedDevice: KnownDevice? = knownDevices.values
|
||||||
|
.firstOrNull { it.lastAddress == scanResult.address }
|
||||||
|
?.also { log(tag, VERBOSE) { "searchHistory1: Recovered previous ID via address: $it" } }
|
||||||
|
|
||||||
|
if (recognizedDevice == null) {
|
||||||
|
val currentMarkers = message.getApplePodsMarkings()
|
||||||
|
recognizedDevice = knownDevices.values
|
||||||
|
.firstOrNull { it.lastMessage.getApplePodsMarkings() == currentMarkers }
|
||||||
|
?.also { log(tag) { "searchHistory1: Close match based on similarity: $currentMarkers" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recognizedDevice == null) {
|
||||||
|
log(tag) { "searchHistory1: Didn't recognize: $message" }
|
||||||
|
}
|
||||||
|
|
||||||
|
return recognizedDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateHistory(device: PodType) {
|
||||||
|
val existing = knownDevices[device.identifier]
|
||||||
|
|
||||||
|
knownDevices[device.identifier] = when {
|
||||||
|
existing != null -> {
|
||||||
|
existing.copy(
|
||||||
|
seenCounter = existing.seenCounter + 1,
|
||||||
|
history = existing.history.plus(device)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
log(tag) { "searchHistory1: Creating new history for $device" }
|
||||||
|
KnownDevice(
|
||||||
|
id = device.identifier,
|
||||||
|
seenFirstAt = device.seenFirstAt,
|
||||||
|
seenCounter = 1,
|
||||||
|
history = listOf(device)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ModelInfo(
|
||||||
|
val full: UShort,
|
||||||
|
val dirty: UByte,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun ProximityPairing.Message.getModelInfo(): ModelInfo = ModelInfo(
|
||||||
|
full = (((data[1].toInt() and 255) shl 8) or (data[2].toInt() and 255)).toUShort(),
|
||||||
|
dirty = data[1]
|
||||||
|
)
|
||||||
|
|
||||||
|
abstract fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean
|
||||||
|
|
||||||
|
abstract fun create(
|
||||||
|
scanResult: BleScanResult,
|
||||||
|
proximityMessage: ProximityPairing.Message,
|
||||||
|
): ApplePods
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
|
abstract class BasicSingleApplePodsFactory(private val tag: String) : ApplePodsFactory<BasicSingleApplePods>(tag)
|
||||||
@@ -8,23 +8,30 @@ import eu.darken.capod.common.isBitSet
|
|||||||
import eu.darken.capod.common.lowerNibble
|
import eu.darken.capod.common.lowerNibble
|
||||||
import eu.darken.capod.common.upperNibble
|
import eu.darken.capod.common.upperNibble
|
||||||
import eu.darken.capod.pods.core.HasCase
|
import eu.darken.capod.pods.core.HasCase
|
||||||
|
import eu.darken.capod.pods.core.HasDualEarDetection
|
||||||
import eu.darken.capod.pods.core.HasDualPods
|
import eu.darken.capod.pods.core.HasDualPods
|
||||||
import eu.darken.capod.pods.core.HasDualPods.Pod
|
import eu.darken.capod.pods.core.HasDualPods.Pod
|
||||||
import eu.darken.capod.pods.core.HasEarDetection
|
|
||||||
|
|
||||||
interface DualApplePods : ApplePods, HasDualPods, HasEarDetection, HasCase {
|
interface DualApplePods : ApplePods, HasDualPods, HasDualEarDetection, HasCase {
|
||||||
|
|
||||||
val microPhonePod: Pod
|
val primaryPod: Pod
|
||||||
get() = when (rawStatus.isBitSet(5)) {
|
get() = when (rawStatus.isBitSet(5)) {
|
||||||
true -> Pod.LEFT
|
true -> Pod.LEFT
|
||||||
false -> Pod.RIGHT
|
false -> Pod.RIGHT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normally values for the left pod are in the lower nibbles, if the left pod is primary (microphone)
|
||||||
|
* If the right pod is the primary, the values are flipped.
|
||||||
|
*/
|
||||||
|
val areValuesFlipped: Boolean
|
||||||
|
get() = !rawStatus.isBitSet(5)
|
||||||
|
|
||||||
override val batteryLeftPodPercent: Float?
|
override val batteryLeftPodPercent: Float?
|
||||||
get() {
|
get() {
|
||||||
val value = when (microPhonePod) {
|
val value = when (areValuesFlipped) {
|
||||||
Pod.LEFT -> rawPodsBattery.lowerNibble.toInt()
|
true -> rawPodsBattery.upperNibble.toInt()
|
||||||
Pod.RIGHT -> rawPodsBattery.upperNibble.toInt()
|
false -> rawPodsBattery.lowerNibble.toInt()
|
||||||
}
|
}
|
||||||
return when (value) {
|
return when (value) {
|
||||||
15 -> null
|
15 -> null
|
||||||
@@ -39,9 +46,9 @@ interface DualApplePods : ApplePods, HasDualPods, HasEarDetection, HasCase {
|
|||||||
|
|
||||||
override val batteryRightPodPercent: Float?
|
override val batteryRightPodPercent: Float?
|
||||||
get() {
|
get() {
|
||||||
val value = when (microPhonePod) {
|
val value = when (areValuesFlipped) {
|
||||||
Pod.LEFT -> rawPodsBattery.upperNibble.toInt()
|
true -> rawPodsBattery.lowerNibble.toInt()
|
||||||
Pod.RIGHT -> rawPodsBattery.lowerNibble.toInt()
|
false -> rawPodsBattery.upperNibble.toInt()
|
||||||
}
|
}
|
||||||
return when (value) {
|
return when (value) {
|
||||||
15 -> null
|
15 -> null
|
||||||
@@ -54,35 +61,55 @@ interface DualApplePods : ApplePods, HasDualPods, HasEarDetection, HasCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val isLeftPodInEar: Boolean
|
val isThisPodInThecase: Boolean
|
||||||
get() = when (microPhonePod) {
|
get() = rawStatus.isBitSet(6)
|
||||||
Pod.LEFT -> rawStatus.isBitSet(1)
|
|
||||||
Pod.RIGHT -> rawStatus.isBitSet(3)
|
val isOnePodInCase: Boolean
|
||||||
|
get() = rawStatus.isBitSet(4)
|
||||||
|
|
||||||
|
val areBothPodsInCase: Boolean
|
||||||
|
get() = rawStatus.isBitSet(2)
|
||||||
|
|
||||||
|
override val isLeftPodInEar: Boolean
|
||||||
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
|
true -> rawStatus.isBitSet(3)
|
||||||
|
false -> rawStatus.isBitSet(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
val isRightPodInEar: Boolean
|
override val isRightPodInEar: Boolean
|
||||||
get() = when (microPhonePod) {
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
Pod.LEFT -> rawStatus.isBitSet(3)
|
true -> rawStatus.isBitSet(1)
|
||||||
Pod.RIGHT -> rawStatus.isBitSet(1)
|
false -> rawStatus.isBitSet(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val isBeingWorn: Boolean
|
/**
|
||||||
get() = isLeftPodInEar && isRightPodInEar
|
* The data flip bit is set if the left pod is primary.
|
||||||
|
* For the pod that is in the case, this is flipped again though.
|
||||||
|
*/
|
||||||
|
val isLeftPodMicrophone: Boolean
|
||||||
|
get() = rawStatus.isBitSet(5) xor isThisPodInThecase
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The data flip bit is UNset if the right pod is primary.
|
||||||
|
* For the pod that is in the case, this is flipped again though.
|
||||||
|
*/
|
||||||
|
val isRightPodMicrophone: Boolean
|
||||||
|
get() = !rawStatus.isBitSet(5) xor isThisPodInThecase
|
||||||
|
|
||||||
val isLeftPodCharging: Boolean
|
val isLeftPodCharging: Boolean
|
||||||
get() = when (microPhonePod) {
|
get() = when (areValuesFlipped) {
|
||||||
Pod.LEFT -> rawCaseBattery.upperNibble.isBitSet(0)
|
false -> rawFlags.isBitSet(0)
|
||||||
Pod.RIGHT -> rawCaseBattery.upperNibble.isBitSet(1)
|
true -> rawFlags.isBitSet(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
val isRightPodCharging: Boolean
|
val isRightPodCharging: Boolean
|
||||||
get() = when (microPhonePod) {
|
get() = when (areValuesFlipped) {
|
||||||
Pod.LEFT -> rawCaseBattery.upperNibble.isBitSet(1)
|
false -> rawFlags.isBitSet(1)
|
||||||
Pod.RIGHT -> rawCaseBattery.upperNibble.isBitSet(0)
|
true -> rawFlags.isBitSet(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = when (val value = rawCaseBattery.lowerNibble.toInt()) {
|
get() = when (val value = rawCaseBattery.toInt()) {
|
||||||
15 -> null
|
15 -> null
|
||||||
else -> if (value > 10) {
|
else -> if (value > 10) {
|
||||||
log { "Case: Above 100% battery: $value" }
|
log { "Case: Above 100% battery: $value" }
|
||||||
@@ -93,11 +120,12 @@ interface DualApplePods : ApplePods, HasDualPods, HasEarDetection, HasCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override val isCaseCharging: Boolean
|
override val isCaseCharging: Boolean
|
||||||
get() = rawCaseBattery.upperNibble.isBitSet(2)
|
get() = rawFlags.isBitSet(2)
|
||||||
|
|
||||||
val caseLidState: LidState
|
val caseLidState: LidState
|
||||||
get() {
|
get() {
|
||||||
return LidState.values().firstOrNull { it.rawRange.contains(rawCaseLidState.toInt()) } ?: LidState.UNKNOWN
|
val rawstate = rawCaseLidState
|
||||||
|
return LidState.values().firstOrNull { it.rawRange.contains(rawstate.toInt()) } ?: LidState.UNKNOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -118,27 +146,24 @@ interface DualApplePods : ApplePods, HasDualPods, HasEarDetection, HasCase {
|
|||||||
val deviceColor: DeviceColor
|
val deviceColor: DeviceColor
|
||||||
get() = DeviceColor.values().firstOrNull { it.raw == rawDeviceColor } ?: DeviceColor.UNKNOWN
|
get() = DeviceColor.values().firstOrNull { it.raw == rawDeviceColor } ?: DeviceColor.UNKNOWN
|
||||||
|
|
||||||
|
enum class DeviceColor(val raw: UByte?) {
|
||||||
|
|
||||||
fun getDeviceColorLabel(context: Context): String = context.getString(deviceColor.labelRes)
|
WHITE(0x00),
|
||||||
|
BLACK(0x01),
|
||||||
|
RED(0x02),
|
||||||
|
BLUE(0x03),
|
||||||
|
PINK(0x04),
|
||||||
|
GRAY(0x05),
|
||||||
|
SILVER(0x06),
|
||||||
|
GOLD(0x07),
|
||||||
|
ROSE_GOLD(0x08),
|
||||||
|
SPACE_GRAY(0x09),
|
||||||
|
DARK_BLUE(0x0a),
|
||||||
|
LIGHT_BLUE(0x0b),
|
||||||
|
YELLOW(0x0c),
|
||||||
|
UNKNOWN(null);
|
||||||
|
|
||||||
enum class DeviceColor(val raw: UByte?, @StringRes val labelRes: Int) {
|
constructor(raw: Int) : this(raw.toUByte())
|
||||||
|
|
||||||
WHITE(0x00, R.string.pods_device_color_white_label),
|
|
||||||
BLACK(0x01, R.string.pods_device_color_black_label),
|
|
||||||
RED(0x02, R.string.pods_device_color_red_label),
|
|
||||||
BLUE(0x03, R.string.pods_device_color_blue_label),
|
|
||||||
PINK(0x04, R.string.pods_device_color_pink_label),
|
|
||||||
GRAY(0x05, R.string.pods_device_color_gray_label),
|
|
||||||
SILVER(0x06, R.string.pods_device_color_silver_label),
|
|
||||||
GOLD(0x07, R.string.pods_device_color_gold_label),
|
|
||||||
ROSE_GOLD(0x08, R.string.pods_device_color_rose_gold_label),
|
|
||||||
SPACE_GRAY(0x09, R.string.pods_device_color_space_gray_label),
|
|
||||||
DARK_BLUE(0x0a, R.string.pods_device_color_dark_blue_label),
|
|
||||||
LIGHT_BLUE(0x0b, R.string.pods_device_color_light_blue_label),
|
|
||||||
YELLOW(0x0c, R.string.pods_device_color_yellow_label),
|
|
||||||
UNKNOWN(null, R.string.general_value_unknown_label);
|
|
||||||
|
|
||||||
constructor(raw: Int, @StringRes labelRes: Int) : this(raw.toUByte(), labelRes)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val connectionState: ConnectionState
|
val connectionState: ConnectionState
|
||||||
@@ -157,4 +182,5 @@ interface DualApplePods : ApplePods, HasDualPods, HasEarDetection, HasCase {
|
|||||||
|
|
||||||
constructor(raw: Int, @StringRes labelRes: Int) : this(raw.toUByte(), labelRes)
|
constructor(raw: Int, @StringRes labelRes: Int) : this(raw.toUByte(), labelRes)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
|
import eu.darken.capod.common.debug.logging.Logging.Priority.DEBUG
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
|
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro
|
||||||
|
|
||||||
|
abstract class DualApplePodsFactory(private val tag: String) : ApplePodsFactory<DualApplePods>(tag) {
|
||||||
|
|
||||||
|
fun DualApplePods.getCaseMatchMarkings() = SplitPodsMarkings(
|
||||||
|
leftPodBattery = batteryLeftPodPercent,
|
||||||
|
rightPodBattery = batteryRightPodPercent,
|
||||||
|
microPhoneLeft = isLeftPodMicrophone,
|
||||||
|
microPhoneRight = isRightPodMicrophone,
|
||||||
|
chargingLeft = isLeftPodCharging,
|
||||||
|
chargingRight = isRightPodCharging,
|
||||||
|
color = deviceColor,
|
||||||
|
model = model
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split pods, one in case, one not.
|
||||||
|
*/
|
||||||
|
data class SplitPodsMarkings(
|
||||||
|
val leftPodBattery: Float?,
|
||||||
|
val rightPodBattery: Float?,
|
||||||
|
val microPhoneLeft: Boolean,
|
||||||
|
val microPhoneRight: Boolean,
|
||||||
|
val chargingLeft: Boolean,
|
||||||
|
val chargingRight: Boolean,
|
||||||
|
val color: DualApplePods.DeviceColor,
|
||||||
|
val model: PodDevice.Model,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun Collection<KnownDevice>.findSplitPodsMatch(device: DualApplePods): Collection<KnownDevice> {
|
||||||
|
val target = device.getCaseMatchMarkings()
|
||||||
|
|
||||||
|
return filter { known ->
|
||||||
|
known.history
|
||||||
|
.filterIsInstance<DualApplePods>()
|
||||||
|
.any { it.getCaseMatchMarkings() == target }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun KnownDevice.getLatestCaseBattery(): Float? = history
|
||||||
|
.filterIsInstance<DualApplePods>()
|
||||||
|
.mapNotNull { it.batteryCasePercent }
|
||||||
|
.lastOrNull()
|
||||||
|
|
||||||
|
fun KnownDevice.getLatestCaseLidState(basic: DualApplePods): DualApplePods.LidState? {
|
||||||
|
val definitive = setOf(DualApplePods.LidState.OPEN, DualApplePods.LidState.CLOSED)
|
||||||
|
if (definitive.contains(basic.caseLidState)) return null
|
||||||
|
|
||||||
|
return history
|
||||||
|
.filterIsInstance<AirPodsPro>()
|
||||||
|
.lastOrNull { it.caseLidState != DualApplePods.LidState.NOT_IN_CASE }
|
||||||
|
?.caseLidState
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchHistory(current: DualApplePods): KnownDevice? {
|
||||||
|
val basicResult = super.searchHistory(current)
|
||||||
|
|
||||||
|
val caseIgnored = knownDevices.values.findSplitPodsMatch(current)
|
||||||
|
|
||||||
|
log(tag, DEBUG) { "searchHistory2: Case ignored matches(${caseIgnored.size}): $caseIgnored" }
|
||||||
|
|
||||||
|
return when (caseIgnored.size) {
|
||||||
|
0 -> basicResult
|
||||||
|
1 -> caseIgnored.single()
|
||||||
|
else -> {
|
||||||
|
log(tag) { "searchHistory2: More than one result when ignoring case markers." }
|
||||||
|
val oldest = caseIgnored.maxByOrNull { it.history.size } ?: return null
|
||||||
|
|
||||||
|
caseIgnored.minus(oldest).forEach {
|
||||||
|
log(tag) { "searchHistory2: Removing outlier: $it" }
|
||||||
|
knownDevices.remove(it.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
oldest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package eu.darken.capod.pods.core.apple
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
import eu.darken.capod.common.isBitSet
|
import eu.darken.capod.common.isBitSet
|
||||||
import eu.darken.capod.common.upperNibble
|
|
||||||
import eu.darken.capod.pods.core.HasEarDetection
|
import eu.darken.capod.pods.core.HasEarDetection
|
||||||
import eu.darken.capod.pods.core.HasSinglePod
|
import eu.darken.capod.pods.core.HasSinglePod
|
||||||
|
|
||||||
@@ -11,7 +10,7 @@ interface SingleApplePods : BasicSingleApplePods, HasEarDetection, HasSinglePod
|
|||||||
get() = rawStatus.isBitSet(1)
|
get() = rawStatus.isBitSet(1)
|
||||||
|
|
||||||
val isHeadsetBeingCharged: Boolean
|
val isHeadsetBeingCharged: Boolean
|
||||||
get() = rawCaseBattery.upperNibble.isBitSet(0)
|
get() = rawFlags.isBitSet(0)
|
||||||
|
|
||||||
override val isBeingWorn: Boolean
|
override val isBeingWorn: Boolean
|
||||||
get() = isHeadphonesBeingWorn
|
get() = isHeadphonesBeingWorn
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
|
abstract class SingleApplePodsFactory(private val tag: String) : ApplePodsFactory<SingleApplePods>(tag)
|
||||||
@@ -11,28 +11,43 @@ 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,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
) : ApplePods {
|
) : ApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.UNKNOWN
|
override val model: PodDevice.Model = PodDevice.Model.UNKNOWN
|
||||||
|
|
||||||
override fun getLabel(context: Context): String = context.getString(R.string.pods_unknown_label)
|
override fun getLabel(context: Context): String = context.getString(R.string.pods_unknown_label)
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : ApplePodsFactory<ApplePods>(TAG) {
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean = true
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean = true
|
||||||
|
|
||||||
override fun create(
|
override fun create(
|
||||||
scanResult: BleScanResult,
|
scanResult: BleScanResult,
|
||||||
proximityMessage: ProximityPairing.Message,
|
proximityMessage: ProximityPairing.Message,
|
||||||
): ApplePods {
|
): ApplePods {
|
||||||
val identifier = recognizeDevice(scanResult, proximityMessage)
|
var basic = UnknownAppleDevice(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return UnknownAppleDevice(
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
identifier = identifier,
|
updateHistory(basic)
|
||||||
scanResult = scanResult,
|
|
||||||
proximityMessage = proximityMessage,
|
if (result == null) return basic
|
||||||
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,22 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
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,
|
||||||
private val cachedBatteryPercentage: Float?
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
) : DualApplePods {
|
) : DualApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN1
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN1
|
||||||
@@ -22,26 +28,35 @@ data class AirPodsGen1 constructor(
|
|||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val caseLidState: DualApplePods.LidState
|
||||||
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
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 identifier = recognizeDevice(scanResult, proximityMessage)
|
var basic = AirPodsGen1(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
val device = AirPodsGen1(
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
identifier = identifier,
|
updateHistory(basic)
|
||||||
scanResult = scanResult,
|
|
||||||
proximityMessage = proximityMessage,
|
if (result == null) return basic
|
||||||
cachedBatteryPercentage = cachedValues[identifier]?.caseBatteryPercentage
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
|
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||||
)
|
)
|
||||||
|
|
||||||
cachedValues[identifier] = ValueCache(
|
|
||||||
caseBatteryPercentage = device.batteryCasePercent
|
|
||||||
)
|
|
||||||
|
|
||||||
return device
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,16 +5,22 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
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,
|
||||||
private val cachedBatteryPercentage: Float?,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
) : DualApplePods {
|
) : DualApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN2
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN2
|
||||||
@@ -22,26 +28,35 @@ data class AirPodsGen2 constructor(
|
|||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val caseLidState: DualApplePods.LidState
|
||||||
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
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 identifier = recognizeDevice(scanResult, proximityMessage)
|
var basic = AirPodsGen2(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
val device = AirPodsGen2(
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
identifier = identifier,
|
updateHistory(basic)
|
||||||
scanResult = scanResult,
|
|
||||||
proximityMessage = proximityMessage,
|
if (result == null) return basic
|
||||||
cachedBatteryPercentage = cachedValues[identifier]?.caseBatteryPercentage
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
|
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||||
)
|
)
|
||||||
|
|
||||||
cachedValues[identifier] = ValueCache(
|
|
||||||
caseBatteryPercentage = device.batteryCasePercent
|
|
||||||
)
|
|
||||||
|
|
||||||
return device
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,42 +5,57 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
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,
|
||||||
private val cachedBatteryPercentage: Float?,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
) : DualApplePods {
|
) : DualApplePods {
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN3
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN3
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val caseLidState: DualApplePods.LidState
|
||||||
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
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 identifier = recognizeDevice(scanResult, proximityMessage)
|
var basic = AirPodsGen3(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
val device = AirPodsGen3(
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
identifier = identifier,
|
updateHistory(basic)
|
||||||
scanResult = scanResult,
|
|
||||||
proximityMessage = proximityMessage,
|
if (result == null) return basic
|
||||||
cachedBatteryPercentage = cachedValues[identifier]?.caseBatteryPercentage
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
|
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||||
)
|
)
|
||||||
|
|
||||||
cachedValues[identifier] = ValueCache(
|
|
||||||
caseBatteryPercentage = device.batteryCasePercent
|
|
||||||
)
|
|
||||||
|
|
||||||
return device
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,31 +5,47 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
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,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
) : SingleApplePods {
|
) : SingleApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
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 identifier = recognizeDevice(scanResult, proximityMessage)
|
var basic = AirPodsMax(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return AirPodsMax(
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
identifier = identifier,
|
updateHistory(basic)
|
||||||
scanResult = scanResult,
|
|
||||||
proximityMessage = proximityMessage,
|
if (result == null) return basic
|
||||||
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,16 +5,23 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
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,
|
||||||
private val cachedBatteryPercentage: Float?,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
private val cachedCaseState: LidState? = null
|
||||||
) : DualApplePods {
|
) : DualApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO
|
||||||
@@ -22,32 +29,40 @@ data class AirPodsPro(
|
|||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val caseLidState: LidState
|
||||||
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
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 identifier = recognizeDevice(scanResult, proximityMessage)
|
var basic = AirPodsPro(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
val device = AirPodsPro(
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
identifier = identifier,
|
updateHistory(basic)
|
||||||
scanResult = scanResult,
|
|
||||||
proximityMessage = proximityMessage,
|
if (result == null) return basic
|
||||||
cachedBatteryPercentage = cachedValues[identifier]?.caseBatteryPercentage
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
|
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||||
)
|
)
|
||||||
|
|
||||||
cachedValues[identifier] = ValueCache(
|
|
||||||
caseBatteryPercentage = device.batteryCasePercent
|
|
||||||
)
|
|
||||||
|
|
||||||
return device
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
companion object {
|
||||||
|
private val DEVICE_CODE = 0x0e20.toUShort()
|
||||||
companion object {
|
private val TAG = logTag("PodDevice", "Apple", "AirPods", "Pro", "Factory")
|
||||||
private val DEVICE_CODE = 0x0e20.toUShort()
|
}
|
||||||
private val TAG = logTag("PodDevice", "Apple", "AirPods", "Pro")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,31 +5,47 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
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,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
) : BasicSingleApplePods {
|
) : BasicSingleApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_FLEX
|
override val model: PodDevice.Model = PodDevice.Model.BEATS_FLEX
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
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 identifier = recognizeDevice(scanResult, proximityMessage)
|
var basic = BeatsFlex(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return BeatsFlex(
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
identifier = identifier,
|
updateHistory(basic)
|
||||||
scanResult = scanResult,
|
|
||||||
proximityMessage = proximityMessage,
|
if (result == null) return basic
|
||||||
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,31 +5,46 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
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,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
) : BasicSingleApplePods {
|
) : BasicSingleApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_SOLO_3
|
override val model: PodDevice.Model = PodDevice.Model.BEATS_SOLO_3
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
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 identifier = recognizeDevice(scanResult, proximityMessage)
|
var basic = BeatsSolo3(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return BeatsSolo3(
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
identifier = identifier,
|
updateHistory(basic)
|
||||||
scanResult = scanResult,
|
|
||||||
proximityMessage = proximityMessage,
|
if (result == null) return basic
|
||||||
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,31 +5,44 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
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,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
) : BasicSingleApplePods {
|
) : BasicSingleApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_STUDIO_3
|
override val model: PodDevice.Model = PodDevice.Model.BEATS_STUDIO_3
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
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 identifier = recognizeDevice(scanResult, proximityMessage)
|
var basic = BeatsStudio3(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return BeatsStudio3(
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
identifier = identifier,
|
updateHistory(basic)
|
||||||
scanResult = scanResult,
|
|
||||||
proximityMessage = proximityMessage,
|
if (result == null) return basic
|
||||||
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,31 +5,47 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
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,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
) : BasicSingleApplePods {
|
) : BasicSingleApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_X
|
override val model: PodDevice.Model = PodDevice.Model.BEATS_X
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
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 identifier = recognizeDevice(scanResult, proximityMessage)
|
var basic = BeatsX(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return BeatsX(
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
identifier = identifier,
|
updateHistory(basic)
|
||||||
scanResult = scanResult,
|
|
||||||
proximityMessage = proximityMessage,
|
if (result == null) return basic
|
||||||
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,30 +5,46 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
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,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
) : BasicSingleApplePods {
|
) : BasicSingleApplePods {
|
||||||
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_3
|
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_3
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
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 identifier = recognizeDevice(scanResult, proximityMessage)
|
var basic = PowerBeats3(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return PowerBeats3(
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
identifier = identifier,
|
updateHistory(basic)
|
||||||
scanResult = scanResult,
|
|
||||||
proximityMessage = proximityMessage,
|
if (result == null) return basic
|
||||||
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,16 +5,22 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
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,
|
||||||
private val cachedBatteryPercentage: Float?,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
) : DualApplePods {
|
) : DualApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_PRO
|
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_PRO
|
||||||
@@ -22,26 +28,35 @@ data class PowerBeatsPro(
|
|||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val caseLidState: DualApplePods.LidState
|
||||||
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
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 identifier = recognizeDevice(scanResult, proximityMessage)
|
var basic = PowerBeatsPro(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
val device = PowerBeatsPro(
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
identifier = identifier,
|
updateHistory(basic)
|
||||||
scanResult = scanResult,
|
|
||||||
proximityMessage = proximityMessage,
|
if (result == null) return basic
|
||||||
cachedBatteryPercentage = cachedValues[identifier]?.caseBatteryPercentage
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
|
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||||
)
|
)
|
||||||
|
|
||||||
cachedValues[identifier] = ValueCache(
|
|
||||||
caseBatteryPercentage = device.batteryCasePercent
|
|
||||||
)
|
|
||||||
|
|
||||||
return device
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package eu.darken.capod.pods.core.apple.protocol
|
|||||||
import android.bluetooth.le.ScanFilter
|
import android.bluetooth.le.ScanFilter
|
||||||
import dagger.Reusable
|
import dagger.Reusable
|
||||||
import eu.darken.capod.common.debug.logging.log
|
import eu.darken.capod.common.debug.logging.log
|
||||||
import eu.darken.capod.common.lowerNibble
|
|
||||||
import eu.darken.capod.common.upperNibble
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
@@ -15,22 +13,11 @@ object ProximityPairing {
|
|||||||
val length: Int,
|
val length: Int,
|
||||||
val data: UByteArray
|
val data: UByteArray
|
||||||
) {
|
) {
|
||||||
data class Markers(
|
|
||||||
val vendor: UByte,
|
|
||||||
val length: UByte,
|
|
||||||
val device: UShort,
|
|
||||||
val batteryData: Set<UByte>,
|
|
||||||
val deviceColor: UByte,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun getRecogMarkers(): Markers = Markers(
|
override fun toString(): String {
|
||||||
vendor = CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING,
|
val dataHex = data.joinToString(separator = " ") { String.format("%02X", it.toByte()) }
|
||||||
length = PROXIMITY_PAIRING_MESSAGE_LENGTH.toUByte(),
|
return "ProximityPairing.Message(type=$type, length=$length, data=$dataHex)"
|
||||||
device = (((data[1].toInt() and 255) shl 8) or (data[2].toInt() and 255)).toUShort(),
|
}
|
||||||
// Make comparison order independent
|
|
||||||
batteryData = setOf(data[4].upperNibble, data[4].lowerNibble),
|
|
||||||
deviceColor = data[7]
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Reusable
|
@Reusable
|
||||||
@@ -74,6 +61,6 @@ object ProximityPairing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private const val CONTINUITY_PROTOCOL_MESSAGE_LENGTH = 27
|
private const val CONTINUITY_PROTOCOL_MESSAGE_LENGTH = 27
|
||||||
private val CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING = 0x07.toUByte()
|
internal val CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING = 0x07.toUByte()
|
||||||
private const val PROXIMITY_PAIRING_MESSAGE_LENGTH = 25
|
internal const val PROXIMITY_PAIRING_MESSAGE_LENGTH = 25
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||||
import eu.darken.capod.main.core.GeneralSettings
|
import eu.darken.capod.main.core.GeneralSettings
|
||||||
import eu.darken.capod.monitor.core.PodMonitor
|
import eu.darken.capod.monitor.core.PodMonitor
|
||||||
|
import eu.darken.capod.pods.core.HasDualEarDetection
|
||||||
import eu.darken.capod.pods.core.HasEarDetection
|
import eu.darken.capod.pods.core.HasEarDetection
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.reaction.settings.ReactionSettings
|
import eu.darken.capod.reaction.settings.ReactionSettings
|
||||||
@@ -71,7 +72,13 @@ class AutoConnect @Inject constructor(
|
|||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
AutoConnectCondition.IN_EAR -> when (mainDevice) {
|
AutoConnectCondition.IN_EAR -> when (mainDevice) {
|
||||||
is HasEarDetection -> mainDevice.isBeingWorn
|
is HasEarDetection -> {
|
||||||
|
if (mainDevice is HasDualEarDetection && reactionSettings.onePodMode.value) {
|
||||||
|
mainDevice.isEitherPodInEar
|
||||||
|
} else {
|
||||||
|
mainDevice.isBeingWorn
|
||||||
|
}
|
||||||
|
}
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||||
import eu.darken.capod.common.flow.withPrevious
|
import eu.darken.capod.common.flow.withPrevious
|
||||||
import eu.darken.capod.monitor.core.PodMonitor
|
import eu.darken.capod.monitor.core.PodMonitor
|
||||||
|
import eu.darken.capod.pods.core.HasDualEarDetection
|
||||||
import eu.darken.capod.pods.core.HasEarDetection
|
import eu.darken.capod.pods.core.HasEarDetection
|
||||||
import eu.darken.capod.reaction.settings.ReactionSettings
|
import eu.darken.capod.reaction.settings.ReactionSettings
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
@@ -25,7 +26,8 @@ class PlayPause @Inject constructor(
|
|||||||
fun monitor() = combine(
|
fun monitor() = combine(
|
||||||
reactionSettings.autoPlay.flow,
|
reactionSettings.autoPlay.flow,
|
||||||
reactionSettings.autoPause.flow,
|
reactionSettings.autoPause.flow,
|
||||||
) { play, pause -> play || pause }
|
reactionSettings.onePodMode.flow,
|
||||||
|
) { play, pause, _ -> play || pause }
|
||||||
.flatMapLatest { if (it) bluetoothManager.connectedDevices() else emptyFlow() }
|
.flatMapLatest { if (it) bluetoothManager.connectedDevices() else emptyFlow() }
|
||||||
.flatMapLatest {
|
.flatMapLatest {
|
||||||
if (it.isEmpty()) {
|
if (it.isEmpty()) {
|
||||||
@@ -42,7 +44,28 @@ class PlayPause @Inject constructor(
|
|||||||
if (previous is HasEarDetection && current is HasEarDetection) {
|
if (previous is HasEarDetection && current is HasEarDetection) {
|
||||||
log(TAG, VERBOSE) { "previous=${previous.isBeingWorn}, current=${current.isBeingWorn}" }
|
log(TAG, VERBOSE) { "previous=${previous.isBeingWorn}, current=${current.isBeingWorn}" }
|
||||||
log(TAG, VERBOSE) { "previous-id=${previous.identifier}, current-id=${current.identifier}" }
|
log(TAG, VERBOSE) { "previous-id=${previous.identifier}, current-id=${current.identifier}" }
|
||||||
if (previous.identifier == current.identifier && previous.isBeingWorn != current.isBeingWorn) {
|
if (previous.identifier != current.identifier) {
|
||||||
|
return@onEach
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previous is HasDualEarDetection && current is HasDualEarDetection && reactionSettings.onePodMode.value) {
|
||||||
|
log(TAG) { "Ear status changed for dual pod device in single pod mode." }
|
||||||
|
val previousWorn = previous.isEitherPodInEar
|
||||||
|
val currentWorn = current.isEitherPodInEar
|
||||||
|
if (!previousWorn && currentWorn && !mediaControl.isPlaying) {
|
||||||
|
if (reactionSettings.autoPlay.value) {
|
||||||
|
mediaControl.sendPlay()
|
||||||
|
} else {
|
||||||
|
log(TAG) { "autoPlay is disabled" }
|
||||||
|
}
|
||||||
|
} else if (!currentWorn && previousWorn && mediaControl.isPlaying) {
|
||||||
|
if (reactionSettings.autoPause.value) {
|
||||||
|
mediaControl.sendPause()
|
||||||
|
} else {
|
||||||
|
log(TAG) { "autoPause is disabled" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (previous.isBeingWorn != current.isBeingWorn) {
|
||||||
log(TAG) { "Ear status changed for monitored device." }
|
log(TAG) { "Ear status changed for monitored device." }
|
||||||
if (current.isBeingWorn && !mediaControl.isPlaying) {
|
if (current.isBeingWorn && !mediaControl.isPlaying) {
|
||||||
if (reactionSettings.autoPlay.value) {
|
if (reactionSettings.autoPlay.value) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package eu.darken.capod.reaction.popup
|
package eu.darken.capod.reaction.popup
|
||||||
|
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
|
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
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
|
||||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||||
@@ -9,9 +9,12 @@ import eu.darken.capod.common.flow.withPrevious
|
|||||||
import eu.darken.capod.monitor.core.PodMonitor
|
import eu.darken.capod.monitor.core.PodMonitor
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.reaction.popup.ui.PopUpNotification
|
import eu.darken.capod.reaction.popup.ui.PopUpWindow
|
||||||
import eu.darken.capod.reaction.settings.ReactionSettings
|
import eu.darken.capod.reaction.settings.ReactionSettings
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -19,9 +22,12 @@ import javax.inject.Singleton
|
|||||||
class PopUpReaction @Inject constructor(
|
class PopUpReaction @Inject constructor(
|
||||||
private val podMonitor: PodMonitor,
|
private val podMonitor: PodMonitor,
|
||||||
private val reactionSettings: ReactionSettings,
|
private val reactionSettings: ReactionSettings,
|
||||||
private val popUpNotification: PopUpNotification,
|
private val popupWindow: PopUpWindow,
|
||||||
|
private val dispatcherProvider: DispatcherProvider,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val coolDowns = mutableMapOf<PodDevice.Id, Instant>()
|
||||||
|
|
||||||
fun monitor(): Flow<Unit> = reactionSettings.showPopUpOnCaseOpen.flow
|
fun monitor(): Flow<Unit> = reactionSettings.showPopUpOnCaseOpen.flow
|
||||||
.flatMapLatest { isEnabled ->
|
.flatMapLatest { isEnabled ->
|
||||||
if (isEnabled) {
|
if (isEnabled) {
|
||||||
@@ -49,17 +55,48 @@ class PopUpReaction @Inject constructor(
|
|||||||
if (isSameDeviceWithCaseNowOpen || isNewDeviceWithJustOpenedCase) {
|
if (isSameDeviceWithCaseNowOpen || isNewDeviceWithJustOpenedCase) {
|
||||||
log(TAG) { "Case lid status changed for monitored device." }
|
log(TAG) { "Case lid status changed for monitored device." }
|
||||||
|
|
||||||
if (current.caseLidState == DualApplePods.LidState.OPEN && current.model != PodDevice.Model.UNKNOWN) {
|
tryPopWindow(current)
|
||||||
log(TAG, INFO) { "Show popup" }
|
|
||||||
popUpNotification.showNotification(current)
|
|
||||||
} else if (current.caseLidState != DualApplePods.LidState.OPEN) {
|
|
||||||
log(TAG, INFO) { "Hide popup" }
|
|
||||||
popUpNotification.hideNotification()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun tryPopWindow(current: DualApplePods) {
|
||||||
|
if (current.caseLidState == DualApplePods.LidState.OPEN) {
|
||||||
|
log(TAG, INFO) { "Show popup" }
|
||||||
|
|
||||||
|
val now = Instant.now()
|
||||||
|
val lastShown = coolDowns[current.identifier] ?: Instant.MIN
|
||||||
|
val sinceLastPop = Duration.between(lastShown, now)
|
||||||
|
log(TAG) { "Time since last popup: $sinceLastPop" }
|
||||||
|
if (sinceLastPop < Duration.ofSeconds(10)) {
|
||||||
|
log(TAG, INFO) { "Popup is still on cooldown: $sinceLastPop" }
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
coolDowns[current.identifier] = Instant.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(dispatcherProvider.Main) {
|
||||||
|
popupWindow.show(current)
|
||||||
|
}
|
||||||
|
} else if (current.caseLidState != DualApplePods.LidState.OPEN) {
|
||||||
|
when (current.caseLidState) {
|
||||||
|
DualApplePods.LidState.CLOSED -> {
|
||||||
|
log(TAG, INFO) { "Lid was actively closed, resetting cooldown." }
|
||||||
|
coolDowns.remove(current.identifier)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
log(TAG, WARN) { "Lid was was not actively closed, refreshing cooldown." }
|
||||||
|
coolDowns[current.identifier] = Instant.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log(TAG, INFO) { "Hide popup" }
|
||||||
|
withContext(dispatcherProvider.Main) {
|
||||||
|
popupWindow.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = logTag("Reaction", "PopUp")
|
private val TAG = logTag("Reaction", "PopUp")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
package eu.darken.capod.reaction.popup.ui
|
|
||||||
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import eu.darken.capod.R
|
|
||||||
import eu.darken.capod.common.BuildConfigWrap
|
|
||||||
import eu.darken.capod.common.debug.logging.log
|
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
|
||||||
import eu.darken.capod.common.notifications.PendingIntentCompat
|
|
||||||
import eu.darken.capod.main.ui.MainActivity
|
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
|
|
||||||
class PopUpNotification @Inject constructor(
|
|
||||||
@ApplicationContext private val context: Context,
|
|
||||||
private val notificationManager: NotificationManager,
|
|
||||||
private val notificationViewFactory: PopUpNotificationViewFactory
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val builder: NotificationCompat.Builder
|
|
||||||
|
|
||||||
init {
|
|
||||||
NotificationChannel(
|
|
||||||
NOTIFICATION_CHANNEL_ID,
|
|
||||||
context.getString(R.string.notification_channel_reaction_popup_label),
|
|
||||||
NotificationManager.IMPORTANCE_HIGH
|
|
||||||
).run {
|
|
||||||
vibrationPattern = LongArray(1) { 0 }
|
|
||||||
enableVibration(true)
|
|
||||||
enableLights(false)
|
|
||||||
setSound(null, null)
|
|
||||||
notificationManager.createNotificationChannel(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
val openIntent = Intent(context, MainActivity::class.java)
|
|
||||||
val openPi = PendingIntent.getActivity(
|
|
||||||
context,
|
|
||||||
0,
|
|
||||||
openIntent,
|
|
||||||
PendingIntentCompat.FLAG_IMMUTABLE
|
|
||||||
)
|
|
||||||
|
|
||||||
builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID).apply {
|
|
||||||
setChannelId(NOTIFICATION_CHANNEL_ID)
|
|
||||||
setContentIntent(openPi)
|
|
||||||
priority = NotificationCompat.PRIORITY_LOW
|
|
||||||
setSmallIcon(R.drawable.ic_device_generic_earbuds)
|
|
||||||
setContentTitle(context.getString(R.string.app_name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBuilder(device: PodDevice): NotificationCompat.Builder = builder.apply {
|
|
||||||
setStyle(NotificationCompat.DecoratedCustomViewStyle())
|
|
||||||
setCustomContentView(notificationViewFactory.createContentView(device))
|
|
||||||
setSmallIcon(device.iconRes)
|
|
||||||
setSubText(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showNotification(device: PodDevice) {
|
|
||||||
val notification = getBuilder(device).build()
|
|
||||||
log(TAG) { "showNotification(): $device" }
|
|
||||||
notificationManager.notify(NOTIFICATION_ID, notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hideNotification() {
|
|
||||||
log(TAG) { "hideNotification()" }
|
|
||||||
notificationManager.cancel(NOTIFICATION_ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val TAG = logTag("Reaction", "PopUp", "Notifications")
|
|
||||||
private val NOTIFICATION_CHANNEL_ID =
|
|
||||||
"${BuildConfigWrap.APPLICATION_ID}.notification.channel.reaction.popup"
|
|
||||||
internal const val NOTIFICATION_ID = 9000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
package eu.darken.capod.reaction.popup.ui
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.widget.RemoteViews
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import eu.darken.capod.R
|
|
||||||
import eu.darken.capod.pods.core.*
|
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
|
||||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
|
|
||||||
class PopUpNotificationViewFactory @Inject constructor(
|
|
||||||
@ApplicationContext private val context: Context
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun createContentView(device: PodDevice): RemoteViews = when (device) {
|
|
||||||
is DualApplePods -> createDualApplePods(device)
|
|
||||||
is SingleApplePods -> createSingleApplePods(device)
|
|
||||||
is BasicSingleApplePods -> createSingleBasicApplePods(device)
|
|
||||||
else -> throw IllegalArgumentException("Unexpected device: $device")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createDualApplePods(device: DualApplePods): RemoteViews = RemoteViews(
|
|
||||||
context.packageName,
|
|
||||||
R.layout.popup_notification_dual_pods
|
|
||||||
).apply {
|
|
||||||
setTextViewText(R.id.pod_label, device.getLabel(context))
|
|
||||||
|
|
||||||
device.getBatteryLevelLeftPod(context)
|
|
||||||
.let { if (device.isLeftPodCharging) "$it⚡" else it }
|
|
||||||
.run { setTextViewText(R.id.pod_left, this) }
|
|
||||||
|
|
||||||
device.getBatteryLevelCase(context)
|
|
||||||
.let { if (device.isCaseCharging) "$it⚡" else it }
|
|
||||||
.run { setTextViewText(R.id.pod_case, this) }
|
|
||||||
|
|
||||||
device.getBatteryLevelRightPod(context)
|
|
||||||
.let { if (device.isRightPodCharging) "$it⚡" else it }
|
|
||||||
.run { setTextViewText(R.id.pod_right, this) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createSingleApplePods(device: SingleApplePods): RemoteViews = RemoteViews(
|
|
||||||
context.packageName,
|
|
||||||
R.layout.popup_notification_single_pods
|
|
||||||
).apply {
|
|
||||||
setTextViewText(R.id.headphones_label, device.getLabel(context))
|
|
||||||
|
|
||||||
device.getBatteryLevelHeadset(context)
|
|
||||||
.let { if (!device.isHeadsetBeingCharged) "$it⚡" else it }
|
|
||||||
.run { setTextViewText(R.id.headphones, this) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createSingleBasicApplePods(device: BasicSingleApplePods): RemoteViews = RemoteViews(
|
|
||||||
context.packageName,
|
|
||||||
R.layout.popup_notification_single_pods_basic
|
|
||||||
).apply {
|
|
||||||
setTextViewText(R.id.headphones_label, device.getLabel(context))
|
|
||||||
|
|
||||||
device.getBatteryLevelHeadset(context)
|
|
||||||
.run { setTextViewText(R.id.headphones, this) }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package eu.darken.capod.reaction.popup.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.view.ContextThemeWrapper
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import eu.darken.capod.R
|
||||||
|
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||||
|
import eu.darken.capod.databinding.PopupNotificationDualPodsBinding
|
||||||
|
import eu.darken.capod.databinding.PopupNotificationSinglePodsBasicBinding
|
||||||
|
import eu.darken.capod.databinding.PopupNotificationSinglePodsBinding
|
||||||
|
import eu.darken.capod.pods.core.*
|
||||||
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
|
class PopUpPodViewFactory @Inject constructor(
|
||||||
|
@ApplicationContext private val appContext: Context,
|
||||||
|
private val debugSettings: DebugSettings,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val context = ContextThemeWrapper(appContext, R.style.AppTheme)
|
||||||
|
private val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||||
|
|
||||||
|
fun createContentView(parent: ViewGroup, device: PodDevice): View = when (device) {
|
||||||
|
is DualApplePods -> createDualApplePods(parent, device)
|
||||||
|
is SingleApplePods -> createSingleApplePods(parent, device) // Unused, has no case to trigger reaction?
|
||||||
|
is BasicSingleApplePods -> createSingleBasicApplePods(
|
||||||
|
parent,
|
||||||
|
device
|
||||||
|
) // Unused, has no case to trigger reaction?
|
||||||
|
else -> throw IllegalArgumentException("Unexpected device: $device")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createDualApplePods(parent: ViewGroup, device: DualApplePods): View =
|
||||||
|
PopupNotificationDualPodsBinding.inflate(layoutInflater, parent, false).apply {
|
||||||
|
device.apply {
|
||||||
|
podIcon.setImageResource(iconRes)
|
||||||
|
podLabel.text = getLabel(context)
|
||||||
|
signal.text = getSignalQuality(context)
|
||||||
|
signal.isInvisible = debugSettings.isDebugModeEnabled.value
|
||||||
|
|
||||||
|
// Left
|
||||||
|
podLeftBatteryIcon.setImageResource(getBatteryDrawable(batteryLeftPodPercent))
|
||||||
|
podLeftBatteryLabel.text = getBatteryLevelLeftPod(context)
|
||||||
|
|
||||||
|
// Case
|
||||||
|
podCaseBatteryIcon.setImageResource(getBatteryDrawable(batteryCasePercent))
|
||||||
|
podCaseBatteryLabel.text = getBatteryLevelCase(context)
|
||||||
|
|
||||||
|
// Right
|
||||||
|
podRightBatteryIcon.setImageResource(getBatteryDrawable(batteryRightPodPercent))
|
||||||
|
podRightBatteryLabel.text = getBatteryLevelRightPod(context)
|
||||||
|
}
|
||||||
|
}.root
|
||||||
|
|
||||||
|
private fun createSingleApplePods(parent: ViewGroup, device: SingleApplePods): View =
|
||||||
|
PopupNotificationSinglePodsBinding.inflate(layoutInflater, parent, false).apply {
|
||||||
|
device.apply {
|
||||||
|
headphonesIcon.setImageResource(iconRes)
|
||||||
|
headphonesLabel.text = getLabel(context)
|
||||||
|
signal.text = getSignalQuality(context)
|
||||||
|
signal.isInvisible = debugSettings.isDebugModeEnabled.value
|
||||||
|
|
||||||
|
headphonesBatteryIcon.setImageResource(getBatteryDrawable(batteryHeadsetPercent))
|
||||||
|
headphonesBatteryLabel.text = getBatteryLevelHeadset(context)
|
||||||
|
}
|
||||||
|
}.root
|
||||||
|
|
||||||
|
private fun createSingleBasicApplePods(parent: ViewGroup, device: BasicSingleApplePods): View =
|
||||||
|
PopupNotificationSinglePodsBasicBinding.inflate(layoutInflater, parent, false).apply {
|
||||||
|
device.apply {
|
||||||
|
headphonesIcon.setImageResource(iconRes)
|
||||||
|
headphonesLabel.text = getLabel(context)
|
||||||
|
signal.text = getSignalQuality(context)
|
||||||
|
signal.isInvisible = debugSettings.isDebugModeEnabled.value
|
||||||
|
|
||||||
|
headphonesBatteryIcon.setImageResource(getBatteryDrawable(batteryHeadsetPercent))
|
||||||
|
headphonesBatteryLabel.text = getBatteryLevelHeadset(context)
|
||||||
|
}
|
||||||
|
}.root
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package eu.darken.capod.reaction.popup.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Context.WINDOW_SERVICE
|
||||||
|
import android.graphics.PixelFormat
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.appcompat.view.ContextThemeWrapper
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import eu.darken.capod.R
|
||||||
|
import eu.darken.capod.common.debug.logging.Logging.Priority.ERROR
|
||||||
|
import eu.darken.capod.common.debug.logging.asLog
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class PopUpWindow @Inject constructor(
|
||||||
|
@ApplicationContext private val appContext: Context,
|
||||||
|
private val podViewFactory: PopUpPodViewFactory
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val context = ContextThemeWrapper(appContext, R.style.AppTheme)
|
||||||
|
private val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||||
|
private val windowManager: WindowManager = context.getSystemService(WINDOW_SERVICE) as WindowManager
|
||||||
|
private val layoutParams = WindowManager.LayoutParams(
|
||||||
|
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||||
|
WindowManager.LayoutParams.WRAP_CONTENT, // Display it on top of other application windows
|
||||||
|
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, // Don't let it grab the input focus
|
||||||
|
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, // Make the underlying application window visible
|
||||||
|
|
||||||
|
PixelFormat.TRANSLUCENT
|
||||||
|
).apply {
|
||||||
|
gravity = Gravity.BOTTOM
|
||||||
|
}
|
||||||
|
private val popUpView: View = layoutInflater.inflate(R.layout.popup_window_container_layout, null).apply {
|
||||||
|
findViewById<View>(R.id.close_action).setOnClickListener { close() }
|
||||||
|
}
|
||||||
|
private val deviceContainer = popUpView.findViewById<FrameLayout>(R.id.popup_content)
|
||||||
|
fun show(device: PodDevice) = try {
|
||||||
|
log(TAG) { "open()" }
|
||||||
|
if (popUpView.windowToken != null || popUpView.parent != null) {
|
||||||
|
log(TAG) { "View already added." }
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
val podView = podViewFactory.createContentView(deviceContainer, device)
|
||||||
|
deviceContainer.removeAllViews()
|
||||||
|
deviceContainer.addView(podView)
|
||||||
|
windowManager.addView(popUpView, layoutParams)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log(TAG, ERROR) { "open() failed: ${e.asLog()}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun close() = try {
|
||||||
|
log(TAG) { "close()" }
|
||||||
|
if (popUpView.parent != null) {
|
||||||
|
windowManager.removeView(popUpView)
|
||||||
|
deviceContainer.removeAllViews()
|
||||||
|
} else {
|
||||||
|
log(TAG) { "View was not added" }
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log(TAG, ERROR) { "close() failed: ${e.asLog()}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = logTag("Reaction", "PopUp", "Window")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,11 +47,17 @@ class ReactionSettings @Inject constructor(
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val onePodMode = preferences.createFlowPreference(
|
||||||
|
"reaction.onepod.enabled",
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
override val preferenceDataStore: PreferenceDataStore = PreferenceStoreMapper(
|
override val preferenceDataStore: PreferenceDataStore = PreferenceStoreMapper(
|
||||||
autoPause,
|
autoPause,
|
||||||
autoPlay,
|
autoPlay,
|
||||||
autoConnect,
|
autoConnect,
|
||||||
autoConnectCondition,
|
autoConnectCondition,
|
||||||
showPopUpOnCaseOpen,
|
showPopUpOnCaseOpen,
|
||||||
|
onePodMode,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M17,5v16c0,0.55 -0.45,1 -1,1H8c-0.55,0 -1,-0.45 -1,-1V5c0,-0.55 0.45,-1 1,-1h2V2h4v2h2C16.55,4 17,4.45 17,5zM15,6H9v14h6V6z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M17,5v16c0,0.55 -0.45,1 -1,1H8c-0.55,0 -1,-0.45 -1,-1V5c0,-0.55 0.45,-1 1,-1h2V2h4v2h2C16.55,4 17,4.45 17,5zM15,6H9v12h6V6z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M17,5v16c0,0.55 -0.45,1 -1,1H8c-0.55,0 -1,-0.45 -1,-1V5c0,-0.55 0.45,-1 1,-1h2V2h4v2h2C16.55,4 17,4.45 17,5zM15,6H9v10h6V6z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M17,5v16c0,0.55 -0.45,1 -1,1H8c-0.55,0 -1,-0.45 -1,-1V5c0,-0.55 0.45,-1 1,-1h2V2h4v2h2C16.55,4 17,4.45 17,5zM15,6H9v8h6V6z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M17,5v16c0,0.55 -0.45,1 -1,1H8c-0.55,0 -1,-0.45 -1,-1V5c0,-0.55 0.45,-1 1,-1h2V2h4v2h2C16.55,4 17,4.45 17,5zM15,6H9v6h6V6z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M17,5v16c0,0.55 -0.45,1 -1,1H8c-0.55,0 -1,-0.45 -1,-1V5c0,-0.55 0.45,-1 1,-1h2V2h4v2h2C16.55,4 17,4.45 17,5zM15,6H9v4h6V6z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M17,5v16c0,0.55 -0.45,1 -1,1H8c-0.55,0 -1,-0.45 -1,-1V5c0,-0.55 0.45,-1 1,-1h2V2h4v2h2C16.55,4 17,4.45 17,5zM15,6H9v2h6V6z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M15.67,4H14V2h-4v2H8.33C7.6,4 7,4.6 7,5.33v15.33C7,21.4 7.6,22 8.33,22h7.33c0.74,0 1.34,-0.6 1.34,-1.33V5.33C17,4.6 16.4,4 15.67,4z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal"
|
||||||
|
android:autoMirrored="true">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M15.67,4L14,4L14,2h-4v2L8.33,4C7.6,4 7,4.6 7,5.33v15.33C7,21.4 7.6,22 8.33,22h7.33c0.74,0 1.34,-0.6 1.34,-1.33L17,5.33C17,4.6 16.4,4 15.67,4zM12.95,17.95h-1.9v-1.9h1.9v1.9zM14.3,12.69s-0.38,0.42 -0.67,0.71c-0.48,0.48 -0.83,1.15 -0.83,1.6h-1.6c0,-0.83 0.46,-1.52 0.93,-2l0.93,-0.94c0.27,-0.27 0.44,-0.65 0.44,-1.06 0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5L9,11c0,-1.66 1.34,-3 3,-3s3,1.34 3,3c0,0.66 -0.27,1.26 -0.7,1.69z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M3,3v8h8L11,3L3,3zM9,9L5,9L5,5h4v4zM3,13v8h8v-8L3,13zM9,19L5,19v-4h4v4zM13,3v8h8L21,3h-8zM19,9h-4L15,5h4v4zM13,13v8h8v-8h-8zM19,19h-4v-4h4v4z"
|
||||||
|
android:fillType="evenOdd" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M17,20c-0.29,0 -0.56,-0.06 -0.76,-0.15 -0.71,-0.37 -1.21,-0.88 -1.71,-2.38 -0.51,-1.56 -1.47,-2.29 -2.39,-3 -0.79,-0.61 -1.61,-1.24 -2.32,-2.53C9.29,10.98 9,9.93 9,9c0,-2.8 2.2,-5 5,-5s5,2.2 5,5h2c0,-3.93 -3.07,-7 -7,-7S7,5.07 7,9c0,1.26 0.38,2.65 1.07,3.9 0.91,1.65 1.98,2.48 2.85,3.15 0.81,0.62 1.39,1.07 1.71,2.05 0.6,1.82 1.37,2.84 2.73,3.55 0.51,0.23 1.07,0.35 1.64,0.35 2.21,0 4,-1.79 4,-4h-2c0,1.1 -0.9,2 -2,2zM7.64,2.64L6.22,1.22C4.23,3.21 3,5.96 3,9s1.23,5.79 3.22,7.78l1.41,-1.41C6.01,13.74 5,11.49 5,9s1.01,-4.74 2.64,-6.36zM11.5,9c0,1.38 1.12,2.5 2.5,2.5s2.5,-1.12 2.5,-2.5 -1.12,-2.5 -2.5,-2.5 -2.5,1.12 -2.5,2.5z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M6.03,3.2C7.15,2.44 8.51,2 10,2c3.93,0 7,3.07 7,7c0,1.26 -0.38,2.65 -1.07,3.9c-0.02,0.04 -0.05,0.08 -0.08,0.13l-1.48,-1.48C14.77,10.69 15,9.8 15,9c0,-2.8 -2.2,-5 -5,-5C9.08,4 8.24,4.26 7.5,4.67L6.03,3.2zM17.21,14.38l1.43,1.43C20.11,13.93 21,11.57 21,9c0,-3.04 -1.23,-5.79 -3.22,-7.78l-1.42,1.42C17.99,4.26 19,6.51 19,9C19,11.02 18.33,12.88 17.21,14.38zM10,6.5c-0.21,0 -0.4,0.03 -0.59,0.08l3.01,3.01C12.47,9.4 12.5,9.21 12.5,9C12.5,7.62 11.38,6.5 10,6.5zM21.19,21.19L2.81,2.81L1.39,4.22l2.13,2.13C3.19,7.16 3,8.05 3,9h2c0,-0.36 0.05,-0.71 0.12,-1.05l6.61,6.61c-0.88,0.68 -1.78,1.41 -2.27,2.9c-0.5,1.5 -1,2.01 -1.71,2.38C7.56,19.94 7.29,20 7,20c-1.1,0 -2,-0.9 -2,-2H3c0,2.21 1.79,4 4,4c0.57,0 1.13,-0.12 1.64,-0.35c1.36,-0.71 2.13,-1.73 2.73,-3.55c0.32,-0.98 0.9,-1.43 1.71,-2.05c0.03,-0.02 0.05,-0.04 0.08,-0.06l6.62,6.62L21.19,21.19z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12,15c1.66,0 2.99,-1.34 2.99,-3L15,6c0,-1.66 -1.34,-3 -3,-3S9,4.34 9,6v6c0,1.66 1.34,3 3,3zM17.3,12c0,3 -2.54,5.1 -5.3,5.1S6.7,15 6.7,12L5,12c0,3.42 2.72,6.23 6,6.72L11,22h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M19,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM14,17h-2L12,9h-2L10,7h4v10z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M16.01,7L16,3h-2v4h-4V3H8v4h-0.01C7,6.99 6,7.99 6,8.99v5.49L9.5,18v3h5v-3l3.5,-3.51v-5.5c0,-1 -1,-2 -1.99,-1.99z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M12,5c-3.87,0 -7,3.13 -7,7h2c0,-2.76 2.24,-5 5,-5s5,2.24 5,5h2c0,-3.87 -3.13,-7 -7,-7zM13,14.29c0.88,-0.39 1.5,-1.26 1.5,-2.29 0,-1.38 -1.12,-2.5 -2.5,-2.5S9.5,10.62 9.5,12c0,1.02 0.62,1.9 1.5,2.29v3.3L7.59,21 9,22.41l3,-3 3,3L16.41,21 13,17.59v-3.3zM12,1C5.93,1 1,5.93 1,12h2c0,-4.97 4.03,-9 9,-9s9,4.03 9,9h2c0,-6.07 -4.93,-11 -11,-11z" />
|
||||||
|
</vector>
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_marginHorizontal="8dp"
|
android:layout_marginHorizontal="8dp"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
@@ -11,43 +11,97 @@
|
|||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1" />
|
android:layout_weight="1" />
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/pod_left"
|
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:orientation="horizontal">
|
||||||
android:drawableStart="@drawable/ic_airpod_left_24"
|
|
||||||
tools:text="100%" />
|
<ImageView
|
||||||
|
android:id="@+id/pod_left_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_airpod_left_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/pod_left_label"
|
||||||
|
style="@style/TextAppearance.Compat.Notification.Title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
tools:text="100%" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_left_charging"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_power_24" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_left_ear"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_hearing_24" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1" />
|
android:layout_weight="1" />
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/pod_case"
|
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:orientation="horizontal">
|
||||||
android:layout_marginHorizontal="8dp"
|
|
||||||
android:drawableStart="@drawable/ic_airpod_case_24"
|
<ImageView
|
||||||
tools:text="100%" />
|
android:id="@+id/pod_case_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_airpod_case_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/pod_case_label"
|
||||||
|
style="@style/TextAppearance.Compat.Notification.Title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
tools:text="100%" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_case_charging"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_power_24" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1" />
|
android:layout_weight="1" />
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/pod_right"
|
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:orientation="horizontal">
|
||||||
android:drawableStart="@drawable/ic_airpod_right_24"
|
|
||||||
tools:text="100%" />
|
<ImageView
|
||||||
|
android:id="@+id/pod_right_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_airpod_right_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/pod_right_label"
|
||||||
|
style="@style/TextAppearance.Compat.Notification.Title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
tools:text="100%" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_right_charging"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_power_24" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_right_ear"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_hearing_24" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_horizontal">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/headphones_label"
|
android:id="@+id/headphones_label"
|
||||||
@@ -10,16 +11,27 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
tools:text="AirPods Max" />
|
tools:text="Beats Solo 3" />
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/headphones"
|
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="16dp"
|
android:layout_marginHorizontal="8dp"
|
||||||
android:drawableStart="@drawable/ic_device_generic_headphones"
|
android:orientation="horizontal">
|
||||||
android:gravity="center"
|
|
||||||
tools:text="100%" />
|
<ImageView
|
||||||
|
android:id="@+id/headphones_battery_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_battery_unknown_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/headphones_battery_label"
|
||||||
|
style="@style/TextAppearance.Compat.Notification.Title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginHorizontal="8dp"
|
||||||
|
tools:text="100%" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_horizontal">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/headphones_label"
|
android:id="@+id/headphones_label"
|
||||||
@@ -12,14 +13,35 @@
|
|||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
tools:text="AirPods Max" />
|
tools:text="AirPods Max" />
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/headphones"
|
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="16dp"
|
android:layout_marginHorizontal="8dp"
|
||||||
android:drawableStart="@drawable/ic_device_generic_headphones"
|
android:orientation="horizontal">
|
||||||
android:gravity="center"
|
|
||||||
tools:text="100%" />
|
<ImageView
|
||||||
|
android:id="@+id/headphones_battery_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_battery_unknown_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/headphones_battery_label"
|
||||||
|
style="@style/TextAppearance.Compat.Notification.Title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginHorizontal="8dp"
|
||||||
|
tools:text="100%" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/headphones_charging"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_power_24" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/headphones_worn"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_hearing_24" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -49,6 +49,18 @@
|
|||||||
app:layout_constraintTop_toBottomOf="@id/permission_label"
|
app:layout_constraintTop_toBottomOf="@id/permission_label"
|
||||||
tools:text="Lorem Ipsum" />
|
tools:text="Lorem Ipsum" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/privacy_policy"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Tooltip"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="@string/settings_privacy_policy_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/permission_description" />
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/grant_action"
|
android:id="@+id/grant_action"
|
||||||
style="@style/Widget.MaterialComponents.Button"
|
style="@style/Widget.MaterialComponents.Button"
|
||||||
@@ -60,7 +72,7 @@
|
|||||||
android:text="@string/general_grant_permission_action"
|
android:text="@string/general_grant_permission_action"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/permission_description" />
|
app:layout_constraintTop_toBottomOf="@id/privacy_policy" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -79,7 +91,7 @@
|
|||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/reception"
|
app:layout_constraintStart_toEndOf="@id/reception"
|
||||||
app:layout_constraintTop_toTopOf="@+id/reception"
|
app:layout_constraintTop_toTopOf="@+id/reception"
|
||||||
app:srcCompat="@drawable/ic_baseline_signal_cellular_alt_24"
|
app:srcCompat="@drawable/ic_baseline_settings_input_antenna_24"
|
||||||
tools:text="Yours (RSSI -61)" />
|
tools:text="Yours (RSSI -61)" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
@@ -87,61 +99,334 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:barrierDirection="top"
|
app:barrierDirection="top"
|
||||||
app:constraint_referenced_ids="pod_left,pod_case,pod_right" />
|
app:constraint_referenced_ids="pod_left_container,pod_case_container,pod_right_container" />
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/pod_left"
|
android:id="@+id/pod_left_container"
|
||||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="4dp"
|
||||||
android:gravity="center_horizontal"
|
android:minHeight="108dp"
|
||||||
app:drawableTopCompat="@drawable/ic_airpod_left_24"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
||||||
app:layout_constraintEnd_toStartOf="@id/pod_case"
|
app:layout_constraintEnd_toStartOf="@id/pod_case_container"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
||||||
app:layout_constraintVertical_bias="0.0"
|
app:layout_constraintVertical_bias="0.0">
|
||||||
tools:text="Left pod\n100%\n(Primary)" />
|
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<ImageView
|
||||||
android:id="@+id/pod_case"
|
android:id="@+id/pod_left_icon"
|
||||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_airpod_left_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_left_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_left_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_left_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:text="@string/pods_dual_left_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_left_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_left_battery_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_left_battery_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_left_battery_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_left_battery_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/general_value_unknown_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_left_battery_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_left_label" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_left_charging_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_power_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_left_charging_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_left_charging_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_left_charging_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/pods_charging_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_left_charging_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_left_battery_label" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_left_microphone_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_keyboard_voice_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_left_microphone_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_left_microphone_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_left_microphone_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/pods_microphone_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_left_microphone_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_left_charging_label" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_left_wear_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_hearing_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_left_wear_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_left_wear_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_left_wear_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/pods_inear_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_left_wear_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_left_microphone_label" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/pod_case_container"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="8dp"
|
android:layout_marginHorizontal="4dp"
|
||||||
android:gravity="center_horizontal"
|
|
||||||
app:drawableTopCompat="@drawable/ic_airpod_case_24"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
||||||
app:layout_constraintEnd_toStartOf="@id/pod_right"
|
app:layout_constraintEnd_toStartOf="@id/pod_right_container"
|
||||||
app:layout_constraintStart_toEndOf="@id/pod_left"
|
app:layout_constraintStart_toEndOf="@id/pod_left_container"
|
||||||
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
||||||
app:layout_constraintVertical_bias="0.0"
|
app:layout_constraintVertical_bias="0.0">
|
||||||
tools:text="Case\n50%" />
|
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<ImageView
|
||||||
android:id="@+id/pod_right"
|
android:id="@+id/pod_case_icon"
|
||||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_airpod_case_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_case_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_case_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_case_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:text="@string/pods_case_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_case_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_case_battery_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_case_battery_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_case_battery_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_case_battery_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/general_value_unknown_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_case_battery_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_case_label" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_case_charging_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_power_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_case_charging_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_case_charging_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_case_charging_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/pods_charging_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_case_charging_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_case_battery_label" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_case_lid_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_grid_view_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_case_lid_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_case_lid_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_case_lid_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/pods_case_status_closed_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_case_lid_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_case_charging_label" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/pod_right_container"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="4dp"
|
||||||
android:layout_marginEnd="16dp"
|
android:layout_marginEnd="16dp"
|
||||||
android:gravity="center_horizontal"
|
|
||||||
app:drawableTopCompat="@drawable/ic_airpod_right_24"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/pod_case"
|
app:layout_constraintStart_toEndOf="@id/pod_case_container"
|
||||||
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
||||||
app:layout_constraintVertical_bias="0.0"
|
app:layout_constraintVertical_bias="0.0">
|
||||||
tools:text="R 90%" />
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_right_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_airpod_right_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_right_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_right_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_right_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:text="@string/pods_dual_right_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_right_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_right_battery_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_right_battery_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_right_battery_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_right_battery_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/general_value_unknown_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_right_battery_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_right_label" />
|
||||||
|
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_right_charging_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_power_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_right_charging_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_right_charging_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_right_charging_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/pods_charging_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_right_charging_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_right_battery_label" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_right_microphone_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_keyboard_voice_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_right_microphone_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_right_microphone_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_right_microphone_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/pods_microphone_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_right_microphone_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_right_charging_label" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_right_wear_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_hearing_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_right_wear_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_right_wear_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_right_wear_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/pods_inear_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_right_wear_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_right_microphone_label" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
android:id="@+id/barrier_bottom"
|
android:id="@+id/barrier_bottom"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:barrierDirection="bottom"
|
app:barrierDirection="bottom"
|
||||||
app:constraint_referenced_ids="pod_left,pod_case,pod_right" />
|
app:constraint_referenced_ids="pod_left_container,pod_case_container,pod_right_container" />
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:id="@+id/status"
|
android:id="@+id/status"
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/reception"
|
app:layout_constraintStart_toEndOf="@id/reception"
|
||||||
app:layout_constraintTop_toTopOf="@+id/reception"
|
app:layout_constraintTop_toTopOf="@+id/reception"
|
||||||
app:srcCompat="@drawable/ic_baseline_signal_cellular_alt_24"
|
app:srcCompat="@drawable/ic_baseline_settings_input_antenna_24"
|
||||||
tools:text="Yours (RSSI -61)" />
|
tools:text="Yours (RSSI -61)" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
@@ -90,21 +90,40 @@
|
|||||||
app:constraint_referenced_ids="headphones"
|
app:constraint_referenced_ids="headphones"
|
||||||
tools:layout_editor_absoluteY="60dp" />
|
tools:layout_editor_absoluteY="60dp" />
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/headphones"
|
android:id="@+id/headphones"
|
||||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:gravity="center_horizontal"
|
android:layout_marginEnd="8dp"
|
||||||
app:drawableTopCompat="@drawable/ic_device_generic_headphones"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_goneMarginBottom="16dp"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
||||||
app:layout_constraintVertical_bias="0.0"
|
app:layout_constraintVertical_bias="0.0"
|
||||||
tools:text="Headphones\n50%\nCharging" />
|
app:layout_goneMarginBottom="16dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/battery_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/battery_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/battery_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/battery_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/general_value_unknown_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/battery_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
android:id="@+id/barrier_bottom"
|
android:id="@+id/barrier_bottom"
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/reception"
|
app:layout_constraintStart_toEndOf="@id/reception"
|
||||||
app:layout_constraintTop_toTopOf="@+id/reception"
|
app:layout_constraintTop_toTopOf="@+id/reception"
|
||||||
app:srcCompat="@drawable/ic_baseline_signal_cellular_alt_24"
|
app:srcCompat="@drawable/ic_baseline_settings_input_antenna_24"
|
||||||
tools:text="Yours (RSSI -61)" />
|
tools:text="Yours (RSSI -61)" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
@@ -90,21 +90,81 @@
|
|||||||
app:constraint_referenced_ids="headphones"
|
app:constraint_referenced_ids="headphones"
|
||||||
tools:layout_editor_absoluteY="60dp" />
|
tools:layout_editor_absoluteY="60dp" />
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/headphones"
|
android:id="@+id/headphones"
|
||||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:gravity="center_horizontal"
|
android:layout_marginEnd="8dp"
|
||||||
app:drawableTopCompat="@drawable/ic_device_generic_headphones"
|
android:minHeight="64dp"
|
||||||
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_goneMarginBottom="16dp"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
||||||
app:layout_constraintVertical_bias="0.0"
|
app:layout_constraintVertical_bias="0.0"
|
||||||
tools:text="Headphones\n50%\nCharging" />
|
app:layout_goneMarginBottom="16dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/battery_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/battery_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/battery_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/battery_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/general_value_unknown_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/battery_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/charging_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_power_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/charging_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/charging_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/charging_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/pods_charging_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/charging_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/battery_label" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/wear_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_hearing_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/wear_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/wear_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/wear_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/headset_being_worn_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/wear_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/charging_label" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
android:id="@+id/barrier_bottom"
|
android:id="@+id/barrier_bottom"
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/reception"
|
app:layout_constraintStart_toEndOf="@id/reception"
|
||||||
app:layout_constraintTop_toTopOf="@+id/reception"
|
app:layout_constraintTop_toTopOf="@+id/reception"
|
||||||
app:srcCompat="@drawable/ic_baseline_signal_cellular_alt_24"
|
app:srcCompat="@drawable/ic_baseline_settings_input_antenna_24"
|
||||||
tools:text="Yours (RSSI -61)" />
|
tools:text="Yours (RSSI -61)" />
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
|||||||
@@ -1,72 +1,182 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content">
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
<ImageView
|
||||||
android:id="@+id/pod_label"
|
android:id="@+id/pod_icon"
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/ic_device_generic_earbuds"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_label"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/pod_label"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_label"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginHorizontal="8dp"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:layout_marginBottom="4dp"
|
app:layout_constraintEnd_toStartOf="@id/signal"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="AirPods Pro" />
|
tools:text="AirPods Pro" />
|
||||||
|
|
||||||
<LinearLayout
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/signal"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="8dp"
|
android:drawableEnd="@drawable/ic_baseline_signal_cellular_alt_24"
|
||||||
android:orientation="horizontal">
|
android:gravity="center"
|
||||||
|
android:minWidth="44dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/pod_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/pod_label"
|
||||||
|
tools:text="-90%" />
|
||||||
|
|
||||||
<FrameLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="0dp"
|
android:id="@id/pod_left_container"
|
||||||
android:layout_height="0dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_weight="1" />
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/pod_case_container"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="spread"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_label">
|
||||||
|
|
||||||
<TextView
|
<ImageView
|
||||||
android:id="@+id/pod_left"
|
android:id="@+id/pod_left_icon"
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:drawableStart="@drawable/ic_airpod_left_24"
|
android:src="@drawable/ic_airpod_left_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_left_battery_label"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/pod_left_battery_icon"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_left_battery_label" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@id/pod_left_battery_icon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_left_battery_label"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/pod_left_battery_label"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_left_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_left_battery_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_left_battery_label"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_left_battery_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="100%" />
|
tools:text="100%" />
|
||||||
|
|
||||||
<FrameLayout
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1" />
|
|
||||||
|
|
||||||
<TextView
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/pod_case"
|
android:id="@+id/pod_case_container"
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/pod_right_container"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="spread"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_left_container"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_label">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_case_icon"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:layout_marginHorizontal="8dp"
|
android:src="@drawable/ic_airpod_case_24"
|
||||||
android:drawableStart="@drawable/ic_airpod_case_24"
|
app:layout_constraintBottom_toBottomOf="@id/pod_case_battery_label"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/pod_case_battery_icon"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_case_battery_label" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@id/pod_case_battery_icon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_case_battery_label"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/pod_case_battery_label"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_case_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_case_battery_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_case_battery_label"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_case_battery_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="100%" />
|
tools:text="100%" />
|
||||||
|
|
||||||
<FrameLayout
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1" />
|
|
||||||
|
|
||||||
<TextView
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/pod_right"
|
android:id="@id/pod_right_container"
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="spread"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_case_container"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_label">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_right_icon"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:drawableStart="@drawable/ic_airpod_right_24"
|
android:src="@drawable/ic_airpod_right_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_right_battery_label"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/pod_right_battery_icon"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_right_battery_label" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@id/pod_right_battery_icon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_right_battery_label"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/pod_right_battery_label"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_right_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_right_battery_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_right_battery_label"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_right_battery_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="100%" />
|
tools:text="100%" />
|
||||||
|
|
||||||
<FrameLayout
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1" />
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</LinearLayout>
|
|
||||||
</LinearLayout>
|
|
||||||
@@ -1,29 +1,81 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content">
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
<ImageView
|
||||||
android:id="@+id/headphones_label"
|
android:id="@+id/headphones_icon"
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/ic_device_generic_earbuds"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/headphones_label"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/headphones_label"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/headphones_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/headphones_label"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginHorizontal="8dp"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:layout_marginBottom="4dp"
|
app:layout_constraintEnd_toStartOf="@id/signal"
|
||||||
android:gravity="center"
|
app:layout_constraintStart_toEndOf="@id/headphones_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="AirPods Max" />
|
tools:text="AirPods Max" />
|
||||||
|
|
||||||
<TextView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:id="@+id/headphones"
|
android:id="@+id/signal"
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:drawableEnd="@drawable/ic_baseline_signal_cellular_alt_24"
|
||||||
android:layout_marginHorizontal="16dp"
|
android:gravity="center"
|
||||||
android:drawableStart="@drawable/ic_device_generic_headphones"
|
android:minWidth="44dp"
|
||||||
tools:text="100%" />
|
app:layout_constraintBottom_toBottomOf="@+id/headphones_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/headphones_label"
|
||||||
|
tools:text="-90%" />
|
||||||
|
|
||||||
</LinearLayout>
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/headphones_container"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="spread"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/headphones_label">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@id/headphones_battery_icon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/headphones_battery_label"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/headphones_battery_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/headphones_battery_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/headphones_battery_label"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/headphones_battery_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="100%" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -1,38 +1,81 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content">
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
<ImageView
|
||||||
|
android:id="@+id/headphones_icon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/ic_device_generic_earbuds"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/headphones_label"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/headphones_label"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/headphones_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:id="@+id/headphones_label"
|
android:id="@+id/headphones_label"
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginHorizontal="8dp"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:layout_marginBottom="4dp"
|
app:layout_constraintEnd_toStartOf="@id/signal"
|
||||||
android:gravity="center"
|
app:layout_constraintStart_toEndOf="@id/headphones_icon"
|
||||||
tools:text="AirPods Max" />
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="Beats Solo 3" />
|
||||||
|
|
||||||
<LinearLayout
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/signal"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_gravity="center"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal">
|
android:drawableEnd="@drawable/ic_baseline_signal_cellular_alt_24"
|
||||||
|
android:gravity="center"
|
||||||
|
android:minWidth="44dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/headphones_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/headphones_label"
|
||||||
|
tools:text="-90%" />
|
||||||
|
|
||||||
<TextView
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/headphones"
|
android:id="@+id/headphones_container"
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="spread"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/headphones_label">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@id/headphones_battery_icon"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
android:layout_marginHorizontal="16dp"
|
app:layout_constraintBottom_toBottomOf="@id/headphones_battery_label"
|
||||||
android:drawableStart="@drawable/ic_device_generic_headphones"
|
app:layout_constraintEnd_toStartOf="@id/headphones_battery_label"
|
||||||
android:gravity="center"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/headphones_battery_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/headphones_battery_label"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/headphones_battery_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="100%" />
|
tools:text="100%" />
|
||||||
|
|
||||||
</LinearLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:minWidth="320dp"
|
||||||
|
android:minHeight="96dp"
|
||||||
|
android:layout_margin="8dp">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/popup_content"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/close_action"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_bias="1.0" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/close_action"
|
||||||
|
style="@style/Widget.Material3.Button.TextButton"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/general_close_action"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
app:layout_constraintHorizontal_bias="1.0"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/popup_content" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
@@ -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