@@ -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
|
||||
|
||||
|
||||
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user