mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
Pod status cards, mvp
This commit is contained in:
@@ -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
@@ -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") {
|
||||
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
+2
-2
@@ -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." }
|
||||
+10
-9
@@ -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")
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -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)
|
||||
|
||||
}
|
||||
+5
-2
@@ -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
-1
@@ -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
-1
@@ -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()
|
||||
}
|
||||
}
|
||||
+9
-7
@@ -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
|
||||
+5
-5
@@ -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()
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -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)
|
||||
}
|
||||
}
|
||||
+13
-16
@@ -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
-1
@@ -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
|
||||
+2
-1
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user