feat: confirmation before key reset; key export

close #32
This commit is contained in:
Miuzarte
2026-07-07 10:51:52 +08:00
parent e1b01a96b0
commit 9aacf65666
5 changed files with 253 additions and 23 deletions

2
.gitignore vendored
View File

@@ -35,7 +35,7 @@ google-services.json
*.hprof
.claude/
.kiro/
AGENTS.md
.kotlin/

View File

@@ -51,6 +51,18 @@ internal object DirectAdbTransport {
val privateKey: PrivateKey get() = keys().first
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
var keyName: String = AppSettings.ADB_KEY_NAME.defaultValue

View File

@@ -38,6 +38,7 @@ import io.github.miuzarte.scrcpyforandroid.scaffolds.SuperTextField
import io.github.miuzarte.scrcpyforandroid.scrcpy.Scrcpy
import io.github.miuzarte.scrcpyforandroid.services.AppRuntime
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.FullscreenVirtualButtonDock
import io.github.miuzarte.scrcpyforandroid.storage.Settings
@@ -47,6 +48,10 @@ import io.github.miuzarte.scrcpyforandroid.ui.*
import kotlinx.coroutines.*
import top.yukonga.miuix.kmp.basic.*
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.OverlayDropdownPreference
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) +
when (val state = updateState) {
AppUpdateChecker.State.Idle -> ""
@@ -935,26 +993,7 @@ fun SettingsPage(
IconButton(
onClick = {
haptic.contextClick()
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,
)
}
}
showResetKeyConfirm = true
},
) {
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(
onClick = {
haptic.contextClick()
@@ -974,7 +1024,7 @@ fun SettingsPage(
},
) {
Icon(
imageVector = Icons.Rounded.FileOpen,
imageVector = MiuixIcons.Download,
contentDescription = stringResource(R.string.cd_select_file),
)
}
@@ -1011,6 +1061,17 @@ fun SettingsPage(
modifier = Modifier.fillMaxWidth(),
trailingIcon = {
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(
onClick = {
haptic.contextClick()
@@ -1018,7 +1079,7 @@ fun SettingsPage(
},
) {
Icon(
imageVector = Icons.Rounded.FileOpen,
imageVector = MiuixIcons.Download,
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(),
)
}
}
}

View File

@@ -228,6 +228,17 @@
<string name="pref_adb_key_import_failed">ADB 密钥导入失败:%1$s</string>
<string name="pref_adb_key_remove">移除</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_summary_auto_discovery">打开配对弹窗后自动搜索可用配对端口</string>
<string name="pref_title_auto_reconnect">自动重连已配对设备</string>

View File

@@ -228,6 +228,17 @@
<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_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_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>