mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-15 02:36:12 -04:00
Improved device cards
This commit is contained in:
@@ -26,7 +26,7 @@ class BleScanner @Inject constructor(
|
||||
// TODO check Bluetooth enabled
|
||||
fun scan(
|
||||
filter: Set<ScanFilter> = ProximityPairing.getBleScanFilter(),
|
||||
mode: Int = ScanSettings.SCAN_MODE_LOW_LATENCY,
|
||||
mode: Int = ScanSettings.SCAN_MODE_LOW_POWER,
|
||||
delay: Long = 1,
|
||||
): Flow<List<ScanResult>> = callbackFlow {
|
||||
val scanner = bluetoothManager.adapter.bluetoothLeScanner
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
package eu.darken.capod.common.debug.autoreport
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceDataStore
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.androidstarter.common.preferences.Settings
|
||||
import eu.darken.capod.common.preferences.PreferenceStoreMapper
|
||||
import eu.darken.capod.common.preferences.createFlowPreference
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
@@ -9,12 +13,26 @@ import javax.inject.Singleton
|
||||
@Singleton
|
||||
class DebugSettings @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
) : Settings() {
|
||||
|
||||
private val prefs by lazy {
|
||||
context.getSharedPreferences("settings_debug", Context.MODE_PRIVATE)
|
||||
override val preferences: SharedPreferences = context.getSharedPreferences("settings_debug", Context.MODE_PRIVATE)
|
||||
|
||||
val isAutoReportEnabled = preferences.createFlowPreference("debug.bugreport.automatic.enabled", true)
|
||||
|
||||
val isDebugModeEnabled = preferences.createFlowPreference("debug.mode.enabled", false)
|
||||
|
||||
override val preferenceDataStore: PreferenceDataStore = object : PreferenceStoreMapper() {
|
||||
override fun getBoolean(key: String, defValue: Boolean): Boolean = when (key) {
|
||||
isAutoReportEnabled.key -> isAutoReportEnabled.value
|
||||
isDebugModeEnabled.key -> isDebugModeEnabled.value
|
||||
else -> super.getBoolean(key, defValue)
|
||||
}
|
||||
|
||||
override fun putBoolean(key: String, value: Boolean) = when (key) {
|
||||
isAutoReportEnabled.key -> isAutoReportEnabled.update { value }
|
||||
isDebugModeEnabled.key -> isDebugModeEnabled.update { value }
|
||||
else -> super.putBoolean(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
val isAutoReportEnabled = prefs.createFlowPreference("bugreport.automatic.enabled", true)
|
||||
|
||||
}
|
||||
@@ -11,4 +11,16 @@ interface BindableVH<ItemT, ViewBindingT : ViewBinding> {
|
||||
fun bind(item: ItemT, payloads: MutableList<Any> = mutableListOf()) = with(viewBinding.value) {
|
||||
onBindData(item, payloads)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
inline fun <reified ItemT, ViewBindingT : ViewBinding> BindableVH<ItemT, ViewBindingT>.binding(
|
||||
payload: Boolean = true,
|
||||
crossinline block: ViewBindingT.(ItemT) -> Unit,
|
||||
): ViewBindingT.(item: ItemT, payloads: List<Any>) -> Unit = { item: ItemT, payloads: List<Any> ->
|
||||
val newestItem = when (payload) {
|
||||
true -> payloads.filterIsInstance<ItemT>().lastOrNull() ?: item
|
||||
false -> item
|
||||
}
|
||||
block(newestItem)
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
class FlowPreference<T> constructor(
|
||||
private val preferences: SharedPreferences,
|
||||
private val key: String,
|
||||
val key: String,
|
||||
private val reader: SharedPreferences.(key: String) -> T,
|
||||
private val writer: SharedPreferences.Editor.(key: String, value: T) -> Unit
|
||||
) {
|
||||
|
||||
@@ -17,22 +17,20 @@ class GeneralSettings @Inject constructor(
|
||||
private val debugSettings: DebugSettings,
|
||||
) : Settings() {
|
||||
|
||||
override val preferences: SharedPreferences = context.getSharedPreferences("settings_general", Context.MODE_PRIVATE)
|
||||
|
||||
override val preferenceDataStore: PreferenceDataStore = object : PreferenceStoreMapper() {
|
||||
override fun getBoolean(key: String, defValue: Boolean): Boolean = when (key) {
|
||||
PK_BUGTRACKING_ENABLED -> debugSettings.isAutoReportEnabled.value
|
||||
else -> super.getBoolean(key, defValue)
|
||||
else -> debugSettings.preferenceDataStore.getBoolean(key, defValue)
|
||||
}
|
||||
|
||||
override fun putBoolean(key: String, value: Boolean) = when (key) {
|
||||
PK_BUGTRACKING_ENABLED -> debugSettings.isAutoReportEnabled.update { value }
|
||||
else -> super.putBoolean(key, value)
|
||||
else -> debugSettings.preferenceDataStore.putBoolean(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
override val preferences: SharedPreferences = context.getSharedPreferences("settings_general", Context.MODE_PRIVATE)
|
||||
|
||||
companion object {
|
||||
internal val TAG = logTag("Core", "Settings")
|
||||
private const val PK_BUGTRACKING_ENABLED = "core.bugtracking.enabled"
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,9 @@ import eu.darken.capod.common.lists.modular.ModularAdapter
|
||||
import eu.darken.capod.common.lists.modular.mods.DataBinderMod
|
||||
import eu.darken.capod.common.lists.modular.mods.TypedVHCreatorMod
|
||||
import eu.darken.capod.main.ui.overview.cards.PermissionCardVH
|
||||
import eu.darken.capod.main.ui.overview.cards.pods.BasicSingleApplePodsCardVH
|
||||
import eu.darken.capod.main.ui.overview.cards.pods.DualApplePodsCardVH
|
||||
import eu.darken.capod.main.ui.overview.cards.pods.SingleApplePodsCardVH
|
||||
import eu.darken.capod.main.ui.overview.cards.pods.UnknownPodDeviceCardVH
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -26,6 +28,8 @@ class OverviewAdapter @Inject constructor() :
|
||||
modules.add(DataBinderMod(data))
|
||||
modules.add(TypedVHCreatorMod({ data[it] is PermissionCardVH.Item }) { PermissionCardVH(it) })
|
||||
modules.add(TypedVHCreatorMod({ data[it] is DualApplePodsCardVH.Item }) { DualApplePodsCardVH(it) })
|
||||
modules.add(TypedVHCreatorMod({ data[it] is SingleApplePodsCardVH.Item }) { SingleApplePodsCardVH(it) })
|
||||
modules.add(TypedVHCreatorMod({ data[it] is BasicSingleApplePodsCardVH.Item }) { BasicSingleApplePodsCardVH(it) })
|
||||
modules.add(TypedVHCreatorMod({ data[it] is UnknownPodDeviceCardVH.Item }) { UnknownPodDeviceCardVH(it) })
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.lifecycle.SavedStateHandle
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import eu.darken.capod.common.coroutine.DispatcherProvider
|
||||
import eu.darken.capod.common.debug.autoreport.DebugSettings
|
||||
import eu.darken.capod.common.debug.recording.core.RecorderModule
|
||||
import eu.darken.capod.common.livedata.SingleLiveEvent
|
||||
import eu.darken.capod.common.navigation.navVia
|
||||
@@ -13,14 +14,20 @@ import eu.darken.capod.common.permissions.Permission
|
||||
import eu.darken.capod.common.permissions.isGrantedOrNotRequired
|
||||
import eu.darken.capod.common.uix.ViewModel3
|
||||
import eu.darken.capod.main.ui.overview.cards.PermissionCardVH
|
||||
import eu.darken.capod.main.ui.overview.cards.pods.BasicSingleApplePodsCardVH
|
||||
import eu.darken.capod.main.ui.overview.cards.pods.DualApplePodsCardVH
|
||||
import eu.darken.capod.main.ui.overview.cards.pods.SingleApplePodsCardVH
|
||||
import eu.darken.capod.main.ui.overview.cards.pods.UnknownPodDeviceCardVH
|
||||
import eu.darken.capod.monitor.core.PodMonitor
|
||||
import eu.darken.capod.monitor.core.worker.MonitorControl
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.isActive
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -32,12 +39,19 @@ class OverviewFragmentVM @Inject constructor(
|
||||
private val recorderModule: RecorderModule,
|
||||
private val monitorControl: MonitorControl,
|
||||
private val podMonitor: PodMonitor,
|
||||
private val debugSettings: DebugSettings,
|
||||
) : ViewModel3(dispatcherProvider = dispatcherProvider) {
|
||||
|
||||
private val enabledState: Flow<Boolean> = flow {
|
||||
emit(false)
|
||||
}
|
||||
|
||||
private val updateTicker = channelFlow<Unit> {
|
||||
while (isActive) {
|
||||
trySend(Unit)
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
private val permissionCheckTrigger = MutableStateFlow(UUID.randomUUID())
|
||||
private val requiredPermissions: Flow<List<Permission>> = permissionCheckTrigger
|
||||
.map {
|
||||
@@ -64,10 +78,12 @@ class OverviewFragmentVM @Inject constructor(
|
||||
}
|
||||
|
||||
val listItems: LiveData<List<OverviewAdapter.Item>> = combine(
|
||||
updateTicker,
|
||||
enabledState,
|
||||
requiredPermissions,
|
||||
pods
|
||||
) { state, permissions, pods ->
|
||||
pods,
|
||||
debugSettings.isDebugModeEnabled.flow
|
||||
) { tick, state, permissions, pods, isDebugMode ->
|
||||
val items = mutableListOf<OverviewAdapter.Item>()
|
||||
|
||||
permissions
|
||||
@@ -82,7 +98,9 @@ class OverviewFragmentVM @Inject constructor(
|
||||
pods
|
||||
.map {
|
||||
when (it) {
|
||||
is DualApplePods -> DualApplePodsCardVH.Item(it)
|
||||
is DualApplePods -> DualApplePodsCardVH.Item(it, showDebug = isDebugMode)
|
||||
is SingleApplePods -> SingleApplePodsCardVH.Item(it, showDebug = isDebugMode)
|
||||
is BasicSingleApplePods -> BasicSingleApplePodsCardVH.Item(it, showDebug = isDebugMode)
|
||||
else -> UnknownPodDeviceCardVH.Item(it)
|
||||
}
|
||||
}
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package eu.darken.capod.main.ui.overview.cards.pods
|
||||
|
||||
import android.icu.text.RelativeDateTimeFormatter
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isGone
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.lists.binding
|
||||
import eu.darken.capod.databinding.OverviewPodsAppleSingleBasicItemBinding
|
||||
import eu.darken.capod.pods.core.apple.BasicSingleApplePods
|
||||
import eu.darken.capod.pods.core.getBatteryLevelHeadset
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
class BasicSingleApplePodsCardVH(parent: ViewGroup) :
|
||||
PodDeviceVH<BasicSingleApplePodsCardVH.Item, OverviewPodsAppleSingleBasicItemBinding>(
|
||||
R.layout.overview_pods_apple_single_basic_item,
|
||||
parent
|
||||
) {
|
||||
|
||||
override val viewBinding = lazy { OverviewPodsAppleSingleBasicItemBinding.bind(itemView) }
|
||||
|
||||
private val lastSeenFormatter = RelativeDateTimeFormatter.getInstance()
|
||||
|
||||
override val onBindData = binding(payload = true) { item: Item ->
|
||||
val device = item.device
|
||||
|
||||
name.text = device.getLabel(context)
|
||||
deviceIcon.setImageResource(device.iconRes)
|
||||
|
||||
val duration = Duration.between(device.lastSeenAt, Instant.now())
|
||||
lastSeen.text = lastSeenFormatter.format(
|
||||
duration.seconds.toDouble(),
|
||||
RelativeDateTimeFormatter.Direction.LAST,
|
||||
RelativeDateTimeFormatter.RelativeUnit.SECONDS
|
||||
)
|
||||
|
||||
reception.text = device.getSignalQuality(context)
|
||||
|
||||
headphones.apply {
|
||||
val sb = StringBuilder(context.getString(R.string.pods_single_headphones_label))
|
||||
sb.append("\n").append(device.getBatteryLevelHeadset(context))
|
||||
text = sb
|
||||
}
|
||||
|
||||
status.apply {
|
||||
val sb = StringBuilder()
|
||||
if (item.showDebug) {
|
||||
sb.append("--- Debug ---")
|
||||
sb.append("\n").append(device.rawDataHex)
|
||||
}
|
||||
text = sb
|
||||
isGone = !item.showDebug
|
||||
}
|
||||
}
|
||||
|
||||
data class Item(
|
||||
override val device: BasicSingleApplePods,
|
||||
override val showDebug: Boolean,
|
||||
) : PodDeviceVH.Item
|
||||
}
|
||||
+21
-23
@@ -3,15 +3,15 @@ package eu.darken.capod.main.ui.overview.cards.pods
|
||||
import android.icu.text.RelativeDateTimeFormatter
|
||||
import android.view.ViewGroup
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.BuildConfigWrap
|
||||
import eu.darken.capod.common.lists.binding
|
||||
import eu.darken.capod.databinding.OverviewPodsAppleDualItemBinding
|
||||
import eu.darken.capod.pods.core.DualPods
|
||||
import eu.darken.capod.pods.core.DualPodDevice.Pod
|
||||
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.LidState
|
||||
import eu.darken.capod.pods.core.getBatteryCase
|
||||
import eu.darken.capod.pods.core.getBatteryLeftPod
|
||||
import eu.darken.capod.pods.core.getBatteryRightPod
|
||||
import eu.darken.capod.pods.core.getBatteryLevelCase
|
||||
import eu.darken.capod.pods.core.getBatteryLevelLeftPod
|
||||
import eu.darken.capod.pods.core.getBatteryLevelRightPod
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
@@ -21,19 +21,13 @@ class DualApplePodsCardVH(parent: ViewGroup) :
|
||||
parent
|
||||
) {
|
||||
|
||||
override val viewBinding = lazy {
|
||||
OverviewPodsAppleDualItemBinding.bind(itemView)
|
||||
}
|
||||
override val viewBinding = lazy { OverviewPodsAppleDualItemBinding.bind(itemView) }
|
||||
|
||||
private val lastSeenFormatter = RelativeDateTimeFormatter.getInstance()
|
||||
|
||||
override val onBindData: OverviewPodsAppleDualItemBinding.(
|
||||
item: Item,
|
||||
payloads: List<Any>
|
||||
) -> Unit = { item, _ ->
|
||||
override val onBindData = binding(payload = true) { item: Item ->
|
||||
val device = item.device
|
||||
|
||||
|
||||
name.apply {
|
||||
val sb = StringBuilder(device.getLabel(context))
|
||||
if (device.deviceColor != DeviceColor.UNKNOWN) {
|
||||
@@ -54,7 +48,7 @@ class DualApplePodsCardVH(parent: ViewGroup) :
|
||||
|
||||
podLeft.apply {
|
||||
val sb = StringBuilder(context.getString(R.string.pods_dual_left_label))
|
||||
sb.append("\n").append(device.getBatteryLeftPod(context))
|
||||
sb.append("\n").append(device.getBatteryLevelLeftPod(context))
|
||||
when {
|
||||
device.isLeftPodCharging -> sb.append("\n").append("Charging")
|
||||
device.isLeftPodInEar -> sb.append("\n").append("In ear")
|
||||
@@ -65,7 +59,7 @@ class DualApplePodsCardVH(parent: ViewGroup) :
|
||||
|
||||
podRight.apply {
|
||||
val sb = StringBuilder("Right pod")
|
||||
sb.append("\n").append(device.getBatteryRightPod(context))
|
||||
sb.append("\n").append(device.getBatteryLevelRightPod(context))
|
||||
when {
|
||||
device.isRightPodCharging -> sb.append("\n").append("Charging")
|
||||
device.isRightPodInEar -> sb.append("\n").append("In ear")
|
||||
@@ -75,13 +69,13 @@ class DualApplePodsCardVH(parent: ViewGroup) :
|
||||
}
|
||||
|
||||
when (device.microPhonePod) {
|
||||
DualPods.Pod.LEFT -> podLeft.append("\n(Microphone)")
|
||||
DualPods.Pod.RIGHT -> podRight.append("\n(Microphone)")
|
||||
Pod.LEFT -> podLeft.append("\n(Microphone)")
|
||||
Pod.RIGHT -> podRight.append("\n(Microphone)")
|
||||
}
|
||||
|
||||
podCase.apply {
|
||||
val sb = StringBuilder("Case")
|
||||
sb.append("\n").append(device.getBatteryCase(context))
|
||||
sb.append("\n").append(device.getBatteryLevelCase(context))
|
||||
if (device.isCaseCharging) sb.append("\n").append("Charging")
|
||||
when (device.caseLidState) {
|
||||
LidState.OPEN -> sb.append("\n").append(context.getString(R.string.pods_case_status_open_label))
|
||||
@@ -93,14 +87,18 @@ class DualApplePodsCardVH(parent: ViewGroup) :
|
||||
text = sb
|
||||
}
|
||||
|
||||
status.text = getString(R.string.pods_status_x_label, device.getConnectionStateLabel(context))
|
||||
if (BuildConfigWrap.DEBUG) {
|
||||
status.append("\n")
|
||||
status.append(device.identifier.toString())
|
||||
status.apply {
|
||||
val sb = StringBuilder(getString(R.string.pods_status_x_label, device.getConnectionStateLabel(context)))
|
||||
if (item.showDebug) {
|
||||
sb.append("\n\n").append("---Debug---")
|
||||
sb.append("\n").append(device.rawDataHex)
|
||||
}
|
||||
text = sb
|
||||
}
|
||||
}
|
||||
|
||||
data class Item(
|
||||
override val device: DualApplePods
|
||||
override val device: DualApplePods,
|
||||
override val showDebug: Boolean,
|
||||
) : PodDeviceVH.Item
|
||||
}
|
||||
@@ -18,6 +18,8 @@ abstract class PodDeviceVH<D : PodDeviceVH.Item, B : ViewBinding>(
|
||||
|
||||
val device: PodDevice
|
||||
|
||||
val showDebug: Boolean
|
||||
|
||||
override val stableId: Long get() = device.identifier.hashCode().toLong()
|
||||
|
||||
override val payloadProvider: ((DifferItem, DifferItem) -> DifferItem?)?
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package eu.darken.capod.main.ui.overview.cards.pods
|
||||
|
||||
import android.icu.text.RelativeDateTimeFormatter
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.view.isGone
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.lists.binding
|
||||
import eu.darken.capod.databinding.OverviewPodsAppleSingleItemBinding
|
||||
import eu.darken.capod.pods.core.apple.SingleApplePods
|
||||
import eu.darken.capod.pods.core.getBatteryLevelHeadset
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
class SingleApplePodsCardVH(parent: ViewGroup) :
|
||||
PodDeviceVH<SingleApplePodsCardVH.Item, OverviewPodsAppleSingleItemBinding>(
|
||||
R.layout.overview_pods_apple_single_item,
|
||||
parent
|
||||
) {
|
||||
|
||||
override val viewBinding = lazy { OverviewPodsAppleSingleItemBinding.bind(itemView) }
|
||||
|
||||
private val lastSeenFormatter = RelativeDateTimeFormatter.getInstance()
|
||||
|
||||
override val onBindData = binding(payload = true) { item: Item ->
|
||||
val device = item.device
|
||||
|
||||
name.text = device.getLabel(context)
|
||||
deviceIcon.setImageResource(device.iconRes)
|
||||
|
||||
val duration = Duration.between(device.lastSeenAt, Instant.now())
|
||||
lastSeen.text = lastSeenFormatter.format(
|
||||
duration.seconds.toDouble(),
|
||||
RelativeDateTimeFormatter.Direction.LAST,
|
||||
RelativeDateTimeFormatter.RelativeUnit.SECONDS
|
||||
)
|
||||
|
||||
reception.text = device.getSignalQuality(context)
|
||||
|
||||
headphones.apply {
|
||||
val sb = StringBuilder(context.getString(R.string.pods_single_headphones_label))
|
||||
sb.append("\n").append(device.getBatteryLevelHeadset(context))
|
||||
when {
|
||||
device.isHeadsetBeingCharged -> sb.append("\n").append("Charging")
|
||||
device.isHeadsetBeingWorn -> sb.append("\n").append("In ear")
|
||||
else -> {}
|
||||
}
|
||||
text = sb
|
||||
}
|
||||
|
||||
status.apply {
|
||||
val sb = StringBuilder()
|
||||
if (item.showDebug) {
|
||||
sb.append("--- Debug ---")
|
||||
sb.append("\n").append(device.rawDataHex)
|
||||
}
|
||||
text = sb
|
||||
isGone = !item.showDebug
|
||||
}
|
||||
}
|
||||
|
||||
data class Item(
|
||||
override val device: SingleApplePods,
|
||||
override val showDebug: Boolean,
|
||||
) : PodDeviceVH.Item
|
||||
}
|
||||
+6
-5
@@ -3,6 +3,7 @@ package eu.darken.capod.main.ui.overview.cards.pods
|
||||
import android.icu.text.RelativeDateTimeFormatter
|
||||
import android.view.ViewGroup
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.lists.binding
|
||||
import eu.darken.capod.databinding.OverviewPodsUnknownItemBinding
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import java.time.Duration
|
||||
@@ -20,10 +21,7 @@ class UnknownPodDeviceCardVH(parent: ViewGroup) :
|
||||
|
||||
private val lastSeenFormatter = RelativeDateTimeFormatter.getInstance()
|
||||
|
||||
override val onBindData: OverviewPodsUnknownItemBinding.(
|
||||
item: Item,
|
||||
payloads: List<Any>
|
||||
) -> Unit = { item, _ ->
|
||||
override val onBindData = binding(payload = true) { item ->
|
||||
val device = item.device
|
||||
name.text = device.getLabel(context)
|
||||
|
||||
@@ -33,9 +31,12 @@ class UnknownPodDeviceCardVH(parent: ViewGroup) :
|
||||
RelativeDateTimeFormatter.Direction.LAST,
|
||||
RelativeDateTimeFormatter.RelativeUnit.SECONDS
|
||||
)
|
||||
|
||||
rawdata.text = device.rawDataHex
|
||||
}
|
||||
|
||||
data class Item(
|
||||
override val device: PodDevice
|
||||
override val device: PodDevice,
|
||||
override val showDebug: Boolean = false
|
||||
) : PodDeviceVH.Item
|
||||
}
|
||||
@@ -31,9 +31,39 @@ class PodMonitor @Inject constructor(
|
||||
}
|
||||
.onStart { emptyList<PodDevice>() }
|
||||
.map { scanResults ->
|
||||
scanResults
|
||||
.sortedByDescending { it.rssi }
|
||||
.mapNotNull { podFactory.createPod(it) }
|
||||
val pods = scanResults
|
||||
.sortedByDescending { it.rssi }
|
||||
.mapNotNull { podFactory.createPod(it) }
|
||||
|
||||
// if (BuildConfigWrap.DEBUG && scanResults.isNotEmpty()) {
|
||||
// val fake1 = AirPodsMax(
|
||||
// identifier = UUID.fromString("63436171-5433-4506-8bf5-faf6c35ffa8a"),
|
||||
// scanResult = scanResults.first(),
|
||||
// proximityMessage = ProximityPairing.Decoder().decode(
|
||||
// message = ContinuityProtocol.Message(
|
||||
// type = 0x07.toUByte(),
|
||||
// length = 25,
|
||||
// data = arrayOf<Byte>(1, 10, 32, 98, 4, -128, 1, 15, 64, 13, 112, 80, 22, -14, 64, -125, 22, -65, 16, 22, 52, -101, 116, -124, -24).toByteArray().toUByteArray()
|
||||
// )
|
||||
// )!!
|
||||
// )
|
||||
// val fake2 = BeatsFlex(
|
||||
// identifier = UUID.fromString("e563098d-ea24-4136-a169-5a59344317c3"),
|
||||
// scanResult = scanResults.first(),
|
||||
// proximityMessage = ProximityPairing.Decoder().decode(
|
||||
// message = ContinuityProtocol.Message(
|
||||
// type = 0x07.toUByte(),
|
||||
// length = 25,
|
||||
// data = arrayOf<Byte>(1, 16, 32, 10, -12, -113, 0, 1, 0, -60, 113, -97, -100, -17, -94, -29, -70, 102, -2, 29, 69, -97, -55, 47, -96).toByteArray().toUByteArray()
|
||||
// )
|
||||
// )!!
|
||||
// )
|
||||
// pods.plus(fake1).plus(fake2)
|
||||
// } else {
|
||||
// pods
|
||||
// }
|
||||
|
||||
pods
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -21,9 +21,9 @@ import eu.darken.capod.common.notifications.PendingIntentCompat
|
||||
import eu.darken.capod.main.ui.MainActivity
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.getBatteryCase
|
||||
import eu.darken.capod.pods.core.getBatteryLeftPod
|
||||
import eu.darken.capod.pods.core.getBatteryRightPod
|
||||
import eu.darken.capod.pods.core.getBatteryLevelCase
|
||||
import eu.darken.capod.pods.core.getBatteryLevelLeftPod
|
||||
import eu.darken.capod.pods.core.getBatteryLevelRightPod
|
||||
import javax.inject.Inject
|
||||
|
||||
|
||||
@@ -70,11 +70,11 @@ class MonitorNotifications @Inject constructor(
|
||||
val infoText = when (device) {
|
||||
is DualApplePods -> {
|
||||
val sb = StringBuilder()
|
||||
sb.append("L: ${device.getBatteryLeftPod(context)}")
|
||||
sb.append("L: ${device.getBatteryLevelLeftPod(context)}")
|
||||
sb.append(" | ")
|
||||
sb.append("C: ${device.getBatteryCase(context)}")
|
||||
sb.append("C: ${device.getBatteryLevelCase(context)}")
|
||||
sb.append(" | ")
|
||||
sb.append("R: ${device.getBatteryRightPod(context)}")
|
||||
sb.append("R: ${device.getBatteryLevelRightPod(context)}")
|
||||
}
|
||||
else -> {
|
||||
"Unknown device"
|
||||
|
||||
+1
-3
@@ -1,8 +1,6 @@
|
||||
package eu.darken.capod.pods.core
|
||||
|
||||
interface DualPods : PodDevice {
|
||||
|
||||
val microPhonePod: Pod
|
||||
interface DualPodDevice : PodDevice {
|
||||
|
||||
enum class Pod {
|
||||
LEFT,
|
||||
@@ -4,14 +4,14 @@ import android.content.Context
|
||||
import eu.darken.capod.R
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun DualPods.getBatteryLeftPod(context: Context): String =
|
||||
fun DualPodDevice.getBatteryLevelLeftPod(context: Context): String =
|
||||
batteryLeftPodPercent?.let { "${(it * 100).roundToInt()}%" }
|
||||
?: context.getString(R.string.general_value_not_available_label)
|
||||
|
||||
fun DualPods.getBatteryRightPod(context: Context): String =
|
||||
fun DualPodDevice.getBatteryLevelRightPod(context: Context): String =
|
||||
batteryRightPodPercent?.let { "${(it * 100).roundToInt()}%" }
|
||||
?: context.getString(R.string.general_value_not_available_label)
|
||||
|
||||
fun DualPods.getBatteryCase(context: Context): String =
|
||||
fun DualPodDevice.getBatteryLevelCase(context: Context): String =
|
||||
batteryCasePercent?.let { "${(it * 100).roundToInt()}%" }
|
||||
?: context.getString(R.string.general_value_not_available_label)
|
||||
|
||||
@@ -12,13 +12,18 @@ interface PodDevice {
|
||||
|
||||
val identifier: UUID
|
||||
|
||||
val scanResult: ScanResult
|
||||
|
||||
val lastSeenAt: Instant
|
||||
|
||||
val scanResult: ScanResult
|
||||
|
||||
val rssi: Int
|
||||
get() = scanResult.rssi
|
||||
|
||||
val rawData: ByteArray
|
||||
|
||||
val rawDataHex: String
|
||||
get() = rawData.joinToString(separator = " ") { String.format("%02X", it) }
|
||||
|
||||
fun getSignalQuality(context: Context): String {
|
||||
val percentage = (100 - abs(rssi))
|
||||
return "~$percentage%"
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package eu.darken.capod.pods.core
|
||||
|
||||
interface SinglePodDevice : PodDevice {
|
||||
|
||||
val batteryHeadsetPercent: Float?
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package eu.darken.capod.pods.core
|
||||
|
||||
import android.content.Context
|
||||
import eu.darken.capod.R
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
fun SinglePodDevice.getBatteryLevelHeadset(context: Context): String =
|
||||
batteryHeadsetPercent?.let { "${(it * 100).roundToInt()}%" }
|
||||
?: context.getString(R.string.general_value_not_available_label)
|
||||
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple
|
||||
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.protocol.ContinuityProtocol
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
|
||||
interface ApplePods : PodDevice {
|
||||
@@ -10,6 +11,9 @@ interface ApplePods : PodDevice {
|
||||
|
||||
val proximityMessage: ProximityPairing.Message
|
||||
|
||||
override val rawData: ByteArray
|
||||
get() = scanResult.scanRecord!!.getManufacturerSpecificData(ContinuityProtocol.APPLE_COMPANY_IDENTIFIER)!!
|
||||
|
||||
// We start counting at the airpods prefix byte
|
||||
val rawPrefix: UByte
|
||||
get() = proximityMessage.data[0]
|
||||
|
||||
@@ -2,10 +2,11 @@ package eu.darken.capod.pods.core.apple
|
||||
|
||||
import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.pods.core.SinglePodDevice
|
||||
|
||||
interface BasicSingleApplePods : ApplePods {
|
||||
interface BasicSingleApplePods : ApplePods, SinglePodDevice {
|
||||
|
||||
val batteryHeadsetPercent: Float?
|
||||
override val batteryHeadsetPercent: Float?
|
||||
get() = when (val value = rawPodsBattery.lowerNibble.toInt()) {
|
||||
15 -> null
|
||||
else -> if (value > 10) {
|
||||
|
||||
@@ -7,12 +7,12 @@ import eu.darken.capod.common.debug.logging.log
|
||||
import eu.darken.capod.common.isBitSet
|
||||
import eu.darken.capod.common.lowerNibble
|
||||
import eu.darken.capod.common.upperNibble
|
||||
import eu.darken.capod.pods.core.DualPods
|
||||
import eu.darken.capod.pods.core.DualPods.Pod
|
||||
import eu.darken.capod.pods.core.DualPodDevice
|
||||
import eu.darken.capod.pods.core.DualPodDevice.Pod
|
||||
|
||||
interface DualApplePods : ApplePods, DualPods {
|
||||
interface DualApplePods : ApplePods, DualPodDevice {
|
||||
|
||||
override val microPhonePod: Pod
|
||||
val microPhonePod: Pod
|
||||
get() = when (rawStatus.isBitSet(5)) {
|
||||
true -> Pod.LEFT
|
||||
false -> Pod.RIGHT
|
||||
|
||||
@@ -2,6 +2,7 @@ package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import android.bluetooth.le.ScanResult
|
||||
import android.content.Context
|
||||
import eu.darken.capod.R
|
||||
import eu.darken.capod.common.debug.logging.logTag
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import eu.darken.capod.pods.core.apple.protocol.ProximityPairing
|
||||
@@ -19,5 +20,8 @@ data class AirPodsPro constructor(
|
||||
return "AirPods Pro"
|
||||
}
|
||||
|
||||
override val iconRes: Int
|
||||
get() = R.drawable.ic_device_airpods_gen2
|
||||
|
||||
override val tag: String = logTag("Pod", "Apple", "AirPods", "Pro")
|
||||
}
|
||||
@@ -33,31 +33,13 @@
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:gravity="center_vertical"
|
||||
app:layout_constraintBottom_toTopOf="@id/barrier_top"
|
||||
app:layout_constraintBottom_toTopOf="@+id/last_seen"
|
||||
app:layout_constraintEnd_toStartOf="@id/reception"
|
||||
app:layout_constraintStart_toEndOf="@id/device_icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
tools:text="Apple AirPods Pro" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/reception"
|
||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center"
|
||||
app:drawableRightCompat="@drawable/ic_baseline_signal_cellular_alt_24"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/name"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Yours (RSSI -61)" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/last_seen"
|
||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||
@@ -70,6 +52,36 @@
|
||||
app:layout_constraintTop_toBottomOf="@id/name"
|
||||
tools:text="3s ago" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/reception"
|
||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:gravity="center"
|
||||
app:layout_constraintBottom_toBottomOf="@id/name"
|
||||
app:layout_constraintEnd_toStartOf="@id/reception_icon"
|
||||
app:layout_constraintStart_toEndOf="@id/name"
|
||||
app:layout_constraintTop_toTopOf="@+id/name"
|
||||
tools:text="Yours (RSSI -61)" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/reception_icon"
|
||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/reception"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/reception"
|
||||
app:layout_constraintTop_toTopOf="@+id/reception"
|
||||
app:srcCompat="@drawable/ic_baseline_signal_cellular_alt_24"
|
||||
tools:text="Yours (RSSI -61)" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier_top"
|
||||
android:layout_width="wrap_content"
|
||||
@@ -137,12 +149,9 @@
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:gravity="center_vertical"
|
||||
app:drawableLeftCompat="@drawable/ic_baseline_apps_24"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/card"
|
||||
style="@style/MyCardView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="8dp"
|
||||
tools:context=".main.ui.MainActivity">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/card_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/device_icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginStart="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/name"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/name"
|
||||
app:srcCompat="@drawable/ic_device_generic_headphones" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/name"
|
||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:gravity="center_vertical"
|
||||
app:layout_constraintBottom_toTopOf="@+id/last_seen"
|
||||
app:layout_constraintEnd_toStartOf="@id/reception"
|
||||
app:layout_constraintStart_toEndOf="@id/device_icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Power Beats 3" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/last_seen"
|
||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/barrier_top"
|
||||
app:layout_constraintEnd_toEndOf="@id/name"
|
||||
app:layout_constraintStart_toStartOf="@id/name"
|
||||
app:layout_constraintTop_toBottomOf="@id/name"
|
||||
tools:text="3s ago" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/reception"
|
||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:gravity="center"
|
||||
app:layout_constraintBottom_toBottomOf="@id/name"
|
||||
app:layout_constraintEnd_toStartOf="@id/reception_icon"
|
||||
app:layout_constraintStart_toEndOf="@id/name"
|
||||
app:layout_constraintTop_toTopOf="@+id/name"
|
||||
tools:text="Yours (RSSI -61)" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/reception_icon"
|
||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/reception"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/reception"
|
||||
app:layout_constraintTop_toTopOf="@+id/reception"
|
||||
app:srcCompat="@drawable/ic_baseline_signal_cellular_alt_24"
|
||||
tools:text="Yours (RSSI -61)" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier_top"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="top"
|
||||
app:constraint_referenced_ids="headphones"
|
||||
tools:layout_editor_absoluteY="60dp" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/headphones"
|
||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
app:drawableTopCompat="@drawable/ic_device_generic_headphones"
|
||||
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_goneMarginBottom="16dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
tools:text="Headphones\n50%\nCharging" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier_bottom"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="headphones" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/status"
|
||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:gravity="center_vertical"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/barrier_bottom"
|
||||
tools:text="Music Active, Case Closed" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@@ -0,0 +1,132 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/card"
|
||||
style="@style/MyCardView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="8dp"
|
||||
tools:context=".main.ui.MainActivity">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/card_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/device_icon"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginStart="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/name"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/name"
|
||||
app:srcCompat="@drawable/ic_device_generic_headphones" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/name"
|
||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:gravity="center_vertical"
|
||||
app:layout_constraintBottom_toTopOf="@+id/last_seen"
|
||||
app:layout_constraintEnd_toStartOf="@id/reception"
|
||||
app:layout_constraintStart_toEndOf="@id/device_icon"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Apple AirPods Max" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/last_seen"
|
||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/barrier_top"
|
||||
app:layout_constraintEnd_toEndOf="@id/name"
|
||||
app:layout_constraintStart_toStartOf="@id/name"
|
||||
app:layout_constraintTop_toBottomOf="@id/name"
|
||||
tools:text="3s ago" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/reception"
|
||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:gravity="center"
|
||||
app:layout_constraintBottom_toBottomOf="@id/name"
|
||||
app:layout_constraintEnd_toStartOf="@id/reception_icon"
|
||||
app:layout_constraintStart_toEndOf="@id/name"
|
||||
app:layout_constraintTop_toTopOf="@+id/name"
|
||||
tools:text="Yours (RSSI -61)" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/reception_icon"
|
||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/reception"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/reception"
|
||||
app:layout_constraintTop_toTopOf="@+id/reception"
|
||||
app:srcCompat="@drawable/ic_baseline_signal_cellular_alt_24"
|
||||
tools:text="Yours (RSSI -61)" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier_top"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="top"
|
||||
app:constraint_referenced_ids="headphones"
|
||||
tools:layout_editor_absoluteY="60dp" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/headphones"
|
||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:gravity="center_horizontal"
|
||||
app:drawableTopCompat="@drawable/ic_device_generic_headphones"
|
||||
app:layout_constraintBottom_toTopOf="@id/barrier_bottom"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_goneMarginBottom="16dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/barrier_top"
|
||||
app:layout_constraintVertical_bias="0.0"
|
||||
tools:text="Headphones\n50%\nCharging" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/barrier_bottom"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:barrierDirection="bottom"
|
||||
app:constraint_referenced_ids="headphones" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/status"
|
||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:gravity="center_vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/barrier_bottom"
|
||||
tools:text="Music Active, Case Closed" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
@@ -18,23 +18,22 @@
|
||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:gravity="center_vertical"
|
||||
app:layout_constraintBottom_toTopOf="@+id/last_seen"
|
||||
app:layout_constraintEnd_toStartOf="@id/reception"
|
||||
app:layout_constraintHorizontal_bias="0.49"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Unknown Device" />
|
||||
tools:text="Apple AirPods Max" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/last_seen"
|
||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/rawdata_label"
|
||||
app:layout_constraintEnd_toEndOf="@id/name"
|
||||
app:layout_constraintStart_toStartOf="@id/name"
|
||||
@@ -43,17 +42,33 @@
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/reception"
|
||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:gravity="center"
|
||||
app:layout_constraintBottom_toBottomOf="@id/name"
|
||||
app:layout_constraintEnd_toStartOf="@id/reception_icon"
|
||||
app:layout_constraintStart_toEndOf="@id/name"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="RSSI -100 dbM" />
|
||||
app:layout_constraintTop_toTopOf="@+id/name"
|
||||
tools:text="Yours (RSSI -61)" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/reception_icon"
|
||||
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/reception"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/reception"
|
||||
app:layout_constraintTop_toTopOf="@+id/reception"
|
||||
app:srcCompat="@drawable/ic_baseline_signal_cellular_alt_24"
|
||||
tools:text="Yours (RSSI -61)" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/rawdata_label"
|
||||
@@ -72,7 +87,7 @@
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/rawdata"
|
||||
style="@style/TextAppearance.MaterialComponents.Body1"
|
||||
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
<string name="pods_connection_state_call_label">Call</string>
|
||||
<string name="pods_connection_state_ringing_label">Ringing</string>
|
||||
<string name="pods_connection_state_hanging_up_label">Hanging up</string>
|
||||
<string name="pods_status_x_label">Status %s</string>
|
||||
<string name="pods_status_x_label">Status: %s</string>
|
||||
|
||||
|
||||
<string name="settings_label">Settings</string>
|
||||
@@ -82,4 +82,7 @@
|
||||
<string name="settings_debug_autoreports_label">Automatic bug reports</string>
|
||||
<string name="settings_debug_autoreports_description">Automatically reports issues, e.g. details on an app crash so I can figure out how to fix it.</string>
|
||||
<string name="device_unknown_label">Unknown device</string>
|
||||
<string name="pods_single_headphones_label">Headphones</string>
|
||||
<string name="settings_debug_mode_label">Debug mode</string>
|
||||
<string name="settings_debug_mode_description">Show additional information to troubleshoot issues.</string>
|
||||
</resources>
|
||||
@@ -5,10 +5,16 @@
|
||||
|
||||
<CheckBoxPreference
|
||||
android:icon="@drawable/ic_spider_thread_onsurface"
|
||||
android:key="core.bugtracking.enabled"
|
||||
android:key="debug.bugreport.automatic.enabled"
|
||||
android:summary="@string/settings_debug_autoreports_description"
|
||||
android:title="@string/settings_debug_autoreports_label" />
|
||||
|
||||
<CheckBoxPreference
|
||||
android:icon="@drawable/ic_baseline_bug_report_24"
|
||||
android:key="debug.mode.enabled"
|
||||
android:summary="@string/settings_debug_mode_description"
|
||||
android:title="@string/settings_debug_mode_label" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
@@ -1,6 +1,6 @@
|
||||
package eu.darken.capod.pods.core.apple
|
||||
|
||||
import eu.darken.capod.pods.core.DualPods
|
||||
import eu.darken.capod.pods.core.DualPodDevice
|
||||
import eu.darken.capod.pods.core.PodDevice
|
||||
import eu.darken.capod.pods.core.apple.airpods.AirPodsGen1
|
||||
import eu.darken.capod.pods.core.apple.airpods.AirPodsPro
|
||||
@@ -16,12 +16,12 @@ class AirPodsFactoryTest : 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") {
|
||||
// 00101011
|
||||
// --^-----
|
||||
microPhonePod shouldBe DualPods.Pod.LEFT
|
||||
microPhonePod shouldBe DualPodDevice.Pod.LEFT
|
||||
}
|
||||
create<DualApplePods>("07 19 01 0E 20 >0B< AA B5 31 00 00 E0 0C A7 8A 60 4B D3 7D F4 60 4F 2C 73 E9 A7 F4") {
|
||||
// 00001011
|
||||
// --^-----
|
||||
microPhonePod shouldBe DualPods.Pod.RIGHT
|
||||
microPhonePod shouldBe DualPodDevice.Pod.RIGHT
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package eu.darken.capod.pods.core.apple.airpods
|
||||
|
||||
import eu.darken.capod.pods.core.DualPods
|
||||
import eu.darken.capod.pods.core.DualPodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import io.kotest.matchers.shouldBe
|
||||
@@ -22,7 +22,7 @@ class AirPodsProTest : BaseAirPodsTest() {
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x00.toUByte()
|
||||
|
||||
microPhonePod shouldBe DualPods.Pod.RIGHT
|
||||
microPhonePod shouldBe DualPodDevice.Pod.RIGHT
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
@@ -39,7 +39,7 @@ class AirPodsProTest : BaseAirPodsTest() {
|
||||
@Test
|
||||
fun `test AirPods from my downstairs neighbour`() = runBlockingTest {
|
||||
create<AirPodsPro>("07 19 01 0E 20 00 F3 8F 02 00 04 79 C6 3F F9 C3 15 D9 11 A1 3C B1 58 66 B9 8B 67") {
|
||||
microPhonePod shouldBe DualPods.Pod.RIGHT
|
||||
microPhonePod shouldBe DualPodDevice.Pod.RIGHT
|
||||
|
||||
batteryLeftPodPercent shouldBe null
|
||||
batteryRightPodPercent shouldBe 0.3f
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package eu.darken.capod.pods.core.apple.beats
|
||||
|
||||
import eu.darken.capod.pods.core.DualPods
|
||||
import eu.darken.capod.pods.core.DualPodDevice
|
||||
import eu.darken.capod.pods.core.apple.BaseAirPodsTest
|
||||
import eu.darken.capod.pods.core.apple.DualApplePods
|
||||
import io.kotest.matchers.shouldBe
|
||||
@@ -22,7 +22,7 @@ class PowerBeatsProTest : BaseAirPodsTest() {
|
||||
rawDeviceColor shouldBe 0x00.toUByte()
|
||||
rawSuffix shouldBe 0x00.toUByte()
|
||||
|
||||
microPhonePod shouldBe DualPods.Pod.RIGHT
|
||||
microPhonePod shouldBe DualPodDevice.Pod.RIGHT
|
||||
|
||||
batteryLeftPodPercent shouldBe 1.0f
|
||||
batteryRightPodPercent shouldBe 1.0f
|
||||
|
||||
Reference in New Issue
Block a user