2
.gitignore
vendored
2
.gitignore
vendored
@@ -35,7 +35,7 @@ google-services.json
|
|||||||
*.hprof
|
*.hprof
|
||||||
|
|
||||||
.claude/
|
.claude/
|
||||||
.kiro/
|
AGENTS.md
|
||||||
|
|
||||||
.kotlin/
|
.kotlin/
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,18 @@ internal object DirectAdbTransport {
|
|||||||
val privateKey: PrivateKey get() = keys().first
|
val privateKey: PrivateKey get() = keys().first
|
||||||
val publicKeyX509: ByteArray get() = keys().second
|
val publicKeyX509: ByteArray get() = keys().second
|
||||||
|
|
||||||
|
fun getPrivateKeyPem(): String? = cachedKeys?.let { (priv, _) ->
|
||||||
|
"-----BEGIN PRIVATE KEY-----\n" +
|
||||||
|
Base64.encodeToString(priv.encoded, Base64.DEFAULT) +
|
||||||
|
"-----END PRIVATE KEY-----"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPublicKeyPem(): String? = cachedKeys?.let { (_, pub) ->
|
||||||
|
"-----BEGIN PUBLIC KEY-----\n" +
|
||||||
|
Base64.encodeToString(pub, Base64.DEFAULT) +
|
||||||
|
"-----END PUBLIC KEY-----"
|
||||||
|
}
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
var keyName: String = AppSettings.ADB_KEY_NAME.defaultValue
|
var keyName: String = AppSettings.ADB_KEY_NAME.defaultValue
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperTextField
|
|||||||
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
|
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
|
||||||
import io.github.miuzarte.scrcpyforandroid.services.AppRuntime
|
import io.github.miuzarte.scrcpyforandroid.services.AppRuntime
|
||||||
import io.github.miuzarte.scrcpyforandroid.services.AppUpdateChecker
|
import io.github.miuzarte.scrcpyforandroid.services.AppUpdateChecker
|
||||||
|
import io.github.miuzarte.scrcpyforandroid.services.LocalInputService
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
|
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings.FullscreenVirtualButtonDock
|
import io.github.miuzarte.scrcpyforandroid.storage.AppSettings.FullscreenVirtualButtonDock
|
||||||
import io.github.miuzarte.scrcpyforandroid.storage.Settings
|
import io.github.miuzarte.scrcpyforandroid.storage.Settings
|
||||||
@@ -47,6 +48,10 @@ import io.github.miuzarte.scrcpyforandroid.ui.*
|
|||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import top.yukonga.miuix.kmp.basic.*
|
import top.yukonga.miuix.kmp.basic.*
|
||||||
import top.yukonga.miuix.kmp.blur.layerBackdrop
|
import top.yukonga.miuix.kmp.blur.layerBackdrop
|
||||||
|
import top.yukonga.miuix.kmp.icon.MiuixIcons
|
||||||
|
import top.yukonga.miuix.kmp.icon.extended.Download
|
||||||
|
import top.yukonga.miuix.kmp.icon.extended.Share
|
||||||
|
import top.yukonga.miuix.kmp.overlay.OverlayDialog
|
||||||
import top.yukonga.miuix.kmp.preference.ArrowPreference
|
import top.yukonga.miuix.kmp.preference.ArrowPreference
|
||||||
import top.yukonga.miuix.kmp.preference.OverlayDropdownPreference
|
import top.yukonga.miuix.kmp.preference.OverlayDropdownPreference
|
||||||
import top.yukonga.miuix.kmp.preference.SwitchPreference
|
import top.yukonga.miuix.kmp.preference.SwitchPreference
|
||||||
@@ -237,6 +242,59 @@ fun SettingsPage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var exportingKey by rememberSaveable { mutableStateOf<String?>(null) }
|
||||||
|
var showResetKeyConfirm by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val exportPrivateKeyPicker = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.CreateDocument("*/*"),
|
||||||
|
) { uri: Uri? ->
|
||||||
|
if (uri == null) return@rememberLauncherForActivityResult
|
||||||
|
scope.launch {
|
||||||
|
runCatching {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
DirectAdbTransport.privateKey
|
||||||
|
val pem = DirectAdbTransport.getPrivateKeyPem()
|
||||||
|
?: error("Private key not available")
|
||||||
|
context.contentResolver.openOutputStream(uri)?.use { os ->
|
||||||
|
os.write(pem.toByteArray())
|
||||||
|
} ?: error("Cannot open output stream")
|
||||||
|
}
|
||||||
|
}.onSuccess {
|
||||||
|
AppRuntime.snackbar(R.string.pref_adb_key_exported_file)
|
||||||
|
}.onFailure { e ->
|
||||||
|
AppRuntime.snackbar(
|
||||||
|
R.string.pref_adb_key_export_failed,
|
||||||
|
e.message ?: e.javaClass.simpleName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val exportPublicKeyPicker = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.CreateDocument("*/*"),
|
||||||
|
) { uri: Uri? ->
|
||||||
|
if (uri == null) return@rememberLauncherForActivityResult
|
||||||
|
scope.launch {
|
||||||
|
runCatching {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
DirectAdbTransport.publicKeyX509
|
||||||
|
val pem = DirectAdbTransport.getPublicKeyPem()
|
||||||
|
?: error("Public key not available")
|
||||||
|
context.contentResolver.openOutputStream(uri)?.use { os ->
|
||||||
|
os.write(pem.toByteArray())
|
||||||
|
} ?: error("Cannot open output stream")
|
||||||
|
}
|
||||||
|
}.onSuccess {
|
||||||
|
AppRuntime.snackbar(R.string.pref_adb_key_exported_file)
|
||||||
|
}.onFailure { e ->
|
||||||
|
AppRuntime.snackbar(
|
||||||
|
R.string.pref_adb_key_export_failed,
|
||||||
|
e.message ?: e.javaClass.simpleName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val updateSummary = stringResource(R.string.pref_update_current, BuildConfig.VERSION_NAME) +
|
val updateSummary = stringResource(R.string.pref_update_current, BuildConfig.VERSION_NAME) +
|
||||||
when (val state = updateState) {
|
when (val state = updateState) {
|
||||||
AppUpdateChecker.State.Idle -> ""
|
AppUpdateChecker.State.Idle -> ""
|
||||||
@@ -935,26 +993,7 @@ fun SettingsPage(
|
|||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
haptic.contextClick()
|
haptic.contextClick()
|
||||||
scope.launch {
|
showResetKeyConfirm = true
|
||||||
runCatching {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
DirectAdbTransport.resetKeys()
|
|
||||||
}
|
|
||||||
}.onSuccess {
|
|
||||||
AppRuntime.snackbar(
|
|
||||||
if (it.removedImportedKey)
|
|
||||||
R.string.pref_adb_key_removed
|
|
||||||
else
|
|
||||||
R.string.pref_adb_key_reset,
|
|
||||||
it.fingerprint,
|
|
||||||
)
|
|
||||||
}.onFailure { e ->
|
|
||||||
AppRuntime.snackbar(
|
|
||||||
R.string.pref_adb_key_import_failed,
|
|
||||||
e.message ?: e.javaClass.simpleName,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -967,6 +1006,17 @@ fun SettingsPage(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
haptic.contextClick()
|
||||||
|
exportingKey = "private"
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = MiuixIcons.Share,
|
||||||
|
contentDescription = stringResource(R.string.cd_export_adb_key),
|
||||||
|
)
|
||||||
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
haptic.contextClick()
|
haptic.contextClick()
|
||||||
@@ -974,7 +1024,7 @@ fun SettingsPage(
|
|||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.FileOpen,
|
imageVector = MiuixIcons.Download,
|
||||||
contentDescription = stringResource(R.string.cd_select_file),
|
contentDescription = stringResource(R.string.cd_select_file),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1011,6 +1061,17 @@ fun SettingsPage(
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
Row(modifier = Modifier.padding(end = UiSpacing.Medium)) {
|
Row(modifier = Modifier.padding(end = UiSpacing.Medium)) {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
haptic.contextClick()
|
||||||
|
exportingKey = "public"
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = MiuixIcons.Share,
|
||||||
|
contentDescription = stringResource(R.string.cd_export_adb_key),
|
||||||
|
)
|
||||||
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
haptic.contextClick()
|
haptic.contextClick()
|
||||||
@@ -1018,7 +1079,7 @@ fun SettingsPage(
|
|||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.FileOpen,
|
imageVector = MiuixIcons.Download,
|
||||||
contentDescription = stringResource(R.string.cd_select_file),
|
contentDescription = stringResource(R.string.cd_select_file),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1192,4 +1253,139 @@ fun SettingsPage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ExportAdbKeyDialog(
|
||||||
|
show = exportingKey != null,
|
||||||
|
isPrivateKey = exportingKey == "private",
|
||||||
|
onDismissRequest = { exportingKey = null },
|
||||||
|
onDismissFinished = { exportingKey = null },
|
||||||
|
onExportToClipboard = {
|
||||||
|
val target = exportingKey
|
||||||
|
scope.launch {
|
||||||
|
runCatching {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
if (target == "private") {
|
||||||
|
DirectAdbTransport.privateKey
|
||||||
|
DirectAdbTransport.getPrivateKeyPem()
|
||||||
|
} else {
|
||||||
|
DirectAdbTransport.publicKeyX509
|
||||||
|
DirectAdbTransport.getPublicKeyPem()
|
||||||
|
} ?: error("Key not available")
|
||||||
|
}
|
||||||
|
}.onSuccess { pem ->
|
||||||
|
LocalInputService.setClipboardText(context, pem)
|
||||||
|
AppRuntime.snackbar(R.string.pref_adb_key_exported_clipboard)
|
||||||
|
}.onFailure { e ->
|
||||||
|
AppRuntime.snackbar(
|
||||||
|
R.string.pref_adb_key_export_failed,
|
||||||
|
e.message ?: e.javaClass.simpleName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onExportToFile = {
|
||||||
|
if (exportingKey == "private") {
|
||||||
|
exportPrivateKeyPicker.launch("adbkey")
|
||||||
|
} else {
|
||||||
|
exportPublicKeyPicker.launch("adbkey.pub")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
OverlayDialog(
|
||||||
|
show = showResetKeyConfirm,
|
||||||
|
title = stringResource(R.string.pref_adb_key_reset_confirm_title),
|
||||||
|
summary = stringResource(R.string.pref_adb_key_reset_confirm_summary),
|
||||||
|
defaultWindowInsetsPadding = false,
|
||||||
|
onDismissRequest = { showResetKeyConfirm = false },
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(UiSpacing.ContentHorizontal),
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
text = stringResource(R.string.button_cancel),
|
||||||
|
onClick = {
|
||||||
|
haptic.contextClick()
|
||||||
|
showResetKeyConfirm = false
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
TextButton(
|
||||||
|
text = stringResource(R.string.button_confirm),
|
||||||
|
onClick = {
|
||||||
|
haptic.confirm()
|
||||||
|
showResetKeyConfirm = false
|
||||||
|
scope.launch {
|
||||||
|
runCatching {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
DirectAdbTransport.resetKeys()
|
||||||
|
}
|
||||||
|
}.onSuccess {
|
||||||
|
AppRuntime.snackbar(
|
||||||
|
if (it.removedImportedKey)
|
||||||
|
R.string.pref_adb_key_removed
|
||||||
|
else
|
||||||
|
R.string.pref_adb_key_reset,
|
||||||
|
it.fingerprint,
|
||||||
|
)
|
||||||
|
}.onFailure { e ->
|
||||||
|
AppRuntime.snackbar(
|
||||||
|
R.string.pref_adb_key_import_failed,
|
||||||
|
e.message ?: e.javaClass.simpleName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
colors = ButtonDefaults.textButtonColorsPrimary(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ExportAdbKeyDialog(
|
||||||
|
show: Boolean,
|
||||||
|
isPrivateKey: Boolean,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onDismissFinished: () -> Unit,
|
||||||
|
onExportToClipboard: () -> Unit,
|
||||||
|
onExportToFile: () -> Unit,
|
||||||
|
) {
|
||||||
|
val haptic = LocalHapticFeedback.current
|
||||||
|
OverlayDialog(
|
||||||
|
show = show,
|
||||||
|
title = stringResource(R.string.pref_adb_key_export),
|
||||||
|
summary = stringResource(
|
||||||
|
if (isPrivateKey) R.string.pref_adb_key_export_private_summary
|
||||||
|
else R.string.pref_adb_key_export_public_summary,
|
||||||
|
),
|
||||||
|
defaultWindowInsetsPadding = false,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
onDismissFinished = onDismissFinished,
|
||||||
|
) {
|
||||||
|
Spacer(Modifier.height(UiSpacing.ContentVertical * 2))
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(UiSpacing.ContentVertical),
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
text = stringResource(R.string.pref_adb_key_export_to_clipboard),
|
||||||
|
onClick = {
|
||||||
|
haptic.contextClick()
|
||||||
|
onExportToClipboard()
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
TextButton(
|
||||||
|
text = stringResource(R.string.pref_adb_key_export_to_file),
|
||||||
|
onClick = {
|
||||||
|
haptic.contextClick()
|
||||||
|
onExportToFile()
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -228,6 +228,17 @@
|
|||||||
<string name="pref_adb_key_import_failed">ADB 密钥导入失败:%1$s</string>
|
<string name="pref_adb_key_import_failed">ADB 密钥导入失败:%1$s</string>
|
||||||
<string name="pref_adb_key_remove">移除</string>
|
<string name="pref_adb_key_remove">移除</string>
|
||||||
<string name="pref_adb_key_regenerate">重新生成</string>
|
<string name="pref_adb_key_regenerate">重新生成</string>
|
||||||
|
<string name="pref_adb_key_export">导出</string>
|
||||||
|
<string name="pref_adb_key_export_private_summary">导出私钥到...</string>
|
||||||
|
<string name="pref_adb_key_export_public_summary">导出公钥到...</string>
|
||||||
|
<string name="pref_adb_key_export_to_clipboard">剪贴板</string>
|
||||||
|
<string name="pref_adb_key_export_to_file">文件</string>
|
||||||
|
<string name="pref_adb_key_exported_clipboard">已导出到剪贴板</string>
|
||||||
|
<string name="pref_adb_key_exported_file">已导出到文件</string>
|
||||||
|
<string name="pref_adb_key_export_failed">导出失败:%1$s</string>
|
||||||
|
<string name="cd_export_adb_key">导出</string>
|
||||||
|
<string name="pref_adb_key_reset_confirm_title">重置 ADB 密钥</string>
|
||||||
|
<string name="pref_adb_key_reset_confirm_summary">重置后,所有已授权的设备都需要重新授权。\n是否继续?</string>
|
||||||
<string name="pref_title_auto_discovery">配对时自动启用发现服务</string>
|
<string name="pref_title_auto_discovery">配对时自动启用发现服务</string>
|
||||||
<string name="pref_summary_auto_discovery">打开配对弹窗后自动搜索可用配对端口</string>
|
<string name="pref_summary_auto_discovery">打开配对弹窗后自动搜索可用配对端口</string>
|
||||||
<string name="pref_title_auto_reconnect">自动重连已配对设备</string>
|
<string name="pref_title_auto_reconnect">自动重连已配对设备</string>
|
||||||
|
|||||||
@@ -228,6 +228,17 @@
|
|||||||
<string name="pref_adb_key_import_failed">ADB key import failed: %1$s</string>
|
<string name="pref_adb_key_import_failed">ADB key import failed: %1$s</string>
|
||||||
<string name="pref_adb_key_remove">Remove</string>
|
<string name="pref_adb_key_remove">Remove</string>
|
||||||
<string name="pref_adb_key_regenerate">Regenerate</string>
|
<string name="pref_adb_key_regenerate">Regenerate</string>
|
||||||
|
<string name="pref_adb_key_export">Export</string>
|
||||||
|
<string name="pref_adb_key_export_private_summary">Export private key to...</string>
|
||||||
|
<string name="pref_adb_key_export_public_summary">Export public key to...</string>
|
||||||
|
<string name="pref_adb_key_export_to_clipboard">Clipboard</string>
|
||||||
|
<string name="pref_adb_key_export_to_file">File</string>
|
||||||
|
<string name="pref_adb_key_exported_clipboard">Exported to clipboard</string>
|
||||||
|
<string name="pref_adb_key_exported_file">Exported to file</string>
|
||||||
|
<string name="pref_adb_key_export_failed">Export failed: %1$s</string>
|
||||||
|
<string name="cd_export_adb_key">Export</string>
|
||||||
|
<string name="pref_adb_key_reset_confirm_title">Reset ADB key</string>
|
||||||
|
<string name="pref_adb_key_reset_confirm_summary">After resetting, all previously authorized devices will need to be re-authorized.\nContinue?</string>
|
||||||
<string name="pref_title_auto_discovery">Auto enable discovery service when pairing</string>
|
<string name="pref_title_auto_discovery">Auto enable discovery service when pairing</string>
|
||||||
<string name="pref_summary_auto_discovery">Automatically search for available pairing ports after opening the pairing dialog</string>
|
<string name="pref_summary_auto_discovery">Automatically search for available pairing ports after opening the pairing dialog</string>
|
||||||
<string name="pref_title_auto_reconnect">Auto reconnect paired devices</string>
|
<string name="pref_title_auto_reconnect">Auto reconnect paired devices</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user