Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afa4d6e807 | ||
|
|
e6b7eda9e7 | ||
|
|
bbc4e05c8f | ||
|
|
50bccbaa2e | ||
|
|
d71e83a2af | ||
|
|
92a67702b7 | ||
|
|
2f9185efa7 | ||
|
|
a10216520b | ||
|
|
d5354e5156 | ||
|
|
a8f9b1f732 | ||
|
|
08a796e621 | ||
|
|
15f3a68f8e | ||
|
|
e0bc6dafa0 | ||
|
|
430c1236d7 | ||
|
|
3ac357161a | ||
|
|
3c5f1bf21f | ||
|
|
5544cd3770 | ||
|
|
71a060feb9 | ||
|
|
25d643257c | ||
|
|
85900e5bff | ||
|
|
d3257d6c5e | ||
|
|
afe1399f72 | ||
|
|
20ce2d3e22 | ||
|
|
cea8f2290d | ||
|
|
1b5b59c41b | ||
|
|
8df7c19e84 | ||
|
|
88b0069b94 | ||
|
|
3b8ddc9f06 | ||
|
|
708c7f38ec | ||
|
|
139daa1cc2 | ||
|
|
b85ac778c0 | ||
|
|
a3e2c9e61d | ||
|
|
ad78a0570c | ||
|
|
7e6ee274d8 | ||
|
|
daf7411c36 | ||
|
|
d3dedbe6cd | ||
|
|
d5411586d5 | ||
|
|
94e2d40554 | ||
|
|
db80fa1b46 | ||
|
|
9b569b8d07 | ||
|
|
90288880fb | ||
|
|
870e518ff6 | ||
|
|
6d89169bae | ||
|
|
ac77ee2703 | ||
|
|
18dcf603e9 | ||
|
|
71aaba1b86 | ||
|
|
64b9d5f17a | ||
|
|
a9f23c4bfe | ||
|
|
f454f46778 | ||
|
|
432936ea2c | ||
|
|
8211b0ac88 | ||
|
|
c314987e13 | ||
|
|
fc6cd74752 | ||
|
|
a47011e579 | ||
|
|
fbeeec6539 | ||
|
|
e1cd79cd9d | ||
|
|
692ced1ee7 | ||
|
|
c5cc127df4 | ||
|
|
5f00f10ae8 | ||
|
|
0fc9716f06 | ||
|
|
48fd3543e3 | ||
|
|
41985794c2 | ||
|
|
87a92fd001 | ||
|
|
f6f53f2a15 | ||
|
|
2531769b79 | ||
|
|
e9fa9f6cb8 | ||
|
|
b98ca86886 | ||
|
|
d917c8ffe6 | ||
|
|
5d44737199 | ||
|
|
6e8693a6b0 | ||
|
|
6d9e7404a1 | ||
|
|
70ceb389b5 | ||
|
|
832e364a2f | ||
|
|
51f9c5c7d8 | ||
|
|
15141c8528 | ||
|
|
0cdac10fd4 | ||
|
|
aaab548bfd | ||
|
|
46611b5606 | ||
|
|
fdbdb46577 | ||
|
|
e7fd81d0eb | ||
|
|
ce50bae6a0 | ||
|
|
8331cf3866 | ||
|
|
670262e53b | ||
|
|
c584f942cf | ||
|
|
e83b6bb583 | ||
|
|
cc5de668f0 | ||
|
|
653c7b8c4b | ||
|
|
4e38bcd4cc | ||
|
|
fcbeda4888 | ||
|
|
4101fddc5b | ||
|
|
86df7bea22 | ||
|
|
4db8c0c286 | ||
|
|
1d03335903 | ||
|
|
b1b541014f | ||
|
|
b40a7e435f | ||
|
|
8305fb4c5f | ||
|
|
6c678be511 | ||
|
|
2823663f09 | ||
|
|
033c2264f8 | ||
|
|
8eb6ce19f7 | ||
|
|
1cfc4e6f65 | ||
|
|
f3609fc3a5 | ||
|
|
16da51d095 |
@@ -7,28 +7,47 @@ on:
|
|||||||
branches: [ main ]
|
branches: [ main ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-test:
|
build-modules:
|
||||||
name: Build and test
|
name: Build apps
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
flavor: [ Foss,Gplay ]
|
||||||
|
variant: [ Debug ]
|
||||||
|
module: [ app,app-wear ]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v3
|
||||||
- name: Set up JDK 11
|
- name: Set up JDK 17
|
||||||
uses: actions/setup-java@v2
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
java-version: '11'
|
java-version: '17'
|
||||||
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 module
|
||||||
run: ./gradlew assembleFossDebug
|
run: ./gradlew ${{ matrix.module }}:assemble${{ matrix.flavor }}${{ matrix.variant }}
|
||||||
- name: Test FOSS variant
|
|
||||||
run: ./gradlew testFossDebugUnitTest
|
|
||||||
|
|
||||||
- name: Build Google Play variant
|
test-modules:
|
||||||
run: ./gradlew assembleGplayDebug
|
name: Unit tests
|
||||||
- name: Test Google Play variant
|
strategy:
|
||||||
run: ./gradlew testGplayDebugUnitTest
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
variant: [ Release ]
|
||||||
|
flavor: [ testFoss,testGplay ]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Set up JDK 17
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
java-version: '17'
|
||||||
|
distribution: 'adopt'
|
||||||
|
cache: gradle
|
||||||
|
- name: Grant execute permission for gradlew
|
||||||
|
run: chmod +x gradlew
|
||||||
|
|
||||||
|
- name: Test modules
|
||||||
|
run: ./gradlew ${{ matrix.flavor }}${{ matrix.variant }}UnitTest
|
||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
java-version: 11
|
java-version: 17
|
||||||
|
|
||||||
- name: Assemble beta APK
|
- name: Assemble beta APK
|
||||||
if: contains(steps.tagger.outputs.tag, '-beta')
|
if: contains(steps.tagger.outputs.tag, '-beta')
|
||||||
@@ -130,7 +130,7 @@ jobs:
|
|||||||
uses: actions/setup-java@v3
|
uses: actions/setup-java@v3
|
||||||
with:
|
with:
|
||||||
distribution: 'adopt'
|
distribution: 'adopt'
|
||||||
java-version: 11
|
java-version: 17
|
||||||
|
|
||||||
- name: Set up ruby env
|
- name: Set up ruby env
|
||||||
uses: ruby/setup-ruby@v1
|
uses: ruby/setup-ruby@v1
|
||||||
|
|||||||
@@ -12,3 +12,4 @@
|
|||||||
*.jks
|
*.jks
|
||||||
.local/*
|
.local/*
|
||||||
/fastlane/report.xml
|
/fastlane/report.xml
|
||||||
|
/fastlane/Appfile
|
||||||
@@ -34,15 +34,22 @@ Currently supported models:
|
|||||||
* Beats Studio 3
|
* Beats Studio 3
|
||||||
* Beats X
|
* Beats X
|
||||||
* Beats Flex
|
* Beats Flex
|
||||||
|
* Beats Fit Pro
|
||||||
|
* Fake AirPods 1. Generation 🎭
|
||||||
|
* Fake AirPods 2. Generation 🎭
|
||||||
|
* Fake AirPods 3. Generation 🎭
|
||||||
|
* Fake AirPods Pro1 🎭
|
||||||
|
* Fake AirPods Pro2 🎭
|
||||||
|
|
||||||
## Download
|
## Download
|
||||||
|
|
||||||
| Source | Status |
|
| Source | Status |
|
||||||
|-----------------------|--------|
|
|------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod) | [](https://play.google.com/store/apps/details?id=eu.darken.capod) |
|
| [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod) | [](https://play.google.com/store/apps/details?id=eu.darken.capod) |
|
||||||
| [Google Play Beta](https://play.google.com/apps/testing/eu.darken.capod) | ? |
|
| [Google Play Beta](https://play.google.com/apps/testing/eu.darken.capod) | [](https://play.google.com/apps/testing/eu.darken.capod) | |
|
||||||
| [Github Releases](https://github.com/d4rken-org/capod/releases) | [](https://github.com/d4rken-org/capod/releases/latest) |
|
| [Github Releases](https://github.com/d4rken-org/capod/releases) | [](https://github.com/d4rken-org/capod/releases/latest) |
|
||||||
| [F-Droid (IzzyOnDroid)](https://apt.izzysoft.de/packages/eu.darken.capod/) | [](https://apt.izzysoft.de/packages/eu.darken.capod/) |
|
| [F-Droid](https://f-droid.org/en/packages/eu.darken.capod/) | [](https://f-droid.org/en/packages/eu.darken.capod/) |
|
||||||
|
| [F-Droid (IzzyOnDroid)](https://apt.izzysoft.de/packages/eu.darken.capod/) | [](https://apt.izzysoft.de/packages/eu.darken.capod/) |
|
||||||
|
|
||||||
|
|
||||||
## Support the project
|
## Support the project
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ apply(plugin = "dagger.hilt.android.plugin")
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdk = ProjectConfig.compileSdk
|
compileSdk = ProjectConfig.compileSdk
|
||||||
|
namespace = "${ProjectConfig.packageName}.common"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
minSdk = ProjectConfig.minSdk
|
minSdk = ProjectConfig.minSdk
|
||||||
@@ -21,6 +22,10 @@ android {
|
|||||||
buildConfigField("String", "APPLICATION_ID", "\"${ProjectConfig.packageName}\"")
|
buildConfigField("String", "APPLICATION_ID", "\"${ProjectConfig.packageName}\"")
|
||||||
buildConfigField("String", "GITSHA", "\"${lastCommitHash()}\"")
|
buildConfigField("String", "GITSHA", "\"${lastCommitHash()}\"")
|
||||||
buildConfigField("String", "BUILDTIME", "\"${buildTime()}\"")
|
buildConfigField("String", "BUILDTIME", "\"${buildTime()}\"")
|
||||||
|
|
||||||
|
manifestPlaceholders["bugsnagApiKey"] = getBugSnagApiKey(
|
||||||
|
File(System.getProperty("user.home"), ".appconfig/${ProjectConfig.packageName}/bugsnag.properties")
|
||||||
|
) ?: "bugsnag_apikey_placeholder"
|
||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
@@ -29,12 +34,12 @@ android {
|
|||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
isCoreLibraryDesugaringEnabled = true
|
isCoreLibraryDesugaringEnabled = true
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_17
|
||||||
}
|
}
|
||||||
|
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = "1.8"
|
jvmTarget = "17"
|
||||||
freeCompilerArgs = freeCompilerArgs + listOf(
|
freeCompilerArgs = freeCompilerArgs + listOf(
|
||||||
"-opt-in=kotlin.ExperimentalStdlibApi",
|
"-opt-in=kotlin.ExperimentalStdlibApi",
|
||||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||||
@@ -95,6 +100,7 @@ dependencies {
|
|||||||
|
|
||||||
addTesting()
|
addTesting()
|
||||||
|
|
||||||
implementation("com.bugsnag:bugsnag-android:5.9.2")
|
"gplayImplementation"("com.bugsnag:bugsnag-android:5.9.2")
|
||||||
|
"gplayImplementation"("com.getkeepsafe.relinker:relinker:1.4.3")
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package eu.darken.capod.debug.autoreport
|
||||||
|
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
|
||||||
|
import eu.darken.capod.debug.autoreport.FossAutoReporting
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
@Module
|
||||||
|
abstract class AutoReportingModule {
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
abstract fun autoreporting(foss: FossAutoReporting): AutomaticBugReporter
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package eu.darken.capod.debug.autoreport
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class FossAutoReporting @Inject constructor() : AutomaticBugReporter {
|
||||||
|
override fun setup(application: Application) {
|
||||||
|
// NOOP
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun notify(throwable: Throwable) {
|
||||||
|
throw IllegalStateException("Who initliazed this? Without setup no calls to here!")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<meta-data
|
||||||
|
android:name="com.bugsnag.android.API_KEY"
|
||||||
|
android:value="${bugsnagApiKey}" />
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package eu.darken.capod.debug.autoreport
|
||||||
|
|
||||||
|
import dagger.Binds
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
@Module
|
||||||
|
abstract class AutoReportingModule {
|
||||||
|
@Binds
|
||||||
|
@Singleton
|
||||||
|
abstract fun autoreporting(foss: GplayAutoReporting): AutomaticBugReporter
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package eu.darken.capod.common.debug.autoreport.bugsnag
|
package eu.darken.capod.debug.autoreport
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
@@ -7,7 +7,7 @@ import com.bugsnag.android.Event
|
|||||||
import com.bugsnag.android.OnErrorCallback
|
import com.bugsnag.android.OnErrorCallback
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import eu.darken.capod.common.BuildConfigWrap
|
import eu.darken.capod.common.BuildConfigWrap
|
||||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
import eu.darken.capod.common.debug.DebugSettings
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||||
import eu.darken.capod.common.debug.logging.asLog
|
import eu.darken.capod.common.debug.logging.asLog
|
||||||
import eu.darken.capod.common.debug.logging.log
|
import eu.darken.capod.common.debug.logging.log
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package eu.darken.capod.common.debug.autoreport.bugsnag
|
package eu.darken.capod.debug.autoreport
|
||||||
|
|
||||||
import com.bugsnag.android.Event
|
import com.bugsnag.android.Event
|
||||||
import eu.darken.capod.common.debug.logging.Logging
|
import eu.darken.capod.common.debug.logging.Logging
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
package eu.darken.capod.common.debug.autoreport
|
package eu.darken.capod.debug.autoreport
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import com.bugsnag.android.Bugsnag
|
import com.bugsnag.android.Bugsnag
|
||||||
import com.bugsnag.android.Configuration
|
import com.bugsnag.android.Configuration
|
||||||
|
import com.getkeepsafe.relinker.ReLinker
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import eu.darken.capod.common.BuildConfigWrap
|
import eu.darken.capod.common.BuildConfigWrap
|
||||||
import eu.darken.capod.common.InstallId
|
import eu.darken.capod.common.InstallId
|
||||||
import eu.darken.capod.common.debug.Bugs
|
import eu.darken.capod.common.debug.Bugs
|
||||||
import eu.darken.capod.common.debug.autoreport.bugsnag.BugsnagErrorHandler
|
import eu.darken.capod.common.debug.DebugSettings
|
||||||
import eu.darken.capod.common.debug.autoreport.bugsnag.BugsnagLogger
|
import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
|
||||||
import eu.darken.capod.common.debug.autoreport.bugsnag.NOPBugsnagErrorHandler
|
|
||||||
import eu.darken.capod.common.debug.logging.Logging
|
import eu.darken.capod.common.debug.logging.Logging
|
||||||
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
|
||||||
@@ -18,19 +19,25 @@ import javax.inject.Provider
|
|||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class AutoReporting @Inject constructor(
|
class GplayAutoReporting @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val debugSettings: DebugSettings,
|
private val debugSettings: DebugSettings,
|
||||||
private val installId: InstallId,
|
private val installId: InstallId,
|
||||||
private val bugsnagLogger: Provider<BugsnagLogger>,
|
private val bugsnagLogger: Provider<BugsnagLogger>,
|
||||||
private val bugsnagErrorHandler: Provider<BugsnagErrorHandler>,
|
private val bugsnagErrorHandler: Provider<BugsnagErrorHandler>,
|
||||||
private val nopBugsnagErrorHandler: Provider<NOPBugsnagErrorHandler>,
|
private val nopBugsnagErrorHandler: Provider<NOPBugsnagErrorHandler>,
|
||||||
) {
|
) : AutomaticBugReporter {
|
||||||
|
|
||||||
fun setup() {
|
override fun setup(application: Application) {
|
||||||
val isEnabled = debugSettings.isAutoReportingEnabled.value
|
val isEnabled = debugSettings.isAutoReportingEnabled.value
|
||||||
log(TAG) { "setup(): isEnabled=$isEnabled" }
|
log(TAG) { "setup(): isEnabled=$isEnabled" }
|
||||||
|
|
||||||
|
if (!isEnabled) return
|
||||||
|
|
||||||
|
ReLinker
|
||||||
|
.log { message -> log(TAG) { "ReLinker: $message" } }
|
||||||
|
.loadLibrary(application, "bugsnag-plugin-android-anr")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val bugsnagConfig = Configuration.load(context).apply {
|
val bugsnagConfig = Configuration.load(context).apply {
|
||||||
if (debugSettings.isAutoReportingEnabled.value) {
|
if (debugSettings.isAutoReportingEnabled.value) {
|
||||||
@@ -48,13 +55,18 @@ class AutoReporting @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Bugsnag.start(context, bugsnagConfig)
|
Bugsnag.start(context, bugsnagConfig)
|
||||||
Bugs.ready = true
|
Bugs.reporter = this
|
||||||
} catch (e: IllegalStateException) {
|
} catch (e: IllegalStateException) {
|
||||||
log(TAG) { "Bugsnag API Key not configured." }
|
log(TAG) { "Bugsnag API Key not configured." }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun notify(throwable: Throwable) {
|
||||||
|
Bugsnag.notify(throwable)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = logTag("Debug", "AutoReport")
|
private val TAG = logTag("Debug", "AutoReport")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package eu.darken.capod.common.debug.autoreport.bugsnag
|
package eu.darken.capod.debug.autoreport
|
||||||
|
|
||||||
import com.bugsnag.android.Event
|
import com.bugsnag.android.Event
|
||||||
import com.bugsnag.android.OnErrorCallback
|
import com.bugsnag.android.OnErrorCallback
|
||||||
@@ -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,10 @@
|
|||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
|
|
||||||
|
fun BroadcastReceiver.PendingResult.finish2(): Boolean = try {
|
||||||
|
finish()
|
||||||
|
true
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
log { "BroadcastReceiver.PendingResult.finish() failed: $e" }
|
||||||
|
false
|
||||||
|
}
|
||||||
@@ -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 = false,
|
||||||
): Flow<List<BleScanResult>> = callbackFlow {
|
disableOffloadBatching: Boolean = false,
|
||||||
log(TAG, VERBOSE) { "scan(filters=$filters, scannerMode=$scannerMode, compatMode=$compatMode)" }
|
disableDirectScanCallback: Boolean = false,
|
||||||
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ package eu.darken.capod.common.bluetooth
|
|||||||
|
|
||||||
import dagger.Reusable
|
import dagger.Reusable
|
||||||
import eu.darken.capod.common.SystemClockWrap
|
import eu.darken.capod.common.SystemClockWrap
|
||||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
import eu.darken.capod.common.debug.DebugSettings
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.random.Random
|
import kotlin.random.Random
|
||||||
@@ -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()
|
||||||
}
|
}
|
||||||
@@ -27,9 +27,7 @@ class FakeBleData @Inject constructor(
|
|||||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
|
||||||
manufacturerSpecificData = mapOf(76 to "07 19 01 02 20 75 AA B6 31 00 05 9C 5A A4 5D C0 2C A0 B4 6F B9 ED 8E CE 03 97 CA".hexToByteArray())
|
manufacturerSpecificData = mapOf(76 to "07 19 01 02 20 75 AA B6 31 00 05 9C 5A A4 5D C0 2C A0 B4 6F B9 ED 8E CE 03 97 CA".hexToByteArray())
|
||||||
).run {
|
).run {
|
||||||
if (Random.nextBoolean()) {
|
fakeDevices.add(this)
|
||||||
fakeDevices.add(this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AirPods Gen2
|
// AirPods Gen2
|
||||||
@@ -40,9 +38,7 @@ class FakeBleData @Inject constructor(
|
|||||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 100,
|
||||||
manufacturerSpecificData = mapOf(76 to "07 19 01 0F 20 75 AA B6 31 00 05 9C 5A A4 5D C0 2C A0 B4 6F B9 ED 8E CE 03 97 CA".hexToByteArray())
|
manufacturerSpecificData = mapOf(76 to "07 19 01 0F 20 75 AA B6 31 00 05 9C 5A A4 5D C0 2C A0 B4 6F B9 ED 8E CE 03 97 CA".hexToByteArray())
|
||||||
).run {
|
).run {
|
||||||
if (Random.nextBoolean()) {
|
fakeDevices.add(this)
|
||||||
fakeDevices.add(this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AirPods Gen3
|
// AirPods Gen3
|
||||||
@@ -53,9 +49,7 @@ class FakeBleData @Inject constructor(
|
|||||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 200,
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 200,
|
||||||
manufacturerSpecificData = mapOf(76 to "07 19 01 13 20 55 AF 56 31 00 06 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88".hexToByteArray())
|
manufacturerSpecificData = mapOf(76 to "07 19 01 13 20 55 AF 56 31 00 06 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88".hexToByteArray())
|
||||||
).run {
|
).run {
|
||||||
if (Random.nextBoolean()) {
|
fakeDevices.add(this)
|
||||||
fakeDevices.add(this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// AirPods Max
|
// AirPods Max
|
||||||
BleScanResult(
|
BleScanResult(
|
||||||
@@ -65,9 +59,7 @@ class FakeBleData @Inject constructor(
|
|||||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 300,
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 300,
|
||||||
manufacturerSpecificData = mapOf(76 to "07 19 01 0A 20 02 05 80 04 0F 44 A7 60 9B F8 3C FD B1 D8 1C 61 EA 82 60 A3 2C 4E".hexToByteArray())
|
manufacturerSpecificData = mapOf(76 to "07 19 01 0A 20 02 05 80 04 0F 44 A7 60 9B F8 3C FD B1 D8 1C 61 EA 82 60 A3 2C 4E".hexToByteArray())
|
||||||
).run {
|
).run {
|
||||||
if (Random.nextBoolean()) {
|
fakeDevices.add(this)
|
||||||
fakeDevices.add(this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// BeatsFlex
|
// BeatsFlex
|
||||||
BleScanResult(
|
BleScanResult(
|
||||||
@@ -77,9 +69,7 @@ class FakeBleData @Inject constructor(
|
|||||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
|
||||||
manufacturerSpecificData = mapOf(76 to "07 19 01 10 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0".hexToByteArray())
|
manufacturerSpecificData = mapOf(76 to "07 19 01 10 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0".hexToByteArray())
|
||||||
).run {
|
).run {
|
||||||
if (Random.nextBoolean()) {
|
fakeDevices.add(this)
|
||||||
fakeDevices.add(this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tws i99999
|
// Tws i99999
|
||||||
@@ -90,9 +80,7 @@ class FakeBleData @Inject constructor(
|
|||||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 400,
|
||||||
manufacturerSpecificData = mapOf(76 to "07 13 01 02 20 71 AA 37 32 00 10 00 64 64 FF 00 00 00 00 00 00".hexToByteArray())
|
manufacturerSpecificData = mapOf(76 to "07 13 01 02 20 71 AA 37 32 00 10 00 64 64 FF 00 00 00 00 00 00".hexToByteArray())
|
||||||
).run {
|
).run {
|
||||||
if (Random.nextBoolean()) {
|
fakeDevices.add(this)
|
||||||
fakeDevices.add(this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown Device
|
// Unknown Device
|
||||||
@@ -103,9 +91,7 @@ class FakeBleData @Inject constructor(
|
|||||||
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 500,
|
generatedAtNanos = SystemClockWrap.elapsedRealtimeNanos + 500,
|
||||||
manufacturerSpecificData = mapOf(76 to "07 19 01 FF 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0".hexToByteArray())
|
manufacturerSpecificData = mapOf(76 to "07 19 01 FF 20 0A F4 8F 00 01 00 C4 71 9F 9C EF A2 E3 BA 66 FE 1D 45 9F C9 2F A0".hexToByteArray())
|
||||||
).run {
|
).run {
|
||||||
if (Random.nextBoolean()) {
|
fakeDevices.add(this)
|
||||||
fakeDevices.add(this)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fakeDevices
|
return fakeDevices
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package eu.darken.capod.common.collections
|
||||||
|
|
||||||
|
|
||||||
|
fun Collection<Int>.median(): Int = this.sorted().let {
|
||||||
|
if (it.size % 2 == 0) {
|
||||||
|
(it[it.size / 2] + it[(it.size - 1) / 2]) / 2
|
||||||
|
} else {
|
||||||
|
it[it.size / 2]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
package eu.darken.capod.common.debug
|
package eu.darken.capod.common.debug
|
||||||
|
|
||||||
import com.bugsnag.android.Bugsnag
|
import eu.darken.capod.common.debug.autoreport.AutomaticBugReporter
|
||||||
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.asLog
|
import eu.darken.capod.common.debug.logging.asLog
|
||||||
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
|
||||||
|
|
||||||
object Bugs {
|
object Bugs {
|
||||||
var ready = false
|
var reporter: AutomaticBugReporter? = null
|
||||||
fun report(
|
fun report(
|
||||||
tag: String,
|
tag: String,
|
||||||
message: String,
|
message: String,
|
||||||
@@ -16,11 +16,9 @@ object Bugs {
|
|||||||
log(TAG, VERBOSE) { "Reporting $exception" }
|
log(TAG, VERBOSE) { "Reporting $exception" }
|
||||||
log(tag, ERROR) { "$message\n${exception.asLog()}" }
|
log(tag, ERROR) { "$message\n${exception.asLog()}" }
|
||||||
|
|
||||||
if (!ready) {
|
reporter?.notify(exception) ?: run {
|
||||||
log(TAG, WARN) { "Bug tracking not initialized yet." }
|
log(TAG, WARN) { "Bug tracking not initialized yet." }
|
||||||
return
|
|
||||||
}
|
}
|
||||||
Bugsnag.notify(exception)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val TAG = logTag("Bugs")
|
private val TAG = logTag("Bugs")
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package eu.darken.capod.common.debug.autoreport
|
package eu.darken.capod.common.debug
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package eu.darken.capod.common.debug.autoreport
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
|
||||||
|
interface AutomaticBugReporter {
|
||||||
|
|
||||||
|
fun setup(application: Application)
|
||||||
|
|
||||||
|
fun notify(throwable: Throwable)
|
||||||
|
}
|
||||||
@@ -9,4 +9,9 @@ object PendingIntentCompat {
|
|||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
}
|
}
|
||||||
|
val FLAG_MUTABLE: Int = if (hasApiLevel(31)) {
|
||||||
|
PendingIntent.FLAG_MUTABLE
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@ import androidx.preference.PreferenceDataStore
|
|||||||
import com.squareup.moshi.Moshi
|
import com.squareup.moshi.Moshi
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import eu.darken.capod.common.bluetooth.ScannerMode
|
import eu.darken.capod.common.bluetooth.ScannerMode
|
||||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
import eu.darken.capod.common.debug.DebugSettings
|
||||||
import eu.darken.capod.common.preferences.PreferenceStoreMapper
|
import eu.darken.capod.common.preferences.PreferenceStoreMapper
|
||||||
import eu.darken.capod.common.preferences.Settings
|
import eu.darken.capod.common.preferences.Settings
|
||||||
import eu.darken.capod.common.preferences.createFlowPreference
|
import eu.darken.capod.common.preferences.createFlowPreference
|
||||||
@@ -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", true)
|
||||||
"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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package eu.darken.capod.monitor.core
|
|
||||||
|
|
||||||
import dagger.BindsInstance
|
|
||||||
import dagger.hilt.DefineComponent
|
|
||||||
import dagger.hilt.components.SingletonComponent
|
|
||||||
|
|
||||||
@MonitorScope
|
|
||||||
@DefineComponent(parent = SingletonComponent::class)
|
|
||||||
interface MonitorComponent {
|
|
||||||
|
|
||||||
@DefineComponent.Builder
|
|
||||||
interface Builder {
|
|
||||||
|
|
||||||
fun coroutineScope(@BindsInstance coroutineScope: MonitorCoroutineScope): Builder
|
|
||||||
|
|
||||||
fun build(): MonitorComponent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
package eu.darken.capod.monitor.core
|
|
||||||
|
|
||||||
import dagger.Binds
|
|
||||||
import dagger.Module
|
|
||||||
import dagger.hilt.InstallIn
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
|
|
||||||
@InstallIn(MonitorComponent::class)
|
|
||||||
@Module()
|
|
||||||
abstract class ProcessorModule {
|
|
||||||
|
|
||||||
@Binds
|
|
||||||
@MonitorScope
|
|
||||||
abstract fun processorScope(scope: MonitorCoroutineScope): CoroutineScope
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package eu.darken.capod.monitor.core
|
|
||||||
|
|
||||||
import javax.inject.Qualifier
|
|
||||||
|
|
||||||
@Qualifier
|
|
||||||
@MustBeDocumented
|
|
||||||
@Retention(AnnotationRetention.RUNTIME)
|
|
||||||
annotation class MonitorScope
|
|
||||||
@@ -4,8 +4,9 @@ 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.DebugSettings
|
||||||
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.Logging.Priority.WARN
|
||||||
import eu.darken.capod.common.debug.logging.asLog
|
import eu.darken.capod.common.debug.logging.asLog
|
||||||
@@ -13,6 +14,7 @@ import eu.darken.capod.common.debug.logging.log
|
|||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.common.flow.replayingShare
|
import eu.darken.capod.common.flow.replayingShare
|
||||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||||
|
import eu.darken.capod.common.flow.throttleLatest
|
||||||
import eu.darken.capod.main.core.GeneralSettings
|
import eu.darken.capod.main.core.GeneralSettings
|
||||||
import eu.darken.capod.main.core.PermissionTool
|
import eu.darken.capod.main.core.PermissionTool
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
@@ -83,26 +85,51 @@ 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) ->
|
.throttleLatest(1000)
|
||||||
|
.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 +161,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 +214,6 @@ class PodMonitor @Inject constructor(
|
|||||||
.also { log(TAG) { "Cached mainDevice is $it" } }
|
.also { log(TAG) { "Cached mainDevice is $it" } }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getUnfilteredFilter(): ScanFilter {
|
|
||||||
return ScanFilter.Builder().build()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = logTag("Monitor", "PodMonitor")
|
private val TAG = logTag("Monitor", "PodMonitor")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
package eu.darken.capod.pods.core
|
package eu.darken.capod.pods.core
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import eu.darken.capod.common.R
|
||||||
|
|
||||||
interface DualPodDevice : PodDevice {
|
interface DualPodDevice : PodDevice {
|
||||||
|
|
||||||
enum class Pod {
|
enum class Pod {
|
||||||
@@ -10,4 +13,12 @@ interface DualPodDevice : PodDevice {
|
|||||||
val batteryLeftPodPercent: Float?
|
val batteryLeftPodPercent: Float?
|
||||||
|
|
||||||
val batteryRightPodPercent: Float?
|
val batteryRightPodPercent: Float?
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
val leftPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_gen1_left
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
val rightPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_gen1_right
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
package eu.darken.capod.pods.core
|
package eu.darken.capod.pods.core
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import eu.darken.capod.common.R
|
||||||
|
|
||||||
interface HasCase {
|
interface HasCase {
|
||||||
|
|
||||||
val batteryCasePercent: Float?
|
val batteryCasePercent: Float?
|
||||||
|
|
||||||
val isCaseCharging: Boolean
|
val isCaseCharging: Boolean
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
val caseIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_gen1_case
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,9 @@ import com.squareup.moshi.Json
|
|||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
import eu.darken.capod.common.R
|
import eu.darken.capod.common.R
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
|
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
@@ -31,14 +34,20 @@ interface PodDevice {
|
|||||||
val rssi: Int
|
val rssi: Int
|
||||||
get() = scanResult.rssi
|
get() = scanResult.rssi
|
||||||
|
|
||||||
val confidence: Float
|
val reliability: Float
|
||||||
|
|
||||||
/**
|
|
||||||
* This is not correct but it works ¯\_(ツ)_/¯
|
|
||||||
* The range of the RSSI is device specific (ROMs).
|
|
||||||
*/
|
|
||||||
val signalQuality: Float
|
val signalQuality: Float
|
||||||
get() = ((100 - abs(rssi)) / 100f) * (max(BASE_CONFIDENCE, confidence))
|
get() {
|
||||||
|
/**
|
||||||
|
* This is not correct but it works ¯\_(ツ)_/¯
|
||||||
|
* The range of the RSSI is device specific (ROMs).
|
||||||
|
*/
|
||||||
|
val sqRssi = ((100 - abs(rssi)) / 100f)
|
||||||
|
val sqReliability = max(BASE_CONFIDENCE, reliability)
|
||||||
|
val sqAge = (Duration.between(seenFirstAt, Instant.now()).toMinutes().coerceAtMost(60) / 60f) * 0.25f
|
||||||
|
log(VERBOSE) { "Signal Quality ($address): rssi=$sqRssi, reliability=$reliability, age=$sqAge" }
|
||||||
|
return (sqRssi + sqReliability + sqAge) / 2f
|
||||||
|
}
|
||||||
|
|
||||||
val rawData: Map<Int, ByteArray>
|
val rawData: Map<Int, ByteArray>
|
||||||
get() = scanResult.manufacturerSpecificData
|
get() = scanResult.manufacturerSpecificData
|
||||||
@@ -60,31 +69,31 @@ interface PodDevice {
|
|||||||
@JsonClass(generateAdapter = false)
|
@JsonClass(generateAdapter = false)
|
||||||
enum class Model(
|
enum class Model(
|
||||||
val label: String,
|
val label: String,
|
||||||
@DrawableRes val iconRes: Int = R.drawable.ic_device_generic_earbuds,
|
@DrawableRes val iconRes: Int = R.drawable.devic_earbuds_generic_both,
|
||||||
) {
|
) {
|
||||||
@Json(name = "airpods.gen1") AIRPODS_GEN1(
|
@Json(name = "airpods.gen1") AIRPODS_GEN1(
|
||||||
label = "AirPods (Gen 1)",
|
label = "AirPods (Gen 1)",
|
||||||
iconRes = R.drawable.ic_device_airpods_gen1,
|
iconRes = R.drawable.devic_airpods_gen1_both,
|
||||||
),
|
),
|
||||||
@Json(name = "airpods.gen2") AIRPODS_GEN2(
|
@Json(name = "airpods.gen2") AIRPODS_GEN2(
|
||||||
"AirPods (Gen 2)",
|
"AirPods (Gen 2)",
|
||||||
R.drawable.ic_device_airpods_gen2,
|
R.drawable.devic_airpods_gen2_both,
|
||||||
),
|
),
|
||||||
@Json(name = "airpods.gen3") AIRPODS_GEN3(
|
@Json(name = "airpods.gen3") AIRPODS_GEN3(
|
||||||
"AirPods (Gen 3)",
|
"AirPods (Gen 3)",
|
||||||
R.drawable.ic_device_airpods_gen2,
|
R.drawable.devic_airpods_gen2_both,
|
||||||
),
|
),
|
||||||
@Json(name = "airpods.pro") AIRPODS_PRO(
|
@Json(name = "airpods.pro") AIRPODS_PRO(
|
||||||
"AirPods Pro",
|
"AirPods Pro",
|
||||||
R.drawable.ic_device_airpods_gen2
|
R.drawable.devic_airpods_pro2_both
|
||||||
),
|
),
|
||||||
@Json(name = "airpods.pro2") AIRPODS_PRO2(
|
@Json(name = "airpods.pro2") AIRPODS_PRO2(
|
||||||
"AirPods Pro 2",
|
"AirPods Pro 2",
|
||||||
R.drawable.ic_device_airpods_gen2
|
R.drawable.devic_airpods_pro2_both
|
||||||
),
|
),
|
||||||
@Json(name = "airpods.max") AIRPODS_MAX(
|
@Json(name = "airpods.max") AIRPODS_MAX(
|
||||||
"AirPods Max",
|
"AirPods Max",
|
||||||
R.drawable.ic_device_generic_headphones
|
R.drawable.devic_headphones_generic
|
||||||
),
|
),
|
||||||
@Json(name = "beats.flex") BEATS_FLEX(
|
@Json(name = "beats.flex") BEATS_FLEX(
|
||||||
"Beats Flex"
|
"Beats Flex"
|
||||||
@@ -104,14 +113,28 @@ interface PodDevice {
|
|||||||
@Json(name = "beats.powerbeats.pro") POWERBEATS_PRO(
|
@Json(name = "beats.powerbeats.pro") POWERBEATS_PRO(
|
||||||
"Power Beats Pro"
|
"Power Beats Pro"
|
||||||
),
|
),
|
||||||
@Json(name = "fakes.tws.i99999") FAKE_AIRPODS_GEN1(
|
@Json(name = "beats.fit.pro") BEATS_FIT_PRO(
|
||||||
"AirPods (Gen 1)? \uD83C\uDFAD"
|
"Beats Fit Pro"
|
||||||
),
|
),
|
||||||
@Json(name = "fakes.varunr.airpodspro") FAKE_AIRPODS_PRO(
|
@Json(name = "fakes.tws.i99999") FAKE_AIRPODS_GEN1(
|
||||||
"AirPods Pro? \uD83C\uDFAD"
|
"AirPods (Gen 1)? \uD83C\uDFAD",
|
||||||
|
R.drawable.devic_airpods_gen1_both,
|
||||||
|
),
|
||||||
|
@Json(name = "fakes.generic.airpods.gen2") FAKE_AIRPODS_GEN2(
|
||||||
|
"AirPods (Gen 2)? \uD83C\uDFAD",
|
||||||
|
R.drawable.devic_airpods_gen2_both,
|
||||||
),
|
),
|
||||||
@Json(name = "fakes.generic.airpods.gen3") FAKE_AIRPODS_GEN3(
|
@Json(name = "fakes.generic.airpods.gen3") FAKE_AIRPODS_GEN3(
|
||||||
"AirPods (Gen 3)? \uD83C\uDFAD"
|
"AirPods (Gen 3)? \uD83C\uDFAD",
|
||||||
|
R.drawable.devic_airpods_gen2_both,
|
||||||
|
),
|
||||||
|
@Json(name = "fakes.varunr.airpodspro") FAKE_AIRPODS_PRO(
|
||||||
|
"AirPods Pro? \uD83C\uDFAD",
|
||||||
|
R.drawable.devic_airpods_pro2_both,
|
||||||
|
),
|
||||||
|
@Json(name = "fakes.generic.airpods.pro2") FAKE_AIRPODS_PRO2(
|
||||||
|
"AirPods Pro2? \uD83C\uDFAD",
|
||||||
|
R.drawable.devic_airpods_pro2_both,
|
||||||
),
|
),
|
||||||
@Json(name = "unknown") UNKNOWN(
|
@Json(name = "unknown") UNKNOWN(
|
||||||
"Unknown"
|
"Unknown"
|
||||||
@@ -119,6 +142,6 @@ interface PodDevice {
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val BASE_CONFIDENCE = 0.5f
|
const val BASE_CONFIDENCE = 0.0f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,8 +25,8 @@ fun SinglePodDevice.getBatteryLevelHeadset(context: Context): String =
|
|||||||
?: context.getString(R.string.general_value_not_available_label)
|
?: context.getString(R.string.general_value_not_available_label)
|
||||||
|
|
||||||
fun PodDevice.getSignalQuality(context: Context): String {
|
fun PodDevice.getSignalQuality(context: Context): String {
|
||||||
val percentage = 100 * signalQuality
|
val multiplier = 100 * signalQuality
|
||||||
return "~${percentage.roundToInt()}%"
|
return "${multiplier.roundToInt()}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@DrawableRes
|
@DrawableRes
|
||||||
|
|||||||
@@ -7,9 +7,7 @@ import dagger.hilt.components.SingletonComponent
|
|||||||
import dagger.multibindings.IntoSet
|
import dagger.multibindings.IntoSet
|
||||||
import eu.darken.capod.pods.core.apple.airpods.*
|
import eu.darken.capod.pods.core.apple.airpods.*
|
||||||
import eu.darken.capod.pods.core.apple.beats.*
|
import eu.darken.capod.pods.core.apple.beats.*
|
||||||
import eu.darken.capod.pods.core.apple.misc.FakeAirPodsGen1
|
import eu.darken.capod.pods.core.apple.misc.*
|
||||||
import eu.darken.capod.pods.core.apple.misc.FakeAirPodsGen3
|
|
||||||
import eu.darken.capod.pods.core.apple.misc.FakeAirPodsPro
|
|
||||||
|
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
@Module
|
@Module
|
||||||
@@ -29,8 +27,11 @@ abstract class AppleFactoryModule {
|
|||||||
@Binds @IntoSet abstract fun beatsX(factory: BeatsX.Factory): ApplePodsFactory<out ApplePods>
|
@Binds @IntoSet abstract fun beatsX(factory: BeatsX.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun powerBeats3(factory: PowerBeats3.Factory): ApplePodsFactory<out ApplePods>
|
@Binds @IntoSet abstract fun powerBeats3(factory: PowerBeats3.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePodsFactory<out ApplePods>
|
@Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePodsFactory<out ApplePods>
|
||||||
|
@Binds @IntoSet abstract fun beatsFitPro(factory: BeatsFitPro.Factory): ApplePodsFactory<out ApplePods>
|
||||||
|
|
||||||
@Binds @IntoSet abstract fun fakeAirPodsGen1(factory: FakeAirPodsGen1.Factory): ApplePodsFactory<out ApplePods>
|
@Binds @IntoSet abstract fun fakeAirPodsGen1(factory: FakeAirPodsGen1.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun fakeAirPodsPro(factory: FakeAirPodsPro.Factory): ApplePodsFactory<out ApplePods>
|
@Binds @IntoSet abstract fun fakeAirPodsGen2(factory: FakeAirPodsGen2.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun fakeAirPodsGen3(factory: FakeAirPodsGen3.Factory): ApplePodsFactory<out ApplePods>
|
@Binds @IntoSet abstract fun fakeAirPodsGen3(factory: FakeAirPodsGen3.Factory): ApplePodsFactory<out ApplePods>
|
||||||
|
@Binds @IntoSet abstract fun fakeAirPodsPro(factory: FakeAirPodsPro.Factory): ApplePodsFactory<out ApplePods>
|
||||||
|
@Binds @IntoSet abstract fun fakeAirPodsPro2(factory: FakeAirPodsPro2.Factory): ApplePodsFactory<out ApplePods>
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package eu.darken.capod.pods.core.apple
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
|
import eu.darken.capod.common.collections.median
|
||||||
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.Logging.Priority.WARN
|
||||||
import eu.darken.capod.common.debug.logging.log
|
import eu.darken.capod.common.debug.logging.log
|
||||||
@@ -8,10 +9,10 @@ import eu.darken.capod.common.lowerNibble
|
|||||||
import eu.darken.capod.common.upperNibble
|
import eu.darken.capod.common.upperNibble
|
||||||
import eu.darken.capod.pods.core.HasCase
|
import eu.darken.capod.pods.core.HasCase
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro
|
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
||||||
|
|
||||||
@@ -38,7 +39,8 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
|||||||
val id: PodDevice.Id,
|
val id: PodDevice.Id,
|
||||||
val seenFirstAt: Instant,
|
val seenFirstAt: Instant,
|
||||||
val seenCounter: Int,
|
val seenCounter: Int,
|
||||||
val history: List<ApplePods>
|
val history: List<ApplePods>,
|
||||||
|
val lastCaseBattery: Float?,
|
||||||
) {
|
) {
|
||||||
val lastMessage: ProximityPairing.Message
|
val lastMessage: ProximityPairing.Message
|
||||||
get() = history.last().proximityMessage
|
get() = history.last().proximityMessage
|
||||||
@@ -46,63 +48,81 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
|||||||
val lastAddress: String
|
val lastAddress: String
|
||||||
get() = history.last().address
|
get() = history.last().address
|
||||||
|
|
||||||
val confidence: Float
|
val reliability: Float
|
||||||
get() = 0.75f + (history.size / (MAX_HISTORY * 4f))
|
get() {
|
||||||
|
if (history.size < 2) return 0f
|
||||||
|
|
||||||
fun averageRssi(latest: Int): Int =
|
val now = Instant.now()
|
||||||
history.map { it.rssi }.plus(latest).takeLast(10).median()
|
|
||||||
|
val pingInterval = List(history.dropLast(1).size) { index ->
|
||||||
|
Duration.between(history[index].seenLastAt, history[index + 1].seenLastAt).toMillis()
|
||||||
|
}.map { it.toInt() }.median()
|
||||||
|
|
||||||
|
val expectedPings: Float = LOOKBACK.toMillis() / pingInterval.toFloat()
|
||||||
|
|
||||||
|
val recentPings = history.filter {
|
||||||
|
Duration.between(it.seenLastAt, now).toMillis() < LOOKBACK.toMillis() + pingInterval
|
||||||
|
}.size
|
||||||
|
|
||||||
|
val reliability: Float = (recentPings / expectedPings)
|
||||||
|
// log("#####") { "interval=$pingInterval, expectedPerMinute=$expectedPings, count=$recentPings" }
|
||||||
|
|
||||||
|
return max(0.0f, reliability).coerceAtMost(1f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun rssiSmoothed(latest: Int): Int {
|
||||||
|
val now = Instant.now()
|
||||||
|
return history
|
||||||
|
.filter { Duration.between(it.seenLastAt, now) < LOOKBACK }
|
||||||
|
.map { it.rssi }
|
||||||
|
.plus(latest)
|
||||||
|
.median()
|
||||||
|
}
|
||||||
|
|
||||||
fun isOlderThan(age: Duration): Boolean {
|
fun isOlderThan(age: Duration): Boolean {
|
||||||
val now = Instant.now()
|
val now = Instant.now()
|
||||||
return Duration.between(history.last().seenLastAt, now) > age
|
return Duration.between(history.last().seenLastAt, now) > age
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun List<Int>.median(): Int = this.sorted().let {
|
|
||||||
if (it.size % 2 == 0)
|
|
||||||
(it[it.size / 2] + it[(it.size - 1) / 2]) / 2
|
|
||||||
else
|
|
||||||
it[it.size / 2]
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String = "KnownDevice(history=${history.size}, last=${history.last()})"
|
override fun toString(): String = "KnownDevice(history=${history.size}, last=${history.last()})"
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val MAX_HISTORY = 20
|
// 30 seconds at fastest interval
|
||||||
|
const val MAX_HISTORY = 60
|
||||||
|
val LOOKBACK = Duration.ofSeconds(30)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal val knownDevices = mutableMapOf<PodDevice.Id, KnownDevice>()
|
internal val knownDevices = mutableMapOf<PodDevice.Id, KnownDevice>()
|
||||||
|
|
||||||
|
|
||||||
fun KnownDevice.getLatestCaseBattery(): Float? = history
|
fun KnownDevice.getLatestCaseBattery(): Float? = this.lastCaseBattery
|
||||||
|
|
||||||
|
private fun Collection<ApplePods>.determineLatestCaseBattery(): Float? = this
|
||||||
.filterIsInstance<HasCase>()
|
.filterIsInstance<HasCase>()
|
||||||
.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(
|
||||||
if (definitive.contains(basic.caseLidState)) return null
|
DualApplePods.LidState.OPEN,
|
||||||
|
DualApplePods.LidState.CLOSED,
|
||||||
|
DualApplePods.LidState.NOT_IN_CASE,
|
||||||
|
)
|
||||||
|
if (definitive.contains(basic.caseLidState)) return basic.caseLidState
|
||||||
|
|
||||||
return history
|
return history
|
||||||
.filterIsInstance<AirPodsPro>() // TODO why is this AirPodsPro specific here?
|
.takeLast(2)
|
||||||
.lastOrNull { it.caseLidState != DualAirPods.LidState.NOT_IN_CASE }
|
.filterIsInstance<DualApplePods>()
|
||||||
|
.lastOrNull { it.caseLidState != DualApplePods.LidState.UNKNOWN }
|
||||||
?.caseLidState
|
?.caseLidState
|
||||||
|
?: DualApplePods.LidState.NOT_IN_CASE
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun historyTrimmer(
|
open fun historyTrimmer(
|
||||||
pods: List<ApplePods>
|
pods: List<ApplePods>
|
||||||
): List<ApplePods> {
|
): List<ApplePods> {
|
||||||
var trimmed = pods.takeLast(KnownDevice.MAX_HISTORY)
|
return pods.takeLast(KnownDevice.MAX_HISTORY)
|
||||||
|
|
||||||
// We want to keep the case information
|
|
||||||
// If both pods are removed, and produce more history, we otherwise lose the case battery info.
|
|
||||||
val caseInfoFat = pods.lastOrNull { it is HasCase && it.batteryCasePercent != null }
|
|
||||||
val caseInfoTrimmed = trimmed.lastOrNull { it is HasCase && it.batteryCasePercent != null }
|
|
||||||
if (caseInfoFat != null && caseInfoTrimmed == null) {
|
|
||||||
trimmed = listOf(caseInfoFat) + trimmed.takeLast(KnownDevice.MAX_HISTORY - 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return trimmed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal open fun searchHistory(current: PodType): KnownDevice? {
|
internal open fun searchHistory(current: PodType): KnownDevice? {
|
||||||
@@ -110,7 +130,7 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
|||||||
val message = current.proximityMessage
|
val message = current.proximityMessage
|
||||||
|
|
||||||
knownDevices.values.toList().forEach { knownDevice ->
|
knownDevices.values.toList().forEach { knownDevice ->
|
||||||
if (knownDevice.isOlderThan(Duration.ofSeconds(20))) {
|
if (knownDevice.isOlderThan(Duration.ofSeconds(30))) {
|
||||||
log(tag, VERBOSE) { "searchHistory1: Removing stale known device: $knownDevice" }
|
log(tag, VERBOSE) { "searchHistory1: Removing stale known device: $knownDevice" }
|
||||||
knownDevices.remove(knownDevice.id)
|
knownDevices.remove(knownDevice.id)
|
||||||
}
|
}
|
||||||
@@ -146,18 +166,22 @@ abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
|||||||
|
|
||||||
knownDevices[device.identifier] = when {
|
knownDevices[device.identifier] = when {
|
||||||
existing != null -> {
|
existing != null -> {
|
||||||
|
val history = existing.history.plus(device)
|
||||||
existing.copy(
|
existing.copy(
|
||||||
seenCounter = existing.seenCounter + 1,
|
seenCounter = existing.seenCounter + 1,
|
||||||
history = existing.history.plus(device)
|
history = history,
|
||||||
|
lastCaseBattery = history.determineLatestCaseBattery() ?: existing.lastCaseBattery
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
log(tag) { "searchHistory1: Creating new history for $device" }
|
log(tag) { "searchHistory1: Creating new history for $device" }
|
||||||
|
val history = listOf(device)
|
||||||
KnownDevice(
|
KnownDevice(
|
||||||
id = device.identifier,
|
id = device.identifier,
|
||||||
seenFirstAt = device.seenFirstAt,
|
seenFirstAt = device.seenFirstAt,
|
||||||
seenCounter = 1,
|
seenCounter = 1,
|
||||||
history = listOf(device)
|
history = history,
|
||||||
|
lastCaseBattery = history.determineLatestCaseBattery()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -17,18 +17,18 @@ data class AirPodsGen1 constructor(
|
|||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: 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
|
||||||
@@ -55,9 +55,9 @@ data class AirPodsGen1 constructor(
|
|||||||
seenFirstAt = result.seenFirstAt,
|
seenFirstAt = result.seenFirstAt,
|
||||||
seenLastAt = scanResult.receivedAt,
|
seenLastAt = scanResult.receivedAt,
|
||||||
seenCounter = result.seenCounter,
|
seenCounter = result.seenCounter,
|
||||||
confidence = result.confidence,
|
reliability = result.reliability,
|
||||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
rssiAverage = result.averageRssi(basic.rssi),
|
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||||
cachedCaseState = result.getLatestCaseLidState(basic)
|
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -17,22 +17,22 @@ data class AirPodsGen2 constructor(
|
|||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: 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) {
|
||||||
|
|
||||||
@@ -54,9 +54,9 @@ data class AirPodsGen2 constructor(
|
|||||||
seenFirstAt = result.seenFirstAt,
|
seenFirstAt = result.seenFirstAt,
|
||||||
seenLastAt = scanResult.receivedAt,
|
seenLastAt = scanResult.receivedAt,
|
||||||
seenCounter = result.seenCounter,
|
seenCounter = result.seenCounter,
|
||||||
confidence = result.confidence,
|
reliability = result.reliability,
|
||||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
rssiAverage = result.averageRssi(basic.rssi),
|
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||||
cachedCaseState = result.getLatestCaseLidState(basic)
|
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -17,21 +17,22 @@ data class AirPodsGen3 constructor(
|
|||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: 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) {
|
||||||
|
|
||||||
@@ -53,9 +54,9 @@ data class AirPodsGen3 constructor(
|
|||||||
seenFirstAt = result.seenFirstAt,
|
seenFirstAt = result.seenFirstAt,
|
||||||
seenLastAt = scanResult.receivedAt,
|
seenLastAt = scanResult.receivedAt,
|
||||||
seenCounter = result.seenCounter,
|
seenCounter = result.seenCounter,
|
||||||
confidence = result.confidence,
|
reliability = result.reliability,
|
||||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
rssiAverage = result.averageRssi(basic.rssi),
|
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||||
cachedCaseState = result.getLatestCaseLidState(basic)
|
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ package eu.darken.capod.pods.core.apple.airpods
|
|||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.common.isBitSet
|
import eu.darken.capod.common.isBitSet
|
||||||
|
import eu.darken.capod.pods.core.HasChargeDetection
|
||||||
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
|
||||||
@@ -19,23 +21,20 @@ data class AirPodsMax(
|
|||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
) : SingleApplePods, HasEarDetection {
|
) : SingleApplePods, HasEarDetection, HasChargeDetection, 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
|
override val isHeadsetBeingCharged: Boolean
|
||||||
get() = rawStatus.isBitSet(1)
|
|
||||||
|
|
||||||
val isHeadsetBeingCharged: Boolean
|
|
||||||
get() = rawFlags.isBitSet(0)
|
get() = rawFlags.isBitSet(0)
|
||||||
|
|
||||||
override val isBeingWorn: Boolean
|
override val isBeingWorn: Boolean
|
||||||
get() = isHeadphonesBeingWorn
|
get() = rawStatus.isBitSet(5)
|
||||||
|
|
||||||
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||||
|
|
||||||
@@ -57,8 +56,8 @@ data class AirPodsMax(
|
|||||||
seenFirstAt = result.seenFirstAt,
|
seenFirstAt = result.seenFirstAt,
|
||||||
seenLastAt = scanResult.receivedAt,
|
seenLastAt = scanResult.receivedAt,
|
||||||
seenCounter = result.seenCounter,
|
seenCounter = result.seenCounter,
|
||||||
confidence = result.confidence,
|
reliability = result.reliability,
|
||||||
rssiAverage = result.averageRssi(basic.rssi),
|
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package eu.darken.capod.pods.core.apple.airpods
|
package eu.darken.capod.pods.core.apple.airpods
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import eu.darken.capod.common.R
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods.LidState
|
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -18,14 +20,30 @@ data class AirPodsPro(
|
|||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: 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: LidState? = null
|
private val cachedCaseState: LidState? = null
|
||||||
) : DualAirPods {
|
) : DualApplePods, HasStateDetectionAirPods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val iconRes: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_both
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val caseIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_case
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val leftPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_left
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val rightPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_right
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
@@ -33,7 +51,7 @@ data class AirPodsPro(
|
|||||||
get() = cachedCaseState ?: super.caseLidState
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
get() = rssiAverage ?: super.rssi
|
get() = rssiAverage ?: super<DualApplePods>.rssi
|
||||||
|
|
||||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
@@ -55,9 +73,9 @@ data class AirPodsPro(
|
|||||||
seenFirstAt = result.seenFirstAt,
|
seenFirstAt = result.seenFirstAt,
|
||||||
seenLastAt = scanResult.receivedAt,
|
seenLastAt = scanResult.receivedAt,
|
||||||
seenCounter = result.seenCounter,
|
seenCounter = result.seenCounter,
|
||||||
confidence = result.confidence,
|
reliability = result.reliability,
|
||||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
rssiAverage = result.averageRssi(basic.rssi),
|
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||||
cachedCaseState = result.getLatestCaseLidState(basic)
|
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package eu.darken.capod.pods.core.apple.airpods
|
package eu.darken.capod.pods.core.apple.airpods
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import eu.darken.capod.common.R
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods.LidState
|
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -18,14 +20,30 @@ data class AirPodsPro2(
|
|||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: 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: LidState? = null
|
private val cachedCaseState: LidState? = null
|
||||||
) : DualAirPods {
|
) : DualApplePods, HasStateDetectionAirPods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO2
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO2
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val iconRes: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_both
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val caseIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_case
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val leftPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_left
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val rightPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_right
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
@@ -33,7 +51,7 @@ data class AirPodsPro2(
|
|||||||
get() = cachedCaseState ?: super.caseLidState
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
get() = rssiAverage ?: super.rssi
|
get() = rssiAverage ?: super<HasStateDetectionAirPods>.rssi
|
||||||
|
|
||||||
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
@@ -55,9 +73,9 @@ data class AirPodsPro2(
|
|||||||
seenFirstAt = result.seenFirstAt,
|
seenFirstAt = result.seenFirstAt,
|
||||||
seenLastAt = scanResult.receivedAt,
|
seenLastAt = scanResult.receivedAt,
|
||||||
seenCounter = result.seenCounter,
|
seenCounter = result.seenCounter,
|
||||||
confidence = result.confidence,
|
reliability = result.reliability,
|
||||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
rssiAverage = result.averageRssi(basic.rssi),
|
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||||
cachedCaseState = result.getLatestCaseLidState(basic)
|
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 reliability: 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,
|
||||||
|
reliability = result.reliability,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||||
|
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DEVICE_CODE = 0x1220.toUShort()
|
||||||
|
private val TAG = logTag("PodDevice", "Beats", "Fit", "Pro")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ data class BeatsFlex(
|
|||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
) : SingleApplePods {
|
) : SingleApplePods {
|
||||||
|
|
||||||
@@ -46,8 +46,8 @@ data class BeatsFlex(
|
|||||||
seenFirstAt = result.seenFirstAt,
|
seenFirstAt = result.seenFirstAt,
|
||||||
seenLastAt = scanResult.receivedAt,
|
seenLastAt = scanResult.receivedAt,
|
||||||
seenCounter = result.seenCounter,
|
seenCounter = result.seenCounter,
|
||||||
confidence = result.confidence,
|
reliability = result.reliability,
|
||||||
rssiAverage = result.averageRssi(basic.rssi),
|
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ data class BeatsSolo3(
|
|||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
) : SingleApplePods {
|
) : SingleApplePods {
|
||||||
|
|
||||||
@@ -45,8 +45,8 @@ data class BeatsSolo3(
|
|||||||
seenFirstAt = result.seenFirstAt,
|
seenFirstAt = result.seenFirstAt,
|
||||||
seenLastAt = scanResult.receivedAt,
|
seenLastAt = scanResult.receivedAt,
|
||||||
seenCounter = result.seenCounter,
|
seenCounter = result.seenCounter,
|
||||||
confidence = result.confidence,
|
reliability = result.reliability,
|
||||||
rssiAverage = result.averageRssi(basic.rssi),
|
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ data class BeatsStudio3(
|
|||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
) : SingleApplePods {
|
) : SingleApplePods {
|
||||||
|
|
||||||
@@ -43,8 +43,8 @@ data class BeatsStudio3(
|
|||||||
seenFirstAt = result.seenFirstAt,
|
seenFirstAt = result.seenFirstAt,
|
||||||
seenLastAt = scanResult.receivedAt,
|
seenLastAt = scanResult.receivedAt,
|
||||||
seenCounter = result.seenCounter,
|
seenCounter = result.seenCounter,
|
||||||
confidence = result.confidence,
|
reliability = result.reliability,
|
||||||
rssiAverage = result.averageRssi(basic.rssi),
|
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ data class BeatsX(
|
|||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
) : SingleApplePods {
|
) : SingleApplePods {
|
||||||
|
|
||||||
@@ -46,8 +46,8 @@ data class BeatsX(
|
|||||||
seenFirstAt = result.seenFirstAt,
|
seenFirstAt = result.seenFirstAt,
|
||||||
seenLastAt = scanResult.receivedAt,
|
seenLastAt = scanResult.receivedAt,
|
||||||
seenCounter = result.seenCounter,
|
seenCounter = result.seenCounter,
|
||||||
confidence = result.confidence,
|
reliability = result.reliability,
|
||||||
rssiAverage = result.averageRssi(basic.rssi),
|
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ data class PowerBeats3(
|
|||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
) : SingleApplePods {
|
) : SingleApplePods {
|
||||||
|
|
||||||
@@ -46,8 +46,8 @@ data class PowerBeats3(
|
|||||||
seenFirstAt = result.seenFirstAt,
|
seenFirstAt = result.seenFirstAt,
|
||||||
seenLastAt = scanResult.receivedAt,
|
seenLastAt = scanResult.receivedAt,
|
||||||
seenCounter = result.seenCounter,
|
seenCounter = result.seenCounter,
|
||||||
confidence = result.confidence,
|
reliability = result.reliability,
|
||||||
rssiAverage = result.averageRssi(basic.rssi),
|
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -17,18 +17,18 @@ data class PowerBeatsPro(
|
|||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: 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
|
||||||
@@ -54,9 +54,9 @@ data class PowerBeatsPro(
|
|||||||
seenFirstAt = result.seenFirstAt,
|
seenFirstAt = result.seenFirstAt,
|
||||||
seenLastAt = scanResult.receivedAt,
|
seenLastAt = scanResult.receivedAt,
|
||||||
seenCounter = result.seenCounter,
|
seenCounter = result.seenCounter,
|
||||||
confidence = result.confidence,
|
reliability = result.reliability,
|
||||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
rssiAverage = result.averageRssi(basic.rssi),
|
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||||
cachedCaseState = result.getLatestCaseLidState(basic)
|
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.common.isBitSet
|
import eu.darken.capod.common.isBitSet
|
||||||
import eu.darken.capod.common.lowerNibble
|
import eu.darken.capod.common.lowerNibble
|
||||||
import eu.darken.capod.common.upperNibble
|
import eu.darken.capod.common.upperNibble
|
||||||
import eu.darken.capod.pods.core.DualPodDevice
|
import eu.darken.capod.pods.core.*
|
||||||
import eu.darken.capod.pods.core.HasCase
|
|
||||||
import eu.darken.capod.pods.core.HasDualMicrophone
|
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
@@ -28,10 +25,10 @@ data class FakeAirPodsGen1 constructor(
|
|||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
) : ApplePods, DualPodDevice, HasDualMicrophone, HasCase {
|
) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN1
|
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN1
|
||||||
|
|
||||||
@@ -79,22 +76,32 @@ data class FakeAirPodsGen1 constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val isThisPodInThecase: Boolean
|
private val isThisPodInThecase: Boolean
|
||||||
get() = rawStatus.isBitSet(6)
|
get() = rawStatus.isBitSet(6)
|
||||||
|
|
||||||
/**
|
override val isLeftPodInEar: Boolean
|
||||||
* The data flip bit is set if the left pod is primary.
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
* For the pod that is in the case, this is flipped again though.
|
true -> rawStatus.isBitSet(3)
|
||||||
*/
|
false -> rawStatus.isBitSet(1)
|
||||||
override val isLeftPodMicrophone: Boolean
|
}
|
||||||
get() = rawStatus.isBitSet(5) xor isThisPodInThecase
|
|
||||||
|
|
||||||
/**
|
override val isRightPodInEar: Boolean
|
||||||
* The data flip bit is UNset if the right pod is primary.
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
* For the pod that is in the case, this is flipped again though.
|
true -> rawStatus.isBitSet(1)
|
||||||
*/
|
false -> rawStatus.isBitSet(3)
|
||||||
override val isRightPodMicrophone: Boolean
|
}
|
||||||
get() = !rawStatus.isBitSet(5) xor isThisPodInThecase
|
|
||||||
|
override val isLeftPodCharging: Boolean
|
||||||
|
get() = when (areValuesFlipped) {
|
||||||
|
false -> rawFlags.isBitSet(0)
|
||||||
|
true -> rawFlags.isBitSet(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isRightPodCharging: Boolean
|
||||||
|
get() = when (areValuesFlipped) {
|
||||||
|
false -> rawFlags.isBitSet(1)
|
||||||
|
true -> rawFlags.isBitSet(0)
|
||||||
|
}
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = when (val value = rawCaseBattery.toInt()) {
|
get() = when (val value = rawCaseBattery.toInt()) {
|
||||||
@@ -131,9 +138,9 @@ data class FakeAirPodsGen1 constructor(
|
|||||||
seenFirstAt = result.seenFirstAt,
|
seenFirstAt = result.seenFirstAt,
|
||||||
seenLastAt = scanResult.receivedAt,
|
seenLastAt = scanResult.receivedAt,
|
||||||
seenCounter = result.seenCounter,
|
seenCounter = result.seenCounter,
|
||||||
confidence = result.confidence,
|
reliability = result.reliability,
|
||||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
rssiAverage = result.averageRssi(basic.rssi),
|
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple.misc
|
||||||
|
|
||||||
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
|
import eu.darken.capod.common.isBitSet
|
||||||
|
import eu.darken.capod.common.lowerNibble
|
||||||
|
import eu.darken.capod.common.upperNibble
|
||||||
|
import eu.darken.capod.pods.core.*
|
||||||
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
|
import java.time.Instant
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar data structure but a lot of placeholder values or hardcoded values
|
||||||
|
*/
|
||||||
|
data class FakeAirPodsGen2 constructor(
|
||||||
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
|
override val seenCounter: Int = 1,
|
||||||
|
override val scanResult: BleScanResult,
|
||||||
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
|
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
||||||
|
|
||||||
|
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN2
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super<ApplePods>.rssi
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normally values for the left pod are in the lower nibbles, if the left pod is primary (microphone)
|
||||||
|
* If the right pod is the primary, the values are flipped.
|
||||||
|
*/
|
||||||
|
val areValuesFlipped: Boolean
|
||||||
|
get() = !rawStatus.isBitSet(5)
|
||||||
|
|
||||||
|
override val batteryLeftPodPercent: Float?
|
||||||
|
get() {
|
||||||
|
val value = when (areValuesFlipped) {
|
||||||
|
true -> rawPodsBattery.upperNibble.toInt()
|
||||||
|
false -> rawPodsBattery.lowerNibble.toInt()
|
||||||
|
}
|
||||||
|
return when (value) {
|
||||||
|
15 -> null
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Left pod: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
(value / 10f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val batteryRightPodPercent: Float?
|
||||||
|
get() {
|
||||||
|
val value = when (areValuesFlipped) {
|
||||||
|
true -> rawPodsBattery.lowerNibble.toInt()
|
||||||
|
false -> rawPodsBattery.upperNibble.toInt()
|
||||||
|
}
|
||||||
|
return when (value) {
|
||||||
|
15 -> null
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Right pod: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
value / 10f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val isThisPodInThecase: Boolean
|
||||||
|
get() = rawStatus.isBitSet(6)
|
||||||
|
|
||||||
|
override val isLeftPodInEar: Boolean
|
||||||
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
|
true -> rawStatus.isBitSet(3)
|
||||||
|
false -> rawStatus.isBitSet(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isRightPodInEar: Boolean
|
||||||
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
|
true -> rawStatus.isBitSet(1)
|
||||||
|
false -> rawStatus.isBitSet(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isLeftPodCharging: Boolean
|
||||||
|
get() = when (areValuesFlipped) {
|
||||||
|
false -> rawFlags.isBitSet(0)
|
||||||
|
true -> rawFlags.isBitSet(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isRightPodCharging: Boolean
|
||||||
|
get() = when (areValuesFlipped) {
|
||||||
|
false -> rawFlags.isBitSet(1)
|
||||||
|
true -> rawFlags.isBitSet(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val batteryCasePercent: Float?
|
||||||
|
get() = when (val value = rawCaseBattery.toInt()) {
|
||||||
|
15 -> cachedBatteryPercentage
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Case: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
value / 10f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isCaseCharging: Boolean
|
||||||
|
get() = rawFlags.isBitSet(2)
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : ApplePodsFactory<FakeAirPodsGen2>(TAG) {
|
||||||
|
|
||||||
|
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||||
|
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
|
||||||
|
getModelInfo().full == DEVICE_CODE && length == 19
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||||
|
var basic = FakeAirPodsGen2(scanResult = scanResult, proximityMessage = message)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
|
updateHistory(basic)
|
||||||
|
|
||||||
|
if (result == null) return basic
|
||||||
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenLastAt = scanResult.receivedAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
reliability = result.reliability,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DEVICE_CODE = 0x0F20.toUShort()
|
||||||
|
private val TAG = logTag("PodDevice", "Apple", "Fake", "AirPods", "Gen2")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,10 +25,10 @@ data class FakeAirPodsGen3 constructor(
|
|||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
) : ApplePods, DualPodDevice, HasDualMicrophone, HasCase, HasChargeDetectionDual {
|
) : ApplePods, DualPodDevice, HasEarDetectionDual, HasChargeDetectionDual, HasCase {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN3
|
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_GEN3
|
||||||
|
|
||||||
@@ -88,22 +88,20 @@ data class FakeAirPodsGen3 constructor(
|
|||||||
true -> rawFlags.isBitSet(0)
|
true -> rawFlags.isBitSet(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
val isThisPodInThecase: Boolean
|
private val isThisPodInThecase: Boolean
|
||||||
get() = rawStatus.isBitSet(6)
|
get() = rawStatus.isBitSet(6)
|
||||||
|
|
||||||
/**
|
override val isLeftPodInEar: Boolean
|
||||||
* The data flip bit is set if the left pod is primary.
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
* For the pod that is in the case, this is flipped again though.
|
true -> rawStatus.isBitSet(3)
|
||||||
*/
|
false -> rawStatus.isBitSet(1)
|
||||||
override val isLeftPodMicrophone: Boolean
|
}
|
||||||
get() = rawStatus.isBitSet(5) xor isThisPodInThecase
|
|
||||||
|
|
||||||
/**
|
override val isRightPodInEar: Boolean
|
||||||
* The data flip bit is UNset if the right pod is primary.
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
* For the pod that is in the case, this is flipped again though.
|
true -> rawStatus.isBitSet(1)
|
||||||
*/
|
false -> rawStatus.isBitSet(3)
|
||||||
override val isRightPodMicrophone: Boolean
|
}
|
||||||
get() = !rawStatus.isBitSet(5) xor isThisPodInThecase
|
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = when (val value = rawCaseBattery.toInt()) {
|
get() = when (val value = rawCaseBattery.toInt()) {
|
||||||
@@ -140,9 +138,9 @@ data class FakeAirPodsGen3 constructor(
|
|||||||
seenFirstAt = result.seenFirstAt,
|
seenFirstAt = result.seenFirstAt,
|
||||||
seenLastAt = scanResult.receivedAt,
|
seenLastAt = scanResult.receivedAt,
|
||||||
seenCounter = result.seenCounter,
|
seenCounter = result.seenCounter,
|
||||||
confidence = result.confidence,
|
reliability = result.reliability,
|
||||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
rssiAverage = result.averageRssi(basic.rssi),
|
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
package eu.darken.capod.pods.core.apple.misc
|
package eu.darken.capod.pods.core.apple.misc
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import eu.darken.capod.common.R
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.debug.logging.log
|
import eu.darken.capod.common.debug.logging.log
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.common.isBitSet
|
import eu.darken.capod.common.isBitSet
|
||||||
import eu.darken.capod.common.lowerNibble
|
import eu.darken.capod.common.lowerNibble
|
||||||
import eu.darken.capod.common.upperNibble
|
import eu.darken.capod.common.upperNibble
|
||||||
import eu.darken.capod.pods.core.DualPodDevice
|
import eu.darken.capod.pods.core.*
|
||||||
import eu.darken.capod.pods.core.HasCase
|
|
||||||
import eu.darken.capod.pods.core.HasDualMicrophone
|
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
@@ -28,13 +27,29 @@ data class FakeAirPodsPro constructor(
|
|||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
private val cachedBatteryPercentage: Float? = null,
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
) : ApplePods, DualPodDevice, HasDualMicrophone, HasCase {
|
) : ApplePods, DualPodDevice, HasChargeDetectionDual, HasEarDetectionDual, HasCase {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_PRO
|
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_PRO
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val iconRes: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_both
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val caseIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_case
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val leftPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_left
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val rightPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_right
|
||||||
|
|
||||||
override val rssi: Int
|
override val rssi: Int
|
||||||
get() = rssiAverage ?: super<ApplePods>.rssi
|
get() = rssiAverage ?: super<ApplePods>.rssi
|
||||||
|
|
||||||
@@ -79,22 +94,32 @@ data class FakeAirPodsPro constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val isThisPodInThecase: Boolean
|
override val isLeftPodCharging: Boolean
|
||||||
|
get() = when (areValuesFlipped) {
|
||||||
|
false -> rawFlags.isBitSet(0)
|
||||||
|
true -> rawFlags.isBitSet(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isRightPodCharging: Boolean
|
||||||
|
get() = when (areValuesFlipped) {
|
||||||
|
false -> rawFlags.isBitSet(1)
|
||||||
|
true -> rawFlags.isBitSet(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val isThisPodInThecase: Boolean
|
||||||
get() = rawStatus.isBitSet(6)
|
get() = rawStatus.isBitSet(6)
|
||||||
|
|
||||||
/**
|
override val isLeftPodInEar: Boolean
|
||||||
* The data flip bit is set if the left pod is primary.
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
* For the pod that is in the case, this is flipped again though.
|
true -> rawStatus.isBitSet(3)
|
||||||
*/
|
false -> rawStatus.isBitSet(1)
|
||||||
override val isLeftPodMicrophone: Boolean
|
}
|
||||||
get() = rawStatus.isBitSet(5) xor isThisPodInThecase
|
|
||||||
|
|
||||||
/**
|
override val isRightPodInEar: Boolean
|
||||||
* The data flip bit is UNset if the right pod is primary.
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
* For the pod that is in the case, this is flipped again though.
|
true -> rawStatus.isBitSet(1)
|
||||||
*/
|
false -> rawStatus.isBitSet(3)
|
||||||
override val isRightPodMicrophone: Boolean
|
}
|
||||||
get() = !rawStatus.isBitSet(5) xor isThisPodInThecase
|
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = when (val value = rawCaseBattery.toInt()) {
|
get() = when (val value = rawCaseBattery.toInt()) {
|
||||||
@@ -131,9 +156,9 @@ data class FakeAirPodsPro constructor(
|
|||||||
seenFirstAt = result.seenFirstAt,
|
seenFirstAt = result.seenFirstAt,
|
||||||
seenLastAt = scanResult.receivedAt,
|
seenLastAt = scanResult.receivedAt,
|
||||||
seenCounter = result.seenCounter,
|
seenCounter = result.seenCounter,
|
||||||
confidence = result.confidence,
|
reliability = result.reliability,
|
||||||
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
rssiAverage = result.averageRssi(basic.rssi),
|
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple.misc
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import eu.darken.capod.common.R
|
||||||
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
|
import eu.darken.capod.common.isBitSet
|
||||||
|
import eu.darken.capod.common.lowerNibble
|
||||||
|
import eu.darken.capod.common.upperNibble
|
||||||
|
import eu.darken.capod.pods.core.*
|
||||||
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.ApplePodsFactory
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
|
import java.time.Instant
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AirPods Pro2 clone
|
||||||
|
* https://discord.com/channels/548521543039189022/927235844127993866/1060911213539774504
|
||||||
|
*/
|
||||||
|
data class FakeAirPodsPro2 constructor(
|
||||||
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
|
override val seenLastAt: Instant = Instant.now(),
|
||||||
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
|
override val seenCounter: Int = 1,
|
||||||
|
override val scanResult: BleScanResult,
|
||||||
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
|
override val reliability: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
private val cachedCaseState: DualApplePods.LidState? = null,
|
||||||
|
) : ApplePods, HasChargeDetectionDual, DualPodDevice, HasEarDetectionDual, HasCase {
|
||||||
|
|
||||||
|
override val model: PodDevice.Model = PodDevice.Model.FAKE_AIRPODS_PRO2
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val iconRes: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_both
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val caseIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_case
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val leftPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_left
|
||||||
|
|
||||||
|
@get:DrawableRes
|
||||||
|
override val rightPodIcon: Int
|
||||||
|
get() = R.drawable.devic_airpods_pro2_right
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: scanResult.rssi
|
||||||
|
|
||||||
|
private val areValuesFlipped: Boolean
|
||||||
|
get() = !rawStatus.isBitSet(5)
|
||||||
|
|
||||||
|
override val batteryLeftPodPercent: Float?
|
||||||
|
get() {
|
||||||
|
val value = when (areValuesFlipped) {
|
||||||
|
true -> rawPodsBattery.upperNibble.toInt()
|
||||||
|
false -> rawPodsBattery.lowerNibble.toInt()
|
||||||
|
}
|
||||||
|
return when (value) {
|
||||||
|
15 -> null
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Left pod: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
(value / 10f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val batteryRightPodPercent: Float?
|
||||||
|
get() {
|
||||||
|
val value = when (areValuesFlipped) {
|
||||||
|
true -> rawPodsBattery.lowerNibble.toInt()
|
||||||
|
false -> rawPodsBattery.upperNibble.toInt()
|
||||||
|
}
|
||||||
|
return when (value) {
|
||||||
|
15 -> null
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Right pod: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
value / 10f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val isThisPodInThecase: Boolean
|
||||||
|
get() = rawStatus.isBitSet(6)
|
||||||
|
|
||||||
|
override val isLeftPodInEar: Boolean
|
||||||
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
|
true -> rawStatus.isBitSet(3)
|
||||||
|
false -> rawStatus.isBitSet(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isRightPodInEar: Boolean
|
||||||
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
|
true -> rawStatus.isBitSet(1)
|
||||||
|
false -> rawStatus.isBitSet(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isLeftPodCharging: Boolean
|
||||||
|
get() = when (areValuesFlipped) {
|
||||||
|
false -> rawFlags.isBitSet(0)
|
||||||
|
true -> rawFlags.isBitSet(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isRightPodCharging: Boolean
|
||||||
|
get() = when (areValuesFlipped) {
|
||||||
|
false -> rawFlags.isBitSet(1)
|
||||||
|
true -> rawFlags.isBitSet(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val batteryCasePercent: Float?
|
||||||
|
get() = when (val value = rawCaseBattery.toInt()) {
|
||||||
|
15 -> cachedBatteryPercentage
|
||||||
|
else -> if (value > 10) {
|
||||||
|
log { "Case: Above 100% battery: $value" }
|
||||||
|
1.0f
|
||||||
|
} else {
|
||||||
|
value / 10f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val isCaseCharging: Boolean
|
||||||
|
get() = rawFlags.isBitSet(2)
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : ApplePodsFactory<FakeAirPodsPro2>(TAG) {
|
||||||
|
|
||||||
|
override fun isResponsible(message: ProximityPairing.Message): Boolean = message.run {
|
||||||
|
// Official message length is 19HEX, i.e. binary 25, did they copy this wrong?
|
||||||
|
getModelInfo().full == DEVICE_CODE && length == 19
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun create(scanResult: BleScanResult, message: ProximityPairing.Message): ApplePods {
|
||||||
|
var basic = FakeAirPodsPro2(scanResult = scanResult, proximityMessage = message)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
|
updateHistory(basic)
|
||||||
|
|
||||||
|
if (result == null) return basic
|
||||||
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
seenFirstAt = result.seenFirstAt,
|
||||||
|
seenLastAt = scanResult.receivedAt,
|
||||||
|
seenCounter = result.seenCounter,
|
||||||
|
reliability = result.reliability,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val DEVICE_CODE = 0x1420.toUShort()
|
||||||
|
private val TAG = logTag("PodDevice", "Apple", "Fake", "AirPods", "Pro2")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ data class UnknownAppleDevice(
|
|||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
override val confidence: Float = 0f,
|
override val reliability: Float = 0f,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
) : ApplePods {
|
) : ApplePods {
|
||||||
|
|
||||||
@@ -49,8 +49,8 @@ data class UnknownAppleDevice(
|
|||||||
seenFirstAt = result.seenFirstAt,
|
seenFirstAt = result.seenFirstAt,
|
||||||
seenLastAt = scanResult.receivedAt,
|
seenLastAt = scanResult.receivedAt,
|
||||||
seenCounter = result.seenCounter,
|
seenCounter = result.seenCounter,
|
||||||
confidence = result.confidence,
|
reliability = result.reliability,
|
||||||
rssiAverage = result.averageRssi(basic.rssi),
|
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ data class UnknownDevice(
|
|||||||
override val seenFirstAt: Instant = Instant.now(),
|
override val seenFirstAt: Instant = Instant.now(),
|
||||||
override val seenCounter: Int = 1,
|
override val seenCounter: Int = 1,
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val confidence: Float = 0f,
|
override val reliability: Float = 0f,
|
||||||
private val rssiAverage: Int? = null,
|
private val rssiAverage: Int? = null,
|
||||||
) : PodDevice {
|
) : PodDevice {
|
||||||
|
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ class UnknownDeviceFactory @Inject constructor() {
|
|||||||
seenFirstAt = result.seenFirstAt,
|
seenFirstAt = result.seenFirstAt,
|
||||||
seenLastAt = scanResult.receivedAt,
|
seenLastAt = scanResult.receivedAt,
|
||||||
seenCounter = result.seenCounter,
|
seenCounter = result.seenCounter,
|
||||||
confidence = result.confidence,
|
reliability = result.confidence,
|
||||||
rssiAverage = result.averageRssi(basic.rssi),
|
rssiAverage = result.rssiSmoothed(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ class UnknownDeviceFactory @Inject constructor() {
|
|||||||
val confidence: Float
|
val confidence: Float
|
||||||
get() = 0.75f + (history.size / (MAX_HISTORY * 4f))
|
get() = 0.75f + (history.size / (MAX_HISTORY * 4f))
|
||||||
|
|
||||||
fun averageRssi(latest: Int): Int =
|
fun rssiSmoothed(latest: Int): Int =
|
||||||
history.map { it.rssi }.plus(latest).takeLast(10).median()
|
history.map { it.rssi }.plus(latest).takeLast(10).median()
|
||||||
|
|
||||||
fun isOlderThan(age: Duration): Boolean {
|
fun isOlderThan(age: Duration): Boolean {
|
||||||
@@ -70,7 +70,7 @@ class UnknownDeviceFactory @Inject constructor() {
|
|||||||
override fun toString(): String = "KnownDevice(history=${history.size}, last=${history.last()})"
|
override fun toString(): String = "KnownDevice(history=${history.size}, last=${history.last()})"
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val MAX_HISTORY = 10
|
const val MAX_HISTORY = 20
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import eu.darken.capod.main.core.GeneralSettings
|
|||||||
import eu.darken.capod.monitor.core.PodMonitor
|
import eu.darken.capod.monitor.core.PodMonitor
|
||||||
import eu.darken.capod.pods.core.HasEarDetection
|
import eu.darken.capod.pods.core.HasEarDetection
|
||||||
import eu.darken.capod.pods.core.HasEarDetectionDual
|
import eu.darken.capod.pods.core.HasEarDetectionDual
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.reaction.core.ReactionSettings
|
import eu.darken.capod.reaction.core.ReactionSettings
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -69,7 +69,7 @@ class AutoConnect @Inject constructor(
|
|||||||
val conditionFulfilled = when (condition) {
|
val conditionFulfilled = when (condition) {
|
||||||
AutoConnectCondition.WHEN_SEEN -> true
|
AutoConnectCondition.WHEN_SEEN -> true
|
||||||
AutoConnectCondition.CASE_OPEN -> when (mainDevice) {
|
AutoConnectCondition.CASE_OPEN -> when (mainDevice) {
|
||||||
is DualAirPods -> mainDevice.caseLidState == DualAirPods.LidState.OPEN
|
is DualApplePods -> mainDevice.caseLidState == DualApplePods.LidState.OPEN
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
AutoConnectCondition.IN_EAR -> when (mainDevice) {
|
AutoConnectCondition.IN_EAR -> when (mainDevice) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package eu.darken.capod.reaction.core.playpause
|
|||||||
import eu.darken.capod.common.MediaControl
|
import eu.darken.capod.common.MediaControl
|
||||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||||
|
import eu.darken.capod.common.debug.logging.Logging.Priority.WARN
|
||||||
import eu.darken.capod.common.debug.logging.log
|
import eu.darken.capod.common.debug.logging.log
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||||
@@ -40,46 +41,51 @@ class PlayPause @Inject constructor(
|
|||||||
}
|
}
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.withPrevious()
|
.withPrevious()
|
||||||
|
.filter { (previous, current) ->
|
||||||
|
if (previous == null || current == null) return@filter false
|
||||||
|
log(TAG, VERBOSE) { "previous-id=${previous.identifier}, current-id=${current.identifier}" }
|
||||||
|
val match = previous.identifier == current.identifier
|
||||||
|
if (!match) log(TAG, WARN) { "Main device switched, skipping reaction." }
|
||||||
|
match
|
||||||
|
}
|
||||||
.onEach { (previous, current) ->
|
.onEach { (previous, current) ->
|
||||||
if (previous is HasEarDetection && current is HasEarDetection) {
|
log(TAG, VERBOSE) { "Checking\nprevious=$previous\ncurrent=$current" }
|
||||||
log(TAG, VERBOSE) { "previous=${previous.isBeingWorn}, current=${current.isBeingWorn}" }
|
|
||||||
log(TAG, VERBOSE) { "previous-id=${previous.identifier}, current-id=${current.identifier}" }
|
|
||||||
if (previous.identifier != current.identifier) {
|
|
||||||
return@onEach
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previous is HasEarDetectionDual && current is HasEarDetectionDual && reactionSettings.onePodMode.value) {
|
val previousWorn: Boolean?
|
||||||
log(TAG) { "Ear status changed for dual pod device in single pod mode." }
|
val currentWorn: Boolean?
|
||||||
val previousWorn = previous.isEitherPodInEar
|
|
||||||
val currentWorn = current.isEitherPodInEar
|
when {
|
||||||
if (!previousWorn && currentWorn && !mediaControl.isPlaying) {
|
reactionSettings.onePodMode.value && previous is HasEarDetectionDual && current is HasEarDetectionDual -> {
|
||||||
if (reactionSettings.autoPlay.value) {
|
previousWorn = previous.isEitherPodInEar
|
||||||
mediaControl.sendPlay()
|
currentWorn = current.isEitherPodInEar
|
||||||
} else {
|
log(TAG, VERBOSE) { "previous: left=${previous.isLeftPodInEar}, right=${previous.isRightPodInEar}" }
|
||||||
log(TAG) { "autoPlay is disabled" }
|
log(TAG, VERBOSE) { "current: left${current.isLeftPodInEar}, right=${current.isRightPodInEar}" }
|
||||||
}
|
}
|
||||||
} else if (!currentWorn && previousWorn && mediaControl.isPlaying) {
|
previous is HasEarDetection && current is HasEarDetection -> {
|
||||||
if (reactionSettings.autoPause.value) {
|
previousWorn = previous.isBeingWorn
|
||||||
mediaControl.sendPause()
|
currentWorn = current.isBeingWorn
|
||||||
} else {
|
log(TAG, VERBOSE) { "prev.isBeingWorn=${previousWorn}, cur.isBeingWorn=${currentWorn}" }
|
||||||
log(TAG) { "autoPause is disabled" }
|
}
|
||||||
}
|
else -> {
|
||||||
}
|
log(TAG, VERBOSE) { "Current devices don't support ear detection." }
|
||||||
} else if (previous.isBeingWorn != current.isBeingWorn) {
|
previousWorn = null
|
||||||
log(TAG) { "Ear status changed for monitored device." }
|
currentWorn = null
|
||||||
if (current.isBeingWorn && !mediaControl.isPlaying) {
|
}
|
||||||
if (reactionSettings.autoPlay.value) {
|
}
|
||||||
mediaControl.sendPlay()
|
|
||||||
} else {
|
if (previousWorn == false && currentWorn == true && !mediaControl.isPlaying) {
|
||||||
log(TAG) { "autoPlay is disabled" }
|
if (reactionSettings.autoPlay.value) {
|
||||||
}
|
log(TAG) { "autoPlay is triggered, sendPlay()" }
|
||||||
} else if (!current.isBeingWorn && mediaControl.isPlaying) {
|
mediaControl.sendPlay()
|
||||||
if (reactionSettings.autoPause.value) {
|
} else {
|
||||||
mediaControl.sendPause()
|
log(TAG, VERBOSE) { "autoPlay is disabled" }
|
||||||
} else {
|
}
|
||||||
log(TAG) { "autoPause is disabled" }
|
} else if (previousWorn == true && currentWorn == false && mediaControl.isPlaying) {
|
||||||
}
|
if (reactionSettings.autoPause.value) {
|
||||||
}
|
log(TAG) { "autoPause is triggered, sendPause()" }
|
||||||
|
mediaControl.sendPause()
|
||||||
|
} else {
|
||||||
|
log(TAG) { "autoPause is disabled" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import eu.darken.capod.common.flow.setupCommonEventHandlers
|
|||||||
import eu.darken.capod.common.flow.withPrevious
|
import eu.darken.capod.common.flow.withPrevious
|
||||||
import eu.darken.capod.monitor.core.PodMonitor
|
import eu.darken.capod.monitor.core.PodMonitor
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.DualAirPods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.reaction.core.ReactionSettings
|
import eu.darken.capod.reaction.core.ReactionSettings
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
@@ -34,7 +34,7 @@ class PopUpReaction @Inject constructor(
|
|||||||
.withPrevious()
|
.withPrevious()
|
||||||
.setupCommonEventHandlers(TAG) { "monitor" }
|
.setupCommonEventHandlers(TAG) { "monitor" }
|
||||||
.mapNotNull { (previous, current) ->
|
.mapNotNull { (previous, current) ->
|
||||||
if (previous !is DualAirPods? || current !is DualAirPods) {
|
if (previous !is DualApplePods? || current !is DualApplePods) {
|
||||||
return@mapNotNull null
|
return@mapNotNull null
|
||||||
}
|
}
|
||||||
log(TAG, VERBOSE) {
|
log(TAG, VERBOSE) {
|
||||||
@@ -57,8 +57,8 @@ class PopUpReaction @Inject constructor(
|
|||||||
tryPopWindow(current)
|
tryPopWindow(current)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun tryPopWindow(current: DualAirPods): Event? = when {
|
private suspend fun tryPopWindow(current: DualApplePods): Event? = when {
|
||||||
current.caseLidState == DualAirPods.LidState.OPEN -> {
|
current.caseLidState == DualApplePods.LidState.OPEN -> {
|
||||||
log(TAG, INFO) { "Show popup" }
|
log(TAG, INFO) { "Show popup" }
|
||||||
|
|
||||||
val now = Instant.now()
|
val now = Instant.now()
|
||||||
@@ -74,9 +74,9 @@ class PopUpReaction @Inject constructor(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
current.caseLidState != DualAirPods.LidState.OPEN -> {
|
current.caseLidState != DualApplePods.LidState.OPEN -> {
|
||||||
when (current.caseLidState) {
|
when (current.caseLidState) {
|
||||||
DualAirPods.LidState.CLOSED -> {
|
DualApplePods.LidState.CLOSED -> {
|
||||||
log(TAG, INFO) { "Lid was actively closed, resetting cooldown." }
|
log(TAG, INFO) { "Lid was actively closed, resetting cooldown." }
|
||||||
coolDowns.remove(current.identifier)
|
coolDowns.remove(current.identifier)
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 7.2 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 5.9 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M6,4H18V5H21V7H18V9H21V11H18V13H21V15H18V17H21V19H18V20H6V19H3V17H6V15H3V13H6V11H3V9H6V7H3V5H6V4M11,15V18H12V15H11M13,15V18H14V15H13M15,15V18H16V15H15Z" />
|
||||||
|
</vector>
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:width="24dp"
|
|
||||||
android:height="24dp"
|
|
||||||
android:tint="?attr/colorControlNormal"
|
|
||||||
android:viewportWidth="24"
|
|
||||||
android:viewportHeight="24">
|
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="m6,11c-1.9954,0 -3,-2.0093 -3,-4s1.0046,-4 3,-4c3.1837,0 5,3.4637 5,6.4886v10.0114c0,0.8284 -0.6716,1.5 -1.5,1.5 -0.8284,0 -1.5,-0.6716 -1.5,-1.5v-8.0113c0,-0.381 -0.1426,-0.7378 -0.3906,-1.0098 -0.4267,0.3435 -0.9676,0.5211 -1.6094,0.5211zM9,11.4887v8.0113c0,0.2761 0.2239,0.5 0.5,0.5s0.5,-0.2239 0.5,-0.5v-10.0114c0,-2.5454 -1.5434,-5.4886 -4,-5.4886 -1.2546,0 -2,1.4907 -2,3s0.7454,3 2,3c0.7809,0 1.2592,-0.3588 1.5257,-1.1581 0.2094,-0.6282 1.1516,-0.3212 0.9508,0.3097l-0.1878,0.5902c0.4499,0.4603 0.7114,1.0823 0.7114,1.7468zM6,8c-0.5523,0 -1,-0.4477 -1,-1s0.4477,-1 1,-1 1,0.4477 1,1 -0.4477,1 -1,1zM16,11.4887v8.0113c0,0.8284 -0.6716,1.5 -1.5,1.5s-1.5,-0.6716 -1.5,-1.5v-10.0114c0,-3.0249 1.8163,-6.4886 5,-6.4886 1.9954,0 3,2.0093 3,4s-1.0046,4 -3,4c-0.6418,0 -1.1826,-0.1777 -1.6094,-0.5211 -0.248,0.2721 -0.3906,0.6288 -0.3906,1.0098zM15.5235,9.1516c-0.2008,-0.631 0.7414,-0.9379 0.9508,-0.3097 0.2665,0.7994 0.7448,1.1581 1.5257,1.1581 1.2546,0 2,-1.4907 2,-3s-0.7454,-3 -2,-3c-2.4566,0 -4,2.9433 -4,5.4886v10.0114c0,0.2761 0.2239,0.5 0.5,0.5s0.5,-0.2239 0.5,-0.5v-8.0113c0,-0.6645 0.2615,-1.2865 0.7114,-1.7468zM18,8c-0.5523,0 -1,-0.4477 -1,-1s0.4477,-1 1,-1 1,0.4477 1,1 -0.4477,1 -1,1z" />
|
|
||||||
</vector>
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="?attr/colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M22.77 19.32L21.7 18.5C21.72 18.33 21.74 18.17 21.74 18S21.73 17.67 21.7 17.5L22.76 16.68C22.85 16.6 22.88 16.47 22.82 16.36L21.82 14.63C21.76 14.5 21.63 14.5 21.5 14.5L20.27 15C20 14.82 19.73 14.65 19.42 14.53L19.23 13.21C19.22 13.09 19.11 13 19 13H17C16.87 13 16.76 13.09 16.74 13.21L16.55 14.53C16.25 14.66 15.96 14.82 15.7 15L14.46 14.5C14.35 14.5 14.22 14.5 14.15 14.63L13.15 16.36C13.09 16.47 13.11 16.6 13.21 16.68L14.27 17.5C14.25 17.67 14.24 17.83 14.24 18S14.25 18.33 14.27 18.5L13.21 19.32C13.12 19.4 13.09 19.53 13.15 19.64L14.15 21.37C14.21 21.5 14.34 21.5 14.46 21.5L15.7 21C15.96 21.18 16.24 21.35 16.55 21.47L16.74 22.79C16.76 22.91 16.86 23 17 23H19C19.11 23 19.22 22.91 19.24 22.79L19.43 21.47C19.73 21.34 20 21.18 20.27 21L21.5 21.5C21.63 21.5 21.76 21.5 21.83 21.37L22.83 19.64C22.89 19.53 22.86 19.4 22.77 19.32M18 19.5C17.16 19.5 16.5 18.83 16.5 18S17.17 16.5 18 16.5 19.5 17.17 19.5 18 18.83 19.5 18 19.5M17.62 3.22C17.43 3.08 17.22 3 17 3H3C2.78 3 2.57 3.08 2.38 3.22C1.95 3.56 1.87 4.19 2.21 4.62L7 10.75V15.87C6.96 16.16 7.06 16.47 7.29 16.7L11.3 20.71C11.4 20.81 11.5 20.88 11.65 20.93C11.22 20 11 19 11 18C11 16.17 11.72 14.41 13 13.1V10.75L17.79 4.62C18.13 4.19 18.05 3.56 17.62 3.22M11 10.05V17.58L9 15.58V10.06L5.04 5H14.96L11 10.05Z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="?attr/colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M5 5V19H7V21H3V3H7V5H5M20 7H7V9H20V7M20 11H7V13H20V11M20 15H7V17H20V15Z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:tint="?attr/colorControlNormal"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M6.91 5.5L9.21 7.79L7.79 9.21L5.5 6.91L3.21 9.21L1.79 7.79L4.09 5.5L1.79 3.21L3.21 1.79L5.5 4.09L7.79 1.79L9.21 3.21M22.21 16.21L20.79 14.79L18.5 17.09L16.21 14.79L14.79 16.21L17.09 18.5L14.79 20.79L16.21 22.21L18.5 19.91L20.79 22.21L22.21 20.79L19.91 18.5M20.4 6.83L17.18 11L15.6 9.73L16.77 8.23A9.08 9.08 0 0 0 10.11 13.85A4.5 4.5 0 1 1 7.5 13A4 4 0 0 1 8.28 13.08A11.27 11.27 0 0 1 16.43 6.26L15 5.18L16.27 3.6M10 17.5A2.5 2.5 0 1 0 7.5 20A2.5 2.5 0 0 0 10 17.5Z" />
|
||||||
|
</vector>
|
||||||
@@ -1,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>
|
||||||
|
|||||||
@@ -58,4 +58,6 @@
|
|||||||
<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>
|
||||||
<string name="first_seen_x">Упершыню быў: %s</string>
|
<string name="first_seen_x">Упершыню быў: %s</string>
|
||||||
|
<string name="permission_post_notifications_label">Адлюстраванне апавяшчэнняў</string>
|
||||||
|
<string name="permission_post_notifications_description">"Дазволіць CAPod адлюстроўваць вам апавяшчэнні аб вашых AirPods: напрыклад, іх бягучы стан пры падключэнні."</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -58,4 +58,6 @@
|
|||||||
<string name="pods_case_unknown_state">Estat desconegut</string>
|
<string name="pods_case_unknown_state">Estat desconegut</string>
|
||||||
<string name="last_seen_x">Últim cop vist: %s</string>
|
<string name="last_seen_x">Últim cop vist: %s</string>
|
||||||
<string name="first_seen_x">Primera vegada vist: %s</string>
|
<string name="first_seen_x">Primera vegada vist: %s</string>
|
||||||
|
<string name="permission_post_notifications_label">Publica notificacions</string>
|
||||||
|
<string name="permission_post_notifications_description">"Permet que el CAPod us mostri notificacions sobre els vostres AirPods, p. ex. el seu estat actual mentre estan connectats."</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -58,4 +58,6 @@
|
|||||||
<string name="pods_case_unknown_state">Neznámý stav</string>
|
<string name="pods_case_unknown_state">Neznámý stav</string>
|
||||||
<string name="last_seen_x">Naposledy spatřeno: %s</string>
|
<string name="last_seen_x">Naposledy spatřeno: %s</string>
|
||||||
<string name="first_seen_x">Poprvé spatřeno: %s</string>
|
<string name="first_seen_x">Poprvé spatřeno: %s</string>
|
||||||
|
<string name="permission_post_notifications_label">Posílat zprávy</string>
|
||||||
|
<string name="permission_post_notifications_description">"Povolte aplikaci CAPod zobrazovat oznámení o sluchátkách AirPods, např. o jejich aktuálním stavu během připojení."</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||