Skip to content

Commit 1c31c18

Browse files
committed
Refactor: Improve Device Handling and UI Elements
This commit introduces several enhancements to device interaction and user interface elements: **Device Handling:** - **Device Selection:** Users can now see a list of connected devices and select a specific device for ADB commands. The selected device is stored in `AdbInput.selectedDevice`. - **`getDeviceList()`:** A new function in `AdbInput.kt` to retrieve a list of connected device serial numbers. - **`DeviceListView` Updates:** - Displays the list of connected devices. - Allows users to select a device by clicking on its serial number. - Shows "(Selected)" next to the currently active device. - Includes a refresh button to update the device list. - Shows a toast message if no devices are found or when a device serial is long-pressed. - **Command Execution:** ADB commands are now executed specifically on the selected device using `adb -s <selectedDevice> ...`. **UI Enhancements:** - **Toast Notifications:** Introduced a `ToastManager` to display short or long duration toast messages. This is used for notifying users about device connection status. - **Icon Resources:** Added `ic_expand_less.svg`, `ic_expand_more.svg`, and `ic_power.svg` for UI elements. - **`NeoIcon` Component:** The `size` parameter is now available to customize icon dimensions. - **`AndroidNavigationView`:** Increased the size of navigation icons (Back, Home, Recent App) to `48.dp`. - **`ControlsView`:** - Replaced the "Power Button" text button with a `NeoIcon` using `ic_power.svg`. - Added dropdown menus for selecting the target device (e.g., "Pixel 6", "Pixel 3") and the unlock method (e.g., "Password", "Pin") for the unlock functionality. - The unlock functionality now considers the selected device and unlock method. - **`MainView`:** - Added an overlay that appears if no device is connected, prompting the user to connect a device. Clicking this overlay shows a toast message. - **Input Handling:** Removed an unnecessary `AdbInput.sendEnter()` call after `AdbInput.sendText()` in `ADBTerminalInputs.kt` for "write" commands. **Code Structure:** - **`ToastDurationType.kt`:** New enum to define toast duration types (SHORT, LONG). - **`setComposeWindowProvider`:** Added a function to provide the `ComposeWindow` instance to `ToastManager`. These changes aim to provide a more user-friendly experience by clearly indicating connected devices, allowing selection, and offering more specific control options.
1 parent 01abca4 commit 1c31c18

File tree

13 files changed

+312
-36
lines changed

13 files changed

+312
-36
lines changed

