mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Compare commits
8
Commits
v2.6.1-rc0
...
v2.7.0-rc0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9fa9f6cb8 | ||
|
|
b98ca86886 | ||
|
|
d917c8ffe6 | ||
|
|
5d44737199 | ||
|
|
6e8693a6b0 | ||
|
|
6d9e7404a1 | ||
|
|
70ceb389b5 | ||
|
|
832e364a2f |
@@ -7,10 +7,14 @@ on:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build-and-test:
|
||||
name: Build and test
|
||||
build-all:
|
||||
name: Assemble all variants
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
flavor: [ Foss,Gplay ]
|
||||
variant: [ Debug ]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 11
|
||||
@@ -19,16 +23,30 @@ jobs:
|
||||
java-version: '11'
|
||||
distribution: 'adopt'
|
||||
cache: gradle
|
||||
|
||||
- name: Grant execute permission for gradlew
|
||||
run: chmod +x gradlew
|
||||
|
||||
- name: Build FOSS variant
|
||||
run: ./gradlew assembleFossDebug
|
||||
- name: Test FOSS variant
|
||||
run: ./gradlew testFossDebugUnitTest
|
||||
- name: Build (Flavor ${{ matrix.flavor }}, Variant ${{ matrix.variant }})
|
||||
run: ./gradlew assemble${{ matrix.flavor }}${{ matrix.variant }}
|
||||
|
||||
- name: Build Google Play variant
|
||||
run: ./gradlew assembleGplayDebug
|
||||
- name: Test Google Play variant
|
||||
run: ./gradlew testGplayDebugUnitTest
|
||||
test-all:
|
||||
name: Run all tests
|
||||
strategy:
|
||||
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,4 +1,35 @@
|
||||
<?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>
|
||||
@@ -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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.bluetooth.le.ScanCallback
|
||||
import android.bluetooth.le.ScanFilter
|
||||
import android.bluetooth.le.ScanResult
|
||||
import android.bluetooth.le.ScanSettings
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.*
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.common.notifications.PendingIntentCompat
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
@@ -25,28 +26,48 @@ class BleScanner @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val bluetoothManager: BluetoothManager2,
|
||||
private val fakeBleData: FakeBleData,
|
||||
private val scanResultForwarder: BleScanResultForwarder,
|
||||
) {
|
||||
|
||||
@SuppressLint("MissingPermission") fun scan(
|
||||
filters: Set<ScanFilter>,
|
||||
scannerMode: ScannerMode,
|
||||
compatMode: Boolean,
|
||||
): Flow<List<BleScanResult>> = callbackFlow {
|
||||
log(TAG, VERBOSE) { "scan(filters=$filters, scannerMode=$scannerMode, compatMode=$compatMode)" }
|
||||
if (compatMode) log(TAG, WARN) { "Using compatibilityMode!" }
|
||||
scannerMode: ScannerMode = ScannerMode.BALANCED,
|
||||
disableOffloadFiltering: Boolean = true,
|
||||
disableOffloadBatching: Boolean = true,
|
||||
disableDirectScanCallback: Boolean = true,
|
||||
): Flow<Collection<BleScanResult>> = callbackFlow {
|
||||
log(TAG) { "scan(filters=$filters, scannerMode=$scannerMode)" }
|
||||
|
||||
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" }
|
||||
} && !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" }
|
||||
} && !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 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() {
|
||||
var lastScanAt = System.currentTimeMillis()
|
||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||
@@ -55,17 +76,8 @@ class BleScanner @Inject constructor(
|
||||
lastScanAt = System.currentTimeMillis()
|
||||
"onScanResult(delay=${delay}ms, callbackType=$callbackType, result=$result)"
|
||||
}
|
||||
val toSend = if (
|
||||
supportsOffloadFiltering
|
||||
|| filters.isEmpty()
|
||||
|| filters.any { it.matchesSafe(result) }
|
||||
) {
|
||||
listOf(BleScanResult.fromScanResult(result))
|
||||
} else {
|
||||
log(TAG, VERBOSE) { "Manual filtering: No match for $result" }
|
||||
emptyList()
|
||||
}
|
||||
trySend(toSend)
|
||||
|
||||
trySend(filterResults(setOf(result)))
|
||||
}
|
||||
|
||||
override fun onBatchScanResults(results: MutableList<ScanResult>) {
|
||||
@@ -75,18 +87,7 @@ class BleScanner @Inject constructor(
|
||||
"onBatchScanResults(delay=${delay}ms, results=$results)"
|
||||
}
|
||||
|
||||
val toSend = 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)
|
||||
trySend(filterResults(results))
|
||||
}
|
||||
|
||||
override fun onScanFailed(errorCode: Int) {
|
||||
@@ -94,58 +95,113 @@ class BleScanner @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val settings = ScanSettings.Builder().apply {
|
||||
setScanMode(
|
||||
when (scannerMode) {
|
||||
ScannerMode.LOW_POWER -> ScanSettings.SCAN_MODE_LOW_POWER
|
||||
ScannerMode.BALANCED -> ScanSettings.SCAN_MODE_BALANCED
|
||||
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)
|
||||
}
|
||||
}
|
||||
val forwarderConsumer = if (disableDirectScanCallback) {
|
||||
scanResultForwarder.results
|
||||
.onEach { results -> trySend(filterResults(results)) }
|
||||
.launchIn(this)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
scanner.startScan(
|
||||
if (supportsOffloadFiltering) filters.toList() else listOf(ScanFilter.Builder().build()),
|
||||
settings,
|
||||
callback
|
||||
)
|
||||
log(TAG) { "BleScanner started (filters=$filters, settings=$settings)" }
|
||||
val flushJob = if (!disableDirectScanCallback) {
|
||||
launch {
|
||||
log(TAG) { "Flush job launched" }
|
||||
while (isActive) {
|
||||
log(TAG, VERBOSE) { "Flushing scan results." }
|
||||
// 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 {
|
||||
flushJob.cancel()
|
||||
scanner.stopScan(callback)
|
||||
forwarderConsumer?.cancel()
|
||||
flushJob?.cancel()
|
||||
if (disableDirectScanCallback) {
|
||||
scanner.stopScan(createStopIntent())
|
||||
} else {
|
||||
scanner.stopScan(callback)
|
||||
}
|
||||
log(TAG) { "BleScanner stopped" }
|
||||
}
|
||||
}
|
||||
.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 {
|
||||
private const val CALLBACK_INTENT_REQUESTCODE = 270
|
||||
private val TAG = logTag("Bluetooth", "BleScanner")
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ class FakeBleData @Inject constructor(
|
||||
private val debugSettings: DebugSettings,
|
||||
) {
|
||||
|
||||
fun maybeAddfakeData(originals: List<BleScanResult>): List<BleScanResult> {
|
||||
fun maybeAddfakeData(originals: Collection<BleScanResult>): Collection<BleScanResult> {
|
||||
if (!debugSettings.showFakeData.value) return originals
|
||||
return originals + getFakeData()
|
||||
}
|
||||
|
||||
+5
@@ -9,4 +9,9 @@ object PendingIntentCompat {
|
||||
} else {
|
||||
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)
|
||||
|
||||
val monitorMode = preferences.createFlowPreference(
|
||||
"core.monitor.mode",
|
||||
MonitorMode.AUTOMATIC,
|
||||
moshi
|
||||
)
|
||||
val monitorMode = preferences.createFlowPreference("core.monitor.mode", MonitorMode.AUTOMATIC, moshi)
|
||||
val scannerMode = preferences.createFlowPreference("core.scanner.mode", ScannerMode.BALANCED, moshi)
|
||||
|
||||
val scannerMode = preferences.createFlowPreference(
|
||||
"core.scanner.mode",
|
||||
ScannerMode.LOW_LATENCY,
|
||||
moshi
|
||||
)
|
||||
val showAll = preferences.createFlowPreference("core.showall.enabled", false)
|
||||
|
||||
val compatibilityMode = preferences.createFlowPreference(
|
||||
"core.compatibility.enabled",
|
||||
val minimumSignalQuality = preferences.createFlowPreference("core.signal.minimum", 0.25f)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
val showAll = preferences.createFlowPreference(
|
||||
"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
|
||||
)
|
||||
val isOffloadedBatchingDisabled = preferences.createFlowPreference("core.compat.offloaded.batching.disabled", false)
|
||||
val useIndirectScanResultCallback = preferences.createFlowPreference("core.compat.indirectcallback.enabled", false)
|
||||
|
||||
override val preferenceDataStore: PreferenceDataStore = PreferenceStoreMapper(
|
||||
monitorMode,
|
||||
scannerMode,
|
||||
compatibilityMode,
|
||||
showAll,
|
||||
minimumSignalQuality,
|
||||
mainDeviceAddress,
|
||||
isOffloadedFilteringDisabled,
|
||||
isOffloadedBatchingDisabled,
|
||||
useIndirectScanResultCallback,
|
||||
debugSettings.isAutoReportingEnabled,
|
||||
)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import android.bluetooth.le.ScanFilter
|
||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||
import eu.darken.capod.common.bluetooth.BleScanner
|
||||
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.debug.autoreport.DebugSettings
|
||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||
@@ -83,26 +84,50 @@ class PodMonitor @Inject constructor(
|
||||
.setupCommonEventHandlers(TAG) { "mainDevice" }
|
||||
.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(
|
||||
generalSettings.scannerMode.flow,
|
||||
generalSettings.compatibilityMode.flow,
|
||||
debugSettings.showUnfiltered.flow
|
||||
) { scannerMode, compatMode, unfiltered ->
|
||||
Triple(scannerMode, compatMode, unfiltered)
|
||||
debugSettings.showUnfiltered.flow,
|
||||
generalSettings.isOffloadedBatchingDisabled.flow,
|
||||
generalSettings.isOffloadedFilteringDisabled.flow,
|
||||
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 {
|
||||
unfiltered -> {
|
||||
options.showUnfiltered -> {
|
||||
log(TAG, WARN) { "Using unfiltered scan mode" }
|
||||
setOf(getUnfilteredFilter())
|
||||
setOf(ScanFilter.Builder().build())
|
||||
}
|
||||
else -> ProximityPairing.getBleScanFilter()
|
||||
}
|
||||
|
||||
bleScanner.scan(
|
||||
filters = filters,
|
||||
scannerMode = mode,
|
||||
compatMode = compat,
|
||||
scannerMode = options.scannerMode,
|
||||
disableOffloadFiltering = options.offloadedFilteringDisabled,
|
||||
disableOffloadBatching = options.offloadedBatchingDisabled,
|
||||
disableDirectScanCallback = options.disableDirectCallback,
|
||||
).map { preFilterAndMap(it) }
|
||||
}
|
||||
|
||||
@@ -134,7 +159,7 @@ class PodMonitor @Inject constructor(
|
||||
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 }
|
||||
.values
|
||||
.map { sameAdrDevs ->
|
||||
@@ -187,10 +212,6 @@ class PodMonitor @Inject constructor(
|
||||
.also { log(TAG) { "Cached mainDevice is $it" } }
|
||||
}
|
||||
|
||||
private fun getUnfilteredFilter(): ScanFilter {
|
||||
return ScanFilter.Builder().build()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG = logTag("Monitor", "PodMonitor")
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -6,6 +6,7 @@
|
||||
<string name="overview_nomaindevice_label">Відсутній первинний пристрій</string>
|
||||
<string name="overview_bluetooth_disabled_label">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_background_location_label">Доступ до місцезнаходження у тлі</string>
|
||||
<string name="permission_ignore_battery_optimizations_label">Вимкнути оптимізацію батареї</string>
|
||||
|
||||
@@ -7,21 +7,21 @@
|
||||
<string name="general_error_label">錯誤</string>
|
||||
<string name="general_grant_permission_action">授予權限</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_description">藍牙已停用,啟用它 ;)</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_description">「藍牙掃描」權限允許該應用程式發現附近的裝置並接收藍牙資料,你的 AirPods 需要如此。</string>
|
||||
<string name="permission_bluetooth_scan_description">「藍牙掃描」權限允許這個應用程式發現附近的裝置並接收藍牙資料,你的 AirPods 需要如此。</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_description">CAPod 使用「精確位置」權限以接收低功耗藍牙資料。你的耳機使用低功耗藍牙技術以廣播其狀態。這個應用程式不會使用藍牙資料來確定你的位置。</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_description">電池效能最佳化使該應用程式在背景時無法可靠地接收藍牙資料。</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>
|
||||
|
||||
@@ -4,26 +4,6 @@
|
||||
package="eu.darken.capod">
|
||||
|
||||
<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" />
|
||||
|
||||
|
||||
@@ -5,29 +5,8 @@
|
||||
|
||||
<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.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-feature
|
||||
|
||||
@@ -25,9 +25,9 @@ class BluetoothEventReceiver : BroadcastReceiver() {
|
||||
@Inject @AppScope lateinit var appScope: CoroutineScope
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
log { "onReceive($context, $intent)" }
|
||||
log(TAG) { "onReceive($context, $intent)" }
|
||||
if (!EXPECTED_ACTIONS.contains(intent.action)) {
|
||||
log(WARN) { "Unknown action: $intent.action" }
|
||||
log(TAG, WARN) { "Unknown action: ${intent.action}" }
|
||||
return
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class BluetoothEventReceiver : BroadcastReceiver() {
|
||||
val supportedFeatures = ContinuityProtocol.BLE_FEATURE_UUIDS.filter { bluetoothDevice.hasFeature(it) }
|
||||
|
||||
if (supportedFeatures.isEmpty()) {
|
||||
log { "Device has no features we support." }
|
||||
log(TAG) { "Device has no features we support." }
|
||||
return
|
||||
} else {
|
||||
log { "Device has the following we features we support $supportedFeatures" }
|
||||
@@ -49,7 +49,7 @@ class BluetoothEventReceiver : BroadcastReceiver() {
|
||||
|
||||
val pending = goAsync()
|
||||
appScope.launch {
|
||||
log { "Starting monitor" }
|
||||
log(TAG) { "Starting monitor" }
|
||||
monitorControl.startMonitor(bluetoothDevice, forceStart = false)
|
||||
pending.finish()
|
||||
}
|
||||
|
||||
@@ -4,29 +4,29 @@
|
||||
<string name="general_done_action">Fertig</string>
|
||||
<string name="general_copy_action">Kopieren</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_close_action">Schließen</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="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_scanner_mode_label">Scannermodus</string>
|
||||
<string name="settings_scanner_mode_description">Soll der Bluetooth Low Energy Datenscanner Leistung priorisieren oder Energie sparen?</string>
|
||||
<string name="settings_scanner_mode_label">Scan-Modus</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_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_description">Zeigen Sie die Geräte anderer Personen in Ihrer Nähe an.</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_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_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_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_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_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_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_description">Wann sollten wir versuchen, eine Verbindung zu Ihrem Gerät herzustellen?</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_none">Keine</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_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_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="debug_debuglog_size_label">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_debuglog_file_label">Aufgenommene Log-Datei</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_desc">Automatische Fehlermeldungen sind anonym. Teilen Sie Ihre Installations- ID mit, wenn der Entwickler Ihre Fehlerberichte finden muss.</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 dem Entwickler mit, wenn er Ihre Fehlerberichte finden muss.</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_description">Ein öffentlicher Issue-Tracker für Fehlerberichte und Funktionsanfragen (nur auf Englisch).</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_acknowledgements_label">Danksagungen</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_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_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_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_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_description">Gamechanger181</string>
|
||||
</resources>
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
<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_signal_minimum_label">最低訊號品質</string>
|
||||
<string name="settings_signal_minimum_description">可以被認為是你的裝置的最低訊號品質。</string>
|
||||
<string name="settings_autoconnect_label">自動連線</string>
|
||||
<string name="settings_autoconnect_description">如果 Android 不會自動連線,我們也可以要求它。這將把監視模式設定為「一律」。</string>
|
||||
<string name="settings_autoconnect_condition_label">自動連線條件</string>
|
||||
@@ -33,7 +33,7 @@
|
||||
<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_description">你的已配對裝置的位址,這個應用程式用這個位址來確定何時連線到你的手機。</string>
|
||||
<string name="settings_maindevice_address_none">無</string>
|
||||
<string name="settings_maindevice_model_label">你的裝置型號</string>
|
||||
<string name="settings_maindevice_model_description">你的主要裝置型號。這能協助這個應用程式在未連線到你的手機時辨識你的裝置。</string>
|
||||
|
||||
@@ -33,6 +33,14 @@
|
||||
<string name="settings_reaction_label">Reactions</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_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_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>
|
||||
@@ -43,6 +51,7 @@
|
||||
<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="notification_channel_device_status_label">Device status</string>
|
||||
|
||||
<string name="debug_debuglog_size_label">Size</string>
|
||||
@@ -91,4 +100,6 @@
|
||||
<string name="translators_thanks_description">darken</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>
|
||||
@@ -14,12 +14,6 @@
|
||||
android:summary="@string/settings_scanner_mode_description"
|
||||
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
|
||||
android:icon="@drawable/ic_baseline_devices_other_24"
|
||||
android:key="core.showall.enabled"
|
||||
@@ -49,6 +43,32 @@
|
||||
android:title="@string/settings_maindevice_model_label" />
|
||||
</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">
|
||||
<Preference
|
||||
android:fragment="eu.darken.capod.main.ui.settings.general.debug.DebugSettingsFragment"
|
||||
|
||||
@@ -6,7 +6,7 @@ CAPod 是一個能提供 AirPods 相關功能的應用程式。
|
||||
* 顯示耳機和充電盒充電狀態。
|
||||
* 顯示有關連線、麥克風、充電盒的附加資訊。
|
||||
* 可以接收並顯示附近的所有裝置。
|
||||
* 耳朵檢測,自動播放/暫停。
|
||||
* 耳朵偵測,自動播放/暫停。
|
||||
* 自動連線手機和 AirPods。
|
||||
* 開啟充電盒時顯示彈出式視窗。
|
||||
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
### Updated by release.sh ###
|
||||
project.versioning.major=2
|
||||
project.versioning.minor=6
|
||||
project.versioning.patch=1
|
||||
project.versioning.minor=7
|
||||
project.versioning.patch=0
|
||||
project.versioning.build=0
|
||||
#############################
|
||||
|
||||
Reference in New Issue
Block a user