Skip to content

Commit c4fe05e

Browse files
committed
Allow device selection
1 parent a50f9a5 commit c4fe05e

File tree

4 files changed

+107
-16
lines changed

4 files changed

+107
-16
lines changed

workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier
1515
import androidx.compose.ui.geometry.Offset
1616
import com.squareup.workflow1.traceviewer.model.NodeUpdate
1717
import com.squareup.workflow1.traceviewer.ui.ColorLegend
18+
import com.squareup.workflow1.traceviewer.ui.DisplayDevices
1819
import com.squareup.workflow1.traceviewer.ui.FrameSelectTab
1920
import com.squareup.workflow1.traceviewer.ui.RightInfoPanel
2021
import com.squareup.workflow1.traceviewer.ui.TraceModeToggleSwitch
@@ -63,8 +64,9 @@ internal fun App(
6364
sandboxState = sandboxState,
6465
) {
6566
// if there is not a file selected and trace mode is live, then don't render anything.
66-
val readyForFileTrace = traceMode is TraceMode.File && selectedTraceFile != null
67-
val readyForLiveTrace = traceMode is TraceMode.Live
67+
val readyForFileTrace = validateFileMode(traceMode)
68+
val readyForLiveTrace = validateLiveMode(traceMode)
69+
6870
if (readyForFileTrace || readyForLiveTrace) {
6971
active = true
7072
RenderTrace(
@@ -92,13 +94,12 @@ internal fun App(
9294
frameIndex = 0
9395
TraceMode.File(null)
9496
} else {
95-
// TODO: TraceRecorder needs to be able to take in multiple clients if this is the case
9697
/*
9798
We set the frame to -1 here since we always increment it during Live mode as the list of
9899
frames get populated, so we avoid off by one when indexing into the frames.
99100
*/
100101
frameIndex = -1
101-
TraceMode.Live
102+
TraceMode.Live()
102103
}
103104
},
104105
traceMode = traceMode,
@@ -118,6 +119,16 @@ internal fun App(
118119
}
119120

120121
if (traceMode is TraceMode.Live) {
122+
if ((traceMode as TraceMode.Live).device == null) {
123+
DisplayDevices(
124+
onDeviceSelected = { selectedDevice ->
125+
traceMode = TraceMode.Live(selectedDevice)
126+
},
127+
devices = listDevices(),
128+
modifier = Modifier.align(Alignment.Center)
129+
)
130+
}
131+
121132
FileDump(
122133
trace = rawRenderPass,
123134
modifier = Modifier.align(Alignment.BottomStart)
@@ -149,5 +160,23 @@ internal class SandboxState {
149160

150161
internal sealed interface TraceMode {
151162
data class File(val file: PlatformFile?) : TraceMode
152-
data object Live : TraceMode
163+
data class Live(val device: String? = null) : TraceMode
164+
}
165+
166+
/**
167+
* Allows users to select from multiple devices that are currently running.
168+
*/
169+
internal fun listDevices(): List<String> {
170+
val process = ProcessBuilder("adb", "devices", "-l").start()
171+
process.waitFor()
172+
// We drop the header "List of devices attached"
173+
return process.inputStream.bufferedReader().readLines().drop(1).dropLast(1)
174+
}
175+
176+
internal fun validateLiveMode(traceMode: TraceMode): Boolean {
177+
return traceMode is TraceMode.Live && traceMode.device != null
178+
}
179+
180+
internal fun validateFileMode(traceMode: TraceMode): Boolean {
181+
return traceMode is TraceMode.File && traceMode.file != null
153182
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.squareup.workflow1.traceviewer.ui
2+
3+
import androidx.compose.foundation.BorderStroke
4+
import androidx.compose.foundation.layout.Box
5+
import androidx.compose.foundation.layout.Column
6+
import androidx.compose.foundation.layout.fillMaxWidth
7+
import androidx.compose.foundation.layout.padding
8+
import androidx.compose.foundation.shape.RoundedCornerShape
9+
import androidx.compose.material.Card
10+
import androidx.compose.material.ExperimentalMaterialApi
11+
import androidx.compose.material.Text
12+
import androidx.compose.runtime.Composable
13+
import androidx.compose.ui.Alignment
14+
import androidx.compose.ui.Modifier
15+
import androidx.compose.ui.graphics.Color
16+
import androidx.compose.ui.unit.dp
17+
18+
@OptIn(ExperimentalMaterialApi::class)
19+
@Composable
20+
internal fun DisplayDevices(
21+
onDeviceSelected: (String) -> Unit,
22+
devices: List<String>,
23+
modifier: Modifier = Modifier,
24+
) {
25+
Box(
26+
modifier = modifier
27+
.fillMaxWidth(),
28+
contentAlignment = Alignment.Center
29+
) {
30+
if (devices.isEmpty()) {
31+
Text(
32+
text = "No device available",
33+
modifier = Modifier.align(Alignment.Center)
34+
)
35+
return@Box
36+
}
37+
38+
val emulatorRegex = Regex("""\bemulator-\d+\b""")
39+
Column {
40+
devices.forEach { device ->
41+
Card(
42+
onClick = {
43+
emulatorRegex.find(device)?.value?.let { emulator ->
44+
onDeviceSelected(emulator)
45+
}
46+
},
47+
shape = RoundedCornerShape(16.dp),
48+
border = BorderStroke(1.dp, Color.Gray),
49+
modifier = Modifier.padding(4.dp),
50+
elevation = 2.dp
51+
) {
52+
Text(
53+
text = device,
54+
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
55+
)
56+
}
57+
}
58+
}
59+
}
60+
}

workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/SocketClient.kt

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,12 @@ import kotlinx.coroutines.withContext
1111
import okio.IOException
1212
import java.net.Socket
1313

14-
internal suspend fun streamRenderPassesFromDevice(parseOnNewRenderPass: (String) -> Unit) {
14+
internal suspend fun streamRenderPassesFromDevice(device: String, parseOnNewRenderPass: (String) -> Unit) {
1515
val renderPassChannel: Channel<String> = Channel(Channel.BUFFERED)
1616
coroutineScope {
1717
launch {
1818
try {
19-
pollSocket(onNewRenderPass = renderPassChannel::send)
19+
pollSocket(device = device, onNewRenderPass = renderPassChannel::send)
2020
} finally {
2121
renderPassChannel.close()
2222
}
@@ -39,10 +39,10 @@ internal suspend fun streamRenderPassesFromDevice(parseOnNewRenderPass: (String)
3939
* @param onNewRenderPass is called from an arbitrary thread, so it is important to ensure that the
4040
* caller is thread safe
4141
*/
42-
private suspend fun pollSocket(onNewRenderPass: suspend (String) -> Unit) {
42+
private suspend fun pollSocket(device: String, onNewRenderPass: suspend (String) -> Unit) {
4343
withContext(Dispatchers.IO) {
4444
try {
45-
runForwardingPortThroughAdb { port ->
45+
runForwardingPortThroughAdb(device) { port ->
4646
Socket("localhost", port).useWithCancellation { socket ->
4747
val reader = socket.getInputStream().bufferedReader()
4848
do {
@@ -60,7 +60,6 @@ private suspend fun pollSocket(onNewRenderPass: suspend (String) -> Unit) {
6060
}
6161
}
6262

63-
6463
/**
6564
* Force [pollSocket] to exit with exception if the coroutine is cancelled. See comment below.
6665
*/
@@ -89,9 +88,9 @@ private suspend fun Socket.useWithCancellation(block: suspend (Socket) -> Unit)
8988
* If block throws or returns on finish, the port forwarding is removed via adb (best effort).
9089
*/
9190
@Suppress("BlockingMethodInNonBlockingContext")
92-
private suspend inline fun runForwardingPortThroughAdb(block: (port: Int) -> Unit) {
91+
private suspend inline fun runForwardingPortThroughAdb(device: String, block: (port: Int) -> Unit) {
9392
val process = ProcessBuilder(
94-
"adb", "forward", "tcp:0", "localabstract:workflow-trace"
93+
"adb", "-s", device, "forward", "tcp:0", "localabstract:workflow-trace"
9594
).start()
9695

9796
// The adb forward command will output the port number it picks to connect.

workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/util/TraceParser.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ internal fun RenderTrace(
3434
) {
3535
var isLoading by remember(traceSource) { mutableStateOf(true) }
3636
var error by remember(traceSource) { mutableStateOf<String?>(null) }
37-
val frames = remember (traceSource){ mutableStateListOf<Node>() }
38-
val fullTree = remember (traceSource){ mutableStateListOf<Node>() }
39-
val affectedNodes = remember (traceSource){ mutableStateListOf<Set<Node>>() }
37+
val frames = remember(traceSource) { mutableStateListOf<Node>() }
38+
val fullTree = remember(traceSource) { mutableStateListOf<Node>() }
39+
val affectedNodes = remember(traceSource) { mutableStateListOf<Set<Node>>() }
4040

4141
// Updates current state with the new data from trace source.
4242
fun addToStates(frame: List<Node>, tree: List<Node>, affected: List<Set<Node>>) {
@@ -83,8 +83,11 @@ internal fun RenderTrace(
8383
}
8484

8585
is TraceMode.Live -> {
86+
checkNotNull(traceSource.device) {
87+
"TraceMode.Live requires a selected device"
88+
}
8689
val adapter: JsonAdapter<List<Node>> = createMoshiAdapter<Node>()
87-
streamRenderPassesFromDevice { renderPass ->
90+
streamRenderPassesFromDevice(traceSource.device) { renderPass ->
8891
val currentTree = fullTree.lastOrNull()
8992
val parseResult = parseLiveTrace(renderPass, adapter, currentTree)
9093
handleParseResult(parseResult, renderPass, onNewFrame)

0 commit comments

Comments
 (0)