composeApp/src/desktopMain/kotlin/id/neotica/modernadb/adb/ADBTerminalInputs.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ fun idiomaticAdbInputs(input: String, callback: ((String) -> Unit)? = null) {
2828
}
2929
input == "power" -> AdbInput.powerButton()
3030
input == "devices" -> {
31-
val output = AdbInput.deviceList()//.inputStream.bufferedReader().readText()
31+
val output = AdbInput.getDevices()//.inputStream.bufferedReader().readText()
3232
callback?.invoke(output)
3333
}
3434
input.startsWith("midtap") -> {
@@ -114,8 +114,6 @@ fun idiomaticAdbInputs(input: String, callback: ((String) -> Unit)? = null) {
114114

115115
AdbInput.sendText(writeInput)
116116
Thread.sleep(200)
117-
AdbInput.sendEnter()
118-
Thread.sleep(300)
119117
callback?.invoke("")
120118
}
121119
else -> {

composeApp/src/desktopMain/kotlin/id/neotica/modernadb/adb/android/AdbInput.kt

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,15 @@ import java.io.File
66
import java.util.concurrent.TimeUnit
77

88
object AdbInput {
9+
var selectedDevice: String? = null
910

1011
private val adbExecutablePath: String by lazy {
1112
val sdkDirs = listOfNotNull(
1213
System.getenv("ANDROID_SDK_ROOT"),
1314
System.getenv("ANDROID_HOME"),
1415
"${System.getProperty("user.home")}/Library/Android/sdk",
1516
"/usr/local/share/android-sdk",
16-
"/opt/android-sdk",
17-
// "generated/moko-resources/desktopMain/res/files/adbmac",
18-
// MR.files
17+
"/opt/android-sdk"
1918
)
2019

2120
val found = sdkDirs
@@ -25,18 +24,18 @@ object AdbInput {
2524
found?.absolutePath ?: error("ADB not found in common locations. Please install Android SDK.")
2625
}
2726

28-
/**
29-
* Executes a command using the system shell, which correctly handles pipes and other features.
30-
* It returns the Process object for further handling.
31-
*/
3227
private fun exec(cmd: String, waitAfter: Long = 100): Process {
3328
val fullCommand = cmd.replaceFirst("adb", adbExecutablePath)
3429
println("DEBUG: Executing command: '$fullCommand'")
30+
val test = "adb devices"
31+
val formattedSelectedAdb = fullCommand.replaceFirst("adb", "adb -s $selectedDevice")
32+
33+
println("modernadb: ${test.replaceFirst("adb", "adb -s $selectedDevice")}")
3534

3635
val processBuilder = if (System.getProperty("os.name").startsWith("Windows")) {
37-
ProcessBuilder("cmd.exe", "/c", fullCommand)
36+
ProcessBuilder("cmd.exe", "/c", formattedSelectedAdb)
3837
} else {
39-
ProcessBuilder("sh", "-c", fullCommand)
38+
ProcessBuilder("sh", "-c", formattedSelectedAdb)
4039
}
4140

4241
val environment = processBuilder.environment()
@@ -60,7 +59,7 @@ object AdbInput {
6059
return process
6160
}
6261

63-
fun deviceList(): String {
62+
fun getDevices(): String {
6463
return try {
6564
val process = exec("adb devices")
6665
val reader = process.inputStream.bufferedReader().readText()
@@ -75,7 +74,31 @@ object AdbInput {
7574
}
7675
}
7776

77+
fun getDeviceList(): List<String> {
78+
return try {
79+
val process = exec("adb devices")
80+
val reader = process.inputStream.bufferedReader().readText()
81+
val result = reader.lines().drop(1).mapNotNull {
82+
val serial = it.split('\t').firstOrNull()
83+
serial?.takeIf { serial.isNotBlank() }
84+
}
85+
86+
println("result: $result")
87+
result
88+
} catch (e: Exception) {
89+
emptyList()
90+
}
91+
}
92+
7893
fun powerButton() = exec("adb shell input keyevent 26")
94+
fun longPressPowerButton() = exec("adb shell input keyevent --longpress 26")
95+
96+
//power
97+
fun shutdown() = exec("adb reboot -p")
98+
fun reboot() = exec("adb reboot")
99+
fun rebootRecovery() = exec("adb reboot recovery")
100+
fun rebootBootloader() = exec("adb reboot bootloader")
101+
79102

80103
fun isAwake(): Boolean {
81104
return try {

composeApp/src/desktopMain/kotlin/id/neotica/modernadb/presentation/AndroidNavigationView.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ import kotlinx.coroutines.launch
3232

3333
@OptIn(InternalComposeUiApi::class, ExperimentalMaterial3Api::class)
3434
@Composable
35-
fun AndroidNavigationView() {
35+
fun AndroidNavigationView(
36+
) {
3637
var expanded by remember { mutableStateOf(false) }
3738

3839
val scope = rememberCoroutineScope()
@@ -51,20 +52,23 @@ fun AndroidNavigationView() {
5152
NeoIcon(
5253
desc = "Back",
5354
image = MR.images.nav_back,
55+
size = 48.dp,
5456
onClick = {
5557
scope.launch { idiomaticAdbInputs("back") }
5658
}
5759
)
5860
NeoIcon(
5961
desc = "Home",
6062
image = MR.images.nav_home,
63+
size = 48.dp,
6164
onClick = {
6265
scope.launch { idiomaticAdbInputs("home") }
6366
}
6467
)
6568
NeoIcon(
6669
desc = "Recent App",
6770
image = MR.images.nav_recent,
71+
size = 48.dp,
6872
onClick = {
6973
scope.launch { idiomaticAdbInputs("switch") }
7074
}

composeApp/src/desktopMain/kotlin/id/neotica/modernadb/presentation/CommandView.kt

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,15 @@ fun CommandView(modifier: Modifier = Modifier) {
4545
){
4646
Column(
4747
modifier = Modifier
48-
// .fillMaxWidth()
4948
.padding(16.dp),
5049
horizontalAlignment = Alignment.CenterHorizontally
5150
) {
5251
Row(
5352
verticalAlignment = Alignment.CenterVertically,
5453
horizontalArrangement = Arrangement.Center
5554
) {
56-
// 2. The Switch logic is now based on the sealed class state.
5755
Switch(
58-
// The switch is "on" (checked) if the mode is Write.
5956
checked = currentMode == InputMode.Write,
60-
// When the switch is toggled, update the state to the corresponding mode.
6157
onCheckedChange = { isChecked ->
6258
currentMode = if (isChecked) InputMode.Write else InputMode.Command
6359
}

composeApp/src/desktopMain/kotlin/id/neotica/modernadb/presentation/ControlsView.kt

Lines changed: 126 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
package id.neotica.modernadb.presentation
22

3+
import androidx.compose.foundation.clickable
4+
import androidx.compose.foundation.layout.Arrangement
5+
import androidx.compose.foundation.layout.Box
36
import androidx.compose.foundation.layout.Column
7+
import androidx.compose.foundation.layout.Row
48
import androidx.compose.foundation.layout.padding
59
import androidx.compose.foundation.text.KeyboardActions
10+
import androidx.compose.material.DropdownMenu
11+
import androidx.compose.material.DropdownMenuItem
12+
import androidx.compose.material.Icon
13+
import androidx.compose.material3.Text
614
import androidx.compose.material.TextField
715
import androidx.compose.material3.Card
816
import androidx.compose.runtime.Composable
@@ -11,15 +19,18 @@ import androidx.compose.runtime.mutableStateOf
1119
import androidx.compose.runtime.remember
1220
import androidx.compose.runtime.rememberCoroutineScope
1321
import androidx.compose.runtime.setValue
22+
import androidx.compose.ui.Alignment
1423
import androidx.compose.ui.Modifier
1524
import androidx.compose.ui.input.key.Key
1625
import androidx.compose.ui.input.key.key
1726
import androidx.compose.ui.input.key.onKeyEvent
1827
import androidx.compose.ui.text.input.PasswordVisualTransformation
1928
import androidx.compose.ui.unit.dp
29+
import dev.icerock.moko.resources.compose.painterResource
2030
import id.neotica.modernadb.adb.android.AdbInput
21-
import id.neotica.modernadb.adb.idiomaticAdbInputs
2231
import id.neotica.modernadb.presentation.components.ButtonBasic
32+
import id.neotica.modernadb.presentation.components.NeoIcon
33+
import id.neotica.modernadb.res.MR
2334
import kotlinx.coroutines.Dispatchers
2435
import kotlinx.coroutines.launch
2536

@@ -29,17 +40,27 @@ fun ControlsView(
2940
) {
3041
val scope = rememberCoroutineScope()
3142

43+
val deviceList = listOf("Pixel 6", "Pixel 3")
44+
var selectedDevice by remember { mutableStateOf(deviceList.first()) }
45+
var dropDownState by remember { mutableStateOf(false) }
46+
47+
val unlockMethods = listOf("Password", "Pin")
48+
var selectedMethod by remember { mutableStateOf(unlockMethods.first()) }
49+
var unlockState by remember { mutableStateOf(false) }
3250
Card {
3351
Column(
3452
modifier = modifier
3553
.padding(16.dp),
3654
) {
37-
ButtonBasic("Power Button") {
38-
scope.launch(Dispatchers.IO) {
39-
idiomaticAdbInputs("power")
40-
}
55+
NeoIcon(
56+
desc = "Power Button",
57+
image = MR.images.ic_power,
58+
onLongClick = { AdbInput.longPressPowerButton() }
59+
) {
60+
AdbInput.powerButton()
4161
}
4262

63+
4364
var password by remember { mutableStateOf("") }
4465

4566
TextField(
@@ -48,13 +69,13 @@ fun ControlsView(
4869
visualTransformation = PasswordVisualTransformation(),
4970
keyboardActions = KeyboardActions {
5071
scope.launch(Dispatchers.IO) {
51-
AdbInput.unlock(password)
72+
unlock(password, selectedDevice, selectedMethod)
5273
}
5374
},
5475
modifier = Modifier.onKeyEvent { keyEvent ->
5576
if (keyEvent.key == Key.Enter) {
5677
scope.launch(Dispatchers.IO) {
57-
AdbInput.unlock(password)
78+
unlock(password, selectedDevice, selectedMethod)
5879
}
5980
true
6081
} else {
@@ -63,9 +84,104 @@ fun ControlsView(
6384
}
6485
)
6586

66-
ButtonBasic("Unlock device") {
67-
scope.launch {
68-
AdbInput.unlock(password)
87+
Row(
88+
modifier = Modifier.padding(16.dp),
89+
horizontalArrangement = Arrangement.spacedBy(16.dp),
90+
verticalAlignment = Alignment.CenterVertically
91+
) {
92+
ButtonBasic("Unlock") {
93+
scope.launch {
94+
unlock(password, selectedDevice, selectedMethod)
95+
}
96+
}
97+
98+
DropdownBasic(
99+
items = deviceList,
100+
selectedItem = selectedDevice,
101+
expanded = dropDownState
102+
) {
103+
selectedDevice = it
104+
}
105+
106+
DropdownBasic(
107+
items = unlockMethods,
108+
selectedItem = selectedMethod,
109+
expanded = unlockState
110+
) {
111+
selectedMethod = it
112+
}
113+
}
114+
115+
var powerState by remember { mutableStateOf(false) }
116+
ButtonBasic("Power") {
117+
powerState = !powerState
118+
}
119+
if (powerState) {
120+
ButtonBasic("Shut down") { AdbInput.shutdown() }
121+
ButtonBasic("Reboot") { AdbInput.reboot() }
122+
ButtonBasic("Reboot Recovery") { AdbInput.rebootRecovery() }
123+
ButtonBasic("Reboot Bootloader") { AdbInput.rebootBootloader() }
124+
}
125+
126+
}
127+
}
128+
}
129+
130+
private fun unlock(password: String, device: String, method: String) {
131+
132+
when (device) {
133+
"Pixel 6" -> {
134+
when (method) {
135+
"Password" -> {}
136+
"Pin" -> AdbInput.unlock(password)
137+
}
138+
}
139+
else -> {
140+
when (method) {
141+
"Password" -> {}
142+
"Pin" -> AdbInput.unlock(password)
143+
}
144+
}
145+
}
146+
147+
}
148+
149+
@Composable
150+
fun DropdownBasic(
151+
items: List<String>,
152+
selectedItem: String,
153+
expanded: Boolean,
154+
dropDownStateCallback: (String) -> Unit
155+
) {
156+
var dropDownState by remember { mutableStateOf(expanded) }
157+
var selectedDevice by remember { mutableStateOf(selectedItem) }
158+
159+
Box {
160+
Row(
161+
modifier = Modifier.clickable {
162+
dropDownState = true
163+
},
164+
horizontalArrangement = Arrangement.Center,
165+
) {
166+
Text(selectedDevice)
167+
Icon(
168+
painter = painterResource(MR.images.ic_expand_more),
169+
contentDescription = "Expand More"
170+
)
171+
}
172+
DropdownMenu(
173+
expanded = dropDownState,
174+
onDismissRequest = {
175+
dropDownState = false
176+
}
177+
) {
178+
items.forEach {
179+
DropdownMenuItem(onClick = {
180+
dropDownStateCallback(it)
181+
selectedDevice = it
182+
dropDownState = false
183+
}) {
184+
Text(it)
69185
}
70186
}
71187
}

0 commit comments

Comments
 (0)