Pod status cards, mvp

This commit is contained in:
darken
2021-12-31 17:35:08 +01:00
parent deb513f998
commit 022e0c7405
55 changed files with 1132 additions and 333 deletions
+2 -11
View File
@@ -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.
* Google Play screenshots.
* Google Play texts & descriptions.
+9 -3
View File
@@ -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") {
+32 -10
View File
@@ -3,15 +3,33 @@
xmlns:tools="http://schemas.android.com/tools"
package="eu.darken.capod">
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<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.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission
android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<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.ACCESS_BACKGROUND_LOCATION" />-->
<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="true" />
<application
android:name="eu.darken.capod.App"
android:name=".App"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
@@ -20,7 +38,7 @@
android:theme="@style/AppTheme">
<activity
android:name="eu.darken.capod.main.ui.MainActivity"
android:name=".main.ui.MainActivity"
android:exported="true"
android:label="@string/app_name">
<intent-filter>
@@ -31,7 +49,7 @@
</activity>
<receiver
android:name="eu.darken.capod.monitor.core.receiver.BluetoothEventReceiver"
android:name=".monitor.core.receiver.BluetoothEventReceiver"
android:enabled="true"
android:exported="true"
android:label="Service trigger">
@@ -55,10 +73,10 @@
<!-- Debug stuff-->
<activity
android:name="eu.darken.capod.common.debug.recording.ui.RecorderActivity"
android:name=".common.debug.recording.ui.RecorderActivity"
android:theme="@style/AppThemeFloating" />
<service android:name="eu.darken.capod.common.debug.recording.core.RecorderService" />
<service android:name=".common.debug.recording.core.RecorderService" />
<!-- Worker stuff-->
<service
@@ -70,6 +88,10 @@
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove" />
<meta-data
android:name="com.bugsnag.android.API_KEY"
android:value="${bugsnagApiKey}" />
</application>
</manifest>
+3 -3
View File
@@ -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()}" }
@@ -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
@@ -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
@@ -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." }
@@ -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<BugsnagLogger>,
private val bugsnagErrorHandler: Provider<BugsnagErrorHandler>,
@@ -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")
}
}
@@ -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)
}
@@ -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 {
@@ -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
@@ -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
@@ -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
}
@@ -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<Boolean> = flow {
emit(false)
}
private val permissionCheckTrigger = MutableStateFlow(UUID.randomUUID())
private val requiredPermissions: Flow<List<Permission>> = permissionCheckTrigger.map {
Permission.values()
.filter { hasApiLevel(it.minApiLevel) && !it.isGranted(context) }
}
val requestPermissionevent = SingleLiveEvent<Permission>()
val listItems: LiveData<List<MainAdapter.Item>> = combine(
enabledState,
requiredPermissions
) { state, permissions ->
val items = mutableListOf<MainAdapter.Item>()
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 {
}
}
@@ -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<ToggleCardVH.Item, MainToggleItemBinding>(
R.layout.main_toggle_item,
parent
) {
override val viewBinding = lazy {
MainToggleItemBinding.bind(itemView)
}
override val onBindData: MainToggleItemBinding.(
item: Item,
payloads: List<Any>
) -> 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()
}
}
@@ -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<MainAdapter.BaseVH<MainAdapter.Item, ViewBinding>>(),
HasAsyncDiffer<MainAdapter.Item> {
class OverviewAdapter @Inject constructor() :
ModularAdapter<OverviewAdapter.BaseVH<OverviewAdapter.Item, ViewBinding>>(),
HasAsyncDiffer<OverviewAdapter.Item> {
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
@@ -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<String>
@@ -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 {
@@ -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<Boolean> = flow {
emit(false)
}
private val permissionCheckTrigger = MutableStateFlow(UUID.randomUUID())
private val requiredPermissions: Flow<List<Permission>> = permissionCheckTrigger
.map {
Permission.values().filter { !it.isGrantedOrNotRequired(context) }
}
.onEach {
if (it.isEmpty()) {
monitorControl.startMonitor()
}
}
val requestPermissionevent = SingleLiveEvent<Permission>()
private val pods: Flow<List<PodDevice>> = requiredPermissions
.flatMapLatest {
if (it.isEmpty()) {
podMonitor.pods
} else {
channelFlow {
send(emptyList())
awaitClose()
}
}
}
val listItems: LiveData<List<OverviewAdapter.Item>> = combine(
enabledState,
requiredPermissions,
pods
) { state, permissions, pods ->
val items = mutableListOf<OverviewAdapter.Item>()
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 {
}
}
@@ -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<DualApplePodsCardVH.Item, OverviewPodsAppleDualItemBinding>(
R.layout.overview_pods_apple_dual_item,
parent
) {
override val viewBinding = lazy {
OverviewPodsAppleDualItemBinding.bind(itemView)
}
override val onBindData: OverviewPodsAppleDualItemBinding.(
item: Item,
payloads: List<Any>
) -> 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()
}
}
@@ -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<PermissionCardVH.Item, MainPermissionItemBinding>(
OverviewAdapter.BaseVH<PermissionCardVH.Item, MainPermissionItemBinding>(
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()
}
}
@@ -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<UnknownPodDeviceCardVH.Item, OverviewPodsUnknownItemBinding>(
R.layout.overview_pods_unknown_item,
parent
) {
override val viewBinding = lazy {
OverviewPodsUnknownItemBinding.bind(itemView)
}
override val onBindData: OverviewPodsUnknownItemBinding.(
item: Item,
payloads: List<Any>
) -> Unit = { item, _ ->
name.text = item.device.getLabel(context)
}
data class Item(
val device: PodDevice
) : OverviewAdapter.Item {
override val stableId: Long = this.javaClass.hashCode().toLong()
}
}
@@ -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")
}
}
@@ -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
@@ -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 {
@@ -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<Unit> {
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<Unit> {
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 {
@@ -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
}
@@ -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)
@@ -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
}
@@ -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
@@ -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")
}
}
@@ -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)
}
}
@@ -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)
@@ -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")
}
@@ -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")
}
@@ -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
) : ApplePods {
override fun getLabel(context: Context): String {
return "AirPods Max"
}
}
@@ -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")
}
@@ -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
) : ApplePods {
override fun getLabel(context: Context): String {
return "Unknown device"
}
}
@@ -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
@@ -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
@@ -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="M4,8h4L8,4L4,4v4zM10,20h4v-4h-4v4zM4,20h4v-4L4,16v4zM4,14h4v-4L4,10v4zM10,14h4v-4h-4v4zM16,4v4h4L20,4h-4zM10,8h4L14,4h-4v4zM16,14h4v-4h-4v4zM16,20h4v-4h-4v4z" />
</vector>
@@ -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.2,3.01C4.44,2.89 3,4.42 3,6.19L3,16c0,2.76 2.24,5 5,5h0c2.76,0 5,-2.24 5,-5V8c0,-1.66 1.34,-3 3,-3h0c1.66,0 3,1.34 3,3v7l-0.83,0c-1.61,0 -3.06,1.18 -3.17,2.79c-0.12,1.69 1.16,3.1 2.8,3.21c1.76,0.12 3.2,-1.42 3.2,-3.18L21,8c0,-2.76 -2.24,-5 -5,-5h0c-2.76,0 -5,2.24 -5,5v8c0,1.66 -1.34,3 -3,3l0,0c-1.66,0 -3,-1.34 -3,-3V9l0.83,0C7.44,9 8.89,7.82 9,6.21C9.11,4.53 7.83,3.11 6.2,3.01z" />
</vector>
@@ -0,0 +1,16 @@
<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="M9,12a3,5.74 0,1 0,6 0a3,5.74 0,1 0,-6 0z" />
<path
android:fillColor="@android:color/white"
android:pathData="M9.04,16.87C8.71,16.95 8.36,17 8,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5c0.36,0 0.71,0.05 1.04,0.13c0.39,-0.56 0.88,-1.12 1.49,-1.63C9.75,5.19 8.9,5 8,5c-3.86,0 -7,3.14 -7,7s3.14,7 7,7c0.9,0 1.75,-0.19 2.53,-0.5C9.92,17.99 9.43,17.43 9.04,16.87z" />
<path
android:fillColor="@android:color/white"
android:pathData="M16,5c-0.9,0 -1.75,0.19 -2.53,0.5c0.61,0.51 1.1,1.07 1.49,1.63C15.29,7.05 15.64,7 16,7c2.76,0 5,2.24 5,5s-2.24,5 -5,5c-0.36,0 -0.71,-0.05 -1.04,-0.13c-0.39,0.56 -0.88,1.12 -1.49,1.63C14.25,18.81 15.1,19 16,19c3.86,0 7,-3.14 7,-7S19.86,5 16,5z" />
</vector>
@@ -0,0 +1,16 @@
<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="M9,12a3,5.74 0,1 0,6 0a3,5.74 0,1 0,-6 0z" />
<path
android:fillColor="@android:color/white"
android:pathData="M7.5,12c0,-0.97 0.23,-4.16 3.03,-6.5C9.75,5.19 8.9,5 8,5c-3.86,0 -7,3.14 -7,7s3.14,7 7,7c0.9,0 1.75,-0.19 2.53,-0.5C7.73,16.16 7.5,12.97 7.5,12z" />
<path
android:fillColor="@android:color/white"
android:pathData="M16,5c-0.9,0 -1.75,0.19 -2.53,0.5c0.61,0.51 1.1,1.07 1.49,1.63C15.29,7.05 15.64,7 16,7c2.76,0 5,2.24 5,5s-2.24,5 -5,5c-0.36,0 -0.71,-0.05 -1.04,-0.13c-0.39,0.56 -0.88,1.12 -1.49,1.63C14.25,18.81 15.1,19 16,19c3.86,0 7,-3.14 7,-7S19.86,5 16,5z" />
</vector>
@@ -0,0 +1,16 @@
<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="M9,12a3,5.74 0,1 0,6 0a3,5.74 0,1 0,-6 0z" />
<path
android:fillColor="@android:color/white"
android:pathData="M16.5,12c0,0.97 -0.23,4.16 -3.03,6.5C14.25,18.81 15.1,19 16,19c3.86,0 7,-3.14 7,-7s-3.14,-7 -7,-7c-0.9,0 -1.75,0.19 -2.53,0.5C16.27,7.84 16.5,11.03 16.5,12z" />
<path
android:fillColor="@android:color/white"
android:pathData="M8,19c0.9,0 1.75,-0.19 2.53,-0.5c-0.61,-0.51 -1.1,-1.07 -1.49,-1.63C8.71,16.95 8.36,17 8,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5c0.36,0 0.71,0.05 1.04,0.13c0.39,-0.56 0.88,-1.12 1.49,-1.63C9.75,5.19 8.9,5 8,5c-3.86,0 -7,3.14 -7,7S4.14,19 8,19z" />
</vector>
@@ -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="M17,4h3v16h-3zM5,14h3v6L5,20zM11,9h3v11h-3z" />
</vector>
+1 -1
View File
@@ -9,7 +9,7 @@
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host"
android:name="eu.darken.capod.main.ui.MainFragment"
android:name="eu.darken.capod.main.ui.overview.OverviewFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
@@ -0,0 +1,132 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/card"
style="@style/MyCardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginVertical="8dp"
tools:context=".main.ui.MainActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/card_content"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/name"
style="@style/TextAppearance.MaterialComponents.Body2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:gravity="center_vertical"
app:drawableLeftCompat="@drawable/ic_baseline_earbuds_24"
app:layout_constraintBottom_toTopOf="@id/barrier_top"
app:layout_constraintEnd_toStartOf="@id/reception"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0"
tools:text="Apple AirPods Pro" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/reception"
style="@style/TextAppearance.MaterialComponents.Body2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:gravity="center"
app:drawableRightCompat="@drawable/ic_baseline_signal_cellular_alt_24"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/name"
app:layout_constraintTop_toTopOf="parent"
tools:text="Yours (RSSI -61)" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="top"
app:constraint_referenced_ids="pod_left,pod_case,pod_right" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/pod_left"
style="@style/TextAppearance.MaterialComponents.Body2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:gravity="center_horizontal"
app:drawableTopCompat="@drawable/ic_baseline_pod_left_24"
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
app:layout_constraintEnd_toStartOf="@id/pod_case"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier_top"
app:layout_constraintVertical_bias="0.0"
tools:text="Left pod\n100%\n(Primary)" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/pod_case"
style="@style/TextAppearance.MaterialComponents.Body2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:gravity="center_horizontal"
app:drawableTopCompat="@drawable/ic_baseline_pod_case_24"
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
app:layout_constraintEnd_toStartOf="@id/pod_right"
app:layout_constraintStart_toEndOf="@id/pod_left"
app:layout_constraintTop_toBottomOf="@id/barrier_top"
app:layout_constraintVertical_bias="0.0"
tools:text="Case\n50%" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/pod_right"
style="@style/TextAppearance.MaterialComponents.Body2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="16dp"
android:gravity="center_horizontal"
app:drawableTopCompat="@drawable/ic_baseline_pod_right_24"
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/pod_case"
app:layout_constraintTop_toBottomOf="@id/barrier_top"
app:layout_constraintVertical_bias="0.0"
tools:text="R 90%" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_bottom"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="pod_left,pod_case,pod_right" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/status"
style="@style/TextAppearance.MaterialComponents.Body2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:gravity="center_vertical"
app:drawableLeftCompat="@drawable/ic_baseline_apps_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier_bottom"
tools:text="Music Active, Case Closed" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/card"
style="@style/MyCardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:context=".main.ui.MainActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/card_content"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/name"
style="@style/TextAppearance.MaterialComponents.Body2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintEnd_toStartOf="@id/reception"
app:layout_constraintHorizontal_bias="0.49"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Unknown Device" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/reception"
style="@style/TextAppearance.MaterialComponents.Body2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginStart="8dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/name"
app:layout_constraintTop_toTopOf="parent"
tools:text="RSSI -100 dbM" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/rawdata_label"
style="@style/TextAppearance.MaterialComponents.Caption"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:text="@string/pods_unknown_raw_data_label"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/name" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/rawdata"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/rawdata_label"
tools:text="07 19 01 0E 20 75 AA B2 31 00 00 2A 46 AF 17 F7 59 C2 71 B8 CA 37 5D 72 5C 15 68" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
+1 -1
View File
@@ -7,7 +7,7 @@
<fragment
android:id="@+id/overviewFragment"
android:name="eu.darken.capod.main.ui.MainFragment"
android:name="eu.darken.capod.main.ui.overview.OverviewFragment"
tools:layout="@layout/main_fragment">
</fragment>
+33
View File
@@ -16,4 +16,37 @@
<string name="debug_debuglog_file_label">Recorded log file</string>
<string name="debug_debuglog_record_action">Record debug log</string>
<string name="general_settings_label">Settings</string>
<string name="permission_bluetooth_label">BLUETOOTH</string>
<string name="permission_bluetooth_description">Allows applications to connect to paired bluetooth devices.</string>
<string name="permission_access_fine_location_description">Allows an app to access precise location.</string>
<string name="permission_access_fine_location_label">ACCESS_FINE_LOCATION</string>
<string name="pods_dual_left_label">Left pod</string>
<string name="pods_dual_right_label">Right pod</string>
<string name="pods_case_status_label">Case</string>
<string name="pods_case_status_open_label">Open</string>
<string name="pods_case_status_closed_label">Closed</string>
<string name="general_value_not_available_label">N/A</string>
<string name="pods_unknown_raw_data_label">Raw data</string>
<string name="general_value_unknown_label">Unknown</string>
<string name="pods_reception_x_label">Reception %s</string>
<string name="pods_device_color_white_label">White</string>
<string name="pods_device_color_yellow_label">Yellow</string>
<string name="pods_device_color_light_blue_label">Light blue</string>
<string name="pods_device_color_dark_blue_label">Dark blue</string>
<string name="pods_device_color_space_gray_label">Space gray</string>
<string name="pods_device_color_rose_gold_label">Rose gold</string>
<string name="pods_device_color_gold_label">Gold</string>
<string name="pods_device_color_silver_label">Silver</string>
<string name="pods_device_color_gray_label">Gray</string>
<string name="pods_device_color_pink_label">Pink</string>
<string name="pods_device_color_blue_label">Blue</string>
<string name="pods_device_color_red_label">Red</string>
<string name="pods_device_color_black_label">Black</string>
<string name="pods_connection_state_disconnected_label">Disconnected</string>
<string name="pods_connection_state_idle_label">Idle</string>
<string name="pods_connection_state_music_label">Music</string>
<string name="pods_connection_state_call_label">Call</string>
<string name="pods_connection_state_ringing_label">Ringing</string>
<string name="pods_connection_state_hanging_up_label">Hanging up</string>
<string name="pods_status_x_label">Status %s</string>
</resources>
@@ -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<AirPodsDevice>("07 19 01 0E 20 >2B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
create<DualApplePods>("07 19 01 0E 20 >2B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00101011
// --^-----
microPhonePod shouldBe AirPodsDevice.Pod.LEFT
microPhonePod shouldBe DualPods.Pod.LEFT
}
create<AirPodsDevice>("07 19 01 0E 20 >0B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
create<DualApplePods>("07 19 01 0E 20 >0B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00001011
// --^-----
microPhonePod shouldBe AirPodsDevice.Pod.RIGHT
microPhonePod shouldBe DualPods.Pod.RIGHT
}
}
@Test
fun `test AirPodDevice - left pod ear status`() = runBlockingTest {
// Left Pod primary
create<AirPodsDevice>("07 19 01 0E 20 >22< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
create<DualApplePods>("07 19 01 0E 20 >22< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00100010
// 765432¹0
isLeftPodInEar shouldBe true
}
create<AirPodsDevice>("07 19 01 0E 20 >20< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
create<DualApplePods>("07 19 01 0E 20 >20< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00100000
// 765432¹0
isLeftPodInEar shouldBe false
}
// Right Pod is primary
create<AirPodsDevice>("07 19 01 0E 20 >09< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
create<DualApplePods>("07 19 01 0E 20 >09< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00001001
// 7654³210
isLeftPodInEar shouldBe true
}
create<AirPodsDevice>("07 19 01 0E 20 >20< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
create<DualApplePods>("07 19 01 0E 20 >20< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00000001
// 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<AirPodsDevice>("07 19 01 0E 20 >29< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
create<DualApplePods>("07 19 01 0E 20 >29< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00101001
// 7654³210
isRightPodInEar shouldBe true
}
create<AirPodsDevice>("07 19 01 0E 20 >21< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
create<DualApplePods>("07 19 01 0E 20 >21< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00100001
// 7654³210
isRightPodInEar shouldBe false
}
// Right Pod is primary
create<AirPodsDevice>("07 19 01 0E 20 >03< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
create<DualApplePods>("07 19 01 0E 20 >03< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00000011
// 765432¹0
isRightPodInEar shouldBe true
}
create<AirPodsDevice>("07 19 01 0E 20 >01< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
create<DualApplePods>("07 19 01 0E 20 >01< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
// 00000001
// 765432¹0
isRightPodInEar shouldBe false
@@ -134,13 +137,13 @@ class AirPodsFactoryTest : BaseTest() {
@Test
fun `test AirPodDevice - battery status`() = runBlockingTest {
// Right Pod is primary
create<AirPodsDevice>("07 19 01 0E 20 0B >98< 94 52 00 05 09 73 3C 3D F9 2C 3E B3 DD 76 02 DD 4E 16 FD FB") {
create<DualApplePods>("07 19 01 0E 20 0B >98< 94 52 00 05 09 73 3C 3D F9 2C 3E B3 DD 76 02 DD 4E 16 FD FB") {
// 88 10001000
batteryLeftPodPercent shouldBe 0.9f
batteryRightPodPercent shouldBe 0.8f
}
// Left Pod primary
create<AirPodsDevice>("07 19 01 0E 20 2B >89< 94 52 00 05 09 73 3C 3D F9 2C 3E B3 DD 76 02 DD 4E 16 FD FB") {
create<DualApplePods>("07 19 01 0E 20 2B >89< 94 52 00 05 09 73 3C 3D F9 2C 3E B3 DD 76 02 DD 4E 16 FD FB") {
// F8 11111000
batteryLeftPodPercent shouldBe 0.9f
batteryRightPodPercent shouldBe 0.8f
@@ -153,13 +156,13 @@ class AirPodsFactoryTest : BaseTest() {
* Right pod is charging
*/
// This is the left
create<AirPodsDevice>("07 19 01 0E 20 51 89 >94< 52 00 00 F4 89 82 6D 3E 27 7F 26 62 57 D0 E2 A6 49 E9 35") {
create<DualApplePods>("07 19 01 0E 20 51 89 >94< 52 00 00 F4 89 82 6D 3E 27 7F 26 62 57 D0 E2 A6 49 E9 35") {
// 1001 0100
isLeftPodCharging shouldBe false
isRightPodCharging shouldBe true
}
// This is the right
create<AirPodsDevice>("07 19 01 0E 20 31 98 >A4< 01 00 00 31 B9 A0 C4 80 CD D1 CF B9 3A 9A 6D 48 31 08 EB") {
create<DualApplePods>("07 19 01 0E 20 31 98 >A4< 01 00 00 31 B9 A0 C4 80 CD D1 CF B9 3A 9A 6D 48 31 08 EB") {
// 1010 0100
isLeftPodCharging shouldBe false
isRightPodCharging shouldBe true
@@ -169,26 +172,26 @@ class AirPodsFactoryTest : BaseTest() {
* Left pod is charging
*/
// This is the left
create<AirPodsDevice>("07 19 01 0E 20 71 98 >94< 52 00 05 A5 37 31 B2 BD 42 68 0C 64 FD 00 99 4A E5 3E F4") {
create<DualApplePods>("07 19 01 0E 20 71 98 >94< 52 00 05 A5 37 31 B2 BD 42 68 0C 64 FD 00 99 4A E5 3E F4") {
// 1001 0100
isLeftPodCharging shouldBe true
isRightPodCharging shouldBe false
}
// This is the right
create<AirPodsDevice>("07 19 01 0E 20 11 89 >A4< 04 00 04 BA 79 1B C0 65 69 C6 9F 19 6E 37 7D 6D 86 8D D9") {
create<DualApplePods>("07 19 01 0E 20 11 89 >A4< 04 00 04 BA 79 1B C0 65 69 C6 9F 19 6E 37 7D 6D 86 8D D9") {
// 1010 0100
isLeftPodCharging shouldBe true
isRightPodCharging shouldBe false
}
// Both charging
create<AirPodsDevice>("07 19 01 0E 20 55 88 >B4< 59 00 05 4B FC DF 68 28 A5 45 52 65 9C FE 51 86 3A B5 DB") {
create<DualApplePods>("07 19 01 0E 20 55 88 >B4< 59 00 05 4B FC DF 68 28 A5 45 52 65 9C FE 51 86 3A B5 DB") {
// 1011 0100
isLeftPodCharging shouldBe true
isRightPodCharging shouldBe true
}
// Both not charging
create<AirPodsDevice>("07 19 01 0E 20 00 F8 >8F< 03 00 05 4C 0F A0 C4 05 24 DD EB AF 92 99 FD 54 B1 06 48") {
create<DualApplePods>("07 19 01 0E 20 00 F8 >8F< 03 00 05 4C 0F A0 C4 05 24 DD EB AF 92 99 FD 54 B1 06 48") {
// 1000 1111
isLeftPodCharging shouldBe false
isRightPodCharging shouldBe false
@@ -197,11 +200,11 @@ class AirPodsFactoryTest : BaseTest() {
@Test
fun `test AirPodDevice - case charging`() = runBlockingTest {
create<AirPodsDevice>("07 19 01 0E 20 75 99 >B4< 31 00 05 77 C8 BA 0C 4E 1F BE AD 70 C5 40 71 D2 E9 17 A2") {
create<DualApplePods>("07 19 01 0E 20 75 99 >B4< 31 00 05 77 C8 BA 0C 4E 1F BE AD 70 C5 40 71 D2 E9 17 A2") {
// 0011 0011
isCaseCharging shouldBe false
}
create<AirPodsDevice>("07 19 01 0E 20 75 A9 >F4< 51 00 05 A0 37 92 35 49 79 CC DC 27 94 8E FB 72 12 94 52") {
create<DualApplePods>("07 19 01 0E 20 75 A9 >F4< 51 00 05 A0 37 92 35 49 79 CC DC 27 94 8E FB 72 12 94 52") {
// 0101 0011
isCaseCharging shouldBe true
}
@@ -210,66 +213,66 @@ class AirPodsFactoryTest : BaseTest() {
@Test
fun `test AirPodDevice - case lid test`() = runBlockingTest {
// Lid open
create<AirPodsDevice>("07 19 01 0E 20 55 AA B4 >31< 00 00 A1 D0 BD 82 D3 52 86 CA FC 11 62 DC 42 C6 92 8E") {
create<DualApplePods>("07 19 01 0E 20 55 AA B4 >31< 00 00 A1 D0 BD 82 D3 52 86 CA FC 11 62 DC 42 C6 92 8E") {
// 31 0011 0001
caseLidState shouldBe AirPodsDevice.LidState.OPEN
caseLidState shouldBe DualApplePods.LidState.OPEN
}
// Lid just closed
create<AirPodsDevice>("07 19 01 0E 20 55 AA B4 >39< 00 00 08 A6 DB 99 E0 5E 14 85 E5 C2 0B 68 D7 FF C3 A1") {
create<DualApplePods>("07 19 01 0E 20 55 AA B4 >39< 00 00 08 A6 DB 99 E0 5E 14 85 E5 C2 0B 68 D7 FF C3 A1") {
// 39 0011 1001
caseLidState shouldBe AirPodsDevice.LidState.UNKNOWN
caseLidState shouldBe DualApplePods.LidState.UNKNOWN
}
// Lid closed
create<AirPodsDevice>("07 19 01 0E 20 55 AA B4 38 00 00 F3 F7 08 3B 98 09 C0 DD E4 BD BD 84 55 56 8B 81") {
create<DualApplePods>("07 19 01 0E 20 55 AA B4 38 00 00 F3 F7 08 3B 98 09 C0 DD E4 BD BD 84 55 56 8B 81") {
// 38 0011 1000
caseLidState shouldBe AirPodsDevice.LidState.CLOSED
caseLidState shouldBe DualApplePods.LidState.CLOSED
}
}
@Test
fun `test AirPodDevice - connection state`() = runBlockingTest {
// Disconnected
create<AirPodsDevice>("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<DualApplePods>("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<AirPodsDevice>("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<DualApplePods>("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<AirPodsDevice>("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<DualApplePods>("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<AirPodsDevice>("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<DualApplePods>("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<AirPodsDevice>("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<DualApplePods>("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<AirPodsDevice>("07 19 01 0E 20 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") {
create<DualApplePods>("07 19 01 0E 20 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") {
// 38 0011 1000
connectionState shouldBe AirPodsDevice.ConnectionState.HANGING_UP
connectionState shouldBe DualApplePods.ConnectionState.HANGING_UP
}
}
@Test
fun `create AirPodsGen1`() = runBlockingTest {
create<AirPodsDevice>("07 19 01 02 20 55 AA 56 31 00 00 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88") {
create<DualApplePods>("07 19 01 02 20 55 AA 56 31 00 00 6F E4 DF 10 AF 10 60 81 03 3B 76 D9 C7 11 22 88") {
this shouldBe instanceOf<AirPodsGen1>()
}
}
@Test
fun `create AirPodsPro`() = runBlockingTest {
create<AirPodsDevice>("07 19 01 0E 20 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") {
create<DualApplePods>("07 19 01 0E 20 2B 99 8F 01 00 >09< 10 30 EE F3 41 B5 D8 9F A3 B0 B4 17 9F 85 97 5F") {
this shouldBe instanceOf<AirPodsPro>()
}
}
@@ -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
@@ -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
}
}
}
@@ -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
}
}
}
@@ -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<AirPodsPro>("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<AirPodsPro>("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<AirPodsPro>("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<AirPodsPro>("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<AirPodsPro>("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<AirPodsPro>("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<AirPodsPro>("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<AirPodsPro>("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<AirPodsPro>("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<AirPodsPro>("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
}
}
}