Merge pull request #1493 from greenart7c3/main

Show a dialog to select a signer when using multiple signers
This commit is contained in:
Vitor Pamplona
2025-09-24 08:38:20 -04:00
committed by GitHub
4 changed files with 131 additions and 2 deletions

View File

@@ -23,12 +23,39 @@ package com.vitorpamplona.amethyst.ui.screen.loggedOff.login
import android.app.Activity import android.app.Activity
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.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.core.graphics.drawable.toBitmap
import coil3.compose.rememberAsyncImagePainter
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.DefaultSignerPermissions import com.vitorpamplona.amethyst.model.DefaultSignerPermissions
import com.vitorpamplona.amethyst.ui.theme.Size0dp import com.vitorpamplona.amethyst.ui.theme.Size0dp
@@ -37,6 +64,7 @@ import com.vitorpamplona.amethyst.ui.theme.Size40dp
import com.vitorpamplona.quartz.nip55AndroidSigner.api.PubKeyResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.PubKeyResult
import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult import com.vitorpamplona.quartz.nip55AndroidSigner.api.SignerResult
import com.vitorpamplona.quartz.nip55AndroidSigner.client.ExternalSignerLogin import com.vitorpamplona.quartz.nip55AndroidSigner.client.ExternalSignerLogin
import com.vitorpamplona.quartz.nip55AndroidSigner.client.getExternalSignersInstalled
import com.vitorpamplona.quartz.utils.Log import com.vitorpamplona.quartz.utils.Log
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -44,6 +72,9 @@ import kotlinx.coroutines.launch
@Composable @Composable
fun ExternalSignerButton(loginViewModel: LoginViewModel) { fun ExternalSignerButton(loginViewModel: LoginViewModel) {
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val context = LocalContext.current
val installedSigners = getExternalSignersInstalled(context)
var shouldSelectSigner by remember { mutableStateOf(false) }
val launcher = val launcher =
rememberLauncherForActivityResult( rememberLauncherForActivityResult(
@@ -63,6 +94,81 @@ fun ExternalSignerButton(loginViewModel: LoginViewModel) {
} }
} }
if (shouldSelectSigner) {
Dialog(
onDismissRequest = {
shouldSelectSigner = false
},
content = {
Surface(
shape = RoundedCornerShape(4.dp),
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.padding(8.dp),
text = stringResource(R.string.select_signer),
fontWeight = FontWeight.Bold,
fontSize = 24.sp,
)
Spacer(Modifier.height(4.dp))
LazyColumn {
items(installedSigners) {
val appName = it.loadLabel(context.packageManager).toString()
val appIcon = it.loadIcon(context.packageManager)
val iconBitmap = appIcon.toBitmap()
Row(
Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 8.dp, top = 8.dp)
.clickable {
if (!loginViewModel.acceptedTerms) {
loginViewModel.termsAcceptanceIsRequiredError = true
} else {
try {
launcher.launch(ExternalSignerLogin.createIntent(DefaultSignerPermissions, it.activityInfo.packageName))
} catch (e: Exception) {
if (e is CancellationException) throw e
Log.e("ExternalSigner", "Error opening Signer app", e)
loginViewModel.errorManager.error(R.string.error_opening_external_signer)
} finally {
shouldSelectSigner = false
}
}
},
) {
val painter =
rememberAsyncImagePainter(
iconBitmap,
)
Image(
painter = painter,
contentDescription = appName,
modifier =
Modifier
.size(48.dp)
.padding(end = 16.dp),
)
Column {
Text(appName)
Text(
it.activityInfo.packageName,
fontSize = 14.sp,
color = Color.Gray,
)
}
}
}
}
}
}
},
)
}
Box(modifier = Modifier.padding(Size40dp, Size20dp, Size40dp, Size0dp)) { Box(modifier = Modifier.padding(Size40dp, Size20dp, Size40dp, Size0dp)) {
LoginWithAmberButton( LoginWithAmberButton(
enabled = loginViewModel.acceptedTerms, enabled = loginViewModel.acceptedTerms,
@@ -71,7 +177,11 @@ fun ExternalSignerButton(loginViewModel: LoginViewModel) {
loginViewModel.termsAcceptanceIsRequiredError = true loginViewModel.termsAcceptanceIsRequiredError = true
} else { } else {
try { try {
if (installedSigners.size == 1) {
launcher.launch(ExternalSignerLogin.createIntent(DefaultSignerPermissions)) launcher.launch(ExternalSignerLogin.createIntent(DefaultSignerPermissions))
} else {
shouldSelectSigner = true
}
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) throw e if (e is CancellationException) throw e
Log.e("ExternalSigner", "Error opening Signer app", e) Log.e("ExternalSigner", "Error opening Signer app", e)

View File

@@ -1289,4 +1289,5 @@
<string name="would_you_like_to_send_the_recent_crash_report_to_amethyst_in_a_dm_no_personal_information_will_be_shared">Would you like to send the recent crash report to Amethyst in a DM? No personal information will be shared</string> <string name="would_you_like_to_send_the_recent_crash_report_to_amethyst_in_a_dm_no_personal_information_will_be_shared">Would you like to send the recent crash report to Amethyst in a DM? No personal information will be shared</string>
<string name="crashreport_found_send">Send it</string> <string name="crashreport_found_send">Send it</string>
<string name="this_message_will_disappear_in_days">This message will disappear in %1$d days</string> <string name="this_message_will_disappear_in_days">This message will disappear in %1$d days</string>
<string name="select_signer">Select Signer</string>
</resources> </resources>

View File

@@ -29,9 +29,15 @@ import com.vitorpamplona.quartz.nip55AndroidSigner.api.foreground.intents.result
import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.Permission import com.vitorpamplona.quartz.nip55AndroidSigner.api.permission.Permission
object ExternalSignerLogin { object ExternalSignerLogin {
fun createIntent(permissions: List<Permission> = LoginRequest.DefaultPermissions): Intent { fun createIntent(
permissions: List<Permission> = LoginRequest.DefaultPermissions,
packageName: String = "",
): Intent {
val intent = LoginRequest.assemble(permissions) val intent = LoginRequest.assemble(permissions)
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP) intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP)
if (packageName.isNotBlank()) {
intent.`package` = packageName
}
return intent return intent
} }

View File

@@ -23,6 +23,7 @@ package com.vitorpamplona.quartz.nip55AndroidSigner.client
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.ResolveInfo
import androidx.core.net.toUri import androidx.core.net.toUri
@SuppressLint("QueryPermissionsNeeded") @SuppressLint("QueryPermissionsNeeded")
@@ -35,3 +36,14 @@ fun isExternalSignerInstalled(context: Context): Boolean =
}, },
0, 0,
).isNotEmpty() ).isNotEmpty()
@SuppressLint("QueryPermissionsNeeded")
fun getExternalSignersInstalled(context: Context): List<ResolveInfo> =
context.packageManager
.queryIntentActivities(
Intent().apply {
action = Intent.ACTION_VIEW
data = "nostrsigner:".toUri()
},
0,
)