From 022e0c7405b636c45c87a1ea92d4244c244a2d3a Mon Sep 17 00:00:00 2001 From: darken Date: Fri, 31 Dec 2021 17:35:08 +0100 Subject: [PATCH] Pod status cards, mvp --- README.md | 13 +- app/build.gradle | 12 +- app/src/main/AndroidManifest.xml | 42 +++- app/src/main/java/eu/darken/capod/App.kt | 6 +- .../java/eu/darken/capod/common/BuildWrap.kt | 2 + .../capod/common/bluetooth/BleScanner.kt | 2 +- .../common/debug/{bugreporting => }/Bugs.kt | 4 +- .../AutoReporting.kt} | 19 +- .../DebugSettings.kt} | 6 +- .../bugsnag/BugsnagErrorHandler.kt | 7 +- .../{ => autoreport}/bugsnag/BugsnagLogger.kt | 2 +- .../bugsnag/NOPBugsnagErrorHandler.kt | 2 +- .../capod/common/permissions/Permission.kt | 21 +- .../eu/darken/capod/main/ui/MainFragmentVM.kt | 78 ------- .../capod/main/ui/cards/ToggleCardVH.kt | 35 ---- .../OverviewAdapter.kt} | 16 +- .../OverviewFragment.kt} | 10 +- .../main/ui/overview/OverviewFragmentVM.kt | 107 ++++++++++ .../ui/overview/cards/DualApplePodsCardVH.kt | 90 ++++++++ .../{ => overview}/cards/PermissionCardVH.kt | 8 +- .../overview/cards/UnknownPodDeviceCardVH.kt | 32 +++ .../capod/monitor/core/MonitorSettings.kt | 16 ++ .../core/receiver/BluetoothEventReceiver.kt | 2 +- .../monitor/core/worker/MonitorControl.kt | 2 +- .../monitor/core/worker/MonitorWorker.kt | 123 ++++++----- .../eu/darken/capod/pods/core/DualPods.kt | 27 +++ .../capod/pods/core/DualPodsExtensions.kt | 17 ++ .../eu/darken/capod/pods/core/PodDevice.kt | 6 +- .../capod/pods/core/airpods/AirPodsFactory.kt | 4 +- .../capod/pods/core/airpods/ApplePods.kt | 3 +- .../capod/pods/core/airpods/DualApplePods.kt | 197 ++++++++++++++++++ .../{AirPodsDevice.kt => SingleApplePods.kt} | 29 ++- .../pods/core/airpods/models/AirPodsGen1.kt | 12 +- .../pods/core/airpods/models/AirPodsGen2.kt | 12 +- .../pods/core/airpods/models/AirPodsMax.kt | 11 +- .../pods/core/airpods/models/AirPodsPro.kt | 11 +- .../core/airpods/models/UnknownAppleDevice.kt | 11 +- .../{ => protocol}/ContinuityProtocol.kt | 2 +- .../{ => protocol}/ProximityPairing.kt | 3 +- .../main/res/drawable/ic_baseline_apps_24.xml | 10 + .../res/drawable/ic_baseline_earbuds_24.xml | 10 + .../res/drawable/ic_baseline_pod_case_24.xml | 16 ++ .../res/drawable/ic_baseline_pod_left_24.xml | 16 ++ .../res/drawable/ic_baseline_pod_right_24.xml | 16 ++ .../ic_baseline_signal_cellular_alt_24.xml | 10 + app/src/main/res/layout/main_activity.xml | 2 +- .../layout/overview_pods_apple_dual_item.xml | 132 ++++++++++++ .../res/layout/overview_pods_unknown_item.xml | 78 +++++++ app/src/main/res/navigation/nav_graph.xml | 2 +- app/src/main/res/values/strings.xml | 33 +++ .../pods/core/airpods/AirPodsFactoryTest.kt | 87 ++++---- .../pods/core/airpods/BaseAirPodsTest.kt | 2 + .../core/airpods/models/AirPodsGen1Test.kt | 8 +- .../core/airpods/models/AirPodsGen2Test.kt | 8 +- .../core/airpods/models/AirPodsProTest.kt | 33 +-- 55 files changed, 1132 insertions(+), 333 deletions(-) rename app/src/main/java/eu/darken/capod/common/debug/{bugreporting => }/Bugs.kt (86%) rename app/src/main/java/eu/darken/capod/common/debug/{bugreporting/BugReporter.kt => autoreport/AutoReporting.kt} (74%) rename app/src/main/java/eu/darken/capod/common/debug/{bugreporting/BugReportSettings.kt => autoreport/DebugSettings.kt} (68%) rename app/src/main/java/eu/darken/capod/common/debug/{ => autoreport}/bugsnag/BugsnagErrorHandler.kt (85%) rename app/src/main/java/eu/darken/capod/common/debug/{ => autoreport}/bugsnag/BugsnagLogger.kt (96%) rename app/src/main/java/eu/darken/capod/common/debug/{ => autoreport}/bugsnag/NOPBugsnagErrorHandler.kt (90%) delete mode 100644 app/src/main/java/eu/darken/capod/main/ui/MainFragmentVM.kt delete mode 100644 app/src/main/java/eu/darken/capod/main/ui/cards/ToggleCardVH.kt rename app/src/main/java/eu/darken/capod/main/ui/{MainAdapter.kt => overview/OverviewAdapter.kt} (63%) rename app/src/main/java/eu/darken/capod/main/ui/{MainFragment.kt => overview/OverviewFragment.kt} (91%) create mode 100644 app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt create mode 100644 app/src/main/java/eu/darken/capod/main/ui/overview/cards/DualApplePodsCardVH.kt rename app/src/main/java/eu/darken/capod/main/ui/{ => overview}/cards/PermissionCardVH.kt (80%) create mode 100644 app/src/main/java/eu/darken/capod/main/ui/overview/cards/UnknownPodDeviceCardVH.kt create mode 100644 app/src/main/java/eu/darken/capod/monitor/core/MonitorSettings.kt create mode 100644 app/src/main/java/eu/darken/capod/pods/core/DualPods.kt create mode 100644 app/src/main/java/eu/darken/capod/pods/core/DualPodsExtensions.kt create mode 100644 app/src/main/java/eu/darken/capod/pods/core/airpods/DualApplePods.kt rename app/src/main/java/eu/darken/capod/pods/core/airpods/{AirPodsDevice.kt => SingleApplePods.kt} (89%) rename app/src/main/java/eu/darken/capod/pods/core/airpods/{ => protocol}/ContinuityProtocol.kt (97%) rename app/src/main/java/eu/darken/capod/pods/core/airpods/{ => protocol}/ProximityPairing.kt (94%) create mode 100644 app/src/main/res/drawable/ic_baseline_apps_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_earbuds_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_pod_case_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_pod_left_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_pod_right_24.xml create mode 100644 app/src/main/res/drawable/ic_baseline_signal_cellular_alt_24.xml create mode 100644 app/src/main/res/layout/overview_pods_apple_dual_item.xml create mode 100644 app/src/main/res/layout/overview_pods_unknown_item.xml diff --git a/README.md b/README.md index ae8e1b26..8174352f 100644 --- a/README.md +++ b/README.md @@ -19,15 +19,6 @@ Supported models: * [Google Play](https://play.google.com/store/apps/details?id=eu.darken.capod) * [GitHub](https://github.com/d4rken/android-airpods-companion/releases/latest) -## Use CAPod as a library - -AirPods use Apple's Continuity Protocol to communicate, specifically the Proximity Pairing Message type. If you want to -build your own app based on this, you can include it like this: - -```groovy -// TODO -``` - ## Get help * [Github Issues](https://github.com/d4rken/android-airpods-companion/issues) @@ -42,5 +33,5 @@ CAPod's code is available under a GPL v3 license, this excludes: * CAPod icons, logos, mascots and marketing materials. * CAPod animations and videos. * CAPod documentation. -* Google Play store screenshots. -* Google Play store texts & descriptions. \ No newline at end of file +* Google Play screenshots. +* Google Play texts & descriptions. \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 9014adf4..fa550a62 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,10 +12,14 @@ def gitSha = 'git rev-parse --short HEAD'.execute([], project.rootDir).text.trim def buildTime = new Date().format("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone("GMT+1")) android { - def packageName = "eu.darken.cap" + def packageName = "eu.darken.capod" compileSdkVersion buildConfig.compileSdk + Properties bugsnagProps = new Properties() + def bugsnagPropsFile = new File(System.properties['user.home'], ".appconfig/${packageName}/bugsnag.properties") + if (bugsnagPropsFile.canRead()) bugsnagProps.load(new FileInputStream(bugsnagPropsFile)) + defaultConfig { applicationId "${packageName}" @@ -29,6 +33,8 @@ android { buildConfigField "String", "GITSHA", "\"${gitSha}\"" buildConfigField "String", "BUILDTIME", "\"${buildTime}\"" + + manifestPlaceholders = [bugsnagApiKey: "fake"] } signingConfigs { @@ -51,12 +57,12 @@ android { buildTypes { def proguardRulesRelease = fileTree(dir: "../proguard", include: ["*.pro"]).asList().toArray() debug { - ext.enableBugsnag = false minifyEnabled false shrinkResources false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt') proguardFiles proguardRulesRelease proguardFiles 'proguard-rules-debug.pro' + manifestPlaceholders = [bugsnagApiKey: bugsnagProps.getProperty("bugsnag.apikey", "")] } release { signingConfig signingConfigs.release @@ -64,11 +70,11 @@ android { abortOnError true fatal 'StopShip' } - ext.enableBugsnag = true minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt') proguardFiles proguardRulesRelease + manifestPlaceholders = [bugsnagApiKey: bugsnagProps.getProperty("bugsnag.apikey", "")] } applicationVariants.all { variant -> if (variant.buildType.name == "debug") { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index a6b2b9a0..ad08eb1c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,15 +3,33 @@ xmlns:tools="http://schemas.android.com/tools" package="eu.darken.capod"> - - + + + - - - + + + + + + + + @@ -31,7 +49,7 @@ @@ -55,10 +73,10 @@ - + + + \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/App.kt b/app/src/main/java/eu/darken/capod/App.kt index 5d11498f..c39631ad 100644 --- a/app/src/main/java/eu/darken/capod/App.kt +++ b/app/src/main/java/eu/darken/capod/App.kt @@ -6,7 +6,7 @@ import androidx.work.Configuration import com.getkeepsafe.relinker.ReLinker import dagger.hilt.android.HiltAndroidApp import eu.darken.capod.common.coroutine.AppScope -import eu.darken.capod.common.debug.bugreporting.BugReporter +import eu.darken.capod.common.debug.autoreport.AutoReporting import eu.darken.capod.common.debug.logging.* import eu.darken.capod.monitor.core.worker.MonitorControl import kotlinx.coroutines.CoroutineScope @@ -17,7 +17,7 @@ import javax.inject.Inject open class App : Application(), Configuration.Provider { @Inject lateinit var workerFactory: HiltWorkerFactory - @Inject lateinit var bugReporter: BugReporter + @Inject lateinit var autoReporting: AutoReporting @Inject lateinit var monitorControl: MonitorControl @Inject @AppScope lateinit var appScope: CoroutineScope @@ -29,7 +29,7 @@ open class App : Application(), Configuration.Provider { .log { message -> log(TAG) { "ReLinker: $message" } } .loadLibrary(this, "bugsnag-plugin-android-anr") - bugReporter.setup() + autoReporting.setup() log(TAG) { "onCreate() done! ${Exception().asLog()}" } diff --git a/app/src/main/java/eu/darken/capod/common/BuildWrap.kt b/app/src/main/java/eu/darken/capod/common/BuildWrap.kt index 47fcf195..9967862e 100644 --- a/app/src/main/java/eu/darken/capod/common/BuildWrap.kt +++ b/app/src/main/java/eu/darken/capod/common/BuildWrap.kt @@ -14,3 +14,5 @@ object BuildWrap { } fun hasApiLevel(level: Int): Boolean = BuildWrap.VERSION.SDK_INT >= level + +fun withinApiLevel(start: Int, end: Int): Boolean = BuildWrap.VERSION.SDK_INT in start..end diff --git a/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt b/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt index b554cc94..aaca78d3 100644 --- a/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt +++ b/app/src/main/java/eu/darken/capod/common/bluetooth/BleScanner.kt @@ -10,7 +10,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext import eu.darken.capod.common.debug.logging.Logging.Priority.* import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.logTag -import eu.darken.capod.pods.core.airpods.ProximityPairing +import eu.darken.capod.pods.core.airpods.protocol.ProximityPairing import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow diff --git a/app/src/main/java/eu/darken/capod/common/debug/bugreporting/Bugs.kt b/app/src/main/java/eu/darken/capod/common/debug/Bugs.kt similarity index 86% rename from app/src/main/java/eu/darken/capod/common/debug/bugreporting/Bugs.kt rename to app/src/main/java/eu/darken/capod/common/debug/Bugs.kt index 508b0ca3..09b7391c 100644 --- a/app/src/main/java/eu/darken/capod/common/debug/bugreporting/Bugs.kt +++ b/app/src/main/java/eu/darken/capod/common/debug/Bugs.kt @@ -1,4 +1,4 @@ -package eu.darken.capod.common.debug.bugreporting +package eu.darken.capod.common.debug import com.bugsnag.android.Bugsnag import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE @@ -8,7 +8,7 @@ import eu.darken.capod.common.debug.logging.logTag object Bugs { var ready = false - fun report(exception: Exception) { + fun report(exception: Throwable) { log(TAG, VERBOSE) { "Reporting $exception" } if (!ready) { log(TAG, WARN) { "Bug tracking not initialized yet." } diff --git a/app/src/main/java/eu/darken/capod/common/debug/bugreporting/BugReporter.kt b/app/src/main/java/eu/darken/capod/common/debug/autoreport/AutoReporting.kt similarity index 74% rename from app/src/main/java/eu/darken/capod/common/debug/bugreporting/BugReporter.kt rename to app/src/main/java/eu/darken/capod/common/debug/autoreport/AutoReporting.kt index 35c6b20c..104be984 100644 --- a/app/src/main/java/eu/darken/capod/common/debug/bugreporting/BugReporter.kt +++ b/app/src/main/java/eu/darken/capod/common/debug/autoreport/AutoReporting.kt @@ -1,13 +1,14 @@ -package eu.darken.capod.common.debug.bugreporting +package eu.darken.capod.common.debug.autoreport import android.content.Context import com.bugsnag.android.Bugsnag import com.bugsnag.android.Configuration import dagger.hilt.android.qualifiers.ApplicationContext import eu.darken.capod.common.InstallId -import eu.darken.capod.common.debug.bugsnag.BugsnagErrorHandler -import eu.darken.capod.common.debug.bugsnag.BugsnagLogger -import eu.darken.capod.common.debug.bugsnag.NOPBugsnagErrorHandler +import eu.darken.capod.common.debug.Bugs +import eu.darken.capod.common.debug.autoreport.bugsnag.BugsnagErrorHandler +import eu.darken.capod.common.debug.autoreport.bugsnag.BugsnagLogger +import eu.darken.capod.common.debug.autoreport.bugsnag.NOPBugsnagErrorHandler import eu.darken.capod.common.debug.logging.Logging import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.logTag @@ -16,9 +17,9 @@ import javax.inject.Provider import javax.inject.Singleton @Singleton -class BugReporter @Inject constructor( +class AutoReporting @Inject constructor( @ApplicationContext private val context: Context, - private val bugReportSettings: BugReportSettings, + private val debugSettings: DebugSettings, private val installId: InstallId, private val bugsnagLogger: Provider, private val bugsnagErrorHandler: Provider, @@ -26,12 +27,12 @@ class BugReporter @Inject constructor( ) { fun setup() { - val isEnabled = bugReportSettings.isEnabled.value + val isEnabled = debugSettings.isAutoReportEnabled.value log(TAG) { "setup(): isEnabled=$isEnabled" } try { val bugsnagConfig = Configuration.load(context).apply { - if (bugReportSettings.isEnabled.value) { + if (debugSettings.isAutoReportEnabled.value) { Logging.install(bugsnagLogger.get()) setUser(installId.id, null, null) autoTrackSessions = true @@ -52,6 +53,6 @@ class BugReporter @Inject constructor( } companion object { - private val TAG = logTag("BugReporter") + private val TAG = logTag("Debug", "AutoReport") } } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/common/debug/bugreporting/BugReportSettings.kt b/app/src/main/java/eu/darken/capod/common/debug/autoreport/DebugSettings.kt similarity index 68% rename from app/src/main/java/eu/darken/capod/common/debug/bugreporting/BugReportSettings.kt rename to app/src/main/java/eu/darken/capod/common/debug/autoreport/DebugSettings.kt index c6aac9df..6c758cf2 100644 --- a/app/src/main/java/eu/darken/capod/common/debug/bugreporting/BugReportSettings.kt +++ b/app/src/main/java/eu/darken/capod/common/debug/autoreport/DebugSettings.kt @@ -1,4 +1,4 @@ -package eu.darken.capod.common.debug.bugreporting +package eu.darken.capod.common.debug.autoreport import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext @@ -7,7 +7,7 @@ import javax.inject.Inject import javax.inject.Singleton @Singleton -class BugReportSettings @Inject constructor( +class DebugSettings @Inject constructor( @ApplicationContext private val context: Context, ) { @@ -15,6 +15,6 @@ class BugReportSettings @Inject constructor( context.getSharedPreferences("bugreport_settings", Context.MODE_PRIVATE) } - val isEnabled = prefs.createFlowPreference("bugreport.automatic.enabled", true) + val isAutoReportEnabled = prefs.createFlowPreference("bugreport.automatic.enabled", true) } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/common/debug/bugsnag/BugsnagErrorHandler.kt b/app/src/main/java/eu/darken/capod/common/debug/autoreport/bugsnag/BugsnagErrorHandler.kt similarity index 85% rename from app/src/main/java/eu/darken/capod/common/debug/bugsnag/BugsnagErrorHandler.kt rename to app/src/main/java/eu/darken/capod/common/debug/autoreport/bugsnag/BugsnagErrorHandler.kt index 3e29bf6d..c99d261a 100644 --- a/app/src/main/java/eu/darken/capod/common/debug/bugsnag/BugsnagErrorHandler.kt +++ b/app/src/main/java/eu/darken/capod/common/debug/autoreport/bugsnag/BugsnagErrorHandler.kt @@ -1,4 +1,4 @@ -package eu.darken.capod.common.debug.bugsnag +package eu.darken.capod.common.debug.autoreport.bugsnag import android.annotation.SuppressLint import android.content.Context @@ -7,6 +7,8 @@ import com.bugsnag.android.Event import com.bugsnag.android.OnErrorCallback import dagger.hilt.android.qualifiers.ApplicationContext import eu.darken.capod.BuildConfig +import eu.darken.capod.common.BuildConfigWrap +import eu.darken.capod.common.debug.autoreport.DebugSettings 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.log @@ -17,6 +19,7 @@ import javax.inject.Singleton class BugsnagErrorHandler @Inject constructor( @ApplicationContext private val context: Context, private val bugsnagLogger: BugsnagLogger, + private val debugSettings: DebugSettings, ) : OnErrorCallback { override fun onError(event: Event): Boolean { @@ -29,7 +32,7 @@ class BugsnagErrorHandler @Inject constructor( context.tryFormattedSignature()?.let { event.addMetadata(tab, "signatures", it) } } - return !BuildConfig.DEBUG + return debugSettings.isAutoReportEnabled.value && !BuildConfigWrap.DEBUG } companion object { diff --git a/app/src/main/java/eu/darken/capod/common/debug/bugsnag/BugsnagLogger.kt b/app/src/main/java/eu/darken/capod/common/debug/autoreport/bugsnag/BugsnagLogger.kt similarity index 96% rename from app/src/main/java/eu/darken/capod/common/debug/bugsnag/BugsnagLogger.kt rename to app/src/main/java/eu/darken/capod/common/debug/autoreport/bugsnag/BugsnagLogger.kt index b77489b6..d80b70cf 100644 --- a/app/src/main/java/eu/darken/capod/common/debug/bugsnag/BugsnagLogger.kt +++ b/app/src/main/java/eu/darken/capod/common/debug/autoreport/bugsnag/BugsnagLogger.kt @@ -1,4 +1,4 @@ -package eu.darken.capod.common.debug.bugsnag +package eu.darken.capod.common.debug.autoreport.bugsnag import com.bugsnag.android.Event import eu.darken.capod.common.debug.logging.Logging diff --git a/app/src/main/java/eu/darken/capod/common/debug/bugsnag/NOPBugsnagErrorHandler.kt b/app/src/main/java/eu/darken/capod/common/debug/autoreport/bugsnag/NOPBugsnagErrorHandler.kt similarity index 90% rename from app/src/main/java/eu/darken/capod/common/debug/bugsnag/NOPBugsnagErrorHandler.kt rename to app/src/main/java/eu/darken/capod/common/debug/autoreport/bugsnag/NOPBugsnagErrorHandler.kt index d61d3114..fa10e669 100644 --- a/app/src/main/java/eu/darken/capod/common/debug/bugsnag/NOPBugsnagErrorHandler.kt +++ b/app/src/main/java/eu/darken/capod/common/debug/autoreport/bugsnag/NOPBugsnagErrorHandler.kt @@ -1,4 +1,4 @@ -package eu.darken.capod.common.debug.bugsnag +package eu.darken.capod.common.debug.autoreport.bugsnag import com.bugsnag.android.Event import com.bugsnag.android.OnErrorCallback diff --git a/app/src/main/java/eu/darken/capod/common/permissions/Permission.kt b/app/src/main/java/eu/darken/capod/common/permissions/Permission.kt index 7b315c85..ee75bc3f 100644 --- a/app/src/main/java/eu/darken/capod/common/permissions/Permission.kt +++ b/app/src/main/java/eu/darken/capod/common/permissions/Permission.kt @@ -6,13 +6,22 @@ import android.os.Build import androidx.annotation.StringRes import androidx.core.content.ContextCompat import eu.darken.capod.R +import eu.darken.capod.common.withinApiLevel enum class Permission( val minApiLevel: Int, + val maxApiLevel: Int = Int.MAX_VALUE, @StringRes val labelRes: Int, @StringRes val descriptionRes: Int, val permissionId: String, ) { + BLUETOOTH( + minApiLevel = Build.VERSION_CODES.BASE, + maxApiLevel = Build.VERSION_CODES.R, + labelRes = R.string.permission_bluetooth_label, + descriptionRes = R.string.permission_bluetooth_description, + permissionId = "android.permission.BLUETOOTH", + ), BLUETOOTH_CONNECT( minApiLevel = Build.VERSION_CODES.S, labelRes = R.string.permission_bluetooth_connect_label, @@ -24,9 +33,17 @@ enum class Permission( labelRes = R.string.permission_bluetooth_scan_label, descriptionRes = R.string.permission_bluetooth_scan_description, permissionId = "android.permission.BLUETOOTH_SCAN", + ), + ACCESS_FINE_LOCATION( + minApiLevel = Build.VERSION_CODES.S, + maxApiLevel = Build.VERSION_CODES.R, + labelRes = R.string.permission_access_fine_location_label, + descriptionRes = R.string.permission_access_fine_location_description, + permissionId = "android.permission.ACCESS_FINE_LOCATION", ) } -fun Permission.isGranted(context: Context): Boolean { - return ContextCompat.checkSelfPermission(context, permissionId) == PackageManager.PERMISSION_GRANTED +fun Permission.isGrantedOrNotRequired(context: Context): Boolean = when { + !withinApiLevel(minApiLevel, maxApiLevel) -> true + else -> ContextCompat.checkSelfPermission(context, permissionId) == PackageManager.PERMISSION_GRANTED } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/ui/MainFragmentVM.kt b/app/src/main/java/eu/darken/capod/main/ui/MainFragmentVM.kt deleted file mode 100644 index 50a22ed3..00000000 --- a/app/src/main/java/eu/darken/capod/main/ui/MainFragmentVM.kt +++ /dev/null @@ -1,78 +0,0 @@ -package eu.darken.capod.main.ui - -import android.content.Context -import androidx.lifecycle.LiveData -import androidx.lifecycle.SavedStateHandle -import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext -import eu.darken.capod.common.coroutine.DispatcherProvider -import eu.darken.capod.common.debug.recording.core.RecorderModule -import eu.darken.capod.common.hasApiLevel -import eu.darken.capod.common.livedata.SingleLiveEvent -import eu.darken.capod.common.permissions.Permission -import eu.darken.capod.common.permissions.isGranted -import eu.darken.capod.common.uix.ViewModel3 -import eu.darken.capod.main.ui.cards.PermissionCardVH -import eu.darken.capod.main.ui.cards.ToggleCardVH -import kotlinx.coroutines.flow.* -import java.util.* -import javax.inject.Inject - -@HiltViewModel -class MainFragmentVM @Inject constructor( - handle: SavedStateHandle, - @ApplicationContext private val context: Context, - dispatcherProvider: DispatcherProvider, - private val recorderModule: RecorderModule, -) : ViewModel3(dispatcherProvider = dispatcherProvider) { - - - private val enabledState: Flow = flow { - emit(false) - } - - private val permissionCheckTrigger = MutableStateFlow(UUID.randomUUID()) - private val requiredPermissions: Flow> = permissionCheckTrigger.map { - Permission.values() - .filter { hasApiLevel(it.minApiLevel) && !it.isGranted(context) } - } - - val requestPermissionevent = SingleLiveEvent() - - val listItems: LiveData> = combine( - enabledState, - requiredPermissions - ) { state, permissions -> - val items = mutableListOf() - ToggleCardVH.Item( - isEnabled = state, - onToggle = { - - } - ).run { items.add(this) } - - permissions - .map { - PermissionCardVH.Item( - permission = it, - onRequest = { requestPermissionevent.postValue(it) } - ) - } - .forEach { items.add(it) } - - items - }.asLiveData2() - - fun onPermissionResult(granted: Boolean) { - if (granted) permissionCheckTrigger.value = UUID.randomUUID() - } - - fun toggleDebugLog() = launch { - recorderModule.startRecorder() - } - - fun goToSettings() = launch { - - } - -} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/ui/cards/ToggleCardVH.kt b/app/src/main/java/eu/darken/capod/main/ui/cards/ToggleCardVH.kt deleted file mode 100644 index 90d2fc84..00000000 --- a/app/src/main/java/eu/darken/capod/main/ui/cards/ToggleCardVH.kt +++ /dev/null @@ -1,35 +0,0 @@ -package eu.darken.capod.main.ui.cards - -import android.view.ViewGroup -import eu.darken.capod.R -import eu.darken.capod.databinding.MainToggleItemBinding -import eu.darken.capod.main.ui.MainAdapter - -class ToggleCardVH(parent: ViewGroup) : - MainAdapter.BaseVH( - R.layout.main_toggle_item, - parent - ) { - - override val viewBinding = lazy { - MainToggleItemBinding.bind(itemView) - } - - override val onBindData: MainToggleItemBinding.( - item: Item, - payloads: List - ) -> Unit = { item, _ -> - toggleAction.text = when (item.isEnabled) { - true -> "Disable" - false -> "Enable" - } - itemView.setOnClickListener { item.onToggle() } - } - - data class Item( - val isEnabled: Boolean, - val onToggle: () -> Unit - ) : MainAdapter.Item { - override val stableId: Long = this.javaClass.hashCode().toLong() - } -} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/ui/MainAdapter.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewAdapter.kt similarity index 63% rename from app/src/main/java/eu/darken/capod/main/ui/MainAdapter.kt rename to app/src/main/java/eu/darken/capod/main/ui/overview/OverviewAdapter.kt index 36d83ac8..fee8dd28 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/MainAdapter.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewAdapter.kt @@ -1,4 +1,4 @@ -package eu.darken.capod.main.ui +package eu.darken.capod.main.ui.overview import android.view.ViewGroup import androidx.annotation.LayoutRes @@ -11,20 +11,22 @@ import eu.darken.capod.common.lists.differ.setupDiffer import eu.darken.capod.common.lists.modular.ModularAdapter import eu.darken.capod.common.lists.modular.mods.DataBinderMod import eu.darken.capod.common.lists.modular.mods.TypedVHCreatorMod -import eu.darken.capod.main.ui.cards.PermissionCardVH -import eu.darken.capod.main.ui.cards.ToggleCardVH +import eu.darken.capod.main.ui.overview.cards.DualApplePodsCardVH +import eu.darken.capod.main.ui.overview.cards.PermissionCardVH +import eu.darken.capod.main.ui.overview.cards.UnknownPodDeviceCardVH import javax.inject.Inject -class MainAdapter @Inject constructor() : - ModularAdapter>(), - HasAsyncDiffer { +class OverviewAdapter @Inject constructor() : + ModularAdapter>(), + HasAsyncDiffer { override val asyncDiffer: AsyncDiffer<*, Item> = setupDiffer() init { modules.add(DataBinderMod(data)) - modules.add(TypedVHCreatorMod({ data[it] is ToggleCardVH.Item }) { ToggleCardVH(it) }) modules.add(TypedVHCreatorMod({ data[it] is PermissionCardVH.Item }) { PermissionCardVH(it) }) + modules.add(TypedVHCreatorMod({ data[it] is DualApplePodsCardVH.Item }) { DualApplePodsCardVH(it) }) + modules.add(TypedVHCreatorMod({ data[it] is UnknownPodDeviceCardVH.Item }) { UnknownPodDeviceCardVH(it) }) } override fun getItemCount(): Int = data.size diff --git a/app/src/main/java/eu/darken/capod/main/ui/MainFragment.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragment.kt similarity index 91% rename from app/src/main/java/eu/darken/capod/main/ui/MainFragment.kt rename to app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragment.kt index 55b99ada..60ff673e 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/MainFragment.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragment.kt @@ -1,4 +1,4 @@ -package eu.darken.capod.main.ui +package eu.darken.capod.main.ui.overview import android.os.Bundle import android.view.View @@ -17,13 +17,13 @@ import eu.darken.capod.databinding.MainFragmentBinding import javax.inject.Inject @AndroidEntryPoint -class MainFragment : Fragment3(R.layout.main_fragment) { +class OverviewFragment : Fragment3(R.layout.main_fragment) { - override val vm: MainFragmentVM by viewModels() + override val vm: OverviewFragmentVM by viewModels() override val ui: MainFragmentBinding by viewBinding() @Inject - lateinit var adapter: MainAdapter + lateinit var adapter: OverviewAdapter lateinit var permissionLauncher: ActivityResultLauncher @@ -37,10 +37,10 @@ class MainFragment : Fragment3(R.layout.main_fragment) { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - ui.apply { list.setupDefaults(adapter) } + ui.toolbar.apply { subtitle = BuildConfigWrap.VERSION_DESCRIPTION setOnMenuItemClickListener { diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt new file mode 100644 index 00000000..3a0d225b --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/OverviewFragmentVM.kt @@ -0,0 +1,107 @@ +package eu.darken.capod.main.ui.overview + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.SavedStateHandle +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import eu.darken.capod.common.coroutine.DispatcherProvider +import eu.darken.capod.common.debug.recording.core.RecorderModule +import eu.darken.capod.common.livedata.SingleLiveEvent +import eu.darken.capod.common.permissions.Permission +import eu.darken.capod.common.permissions.isGrantedOrNotRequired +import eu.darken.capod.common.uix.ViewModel3 +import eu.darken.capod.main.ui.overview.cards.DualApplePodsCardVH +import eu.darken.capod.main.ui.overview.cards.PermissionCardVH +import eu.darken.capod.main.ui.overview.cards.UnknownPodDeviceCardVH +import eu.darken.capod.monitor.core.PodMonitor +import eu.darken.capod.monitor.core.worker.MonitorControl +import eu.darken.capod.pods.core.PodDevice +import eu.darken.capod.pods.core.airpods.DualApplePods +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.* +import java.util.* +import javax.inject.Inject + +@HiltViewModel +class OverviewFragmentVM @Inject constructor( + handle: SavedStateHandle, + @ApplicationContext private val context: Context, + dispatcherProvider: DispatcherProvider, + private val recorderModule: RecorderModule, + private val monitorControl: MonitorControl, + private val podMonitor: PodMonitor, +) : ViewModel3(dispatcherProvider = dispatcherProvider) { + + + private val enabledState: Flow = flow { + emit(false) + } + + private val permissionCheckTrigger = MutableStateFlow(UUID.randomUUID()) + private val requiredPermissions: Flow> = permissionCheckTrigger + .map { + Permission.values().filter { !it.isGrantedOrNotRequired(context) } + } + .onEach { + if (it.isEmpty()) { + monitorControl.startMonitor() + } + } + + val requestPermissionevent = SingleLiveEvent() + + private val pods: Flow> = requiredPermissions + .flatMapLatest { + if (it.isEmpty()) { + podMonitor.pods + } else { + channelFlow { + send(emptyList()) + awaitClose() + } + } + } + + val listItems: LiveData> = combine( + enabledState, + requiredPermissions, + pods + ) { state, permissions, pods -> + val items = mutableListOf() + + permissions + .map { + PermissionCardVH.Item( + permission = it, + onRequest = { requestPermissionevent.postValue(it) } + ) + } + .forEach { items.add(it) } + + pods + .sortedBy { it.rssi } + .map { + when (it) { + is DualApplePods -> DualApplePodsCardVH.Item(it) + else -> UnknownPodDeviceCardVH.Item(it) + } + } + .run { items.addAll(this) } + + items + }.asLiveData2() + + fun onPermissionResult(granted: Boolean) { + if (granted) permissionCheckTrigger.value = UUID.randomUUID() + } + + fun toggleDebugLog() = launch { + recorderModule.startRecorder() + } + + fun goToSettings() = launch { + + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/DualApplePodsCardVH.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/DualApplePodsCardVH.kt new file mode 100644 index 00000000..ef2d7c05 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/DualApplePodsCardVH.kt @@ -0,0 +1,90 @@ +package eu.darken.capod.main.ui.overview.cards + +import android.view.ViewGroup +import eu.darken.capod.R +import eu.darken.capod.databinding.OverviewPodsAppleDualItemBinding +import eu.darken.capod.main.ui.overview.OverviewAdapter +import eu.darken.capod.pods.core.DualPods +import eu.darken.capod.pods.core.airpods.DualApplePods +import eu.darken.capod.pods.core.airpods.DualApplePods.DeviceColor +import eu.darken.capod.pods.core.airpods.DualApplePods.LidState +import eu.darken.capod.pods.core.getBatteryCase +import eu.darken.capod.pods.core.getBatteryLeftPod +import eu.darken.capod.pods.core.getBatteryRightPod + +class DualApplePodsCardVH(parent: ViewGroup) : + OverviewAdapter.BaseVH( + R.layout.overview_pods_apple_dual_item, + parent + ) { + + override val viewBinding = lazy { + OverviewPodsAppleDualItemBinding.bind(itemView) + } + + override val onBindData: OverviewPodsAppleDualItemBinding.( + item: Item, + payloads: List + ) -> Unit = { item, _ -> + val device = item.device + + name.apply { + val sb = StringBuilder(device.getLabel(context)) + if (device.deviceColor != DeviceColor.UNKNOWN) { + sb.append(" (${device.getDeviceColorLabel(context)})") + } + text = sb + } + + reception.text = "${device.rssi} (RSSI)" + + podLeft.apply { + val sb = StringBuilder(context.getString(R.string.pods_dual_left_label)) + sb.append("\n").append(device.getBatteryLeftPod(context)) + when { + device.isLeftPodCharging -> sb.append("\n").append("Charging") + device.isLeftPodInEar -> sb.append("\n").append("In ear") + else -> {} + } + text = sb + } + + podRight.apply { + val sb = StringBuilder("Right pod") + sb.append("\n").append(device.getBatteryRightPod(context)) + when { + device.isRightPodCharging -> sb.append("\n").append("Charging") + device.isRightPodInEar -> sb.append("\n").append("In ear") + else -> {} + } + text = sb + } + + when (device.microPhonePod) { + DualPods.Pod.LEFT -> podLeft.append("\n(Microphone)") + DualPods.Pod.RIGHT -> podRight.append("\n(Microphone)") + } + + podCase.apply { + val sb = StringBuilder("Case") + sb.append("\n").append(device.getBatteryCase(context)) + if (device.isCaseCharging) sb.append("\n").append("Charging") + when (device.caseLidState) { + LidState.OPEN -> sb.append("\n").append(context.getString(R.string.pods_case_status_open_label)) + LidState.CLOSED -> sb.append("\n").append(context.getString(R.string.pods_case_status_closed_label)) + LidState.NOT_IN_CASE, + LidState.UNKNOWN -> { + } + } + text = sb + } + + status.text = getString(R.string.pods_status_x_label, device.getConnectionStateLabel(context)) + } + + data class Item( + val device: DualApplePods + ) : OverviewAdapter.Item { + override val stableId: Long = this.javaClass.hashCode().toLong() + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/ui/cards/PermissionCardVH.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/PermissionCardVH.kt similarity index 80% rename from app/src/main/java/eu/darken/capod/main/ui/cards/PermissionCardVH.kt rename to app/src/main/java/eu/darken/capod/main/ui/overview/cards/PermissionCardVH.kt index f47fac9d..21b179ff 100644 --- a/app/src/main/java/eu/darken/capod/main/ui/cards/PermissionCardVH.kt +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/PermissionCardVH.kt @@ -1,13 +1,13 @@ -package eu.darken.capod.main.ui.cards +package eu.darken.capod.main.ui.overview.cards import android.view.ViewGroup import eu.darken.capod.R import eu.darken.capod.common.permissions.Permission import eu.darken.capod.databinding.MainPermissionItemBinding -import eu.darken.capod.main.ui.MainAdapter +import eu.darken.capod.main.ui.overview.OverviewAdapter class PermissionCardVH(parent: ViewGroup) : - MainAdapter.BaseVH( + OverviewAdapter.BaseVH( R.layout.main_permission_item, parent ) { @@ -28,7 +28,7 @@ class PermissionCardVH(parent: ViewGroup) : data class Item( val permission: Permission, val onRequest: (Permission) -> Unit - ) : MainAdapter.Item { + ) : OverviewAdapter.Item { override val stableId: Long = this.javaClass.hashCode().toLong() } } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/main/ui/overview/cards/UnknownPodDeviceCardVH.kt b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/UnknownPodDeviceCardVH.kt new file mode 100644 index 00000000..48582685 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/main/ui/overview/cards/UnknownPodDeviceCardVH.kt @@ -0,0 +1,32 @@ +package eu.darken.capod.main.ui.overview.cards + +import android.view.ViewGroup +import eu.darken.capod.R +import eu.darken.capod.databinding.OverviewPodsUnknownItemBinding +import eu.darken.capod.main.ui.overview.OverviewAdapter +import eu.darken.capod.pods.core.PodDevice + +class UnknownPodDeviceCardVH(parent: ViewGroup) : + OverviewAdapter.BaseVH( + R.layout.overview_pods_unknown_item, + parent + ) { + + override val viewBinding = lazy { + OverviewPodsUnknownItemBinding.bind(itemView) + } + + override val onBindData: OverviewPodsUnknownItemBinding.( + item: Item, + payloads: List + ) -> Unit = { item, _ -> + + name.text = item.device.getLabel(context) + } + + data class Item( + val device: PodDevice + ) : OverviewAdapter.Item { + override val stableId: Long = this.javaClass.hashCode().toLong() + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/monitor/core/MonitorSettings.kt b/app/src/main/java/eu/darken/capod/monitor/core/MonitorSettings.kt new file mode 100644 index 00000000..14dc0853 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/monitor/core/MonitorSettings.kt @@ -0,0 +1,16 @@ +package eu.darken.capod.monitor.core + +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class MonitorSettings @Inject constructor() { + + val mode: Mode = Mode.ALWAYS + + enum class Mode(val raw: String) { + MANUAL("monitor.mode.manual"), + AUTOMATIC("monitor.mode.automatic"), + ALWAYS("monitor.mode.always") + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/monitor/core/receiver/BluetoothEventReceiver.kt b/app/src/main/java/eu/darken/capod/monitor/core/receiver/BluetoothEventReceiver.kt index 2b041e7d..2d544bc5 100644 --- a/app/src/main/java/eu/darken/capod/monitor/core/receiver/BluetoothEventReceiver.kt +++ b/app/src/main/java/eu/darken/capod/monitor/core/receiver/BluetoothEventReceiver.kt @@ -13,7 +13,7 @@ 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 eu.darken.capod.monitor.core.worker.MonitorControl -import eu.darken.capod.pods.core.airpods.ContinuityProtocol +import eu.darken.capod.pods.core.airpods.protocol.ContinuityProtocol import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject diff --git a/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorControl.kt b/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorControl.kt index d243c096..19479ab3 100644 --- a/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorControl.kt +++ b/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorControl.kt @@ -22,7 +22,7 @@ class MonitorControl @Inject constructor( suspend fun startMonitor( bluetoothDevice: BluetoothDevice? = null, - forceStart: Boolean + forceStart: Boolean = false, ): Unit = withContext(dispatcherProvider.IO) { val workerData = Data.Builder().apply { diff --git a/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt b/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt index e8e8c6ff..f65ce472 100644 --- a/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt +++ b/app/src/main/java/eu/darken/capod/monitor/core/worker/MonitorWorker.kt @@ -8,17 +8,21 @@ import androidx.work.WorkerParameters import dagger.assisted.Assisted import dagger.assisted.AssistedInject import dagger.hilt.EntryPoints -import eu.darken.capod.common.debug.logging.Logging.Priority.ERROR -import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE +import eu.darken.capod.common.debug.Bugs +import eu.darken.capod.common.debug.logging.Logging.Priority.* import eu.darken.capod.common.debug.logging.asLog import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.common.flow.setupCommonEventHandlers +import eu.darken.capod.common.permissions.Permission +import eu.darken.capod.common.permissions.isGrantedOrNotRequired import eu.darken.capod.monitor.core.MonitorComponent import eu.darken.capod.monitor.core.MonitorCoroutineScope +import eu.darken.capod.monitor.core.MonitorSettings import eu.darken.capod.monitor.ui.MonitorNotifications -import eu.darken.capod.pods.core.airpods.ContinuityProtocol +import eu.darken.capod.pods.core.airpods.protocol.ContinuityProtocol import kotlinx.coroutines.cancel +import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* @@ -30,6 +34,7 @@ class MonitorWorker @AssistedInject constructor( monitorComponentBuilder: MonitorComponent.Builder, private val monitorNotifications: MonitorNotifications, private val notificationManager: NotificationManager, + private val monitorSettings: MonitorSettings, ) : CoroutineWorker(context, params) { private val workerScope = MonitorCoroutineScope() @@ -53,66 +58,80 @@ class MonitorWorker @AssistedInject constructor( log(TAG, VERBOSE) { "init(): workerId=$id" } } - override suspend fun doWork(): Result = try { - val start = System.currentTimeMillis() - log(TAG, VERBOSE) { "Executing $inputData now (runAttemptCount=$runAttemptCount)" } + override suspend fun doWork(): Result { + try { + val start = System.currentTimeMillis() + log(TAG, VERBOSE) { "Executing $inputData now (runAttemptCount=$runAttemptCount)" } - bluetoothManager2 - .isBluetoothEnabled - .flatMapLatest { bluetoothManager2.connectedDevices() } - .map { devices -> - devices.any { device -> - ContinuityProtocol.BLE_FEATURE_UUIDS.any { feature -> - device.hasFeature(feature) + val missingPermissions = Permission.values().filter { !it.isGrantedOrNotRequired(context) } + if (missingPermissions.isNotEmpty()) { + log(TAG, WARN) { "Aborting, missing permissions: $missingPermissions" } + return Result.success() + } + + bluetoothManager2 + .isBluetoothEnabled + .flatMapLatest { bluetoothManager2.connectedDevices() } + .map { devices -> + devices.any { device -> + ContinuityProtocol.BLE_FEATURE_UUIDS.any { feature -> + device.hasFeature(feature) + } } } - } - .setupCommonEventHandlers(TAG) { "ConnectedDevices" } - .flatMapLatest { arePodsConnected -> - flow { - if (arePodsConnected) { - log(TAG) { "Pods are connected, aborting any timeout." } - } else { - log(TAG) { "No Pods are connected, canceling worker soon." } - delay(60 * 1000) - log(TAG) { "Canceling worker now, still no Pods connected." } - // FIXME -// workerScope.coroutineContext.cancelChildren() + .setupCommonEventHandlers(TAG) { "ConnectedDevices" } + .flatMapLatest { arePodsConnected -> + log(TAG) { "Monitor mode: ${monitorSettings.mode}" } + when (monitorSettings.mode) { + MonitorSettings.Mode.MANUAL -> emptyFlow() + MonitorSettings.Mode.ALWAYS -> emptyFlow() + MonitorSettings.Mode.AUTOMATIC -> flow { + if (arePodsConnected) { + log(TAG) { "Pods are connected, aborting any timeout." } + } else { + log(TAG) { "No Pods are connected, canceling worker soon." } + delay(60 * 1000) + log(TAG) { "Canceling worker now, still no Pods connected." } + + workerScope.coroutineContext.cancelChildren() + } + } } } - } - .launchIn(workerScope) + .launchIn(workerScope) - val monitorJob = podMonitor.pods - .setupCommonEventHandlers(TAG) { "PodMonitor" } - .onStart { - setForeground(monitorNotifications.getForegroundInfo(null)) - } - .onEach { - notificationManager.notify( - MonitorNotifications.NOTIFICATION_ID, - monitorNotifications.getNotification(it.firstOrNull()) - ) - } - .launchIn(workerScope) + val monitorJob = podMonitor.pods + .setupCommonEventHandlers(TAG) { "PodMonitor" } + .onStart { + setForeground(monitorNotifications.getForegroundInfo(null)) + } + .onEach { + notificationManager.notify( + MonitorNotifications.NOTIFICATION_ID, + monitorNotifications.getNotification(it.firstOrNull()) + ) + } + .launchIn(workerScope) - log(TAG, VERBOSE) { "monitor job is active" } - monitorJob.join() - log(TAG, VERBOSE) { "monitor job quit" } + log(TAG, VERBOSE) { "Monitor job is active" } + monitorJob.join() + log(TAG, VERBOSE) { "Monitor job quit" } - val duration = System.currentTimeMillis() - start + val duration = System.currentTimeMillis() - start - log(TAG, VERBOSE) { "Execution finished after ${duration}ms, $inputData" } + log(TAG, VERBOSE) { "Execution finished after ${duration}ms, $inputData" } - Result.success(inputData) - } catch (e: Throwable) { - log(TAG, ERROR) { "Execution failed:\n${e.asLog()}" } - finishedWithError = true - // TODO update result? - Result.failure(inputData) - } finally { - this.workerScope.cancel("Worker finished (withError?=$finishedWithError).") + return Result.success(inputData) + } catch (e: Throwable) { + log(TAG, ERROR) { "Execution failed:\n${e.asLog()}" } + Bugs.report(e) + finishedWithError = true + // TODO update result? + return Result.failure(inputData) + } finally { + this.workerScope.cancel("Worker finished (withError?=$finishedWithError).") + } } companion object { diff --git a/app/src/main/java/eu/darken/capod/pods/core/DualPods.kt b/app/src/main/java/eu/darken/capod/pods/core/DualPods.kt new file mode 100644 index 00000000..4e3c662f --- /dev/null +++ b/app/src/main/java/eu/darken/capod/pods/core/DualPods.kt @@ -0,0 +1,27 @@ +package eu.darken.capod.pods.core + +interface DualPods : PodDevice { + + val microPhonePod: Pod + + enum class Pod { + LEFT, + RIGHT + } + + val batteryLeftPodPercent: Float? + + val batteryRightPodPercent: Float? + + val batteryCasePercent: Float? + + val isLeftPodInEar: Boolean + + val isRightPodInEar: Boolean + + val isCaseCharging: Boolean + + val isLeftPodCharging: Boolean + + val isRightPodCharging: Boolean +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/DualPodsExtensions.kt b/app/src/main/java/eu/darken/capod/pods/core/DualPodsExtensions.kt new file mode 100644 index 00000000..f952ab59 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/pods/core/DualPodsExtensions.kt @@ -0,0 +1,17 @@ +package eu.darken.capod.pods.core + +import android.content.Context +import eu.darken.capod.R +import kotlin.math.roundToInt + +fun DualPods.getBatteryLeftPod(context: Context): String = + batteryLeftPodPercent?.let { "${(it * 100).roundToInt()}%" } + ?: context.getString(R.string.general_value_not_available_label) + +fun DualPods.getBatteryRightPod(context: Context): String = + batteryRightPodPercent?.let { "${(it * 100).roundToInt()}%" } + ?: context.getString(R.string.general_value_not_available_label) + +fun DualPods.getBatteryCase(context: Context): String = + batteryCasePercent?.let { "${(it * 100).roundToInt()}%" } + ?: context.getString(R.string.general_value_not_available_label) diff --git a/app/src/main/java/eu/darken/capod/pods/core/PodDevice.kt b/app/src/main/java/eu/darken/capod/pods/core/PodDevice.kt index 5af6663f..61375fc5 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/PodDevice.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/PodDevice.kt @@ -1,10 +1,14 @@ package eu.darken.capod.pods.core import android.bluetooth.le.ScanResult +import android.content.Context interface PodDevice { val scanResult: ScanResult - interface Status + val rssi: Int + get() = scanResult.rssi + + fun getLabel(context: Context): String } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/airpods/AirPodsFactory.kt b/app/src/main/java/eu/darken/capod/pods/core/airpods/AirPodsFactory.kt index 4d2e5113..22649562 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/airpods/AirPodsFactory.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/airpods/AirPodsFactory.kt @@ -2,7 +2,7 @@ package eu.darken.capod.pods.core.airpods import android.bluetooth.le.ScanResult import dagger.Reusable -import eu.darken.capod.common.debug.bugreporting.Bugs +import eu.darken.capod.common.debug.Bugs import eu.darken.capod.common.debug.logging.Logging.Priority.INFO import eu.darken.capod.common.debug.logging.Logging.Priority.WARN import eu.darken.capod.common.debug.logging.asLog @@ -13,6 +13,8 @@ import eu.darken.capod.pods.core.airpods.models.AirPodsGen1 import eu.darken.capod.pods.core.airpods.models.AirPodsGen2 import eu.darken.capod.pods.core.airpods.models.AirPodsPro import eu.darken.capod.pods.core.airpods.models.UnknownAppleDevice +import eu.darken.capod.pods.core.airpods.protocol.ContinuityProtocol +import eu.darken.capod.pods.core.airpods.protocol.ProximityPairing import javax.inject.Inject @Reusable diff --git a/app/src/main/java/eu/darken/capod/pods/core/airpods/ApplePods.kt b/app/src/main/java/eu/darken/capod/pods/core/airpods/ApplePods.kt index 347a6fbc..33df1d5a 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/airpods/ApplePods.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/airpods/ApplePods.kt @@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.airpods import eu.darken.capod.common.debug.logging.logTag import eu.darken.capod.pods.core.PodDevice +import eu.darken.capod.pods.core.airpods.protocol.ProximityPairing interface ApplePods : PodDevice { @@ -9,6 +10,6 @@ interface ApplePods : PodDevice { companion object { - val TAG = logTag("Pod", "BaseAirPods") + val TAG = logTag("Pod", "AppleDevice") } } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/airpods/DualApplePods.kt b/app/src/main/java/eu/darken/capod/pods/core/airpods/DualApplePods.kt new file mode 100644 index 00000000..bb7704d6 --- /dev/null +++ b/app/src/main/java/eu/darken/capod/pods/core/airpods/DualApplePods.kt @@ -0,0 +1,197 @@ +package eu.darken.capod.pods.core.airpods + +import android.content.Context +import androidx.annotation.StringRes +import eu.darken.capod.R +import eu.darken.capod.common.debug.logging.log +import eu.darken.capod.common.isBitSet +import eu.darken.capod.common.lowerNibble +import eu.darken.capod.common.upperNibble +import eu.darken.capod.pods.core.DualPods +import eu.darken.capod.pods.core.DualPods.Pod + +interface DualApplePods : ApplePods, DualPods { + + val tag: String + + // We start counting at the airpods prefix byte + val rawPrefix: UByte + get() = proximityMessage.data[0] + + val rawDeviceModel: UShort + get() = (((proximityMessage.data[1].toInt() and 255) shl 8) or (proximityMessage.data[2].toInt() and 255)).toUShort() + + val rawStatus: UByte + get() = proximityMessage.data[3] + + val rawPodsBattery: UByte + get() = proximityMessage.data[4] + + val rawCaseBattery: UByte + get() = proximityMessage.data[5] + + val rawCaseLidState: UByte + get() = proximityMessage.data[6] + + val rawDeviceColor: UByte + get() = proximityMessage.data[7] + + val rawSuffix: UByte + get() = proximityMessage.data[8] + + override val microPhonePod: Pod + get() = when (rawStatus.isBitSet(5)) { + true -> Pod.LEFT + false -> Pod.RIGHT + } + + override val batteryLeftPodPercent: Float? + get() { + val value = when (microPhonePod) { + Pod.LEFT -> rawPodsBattery.lowerNibble.toInt() + Pod.RIGHT -> rawPodsBattery.upperNibble.toInt() + } + return when (value) { + 15 -> null + else -> if (value > 10) { + log(tag) { "Left pod: Above 100% battery: $value" } + 1.0f + } else { + (value / 10f) + } + } + } + + override val batteryRightPodPercent: Float? + get() { + val value = when (microPhonePod) { + Pod.LEFT -> rawPodsBattery.upperNibble.toInt() + Pod.RIGHT -> rawPodsBattery.lowerNibble.toInt() + } + return when (value) { + 15 -> null + else -> if (value > 10) { + log(tag) { "Right pod: Above 100% battery: $value" } + 1.0f + } else { + value / 10f + } + } + } + + override val batteryCasePercent: Float? + get() = when (val value = rawCaseBattery.lowerNibble.toInt()) { + 15 -> null + else -> if (value > 10) { + log(tag) { "Case: Above 100% battery: $value" } + 1.0f + } else { + value / 10f + } + } + + override val isLeftPodInEar: Boolean + get() = when (microPhonePod) { + Pod.LEFT -> rawStatus.isBitSet(1) + Pod.RIGHT -> rawStatus.isBitSet(3) + } + + override val isRightPodInEar: Boolean + get() = when (microPhonePod) { + Pod.LEFT -> rawStatus.isBitSet(3) + Pod.RIGHT -> rawStatus.isBitSet(1) + } + + val status: Status + get() = Status.values().firstOrNull { it.raw == rawStatus } ?: Status.UNKNOWN + + enum class Status(val raw: UByte?) { + BOTH_AIRPODS_IN_CASE(0x55), + LEFT_IN_EAR(0x33), + AIRPLANE(0x02), + UNKNOWN(null); + + constructor(raw: Int) : this(raw.toUByte()) + } + + // 1010101 0x55 85 Both In Case + // 0101011 0x2b 43 Both In Ear + // 0101001 0x29 41 Right in Ear + // 0100011 0x23 35 Left In Ear + // 0100001 0x21 33 Neither in Ear or In Case + // 1110001 0x71 113 Left in Case, Right on Desk + // 0010001 0x11 17 Left in Case, Right on Desk + // 0010011 0x13 19 Left in Case, Right in Ear + // 1110011 0x73 115 Left in Case, Right in Ear + + + override val isCaseCharging: Boolean + get() = rawCaseBattery.upperNibble.isBitSet(2) + + override val isLeftPodCharging: Boolean + get() = when (microPhonePod) { + Pod.LEFT -> rawCaseBattery.upperNibble.isBitSet(0) + Pod.RIGHT -> rawCaseBattery.upperNibble.isBitSet(1) + } + + override val isRightPodCharging: Boolean + get() = when (microPhonePod) { + Pod.LEFT -> rawCaseBattery.upperNibble.isBitSet(1) + Pod.RIGHT -> rawCaseBattery.upperNibble.isBitSet(0) + } + + val caseLidState: LidState + get() = LidState.values().firstOrNull { it.raw == rawCaseLidState } ?: LidState.UNKNOWN + + enum class LidState(val raw: UByte?) { + OPEN(0x31), + CLOSED(0x38), + NOT_IN_CASE(0x01), + UNKNOWN(null); + + constructor(raw: Int) : this(raw.toUByte()) + } + + val deviceColor: DeviceColor + get() = DeviceColor.values().firstOrNull { it.raw == rawDeviceColor } ?: DeviceColor.UNKNOWN + + + fun getDeviceColorLabel(context: Context): String = context.getString(deviceColor.labelRes) + + enum class DeviceColor(val raw: UByte?, @StringRes val labelRes: Int) { + + WHITE(0x00, R.string.pods_device_color_white_label), + BLACK(0x01, R.string.pods_device_color_black_label), + RED(0x02, R.string.pods_device_color_red_label), + BLUE(0x03, R.string.pods_device_color_blue_label), + PINK(0x04, R.string.pods_device_color_pink_label), + GRAY(0x05, R.string.pods_device_color_gray_label), + SILVER(0x06, R.string.pods_device_color_silver_label), + GOLD(0x07, R.string.pods_device_color_gold_label), + ROSE_GOLD(0x08, R.string.pods_device_color_rose_gold_label), + SPACE_GRAY(0x09, R.string.pods_device_color_space_gray_label), + DARK_BLUE(0x0a, R.string.pods_device_color_dark_blue_label), + LIGHT_BLUE(0x0b, R.string.pods_device_color_light_blue_label), + YELLOW(0x0c, R.string.pods_device_color_yellow_label), + UNKNOWN(null, R.string.general_value_unknown_label); + + constructor(raw: Int, @StringRes labelRes: Int) : this(raw.toUByte(), labelRes) + } + + val connectionState: ConnectionState + get() = ConnectionState.values().firstOrNull { rawSuffix == it.raw } ?: ConnectionState.UNKNOWN + + fun getConnectionStateLabel(context: Context): String = context.getString(connectionState.labelRes) + + enum class ConnectionState(val raw: UByte?, @StringRes val labelRes: Int) { + 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.general_value_unknown_label); + + constructor(raw: Int, @StringRes labelRes: Int) : this(raw.toUByte(), labelRes) + } +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/airpods/AirPodsDevice.kt b/app/src/main/java/eu/darken/capod/pods/core/airpods/SingleApplePods.kt similarity index 89% rename from app/src/main/java/eu/darken/capod/pods/core/airpods/AirPodsDevice.kt rename to app/src/main/java/eu/darken/capod/pods/core/airpods/SingleApplePods.kt index 68c0785f..7aacec38 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/airpods/AirPodsDevice.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/airpods/SingleApplePods.kt @@ -4,8 +4,10 @@ import eu.darken.capod.common.debug.logging.log import eu.darken.capod.common.isBitSet import eu.darken.capod.common.lowerNibble import eu.darken.capod.common.upperNibble +import eu.darken.capod.pods.core.DualPods +import eu.darken.capod.pods.core.DualPods.Pod -interface AirPodsDevice : ApplePods { +interface SingleApplePods : ApplePods, DualPods { val tag: String @@ -34,18 +36,13 @@ interface AirPodsDevice : ApplePods { val rawSuffix: UByte get() = proximityMessage.data[8] - val microPhonePod: Pod + override val microPhonePod: Pod get() = when (rawStatus.isBitSet(5)) { true -> Pod.LEFT false -> Pod.RIGHT } - enum class Pod { - LEFT, - RIGHT - } - - val batteryLeftPodPercent: Float? + override val batteryLeftPodPercent: Float? get() { val value = when (microPhonePod) { Pod.LEFT -> rawPodsBattery.lowerNibble.toInt() @@ -53,7 +50,7 @@ interface AirPodsDevice : ApplePods { } return when (value) { 15 -> null - else -> if (value >= 10) { + else -> if (value > 10) { log(tag) { "Left pod: Above 100% battery: $value" } 1.0f } else { @@ -62,7 +59,7 @@ interface AirPodsDevice : ApplePods { } } - val batteryRightPodPercent: Float? + override val batteryRightPodPercent: Float? get() { val value = when (microPhonePod) { Pod.LEFT -> rawPodsBattery.upperNibble.toInt() @@ -79,7 +76,7 @@ interface AirPodsDevice : ApplePods { } } - val batteryCasePercent: Float? + override val batteryCasePercent: Float? get() = when (val value = rawCaseBattery.lowerNibble.toInt()) { 15 -> null else -> if (value > 10) { @@ -90,13 +87,13 @@ interface AirPodsDevice : ApplePods { } } - val isLeftPodInEar: Boolean + override val isLeftPodInEar: Boolean get() = when (microPhonePod) { Pod.LEFT -> rawStatus.isBitSet(1) Pod.RIGHT -> rawStatus.isBitSet(3) } - val isRightPodInEar: Boolean + override val isRightPodInEar: Boolean get() = when (microPhonePod) { Pod.LEFT -> rawStatus.isBitSet(3) Pod.RIGHT -> rawStatus.isBitSet(1) @@ -125,16 +122,16 @@ interface AirPodsDevice : ApplePods { // 1110011 0x73 115 Left in Case, Right in Ear - val isCaseCharging: Boolean + override val isCaseCharging: Boolean get() = rawCaseBattery.upperNibble.isBitSet(2) - val isLeftPodCharging: Boolean + override val isLeftPodCharging: Boolean get() = when (microPhonePod) { Pod.LEFT -> rawCaseBattery.upperNibble.isBitSet(0) Pod.RIGHT -> rawCaseBattery.upperNibble.isBitSet(1) } - val isRightPodCharging: Boolean + override val isRightPodCharging: Boolean get() = when (microPhonePod) { Pod.LEFT -> rawCaseBattery.upperNibble.isBitSet(1) Pod.RIGHT -> rawCaseBattery.upperNibble.isBitSet(0) diff --git a/app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen1.kt b/app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen1.kt index 504ebfec..698fe8c2 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen1.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen1.kt @@ -1,13 +1,19 @@ package eu.darken.capod.pods.core.airpods.models import android.bluetooth.le.ScanResult +import android.content.Context import eu.darken.capod.common.debug.logging.logTag -import eu.darken.capod.pods.core.airpods.AirPodsDevice -import eu.darken.capod.pods.core.airpods.ProximityPairing +import eu.darken.capod.pods.core.airpods.DualApplePods +import eu.darken.capod.pods.core.airpods.protocol.ProximityPairing data class AirPodsGen1 constructor( override val scanResult: ScanResult, override val proximityMessage: ProximityPairing.Message -) : AirPodsDevice { +) : DualApplePods { + + override fun getLabel(context: Context): String { + return "AirPods (Gen 1)" + } + override val tag: String = logTag("Pod", "AirPodsGen1") } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen2.kt b/app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen2.kt index a2a487fa..d1024a17 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen2.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen2.kt @@ -1,13 +1,19 @@ package eu.darken.capod.pods.core.airpods.models import android.bluetooth.le.ScanResult +import android.content.Context import eu.darken.capod.common.debug.logging.logTag -import eu.darken.capod.pods.core.airpods.AirPodsDevice -import eu.darken.capod.pods.core.airpods.ProximityPairing +import eu.darken.capod.pods.core.airpods.DualApplePods +import eu.darken.capod.pods.core.airpods.protocol.ProximityPairing data class AirPodsGen2 constructor( override val scanResult: ScanResult, override val proximityMessage: ProximityPairing.Message -) : AirPodsDevice { +) : DualApplePods { + + override fun getLabel(context: Context): String { + return "AirPods (Gen 2)" + } + override val tag: String = logTag("Pod", "AirPodsGen2") } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsMax.kt b/app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsMax.kt index 409609af..61a48647 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsMax.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsMax.kt @@ -1,10 +1,17 @@ package eu.darken.capod.pods.core.airpods.models import android.bluetooth.le.ScanResult +import android.content.Context import eu.darken.capod.pods.core.airpods.ApplePods -import eu.darken.capod.pods.core.airpods.ProximityPairing +import eu.darken.capod.pods.core.airpods.protocol.ProximityPairing data class AirPodsMax constructor( override val scanResult: ScanResult, override val proximityMessage: ProximityPairing.Message -) : ApplePods \ No newline at end of file +) : ApplePods { + + override fun getLabel(context: Context): String { + return "AirPods Max" + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsPro.kt b/app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsPro.kt index 46e5b6c0..2b9c1ec7 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsPro.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/airpods/models/AirPodsPro.kt @@ -1,14 +1,19 @@ package eu.darken.capod.pods.core.airpods.models import android.bluetooth.le.ScanResult +import android.content.Context import eu.darken.capod.common.debug.logging.logTag -import eu.darken.capod.pods.core.airpods.AirPodsDevice -import eu.darken.capod.pods.core.airpods.ProximityPairing +import eu.darken.capod.pods.core.airpods.DualApplePods +import eu.darken.capod.pods.core.airpods.protocol.ProximityPairing data class AirPodsPro constructor( override val scanResult: ScanResult, override val proximityMessage: ProximityPairing.Message -) : AirPodsDevice { +) : DualApplePods { + + override fun getLabel(context: Context): String { + return "AirPods Pro" + } override val tag: String = logTag("Pod", "AirPodsPro") } \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/airpods/models/UnknownAppleDevice.kt b/app/src/main/java/eu/darken/capod/pods/core/airpods/models/UnknownAppleDevice.kt index 06cc3f11..cd3e6724 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/airpods/models/UnknownAppleDevice.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/airpods/models/UnknownAppleDevice.kt @@ -1,10 +1,17 @@ package eu.darken.capod.pods.core.airpods.models import android.bluetooth.le.ScanResult +import android.content.Context import eu.darken.capod.pods.core.airpods.ApplePods -import eu.darken.capod.pods.core.airpods.ProximityPairing +import eu.darken.capod.pods.core.airpods.protocol.ProximityPairing data class UnknownAppleDevice constructor( override val scanResult: ScanResult, override val proximityMessage: ProximityPairing.Message -) : ApplePods \ No newline at end of file +) : ApplePods { + + override fun getLabel(context: Context): String { + return "Unknown device" + } + +} \ No newline at end of file diff --git a/app/src/main/java/eu/darken/capod/pods/core/airpods/ContinuityProtocol.kt b/app/src/main/java/eu/darken/capod/pods/core/airpods/protocol/ContinuityProtocol.kt similarity index 97% rename from app/src/main/java/eu/darken/capod/pods/core/airpods/ContinuityProtocol.kt rename to app/src/main/java/eu/darken/capod/pods/core/airpods/protocol/ContinuityProtocol.kt index a3151aec..40167022 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/airpods/ContinuityProtocol.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/airpods/protocol/ContinuityProtocol.kt @@ -1,4 +1,4 @@ -package eu.darken.capod.pods.core.airpods +package eu.darken.capod.pods.core.airpods.protocol import android.bluetooth.le.ScanResult import android.os.ParcelUuid diff --git a/app/src/main/java/eu/darken/capod/pods/core/airpods/ProximityPairing.kt b/app/src/main/java/eu/darken/capod/pods/core/airpods/protocol/ProximityPairing.kt similarity index 94% rename from app/src/main/java/eu/darken/capod/pods/core/airpods/ProximityPairing.kt rename to app/src/main/java/eu/darken/capod/pods/core/airpods/protocol/ProximityPairing.kt index c944f055..bd7e5841 100644 --- a/app/src/main/java/eu/darken/capod/pods/core/airpods/ProximityPairing.kt +++ b/app/src/main/java/eu/darken/capod/pods/core/airpods/protocol/ProximityPairing.kt @@ -1,8 +1,9 @@ -package eu.darken.capod.pods.core.airpods +package eu.darken.capod.pods.core.airpods.protocol import android.bluetooth.le.ScanFilter import dagger.Reusable import eu.darken.capod.common.debug.logging.log +import eu.darken.capod.pods.core.airpods.ApplePods import javax.inject.Inject diff --git a/app/src/main/res/drawable/ic_baseline_apps_24.xml b/app/src/main/res/drawable/ic_baseline_apps_24.xml new file mode 100644 index 00000000..f814c66e --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_apps_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_earbuds_24.xml b/app/src/main/res/drawable/ic_baseline_earbuds_24.xml new file mode 100644 index 00000000..8d729ff2 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_earbuds_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_pod_case_24.xml b/app/src/main/res/drawable/ic_baseline_pod_case_24.xml new file mode 100644 index 00000000..35655ef7 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_pod_case_24.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_baseline_pod_left_24.xml b/app/src/main/res/drawable/ic_baseline_pod_left_24.xml new file mode 100644 index 00000000..6b2642ed --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_pod_left_24.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_baseline_pod_right_24.xml b/app/src/main/res/drawable/ic_baseline_pod_right_24.xml new file mode 100644 index 00000000..48c1a736 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_pod_right_24.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_baseline_signal_cellular_alt_24.xml b/app/src/main/res/drawable/ic_baseline_signal_cellular_alt_24.xml new file mode 100644 index 00000000..f80d2d37 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_signal_cellular_alt_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/main_activity.xml b/app/src/main/res/layout/main_activity.xml index b3775026..b1294687 100644 --- a/app/src/main/res/layout/main_activity.xml +++ b/app/src/main/res/layout/main_activity.xml @@ -9,7 +9,7 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/overview_pods_unknown_item.xml b/app/src/main/res/layout/overview_pods_unknown_item.xml new file mode 100644 index 00000000..b312b2f0 --- /dev/null +++ b/app/src/main/res/layout/overview_pods_unknown_item.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 6d72adaa..a09bb8cb 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -7,7 +7,7 @@ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 225b8535..291dbc0e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,4 +16,37 @@ Recorded log file Record debug log Settings + BLUETOOTH + Allows applications to connect to paired bluetooth devices. + Allows an app to access precise location. + ACCESS_FINE_LOCATION + Left pod + Right pod + Case + Open + Closed + N/A + Raw data + Unknown + Reception %s + White + Yellow + Light blue + Dark blue + Space gray + Rose gold + Gold + Silver + Gray + Pink + Blue + Red + Black + Disconnected + Idle + Music + Call + Ringing + Hanging up + Status %s \ No newline at end of file diff --git a/app/src/test/java/eu/darken/capod/pods/core/airpods/AirPodsFactoryTest.kt b/app/src/test/java/eu/darken/capod/pods/core/airpods/AirPodsFactoryTest.kt index 43917c79..25dd8e8a 100644 --- a/app/src/test/java/eu/darken/capod/pods/core/airpods/AirPodsFactoryTest.kt +++ b/app/src/test/java/eu/darken/capod/pods/core/airpods/AirPodsFactoryTest.kt @@ -3,10 +3,13 @@ package eu.darken.capod.pods.core.airpods import android.bluetooth.BluetoothDevice import android.bluetooth.le.ScanRecord import android.bluetooth.le.ScanResult +import eu.darken.capod.pods.core.DualPods import eu.darken.capod.pods.core.PodDevice import eu.darken.capod.pods.core.airpods.models.AirPodsGen1 import eu.darken.capod.pods.core.airpods.models.AirPodsPro import eu.darken.capod.pods.core.airpods.models.UnknownAppleDevice +import eu.darken.capod.pods.core.airpods.protocol.ContinuityProtocol +import eu.darken.capod.pods.core.airpods.protocol.ProximityPairing import io.kotest.matchers.shouldBe import io.kotest.matchers.types.instanceOf import io.mockk.MockKAnnotations @@ -65,39 +68,39 @@ class AirPodsFactoryTest : BaseTest() { @Test fun `test AirPodDevice - active microphone`() = runBlockingTest { - create("07 19 01 0E 20 >2B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { + create("07 19 01 0E 20 >2B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { // 00101011 // --^----- - microPhonePod shouldBe AirPodsDevice.Pod.LEFT + microPhonePod shouldBe DualPods.Pod.LEFT } - create("07 19 01 0E 20 >0B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { + create("07 19 01 0E 20 >0B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { // 00001011 // --^----- - microPhonePod shouldBe AirPodsDevice.Pod.RIGHT + microPhonePod shouldBe DualPods.Pod.RIGHT } } @Test fun `test AirPodDevice - left pod ear status`() = runBlockingTest { // Left Pod primary - create("07 19 01 0E 20 >22< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { + create("07 19 01 0E 20 >22< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { // 00100010 // 765432¹0 isLeftPodInEar shouldBe true } - create("07 19 01 0E 20 >20< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { + create("07 19 01 0E 20 >20< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { // 00100000 // 765432¹0 isLeftPodInEar shouldBe false } // Right Pod is primary - create("07 19 01 0E 20 >09< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { + create("07 19 01 0E 20 >09< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { // 00001001 // 7654³210 isLeftPodInEar shouldBe true } - create("07 19 01 0E 20 >20< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { + create("07 19 01 0E 20 >20< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { // 00000001 // 7654³210 isLeftPodInEar shouldBe false @@ -107,24 +110,24 @@ class AirPodsFactoryTest : BaseTest() { @Test fun `test AirPodDevice - right pod ear status`() = runBlockingTest { // Left Pod primary - create("07 19 01 0E 20 >29< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { + create("07 19 01 0E 20 >29< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { // 00101001 // 7654³210 isRightPodInEar shouldBe true } - create("07 19 01 0E 20 >21< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { + create("07 19 01 0E 20 >21< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { // 00100001 // 7654³210 isRightPodInEar shouldBe false } // Right Pod is primary - create("07 19 01 0E 20 >03< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { + create("07 19 01 0E 20 >03< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { // 00000011 // 765432¹0 isRightPodInEar shouldBe true } - create("07 19 01 0E 20 >01< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { + create("07 19 01 0E 20 >01< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") { // 00000001 // 765432¹0 isRightPodInEar shouldBe false @@ -134,13 +137,13 @@ class AirPodsFactoryTest : BaseTest() { @Test fun `test AirPodDevice - battery status`() = runBlockingTest { // Right Pod is primary - create("07 19 01 0E 20 0B >98< 94 52 00 05 09 73 3C 3D F9 2C 3E B3 DD 76 02 DD 4E 16 FD FB") { + create("07 19 01 0E 20 0B >98< 94 52 00 05 09 73 3C 3D F9 2C 3E B3 DD 76 02 DD 4E 16 FD FB") { // 88 10001000 batteryLeftPodPercent shouldBe 0.9f batteryRightPodPercent shouldBe 0.8f } // Left Pod primary - create("07 19 01 0E 20 2B >89< 94 52 00 05 09 73 3C 3D F9 2C 3E B3 DD 76 02 DD 4E 16 FD FB") { + create("07 19 01 0E 20 2B >89< 94 52 00 05 09 73 3C 3D F9 2C 3E B3 DD 76 02 DD 4E 16 FD FB") { // F8 11111000 batteryLeftPodPercent shouldBe 0.9f batteryRightPodPercent shouldBe 0.8f @@ -153,13 +156,13 @@ class AirPodsFactoryTest : BaseTest() { * Right pod is charging */ // This is the left - create("07 19 01 0E 20 51 89 >94< 52 00 00 F4 89 82 6D 3E 27 7F 26 62 57 D0 E2 A6 49 E9 35") { + create("07 19 01 0E 20 51 89 >94< 52 00 00 F4 89 82 6D 3E 27 7F 26 62 57 D0 E2 A6 49 E9 35") { // 1001 0100 isLeftPodCharging shouldBe false isRightPodCharging shouldBe true } // This is the right - create("07 19 01 0E 20 31 98 >A4< 01 00 00 31 B9 A0 C4 80 CD D1 CF B9 3A 9A 6D 48 31 08 EB") { + create("07 19 01 0E 20 31 98 >A4< 01 00 00 31 B9 A0 C4 80 CD D1 CF B9 3A 9A 6D 48 31 08 EB") { // 1010 0100 isLeftPodCharging shouldBe false isRightPodCharging shouldBe true @@ -169,26 +172,26 @@ class AirPodsFactoryTest : BaseTest() { * Left pod is charging */ // This is the left - create("07 19 01 0E 20 71 98 >94< 52 00 05 A5 37 31 B2 BD 42 68 0C 64 FD 00 99 4A E5 3E F4") { + create("07 19 01 0E 20 71 98 >94< 52 00 05 A5 37 31 B2 BD 42 68 0C 64 FD 00 99 4A E5 3E F4") { // 1001 0100 isLeftPodCharging shouldBe true isRightPodCharging shouldBe false } // This is the right - create("07 19 01 0E 20 11 89 >A4< 04 00 04 BA 79 1B C0 65 69 C6 9F 19 6E 37 7D 6D 86 8D D9") { + create("07 19 01 0E 20 11 89 >A4< 04 00 04 BA 79 1B C0 65 69 C6 9F 19 6E 37 7D 6D 86 8D D9") { // 1010 0100 isLeftPodCharging shouldBe true isRightPodCharging shouldBe false } // Both charging - create("07 19 01 0E 20 55 88 >B4< 59 00 05 4B FC DF 68 28 A5 45 52 65 9C FE 51 86 3A B5 DB") { + create("07 19 01 0E 20 55 88 >B4< 59 00 05 4B FC DF 68 28 A5 45 52 65 9C FE 51 86 3A B5 DB") { // 1011 0100 isLeftPodCharging shouldBe true isRightPodCharging shouldBe true } // Both not charging - create("07 19 01 0E 20 00 F8 >8F< 03 00 05 4C 0F A0 C4 05 24 DD EB AF 92 99 FD 54 B1 06 48") { + create("07 19 01 0E 20 00 F8 >8F< 03 00 05 4C 0F A0 C4 05 24 DD EB AF 92 99 FD 54 B1 06 48") { // 1000 1111 isLeftPodCharging shouldBe false isRightPodCharging shouldBe false @@ -197,11 +200,11 @@ class AirPodsFactoryTest : BaseTest() { @Test fun `test AirPodDevice - case charging`() = runBlockingTest { - create("07 19 01 0E 20 75 99 >B4< 31 00 05 77 C8 BA 0C 4E 1F BE AD 70 C5 40 71 D2 E9 17 A2") { + create("07 19 01 0E 20 75 99 >B4< 31 00 05 77 C8 BA 0C 4E 1F BE AD 70 C5 40 71 D2 E9 17 A2") { // 0011 0011 isCaseCharging shouldBe false } - create("07 19 01 0E 20 75 A9 >F4< 51 00 05 A0 37 92 35 49 79 CC DC 27 94 8E FB 72 12 94 52") { + create("07 19 01 0E 20 75 A9 >F4< 51 00 05 A0 37 92 35 49 79 CC DC 27 94 8E FB 72 12 94 52") { // 0101 0011 isCaseCharging shouldBe true } @@ -210,66 +213,66 @@ class AirPodsFactoryTest : BaseTest() { @Test fun `test AirPodDevice - case lid test`() = runBlockingTest { // Lid open - create("07 19 01 0E 20 55 AA B4 >31< 00 00 A1 D0 BD 82 D3 52 86 CA FC 11 62 DC 42 C6 92 8E") { + create("07 19 01 0E 20 55 AA B4 >31< 00 00 A1 D0 BD 82 D3 52 86 CA FC 11 62 DC 42 C6 92 8E") { // 31 0011 0001 - caseLidState shouldBe AirPodsDevice.LidState.OPEN + caseLidState shouldBe DualApplePods.LidState.OPEN } // Lid just closed - create("07 19 01 0E 20 55 AA B4 >39< 00 00 08 A6 DB 99 E0 5E 14 85 E5 C2 0B 68 D7 FF C3 A1") { + create("07 19 01 0E 20 55 AA B4 >39< 00 00 08 A6 DB 99 E0 5E 14 85 E5 C2 0B 68 D7 FF C3 A1") { // 39 0011 1001 - caseLidState shouldBe AirPodsDevice.LidState.UNKNOWN + caseLidState shouldBe DualApplePods.LidState.UNKNOWN } // Lid closed - create("07 19 01 0E 20 55 AA B4 38 00 00 F3 F7 08 3B 98 09 C0 DD E4 BD BD 84 55 56 8B 81") { + create("07 19 01 0E 20 55 AA B4 38 00 00 F3 F7 08 3B 98 09 C0 DD E4 BD BD 84 55 56 8B 81") { // 38 0011 1000 - caseLidState shouldBe AirPodsDevice.LidState.CLOSED + caseLidState shouldBe DualApplePods.LidState.CLOSED } } @Test fun `test AirPodDevice - connection state`() = runBlockingTest { // Disconnected - create("07 19 01 0E 20 2B AA 8F 01 00 >00< 62 D4 BB F1 A7 F8 64 98 D2 C8 BD 7B 3A EF 2E 15") { + create("07 19 01 0E 20 2B AA 8F 01 00 >00< 62 D4 BB F1 A7 F8 64 98 D2 C8 BD 7B 3A EF 2E 15") { // 31 0011 0001 - connectionState shouldBe AirPodsDevice.ConnectionState.DISCONNECTED + connectionState shouldBe DualApplePods.ConnectionState.DISCONNECTED } // Connected idle - create("07 19 01 0E 20 2B AA 8F 01 00 >04< 1D 69 69 9C C2 51 F3 1F BF 6E 45 DA 90 4A A3 E3") { + create("07 19 01 0E 20 2B AA 8F 01 00 >04< 1D 69 69 9C C2 51 F3 1F BF 6E 45 DA 90 4A A3 E3") { // 39 0011 1001 - connectionState shouldBe AirPodsDevice.ConnectionState.IDLE + connectionState shouldBe DualApplePods.ConnectionState.IDLE } // Connected and playing music - create("07 19 01 0E 20 2B A9 8F 01 00 >05< 14 F7 CB 49 9F D3 B3 22 77 D2 22 F1 74 8C AC A6") { + create("07 19 01 0E 20 2B A9 8F 01 00 >05< 14 F7 CB 49 9F D3 B3 22 77 D2 22 F1 74 8C AC A6") { // 38 0011 1000 - connectionState shouldBe AirPodsDevice.ConnectionState.MUSIC + connectionState shouldBe DualApplePods.ConnectionState.MUSIC } // Connected and call active - create("07 19 01 0E 20 2B 99 8F 01 00 >06< 0F 4B 43 25 E0 4A 73 63 14 22 C2 3C 89 13 BD 97") { + create("07 19 01 0E 20 2B 99 8F 01 00 >06< 0F 4B 43 25 E0 4A 73 63 14 22 C2 3C 89 13 BD 97") { // 38 0011 1000 - connectionState shouldBe AirPodsDevice.ConnectionState.CALL + connectionState shouldBe DualApplePods.ConnectionState.CALL } // Connected and call active - create("07 19 01 0E 20 2B 99 8F 01 00 >07< E7 DF 76 44 85 B5 30 F4 95 14 02 DC A1 A4 8A 09") { + create("07 19 01 0E 20 2B 99 8F 01 00 >07< E7 DF 76 44 85 B5 30 F4 95 14 02 DC A1 A4 8A 09") { // 38 0011 1000 - connectionState shouldBe AirPodsDevice.ConnectionState.RINGING + connectionState shouldBe DualApplePods.ConnectionState.RINGING } // Switching? - create("07 19 01 0E 20 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") { + create("07 19 01 0E 20 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") { // 38 0011 1000 - connectionState shouldBe AirPodsDevice.ConnectionState.HANGING_UP + connectionState shouldBe DualApplePods.ConnectionState.HANGING_UP } } @Test fun `create AirPodsGen1`() = runBlockingTest { - create("07 19 01 02 20 55 AA 56 31 00 00 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88") { + create("07 19 01 02 20 55 AA 56 31 00 00 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88") { this shouldBe instanceOf() } } @Test fun `create AirPodsPro`() = runBlockingTest { - create("07 19 01 0E 20 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") { + create("07 19 01 0E 20 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") { this shouldBe instanceOf() } } diff --git a/app/src/test/java/eu/darken/capod/pods/core/airpods/BaseAirPodsTest.kt b/app/src/test/java/eu/darken/capod/pods/core/airpods/BaseAirPodsTest.kt index 88cecc78..0bc8ac0d 100644 --- a/app/src/test/java/eu/darken/capod/pods/core/airpods/BaseAirPodsTest.kt +++ b/app/src/test/java/eu/darken/capod/pods/core/airpods/BaseAirPodsTest.kt @@ -4,6 +4,8 @@ import android.bluetooth.BluetoothDevice import android.bluetooth.le.ScanRecord import android.bluetooth.le.ScanResult import eu.darken.capod.pods.core.PodDevice +import eu.darken.capod.pods.core.airpods.protocol.ContinuityProtocol +import eu.darken.capod.pods.core.airpods.protocol.ProximityPairing import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.impl.annotations.MockK diff --git a/app/src/test/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen1Test.kt b/app/src/test/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen1Test.kt index b8e1f98e..f505e71b 100644 --- a/app/src/test/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen1Test.kt +++ b/app/src/test/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen1Test.kt @@ -1,7 +1,7 @@ package eu.darken.capod.pods.core.airpods.models -import eu.darken.capod.pods.core.airpods.AirPodsDevice import eu.darken.capod.pods.core.airpods.BaseAirPodsTest +import eu.darken.capod.pods.core.airpods.DualApplePods import io.kotest.matchers.shouldBe import kotlinx.coroutines.test.runBlockingTest import org.junit.jupiter.api.Test @@ -32,11 +32,11 @@ class AirPodsGen1Test : BaseAirPodsTest() { isRightPodInEar shouldBe false batteryCasePercent shouldBe 0.6f - caseLidState shouldBe AirPodsDevice.LidState.OPEN + caseLidState shouldBe DualApplePods.LidState.OPEN - connectionState shouldBe AirPodsDevice.ConnectionState.DISCONNECTED + connectionState shouldBe DualApplePods.ConnectionState.DISCONNECTED - deviceColor shouldBe AirPodsDevice.DeviceColor.WHITE + deviceColor shouldBe DualApplePods.DeviceColor.WHITE } } } \ No newline at end of file diff --git a/app/src/test/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen2Test.kt b/app/src/test/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen2Test.kt index e632db4d..7c32a749 100644 --- a/app/src/test/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen2Test.kt +++ b/app/src/test/java/eu/darken/capod/pods/core/airpods/models/AirPodsGen2Test.kt @@ -1,7 +1,7 @@ package eu.darken.capod.pods.core.airpods.models -import eu.darken.capod.pods.core.airpods.AirPodsDevice import eu.darken.capod.pods.core.airpods.BaseAirPodsTest +import eu.darken.capod.pods.core.airpods.DualApplePods import io.kotest.matchers.shouldBe import kotlinx.coroutines.test.runBlockingTest import org.junit.jupiter.api.Test @@ -31,11 +31,11 @@ class AirPodsGen2Test : BaseAirPodsTest() { isRightPodInEar shouldBe true batteryCasePercent shouldBe null - caseLidState shouldBe AirPodsDevice.LidState.NOT_IN_CASE + caseLidState shouldBe DualApplePods.LidState.NOT_IN_CASE - connectionState shouldBe AirPodsDevice.ConnectionState.MUSIC + connectionState shouldBe DualApplePods.ConnectionState.MUSIC - deviceColor shouldBe AirPodsDevice.DeviceColor.WHITE + deviceColor shouldBe DualApplePods.DeviceColor.WHITE } } } \ No newline at end of file diff --git a/app/src/test/java/eu/darken/capod/pods/core/airpods/models/AirPodsProTest.kt b/app/src/test/java/eu/darken/capod/pods/core/airpods/models/AirPodsProTest.kt index 735b8ed5..1a93d449 100644 --- a/app/src/test/java/eu/darken/capod/pods/core/airpods/models/AirPodsProTest.kt +++ b/app/src/test/java/eu/darken/capod/pods/core/airpods/models/AirPodsProTest.kt @@ -1,7 +1,8 @@ package eu.darken.capod.pods.core.airpods.models -import eu.darken.capod.pods.core.airpods.AirPodsDevice +import eu.darken.capod.pods.core.DualPods import eu.darken.capod.pods.core.airpods.BaseAirPodsTest +import eu.darken.capod.pods.core.airpods.DualApplePods import io.kotest.matchers.shouldBe import kotlinx.coroutines.test.runBlockingTest import org.junit.jupiter.api.Test @@ -21,7 +22,7 @@ class AirPodsProTest : BaseAirPodsTest() { rawDeviceColor shouldBe 0x00.toUByte() rawSuffix shouldBe 0x00.toUByte() - microPhonePod shouldBe AirPodsDevice.Pod.RIGHT + microPhonePod shouldBe DualPods.Pod.RIGHT batteryLeftPodPercent shouldBe 1.0f batteryRightPodPercent shouldBe 1.0f @@ -31,14 +32,14 @@ class AirPodsProTest : BaseAirPodsTest() { isLeftPodCharging shouldBe true batteryCasePercent shouldBe 0.5f - deviceColor shouldBe AirPodsDevice.DeviceColor.WHITE + deviceColor shouldBe DualApplePods.DeviceColor.WHITE } } @Test fun `test AirPods from my downstairs neighbour`() = runBlockingTest { create("07 19 01 0E 20 00 F3 8F 02 00 04 79 C6 3F F9 C3 15 D9 11 A1 3C B1 58 66 B9 8B 67") { - microPhonePod shouldBe AirPodsDevice.Pod.RIGHT + microPhonePod shouldBe DualPods.Pod.RIGHT batteryLeftPodPercent shouldBe null batteryRightPodPercent shouldBe 0.3f @@ -48,7 +49,7 @@ class AirPodsProTest : BaseAirPodsTest() { isLeftPodCharging shouldBe false batteryCasePercent shouldBe null - deviceColor shouldBe AirPodsDevice.DeviceColor.WHITE + deviceColor shouldBe DualApplePods.DeviceColor.WHITE } } @@ -64,7 +65,7 @@ class AirPodsProTest : BaseAirPodsTest() { isLeftPodCharging shouldBe false batteryCasePercent shouldBe null - deviceColor shouldBe AirPodsDevice.DeviceColor.WHITE + deviceColor shouldBe DualApplePods.DeviceColor.WHITE } create("0719010e202b668f01000500000000000000000000000000000000") { @@ -76,7 +77,7 @@ class AirPodsProTest : BaseAirPodsTest() { isLeftPodCharging shouldBe false batteryCasePercent shouldBe null - deviceColor shouldBe AirPodsDevice.DeviceColor.WHITE + deviceColor shouldBe DualApplePods.DeviceColor.WHITE } create("0719010e202b668f01000400000000000000000000000000000000") { @@ -88,7 +89,7 @@ class AirPodsProTest : BaseAirPodsTest() { isLeftPodCharging shouldBe false batteryCasePercent shouldBe null - deviceColor shouldBe AirPodsDevice.DeviceColor.WHITE + deviceColor shouldBe DualApplePods.DeviceColor.WHITE } create("0719010e200b668f01000500000000000000000000000000000000") { @@ -100,7 +101,7 @@ class AirPodsProTest : BaseAirPodsTest() { isLeftPodCharging shouldBe false batteryCasePercent shouldBe null - deviceColor shouldBe AirPodsDevice.DeviceColor.WHITE + deviceColor shouldBe DualApplePods.DeviceColor.WHITE } create("0719010e2003668f01000500000000000000000000000000000000") { @@ -112,7 +113,7 @@ class AirPodsProTest : BaseAirPodsTest() { isLeftPodCharging shouldBe false batteryCasePercent shouldBe null - deviceColor shouldBe AirPodsDevice.DeviceColor.WHITE + deviceColor shouldBe DualApplePods.DeviceColor.WHITE } create("0719010e2001668f01000500000000000000000000000000000000") { @@ -124,7 +125,7 @@ class AirPodsProTest : BaseAirPodsTest() { isLeftPodCharging shouldBe false batteryCasePercent shouldBe null - deviceColor shouldBe AirPodsDevice.DeviceColor.WHITE + deviceColor shouldBe DualApplePods.DeviceColor.WHITE } create("0719010e2009668f01000500000000000000000000000000000000") { @@ -136,7 +137,7 @@ class AirPodsProTest : BaseAirPodsTest() { isLeftPodCharging shouldBe false batteryCasePercent shouldBe null - deviceColor shouldBe AirPodsDevice.DeviceColor.WHITE + deviceColor shouldBe DualApplePods.DeviceColor.WHITE } create("0719010e2053669653000500000000000000000000000000000000") { @@ -148,7 +149,7 @@ class AirPodsProTest : BaseAirPodsTest() { isLeftPodCharging shouldBe false batteryCasePercent shouldBe 0.6f - deviceColor shouldBe AirPodsDevice.DeviceColor.WHITE + deviceColor shouldBe DualApplePods.DeviceColor.WHITE } create("0719010e203366a602000500000000000000000000000000000000") { @@ -160,7 +161,7 @@ class AirPodsProTest : BaseAirPodsTest() { isLeftPodCharging shouldBe false batteryCasePercent shouldBe 0.6f - deviceColor shouldBe AirPodsDevice.DeviceColor.WHITE + deviceColor shouldBe DualApplePods.DeviceColor.WHITE } create("0719010e202b768f02000500000000000000000000000000000000") { @@ -172,7 +173,7 @@ class AirPodsProTest : BaseAirPodsTest() { isLeftPodCharging shouldBe false batteryCasePercent shouldBe null - deviceColor shouldBe AirPodsDevice.DeviceColor.WHITE + deviceColor shouldBe DualApplePods.DeviceColor.WHITE } } @@ -188,7 +189,7 @@ class AirPodsProTest : BaseAirPodsTest() { isLeftPodCharging shouldBe false batteryCasePercent shouldBe 0.6f - deviceColor shouldBe AirPodsDevice.DeviceColor.WHITE + deviceColor shouldBe DualApplePods.DeviceColor.WHITE } } } \ No newline at end of file