Compare commits

...
19 Commits
Author SHA1 Message Date
darken 48fd3543e3 Release: 2.7.2-rc0 2023-01-30 13:38:53 +01:00
Matthias Urhahn 41985794c2 Adjust supported AirPods Max features. (#86)
Remove state detection (was UNKNOWN when it should have been MUSIC).
Add PodStyle detection.
2023-01-30 13:37:40 +01:00
Matthias Urhahn 87a92fd001 Fix feature display for single pods inside the app. (#87) 2023-01-30 13:37:31 +01:00
darken f6f53f2a15 Release: 2.7.1-rc1 2023-01-28 22:14:47 +01:00
Matthias Urhahn 2531769b79 Update translations (#85)
Add arabic and update chinese (TW)
2023-01-28 22:12:25 +01:00
darken e9fa9f6cb8 Release: 2.7.0-rc0 2023-01-25 16:54:32 +01:00
Matthias Urhahn b98ca86886 Faster CI checks (#83)
* Parallelize CI code checks

* Only assemble debug variant, otherwise we need to setup signing.
2023-01-25 16:47:46 +01:00
Matthias Urhahn d917c8ffe6 New compatibility option for receiving BLE data (#82)
* Improve compat options: Add alternative method for receiving BLE scan results via PendingIntents

* Add missing logtags

* Reduce log spam

* Make the linter happy
2023-01-25 16:27:29 +01:00
Matthias Urhahn 5d44737199 Merge pull request #80 from d4rken-org/compatibility_options
Improve compatibility options
2023-01-25 14:56:10 +01:00
Matthias Urhahn 6e8693a6b0 Merge pull request #81 from d4rken-org/modularize_permissions
Move shared permissions into the common module
2023-01-25 14:55:54 +01:00
darken 6d9e7404a1 Improve compatibility options 2023-01-25 14:46:20 +01:00
darken 70ceb389b5 Move shared permissions into the common module. 2023-01-25 14:44:38 +01:00
darken 832e364a2f Update translations 2023-01-25 09:26:50 +01:00
darken 51f9c5c7d8 Release: 2.6.1-rc0 2023-01-20 12:55:28 +01:00
Matthias Urhahn 15141c8528 More granular pod features (#76)
* Make pod features more granular
Don't assume all AirPods/Beats return a correct connection state.

* Refactoring

* Fix refactoring regression

* AirPods Gen1 don't support state detection
2023-01-18 22:15:38 +01:00
Matthias Urhahn 0cdac10fd4 Update README.md 2023-01-14 16:45:11 +01:00
darken aaab548bfd Release: 2.6.0-rc0 2023-01-14 16:44:16 +01:00
Matthias Urhahn 46611b5606 Add preliminary support for Beats Fit Pro (#75) 2023-01-14 16:35:04 +01:00
Matthias Urhahn fdbdb46577 Overhaul play/pause reaction code (#74) 2023-01-14 16:21:57 +01:00
63 changed files with 948 additions and 435 deletions
+30 -12
View File
@@ -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 }}
+1
View File
@@ -34,6 +34,7 @@ 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 1. Generation 🎭
* Fake AirPods 2. Generation 🎭 * Fake AirPods 2. Generation 🎭
* Fake AirPods 3. Generation 🎭 * Fake AirPods 3. Generation 🎭
+1 -1
View File
@@ -1 +1 @@
2.5.3-rc1 20503010 2.7.2-rc0 20702000
+32 -1
View File
@@ -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")
} }
@@ -104,6 +104,9 @@ interface PodDevice {
@Json(name = "beats.powerbeats.pro") POWERBEATS_PRO( @Json(name = "beats.powerbeats.pro") POWERBEATS_PRO(
"Power Beats Pro" "Power Beats Pro"
), ),
@Json(name = "beats.fit.pro") BEATS_FIT_PRO(
"Beats Fit Pro"
),
@Json(name = "fakes.tws.i99999") FAKE_AIRPODS_GEN1( @Json(name = "fakes.tws.i99999") FAKE_AIRPODS_GEN1(
"AirPods (Gen 1)? \uD83C\uDFAD", "AirPods (Gen 1)? \uD83C\uDFAD",
R.drawable.devic_airpods_gen1_both, R.drawable.devic_airpods_gen1_both,
@@ -27,6 +27,7 @@ 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 fakeAirPodsGen2(factory: FakeAirPodsGen2.Factory): ApplePodsFactory<out ApplePods> @Binds @IntoSet abstract fun fakeAirPodsGen2(factory: FakeAirPodsGen2.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) {
@@ -6,6 +6,7 @@ import eu.darken.capod.common.isBitSet
import eu.darken.capod.pods.core.HasEarDetection import eu.darken.capod.pods.core.HasEarDetection
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.HasAppleColor
import eu.darken.capod.pods.core.apple.SingleApplePods import eu.darken.capod.pods.core.apple.SingleApplePods
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
@@ -21,12 +22,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, HasAppleColor {
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)
@@ -6,8 +6,8 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.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
@@ -24,7 +24,7 @@ data class AirPodsPro(
private val rssiAverage: Int? = null, private val rssiAverage: Int? = null,
private val cachedBatteryPercentage: Float? = null, private val cachedBatteryPercentage: Float? = null,
private val cachedCaseState: LidState? = null private val cachedCaseState: LidState? = null
) : DualAirPods { ) : DualApplePods, HasStateDetectionAirPods {
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO
@@ -51,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) {
@@ -6,8 +6,8 @@ import eu.darken.capod.common.bluetooth.BleScanResult
import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.PodDevice
import eu.darken.capod.pods.core.apple.ApplePods import eu.darken.capod.pods.core.apple.ApplePods
import eu.darken.capod.pods.core.apple.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
@@ -24,7 +24,7 @@ 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
@@ -51,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
@@ -11,7 +11,7 @@ import eu.darken.capod.common.upperNibble
import eu.darken.capod.pods.core.* import eu.darken.capod.pods.core.*
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.DualAirPods import eu.darken.capod.pods.core.apple.DualApplePods
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
import java.time.Instant import java.time.Instant
import javax.inject.Inject import javax.inject.Inject
@@ -30,7 +30,7 @@ data class FakeAirPodsPro2 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,
) : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase { ) : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase {
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_PRO2 override val model: PodDevice.Model = PodDevice.Model.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)
} }
@@ -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>
@@ -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,14 +1,61 @@
<?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="app_name_foss">CAPod FOSS</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>
<string name="overview_nomaindevice_label">لا يوجد جهاز أساسي</string>
<string name="overview_nomaindevice_description">ِمن غير المحتمل أن تكون جميع الأجهزة المكشوفة مِلكك، شغل جهازك وقم بتوصيله أو اضبط الإعدادات.</string>
<string name="overview_bluetooth_disabled_label">تم تعطيل البلوتوت</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_access_fine_location_label">الوصول إلى موقع جيد</string> <string name="permission_access_fine_location_label">الوصول إلى موقع جيد</string>
<string name="permission_access_fine_location_description">يستخدم CAPod إذن \"الموقع الدقيق\" لتلقي بيانات البلوتوث منخفضة الطاقة. تستخدم سماعات الرأس بلوتوث منخفض الطاقة لبث حالتها. لن يستخدم هذا التطبيق بيانات البلوتوث لتحديد موقعك.</string> <string name="permission_access_fine_location_description">يستخدم CAPod إذن \"الموقع الدقيق\" لتلقي بيانات البلوتوث منخفضة الطاقة. تستخدم سماعات الرأس بلوتوث منخفض الطاقة لبث حالتها. لن يستخدم هذا التطبيق بيانات البلوتوث لتحديد موقعك.</string>
<string name="permission_background_location_label">الوصول إلى الموقع في الخلفية</string> <string name="permission_background_location_label">الوصول إلى الموقع في الخلفية</string>
<string name="permission_background_location_description">يستخدم تطبيق CAPod \"الوصول إلى موقع الخلفية\" و\"الاتصال التلقائي\" أثناء إغلاق التطبيق. يتيح الوصول إلى الموقع في الخلفية التطبيق في تلقي بيانات البلوتوت منخفضة الطاقة أثناء وجودها في الخلفية. هذا التطبيق لن يستخدم بيانات البلوتوت لتحديد موقعك.</string>
<string name="permission_ignore_battery_optimizations_label">تعطيل خاصية تحسينات البطارية</string>
<string name="permission_ignore_battery_optimizations_description">تحسينات البطارية تمنع هذا التطبيق من تلقي بيانات البلوتوت بشكل موثوق أثناء وجود التطبيق في الخلفية.</string>
<string name="permission_required_title">الإذن التالي مطلوب:</string>
<string name="permission_system_alert_window_label">نافذة تنبيه النظام</string>
<string name="permission_system_alert_window_description">اسم لتطبيق CAPod بالظهور فوق التطبيقات الأخرى لتفعيل مِيزة \"الإشعارات المنبثقة\".</string>
<string name="settings_scanner_mode_lowpower_label">طاقة مُنخفضة</string>
<string name="settings_scanner_mode_balanced_label">متوازن</string>
<string name="settings_scanner_mode_lowlatency_label">وقت استجابة مُنخفض</string>
<string name="settings_monitor_mode_manual_label">عندما يُفتح التطبيق</string>
<string name="settings_monitor_mode_automatic_label">عندما يتم توصيل الجهاز</string>
<string name="settings_monitor_mode_always_label">دائمًا</string>
<string name="settings_reaction_autoconnect_whenseen_label">عندما يُنظر إليه</string>
<string name="settings_reaction_autoconnect_caseopen_label">العُلبة مفتوحة</string>
<string name="settings_reaction_autoconnect_inear_label">في الأذن</string>
<string name="pods_dual_left_label">السماعة اليسرى</string>
<string name="pods_dual_right_label">السماعة اليمنى</string>
<string name="pods_case_label">العلبة</string>
<string name="pods_case_status_open_label">فتح</string>
<string name="pods_case_status_closed_label">مُغلق</string>
<string name="pods_connection_state_disconnected_label">غير مُتصل بجهاز</string>
<string name="pods_connection_state_idle_label">مُتصل بجهاز، لكنه فارغ</string>
<string name="pods_connection_state_music_label">في وضع الموسيقى</string>
<string name="pods_connection_state_call_label">في وضع الاتصال</string>
<string name="pods_connection_state_ringing_label">الرنين</string>
<string name="pods_connection_state_hanging_up_label">إنهاء المكالمة</string>
<string name="pods_connection_state_unknown_label">حالة اتصال غير معروفة</string>
<string name="pods_unknown_raw_data_label">مُسودة بيانات</string>
<string name="pods_unknown_label">جهاز غير معروف</string>
<string name="pods_unknown_contact_dev">هذا جهاز غير معروف لكنه يستخدم صيغة رسالة مشابهة. دعنا نضيف دعما لها. اتصل بي :)</string>
<string name="pods_none_label_short">لا يوجد جهاز</string>
<string name="pods_charging_label">في حالة الشحن</string>
<string name="pods_inear_label">في الأذن</string>
<string name="pods_microphone_label">ميكروفون</string>
<string name="pods_yours">الخاصة بك</string>
<string name="headset_being_worn_label">يتم ارتداؤها</string>
<string name="pods_case_unknown_state">حالة غير معروفة</string>
<string name="last_seen_x">آخر ظهور: %s</string>
<string name="first_seen_x">أول ظهور:%s</string>
</resources> </resources>
@@ -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>
@@ -53,7 +53,7 @@
<string name="pods_charging_label">充電中</string> <string name="pods_charging_label">充電中</string>
<string name="pods_inear_label">在耳中</string> <string name="pods_inear_label">在耳中</string>
<string name="pods_microphone_label">麥克風</string> <string name="pods_microphone_label">麥克風</string>
<string name="pods_yours"></string> <string name="pods_yours"></string>
<string name="headset_being_worn_label">正被佩戴</string> <string name="headset_being_worn_label">正被佩戴</string>
<string name="pods_case_unknown_state">未知狀態</string> <string name="pods_case_unknown_state">未知狀態</string>
<string name="last_seen_x">最後連線:%s</string> <string name="last_seen_x">最後連線:%s</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
} }
@@ -2,6 +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.HasAppleColor
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
@@ -53,4 +54,27 @@ class AirPodsMaxTest : BaseAirPodsTest() {
isHeadphonesBeingWorn shouldBe true isHeadphonesBeingWorn shouldBe true
} }
} }
@Test
fun `some dude at the gym`() = runTest {
create<AirPodsMax>("07 19 01 0A 20 23 07 80 03 03 65 1F 28 32 D0 D9 71 43 00 9A 40 E7 6B EA 6C 2C FB") {
rawPrefix shouldBe 0x01.toUByte()
rawDeviceModel shouldBe 0x0A20.toUShort()
rawStatus shouldBe 0x23.toUByte()
rawPodsBattery shouldBe 0x07.toUByte()
rawFlags shouldBe 0x8.toUShort()
rawCaseBattery shouldBe 0x0.toUShort()
rawCaseLidState shouldBe 0x03.toUByte()
rawDeviceColor shouldBe 0x03.toUByte()
rawSuffix shouldBe 0x65.toUByte()
batteryHeadsetPercent shouldBe 0.7f
isHeadsetBeingCharged shouldBe false
isHeadphonesBeingWorn shouldBe true
podStyle shouldBe HasAppleColor.DeviceColor.BLUE
}
}
} }
@@ -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
}
}
}
-20
View File
@@ -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,7 +32,7 @@ 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}]")
} }
} }
+5 -1
View File
@@ -1,2 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources></resources> <resources>
<string name="foss_upgrade_donate_label">تبرُّع</string>
<string name="foss_upgrade_alreadydonated_label">أنا متبرع بالفعل</string>
<string name="foss_upgrade_no_money_label">أُنفق كل أموالي على AirPods</string>
</resources>
+4 -1
View File
@@ -1,2 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources></resources> <resources>
<string name="upgrades_gplay_unavailable_error">خدمات Google Play غير متاحة.</string>
<string name="upgrades_no_purchases_found_check_account">لم يتم العثور على أي مشتريات. هل تستخدم الحساب الصحيح؟</string>
</resources>
-21
View File
@@ -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,7 +34,7 @@ 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}]")
} }
} }
@@ -129,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)
@@ -42,8 +42,8 @@ class SinglePodsCardVH(parent: ViewGroup) :
// Charge state // Charge state
device.apply { device.apply {
if (this is HasChargeDetection) { if (this is HasChargeDetection) {
chargingIcon.isInvisible = isHeadsetBeingCharged chargingIcon.isInvisible = !isHeadsetBeingCharged
chargingLabel.isInvisible = isHeadsetBeingCharged chargingLabel.isInvisible = !isHeadsetBeingCharged
} else { } else {
chargingIcon.isGone = true chargingIcon.isGone = true
chargingLabel.isGone = true chargingLabel.isGone = true
@@ -53,8 +53,8 @@ class SinglePodsCardVH(parent: ViewGroup) :
// Has ear detection // Has ear detection
device.apply { device.apply {
if (this is HasEarDetection) { if (this is HasEarDetection) {
wearIcon.isInvisible = isBeingWorn wearIcon.isInvisible = !isBeingWorn
wearLabel.isInvisible = isBeingWorn wearLabel.isInvisible = !isBeingWorn
} else { } else {
wearIcon.isGone = true wearIcon.isGone = true
wearLabel.isGone = true wearLabel.isGone = true
@@ -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,12 +16,12 @@ 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 {
@@ -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)
+63
View File
@@ -7,10 +7,73 @@
<string name="general_upgrade_action">ترقية</string> <string name="general_upgrade_action">ترقية</string>
<string name="general_check_action">فحص</string> <string name="general_check_action">فحص</string>
<string name="general_close_action">إغلاق</string> <string name="general_close_action">إغلاق</string>
<string name="upgrade_capod_label">ترقية CAPod</string>
<string name="upgrade_capod_description">أُحصل على مِيزات إضافية وادعم المُطور.</string>
<string name="settings_monitor_mode_label">وضع المراقب</string>
<string name="settings_monitor_mode_description">في أي ظرف تريد أن يراقب فيه هذا التطبيق ببانات البلوتوت.</string>
<string name="settings_scanner_mode_label">وضع الماسح</string>
<string name="settings_scanner_mode_description">هل يجب على ماسح البيانات منخفض الطاقة التي تعمل بتقنية البلوتوت إعطاء الأولوية للأداء أم الحفاظ على الطاقة؟</string>
<string name="settings_autopause_label">الإيقاف التلقائي</string>
<string name="settings_autopause_description">إيقاف الصوت مؤقتًا عند إزالة الجهاز من أذنك.</string>
<string name="settings_showall_label">عرض كل الأجهزة</string>
<string name="settings_showall_description">عرض أجهزة الآخرين القريبة منك.</string>
<string name="settings_autopplay_label">التشغيل التلقائي</string>
<string name="settings_autoplay_description">بدأ تشغيل الصوت عند ارتداء الجهاز.</string>
<string name="settings_fake_data_label">بيانات مزيفة</string>
<string name="settings_fake_data_description">عرض بيانات مزيفة، مما يعني محاكاة أجهزة غير موجودة.</string>
<string name="settings_debug_label">إعدادات التصحيح</string>
<string name="settings_debug_description">إعدادات إضافية للمساعدة في استكشاف مشكلات التطبيق وإصلاحها.</string>
<string name="settings_signal_minimum_label">الحد الأدنى من جودة الإشارة</string>
<string name="settings_signal_minimum_description">الحد الأدنى من جودة الإشارة التي يجب أن يَعتبرها الجهاز لك.</string>
<string name="settings_autoconnect_label">الاتصال التلقائي</string>
<string name="settings_autoconnect_description">إذا لم يتصل الأندرويد تلقائيًا. فيمكننا أنة نطلبه أيضًا. سيؤدي هذا إلى ضبط إعداد وضع المراقب إلى \"دائما\".</string>
<string name="settings_autoconnect_condition_label">حالة الاتصال التلقائي</string>
<string name="settings_autoconnect_condition_description">متى يجب أن نحاول الاتصال بجهازك؟</string>
<string name="settings_reaction_label">التفاعل</string>
<string name="settings_reaction_description">التفاعل مع الأحداث والسلوكيات.</string>
<string name="settings_category_yourdevice_label">جهازك</string>
<string name="settings_maindevice_address_label">عنوان جهازك</string>
<string name="settings_maindevice_address_description">عنوان جهازك المقترن. يستخدم التطبيق هذا لتحديد متى يكون متصلا بهاتفك.</string>
<string name="settings_maindevice_address_none">غير موجود</string>
<string name="settings_maindevice_model_label">طِراز جهازك</string>
<string name="settings_maindevice_model_description">طِراز جهازك الرئيسي. يساعد هذا التطبيق في التعرف على جهازك عندما لا يكون متصلاً بهاتفك.</string>
<string name="settings_popup_caseopen_label">عرض إشعار منبثق</string>
<string name="settings_popup_caseopen_description">عرض إشعار منبثق عند فتح علبة الجهاز (تجريبي).</string>
<string name="settings_onepod_mode_label">وضعية السماعة الواحدة</string>
<string name="settings_onepod_mode_description">ليس بالضرورة ارتداء كِلتا السمَّاعتين، ارتداء سمَّاعة واحدة يكفي لتحريك ردود الفعل.</string>
<string name="notification_channel_device_status_label">حالة الجهاز</string> <string name="notification_channel_device_status_label">حالة الجهاز</string>
<string name="debug_debuglog_size_label">الحجم</string> <string name="debug_debuglog_size_label">الحجم</string>
<string name="debug_debuglog_size_compressed_label">الحجم المضغوط</string> <string name="debug_debuglog_size_compressed_label">الحجم المضغوط</string>
<string name="debug_notification_channel_label">اشعارات التصحيح</string> <string name="debug_notification_channel_label">اشعارات التصحيح</string>
<string name="debug_debuglog_file_label">ملف مسجل</string> <string name="debug_debuglog_file_label">ملف مسجل</string>
<string name="debug_debuglog_record_action">ملف التصحيح مسجل</string> <string name="debug_debuglog_record_action">ملف التصحيح مسجل</string>
<string name="settings_support_installid_label">مُعرف التثبيت</string>
<string name="settings_support_installid_desc">تقارير الأخطاء والمشاكل التلقائية تكون مجهول المصدر. شارك مُعرف التثبيت الخاص بك إذا احتاج المطور إلى العثور على الأخطاء والمشاكل الخاصة بك.</string>
<string name="settings_support_label">الدعم</string>
<string name="settings_support_description">إن كنت بحاجة لبعض المساعدة.</string>
<string name="issue_tracker_label">تَعقب المشكلة أو الخطأ</string>
<string name="issue_tracker_description">أداة تعقب المشكلات العامة لتقارير الأخطاء وطلبات إضافة الميزات للتطبيق (باللغة الإنجليزية فقط).</string>
<string name="discord_label">ديسكورد</string>
<string name="discord_description">مكان لطرح الأسئلة.</string>
<string name="changelog_label">سِجل التغيير</string>
<string name="settings_label">الإعدادات</string>
<string name="settings_privacy_policy_label">سياسة الخصوصية</string>
<string name="settings_privacy_policy_desc">التعامل مع البيانات بمسؤولية.</string>
<string name="settings_licenses_label">التراخيص</string>
<string name="settings_category_other_label">أخرى</string>
<string name="settings_general_label">الإعدادات</string>
<string name="settings_general_description">التعديلات العامة التي تؤثر على التطبيق بِرُمَّته.</string>
<string name="settings_acknowledgements_label">شكر وتقدير</string>
<string name="settings_debug_autoreports_label">إرسال تقارير الأخطاء تلقائيًا</string>
<string name="settings_debug_autoreports_description">الإبلاغ عن المشكلات تلقائيًا. كمثال: تفاصيل حول تعطُّل التطبيق حتى أتمكن من معرفة كيفية إصلاحه.</string>
<string name="settings_debug_mode_label">وضع التصحيح</string>
<string name="settings_debug_mode_description">عرض معلومات إضافية لاستكشاف المشكلات وإصلاحها.</string>
<string name="settings_blescanner_unfiltered_label">بيانات BLE الغير المفلترة</string>
<string name="settings_blescanner_unfiltered_description">احذف أي عوامل فَلترة من الماسح BLE لإظهار جميع بيانات BLE التي يتم بثُّها. هذا مفيد في إضافة دعم لأنواع سماعات رأس جديدة.</string>
<string name="settings_compatibility_mode_label">وضع التوافق</string>
<string name="settings_compatibility_mode_description">تعطيل التحسينات لتحسين التوافق. جرب هذه الخاصية إذا كنت لا ترى أي بيانات.</string>
<string name="help_translate_label">التَّرْجَمَةً</string>
<string name="help_translate_description">ساعد في تَرْجَمَة هذا التطبيق إلى لغتك المفضلة.</string>
<string name="translators_thanks_title">المُترجمون</string>
<string name="translators_thanks_description">darken</string>
</resources> </resources>
+20 -20
View File
@@ -4,29 +4,29 @@
<string name="general_done_action">Fertig</string> <string name="general_done_action">Fertig</string>
<string name="general_copy_action">Kopieren</string> <string name="general_copy_action">Kopieren</string>
<string name="general_thank_you_label">Danke</string> <string name="general_thank_you_label">Danke</string>
<string name="general_upgrade_action">Aktualisierung</string> <string name="general_upgrade_action">Upgrade</string>
<string name="general_check_action">Überprüfen</string> <string name="general_check_action">Überprüfen</string>
<string name="general_close_action">Schließen</string> <string name="general_close_action">Schließen</string>
<string name="upgrade_capod_label">Verbesser CAPod</string> <string name="upgrade_capod_label">Verbesser CAPod</string>
<string name="upgrade_capod_description">Erhalten Sie zusätzliche Funktionen und unterstützen Sie den Entwickler.</string> <string name="upgrade_capod_description">Erhalten Sie zusätzliche Funktionen und unterstützen Sie den Entwickler.</string>
<string name="settings_monitor_mode_label">Überwachungsmodus</string> <string name="settings_monitor_mode_label">Überwachungsmodus</string>
<string name="settings_monitor_mode_description">Unter welchen Umständen überwacht diese App Bluetooth-Daten.</string> <string name="settings_monitor_mode_description">Unter welchen Umständen überwacht diese App Bluetooth-Daten.</string>
<string name="settings_scanner_mode_label">Scannermodus</string> <string name="settings_scanner_mode_label">Scan-Modus</string>
<string name="settings_scanner_mode_description">Soll der Bluetooth Low Energy Datenscanner Leistung priorisieren oder Energie sparen?</string> <string name="settings_scanner_mode_description">Soll die Bluetooth Low Energy Überwachung Leistung priorisieren oder Energie sparen?</string>
<string name="settings_autopause_label">Automatische Pause</string> <string name="settings_autopause_label">Automatische Pause</string>
<string name="settings_autopause_description">Halten Sie den Ton an, wenn Sie das Gerät von Ihrem Ohr entfernen.</string> <string name="settings_autopause_description">Musik pausieren, wenn der Kopfhörer vom Ohr entfernt wird.</string>
<string name="settings_showall_label">Alle Geräte anzeigen</string> <string name="settings_showall_label">Alle Geräte anzeigen</string>
<string name="settings_showall_description">Zeigen Sie die Geräte anderer Personen in Ihrer Nähe an.</string> <string name="settings_showall_description">Alle in der Nähe befindlichen Geräte anzeigen (auch die von anderen Personen).</string>
<string name="settings_autopplay_label">Automatisches Abspielen</string> <string name="settings_autopplay_label">Automatisches abspielen</string>
<string name="settings_autoplay_description">Starten Sie die Audiowiedergabe, wenn das Gerät getragen wird.</string> <string name="settings_autoplay_description">Audiowiedergabe starten, wenn das Gerät getragen wird.</string>
<string name="settings_fake_data_label">Gefälschte Daten</string> <string name="settings_fake_data_label">Gefälschte Daten</string>
<string name="settings_fake_data_description">Falsche Daten anzeigen, also nicht existierende Geräte simulieren.</string> <string name="settings_fake_data_description">Gefälschte Test-Daten anzeigen, d.h. nicht existierende Geräte simulieren.</string>
<string name="settings_debug_label">Debug-Einstellungen</string> <string name="settings_debug_label">Debug-Einstellungen</string>
<string name="settings_debug_description">Zusätzliche Einstellungen zur Behebung von Problemen mit der App.</string> <string name="settings_debug_description">Zusätzliche Einstellungen zur Behebung von Problemen mit der App.</string>
<string name="settings_signal_minimum_label">Minimale Signalqualität</string> <string name="settings_signal_minimum_label">Minimale Signalqualität</string>
<string name="settings_signal_minimum_description">Die minimale Signalqualität, die ein Gerät haben muss, muss als Ihre angesehen werden.</string> <string name="settings_signal_minimum_description">Die minimale Signalqualität, die ein Gerät haben muss, damit als Ihres erkannt wird.</string>
<string name="settings_autoconnect_label">Automatisch verbinden</string> <string name="settings_autoconnect_label">Automatisch verbinden</string>
<string name="settings_autoconnect_description">Wenn Android nicht automatisch eine Verbindung herstellt, können wir es auch fragen. Dadurch wird die Monitormodus-Einstellung auf „Immer“ gesetzt.</string> <string name="settings_autoconnect_description">Wenn Android keine automatische Verbindung herstellt, können wir es auch auslösen. Durch diese Option wird die Monitormodus-Einstellung auf „Immer“ gesetzt.</string>
<string name="settings_autoconnect_condition_label">Bedingung für automatische Verbindung</string> <string name="settings_autoconnect_condition_label">Bedingung für automatische Verbindung</string>
<string name="settings_autoconnect_condition_description">Wann sollten wir versuchen, eine Verbindung zu Ihrem Gerät herzustellen?</string> <string name="settings_autoconnect_condition_description">Wann sollten wir versuchen, eine Verbindung zu Ihrem Gerät herzustellen?</string>
<string name="settings_reaction_label">Reaktionen</string> <string name="settings_reaction_label">Reaktionen</string>
@@ -36,21 +36,21 @@
<string name="settings_maindevice_address_description">Die Adresse Ihres gekoppelten Geräts. Die App verwendet dies, um festzustellen, wann sie mit Ihrem Telefon verbunden ist.</string> <string name="settings_maindevice_address_description">Die Adresse Ihres gekoppelten Geräts. Die App verwendet dies, um festzustellen, wann sie mit Ihrem Telefon verbunden ist.</string>
<string name="settings_maindevice_address_none">Keine</string> <string name="settings_maindevice_address_none">Keine</string>
<string name="settings_maindevice_model_label">Ihr Gerätemodell</string> <string name="settings_maindevice_model_label">Ihr Gerätemodell</string>
<string name="settings_maindevice_model_description">Das Modell Ihres Hauptgeräts. Dies hilft der App, Ihr Gerät zu erkennen, wenn es nicht mit Ihrem Telefon verbunden ist.</string> <string name="settings_maindevice_model_description">Das Modell Ihres Hauptgeräts. Dies hilft der App, Ihr Gerät zu erkennen, wenn es nicht mit Ihrem Telefon verbunden ist.</string>
<string name="settings_popup_caseopen_label">Popup zeigen</string> <string name="settings_popup_caseopen_label">Popup zeigen</string>
<string name="settings_popup_caseopen_description">Ein Popup anzeigen, wenn das Gerätegehäuse geöffnet wird (experimentell).</string> <string name="settings_popup_caseopen_description">Ein Popup anzeigen, wenn das Gerätegehäuse geöffnet wird (experimentell).</string>
<string name="settings_onepod_mode_label">Ein-Pod-Modus</string> <string name="settings_onepod_mode_label">Ein-Pod-Modus</string>
<string name="settings_onepod_mode_description">Das Tragen beider Pods ist nicht erforderlich, das Tragen eines einzigen Pods reicht aus, um Reaktionen auszulösen.</string> <string name="settings_onepod_mode_description">Das Tragen beider Pods ist nicht erforderlich. Das Tragen eines einzigen Pods reicht aus, um Reaktionen auszulösen.</string>
<string name="notification_channel_device_status_label">Geräte Status</string> <string name="notification_channel_device_status_label">Geräte Status</string>
<string name="debug_debuglog_size_label">Größe</string> <string name="debug_debuglog_size_label">Größe</string>
<string name="debug_debuglog_size_compressed_label">Komprimierte Größe</string> <string name="debug_debuglog_size_compressed_label">Komprimierte Größe</string>
<string name="debug_notification_channel_label">Debug Benachrichtigungen</string> <string name="debug_notification_channel_label">Debug Benachrichtigungen</string>
<string name="debug_debuglog_file_label">Aufgenommene Log-Datei</string> <string name="debug_debuglog_file_label">Aufgenommene Log-Datei</string>
<string name="debug_debuglog_record_action">Debug Log aufnehmen</string> <string name="debug_debuglog_record_action">Debug Log aufnehmen</string>
<string name="settings_support_installid_label">ID installieren</string> <string name="settings_support_installid_label">Installations-ID</string>
<string name="settings_support_installid_desc">Automatische Fehlermeldungen sind anonym. Teilen Sie Ihre Installations- ID mit, wenn der Entwickler Ihre Fehlerberichte finden muss.</string> <string name="settings_support_installid_desc">Automatische Fehlermeldungen sind anonym. Teilen Sie Ihre Installations- ID dem Entwickler mit, wenn er Ihre Fehlerberichte finden muss.</string>
<string name="settings_support_label">Unterstützung</string> <string name="settings_support_label">Unterstützung</string>
<string name="settings_support_description">Wenn Sie Hilfe brauchen.</string> <string name="settings_support_description">Wenn du Hilfe brauchst.</string>
<string name="issue_tracker_label">Issue-Tracker</string> <string name="issue_tracker_label">Issue-Tracker</string>
<string name="issue_tracker_description">Ein öffentlicher Issue-Tracker für Fehlerberichte und Funktionsanfragen (nur auf Englisch).</string> <string name="issue_tracker_description">Ein öffentlicher Issue-Tracker für Fehlerberichte und Funktionsanfragen (nur auf Englisch).</string>
<string name="discord_label">Discord</string> <string name="discord_label">Discord</string>
@@ -65,15 +65,15 @@
<string name="settings_general_description">Allgemeine Optimierungen, die die gesamte App betreffen.</string> <string name="settings_general_description">Allgemeine Optimierungen, die die gesamte App betreffen.</string>
<string name="settings_acknowledgements_label">Danksagungen</string> <string name="settings_acknowledgements_label">Danksagungen</string>
<string name="settings_debug_autoreports_label">Automatische Fehlerberichte</string> <string name="settings_debug_autoreports_label">Automatische Fehlerberichte</string>
<string name="settings_debug_autoreports_description">Meldet Probleme automatisch, Details zu einem App-Absturz, damit ich herausfinden kann, wie ich ihn beheben kann.</string> <string name="settings_debug_autoreports_description">Meldet Probleme automatisch, z.B. Details zu einem App-Absturz, damit ich herausfinden kann, wie ich ihn beheben kann.</string>
<string name="settings_debug_mode_label">Debug-Modus</string> <string name="settings_debug_mode_label">Debug-Modus</string>
<string name="settings_debug_mode_description">Zeigen Sie zusätzliche Informationen an, um Probleme zu beheben.</string> <string name="settings_debug_mode_description">Zeige zusätzliche Informationen an, um Probleme zu beheben.</string>
<string name="settings_blescanner_unfiltered_label">Ungefilterte BLE-Daten</string> <string name="settings_blescanner_unfiltered_label">Ungefilterte BLE-Daten</string>
<string name="settings_blescanner_unfiltered_description">Entfernen Sie alle Filter aus dem BLE-Scanner, um alle übertragenen BLE-Daten anzuzeigen. Nützlich, um Unterstützung für neue Kopfhörertypen hinzuzufügen.</string> <string name="settings_blescanner_unfiltered_description">Entfernt alle Filter aus dem BLE-Scanner, um alle übertragenen BLE-Daten anzuzeigen. Nützlich, um Unterstützung für neue Kopfhörertypen hinzuzufügen.</string>
<string name="settings_compatibility_mode_label">Kompatibilitätsmodus</string> <string name="settings_compatibility_mode_label">Kompatibilitätsmodus</string>
<string name="settings_compatibility_mode_description">Deaktivieren Sie Optimierungen, um die Kompatibilität zu verbessern. Versuchen Sie dies, wenn Sie keine Daten sehen.</string> <string name="settings_compatibility_mode_description">Deaktivieren Optimierungen, um die Kompatibilität zu verbessern. Probier diese Option, wenn gar keine Daten angezeigt werden.</string>
<string name="help_translate_label">Übersetzung</string> <string name="help_translate_label">Übersetzung</string>
<string name="help_translate_description">Helfen Sie mit, diese App in Ihre Lieblingssprache zu übersetzen.</string> <string name="help_translate_description">Hilf mit diese App in deine Lieblingssprache zu übersetzen.</string>
<string name="translators_thanks_title">Übersetzer</string> <string name="translators_thanks_title">Übersetzer</string>
<string name="translators_thanks_description">Gamechanger181</string> <string name="translators_thanks_description">Gamechanger181</string>
</resources> </resources>
+11 -11
View File
@@ -23,20 +23,20 @@
<string name="settings_fake_data_description">顯示假資料,模擬不存在的裝置。</string> <string name="settings_fake_data_description">顯示假資料,模擬不存在的裝置。</string>
<string name="settings_debug_label">偵錯設定</string> <string name="settings_debug_label">偵錯設定</string>
<string name="settings_debug_description">一個能協助這個應用程式故障檢修的附加設定。</string> <string name="settings_debug_description">一個能協助這個應用程式故障檢修的附加設定。</string>
<string name="settings_signal_minimum_label">最低訊號質</string> <string name="settings_signal_minimum_label">最低訊號</string>
<string name="settings_signal_minimum_description">可以被認為是的裝置的最低訊號質</string> <string name="settings_signal_minimum_description">可以被認為是的裝置的最低訊號質。</string>
<string name="settings_autoconnect_label">自動連線</string> <string name="settings_autoconnect_label">自動連線</string>
<string name="settings_autoconnect_description">如果 Android 不會自動連線,我們也可以要求它。這將把監視模式設定為「一律」。</string> <string name="settings_autoconnect_description">如果 Android 不會自動連線,我們也可以要求它。這將把監視模式設定為「一律」。</string>
<string name="settings_autoconnect_condition_label">自動連線條件</string> <string name="settings_autoconnect_condition_label">自動連線條件</string>
<string name="settings_autoconnect_condition_description">何時連線到的裝置?</string> <string name="settings_autoconnect_condition_description">何時連線到的裝置?</string>
<string name="settings_reaction_label">反應</string> <string name="settings_reaction_label">反應</string>
<string name="settings_reaction_description">對事件和行為作出反應。</string> <string name="settings_reaction_description">對事件和行為作出反應。</string>
<string name="settings_category_yourdevice_label">的裝置</string> <string name="settings_category_yourdevice_label">的裝置</string>
<string name="settings_maindevice_address_label">的裝置位址</string> <string name="settings_maindevice_address_label">的裝置位址</string>
<string name="settings_maindevice_address_description">的已配對裝置的位址這個應用程式用位址來確定何時連線到的手機。</string> <string name="settings_maindevice_address_description">的已配對裝置的位址這個應用程式用位址來確定何時連線到的手機。</string>
<string name="settings_maindevice_address_none"></string> <string name="settings_maindevice_address_none"></string>
<string name="settings_maindevice_model_label">的裝置型號</string> <string name="settings_maindevice_model_label">的裝置型號</string>
<string name="settings_maindevice_model_description">的主要裝置型號這能助這個應用程式在未連線到的手機時辨識的裝置。</string> <string name="settings_maindevice_model_description">的主要裝置型號這能助這個應用程式在未連線到的手機時辨識的裝置。</string>
<string name="settings_popup_caseopen_label">顯示彈出式視窗</string> <string name="settings_popup_caseopen_label">顯示彈出式視窗</string>
<string name="settings_popup_caseopen_description">充電盒開啟時顯示彈出式視窗 (實驗性)</string> <string name="settings_popup_caseopen_description">充電盒開啟時顯示彈出式視窗 (實驗性)</string>
<string name="settings_onepod_mode_label">單耳模式</string> <string name="settings_onepod_mode_label">單耳模式</string>
@@ -48,9 +48,9 @@
<string name="debug_debuglog_file_label">已記錄的記錄檔</string> <string name="debug_debuglog_file_label">已記錄的記錄檔</string>
<string name="debug_debuglog_record_action">記錄偵錯記錄</string> <string name="debug_debuglog_record_action">記錄偵錯記錄</string>
<string name="settings_support_installid_label">安裝 ID</string> <string name="settings_support_installid_label">安裝 ID</string>
<string name="settings_support_installid_desc">自動傳送的錯誤報告是匿名的。如果開發人員需要找出的錯誤報告將分享你的安裝 ID。</string> <string name="settings_support_installid_desc">自動傳送的錯誤報告是匿名的。如果開發人員需要找出的錯誤報告將分享你的安裝 ID。</string>
<string name="settings_support_label">支援</string> <string name="settings_support_label">支援</string>
<string name="settings_support_description">如果需要協助。</string> <string name="settings_support_description">如果需要協助。</string>
<string name="issue_tracker_label">問題追蹤器</string> <string name="issue_tracker_label">問題追蹤器</string>
<string name="issue_tracker_description">一個用於錯誤回報和功能需求的公用問題追蹤器 (僅英文)。</string> <string name="issue_tracker_description">一個用於錯誤回報和功能需求的公用問題追蹤器 (僅英文)。</string>
<string name="discord_label">Discord</string> <string name="discord_label">Discord</string>
@@ -73,7 +73,7 @@
<string name="settings_compatibility_mode_label">相容模式</string> <string name="settings_compatibility_mode_label">相容模式</string>
<string name="settings_compatibility_mode_description">停用最佳化以提高相容性。請在未看到任何資料時開啟。</string> <string name="settings_compatibility_mode_description">停用最佳化以提高相容性。請在未看到任何資料時開啟。</string>
<string name="help_translate_label">翻譯</string> <string name="help_translate_label">翻譯</string>
<string name="help_translate_description">協助翻譯為的最愛語言。</string> <string name="help_translate_description">協助翻譯為的最愛語言。</string>
<string name="translators_thanks_title">翻譯人員</string> <string name="translators_thanks_title">翻譯人員</string>
<string name="translators_thanks_description">人工知能</string> <string name="translators_thanks_description">人工知能</string>
</resources> </resources>
+11
View File
@@ -33,6 +33,14 @@
<string name="settings_reaction_label">Reactions</string> <string name="settings_reaction_label">Reactions</string>
<string name="settings_reaction_description">React to events and behaviors.</string> <string name="settings_reaction_description">React to events and behaviors.</string>
<string name="settings_category_yourdevice_label">Your device</string> <string name="settings_category_yourdevice_label">Your device</string>
<string name="settings_category_compatibility_options_title">Compatibility options</string>
<string name="settings_category_compatibility_options_description">Don\'t touch if everything works ;)</string>
<string name="settings_compat_offloaded_filtering_disabled_title">Disable hardware filtering</string>
<string name="settings_compat_offloaded_filtering_disabled_summary">Don\'t delegate data filtering to the system, instead get all data and filter within the app.</string>
<string name="settings_compat_offloaded_batching_disabled_title">Disable hardware batching</string>
<string name="settings_compat_offloaded_batching_disabled_summary">Don\'t let the system group collected BLE data before forwarding it to us.</string>
<string name="settings_maindevice_address_label">Your device address</string> <string name="settings_maindevice_address_label">Your device address</string>
<string name="settings_maindevice_address_description">The address of your paired device. The app uses this to determine when it is connected to your phone.</string> <string name="settings_maindevice_address_description">The address of your paired device. The app uses this to determine when it is connected to your phone.</string>
<string name="settings_maindevice_address_none">None</string> <string name="settings_maindevice_address_none">None</string>
@@ -43,6 +51,7 @@
<string name="settings_onepod_mode_label">One pod mode</string> <string name="settings_onepod_mode_label">One pod mode</string>
<string name="settings_onepod_mode_description">Wearing both pods is not required, wearing a single pod is sufficient to trigger reactions.</string> <string name="settings_onepod_mode_description">Wearing both pods is not required, wearing a single pod is sufficient to trigger reactions.</string>
<string name="notification_channel_device_status_label">Device status</string> <string name="notification_channel_device_status_label">Device status</string>
<string name="debug_debuglog_size_label">Size</string> <string name="debug_debuglog_size_label">Size</string>
@@ -91,4 +100,6 @@
<string name="translators_thanks_description">darken</string> <string name="translators_thanks_description">darken</string>
<string name="widget_description">A widget showing the last known device status.</string> <string name="widget_description">A widget showing the last known device status.</string>
<string name="settings_compat_indirectcallback_title">Indirect data delivery</string>
<string name="settings_compat_indirectcallback_summary">Use an alternative method to receive BLE data from the system (broadcast instead of callback).</string>
</resources> </resources>
+26 -6
View File
@@ -14,12 +14,6 @@
android:summary="@string/settings_scanner_mode_description" android:summary="@string/settings_scanner_mode_description"
android:title="@string/settings_scanner_mode_label" /> android:title="@string/settings_scanner_mode_label" />
<CheckBoxPreference
android:icon="@drawable/ic_baseline_ghost_24"
android:key="core.compatibility.enabled"
android:summary="@string/settings_compatibility_mode_description"
android:title="@string/settings_compatibility_mode_label" />
<CheckBoxPreference <CheckBoxPreference
android:icon="@drawable/ic_baseline_devices_other_24" android:icon="@drawable/ic_baseline_devices_other_24"
android:key="core.showall.enabled" android:key="core.showall.enabled"
@@ -49,6 +43,32 @@
android:title="@string/settings_maindevice_model_label" /> android:title="@string/settings_maindevice_model_label" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory
android:singleLineTitle="false"
android:summary="@string/settings_category_compatibility_options_description"
android:title="@string/settings_category_compatibility_options_title"
app:icon="@drawable/ic_chip_24">
<CheckBoxPreference
android:icon="@drawable/ic_filter_cog_outline_24"
android:key="core.compat.offloaded.filtering.disabled"
android:summary="@string/settings_compat_offloaded_filtering_disabled_summary"
android:title="@string/settings_compat_offloaded_filtering_disabled_title" />
<CheckBoxPreference
android:icon="@drawable/ic_format_list_group_24"
android:key="core.compat.offloaded.batching.disabled"
android:summary="@string/settings_compat_offloaded_batching_disabled_summary"
android:title="@string/settings_compat_offloaded_batching_disabled_title" />
<CheckBoxPreference
android:icon="@drawable/ic_strategy_24"
android:key="core.compat.indirectcallback.enabled"
android:summary="@string/settings_compat_indirectcallback_summary"
android:title="@string/settings_compat_indirectcallback_title" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/settings_category_other_label"> <PreferenceCategory android:title="@string/settings_category_other_label">
<Preference <Preference
android:fragment="eu.darken.capod.main.ui.settings.general.debug.DebugSettingsFragment" android:fragment="eu.darken.capod.main.ui.settings.general.debug.DebugSettingsFragment"
@@ -1,18 +1,18 @@
CAPod is a companion app for AirPods. CAPod - رفيق سماعات AirPod.
Features: الميزات:
* Battery level for pods and cases. * مستوى البطارية الخاص بالسماعات وعُلبها.
* Charging status for pods and case. * حالة الشحن الخاص بالسماعات وعُلبها.
* Additional infos about connection, microphone and case. * معلومات إضافية حول حالة الاتصال، الميكروفون والعُلبة.
* Can receive and show all nearby devices. * يمكن عرض واستقبال جميع الأجهزة القريبة.
* Ear detection with automatic play/pause. * مُستشعر الأذن مع التشغيل التلقائي/الإيقاف المؤقت.
* Automatically connect phone and AirPods. * توصيل الهاتف و سماعات AirPods بشكل تلقائي.
* Show popup when case is opened. * إظهار إشعار مُنبثق عند فتح علبة السماعات.
CAPod is ad-free. Some features require an in-app purchase. CAPod خالي من الإعلانات. بعض الميزات تتطلب الشراء داخل التطبيق.
Most popular AirPods and Beats devices are supported. يدعم الأجهزة الأكثر شيوعًا مثل AirPods و Beats.
If your device is similar to AirPods but not yet supported, send me a short mail. إذا كان جهازك مشابهًا لأجهزة AirPods ولكنه غير مدعوم في التطبيق بعد فأرسل لي رسالة إلكترونية قصيرة.
Got a cool idea for a new feature? Reach out! هل لديك فكرة رائعة لمِيزة جديدة؟ تواصل!
@@ -1 +1 @@
CAPod is a companion app for AirPods on Android. CAPod هو تطبيق مُصاحب لسماعات AirPods على الأندرويد.
+1 -1
View File
@@ -1 +1 @@
CAPod - Companion for AirPods CAPod - رفيق سماعات AirPod
@@ -6,13 +6,13 @@ CAPod 是一個能提供 AirPods 相關功能的應用程式。
* 顯示耳機和充電盒充電狀態。 * 顯示耳機和充電盒充電狀態。
* 顯示有關連線、麥克風、充電盒的附加資訊。 * 顯示有關連線、麥克風、充電盒的附加資訊。
* 可以接收並顯示附近的所有裝置。 * 可以接收並顯示附近的所有裝置。
* 耳朵測,自動播放/暫停。 * 耳朵測,自動播放/暫停。
* 自動連線手機和 AirPods。 * 自動連線手機和 AirPods。
* 開啟充電盒時顯示彈出式視窗。 * 開啟充電盒時顯示彈出式視窗。
CAPod 是無廣告的應用程式。 一些功能需要應用內購。 CAPod 是無廣告的應用程式。 一些功能需要應用內購。
支援大部分流行的 AirPods 和 Beats 裝置。 支援大部分流行的 AirPods 和 Beats 裝置。
如果的裝置和 AirPods 相似但不支援,請寄給我一封郵件。 如果的裝置和 AirPods 相似但不支援,請寄給我一封郵件。
有新功能的好點子? 與我溝通一下吧! 有新功能的好點子? 與我溝通一下吧!
+3 -3
View File
@@ -1,6 +1,6 @@
### Updated by release.sh ### ### Updated by release.sh ###
project.versioning.major=2 project.versioning.major=2
project.versioning.minor=5 project.versioning.minor=7
project.versioning.patch=3 project.versioning.patch=2
project.versioning.build=1 project.versioning.build=0
############################# #############################