使用Compose Desktop开发一款Adb文件管理器

759 阅读3分钟

最近一直在学习Compose知识,花了些时间用Compose Desktop写了一款公司内部使用的小工具,文件管理器只是其中一项功能。我将其分离出来,先看最终实现效果

微信图片_20231228194051.png

效果是不是改可以,对比Android Studio自带的Device Explorer优化了导入功能,支持多文件拖拽文件导入,增加了直接编辑保存文本功能。

实现思路

所有实现操作均通过Adb命令,通过代码Runtime.getRuntime().exec()执行

一、文件列表查询

C:\Users\Administrator>adb shell ls -l -p / | sort
dr-xr-xr-x   17 root   root         0 1970-01-01 08:00 sys/
dr-xr-xr-x 1004 root   root         0 1970-01-01 08:00 proc/
drwx--x---    4 shell  everybody   80 1972-01-02 15:51 storage/
drwxr-xr-x    1 root   root      1024 2023-11-23 00:36 system/
drwxr-xr-x    2 root   root        27 2009-01-01 08:00 acct/
drwxr-xr-x    2 root   root        27 2009-01-01 08:00 debug_ramdisk/
drwxr-xr-x    2 root   root        27 2009-01-01 08:00 oem/
lrw-r--r--    1 root   root        17 2009-01-01 08:00 d -> /sys/kernel/debug
lrw-r--r--    1 root   root        21 2009-01-01 08:00 sdcard -> /storage/self/primary
...
total 86

由于打印文件太长,我这里做了部分删减,下面我来简单分析这组信息,如:dr-xr-xr-x,首字母d代表他是一个目录,如果l代表是一个链接文件,可以理解为windows快捷方式,如lrw-r--r-- 1 root root 17 2009-01-01 08:00 d -> /sys/kernel/debug,实际跳转的路径是/sys/kernel/debug,而不是d,这个地方要处理如果首字母是-测代表是是一个文件,通过以上命令就实现了文件管理器核心的功能文件显示

二、文件编辑

编辑.png 如图,我进入到了/Storage/emulated/0/Download路径下,我要对以上文件编辑,那么我知道该文件的路径是:/Storage/emulated/0/Download/aa.txt,通过

C:\Users\Administrator>adb shell cat /storage/emulated/0/Download/aa.txt
123444ww454545

我们可以得到文件的内容是123444ww454545,然后我们用代码吧UI画出来

@OptIn(ExperimentalFoundationApi::class)
@Composable
fun FileEditUI(
    adbDevicePoller: AdbDevicePoller,
    scope: CoroutineScope,
    dirList: List<String>,
    currentFileName: String,
    editDialog: Boolean,
    editString: String,
    editCommandCallback: () -> Unit
) {
    var edit by remember { mutableStateOf("") }
    edit = editString
    if (editDialog) {
        val bringIntoViewRequester = remember { BringIntoViewRequester() }
        val scrollState = rememberScrollState()
        Window(state = rememberWindowState(width = 600.dp, height = 800.dp),
            title = "编辑", onCloseRequest = {
                editCommandCallback.invoke()
            }) {
            Box {
                BasicTextField(value = edit, onValueChange = {
                    edit = it
                }, modifier = Modifier
                    .fillMaxSize()
                    .verticalScroll(scrollState)
                    .bringIntoViewRequester(bringIntoViewRequester)
                    .onFocusChanged {
                        if (it.isFocused) {
                            scope.launch {
                                bringIntoViewRequester.bringIntoView()
                            }
                        }
                    }

                )
                VerticalScrollbar(
                    modifier = Modifier.align(Alignment.CenterEnd),
                    adapter = rememberScrollbarAdapter(scrollState)
                )
                Button(
                    modifier = Modifier.padding(bottom = 20.dp, end = 30.dp)
                        .wrapContentSize()
                        .align(Alignment.BottomEnd),
                    onClick = {
                        val escapedJsonString = edit.replace(""", "\"")
                        scope.launch {
                            val dir = dirList.joinToString("/")
                            adbDevicePoller.exec(
                                """shell "cat > /${dir}/${currentFileName}" <<< '$escapedJsonString'"""
                            ) {
                                editCommandCallback.invoke()
                            }
                        }
                    }) {
                    Text("保存")
                }
            }
        }
    }
}

编辑2.png 通过以上代码可以看到,我这里点击保存执行了

adbDevicePoller.exec("""shell "cat > /${dir}/${currentFileName}" <<< '$escapedJsonString'""")

比如我想对刚刚的文件重新写入个0进去,只需执行

adb shell "cat > /Storage/emulated/0/Download/aa.txt" <<< '0'"

三、文件拖拽导入实现

data class DropBoundsBean(
    var x: Float = 0f,
    var y: Float = 0f,
    var width: Int = 600.dp.value.toInt(),
    var height: Int = 480.dp.value.toInt(),
)

@Composable
fun DropBoxPanel(
    modifier: Modifier,
    window: ComposeWindow,
    component: JPanel = JPanel(),
    onFileDrop: (List<String>) -> Unit
) {

    val dropBoundsBean = remember {
        mutableStateOf(DropBoundsBean())
    }

    Box(
        modifier = modifier.onPlaced {
            dropBoundsBean.value = DropBoundsBean(
                x = it.positionInWindow().x,
                y = it.positionInWindow().y,
                width = it.size.width,
                height = it.size.height
            )
        }) {
        LaunchedEffect(true) {
            component.setBounds(
                dropBoundsBean.value.x.roundToInt(),
                dropBoundsBean.value.y.roundToInt(),
                dropBoundsBean.value.width,
                dropBoundsBean.value.height
            )
            window.contentPane.add(component)

            object : DropTarget(component, object : DropTargetAdapter() {
                override fun drop(event: DropTargetDropEvent) {
                    event.acceptDrop(DnDConstants.ACTION_REFERENCE)
                    val dataFlavors = event.transferable.transferDataFlavors
                    println(dataFlavors)
                    dataFlavors.forEach {
                        if (it == DataFlavor.javaFileListFlavor) {
                            val list = event.transferable.getTransferData(it) as List<*>

                            val pathList = mutableListOf<String>()
                            list.forEach { filePath ->
                                pathList.add(filePath.toString())
                            }
                            onFileDrop(pathList)
                        }
                    }
                    event.dropComplete(true)
                }
            }) {
                //  dragExit
            }
        }

        SideEffect {
            component.setBounds(
                dropBoundsBean.value.x.roundToInt(),
                dropBoundsBean.value.y.roundToInt(),
                dropBoundsBean.value.width,
                dropBoundsBean.value.height
            )
        }

        DisposableEffect(true) {
            onDispose {
                window.contentPane.remove(component)
            }
        }
    }
}

四、其他

// 文件导入
adb push [targetPath] [originPath]
// 文件导出
adb pull [originPath] [targetPath]
// 文件夹创建
adb shell mkdir [originPath/dirName]

总结

本文通过对adb命令的应用,列举了在开发遇到的一些痛点,提前总结出来,文章中所有代码已经开源到Github中,第一次分享,如果对文章有疑问或者存在错误,欢迎在评论区探讨!