chore(screenshots): Improve dashboard and add-profile previews
Use AAP-connected mocks for the dark dashboard to showcase full connectivity badges (BLE + IRK + encrypted + AAP) and the ANC mode selector. Populate the add-profile preview with a demo name, model, and paired device. Add user-facing labels to the dashboard mock devices. Refactor BluetoothDevice2 so name/address are primary constructor fields, letting previews construct one without a real Android BluetoothDevice. Bump the screenshot test JVM heap to 4g — the smoke batch (42 renders) was hitting the test executor's default ceiling.
@@ -130,6 +130,7 @@ android {
|
||||
//noinspection WrongGradleMethod
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform()
|
||||
maxHeapSize = "4g"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.darken.capod.common.bluetooth.BluetoothDevice2
|
||||
import eu.darken.capod.common.compose.PreviewWrapper
|
||||
import eu.darken.capod.common.compose.preview.MOCK_NOW
|
||||
import eu.darken.capod.common.compose.preview.MockPodDataProvider
|
||||
@@ -37,16 +38,25 @@ import eu.darken.capod.reaction.ui.popup.PopUpContent as PopUpCard
|
||||
internal const val DS = "spec:width=1080px,height=2400px,dpi=428"
|
||||
|
||||
@Composable
|
||||
internal fun DashboardContent() = PreviewWrapper {
|
||||
internal fun DashboardContent(showAap: Boolean = false) = PreviewWrapper {
|
||||
val devices = if (showAap) {
|
||||
listOf(
|
||||
MockPodDataProvider.dualPodMonitoredWithAap(),
|
||||
MockPodDataProvider.singlePodMonitoredWithAap(),
|
||||
MockPodDataProvider.unknownMonitored(),
|
||||
)
|
||||
} else {
|
||||
listOf(
|
||||
MockPodDataProvider.dualPodMonitoredMixed(),
|
||||
MockPodDataProvider.singlePodMonitored(),
|
||||
MockPodDataProvider.unknownMonitored(),
|
||||
)
|
||||
}
|
||||
OverviewScreen(
|
||||
state = OverviewViewModel.State(
|
||||
now = MOCK_NOW,
|
||||
permissions = emptySet(),
|
||||
devices = listOf(
|
||||
MockPodDataProvider.dualPodMonitoredMixed(),
|
||||
MockPodDataProvider.singlePodMonitored(),
|
||||
MockPodDataProvider.unknownMonitored(),
|
||||
),
|
||||
devices = devices,
|
||||
isDebugMode = false,
|
||||
isBluetoothEnabled = true,
|
||||
profiles = listOf(
|
||||
@@ -83,19 +93,38 @@ internal fun DeviceProfilesContent() = PreviewWrapper {
|
||||
|
||||
@Composable
|
||||
internal fun AddProfileContent() = PreviewWrapper {
|
||||
val pairedAirPods = BluetoothDevice2(
|
||||
address = "AA:BB:CC:DD:EE:FF",
|
||||
name = "AirPods Pro",
|
||||
seenFirstAt = MOCK_NOW,
|
||||
)
|
||||
val bondedItems = listOf(
|
||||
DeviceProfileCreationViewModel.BondedDeviceItem(
|
||||
device = pairedAirPods,
|
||||
claimedByProfile = null,
|
||||
),
|
||||
DeviceProfileCreationViewModel.BondedDeviceItem(
|
||||
device = BluetoothDevice2(
|
||||
address = "11:22:33:44:55:66",
|
||||
name = "Living Room TV",
|
||||
seenFirstAt = MOCK_NOW,
|
||||
),
|
||||
claimedByProfile = null,
|
||||
),
|
||||
)
|
||||
DeviceProfileCreationScreen(
|
||||
state = DeviceProfileCreationViewModel.State(
|
||||
isEditMode = false,
|
||||
name = "",
|
||||
name = "My AirPods Pro",
|
||||
nameError = null,
|
||||
selectedModel = null,
|
||||
selectedModel = PodModel.AIRPODS_PRO2,
|
||||
availableModels = PodModel.entries.filter { it != PodModel.UNKNOWN },
|
||||
identityKey = null,
|
||||
encryptionKey = null,
|
||||
selectedDevice = null,
|
||||
bondedDeviceItems = emptyList(),
|
||||
selectedDevice = pairedAirPods,
|
||||
bondedDeviceItems = bondedItems,
|
||||
minimumSignalQuality = 0.15f,
|
||||
canSave = false,
|
||||
canSave = true,
|
||||
),
|
||||
onBack = {},
|
||||
onSave = {},
|
||||
@@ -212,7 +241,7 @@ private fun PreviewDashboardLight() = DashboardContent()
|
||||
|
||||
@Preview(name = "2 - Dashboard Dark", locale = "en", device = DS, uiMode = Configuration.UI_MODE_NIGHT_YES, showSystemUi = true)
|
||||
@Composable
|
||||
private fun PreviewDashboardDark() = DashboardContent()
|
||||
private fun PreviewDashboardDark() = DashboardContent(showAap = true)
|
||||
|
||||
@Preview(name = "3 - Case Pop-up", device = DS, showSystemUi = true)
|
||||
@Composable
|
||||
|
||||
@@ -4,12 +4,8 @@ import android.bluetooth.BluetoothDevice
|
||||
import java.time.Instant
|
||||
|
||||
data class BluetoothDevice2(
|
||||
internal val internal: BluetoothDevice,
|
||||
val address: BluetoothAddress,
|
||||
val name: String?,
|
||||
val seenFirstAt: Instant,
|
||||
) {
|
||||
val address: BluetoothAddress
|
||||
get() = internal.address
|
||||
|
||||
val name: String?
|
||||
get() = internal.name
|
||||
}
|
||||
internal val internal: BluetoothDevice? = null,
|
||||
)
|
||||
|
||||
@@ -255,6 +255,8 @@ class BluetoothManager2 @Inject constructor(
|
||||
}
|
||||
.map { device ->
|
||||
BluetoothDevice2(
|
||||
address = device.address,
|
||||
name = device.name,
|
||||
internal = device,
|
||||
seenFirstAt = seenDevicesLock.withLock {
|
||||
seenDevicesCache[device.address] ?: run {
|
||||
@@ -304,6 +306,8 @@ class BluetoothManager2 @Inject constructor(
|
||||
val wrappedDevices = rawDevices.map { device ->
|
||||
|
||||
BluetoothDevice2(
|
||||
address = device.address,
|
||||
name = device.name,
|
||||
internal = device,
|
||||
seenFirstAt = seenDevicesLock.withLock {
|
||||
seenDevicesCache[device.address] ?: run {
|
||||
@@ -408,9 +412,10 @@ class BluetoothManager2 @Inject constructor(
|
||||
@android.annotation.SuppressLint("MissingPermission")
|
||||
fun setDeviceAlias(device: BluetoothDevice2, alias: String): Boolean {
|
||||
return try {
|
||||
val target = device.internal ?: return false
|
||||
val method = BluetoothDevice::class.java.getDeclaredMethod("setAlias", String::class.java)
|
||||
.apply { isAccessible = true }
|
||||
val result = method.invoke(device.internal, alias) as? Boolean ?: false
|
||||
val result = method.invoke(target, alias) as? Boolean ?: false
|
||||
log(TAG) { "setDeviceAlias(${device.address}, $alias) -> $result" }
|
||||
result
|
||||
} catch (e: Exception) {
|
||||
@@ -424,11 +429,12 @@ class BluetoothManager2 @Inject constructor(
|
||||
try {
|
||||
log(TAG) { "Nudging Android connection to $device" }
|
||||
|
||||
val target = device.internal ?: return@map false
|
||||
val connectMethod = BluetoothHeadset::class.java.getDeclaredMethod(
|
||||
"connect", BluetoothDevice::class.java
|
||||
).apply { isAccessible = true }
|
||||
|
||||
val accepted = connectMethod.invoke(bluetoothProfile.proxy, device.internal) as? Boolean ?: false
|
||||
val accepted = connectMethod.invoke(bluetoothProfile.proxy, target) as? Boolean ?: false
|
||||
log(TAG) { "Nudged connection to $device — accepted=$accepted" }
|
||||
accepted
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -180,18 +180,21 @@ object MockPodDataProvider {
|
||||
|
||||
fun dualPodMonitoredMixed(): PodDevice = PodDevice(
|
||||
profileId = "preview-dual-mixed",
|
||||
label = "My AirPods Pro",
|
||||
ble = airPodsProMixed(),
|
||||
aap = null,
|
||||
)
|
||||
|
||||
fun dualPodMonitoredWithKeys(): PodDevice = PodDevice(
|
||||
profileId = "preview-dual-keys",
|
||||
label = "My AirPods Pro",
|
||||
ble = airPodsProWithKeys(),
|
||||
aap = null,
|
||||
)
|
||||
|
||||
fun dualPodMonitoredWithAap(): PodDevice = PodDevice(
|
||||
profileId = "preview-dual-aap",
|
||||
label = "My AirPods Pro",
|
||||
ble = airPodsProWithKeys(),
|
||||
aap = AapPodState(
|
||||
connectionState = AapPodState.ConnectionState.READY,
|
||||
@@ -227,12 +230,14 @@ object MockPodDataProvider {
|
||||
|
||||
fun singlePodMonitored(): PodDevice = PodDevice(
|
||||
profileId = "preview-single",
|
||||
label = "AirPods Max",
|
||||
ble = airPodsMax(),
|
||||
aap = null,
|
||||
)
|
||||
|
||||
fun singlePodMonitoredWithAap(): PodDevice = PodDevice(
|
||||
profileId = "preview-single-aap",
|
||||
label = "AirPods Max",
|
||||
ble = airPodsMaxCharging(),
|
||||
aap = AapPodState(
|
||||
connectionState = AapPodState.ConnectionState.READY,
|
||||
|
||||
@@ -76,7 +76,7 @@ class AapAutoConnect @Inject constructor(
|
||||
|
||||
log(TAG) { "AAP connecting to $address (${profile.label})" }
|
||||
try {
|
||||
aapManager.connect(address, bonded.internal, profile.model)
|
||||
aapManager.connect(address, bonded.internal!!, profile.model)
|
||||
log(TAG) { "AAP connected to $address" }
|
||||
} catch (e: Exception) {
|
||||
log(TAG, WARN) { "AAP initial connect failed for $address: ${e.message}" }
|
||||
@@ -99,7 +99,7 @@ class AapAutoConnect @Inject constructor(
|
||||
|
||||
try {
|
||||
log(TAG) { "AAP initial retry ${attempt + 1} for $address after ${delayMs}ms" }
|
||||
aapManager.connect(address, bonded.internal, profile.model)
|
||||
aapManager.connect(address, bonded.internal!!, profile.model)
|
||||
log(TAG) { "AAP connected to $address on retry ${attempt + 1}" }
|
||||
break
|
||||
} catch (retryException: Exception) {
|
||||
@@ -159,7 +159,7 @@ class AapAutoConnect @Inject constructor(
|
||||
|
||||
try {
|
||||
log(TAG) { "AAP reconnect attempt ${attempt + 1} for $address in ${delayMs}ms" }
|
||||
aapManager.connect(address, bonded.internal, profile.model)
|
||||
aapManager.connect(address, bonded.internal!!, profile.model)
|
||||
log(TAG) { "AAP reconnected to $address" }
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
@@ -211,7 +211,7 @@ class AapAutoConnect @Inject constructor(
|
||||
val bonded = bluetoothManager.bondedDevices().first()
|
||||
.firstOrNull { it.address == address }
|
||||
if (bonded != null) {
|
||||
aapManager.connect(address, bonded.internal, detectedModel)
|
||||
aapManager.connect(address, bonded.internal!!, detectedModel)
|
||||
log(TAG) { "AAP reconnected $address with corrected model $detectedModel" }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -12,7 +12,7 @@ fun DashboardLight() = DashboardContent()
|
||||
@PreviewTest
|
||||
@PlayStoreLocalesDark
|
||||
@Composable
|
||||
fun DashboardDark() = DashboardContent()
|
||||
fun DashboardDark() = DashboardContent(showAap = true)
|
||||
|
||||
@PreviewTest
|
||||
@PlayStoreLocales
|
||||
|
||||
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 144 KiB After Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 151 KiB |
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 129 KiB After Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 178 KiB After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 134 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 150 KiB |