Skip to content

Commit a839c54

Browse files
committed
Feat: Implement APK installation via drag and drop
This commit introduces the ability to install APK files by dragging and dropping them onto the application. Key changes: - Added `FileDropTarget.kt`: - A new composable that acts as a drop target for files. - It highlights when a file is being dragged over it. - Validates if the dropped file is an APK. - Displays the file path of the dropped APK. - Provides "Install APK" and "Cancel" buttons. - Added `AdbInput.install(apk: String)`: - A new function in `AdbInput.kt` to execute the `adb install` command with the provided APK file path. - Integrated `FileDropTarget` into `MainView.kt`: - The `FileDropTarget` composable is now included in the UI. - `MainView`'s layout logic was slightly refactored by introducing `FirstTab` and `SecondTab` composables to better organize the UI elements. `FileDropTarget` is placed within `SecondTab`. When an APK file is dropped: - If the file is a valid APK, its path is displayed, and an "Install APK" button appears. - Clicking "Install APK" will attempt to install the application on the connected Android device. - A toast message "Not an APK file" is shown if a non-APK file is dropped. - The "Cancel" button clears the selected file path.
1 parent 911cac6 commit a839c54

File tree

20 files changed

+509
-143
lines changed

20 files changed

+509
-143
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
package id.neotica.modernadb.adb
1+
package id.neotica.modernadb.data.adb
22

3-
import id.neotica.modernadb.adb.android.AdbInput
3+
import id.neotica.modernadb.data.adb.android.AdbInput
44

