Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9fa9f6cb8 | ||
|
|
b98ca86886 | ||
|
|
d917c8ffe6 | ||
|
|
5d44737199 | ||
|
|
6e8693a6b0 | ||
|
|
6d9e7404a1 | ||
|
|
70ceb389b5 | ||
|
|
832e364a2f | ||
|
|
51f9c5c7d8 | ||
|
|
15141c8528 | ||
|
|
0cdac10fd4 | ||
|
|
aaab548bfd | ||
|
|
46611b5606 | ||
|
|
fdbdb46577 | ||
|
|
e7fd81d0eb | ||
|
|
ce50bae6a0 | ||
|
|
8331cf3866 | ||
|
|
670262e53b | ||
|
|
c584f942cf | ||
|
|
e83b6bb583 | ||
|
|
cc5de668f0 | ||
|
|
653c7b8c4b | ||
|
|
4e38bcd4cc | ||
|
|
fcbeda4888 | ||
|
|
4101fddc5b | ||
|
|
86df7bea22 | ||
|
|
4db8c0c286 | ||
|
|
1d03335903 | ||
|
|
b1b541014f | ||
|
|
b40a7e435f | ||
|
|
8305fb4c5f | ||
|
|
6c678be511 | ||
|
|
2823663f09 | ||
|
|
033c2264f8 | ||
|
|
8eb6ce19f7 | ||
|
|
1cfc4e6f65 | ||
|
|
f3609fc3a5 | ||
|
|
16da51d095 |
@@ -7,10 +7,14 @@ on:
|
|||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-test:
|
build-all:
|
||||||
name: Build and test
|
name: Assemble all variants
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
flavor: [ Foss,Gplay ]
|
||||||
|
variant: [ Debug ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK 11
|
||||||
@@ -19,16 +23,30 @@ jobs:
|
|||||||
java-version: '11'
|
java-version: '11'
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
cache: gradle
|
cache: gradle
|
||||||
|
|
||||||
- name: Grant execute permission for gradlew
|
- name: Grant execute permission for gradlew
|
||||||
run: chmod +x gradlew
|
run: chmod +x gradlew
|
||||||
|
|
||||||
- name: Build FOSS variant
|
- name: Build (Flavor ${{ matrix.flavor }}, Variant ${{ matrix.variant }})
|
||||||
run: ./gradlew assembleFossDebug
|
run: ./gradlew assemble${{ matrix.flavor }}${{ matrix.variant }}
|
||||||
- name: Test FOSS variant
|
|
||||||
run: ./gradlew testFossDebugUnitTest
|
|
||||||
|
|
||||||
- name: Build Google Play variant
|
test-all:
|
||||||
run: ./gradlew assembleGplayDebug
|
name: Run all tests
|
||||||
- name: Test Google Play variant
|
strategy:
|
||||||
run: ./gradlew testGplayDebugUnitTest
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
flavor: [ Foss,Gplay ]
|
||||||
|
variant: [ Release ]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up JDK 11
|
||||||
|
uses: actions/setup-java@v2
|
||||||
|
with:
|
||||||
|
java-version: '11'
|
||||||
|
distribution: 'adopt'
|
||||||
|
cache: gradle
|
||||||
|
- name: Grant execute permission for gradlew
|
||||||
|
run: chmod +x gradlew
|
||||||
|
|
||||||
|
- name: Run tests (Flavor ${{ matrix.flavor }}, Variant ${{ matrix.variant }})
|
||||||
|
run: ./gradlew test${{ matrix.flavor }}${{ matrix.variant }}
|
||||||
|
|||||||
@@ -34,15 +34,21 @@ Currently supported models:
|
|||||||
* Beats Studio 3
|
* Beats Studio 3
|
||||||
* Beats X
|
* Beats X
|
||||||
* Beats Flex
|
* Beats Flex
|
||||||
|
* Beats Fit Pro
|
||||||
|
* Fake AirPods 1. Generation 🎭
|
||||||
|
* Fake AirPods 2. Generation 🎭
|
||||||
|
* Fake AirPods 3. Generation 🎭
|
||||||
|
* Fake AirPods Pro1 🎭
|
||||||
|
* Fake AirPods Pro2 🎭
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
| Source | Status |
|
| Source | Status |
|
||||||
|-----------------------|--------|
|
|------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod) | [](https://play.google.com/store/apps/details?id=eu.darken.capod) |
|
| [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod) | [](https://play.google.com/store/apps/details?id=eu.darken.capod) |
|
||||||
| [Google Play Beta](https://play.google.com/apps/testing/eu.darken.capod) | ? |
|
| [Google Play Beta](https://play.google.com/apps/testing/eu.darken.capod) | [](https://play.google.com/apps/testing/eu.darken.capod) | |
|
||||||
| [Github Releases](https://github.com/d4rken-org/capod/releases) | [](https://github.com/d4rken-org/capod/releases/latest) |
|
| [Github Releases](https://github.com/d4rken-org/capod/releases) | [](https://github.com/d4rken-org/capod/releases/latest) |
|
||||||
| [F-Droid (IzzyOnDroid)](https://apt.izzysoft.de/packages/eu.darken.capod/) | [](https://apt.izzysoft.de/packages/eu.darken.capod/) |
|
| [F-Droid (IzzyOnDroid)](https://apt.izzysoft.de/packages/eu.darken.capod/) | [](https://apt.izzysoft.de/packages/eu.darken.capod/) |
|
||||||
|
|
||||||
|
|
||||||
## Support the project
|
## Support the project
|
||||||
|
|||||||
@@ -1,4 +1,35 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest package="eu.darken.capod.common">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="eu.darken.capod.common">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||||
|
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.BLUETOOTH"
|
||||||
|
android:maxSdkVersion="30" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.BLUETOOTH_ADMIN"
|
||||||
|
android:maxSdkVersion="30" />
|
||||||
|
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.ACCESS_COARSE_LOCATION"
|
||||||
|
android:maxSdkVersion="30" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.ACCESS_FINE_LOCATION"
|
||||||
|
android:maxSdkVersion="30" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.BLUETOOTH_SCAN"
|
||||||
|
android:usesPermissionFlags="neverForLocation" />
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<receiver
|
||||||
|
android:name=".bluetooth.BleScanResultReceiver"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="eu.darken.capod.bluetooth.DELIVER_SCAN_RESULTS" />
|
||||||
|
</intent-filter>
|
||||||
|
</receiver>
|
||||||
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package eu.darken.capod.common.bluetooth
|
||||||
|
|
||||||
|
import android.bluetooth.le.ScanResult
|
||||||
|
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||||
|
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class BleScanResultForwarder @Inject constructor() {
|
||||||
|
|
||||||
|
private val forwarder = MutableSharedFlow<Collection<ScanResult>>(
|
||||||
|
replay = 0,
|
||||||
|
extraBufferCapacity = 128,
|
||||||
|
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||||
|
)
|
||||||
|
val results: Flow<Collection<ScanResult>> = forwarder
|
||||||
|
|
||||||
|
fun forward(scanResults: Collection<ScanResult>) {
|
||||||
|
log(TAG, VERBOSE) { "forward($scanResults)" }
|
||||||
|
val success = forwarder.tryEmit(scanResults)
|
||||||
|
if (!success) log(TAG, WARN) { "Failed to forward (overflow?) $scanResults" }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = logTag("Bluetooth", "BleScanner", "Forwarder")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package eu.darken.capod.common.bluetooth
|
||||||
|
|
||||||
|
import android.bluetooth.le.BluetoothLeScanner
|
||||||
|
import android.bluetooth.le.ScanResult
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import eu.darken.capod.common.coroutine.AppScope
|
||||||
|
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||||
|
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class BleScanResultReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
|
@Inject @AppScope lateinit var appScope: CoroutineScope
|
||||||
|
@Inject lateinit var scanResultForwarder: BleScanResultForwarder
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
log(TAG, VERBOSE) { "onReceive($context, $intent)" }
|
||||||
|
if (intent.action != ACTION) {
|
||||||
|
log(TAG, WARN) { "Unknown action: ${intent.action}" }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (intent.extras == null) {
|
||||||
|
log(TAG) { "Extras are null!" }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val errorCode = intent.getIntExtra(BluetoothLeScanner.EXTRA_ERROR_CODE, 0)
|
||||||
|
log(TAG, VERBOSE) { "errorCode=$errorCode" }
|
||||||
|
if (errorCode != 0) {
|
||||||
|
log(TAG, WARN) { "ScanCallback error code: $errorCode" }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val callbackType = intent.getIntExtra(BluetoothLeScanner.EXTRA_CALLBACK_TYPE, -1)
|
||||||
|
log(TAG, VERBOSE) { "callbackType=$callbackType" }
|
||||||
|
|
||||||
|
val scanResults = intent.getParcelableArrayListExtra<ScanResult>(BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT)
|
||||||
|
log(TAG, VERBOSE) { "scanResults=$scanResults" }
|
||||||
|
|
||||||
|
if (scanResults == null) {
|
||||||
|
log(TAG) { "Scan results were empty!" }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val pending = goAsync()
|
||||||
|
appScope.launch {
|
||||||
|
scanResultForwarder.forward(scanResults)
|
||||||
|
pending.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = logTag("Bluetooth", "BleScanner", "Forwarder", "Receiver")
|
||||||
|
const val ACTION = "eu.darken.capod.bluetooth.DELIVER_SCAN_RESULTS"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,21 @@
|
|||||||
package eu.darken.capod.common.bluetooth
|
package eu.darken.capod.common.bluetooth
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.bluetooth.le.ScanCallback
|
import android.bluetooth.le.ScanCallback
|
||||||
import android.bluetooth.le.ScanFilter
|
import android.bluetooth.le.ScanFilter
|
||||||
import android.bluetooth.le.ScanResult
|
import android.bluetooth.le.ScanResult
|
||||||
import android.bluetooth.le.ScanSettings
|
import android.bluetooth.le.ScanSettings
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
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
|
||||||
|
import eu.darken.capod.common.notifications.PendingIntentCompat
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.flow.callbackFlow
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -25,28 +26,48 @@ class BleScanner @Inject constructor(
|
|||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val bluetoothManager: BluetoothManager2,
|
private val bluetoothManager: BluetoothManager2,
|
||||||
private val fakeBleData: FakeBleData,
|
private val fakeBleData: FakeBleData,
|
||||||
|
private val scanResultForwarder: BleScanResultForwarder,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@SuppressLint("MissingPermission") fun scan(
|
@SuppressLint("MissingPermission") fun scan(
|
||||||
filters: Set<ScanFilter>,
|
filters: Set<ScanFilter>,
|
||||||
scannerMode: ScannerMode,
|
scannerMode: ScannerMode = ScannerMode.BALANCED,
|
||||||
compatMode: Boolean,
|
disableOffloadFiltering: Boolean = true,
|
||||||
): Flow<List<BleScanResult>> = callbackFlow {
|
disableOffloadBatching: Boolean = true,
|
||||||
log(TAG, VERBOSE) { "scan(filters=$filters, scannerMode=$scannerMode, compatMode=$compatMode)" }
|
disableDirectScanCallback: Boolean = true,
|
||||||
if (compatMode) log(TAG, WARN) { "Using compatibilityMode!" }
|
): Flow<Collection<BleScanResult>> = callbackFlow {
|
||||||
|
log(TAG) { "scan(filters=$filters, scannerMode=$scannerMode)" }
|
||||||
|
|
||||||
val adapter = bluetoothManager.adapter ?: throw IllegalStateException("Bluetooth adapter unavailable")
|
val adapter = bluetoothManager.adapter ?: throw IllegalStateException("Bluetooth adapter unavailable")
|
||||||
|
|
||||||
val supportsOffloadFiltering = adapter.isOffloadedFilteringSupported.also {
|
val useOffloadedFiltering = adapter.isOffloadedFilteringSupported.also {
|
||||||
log(TAG, if (it) DEBUG else WARN) { "isOffloadedFilteringSupported=$it" }
|
log(TAG, if (it) DEBUG else WARN) { "isOffloadedFilteringSupported=$it" }
|
||||||
} && !compatMode
|
} && !disableOffloadFiltering
|
||||||
|
if (disableOffloadFiltering) log(TAG, WARN) { "Offloaded filtering is disabled!" }
|
||||||
|
|
||||||
val supportsOffloadBatching = adapter.isOffloadedScanBatchingSupported.also {
|
val useOffloadedBatching = adapter.isOffloadedScanBatchingSupported.also {
|
||||||
log(TAG, if (it) DEBUG else WARN) { "isOffloadedScanBatchingSupported=$it" }
|
log(TAG, if (it) DEBUG else WARN) { "isOffloadedScanBatchingSupported=$it" }
|
||||||
} && !compatMode
|
} && !disableOffloadBatching
|
||||||
|
if (disableOffloadBatching) log(TAG, WARN) { "Offloaded scan-batching is disabled!" }
|
||||||
|
|
||||||
|
if (disableDirectScanCallback) log(TAG, WARN) { "Direct scan callback is disabled!" }
|
||||||
|
|
||||||
val scanner = bluetoothManager.scanner ?: throw IllegalStateException("BLE scanner unavailable")
|
val scanner = bluetoothManager.scanner ?: throw IllegalStateException("BLE scanner unavailable")
|
||||||
|
|
||||||
|
val filterResults: (Collection<ScanResult>) -> Collection<BleScanResult> = { results ->
|
||||||
|
results
|
||||||
|
.filter { result ->
|
||||||
|
val passed = when {
|
||||||
|
useOffloadedFiltering -> true
|
||||||
|
filters.isEmpty() -> true
|
||||||
|
else -> filters.any { it.matches(result) }
|
||||||
|
}
|
||||||
|
if (!passed) log(TAG, VERBOSE) { "Manually filtered $result" }
|
||||||
|
passed
|
||||||
|
}
|
||||||
|
.map { BleScanResult.fromScanResult(it) }
|
||||||
|
}
|
||||||
|
|
||||||
val callback = object : ScanCallback() {
|
val callback = object : ScanCallback() {
|
||||||
var lastScanAt = System.currentTimeMillis()
|
var lastScanAt = System.currentTimeMillis()
|
||||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||||
@@ -55,17 +76,8 @@ class BleScanner @Inject constructor(
|
|||||||
lastScanAt = System.currentTimeMillis()
|
lastScanAt = System.currentTimeMillis()
|
||||||
"onScanResult(delay=${delay}ms, callbackType=$callbackType, result=$result)"
|
"onScanResult(delay=${delay}ms, callbackType=$callbackType, result=$result)"
|
||||||
}
|
}
|
||||||
val toSend = if (
|
|
||||||
supportsOffloadFiltering
|
trySend(filterResults(setOf(result)))
|
||||||
|| filters.isEmpty()
|
|
||||||
|| filters.any { it.matchesSafe(result) }
|
|
||||||
) {
|
|
||||||
listOf(BleScanResult.fromScanResult(result))
|
|
||||||
} else {
|
|
||||||
log(TAG, VERBOSE) { "Manual filtering: No match for $result" }
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
trySend(toSend)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBatchScanResults(results: MutableList<ScanResult>) {
|
override fun onBatchScanResults(results: MutableList<ScanResult>) {
|
||||||
@@ -75,18 +87,7 @@ class BleScanner @Inject constructor(
|
|||||||
"onBatchScanResults(delay=${delay}ms, results=$results)"
|
"onBatchScanResults(delay=${delay}ms, results=$results)"
|
||||||
}
|
}
|
||||||
|
|
||||||
val toSend = results
|
trySend(filterResults(results))
|
||||||
.filter { result ->
|
|
||||||
val passed = when {
|
|
||||||
supportsOffloadFiltering -> true
|
|
||||||
filters.isEmpty() -> true
|
|
||||||
else -> filters.any { it.matches(result) }
|
|
||||||
}
|
|
||||||
if (!passed) log(TAG, VERBOSE) { "Manually filtered $result" }
|
|
||||||
passed
|
|
||||||
}
|
|
||||||
.map { BleScanResult.fromScanResult(it) }
|
|
||||||
trySend(toSend)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScanFailed(errorCode: Int) {
|
override fun onScanFailed(errorCode: Int) {
|
||||||
@@ -94,58 +95,113 @@ class BleScanner @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val forwarderConsumer = if (disableDirectScanCallback) {
|
||||||
val settings = ScanSettings.Builder().apply {
|
scanResultForwarder.results
|
||||||
setScanMode(
|
.onEach { results -> trySend(filterResults(results)) }
|
||||||
when (scannerMode) {
|
.launchIn(this)
|
||||||
ScannerMode.LOW_POWER -> ScanSettings.SCAN_MODE_LOW_POWER
|
} else {
|
||||||
ScannerMode.BALANCED -> ScanSettings.SCAN_MODE_BALANCED
|
null
|
||||||
ScannerMode.LOW_LATENCY -> ScanSettings.SCAN_MODE_LOW_LATENCY
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (supportsOffloadBatching) {
|
|
||||||
setReportDelay(
|
|
||||||
when (scannerMode) {
|
|
||||||
ScannerMode.LOW_POWER -> 2000L
|
|
||||||
ScannerMode.BALANCED -> 1000L
|
|
||||||
ScannerMode.LOW_LATENCY -> 500L
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
log(TAG, VERBOSE) { "Settings created for offloaded filtering: $settings" }
|
|
||||||
|
|
||||||
val flushJob = launch {
|
|
||||||
log(TAG) { "Flush job launched" }
|
|
||||||
while (isActive) {
|
|
||||||
// Can undercut the minimum setReportDelay(), e.g. 5000ms on a Pixel5@12
|
|
||||||
log(TAG, VERBOSE) { "Flushing scan results." }
|
|
||||||
adapter.bluetoothLeScanner.flushPendingScanResults(callback)
|
|
||||||
when (scannerMode) {
|
|
||||||
ScannerMode.LOW_POWER -> break
|
|
||||||
ScannerMode.BALANCED -> delay(1000)
|
|
||||||
ScannerMode.LOW_LATENCY -> delay(500)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
scanner.startScan(
|
val flushJob = if (!disableDirectScanCallback) {
|
||||||
if (supportsOffloadFiltering) filters.toList() else listOf(ScanFilter.Builder().build()),
|
launch {
|
||||||
settings,
|
log(TAG) { "Flush job launched" }
|
||||||
callback
|
while (isActive) {
|
||||||
)
|
log(TAG, VERBOSE) { "Flushing scan results." }
|
||||||
log(TAG) { "BleScanner started (filters=$filters, settings=$settings)" }
|
// Can undercut the minimum setReportDelay(), e.g. 5000ms on a Pixel5@12
|
||||||
|
adapter.bluetoothLeScanner.flushPendingScanResults(callback)
|
||||||
|
when (scannerMode) {
|
||||||
|
ScannerMode.LOW_POWER -> break
|
||||||
|
ScannerMode.BALANCED -> delay(2000)
|
||||||
|
ScannerMode.LOW_LATENCY -> delay(500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val filterList = when {
|
||||||
|
useOffloadedFiltering -> filters.toList()
|
||||||
|
else -> emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
val scanSettings = ScanSettings.Builder().apply {
|
||||||
|
setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
|
||||||
|
when (scannerMode) {
|
||||||
|
ScannerMode.LOW_POWER -> {
|
||||||
|
setScanMode(ScanSettings.SCAN_MODE_LOW_POWER)
|
||||||
|
setMatchMode(ScanSettings.MATCH_MODE_STICKY)
|
||||||
|
setNumOfMatches(ScanSettings.MATCH_NUM_FEW_ADVERTISEMENT)
|
||||||
|
}
|
||||||
|
ScannerMode.BALANCED -> {
|
||||||
|
setScanMode(ScanSettings.SCAN_MODE_BALANCED)
|
||||||
|
setMatchMode(ScanSettings.MATCH_MODE_STICKY)
|
||||||
|
setNumOfMatches(ScanSettings.MATCH_NUM_FEW_ADVERTISEMENT)
|
||||||
|
}
|
||||||
|
ScannerMode.LOW_LATENCY -> {
|
||||||
|
setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
|
||||||
|
setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
|
||||||
|
setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val delay = if (useOffloadedBatching) {
|
||||||
|
when (scannerMode) {
|
||||||
|
ScannerMode.LOW_POWER -> 2000L
|
||||||
|
ScannerMode.BALANCED -> 1000L
|
||||||
|
ScannerMode.LOW_LATENCY -> 500L
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
0L // Anything > 0 enables batching
|
||||||
|
}
|
||||||
|
setReportDelay(delay)
|
||||||
|
}.build()
|
||||||
|
|
||||||
|
if (disableDirectScanCallback) {
|
||||||
|
val callbackIntent = createStartIntent()
|
||||||
|
log(TAG) { "Intent callback: startScan(filters=$filters, settings=$scanSettings, callbackIntent=$callbackIntent)" }
|
||||||
|
scanner.startScan(filterList, scanSettings, callbackIntent)
|
||||||
|
} else {
|
||||||
|
log(TAG) { "Direct callback: startScan(filters=$filters, settings=$scanSettings, callback=$callback)" }
|
||||||
|
scanner.startScan(filterList, scanSettings, callback)
|
||||||
|
}
|
||||||
|
|
||||||
awaitClose {
|
awaitClose {
|
||||||
flushJob.cancel()
|
forwarderConsumer?.cancel()
|
||||||
scanner.stopScan(callback)
|
flushJob?.cancel()
|
||||||
|
if (disableDirectScanCallback) {
|
||||||
|
scanner.stopScan(createStopIntent())
|
||||||
|
} else {
|
||||||
|
scanner.stopScan(callback)
|
||||||
|
}
|
||||||
log(TAG) { "BleScanner stopped" }
|
log(TAG) { "BleScanner stopped" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map { fakeBleData.maybeAddfakeData(it) }
|
.map { fakeBleData.maybeAddfakeData(it) }
|
||||||
|
|
||||||
|
private val receiverIntent by lazy {
|
||||||
|
Intent(context, BleScanResultReceiver::class.java).apply {
|
||||||
|
action = BleScanResultReceiver.ACTION
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createStartIntent(): PendingIntent = PendingIntent.getBroadcast(
|
||||||
|
context,
|
||||||
|
CALLBACK_INTENT_REQUESTCODE,
|
||||||
|
receiverIntent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_MUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun createStopIntent(): PendingIntent = PendingIntent.getBroadcast(
|
||||||
|
context,
|
||||||
|
270,
|
||||||
|
receiverIntent,
|
||||||
|
PendingIntentCompat.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val CALLBACK_INTENT_REQUESTCODE = 270
|
||||||
private val TAG = logTag("Bluetooth", "BleScanner")
|
private val TAG = logTag("Bluetooth", "BleScanner")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ class FakeBleData @Inject constructor(
|
|||||||
private val debugSettings: DebugSettings,
|
private val debugSettings: DebugSettings,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun maybeAddfakeData(originals: List<BleScanResult>): List<BleScanResult> {
|
fun maybeAddfakeData(originals: Collection<BleScanResult>): Collection<BleScanResult> {
|
||||||
if (!debugSettings.showFakeData.value) return originals
|
if (!debugSettings.showFakeData.value) return originals
|
||||||
return originals + getFakeData()
|
return originals + getFakeData()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,4 +9,9 @@ object PendingIntentCompat {
|
|||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
val FLAG_MUTABLE: Int = if (hasApiLevel(31)) {
|
||||||
|
PendingIntent.FLAG_MUTABLE
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -23,51 +23,32 @@ class GeneralSettings @Inject constructor(
|
|||||||
|
|
||||||
override val preferences: SharedPreferences = context.getSharedPreferences("settings_general", Context.MODE_PRIVATE)
|
override val preferences: SharedPreferences = context.getSharedPreferences("settings_general", Context.MODE_PRIVATE)
|
||||||
|
|
||||||
val monitorMode = preferences.createFlowPreference(
|
val monitorMode = preferences.createFlowPreference("core.monitor.mode", MonitorMode.AUTOMATIC, moshi)
|
||||||
"core.monitor.mode",
|
val scannerMode = preferences.createFlowPreference("core.scanner.mode", ScannerMode.BALANCED, moshi)
|
||||||
MonitorMode.AUTOMATIC,
|
|
||||||
moshi
|
|
||||||
)
|
|
||||||
|
|
||||||
val scannerMode = preferences.createFlowPreference(
|
val showAll = preferences.createFlowPreference("core.showall.enabled", false)
|
||||||
"core.scanner.mode",
|
|
||||||
ScannerMode.LOW_LATENCY,
|
|
||||||
moshi
|
|
||||||
)
|
|
||||||
|
|
||||||
val compatibilityMode = preferences.createFlowPreference(
|
val minimumSignalQuality = preferences.createFlowPreference("core.signal.minimum", 0.25f)
|
||||||
"core.compatibility.enabled",
|
|
||||||
|
val mainDeviceAddress = preferences.createFlowPreference<String?>("core.maindevice.address", null)
|
||||||
|
val mainDeviceModel = preferences.createFlowPreference("core.maindevice.model", PodDevice.Model.UNKNOWN, moshi)
|
||||||
|
|
||||||
|
val isOffloadedFilteringDisabled = preferences.createFlowPreference(
|
||||||
|
"core.compat.offloaded.filtering.disabled",
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
val isOffloadedBatchingDisabled = preferences.createFlowPreference("core.compat.offloaded.batching.disabled", false)
|
||||||
val showAll = preferences.createFlowPreference(
|
val useIndirectScanResultCallback = preferences.createFlowPreference("core.compat.indirectcallback.enabled", false)
|
||||||
"core.showall.enabled",
|
|
||||||
false
|
|
||||||
)
|
|
||||||
|
|
||||||
val minimumSignalQuality = preferences.createFlowPreference(
|
|
||||||
"core.signal.minimum",
|
|
||||||
0.25f
|
|
||||||
)
|
|
||||||
|
|
||||||
val mainDeviceAddress = preferences.createFlowPreference<String?>(
|
|
||||||
"core.maindevice.address",
|
|
||||||
null
|
|
||||||
)
|
|
||||||
|
|
||||||
val mainDeviceModel = preferences.createFlowPreference<PodDevice.Model>(
|
|
||||||
"core.maindevice.model",
|
|
||||||
PodDevice.Model.UNKNOWN,
|
|
||||||
moshi
|
|
||||||
)
|
|
||||||
|
|
||||||
override val preferenceDataStore: PreferenceDataStore = PreferenceStoreMapper(
|
override val preferenceDataStore: PreferenceDataStore = PreferenceStoreMapper(
|
||||||
monitorMode,
|
monitorMode,
|
||||||
scannerMode,
|
scannerMode,
|
||||||
compatibilityMode,
|
|
||||||
showAll,
|
showAll,
|
||||||
minimumSignalQuality,
|
minimumSignalQuality,
|
||||||
mainDeviceAddress,
|
mainDeviceAddress,
|
||||||
|
isOffloadedFilteringDisabled,
|
||||||
|
isOffloadedBatchingDisabled,
|
||||||
|
useIndirectScanResultCallback,
|
||||||
debugSettings.isAutoReportingEnabled,
|
debugSettings.isAutoReportingEnabled,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import android.bluetooth.le.ScanFilter
|
|||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
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.bluetooth.ScannerMode
|
||||||
import eu.darken.capod.common.coroutine.AppScope
|
import eu.darken.capod.common.coroutine.AppScope
|
||||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||||
@@ -83,26 +84,50 @@ class PodMonitor @Inject constructor(
|
|||||||
.setupCommonEventHandlers(TAG) { "mainDevice" }
|
.setupCommonEventHandlers(TAG) { "mainDevice" }
|
||||||
.replayingShare(appScope)
|
.replayingShare(appScope)
|
||||||
|
|
||||||
|
private data class ScannerOptions(
|
||||||
|
val scannerMode: ScannerMode,
|
||||||
|
val showUnfiltered: Boolean,
|
||||||
|
val offloadedFilteringDisabled: Boolean,
|
||||||
|
val offloadedBatchingDisabled: Boolean,
|
||||||
|
val disableDirectCallback: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
private fun createBleScanner() = combine(
|
private fun createBleScanner() = combine(
|
||||||
generalSettings.scannerMode.flow,
|
generalSettings.scannerMode.flow,
|
||||||
generalSettings.compatibilityMode.flow,
|
debugSettings.showUnfiltered.flow,
|
||||||
debugSettings.showUnfiltered.flow
|
generalSettings.isOffloadedBatchingDisabled.flow,
|
||||||
) { scannerMode, compatMode, unfiltered ->
|
generalSettings.isOffloadedFilteringDisabled.flow,
|
||||||
Triple(scannerMode, compatMode, unfiltered)
|
generalSettings.useIndirectScanResultCallback.flow,
|
||||||
|
) {
|
||||||
|
scannermode,
|
||||||
|
showUnfiltered,
|
||||||
|
isOffloadedBatchingDisabled,
|
||||||
|
isOffloadedFilteringDisabled,
|
||||||
|
useIndirectScanResultCallback,
|
||||||
|
->
|
||||||
|
ScannerOptions(
|
||||||
|
scannerMode = scannermode,
|
||||||
|
showUnfiltered = showUnfiltered,
|
||||||
|
offloadedFilteringDisabled = isOffloadedFilteringDisabled,
|
||||||
|
offloadedBatchingDisabled = isOffloadedBatchingDisabled,
|
||||||
|
disableDirectCallback = useIndirectScanResultCallback,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.flatMapLatest { (mode, compat, unfiltered) ->
|
.flatMapLatest { options ->
|
||||||
val filters = when {
|
val filters = when {
|
||||||
unfiltered -> {
|
options.showUnfiltered -> {
|
||||||
log(TAG, WARN) { "Using unfiltered scan mode" }
|
log(TAG, WARN) { "Using unfiltered scan mode" }
|
||||||
setOf(getUnfilteredFilter())
|
setOf(ScanFilter.Builder().build())
|
||||||
}
|
}
|
||||||
else -> ProximityPairing.getBleScanFilter()
|
else -> ProximityPairing.getBleScanFilter()
|
||||||
}
|
}
|
||||||
|
|
||||||
bleScanner.scan(
|
bleScanner.scan(
|
||||||
filters = filters,
|
filters = filters,
|
||||||
scannerMode = mode,
|
scannerMode = options.scannerMode,
|
||||||
compatMode = compat,
|
disableOffloadFiltering = options.offloadedFilteringDisabled,
|
||||||
|
disableOffloadBatching = options.offloadedBatchingDisabled,
|
||||||
|
disableDirectScanCallback = options.disableDirectCallback,
|
||||||
).map { preFilterAndMap(it) }
|
).map { preFilterAndMap(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +159,7 @@ class PodMonitor @Inject constructor(
|
|||||||
return pods
|
return pods
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun preFilterAndMap(rawResults: List<BleScanResult>): List<PodFactory.Result> = rawResults
|
private suspend fun preFilterAndMap(rawResults: Collection<BleScanResult>): List<PodFactory.Result> = rawResults
|
||||||
.groupBy { it.address }
|
.groupBy { it.address }
|
||||||
.values
|
.values
|
||||||
.map { sameAdrDevs ->
|
.map { sameAdrDevs ->
|
||||||
@@ -187,10 +212,6 @@ class PodMonitor @Inject constructor(
|
|||||||
.also { log(TAG) { "Cached mainDevice is $it" } }
|
.also { log(TAG) { "Cached mainDevice is $it" } }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getUnfilteredFilter(): ScanFilter {
|
|
||||||
return ScanFilter.Builder().build()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = logTag("Monitor", "PodMonitor")
|
private val TAG = logTag("Monitor", "PodMonitor")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package eu.darken.capod.pods.core
|
package eu.darken.capod.pods.core
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import eu.darken.capod.common.R
|
||||||
|
|
||||||
interface DualPodDevice : PodDevice {
|
interface DualPodDevice : PodDevice {
|
||||||
|
|
||||||
enum class Pod {
|
enum class Pod {
|
||||||
@@ -10,4 +13,12 @@ interface DualPodDevice : PodDevice {
|
|||||||
val batteryLeftPodPercent: Float?
|
val batteryLeftPodPercent: Float?
|
||||||
|
|
||||||
val batteryRightPodPercent: Float?
|
val batteryRightPodPercent: Float?
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
val leftPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_gen1_left
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
val rightPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_gen1_right
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
package eu.darken.capod.pods.core
|
package eu.darken.capod.pods.core
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import eu.darken.capod.common.R
|
||||||
|
|
||||||
interface HasCase {
|
interface HasCase {
|
||||||
|
|
||||||
val batteryCasePercent: Float?
|
val batteryCasePercent: Float?
|
||||||
|
|
||||||
val isCaseCharging: Boolean
|
val isCaseCharging: Boolean
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
val caseIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_gen1_case
|
||||||
}
|
}
|
||||||
@@ -60,31 +60,31 @@ interface PodDevice {
|
|||||||
@JsonClass(generateAdapter = false)
|
@JsonClass(generateAdapter = false)
|
||||||
enum class Model(
|
enum class Model(
|
||||||
val label: String,
|
val label: String,
|
||||||
@DrawableRes val iconRes: Int = R.drawable.ic_device_generic_earbuds,
|
@DrawableRes val iconRes: Int = R.drawable.devic_earbuds_generic_both,
|
||||||
) {
|
) {
|
||||||
@Json(name = "airpods.gen1") AIRPODS_GEN1(
|
@Json(name = "airpods.gen1") AIRPODS_GEN1(
|
||||||
label = "AirPods (Gen 1)",
|
label = "AirPods (Gen 1)",
|
||||||
iconRes = R.drawable.ic_device_airpods_gen1,
|
iconRes = R.drawable.devic_airpods_gen1_both,
|
||||||
),
|
),
|
||||||
@Json(name = "airpods.gen2") AIRPODS_GEN2(
|
@Json(name = "airpods.gen2") AIRPODS_GEN2(
|
||||||
"AirPods (Gen 2)",
|
"AirPods (Gen 2)",
|
||||||
R.drawable.ic_device_airpods_gen2,
|
R.drawable.devic_airpods_gen2_both,
|
||||||
),
|
),
|
||||||
@Json(name = "airpods.gen3") AIRPODS_GEN3(
|
@Json(name = "airpods.gen3") AIRPODS_GEN3(
|
||||||
"AirPods (Gen 3)",
|
"AirPods (Gen 3)",
|
||||||
R.drawable.ic_device_airpods_gen2,
|
R.drawable.devic_airpods_gen2_both,
|
||||||
),
|
),
|
||||||
@Json(name = "airpods.pro") AIRPODS_PRO(
|
@Json(name = "airpods.pro") AIRPODS_PRO(
|
||||||
"AirPods Pro",
|
"AirPods Pro",
|
||||||
R.drawable.ic_device_airpods_gen2
|
R.drawable.devic_airpods_pro2_both
|
||||||
),
|
),
|
||||||
@Json(name = "airpods.pro2") AIRPODS_PRO2(
|
@Json(name = "airpods.pro2") AIRPODS_PRO2(
|
||||||
"AirPods Pro 2",
|
"AirPods Pro 2",
|
||||||
R.drawable.ic_device_airpods_gen2
|
R.drawable.devic_airpods_pro2_both
|
||||||
),
|
),
|
||||||
@Json(name = "airpods.max") AIRPODS_MAX(
|
@Json(name = "airpods.max") AIRPODS_MAX(
|
||||||
"AirPods Max",
|
"AirPods Max",
|
||||||
R.drawable.ic_device_generic_headphones
|
R.drawable.devic_headphones_generic
|
||||||
),
|
),
|
||||||
@Json(name = "beats.flex") BEATS_FLEX(
|
@Json(name = "beats.flex") BEATS_FLEX(
|
||||||
"Beats Flex"
|
"Beats Flex"
|
||||||
@@ -104,14 +104,28 @@ interface PodDevice {
|
|||||||
@Json(name = "beats.powerbeats.pro") POWERBEATS_PRO(
|
@Json(name = "beats.powerbeats.pro") POWERBEATS_PRO(
|
||||||
"Power Beats Pro"
|
"Power Beats Pro"
|
||||||
),
|
),
|
||||||
@Json(name = "fakes.tws.i99999") FAKE_AIRPODS_GEN1(
|
@Json(name = "beats.fit.pro") BEATS_FIT_PRO(
|
||||||
"AirPods (Gen 1)? \uD83C\uDFAD"
|
"Beats Fit Pro"
|
||||||
),
|
),
|
||||||
@Json(name = "fakes.varunr.airpodspro") FAKE_AIRPODS_PRO(
|
@Json(name = "fakes.tws.i99999") FAKE_AIRPODS_GEN1(
|
||||||
"AirPods Pro? \uD83C\uDFAD"
|
"AirPods (Gen 1)? \uD83C\uDFAD",
|
||||||
|
R.drawable.devic_airpods_gen1_both,
|
||||||
|
),
|
||||||
|
@Json(name = "fakes.generic.airpods.gen2") FAKE_AIRPODS_GEN2(
|
||||||
|
"AirPods (Gen 2)? \uD83C\uDFAD",
|
||||||
|
R.drawable.devic_airpods_gen2_both,
|
||||||
),
|
),
|
||||||
@Json(name = "fakes.generic.airpods.gen3") FAKE_AIRPODS_GEN3(
|
@Json(name = "fakes.generic.airpods.gen3") FAKE_AIRPODS_GEN3(
|
||||||
"AirPods (Gen 3)? \uD83C\uDFAD"
|
"AirPods (Gen 3)? \uD83C\uDFAD",
|
||||||
|
R.drawable.devic_airpods_gen2_both,
|
||||||
|
),
|
||||||
|
@Json(name = "fakes.varunr.airpodspro") FAKE_AIRPODS_PRO(
|
||||||
|
"AirPods Pro? \uD83C\uDFAD",
|
||||||
|
R.drawable.devic_airpods_pro2_both,
|
||||||
|
),
|
||||||
|
@Json(name = "fakes.generic.airpods.pro2") FAKE_AIRPODS_PRO2(
|
||||||
|
"AirPods Pro2? \uD83C\uDFAD",
|
||||||
|
R.drawable.devic_airpods_pro2_both,
|
||||||
),
|
),
|
||||||
@Json(name = "unknown") UNKNOWN(
|
@Json(name = "unknown") UNKNOWN(
|
||||||
"Unknown"
|
"Unknown"
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ import dagger.hilt.components.SingletonComponent
|
|||||||
import dagger.multibindings.IntoSet
|
import dagger.multibindings.IntoSet
|
||||||
import eu.darken.capod.pods.core.apple.airpods.*
|
import eu.darken.capod.pods.core.apple.airpods.*
|
||||||
import eu.darken.capod.pods.core.apple.beats.*
|
import eu.darken.capod.pods.core.apple.beats.*
|
||||||
import eu.darken.capod.pods.core.apple.misc.FakeAirPodsGen1
|
import eu.darken.capod.pods.core.apple.misc.*
|
||||||
import eu.darken.capod.pods.core.apple.misc.FakeAirPodsGen3
|
|
||||||
import eu.darken.capod.pods.core.apple.misc.FakeAirPodsPro
|
|
||||||
|
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
@Module
|
@Module
|
||||||
@@ -29,8 +27,11 @@ abstract class AppleFactoryModule {
|
|||||||
@Binds @IntoSet abstract fun beatsX(factory: BeatsX.Factory): ApplePodsFactory<out ApplePods>
|
@Binds @IntoSet abstract fun beatsX(factory: BeatsX.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun powerBeats3(factory: PowerBeats3.Factory): ApplePodsFactory<out ApplePods>
|
@Binds @IntoSet abstract fun powerBeats3(factory: PowerBeats3.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePodsFactory<out ApplePods>
|
@Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePodsFactory<out ApplePods>
|
||||||
|
@Binds @IntoSet abstract fun beatsFitPro(factory: BeatsFitPro.Factory): ApplePodsFactory<out ApplePods>
|
||||||
|
|
||||||
@Binds @IntoSet abstract fun fakeAirPodsGen1(factory: FakeAirPodsGen1.Factory): ApplePodsFactory<out ApplePods>
|
@Binds @IntoSet abstract fun fakeAirPodsGen1(factory: FakeAirPodsGen1.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun fakeAirPodsPro(factory: FakeAirPodsPro.Factory): ApplePodsFactory<out ApplePods>
|
@Binds @IntoSet abstract fun fakeAirPodsGen2(factory: FakeAirPodsGen2.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun fakeAirPodsGen3(factory: FakeAirPodsGen3.Factory): ApplePodsFactory<out ApplePods>
|
@Binds @IntoSet abstract fun fakeAirPodsGen3(factory: FakeAirPodsGen3.Factory): ApplePodsFactory<out ApplePods>
|
||||||
|
@Binds @IntoSet abstract fun fakeAirPodsPro(factory: FakeAirPodsPro.Factory): ApplePodsFactory<out ApplePods>
|
||||||
|
@Binds @IntoSet abstract fun fakeAirPodsPro2(factory: FakeAirPodsPro2.Factory): ApplePodsFactory<out ApplePods>
|
||||||
}
|
}
|
||||||
@@ -79,13 +79,13 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
|||||||
.mapNotNull { it.batteryCasePercent }
|
.mapNotNull { it.batteryCasePercent }
|
||||||
.lastOrNull()
|
.lastOrNull()
|
||||||
|
|
||||||
fun KnownDevice.getLatestCaseLidState(basic: DualAirPods): DualAirPods.LidState? {
|
fun KnownDevice.getLatestCaseLidState(basic: DualApplePods): DualApplePods.LidState? {
|
||||||
val definitive = setOf(DualAirPods.LidState.OPEN, DualAirPods.LidState.CLOSED)
|
val definitive = setOf(DualApplePods.LidState.OPEN, DualApplePods.LidState.CLOSED)
|
||||||
if (definitive.contains(basic.caseLidState)) return null
|
if (definitive.contains(basic.caseLidState)) return null
|
||||||
|
|
||||||
return history
|
return history
|
||||||
.filterIsInstance<AirPodsPro>() // TODO why is this AirPodsPro specific here?
|
.filterIsInstance<AirPodsPro>() // TODO why is this AirPodsPro specific here?
|
||||||
.lastOrNull { it.caseLidState != DualAirPods.LidState.NOT_IN_CASE }
|
.lastOrNull { it.caseLidState != DualApplePods.LidState.NOT_IN_CASE }
|
||||||
?.caseLidState
|
?.caseLidState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
package eu.darken.capod.pods.core.apple
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import eu.darken.capod.common.R
|
|
||||||
import eu.darken.capod.common.debug.logging.log
|
import eu.darken.capod.common.debug.logging.log
|
||||||
import eu.darken.capod.common.isBitSet
|
import eu.darken.capod.common.isBitSet
|
||||||
import eu.darken.capod.common.lowerNibble
|
import eu.darken.capod.common.lowerNibble
|
||||||
@@ -10,8 +7,8 @@ import eu.darken.capod.common.upperNibble
|
|||||||
import eu.darken.capod.pods.core.*
|
import eu.darken.capod.pods.core.*
|
||||||
import eu.darken.capod.pods.core.DualPodDevice.Pod
|
import eu.darken.capod.pods.core.DualPodDevice.Pod
|
||||||
|
|
||||||
interface DualAirPods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase,
|
interface DualApplePods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase,
|
||||||
HasStateDetection, HasDualMicrophone, HasAppleColor {
|
HasDualMicrophone, HasAppleColor {
|
||||||
|
|
||||||
val primaryPod: Pod
|
val primaryPod: Pod
|
||||||
get() = when (rawStatus.isBitSet(5)) {
|
get() = when (rawStatus.isBitSet(5)) {
|
||||||
@@ -142,21 +139,4 @@ interface DualAirPods : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEar
|
|||||||
UNKNOWN(0xFF..0xFF);
|
UNKNOWN(0xFF..0xFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
override val state: ConnectionState
|
|
||||||
get() = ConnectionState.values().firstOrNull { rawSuffix == it.raw } ?: ConnectionState.UNKNOWN
|
|
||||||
|
|
||||||
enum class ConnectionState(val raw: UByte?, @StringRes val labelRes: Int) : HasStateDetection.State {
|
|
||||||
DISCONNECTED(0x00, R.string.pods_connection_state_disconnected_label),
|
|
||||||
IDLE(0x04, R.string.pods_connection_state_idle_label),
|
|
||||||
MUSIC(0x05, R.string.pods_connection_state_music_label),
|
|
||||||
CALL(0x06, R.string.pods_connection_state_call_label),
|
|
||||||
RINGING(0x07, R.string.pods_connection_state_ringing_label),
|
|
||||||
HANGING_UP(0x09, R.string.pods_connection_state_hanging_up_label),
|
|
||||||
UNKNOWN(null, R.string.pods_connection_state_unknown_label);
|
|
||||||
|
|
||||||
override fun getLabel(context: Context): String = context.getString(labelRes)
|
|
||||||
|
|
||||||
constructor(raw: Int, @StringRes labelRes: Int) : this(raw.toUByte(), labelRes)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,9 +4,9 @@ import eu.darken.capod.common.debug.logging.Logging.Priority.DEBUG
|
|||||||
import eu.darken.capod.common.debug.logging.log
|
import eu.darken.capod.common.debug.logging.log
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
|
|
||||||
abstract class DualApplePodsFactory(private val tag: String) : ApplePodsFactory<DualAirPods>(tag) {
|
abstract class DualApplePodsFactory(private val tag: String) : ApplePodsFactory<DualApplePods>(tag) {
|
||||||
|
|
||||||
fun DualAirPods.getCaseMatchMarkings() = SplitPodsMarkings(
|
fun DualApplePods.getCaseMatchMarkings() = SplitPodsMarkings(
|
||||||
leftPodBattery = batteryLeftPodPercent,
|
leftPodBattery = batteryLeftPodPercent,
|
||||||
rightPodBattery = batteryRightPodPercent,
|
rightPodBattery = batteryRightPodPercent,
|
||||||
microPhoneLeft = isLeftPodMicrophone,
|
microPhoneLeft = isLeftPodMicrophone,
|
||||||
@@ -31,17 +31,17 @@ abstract class DualApplePodsFactory(private val tag: String) : ApplePodsFactory<
|
|||||||
val model: PodDevice.Model,
|
val model: PodDevice.Model,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun Collection<KnownDevice>.findSplitPodsMatch(device: DualAirPods): Collection<KnownDevice> {
|
private fun Collection<KnownDevice>.findSplitPodsMatch(device: DualApplePods): Collection<KnownDevice> {
|
||||||
val target = device.getCaseMatchMarkings()
|
val target = device.getCaseMatchMarkings()
|
||||||
|
|
||||||
return filter { known ->
|
return filter { known ->
|
||||||
known.history
|
known.history
|
||||||
.filterIsInstance<DualAirPods>()
|
.filterIsInstance<DualApplePods>()
|
||||||
.any { it.getCaseMatchMarkings() == target }
|
.any { it.getCaseMatchMarkings() == target }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchHistory(current: DualAirPods): KnownDevice? {
|
override fun searchHistory(current: DualApplePods): KnownDevice? {
|
||||||
val basicResult = super.searchHistory(current)
|
val basicResult = super.searchHistory(current)
|
||||||
|
|
||||||
val caseIgnored = knownDevices.values.findSplitPodsMatch(current)
|
val caseIgnored = knownDevices.values.findSplitPodsMatch(current)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
|
|||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -20,15 +20,15 @@ data class AirPodsGen1 constructor(
|
|||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: DualAirPods.LidState? = null
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
) : DualAirPods {
|
) : DualApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN1
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN1
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
override val caseLidState: DualAirPods.LidState
|
override val caseLidState: DualApplePods.LidState
|
||||||
get() = cachedCaseState ?: super.caseLidState
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
|
|||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -20,19 +20,19 @@ data class AirPodsGen2 constructor(
|
|||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: DualAirPods.LidState? = null
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
) : DualAirPods {
|
) : DualApplePods, HasStateDetectionAirPods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN2
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN2
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
override val caseLidState: DualAirPods.LidState
|
override val caseLidState: DualApplePods.LidState
|
||||||
get() = cachedCaseState ?: super.caseLidState
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
get() = rssiAverage ?: super.rssi
|
get() = rssiAverage ?: super<DualApplePods>.rssi
|
||||||
|
|
||||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
|
|||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -20,18 +20,19 @@ data class AirPodsGen3 constructor(
|
|||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: DualAirPods.LidState? = null
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
) : DualAirPods {
|
) : DualApplePods, HasStateDetectionAirPods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN3
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN3
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
override val caseLidState: DualAirPods.LidState
|
override val caseLidState: DualApplePods.LidState
|
||||||
get() = cachedCaseState ?: super.caseLidState
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
get() = rssiAverage ?: super.rssi
|
get() = rssiAverage ?: super<DualApplePods>.rssi
|
||||||
|
|
||||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
|
|||||||
@@ -21,12 +21,12 @@ data class AirPodsMax(
|
|||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
) : SingleApplePods, HasEarDetection {
|
) : SingleApplePods, HasEarDetection, HasStateDetectionAirPods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
get() = rssiAverage ?: super.rssi
|
get() = rssiAverage ?: super<SingleApplePods>.rssi
|
||||||
|
|
||||||
val isHeadphonesBeingWorn: Boolean
|
val isHeadphonesBeingWorn: Boolean
|
||||||
get() = rawStatus.isBitSet(1)
|
get() = rawStatus.isBitSet(1)
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package eu.darken.capod.pods.core.apple.airpods
|
package eu.darken.capod.pods.core.apple.airpods
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import eu.darken.capod.common.R
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods.LidState
|
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -22,10 +24,26 @@ data class AirPodsPro(
|
|||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: LidState? = null
|
private val cachedCaseState: LidState? = null
|
||||||
) : DualAirPods {
|
) : DualApplePods, HasStateDetectionAirPods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val iconRes: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_both
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val caseIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_case
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val leftPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_left
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val rightPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_right
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
@@ -33,7 +51,7 @@ data class AirPodsPro(
|
|||||||
get() = cachedCaseState ?: super.caseLidState
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
get() = rssiAverage ?: super.rssi
|
get() = rssiAverage ?: super<DualApplePods>.rssi
|
||||||
|
|
||||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package eu.darken.capod.pods.core.apple.airpods
|
package eu.darken.capod.pods.core.apple.airpods
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import eu.darken.capod.common.R
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods.LidState
|
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -22,10 +24,26 @@ data class AirPodsPro2(
|
|||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: LidState? = null
|
private val cachedCaseState: LidState? = null
|
||||||
) : DualAirPods {
|
) : DualApplePods, HasStateDetectionAirPods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO2
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO2
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val iconRes: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_both
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val caseIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_case
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val leftPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_left
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val rightPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_right
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
@@ -33,7 +51,7 @@ data class AirPodsPro2(
|
|||||||
get() = cachedCaseState ?: super.caseLidState
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
get() = rssiAverage ?: super.rssi
|
get() = rssiAverage ?: super<HasStateDetectionAirPods>.rssi
|
||||||
|
|
||||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple.airpods
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import eu.darken.capod.common.R
|
||||||
|
import eu.darken.capod.pods.core.HasStateDetection
|
||||||
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
|
|
||||||
|
interface HasStateDetectionAirPods : HasStateDetection, ApplePods {
|
||||||
|
|
||||||
|
override val state: ConnectionState
|
||||||
|
get() = ConnectionState.values().firstOrNull { rawSuffix == it.raw } ?: ConnectionState.UNKNOWN
|
||||||
|
|
||||||
|
enum class ConnectionState(val raw: UByte?, @StringRes val labelRes: Int) : HasStateDetection.State {
|
||||||
|
DISCONNECTED(0x00, R.string.pods_connection_state_disconnected_label),
|
||||||
|
IDLE(0x04, R.string.pods_connection_state_idle_label),
|
||||||
|
MUSIC(0x05, R.string.pods_connection_state_music_label),
|
||||||
|
CALL(0x06, R.string.pods_connection_state_call_label),
|
||||||
|
RINGING(0x07, R.string.pods_connection_state_ringing_label),
|
||||||
|
HANGING_UP(0x09, R.string.pods_connection_state_hanging_up_label),
|
||||||
|
UNKNOWN(null, R.string.pods_connection_state_unknown_label);
|
||||||
|
|
||||||
|
override fun getLabel(context: Context): String = context.getString(labelRes)
|
||||||
|
|
||||||
|
constructor(raw: Int, @StringRes labelRes: Int) : this(raw.toUByte(), labelRes)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple.beats
|
||||||
|
|
||||||
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
|
import java.time.Instant
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
data class BeatsFitPro(
|
||||||
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
|
override val seenCounter: Int = 1,
|
||||||
|
override val scanResult: BleScanResult,
|
||||||
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
|
) : DualApplePods {
|
||||||
|
|
||||||
|
override val model: PodDevice.Model = PodDevice.Model.BEATS_FIT_PRO
|
||||||
|
|
||||||
|
override val batteryCasePercent: Float?
|
||||||
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
|
override val caseLidState: DualApplePods.LidState
|
||||||
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
|
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||||
|
getModelInfo().full == DEVICE_CODE && length == ProximityPairing.PAIRING_MESSAGE_LENGTH
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||||
|
var basic = BeatsFitPro(scanResult = scanResult, proximityMessage = message)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
|
updateHistory(basic)
|
||||||
|
|
||||||
|
if (result == null) return basic
|
||||||
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenLastAt = scanResult.receivedAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
|
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DEVICE_CODE = 0x1220.toUShort()
|
||||||
|
private val TAG = logTag("PodDevice", "Beats", "Fit", "Pro")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
|
|||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -20,15 +20,15 @@ data class PowerBeatsPro(
|
|||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
private val cachedCaseState: DualAirPods.LidState? = null
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
) : DualAirPods {
|
) : DualApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_PRO
|
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_PRO
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
override val caseLidState: DualAirPods.LidState
|
override val caseLidState: DualApplePods.LidState
|
||||||
get() = cachedCaseState ?: super.caseLidState
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.common.isBitSet
|
import eu.darken.capod.common.isBitSet
|
||||||
import eu.darken.capod.common.lowerNibble
|
import eu.darken.capod.common.lowerNibble
|
||||||
import eu.darken.capod.common.upperNibble
|
import eu.darken.capod.common.upperNibble
|
||||||
import eu.darken.capod.pods.core.DualPodDevice
|
import eu.darken.capod.pods.core.*
|
||||||
import eu.darken.capod.pods.core.HasCase
|
|
||||||
import eu.darken.capod.pods.core.HasDualMicrophone
|
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
@@ -31,7 +28,7 @@ data class FakeAirPodsGen1 constructor(
|
|||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
) : ApplePods, DualPodDevice, HasDualMicrophone, HasCase {
|
) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN1
|
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN1
|
||||||
|
|
||||||
@@ -79,22 +76,32 @@ data class FakeAirPodsGen1 constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val isThisPodInThecase: Boolean
|
private val isThisPodInThecase: Boolean
|
||||||
get() = rawStatus.isBitSet(6)
|
get() = rawStatus.isBitSet(6)
|
||||||
|
|
||||||
/**
|
override val isLeftPodInEar: Boolean
|
||||||
* The data flip bit is set if the left pod is primary.
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
* For the pod that is in the case, this is flipped again though.
|
true -> rawStatus.isBitSet(3)
|
||||||
*/
|
false -> rawStatus.isBitSet(1)
|
||||||
override val isLeftPodMicrophone: Boolean
|
}
|
||||||
get() = rawStatus.isBitSet(5) xor isThisPodInThecase
|
|
||||||
|
|
||||||
/**
|
override val isRightPodInEar: Boolean
|
||||||
* The data flip bit is UNset if the right pod is primary.
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
* For the pod that is in the case, this is flipped again though.
|
true -> rawStatus.isBitSet(1)
|
||||||
*/
|
false -> rawStatus.isBitSet(3)
|
||||||
override val isRightPodMicrophone: Boolean
|
}
|
||||||
get() = !rawStatus.isBitSet(5) xor isThisPodInThecase
|
|
||||||
|
override val isLeftPodCharging: Boolean
|
||||||
|
get() = when (areValuesFlipped) {
|
||||||
|
false -> rawFlags.isBitSet(0)
|
||||||
|
true -> rawFlags.isBitSet(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isRightPodCharging: Boolean
|
||||||
|
get() = when (areValuesFlipped) {
|
||||||
|
false -> rawFlags.isBitSet(1)
|
||||||
|
true -> rawFlags.isBitSet(0)
|
||||||
|
}
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = when (val value = rawCaseBattery.toInt()) {
|
get() = when (val value = rawCaseBattery.toInt()) {
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple.misc
|
||||||
|
|
||||||
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
|
import eu.darken.capod.common.isBitSet
|
||||||
|
import eu.darken.capod.common.lowerNibble
|
||||||
|
import eu.darken.capod.common.upperNibble
|
||||||
|
import eu.darken.capod.pods.core.*
|
||||||
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
|
import java.time.Instant
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar data structure but a lot of placeholder values or hardcoded values
|
||||||
|
*/
|
||||||
|
data class FakeAirPodsGen2 constructor(
|
||||||
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
|
override val seenCounter: Int = 1,
|
||||||
|
override val scanResult: BleScanResult,
|
||||||
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
||||||
|
|
||||||
|
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN2
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super<ApplePods>.rssi
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normally values for the left pod are in the lower nibbles, if the left pod is primary (microphone)
|
||||||
|
* If the right pod is the primary, the values are flipped.
|
||||||
|
*/
|
||||||
|
val areValuesFlipped: Boolean
|
||||||
|
get() = !rawStatus.isBitSet(5)
|
||||||
|
|
||||||
|
override val batteryLeftPodPercent: Float?
|
||||||
|
get() {
|
||||||
|
val value = when (areValuesFlipped) {
|
||||||
|
true -> rawPodsBattery.upperNibble.toInt()
|
||||||
|
false -> rawPodsBattery.lowerNibble.toInt()
|
||||||
|
}
|
||||||
|
return when (value) {
|
||||||
|
15 -> null
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Left pod: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
(value / 10f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val batteryRightPodPercent: Float?
|
||||||
|
get() {
|
||||||
|
val value = when (areValuesFlipped) {
|
||||||
|
true -> rawPodsBattery.lowerNibble.toInt()
|
||||||
|
false -> rawPodsBattery.upperNibble.toInt()
|
||||||
|
}
|
||||||
|
return when (value) {
|
||||||
|
15 -> null
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Right pod: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
value / 10f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val isThisPodInThecase: Boolean
|
||||||
|
get() = rawStatus.isBitSet(6)
|
||||||
|
|
||||||
|
override val isLeftPodInEar: Boolean
|
||||||
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
|
true -> rawStatus.isBitSet(3)
|
||||||
|
false -> rawStatus.isBitSet(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isRightPodInEar: Boolean
|
||||||
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
|
true -> rawStatus.isBitSet(1)
|
||||||
|
false -> rawStatus.isBitSet(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isLeftPodCharging: Boolean
|
||||||
|
get() = when (areValuesFlipped) {
|
||||||
|
false -> rawFlags.isBitSet(0)
|
||||||
|
true -> rawFlags.isBitSet(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isRightPodCharging: Boolean
|
||||||
|
get() = when (areValuesFlipped) {
|
||||||
|
false -> rawFlags.isBitSet(1)
|
||||||
|
true -> rawFlags.isBitSet(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val batteryCasePercent: Float?
|
||||||
|
get() = when (val value = rawCaseBattery.toInt()) {
|
||||||
|
15 -> cachedBatteryPercentage
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Case: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
value / 10f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isCaseCharging: Boolean
|
||||||
|
get() = rawFlags.isBitSet(2)
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : ApplePodsFactory<FakeAirPodsGen2>(TAG) {
|
||||||
|
|
||||||
|
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||||
|
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
|
||||||
|
getModelInfo().full == DEVICE_CODE && length == 19
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||||
|
var basic = FakeAirPodsGen2(scanResult = scanResult, proximityMessage = message)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
|
updateHistory(basic)
|
||||||
|
|
||||||
|
if (result == null) return basic
|
||||||
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenLastAt = scanResult.receivedAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DEVICE_CODE = 0x0F20.toUShort()
|
||||||
|
private val TAG = logTag("PodDevice", "Apple", "Fake", "AirPods", "Gen2")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,7 +28,7 @@ data class FakeAirPodsGen3 constructor(
|
|||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
) : ApplePods, DualPodDevice, HasDualMicrophone, HasCase, HasChargeDetectionDual {
|
) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN3
|
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN3
|
||||||
|
|
||||||
@@ -88,22 +88,20 @@ data class FakeAirPodsGen3 constructor(
|
|||||||
true -> rawFlags.isBitSet(0)
|
true -> rawFlags.isBitSet(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
val isThisPodInThecase: Boolean
|
private val isThisPodInThecase: Boolean
|
||||||
get() = rawStatus.isBitSet(6)
|
get() = rawStatus.isBitSet(6)
|
||||||
|
|
||||||
/**
|
override val isLeftPodInEar: Boolean
|
||||||
* The data flip bit is set if the left pod is primary.
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
* For the pod that is in the case, this is flipped again though.
|
true -> rawStatus.isBitSet(3)
|
||||||
*/
|
false -> rawStatus.isBitSet(1)
|
||||||
override val isLeftPodMicrophone: Boolean
|
}
|
||||||
get() = rawStatus.isBitSet(5) xor isThisPodInThecase
|
|
||||||
|
|
||||||
/**
|
override val isRightPodInEar: Boolean
|
||||||
* The data flip bit is UNset if the right pod is primary.
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
* For the pod that is in the case, this is flipped again though.
|
true -> rawStatus.isBitSet(1)
|
||||||
*/
|
false -> rawStatus.isBitSet(3)
|
||||||
override val isRightPodMicrophone: Boolean
|
}
|
||||||
get() = !rawStatus.isBitSet(5) xor isThisPodInThecase
|
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = when (val value = rawCaseBattery.toInt()) {
|
get() = when (val value = rawCaseBattery.toInt()) {
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
package eu.darken.capod.pods.core.apple.misc
|
package eu.darken.capod.pods.core.apple.misc
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import eu.darken.capod.common.R
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
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.isBitSet
|
import eu.darken.capod.common.isBitSet
|
||||||
import eu.darken.capod.common.lowerNibble
|
import eu.darken.capod.common.lowerNibble
|
||||||
import eu.darken.capod.common.upperNibble
|
import eu.darken.capod.common.upperNibble
|
||||||
import eu.darken.capod.pods.core.DualPodDevice
|
import eu.darken.capod.pods.core.*
|
||||||
import eu.darken.capod.pods.core.HasCase
|
|
||||||
import eu.darken.capod.pods.core.HasDualMicrophone
|
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
@@ -31,10 +30,26 @@ data class FakeAirPodsPro constructor(
|
|||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
) : ApplePods, DualPodDevice, HasDualMicrophone, HasCase {
|
) : ApplePods, DualPodDevice, HasChargeDetectionDual, HasEarDetectionDual, HasCase {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_PRO
|
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_PRO
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val iconRes: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_both
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val caseIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_case
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val leftPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_left
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val rightPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_right
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
get() = rssiAverage ?: super<ApplePods>.rssi
|
get() = rssiAverage ?: super<ApplePods>.rssi
|
||||||
|
|
||||||
@@ -79,22 +94,32 @@ data class FakeAirPodsPro constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val isThisPodInThecase: Boolean
|
override val isLeftPodCharging: Boolean
|
||||||
|
get() = when (areValuesFlipped) {
|
||||||
|
false -> rawFlags.isBitSet(0)
|
||||||
|
true -> rawFlags.isBitSet(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isRightPodCharging: Boolean
|
||||||
|
get() = when (areValuesFlipped) {
|
||||||
|
false -> rawFlags.isBitSet(1)
|
||||||
|
true -> rawFlags.isBitSet(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val isThisPodInThecase: Boolean
|
||||||
get() = rawStatus.isBitSet(6)
|
get() = rawStatus.isBitSet(6)
|
||||||
|
|
||||||
/**
|
override val isLeftPodInEar: Boolean
|
||||||
* The data flip bit is set if the left pod is primary.
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
* For the pod that is in the case, this is flipped again though.
|
true -> rawStatus.isBitSet(3)
|
||||||
*/
|
false -> rawStatus.isBitSet(1)
|
||||||
override val isLeftPodMicrophone: Boolean
|
}
|
||||||
get() = rawStatus.isBitSet(5) xor isThisPodInThecase
|
|
||||||
|
|
||||||
/**
|
override val isRightPodInEar: Boolean
|
||||||
* The data flip bit is UNset if the right pod is primary.
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
* For the pod that is in the case, this is flipped again though.
|
true -> rawStatus.isBitSet(1)
|
||||||
*/
|
false -> rawStatus.isBitSet(3)
|
||||||
override val isRightPodMicrophone: Boolean
|
}
|
||||||
get() = !rawStatus.isBitSet(5) xor isThisPodInThecase
|
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = when (val value = rawCaseBattery.toInt()) {
|
get() = when (val value = rawCaseBattery.toInt()) {
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple.misc
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import eu.darken.capod.common.R
|
||||||
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
|
import eu.darken.capod.common.isBitSet
|
||||||
|
import eu.darken.capod.common.lowerNibble
|
||||||
|
import eu.darken.capod.common.upperNibble
|
||||||
|
import eu.darken.capod.pods.core.*
|
||||||
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
|
import java.time.Instant
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AirPods Pro2 clone
|
||||||
|
* https://discord.com/channels/548521543039189022/927235844127993866/1060911213539774504
|
||||||
|
*/
|
||||||
|
data class FakeAirPodsPro2 constructor(
|
||||||
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
|
override val seenCounter: Int = 1,
|
||||||
|
override val scanResult: BleScanResult,
|
||||||
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
private val cachedCaseState: DualApplePods.LidState? = null,
|
||||||
|
) : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase {
|
||||||
|
|
||||||
|
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_PRO2
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val iconRes: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_both
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val caseIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_case
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val leftPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_left
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val rightPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_right
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: scanResult.rssi
|
||||||
|
|
||||||
|
private val areValuesFlipped: Boolean
|
||||||
|
get() = !rawStatus.isBitSet(5)
|
||||||
|
|
||||||
|
override val batteryLeftPodPercent: Float?
|
||||||
|
get() {
|
||||||
|
val value = when (areValuesFlipped) {
|
||||||
|
true -> rawPodsBattery.upperNibble.toInt()
|
||||||
|
false -> rawPodsBattery.lowerNibble.toInt()
|
||||||
|
}
|
||||||
|
return when (value) {
|
||||||
|
15 -> null
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Left pod: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
(value / 10f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val batteryRightPodPercent: Float?
|
||||||
|
get() {
|
||||||
|
val value = when (areValuesFlipped) {
|
||||||
|
true -> rawPodsBattery.lowerNibble.toInt()
|
||||||
|
false -> rawPodsBattery.upperNibble.toInt()
|
||||||
|
}
|
||||||
|
return when (value) {
|
||||||
|
15 -> null
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Right pod: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
value / 10f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val isThisPodInThecase: Boolean
|
||||||
|
get() = rawStatus.isBitSet(6)
|
||||||
|
|
||||||
|
override val isLeftPodInEar: Boolean
|
||||||
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
|
true -> rawStatus.isBitSet(3)
|
||||||
|
false -> rawStatus.isBitSet(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isRightPodInEar: Boolean
|
||||||
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
|
true -> rawStatus.isBitSet(1)
|
||||||
|
false -> rawStatus.isBitSet(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isLeftPodCharging: Boolean
|
||||||
|
get() = when (areValuesFlipped) {
|
||||||
|
false -> rawFlags.isBitSet(0)
|
||||||
|
true -> rawFlags.isBitSet(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isRightPodCharging: Boolean
|
||||||
|
get() = when (areValuesFlipped) {
|
||||||
|
false -> rawFlags.isBitSet(1)
|
||||||
|
true -> rawFlags.isBitSet(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val batteryCasePercent: Float?
|
||||||
|
get() = when (val value = rawCaseBattery.toInt()) {
|
||||||
|
15 -> cachedBatteryPercentage
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Case: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
value / 10f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isCaseCharging: Boolean
|
||||||
|
get() = rawFlags.isBitSet(2)
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : ApplePodsFactory<FakeAirPodsPro2>(TAG) {
|
||||||
|
|
||||||
|
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||||
|
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
|
||||||
|
getModelInfo().full == DEVICE_CODE && length == 19
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||||
|
var basic = FakeAirPodsPro2(scanResult = scanResult, proximityMessage = message)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
|
updateHistory(basic)
|
||||||
|
|
||||||
|
if (result == null) return basic
|
||||||
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenLastAt = scanResult.receivedAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
confidence = result.confidence,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DEVICE_CODE = 0x1420.toUShort()
|
||||||
|
private val TAG = logTag("PodDevice", "Apple", "Fake", "AirPods", "Pro2")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ 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.HasEarDetection
|
import eu.darken.capod.pods.core.HasEarDetection
|
||||||
import eu.darken.capod.pods.core.HasEarDetectionDual
|
import eu.darken.capod.pods.core.HasEarDetectionDual
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.reaction.core.ReactionSettings
|
import eu.darken.capod.reaction.core.ReactionSettings
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -69,7 +69,7 @@ class AutoConnect @Inject constructor(
|
|||||||
val conditionFulfilled = when (condition) {
|
val conditionFulfilled = when (condition) {
|
||||||
AutoConnectCondition.WHEN_SEEN -> true
|
AutoConnectCondition.WHEN_SEEN -> true
|
||||||
AutoConnectCondition.CASE_OPEN -> when (mainDevice) {
|
AutoConnectCondition.CASE_OPEN -> when (mainDevice) {
|
||||||
is DualAirPods -> mainDevice.caseLidState == DualAirPods.LidState.OPEN
|
is DualApplePods -> mainDevice.caseLidState == DualApplePods.LidState.OPEN
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
AutoConnectCondition.IN_EAR -> when (mainDevice) {
|
AutoConnectCondition.IN_EAR -> when (mainDevice) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package eu.darken.capod.reaction.core.playpause
|
|||||||
import eu.darken.capod.common.MediaControl
|
import eu.darken.capod.common.MediaControl
|
||||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||||
|
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||||
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
|
||||||
@@ -40,46 +41,51 @@ class PlayPause @Inject constructor(
|
|||||||
}
|
}
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.withPrevious()
|
.withPrevious()
|
||||||
|
.filter { (previous, current) ->
|
||||||
|
if (previous == null || current == null) return@filter false
|
||||||
|
log(TAG, VERBOSE) { "previous-id=${previous.identifier}, current-id=${current.identifier}" }
|
||||||
|
val match = previous.identifier == current.identifier
|
||||||
|
if (!match) log(TAG, WARN) { "Main device switched, skipping reaction." }
|
||||||
|
match
|
||||||
|
}
|
||||||
.onEach { (previous, current) ->
|
.onEach { (previous, current) ->
|
||||||
if (previous is HasEarDetection && current is HasEarDetection) {
|
log(TAG, VERBOSE) { "Checking\nprevious=$previous\ncurrent=$current" }
|
||||||
log(TAG, VERBOSE) { "previous=${previous.isBeingWorn}, current=${current.isBeingWorn}" }
|
|
||||||
log(TAG, VERBOSE) { "previous-id=${previous.identifier}, current-id=${current.identifier}" }
|
|
||||||
if (previous.identifier != current.identifier) {
|
|
||||||
return@onEach
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previous is HasEarDetectionDual && current is HasEarDetectionDual && reactionSettings.onePodMode.value) {
|
val previousWorn: Boolean?
|
||||||
log(TAG) { "Ear status changed for dual pod device in single pod mode." }
|
val currentWorn: Boolean?
|
||||||
val previousWorn = previous.isEitherPodInEar
|
|
||||||
val currentWorn = current.isEitherPodInEar
|
when {
|
||||||
if (!previousWorn && currentWorn && !mediaControl.isPlaying) {
|
reactionSettings.onePodMode.value && previous is HasEarDetectionDual && current is HasEarDetectionDual -> {
|
||||||
if (reactionSettings.autoPlay.value) {
|
previousWorn = previous.isEitherPodInEar
|
||||||
mediaControl.sendPlay()
|
currentWorn = current.isEitherPodInEar
|
||||||
} else {
|
log(TAG, VERBOSE) { "previous: left=${previous.isLeftPodInEar}, right=${previous.isRightPodInEar}" }
|
||||||
log(TAG) { "autoPlay is disabled" }
|
log(TAG, VERBOSE) { "current: left${current.isLeftPodInEar}, right=${current.isRightPodInEar}" }
|
||||||
}
|
}
|
||||||
} else if (!currentWorn && previousWorn && mediaControl.isPlaying) {
|
previous is HasEarDetection && current is HasEarDetection -> {
|
||||||
if (reactionSettings.autoPause.value) {
|
previousWorn = previous.isBeingWorn
|
||||||
mediaControl.sendPause()
|
currentWorn = current.isBeingWorn
|
||||||
} else {
|
log(TAG, VERBOSE) { "prev.isBeingWorn=${previousWorn}, cur.isBeingWorn=${currentWorn}" }
|
||||||
log(TAG) { "autoPause is disabled" }
|
}
|
||||||
}
|
else -> {
|
||||||
}
|
log(TAG, VERBOSE) { "Current devices don't support ear detection." }
|
||||||
} else if (previous.isBeingWorn != current.isBeingWorn) {
|
previousWorn = null
|
||||||
log(TAG) { "Ear status changed for monitored device." }
|
currentWorn = null
|
||||||
if (current.isBeingWorn && !mediaControl.isPlaying) {
|
}
|
||||||
if (reactionSettings.autoPlay.value) {
|
}
|
||||||
mediaControl.sendPlay()
|
|
||||||
} else {
|
if (previousWorn == false && currentWorn == true && !mediaControl.isPlaying) {
|
||||||
log(TAG) { "autoPlay is disabled" }
|
if (reactionSettings.autoPlay.value) {
|
||||||
}
|
log(TAG) { "autoPlay is triggered, sendPlay()" }
|
||||||
} else if (!current.isBeingWorn && mediaControl.isPlaying) {
|
mediaControl.sendPlay()
|
||||||
if (reactionSettings.autoPause.value) {
|
} else {
|
||||||
mediaControl.sendPause()
|
log(TAG, VERBOSE) { "autoPlay is disabled" }
|
||||||
} else {
|
}
|
||||||
log(TAG) { "autoPause is disabled" }
|
} else if (previousWorn == true && currentWorn == false && mediaControl.isPlaying) {
|
||||||
}
|
if (reactionSettings.autoPause.value) {
|
||||||
}
|
log(TAG) { "autoPause is triggered, sendPause()" }
|
||||||
|
mediaControl.sendPause()
|
||||||
|
} else {
|
||||||
|
log(TAG) { "autoPause is disabled" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import eu.darken.capod.common.flow.setupCommonEventHandlers
|
|||||||
import eu.darken.capod.common.flow.withPrevious
|
import eu.darken.capod.common.flow.withPrevious
|
||||||
import eu.darken.capod.monitor.core.PodMonitor
|
import eu.darken.capod.monitor.core.PodMonitor
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.reaction.core.ReactionSettings
|
import eu.darken.capod.reaction.core.ReactionSettings
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
@@ -34,7 +34,7 @@ class PopUpReaction @Inject constructor(
|
|||||||
.withPrevious()
|
.withPrevious()
|
||||||
.setupCommonEventHandlers(TAG) { "monitor" }
|
.setupCommonEventHandlers(TAG) { "monitor" }
|
||||||
.mapNotNull { (previous, current) ->
|
.mapNotNull { (previous, current) ->
|
||||||
if (previous !is DualAirPods? || current !is DualAirPods) {
|
if (previous !is DualApplePods? || current !is DualApplePods) {
|
||||||
return@mapNotNull null
|
return@mapNotNull null
|
||||||
}
|
}
|
||||||
log(TAG, VERBOSE) {
|
log(TAG, VERBOSE) {
|
||||||
@@ -57,8 +57,8 @@ class PopUpReaction @Inject constructor(
|
|||||||
tryPopWindow(current)
|
tryPopWindow(current)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun tryPopWindow(current: DualAirPods): Event? = when {
|
private suspend fun tryPopWindow(current: DualApplePods): Event? = when {
|
||||||
current.caseLidState == DualAirPods.LidState.OPEN -> {
|
current.caseLidState == DualApplePods.LidState.OPEN -> {
|
||||||
log(TAG, INFO) { "Show popup" }
|
log(TAG, INFO) { "Show popup" }
|
||||||
|
|
||||||
val now = Instant.now()
|
val now = Instant.now()
|
||||||
@@ -74,9 +74,9 @@ class PopUpReaction @Inject constructor(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
current.caseLidState != DualAirPods.LidState.OPEN -> {
|
current.caseLidState != DualApplePods.LidState.OPEN -> {
|
||||||
when (current.caseLidState) {
|
when (current.caseLidState) {
|
||||||
DualAirPods.LidState.CLOSED -> {
|
DualApplePods.LidState.CLOSED -> {
|
||||||
log(TAG, INFO) { "Lid was actively closed, resetting cooldown." }
|
log(TAG, INFO) { "Lid was actively closed, resetting cooldown." }
|
||||||
coolDowns.remove(current.identifier)
|
coolDowns.remove(current.identifier)
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -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,4H18V5H21V7H18V9H21V11H18V13H21V15H18V17H21V19H18V20H6V19H3V17H6V15H3V13H6V11H3V9H6V7H3V5H6V4M11,15V18H12V15H11M13,15V18H14V15H13M15,15V18H16V15H15Z" />
|
||||||
|
</vector>
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:tint="?attr/colorControlNormal"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="m6,11c-1.9954,0 -3,-2.0093 -3,-4s1.0046,-4 3,-4c3.1837,0 5,3.4637 5,6.4886v10.0114c0,0.8284 -0.6716,1.5 -1.5,1.5 -0.8284,0 -1.5,-0.6716 -1.5,-1.5v-8.0113c0,-0.381 -0.1426,-0.7378 -0.3906,-1.0098 -0.4267,0.3435 -0.9676,0.5211 -1.6094,0.5211zM9,11.4887v8.0113c0,0.2761 0.2239,0.5 0.5,0.5s0.5,-0.2239 0.5,-0.5v-10.0114c0,-2.5454 -1.5434,-5.4886 -4,-5.4886 -1.2546,0 -2,1.4907 -2,3s0.7454,3 2,3c0.7809,0 1.2592,-0.3588 1.5257,-1.1581 0.2094,-0.6282 1.1516,-0.3212 0.9508,0.3097l-0.1878,0.5902c0.4499,0.4603 0.7114,1.0823 0.7114,1.7468zM6,8c-0.5523,0 -1,-0.4477 -1,-1s0.4477,-1 1,-1 1,0.4477 1,1 -0.4477,1 -1,1zM16,11.4887v8.0113c0,0.8284 -0.6716,1.5 -1.5,1.5s-1.5,-0.6716 -1.5,-1.5v-10.0114c0,-3.0249 1.8163,-6.4886 5,-6.4886 1.9954,0 3,2.0093 3,4s-1.0046,4 -3,4c-0.6418,0 -1.1826,-0.1777 -1.6094,-0.5211 -0.248,0.2721 -0.3906,0.6288 -0.3906,1.0098zM15.5235,9.1516c-0.2008,-0.631 0.7414,-0.9379 0.9508,-0.3097 0.2665,0.7994 0.7448,1.1581 1.5257,1.1581 1.2546,0 2,-1.4907 2,-3s-0.7454,-3 -2,-3c-2.4566,0 -4,2.9433 -4,5.4886v10.0114c0,0.2761 0.2239,0.5 0.5,0.5s0.5,-0.2239 0.5,-0.5v-8.0113c0,-0.6645 0.2615,-1.2865 0.7114,-1.7468zM18,8c-0.5523,0 -1,-0.4477 -1,-1s0.4477,-1 1,-1 1,0.4477 1,1 -0.4477,1 -1,1z" />
|
|
||||||
</vector>
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="?attr/colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M22.77 19.32L21.7 18.5C21.72 18.33 21.74 18.17 21.74 18S21.73 17.67 21.7 17.5L22.76 16.68C22.85 16.6 22.88 16.47 22.82 16.36L21.82 14.63C21.76 14.5 21.63 14.5 21.5 14.5L20.27 15C20 14.82 19.73 14.65 19.42 14.53L19.23 13.21C19.22 13.09 19.11 13 19 13H17C16.87 13 16.76 13.09 16.74 13.21L16.55 14.53C16.25 14.66 15.96 14.82 15.7 15L14.46 14.5C14.35 14.5 14.22 14.5 14.15 14.63L13.15 16.36C13.09 16.47 13.11 16.6 13.21 16.68L14.27 17.5C14.25 17.67 14.24 17.83 14.24 18S14.25 18.33 14.27 18.5L13.21 19.32C13.12 19.4 13.09 19.53 13.15 19.64L14.15 21.37C14.21 21.5 14.34 21.5 14.46 21.5L15.7 21C15.96 21.18 16.24 21.35 16.55 21.47L16.74 22.79C16.76 22.91 16.86 23 17 23H19C19.11 23 19.22 22.91 19.24 22.79L19.43 21.47C19.73 21.34 20 21.18 20.27 21L21.5 21.5C21.63 21.5 21.76 21.5 21.83 21.37L22.83 19.64C22.89 19.53 22.86 19.4 22.77 19.32M18 19.5C17.16 19.5 16.5 18.83 16.5 18S17.17 16.5 18 16.5 19.5 17.17 19.5 18 18.83 19.5 18 19.5M17.62 3.22C17.43 3.08 17.22 3 17 3H3C2.78 3 2.57 3.08 2.38 3.22C1.95 3.56 1.87 4.19 2.21 4.62L7 10.75V15.87C6.96 16.16 7.06 16.47 7.29 16.7L11.3 20.71C11.4 20.81 11.5 20.88 11.65 20.93C11.22 20 11 19 11 18C11 16.17 11.72 14.41 13 13.1V10.75L17.79 4.62C18.13 4.19 18.05 3.56 17.62 3.22M11 10.05V17.58L9 15.58V10.06L5.04 5H14.96L11 10.05Z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="?attr/colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M5 5V19H7V21H3V3H7V5H5M20 7H7V9H20V7M20 11H7V13H20V11M20 15H7V17H20V15Z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="?attr/colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M6.91 5.5L9.21 7.79L7.79 9.21L5.5 6.91L3.21 9.21L1.79 7.79L4.09 5.5L1.79 3.21L3.21 1.79L5.5 4.09L7.79 1.79L9.21 3.21M22.21 16.21L20.79 14.79L18.5 17.09L16.21 14.79L14.79 16.21L17.09 18.5L14.79 20.79L16.21 22.21L18.5 19.91L20.79 22.21L22.21 20.79L19.91 18.5M20.4 6.83L17.18 11L15.6 9.73L16.77 8.23A9.08 9.08 0 0 0 10.11 13.85A4.5 4.5 0 1 1 7.5 13A4 4 0 0 1 8.28 13.08A11.27 11.27 0 0 1 16.43 6.26L15 5.18L16.27 3.6M10 17.5A2.5 2.5 0 1 0 7.5 20A2.5 2.5 0 0 0 10 17.5Z" />
|
||||||
|
</vector>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||||
|
<string name="app_name_pro">CAPod Pro</string>
|
||||||
<string name="general_value_not_available_label">Δε διατίθεται</string>
|
<string name="general_value_not_available_label">Δε διατίθεται</string>
|
||||||
<string name="general_error_label">Σφάλμα</string>
|
<string name="general_error_label">Σφάλμα</string>
|
||||||
<string name="general_grant_permission_action">Παραχώρηση άδειας</string>
|
<string name="general_grant_permission_action">Παραχώρηση άδειας</string>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||||
|
<string name="app_name">CAPod</string>
|
||||||
|
<string name="app_name_pro">CAPod Pro</string>
|
||||||
<string name="general_error_label">Erro</string>
|
<string name="general_error_label">Erro</string>
|
||||||
<string name="general_grant_permission_action">Conceder permissão</string>
|
<string name="general_grant_permission_action">Conceder permissão</string>
|
||||||
<string name="overview_nomaindevice_label">Nenhum dispositivo principal</string>
|
<string name="overview_nomaindevice_label">Nenhum dispositivo principal</string>
|
||||||
@@ -10,6 +12,7 @@
|
|||||||
<string name="permission_bluetooth_connect_description">Este aplicativo requer a permissão \"Conexão Bluetooth\" para interagir com dispositivos emparelhados e iniciar conexões.</string>
|
<string name="permission_bluetooth_connect_description">Este aplicativo requer a permissão \"Conexão Bluetooth\" para interagir com dispositivos emparelhados e iniciar conexões.</string>
|
||||||
<string name="permission_bluetooth_scan_label">Escaneando bluetooth</string>
|
<string name="permission_bluetooth_scan_label">Escaneando bluetooth</string>
|
||||||
<string name="permission_bluetooth_scan_description">A permissão \"Escaneamento de Bluetooth\" permite que este aplicativo descubra e receba dados de Bluetooth de dispositivos próximos, como seus AirPods.</string>
|
<string name="permission_bluetooth_scan_description">A permissão \"Escaneamento de Bluetooth\" permite que este aplicativo descubra e receba dados de Bluetooth de dispositivos próximos, como seus AirPods.</string>
|
||||||
|
<string name="permission_bluetooth_label">Bluetooth</string>
|
||||||
<string name="permission_bluetooth_description">Este aplicativo requer a permissão \"Bluetooth\" para se conectar a dispositivos bluetooth emparelhados.</string>
|
<string name="permission_bluetooth_description">Este aplicativo requer a permissão \"Bluetooth\" para se conectar a dispositivos bluetooth emparelhados.</string>
|
||||||
<string name="permission_access_fine_location_label">Acesse a boa localização</string>
|
<string name="permission_access_fine_location_label">Acesse a boa localização</string>
|
||||||
<string name="permission_access_fine_location_description">O CAPod usa a permissão \"Boa localização\" para receber dados de Bluetooth Low Energy. Seus fones de ouvido usam a tecnologia Bluetooth Low Energy para transmitir seu status. Este aplicativo NÃO usará dados Bluetooth para determinar sua localização.</string>
|
<string name="permission_access_fine_location_description">O CAPod usa a permissão \"Boa localização\" para receber dados de Bluetooth Low Energy. Seus fones de ouvido usam a tecnologia Bluetooth Low Energy para transmitir seu status. Este aplicativo NÃO usará dados Bluetooth para determinar sua localização.</string>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<string name="overview_nomaindevice_label">Відсутній первинний пристрій</string>
|
<string name="overview_nomaindevice_label">Відсутній первинний пристрій</string>
|
||||||
<string name="overview_bluetooth_disabled_label">Bluetooth вимкнено</string>
|
<string name="overview_bluetooth_disabled_label">Bluetooth вимкнено</string>
|
||||||
<string name="overview_bluetooth_disabled_description">Bluetooth вимкнено, увімкніть його ;)</string>
|
<string name="overview_bluetooth_disabled_description">Bluetooth вимкнено, увімкніть його ;)</string>
|
||||||
|
<string name="permission_bluetooth_label">Bluetooth</string>
|
||||||
<string name="permission_access_fine_location_label">Доступ до точного місцезнаходження</string>
|
<string name="permission_access_fine_location_label">Доступ до точного місцезнаходження</string>
|
||||||
<string name="permission_background_location_label">Доступ до місцезнаходження у тлі</string>
|
<string name="permission_background_location_label">Доступ до місцезнаходження у тлі</string>
|
||||||
<string name="permission_ignore_battery_optimizations_label">Вимкнути оптимізацію батареї</string>
|
<string name="permission_ignore_battery_optimizations_label">Вимкнути оптимізацію батареї</string>
|
||||||
|
|||||||
@@ -7,21 +7,21 @@
|
|||||||
<string name="general_error_label">錯誤</string>
|
<string name="general_error_label">錯誤</string>
|
||||||
<string name="general_grant_permission_action">授予權限</string>
|
<string name="general_grant_permission_action">授予權限</string>
|
||||||
<string name="overview_nomaindevice_label">無主要裝置</string>
|
<string name="overview_nomaindevice_label">無主要裝置</string>
|
||||||
<string name="overview_nomaindevice_description">所有檢測到的裝置似乎並不是你的。開啟並連線你的裝置,也可以調整設定。</string>
|
<string name="overview_nomaindevice_description">所有偵測到的裝置似乎並不是你的。開啟並連線你的裝置,也可以調整設定。</string>
|
||||||
<string name="overview_bluetooth_disabled_label">藍牙已停用</string>
|
<string name="overview_bluetooth_disabled_label">藍牙已停用</string>
|
||||||
<string name="overview_bluetooth_disabled_description">藍牙已停用,啟用它 ;)</string>
|
<string name="overview_bluetooth_disabled_description">藍牙已停用,啟用它 ;)</string>
|
||||||
<string name="permission_bluetooth_connect_label">藍牙連線</string>
|
<string name="permission_bluetooth_connect_label">藍牙連線</string>
|
||||||
<string name="permission_bluetooth_connect_description">本應用程式需要「藍牙連線」權限以與已配對的裝置交互或配對新裝置。</string>
|
<string name="permission_bluetooth_connect_description">這個應用程式需要「藍牙連線」權限以與已配對的裝置交互或配對新裝置。</string>
|
||||||
<string name="permission_bluetooth_scan_label">藍牙掃描</string>
|
<string name="permission_bluetooth_scan_label">藍牙掃描</string>
|
||||||
<string name="permission_bluetooth_scan_description">「藍牙掃描」權限允許該應用程式發現附近的裝置並接收藍牙資料,你的 AirPods 需要如此。</string>
|
<string name="permission_bluetooth_scan_description">「藍牙掃描」權限允許這個應用程式發現附近的裝置並接收藍牙資料,你的 AirPods 需要如此。</string>
|
||||||
<string name="permission_bluetooth_label">藍牙</string>
|
<string name="permission_bluetooth_label">藍牙</string>
|
||||||
<string name="permission_bluetooth_description">該應用程式需要「藍牙」權限與已配對裝置連線。</string>
|
<string name="permission_bluetooth_description">這個應用程式需要「藍牙」權限與已配對裝置連線。</string>
|
||||||
<string name="permission_access_fine_location_label">存取精確位置</string>
|
<string name="permission_access_fine_location_label">存取精確位置</string>
|
||||||
<string name="permission_access_fine_location_description">CAPod 使用「精確位置」權限以接收低功耗藍牙資料。你的耳機使用低功耗藍牙技術以廣播其狀態。這個應用程式不會使用藍牙資料來確定你的位置。</string>
|
<string name="permission_access_fine_location_description">CAPod 使用「精確位置」權限以接收低功耗藍牙資料。你的耳機使用低功耗藍牙技術以廣播其狀態。這個應用程式不會使用藍牙資料來確定你的位置。</string>
|
||||||
<string name="permission_background_location_label">背景位置存取</string>
|
<string name="permission_background_location_label">背景位置存取</string>
|
||||||
<string name="permission_background_location_description">CAPods 在應用程式關閉時使用「背景位置存取」來啟用諸如「顯示彈出式視窗」和「自動連線」等功能。背景位置存取允許該應用程式在背景接收低功耗藍牙資料。這個應用程式不會使用藍牙資料來確定你的位置。</string>
|
<string name="permission_background_location_description">CAPods 在應用程式關閉時使用「背景位置存取」來啟用諸如「顯示彈出式視窗」和「自動連線」等功能。背景位置存取允許這個應用程式在背景接收低功耗藍牙資料。這個應用程式不會使用藍牙資料來確定你的位置。</string>
|
||||||
<string name="permission_ignore_battery_optimizations_label">停用電池效能最佳化</string>
|
<string name="permission_ignore_battery_optimizations_label">停用電池效能最佳化</string>
|
||||||
<string name="permission_ignore_battery_optimizations_description">電池效能最佳化使該應用程式在背景時無法可靠地接收藍牙資料。</string>
|
<string name="permission_ignore_battery_optimizations_description">電池效能最佳化使這個應用程式在背景時無法可靠地接收藍牙資料。</string>
|
||||||
<string name="permission_required_title">可能要求下列權限:</string>
|
<string name="permission_required_title">可能要求下列權限:</string>
|
||||||
<string name="permission_system_alert_window_label">系統警報視窗</string>
|
<string name="permission_system_alert_window_label">系統警報視窗</string>
|
||||||
<string name="permission_system_alert_window_description">允許 CAPod 在其他應用程式上繪圖,使「顯示彈出式視窗」功能成為可能。</string>
|
<string name="permission_system_alert_window_description">允許 CAPod 在其他應用程式上繪圖,使「顯示彈出式視窗」功能成為可能。</string>
|
||||||
|
|||||||
@@ -14,14 +14,14 @@ class AirPodsFactoryTest : BaseAirPodsTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `create AirPodsGen1`() = runTest {
|
fun `create AirPodsGen1`() = runTest {
|
||||||
create<DualAirPods>("07 19 01 02 20 55 AA 56 31 00 00 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88") {
|
create<DualApplePods>("07 19 01 02 20 55 AA 56 31 00 00 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88") {
|
||||||
this shouldBe instanceOf<AirPodsGen1>()
|
this shouldBe instanceOf<AirPodsGen1>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `create AirPodsPro`() = runTest {
|
fun `create AirPodsPro`() = runTest {
|
||||||
create<DualAirPods>("07 19 01 0E 20 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") {
|
create<DualApplePods>("07 19 01 0E 20 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") {
|
||||||
this shouldBe instanceOf<AirPodsPro>()
|
this shouldBe instanceOf<AirPodsPro>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.darken.capod.pods.core.apple
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
|
import eu.darken.capod.pods.core.apple.airpods.HasStateDetectionAirPods
|
||||||
import io.kotest.matchers.shouldBe
|
import io.kotest.matchers.shouldBe
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
@@ -8,7 +9,7 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test bit mapping`() = runTest {
|
fun `test bit mapping`() = runTest {
|
||||||
create<DualAirPods>("07 19 01 0E 20 54 AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
create<DualApplePods>("07 19 01 0E 20 54 AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||||
|
|
||||||
rawPrefix shouldBe 0x01.toUByte()
|
rawPrefix shouldBe 0x01.toUByte()
|
||||||
rawDeviceModel shouldBe 0x0e20.toUShort()
|
rawDeviceModel shouldBe 0x0e20.toUShort()
|
||||||
@@ -24,13 +25,13 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test AirPodDevice - active microphone`() = runTest {
|
fun `test AirPodDevice - active microphone`() = runTest {
|
||||||
create<DualAirPods>("07 19 01 0E 20 >2B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
create<DualApplePods>("07 19 01 0E 20 >2B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||||
// 00101011
|
// 00101011
|
||||||
// --^-----
|
// --^-----
|
||||||
isLeftPodMicrophone shouldBe true
|
isLeftPodMicrophone shouldBe true
|
||||||
isRightPodMicrophone shouldBe false
|
isRightPodMicrophone shouldBe false
|
||||||
}
|
}
|
||||||
create<DualAirPods>("07 19 01 0E 20 >0B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
create<DualApplePods>("07 19 01 0E 20 >0B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||||
// 00001011
|
// 00001011
|
||||||
// --^-----
|
// --^-----
|
||||||
isLeftPodMicrophone shouldBe false
|
isLeftPodMicrophone shouldBe false
|
||||||
@@ -41,24 +42,24 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun `test AirPodDevice - left pod ear status`() = runTest {
|
fun `test AirPodDevice - left pod ear status`() = runTest {
|
||||||
// Left Pod primary
|
// Left Pod primary
|
||||||
create<DualAirPods>("07 19 01 0E 20 >22< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
create<DualApplePods>("07 19 01 0E 20 >22< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||||
// 00100010
|
// 00100010
|
||||||
// 765432¹0
|
// 765432¹0
|
||||||
isLeftPodInEar shouldBe true
|
isLeftPodInEar shouldBe true
|
||||||
}
|
}
|
||||||
create<DualAirPods>("07 19 01 0E 20 >20< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
create<DualApplePods>("07 19 01 0E 20 >20< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||||
// 00100000
|
// 00100000
|
||||||
// 765432¹0
|
// 765432¹0
|
||||||
isLeftPodInEar shouldBe false
|
isLeftPodInEar shouldBe false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Right Pod is primary
|
// Right Pod is primary
|
||||||
create<DualAirPods>("07 19 01 0E 20 >09< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
create<DualApplePods>("07 19 01 0E 20 >09< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||||
// 00001001
|
// 00001001
|
||||||
// 7654³210
|
// 7654³210
|
||||||
isLeftPodInEar shouldBe true
|
isLeftPodInEar shouldBe true
|
||||||
}
|
}
|
||||||
create<DualAirPods>("07 19 01 0E 20 >20< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
create<DualApplePods>("07 19 01 0E 20 >20< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||||
// 00000001
|
// 00000001
|
||||||
// 7654³210
|
// 7654³210
|
||||||
isLeftPodInEar shouldBe false
|
isLeftPodInEar shouldBe false
|
||||||
@@ -68,24 +69,24 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun `test AirPodDevice - right pod ear status`() = runTest {
|
fun `test AirPodDevice - right pod ear status`() = runTest {
|
||||||
// Left Pod primary
|
// Left Pod primary
|
||||||
create<DualAirPods>("07 19 01 0E 20 >29< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
create<DualApplePods>("07 19 01 0E 20 >29< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||||
// 00101001
|
// 00101001
|
||||||
// 7654³210
|
// 7654³210
|
||||||
isRightPodInEar shouldBe true
|
isRightPodInEar shouldBe true
|
||||||
}
|
}
|
||||||
create<DualAirPods>("07 19 01 0E 20 >21< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
create<DualApplePods>("07 19 01 0E 20 >21< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||||
// 00100001
|
// 00100001
|
||||||
// 7654³210
|
// 7654³210
|
||||||
isRightPodInEar shouldBe false
|
isRightPodInEar shouldBe false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Right Pod is primary
|
// Right Pod is primary
|
||||||
create<DualAirPods>("07 19 01 0E 20 >03< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
create<DualApplePods>("07 19 01 0E 20 >03< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||||
// 00000011
|
// 00000011
|
||||||
// 765432¹0
|
// 765432¹0
|
||||||
isRightPodInEar shouldBe true
|
isRightPodInEar shouldBe true
|
||||||
}
|
}
|
||||||
create<DualAirPods>("07 19 01 0E 20 >01< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
create<DualApplePods>("07 19 01 0E 20 >01< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||||
// 00000001
|
// 00000001
|
||||||
// 765432¹0
|
// 765432¹0
|
||||||
isRightPodInEar shouldBe false
|
isRightPodInEar shouldBe false
|
||||||
@@ -95,13 +96,13 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun `test AirPodDevice - battery status`() = runTest {
|
fun `test AirPodDevice - battery status`() = runTest {
|
||||||
// Right Pod is primary
|
// Right Pod is primary
|
||||||
create<DualAirPods>("07 19 01 0E 20 0B >98< 94 52 00 05 09 73 3C 3D F9 2C 3E B3 DD 76 02 DD 4E 16 FD FB") {
|
create<DualApplePods>("07 19 01 0E 20 0B >98< 94 52 00 05 09 73 3C 3D F9 2C 3E B3 DD 76 02 DD 4E 16 FD FB") {
|
||||||
// 88 10001000
|
// 88 10001000
|
||||||
batteryLeftPodPercent shouldBe 0.9f
|
batteryLeftPodPercent shouldBe 0.9f
|
||||||
batteryRightPodPercent shouldBe 0.8f
|
batteryRightPodPercent shouldBe 0.8f
|
||||||
}
|
}
|
||||||
// Left Pod primary
|
// Left Pod primary
|
||||||
create<DualAirPods>("07 19 01 0E 20 2B >89< 94 52 00 05 09 73 3C 3D F9 2C 3E B3 DD 76 02 DD 4E 16 FD FB") {
|
create<DualApplePods>("07 19 01 0E 20 2B >89< 94 52 00 05 09 73 3C 3D F9 2C 3E B3 DD 76 02 DD 4E 16 FD FB") {
|
||||||
// F8 11111000
|
// F8 11111000
|
||||||
batteryLeftPodPercent shouldBe 0.9f
|
batteryLeftPodPercent shouldBe 0.9f
|
||||||
batteryRightPodPercent shouldBe 0.8f
|
batteryRightPodPercent shouldBe 0.8f
|
||||||
@@ -114,13 +115,13 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
|||||||
* Right pod is charging
|
* Right pod is charging
|
||||||
*/
|
*/
|
||||||
// This is the left
|
// This is the left
|
||||||
create<DualAirPods>("07 19 01 0E 20 51 89 >94< 52 00 00 F4 89 82 6D 3E 27 7F 26 62 57 D0 E2 A6 49 E9 35") {
|
create<DualApplePods>("07 19 01 0E 20 51 89 >94< 52 00 00 F4 89 82 6D 3E 27 7F 26 62 57 D0 E2 A6 49 E9 35") {
|
||||||
// 1001 0100
|
// 1001 0100
|
||||||
isLeftPodCharging shouldBe false
|
isLeftPodCharging shouldBe false
|
||||||
isRightPodCharging shouldBe true
|
isRightPodCharging shouldBe true
|
||||||
}
|
}
|
||||||
// This is the right
|
// This is the right
|
||||||
create<DualAirPods>("07 19 01 0E 20 31 98 >A4< 01 00 00 31 B9 A0 C4 80 CD D1 CF B9 3A 9A 6D 48 31 08 EB") {
|
create<DualApplePods>("07 19 01 0E 20 31 98 >A4< 01 00 00 31 B9 A0 C4 80 CD D1 CF B9 3A 9A 6D 48 31 08 EB") {
|
||||||
// 1010 0100
|
// 1010 0100
|
||||||
isLeftPodCharging shouldBe false
|
isLeftPodCharging shouldBe false
|
||||||
isRightPodCharging shouldBe true
|
isRightPodCharging shouldBe true
|
||||||
@@ -130,26 +131,26 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
|||||||
* Left pod is charging
|
* Left pod is charging
|
||||||
*/
|
*/
|
||||||
// This is the left
|
// This is the left
|
||||||
create<DualAirPods>("07 19 01 0E 20 71 98 >94< 52 00 05 A5 37 31 B2 BD 42 68 0C 64 FD 00 99 4A E5 3E F4") {
|
create<DualApplePods>("07 19 01 0E 20 71 98 >94< 52 00 05 A5 37 31 B2 BD 42 68 0C 64 FD 00 99 4A E5 3E F4") {
|
||||||
// 1001 0100
|
// 1001 0100
|
||||||
isLeftPodCharging shouldBe true
|
isLeftPodCharging shouldBe true
|
||||||
isRightPodCharging shouldBe false
|
isRightPodCharging shouldBe false
|
||||||
}
|
}
|
||||||
// This is the right
|
// This is the right
|
||||||
create<DualAirPods>("07 19 01 0E 20 11 89 >A4< 04 00 04 BA 79 1B C0 65 69 C6 9F 19 6E 37 7D 6D 86 8D D9") {
|
create<DualApplePods>("07 19 01 0E 20 11 89 >A4< 04 00 04 BA 79 1B C0 65 69 C6 9F 19 6E 37 7D 6D 86 8D D9") {
|
||||||
// 1010 0100
|
// 1010 0100
|
||||||
isLeftPodCharging shouldBe true
|
isLeftPodCharging shouldBe true
|
||||||
isRightPodCharging shouldBe false
|
isRightPodCharging shouldBe false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Both charging
|
// Both charging
|
||||||
create<DualAirPods>("07 19 01 0E 20 55 88 >B4< 59 00 05 4B FC DF 68 28 A5 45 52 65 9C FE 51 86 3A B5 DB") {
|
create<DualApplePods>("07 19 01 0E 20 55 88 >B4< 59 00 05 4B FC DF 68 28 A5 45 52 65 9C FE 51 86 3A B5 DB") {
|
||||||
// 1011 0100
|
// 1011 0100
|
||||||
isLeftPodCharging shouldBe true
|
isLeftPodCharging shouldBe true
|
||||||
isRightPodCharging shouldBe true
|
isRightPodCharging shouldBe true
|
||||||
}
|
}
|
||||||
// Both not charging
|
// Both not charging
|
||||||
create<DualAirPods>("07 19 01 0E 20 00 F8 >8F< 03 00 05 4C 0F A0 C4 05 24 DD EB AF 92 99 FD 54 B1 06 48") {
|
create<DualApplePods>("07 19 01 0E 20 00 F8 >8F< 03 00 05 4C 0F A0 C4 05 24 DD EB AF 92 99 FD 54 B1 06 48") {
|
||||||
// 1000 1111
|
// 1000 1111
|
||||||
isLeftPodCharging shouldBe false
|
isLeftPodCharging shouldBe false
|
||||||
isRightPodCharging shouldBe false
|
isRightPodCharging shouldBe false
|
||||||
@@ -158,11 +159,11 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test AirPodDevice - case charging`() = runTest {
|
fun `test AirPodDevice - case charging`() = runTest {
|
||||||
create<DualAirPods>("07 19 01 0E 20 75 99 >B4< 31 00 05 77 C8 BA 0C 4E 1F BE AD 70 C5 40 71 D2 E9 17 A2") {
|
create<DualApplePods>("07 19 01 0E 20 75 99 >B4< 31 00 05 77 C8 BA 0C 4E 1F BE AD 70 C5 40 71 D2 E9 17 A2") {
|
||||||
// 0011 0011
|
// 0011 0011
|
||||||
isCaseCharging shouldBe false
|
isCaseCharging shouldBe false
|
||||||
}
|
}
|
||||||
create<DualAirPods>("07 19 01 0E 20 75 A9 >F4< 51 00 05 A0 37 92 35 49 79 CC DC 27 94 8E FB 72 12 94 52") {
|
create<DualApplePods>("07 19 01 0E 20 75 A9 >F4< 51 00 05 A0 37 92 35 49 79 CC DC 27 94 8E FB 72 12 94 52") {
|
||||||
// 0101 0011
|
// 0101 0011
|
||||||
isCaseCharging shouldBe true
|
isCaseCharging shouldBe true
|
||||||
}
|
}
|
||||||
@@ -171,75 +172,75 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun `test AirPodDevice - case lid test`() = runTest {
|
fun `test AirPodDevice - case lid test`() = runTest {
|
||||||
// Lid open
|
// Lid open
|
||||||
create<DualAirPods>("07 19 01 0E 20 55 AA B4 >31< 00 00 A1 D0 BD 82 D3 52 86 CA FC 11 62 DC 42 C6 92 8E") {
|
create<DualApplePods>("07 19 01 0E 20 55 AA B4 >31< 00 00 A1 D0 BD 82 D3 52 86 CA FC 11 62 DC 42 C6 92 8E") {
|
||||||
// 31 0011 0001
|
// 31 0011 0001
|
||||||
caseLidState shouldBe DualAirPods.LidState.OPEN
|
caseLidState shouldBe DualApplePods.LidState.OPEN
|
||||||
}
|
}
|
||||||
// Lid open, left pod in case
|
// Lid open, left pod in case
|
||||||
create<DualAirPods>("07 19 01 0E 20 51 9A 93 >31< 00 00 95 D0 A5 D7 E3 F4 F1 38 38 99 61 3B 57 95 37 B7") {
|
create<DualApplePods>("07 19 01 0E 20 51 9A 93 >31< 00 00 95 D0 A5 D7 E3 F4 F1 38 38 99 61 3B 57 95 37 B7") {
|
||||||
// 31 0011 0001
|
// 31 0011 0001
|
||||||
caseLidState shouldBe DualAirPods.LidState.OPEN
|
caseLidState shouldBe DualApplePods.LidState.OPEN
|
||||||
}
|
}
|
||||||
// Lid open, left pod in case
|
// Lid open, left pod in case
|
||||||
create<DualAirPods>("07 19 01 0E 20 71 A9 92 31 00 00 DB 48 7F 32 8C CE 80 6F D9 27 98 D6 76 45 9D 62") {
|
create<DualApplePods>("07 19 01 0E 20 71 A9 92 31 00 00 DB 48 7F 32 8C CE 80 6F D9 27 98 D6 76 45 9D 62") {
|
||||||
// 31 0011 0001
|
// 31 0011 0001
|
||||||
caseLidState shouldBe DualAirPods.LidState.OPEN
|
caseLidState shouldBe DualApplePods.LidState.OPEN
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Lid just closed
|
// Lid just closed
|
||||||
create<DualAirPods>("07 19 01 0E 20 55 AA B4 >39< 00 00 08 A6 DB 99 E0 5E 14 85 E5 C2 0B 68 D7 FF C3 A1") {
|
create<DualApplePods>("07 19 01 0E 20 55 AA B4 >39< 00 00 08 A6 DB 99 E0 5E 14 85 E5 C2 0B 68 D7 FF C3 A1") {
|
||||||
// 39 0011 1001
|
// 39 0011 1001
|
||||||
caseLidState shouldBe DualAirPods.LidState.CLOSED
|
caseLidState shouldBe DualApplePods.LidState.CLOSED
|
||||||
}
|
}
|
||||||
// Lid closed
|
// Lid closed
|
||||||
create<DualAirPods>("07 19 01 0E 20 55 AA B4 38 00 00 F3 F7 08 3B 98 09 C0 DD E4 BD BD 84 55 56 8B 81") {
|
create<DualApplePods>("07 19 01 0E 20 55 AA B4 38 00 00 F3 F7 08 3B 98 09 C0 DD E4 BD BD 84 55 56 8B 81") {
|
||||||
// 38 0011 1000
|
// 38 0011 1000
|
||||||
caseLidState shouldBe DualAirPods.LidState.CLOSED
|
caseLidState shouldBe DualApplePods.LidState.CLOSED
|
||||||
}
|
}
|
||||||
// Lid closed, right pod in case
|
// Lid closed, right pod in case
|
||||||
create<DualAirPods>("07 19 01 0E 20 51 9A 93 >38< 00 00 3A D8 85 76 B0 91 48 31 DA FF 6C 4A 2B C2 67 F4") {
|
create<DualApplePods>("07 19 01 0E 20 51 9A 93 >38< 00 00 3A D8 85 76 B0 91 48 31 DA FF 6C 4A 2B C2 67 F4") {
|
||||||
// 38 0011 1000
|
// 38 0011 1000
|
||||||
caseLidState shouldBe DualAirPods.LidState.CLOSED
|
caseLidState shouldBe DualApplePods.LidState.CLOSED
|
||||||
}
|
}
|
||||||
// Lid closed, left pod in case
|
// Lid closed, left pod in case
|
||||||
create<DualAirPods>("07 19 01 0E 20 71 A9 92 38 00 00 44 91 C4 8B 85 98 DD 55 4E 6A CA BC B5 CA 8D 37") {
|
create<DualApplePods>("07 19 01 0E 20 71 A9 92 38 00 00 44 91 C4 8B 85 98 DD 55 4E 6A CA BC B5 CA 8D 37") {
|
||||||
// 38 0011 1000
|
// 38 0011 1000
|
||||||
caseLidState shouldBe DualAirPods.LidState.CLOSED
|
caseLidState shouldBe DualApplePods.LidState.CLOSED
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `test AirPodDevice - connection state`() = runTest {
|
fun `test AirPodDevice - connection state`() = runTest {
|
||||||
// Disconnected
|
// Disconnected
|
||||||
create<DualAirPods>("07 19 01 0E 20 2B AA 8F 01 00 >00< 62 D4 BB F1 A7 F8 64 98 D2 C8 BD 7B 3A EF 2E 15") {
|
create<HasStateDetectionAirPods>("07 19 01 0E 20 2B AA 8F 01 00 >00< 62 D4 BB F1 A7 F8 64 98 D2 C8 BD 7B 3A EF 2E 15") {
|
||||||
// 31 0011 0001
|
// 31 0011 0001
|
||||||
state shouldBe DualAirPods.ConnectionState.DISCONNECTED
|
state shouldBe HasStateDetectionAirPods.ConnectionState.DISCONNECTED
|
||||||
}
|
}
|
||||||
// Connected idle
|
// Connected idle
|
||||||
create<DualAirPods>("07 19 01 0E 20 2B AA 8F 01 00 >04< 1D 69 69 9C C2 51 F3 1F BF 6E 45 DA 90 4A A3 E3") {
|
create<HasStateDetectionAirPods>("07 19 01 0E 20 2B AA 8F 01 00 >04< 1D 69 69 9C C2 51 F3 1F BF 6E 45 DA 90 4A A3 E3") {
|
||||||
// 39 0011 1001
|
// 39 0011 1001
|
||||||
state shouldBe DualAirPods.ConnectionState.IDLE
|
state shouldBe HasStateDetectionAirPods.ConnectionState.IDLE
|
||||||
}
|
}
|
||||||
// Connected and playing music
|
// Connected and playing music
|
||||||
create<DualAirPods>("07 19 01 0E 20 2B A9 8F 01 00 >05< 14 F7 CB 49 9F D3 B3 22 77 D2 22 F1 74 8C AC A6") {
|
create<HasStateDetectionAirPods>("07 19 01 0E 20 2B A9 8F 01 00 >05< 14 F7 CB 49 9F D3 B3 22 77 D2 22 F1 74 8C AC A6") {
|
||||||
// 38 0011 1000
|
// 38 0011 1000
|
||||||
state shouldBe DualAirPods.ConnectionState.MUSIC
|
state shouldBe HasStateDetectionAirPods.ConnectionState.MUSIC
|
||||||
}
|
}
|
||||||
// Connected and call active
|
// Connected and call active
|
||||||
create<DualAirPods>("07 19 01 0E 20 2B 99 8F 01 00 >06< 0F 4B 43 25 E0 4A 73 63 14 22 C2 3C 89 13 BD 97") {
|
create<HasStateDetectionAirPods>("07 19 01 0E 20 2B 99 8F 01 00 >06< 0F 4B 43 25 E0 4A 73 63 14 22 C2 3C 89 13 BD 97") {
|
||||||
// 38 0011 1000
|
// 38 0011 1000
|
||||||
state shouldBe DualAirPods.ConnectionState.CALL
|
state shouldBe HasStateDetectionAirPods.ConnectionState.CALL
|
||||||
}
|
}
|
||||||
// Connected and call active
|
// Connected and call active
|
||||||
create<DualAirPods>("07 19 01 0E 20 2B 99 8F 01 00 >07< E7 DF 76 44 85 B5 30 F4 95 14 02 DC A1 A4 8A 09") {
|
create<HasStateDetectionAirPods>("07 19 01 0E 20 2B 99 8F 01 00 >07< E7 DF 76 44 85 B5 30 F4 95 14 02 DC A1 A4 8A 09") {
|
||||||
// 38 0011 1000
|
// 38 0011 1000
|
||||||
state shouldBe DualAirPods.ConnectionState.RINGING
|
state shouldBe HasStateDetectionAirPods.ConnectionState.RINGING
|
||||||
}
|
}
|
||||||
// Switching?
|
// Switching?
|
||||||
create<DualAirPods>("07 19 01 0E 20 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") {
|
create<HasStateDetectionAirPods>("07 19 01 0E 20 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") {
|
||||||
// 38 0011 1000
|
// 38 0011 1000
|
||||||
state shouldBe DualAirPods.ConnectionState.HANGING_UP
|
state shouldBe HasStateDetectionAirPods.ConnectionState.HANGING_UP
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package eu.darken.capod.pods.core.apple.airpods
|
|||||||
|
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.pods.core.apple.HasAppleColor
|
import eu.darken.capod.pods.core.apple.HasAppleColor
|
||||||
import io.kotest.matchers.shouldBe
|
import io.kotest.matchers.shouldBe
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
@@ -35,9 +35,7 @@ class AirPodsGen1Test : BaseAirPodsTest() {
|
|||||||
isRightPodInEar shouldBe false
|
isRightPodInEar shouldBe false
|
||||||
batteryCasePercent shouldBe 0.6f
|
batteryCasePercent shouldBe 0.6f
|
||||||
|
|
||||||
caseLidState shouldBe DualAirPods.LidState.OPEN
|
caseLidState shouldBe DualApplePods.LidState.OPEN
|
||||||
|
|
||||||
state shouldBe DualAirPods.ConnectionState.DISCONNECTED
|
|
||||||
|
|
||||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package eu.darken.capod.pods.core.apple.airpods
|
|||||||
|
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.pods.core.apple.HasAppleColor
|
import eu.darken.capod.pods.core.apple.HasAppleColor
|
||||||
import io.kotest.matchers.shouldBe
|
import io.kotest.matchers.shouldBe
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
@@ -34,9 +34,9 @@ class AirPodsGen2Test : BaseAirPodsTest() {
|
|||||||
isRightPodInEar shouldBe true
|
isRightPodInEar shouldBe true
|
||||||
batteryCasePercent shouldBe null
|
batteryCasePercent shouldBe null
|
||||||
|
|
||||||
caseLidState shouldBe DualAirPods.LidState.NOT_IN_CASE
|
caseLidState shouldBe DualApplePods.LidState.NOT_IN_CASE
|
||||||
|
|
||||||
state shouldBe DualAirPods.ConnectionState.MUSIC
|
state shouldBe HasStateDetectionAirPods.ConnectionState.MUSIC
|
||||||
|
|
||||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package eu.darken.capod.pods.core.apple.airpods
|
|||||||
|
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.pods.core.apple.HasAppleColor
|
import eu.darken.capod.pods.core.apple.HasAppleColor
|
||||||
import io.kotest.matchers.shouldBe
|
import io.kotest.matchers.shouldBe
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
@@ -34,9 +34,9 @@ class AirPodsGen3Test : BaseAirPodsTest() {
|
|||||||
isRightPodInEar shouldBe false
|
isRightPodInEar shouldBe false
|
||||||
batteryCasePercent shouldBe 0.9f
|
batteryCasePercent shouldBe 0.9f
|
||||||
|
|
||||||
caseLidState shouldBe DualAirPods.LidState.OPEN
|
caseLidState shouldBe DualApplePods.LidState.OPEN
|
||||||
|
|
||||||
state shouldBe DualAirPods.ConnectionState.IDLE
|
state shouldBe HasStateDetectionAirPods.ConnectionState.IDLE
|
||||||
|
|
||||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||||
|
|
||||||
@@ -58,9 +58,9 @@ class AirPodsGen3Test : BaseAirPodsTest() {
|
|||||||
isRightPodInEar shouldBe true
|
isRightPodInEar shouldBe true
|
||||||
batteryCasePercent shouldBe null
|
batteryCasePercent shouldBe null
|
||||||
|
|
||||||
caseLidState shouldBe DualAirPods.LidState.NOT_IN_CASE
|
caseLidState shouldBe DualApplePods.LidState.NOT_IN_CASE
|
||||||
|
|
||||||
state shouldBe DualAirPods.ConnectionState.UNKNOWN
|
state shouldBe HasStateDetectionAirPods.ConnectionState.UNKNOWN
|
||||||
|
|
||||||
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
podStyle.identifier shouldBe HasAppleColor.DeviceColor.WHITE.name
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple.beats
|
||||||
|
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
|
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||||
|
import eu.darken.capod.pods.core.apple.HasAppleColor
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class BeatsFitProTest : BaseAirPodsTest() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From https://github.com/d4rken-org/capod/issues/33#issuecomment-1256235651
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun `test basics`() = runTest {
|
||||||
|
create<BeatsFitPro>("07 19 01 12 20 20 FA 8F 01 11 24 9B 9B 23 52 60 5A 8C 32 1C A5 C2 81 51 82 AF C8") {
|
||||||
|
rawPrefix shouldBe 0x01.toUByte()
|
||||||
|
rawDeviceModel shouldBe 0x1220.toUShort()
|
||||||
|
rawStatus shouldBe 0x20.toUByte()
|
||||||
|
rawPodsBattery shouldBe 0xFA.toUByte()
|
||||||
|
rawFlags shouldBe 0x8.toUShort()
|
||||||
|
rawCaseBattery shouldBe 0xF.toUShort()
|
||||||
|
rawCaseLidState shouldBe 0x01.toUByte()
|
||||||
|
rawDeviceColor shouldBe 0x11.toUByte()
|
||||||
|
rawSuffix shouldBe 0x24.toUByte()
|
||||||
|
|
||||||
|
isLeftPodMicrophone shouldBe true
|
||||||
|
isRightPodMicrophone shouldBe false
|
||||||
|
|
||||||
|
batteryLeftPodPercent shouldBe 1.0f
|
||||||
|
batteryRightPodPercent shouldBe null
|
||||||
|
|
||||||
|
isCaseCharging shouldBe false
|
||||||
|
isRightPodCharging shouldBe false
|
||||||
|
isLeftPodCharging shouldBe false
|
||||||
|
batteryCasePercent shouldBe null
|
||||||
|
podStyle.identifier shouldBe HasAppleColor.DeviceColor.UNKNOWN.name
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.BEATS_FIT_PRO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,9 +10,9 @@ class FakeAirPodsGen1Test : BaseAirPodsTest() {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `charging in box`() = runTest {
|
fun `charging in box`() = runTest {
|
||||||
create<FakeAirPodsGen1>("07 13 01 02 20 71 AA 37 32 00 10 00 64 64 FF 00 00 00 00 00 00") {
|
create<FakeAirPodsGen2>("07 13 01 0F 20 71 AA 37 32 00 10 00 64 64 FF 00 00 00 00 00 00") {
|
||||||
rawPrefix shouldBe 0x01.toUByte()
|
rawPrefix shouldBe 0x01.toUByte()
|
||||||
rawDeviceModel shouldBe 0x0220.toUShort()
|
rawDeviceModel shouldBe 0x0F20.toUShort()
|
||||||
rawStatus shouldBe 0x71.toUByte()
|
rawStatus shouldBe 0x71.toUByte()
|
||||||
rawPodsBattery shouldBe 0xAA.toUByte()
|
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||||
rawFlags shouldBe 0x3.toUShort()
|
rawFlags shouldBe 0x3.toUShort()
|
||||||
@@ -24,11 +24,17 @@ class FakeAirPodsGen1Test : BaseAirPodsTest() {
|
|||||||
batteryLeftPodPercent shouldBe 1.0f
|
batteryLeftPodPercent shouldBe 1.0f
|
||||||
batteryRightPodPercent shouldBe 1.0f
|
batteryRightPodPercent shouldBe 1.0f
|
||||||
|
|
||||||
|
isLeftPodInEar shouldBe false
|
||||||
|
isRightPodInEar shouldBe false
|
||||||
|
|
||||||
|
isLeftPodCharging shouldBe true
|
||||||
|
isRightPodCharging shouldBe true
|
||||||
|
|
||||||
isCaseCharging shouldBe false
|
isCaseCharging shouldBe false
|
||||||
|
|
||||||
batteryCasePercent shouldBe 0.7f
|
batteryCasePercent shouldBe 0.7f
|
||||||
|
|
||||||
model shouldBe PodDevice.Model.FAKE_AIRPODS_GEN1
|
model shouldBe PodDevice.Model.FAKE_AIRPODS_GEN2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple.misc
|
||||||
|
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
|
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class FakeAirPodsGen2Test : BaseAirPodsTest() {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `charging in box`() = runTest {
|
||||||
|
create<FakeAirPodsGen1>("07 13 01 02 20 71 AA 37 32 00 10 00 64 64 FF 00 00 00 00 00 00") {
|
||||||
|
rawPrefix shouldBe 0x01.toUByte()
|
||||||
|
rawDeviceModel shouldBe 0x0220.toUShort()
|
||||||
|
rawStatus shouldBe 0x71.toUByte()
|
||||||
|
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||||
|
rawFlags shouldBe 0x3.toUShort()
|
||||||
|
rawCaseBattery shouldBe 0x7.toUShort()
|
||||||
|
rawCaseLidState shouldBe 0x32.toUByte()
|
||||||
|
rawDeviceColor shouldBe 0x00.toUByte()
|
||||||
|
rawSuffix shouldBe 0x10.toUByte()
|
||||||
|
|
||||||
|
batteryLeftPodPercent shouldBe 1.0f
|
||||||
|
batteryRightPodPercent shouldBe 1.0f
|
||||||
|
|
||||||
|
isLeftPodInEar shouldBe false
|
||||||
|
isRightPodInEar shouldBe false
|
||||||
|
|
||||||
|
isLeftPodCharging shouldBe true
|
||||||
|
isRightPodCharging shouldBe true
|
||||||
|
|
||||||
|
isCaseCharging shouldBe false
|
||||||
|
|
||||||
|
batteryCasePercent shouldBe 0.7f
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.FAKE_AIRPODS_GEN1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,9 @@ class FakeAirPodsGen3Test : BaseAirPodsTest() {
|
|||||||
batteryLeftPodPercent shouldBe 1f
|
batteryLeftPodPercent shouldBe 1f
|
||||||
batteryRightPodPercent shouldBe 1f
|
batteryRightPodPercent shouldBe 1f
|
||||||
|
|
||||||
|
isLeftPodInEar shouldBe false
|
||||||
|
isRightPodInEar shouldBe false
|
||||||
|
|
||||||
isCaseCharging shouldBe false
|
isCaseCharging shouldBe false
|
||||||
isLeftPodCharging shouldBe true
|
isLeftPodCharging shouldBe true
|
||||||
isRightPodCharging shouldBe true
|
isRightPodCharging shouldBe true
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple.misc
|
||||||
|
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
|
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
class FakeAirPodsPro2Test : BaseAirPodsTest() {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `guessed data`() = runTest {
|
||||||
|
create<FakeAirPodsPro2>("07 13 01 14 20 75 AA 58 35 00 10 00 E4 E4 26 00 00 00 00 00 00") {
|
||||||
|
rawPrefix shouldBe 0x01.toUByte()
|
||||||
|
rawDeviceModel shouldBe 0x1420.toUShort()
|
||||||
|
rawStatus shouldBe 0x75.toUByte()
|
||||||
|
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||||
|
rawFlags shouldBe 0x5.toUShort()
|
||||||
|
rawCaseBattery shouldBe 0x8.toUShort()
|
||||||
|
rawCaseLidState shouldBe 0x35.toUByte()
|
||||||
|
rawDeviceColor shouldBe 0x00.toUByte()
|
||||||
|
rawSuffix shouldBe 0x10.toUByte()
|
||||||
|
|
||||||
|
batteryLeftPodPercent shouldBe 1.0f
|
||||||
|
batteryRightPodPercent shouldBe 1.0f
|
||||||
|
|
||||||
|
isCaseCharging shouldBe true
|
||||||
|
|
||||||
|
batteryCasePercent shouldBe 0.8f
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.FAKE_AIRPODS_PRO2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://discord.com/channels/548521543039189022/927235844127993866/1063027552765087754
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun `user supplied`() = runTest {
|
||||||
|
create<FakeAirPodsPro2>("07 13 01 14 20 75 AA 72 39 00 00 6F E4 E4 93 30 00 30 30 30 30") {
|
||||||
|
rawPrefix shouldBe 0x01.toUByte()
|
||||||
|
rawDeviceModel shouldBe 0x1420.toUShort()
|
||||||
|
rawStatus shouldBe 0x75.toUByte()
|
||||||
|
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||||
|
rawFlags shouldBe 0x7.toUShort()
|
||||||
|
rawCaseBattery shouldBe 0x2.toUShort()
|
||||||
|
rawCaseLidState shouldBe 0x39.toUByte()
|
||||||
|
rawDeviceColor shouldBe 0x00.toUByte()
|
||||||
|
rawSuffix shouldBe 0x0.toUByte()
|
||||||
|
|
||||||
|
batteryLeftPodPercent shouldBe 1.0f
|
||||||
|
batteryRightPodPercent shouldBe 1.0f
|
||||||
|
|
||||||
|
isCaseCharging shouldBe true
|
||||||
|
|
||||||
|
batteryCasePercent shouldBe 0.2f
|
||||||
|
|
||||||
|
model shouldBe PodDevice.Model.FAKE_AIRPODS_PRO2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,12 @@ class FakeAirPodsProTest : BaseAirPodsTest() {
|
|||||||
batteryLeftPodPercent shouldBe 1.0f
|
batteryLeftPodPercent shouldBe 1.0f
|
||||||
batteryRightPodPercent shouldBe 1.0f
|
batteryRightPodPercent shouldBe 1.0f
|
||||||
|
|
||||||
|
isLeftPodCharging shouldBe true
|
||||||
|
isRightPodCharging shouldBe true
|
||||||
|
|
||||||
|
isLeftPodInEar shouldBe false
|
||||||
|
isRightPodInEar shouldBe false
|
||||||
|
|
||||||
isCaseCharging shouldBe false
|
isCaseCharging shouldBe false
|
||||||
|
|
||||||
batteryCasePercent shouldBe 0.7f
|
batteryCasePercent shouldBe 0.7f
|
||||||
|
|||||||
@@ -4,26 +4,6 @@
|
|||||||
package="eu.darken.capod">
|
package="eu.darken.capod">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
|
||||||
|
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.BLUETOOTH"
|
|
||||||
android:maxSdkVersion="30" />
|
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.BLUETOOTH_ADMIN"
|
|
||||||
android:maxSdkVersion="30" />
|
|
||||||
|
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.ACCESS_COARSE_LOCATION"
|
|
||||||
android:maxSdkVersion="30" />
|
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.ACCESS_FINE_LOCATION"
|
|
||||||
android:maxSdkVersion="30" />
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.BLUETOOTH_SCAN"
|
|
||||||
android:usesPermissionFlags="neverForLocation" />
|
|
||||||
|
|
||||||
<uses-feature android:name="android.hardware.type.watch" />
|
<uses-feature android:name="android.hardware.type.watch" />
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import eu.darken.capod.R
|
|||||||
import eu.darken.capod.common.lists.binding
|
import eu.darken.capod.common.lists.binding
|
||||||
import eu.darken.capod.databinding.OverviewPodsDualItemBinding
|
import eu.darken.capod.databinding.OverviewPodsDualItemBinding
|
||||||
import eu.darken.capod.pods.core.*
|
import eu.darken.capod.pods.core.*
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
class DualPodsCardVH(parent: ViewGroup) :
|
class DualPodsCardVH(parent: ViewGroup) :
|
||||||
@@ -32,11 +32,13 @@ class DualPodsCardVH(parent: ViewGroup) :
|
|||||||
if (item.isMainPod) setTypeface(null, Typeface.BOLD)
|
if (item.isMainPod) setTypeface(null, Typeface.BOLD)
|
||||||
else setTypeface(null, Typeface.NORMAL)
|
else setTypeface(null, Typeface.NORMAL)
|
||||||
|
|
||||||
if (device is DualAirPods && item.showDebug) {
|
if (device is DualApplePods && item.showDebug) {
|
||||||
append(" [${device.primaryPod.name}]")
|
append(" [${device.primaryPod.name}]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
deviceIcon.setImageResource(device.iconRes)
|
deviceIcon.setImageResource(device.iconRes)
|
||||||
|
podLeftIcon.setImageResource(device.leftPodIcon)
|
||||||
|
podRightIcon.setImageResource(device.rightPodIcon)
|
||||||
|
|
||||||
lastSeen.text = device.lastSeenFormatted(item.now)
|
lastSeen.text = device.lastSeenFormatted(item.now)
|
||||||
|
|
||||||
@@ -91,6 +93,7 @@ class DualPodsCardVH(parent: ViewGroup) :
|
|||||||
// Case charge state
|
// Case charge state
|
||||||
device.apply {
|
device.apply {
|
||||||
if (this is HasCase) {
|
if (this is HasCase) {
|
||||||
|
podCaseIcon.setImageResource(caseIcon)
|
||||||
podCaseBatteryIcon.isGone = false
|
podCaseBatteryIcon.isGone = false
|
||||||
podCaseBatteryIcon.setImageResource(getBatteryDrawable(batteryCasePercent))
|
podCaseBatteryIcon.setImageResource(getBatteryDrawable(batteryCasePercent))
|
||||||
podCaseBatteryLabel.text = getBatteryLevelCase(context)
|
podCaseBatteryLabel.text = getBatteryLevelCase(context)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
android:layout_marginBottom="2dp"
|
android:layout_marginBottom="2dp"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:srcCompat="@drawable/ic_airpod_both_24" />
|
app:srcCompat="@drawable/devic_airpods_gen1_both" />
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:id="@+id/name"
|
android:id="@+id/name"
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon"
|
||||||
android:layout_marginHorizontal="2dp"
|
android:layout_marginHorizontal="2dp"
|
||||||
android:contentDescription="@string/pods_dual_left_label"
|
android:contentDescription="@string/pods_dual_left_label"
|
||||||
android:src="@drawable/ic_airpod_left_24"
|
android:src="@drawable/devic_airpods_gen1_left"
|
||||||
app:layout_constraintEnd_toStartOf="@+id/pod_left_wear_icon"
|
app:layout_constraintEnd_toStartOf="@+id/pod_left_wear_icon"
|
||||||
app:layout_constraintHorizontal_chainStyle="spread"
|
app:layout_constraintHorizontal_chainStyle="spread"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_case_icon"
|
android:id="@+id/pod_case_icon"
|
||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon"
|
||||||
android:src="@drawable/ic_airpod_case_24"
|
android:src="@drawable/devic_airpods_gen1_case"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
@@ -198,7 +198,7 @@
|
|||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon"
|
||||||
android:layout_marginHorizontal="2dp"
|
android:layout_marginHorizontal="2dp"
|
||||||
android:contentDescription="@string/pods_dual_right_label"
|
android:contentDescription="@string/pods_dual_right_label"
|
||||||
android:src="@drawable/ic_airpod_right_24"
|
android:src="@drawable/devic_airpods_gen1_right"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
app:layout_constraintHorizontal_chainStyle="spread_inside"
|
||||||
app:layout_constraintStart_toEndOf="@+id/pod_right_wear_icon"
|
app:layout_constraintStart_toEndOf="@+id/pod_right_wear_icon"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:srcCompat="@drawable/ic_device_generic_headphones" />
|
app:srcCompat="@drawable/devic_headphones_generic" />
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:id="@+id/name"
|
android:id="@+id/name"
|
||||||
|
|||||||
@@ -4,6 +4,10 @@
|
|||||||
<style name="PodInfoItemIcon" parent="TextAppearance.Material3.BodySmall">
|
<style name="PodInfoItemIcon" parent="TextAppearance.Material3.BodySmall">
|
||||||
<item name="android:layout_height">16dp</item>
|
<item name="android:layout_height">16dp</item>
|
||||||
<item name="android:layout_width">16dp</item>
|
<item name="android:layout_width">16dp</item>
|
||||||
|
<item name="android:tintMode">multiply</item>
|
||||||
|
<item name="android:tint">?android:attr/colorAccent</item>
|
||||||
|
<item name="tint">?android:attr/colorAccent</item>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="PodInfoItemText" parent="TextAppearance.Material3.BodyMedium">
|
<style name="PodInfoItemText" parent="TextAppearance.Material3.BodyMedium">
|
||||||
|
|||||||
@@ -5,29 +5,8 @@
|
|||||||
|
|
||||||
<uses-permission-sdk-23 android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
<uses-permission-sdk-23 android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.BLUETOOTH"
|
|
||||||
android:maxSdkVersion="30" />
|
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.BLUETOOTH_ADMIN"
|
|
||||||
android:maxSdkVersion="30" />
|
|
||||||
|
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.ACCESS_COARSE_LOCATION"
|
|
||||||
android:maxSdkVersion="30" />
|
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.ACCESS_FINE_LOCATION"
|
|
||||||
android:maxSdkVersion="30" />
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
|
||||||
<uses-permission
|
|
||||||
android:name="android.permission.BLUETOOTH_SCAN"
|
|
||||||
android:usesPermissionFlags="neverForLocation" />
|
|
||||||
|
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
|
|||||||
@@ -8,8 +8,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.OverviewPodsDualItemBinding
|
import eu.darken.capod.databinding.OverviewPodsDualItemBinding
|
||||||
import eu.darken.capod.pods.core.*
|
import eu.darken.capod.pods.core.*
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods.LidState
|
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
||||||
@@ -34,11 +34,13 @@ class DualPodsCardVH(parent: ViewGroup) :
|
|||||||
if (item.isMainPod) setTypeface(null, Typeface.BOLD)
|
if (item.isMainPod) setTypeface(null, Typeface.BOLD)
|
||||||
else setTypeface(null, Typeface.NORMAL)
|
else setTypeface(null, Typeface.NORMAL)
|
||||||
|
|
||||||
if (device is DualAirPods && item.showDebug) {
|
if (device is DualApplePods && item.showDebug) {
|
||||||
append(" [${device.primaryPod.name}]")
|
append(" [${device.primaryPod.name}]")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
deviceIcon.setImageResource(device.iconRes)
|
deviceIcon.setImageResource(device.iconRes)
|
||||||
|
podLeftIcon.setImageResource(device.leftPodIcon)
|
||||||
|
podRightIcon.setImageResource(device.rightPodIcon)
|
||||||
|
|
||||||
lastSeen.text = context.getString(R.string.last_seen_x, 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.text = context.getString(R.string.first_seen_x, device.firstSeenFormatted(item.now))
|
||||||
@@ -109,6 +111,7 @@ class DualPodsCardVH(parent: ViewGroup) :
|
|||||||
// Case charge state
|
// Case charge state
|
||||||
device.apply {
|
device.apply {
|
||||||
if (this is HasCase) {
|
if (this is HasCase) {
|
||||||
|
podCaseIcon.setImageResource(caseIcon)
|
||||||
podCaseBatteryIcon.isGone = false
|
podCaseBatteryIcon.isGone = false
|
||||||
podCaseBatteryIcon.setImageResource(getBatteryDrawable(batteryCasePercent))
|
podCaseBatteryIcon.setImageResource(getBatteryDrawable(batteryCasePercent))
|
||||||
podCaseBatteryLabel.text = getBatteryLevelCase(context)
|
podCaseBatteryLabel.text = getBatteryLevelCase(context)
|
||||||
@@ -126,7 +129,7 @@ class DualPodsCardVH(parent: ViewGroup) :
|
|||||||
|
|
||||||
// Case lid state
|
// Case lid state
|
||||||
device.apply {
|
device.apply {
|
||||||
if (this is DualAirPods) {
|
if (this is DualApplePods) {
|
||||||
podCaseLidLabel.text = when (caseLidState) {
|
podCaseLidLabel.text = when (caseLidState) {
|
||||||
LidState.OPEN -> context.getString(R.string.pods_case_status_open_label)
|
LidState.OPEN -> context.getString(R.string.pods_case_status_open_label)
|
||||||
LidState.CLOSED -> context.getString(R.string.pods_case_status_closed_label)
|
LidState.CLOSED -> context.getString(R.string.pods_case_status_closed_label)
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ class WidgetProvider : AppWidgetProvider() {
|
|||||||
setTextViewText(R.id.headphones_label, podDevice.getLabel(context))
|
setTextViewText(R.id.headphones_label, podDevice.getLabel(context))
|
||||||
|
|
||||||
// Left
|
// Left
|
||||||
|
setImageViewResource(R.id.pod_left_icon, podDevice.leftPodIcon)
|
||||||
setTextViewText(R.id.pod_left_label, podDevice.getBatteryLevelLeftPod(context))
|
setTextViewText(R.id.pod_left_label, podDevice.getBatteryLevelLeftPod(context))
|
||||||
setViewVisibility(
|
setViewVisibility(
|
||||||
R.id.pod_left_charging,
|
R.id.pod_left_charging,
|
||||||
@@ -183,6 +184,7 @@ class WidgetProvider : AppWidgetProvider() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Case
|
// Case
|
||||||
|
(podDevice as? HasCase)?.let { setImageViewResource(R.id.pod_case_icon, it.caseIcon) }
|
||||||
setTextViewText(R.id.pod_case_label, (podDevice as? HasCase)?.getBatteryLevelCase(context))
|
setTextViewText(R.id.pod_case_label, (podDevice as? HasCase)?.getBatteryLevelCase(context))
|
||||||
setViewVisibility(
|
setViewVisibility(
|
||||||
R.id.pod_case_charging,
|
R.id.pod_case_charging,
|
||||||
@@ -190,6 +192,7 @@ class WidgetProvider : AppWidgetProvider() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Right
|
// Right
|
||||||
|
setImageViewResource(R.id.pod_right_icon, podDevice.rightPodIcon)
|
||||||
setTextViewText(R.id.pod_right_label, podDevice.getBatteryLevelRightPod(context))
|
setTextViewText(R.id.pod_right_label, podDevice.getBatteryLevelRightPod(context))
|
||||||
setViewVisibility(
|
setViewVisibility(
|
||||||
R.id.pod_right_charging,
|
R.id.pod_right_charging,
|
||||||
@@ -216,6 +219,7 @@ class WidgetProvider : AppWidgetProvider() {
|
|||||||
setOnClickPendingIntent(R.id.widget_root, pendingIntent)
|
setOnClickPendingIntent(R.id.widget_root, pendingIntent)
|
||||||
|
|
||||||
setTextViewText(R.id.headphones_label, podDevice.getLabel(context))
|
setTextViewText(R.id.headphones_label, podDevice.getLabel(context))
|
||||||
|
setImageViewResource(R.id.headphones_icon, podDevice.iconRes)
|
||||||
setImageViewResource(R.id.headphones_battery_icon, getBatteryDrawable(podDevice.batteryHeadsetPercent))
|
setImageViewResource(R.id.headphones_battery_icon, getBatteryDrawable(podDevice.batteryHeadsetPercent))
|
||||||
setTextViewText(R.id.headphones_battery_label, podDevice.getBatteryLevelHeadset(context))
|
setTextViewText(R.id.headphones_battery_label, podDevice.getBatteryLevelHeadset(context))
|
||||||
|
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ class BluetoothEventReceiver : BroadcastReceiver() {
|
|||||||
@Inject @AppScope lateinit var appScope: CoroutineScope
|
@Inject @AppScope lateinit var appScope: CoroutineScope
|
||||||
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
log { "onReceive($context, $intent)" }
|
log(TAG) { "onReceive($context, $intent)" }
|
||||||
if (!EXPECTED_ACTIONS.contains(intent.action)) {
|
if (!EXPECTED_ACTIONS.contains(intent.action)) {
|
||||||
log(WARN) { "Unknown action: $intent.action" }
|
log(TAG, WARN) { "Unknown action: ${intent.action}" }
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ class BluetoothEventReceiver : BroadcastReceiver() {
|
|||||||
val supportedFeatures = ContinuityProtocol.BLE_FEATURE_UUIDS.filter { bluetoothDevice.hasFeature(it) }
|
val supportedFeatures = ContinuityProtocol.BLE_FEATURE_UUIDS.filter { bluetoothDevice.hasFeature(it) }
|
||||||
|
|
||||||
if (supportedFeatures.isEmpty()) {
|
if (supportedFeatures.isEmpty()) {
|
||||||
log { "Device has no features we support." }
|
log(TAG) { "Device has no features we support." }
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
log { "Device has the following we features we support $supportedFeatures" }
|
log { "Device has the following we features we support $supportedFeatures" }
|
||||||
@@ -49,7 +49,7 @@ class BluetoothEventReceiver : BroadcastReceiver() {
|
|||||||
|
|
||||||
val pending = goAsync()
|
val pending = goAsync()
|
||||||
appScope.launch {
|
appScope.launch {
|
||||||
log { "Starting monitor" }
|
log(TAG) { "Starting monitor" }
|
||||||
monitorControl.startMonitor(bluetoothDevice, forceStart = false)
|
monitorControl.startMonitor(bluetoothDevice, forceStart = false)
|
||||||
pending.finish()
|
pending.finish()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import android.widget.RemoteViews
|
|||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.pods.core.*
|
import eu.darken.capod.pods.core.*
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -16,26 +16,29 @@ class MonitorNotificationViewFactory @Inject constructor(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
fun createContentView(device: PodDevice): RemoteViews = when (device) {
|
fun createContentView(device: PodDevice): RemoteViews = when (device) {
|
||||||
is DualAirPods -> createDualApplePods(device)
|
is DualApplePods -> createDualApplePods(device)
|
||||||
is SingleApplePods -> createSingleApplePods(device)
|
is SingleApplePods -> createSingleApplePods(device)
|
||||||
else -> createUnknownDevice(device)
|
else -> createUnknownDevice(device)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createDualApplePods(device: DualAirPods): RemoteViews = RemoteViews(
|
private fun createDualApplePods(device: DualApplePods): RemoteViews = RemoteViews(
|
||||||
context.packageName,
|
context.packageName,
|
||||||
R.layout.monitor_notification_dual_pods_small
|
R.layout.monitor_notification_dual_pods_small
|
||||||
).apply {
|
).apply {
|
||||||
device.apply {
|
device.apply {
|
||||||
// Left
|
// Left
|
||||||
|
setImageViewResource(R.id.pod_left_icon, device.leftPodIcon)
|
||||||
setTextViewText(R.id.pod_left_label, getBatteryLevelLeftPod(context))
|
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_charging, if (isLeftPodCharging) View.VISIBLE else View.GONE)
|
||||||
setViewVisibility(R.id.pod_left_ear, if (isLeftPodInEar) View.VISIBLE else View.GONE)
|
setViewVisibility(R.id.pod_left_ear, if (isLeftPodInEar) View.VISIBLE else View.GONE)
|
||||||
|
|
||||||
// Case
|
// Case
|
||||||
|
setImageViewResource(R.id.pod_case_icon, device.caseIcon)
|
||||||
setTextViewText(R.id.pod_case_label, getBatteryLevelCase(context))
|
setTextViewText(R.id.pod_case_label, getBatteryLevelCase(context))
|
||||||
setViewVisibility(R.id.pod_case_charging, if (isCaseCharging) View.VISIBLE else View.GONE)
|
setViewVisibility(R.id.pod_case_charging, if (isCaseCharging) View.VISIBLE else View.GONE)
|
||||||
|
|
||||||
// Right
|
// Right
|
||||||
|
setImageViewResource(R.id.pod_right_icon, device.rightPodIcon)
|
||||||
setTextViewText(R.id.pod_right_label, getBatteryLevelRightPod(context))
|
setTextViewText(R.id.pod_right_label, getBatteryLevelRightPod(context))
|
||||||
setViewVisibility(R.id.pod_right_charging, if (isRightPodCharging) View.VISIBLE else View.GONE)
|
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)
|
setViewVisibility(R.id.pod_right_ear, if (isRightPodInEar) View.VISIBLE else View.GONE)
|
||||||
@@ -48,6 +51,7 @@ class MonitorNotificationViewFactory @Inject constructor(
|
|||||||
).apply {
|
).apply {
|
||||||
device.apply {
|
device.apply {
|
||||||
setTextViewText(R.id.headphones_label, getLabel(context))
|
setTextViewText(R.id.headphones_label, getLabel(context))
|
||||||
|
setImageViewResource(R.id.headphones_icon, device.iconRes)
|
||||||
setImageViewResource(R.id.headphones_battery_icon, getBatteryDrawable(batteryHeadsetPercent))
|
setImageViewResource(R.id.headphones_battery_icon, getBatteryDrawable(batteryHeadsetPercent))
|
||||||
setTextViewText(R.id.headphones_battery_label, getBatteryLevelHeadset(context))
|
setTextViewText(R.id.headphones_battery_label, getBatteryLevelHeadset(context))
|
||||||
if (this is HasEarDetection) {
|
if (this is HasEarDetection) {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ class MonitorNotifications @Inject constructor(
|
|||||||
setChannelId(NOTIFICATION_CHANNEL_ID)
|
setChannelId(NOTIFICATION_CHANNEL_ID)
|
||||||
setContentIntent(openPi)
|
setContentIntent(openPi)
|
||||||
priority = NotificationCompat.PRIORITY_LOW
|
priority = NotificationCompat.PRIORITY_LOW
|
||||||
setSmallIcon(R.drawable.ic_device_generic_earbuds)
|
setSmallIcon(R.drawable.devic_earbuds_generic_both)
|
||||||
setOngoing(true)
|
setOngoing(true)
|
||||||
setContentTitle(context.getString(R.string.app_name))
|
setContentTitle(context.getString(R.string.app_name))
|
||||||
}
|
}
|
||||||
@@ -63,7 +63,7 @@ class MonitorNotifications @Inject constructor(
|
|||||||
setStyle(NotificationCompat.BigTextStyle())
|
setStyle(NotificationCompat.BigTextStyle())
|
||||||
setContentTitle(context.getString(R.string.pods_none_label_short))
|
setContentTitle(context.getString(R.string.pods_none_label_short))
|
||||||
setSubText(context.getString(R.string.app_name))
|
setSubText(context.getString(R.string.app_name))
|
||||||
setSmallIcon(R.drawable.ic_device_generic_earbuds)
|
setSmallIcon(R.drawable.devic_earbuds_generic_both)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import eu.darken.capod.common.debug.autoreport.DebugSettings
|
|||||||
import eu.darken.capod.databinding.PopupNotificationDualPodsBinding
|
import eu.darken.capod.databinding.PopupNotificationDualPodsBinding
|
||||||
import eu.darken.capod.databinding.PopupNotificationSinglePodsBinding
|
import eu.darken.capod.databinding.PopupNotificationSinglePodsBinding
|
||||||
import eu.darken.capod.pods.core.*
|
import eu.darken.capod.pods.core.*
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@@ -26,13 +26,13 @@ class PopUpPodViewFactory @Inject constructor(
|
|||||||
private val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
private val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||||
|
|
||||||
fun createContentView(parent: ViewGroup, device: PodDevice): View = when (device) {
|
fun createContentView(parent: ViewGroup, device: PodDevice): View = when (device) {
|
||||||
is DualAirPods -> createDualApplePods(parent, device)
|
is DualApplePods -> createDualApplePods(parent, device)
|
||||||
// Unused, has no case to trigger reaction?
|
// Unused, has no case to trigger reaction?
|
||||||
is SingleApplePods -> createSingleApplePods(parent, device)
|
is SingleApplePods -> createSingleApplePods(parent, device)
|
||||||
else -> throw IllegalArgumentException("Unexpected device: $device")
|
else -> throw IllegalArgumentException("Unexpected device: $device")
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createDualApplePods(parent: ViewGroup, device: DualAirPods): View =
|
private fun createDualApplePods(parent: ViewGroup, device: DualApplePods): View =
|
||||||
PopupNotificationDualPodsBinding.inflate(layoutInflater, parent, false).apply {
|
PopupNotificationDualPodsBinding.inflate(layoutInflater, parent, false).apply {
|
||||||
device.apply {
|
device.apply {
|
||||||
podIcon.setImageResource(iconRes)
|
podIcon.setImageResource(iconRes)
|
||||||
@@ -41,14 +41,17 @@ class PopUpPodViewFactory @Inject constructor(
|
|||||||
signal.isInvisible = debugSettings.isDebugModeEnabled.value
|
signal.isInvisible = debugSettings.isDebugModeEnabled.value
|
||||||
|
|
||||||
// Left
|
// Left
|
||||||
|
podLeftIcon.setImageResource(device.leftPodIcon)
|
||||||
podLeftBatteryIcon.setImageResource(getBatteryDrawable(batteryLeftPodPercent))
|
podLeftBatteryIcon.setImageResource(getBatteryDrawable(batteryLeftPodPercent))
|
||||||
podLeftBatteryLabel.text = getBatteryLevelLeftPod(context)
|
podLeftBatteryLabel.text = getBatteryLevelLeftPod(context)
|
||||||
|
|
||||||
// Case
|
// Case
|
||||||
|
podCaseIcon.setImageResource(device.caseIcon)
|
||||||
podCaseBatteryIcon.setImageResource(getBatteryDrawable(batteryCasePercent))
|
podCaseBatteryIcon.setImageResource(getBatteryDrawable(batteryCasePercent))
|
||||||
podCaseBatteryLabel.text = getBatteryLevelCase(context)
|
podCaseBatteryLabel.text = getBatteryLevelCase(context)
|
||||||
|
|
||||||
// Right
|
// Right
|
||||||
|
podRightIcon.setImageResource(device.rightPodIcon)
|
||||||
podRightBatteryIcon.setImageResource(getBatteryDrawable(batteryRightPodPercent))
|
podRightBatteryIcon.setImageResource(getBatteryDrawable(batteryRightPodPercent))
|
||||||
podRightBatteryLabel.text = getBatteryLevelRightPod(context)
|
podRightBatteryLabel.text = getBatteryLevelRightPod(context)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
<?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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
style="@style/Notification.Container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
style="@style/Notification.Container"
|
|
||||||
android:layout_marginHorizontal="8dp"
|
android:layout_marginHorizontal="8dp"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal"
|
||||||
|
android:paddingVertical="8dp">
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
@@ -15,12 +16,13 @@
|
|||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_left_icon"
|
android:id="@+id/pod_left_icon"
|
||||||
style="@style/PodInfoItemIcon.Notification"
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
android:src="@drawable/ic_airpod_left_24" />
|
android:src="@drawable/devic_airpods_gen1_left" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/pod_left_label"
|
android:id="@+id/pod_left_label"
|
||||||
@@ -28,16 +30,21 @@
|
|||||||
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:paddingHorizontal="2dp"
|
||||||
tools:text="100%" />
|
tools:text="100%" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_left_charging"
|
android:id="@+id/pod_left_charging"
|
||||||
style="@style/PodInfoItemIcon.Notification"
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
|
android:layout_width="18dp"
|
||||||
|
android:layout_height="18dp"
|
||||||
android:src="@drawable/ic_baseline_power_24" />
|
android:src="@drawable/ic_baseline_power_24" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_left_ear"
|
android:id="@+id/pod_left_ear"
|
||||||
style="@style/PodInfoItemIcon.Notification"
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
|
android:layout_width="18dp"
|
||||||
|
android:layout_height="18dp"
|
||||||
android:src="@drawable/ic_baseline_hearing_24" />
|
android:src="@drawable/ic_baseline_hearing_24" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
@@ -49,12 +56,13 @@
|
|||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_case_icon"
|
android:id="@+id/pod_case_icon"
|
||||||
style="@style/PodInfoItemIcon.Notification"
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
android:src="@drawable/ic_airpod_case_24" />
|
android:src="@drawable/devic_airpods_gen1_case" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/pod_case_label"
|
android:id="@+id/pod_case_label"
|
||||||
@@ -62,11 +70,14 @@
|
|||||||
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:paddingHorizontal="4dp"
|
||||||
tools:text="100%" />
|
tools:text="100%" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_case_charging"
|
android:id="@+id/pod_case_charging"
|
||||||
style="@style/PodInfoItemIcon.Notification"
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
|
android:layout_width="18dp"
|
||||||
|
android:layout_height="18dp"
|
||||||
android:src="@drawable/ic_baseline_power_24" />
|
android:src="@drawable/ic_baseline_power_24" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
@@ -78,12 +89,13 @@
|
|||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_right_icon"
|
android:id="@+id/pod_right_icon"
|
||||||
style="@style/PodInfoItemIcon.Notification"
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
android:src="@drawable/ic_airpod_right_24" />
|
android:src="@drawable/devic_airpods_gen1_right" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/pod_right_label"
|
android:id="@+id/pod_right_label"
|
||||||
@@ -91,16 +103,21 @@
|
|||||||
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:paddingHorizontal="2dp"
|
||||||
tools:text="100%" />
|
tools:text="100%" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_right_charging"
|
android:id="@+id/pod_right_charging"
|
||||||
style="@style/PodInfoItemIcon.Notification"
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
|
android:layout_width="18dp"
|
||||||
|
android:layout_height="18dp"
|
||||||
android:src="@drawable/ic_baseline_power_24" />
|
android:src="@drawable/ic_baseline_power_24" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_right_ear"
|
android:id="@+id/pod_right_ear"
|
||||||
style="@style/PodInfoItemIcon.Notification"
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
|
android:layout_width="18dp"
|
||||||
|
android:layout_height="18dp"
|
||||||
android:src="@drawable/ic_baseline_hearing_24" />
|
android:src="@drawable/ic_baseline_hearing_24" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
style="@style/Notification.Container"
|
style="@style/Notification.Container"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingVertical="8dp"
|
||||||
android:gravity="center_horizontal">
|
android:gravity="center_horizontal">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
android:layout_marginHorizontal="8dp"
|
android:layout_marginHorizontal="8dp"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
@@ -37,11 +39,15 @@
|
|||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/headphones_charging"
|
android:id="@+id/headphones_charging"
|
||||||
style="@style/PodInfoItemIcon.Notification"
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
|
android:layout_width="18dp"
|
||||||
|
android:layout_height="18dp"
|
||||||
android:src="@drawable/ic_baseline_power_24" />
|
android:src="@drawable/ic_baseline_power_24" />
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/headphones_worn"
|
android:id="@+id/headphones_worn"
|
||||||
style="@style/PodInfoItemIcon.Notification"
|
style="@style/PodInfoItemIcon.Notification"
|
||||||
|
android:layout_width="18dp"
|
||||||
|
android:layout_height="18dp"
|
||||||
android:src="@drawable/ic_baseline_hearing_24" />
|
android:src="@drawable/ic_baseline_hearing_24" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<?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:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
style="@style/Notification.Container"
|
style="@style/Notification.Container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
android:paddingVertical="8dp"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
@@ -11,8 +13,8 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="8dp"
|
android:layout_marginHorizontal="8dp"
|
||||||
android:drawableStart="@drawable/ic_baseline_question_mark_24"
|
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
tools:text="100%" />
|
tools:text="100%"
|
||||||
|
app:drawableStartCompat="@drawable/ic_baseline_question_mark_24" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -20,10 +20,11 @@
|
|||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
android:layout_height="24dp"
|
android:layout_height="24dp"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/name"
|
app:layout_constraintBottom_toBottomOf="@id/name"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@id/name"
|
app:layout_constraintTop_toTopOf="@id/name"
|
||||||
app:srcCompat="@drawable/ic_airpod_both_24" />
|
app:srcCompat="@drawable/devic_airpods_gen1_both" />
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:id="@+id/name"
|
android:id="@+id/name"
|
||||||
@@ -118,7 +119,7 @@
|
|||||||
android:id="@+id/pod_left_icon"
|
android:id="@+id/pod_left_icon"
|
||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon"
|
||||||
android:contentDescription="@string/pods_dual_left_label"
|
android:contentDescription="@string/pods_dual_left_label"
|
||||||
android:src="@drawable/ic_airpod_left_24"
|
android:src="@drawable/devic_airpods_gen1_left"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/pod_left_label"
|
app:layout_constraintBottom_toBottomOf="@id/pod_left_label"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@id/pod_left_label" />
|
app:layout_constraintTop_toTopOf="@id/pod_left_label" />
|
||||||
@@ -230,7 +231,7 @@
|
|||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_case_icon"
|
android:id="@+id/pod_case_icon"
|
||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon"
|
||||||
android:src="@drawable/ic_airpod_case_24"
|
android:src="@drawable/devic_airpods_gen1_case"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/pod_case_label"
|
app:layout_constraintBottom_toBottomOf="@id/pod_case_label"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@id/pod_case_label" />
|
app:layout_constraintTop_toTopOf="@id/pod_case_label" />
|
||||||
@@ -324,7 +325,7 @@
|
|||||||
android:id="@+id/pod_right_icon"
|
android:id="@+id/pod_right_icon"
|
||||||
style="@style/PodInfoItemIcon"
|
style="@style/PodInfoItemIcon"
|
||||||
android:contentDescription="@string/pods_dual_right_label"
|
android:contentDescription="@string/pods_dual_right_label"
|
||||||
android:src="@drawable/ic_airpod_right_24"
|
android:src="@drawable/devic_airpods_gen1_right"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/pod_right_label"
|
app:layout_constraintBottom_toBottomOf="@id/pod_right_label"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@id/pod_right_label" />
|
app:layout_constraintTop_toTopOf="@id/pod_right_label" />
|
||||||
|
|||||||
@@ -19,11 +19,12 @@
|
|||||||
android:id="@+id/device_icon"
|
android:id="@+id/device_icon"
|
||||||
android:layout_width="24dp"
|
android:layout_width="24dp"
|
||||||
android:layout_height="24dp"
|
android:layout_height="24dp"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/name"
|
app:layout_constraintBottom_toBottomOf="@id/name"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="@id/name"
|
app:layout_constraintTop_toTopOf="@id/name"
|
||||||
app:srcCompat="@drawable/ic_device_generic_headphones" />
|
app:srcCompat="@drawable/devic_headphones_generic" />
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:id="@+id/name"
|
android:id="@+id/name"
|
||||||
|
|||||||
@@ -7,9 +7,10 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_icon"
|
android:id="@+id/pod_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
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"
|
android:src="@drawable/devic_earbuds_generic_both"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/pod_label"
|
app:layout_constraintBottom_toBottomOf="@id/pod_label"
|
||||||
app:layout_constraintEnd_toStartOf="@id/pod_label"
|
app:layout_constraintEnd_toStartOf="@id/pod_label"
|
||||||
app:layout_constraintHorizontal_bias="0.0"
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
@@ -54,10 +55,11 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_left_icon"
|
android:id="@+id/pod_left_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
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:src="@drawable/ic_airpod_left_24"
|
android:src="@drawable/devic_airpods_gen1_left"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/pod_left_battery_label"
|
app:layout_constraintBottom_toBottomOf="@id/pod_left_battery_label"
|
||||||
app:layout_constraintEnd_toStartOf="@id/pod_left_battery_icon"
|
app:layout_constraintEnd_toStartOf="@id/pod_left_battery_icon"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
@@ -65,6 +67,7 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@id/pod_left_battery_icon"
|
android:id="@id/pod_left_battery_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:src="@drawable/ic_baseline_battery_unknown_24"
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
@@ -100,10 +103,11 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_case_icon"
|
android:id="@+id/pod_case_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
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:src="@drawable/ic_airpod_case_24"
|
android:src="@drawable/devic_airpods_gen1_case"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/pod_case_battery_label"
|
app:layout_constraintBottom_toBottomOf="@id/pod_case_battery_label"
|
||||||
app:layout_constraintEnd_toStartOf="@id/pod_case_battery_icon"
|
app:layout_constraintEnd_toStartOf="@id/pod_case_battery_icon"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
@@ -111,6 +115,7 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@id/pod_case_battery_icon"
|
android:id="@id/pod_case_battery_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:src="@drawable/ic_baseline_battery_unknown_24"
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
@@ -145,10 +150,11 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/pod_right_icon"
|
android:id="@+id/pod_right_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
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:src="@drawable/ic_airpod_right_24"
|
android:src="@drawable/devic_airpods_gen1_right"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/pod_right_battery_label"
|
app:layout_constraintBottom_toBottomOf="@id/pod_right_battery_label"
|
||||||
app:layout_constraintEnd_toStartOf="@id/pod_right_battery_icon"
|
app:layout_constraintEnd_toStartOf="@id/pod_right_battery_icon"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
@@ -156,6 +162,7 @@
|
|||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@id/pod_right_battery_icon"
|
android:id="@id/pod_right_battery_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:src="@drawable/ic_baseline_battery_unknown_24"
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
|
|||||||