Просмотр исходного кода

greeter, info pre-implementation

add kapt

code improvements

greeter
Milan Jurkulak 2 лет назад
Родитель
Сommit
11a5bcc987
41 измененных файлов с 624 добавлено и 352 удалено
  1. 1 0
      gradle/libs.versions.toml
  2. 10 6
      src/main/kotlin/eu/mjdev/desktop/Main.kt
  3. 1 1
      src/main/kotlin/eu/mjdev/desktop/components/custom/UserAvatar.kt
  4. 1 1
      src/main/kotlin/eu/mjdev/desktop/components/desktop/Desktop.kt
  5. 69 6
      src/main/kotlin/eu/mjdev/desktop/components/greeter/Greeter.kt
  6. 2 4
      src/main/kotlin/eu/mjdev/desktop/components/image/ImageAny.kt
  7. 57 0
      src/main/kotlin/eu/mjdev/desktop/components/info/InfoWindow.kt
  8. 101 0
      src/main/kotlin/eu/mjdev/desktop/components/input/PasswordTextView.kt
  9. 5 2
      src/main/kotlin/eu/mjdev/desktop/data/App.kt
  10. 60 23
      src/main/kotlin/eu/mjdev/desktop/data/User.kt
  11. 16 14
      src/main/kotlin/eu/mjdev/desktop/helpers/adb/AdbDiscover.kt
  12. 16 11
      src/main/kotlin/eu/mjdev/desktop/helpers/adb/IAdb.kt
  13. 3 3
      src/main/kotlin/eu/mjdev/desktop/helpers/adb/adbserver/AdbBinary.kt
  14. 8 51
      src/main/kotlin/eu/mjdev/desktop/helpers/adb/adbserver/AdbServer.kt
  15. 62 0
      src/main/kotlin/eu/mjdev/desktop/helpers/adb/adbserver/AdbServerAdb.kt
  16. 10 0
      src/main/kotlin/eu/mjdev/desktop/helpers/adb/forwarding/TcpForwardDescriptor.kt
  17. 7 31
      src/main/kotlin/eu/mjdev/desktop/helpers/adb/forwarding/TcpForwarder.kt
  18. 14 26
      src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbConnection.kt
  19. 1 1
      src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbConstants.kt
  20. 12 6
      src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbImpl.kt
  21. 0 23
      src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbKeyPair.kt
  22. 13 11
      src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbMessage.kt
  23. 4 2
      src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbMessageQueue.kt
  24. 3 1
      src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbReader.kt
  25. 8 38
      src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbShell.kt
  26. 26 0
      src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbShellPacket.kt
  27. 11 0
      src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbShellResponse.kt
  28. 4 21
      src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbStream.kt
  29. 1 1
      src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbStreamClosed.kt
  30. 15 24
      src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbSync.kt
  31. 10 11
      src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbWriter.kt
  32. 5 0
      src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/ConnectionString.kt
  33. 9 0
      src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/IAdbStream.kt
  34. 1 1
      src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/Logging.kt
  35. 2 5
      src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/MessageQueue.kt
  36. 6 0
      src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/Packet.kt
  37. 8 1
      src/main/kotlin/eu/mjdev/desktop/helpers/system/Command.kt
  38. 10 0
      src/main/kotlin/eu/mjdev/desktop/helpers/system/Environment.kt
  39. 1 3
      src/main/kotlin/eu/mjdev/desktop/helpers/system/OsRelease.kt
  40. 4 6
      src/main/kotlin/eu/mjdev/desktop/helpers/system/UserDirs.kt
  41. 27 18
      src/main/kotlin/eu/mjdev/desktop/provider/DesktopProvider.kt

+ 1 - 0
gradle/libs.versions.toml