55
fun idiomaticAdbInputs(input: String, callback: ((String) -> Unit)? = null) {
66
when {

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

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package id.neotica.modernadb.adb.android
1+
package id.neotica.modernadb.data.adb.android
22

33
import java.io.File
44
import java.util.concurrent.TimeUnit
@@ -34,7 +34,15 @@ object AdbInput {
3434
}
3535

3636
// Add the the command parts
37-
commandParts.addAll(command.split(" "))
37+
if (command.startsWith("install ")) {
38+
// Split the command into only two parts: "install" and the file path.
39+
// The 'limit = 2' is crucial as it stops splitting after the first space.
40+
val parts = command.split(" ", limit = 2)
41+
commandParts.addAll(parts)
42+
} else {
43+
// Keep the original logic for all other commands.
44+
commandParts.addAll(command.split(" "))
45+
}
3846

3947
println("DEBUG: Executing command parts: $commandParts")
4048

@@ -151,8 +159,20 @@ object AdbInput {
151159

152160
fun activityManager() = exec("shell am start -a android.intent.action.VIEW")
153161

162+
//adb connect wifi adb
163+
fun connectWireless(port: String): String {
164+
val connect = exec("connect $port")
165+
return connect.inputStream.bufferedReader().readText()
166+
}
167+
154168
//apk install
155-
fun install(apk: String) = exec("install \"$apk\"")
169+
fun install(apk: String): String {
170+
val install = exec("install $apk")
171+
// val install = exec("install")
172+
println("✨ install $install")
173+
println("✨ apk $apk")
174+
return install.inputStream.bufferedReader().readText()
175+
}
156176

157177
private fun formatMessage(input: String): String {
158178
return input.replace(" ", "%s")
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package id.neotica.modernadb.domain
2+
3+
sealed class NeoResult<T>(
4+
val data: T? = null,
5+
val errorMessage: String? = null
6+
) {
7+
class Success<T>(data: T) : NeoResult<T>(data)
8+
class Loading<T>(data: T? = null) : NeoResult<T>(data)
9+
class Error<T>(errorMessage: String? = null) : NeoResult<T>(null, errorMessage)
10+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
package id.neotica.modernadb
22

3+
import androidx.compose.foundation.background
4+
import androidx.compose.foundation.layout.Box
5+
import androidx.compose.foundation.layout.fillMaxSize
6+
import androidx.compose.ui.Modifier
37
import androidx.compose.ui.window.Window
48
import androidx.compose.ui.window.application
9+
import id.neotica.modernadb.presentation.theme.DarkBackground
510

611
fun main() = application {
712
Window(
813
onCloseRequest = ::exitApplication,
914
title = "ModernADB",
15+
// transparent = true,
16+
// undecorated = true
1017
) {
18+
Box(
19+
Modifier.fillMaxSize()
20+
.background(DarkBackground)
21+
)
1122
App()
1223
}
1324
}

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

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import androidx.compose.foundation.layout.Column
66
import androidx.compose.foundation.layout.Row
77
import androidx.compose.foundation.layout.padding
88
import androidx.compose.material.Switch
9-
import androidx.compose.material3.Card
109
import androidx.compose.material3.ExperimentalMaterial3Api
1110
import androidx.compose.material3.Text
1211
import androidx.compose.runtime.Composable
@@ -18,14 +17,17 @@ import androidx.compose.runtime.setValue
1817
import androidx.compose.ui.Alignment
1918
import androidx.compose.ui.InternalComposeUiApi
2019
import androidx.compose.ui.Modifier
21-
import androidx.compose.ui.graphics.Color
2220
import androidx.compose.ui.text.style.TextAlign
2321
import androidx.compose.ui.text.style.TextDecoration
2422
import androidx.compose.ui.unit.dp
25-
import id.neotica.modernadb.adb.android.AdbInput
26-
import id.neotica.modernadb.adb.idiomaticAdbInputs
23+
import id.neotica.modernadb.data.adb.android.AdbInput
24+
import id.neotica.modernadb.data.adb.idiomaticAdbInputs
2725
import id.neotica.modernadb.presentation.components.ButtonBasic
26+
import id.neotica.modernadb.presentation.components.NeoCard
2827
import id.neotica.modernadb.presentation.components.NeoIcon
28+
import id.neotica.modernadb.presentation.components.NeoSwitchColor
29+
import id.neotica.modernadb.presentation.theme.DarkPrimary
30+
import id.neotica.modernadb.presentation.theme.DarkPrimaryTransparent2
2931
import id.neotica.modernadb.res.MR
3032
import kotlinx.coroutines.Dispatchers
3133
import kotlinx.coroutines.launch
@@ -38,8 +40,8 @@ fun AndroidNavigationView(
3840

3941
val scope = rememberCoroutineScope()
4042

41-
Card(
42-
Modifier.padding(horizontal = 16.dp)
43+
NeoCard (
44+
modifier = Modifier.padding(horizontal = 16.dp)
4345
) {
4446
Column(
4547
modifier = Modifier
@@ -79,7 +81,7 @@ fun AndroidNavigationView(
7981
text = "collapse!",
8082
textAlign = TextAlign.End,
8183
textDecoration = TextDecoration.Underline,
82-
color = Color.Blue,
84+
color = DarkPrimary,
8385
modifier = Modifier
8486
.clickable { expanded = !expanded }
8587
)
@@ -91,9 +93,10 @@ fun AndroidNavigationView(
9193
) {
9294
Text(
9395
text = "keys",
94-
color = if (currentMode == ControlMode.Keys) Color.Black else Color.Gray
96+
color = if (currentMode == ControlMode.Keys) DarkPrimary else DarkPrimaryTransparent2
9597
)
9698
Switch(
99+
colors = NeoSwitchColor(),
97100
// The switch is "on" (checked) if the mode is Write.
98101
checked = currentMode == ControlMode.Swipe,
99102
// When the switch is toggled, update the state to the corresponding mode.
@@ -103,7 +106,7 @@ fun AndroidNavigationView(
103106
)
104107
Text(
105108
text = "swipe",
106-
color = if (currentMode == ControlMode.Swipe) Color.Black else Color.Gray
109+
color = if (currentMode == ControlMode.Swipe) DarkPrimary else DarkPrimaryTransparent2
107110
)
108111
}
109112

@@ -118,7 +121,7 @@ fun AndroidNavigationView(
118121
text = "expand...",
119122
textAlign = TextAlign.End,
120123
textDecoration = TextDecoration.Underline,
121-
color = Color.Blue,
124+
color = DarkPrimary,
122125
modifier = Modifier
123126
.clickable { expanded = !expanded }
124127
)

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

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import androidx.compose.foundation.text.KeyboardOptions
1111
import androidx.compose.material.Switch
1212
import androidx.compose.material3.Button
1313
import androidx.compose.material3.ButtonDefaults
14-
import androidx.compose.material3.Card
1514
import androidx.compose.material3.Text
1615
import androidx.compose.material3.TextField
1716
import androidx.compose.runtime.Composable
@@ -28,7 +27,11 @@ import androidx.compose.ui.input.key.key
2827
import androidx.compose.ui.input.key.onKeyEvent
2928
import androidx.compose.ui.text.input.ImeAction
3029
import androidx.compose.ui.unit.dp
31-
import id.neotica.modernadb.adb.idiomaticAdbInputs
30+
import id.neotica.modernadb.data.adb.idiomaticAdbInputs
31+
import id.neotica.modernadb.presentation.components.NeoCard
32+
import id.neotica.modernadb.presentation.components.NeoSwitchColor
33+
import id.neotica.modernadb.presentation.components.NeoText
34+
import id.neotica.modernadb.presentation.components.NeoTextFieldColor
3235
import kotlinx.coroutines.Dispatchers
3336
import kotlinx.coroutines.launch
3437

@@ -50,7 +53,7 @@ fun CommandView(modifier: Modifier = Modifier) {
5053
input = ""
5154
}
5255

53-
Card(
56+
NeoCard(
5457
modifier = modifier
5558
){
5659
Column(
@@ -66,16 +69,18 @@ fun CommandView(modifier: Modifier = Modifier) {
6669
checked = currentMode == InputMode.Write,
6770
onCheckedChange = { isChecked ->
6871
currentMode = if (isChecked) InputMode.Write else InputMode.Command
69-
}
72+
},
73+
colors = NeoSwitchColor()
7074
)
7175

7276
Spacer(modifier = Modifier.width(16.dp))
7377

74-
Text(
78+
NeoText(
7579
text = "Mode: ${currentMode.input}",
7680
)
7781
}
7882
TextField(
83+
colors = NeoTextFieldColor(),
7984
value = input,
8085
onValueChange = {
8186
if (!it.contains('\n')) {
@@ -99,7 +104,8 @@ fun CommandView(modifier: Modifier = Modifier) {
99104
writeInput()
100105
}
101106
},
102-
modifier = Modifier.onKeyEvent { keyEvent ->
107+
modifier = Modifier
108+
.onKeyEvent { keyEvent ->
103109
if (keyEvent.key == Key.Enter) {
104110
scope.launch(Dispatchers.IO) {
105111
writeInput()

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

Lines changed: 36 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package id.neotica.modernadb.presentation
22

3+
import androidx.compose.foundation.background
34
import androidx.compose.foundation.clickable
45
import androidx.compose.foundation.layout.Arrangement
56
import androidx.compose.foundation.layout.Box
@@ -10,13 +11,9 @@ import androidx.compose.foundation.text.KeyboardActions
1011
import androidx.compose.material.DropdownMenu
1112
import androidx.compose.material.DropdownMenuItem
1213
import androidx.compose.material.Icon
13-
import androidx.compose.material3.Text
14-
import androidx.compose.material.TextField
15-
import androidx.compose.material3.Card
1614
import androidx.compose.material3.ExperimentalMaterial3Api
17-
import androidx.compose.material3.TooltipBox
18-
import androidx.compose.material3.TooltipDefaults
19-
import androidx.compose.material3.rememberTooltipState
15+
import androidx.compose.material3.Text
16+
import androidx.compose.material3.TextField
2017
import androidx.compose.runtime.Composable
2118
import androidx.compose.runtime.getValue
2219
import androidx.compose.runtime.mutableStateOf
@@ -25,15 +22,21 @@ import androidx.compose.runtime.rememberCoroutineScope
2522
import androidx.compose.runtime.setValue
2623
import androidx.compose.ui.Alignment
2724
import androidx.compose.ui.Modifier
25+
import androidx.compose.ui.graphics.Color
2826
import androidx.compose.ui.input.key.Key
2927
import androidx.compose.ui.input.key.key
3028
import androidx.compose.ui.input.key.onKeyEvent
3129
import androidx.compose.ui.text.input.PasswordVisualTransformation
3230
import androidx.compose.ui.unit.dp
3331
import dev.icerock.moko.resources.compose.painterResource
34-
import id.neotica.modernadb.adb.android.AdbInput
32+
import id.neotica.modernadb.data.adb.android.AdbInput
33+
import id.neotica.modernadb.data.adb.idiomaticAdbInputs
3534
import id.neotica.modernadb.presentation.components.ButtonBasic
35+
import id.neotica.modernadb.presentation.components.NeoCard
3636
import id.neotica.modernadb.presentation.components.NeoIcon
37+
import id.neotica.modernadb.presentation.components.NeoTextFieldColor
38+
import id.neotica.modernadb.presentation.components.NeoToolTip
39+
import id.neotica.modernadb.presentation.theme.DarkBackground
3740
import id.neotica.modernadb.res.MR
3841
import id.neotica.modernadb.utils.device.SupportedDevice
3942
import kotlinx.coroutines.Dispatchers
@@ -54,7 +57,7 @@ fun ControlsView(
5457
val unlockMethods = listOf("Pin", "Password")
5558
var selectedMethod by remember { mutableStateOf(unlockMethods.first()) }
5659
var unlockState by remember { mutableStateOf(false) }
57-
Card {
60+
NeoCard() {
5861
Column(
5962
modifier = modifier
6063
.padding(16.dp),
@@ -71,6 +74,7 @@ fun ControlsView(
7174
var password by remember { mutableStateOf("") }
7275

7376
TextField(
77+
colors = NeoTextFieldColor(),
7478
value = password,
7579
onValueChange = { password = it },
7680
visualTransformation = PasswordVisualTransformation(),
@@ -97,11 +101,7 @@ fun ControlsView(
97101
verticalAlignment = Alignment.CenterVertically
98102
) {
99103

100-
TooltipBox(
101-
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
102-
tooltip = { Text("Unlocking device is currently experimental.") },
103-
state = rememberTooltipState()
104-
) {
104+
NeoToolTip("Unlocking device is currently experimental.") {
105105
ButtonBasic("Unlock") {
106106
scope.launch {
107107
unlock(password, selectedDevice, selectedMethod)
@@ -110,21 +110,24 @@ fun ControlsView(
110110
}
111111

112112

113-
DropdownBasic(
114-
items = deviceList,
115-
selectedItem = selectedDevice,
116-
expanded = dropDownState
117-
) {
118-
selectedDevice = it
119-
}
113+
Column {
114+
DropdownBasic(
115+
items = deviceList,
116+
selectedItem = selectedDevice,
117+
expanded = dropDownState
118+
) {
119+
selectedDevice = it
120+
}
120121

121-
DropdownBasic(
122-
items = unlockMethods,
123-
selectedItem = selectedMethod,
124-
expanded = unlockState
125-
) {
126-
selectedMethod = it
122+
DropdownBasic(
123+
items = unlockMethods,
124+
selectedItem = selectedMethod,
125+
expanded = unlockState
126+
) {
127+
selectedMethod = it
128+
}
127129
}
130+
128131
}
129132

130133
var powerState by remember { mutableStateOf(false) }
@@ -148,7 +151,9 @@ private fun unlock(password: String, device: String, method: String) {
148151
when (device) {
149152
SupportedDevice.PIXEL_6.displayName -> {
150153
when (method) {
151-
"Password" -> {}
154+
"Password" -> {
155+
idiomaticAdbInputs("w $password")
156+
}
152157
"Pin" -> AdbInput.unlock(password)
153158
}
154159
}
@@ -159,6 +164,7 @@ private fun unlock(password: String, device: String, method: String) {
159164
}
160165
}
161166
}
167+
//192.168.0.110:39243
162168

163169
}
164170

@@ -184,10 +190,12 @@ fun DropdownBasic(
184190
Text(selectedDevice)
185191
Icon(
186192
painter = painterResource(MR.images.ic_expand_more),
187-
contentDescription = "Expand More"
193+
contentDescription = "Expand More",
194+
tint = Color.White
188195
)
189196
}
190197
DropdownMenu(
198+
modifier = Modifier.background(DarkBackground),
191199
expanded = dropDownState,
192200
onDismissRequest = {
193201
dropDownState = false

0 commit comments

Comments
 (0)