mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-15 10:46:12 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
adef0b39e9 | ||
|
|
af12a0e5cb | ||
|
|
90fc2fac24 | ||
|
|
3be7d8b43f | ||
|
|
a35d8c57b4 | ||
|
|
f792431710 | ||
|
|
7ff20acfc0 | ||
|
|
8e20e0e108 | ||
|
|
a6fda04640 | ||
|
|
0bbcfb5e88 | ||
|
|
e79ba6bfe5 | ||
|
|
369b0207c4 | ||
|
|
ce737b45b9 | ||
|
|
cde3afdcd9 | ||
|
|
07b3f4320e | ||
|
|
e1c71cf6b6 | ||
|
|
c233cd2445 | ||
|
|
8f609ca989 | ||
|
|
2de2369600 | ||
|
|
d5d2ae784c | ||
|
|
56bedb64c4 | ||
|
|
3b48ca90d3 | ||
|
|
272ecc006f | ||
|
|
ddacfa6841 | ||
|
|
c5b1c417a4 | ||
|
|
38bc3be5b7 | ||
|
|
57cd823b63 | ||
|
|
6cd799130e | ||
|
|
a1c58e0a32 | ||
|
|
9cd4cea63d | ||
|
|
1d5e13cc24 | ||
|
|
6d2c4ba8ca | ||
|
|
357baa12e6 | ||
|
|
909a9ba194 | ||
|
|
79f68c3a0d |
-3
@@ -109,9 +109,6 @@ class BillingClientConnectionProvider @Inject constructor(
|
|||||||
delay(3000 * attempt)
|
delay(3000 * attempt)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
.catch {
|
|
||||||
log(TAG, ERROR) { "Unable to provide client connection:\n${it.asLog()}" }
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG: String = logTag("Upgrade", "Gplay", "Billing", "Client", "ConnectionProvider")
|
val TAG: String = logTag("Upgrade", "Gplay", "Billing", "Client", "ConnectionProvider")
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class BillingDataRepo @Inject constructor(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
private val connectionProvider = billingClientConnectionProvider.connection
|
private val connectionProvider = billingClientConnectionProvider.connection
|
||||||
|
.catch { log(TAG, ERROR) { "Unable to provide client connection:\n${it.asLog()}" } }
|
||||||
.replayingShare(scope)
|
.replayingShare(scope)
|
||||||
|
|
||||||
val billingData: Flow<BillingData> = connectionProvider
|
val billingData: Flow<BillingData> = connectionProvider
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
android:usesPermissionFlags="neverForLocation" />
|
android:usesPermissionFlags="neverForLocation" />
|
||||||
|
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.bluetooth_le"
|
android:name="android.hardware.bluetooth_le"
|
||||||
android:required="true" />
|
android:required="true" />
|
||||||
|
|||||||
@@ -16,5 +16,6 @@ object BuildConfigWrap {
|
|||||||
val VERSION_NAME: String = BuildConfig.VERSION_NAME
|
val VERSION_NAME: String = BuildConfig.VERSION_NAME
|
||||||
val GIT_SHA: String = BuildConfig.GITSHA
|
val GIT_SHA: String = BuildConfig.GITSHA
|
||||||
|
|
||||||
val VERSION_DESCRIPTION: String = "v$VERSION_NAME ($VERSION_CODE) [$GIT_SHA] ${FLAVOR}_$BUILD_TYPE"
|
val VERSION_DESCRIPTION_LONG: String = "v$VERSION_NAME ($VERSION_CODE) [$GIT_SHA] ${FLAVOR}_$BUILD_TYPE"
|
||||||
|
val VERSION_DESCRIPTION_SHORT: String = "v$VERSION_NAME [$GIT_SHA] $FLAVOR"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,16 @@ import java.util.*
|
|||||||
fun Byte.toHex(): String = String.format("%02X", this)
|
fun Byte.toHex(): String = String.format("%02X", this)
|
||||||
fun UByte.toHex(): String = this.toByte().toHex()
|
fun UByte.toHex(): String = this.toByte().toHex()
|
||||||
|
|
||||||
val Byte.upperNibble get() = (this.toInt() shr 4 and 0b1111).toByte()
|
val Byte.upperNibble get() = (this.toInt() shr 4 and 0b1111).toShort()
|
||||||
val Byte.lowerNibble get() = (this.toInt() and 0b1111).toByte()
|
val Byte.lowerNibble get() = (this.toInt() and 0b1111).toShort()
|
||||||
val UByte.upperNibble get() = (this.toInt() shr 4 and 0b1111).toUByte()
|
val UByte.upperNibble get() = (this.toInt() shr 4 and 0b1111).toUShort()
|
||||||
val UByte.lowerNibble get() = (this.toInt() and 0b1111).toUByte()
|
val UByte.lowerNibble get() = (this.toInt() and 0b1111).toUShort()
|
||||||
|
|
||||||
fun Byte.isBitSet(pos: Int): Boolean = BitSet.valueOf(arrayOf(this).toByteArray()).get(pos)
|
fun Byte.isBitSet(pos: Int): Boolean = BitSet.valueOf(arrayOf(this).toByteArray()).get(pos)
|
||||||
fun UByte.isBitSet(pos: Int): Boolean = this.toByte().isBitSet(pos)
|
fun UByte.isBitSet(pos: Int): Boolean = this.toByte().isBitSet(pos)
|
||||||
|
|
||||||
|
fun Short.isBitSet(pos: Int): Boolean = this.toByte().isBitSet(pos)
|
||||||
|
fun UShort.isBitSet(pos: Int): Boolean = this.toShort().isBitSet(pos)
|
||||||
|
|
||||||
|
fun UShort.toBinaryString(): String = Integer.toBinaryString(this.toInt()).padStart(4, '0')
|
||||||
|
fun UByte.toBinaryString(): String = Integer.toBinaryString(this.toInt()).padStart(8, '0')
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package eu.darken.capod.common
|
||||||
|
|
||||||
|
object PrivacyPolicy {
|
||||||
|
const val URL = "https://raw.githubusercontent.com/d4rken/capod-public/main/PRIVACY_POLICY.md"
|
||||||
|
}
|
||||||
@@ -72,8 +72,8 @@ class BleScanner @Inject constructor(
|
|||||||
)
|
)
|
||||||
if (adapter.isOffloadedScanBatchingSupported) {
|
if (adapter.isOffloadedScanBatchingSupported) {
|
||||||
when (scannerMode) {
|
when (scannerMode) {
|
||||||
ScannerMode.LOW_POWER -> setReportDelay(5000)
|
ScannerMode.LOW_POWER -> setReportDelay(2000)
|
||||||
ScannerMode.BALANCED -> setReportDelay(2500)
|
ScannerMode.BALANCED -> setReportDelay(1000)
|
||||||
ScannerMode.LOW_LATENCY -> setReportDelay(500)
|
ScannerMode.LOW_LATENCY -> setReportDelay(500)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -87,7 +87,7 @@ class BleScanner @Inject constructor(
|
|||||||
adapter.bluetoothLeScanner.flushPendingScanResults(callback)
|
adapter.bluetoothLeScanner.flushPendingScanResults(callback)
|
||||||
when (scannerMode) {
|
when (scannerMode) {
|
||||||
ScannerMode.LOW_POWER -> break
|
ScannerMode.LOW_POWER -> break
|
||||||
ScannerMode.BALANCED -> delay(2500)
|
ScannerMode.BALANCED -> delay(1000)
|
||||||
ScannerMode.LOW_LATENCY -> delay(500)
|
ScannerMode.LOW_LATENCY -> delay(500)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class RecorderModule @Inject constructor(
|
|||||||
context.startServiceCompat(Intent(context, RecorderService::class.java))
|
context.startServiceCompat(Intent(context, RecorderService::class.java))
|
||||||
|
|
||||||
log(TAG, INFO) { "Build.Fingerprint: ${Build.FINGERPRINT}" }
|
log(TAG, INFO) { "Build.Fingerprint: ${Build.FINGERPRINT}" }
|
||||||
log(TAG, INFO) { "BuildConfig.Versions: ${BuildConfigWrap.VERSION_DESCRIPTION}" }
|
log(TAG, INFO) { "BuildConfig.Versions: ${BuildConfigWrap.VERSION_DESCRIPTION_LONG}" }
|
||||||
|
|
||||||
copy(
|
copy(
|
||||||
recorder = newRecorder
|
recorder = newRecorder
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ class RecorderActivityVM @Inject constructor(
|
|||||||
type = "application/zip"
|
type = "application/zip"
|
||||||
|
|
||||||
addCategory(Intent.CATEGORY_DEFAULT)
|
addCategory(Intent.CATEGORY_DEFAULT)
|
||||||
putExtra(Intent.EXTRA_SUBJECT, "CAPod DebugLog - ${BuildConfigWrap.VERSION_DESCRIPTION})")
|
putExtra(Intent.EXTRA_SUBJECT, "CAPod DebugLog - ${BuildConfigWrap.VERSION_DESCRIPTION_LONG})")
|
||||||
putExtra(Intent.EXTRA_TEXT, "Your text here.")
|
putExtra(Intent.EXTRA_TEXT, "Your text here.")
|
||||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ fun <T : Any> Flow<T>.shareLatest(
|
|||||||
)
|
)
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
|
|
||||||
fun <T : Any> Flow<T>.replayingShare(scope: CoroutineScope) = this.shareIn(
|
fun <T : Any?> Flow<T>.replayingShare(scope: CoroutineScope) = this.shareIn(
|
||||||
scope = scope,
|
scope = scope,
|
||||||
replay = 1,
|
replay = 1,
|
||||||
started = SharingStarted.WhileSubscribed(replayExpiration = Duration.ZERO)
|
started = SharingStarted.WhileSubscribed(replayExpiration = Duration.ZERO)
|
||||||
|
|||||||
@@ -62,6 +62,15 @@ enum class Permission(
|
|||||||
val pwm = it.getSystemService(Context.POWER_SERVICE) as PowerManager
|
val pwm = it.getSystemService(Context.POWER_SERVICE) as PowerManager
|
||||||
pwm.isIgnoringBatteryOptimizations(BuildConfigWrap.APPLICATION_ID)
|
pwm.isIgnoringBatteryOptimizations(BuildConfigWrap.APPLICATION_ID)
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
SYSTEM_ALERT_WINDOW(
|
||||||
|
minApiLevel = Build.VERSION_CODES.BASE,
|
||||||
|
labelRes = R.string.permission_system_alert_window_label,
|
||||||
|
descriptionRes = R.string.permission_system_alert_window_description,
|
||||||
|
permissionId = "android.permission.SYSTEM_ALERT_WINDOW",
|
||||||
|
isGranted = {
|
||||||
|
android.provider.Settings.canDrawOverlays(it)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class GeneralSettings @Inject constructor(
|
|||||||
|
|
||||||
val scannerMode = preferences.createFlowPreference(
|
val scannerMode = preferences.createFlowPreference(
|
||||||
"core.scanner.mode",
|
"core.scanner.mode",
|
||||||
ScannerMode.BALANCED,
|
ScannerMode.LOW_LATENCY,
|
||||||
moshi
|
moshi
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import eu.darken.capod.common.debug.logging.log
|
|||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.common.permissions.Permission
|
import eu.darken.capod.common.permissions.Permission
|
||||||
import eu.darken.capod.common.permissions.isRequired
|
import eu.darken.capod.common.permissions.isRequired
|
||||||
|
import eu.darken.capod.reaction.settings.ReactionSettings
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
@@ -18,6 +19,7 @@ import javax.inject.Singleton
|
|||||||
class PermissionTool @Inject constructor(
|
class PermissionTool @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val generalSettings: GeneralSettings,
|
private val generalSettings: GeneralSettings,
|
||||||
|
private val reactionSettings: ReactionSettings,
|
||||||
) {
|
) {
|
||||||
private val permissionCheckTrigger = MutableStateFlow(UUID.randomUUID())
|
private val permissionCheckTrigger = MutableStateFlow(UUID.randomUUID())
|
||||||
|
|
||||||
@@ -29,9 +31,12 @@ class PermissionTool @Inject constructor(
|
|||||||
val missingPermissions: Flow<Set<Permission>> = combine(
|
val missingPermissions: Flow<Set<Permission>> = combine(
|
||||||
permissionCheckTrigger,
|
permissionCheckTrigger,
|
||||||
generalSettings.monitorMode.flow,
|
generalSettings.monitorMode.flow,
|
||||||
) { _, monitorMode ->
|
reactionSettings.showPopUpOnCaseOpen.flow
|
||||||
|
) { _, monitorMode, showPopUp ->
|
||||||
Permission.values()
|
Permission.values()
|
||||||
.filter { it != Permission.IGNORE_BATTERY_OPTIMIZATION || monitorMode == MonitorMode.ALWAYS }
|
.filter { it != Permission.IGNORE_BATTERY_OPTIMIZATION || monitorMode == MonitorMode.ALWAYS }
|
||||||
|
.filter { it != Permission.ACCESS_BACKGROUND_LOCATION || monitorMode == MonitorMode.ALWAYS }
|
||||||
|
.filter { it != Permission.SYSTEM_ALERT_WINDOW || showPopUp }
|
||||||
.filter { it.isRequired(context) }
|
.filter { it.isRequired(context) }
|
||||||
.toSet()
|
.toSet()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,11 +33,11 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) {
|
|||||||
lateinit var adapter: OverviewAdapter
|
lateinit var adapter: OverviewAdapter
|
||||||
|
|
||||||
lateinit var permissionLauncher: ActivityResultLauncher<String>
|
lateinit var permissionLauncher: ActivityResultLauncher<String>
|
||||||
var awaitingIgnoreBatteryOptimization = false
|
var awaitingPermission = false
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
awaitingIgnoreBatteryOptimization = savedInstanceState?.getBoolean("awaitingIgnoreBatteryOptimization") ?: false
|
awaitingPermission = savedInstanceState?.getBoolean("awaitingPermission") ?: false
|
||||||
|
|
||||||
permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
permissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||||
log { "Request for $id was granted=$granted" }
|
log { "Request for $id was granted=$granted" }
|
||||||
@@ -77,16 +77,28 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
vm.requestPermissionEvent.observe2(ui) {
|
vm.requestPermissionEvent.observe2(ui) {
|
||||||
if (it == Permission.IGNORE_BATTERY_OPTIMIZATION) {
|
when (it) {
|
||||||
awaitingIgnoreBatteryOptimization = true
|
Permission.IGNORE_BATTERY_OPTIMIZATION -> {
|
||||||
startActivity(
|
awaitingPermission = true
|
||||||
Intent(
|
startActivity(
|
||||||
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
|
Intent(
|
||||||
Uri.parse("package:${requireContext().packageName}")
|
Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
|
||||||
|
Uri.parse("package:${requireContext().packageName}")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
} else {
|
Permission.SYSTEM_ALERT_WINDOW -> {
|
||||||
permissionLauncher.launch(it.permissionId)
|
awaitingPermission = true
|
||||||
|
startActivity(
|
||||||
|
Intent(
|
||||||
|
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
|
||||||
|
Uri.parse("package:${requireContext().packageName}")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
permissionLauncher.launch(it.permissionId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,15 +147,15 @@ class OverviewFragment : Fragment3(R.layout.main_fragment) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
outState.putBoolean("awaitingIgnoreBatteryOptimization", awaitingIgnoreBatteryOptimization)
|
outState.putBoolean("awaitingPermission", awaitingPermission)
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
if (awaitingIgnoreBatteryOptimization) {
|
if (awaitingPermission) {
|
||||||
awaitingIgnoreBatteryOptimization = false
|
awaitingPermission = false
|
||||||
log { "awaitingIgnoreBatteryOptimization=true" }
|
log { "awaitingPermission=true" }
|
||||||
vm.onPermissionResult(true)
|
vm.onPermissionResult(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.app.Activity
|
|||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import eu.darken.capod.common.WebpageTool
|
||||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||||
@@ -45,6 +46,7 @@ class OverviewFragmentVM @Inject constructor(
|
|||||||
debugSettings: DebugSettings,
|
debugSettings: DebugSettings,
|
||||||
private val upgradeRepo: UpgradeRepo,
|
private val upgradeRepo: UpgradeRepo,
|
||||||
private val bluetoothManager: BluetoothManager2,
|
private val bluetoothManager: BluetoothManager2,
|
||||||
|
private val webpageTool: WebpageTool,
|
||||||
) : ViewModel3(dispatcherProvider = dispatcherProvider) {
|
) : ViewModel3(dispatcherProvider = dispatcherProvider) {
|
||||||
|
|
||||||
val upgradeState = upgradeRepo.upgradeInfo.asLiveData2()
|
val upgradeState = upgradeRepo.upgradeInfo.asLiveData2()
|
||||||
@@ -104,16 +106,15 @@ class OverviewFragmentVM @Inject constructor(
|
|||||||
debugSettings.isDebugModeEnabled.flow,
|
debugSettings.isDebugModeEnabled.flow,
|
||||||
generalSettings.showAll.flow,
|
generalSettings.showAll.flow,
|
||||||
bluetoothManager.isBluetoothEnabled,
|
bluetoothManager.isBluetoothEnabled,
|
||||||
) { _, permissions, pods, isDebugMode, showAll, isBluetoothEnabled ->
|
podMonitor.mainDevice,
|
||||||
|
) { _, permissions, pods, isDebugMode, showAll, isBluetoothEnabled, mainPod ->
|
||||||
val items = mutableListOf<OverviewAdapter.Item>()
|
val items = mutableListOf<OverviewAdapter.Item>()
|
||||||
|
|
||||||
val mainPod = podMonitor.mainDevice.first()
|
|
||||||
|
|
||||||
permissions
|
permissions
|
||||||
.map { perm ->
|
.map { perm ->
|
||||||
PermissionCardVH.Item(
|
PermissionCardVH.Item(
|
||||||
permission = perm,
|
permission = perm,
|
||||||
onRequest = { requestPermissionEvent.postValue(it) }
|
onRequest = { requestPermissionEvent.postValue(it) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.run { items.addAll(this) }
|
.run { items.addAll(this) }
|
||||||
@@ -127,37 +128,35 @@ class OverviewFragmentVM @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (permissions.isEmpty() && isBluetoothEnabled) {
|
if (permissions.isEmpty() && isBluetoothEnabled) {
|
||||||
pods
|
pods.map {
|
||||||
.map {
|
val now = Instant.now()
|
||||||
val now = Instant.now()
|
when (it) {
|
||||||
when (it) {
|
is DualApplePods -> DualApplePodsCardVH.Item(
|
||||||
is DualApplePods -> DualApplePodsCardVH.Item(
|
now = now,
|
||||||
now = now,
|
device = it,
|
||||||
device = it,
|
showDebug = isDebugMode,
|
||||||
showDebug = isDebugMode,
|
isMainPod = it == mainPod,
|
||||||
isMainPod = it == mainPod,
|
)
|
||||||
)
|
is SingleApplePods -> SingleApplePodsCardVH.Item(
|
||||||
is SingleApplePods -> SingleApplePodsCardVH.Item(
|
now = now,
|
||||||
now = now,
|
device = it,
|
||||||
device = it,
|
showDebug = isDebugMode,
|
||||||
showDebug = isDebugMode,
|
isMainPod = it == mainPod,
|
||||||
isMainPod = it == mainPod,
|
)
|
||||||
)
|
is BasicSingleApplePods -> BasicSingleApplePodsCardVH.Item(
|
||||||
is BasicSingleApplePods -> BasicSingleApplePodsCardVH.Item(
|
now = now,
|
||||||
now = now,
|
device = it,
|
||||||
device = it,
|
showDebug = isDebugMode,
|
||||||
showDebug = isDebugMode,
|
isMainPod = it == mainPod,
|
||||||
isMainPod = it == mainPod,
|
)
|
||||||
)
|
else -> UnknownPodDeviceCardVH.Item(
|
||||||
else -> UnknownPodDeviceCardVH.Item(
|
now = now,
|
||||||
now = now,
|
device = it,
|
||||||
device = it,
|
showDebug = isDebugMode,
|
||||||
showDebug = isDebugMode,
|
isMainPod = it == mainPod,
|
||||||
isMainPod = it == mainPod,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.run { items.addAll(this) }
|
}.run { items.addAll(this) }
|
||||||
}
|
}
|
||||||
|
|
||||||
items
|
items
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
package eu.darken.capod.main.ui.overview.cards
|
package eu.darken.capod.main.ui.overview.cards
|
||||||
|
|
||||||
|
import android.text.Html
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
|
import eu.darken.capod.common.PrivacyPolicy
|
||||||
import eu.darken.capod.common.lists.binding
|
import eu.darken.capod.common.lists.binding
|
||||||
import eu.darken.capod.common.lists.differ.DifferItem
|
import eu.darken.capod.common.lists.differ.DifferItem
|
||||||
import eu.darken.capod.common.permissions.Permission
|
import eu.darken.capod.common.permissions.Permission
|
||||||
@@ -25,6 +28,12 @@ class PermissionCardVH(parent: ViewGroup) :
|
|||||||
permissionLabel.setText(item.permission.labelRes)
|
permissionLabel.setText(item.permission.labelRes)
|
||||||
permissionDescription.setText(item.permission.descriptionRes)
|
permissionDescription.setText(item.permission.descriptionRes)
|
||||||
grantAction.setOnClickListener { item.onRequest(item.permission) }
|
grantAction.setOnClickListener { item.onRequest(item.permission) }
|
||||||
|
privacyPolicy.apply {
|
||||||
|
movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
val ppText = getString(R.string.settings_privacy_policy_label)
|
||||||
|
val ppLink = PrivacyPolicy.URL
|
||||||
|
text = Html.fromHtml("<html><a href=\"$ppLink\">$ppText</a></html>", 0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Item(
|
data class Item(
|
||||||
|
|||||||
+3
-5
@@ -7,6 +7,7 @@ import eu.darken.capod.R
|
|||||||
import eu.darken.capod.common.lists.binding
|
import eu.darken.capod.common.lists.binding
|
||||||
import eu.darken.capod.databinding.OverviewPodsAppleSingleBasicItemBinding
|
import eu.darken.capod.databinding.OverviewPodsAppleSingleBasicItemBinding
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||||
|
import eu.darken.capod.pods.core.getBatteryDrawable
|
||||||
import eu.darken.capod.pods.core.getBatteryLevelHeadset
|
import eu.darken.capod.pods.core.getBatteryLevelHeadset
|
||||||
import eu.darken.capod.pods.core.getSignalQuality
|
import eu.darken.capod.pods.core.getSignalQuality
|
||||||
import eu.darken.capod.pods.core.lastSeenFormatted
|
import eu.darken.capod.pods.core.lastSeenFormatted
|
||||||
@@ -38,11 +39,8 @@ class BasicSingleApplePodsCardVH(parent: ViewGroup) :
|
|||||||
reception.append("\n(${getString(R.string.pods_yours)})")
|
reception.append("\n(${getString(R.string.pods_yours)})")
|
||||||
}
|
}
|
||||||
|
|
||||||
headphones.apply {
|
batteryLabel.text = device.getBatteryLevelHeadset(context)
|
||||||
val sb = StringBuilder(context.getString(R.string.pods_single_headphones_label))
|
batteryIcon.setImageResource(getBatteryDrawable(device.batteryHeadsetPercent))
|
||||||
sb.append("\n").append(device.getBatteryLevelHeadset(context))
|
|
||||||
text = sb
|
|
||||||
}
|
|
||||||
|
|
||||||
status.apply {
|
status.apply {
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
|
|||||||
+53
-43
@@ -2,11 +2,11 @@ package eu.darken.capod.main.ui.overview.cards.pods
|
|||||||
|
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.lists.binding
|
import eu.darken.capod.common.lists.binding
|
||||||
import eu.darken.capod.databinding.OverviewPodsAppleDualItemBinding
|
import eu.darken.capod.databinding.OverviewPodsAppleDualItemBinding
|
||||||
import eu.darken.capod.pods.core.*
|
import eu.darken.capod.pods.core.*
|
||||||
import eu.darken.capod.pods.core.HasDualPods.Pod
|
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods.DeviceColor
|
import eu.darken.capod.pods.core.apple.DualApplePods.DeviceColor
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||||
@@ -24,61 +24,71 @@ class DualApplePodsCardVH(parent: ViewGroup) :
|
|||||||
val device = item.device
|
val device = item.device
|
||||||
name.apply {
|
name.apply {
|
||||||
val sb = StringBuilder(device.getLabel(context))
|
val sb = StringBuilder(device.getLabel(context))
|
||||||
if (device.deviceColor != DeviceColor.UNKNOWN) {
|
if (!listOf(DeviceColor.WHITE, DeviceColor.UNKNOWN).contains(device.deviceColor)) {
|
||||||
sb.append(" (${device.getDeviceColorLabel(context)})")
|
sb.append(" (${device.deviceColor.name})")
|
||||||
}
|
}
|
||||||
text = sb
|
text = sb
|
||||||
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
|
if (item.isMainPod) setTypeface(typeface, Typeface.BOLD)
|
||||||
else setTypeface(typeface, Typeface.NORMAL)
|
else setTypeface(typeface, Typeface.NORMAL)
|
||||||
|
if (item.showDebug) {
|
||||||
|
append(" [${device.primaryPod.name}]")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
deviceIcon.setImageResource(device.iconRes)
|
deviceIcon.setImageResource(device.iconRes)
|
||||||
|
|
||||||
lastSeen.text = device.lastSeenFormatted(item.now)
|
lastSeen.text = device.lastSeenFormatted(item.now)
|
||||||
|
|
||||||
reception.text = device.getSignalQuality(context)
|
reception.text = device.getSignalQuality(context).let {
|
||||||
if (item.isMainPod) {
|
if (item.isMainPod) "$it\n(${getString(R.string.pods_yours)})" else it
|
||||||
reception.append("\n(${getString(R.string.pods_yours)})")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
podLeft.apply {
|
// Left Pod
|
||||||
val sb = StringBuilder(context.getString(R.string.pods_dual_left_label))
|
device.apply {
|
||||||
sb.append("\n").append(device.getBatteryLevelLeftPod(context))
|
podLeftBatteryIcon.setImageResource(getBatteryDrawable(batteryLeftPodPercent))
|
||||||
when {
|
podLeftBatteryLabel.text = getBatteryLevelLeftPod(context)
|
||||||
device.isLeftPodCharging -> sb.append("\n").append(getString(R.string.pods_charging_label))
|
|
||||||
device.isLeftPodInEar -> sb.append("\n").append(getString(R.string.pods_inear_label))
|
podLeftChargingIcon.isInvisible = !isLeftPodCharging
|
||||||
else -> {}
|
podLeftChargingLabel.isInvisible = !isLeftPodCharging
|
||||||
|
|
||||||
|
podLeftMicrophoneIcon.isInvisible = !isLeftPodMicrophone
|
||||||
|
podLeftMicrophoneLabel.isInvisible = !isLeftPodMicrophone
|
||||||
|
|
||||||
|
podLeftWearIcon.isInvisible = !isLeftPodInEar
|
||||||
|
podLeftWearLabel.isInvisible = !isLeftPodInEar
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right Pod
|
||||||
|
device.apply {
|
||||||
|
podRightBatteryIcon.setImageResource(getBatteryDrawable(batteryRightPodPercent))
|
||||||
|
podRightBatteryLabel.text = getBatteryLevelRightPod(context)
|
||||||
|
|
||||||
|
podRightChargingIcon.isInvisible = !isRightPodCharging
|
||||||
|
podRightChargingLabel.isInvisible = !isRightPodCharging
|
||||||
|
|
||||||
|
podRightMicrophoneIcon.isInvisible = !isRightPodMicrophone
|
||||||
|
podRightMicrophoneLabel.isInvisible = !isRightPodMicrophone
|
||||||
|
|
||||||
|
podRightWearIcon.isInvisible = !isRightPodInEar
|
||||||
|
podRightWearLabel.isInvisible = !isRightPodInEar
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case
|
||||||
|
device.apply {
|
||||||
|
podCaseBatteryIcon.setImageResource(getBatteryDrawable(batteryCasePercent))
|
||||||
|
podCaseBatteryLabel.text = getBatteryLevelCase(context)
|
||||||
|
|
||||||
|
podCaseChargingIcon.isInvisible = !isCaseCharging
|
||||||
|
podCaseChargingLabel.isInvisible = !isCaseCharging
|
||||||
|
|
||||||
|
podCaseLidLabel.text = when (caseLidState) {
|
||||||
|
LidState.OPEN -> context.getString(R.string.pods_case_status_open_label)
|
||||||
|
LidState.CLOSED -> context.getString(R.string.pods_case_status_closed_label)
|
||||||
|
else -> context.getString(R.string.general_value_unknown_label)
|
||||||
}
|
}
|
||||||
text = sb
|
|
||||||
}
|
|
||||||
|
|
||||||
podRight.apply {
|
val hideInfo = !listOf(LidState.OPEN, LidState.CLOSED).contains(caseLidState)
|
||||||
val sb = StringBuilder(getString(R.string.pods_dual_right_label))
|
podCaseLidIcon.isInvisible = hideInfo
|
||||||
sb.append("\n").append(device.getBatteryLevelRightPod(context))
|
podCaseLidLabel.isInvisible = hideInfo
|
||||||
when {
|
|
||||||
device.isRightPodCharging -> sb.append("\n").append(getString(R.string.pods_charging_label))
|
|
||||||
device.isRightPodInEar -> sb.append("\n").append(getString(R.string.pods_inear_label))
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
text = sb
|
|
||||||
}
|
|
||||||
|
|
||||||
when (device.microPhonePod) {
|
|
||||||
Pod.LEFT -> podLeft.append("\n(${context.getString(R.string.pods_microphone_label)})")
|
|
||||||
Pod.RIGHT -> podRight.append("\n(${context.getString(R.string.pods_microphone_label)})")
|
|
||||||
}
|
|
||||||
|
|
||||||
podCase.apply {
|
|
||||||
val sb = StringBuilder(getString(R.string.pods_case_label))
|
|
||||||
sb.append("\n").append(device.getBatteryLevelCase(context))
|
|
||||||
if (device.isCaseCharging) sb.append("\n").append(getString(R.string.pods_charging_label))
|
|
||||||
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.apply {
|
status.apply {
|
||||||
|
|||||||
+10
-10
@@ -3,10 +3,12 @@ package eu.darken.capod.main.ui.overview.cards.pods
|
|||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.lists.binding
|
import eu.darken.capod.common.lists.binding
|
||||||
import eu.darken.capod.databinding.OverviewPodsAppleSingleItemBinding
|
import eu.darken.capod.databinding.OverviewPodsAppleSingleItemBinding
|
||||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||||
|
import eu.darken.capod.pods.core.getBatteryDrawable
|
||||||
import eu.darken.capod.pods.core.getBatteryLevelHeadset
|
import eu.darken.capod.pods.core.getBatteryLevelHeadset
|
||||||
import eu.darken.capod.pods.core.getSignalQuality
|
import eu.darken.capod.pods.core.getSignalQuality
|
||||||
import eu.darken.capod.pods.core.lastSeenFormatted
|
import eu.darken.capod.pods.core.lastSeenFormatted
|
||||||
@@ -38,16 +40,14 @@ class SingleApplePodsCardVH(parent: ViewGroup) :
|
|||||||
reception.append("\n(${getString(R.string.pods_yours)})")
|
reception.append("\n(${getString(R.string.pods_yours)})")
|
||||||
}
|
}
|
||||||
|
|
||||||
headphones.apply {
|
batteryLabel.text = device.getBatteryLevelHeadset(context)
|
||||||
val sb = StringBuilder(context.getString(R.string.pods_single_headphones_label))
|
batteryIcon.setImageResource(getBatteryDrawable(device.batteryHeadsetPercent))
|
||||||
sb.append("\n").append(device.getBatteryLevelHeadset(context))
|
|
||||||
when {
|
chargingIcon.isInvisible = device.isHeadsetBeingCharged
|
||||||
device.isHeadsetBeingCharged -> sb.append("\n").append("Charging")
|
chargingLabel.isInvisible = device.isHeadsetBeingCharged
|
||||||
device.isHeadphonesBeingWorn -> sb.append("\n").append("In ear")
|
|
||||||
else -> {}
|
wearIcon.isInvisible = device.isHeadphonesBeingWorn
|
||||||
}
|
wearLabel.isInvisible = device.isHeadphonesBeingWorn
|
||||||
text = sb
|
|
||||||
}
|
|
||||||
|
|
||||||
status.apply {
|
status.apply {
|
||||||
val sb = StringBuilder()
|
val sb = StringBuilder()
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class SettingsFragment : Fragment2(R.layout.settings_fragment),
|
|||||||
}
|
}
|
||||||
|
|
||||||
ui.toolbar.apply {
|
ui.toolbar.apply {
|
||||||
subtitle = BuildConfigWrap.VERSION_DESCRIPTION
|
subtitle = BuildConfigWrap.VERSION_DESCRIPTION_SHORT
|
||||||
setNavigationOnClickListener { requireActivity().onBackPressed() }
|
setNavigationOnClickListener { requireActivity().onBackPressed() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||||||
import eu.darken.androidstarter.common.preferences.Settings
|
import eu.darken.androidstarter.common.preferences.Settings
|
||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import eu.darken.capod.common.BuildConfigWrap
|
import eu.darken.capod.common.BuildConfigWrap
|
||||||
|
import eu.darken.capod.common.PrivacyPolicy
|
||||||
import eu.darken.capod.common.WebpageTool
|
import eu.darken.capod.common.WebpageTool
|
||||||
import eu.darken.capod.common.uix.PreferenceFragment2
|
import eu.darken.capod.common.uix.PreferenceFragment2
|
||||||
import eu.darken.capod.main.core.GeneralSettings
|
import eu.darken.capod.main.core.GeneralSettings
|
||||||
@@ -34,7 +35,11 @@ class SettingsIndexFragment : PreferenceFragment2() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onPreferencesCreated() {
|
override fun onPreferencesCreated() {
|
||||||
findPreference<Preference>("core.changelog")!!.summary = BuildConfigWrap.VERSION_DESCRIPTION
|
findPreference<Preference>("core.changelog")!!.summary = BuildConfigWrap.VERSION_DESCRIPTION_LONG
|
||||||
|
findPreference<Preference>("core.privacy")!!.setOnPreferenceClickListener {
|
||||||
|
webpageTool.open(PrivacyPolicy.URL)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
super.onPreferencesCreated()
|
super.onPreferencesCreated()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class SupportFragmentVM @Inject constructor(
|
|||||||
|
|
||||||
bodyInfo.append("--- Infos for the developer ---\n")
|
bodyInfo.append("--- Infos for the developer ---\n")
|
||||||
|
|
||||||
bodyInfo.append("App version: ").append(BuildConfigWrap.VERSION_DESCRIPTION).append("\n")
|
bodyInfo.append("App version: ").append(BuildConfigWrap.VERSION_DESCRIPTION_LONG).append("\n")
|
||||||
|
|
||||||
bodyInfo.append("Device: ").append(Build.FINGERPRINT).append("\n")
|
bodyInfo.append("Device: ").append(Build.FINGERPRINT).append("\n")
|
||||||
bodyInfo.append("Install ID: ").append(installId.id).append("\n")
|
bodyInfo.append("Install ID: ").append(installId.id).append("\n")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package eu.darken.capod.monitor.core
|
package eu.darken.capod.monitor.core
|
||||||
|
|
||||||
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
import eu.darken.capod.common.bluetooth.BleScanner
|
import eu.darken.capod.common.bluetooth.BleScanner
|
||||||
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
import eu.darken.capod.common.bluetooth.BluetoothManager2
|
||||||
import eu.darken.capod.common.coroutine.AppScope
|
import eu.darken.capod.common.coroutine.AppScope
|
||||||
@@ -9,9 +10,11 @@ import eu.darken.capod.common.debug.logging.log
|
|||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.common.flow.replayingShare
|
import eu.darken.capod.common.flow.replayingShare
|
||||||
import eu.darken.capod.main.core.GeneralSettings
|
import eu.darken.capod.main.core.GeneralSettings
|
||||||
|
import eu.darken.capod.main.core.ScannerMode
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.PodFactory
|
import eu.darken.capod.pods.core.PodFactory
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
@@ -32,35 +35,44 @@ class PodMonitor @Inject constructor(
|
|||||||
private val deviceCache = mutableMapOf<PodDevice.Id, PodDevice>()
|
private val deviceCache = mutableMapOf<PodDevice.Id, PodDevice>()
|
||||||
private val cacheLock = Mutex()
|
private val cacheLock = Mutex()
|
||||||
|
|
||||||
|
private suspend fun List<BleScanResult>.preFilterAndMap(
|
||||||
|
scannerMode: ScannerMode
|
||||||
|
): List<PodDevice> = this
|
||||||
|
.groupBy { it.address }
|
||||||
|
.values
|
||||||
|
.map { sameAdrDevs ->
|
||||||
|
// For each address we only want the newest result, upstream may batch data
|
||||||
|
val newest = sameAdrDevs.maxByOrNull { it.generatedAtNanos }!!
|
||||||
|
sameAdrDevs.minus(newest).let {
|
||||||
|
if (it.isNotEmpty()) log(TAG, VERBOSE) { "Discarding stale results: $it" }
|
||||||
|
}
|
||||||
|
newest
|
||||||
|
}
|
||||||
|
.mapNotNull { podFactory.createPod(it) }
|
||||||
|
|
||||||
val devices: Flow<List<PodDevice>> = bluetoothManager.isBluetoothEnabled
|
val devices: Flow<List<PodDevice>> = bluetoothManager.isBluetoothEnabled
|
||||||
.flatMapLatest { isBluetoothEnabled ->
|
.flatMapLatest { isBluetoothEnabled ->
|
||||||
if (isBluetoothEnabled) {
|
if (isBluetoothEnabled) {
|
||||||
|
log(TAG) { "Bluetooth is enabled" }
|
||||||
generalSettings.scannerMode.flow
|
generalSettings.scannerMode.flow
|
||||||
|
.flatMapLatest { mode ->
|
||||||
|
bleScanner.scan(scannerMode = mode).map { it.preFilterAndMap(mode) }
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
log(TAG, WARN) { "Bluetooth is currently disabled" }
|
log(TAG, WARN) { "Bluetooth is currently disabled" }
|
||||||
emptyFlow()
|
flowOf(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.flatMapLatest { bleScanner.scan(scannerMode = it) }
|
.map { newPods ->
|
||||||
.map { result ->
|
|
||||||
// For each address we only want the newest result, upstream may batch data
|
|
||||||
result.groupBy { it.address }
|
|
||||||
.values
|
|
||||||
.map { sameAdrDevs ->
|
|
||||||
val newest = sameAdrDevs.maxByOrNull { it.generatedAtNanos }!!
|
|
||||||
sameAdrDevs.minus(newest).let {
|
|
||||||
if (it.isNotEmpty()) log(TAG, VERBOSE) { "Discarding stale results: $it" }
|
|
||||||
}
|
|
||||||
newest
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.map { scanResults ->
|
|
||||||
val newPods = scanResults
|
|
||||||
.mapNotNull { podFactory.createPod(it) }
|
|
||||||
|
|
||||||
val pods = mutableMapOf<PodDevice.Id, PodDevice>()
|
val pods = mutableMapOf<PodDevice.Id, PodDevice>()
|
||||||
|
|
||||||
cacheLock.withLock {
|
cacheLock.withLock {
|
||||||
|
if (newPods == null) {
|
||||||
|
log(TAG) { "Null result, Bluetooth is disabled." }
|
||||||
|
deviceCache.clear()
|
||||||
|
return@map emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
val now = Instant.now()
|
val now = Instant.now()
|
||||||
deviceCache.toList().forEach { (key, value) ->
|
deviceCache.toList().forEach { (key, value) ->
|
||||||
if (Duration.between(value.lastSeenAt, now) > Duration.ofSeconds(20)) {
|
if (Duration.between(value.lastSeenAt, now) > Duration.ofSeconds(20)) {
|
||||||
@@ -77,47 +89,52 @@ class PodMonitor @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
val aboveThreshold = mutableListOf<PodDevice>()
|
|
||||||
val belowThreshold = mutableListOf<PodDevice>()
|
|
||||||
val minimumSignalQuality = generalSettings.minimumSignalQuality.value
|
|
||||||
pods.values.forEach { device ->
|
|
||||||
if (device.signalQuality > minimumSignalQuality) aboveThreshold.add(device)
|
|
||||||
else belowThreshold.add(device)
|
|
||||||
}
|
|
||||||
val now = Instant.now()
|
val now = Instant.now()
|
||||||
aboveThreshold.sortedWith(compareBy<PodDevice> {
|
val minimumSignalQuality = generalSettings.minimumSignalQuality.value
|
||||||
Duration.between(
|
val mainDeviceModel = generalSettings.mainDeviceModel.value
|
||||||
it.lastSeenAt,
|
|
||||||
now
|
val aboveTresholdSorted = pods.values
|
||||||
|
.filter { it.signalQuality > minimumSignalQuality }
|
||||||
|
.sortedWith(
|
||||||
|
compareByDescending<PodDevice> { it.model == mainDeviceModel && it.model != PodDevice.Model.UNKNOWN }
|
||||||
|
.thenBy {
|
||||||
|
val age = Duration.between(it.lastSeenAt, now).seconds
|
||||||
|
if (age < 3) 0L else age
|
||||||
|
}
|
||||||
|
.thenByDescending { it.signalQuality }
|
||||||
)
|
)
|
||||||
}.thenByDescending { it.rssi })
|
val belowThresholdSorted = pods.values
|
||||||
.plus(belowThreshold.sortedWith(compareBy<PodDevice> {
|
.filter { it.signalQuality <= minimumSignalQuality }
|
||||||
Duration.between(
|
.sortedWith(
|
||||||
it.lastSeenAt,
|
compareBy<PodDevice> { Duration.between(it.lastSeenAt, now) }.thenByDescending { it.signalQuality }
|
||||||
now
|
)
|
||||||
)
|
|
||||||
}.thenByDescending { it.rssi }))
|
val presorted = (aboveTresholdSorted + belowThresholdSorted)
|
||||||
// pods.values.sortedByDescending { it.rssi }
|
val main = presorted.determineMainDevice()
|
||||||
|
presorted.sortedByDescending { it == main }
|
||||||
}
|
}
|
||||||
.onStart { emit(emptyList()) }
|
.onStart { emit(emptyList()) }
|
||||||
.catch {
|
.retryWhen { cause, attempt ->
|
||||||
log(TAG, ERROR) { "PodMonitor failed:\n${it.asLog()}" }
|
log(TAG, WARN) { "PodMonitor failed (attempt=$attempt), will retry: ${cause.asLog()}" }
|
||||||
|
delay(3000)
|
||||||
|
true
|
||||||
}
|
}
|
||||||
.replayingShare(appScope)
|
.replayingShare(appScope)
|
||||||
|
|
||||||
val mainDevice: Flow<PodDevice?>
|
val mainDevice: Flow<PodDevice?>
|
||||||
get() = devices.map { devices ->
|
get() = devices
|
||||||
devices.maxByOrNull { it.rssi }?.let filter@{ device ->
|
.map { it.determineMainDevice() }
|
||||||
val minimumSignalQuality = generalSettings.minimumSignalQuality.value
|
.replayingShare(appScope)
|
||||||
|
|
||||||
generalSettings.mainDeviceModel.value.let {
|
private fun List<PodDevice>.determineMainDevice(): PodDevice? = this.firstOrNull()?.let {
|
||||||
if (device.model != it && it != PodDevice.Model.UNKNOWN) return@filter null
|
val mainDeviceModel = generalSettings.mainDeviceModel.value
|
||||||
}
|
when {
|
||||||
|
it.model == PodDevice.Model.UNKNOWN -> null
|
||||||
if (device.signalQuality > minimumSignalQuality) device else null
|
mainDeviceModel != PodDevice.Model.UNKNOWN && it.model != mainDeviceModel -> null
|
||||||
}
|
it.signalQuality <= generalSettings.minimumSignalQuality.value -> null
|
||||||
|
else -> it
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = logTag("Monitor", "PodMonitor")
|
private val TAG = logTag("Monitor", "PodMonitor")
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package eu.darken.capod.monitor.core
|
||||||
|
|
||||||
|
import dagger.Reusable
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@Reusable
|
||||||
|
class PodSorter @Inject constructor(
|
||||||
|
|
||||||
|
)
|
||||||
@@ -21,6 +21,7 @@ import eu.darken.capod.main.core.PermissionTool
|
|||||||
import eu.darken.capod.monitor.core.*
|
import eu.darken.capod.monitor.core.*
|
||||||
import eu.darken.capod.monitor.ui.MonitorNotifications
|
import eu.darken.capod.monitor.ui.MonitorNotifications
|
||||||
import eu.darken.capod.reaction.ReactionHub
|
import eu.darken.capod.reaction.ReactionHub
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.cancel
|
import kotlinx.coroutines.cancel
|
||||||
import kotlinx.coroutines.cancelChildren
|
import kotlinx.coroutines.cancelChildren
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@@ -68,9 +69,13 @@ class MonitorWorker @AssistedInject constructor(
|
|||||||
|
|
||||||
Result.success(inputData)
|
Result.success(inputData)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
Bugs.report(tag = TAG, "Execution failed", exception = e)
|
if (e !is CancellationException) {
|
||||||
finishedWithError = true
|
Bugs.report(tag = TAG, "Execution failed", exception = e)
|
||||||
Result.failure(inputData)
|
finishedWithError = true
|
||||||
|
Result.failure(inputData)
|
||||||
|
} else {
|
||||||
|
Result.success()
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
this.workerScope.cancel("Worker finished (withError?=$finishedWithError).")
|
this.workerScope.cancel("Worker finished (withError?=$finishedWithError).")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package eu.darken.capod.monitor.ui
|
package eu.darken.capod.monitor.ui
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.view.View
|
||||||
import android.widget.RemoteViews
|
import android.widget.RemoteViews
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
@@ -26,38 +27,45 @@ class MonitorNotificationViewFactory @Inject constructor(
|
|||||||
context.packageName,
|
context.packageName,
|
||||||
R.layout.monitor_notification_dual_pods_small
|
R.layout.monitor_notification_dual_pods_small
|
||||||
).apply {
|
).apply {
|
||||||
device.getBatteryLevelLeftPod(context)
|
device.apply {
|
||||||
.let { if (device.isLeftPodCharging) "$it⚡" else it }
|
// Left
|
||||||
.run { setTextViewText(R.id.pod_left, this) }
|
setTextViewText(R.id.pod_left_label, getBatteryLevelLeftPod(context))
|
||||||
|
setViewVisibility(R.id.pod_left_charging, if (isLeftPodCharging) View.VISIBLE else View.GONE)
|
||||||
|
setViewVisibility(R.id.pod_left_ear, if (isLeftPodInEar) View.VISIBLE else View.GONE)
|
||||||
|
|
||||||
device.getBatteryLevelCase(context)
|
// Case
|
||||||
.let { if (device.isCaseCharging) "$it⚡" else it }
|
setTextViewText(R.id.pod_case_label, getBatteryLevelCase(context))
|
||||||
.run { setTextViewText(R.id.pod_case, this) }
|
setViewVisibility(R.id.pod_case_charging, if (isCaseCharging) View.VISIBLE else View.GONE)
|
||||||
|
|
||||||
device.getBatteryLevelRightPod(context)
|
// Right
|
||||||
.let { if (device.isRightPodCharging) "$it⚡" else it }
|
setTextViewText(R.id.pod_right_label, getBatteryLevelRightPod(context))
|
||||||
.run { setTextViewText(R.id.pod_right, this) }
|
setViewVisibility(R.id.pod_right_charging, if (isRightPodCharging) View.VISIBLE else View.GONE)
|
||||||
|
setViewVisibility(R.id.pod_right_ear, if (isRightPodInEar) View.VISIBLE else View.GONE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSingleApplePods(device: SingleApplePods): RemoteViews = RemoteViews(
|
private fun createSingleApplePods(device: SingleApplePods): RemoteViews = RemoteViews(
|
||||||
context.packageName,
|
context.packageName,
|
||||||
R.layout.monitor_notification_single_pods_small
|
R.layout.monitor_notification_single_pods_small
|
||||||
).apply {
|
).apply {
|
||||||
setTextViewText(R.id.headphones_label, device.getLabel(context))
|
device.apply {
|
||||||
|
setTextViewText(R.id.headphones_label, getLabel(context))
|
||||||
device.getBatteryLevelHeadset(context)
|
setImageViewResource(R.id.headphones_battery_icon, getBatteryDrawable(batteryHeadsetPercent))
|
||||||
.let { if (!device.isHeadsetBeingCharged) "$it⚡" else it }
|
setTextViewText(R.id.headphones_battery_label, getBatteryLevelHeadset(context))
|
||||||
.run { setTextViewText(R.id.headphones, this) }
|
setViewVisibility(R.id.headphones_charging, if (isHeadsetBeingCharged) View.VISIBLE else View.GONE)
|
||||||
|
setViewVisibility(R.id.headphones_worn, if (isHeadphonesBeingWorn) View.VISIBLE else View.GONE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createSingleBasicApplePods(device: BasicSingleApplePods): RemoteViews = RemoteViews(
|
private fun createSingleBasicApplePods(device: BasicSingleApplePods): RemoteViews = RemoteViews(
|
||||||
context.packageName,
|
context.packageName,
|
||||||
R.layout.monitor_notification_single_pods_basic_small
|
R.layout.monitor_notification_single_pods_basic_small
|
||||||
).apply {
|
).apply {
|
||||||
setTextViewText(R.id.headphones_label, device.getLabel(context))
|
device.apply {
|
||||||
|
setTextViewText(R.id.headphones_label, getLabel(context))
|
||||||
device.getBatteryLevelHeadset(context)
|
setImageViewResource(R.id.headphones_battery_icon, getBatteryDrawable(batteryHeadsetPercent))
|
||||||
.run { setTextViewText(R.id.headphones, this) }
|
setTextViewText(R.id.headphones_battery_label, getBatteryLevelHeadset(context))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createUnknownDevice(device: PodDevice): RemoteViews = RemoteViews(
|
private fun createUnknownDevice(device: PodDevice): RemoteViews = RemoteViews(
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package eu.darken.capod.pods.core
|
||||||
|
|
||||||
|
interface HasDualEarDetection : HasEarDetection {
|
||||||
|
|
||||||
|
val isLeftPodInEar: Boolean
|
||||||
|
|
||||||
|
val isRightPodInEar: Boolean
|
||||||
|
|
||||||
|
val isEitherPodInEar: Boolean
|
||||||
|
get() = isLeftPodInEar || isRightPodInEar
|
||||||
|
|
||||||
|
override val isBeingWorn: Boolean
|
||||||
|
get() = isLeftPodInEar && isRightPodInEar
|
||||||
|
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import eu.darken.capod.common.bluetooth.BleScanResult
|
|||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
interface PodDevice {
|
interface PodDevice {
|
||||||
|
|
||||||
@@ -16,6 +17,9 @@ interface PodDevice {
|
|||||||
|
|
||||||
val model: Model
|
val model: Model
|
||||||
|
|
||||||
|
val address: String
|
||||||
|
get() = scanResult.address
|
||||||
|
|
||||||
val lastSeenAt: Instant
|
val lastSeenAt: Instant
|
||||||
|
|
||||||
val scanResult: BleScanResult
|
val scanResult: BleScanResult
|
||||||
@@ -23,12 +27,14 @@ interface PodDevice {
|
|||||||
val rssi: Int
|
val rssi: Int
|
||||||
get() = scanResult.rssi
|
get() = scanResult.rssi
|
||||||
|
|
||||||
|
val confidence: Float
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is not correct but it works ¯\_(ツ)_/¯
|
* This is not correct but it works ¯\_(ツ)_/¯
|
||||||
* The range of the RSSI is device specific (ROMs).
|
* The range of the RSSI is device specific (ROMs).
|
||||||
*/
|
*/
|
||||||
val signalQuality: Float
|
val signalQuality: Float
|
||||||
get() = (100 - abs(rssi)) / 100f
|
get() = ((100 - abs(rssi)) / 100f) * (max(BASE_CONFIDENCE, confidence))
|
||||||
|
|
||||||
val rawData: ByteArray
|
val rawData: ByteArray
|
||||||
|
|
||||||
@@ -91,4 +97,8 @@ interface PodDevice {
|
|||||||
"Unknown"
|
"Unknown"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val BASE_CONFIDENCE = 0.5f
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.icu.text.RelativeDateTimeFormatter
|
import android.icu.text.RelativeDateTimeFormatter
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
import eu.darken.capod.R
|
import eu.darken.capod.R
|
||||||
import java.time.Duration
|
import java.time.Duration
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -28,6 +29,19 @@ fun PodDevice.getSignalQuality(context: Context): String {
|
|||||||
return "~${percentage.roundToInt()}%"
|
return "~${percentage.roundToInt()}%"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DrawableRes
|
||||||
|
fun getBatteryDrawable(percent: Float?): Int = when {
|
||||||
|
percent == null -> R.drawable.ic_baseline_battery_unknown_24
|
||||||
|
percent > 0.95f -> R.drawable.ic_baseline_battery_full_24
|
||||||
|
percent > 0.80f -> R.drawable.ic_baseline_battery_6_bar_24
|
||||||
|
percent > 0.65f -> R.drawable.ic_baseline_battery_5_bar_24
|
||||||
|
percent > 0.50f -> R.drawable.ic_baseline_battery_4_bar_24
|
||||||
|
percent > 0.35f -> R.drawable.ic_baseline_battery_3_bar_24
|
||||||
|
percent > 0.20f -> R.drawable.ic_baseline_battery_2_bar_24
|
||||||
|
percent > 0.05f -> R.drawable.ic_baseline_battery_1_bar_24
|
||||||
|
else -> R.drawable.ic_baseline_battery_0_bar_24
|
||||||
|
}
|
||||||
|
|
||||||
private val lastSeenFormatter = RelativeDateTimeFormatter.getInstance()
|
private val lastSeenFormatter = RelativeDateTimeFormatter.getInstance()
|
||||||
|
|
||||||
fun PodDevice.lastSeenFormatted(now: Instant): String {
|
fun PodDevice.lastSeenFormatted(now: Instant): String {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import javax.inject.Singleton
|
|||||||
class AppleFactory @Inject constructor(
|
class AppleFactory @Inject constructor(
|
||||||
private val continuityProtocolDecoder: ContinuityProtocol.Decoder,
|
private val continuityProtocolDecoder: ContinuityProtocol.Decoder,
|
||||||
private val proximityPairingDecoder: ProximityPairing.Decoder,
|
private val proximityPairingDecoder: ProximityPairing.Decoder,
|
||||||
private val podFactories: @JvmSuppressWildcards Set<ApplePods.Factory>,
|
private val podFactories: @JvmSuppressWildcards Set<ApplePodsFactory<out ApplePods>>,
|
||||||
private val unknownAppleFactory: UnknownAppleDevice.Factory,
|
private val unknownAppleFactory: UnknownAppleDevice.Factory,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -57,24 +57,27 @@ class AppleFactory @Inject constructor(
|
|||||||
|
|
||||||
log(TAG, INFO) { "Decoding $scanResult" }
|
log(TAG, INFO) { "Decoding $scanResult" }
|
||||||
|
|
||||||
var factory = podFactories.firstOrNull { it.isResponsible(pm) }
|
val factory = podFactories.firstOrNull { it.isResponsible(pm) }
|
||||||
|
|
||||||
if (factory == null) {
|
val device = (factory ?: unknownAppleFactory).create(
|
||||||
|
scanResult = scanResult,
|
||||||
|
proximityMessage = pm,
|
||||||
|
)
|
||||||
|
|
||||||
|
if (factory == null && !SILENCED_PMS.contains(device.rawDeviceModel) && scanResult.address != "6E:9E:D1:49:D2:6D") {
|
||||||
|
SILENCED_PMS.add(device.rawDeviceModel)
|
||||||
Bugs.report(
|
Bugs.report(
|
||||||
tag = TAG,
|
tag = TAG,
|
||||||
message = "Unknown proximity message type",
|
message = "Unknown proximity message type",
|
||||||
exception = IllegalArgumentException("Unknown ProximityMessage: $pm")
|
exception = IllegalArgumentException("Unknown ProximityMessage: $pm")
|
||||||
)
|
)
|
||||||
factory = unknownAppleFactory
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return factory.create(
|
return@withLock device
|
||||||
scanResult = scanResult,
|
|
||||||
proximityMessage = pm,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private val SILENCED_PMS = mutableSetOf<UShort>()
|
||||||
private val TAG = logTag("Pod", "Apple", "Factory")
|
private val TAG = logTag("Pod", "Apple", "Factory")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,17 +12,17 @@ import eu.darken.capod.pods.core.apple.beats.*
|
|||||||
@Module
|
@Module
|
||||||
abstract class AppleFactoryModule {
|
abstract class AppleFactoryModule {
|
||||||
|
|
||||||
@Binds @IntoSet abstract fun airPodsGen1(factory: AirPodsGen1.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun airPodsGen1(factory: AirPodsGen1.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun airPodsGen2(factory: AirPodsGen2.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun airPodsGen2(factory: AirPodsGen2.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun airPodsGen3(factory: AirPodsGen3.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun airPodsGen3(factory: AirPodsGen3.Factory): ApplePodsFactory<out ApplePods>
|
||||||
|
|
||||||
@Binds @IntoSet abstract fun airPodsPro(factory: AirPodsPro.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun airPodsPro(factory: AirPodsPro.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun airPodsMax(factory: AirPodsMax.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun airPodsMax(factory: AirPodsMax.Factory): ApplePodsFactory<out ApplePods>
|
||||||
|
|
||||||
@Binds @IntoSet abstract fun beatsFlex(factory: BeatsFlex.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun beatsFlex(factory: BeatsFlex.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun beatsSolo3(factory: BeatsSolo3.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun beatsSolo3(factory: BeatsSolo3.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun beatsStudio3(factory: BeatsStudio3.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun beatsStudio3(factory: BeatsStudio3.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun beatsX(factory: BeatsX.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun beatsX(factory: BeatsX.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun powerBeats3(factory: PowerBeats3.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun powerBeats3(factory: PowerBeats3.Factory): ApplePodsFactory<out ApplePods>
|
||||||
@Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePods.Factory
|
@Binds @IntoSet abstract fun powerBeatsPro(factory: PowerBeatsPro.Factory): ApplePodsFactory<out ApplePods>
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
package eu.darken.capod.pods.core.apple
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
import eu.darken.capod.common.SystemClockWrap
|
import eu.darken.capod.common.lowerNibble
|
||||||
import eu.darken.capod.common.bluetooth.BleScanResult
|
import eu.darken.capod.common.toHex
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.DEBUG
|
import eu.darken.capod.common.upperNibble
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
|
||||||
import eu.darken.capod.common.debug.logging.log
|
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Duration
|
|
||||||
|
|
||||||
interface ApplePods : PodDevice {
|
interface ApplePods : PodDevice {
|
||||||
|
|
||||||
@@ -27,11 +24,20 @@ interface ApplePods : PodDevice {
|
|||||||
val rawStatus: UByte
|
val rawStatus: UByte
|
||||||
get() = proximityMessage.data[3]
|
get() = proximityMessage.data[3]
|
||||||
|
|
||||||
|
val rawStatusHex: String
|
||||||
|
get() = rawStatus.toHex()
|
||||||
|
|
||||||
val rawPodsBattery: UByte
|
val rawPodsBattery: UByte
|
||||||
get() = proximityMessage.data[4]
|
get() = proximityMessage.data[4]
|
||||||
|
|
||||||
val rawCaseBattery: UByte
|
val rawPodsBatteryHex: String
|
||||||
get() = proximityMessage.data[5]
|
get() = rawPodsBattery.toHex()
|
||||||
|
|
||||||
|
val rawFlags: UShort
|
||||||
|
get() = proximityMessage.data[5].upperNibble
|
||||||
|
|
||||||
|
val rawCaseBattery: UShort
|
||||||
|
get() = proximityMessage.data[5].lowerNibble
|
||||||
|
|
||||||
val rawCaseLidState: UByte
|
val rawCaseLidState: UByte
|
||||||
get() = proximityMessage.data[6]
|
get() = proximityMessage.data[6]
|
||||||
@@ -43,102 +49,4 @@ interface ApplePods : PodDevice {
|
|||||||
get() = proximityMessage.data[8]
|
get() = proximityMessage.data[8]
|
||||||
|
|
||||||
|
|
||||||
abstract class Factory(private val tag: String) {
|
|
||||||
|
|
||||||
internal data class ValueCache(
|
|
||||||
val caseBatteryPercentage: Float?
|
|
||||||
)
|
|
||||||
|
|
||||||
internal val cachedValues = mutableMapOf<PodDevice.Id, ValueCache>()
|
|
||||||
|
|
||||||
internal data class KnownDevice(
|
|
||||||
val identifier: PodDevice.Id,
|
|
||||||
val scanResult: BleScanResult,
|
|
||||||
val message: ProximityPairing.Message,
|
|
||||||
) {
|
|
||||||
val address: String
|
|
||||||
get() = scanResult.address
|
|
||||||
|
|
||||||
val rssi: Int
|
|
||||||
get() = scanResult.rssi
|
|
||||||
|
|
||||||
val timestampNanos: Duration
|
|
||||||
get() = Duration.ofNanos(scanResult.generatedAtNanos)
|
|
||||||
|
|
||||||
fun isOlderThan(age: Duration): Boolean {
|
|
||||||
val now = Duration.ofNanos(SystemClockWrap.elapsedRealtimeNanos)
|
|
||||||
return now - timestampNanos > age
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val knownDevs = mutableMapOf<PodDevice.Id, KnownDevice>()
|
|
||||||
|
|
||||||
internal fun recognizeDevice(scanResult: BleScanResult, message: ProximityPairing.Message): PodDevice.Id {
|
|
||||||
val address = scanResult.address
|
|
||||||
|
|
||||||
var identifier: PodDevice.Id? = null
|
|
||||||
|
|
||||||
knownDevs.values.firstOrNull { it.address == address }?.let {
|
|
||||||
log(tag, VERBOSE) { "recognizeDevice: Recovered previous ID via address: $it" }
|
|
||||||
knownDevs[it.identifier] = it.copy(
|
|
||||||
scanResult = scanResult,
|
|
||||||
message = message,
|
|
||||||
)
|
|
||||||
identifier = it.identifier
|
|
||||||
}
|
|
||||||
|
|
||||||
if (identifier == null) {
|
|
||||||
val currentMarkers = message.getRecogMarkers()
|
|
||||||
knownDevs.values
|
|
||||||
.firstOrNull { it.message.getRecogMarkers() == currentMarkers }
|
|
||||||
?.let {
|
|
||||||
log(tag, DEBUG) { "recognizeDevice: Close match based similarity markers." }
|
|
||||||
log(tag, DEBUG) { "recognizeDevice: Old marker: ${it.message.getRecogMarkers()}" }
|
|
||||||
log(tag, DEBUG) { "recognizeDevice: New marker: $currentMarkers" }
|
|
||||||
knownDevs[it.identifier] = it.copy(
|
|
||||||
scanResult = scanResult,
|
|
||||||
message = message,
|
|
||||||
)
|
|
||||||
identifier = it.identifier
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (identifier == null) {
|
|
||||||
log(tag, VERBOSE) { "recognizeDevice: Mapping as new device" }
|
|
||||||
identifier = PodDevice.Id()
|
|
||||||
}
|
|
||||||
|
|
||||||
knownDevs[identifier!!] = KnownDevice(
|
|
||||||
identifier = identifier!!,
|
|
||||||
scanResult = scanResult,
|
|
||||||
message = message
|
|
||||||
)
|
|
||||||
|
|
||||||
knownDevs.values.toList().forEach { knownDevice ->
|
|
||||||
if (knownDevice.isOlderThan(Duration.ofSeconds(20))) {
|
|
||||||
log(tag, VERBOSE) { "recognizeDevice: Removing stale known device: $knownDevice" }
|
|
||||||
knownDevs.remove(knownDevice.identifier)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return identifier!!
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ModelInfo(
|
|
||||||
val full: UShort,
|
|
||||||
val dirty: UByte,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun ProximityPairing.Message.getModelInfo(): ModelInfo = ModelInfo(
|
|
||||||
full = (((data[1].toInt() and 255) shl 8) or (data[2].toInt() and 255)).toUShort(),
|
|
||||||
dirty = data[1]
|
|
||||||
)
|
|
||||||
|
|
||||||
abstract fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean
|
|
||||||
|
|
||||||
abstract fun create(
|
|
||||||
scanResult: BleScanResult,
|
|
||||||
proximityMessage: ProximityPairing.Message,
|
|
||||||
): ApplePods
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
|
import eu.darken.capod.common.SystemClockWrap
|
||||||
|
import eu.darken.capod.common.bluetooth.BleScanResult
|
||||||
|
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
|
import eu.darken.capod.common.lowerNibble
|
||||||
|
import eu.darken.capod.common.upperNibble
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
|
import java.time.Duration
|
||||||
|
|
||||||
|
abstract class ApplePodsFactory<PodType : ApplePods>(private val tag: String) {
|
||||||
|
|
||||||
|
data class Markings(
|
||||||
|
val vendor: UByte,
|
||||||
|
val length: UByte,
|
||||||
|
val device: UShort,
|
||||||
|
val podBatteryData: Set<UShort>,
|
||||||
|
val caseBatteryData: UShort,
|
||||||
|
val deviceColor: UByte,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun ProximityPairing.Message.getApplePodsMarkings(): Markings = Markings(
|
||||||
|
vendor = ProximityPairing.CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING,
|
||||||
|
length = ProximityPairing.PROXIMITY_PAIRING_MESSAGE_LENGTH.toUByte(),
|
||||||
|
device = (((data[1].toInt() and 255) shl 8) or (data[2].toInt() and 255)).toUShort(),
|
||||||
|
// Make comparison order independent
|
||||||
|
podBatteryData = setOf(data[4].upperNibble, data[4].lowerNibble),
|
||||||
|
caseBatteryData = data[5].lowerNibble,
|
||||||
|
deviceColor = data[7]
|
||||||
|
)
|
||||||
|
|
||||||
|
data class KnownDevice(
|
||||||
|
val id: PodDevice.Id,
|
||||||
|
val history: List<ApplePods>
|
||||||
|
) {
|
||||||
|
val lastMessage: ProximityPairing.Message
|
||||||
|
get() = history.last().proximityMessage
|
||||||
|
|
||||||
|
val lastAddress: String
|
||||||
|
get() = history.last().address
|
||||||
|
|
||||||
|
val confidence: Float
|
||||||
|
get() = 0.75f + (history.size / (MAX_HISTORY * 4f))
|
||||||
|
|
||||||
|
fun averageRssi(latest: Int): Int =
|
||||||
|
history.map { it.rssi }.plus(latest).takeLast(10).average().toInt()
|
||||||
|
|
||||||
|
val timestampNanos: Duration
|
||||||
|
get() = Duration.ofNanos(history.last().scanResult.generatedAtNanos)
|
||||||
|
|
||||||
|
fun isOlderThan(age: Duration): Boolean {
|
||||||
|
val now = Duration.ofNanos(SystemClockWrap.elapsedRealtimeNanos)
|
||||||
|
return now - timestampNanos > age
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = "KnownDevice(history=${history.size}, last=${history.last()})"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val MAX_HISTORY = 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val knownDevices = mutableMapOf<PodDevice.Id, KnownDevice>()
|
||||||
|
|
||||||
|
internal open fun searchHistory(current: PodType): KnownDevice? {
|
||||||
|
val scanResult = current.scanResult
|
||||||
|
val message = current.proximityMessage
|
||||||
|
|
||||||
|
knownDevices.values.toList().forEach { knownDevice ->
|
||||||
|
if (knownDevice.isOlderThan(Duration.ofSeconds(20))) {
|
||||||
|
log(tag, VERBOSE) { "searchHistory1: Removing stale known device: $knownDevice" }
|
||||||
|
knownDevices.remove(knownDevice.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
knownDevices.values
|
||||||
|
.filter { it.history.size > KnownDevice.MAX_HISTORY }
|
||||||
|
.toList()
|
||||||
|
.forEach {
|
||||||
|
knownDevices[it.id] = it.copy(history = it.history.takeLast(KnownDevice.MAX_HISTORY))
|
||||||
|
}
|
||||||
|
|
||||||
|
var recognizedDevice: KnownDevice? = knownDevices.values
|
||||||
|
.firstOrNull { it.lastAddress == scanResult.address }
|
||||||
|
?.also { log(tag, VERBOSE) { "searchHistory1: Recovered previous ID via address: $it" } }
|
||||||
|
|
||||||
|
if (recognizedDevice == null) {
|
||||||
|
val currentMarkers = message.getApplePodsMarkings()
|
||||||
|
recognizedDevice = knownDevices.values
|
||||||
|
.firstOrNull { it.lastMessage.getApplePodsMarkings() == currentMarkers }
|
||||||
|
?.also { log(tag) { "searchHistory1: Close match based on similarity: $currentMarkers" } }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recognizedDevice == null) {
|
||||||
|
log(tag) { "searchHistory1: Didn't recognize: $message" }
|
||||||
|
}
|
||||||
|
|
||||||
|
return recognizedDevice
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateHistory(device: PodType) {
|
||||||
|
val existing = knownDevices[device.identifier]
|
||||||
|
|
||||||
|
knownDevices[device.identifier] = when {
|
||||||
|
existing != null -> {
|
||||||
|
existing.copy(history = existing.history.plus(device))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
log(tag) { "searchHistory1: Creating new history for $device" }
|
||||||
|
KnownDevice(
|
||||||
|
id = device.identifier,
|
||||||
|
history = listOf(device)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ModelInfo(
|
||||||
|
val full: UShort,
|
||||||
|
val dirty: UByte,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun ProximityPairing.Message.getModelInfo(): ModelInfo = ModelInfo(
|
||||||
|
full = (((data[1].toInt() and 255) shl 8) or (data[2].toInt() and 255)).toUShort(),
|
||||||
|
dirty = data[1]
|
||||||
|
)
|
||||||
|
|
||||||
|
abstract fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean
|
||||||
|
|
||||||
|
abstract fun create(
|
||||||
|
scanResult: BleScanResult,
|
||||||
|
proximityMessage: ProximityPairing.Message,
|
||||||
|
): ApplePods
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
|
abstract class BasicSingleApplePodsFactory(private val tag: String) : ApplePodsFactory<BasicSingleApplePods>(tag)
|
||||||
@@ -8,23 +8,30 @@ import eu.darken.capod.common.isBitSet
|
|||||||
import eu.darken.capod.common.lowerNibble
|
import eu.darken.capod.common.lowerNibble
|
||||||
import eu.darken.capod.common.upperNibble
|
import eu.darken.capod.common.upperNibble
|
||||||
import eu.darken.capod.pods.core.HasCase
|
import eu.darken.capod.pods.core.HasCase
|
||||||
|
import eu.darken.capod.pods.core.HasDualEarDetection
|
||||||
import eu.darken.capod.pods.core.HasDualPods
|
import eu.darken.capod.pods.core.HasDualPods
|
||||||
import eu.darken.capod.pods.core.HasDualPods.Pod
|
import eu.darken.capod.pods.core.HasDualPods.Pod
|
||||||
import eu.darken.capod.pods.core.HasEarDetection
|
|
||||||
|
|
||||||
interface DualApplePods : ApplePods, HasDualPods, HasEarDetection, HasCase {
|
interface DualApplePods : ApplePods, HasDualPods, HasDualEarDetection, HasCase {
|
||||||
|
|
||||||
val microPhonePod: Pod
|
val primaryPod: Pod
|
||||||
get() = when (rawStatus.isBitSet(5)) {
|
get() = when (rawStatus.isBitSet(5)) {
|
||||||
true -> Pod.LEFT
|
true -> Pod.LEFT
|
||||||
false -> Pod.RIGHT
|
false -> Pod.RIGHT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normally values for the left pod are in the lower nibbles, if the left pod is primary (microphone)
|
||||||
|
* If the right pod is the primary, the values are flipped.
|
||||||
|
*/
|
||||||
|
val areValuesFlipped: Boolean
|
||||||
|
get() = !rawStatus.isBitSet(5)
|
||||||
|
|
||||||
override val batteryLeftPodPercent: Float?
|
override val batteryLeftPodPercent: Float?
|
||||||
get() {
|
get() {
|
||||||
val value = when (microPhonePod) {
|
val value = when (areValuesFlipped) {
|
||||||
Pod.LEFT -> rawPodsBattery.lowerNibble.toInt()
|
true -> rawPodsBattery.upperNibble.toInt()
|
||||||
Pod.RIGHT -> rawPodsBattery.upperNibble.toInt()
|
false -> rawPodsBattery.lowerNibble.toInt()
|
||||||
}
|
}
|
||||||
return when (value) {
|
return when (value) {
|
||||||
15 -> null
|
15 -> null
|
||||||
@@ -39,9 +46,9 @@ interface DualApplePods : ApplePods, HasDualPods, HasEarDetection, HasCase {
|
|||||||
|
|
||||||
override val batteryRightPodPercent: Float?
|
override val batteryRightPodPercent: Float?
|
||||||
get() {
|
get() {
|
||||||
val value = when (microPhonePod) {
|
val value = when (areValuesFlipped) {
|
||||||
Pod.LEFT -> rawPodsBattery.upperNibble.toInt()
|
true -> rawPodsBattery.lowerNibble.toInt()
|
||||||
Pod.RIGHT -> rawPodsBattery.lowerNibble.toInt()
|
false -> rawPodsBattery.upperNibble.toInt()
|
||||||
}
|
}
|
||||||
return when (value) {
|
return when (value) {
|
||||||
15 -> null
|
15 -> null
|
||||||
@@ -54,35 +61,55 @@ interface DualApplePods : ApplePods, HasDualPods, HasEarDetection, HasCase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val isLeftPodInEar: Boolean
|
val isThisPodInThecase: Boolean
|
||||||
get() = when (microPhonePod) {
|
get() = rawStatus.isBitSet(6)
|
||||||
Pod.LEFT -> rawStatus.isBitSet(1)
|
|
||||||
Pod.RIGHT -> rawStatus.isBitSet(3)
|
val isOnePodInCase: Boolean
|
||||||
|
get() = rawStatus.isBitSet(4)
|
||||||
|
|
||||||
|
val areBothPodsInCase: Boolean
|
||||||
|
get() = rawStatus.isBitSet(2)
|
||||||
|
|
||||||
|
override val isLeftPodInEar: Boolean
|
||||||
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
|
true -> rawStatus.isBitSet(3)
|
||||||
|
false -> rawStatus.isBitSet(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
val isRightPodInEar: Boolean
|
override val isRightPodInEar: Boolean
|
||||||
get() = when (microPhonePod) {
|
get() = when (areValuesFlipped xor isThisPodInThecase) {
|
||||||
Pod.LEFT -> rawStatus.isBitSet(3)
|
true -> rawStatus.isBitSet(1)
|
||||||
Pod.RIGHT -> rawStatus.isBitSet(1)
|
false -> rawStatus.isBitSet(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val isBeingWorn: Boolean
|
/**
|
||||||
get() = isLeftPodInEar && isRightPodInEar
|
* The data flip bit is set if the left pod is primary.
|
||||||
|
* For the pod that is in the case, this is flipped again though.
|
||||||
|
*/
|
||||||
|
val isLeftPodMicrophone: Boolean
|
||||||
|
get() = rawStatus.isBitSet(5) xor isThisPodInThecase
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The data flip bit is UNset if the right pod is primary.
|
||||||
|
* For the pod that is in the case, this is flipped again though.
|
||||||
|
*/
|
||||||
|
val isRightPodMicrophone: Boolean
|
||||||
|
get() = !rawStatus.isBitSet(5) xor isThisPodInThecase
|
||||||
|
|
||||||
val isLeftPodCharging: Boolean
|
val isLeftPodCharging: Boolean
|
||||||
get() = when (microPhonePod) {
|
get() = when (areValuesFlipped) {
|
||||||
Pod.LEFT -> rawCaseBattery.upperNibble.isBitSet(0)
|
false -> rawFlags.isBitSet(0)
|
||||||
Pod.RIGHT -> rawCaseBattery.upperNibble.isBitSet(1)
|
true -> rawFlags.isBitSet(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
val isRightPodCharging: Boolean
|
val isRightPodCharging: Boolean
|
||||||
get() = when (microPhonePod) {
|
get() = when (areValuesFlipped) {
|
||||||
Pod.LEFT -> rawCaseBattery.upperNibble.isBitSet(1)
|
false -> rawFlags.isBitSet(1)
|
||||||
Pod.RIGHT -> rawCaseBattery.upperNibble.isBitSet(0)
|
true -> rawFlags.isBitSet(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = when (val value = rawCaseBattery.lowerNibble.toInt()) {
|
get() = when (val value = rawCaseBattery.toInt()) {
|
||||||
15 -> null
|
15 -> null
|
||||||
else -> if (value > 10) {
|
else -> if (value > 10) {
|
||||||
log { "Case: Above 100% battery: $value" }
|
log { "Case: Above 100% battery: $value" }
|
||||||
@@ -93,11 +120,12 @@ interface DualApplePods : ApplePods, HasDualPods, HasEarDetection, HasCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override val isCaseCharging: Boolean
|
override val isCaseCharging: Boolean
|
||||||
get() = rawCaseBattery.upperNibble.isBitSet(2)
|
get() = rawFlags.isBitSet(2)
|
||||||
|
|
||||||
val caseLidState: LidState
|
val caseLidState: LidState
|
||||||
get() {
|
get() {
|
||||||
return LidState.values().firstOrNull { it.rawRange.contains(rawCaseLidState.toInt()) } ?: LidState.UNKNOWN
|
val rawstate = rawCaseLidState
|
||||||
|
return LidState.values().firstOrNull { it.rawRange.contains(rawstate.toInt()) } ?: LidState.UNKNOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -118,27 +146,24 @@ interface DualApplePods : ApplePods, HasDualPods, HasEarDetection, HasCase {
|
|||||||
val deviceColor: DeviceColor
|
val deviceColor: DeviceColor
|
||||||
get() = DeviceColor.values().firstOrNull { it.raw == rawDeviceColor } ?: DeviceColor.UNKNOWN
|
get() = DeviceColor.values().firstOrNull { it.raw == rawDeviceColor } ?: DeviceColor.UNKNOWN
|
||||||
|
|
||||||
|
enum class DeviceColor(val raw: UByte?) {
|
||||||
|
|
||||||
fun getDeviceColorLabel(context: Context): String = context.getString(deviceColor.labelRes)
|
WHITE(0x00),
|
||||||
|
BLACK(0x01),
|
||||||
|
RED(0x02),
|
||||||
|
BLUE(0x03),
|
||||||
|
PINK(0x04),
|
||||||
|
GRAY(0x05),
|
||||||
|
SILVER(0x06),
|
||||||
|
GOLD(0x07),
|
||||||
|
ROSE_GOLD(0x08),
|
||||||
|
SPACE_GRAY(0x09),
|
||||||
|
DARK_BLUE(0x0a),
|
||||||
|
LIGHT_BLUE(0x0b),
|
||||||
|
YELLOW(0x0c),
|
||||||
|
UNKNOWN(null);
|
||||||
|
|
||||||
enum class DeviceColor(val raw: UByte?, @StringRes val labelRes: Int) {
|
constructor(raw: Int) : this(raw.toUByte())
|
||||||
|
|
||||||
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
|
val connectionState: ConnectionState
|
||||||
@@ -157,4 +182,5 @@ interface DualApplePods : ApplePods, HasDualPods, HasEarDetection, HasCase {
|
|||||||
|
|
||||||
constructor(raw: Int, @StringRes labelRes: Int) : this(raw.toUByte(), labelRes)
|
constructor(raw: Int, @StringRes labelRes: Int) : this(raw.toUByte(), labelRes)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
|
import eu.darken.capod.common.debug.logging.Logging.Priority.DEBUG
|
||||||
|
import eu.darken.capod.common.debug.logging.log
|
||||||
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
|
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro
|
||||||
|
|
||||||
|
abstract class DualApplePodsFactory(private val tag: String) : ApplePodsFactory<DualApplePods>(tag) {
|
||||||
|
|
||||||
|
fun DualApplePods.getCaseMatchMarkings() = SplitPodsMarkings(
|
||||||
|
leftPodBattery = batteryLeftPodPercent,
|
||||||
|
rightPodBattery = batteryRightPodPercent,
|
||||||
|
microPhoneLeft = isLeftPodMicrophone,
|
||||||
|
microPhoneRight = isRightPodMicrophone,
|
||||||
|
chargingLeft = isLeftPodCharging,
|
||||||
|
chargingRight = isRightPodCharging,
|
||||||
|
color = deviceColor,
|
||||||
|
model = model
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split pods, one in case, one not.
|
||||||
|
*/
|
||||||
|
data class SplitPodsMarkings(
|
||||||
|
val leftPodBattery: Float?,
|
||||||
|
val rightPodBattery: Float?,
|
||||||
|
val microPhoneLeft: Boolean,
|
||||||
|
val microPhoneRight: Boolean,
|
||||||
|
val chargingLeft: Boolean,
|
||||||
|
val chargingRight: Boolean,
|
||||||
|
val color: DualApplePods.DeviceColor,
|
||||||
|
val model: PodDevice.Model,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun Collection<KnownDevice>.findSplitPodsMatch(device: DualApplePods): Collection<KnownDevice> {
|
||||||
|
val target = device.getCaseMatchMarkings()
|
||||||
|
|
||||||
|
return filter { known ->
|
||||||
|
known.history
|
||||||
|
.filterIsInstance<DualApplePods>()
|
||||||
|
.any { it.getCaseMatchMarkings() == target }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun KnownDevice.getLatestCaseBattery(): Float? = history
|
||||||
|
.filterIsInstance<DualApplePods>()
|
||||||
|
.mapNotNull { it.batteryCasePercent }
|
||||||
|
.lastOrNull()
|
||||||
|
|
||||||
|
fun KnownDevice.getLatestCaseLidState(basic: DualApplePods): DualApplePods.LidState? {
|
||||||
|
val definitive = setOf(DualApplePods.LidState.OPEN, DualApplePods.LidState.CLOSED)
|
||||||
|
if (definitive.contains(basic.caseLidState)) return null
|
||||||
|
|
||||||
|
return history
|
||||||
|
.filterIsInstance<AirPodsPro>()
|
||||||
|
.lastOrNull { it.caseLidState != DualApplePods.LidState.NOT_IN_CASE }
|
||||||
|
?.caseLidState
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchHistory(current: DualApplePods): KnownDevice? {
|
||||||
|
val basicResult = super.searchHistory(current)
|
||||||
|
|
||||||
|
val caseIgnored = knownDevices.values.findSplitPodsMatch(current)
|
||||||
|
|
||||||
|
log(tag, DEBUG) { "searchHistory2: Case ignored matches(${caseIgnored.size}): $caseIgnored" }
|
||||||
|
|
||||||
|
return when (caseIgnored.size) {
|
||||||
|
0 -> basicResult
|
||||||
|
1 -> caseIgnored.single()
|
||||||
|
else -> {
|
||||||
|
log(tag) { "searchHistory2: More than one result when ignoring case markers." }
|
||||||
|
val oldest = caseIgnored.maxByOrNull { it.history.size } ?: return null
|
||||||
|
|
||||||
|
caseIgnored.minus(oldest).forEach {
|
||||||
|
log(tag) { "searchHistory2: Removing outlier: $it" }
|
||||||
|
knownDevices.remove(it.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
oldest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package eu.darken.capod.pods.core.apple
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
import eu.darken.capod.common.isBitSet
|
import eu.darken.capod.common.isBitSet
|
||||||
import eu.darken.capod.common.upperNibble
|
|
||||||
import eu.darken.capod.pods.core.HasEarDetection
|
import eu.darken.capod.pods.core.HasEarDetection
|
||||||
import eu.darken.capod.pods.core.HasSinglePod
|
import eu.darken.capod.pods.core.HasSinglePod
|
||||||
|
|
||||||
@@ -11,7 +10,7 @@ interface SingleApplePods : BasicSingleApplePods, HasEarDetection, HasSinglePod
|
|||||||
get() = rawStatus.isBitSet(1)
|
get() = rawStatus.isBitSet(1)
|
||||||
|
|
||||||
val isHeadsetBeingCharged: Boolean
|
val isHeadsetBeingCharged: Boolean
|
||||||
get() = rawCaseBattery.upperNibble.isBitSet(0)
|
get() = rawFlags.isBitSet(0)
|
||||||
|
|
||||||
override val isBeingWorn: Boolean
|
override val isBeingWorn: Boolean
|
||||||
get() = isHeadphonesBeingWorn
|
get() = isHeadphonesBeingWorn
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
|
abstract class SingleApplePodsFactory(private val tag: String) : ApplePodsFactory<SingleApplePods>(tag)
|
||||||
@@ -13,26 +13,36 @@ data class UnknownAppleDevice(
|
|||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val lastSeenAt: Instant = Instant.now(),
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
|
override val confidence: Float = 0f,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
) : ApplePods {
|
) : ApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.UNKNOWN
|
override val model: PodDevice.Model = PodDevice.Model.UNKNOWN
|
||||||
|
|
||||||
override fun getLabel(context: Context): String = context.getString(R.string.pods_unknown_label)
|
override fun getLabel(context: Context): String = context.getString(R.string.pods_unknown_label)
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : ApplePodsFactory<ApplePods>(TAG) {
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean = true
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean = true
|
||||||
|
|
||||||
override fun create(
|
override fun create(
|
||||||
scanResult: BleScanResult,
|
scanResult: BleScanResult,
|
||||||
proximityMessage: ProximityPairing.Message,
|
proximityMessage: ProximityPairing.Message,
|
||||||
): ApplePods {
|
): ApplePods {
|
||||||
val identifier = recognizeDevice(scanResult, proximityMessage)
|
val basic = UnknownAppleDevice(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return UnknownAppleDevice(
|
updateHistory(basic)
|
||||||
identifier = identifier,
|
|
||||||
scanResult = scanResult,
|
if (result == null) return basic
|
||||||
proximityMessage = proximityMessage,
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -14,7 +15,10 @@ data class AirPodsGen1 constructor(
|
|||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val lastSeenAt: Instant = Instant.now(),
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
private val cachedBatteryPercentage: Float?
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
) : DualApplePods {
|
) : DualApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN1
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN1
|
||||||
@@ -22,26 +26,32 @@ data class AirPodsGen1 constructor(
|
|||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val caseLidState: DualApplePods.LidState
|
||||||
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||||
val identifier = recognizeDevice(scanResult, proximityMessage)
|
val basic = AirPodsGen1(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
val device = AirPodsGen1(
|
updateHistory(basic)
|
||||||
identifier = identifier,
|
|
||||||
scanResult = scanResult,
|
if (result == null) return basic
|
||||||
proximityMessage = proximityMessage,
|
|
||||||
cachedBatteryPercentage = cachedValues[identifier]?.caseBatteryPercentage
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
confidence = result.confidence,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
|
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||||
)
|
)
|
||||||
|
|
||||||
cachedValues[identifier] = ValueCache(
|
|
||||||
caseBatteryPercentage = device.batteryCasePercent
|
|
||||||
)
|
|
||||||
|
|
||||||
return device
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -14,7 +15,10 @@ data class AirPodsGen2 constructor(
|
|||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val lastSeenAt: Instant = Instant.now(),
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
private val cachedBatteryPercentage: Float?,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
) : DualApplePods {
|
) : DualApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN2
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN2
|
||||||
@@ -22,26 +26,32 @@ data class AirPodsGen2 constructor(
|
|||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val caseLidState: DualApplePods.LidState
|
||||||
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||||
val identifier = recognizeDevice(scanResult, proximityMessage)
|
val basic = AirPodsGen2(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
val device = AirPodsGen2(
|
updateHistory(basic)
|
||||||
identifier = identifier,
|
|
||||||
scanResult = scanResult,
|
if (result == null) return basic
|
||||||
proximityMessage = proximityMessage,
|
|
||||||
cachedBatteryPercentage = cachedValues[identifier]?.caseBatteryPercentage
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
confidence = result.confidence,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
|
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||||
)
|
)
|
||||||
|
|
||||||
cachedValues[identifier] = ValueCache(
|
|
||||||
caseBatteryPercentage = device.batteryCasePercent
|
|
||||||
)
|
|
||||||
|
|
||||||
return device
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -14,33 +15,42 @@ data class AirPodsGen3 constructor(
|
|||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val lastSeenAt: Instant = Instant.now(),
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
private val cachedBatteryPercentage: Float?,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
) : DualApplePods {
|
) : DualApplePods {
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN3
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_GEN3
|
||||||
|
|
||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val caseLidState: DualApplePods.LidState
|
||||||
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||||
val identifier = recognizeDevice(scanResult, proximityMessage)
|
val basic = AirPodsGen3(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
val device = AirPodsGen3(
|
updateHistory(basic)
|
||||||
identifier = identifier,
|
|
||||||
scanResult = scanResult,
|
if (result == null) return basic
|
||||||
proximityMessage = proximityMessage,
|
|
||||||
cachedBatteryPercentage = cachedValues[identifier]?.caseBatteryPercentage
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
confidence = result.confidence,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
|
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||||
)
|
)
|
||||||
|
|
||||||
cachedValues[identifier] = ValueCache(
|
|
||||||
caseBatteryPercentage = device.batteryCasePercent
|
|
||||||
)
|
|
||||||
|
|
||||||
return device
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.SingleApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -14,22 +15,32 @@ data class AirPodsMax(
|
|||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val lastSeenAt: Instant = Instant.now(),
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
) : SingleApplePods {
|
) : SingleApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_MAX
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : SingleApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY
|
proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||||
val identifier = recognizeDevice(scanResult, proximityMessage)
|
val basic = AirPodsMax(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return AirPodsMax(
|
updateHistory(basic)
|
||||||
identifier = identifier,
|
|
||||||
scanResult = scanResult,
|
if (result == null) return basic
|
||||||
proximityMessage = proximityMessage,
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePods.LidState
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -14,7 +16,10 @@ data class AirPodsPro(
|
|||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val lastSeenAt: Instant = Instant.now(),
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
private val cachedBatteryPercentage: Float?,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
private val cachedCaseState: LidState? = null
|
||||||
) : DualApplePods {
|
) : DualApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO
|
override val model: PodDevice.Model = PodDevice.Model.AIRPODS_PRO
|
||||||
@@ -22,32 +27,38 @@ data class AirPodsPro(
|
|||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val caseLidState: LidState
|
||||||
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||||
val identifier = recognizeDevice(scanResult, proximityMessage)
|
var basic = AirPodsPro(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
val device = AirPodsPro(
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
identifier = identifier,
|
updateHistory(basic)
|
||||||
scanResult = scanResult,
|
|
||||||
proximityMessage = proximityMessage,
|
if (result == null) return basic
|
||||||
cachedBatteryPercentage = cachedValues[identifier]?.caseBatteryPercentage
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
confidence = result.confidence,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
|
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||||
)
|
)
|
||||||
|
|
||||||
cachedValues[identifier] = ValueCache(
|
|
||||||
caseBatteryPercentage = device.batteryCasePercent
|
|
||||||
)
|
|
||||||
|
|
||||||
return device
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
companion object {
|
||||||
|
private val DEVICE_CODE = 0x0e20.toUShort()
|
||||||
companion object {
|
private val TAG = logTag("PodDevice", "Apple", "AirPods", "Pro", "Factory")
|
||||||
private val DEVICE_CODE = 0x0e20.toUShort()
|
}
|
||||||
private val TAG = logTag("PodDevice", "Apple", "AirPods", "Pro")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -13,23 +14,33 @@ data class BeatsFlex(
|
|||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val lastSeenAt: Instant = Instant.now(),
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
) : BasicSingleApplePods {
|
) : BasicSingleApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_FLEX
|
override val model: PodDevice.Model = PodDevice.Model.BEATS_FLEX
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||||
val identifier = recognizeDevice(scanResult, proximityMessage)
|
val basic = BeatsFlex(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return BeatsFlex(
|
updateHistory(basic)
|
||||||
identifier = identifier,
|
|
||||||
scanResult = scanResult,
|
if (result == null) return basic
|
||||||
proximityMessage = proximityMessage,
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -13,23 +14,32 @@ data class BeatsSolo3(
|
|||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val lastSeenAt: Instant = Instant.now(),
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
) : BasicSingleApplePods {
|
) : BasicSingleApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_SOLO_3
|
override val model: PodDevice.Model = PodDevice.Model.BEATS_SOLO_3
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||||
val identifier = recognizeDevice(scanResult, proximityMessage)
|
val basic = BeatsSolo3(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return BeatsSolo3(
|
updateHistory(basic)
|
||||||
identifier = identifier,
|
|
||||||
scanResult = scanResult,
|
if (result == null) return basic
|
||||||
proximityMessage = proximityMessage,
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -13,23 +14,30 @@ data class BeatsStudio3(
|
|||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val lastSeenAt: Instant = Instant.now(),
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
) : BasicSingleApplePods {
|
) : BasicSingleApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_STUDIO_3
|
override val model: PodDevice.Model = PodDevice.Model.BEATS_STUDIO_3
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY
|
proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||||
val identifier = recognizeDevice(scanResult, proximityMessage)
|
val basic = BeatsStudio3(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return BeatsStudio3(
|
updateHistory(basic)
|
||||||
identifier = identifier,
|
|
||||||
scanResult = scanResult,
|
if (result == null) return basic
|
||||||
proximityMessage = proximityMessage,
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -13,23 +14,33 @@ data class BeatsX(
|
|||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val lastSeenAt: Instant = Instant.now(),
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
) : BasicSingleApplePods {
|
) : BasicSingleApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.BEATS_X
|
override val model: PodDevice.Model = PodDevice.Model.BEATS_X
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||||
val identifier = recognizeDevice(scanResult, proximityMessage)
|
val basic = BeatsX(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return BeatsX(
|
updateHistory(basic)
|
||||||
identifier = identifier,
|
|
||||||
scanResult = scanResult,
|
if (result == null) return basic
|
||||||
proximityMessage = proximityMessage,
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -13,22 +14,32 @@ data class PowerBeats3(
|
|||||||
override val identifier: PodDevice.Id = PodDevice.Id(),
|
override val identifier: PodDevice.Id = PodDevice.Id(),
|
||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val lastSeenAt: Instant = Instant.now(),
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
) : BasicSingleApplePods {
|
) : BasicSingleApplePods {
|
||||||
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_3
|
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_3
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : BasicSingleApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
proximityMessage.getModelInfo().full == DEVICE_CODE
|
proximityMessage.getModelInfo().full == DEVICE_CODE
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||||
val identifier = recognizeDevice(scanResult, proximityMessage)
|
val basic = PowerBeats3(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
return PowerBeats3(
|
updateHistory(basic)
|
||||||
identifier = identifier,
|
|
||||||
scanResult = scanResult,
|
if (result == null) return basic
|
||||||
proximityMessage = proximityMessage,
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
confidence = result.confidence,
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.ApplePods
|
import eu.darken.capod.pods.core.apple.ApplePods
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePodsFactory
|
||||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -14,7 +15,10 @@ data class PowerBeatsPro(
|
|||||||
override val lastSeenAt: Instant = Instant.now(),
|
override val lastSeenAt: Instant = Instant.now(),
|
||||||
override val scanResult: BleScanResult,
|
override val scanResult: BleScanResult,
|
||||||
override val proximityMessage: ProximityPairing.Message,
|
override val proximityMessage: ProximityPairing.Message,
|
||||||
private val cachedBatteryPercentage: Float?,
|
override val confidence: Float = PodDevice.BASE_CONFIDENCE,
|
||||||
|
private val rssiAverage: Int? = null,
|
||||||
|
private val cachedBatteryPercentage: Float? = null,
|
||||||
|
private val cachedCaseState: DualApplePods.LidState? = null
|
||||||
) : DualApplePods {
|
) : DualApplePods {
|
||||||
|
|
||||||
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_PRO
|
override val model: PodDevice.Model = PodDevice.Model.POWERBEATS_PRO
|
||||||
@@ -22,26 +26,33 @@ data class PowerBeatsPro(
|
|||||||
override val batteryCasePercent: Float?
|
override val batteryCasePercent: Float?
|
||||||
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
get() = super.batteryCasePercent ?: cachedBatteryPercentage
|
||||||
|
|
||||||
class Factory @Inject constructor() : ApplePods.Factory(TAG) {
|
override val caseLidState: DualApplePods.LidState
|
||||||
|
get() = cachedCaseState ?: super.caseLidState
|
||||||
|
|
||||||
|
override val rssi: Int
|
||||||
|
get() = rssiAverage ?: super.rssi
|
||||||
|
|
||||||
|
class Factory @Inject constructor() : DualApplePodsFactory(TAG) {
|
||||||
|
|
||||||
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
override fun isResponsible(proximityMessage: ProximityPairing.Message): Boolean =
|
||||||
proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY
|
proximityMessage.getModelInfo().dirty == DEVICE_CODE_DIRTY
|
||||||
|
|
||||||
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
override fun create(scanResult: BleScanResult, proximityMessage: ProximityPairing.Message): ApplePods {
|
||||||
val identifier = recognizeDevice(scanResult, proximityMessage)
|
var basic = PowerBeatsPro(scanResult = scanResult, proximityMessage = proximityMessage)
|
||||||
|
val result = searchHistory(basic)
|
||||||
|
|
||||||
val device = PowerBeatsPro(
|
if (result != null) basic = basic.copy(identifier = result.id)
|
||||||
identifier = identifier,
|
updateHistory(basic)
|
||||||
scanResult = scanResult,
|
|
||||||
proximityMessage = proximityMessage,
|
if (result == null) return basic
|
||||||
cachedBatteryPercentage = cachedValues[identifier]?.caseBatteryPercentage
|
|
||||||
|
return basic.copy(
|
||||||
|
identifier = result.id,
|
||||||
|
confidence = result.confidence,
|
||||||
|
cachedBatteryPercentage = result.getLatestCaseBattery(),
|
||||||
|
rssiAverage = result.averageRssi(basic.rssi),
|
||||||
|
cachedCaseState = result.getLatestCaseLidState(basic)
|
||||||
)
|
)
|
||||||
|
|
||||||
cachedValues[identifier] = ValueCache(
|
|
||||||
caseBatteryPercentage = device.batteryCasePercent
|
|
||||||
)
|
|
||||||
|
|
||||||
return device
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ package eu.darken.capod.pods.core.apple.protocol
|
|||||||
import android.bluetooth.le.ScanFilter
|
import android.bluetooth.le.ScanFilter
|
||||||
import dagger.Reusable
|
import dagger.Reusable
|
||||||
import eu.darken.capod.common.debug.logging.log
|
import eu.darken.capod.common.debug.logging.log
|
||||||
import eu.darken.capod.common.lowerNibble
|
|
||||||
import eu.darken.capod.common.upperNibble
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
@@ -15,22 +13,11 @@ object ProximityPairing {
|
|||||||
val length: Int,
|
val length: Int,
|
||||||
val data: UByteArray
|
val data: UByteArray
|
||||||
) {
|
) {
|
||||||
data class Markers(
|
|
||||||
val vendor: UByte,
|
|
||||||
val length: UByte,
|
|
||||||
val device: UShort,
|
|
||||||
val batteryData: Set<UByte>,
|
|
||||||
val deviceColor: UByte,
|
|
||||||
)
|
|
||||||
|
|
||||||
fun getRecogMarkers(): Markers = Markers(
|
override fun toString(): String {
|
||||||
vendor = CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING,
|
val dataHex = data.joinToString(separator = " ") { String.format("%02X", it.toByte()) }
|
||||||
length = PROXIMITY_PAIRING_MESSAGE_LENGTH.toUByte(),
|
return "ProximityPairing.Message(type=$type, length=$length, data=$dataHex)"
|
||||||
device = (((data[1].toInt() and 255) shl 8) or (data[2].toInt() and 255)).toUShort(),
|
}
|
||||||
// Make comparison order independent
|
|
||||||
batteryData = setOf(data[4].upperNibble, data[4].lowerNibble),
|
|
||||||
deviceColor = data[7]
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Reusable
|
@Reusable
|
||||||
@@ -74,6 +61,6 @@ object ProximityPairing {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private const val CONTINUITY_PROTOCOL_MESSAGE_LENGTH = 27
|
private const val CONTINUITY_PROTOCOL_MESSAGE_LENGTH = 27
|
||||||
private val CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING = 0x07.toUByte()
|
internal val CONTINUITY_PROTOCOL_MESSAGE_TYPE_PROXIMITY_PAIRING = 0x07.toUByte()
|
||||||
private const val PROXIMITY_PAIRING_MESSAGE_LENGTH = 25
|
internal const val PROXIMITY_PAIRING_MESSAGE_LENGTH = 25
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||||
import eu.darken.capod.main.core.GeneralSettings
|
import eu.darken.capod.main.core.GeneralSettings
|
||||||
import eu.darken.capod.monitor.core.PodMonitor
|
import eu.darken.capod.monitor.core.PodMonitor
|
||||||
|
import eu.darken.capod.pods.core.HasDualEarDetection
|
||||||
import eu.darken.capod.pods.core.HasEarDetection
|
import eu.darken.capod.pods.core.HasEarDetection
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.reaction.settings.ReactionSettings
|
import eu.darken.capod.reaction.settings.ReactionSettings
|
||||||
@@ -71,7 +72,13 @@ class AutoConnect @Inject constructor(
|
|||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
AutoConnectCondition.IN_EAR -> when (mainDevice) {
|
AutoConnectCondition.IN_EAR -> when (mainDevice) {
|
||||||
is HasEarDetection -> mainDevice.isBeingWorn
|
is HasEarDetection -> {
|
||||||
|
if (mainDevice is HasDualEarDetection && reactionSettings.onePodMode.value) {
|
||||||
|
mainDevice.isEitherPodInEar
|
||||||
|
} else {
|
||||||
|
mainDevice.isBeingWorn
|
||||||
|
}
|
||||||
|
}
|
||||||
else -> true
|
else -> true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import eu.darken.capod.common.debug.logging.logTag
|
|||||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||||
import eu.darken.capod.common.flow.withPrevious
|
import eu.darken.capod.common.flow.withPrevious
|
||||||
import eu.darken.capod.monitor.core.PodMonitor
|
import eu.darken.capod.monitor.core.PodMonitor
|
||||||
|
import eu.darken.capod.pods.core.HasDualEarDetection
|
||||||
import eu.darken.capod.pods.core.HasEarDetection
|
import eu.darken.capod.pods.core.HasEarDetection
|
||||||
import eu.darken.capod.reaction.settings.ReactionSettings
|
import eu.darken.capod.reaction.settings.ReactionSettings
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
@@ -25,7 +26,8 @@ class PlayPause @Inject constructor(
|
|||||||
fun monitor() = combine(
|
fun monitor() = combine(
|
||||||
reactionSettings.autoPlay.flow,
|
reactionSettings.autoPlay.flow,
|
||||||
reactionSettings.autoPause.flow,
|
reactionSettings.autoPause.flow,
|
||||||
) { play, pause -> play || pause }
|
reactionSettings.onePodMode.flow,
|
||||||
|
) { play, pause, _ -> play || pause }
|
||||||
.flatMapLatest { if (it) bluetoothManager.connectedDevices() else emptyFlow() }
|
.flatMapLatest { if (it) bluetoothManager.connectedDevices() else emptyFlow() }
|
||||||
.flatMapLatest {
|
.flatMapLatest {
|
||||||
if (it.isEmpty()) {
|
if (it.isEmpty()) {
|
||||||
@@ -42,7 +44,28 @@ class PlayPause @Inject constructor(
|
|||||||
if (previous is HasEarDetection && current is HasEarDetection) {
|
if (previous is HasEarDetection && current is HasEarDetection) {
|
||||||
log(TAG, VERBOSE) { "previous=${previous.isBeingWorn}, current=${current.isBeingWorn}" }
|
log(TAG, VERBOSE) { "previous=${previous.isBeingWorn}, current=${current.isBeingWorn}" }
|
||||||
log(TAG, VERBOSE) { "previous-id=${previous.identifier}, current-id=${current.identifier}" }
|
log(TAG, VERBOSE) { "previous-id=${previous.identifier}, current-id=${current.identifier}" }
|
||||||
if (previous.identifier == current.identifier && previous.isBeingWorn != current.isBeingWorn) {
|
if (previous.identifier != current.identifier) {
|
||||||
|
return@onEach
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previous is HasDualEarDetection && current is HasDualEarDetection && reactionSettings.onePodMode.value) {
|
||||||
|
log(TAG) { "Ear status changed for dual pod device in single pod mode." }
|
||||||
|
val previousWorn = previous.isEitherPodInEar
|
||||||
|
val currentWorn = current.isEitherPodInEar
|
||||||
|
if (!previousWorn && currentWorn && !mediaControl.isPlaying) {
|
||||||
|
if (reactionSettings.autoPlay.value) {
|
||||||
|
mediaControl.sendPlay()
|
||||||
|
} else {
|
||||||
|
log(TAG) { "autoPlay is disabled" }
|
||||||
|
}
|
||||||
|
} else if (!currentWorn && previousWorn && mediaControl.isPlaying) {
|
||||||
|
if (reactionSettings.autoPause.value) {
|
||||||
|
mediaControl.sendPause()
|
||||||
|
} else {
|
||||||
|
log(TAG) { "autoPause is disabled" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (previous.isBeingWorn != current.isBeingWorn) {
|
||||||
log(TAG) { "Ear status changed for monitored device." }
|
log(TAG) { "Ear status changed for monitored device." }
|
||||||
if (current.isBeingWorn && !mediaControl.isPlaying) {
|
if (current.isBeingWorn && !mediaControl.isPlaying) {
|
||||||
if (reactionSettings.autoPlay.value) {
|
if (reactionSettings.autoPlay.value) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package eu.darken.capod.reaction.popup
|
package eu.darken.capod.reaction.popup
|
||||||
|
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.INFO
|
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||||
import eu.darken.capod.common.debug.logging.Logging.Priority.VERBOSE
|
import eu.darken.capod.common.debug.logging.Logging.Priority.*
|
||||||
import eu.darken.capod.common.debug.logging.log
|
import eu.darken.capod.common.debug.logging.log
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
import eu.darken.capod.common.debug.logging.logTag
|
||||||
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
import eu.darken.capod.common.flow.setupCommonEventHandlers
|
||||||
@@ -9,9 +9,12 @@ import eu.darken.capod.common.flow.withPrevious
|
|||||||
import eu.darken.capod.monitor.core.PodMonitor
|
import eu.darken.capod.monitor.core.PodMonitor
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
import eu.darken.capod.pods.core.PodDevice
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import eu.darken.capod.reaction.popup.ui.PopUpNotification
|
import eu.darken.capod.reaction.popup.ui.PopUpWindow
|
||||||
import eu.darken.capod.reaction.settings.ReactionSettings
|
import eu.darken.capod.reaction.settings.ReactionSettings
|
||||||
import kotlinx.coroutines.flow.*
|
import kotlinx.coroutines.flow.*
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.time.Duration
|
||||||
|
import java.time.Instant
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@@ -19,9 +22,12 @@ import javax.inject.Singleton
|
|||||||
class PopUpReaction @Inject constructor(
|
class PopUpReaction @Inject constructor(
|
||||||
private val podMonitor: PodMonitor,
|
private val podMonitor: PodMonitor,
|
||||||
private val reactionSettings: ReactionSettings,
|
private val reactionSettings: ReactionSettings,
|
||||||
private val popUpNotification: PopUpNotification,
|
private val popupWindow: PopUpWindow,
|
||||||
|
private val dispatcherProvider: DispatcherProvider,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val coolDowns = mutableMapOf<PodDevice.Id, Instant>()
|
||||||
|
|
||||||
fun monitor(): Flow<Unit> = reactionSettings.showPopUpOnCaseOpen.flow
|
fun monitor(): Flow<Unit> = reactionSettings.showPopUpOnCaseOpen.flow
|
||||||
.flatMapLatest { isEnabled ->
|
.flatMapLatest { isEnabled ->
|
||||||
if (isEnabled) {
|
if (isEnabled) {
|
||||||
@@ -49,17 +55,48 @@ class PopUpReaction @Inject constructor(
|
|||||||
if (isSameDeviceWithCaseNowOpen || isNewDeviceWithJustOpenedCase) {
|
if (isSameDeviceWithCaseNowOpen || isNewDeviceWithJustOpenedCase) {
|
||||||
log(TAG) { "Case lid status changed for monitored device." }
|
log(TAG) { "Case lid status changed for monitored device." }
|
||||||
|
|
||||||
if (current.caseLidState == DualApplePods.LidState.OPEN && current.model != PodDevice.Model.UNKNOWN) {
|
tryPopWindow(current)
|
||||||
log(TAG, INFO) { "Show popup" }
|
|
||||||
popUpNotification.showNotification(current)
|
|
||||||
} else if (current.caseLidState != DualApplePods.LidState.OPEN) {
|
|
||||||
log(TAG, INFO) { "Hide popup" }
|
|
||||||
popUpNotification.hideNotification()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun tryPopWindow(current: DualApplePods) {
|
||||||
|
if (current.caseLidState == DualApplePods.LidState.OPEN) {
|
||||||
|
log(TAG, INFO) { "Show popup" }
|
||||||
|
|
||||||
|
val now = Instant.now()
|
||||||
|
val lastShown = coolDowns[current.identifier] ?: Instant.MIN
|
||||||
|
val sinceLastPop = Duration.between(lastShown, now)
|
||||||
|
log(TAG) { "Time since last popup: $sinceLastPop" }
|
||||||
|
if (sinceLastPop < Duration.ofSeconds(10)) {
|
||||||
|
log(TAG, INFO) { "Popup is still on cooldown: $sinceLastPop" }
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
coolDowns[current.identifier] = Instant.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
withContext(dispatcherProvider.Main) {
|
||||||
|
popupWindow.show(current)
|
||||||
|
}
|
||||||
|
} else if (current.caseLidState != DualApplePods.LidState.OPEN) {
|
||||||
|
when (current.caseLidState) {
|
||||||
|
DualApplePods.LidState.CLOSED -> {
|
||||||
|
log(TAG, INFO) { "Lid was actively closed, resetting cooldown." }
|
||||||
|
coolDowns.remove(current.identifier)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
log(TAG, WARN) { "Lid was was not actively closed, refreshing cooldown." }
|
||||||
|
coolDowns[current.identifier] = Instant.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log(TAG, INFO) { "Hide popup" }
|
||||||
|
withContext(dispatcherProvider.Main) {
|
||||||
|
popupWindow.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = logTag("Reaction", "PopUp")
|
private val TAG = logTag("Reaction", "PopUp")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
package eu.darken.capod.reaction.popup.ui
|
|
||||||
|
|
||||||
import android.app.NotificationChannel
|
|
||||||
import android.app.NotificationManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import eu.darken.capod.R
|
|
||||||
import eu.darken.capod.common.BuildConfigWrap
|
|
||||||
import eu.darken.capod.common.debug.logging.log
|
|
||||||
import eu.darken.capod.common.debug.logging.logTag
|
|
||||||
import eu.darken.capod.common.notifications.PendingIntentCompat
|
|
||||||
import eu.darken.capod.main.ui.MainActivity
|
|
||||||
import eu.darken.capod.pods.core.PodDevice
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
|
|
||||||
class PopUpNotification @Inject constructor(
|
|
||||||
@ApplicationContext private val context: Context,
|
|
||||||
private val notificationManager: NotificationManager,
|
|
||||||
private val notificationViewFactory: PopUpNotificationViewFactory
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val builder: NotificationCompat.Builder
|
|
||||||
|
|
||||||
init {
|
|
||||||
NotificationChannel(
|
|
||||||
NOTIFICATION_CHANNEL_ID,
|
|
||||||
context.getString(R.string.notification_channel_reaction_popup_label),
|
|
||||||
NotificationManager.IMPORTANCE_HIGH
|
|
||||||
).run {
|
|
||||||
vibrationPattern = LongArray(1) { 0 }
|
|
||||||
enableVibration(true)
|
|
||||||
enableLights(false)
|
|
||||||
setSound(null, null)
|
|
||||||
notificationManager.createNotificationChannel(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
val openIntent = Intent(context, MainActivity::class.java)
|
|
||||||
val openPi = PendingIntent.getActivity(
|
|
||||||
context,
|
|
||||||
0,
|
|
||||||
openIntent,
|
|
||||||
PendingIntentCompat.FLAG_IMMUTABLE
|
|
||||||
)
|
|
||||||
|
|
||||||
builder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID).apply {
|
|
||||||
setChannelId(NOTIFICATION_CHANNEL_ID)
|
|
||||||
setContentIntent(openPi)
|
|
||||||
priority = NotificationCompat.PRIORITY_LOW
|
|
||||||
setSmallIcon(R.drawable.ic_device_generic_earbuds)
|
|
||||||
setContentTitle(context.getString(R.string.app_name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBuilder(device: PodDevice): NotificationCompat.Builder = builder.apply {
|
|
||||||
setStyle(NotificationCompat.DecoratedCustomViewStyle())
|
|
||||||
setCustomContentView(notificationViewFactory.createContentView(device))
|
|
||||||
setSmallIcon(device.iconRes)
|
|
||||||
setSubText(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showNotification(device: PodDevice) {
|
|
||||||
val notification = getBuilder(device).build()
|
|
||||||
log(TAG) { "showNotification(): $device" }
|
|
||||||
notificationManager.notify(NOTIFICATION_ID, notification)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hideNotification() {
|
|
||||||
log(TAG) { "hideNotification()" }
|
|
||||||
notificationManager.cancel(NOTIFICATION_ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val TAG = logTag("Reaction", "PopUp", "Notifications")
|
|
||||||
private val NOTIFICATION_CHANNEL_ID =
|
|
||||||
"${BuildConfigWrap.APPLICATION_ID}.notification.channel.reaction.popup"
|
|
||||||
internal const val NOTIFICATION_ID = 9000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
package eu.darken.capod.reaction.popup.ui
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.widget.RemoteViews
|
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
|
||||||
import eu.darken.capod.R
|
|
||||||
import eu.darken.capod.pods.core.*
|
|
||||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
|
||||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
|
|
||||||
class PopUpNotificationViewFactory @Inject constructor(
|
|
||||||
@ApplicationContext private val context: Context
|
|
||||||
) {
|
|
||||||
|
|
||||||
fun createContentView(device: PodDevice): RemoteViews = when (device) {
|
|
||||||
is DualApplePods -> createDualApplePods(device)
|
|
||||||
is SingleApplePods -> createSingleApplePods(device)
|
|
||||||
is BasicSingleApplePods -> createSingleBasicApplePods(device)
|
|
||||||
else -> throw IllegalArgumentException("Unexpected device: $device")
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createDualApplePods(device: DualApplePods): RemoteViews = RemoteViews(
|
|
||||||
context.packageName,
|
|
||||||
R.layout.popup_notification_dual_pods
|
|
||||||
).apply {
|
|
||||||
setTextViewText(R.id.pod_label, device.getLabel(context))
|
|
||||||
|
|
||||||
device.getBatteryLevelLeftPod(context)
|
|
||||||
.let { if (device.isLeftPodCharging) "$it⚡" else it }
|
|
||||||
.run { setTextViewText(R.id.pod_left, this) }
|
|
||||||
|
|
||||||
device.getBatteryLevelCase(context)
|
|
||||||
.let { if (device.isCaseCharging) "$it⚡" else it }
|
|
||||||
.run { setTextViewText(R.id.pod_case, this) }
|
|
||||||
|
|
||||||
device.getBatteryLevelRightPod(context)
|
|
||||||
.let { if (device.isRightPodCharging) "$it⚡" else it }
|
|
||||||
.run { setTextViewText(R.id.pod_right, this) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createSingleApplePods(device: SingleApplePods): RemoteViews = RemoteViews(
|
|
||||||
context.packageName,
|
|
||||||
R.layout.popup_notification_single_pods
|
|
||||||
).apply {
|
|
||||||
setTextViewText(R.id.headphones_label, device.getLabel(context))
|
|
||||||
|
|
||||||
device.getBatteryLevelHeadset(context)
|
|
||||||
.let { if (!device.isHeadsetBeingCharged) "$it⚡" else it }
|
|
||||||
.run { setTextViewText(R.id.headphones, this) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createSingleBasicApplePods(device: BasicSingleApplePods): RemoteViews = RemoteViews(
|
|
||||||
context.packageName,
|
|
||||||
R.layout.popup_notification_single_pods_basic
|
|
||||||
).apply {
|
|
||||||
setTextViewText(R.id.headphones_label, device.getLabel(context))
|
|
||||||
|
|
||||||
device.getBatteryLevelHeadset(context)
|
|
||||||
.run { setTextViewText(R.id.headphones, this) }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package eu.darken.capod.reaction.popup.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.view.ContextThemeWrapper
|
||||||
|
import androidx.core.view.isInvisible
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import eu.darken.capod.R
|
||||||
|
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||||
|
import eu.darken.capod.databinding.PopupNotificationDualPodsBinding
|
||||||
|
import eu.darken.capod.databinding.PopupNotificationSinglePodsBasicBinding
|
||||||
|
import eu.darken.capod.databinding.PopupNotificationSinglePodsBinding
|
||||||
|
import eu.darken.capod.pods.core.*
|
||||||
|
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
|
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
||||||
|
class PopUpPodViewFactory @Inject constructor(
|
||||||
|
@ApplicationContext private val appContext: Context,
|
||||||
|
private val debugSettings: DebugSettings,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val context = ContextThemeWrapper(appContext, R.style.AppTheme)
|
||||||
|
private val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||||
|
|
||||||
|
fun createContentView(parent: ViewGroup, device: PodDevice): View = when (device) {
|
||||||
|
is DualApplePods -> createDualApplePods(parent, device)
|
||||||
|
is SingleApplePods -> createSingleApplePods(parent, device) // Unused, has no case to trigger reaction?
|
||||||
|
is BasicSingleApplePods -> createSingleBasicApplePods(
|
||||||
|
parent,
|
||||||
|
device
|
||||||
|
) // Unused, has no case to trigger reaction?
|
||||||
|
else -> throw IllegalArgumentException("Unexpected device: $device")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createDualApplePods(parent: ViewGroup, device: DualApplePods): View =
|
||||||
|
PopupNotificationDualPodsBinding.inflate(layoutInflater, parent, false).apply {
|
||||||
|
device.apply {
|
||||||
|
podIcon.setImageResource(iconRes)
|
||||||
|
podLabel.text = getLabel(context)
|
||||||
|
signal.text = getSignalQuality(context)
|
||||||
|
signal.isInvisible = debugSettings.isDebugModeEnabled.value
|
||||||
|
|
||||||
|
// Left
|
||||||
|
podLeftBatteryIcon.setImageResource(getBatteryDrawable(batteryLeftPodPercent))
|
||||||
|
podLeftBatteryLabel.text = getBatteryLevelLeftPod(context)
|
||||||
|
|
||||||
|
// Case
|
||||||
|
podCaseBatteryIcon.setImageResource(getBatteryDrawable(batteryCasePercent))
|
||||||
|
podCaseBatteryLabel.text = getBatteryLevelCase(context)
|
||||||
|
|
||||||
|
// Right
|
||||||
|
podRightBatteryIcon.setImageResource(getBatteryDrawable(batteryRightPodPercent))
|
||||||
|
podRightBatteryLabel.text = getBatteryLevelRightPod(context)
|
||||||
|
}
|
||||||
|
}.root
|
||||||
|
|
||||||
|
private fun createSingleApplePods(parent: ViewGroup, device: SingleApplePods): View =
|
||||||
|
PopupNotificationSinglePodsBinding.inflate(layoutInflater, parent, false).apply {
|
||||||
|
device.apply {
|
||||||
|
headphonesIcon.setImageResource(iconRes)
|
||||||
|
headphonesLabel.text = getLabel(context)
|
||||||
|
signal.text = getSignalQuality(context)
|
||||||
|
signal.isInvisible = debugSettings.isDebugModeEnabled.value
|
||||||
|
|
||||||
|
headphonesBatteryIcon.setImageResource(getBatteryDrawable(batteryHeadsetPercent))
|
||||||
|
headphonesBatteryLabel.text = getBatteryLevelHeadset(context)
|
||||||
|
}
|
||||||
|
}.root
|
||||||
|
|
||||||
|
private fun createSingleBasicApplePods(parent: ViewGroup, device: BasicSingleApplePods): View =
|
||||||
|
PopupNotificationSinglePodsBasicBinding.inflate(layoutInflater, parent, false).apply {
|
||||||
|
device.apply {
|
||||||
|
headphonesIcon.setImageResource(iconRes)
|
||||||
|
headphonesLabel.text = getLabel(context)
|
||||||
|
signal.text = getSignalQuality(context)
|
||||||
|
signal.isInvisible = debugSettings.isDebugModeEnabled.value
|
||||||
|
|
||||||
|
headphonesBatteryIcon.setImageResource(getBatteryDrawable(batteryHeadsetPercent))
|
||||||
|
headphonesBatteryLabel.text = getBatteryLevelHeadset(context)
|
||||||
|
}
|
||||||
|
}.root
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package eu.darken.capod.reaction.popup.ui
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Context.WINDOW_SERVICE
|
||||||
|
import android.graphics.PixelFormat
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import androidx.appcompat.view.ContextThemeWrapper
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import eu.darken.capod.R
|
||||||
|
import eu.darken.capod.common.debug.logging.Logging.Priority.ERROR
|
||||||
|
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.pods.core.PodDevice
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class PopUpWindow @Inject constructor(
|
||||||
|
@ApplicationContext private val appContext: Context,
|
||||||
|
private val podViewFactory: PopUpPodViewFactory
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val context = ContextThemeWrapper(appContext, R.style.AppTheme)
|
||||||
|
private val layoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||||
|
private val windowManager: WindowManager = context.getSystemService(WINDOW_SERVICE) as WindowManager
|
||||||
|
private val layoutParams = WindowManager.LayoutParams(
|
||||||
|
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||||
|
WindowManager.LayoutParams.WRAP_CONTENT, // Display it on top of other application windows
|
||||||
|
WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY, // Don't let it grab the input focus
|
||||||
|
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, // Make the underlying application window visible
|
||||||
|
|
||||||
|
PixelFormat.TRANSLUCENT
|
||||||
|
).apply {
|
||||||
|
gravity = Gravity.BOTTOM
|
||||||
|
}
|
||||||
|
private val popUpView: View = layoutInflater.inflate(R.layout.popup_window_container_layout, null).apply {
|
||||||
|
findViewById<View>(R.id.close_action).setOnClickListener { close() }
|
||||||
|
}
|
||||||
|
private val deviceContainer = popUpView.findViewById<FrameLayout>(R.id.popup_content)
|
||||||
|
fun show(device: PodDevice) = try {
|
||||||
|
log(TAG) { "open()" }
|
||||||
|
if (popUpView.windowToken != null || popUpView.parent != null) {
|
||||||
|
log(TAG) { "View already added." }
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
|
val podView = podViewFactory.createContentView(deviceContainer, device)
|
||||||
|
deviceContainer.removeAllViews()
|
||||||
|
deviceContainer.addView(podView)
|
||||||
|
windowManager.addView(popUpView, layoutParams)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log(TAG, ERROR) { "open() failed: ${e.asLog()}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun close() = try {
|
||||||
|
log(TAG) { "close()" }
|
||||||
|
if (popUpView.parent != null) {
|
||||||
|
windowManager.removeView(popUpView)
|
||||||
|
deviceContainer.removeAllViews()
|
||||||
|
} else {
|
||||||
|
log(TAG) { "View was not added" }
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log(TAG, ERROR) { "close() failed: ${e.asLog()}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = logTag("Reaction", "PopUp", "Window")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,11 +47,17 @@ class ReactionSettings @Inject constructor(
|
|||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val onePodMode = preferences.createFlowPreference(
|
||||||
|
"reaction.onepod.enabled",
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
override val preferenceDataStore: PreferenceDataStore = PreferenceStoreMapper(
|
override val preferenceDataStore: PreferenceDataStore = PreferenceStoreMapper(
|
||||||
autoPause,
|
autoPause,
|
||||||
autoPlay,
|
autoPlay,
|
||||||
autoConnect,
|
autoConnect,
|
||||||
autoConnectCondition,
|
autoConnectCondition,
|
||||||
showPopUpOnCaseOpen,
|
showPopUpOnCaseOpen,
|
||||||
|
onePodMode,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -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,5v16c0,0.55 -0.45,1 -1,1H8c-0.55,0 -1,-0.45 -1,-1V5c0,-0.55 0.45,-1 1,-1h2V2h4v2h2C16.55,4 17,4.45 17,5zM15,6H9v14h6V6z" />
|
||||||
|
</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,5v16c0,0.55 -0.45,1 -1,1H8c-0.55,0 -1,-0.45 -1,-1V5c0,-0.55 0.45,-1 1,-1h2V2h4v2h2C16.55,4 17,4.45 17,5zM15,6H9v12h6V6z" />
|
||||||
|
</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,5v16c0,0.55 -0.45,1 -1,1H8c-0.55,0 -1,-0.45 -1,-1V5c0,-0.55 0.45,-1 1,-1h2V2h4v2h2C16.55,4 17,4.45 17,5zM15,6H9v10h6V6z" />
|
||||||
|
</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,5v16c0,0.55 -0.45,1 -1,1H8c-0.55,0 -1,-0.45 -1,-1V5c0,-0.55 0.45,-1 1,-1h2V2h4v2h2C16.55,4 17,4.45 17,5zM15,6H9v8h6V6z" />
|
||||||
|
</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,5v16c0,0.55 -0.45,1 -1,1H8c-0.55,0 -1,-0.45 -1,-1V5c0,-0.55 0.45,-1 1,-1h2V2h4v2h2C16.55,4 17,4.45 17,5zM15,6H9v6h6V6z" />
|
||||||
|
</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,5v16c0,0.55 -0.45,1 -1,1H8c-0.55,0 -1,-0.45 -1,-1V5c0,-0.55 0.45,-1 1,-1h2V2h4v2h2C16.55,4 17,4.45 17,5zM15,6H9v4h6V6z" />
|
||||||
|
</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,5v16c0,0.55 -0.45,1 -1,1H8c-0.55,0 -1,-0.45 -1,-1V5c0,-0.55 0.45,-1 1,-1h2V2h4v2h2C16.55,4 17,4.45 17,5zM15,6H9v2h6V6z" />
|
||||||
|
</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="M15.67,4H14V2h-4v2H8.33C7.6,4 7,4.6 7,5.33v15.33C7,21.4 7.6,22 8.33,22h7.33c0.74,0 1.34,-0.6 1.34,-1.33V5.33C17,4.6 16.4,4 15.67,4z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<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"
|
||||||
|
android:autoMirrored="true">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M15.67,4L14,4L14,2h-4v2L8.33,4C7.6,4 7,4.6 7,5.33v15.33C7,21.4 7.6,22 8.33,22h7.33c0.74,0 1.34,-0.6 1.34,-1.33L17,5.33C17,4.6 16.4,4 15.67,4zM12.95,17.95h-1.9v-1.9h1.9v1.9zM14.3,12.69s-0.38,0.42 -0.67,0.71c-0.48,0.48 -0.83,1.15 -0.83,1.6h-1.6c0,-0.83 0.46,-1.52 0.93,-2l0.93,-0.94c0.27,-0.27 0.44,-0.65 0.44,-1.06 0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5L9,11c0,-1.66 1.34,-3 3,-3s3,1.34 3,3c0,0.66 -0.27,1.26 -0.7,1.69z" />
|
||||||
|
</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="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<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="M3,3v8h8L11,3L3,3zM9,9L5,9L5,5h4v4zM3,13v8h8v-8L3,13zM9,19L5,19v-4h4v4zM13,3v8h8L21,3h-8zM19,9h-4L15,5h4v4zM13,13v8h8v-8h-8zM19,19h-4v-4h4v4z"
|
||||||
|
android:fillType="evenOdd" />
|
||||||
|
</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,20c-0.29,0 -0.56,-0.06 -0.76,-0.15 -0.71,-0.37 -1.21,-0.88 -1.71,-2.38 -0.51,-1.56 -1.47,-2.29 -2.39,-3 -0.79,-0.61 -1.61,-1.24 -2.32,-2.53C9.29,10.98 9,9.93 9,9c0,-2.8 2.2,-5 5,-5s5,2.2 5,5h2c0,-3.93 -3.07,-7 -7,-7S7,5.07 7,9c0,1.26 0.38,2.65 1.07,3.9 0.91,1.65 1.98,2.48 2.85,3.15 0.81,0.62 1.39,1.07 1.71,2.05 0.6,1.82 1.37,2.84 2.73,3.55 0.51,0.23 1.07,0.35 1.64,0.35 2.21,0 4,-1.79 4,-4h-2c0,1.1 -0.9,2 -2,2zM7.64,2.64L6.22,1.22C4.23,3.21 3,5.96 3,9s1.23,5.79 3.22,7.78l1.41,-1.41C6.01,13.74 5,11.49 5,9s1.01,-4.74 2.64,-6.36zM11.5,9c0,1.38 1.12,2.5 2.5,2.5s2.5,-1.12 2.5,-2.5 -1.12,-2.5 -2.5,-2.5 -2.5,1.12 -2.5,2.5z" />
|
||||||
|
</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.03,3.2C7.15,2.44 8.51,2 10,2c3.93,0 7,3.07 7,7c0,1.26 -0.38,2.65 -1.07,3.9c-0.02,0.04 -0.05,0.08 -0.08,0.13l-1.48,-1.48C14.77,10.69 15,9.8 15,9c0,-2.8 -2.2,-5 -5,-5C9.08,4 8.24,4.26 7.5,4.67L6.03,3.2zM17.21,14.38l1.43,1.43C20.11,13.93 21,11.57 21,9c0,-3.04 -1.23,-5.79 -3.22,-7.78l-1.42,1.42C17.99,4.26 19,6.51 19,9C19,11.02 18.33,12.88 17.21,14.38zM10,6.5c-0.21,0 -0.4,0.03 -0.59,0.08l3.01,3.01C12.47,9.4 12.5,9.21 12.5,9C12.5,7.62 11.38,6.5 10,6.5zM21.19,21.19L2.81,2.81L1.39,4.22l2.13,2.13C3.19,7.16 3,8.05 3,9h2c0,-0.36 0.05,-0.71 0.12,-1.05l6.61,6.61c-0.88,0.68 -1.78,1.41 -2.27,2.9c-0.5,1.5 -1,2.01 -1.71,2.38C7.56,19.94 7.29,20 7,20c-1.1,0 -2,-0.9 -2,-2H3c0,2.21 1.79,4 4,4c0.57,0 1.13,-0.12 1.64,-0.35c1.36,-0.71 2.13,-1.73 2.73,-3.55c0.32,-0.98 0.9,-1.43 1.71,-2.05c0.03,-0.02 0.05,-0.04 0.08,-0.06l6.62,6.62L21.19,21.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="M12,15c1.66,0 2.99,-1.34 2.99,-3L15,6c0,-1.66 -1.34,-3 -3,-3S9,4.34 9,6v6c0,1.66 1.34,3 3,3zM17.3,12c0,3 -2.54,5.1 -5.3,5.1S6.7,15 6.7,12L5,12c0,3.42 2.72,6.23 6,6.72L11,22h2v-3.28c3.28,-0.48 6,-3.3 6,-6.72h-1.7z" />
|
||||||
|
</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="M19,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM14,17h-2L12,9h-2L10,7h4v10z" />
|
||||||
|
</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="M16.01,7L16,3h-2v4h-4V3H8v4h-0.01C7,6.99 6,7.99 6,8.99v5.49L9.5,18v3h5v-3l3.5,-3.51v-5.5c0,-1 -1,-2 -1.99,-1.99z" />
|
||||||
|
</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="M12,5c-3.87,0 -7,3.13 -7,7h2c0,-2.76 2.24,-5 5,-5s5,2.24 5,5h2c0,-3.87 -3.13,-7 -7,-7zM13,14.29c0.88,-0.39 1.5,-1.26 1.5,-2.29 0,-1.38 -1.12,-2.5 -2.5,-2.5S9.5,10.62 9.5,12c0,1.02 0.62,1.9 1.5,2.29v3.3L7.59,21 9,22.41l3,-3 3,3L16.41,21 13,17.59v-3.3zM12,1C5.93,1 1,5.93 1,12h2c0,-4.97 4.03,-9 9,-9s9,4.03 9,9h2c0,-6.07 -4.93,-11 -11,-11z" />
|
||||||
|
</vector>
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_marginHorizontal="8dp"
|
android:layout_marginHorizontal="8dp"
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
@@ -11,43 +11,97 @@
|
|||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1" />
|
android:layout_weight="1" />
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/pod_left"
|
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:orientation="horizontal">
|
||||||
android:drawableStart="@drawable/ic_airpod_left_24"
|
|
||||||
tools:text="100%" />
|
<ImageView
|
||||||
|
android:id="@+id/pod_left_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_airpod_left_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/pod_left_label"
|
||||||
|
style="@style/TextAppearance.Compat.Notification.Title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
tools:text="100%" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_left_charging"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_power_24" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_left_ear"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_hearing_24" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1" />
|
android:layout_weight="1" />
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/pod_case"
|
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:orientation="horizontal">
|
||||||
android:layout_marginHorizontal="8dp"
|
|
||||||
android:drawableStart="@drawable/ic_airpod_case_24"
|
<ImageView
|
||||||
tools:text="100%" />
|
android:id="@+id/pod_case_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_airpod_case_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/pod_case_label"
|
||||||
|
style="@style/TextAppearance.Compat.Notification.Title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
tools:text="100%" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_case_charging"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_power_24" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1" />
|
android:layout_weight="1" />
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/pod_right"
|
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:orientation="horizontal">
|
||||||
android:drawableStart="@drawable/ic_airpod_right_24"
|
|
||||||
tools:text="100%" />
|
<ImageView
|
||||||
|
android:id="@+id/pod_right_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_airpod_right_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/pod_right_label"
|
||||||
|
style="@style/TextAppearance.Compat.Notification.Title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
tools:text="100%" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_right_charging"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_power_24" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_right_ear"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_hearing_24" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_horizontal">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/headphones_label"
|
android:id="@+id/headphones_label"
|
||||||
@@ -10,16 +11,27 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
tools:text="AirPods Max" />
|
tools:text="Beats Solo 3" />
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/headphones"
|
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="16dp"
|
android:layout_marginHorizontal="8dp"
|
||||||
android:drawableStart="@drawable/ic_device_generic_headphones"
|
android:orientation="horizontal">
|
||||||
android:gravity="center"
|
|
||||||
tools:text="100%" />
|
<ImageView
|
||||||
|
android:id="@+id/headphones_battery_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_battery_unknown_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/headphones_battery_label"
|
||||||
|
style="@style/TextAppearance.Compat.Notification.Title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginHorizontal="8dp"
|
||||||
|
tools:text="100%" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_horizontal">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/headphones_label"
|
android:id="@+id/headphones_label"
|
||||||
@@ -12,14 +13,35 @@
|
|||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
tools:text="AirPods Max" />
|
tools:text="AirPods Max" />
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/headphones"
|
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="16dp"
|
android:layout_marginHorizontal="8dp"
|
||||||
android:drawableStart="@drawable/ic_device_generic_headphones"
|
android:orientation="horizontal">
|
||||||
android:gravity="center"
|
|
||||||
tools:text="100%" />
|
<ImageView
|
||||||
|
android:id="@+id/headphones_battery_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_battery_unknown_24" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/headphones_battery_label"
|
||||||
|
style="@style/TextAppearance.Compat.Notification.Title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginHorizontal="8dp"
|
||||||
|
tools:text="100%" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/headphones_charging"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_power_24" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/headphones_worn"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_hearing_24" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
@@ -49,6 +49,18 @@
|
|||||||
app:layout_constraintTop_toBottomOf="@id/permission_label"
|
app:layout_constraintTop_toBottomOf="@id/permission_label"
|
||||||
tools:text="Lorem Ipsum" />
|
tools:text="Lorem Ipsum" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/privacy_policy"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Tooltip"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:text="@string/settings_privacy_policy_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/permission_description" />
|
||||||
|
|
||||||
<com.google.android.material.button.MaterialButton
|
<com.google.android.material.button.MaterialButton
|
||||||
android:id="@+id/grant_action"
|
android:id="@+id/grant_action"
|
||||||
style="@style/Widget.MaterialComponents.Button"
|
style="@style/Widget.MaterialComponents.Button"
|
||||||
@@ -60,7 +72,7 @@
|
|||||||
android:text="@string/general_grant_permission_action"
|
android:text="@string/general_grant_permission_action"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/permission_description" />
|
app:layout_constraintTop_toBottomOf="@id/privacy_policy" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</com.google.android.material.card.MaterialCardView>
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/reception"
|
app:layout_constraintStart_toEndOf="@id/reception"
|
||||||
app:layout_constraintTop_toTopOf="@+id/reception"
|
app:layout_constraintTop_toTopOf="@+id/reception"
|
||||||
app:srcCompat="@drawable/ic_baseline_signal_cellular_alt_24"
|
app:srcCompat="@drawable/ic_baseline_settings_input_antenna_24"
|
||||||
tools:text="Yours (RSSI -61)" />
|
tools:text="Yours (RSSI -61)" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
@@ -87,61 +87,334 @@
|
|||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:barrierDirection="top"
|
app:barrierDirection="top"
|
||||||
app:constraint_referenced_ids="pod_left,pod_case,pod_right" />
|
app:constraint_referenced_ids="pod_left_container,pod_case_container,pod_right_container" />
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/pod_left"
|
android:id="@+id/pod_left_container"
|
||||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginEnd="8dp"
|
android:layout_marginEnd="4dp"
|
||||||
android:gravity="center_horizontal"
|
android:minHeight="108dp"
|
||||||
app:drawableTopCompat="@drawable/ic_airpod_left_24"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
||||||
app:layout_constraintEnd_toStartOf="@id/pod_case"
|
app:layout_constraintEnd_toStartOf="@id/pod_case_container"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
||||||
app:layout_constraintVertical_bias="0.0"
|
app:layout_constraintVertical_bias="0.0">
|
||||||
tools:text="Left pod\n100%\n(Primary)" />
|
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<ImageView
|
||||||
android:id="@+id/pod_case"
|
android:id="@+id/pod_left_icon"
|
||||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_airpod_left_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_left_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_left_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_left_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:text="@string/pods_dual_left_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_left_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_left_battery_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_left_battery_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_left_battery_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_left_battery_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/general_value_unknown_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_left_battery_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_left_label" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_left_charging_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_power_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_left_charging_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_left_charging_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_left_charging_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/pods_charging_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_left_charging_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_left_battery_label" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_left_microphone_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_keyboard_voice_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_left_microphone_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_left_microphone_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_left_microphone_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/pods_microphone_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_left_microphone_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_left_charging_label" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_left_wear_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_hearing_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_left_wear_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_left_wear_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_left_wear_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/pods_inear_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_left_wear_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_left_microphone_label" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/pod_case_container"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="8dp"
|
android:layout_marginHorizontal="4dp"
|
||||||
android:gravity="center_horizontal"
|
|
||||||
app:drawableTopCompat="@drawable/ic_airpod_case_24"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
||||||
app:layout_constraintEnd_toStartOf="@id/pod_right"
|
app:layout_constraintEnd_toStartOf="@id/pod_right_container"
|
||||||
app:layout_constraintStart_toEndOf="@id/pod_left"
|
app:layout_constraintStart_toEndOf="@id/pod_left_container"
|
||||||
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
||||||
app:layout_constraintVertical_bias="0.0"
|
app:layout_constraintVertical_bias="0.0">
|
||||||
tools:text="Case\n50%" />
|
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<ImageView
|
||||||
android:id="@+id/pod_right"
|
android:id="@+id/pod_case_icon"
|
||||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_airpod_case_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_case_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_case_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_case_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:text="@string/pods_case_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_case_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_case_battery_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_case_battery_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_case_battery_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_case_battery_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/general_value_unknown_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_case_battery_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_case_label" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_case_charging_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_power_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_case_charging_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_case_charging_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_case_charging_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/pods_charging_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_case_charging_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_case_battery_label" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_case_lid_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_grid_view_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_case_lid_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_case_lid_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_case_lid_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/pods_case_status_closed_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_case_lid_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_case_charging_label" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/pod_right_container"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="4dp"
|
||||||
android:layout_marginEnd="16dp"
|
android:layout_marginEnd="16dp"
|
||||||
android:gravity="center_horizontal"
|
|
||||||
app:drawableTopCompat="@drawable/ic_airpod_right_24"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/pod_case"
|
app:layout_constraintStart_toEndOf="@id/pod_case_container"
|
||||||
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
||||||
app:layout_constraintVertical_bias="0.0"
|
app:layout_constraintVertical_bias="0.0">
|
||||||
tools:text="R 90%" />
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_right_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_airpod_right_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_right_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_right_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_right_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:text="@string/pods_dual_right_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_right_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_right_battery_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_right_battery_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_right_battery_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_right_battery_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/general_value_unknown_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_right_battery_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_right_label" />
|
||||||
|
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_right_charging_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_power_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_right_charging_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_right_charging_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_right_charging_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/pods_charging_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_right_charging_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_right_battery_label" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_right_microphone_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_keyboard_voice_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_right_microphone_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_right_microphone_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_right_microphone_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/pods_microphone_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_right_microphone_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_right_charging_label" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_right_wear_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_hearing_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_right_wear_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_right_wear_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_right_wear_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/pods_inear_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_right_wear_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_right_microphone_label" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
android:id="@+id/barrier_bottom"
|
android:id="@+id/barrier_bottom"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
app:barrierDirection="bottom"
|
app:barrierDirection="bottom"
|
||||||
app:constraint_referenced_ids="pod_left,pod_case,pod_right" />
|
app:constraint_referenced_ids="pod_left_container,pod_case_container,pod_right_container" />
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:id="@+id/status"
|
android:id="@+id/status"
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/reception"
|
app:layout_constraintStart_toEndOf="@id/reception"
|
||||||
app:layout_constraintTop_toTopOf="@+id/reception"
|
app:layout_constraintTop_toTopOf="@+id/reception"
|
||||||
app:srcCompat="@drawable/ic_baseline_signal_cellular_alt_24"
|
app:srcCompat="@drawable/ic_baseline_settings_input_antenna_24"
|
||||||
tools:text="Yours (RSSI -61)" />
|
tools:text="Yours (RSSI -61)" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
@@ -90,21 +90,40 @@
|
|||||||
app:constraint_referenced_ids="headphones"
|
app:constraint_referenced_ids="headphones"
|
||||||
tools:layout_editor_absoluteY="60dp" />
|
tools:layout_editor_absoluteY="60dp" />
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/headphones"
|
android:id="@+id/headphones"
|
||||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:gravity="center_horizontal"
|
android:layout_marginEnd="8dp"
|
||||||
app:drawableTopCompat="@drawable/ic_device_generic_headphones"
|
|
||||||
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_goneMarginBottom="16dp"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
||||||
app:layout_constraintVertical_bias="0.0"
|
app:layout_constraintVertical_bias="0.0"
|
||||||
tools:text="Headphones\n50%\nCharging" />
|
app:layout_goneMarginBottom="16dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/battery_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/battery_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/battery_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/battery_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/general_value_unknown_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/battery_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
android:id="@+id/barrier_bottom"
|
android:id="@+id/barrier_bottom"
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/reception"
|
app:layout_constraintStart_toEndOf="@id/reception"
|
||||||
app:layout_constraintTop_toTopOf="@+id/reception"
|
app:layout_constraintTop_toTopOf="@+id/reception"
|
||||||
app:srcCompat="@drawable/ic_baseline_signal_cellular_alt_24"
|
app:srcCompat="@drawable/ic_baseline_settings_input_antenna_24"
|
||||||
tools:text="Yours (RSSI -61)" />
|
tools:text="Yours (RSSI -61)" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
@@ -90,21 +90,81 @@
|
|||||||
app:constraint_referenced_ids="headphones"
|
app:constraint_referenced_ids="headphones"
|
||||||
tools:layout_editor_absoluteY="60dp" />
|
tools:layout_editor_absoluteY="60dp" />
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/headphones"
|
android:id="@+id/headphones"
|
||||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:gravity="center_horizontal"
|
android:layout_marginEnd="8dp"
|
||||||
app:drawableTopCompat="@drawable/ic_device_generic_headphones"
|
android:minHeight="64dp"
|
||||||
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_goneMarginBottom="16dp"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
||||||
app:layout_constraintVertical_bias="0.0"
|
app:layout_constraintVertical_bias="0.0"
|
||||||
tools:text="Headphones\n50%\nCharging" />
|
app:layout_goneMarginBottom="16dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/battery_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/battery_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/battery_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/battery_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/general_value_unknown_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/battery_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/charging_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_power_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/charging_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/charging_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/charging_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/pods_charging_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/charging_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/battery_label" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/wear_icon"
|
||||||
|
style="@style/PodInfoItemIcon"
|
||||||
|
android:src="@drawable/ic_baseline_hearing_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/wear_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/wear_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/wear_label"
|
||||||
|
style="@style/PodInfoItemText"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:text="@string/headset_being_worn_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/wear_icon"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/charging_label" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Barrier
|
<androidx.constraintlayout.widget.Barrier
|
||||||
android:id="@+id/barrier_bottom"
|
android:id="@+id/barrier_bottom"
|
||||||
|
|||||||
@@ -66,7 +66,7 @@
|
|||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/reception"
|
app:layout_constraintStart_toEndOf="@id/reception"
|
||||||
app:layout_constraintTop_toTopOf="@+id/reception"
|
app:layout_constraintTop_toTopOf="@+id/reception"
|
||||||
app:srcCompat="@drawable/ic_baseline_signal_cellular_alt_24"
|
app:srcCompat="@drawable/ic_baseline_settings_input_antenna_24"
|
||||||
tools:text="Yours (RSSI -61)" />
|
tools:text="Yours (RSSI -61)" />
|
||||||
|
|
||||||
<com.google.android.material.textview.MaterialTextView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
|||||||
@@ -1,72 +1,182 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout 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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content">
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
<ImageView
|
||||||
android:id="@+id/pod_label"
|
android:id="@+id/pod_icon"
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/ic_device_generic_earbuds"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_label"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/pod_label"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_label"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginHorizontal="8dp"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:layout_marginBottom="4dp"
|
app:layout_constraintEnd_toStartOf="@id/signal"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="AirPods Pro" />
|
tools:text="AirPods Pro" />
|
||||||
|
|
||||||
<LinearLayout
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/signal"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginHorizontal="8dp"
|
android:drawableEnd="@drawable/ic_baseline_signal_cellular_alt_24"
|
||||||
android:orientation="horizontal">
|
android:gravity="center"
|
||||||
|
android:minWidth="44dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/pod_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/pod_label"
|
||||||
|
tools:text="-90%" />
|
||||||
|
|
||||||
<FrameLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="0dp"
|
android:id="@id/pod_left_container"
|
||||||
android:layout_height="0dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_weight="1" />
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/pod_case_container"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="spread"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_label">
|
||||||
|
|
||||||
<TextView
|
<ImageView
|
||||||
android:id="@+id/pod_left"
|
android:id="@+id/pod_left_icon"
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:drawableStart="@drawable/ic_airpod_left_24"
|
android:src="@drawable/ic_airpod_left_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_left_battery_label"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/pod_left_battery_icon"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_left_battery_label" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@id/pod_left_battery_icon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_left_battery_label"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/pod_left_battery_label"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_left_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_left_battery_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_left_battery_label"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_left_battery_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="100%" />
|
tools:text="100%" />
|
||||||
|
|
||||||
<FrameLayout
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1" />
|
|
||||||
|
|
||||||
<TextView
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/pod_case"
|
android:id="@+id/pod_case_container"
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/pod_right_container"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="spread"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_left_container"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_label">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_case_icon"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:layout_marginHorizontal="8dp"
|
android:src="@drawable/ic_airpod_case_24"
|
||||||
android:drawableStart="@drawable/ic_airpod_case_24"
|
app:layout_constraintBottom_toBottomOf="@id/pod_case_battery_label"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/pod_case_battery_icon"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_case_battery_label" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@id/pod_case_battery_icon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_case_battery_label"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/pod_case_battery_label"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_case_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_case_battery_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_case_battery_label"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_case_battery_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="100%" />
|
tools:text="100%" />
|
||||||
|
|
||||||
<FrameLayout
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1" />
|
|
||||||
|
|
||||||
<TextView
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/pod_right"
|
android:id="@id/pod_right_container"
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="spread"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_case_container"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/pod_label">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/pod_right_icon"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
android:drawableStart="@drawable/ic_airpod_right_24"
|
android:src="@drawable/ic_airpod_right_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_right_battery_label"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/pod_right_battery_icon"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_right_battery_label" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@id/pod_right_battery_icon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/pod_right_battery_label"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/pod_right_battery_label"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_right_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/pod_right_battery_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/pod_right_battery_label"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/pod_right_battery_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="100%" />
|
tools:text="100%" />
|
||||||
|
|
||||||
<FrameLayout
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1" />
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
</LinearLayout>
|
|
||||||
</LinearLayout>
|
|
||||||
@@ -1,29 +1,81 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout 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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content">
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
<ImageView
|
||||||
android:id="@+id/headphones_label"
|
android:id="@+id/headphones_icon"
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/ic_device_generic_earbuds"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/headphones_label"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/headphones_label"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/headphones_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/headphones_label"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginHorizontal="8dp"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:layout_marginBottom="4dp"
|
app:layout_constraintEnd_toStartOf="@id/signal"
|
||||||
android:gravity="center"
|
app:layout_constraintStart_toEndOf="@id/headphones_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="AirPods Max" />
|
tools:text="AirPods Max" />
|
||||||
|
|
||||||
<TextView
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:id="@+id/headphones"
|
android:id="@+id/signal"
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:drawableEnd="@drawable/ic_baseline_signal_cellular_alt_24"
|
||||||
android:layout_marginHorizontal="16dp"
|
android:gravity="center"
|
||||||
android:drawableStart="@drawable/ic_device_generic_headphones"
|
android:minWidth="44dp"
|
||||||
tools:text="100%" />
|
app:layout_constraintBottom_toBottomOf="@+id/headphones_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/headphones_label"
|
||||||
|
tools:text="-90%" />
|
||||||
|
|
||||||
</LinearLayout>
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/headphones_container"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="spread"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/headphones_label">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@id/headphones_battery_icon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/headphones_battery_label"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/headphones_battery_label"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/headphones_battery_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/headphones_battery_label"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/headphones_battery_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="100%" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -1,38 +1,81 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout 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"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content">
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<TextView
|
<ImageView
|
||||||
|
android:id="@+id/headphones_icon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:src="@drawable/ic_device_generic_earbuds"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/headphones_label"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/headphones_label"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/headphones_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
android:id="@+id/headphones_label"
|
android:id="@+id/headphones_label"
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginHorizontal="8dp"
|
||||||
android:layout_marginTop="4dp"
|
android:layout_marginTop="4dp"
|
||||||
android:layout_marginBottom="4dp"
|
app:layout_constraintEnd_toStartOf="@id/signal"
|
||||||
android:gravity="center"
|
app:layout_constraintStart_toEndOf="@id/headphones_icon"
|
||||||
tools:text="AirPods Max" />
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="Beats Solo 3" />
|
||||||
|
|
||||||
<LinearLayout
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/signal"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_gravity="center"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal">
|
android:drawableEnd="@drawable/ic_baseline_signal_cellular_alt_24"
|
||||||
|
android:gravity="center"
|
||||||
|
android:minWidth="44dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@+id/headphones_label"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@+id/headphones_label"
|
||||||
|
tools:text="-90%" />
|
||||||
|
|
||||||
<TextView
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/headphones"
|
android:id="@+id/headphones_container"
|
||||||
style="@style/TextAppearance.Compat.Notification.Title"
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="spread"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/headphones_label">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@id/headphones_battery_icon"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center"
|
android:src="@drawable/ic_baseline_battery_unknown_24"
|
||||||
android:layout_marginHorizontal="16dp"
|
app:layout_constraintBottom_toBottomOf="@id/headphones_battery_label"
|
||||||
android:drawableStart="@drawable/ic_device_generic_headphones"
|
app:layout_constraintEnd_toStartOf="@id/headphones_battery_label"
|
||||||
android:gravity="center"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="@id/headphones_battery_label" />
|
||||||
|
|
||||||
|
<com.google.android.material.textview.MaterialTextView
|
||||||
|
android:id="@+id/headphones_battery_label"
|
||||||
|
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="4dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/headphones_battery_icon"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:text="100%" />
|
tools:text="100%" />
|
||||||
|
|
||||||
</LinearLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?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"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:minWidth="320dp"
|
||||||
|
android:minHeight="96dp"
|
||||||
|
android:layout_margin="8dp">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/popup_content"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_constraintBottom_toTopOf="@id/close_action"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_bias="1.0" />
|
||||||
|
|
||||||
|
<com.google.android.material.button.MaterialButton
|
||||||
|
android:id="@+id/close_action"
|
||||||
|
style="@style/Widget.Material3.Button.TextButton"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/general_close_action"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
app:layout_constraintHorizontal_bias="1.0"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/popup_content" />
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
<string name="general_grant_permission_action">Grant permission</string>
|
<string name="general_grant_permission_action">Grant permission</string>
|
||||||
<string name="general_upgrade_action">Upgrade</string>
|
<string name="general_upgrade_action">Upgrade</string>
|
||||||
<string name="general_check_action">Check</string>
|
<string name="general_check_action">Check</string>
|
||||||
|
<string name="general_close_action">Close</string>
|
||||||
|
|
||||||
<string name="debug_debuglog_size_label">Size</string>
|
<string name="debug_debuglog_size_label">Size</string>
|
||||||
<string name="debug_debuglog_size_compressed_label">Compressed size</string>
|
<string name="debug_debuglog_size_compressed_label">Compressed size</string>
|
||||||
@@ -23,39 +24,23 @@
|
|||||||
<string name="debug_debuglog_record_action">Record debug log</string>
|
<string name="debug_debuglog_record_action">Record debug log</string>
|
||||||
|
|
||||||
<string name="permission_bluetooth_connect_label">Bluetooth connect</string>
|
<string name="permission_bluetooth_connect_label">Bluetooth connect</string>
|
||||||
<string name="permission_bluetooth_connect_description">This permission is required to connect and interact with paired Bluetooth devices.</string>
|
<string name="permission_bluetooth_connect_description">This app requires the \"Bluetooth connect\" permission to interact with paired devices and initiate connections.</string>
|
||||||
<string name="permission_bluetooth_scan_label">Bluetooth scanning</string>
|
<string name="permission_bluetooth_scan_label">Bluetooth scanning</string>
|
||||||
<string name="permission_bluetooth_scan_description">The Bluetooth scanning permissions allows apps to discover and receive Bluetooth data from nearby devices.</string>
|
<string name="permission_bluetooth_scan_description">The \"Bluetooth scanning\" permission allows this app to discover and receive Bluetooth data from nearby devices such as your AirPods.</string>
|
||||||
<string name="permission_bluetooth_label">Bluetooth</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_bluetooth_description">This app requires the \"Bluetooth\" to connect to paired bluetooth devices.</string>
|
||||||
<string name="permission_access_fine_location_label">Access fine location</string>
|
<string name="permission_access_fine_location_label">Access fine location</string>
|
||||||
<string name="permission_access_fine_location_description">The "fine location" permission is required to receive Bluetooth Low Energy data. This app will never access your location! But data from known devices (e.g. AirTags), could be used to determine your location, this is why access to Bluetooth data is guarded by a location permission.</string>
|
<string name="permission_access_fine_location_description">CAPod uses the \"fine location\" permission to receive Bluetooth Low Energy data. Your headphones use Bluetooth Low Energy technology to broadcast their status. This app will NOT use Bluetooth data to determine your location.</string>
|
||||||
<string name="permission_background_location_label">Background location access</string>
|
<string name="permission_background_location_label">Background location access</string>
|
||||||
<string name="permission_background_location_description">Background location access is required to receive Bluetooth data while the app is in the background. This app will never access your location! The permission is called "location" because Bluetooth technology can also be used to determine your location.</string>
|
<string name="permission_background_location_description">CAPod uses \"background location access\" to enable features like \"Show popup\" and \"AutoConnect\" while the app is closed. Background location access allows the app to receive Bluetooth Low Energy data while it is in the background. This app will NOT use Bluetooth data to determine your location.</string>
|
||||||
<string name="permission_ignore_battery_optimizations_label">Disable battery optimizations</string>
|
<string name="permission_ignore_battery_optimizations_label">Disable battery optimizations</string>
|
||||||
<string name="permission_ignore_battery_optimizations_description">Battery optimizations can prevent CAPod from receiving Bluetooth data while the app is in the background.</string>
|
<string name="permission_ignore_battery_optimizations_description">Battery optimizations prevent this app from reliably receiving Bluetooth data while the app is in the background.</string>
|
||||||
|
|
||||||
<string name="pods_single_basic_status_short">Headphones: %1$s</string>
|
|
||||||
<string name="pods_single_headphones_label">Headphones</string>
|
|
||||||
<string name="pods_dual_left_label">Left pod</string>
|
<string name="pods_dual_left_label">Left pod</string>
|
||||||
<string name="pods_dual_right_label">Right pod</string>
|
<string name="pods_dual_right_label">Right pod</string>
|
||||||
<string name="pods_case_label">Case</string>
|
<string name="pods_case_label">Case</string>
|
||||||
<string name="pods_case_status_open_label">Open</string>
|
<string name="pods_case_status_open_label">Open</string>
|
||||||
<string name="pods_case_status_closed_label">Closed</string>
|
<string name="pods_case_status_closed_label">Closed</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">Not connected to a device</string>
|
<string name="pods_connection_state_disconnected_label">Not connected to a device</string>
|
||||||
<string name="pods_connection_state_idle_label">Connected to a device, but idle</string>
|
<string name="pods_connection_state_idle_label">Connected to a device, but idle</string>
|
||||||
<string name="pods_connection_state_music_label">In music mode</string>
|
<string name="pods_connection_state_music_label">In music mode</string>
|
||||||
@@ -70,6 +55,7 @@
|
|||||||
<string name="pods_inear_label">In ear</string>
|
<string name="pods_inear_label">In ear</string>
|
||||||
<string name="pods_microphone_label">Microphone</string>
|
<string name="pods_microphone_label">Microphone</string>
|
||||||
<string name="pods_yours">Yours</string>
|
<string name="pods_yours">Yours</string>
|
||||||
|
<string name="headset_being_worn_label">Being worn</string>
|
||||||
|
|
||||||
<string name="overview_nomaindevice_label">No primary device</string>
|
<string name="overview_nomaindevice_label">No primary device</string>
|
||||||
<string name="overview_nomaindevice_description">All detected devices are unlikely to be yours. Power on and connect your device or adjust the settings.</string>
|
<string name="overview_nomaindevice_description">All detected devices are unlikely to be yours. Power on and connect your device or adjust the settings.</string>
|
||||||
@@ -84,7 +70,6 @@
|
|||||||
<string name="settings_acknowledgements_label">Acknowledgements</string>
|
<string name="settings_acknowledgements_label">Acknowledgements</string>
|
||||||
|
|
||||||
<string name="changelog_label">Changelog</string>
|
<string name="changelog_label">Changelog</string>
|
||||||
<string name="documentation_label">Documentation</string>
|
|
||||||
<string name="settings_support_email_developer_label">Email developer</string>
|
<string name="settings_support_email_developer_label">Email developer</string>
|
||||||
|
|
||||||
<string name="settings_support_installid_label">Install ID</string>
|
<string name="settings_support_installid_label">Install ID</string>
|
||||||
@@ -148,4 +133,8 @@
|
|||||||
<string name="overview_bluetooth_disabled_description">Bluetooth is disabled, enable it ;).</string>
|
<string name="overview_bluetooth_disabled_description">Bluetooth is disabled, enable it ;).</string>
|
||||||
<string name="help_translate_label">Translation</string>
|
<string name="help_translate_label">Translation</string>
|
||||||
<string name="help_translate_description">Help translate this into your favorite language.</string>
|
<string name="help_translate_description">Help translate this into your favorite language.</string>
|
||||||
|
<string name="settings_onepod_mode_label">One pod mode</string>
|
||||||
|
<string name="settings_onepod_mode_description">Wearing both pods is not required, wearing a single pod is sufficient to trigger reactions.</string>
|
||||||
|
<string name="permission_system_alert_window_label">System Alert Window</string>
|
||||||
|
<string name="permission_system_alert_window_description">Allow CAPod to draw over other apps to make the feature \"Show PopUp\" possible.</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -6,4 +6,14 @@
|
|||||||
<item name="android:layout_marginStart">8dp</item>
|
<item name="android:layout_marginStart">8dp</item>
|
||||||
<item name="android:layout_marginEnd">8dp</item>
|
<item name="android:layout_marginEnd">8dp</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="PodInfoItemIcon">
|
||||||
|
<item name="android:layout_height">20dp</item>
|
||||||
|
<item name="android:layout_width">20dp</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="PodInfoItemText" parent="TextAppearance.MaterialComponents.Body2">
|
||||||
|
<item name="android:singleLine">true</item>
|
||||||
|
<item name="android:ellipsize">end</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -20,16 +20,6 @@
|
|||||||
app:title="@string/settings_support_label" />
|
app:title="@string/settings_support_label" />
|
||||||
|
|
||||||
<PreferenceCategory android:title="@string/settings_category_other_label">
|
<PreferenceCategory android:title="@string/settings_category_other_label">
|
||||||
|
|
||||||
<Preference
|
|
||||||
android:icon="@drawable/ic_baseline_book_24"
|
|
||||||
app:summary="@string/settings_privacy_policy_desc"
|
|
||||||
app:title="@string/settings_privacy_policy_label">
|
|
||||||
<intent
|
|
||||||
android:action="android.intent.action.VIEW"
|
|
||||||
android:data="https://github.com/d4rken/capod-public/blob/main/PRIVACY_POLICY.md" />
|
|
||||||
</Preference>
|
|
||||||
|
|
||||||
<Preference
|
<Preference
|
||||||
android:icon="@drawable/ic_changelog_onsurface"
|
android:icon="@drawable/ic_changelog_onsurface"
|
||||||
android:key="core.changelog"
|
android:key="core.changelog"
|
||||||
@@ -56,5 +46,11 @@
|
|||||||
app:summary="@string/general_thank_you_label"
|
app:summary="@string/general_thank_you_label"
|
||||||
app:title="@string/settings_acknowledgements_label" />
|
app:title="@string/settings_acknowledgements_label" />
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:key="core.privacy"
|
||||||
|
android:icon="@drawable/ic_baseline_book_24"
|
||||||
|
app:summary="@string/settings_privacy_policy_desc"
|
||||||
|
app:title="@string/settings_privacy_policy_label" />
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
</PreferenceScreen>
|
</PreferenceScreen>
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<CheckBoxPreference
|
||||||
|
android:icon="@drawable/ic_baseline_looks_one_24"
|
||||||
|
android:key="reaction.onepod.enabled"
|
||||||
|
android:summary="@string/settings_onepod_mode_description"
|
||||||
|
android:title="@string/settings_onepod_mode_label" />
|
||||||
|
|
||||||
|
|
||||||
<CheckBoxPreference
|
<CheckBoxPreference
|
||||||
android:icon="@drawable/ic_baseline_play_circle_24"
|
android:icon="@drawable/ic_baseline_play_circle_24"
|
||||||
android:key="reaction.autoplay.enabled"
|
android:key="reaction.autoplay.enabled"
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ class BasicSingleApplePodsTest : BaseAirPodsTest() {
|
|||||||
rawDeviceModel shouldBe 0x0520.toUShort()
|
rawDeviceModel shouldBe 0x0520.toUShort()
|
||||||
rawStatus shouldBe 0x00.toUByte()
|
rawStatus shouldBe 0x00.toUByte()
|
||||||
rawPodsBattery shouldBe 0xF5.toUByte()
|
rawPodsBattery shouldBe 0xF5.toUByte()
|
||||||
rawCaseBattery shouldBe 0x0F.toUByte()
|
rawFlags shouldBe 0x0.toUShort()
|
||||||
|
rawCaseBattery shouldBe 0xF.toUShort()
|
||||||
rawCaseLidState shouldBe 0x01.toUByte()
|
rawCaseLidState shouldBe 0x01.toUByte()
|
||||||
rawDeviceColor shouldBe 0x01.toUByte()
|
rawDeviceColor shouldBe 0x01.toUByte()
|
||||||
rawSuffix shouldBe 0x00.toUByte()
|
rawSuffix shouldBe 0x00.toUByte()
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.darken.capod.pods.core.apple
|
package eu.darken.capod.pods.core.apple
|
||||||
|
|
||||||
import eu.darken.capod.pods.core.HasDualPods
|
|
||||||
import io.kotest.matchers.shouldBe
|
import io.kotest.matchers.shouldBe
|
||||||
import kotlinx.coroutines.test.runBlockingTest
|
import kotlinx.coroutines.test.runBlockingTest
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
@@ -15,7 +14,8 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
|||||||
rawDeviceModel shouldBe 0x0e20.toUShort()
|
rawDeviceModel shouldBe 0x0e20.toUShort()
|
||||||
rawStatus shouldBe 0x54.toUByte()
|
rawStatus shouldBe 0x54.toUByte()
|
||||||
rawPodsBattery shouldBe 0xAA.toUByte()
|
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||||
rawCaseBattery shouldBe 0xB5.toUByte()
|
rawFlags shouldBe 0xB.toUShort()
|
||||||
|
rawCaseBattery shouldBe 0x5.toUShort()
|
||||||
rawCaseLidState shouldBe 0x31.toUByte()
|
rawCaseLidState shouldBe 0x31.toUByte()
|
||||||
rawDeviceColor shouldBe 0x00.toUByte()
|
rawDeviceColor shouldBe 0x00.toUByte()
|
||||||
rawSuffix shouldBe 0x00.toUByte()
|
rawSuffix shouldBe 0x00.toUByte()
|
||||||
@@ -27,12 +27,14 @@ class DualApplePodsTest : BaseAirPodsTest() {
|
|||||||
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") {
|
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
|
// 00101011
|
||||||
// --^-----
|
// --^-----
|
||||||
microPhonePod shouldBe HasDualPods.Pod.LEFT
|
isLeftPodMicrophone shouldBe true
|
||||||
|
isRightPodMicrophone shouldBe false
|
||||||
}
|
}
|
||||||
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") {
|
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
|
// 00001011
|
||||||
// --^-----
|
// --^-----
|
||||||
microPhonePod shouldBe HasDualPods.Pod.RIGHT
|
isLeftPodMicrophone shouldBe false
|
||||||
|
isRightPodMicrophone shouldBe true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ class SingleApplePodsTest : BaseAirPodsTest() {
|
|||||||
rawDeviceModel shouldBe 0x0A20.toUShort()
|
rawDeviceModel shouldBe 0x0A20.toUShort()
|
||||||
rawStatus shouldBe 0x62.toUByte()
|
rawStatus shouldBe 0x62.toUByte()
|
||||||
rawPodsBattery shouldBe 0x04.toUByte()
|
rawPodsBattery shouldBe 0x04.toUByte()
|
||||||
rawCaseBattery shouldBe 0x80.toUByte()
|
rawFlags shouldBe 0x8.toUShort()
|
||||||
|
rawCaseBattery shouldBe 0x0.toUShort()
|
||||||
rawCaseLidState shouldBe 0x01.toUByte()
|
rawCaseLidState shouldBe 0x01.toUByte()
|
||||||
rawDeviceColor shouldBe 0x0F.toUByte()
|
rawDeviceColor shouldBe 0x0F.toUByte()
|
||||||
rawSuffix shouldBe 0x40.toUByte()
|
rawSuffix shouldBe 0x40.toUByte()
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ class AirPodsGen1Test : BaseAirPodsTest() {
|
|||||||
rawDeviceModel shouldBe 0x0220.toUShort()
|
rawDeviceModel shouldBe 0x0220.toUShort()
|
||||||
rawStatus shouldBe 0x55.toUByte()
|
rawStatus shouldBe 0x55.toUByte()
|
||||||
rawPodsBattery shouldBe 0xAF.toUByte()
|
rawPodsBattery shouldBe 0xAF.toUByte()
|
||||||
rawCaseBattery shouldBe 0x56.toUByte()
|
rawFlags shouldBe 0x5.toUShort()
|
||||||
|
rawCaseBattery shouldBe 0x6.toUShort()
|
||||||
rawCaseLidState shouldBe 0x31.toUByte()
|
rawCaseLidState shouldBe 0x31.toUByte()
|
||||||
rawDeviceColor shouldBe 0x00.toUByte()
|
rawDeviceColor shouldBe 0x00.toUByte()
|
||||||
rawSuffix shouldBe 0x00.toUByte()
|
rawSuffix shouldBe 0x00.toUByte()
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ class AirPodsGen2Test : BaseAirPodsTest() {
|
|||||||
rawDeviceModel shouldBe 0x0F20.toUShort()
|
rawDeviceModel shouldBe 0x0F20.toUShort()
|
||||||
rawStatus shouldBe 0x02.toUByte()
|
rawStatus shouldBe 0x02.toUByte()
|
||||||
rawPodsBattery shouldBe 0xF9.toUByte()
|
rawPodsBattery shouldBe 0xF9.toUByte()
|
||||||
rawCaseBattery shouldBe 0x8F.toUByte()
|
rawFlags shouldBe 0x8.toUShort()
|
||||||
|
rawCaseBattery shouldBe 0xF.toUShort()
|
||||||
rawCaseLidState shouldBe 0x01.toUByte()
|
rawCaseLidState shouldBe 0x01.toUByte()
|
||||||
rawDeviceColor shouldBe 0x00.toUByte()
|
rawDeviceColor shouldBe 0x00.toUByte()
|
||||||
rawSuffix shouldBe 0x05.toUByte()
|
rawSuffix shouldBe 0x05.toUByte()
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ class AirPodsGen3Test : BaseAirPodsTest() {
|
|||||||
rawDeviceModel shouldBe 0x1320.toUShort()
|
rawDeviceModel shouldBe 0x1320.toUShort()
|
||||||
rawStatus shouldBe 0x75.toUByte()
|
rawStatus shouldBe 0x75.toUByte()
|
||||||
rawPodsBattery shouldBe 0xAA.toUByte()
|
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||||
rawCaseBattery shouldBe 0xB9.toUByte()
|
rawFlags shouldBe 0xB.toUShort()
|
||||||
|
rawCaseBattery shouldBe 0x9.toUShort()
|
||||||
rawCaseLidState shouldBe 0x31.toUByte()
|
rawCaseLidState shouldBe 0x31.toUByte()
|
||||||
rawDeviceColor shouldBe 0x00.toUByte()
|
rawDeviceColor shouldBe 0x00.toUByte()
|
||||||
rawSuffix shouldBe 0x04.toUByte()
|
rawSuffix shouldBe 0x04.toUByte()
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ class AirPodsMaxTest : BaseAirPodsTest() {
|
|||||||
rawDeviceModel shouldBe 0x0A20.toUShort()
|
rawDeviceModel shouldBe 0x0A20.toUShort()
|
||||||
rawStatus shouldBe 0x62.toUByte()
|
rawStatus shouldBe 0x62.toUByte()
|
||||||
rawPodsBattery shouldBe 0x04.toUByte()
|
rawPodsBattery shouldBe 0x04.toUByte()
|
||||||
rawCaseBattery shouldBe 0x80.toUByte()
|
rawFlags shouldBe 0x8.toUShort()
|
||||||
|
rawCaseBattery shouldBe 0x0.toUShort()
|
||||||
rawCaseLidState shouldBe 0x01.toUByte()
|
rawCaseLidState shouldBe 0x01.toUByte()
|
||||||
rawDeviceColor shouldBe 0x0F.toUByte()
|
rawDeviceColor shouldBe 0x0F.toUByte()
|
||||||
rawSuffix shouldBe 0x40.toUByte()
|
rawSuffix shouldBe 0x40.toUByte()
|
||||||
@@ -36,7 +37,8 @@ class AirPodsMaxTest : BaseAirPodsTest() {
|
|||||||
rawDeviceModel shouldBe 0x0A20.toUShort()
|
rawDeviceModel shouldBe 0x0A20.toUShort()
|
||||||
rawStatus shouldBe 0x02.toUByte()
|
rawStatus shouldBe 0x02.toUByte()
|
||||||
rawPodsBattery shouldBe 0x05.toUByte()
|
rawPodsBattery shouldBe 0x05.toUByte()
|
||||||
rawCaseBattery shouldBe 0x80.toUByte()
|
rawFlags shouldBe 0x8.toUShort()
|
||||||
|
rawCaseBattery shouldBe 0x0.toUShort()
|
||||||
rawCaseLidState shouldBe 0x04.toUByte()
|
rawCaseLidState shouldBe 0x04.toUByte()
|
||||||
rawDeviceColor shouldBe 0x0F.toUByte()
|
rawDeviceColor shouldBe 0x0F.toUByte()
|
||||||
rawSuffix shouldBe 0x44.toUByte()
|
rawSuffix shouldBe 0x44.toUByte()
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package eu.darken.capod.pods.core.apple.airpods
|
package eu.darken.capod.pods.core.apple.airpods
|
||||||
|
|
||||||
import eu.darken.capod.pods.core.HasDualPods
|
|
||||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||||
import io.kotest.matchers.shouldBe
|
import io.kotest.matchers.shouldBe
|
||||||
@@ -17,12 +16,14 @@ class AirPodsProTest : BaseAirPodsTest() {
|
|||||||
rawDeviceModel shouldBe 0x0e20.toUShort()
|
rawDeviceModel shouldBe 0x0e20.toUShort()
|
||||||
rawStatus shouldBe 0x54.toUByte()
|
rawStatus shouldBe 0x54.toUByte()
|
||||||
rawPodsBattery shouldBe 0xAA.toUByte()
|
rawPodsBattery shouldBe 0xAA.toUByte()
|
||||||
rawCaseBattery shouldBe 0xB5.toUByte()
|
rawFlags shouldBe 0xB.toUShort()
|
||||||
|
rawCaseBattery shouldBe 0x5.toUShort()
|
||||||
rawCaseLidState shouldBe 0x31.toUByte()
|
rawCaseLidState shouldBe 0x31.toUByte()
|
||||||
rawDeviceColor shouldBe 0x00.toUByte()
|
rawDeviceColor shouldBe 0x00.toUByte()
|
||||||
rawSuffix shouldBe 0x00.toUByte()
|
rawSuffix shouldBe 0x00.toUByte()
|
||||||
|
|
||||||
microPhonePod shouldBe HasDualPods.Pod.RIGHT
|
isLeftPodMicrophone shouldBe true
|
||||||
|
isRightPodMicrophone shouldBe false
|
||||||
|
|
||||||
batteryLeftPodPercent shouldBe 1.0f
|
batteryLeftPodPercent shouldBe 1.0f
|
||||||
batteryRightPodPercent shouldBe 1.0f
|
batteryRightPodPercent shouldBe 1.0f
|
||||||
@@ -39,7 +40,8 @@ class AirPodsProTest : BaseAirPodsTest() {
|
|||||||
@Test
|
@Test
|
||||||
fun `test AirPods from my downstairs neighbour`() = runBlockingTest {
|
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") {
|
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 HasDualPods.Pod.RIGHT
|
isLeftPodMicrophone shouldBe false
|
||||||
|
isRightPodMicrophone shouldBe true
|
||||||
|
|
||||||
batteryLeftPodPercent shouldBe null
|
batteryLeftPodPercent shouldBe null
|
||||||
batteryRightPodPercent shouldBe 0.3f
|
batteryRightPodPercent shouldBe 0.3f
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user