@@ -106,5 +106,6 @@ dbus-java-transport-native-unixsocket = { module = "com.github.hypfvieh:dbus-jav
 
 [plugins]
 kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
+kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
 compose-desktop = { id = "org.jetbrains.compose", version.ref = "compose" }
 kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

+ 10 - 6
src/main/kotlin/eu/mjdev/desktop/Main.kt

@@ -10,6 +10,7 @@ import eu.mjdev.desktop.components.appsmenu.AppsMenu
 import eu.mjdev.desktop.components.controlcenter.ControlCenter
 import eu.mjdev.desktop.components.desktop.Desktop
 import eu.mjdev.desktop.components.desktoppanel.DesktopPanel
+import eu.mjdev.desktop.components.greeter.Greeter
 import eu.mjdev.desktop.components.sliding.VisibilityState.Companion.rememberVisibilityState
 import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
 import eu.mjdev.desktop.provider.DesktopProvider.Companion.rememberDesktopProvider
@@ -43,9 +44,7 @@ fun main(
         }
     }
     MaterialTheme {
-        CompositionLocalProvider(
-            LocalDesktop provides api
-        ) {
+        CompositionLocalProvider(LocalDesktop provides api) {
             MainWindow(
                 panelState = panelState,
                 controlCenterState = controlCenterState,
@@ -58,14 +57,19 @@ fun main(
                     panelState = panelState,
                     menuState = menuState,
                     onMenuIconClicked = { if (!menuState.isVisible) menuState.show() },
-                    onFocusChange = { focus -> handlePanelFocus(focus) })
+                    onFocusChange = { focus -> handlePanelFocus(focus) }
+                )
                 AppsMenu(
                     menuState = menuState,
                     panelState = panelState,
-                    onFocusChange = { focus -> handleMenuFocus(focus) })
+                    onFocusChange = { focus -> handleMenuFocus(focus) }
+                )
                 ControlCenter(
                     controlCenterState = controlCenterState,
-                    onFocusChange = { focus -> handleControlCenterFocus(focus) })
+                    onFocusChange = { focus -> handleControlCenterFocus(focus) }
+                )
+                Greeter()
+//                InfoWindow()
             }
             DisposableEffect(Unit) {
                 onDispose {

+ 1 - 1
src/main/kotlin/eu/mjdev/desktop/components/custom/UserAvatar.kt

@@ -58,7 +58,7 @@ fun UserAvatar(
             else Alignment.Start
         ) {
             TextAny(
-                text = api.currentUser.name ?: api.homeDir.name ?: "-",
+                text = api.currentUser.userName,
                 color = textColor,
                 textAlign = TextAlign.Start,
                 fontWeight = FontWeight.Bold

+ 1 - 1
src/main/kotlin/eu/mjdev/desktop/components/desktop/Desktop.kt

@@ -29,7 +29,7 @@ fun Desktop(
                 .align(Alignment.BottomEnd)
         )
         FolderView(
-            path = api.userDirs.desktopDirectory,
+            path = api.currentUser.userDirs.desktopDirectory,
             showHomeFolder = true,
             orientation = Orientation.Vertical,
             modifier = Modifier.size(constraints.maxWidth.dp, constraints.maxHeight.dp)

+ 69 - 6
src/main/kotlin/eu/mjdev/desktop/components/greeter/Greeter.kt

@@ -1,17 +1,80 @@
 package eu.mjdev.desktop.components.greeter
 
 import androidx.compose.desktop.ui.tooling.preview.Preview
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.TextFieldDefaults
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.WindowPosition
+import eu.mjdev.desktop.components.custom.UserAvatar
+import eu.mjdev.desktop.components.input.PasswordTextView
+import eu.mjdev.desktop.data.User
+import eu.mjdev.desktop.provider.DesktopProvider.Companion.withDesktopScope
+import eu.mjdev.desktop.windows.ChromeWindow
+import eu.mjdev.desktop.extensions.ColorUtils.alpha
+import eu.mjdev.desktop.extensions.Compose.rememberState
 
+// todo : all users
+@OptIn(ExperimentalFoundationApi::class)
 @Suppress("FunctionName", "unused")
 @Preview
 @Composable
-fun Greeter(
-    modifier: Modifier = Modifier
-) = Box(
-    modifier = modifier
-) {
-
+fun Greeter() = withDesktopScope {
+    val user: User by rememberState(api.currentUser)
+    var isUserLoggedIn by rememberState(user.isLoggedIn)
+    var passwordVisible by rememberState(false)
+    val onDone: (password: String) -> Unit = { password ->
+        isUserLoggedIn = user.login(password)
+    }
+    ChromeWindow(
+        position = WindowPosition.Aligned(Alignment.Center),
+        visible = isUserLoggedIn.not(),
+        enabled = true,
+        transparent = true,
+        resizable = false,
+        alwaysOnTop = true,
+        size = api.containerSize
+    ) {
+        Box(
+            modifier = Modifier.fillMaxSize()
+                .background(backgroundColor.alpha(0.9f)),
+            contentAlignment = Alignment.Center
+        ) {
+            Column(
+                modifier = Modifier.width(256.dp)
+            ) {
+                UserAvatar(
+                    modifier = Modifier,
+                    avatarSize = 128.dp,
+                    orientation = Orientation.Vertical,
+                    onUserAvatarClick = {
+                        passwordVisible = true
+                    }
+                )
+                if (passwordVisible) {
+                    PasswordTextView(
+                        modifier = Modifier.width(256.dp),
+                        colors = TextFieldDefaults.outlinedTextFieldColors(
+                            backgroundColor = iconsTintColor.copy(alpha = 0.1f),
+                            textColor = iconsTintColor.copy(alpha = 0.9f),
+                            cursorColor = iconsTintColor,
+                            focusedBorderColor = iconsTintColor,
+                            unfocusedBorderColor = iconsTintColor
+                        ),
+                        onDone = onDone
+                    )
+                }
+            }
+        }
+    }
 }

+ 2 - 4
src/main/kotlin/eu/mjdev/desktop/components/image/ImageAny.kt

@@ -17,8 +17,7 @@ import androidx.compose.ui.layout.ContentScale
 import coil3.compose.AsyncImage
 import eu.mjdev.desktop.components.video.VideoView
 import eu.mjdev.desktop.extensions.Compose.asyncImageLoader
-import eu.mjdev.desktop.provider.DesktopProvider
-import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
+import eu.mjdev.desktop.provider.DesktopProvider.Companion.withDesktopScope
 import org.jetbrains.skia.Bitmap
 import java.io.File
 import androidx.compose.foundation.Image as ComposeImage
@@ -28,7 +27,6 @@ import androidx.compose.foundation.Image as ComposeImage
 @Composable
 fun ImageAny(
     src: Any?,
-    api: DesktopProvider = LocalDesktop.current,
     contentDescription: String? = "",
     modifier: Modifier = Modifier,
     alignment: Alignment = Alignment.Center,
@@ -38,7 +36,7 @@ fun ImageAny(
     filterQuality: FilterQuality = DefaultFilterQuality,
     onLoading: () -> Unit = {},
     onFail: (error: Throwable) -> Unit = {}
-) {
+) = withDesktopScope {
     // todo from mime
     val isGif = src.toString().trim().endsWith(".gif")
     val isVideo = src.toString().trim().let {

+ 57 - 0
src/main/kotlin/eu/mjdev/desktop/components/info/InfoWindow.kt

@@ -0,0 +1,57 @@
+package eu.mjdev.desktop.components.info
+
+import androidx.compose.desktop.ui.tooling.preview.Preview
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.window.WindowPosition
+import eu.mjdev.desktop.components.background.BackgroundImage
+import eu.mjdev.desktop.extensions.Modifier.dropShadow
+import eu.mjdev.desktop.extensions.Modifier.rectShadow
+import eu.mjdev.desktop.provider.DesktopProvider.Companion.withDesktopScope
+import eu.mjdev.desktop.windows.ChromeWindow
+
+@Suppress("FunctionName")
+@Preview
+@Composable
+fun InfoWindow() = withDesktopScope {
+    ChromeWindow(
+        visible = true,
+        size = DpSize(808.dp, 608.dp),
+        position = WindowPosition.Aligned(Alignment.Center)
+    ) {
+        Box(
+            modifier = Modifier.fillMaxSize()
+        ) {
+            Box(
+                modifier = Modifier.fillMaxSize()
+                    .background(backgroundColor),
+                contentAlignment = Alignment.Center
+            ) {
+                BackgroundImage(
+                    modifier = Modifier.fillMaxSize()
+                )
+                Box(
+                    modifier = Modifier.fillMaxWidth()
+                        .height(200.dp)
+                        .background(iconsTintColor)
+                        .align(Alignment.BottomCenter)
+                ) {
+                    // todo
+                }
+            }
+        }
+    }
+}

+ 101 - 0
src/main/kotlin/eu/mjdev/desktop/components/input/PasswordTextView.kt

@@ -0,0 +1,101 @@
+package eu.mjdev.desktop.components.input
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.onClick
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.Icon
+import androidx.compose.material.OutlinedTextField
+import androidx.compose.material.TextFieldColors
+import androidx.compose.material.TextFieldDefaults
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.Send
+import androidx.compose.material.icons.filled.Visibility
+import androidx.compose.material.icons.filled.VisibilityOff
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.text.input.PasswordVisualTransformation
+import androidx.compose.ui.text.input.VisualTransformation
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+
+@Suppress("FunctionName")
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun PasswordTextView(
+    modifier: Modifier = Modifier,
+    password: MutableState<String> = rememberSaveable { mutableStateOf("") },
+    passwordVisible: MutableState<Boolean> = rememberSaveable { mutableStateOf(false) },
+    label: @Composable (() -> Unit)? = null,
+    placeholder: @Composable () -> Unit = {},
+    shape: Shape = RoundedCornerShape(8.dp),
+    iconSize: Dp = 24.dp,
+    colors: TextFieldColors = TextFieldDefaults.outlinedTextFieldColors(),
+    contentDescription: String = "Enter password",
+    hidePasswordDescription: String = "Hide password",
+    showPasswordDescription: String = "Show password",
+    onDone: (password: String) -> Unit = {}
+) {
+    val iconVisible = remember {
+        derivedStateOf {
+            if (passwordVisible.value) Icons.Filled.Visibility else Icons.Filled.VisibilityOff
+        }
+    }
+    val iconSend = remember {
+        derivedStateOf {
+            if (password.value.isNotEmpty()) Icons.AutoMirrored.Filled.Send else null
+        }
+    }
+    val description = remember {
+        derivedStateOf {
+            if (passwordVisible.value) hidePasswordDescription else showPasswordDescription
+        }
+    }
+    OutlinedTextField(
+        value = password.value,
+        onValueChange = { v: String -> password.value = v },
+        modifier = modifier,
+        enabled = true,
+        readOnly = false,
+        label = label,
+        singleLine = true,
+        maxLines = 1,
+        minLines = 1,
+        shape = shape,
+        colors = colors,
+        placeholder = placeholder,
+        visualTransformation = if (passwordVisible.value) VisualTransformation.None else PasswordVisualTransformation(),
+        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
+        trailingIcon = {
+            Row(
+                verticalAlignment = Alignment.CenterVertically,
+            ) {
+                Icon(
+                    modifier = Modifier.size(iconSize).onClick { passwordVisible.value = !passwordVisible.value },
+                    imageVector = iconVisible.value,
+                    tint = colors.textColor(true).value,
+                    contentDescription = description.value
+                )
+                if (iconSend.value != null) {
+                    Icon(
+                        modifier = Modifier.padding(4.dp).size(iconSize).onClick { onDone(password.value) },
+                        imageVector = iconSend.value!!,
+                        tint = colors.textColor(true).value,
+                        contentDescription = contentDescription
+                    )
+                }
+            }
+        }
+    )
+}

+ 5 - 2
src/main/kotlin/eu/mjdev/desktop/data/App.kt

@@ -5,6 +5,8 @@ import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.graphics.Color
 import eu.mjdev.desktop.extensions.Custom.command
 import eu.mjdev.desktop.helpers.exception.EmptyException.Companion.EmptyException
+import eu.mjdev.desktop.helpers.system.Command
+import eu.mjdev.desktop.helpers.system.Environment
 import eu.mjdev.desktop.provider.DesktopProvider
 import java.io.File
 
@@ -44,7 +46,7 @@ class App(
 
     var process: Process? = null
     val isRunning: Boolean
-        get() = (process?.isAlive ?: false) || process?.children()?.anyMatch { it.isAlive } == true
+        get() = (process?.isAlive == true) || process?.children()?.anyMatch { it.isAlive } == true
 
     var isStarted: Boolean = false
 
@@ -75,7 +77,8 @@ class App(
     fun start() = runCatching {
         triggerStart()
         println("Starting app: $name [$cmd]")
-        ProcessBuilder("/bin/bash", "-c", cmd).start().also {
+        val env = Environment()
+        Command("/bin/bash", "-c", cmd).executeWithEnv(env).also {
             process = it
         }.apply {
             onExit().thenRun {

+ 60 - 23
src/main/kotlin/eu/mjdev/desktop/data/User.kt

@@ -1,34 +1,71 @@
 package eu.mjdev.desktop.data
 
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.AccountCircle
-import androidx.compose.runtime.mutableStateOf
 import eu.mjdev.desktop.helpers.system.Command
+import eu.mjdev.desktop.helpers.system.UserDirs
+import java.io.File
 
-@Suppress("MemberVisibilityCanBePrivate")
+// todo
+@Suppress("MemberVisibilityCanBePrivate", "unused")
 class User(
-    name: String?,
-    picture: Any?,
-    config: DesktopConfig,
-    theme: Theme
+    val data: String,
+    val dataItems: List<String> = data.split(":"),
+    val config: DesktopConfig = DesktopConfig.Default, // todo load
+    val theme: Theme = Theme.Default // todo load
 ) {
-    val nameState = mutableStateOf(name)
-    val pictureState = mutableStateOf(picture)
-    val configState = mutableStateOf(config)
-    val themeState = mutableStateOf(theme)
+    val userName: String
+        get() = dataItems[0]
+    val password: String
+        get() = dataItems[1]
+    val uid: String
+        get() = dataItems[2]
+    val gid: String
+        get() = dataItems[3]
+    val description: String
+        get() = dataItems[4]
+    val home: String
+        get() = dataItems[5]
+    val shell: String
+        get() = dataItems[6]
+    val picture: Any?
+        get() = loadPicture(dataItems[5], ".face")
 
-    val name get() = nameState.value
-    val picture get() = pictureState.value
-    val config get() = configState.value
-    val theme get() = themeState.value
+    val homeDir
+        get() = File(home)
 
-    companion object {
+    val isLoggedIn
+        get() = isLoggedIn(userName)
+
+    val userDirs
+        get() = UserDirs(homeDir)
+
+    override fun toString(): String {
+        return userName
+    }
+
+    fun login(
+        string: String
+    ): Boolean {
         // todo
-        fun load() = User(
-            name = Command("whoami").execute(),
-            picture = Icons.Filled.AccountCircle,
-            config = DesktopConfig.Default,
-            theme = Theme.Default
-        )
+        return true
+    }
+
+    companion object {
+        val Nobody = User(":x:-1:-1:::")
+
+        fun isLoggedIn(un: String): Boolean =
+            Command("whoami").execute()?.trim()?.contentEquals(un) == true
+
+        fun loadPicture(home: String, pic: String): File = loadPicture(File(home), pic)
+
+        fun loadPicture(homeDir: File, picName: String): File = File(homeDir, picName)
+
+        val allUsers: List<User>
+            get() = File("/etc/passwd")
+                .readLines()
+                .filter { t ->
+                    t.contains("/home")
+                }.map {
+                    User(it)
+                }
     }
 }

+ 16 - 14
src/main/kotlin/eu/mjdev/desktop/helpers/adb/AdbDiscover.kt

@@ -9,23 +9,25 @@ import kotlinx.coroutines.launch
 class AdbDiscover(
     coroutineScope: CoroutineScope? = null,
     discoverDelay: Long = 1000L,
-    onAdded: suspend (Adb) -> Unit,
-//    onRemoved: suspend (Dadb) -> Unit,
+    onAdded: suspend (IAdb) -> Unit,
+    onRemoved: suspend (IAdb) -> Unit,
 ) {
-    val devices = mutableMapOf<String, Adb>()
+    val devices = mutableListOf<IAdb>()
 
     init {
         coroutineScope?.launch {
             while (isActive) {
                 runCatching {
-                    val newDevices = Adb.discover()
-                    if (newDevices.isNotEmpty()) {
-                        newDevices.forEach { entry ->
-                            if (!devices.containsKey(entry.toString())) {
-                                devices[entry.toString()] = entry
-                                onAdded(entry)
-                            }
-                        }
+                    val newDevices: List<IAdb> = IAdb.discover().toList()
+                    val removed = devices.filter { d -> !newDevices.contains(d) }
+                    val added = newDevices.filter { d -> !devices.contains(d) }
+                    removed.forEach { d ->
+                        devices.remove(d)
+                        onRemoved(d)
+                    }
+                    added.forEach { d ->
+                        devices.add(d)
+                        onAdded(d)
                     }
                 }.onFailure { e ->
                     println("Error : ${e.message}")
@@ -38,12 +40,12 @@ class AdbDiscover(
     companion object {
         fun adbDevicesHandler(
             coroutineScope: CoroutineScope? = null,
-//            onRemoved: suspend (Dadb) -> Unit = {},
-            onAdded: suspend (Adb) -> Unit = {},
+            onRemoved: suspend (IAdb) -> Unit = {},
+            onAdded: suspend (IAdb) -> Unit = {},
         ) = AdbDiscover(
             coroutineScope = coroutineScope,
             onAdded = onAdded,
-//            onRemoved = onRemoved
+            onRemoved = onRemoved
         )
     }
 }

+ 16 - 11
src/main/kotlin/eu/mjdev/desktop/helpers/adb/Adb.kt → src/main/kotlin/eu/mjdev/desktop/helpers/adb/IAdb.kt

@@ -9,9 +9,15 @@ import java.io.File
 import java.nio.file.Files
 
 @Suppress("unused")
-interface Adb : AutoCloseable {
+interface IAdb : AutoCloseable {
+
+    val host: String
+    val port: Int
+    val deviceQuery: String
+    val name: String
+
     @Throws(IOException::class)
-    fun open(destination: String): AdbStream
+    fun open(destination: String): IAdbStream
 
     fun supportsFeature(feature: String): Boolean
 
@@ -96,7 +102,6 @@ interface Adb : AutoCloseable {
 
     @Throws(IOException::class)
     fun installMultiple(apks: List<File>, vararg options: String) {
-        // http://aospxref.com/android-12.0.0_r3/xref/packages/modules/adb/client/adb_install.cpp#538
         if (supportsFeature("cmd")) {
             val totalLength = apks.map { it.length() }.reduce { acc, l -> acc + l }
             execCmd("package", "install-create", "-S", totalLength.toString(), *options).use { createStream ->
@@ -186,14 +191,14 @@ interface Adb : AutoCloseable {
     }
 
     @Throws(IOException::class)
-    fun execCmd(vararg command: String): AdbStream {
+    fun execCmd(vararg command: String): IAdbStream {
         if (!supportsFeature("cmd")) throw UnsupportedOperationException("cmd is not supported on this version of Android")
         val destination = (listOf("exec:cmd") + command).joinToString(" ")
         return open(destination)
     }
 
     @Throws(IOException::class)
-    fun abbExec(vararg command: String): AdbStream {
+    fun abbExec(vararg command: String): IAdbStream {
         if (!supportsFeature("abb_exec")) throw UnsupportedOperationException("abb_exec is not supported on this version of Android")
         val destination = "abb_exec:${command.joinToString("\u0000")}"
         return open(destination)
@@ -243,21 +248,21 @@ interface Adb : AutoCloseable {
             keyPair: AdbKeyPair? = AdbKeyPair.readDefault(),
             connectTimeout: Int = 0,
             socketTimeout: Int = 0
-        ): Adb = AdbImpl(host, port, keyPair, connectTimeout, socketTimeout)
+        ): IAdb = AdbImpl(host, port, keyPair, connectTimeout, socketTimeout)
 
         @JvmStatic
         @JvmOverloads
         fun discover(
             host: String = "localhost",
             keyPair: AdbKeyPair? = AdbKeyPair.readDefault()
-        ): List<Adb> {
+        ): List<IAdb> {
             return list(host, keyPair)
         }
 
         @JvmStatic
         @JvmOverloads
-        fun list(host: String = "localhost", keyPair: AdbKeyPair? = AdbKeyPair.readDefault()): List<Adb> {
-            val dadbs = AdbServer.listDadbs(adbServerHost = host)
+        fun list(host: String = "localhost", keyPair: AdbKeyPair? = AdbKeyPair.readDefault()): List<IAdb> {
+            val dadbs = AdbServer.listAdbs(adbServerHost = host)
             if (dadbs.isNotEmpty()) return dadbs
             return (MIN_EMULATOR_PORT..MAX_EMULATOR_PORT).mapNotNull { port ->
                 val dadb = create(host, port, keyPair)
@@ -274,7 +279,7 @@ interface Adb : AutoCloseable {
             }
         }
 
-        private fun waitRootOrClose(dadb: Adb, root: Boolean) {
+        private fun waitRootOrClose(dadb: IAdb, root: Boolean) {
             while (true) {
                 try {
                     val response = dadb.shell("getprop service.adb.root")
@@ -286,7 +291,7 @@ interface Adb : AutoCloseable {
             }
         }
 
-        private fun restartAdb(dadb: Adb, destination: String): String {
+        private fun restartAdb(dadb: IAdb, destination: String): String {
             dadb.open(destination).use { stream ->
                 return stream.source.readUntil('\n'.code.toByte()).readString(Charsets.UTF_8)
             }

+ 3 - 3
src/main/kotlin/eu/mjdev/desktop/helpers/adb/adbserver/AdbBinary.kt

@@ -5,14 +5,14 @@ import java.io.IOException
 import java.net.Socket
 import java.util.*
 
-internal object AdbBinary {
+object AdbBinary {
     private val ADB_BINARY: File? by lazy { find() }
 
     fun tryStartServer(adbServerHost: String, adbServerPort: Int): Boolean {
         return try {
             ensureServerRunning(adbServerHost, adbServerPort)
             true
-        } catch (ignore: Exception) {
+        } catch (_: Exception) {
             false
         }
     }
@@ -47,7 +47,7 @@ internal object AdbBinary {
         return try {
             Socket(adbServerHost, adbServerPort).close()
             true
-        } catch (e: Exception) {
+        } catch (_: Exception) {
             false
         }
     }

+ 8 - 51
src/main/kotlin/eu/mjdev/desktop/helpers/adb/adbserver/AdbServer.kt

@@ -1,10 +1,7 @@
 package eu.mjdev.desktop.helpers.adb.adbserver
 
-import eu.mjdev.desktop.helpers.adb.Adb
-import eu.mjdev.desktop.helpers.adb.helpers.AdbStream
-import okio.buffer
-import okio.sink
-import okio.source
+import eu.mjdev.desktop.helpers.adb.IAdb
+
 import java.io.DataInputStream
 import java.io.DataOutputStream
 import java.io.IOException
@@ -42,15 +39,15 @@ object AdbServer {
      */
     @JvmStatic
     @JvmOverloads
-    fun createDadb(
+    fun createAdb(
         adbServerHost: String = "localhost",
         adbServerPort: Int = 5037,
         deviceQuery: String = "host:transport-any"
-    ): Adb {
+    ): IAdb {
         val name = deviceQuery
             .removePrefix("host:") // Use the device query without the host: prefix
             .removePrefix("transport:") // If it's a serial-number, just show that
-        return AdbServerDadb(adbServerHost, adbServerPort, deviceQuery, name)
+        return AdbServerAdb(adbServerHost, adbServerPort, deviceQuery, name)
     }
 
     /**
@@ -58,10 +55,10 @@ object AdbServer {
      */
     @JvmStatic
     @JvmOverloads
-    fun listDadbs(
+    fun listAdbs(
         adbServerHost: String = "localhost",
         adbServerPort: Int = 5037,
-    ): List<Adb> {
+    ): List<IAdb> {
         if (!AdbBinary.tryStartServer(adbServerHost, adbServerPort)) {
             return emptyList()
         }
@@ -78,7 +75,7 @@ object AdbServer {
                 } else {
                     parts[0]
                 }
-            }.map { createDadb(adbServerHost, adbServerPort, "host:transport:${it}") }
+            }.map { createAdb(adbServerHost, adbServerPort, "host:transport:${it}") }
     }
 
     internal fun readString(inputStream: DataInputStream): String {
@@ -115,44 +112,4 @@ object AdbServer {
     }
 }
 
-private class AdbServerDadb(
-    private val host: String,
-    private val port: Int,
-    private val deviceQuery: String,
-    private val name: String,
-) : Adb {
-
-    private val supportedFeatures: Set<String>
-
-    init {
-        supportedFeatures = open("host:features").use {
-            val features = AdbServer.readString(DataInputStream(it.source.inputStream()))
-            features.split(",").toSet()
-        }
-    }
-
-    override fun open(destination: String): AdbStream {
-        AdbBinary.ensureServerRunning(host, port)
-        val socket = Socket(host, port)
-        AdbServer.send(socket, deviceQuery)
-        AdbServer.send(socket, destination)
-        return object : AdbStream {
-
-            override val source = socket.source().buffer()
-
-            override val sink = socket.sink().buffer()
 
-            override fun close() = socket.close()
-        }
-    }
-
-    override fun supportsFeature(feature: String): Boolean {
-        return feature in supportedFeatures
-    }
-
-    override fun close() {}
-
-    override fun toString(): String {
-        return name
-    }
-}

+ 62 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/adb/adbserver/AdbServerAdb.kt

@@ -0,0 +1,62 @@
+package eu.mjdev.desktop.helpers.adb.adbserver
+
+import eu.mjdev.desktop.helpers.adb.IAdb
+import eu.mjdev.desktop.helpers.adb.helpers.IAdbStream
+import okio.buffer
+import okio.sink
+import okio.source
+import java.io.DataInputStream
+import java.net.Socket
+
+class AdbServerAdb(
+    override val host: String,
+    override val port: Int,
+    override val deviceQuery: String,
+    override val name: String,
+) : IAdb {
+    private val supportedFeatures: Set<String>
+
+    init {
+        supportedFeatures = open("host:features").use {
+            val features = AdbServer.readString(DataInputStream(it.source.inputStream()))
+            features.split(",").toSet()
+        }
+    }
+
+    override fun open(destination: String): IAdbStream {
+        AdbBinary.ensureServerRunning(host, port)
+        val socket = Socket(host, port)
+        AdbServer.send(socket, deviceQuery)
+        AdbServer.send(socket, destination)
+        return object : IAdbStream {
+            override val source = socket.source().buffer()
+            override val sink = socket.sink().buffer()
+            override fun close() = socket.close()
+        }
+    }
+
+    override fun supportsFeature(feature: String): Boolean {
+        return feature in supportedFeatures
+    }
+
+    override fun close() {}
+
+    override fun toString(): String {
+        return name
+    }
+
+    override fun equals(other: Any?): Boolean = when (other) {
+        null -> false
+        !is IAdb -> false
+        else -> other.name.contentEquals(this.name) == true
+    }
+
+    override fun hashCode(): Int {
+        var result = host.hashCode()
+        result = 31 * result + port
+        result = 31 * result + deviceQuery.hashCode()
+        result = 31 * result + name.hashCode()
+        result = 31 * result + supportedFeatures.hashCode()
+        return result
+    }
+}

+ 10 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/adb/forwarding/TcpForwardDescriptor.kt

@@ -0,0 +1,10 @@
+package eu.mjdev.desktop.helpers.adb.forwarding
+
+data class TcpForwardDescriptor(
+    val resource: AutoCloseable,
+    val localPort: Int
+) : AutoCloseable {
+    override fun close() {
+        resource.close()
+    }
+}

+ 7 - 31
src/main/kotlin/eu/mjdev/desktop/helpers/adb/forwarding/TcpForwarder.kt

@@ -1,6 +1,6 @@
 package eu.mjdev.desktop.helpers.adb.forwarding
 
-import eu.mjdev.desktop.helpers.adb.Adb
+import eu.mjdev.desktop.helpers.adb.IAdb
 import eu.mjdev.desktop.helpers.adb.helpers.log
 import okio.*
 import java.io.IOException
@@ -13,19 +13,12 @@ import java.util.concurrent.TimeUnit
 import java.util.concurrent.TimeoutException
 import kotlin.concurrent.thread
 
-data class TcpForwardDescriptor(private val resource: AutoCloseable, val localPort: Int) : AutoCloseable {
-    override fun close() {
-        resource.close()
-    }
-}
-
 @Suppress("SameParameterValue")
 internal class TcpForwarder(
-    private val dadb: Adb,
+    private val adb: IAdb,
     private val targetPort: Int,
     private val hostPort: Int? = null,
 ) : AutoCloseable {
-
     private var state: State = State.STOPPED
     private var serverThread: Thread? = null
     private var server: ServerSocket? = null
@@ -33,14 +26,12 @@ internal class TcpForwarder(
 
     fun start(): Int {
         check(state == State.STOPPED) { "Forwarder is already started at port $hostPort" }
-
         moveToState(State.STARTING)
-
         clientExecutor = Executors.newCachedThreadPool()
         serverThread = thread {
             try {
                 handleForwarding()
-            } catch (ignored: SocketException) {
+            } catch (_: SocketException) {
                 // Do nothing
             } catch (e: IOException) {
                 log { "could not start TCP port forwarding: ${e.message}" }
@@ -48,33 +39,26 @@ internal class TcpForwarder(
                 moveToState(State.STOPPED)
             }
         }
-
         waitFor(10, 5000) {
             state == State.STARTED
         }
-
         return server!!.localPort
     }
 
     private fun handleForwarding() {
         val serverRef = ServerSocket(hostPort ?: 0)
         server = serverRef
-
         moveToState(State.STARTED)
-
         while (!Thread.interrupted()) {
             val client = serverRef.accept()
-
             clientExecutor?.execute {
-                val adbStream = dadb.open("tcp:$targetPort")
-
+                val adbStream = adb.open("tcp:$targetPort")
                 val readerThread = thread {
                     forward(
                         client.getInputStream().source(),
                         adbStream.sink
                     )
                 }
-
                 try {
                     forward(
                         adbStream.source,
@@ -83,7 +67,6 @@ internal class TcpForwarder(
                 } finally {
                     adbStream.close()
                     client.close()
-
                     readerThread.interrupt()
                 }
             }
@@ -94,15 +77,10 @@ internal class TcpForwarder(
         if (state == State.STOPPED || state == State.STOPPING) {
             return
         }
-
-        // Make sure that we are not stopping the server while it is in a transient state
-        // to avoid surprises
         waitFor(10, 5000) {
             state == State.STARTED
         }
-
         moveToState(State.STOPPING)
-
         server?.close()
         server = null
         serverThread?.interrupt()
@@ -110,7 +88,6 @@ internal class TcpForwarder(
         clientExecutor?.shutdown()
         clientExecutor?.awaitTermination(5, TimeUnit.SECONDS)
         clientExecutor = null
-
         waitFor(10, 5000) {
             state == State.STOPPED
         }
@@ -125,13 +102,13 @@ internal class TcpForwarder(
                     } else {
                         return
                     }
-                } catch (ignored: IOException) {
+                } catch (_: IOException) {
                     // Do nothing
                 }
             }
-        } catch (ignored: InterruptedException) {
+        } catch (_: InterruptedException) {
             // Do nothing
-        } catch (ignored: InterruptedIOException) {
+        } catch (_: InterruptedIOException) {
             // do nothing
         }
     }
@@ -168,5 +145,4 @@ internal class TcpForwarder(
             lastCheck = System.currentTimeMillis()
         }
     }
-
 }

+ 14 - 26
src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbConnection.kt

@@ -10,26 +10,24 @@ import java.io.IOException
 import java.net.Socket
 import java.util.*
 
-@Suppress("unused")
-internal class AdbConnection internal constructor(
+internal class AdbConnection(
     adbReader: AdbReader,
     private val adbWriter: AdbWriter,
     private val closeable: Closeable?,
     private val supportedFeatures: Set<String>,
-    private val version: Int,
+    @Suppress("unused") private val version: Int,
     private val maxPayloadSize: Int
 ) : AutoCloseable {
-
     private val random = Random()
     private val messageQueue = AdbMessageQueue(adbReader)
 
     @Throws(IOException::class)
-    fun open(destination: String): AdbStream {
+    fun open(destination: String): IAdbStream {
         val localId = newId()
         messageQueue.startListening(localId)
         try {
             adbWriter.writeOpen(localId, destination)
-            val message = messageQueue.take(localId, Constants.CMD_OKAY)
+            val message = messageQueue.take(localId, AdbConstants.CMD_OKAY)
             val remoteId = message.arg0
             return AdbStreamImpl(messageQueue, adbWriter, maxPayloadSize, localId, remoteId)
         } catch (e: Throwable) {
@@ -46,6 +44,7 @@ internal class AdbConnection internal constructor(
         return random.nextInt()
     }
 
+    @Suppress("unused")
     @TestOnly
     internal fun ensureEmpty() {
         messageQueue.ensureEmpty()
@@ -56,19 +55,18 @@ internal class AdbConnection internal constructor(
             messageQueue.close()
             adbWriter.close()
             closeable?.close()
-        } catch (ignore: Throwable) {
+        } catch (_: Throwable) {
         }
     }
 
     companion object {
-
         fun connect(socket: Socket, keyPair: AdbKeyPair? = null): AdbConnection {
             val source = socket.source()
             val sink = socket.sink()
             return connect(source, sink, keyPair, socket)
         }
 
-        private fun connect(
+        fun connect(
             source: Source,
             sink: Sink,
             keyPair: AdbKeyPair? = null,
@@ -86,40 +84,32 @@ internal class AdbConnection internal constructor(
             }
         }
 
-        private fun connect(
+        fun connect(
             adbReader: AdbReader,
             adbWriter: AdbWriter,
             keyPair: AdbKeyPair?,
             closeable: Closeable?
         ): AdbConnection {
             adbWriter.writeConnect()
-
             var message = adbReader.readMessage()
-
-            if (message.command == Constants.CMD_AUTH) {
+            if (message.command == AdbConstants.CMD_AUTH) {
                 checkNotNull(keyPair) { "Authentication required but no KeyPair provided" }
-                check(message.arg0 == Constants.AUTH_TYPE_TOKEN) { "Unsupported auth type: $message" }
-
+                check(message.arg0 == AdbConstants.AUTH_TYPE_TOKEN) { "Unsupported auth type: $message" }
                 val signature = keyPair.signPayload(message)
-                adbWriter.writeAuth(Constants.AUTH_TYPE_SIGNATURE, signature)
-
+                adbWriter.writeAuth(AdbConstants.AUTH_TYPE_SIGNATURE, signature)
                 message = adbReader.readMessage()
-                if (message.command == Constants.CMD_AUTH) {
-                    adbWriter.writeAuth(Constants.AUTH_TYPE_RSA_PUBLIC, keyPair.publicKeyBytes)
+                if (message.command == AdbConstants.CMD_AUTH) {
+                    adbWriter.writeAuth(AdbConstants.AUTH_TYPE_RSA_PUBLIC, keyPair.publicKeyBytes)
                     message = adbReader.readMessage()
                 }
             }
-
-            if (message.command != Constants.CMD_CNXN) throw IOException("Connection failed: $message")
-
+            if (message.command != AdbConstants.CMD_CNXN) throw IOException("Connection failed: $message")
             val connectionString = parseConnectionString(String(message.payload))
             val version = message.arg0
             val maxPayloadSize = message.arg1
-
             return AdbConnection(adbReader, adbWriter, closeable, connectionString.features, version, maxPayloadSize)
         }
 
-        // ie: "device::ro.product.name=sdk_gphone_x86;ro.product.model=Android SDK built for x86;ro.product.device=generic_x86;features=fixed_push_symlink_timestamp,apex,fixed_push_mkdir,stat_v2,abb_exec,cmd,abb,shell_v2"
         private fun parseConnectionString(connectionString: String): ConnectionString {
             val keyValues = connectionString.substringAfter("device::")
                 .split(";")
@@ -132,5 +122,3 @@ internal class AdbConnection internal constructor(
         }
     }
 }
-
-private data class ConnectionString(val features: Set<String>)

+ 1 - 1
src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/Constants.kt → src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbConstants.kt

@@ -1,6 +1,6 @@
 package eu.mjdev.desktop.helpers.adb.helpers
 
-internal object Constants {
+object AdbConstants {
     const val AUTH_TYPE_TOKEN = 1
     const val AUTH_TYPE_SIGNATURE = 2
     const val AUTH_TYPE_RSA_PUBLIC = 3

+ 12 - 6
src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbImpl.kt

@@ -1,18 +1,18 @@
 package eu.mjdev.desktop.helpers.adb.helpers
 
-import eu.mjdev.desktop.helpers.adb.Adb
+import eu.mjdev.desktop.helpers.adb.IAdb
 import org.jetbrains.annotations.TestOnly
 import java.net.InetSocketAddress
 import java.net.Socket
 
 @Suppress("unused")
-internal class AdbImpl @Throws(IllegalArgumentException::class) constructor(
-    private val host: String,
-    private val port: Int,
+class AdbImpl @Throws(IllegalArgumentException::class) constructor(
+    override val host: String,
+    override val port: Int,
     private val keyPair: AdbKeyPair? = null,
     private val connectTimeout: Int = 0,
-    private val socketTimeout: Int = 0
-) : Adb {
+    private val socketTimeout: Int = 0,
+) : IAdb {
     init {
         if (port < 0) {
             throw IllegalArgumentException("port must be >= 0")
@@ -27,6 +27,12 @@ internal class AdbImpl @Throws(IllegalArgumentException::class) constructor(
 
     private var connection: Pair<AdbConnection, Socket>? = null
 
+    override val deviceQuery: String
+        get() = ""
+
+    override val name: String
+        get() = ""
+
     override fun open(destination: String) = connection().open(destination)
 
     override fun supportsFeature(feature: String): Boolean {

+ 0 - 23
src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbKeyPair.kt

@@ -23,7 +23,6 @@ class AdbKeyPair(
     }
 
     companion object {
-
         private const val KEY_LENGTH_BITS = 2048
         private const val KEY_LENGTH_BYTES = KEY_LENGTH_BITS / 8
         private const val KEY_LENGTH_WORDS = KEY_LENGTH_BYTES / 4
@@ -55,11 +54,9 @@ class AdbKeyPair(
         fun readDefault(): AdbKeyPair {
             val privateKeyFile = File(System.getenv("HOME"), ".android/adbkey")
             val publicKeyFile = File(System.getenv("HOME"), ".android/adbkey.pub")
-
             if (!privateKeyFile.exists()) {
                 generate(privateKeyFile, publicKeyFile)
             }
-
             return read(privateKeyFile, publicKeyFile)
         }
 
@@ -72,7 +69,6 @@ class AdbKeyPair(
             } else {
                 ByteArray(0)
             }
-
             return AdbKeyPair(privateKey, publicKeyBytes)
         }
 
@@ -82,17 +78,14 @@ class AdbKeyPair(
                 it.initialize(KEY_LENGTH_BITS)
                 it.genKeyPair()
             }
-
             privateKeyFile.absoluteFile.parentFile?.mkdirs()
             publicKeyFile.absoluteFile.parentFile?.mkdirs()
-
             privateKeyFile.writer().use { out ->
                 val base64 = Base64.getMimeEncoder(64, "\n".toByteArray())
                 out.write("-----BEGIN PRIVATE KEY-----\n")
                 out.write(base64.encodeToString(keyPair.private.encoded))
                 out.write("\n-----END PRIVATE KEY-----")
             }
-
             publicKeyFile.writer().use { out ->
                 val base64 = Base64.getEncoder()
                 val bytes = convertRsaPublicKeyToAdbFormat(keyPair.public as RSAPublicKey)
@@ -108,22 +101,8 @@ class AdbKeyPair(
             return publicKeyBytes
         }
 
-        // https://github.com/cgutman/AdbLib/blob/d6937951eb98557c76ee2081e383d50886ce109a/src/com/cgutman/adblib/AdbCrypto.java#L83-L137
         @Suppress("JoinDeclarationAndAssignment")
         private fun convertRsaPublicKeyToAdbFormat(pubkey: RSAPublicKey): ByteArray {
-            /*
-             * ADB literally just saves the RSAPublicKey struct to a file.
-             *
-             * typedef struct RSAPublicKey {
-             * int len; // Length of n[] in number of uint32_t
-             * uint32_t n0inv;  // -1 / n[0] mod 2^32
-             * uint32_t n[RSANUMWORDS]; // modulus as little endian array
-             * uint32_t rr[RSANUMWORDS]; // R^2 as little endian array
-             * int exponent; // 3 or 65537
-             * } RSAPublicKey;
-             */
-
-            /* ------ This part is a Java-ified version of RSA_to_RSAPublicKey from adb_host_auth.c ------ */
             val r32: BigInteger
             val r: BigInteger
             var rr: BigInteger
@@ -149,8 +128,6 @@ class AdbKeyPair(
                 rem = res[1]
                 myN[i] = rem.toInt()
             }
-
-            /* ------------------------------------------------------------------------------------------- */
             val bbuf: ByteBuffer = ByteBuffer.allocate(524).order(ByteOrder.LITTLE_ENDIAN)
             bbuf.putInt(KEY_LENGTH_WORDS)
             bbuf.putInt(n0inv.negate().toInt())

+ 13 - 11
src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbMessage.kt

@@ -1,5 +1,7 @@
 package eu.mjdev.desktop.helpers.adb.helpers
 
+import eu.mjdev.desktop.helpers.adb.helpers.AdbShellStream.Companion.ID_EXIT
+import eu.mjdev.desktop.helpers.adb.helpers.AdbSyncStream.Companion.SYNC_IDS
 import okio.Buffer
 import okio.BufferedSource
 import java.nio.charset.StandardCharsets
@@ -14,15 +16,15 @@ internal class AdbMessage(
     val magic: Int,
     val payload: ByteArray
 ) {
-
-    override fun toString() = "${commandStr()}[${argStr(arg0)}, ${argStr(arg1)}] ${payloadStr()}"
+    override fun toString() =
+        "${commandStr()}[${argStr(arg0)}, ${argStr(arg1)}] ${payloadStr()}"
 
     private fun payloadStr(): String {
         if (payloadLength == 0) return ""
         return when (command) {
-            Constants.CMD_AUTH -> if (arg0 == Constants.AUTH_TYPE_RSA_PUBLIC) String(payload) else "auth[${payloadLength}]"
-            Constants.CMD_WRTE -> writePayloadStr()
-            Constants.CMD_OPEN -> String(payload, 0, payloadLength - 1)
+            AdbConstants.CMD_AUTH -> if (arg0 == AdbConstants.AUTH_TYPE_RSA_PUBLIC) String(payload) else "auth[${payloadLength}]"
+            AdbConstants.CMD_WRTE -> writePayloadStr()
+            AdbConstants.CMD_OPEN -> String(payload, 0, payloadLength - 1)
             else -> "payload[$payloadLength]"
         }
     }
@@ -61,12 +63,12 @@ internal class AdbMessage(
     private fun argStr(arg: Int) = String.format("%X", arg)
 
     private fun commandStr() = when (command) {
-        Constants.CMD_AUTH -> "AUTH"
-        Constants.CMD_CNXN -> "CNXN"
-        Constants.CMD_OPEN -> "OPEN"
-        Constants.CMD_OKAY -> "OKAY"
-        Constants.CMD_CLSE -> "CLSE"
-        Constants.CMD_WRTE -> "WRTE"
+        AdbConstants.CMD_AUTH -> "AUTH"
+        AdbConstants.CMD_CNXN -> "CNXN"
+        AdbConstants.CMD_OPEN -> "OPEN"
+        AdbConstants.CMD_OKAY -> "OKAY"
+        AdbConstants.CMD_CLSE -> "CLSE"
+        AdbConstants.CMD_WRTE -> "WRTE"
         else -> "????"
     }
 }

+ 4 - 2
src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbMessageQueue.kt

@@ -1,6 +1,8 @@
 package eu.mjdev.desktop.helpers.adb.helpers
 
-internal class AdbMessageQueue(private val adbReader: AdbReader) : AutoCloseable, MessageQueue<AdbMessage>() {
+internal class AdbMessageQueue(
+    private val adbReader: AdbReader
+) : AutoCloseable, MessageQueue<AdbMessage>() {
     override fun readMessage() = adbReader.readMessage()
 
     override fun getLocalId(message: AdbMessage) = message.arg1
@@ -9,5 +11,5 @@ internal class AdbMessageQueue(private val adbReader: AdbReader) : AutoCloseable
 
     override fun close() = adbReader.close()
 
-    override fun isCloseCommand(message: AdbMessage) = message.command == Constants.CMD_CLSE
+    override fun isCloseCommand(message: AdbMessage) = message.command == AdbConstants.CMD_CLSE
 }

+ 3 - 1
src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbReader.kt

@@ -3,7 +3,9 @@ package eu.mjdev.desktop.helpers.adb.helpers
 import okio.Source
 import okio.buffer
 
-internal class AdbReader(source: Source) : AutoCloseable {
+internal class AdbReader(
+    source: Source
+) : AutoCloseable {
     private val bufferedSource = source.buffer()
 
     fun readMessage(): AdbMessage {

+ 8 - 38
src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbShell.kt

@@ -1,19 +1,10 @@
-@file:JvmName("AdbShell")
-@file:Suppress("unused")
-
 package eu.mjdev.desktop.helpers.adb.helpers
 
 import eu.mjdev.desktop.helpers.adb.helpers.AdbShellPacket.*
 import java.io.IOException
 
-const val ID_STDIN = 0
-const val ID_STDOUT = 1
-const val ID_STDERR = 2
-const val ID_EXIT = 3
-const val ID_CLOSE_STDIN = 3
-
 class AdbShellStream(
-    private val stream: AdbStream
+    private val stream: IAdbStream
 ) : AutoCloseable {
     @Throws(IOException::class)
     fun readAll(): AdbShellResponse {
@@ -83,35 +74,14 @@ class AdbShellStream(
         check(id != ID_EXIT || length == 1) { "Shell exit packet does not have payload length == 1: $length" }
         return length
     }
-}
-
-sealed class AdbShellPacket(
-    open val payload: ByteArray
-) {
-    abstract val id: Int
-
-    class StdOut(override val payload: ByteArray) : AdbShellPacket(payload) {
-        override val id: Int = ID_STDOUT
-        override fun toString() = "STDOUT: ${String(payload)}"
-    }
 
-    class StdError(override val payload: ByteArray) : AdbShellPacket(payload) {
-        override val id: Int = ID_STDERR
-        override fun toString() = "STDERR: ${String(payload)}"
-    }
+    companion object {
+        const val ID_STDIN = 0
+        const val ID_STDOUT = 1
+        const val ID_STDERR = 2
+        const val ID_EXIT = 3
 
-    class Exit(override val payload: ByteArray) : AdbShellPacket(payload) {
-        override val id: Int = ID_EXIT
-        override fun toString() = "EXIT: ${payload[0]}"
+        @Suppress("unused")
+        const val ID_CLOSE_STDIN = 3
     }
 }
-
-class AdbShellResponse(
-    val output: String,
-    val errorOutput: String,
-    val exitCode: Int
-) {
-    val allOutput: String by lazy { "$output$errorOutput" }
-
-    override fun toString() = "Shell response ($exitCode):\n$allOutput"
-}

+ 26 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbShellPacket.kt

@@ -0,0 +1,26 @@
+package eu.mjdev.desktop.helpers.adb.helpers
+
+import eu.mjdev.desktop.helpers.adb.helpers.AdbShellStream.Companion.ID_EXIT
+import eu.mjdev.desktop.helpers.adb.helpers.AdbShellStream.Companion.ID_STDERR
+import eu.mjdev.desktop.helpers.adb.helpers.AdbShellStream.Companion.ID_STDOUT
+
+sealed class AdbShellPacket(
+    open val payload: ByteArray
+) {
+    abstract val id: Int
+
+    class StdOut(override val payload: ByteArray) : AdbShellPacket(payload) {
+        override val id: Int = ID_STDOUT
+        override fun toString() = "STDOUT: ${String(payload)}"
+    }
+
+    class StdError(override val payload: ByteArray) : AdbShellPacket(payload) {
+        override val id: Int = ID_STDERR
+        override fun toString() = "STDERR: ${String(payload)}"
+    }
+
+    class Exit(override val payload: ByteArray) : AdbShellPacket(payload) {
+        override val id: Int = ID_EXIT
+        override fun toString() = "EXIT: ${payload[0]}"
+    }
+}

+ 11 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbShellResponse.kt

@@ -0,0 +1,11 @@
+package eu.mjdev.desktop.helpers.adb.helpers
+
+class AdbShellResponse(
+    val output: String,
+    val errorOutput: String,
+    val exitCode: Int
+) {
+    val allOutput: String by lazy { "$output$errorOutput" }
+
+    override fun toString() = "Shell response ($exitCode):\n$allOutput"
+}

+ 4 - 21
src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbStream.kt

@@ -4,49 +4,36 @@ import okio.*
 import java.lang.Integer.min
 import java.nio.ByteBuffer
 
-interface AdbStream : AutoCloseable {
-    val source: BufferedSource
-
-    val sink: BufferedSink
-}
-
-internal class AdbStreamImpl internal constructor(
+class AdbStreamImpl internal constructor(
     private val messageQueue: AdbMessageQueue,
     private val adbWriter: AdbWriter,
     private val maxPayloadSize: Int,
     private val localId: Int,
     private val remoteId: Int
-) : AdbStream {
+) : IAdbStream {
     private var isClosed = false
 
     override val source = object : Source {
-
         private var message: AdbMessage? = null
         private var bytesRead = 0
 
         override fun read(sink: Buffer, byteCount: Long): Long {
             val message = message() ?: return -1
-
             val bytesRemaining = message.payloadLength - bytesRead
             val bytesToRead = min(byteCount.toInt(), bytesRemaining)
-
             sink.write(message.payload, bytesRead, bytesToRead)
-
             bytesRead += bytesToRead
-
             check(bytesRead <= message.payloadLength)
-
             if (bytesRead == message.payloadLength) {
                 this.message = null
                 adbWriter.writeOkay(localId, remoteId)
             }
-
             return bytesToRead.toLong()
         }
 
         private fun message(): AdbMessage? {
             message?.let { return it }
-            val nextMessage = nextMessage(Constants.CMD_WRTE)
+            val nextMessage = nextMessage(AdbConstants.CMD_WRTE)
             message = nextMessage
             bytesRead = 0
             return nextMessage
@@ -72,10 +59,8 @@ internal class AdbStreamImpl internal constructor(
         private fun writeToBuffer(source: BufferedSource, byteCount: Long): Int {
             val bytesToWrite = min(buffer.remaining(), byteCount.toInt())
             val bytesWritten = source.read(buffer.array(), buffer.position(), bytesToWrite)
-
             buffer.position(buffer.position() + bytesWritten)
             if (buffer.remaining() == 0) flush()
-
             return bytesWritten
         }
 
@@ -95,7 +80,7 @@ internal class AdbStreamImpl internal constructor(
     ): AdbMessage? {
         return try {
             messageQueue.take(localId, command)
-        } catch (e: IOException) {
+        } catch (_: IOException) {
             close()
             return null
         }
@@ -104,9 +89,7 @@ internal class AdbStreamImpl internal constructor(
     override fun close() {
         if (isClosed) return
         isClosed = true
-
         adbWriter.writeClose(localId, remoteId)
-
         messageQueue.stopListening(localId)
     }
 }

+ 1 - 1
src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbStreamClosed.kt

@@ -2,6 +2,6 @@ package eu.mjdev.desktop.helpers.adb.helpers
 
 import java.io.IOException
 
-internal class AdbStreamClosed(
+class AdbStreamClosed(
     localId: Int
 ) : IOException(String.format("ADB stream is closed for localId: %x", localId))

+ 15 - 24
src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbSync.kt

@@ -6,22 +6,8 @@ import okio.Source
 import java.io.IOException
 import java.nio.charset.StandardCharsets
 
-internal const val LIST = "LIST"
-internal const val RECV = "RECV"
-internal const val SEND = "SEND"
-internal const val STAT = "STAT"
-internal const val DATA = "DATA"
-internal const val DONE = "DONE"
-internal const val OKAY = "OKAY"
-internal const val QUIT = "QUIT"
-internal const val FAIL = "FAIL"
-
-internal val SYNC_IDS = setOf(LIST, RECV, SEND, STAT, DATA, DONE, OKAY, QUIT, FAIL)
-
-private class Packet(val id: String, val arg: Int)
-
 class AdbSyncStream(
-    private val stream: AdbStream
+    private val stream: IAdbStream
 ) : AutoCloseable {
     private val buffer = Buffer()
 
@@ -29,14 +15,11 @@ class AdbSyncStream(
     fun send(source: Source, remotePath: String, mode: Int, lastModifiedMs: Long) {
         val remote = "$remotePath,$mode"
         writePacket(SEND, remote.length)
-
         stream.sink.apply {
             writeString(remote, StandardCharsets.UTF_8)
             flush()
         }
-
         buffer.clear()
-
         while (true) {
             val read = source.read(buffer, 64_000)
             if (read == -1L) break
@@ -44,11 +27,8 @@ class AdbSyncStream(
             val sent = stream.sink.writeAll(buffer)
             check(read == sent)
         }
-
         writePacket(DONE, (lastModifiedMs / 1000).toInt())
-
         stream.sink.flush()
-
         val packet = readPacket()
         if (packet.id != OKAY) throw IOException("Unexpected sync packet id: ${packet.id}")
     }
@@ -60,9 +40,7 @@ class AdbSyncStream(
             writeString(remotePath, StandardCharsets.UTF_8)
             flush()
         }
-
         buffer.clear()
-
         while (true) {
             val packet = readPacket()
             if (packet.id == DONE) break
@@ -75,7 +53,6 @@ class AdbSyncStream(
             stream.source.readFully(buffer, chunkSize.toLong())
             buffer.readAll(sink)
         }
-
         sink.flush()
     }
 
@@ -97,4 +74,18 @@ class AdbSyncStream(
         writePacket(QUIT, 0)
         stream.close()
     }
+
+    companion object {
+        internal const val LIST = "LIST"
+        internal const val RECV = "RECV"
+        internal const val SEND = "SEND"
+        internal const val STAT = "STAT"
+        internal const val DATA = "DATA"
+        internal const val DONE = "DONE"
+        internal const val OKAY = "OKAY"
+        internal const val QUIT = "QUIT"
+        internal const val FAIL = "FAIL"
+
+        val SYNC_IDS = setOf(LIST, RECV, SEND, STAT, DATA, DONE, OKAY, QUIT, FAIL)
+    }
 }

+ 10 - 11
src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/AdbWriter.kt

@@ -9,16 +9,16 @@ internal class AdbWriter(sink: Sink) : AutoCloseable {
     private val bufferedSink = sink.buffer()
 
     fun writeConnect() = write(
-        Constants.CMD_CNXN,
-        Constants.CONNECT_VERSION,
-        Constants.CONNECT_MAXDATA,
-        Constants.CONNECT_PAYLOAD,
+        AdbConstants.CMD_CNXN,
+        AdbConstants.CONNECT_VERSION,
+        AdbConstants.CONNECT_MAXDATA,
+        AdbConstants.CONNECT_PAYLOAD,
         0,
-        Constants.CONNECT_PAYLOAD.size
+        AdbConstants.CONNECT_PAYLOAD.size
     )
 
     fun writeAuth(authType: Int, authPayload: ByteArray) = write(
-        Constants.CMD_AUTH,
+        AdbConstants.CMD_AUTH,
         authType,
         0,
         authPayload,
@@ -32,19 +32,19 @@ internal class AdbWriter(sink: Sink) : AutoCloseable {
         buffer.put(destinationBytes)
         buffer.put(0)
         val payload = buffer.array()
-        write(Constants.CMD_OPEN, localId, 0, payload, 0, payload.size)
+        write(AdbConstants.CMD_OPEN, localId, 0, payload, 0, payload.size)
     }
 
     fun writeWrite(localId: Int, remoteId: Int, payload: ByteArray, offset: Int, length: Int) {
-        write(Constants.CMD_WRTE, localId, remoteId, payload, offset, length)
+        write(AdbConstants.CMD_WRTE, localId, remoteId, payload, offset, length)
     }
 
     fun writeClose(localId: Int, remoteId: Int) {
-        write(Constants.CMD_CLSE, localId, remoteId, null, 0, 0)
+        write(AdbConstants.CMD_CLSE, localId, remoteId, null, 0, 0)
     }
 
     fun writeOkay(localId: Int, remoteId: Int) {
-        write(Constants.CMD_OKAY, localId, remoteId, null, 0, 0)
+        write(AdbConstants.CMD_OKAY, localId, remoteId, null, 0, 0)
     }
 
     fun write(
@@ -94,7 +94,6 @@ internal class AdbWriter(sink: Sink) : AutoCloseable {
     }
 
     companion object {
-
         private fun payloadChecksum(payload: ByteArray): Int {
             var checksum = 0
             for (byte in payload) {

+ 5 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/ConnectionString.kt

@@ -0,0 +1,5 @@
+package eu.mjdev.desktop.helpers.adb.helpers
+
+data class ConnectionString(
+    val features: Set<String>
+)

+ 9 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/IAdbStream.kt

@@ -0,0 +1,9 @@
+package eu.mjdev.desktop.helpers.adb.helpers
+
+import okio.BufferedSink
+import okio.BufferedSource
+
+interface IAdbStream : AutoCloseable {
+    val source: BufferedSource
+    val sink: BufferedSink
+}

+ 1 - 1
src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/Logging.kt

@@ -3,7 +3,7 @@ package eu.mjdev.desktop.helpers.adb.helpers
 // todo
 private const val ENABLED = true
 
-internal fun log(block: () -> String) {
+fun log(block: () -> String) {
     if (ENABLED) {
         println(block())
     }

+ 2 - 5
src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/MessageQueue.kt

@@ -6,7 +6,7 @@ import java.util.concurrent.ConcurrentHashMap
 import java.util.concurrent.ConcurrentLinkedQueue
 import java.util.concurrent.locks.ReentrantLock
 
-internal abstract class MessageQueue<V> {
+abstract class MessageQueue<V> {
     private val readLock = ReentrantLock()
     private val queueLock = ReentrantLock()
     private val queueCond = queueLock.newCondition()
@@ -39,6 +39,7 @@ internal abstract class MessageQueue<V> {
 
     @TestOnly
     fun ensureEmpty() {
+        @Suppress("UsePropertyAccessSyntax")
         check(queues.isEmpty()) { "Queues is not empty: ${queues.keys.map { String.format("%X", it) }}" }
         check(openStreams.isEmpty())
     }
@@ -63,17 +64,13 @@ internal abstract class MessageQueue<V> {
     private fun read() {
         val message = readMessage()
         val localId = getLocalId(message)
-
         if (isCloseCommand(message)) {
             openStreams.remove(localId)
             return
         }
-
         val streamQueues = queues[localId] ?: return
-
         val command = getCommand(message)
         val commandQueue = streamQueues.computeIfAbsent(command) { ConcurrentLinkedQueue() }
-
         commandQueue.add(message)
     }
 }

+ 6 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/adb/helpers/Packet.kt

@@ -0,0 +1,6 @@
+package eu.mjdev.desktop.helpers.adb.helpers
+
+class Packet(
+    val id: String,
+    val arg: Int
+)

+ 8 - 1
src/main/kotlin/eu/mjdev/desktop/helpers/system/Command.kt

@@ -21,7 +21,7 @@ class Command(
 
     fun execute(): String? = runCatching {
         StringJoiner(System.lineSeparator()).let { sj ->
-            val p = ProcessBuilder(_cmd).start()
+            val p = Runtime.getRuntime().exec(_cmd.toTypedArray())
             BufferedReader(InputStreamReader(p.inputStream))
                 .lines()
                 .iterator()
@@ -37,6 +37,13 @@ class Command(
         error = e
     }.getOrNull()
 
+    fun executeWithEnv(
+        env: Environment
+    ): Process = Runtime.getRuntime().exec(
+        _cmd.toTypedArray(),
+        env.toTypedArray()
+    )
+
     companion object {
         inline fun <reified T> String.to(): T? = runCatching {
             Gson().fromJson(this, T::class.java)

+ 10 - 0
src/main/kotlin/eu/mjdev/desktop/helpers/system/Environment.kt

@@ -0,0 +1,10 @@
+package eu.mjdev.desktop.helpers.system
+
+// todo from bash
+class Environment(
+    val data: String = Command("env").execute() ?: ""
+) : ArrayList<String>() {
+    init {
+        addAll(data.split("\n"))
+    }
+}

+ 1 - 3
src/main/kotlin/eu/mjdev/desktop/helpers/system/OsRelease.kt

@@ -1,11 +1,9 @@
 package eu.mjdev.desktop.helpers.system
 
-import eu.mjdev.desktop.helpers.system.UserDirs.Companion.XDG_DESKTOP_DIR
-import eu.mjdev.desktop.provider.DesktopProvider
 import java.io.File
 
+@Suppress("unused")
 class OsRelease(
-    api: DesktopProvider,
     configFile: File = File("/etc/os-release"),
     configFileContent: List<String> = configFile.readLines().filter {
         (!it.startsWith("#")) && (!it.trim().isEmpty())

+ 4 - 6
src/main/kotlin/eu/mjdev/desktop/helpers/system/UserDirs.kt

@@ -1,19 +1,17 @@
 package eu.mjdev.desktop.helpers.system
 
-import eu.mjdev.desktop.provider.DesktopProvider
 import java.io.File
 
 @Suppress("unused")
 class UserDirs(
-    api: DesktopProvider,
-    homeDir: File = api.homeDir,
-    configFile: File = File(api.homeDir, "/.config/user-dirs.dirs"),
-    configFileContent: List<String> = configFile.readLines().filter {
+    homeDir: File,
+    configFile: File = File(homeDir, "/.config/user-dirs.dirs"),
+    configFileContent: List<String> = runCatching {  configFile.readLines().filter {
         (!it.startsWith("#")) && (!it.trim().isEmpty())
     }.map {
         it.replace("\$HOME", homeDir.absolutePath)
             .replace("\"", "")
-    }
+    } }.getOrElse { emptyList() }
 ) : HashMap<String, File>() {
     init {
         configFileContent.map {

+ 27 - 18
src/main/kotlin/eu/mjdev/desktop/provider/DesktopProvider.kt

@@ -1,7 +1,6 @@
 package eu.mjdev.desktop.provider
 
 import androidx.compose.runtime.*
-import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.dp
 import coil3.ImageLoader
@@ -14,7 +13,6 @@ import eu.mjdev.desktop.helpers.internal.Palette
 import eu.mjdev.desktop.helpers.managers.*
 import eu.mjdev.desktop.helpers.system.Command
 import eu.mjdev.desktop.helpers.system.OsRelease
-import eu.mjdev.desktop.helpers.system.UserDirs
 import eu.mjdev.desktop.provider.AIProvider.AiPluginGemini
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
@@ -34,21 +32,21 @@ class DesktopProvider(
     val ai: AIProvider = AIProvider(scope, AiPluginGemini(scope)),
     val windows: WindowsManager = WindowsManager(),
     val gnome: GnomeManager = GnomeManager(),
-    val currentUser: User = User.load() // todo manager
+    val allUsers: List<User> = User.allUsers,
 ) {
-    val homeDir
-        get() = File("/home/${currentUser.name}")
-
-    val userDirs = UserDirs(this)
-
-    val osDetails = OsRelease(this)
-
+    val osDetails = OsRelease()
     val palette: Palette = Palette(scope, currentUser.theme.backgroundColor)
-
     val controlCenterPagesState: MutableState<List<ControlCenterPage>> = mutableStateOf(CONTROL_CENTER_PAGES)
 
-    val machineName
-        get() = Command("hostname").execute()
+    // todo state
+    val currentUser: User
+        get() = allUsers.filter { u -> u.isLoggedIn == true }.firstOrNull() ?: User.Nobody
+
+    val homeDir : File
+        get() = currentUser.homeDir
+
+    val machineName : String
+        get() = Command("hostname").execute()?.trim().orEmpty()
 
 //    private val engine: ScriptEngine by lazy {
 //        scriptManager.getEngineByName("JavaScript").apply {
@@ -57,11 +55,16 @@ class DesktopProvider(
 //    }
 
     val adbHandler = adbDevicesHandler(
-        coroutineScope = scope
-    ) { device ->
-        println("Device discovered : $device")
-        // when device connected
-    }
+        coroutineScope = scope,
+        onAdded = { device ->
+            println("Device discovered : $device")
+            // when device connected
+        },
+        onRemoved = { device ->
+            println("Device removed : $device")
+            // when device removed
+        }
+    )
 
     val containerSize: DpSize by lazy {
         Toolkit.getDefaultToolkit().screenSize.let {
@@ -88,6 +91,8 @@ class DesktopProvider(
     init {
         currentUser.theme.controlCenterExpandedWidth = containerSize.width.div(4)
         mounts.init()
+
+        println(allUsers)
     }
 
     fun dispose() {
@@ -144,6 +149,10 @@ class DesktopProvider(
     ) {
         val scope
             get() = api.scope
+
+        val currentUser
+            get() = api.currentUser
+
         val backgroundColorState
             get() = api.palette.backgroundColorState
         val backgroundColor