fix: p12 import — pick file first, then password (matches normal usage)

The Choose .p12 button was gated on a non-blank password, which forced users
to type the password before selecting the file. Now the file picker is always
available; after selection the filename is shown, then the password field and
an Import button appear.
This commit is contained in:
atsunatsu committed 2026-09-25 22:56:36 +08:00
1 parent 797b1c19f7
commit e19f098aa5
3 files changed
+46 -18

No files matched your search

@@ -202,6 +202,7 @@
<string name="prefs_lotw_upload_cert_hint">导入你的 TrustedQSL .p12 证书(与 TQSL 同款)用于签名上传通联。密码仅加密保存在手机本地。</string> <string name="prefs_lotw_upload_cert_hint">导入你的 TrustedQSL .p12 证书(与 TQSL 同款)用于签名上传通联。密码仅加密保存在手机本地。</string>
<string name="prefs_lotw_upload_password">证书密码</string> <string name="prefs_lotw_upload_password">证书密码</string>
<string name="prefs_lotw_upload_import">选择 .p12 文件</string> <string name="prefs_lotw_upload_import">选择 .p12 文件</string>
<string name="prefs_lotw_upload_import_confirm">导入</string>
<string name="prefs_lotw_upload_cert_info">%1$s · DXCC %2$d · 有效期至 %3$s</string> <string name="prefs_lotw_upload_cert_info">%1$s · DXCC %2$d · 有效期至 %3$s</string>
<string name="prefs_lotw_upload_remove">移除证书</string> <string name="prefs_lotw_upload_remove">移除证书</string>
<string name="prefs_lotw_upload_station_title">台址</string> <string name="prefs_lotw_upload_station_title">台址</string>
@@ -233,6 +233,7 @@
<string name="prefs_lotw_upload_cert_hint">Import your TrustedQSL .p12 certificate (same one you use in TQSL) to sign and upload QSOs. The password is stored encrypted on this device only.</string> <string name="prefs_lotw_upload_cert_hint">Import your TrustedQSL .p12 certificate (same one you use in TQSL) to sign and upload QSOs. The password is stored encrypted on this device only.</string>
<string name="prefs_lotw_upload_password">Certificate password</string> <string name="prefs_lotw_upload_password">Certificate password</string>
<string name="prefs_lotw_upload_import">Choose .p12 file</string> <string name="prefs_lotw_upload_import">Choose .p12 file</string>
<string name="prefs_lotw_upload_import_confirm">Import</string>
<string name="prefs_lotw_upload_cert_info">%1$s · DXCC %2$d · expires %3$s</string> <string name="prefs_lotw_upload_cert_info">%1$s · DXCC %2$d · expires %3$s</string>
<string name="prefs_lotw_upload_remove">Remove certificate</string> <string name="prefs_lotw_upload_remove">Remove certificate</string>
<string name="prefs_lotw_upload_station_title">Station location</string> <string name="prefs_lotw_upload_station_title">Station location</string>
@@ -9,6 +9,8 @@
*/ */
package com.rtbishop.look4sat.feature.settings package com.rtbishop.look4sat.feature.settings
import android.net.Uri
import android.provider.OpenableColumns
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -39,6 +41,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.rtbishop.look4sat.core.domain.repository.LoTWCertificate import com.rtbishop.look4sat.core.domain.repository.LoTWCertificate
@@ -82,24 +85,24 @@ fun LoTWUploadConfigDialog(
onSaveStation: (LoTWStation) -> Unit onSaveStation: (LoTWStation) -> Unit
) { ) {
var password by remember { mutableStateOf("") } var password by remember { mutableStateOf("") }
var selectedFile by remember { mutableStateOf<Uri?>(null) }
var grid by remember { mutableStateOf(station?.grid.orEmpty()) } var grid by remember { mutableStateOf(station?.grid.orEmpty()) }
var cqZone by remember { mutableStateOf(station?.cqZone.orEmpty()) } var cqZone by remember { mutableStateOf(station?.cqZone.orEmpty()) }
var ituZone by remember { mutableStateOf(station?.ituZone.orEmpty()) } var ituZone by remember { mutableStateOf(station?.ituZone.orEmpty()) }
var iota by remember { mutableStateOf(station?.iota.orEmpty()) } var iota by remember { mutableStateOf(station?.iota.orEmpty()) }
val context = LocalContext.current val context = LocalContext.current
// Pick the file first, then ask for the password — matches normal usage.
val filePicker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> val filePicker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
if (uri != null && password.isNotBlank()) { if (uri != null) selectedFile = uri
val bytes = runCatching {
context.contentResolver.openInputStream(uri)?.use { it.readBytes() }
}.getOrNull()
if (bytes != null && bytes.isNotEmpty()) {
onImport(bytes, password.toCharArray())
password = ""
}
}
} }
fun displayName(uri: Uri): String = runCatching {
context.contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) cursor.getString(0) else null
}
}.getOrNull() ?: uri.lastPathSegment.orEmpty()
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text(stringResource(R.string.prefs_lotw_upload_title)) }, title = { Text(stringResource(R.string.prefs_lotw_upload_title)) },
@@ -113,19 +116,42 @@ fun LoTWUploadConfigDialog(
} }
if (certificate == null) { if (certificate == null) {
Text(stringResource(R.string.prefs_lotw_upload_cert_hint), fontSize = 13.sp) Text(stringResource(R.string.prefs_lotw_upload_cert_hint), fontSize = 13.sp)
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text(stringResource(R.string.prefs_lotw_upload_password), fontSize = 13.sp) },
singleLine = true,
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth()
)
Button( Button(
onClick = { filePicker.launch(arrayOf("*/*")) }, onClick = { filePicker.launch(arrayOf("*/*")) },
enabled = password.isNotBlank() && !busy, enabled = !busy,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { Text(stringResource(R.string.prefs_lotw_upload_import), fontSize = 13.sp) } ) { Text(stringResource(R.string.prefs_lotw_upload_import), fontSize = 13.sp) }
selectedFile?.let { uri ->
Text(
text = displayName(uri),
fontSize = 12.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
color = MaterialTheme.colorScheme.primary
)
OutlinedTextField(
value = password,
onValueChange = { password = it },
label = { Text(stringResource(R.string.prefs_lotw_upload_password), fontSize = 13.sp) },
singleLine = true,
visualTransformation = PasswordVisualTransformation(),
modifier = Modifier.fillMaxWidth()
)
Button(
onClick = {
val bytes = runCatching {
context.contentResolver.openInputStream(uri)?.use { it.readBytes() }
}.getOrNull()
if (bytes != null && bytes.isNotEmpty()) {
onImport(bytes, password.toCharArray())
password = ""
selectedFile = null
}
},
enabled = password.isNotBlank() && !busy,
modifier = Modifier.fillMaxWidth()
) { Text(stringResource(R.string.prefs_lotw_upload_import_confirm), fontSize = 13.sp) }
}
} else { } else {
Text(stringResource(R.string.prefs_lotw_upload_cert_info, certificate.callsign, certificate.dxcc, certificate.expires), fontSize = 13.sp) Text(stringResource(R.string.prefs_lotw_upload_cert_info, certificate.callsign, certificate.dxcc, certificate.expires), fontSize = 13.sp)
OutlinedButton(onClick = onRemove, enabled = !busy) { OutlinedButton(onClick = onRemove, enabled = !busy) {