From 4189e61a660abe8735c313a3ed9c353633a869ba Mon Sep 17 00:00:00 2001 From: renell-kisten Date: Tue, 30 Jun 2026 12:13:59 +0100 Subject: [PATCH] Align CM API, add PNV ID card, and improve newer-device handling - New CM API alignment - Added PNV-specific ID card and profile editor - Improved handling for newer phones (e.g., Pixel 10A) --- app/src/main/AndroidManifest.xml | 8 +- .../credman/cmwallet/CmWalletApplication.kt | 4 + .../data/repository/CredentialRepository.kt | 9 +- .../credman/cmwallet/pnv/PnvTelephonyProbe.kt | 73 ++++++++ .../credman/cmwallet/pnv/PnvTokenRegistry.kt | 138 +++++++++++--- .../com/credman/cmwallet/ui/HomeScreen.kt | 169 ++++++++++++++++++ .../com/credman/cmwallet/ui/HomeViewModel.kt | 15 ++ 7 files changed, 383 insertions(+), 33 deletions(-) create mode 100644 app/src/main/java/com/credman/cmwallet/pnv/PnvTelephonyProbe.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2513dba..a4be79c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,8 @@ - - - @@ -45,4 +45,6 @@ + + \ No newline at end of file diff --git a/app/src/main/java/com/credman/cmwallet/CmWalletApplication.kt b/app/src/main/java/com/credman/cmwallet/CmWalletApplication.kt index c35033b..bdf7ee7 100644 --- a/app/src/main/java/com/credman/cmwallet/CmWalletApplication.kt +++ b/app/src/main/java/com/credman/cmwallet/CmWalletApplication.kt @@ -1,6 +1,7 @@ package com.credman.cmwallet import android.app.Application +import android.content.Context import android.graphics.Bitmap import android.util.Log import androidx.core.graphics.drawable.toBitmap @@ -27,6 +28,7 @@ import kotlin.io.encoding.ExperimentalEncodingApi class CmWalletApplication : Application() { companion object { + lateinit var appContext: Context lateinit var database: CredentialDatabase lateinit var credentialRepo: CredentialRepository @@ -56,6 +58,7 @@ class CmWalletApplication : Application() { @OptIn(ExperimentalDigitalCredentialApi::class, ExperimentalEncodingApi::class) override fun onCreate() { super.onCreate() + appContext = applicationContext TrustedTime.createClient(this).addOnSuccessListener { trustedTimeClient = it }.addOnFailureListener { @@ -96,6 +99,7 @@ class CmWalletApplication : Application() { // Phone number verification demo credentialRepo.registerPhoneNumberVerification( + appContext, registryManager, loadPhoneNumberMatcher() ) diff --git a/app/src/main/java/com/credman/cmwallet/data/repository/CredentialRepository.kt b/app/src/main/java/com/credman/cmwallet/data/repository/CredentialRepository.kt index 6de5c0d..3898bc6 100644 --- a/app/src/main/java/com/credman/cmwallet/data/repository/CredentialRepository.kt +++ b/app/src/main/java/com/credman/cmwallet/data/repository/CredentialRepository.kt @@ -1,6 +1,7 @@ package com.credman.cmwallet.data.repository import android.graphics.BitmapFactory +import android.content.Context import android.util.Log import androidx.credentials.DigitalCredential import androidx.credentials.ExperimentalDigitalCredentialApi @@ -91,12 +92,8 @@ class CredentialRepository { } @OptIn(ExperimentalDigitalCredentialApi::class) - suspend fun registerPhoneNumberVerification(registryManager: RegistryManager, pnvMatcher: ByteArray) { - val testPhoneNumberTokens = listOf( - PnvTokenRegistry.TEST_PNV_1_GET_PHONE_NUMBER, - PnvTokenRegistry.TEST_PNV_1_VERIFY_PHONE_NUMBER, - PnvTokenRegistry.TEST_PNV_2_VERIFY_PHONE_NUMBER - ) + suspend fun registerPhoneNumberVerification(context: Context, registryManager: RegistryManager, pnvMatcher: ByteArray) { + val testPhoneNumberTokens = PnvTokenRegistry.getConfiguredTokens(context) registryManager.registerCredentials( request = object : RegisterCredentialsRequest( diff --git a/app/src/main/java/com/credman/cmwallet/pnv/PnvTelephonyProbe.kt b/app/src/main/java/com/credman/cmwallet/pnv/PnvTelephonyProbe.kt new file mode 100644 index 0000000..33253a8 --- /dev/null +++ b/app/src/main/java/com/credman/cmwallet/pnv/PnvTelephonyProbe.kt @@ -0,0 +1,73 @@ +package com.credman.cmwallet.pnv + +import android.Manifest +import android.content.Context +import android.content.pm.PackageManager +import android.os.Build +import android.telephony.SubscriptionManager +import android.telephony.TelephonyManager +import androidx.core.content.ContextCompat + +object PnvTelephonyProbe { + data class ProbeResult( + val profile: PnvTokenRegistry.Companion.EditablePnvProfile, + val message: String, + ) + + fun deriveProfile( + context: Context, + current: PnvTokenRegistry.Companion.EditablePnvProfile, + ): ProbeResult { + val tm = context.getSystemService(TelephonyManager::class.java) + val hasPhoneState = ContextCompat.checkSelfPermission( + context, + Manifest.permission.READ_PHONE_STATE, + ) == PackageManager.PERMISSION_GRANTED + + val hasPhoneNumbers = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + ContextCompat.checkSelfPermission( + context, + Manifest.permission.READ_PHONE_NUMBERS, + ) == PackageManager.PERMISSION_GRANTED + } else { + true + } + + val simOperator = tm?.simOperator?.takeIf { it.isNotBlank() } + val carrierHint = simOperator ?: current.carrierHint + + val subManager = context.getSystemService(SubscriptionManager::class.java) + val activeSubs = try { + if (hasPhoneState) subManager?.activeSubscriptionInfoList.orEmpty() else emptyList() + } catch (_: SecurityException) { + emptyList() + } + + val subscriptionHint1 = activeSubs.getOrNull(0)?.subscriptionId ?: current.subscriptionHint1 + val subscriptionHint2 = activeSubs.getOrNull(1)?.subscriptionId ?: current.subscriptionHint2 + + val numberFromSub = activeSubs.firstOrNull()?.number?.trim()?.takeIf { it.isNotBlank() } + val numberFromLine1 = try { + if (hasPhoneNumbers) tm?.line1Number?.trim()?.takeIf { it.isNotBlank() } else null + } catch (_: SecurityException) { + null + } + val phoneNumber = numberFromSub ?: numberFromLine1 ?: current.phoneNumberHint + + val message = if (simOperator == null && numberFromSub == null && numberFromLine1 == null) { + "Could not fully read telephony details (permissions/carrier restrictions). Kept previous values where needed." + } else { + "Imported telephony details from device SIM/subscriptions." + } + + return ProbeResult( + profile = current.copy( + phoneNumberHint = phoneNumber, + carrierHint = carrierHint, + subscriptionHint1 = subscriptionHint1, + subscriptionHint2 = subscriptionHint2, + ), + message = message, + ) + } +} diff --git a/app/src/main/java/com/credman/cmwallet/pnv/PnvTokenRegistry.kt b/app/src/main/java/com/credman/cmwallet/pnv/PnvTokenRegistry.kt index 8933681..91d8275 100644 --- a/app/src/main/java/com/credman/cmwallet/pnv/PnvTokenRegistry.kt +++ b/app/src/main/java/com/credman/cmwallet/pnv/PnvTokenRegistry.kt @@ -1,10 +1,12 @@ package com.credman.cmwallet.pnv +import android.content.Context import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import android.util.Base64 import android.util.Log import androidx.credentials.provider.CallingAppInfo +import com.credman.cmwallet.CmWalletApplication import com.credman.cmwallet.CmWalletApplication.Companion.TAG import com.credman.cmwallet.CmWalletApplication.Companion.computeClientId import com.credman.cmwallet.createJWTES256 @@ -22,6 +24,7 @@ import com.credman.cmwallet.openid4vp.OpenId4VP import com.credman.cmwallet.openid4vp.OpenId4VP.Companion.IDENTIFIERS_1_0 import com.credman.cmwallet.pnv.PnvTokenRegistry.Companion.TEST_PNV_1_GET_PHONE_NUMBER import com.credman.cmwallet.pnv.PnvTokenRegistry.Companion.TEST_PNV_1_VERIFY_PHONE_NUMBER +import com.credman.cmwallet.pnv.PnvTokenRegistry.Companion.TEST_PNV_2_GET_PHONE_NUMBER import com.credman.cmwallet.pnv.PnvTokenRegistry.Companion.TEST_PNV_2_VERIFY_PHONE_NUMBER import com.credman.cmwallet.pnv.PnvTokenRegistry.Companion.VCT_GET_PHONE_NUMBER import com.credman.cmwallet.pnv.PnvTokenRegistry.Companion.VCT_VERIFY_PHONE_NUMBER @@ -88,6 +91,13 @@ data class PnvTokenRegistry( } companion object { + private const val PNV_PREFS = "pnv_profile" + private const val KEY_PHONE_NUMBER = "phone_number" + private const val KEY_CARRIER_HINT = "carrier_hint" + private const val KEY_ANDROID_CARRIER_HINT = "android_carrier_hint" + private const val KEY_SUBSCRIPTION_HINT_1 = "subscription_hint_1" + private const val KEY_SUBSCRIPTION_HINT_2 = "subscription_hint_2" + const val VCT_GET_PHONE_NUMBER = "number-verification/device-phone-number/ts43" const val VCT_VERIFY_PHONE_NUMBER = "number-verification/verify/ts43" const val PNV_CRED_FORMAT = "dc-authorization+sd-jwt" @@ -104,42 +114,122 @@ data class PnvTokenRegistry( internal const val SHARED_ATTRIBUTE_DISPLAY_NAME = "shared_attribute_display_name" internal const val ISS_ALLOWLIST = "iss_allowlist" + data class EditablePnvProfile( + val phoneNumberHint: String, + val carrierHint: String, + val androidCarrierHint: String, + val subscriptionHint1: Int, + val subscriptionHint2: Int, + ) + + fun defaultProfile() = EditablePnvProfile( + phoneNumberHint = "+447386537913", + carrierHint = "23415", + androidCarrierHint = "3", + subscriptionHint1 = 1, + subscriptionHint2 = 2, + ) + + fun loadEditableProfile(context: Context): EditablePnvProfile { + val prefs = context.getSharedPreferences(PNV_PREFS, Context.MODE_PRIVATE) + val defaults = defaultProfile() + return EditablePnvProfile( + phoneNumberHint = prefs.getString(KEY_PHONE_NUMBER, defaults.phoneNumberHint) ?: defaults.phoneNumberHint, + carrierHint = prefs.getString(KEY_CARRIER_HINT, defaults.carrierHint) ?: defaults.carrierHint, + androidCarrierHint = prefs.getString(KEY_ANDROID_CARRIER_HINT, defaults.androidCarrierHint) ?: defaults.androidCarrierHint, + subscriptionHint1 = prefs.getInt(KEY_SUBSCRIPTION_HINT_1, defaults.subscriptionHint1), + subscriptionHint2 = prefs.getInt(KEY_SUBSCRIPTION_HINT_2, defaults.subscriptionHint2), + ) + } + + fun saveEditableProfile(context: Context, profile: EditablePnvProfile) { + context.getSharedPreferences(PNV_PREFS, Context.MODE_PRIVATE) + .edit() + .putString(KEY_PHONE_NUMBER, profile.phoneNumberHint) + .putString(KEY_CARRIER_HINT, profile.carrierHint) + .putString(KEY_ANDROID_CARRIER_HINT, profile.androidCarrierHint) + .putInt(KEY_SUBSCRIPTION_HINT_1, profile.subscriptionHint1) + .putInt(KEY_SUBSCRIPTION_HINT_2, profile.subscriptionHint2) + .apply() + } + val TEST_PNV_1_GET_PHONE_NUMBER = PnvTokenRegistry( tokenId = "pnv_1", vct = VCT_GET_PHONE_NUMBER, - title = "Terrific Telecom", - subtitle = "+1 (650) 215-4321", - providerConsent = "CMWallet will enable your carrier (Terrific Telecom) to share your phone number with \${CALLER_DISPLAY_NAME}", + title = "Vodafone UK", + subtitle = "+44 7386 537913", + providerConsent = "CMWallet will enable your carrier (Vodafone UK) to share your phone number with \${CALLER_DISPLAY_NAME}", subscriptionHint = 1, - carrierHint = "310250", + carrierHint = "23415", androidCarrierHint = "3", - phoneNumberHint = "+16502154321", + phoneNumberHint = "+447386537913", iss = "https://example.terrific-telecom.dev", icon = "iVBORw0KGgoAAAANSUhEUgAAAA4AAAAWCAYAAADwza0nAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACBSURBVHgB7ZTBDUVQEEXvff8VoITfC0YZatABHWiH6IUOaICHBCuReXbEWd1kcmZmMRmGIinhSpABFBDoJpqccSKtA/7wY7C71FQ1NUaUyKIg4Ba8MbiJ3YPnqvcnfuI7xAfdqr0qXjU9FTXTGUnca//NIYGdoflla1BbDsPIqZgBHcEomi+uUHMAAAAASUVORK5CYII=", phoneNumberAttributeDisplayName = "Phone number", - supportedAggregatorIssNames = null, + supportedAggregatorIssNames = setOf( + "developer.vodafone.com", + "https://developer.vodafone.com", + "https://developer.vodafone.com/" + ), verifierTermsPrefix = "Provider Terms:\n", // verifierTermsPrefix = null, ) val TEST_PNV_1_VERIFY_PHONE_NUMBER = TEST_PNV_1_GET_PHONE_NUMBER.copy( + tokenId = "pnv_1_verify", vct = VCT_VERIFY_PHONE_NUMBER, ) - val TEST_PNV_2_VERIFY_PHONE_NUMBER = PnvTokenRegistry( + val TEST_PNV_2_GET_PHONE_NUMBER = PnvTokenRegistry( tokenId = "pnv_2", - vct = VCT_VERIFY_PHONE_NUMBER, - title = "Work Number", - subtitle = "Timely Telecom", - providerConsent = "CMWallet will enable your carrier (Timely Telecom) to share your phone number with \${CALLER_DISPLAY_NAME}", + vct = VCT_GET_PHONE_NUMBER, + title = "Vodafone UK (SIM 2)", + subtitle = "+44 7386 537913", + providerConsent = "CMWallet will enable your carrier (Vodafone UK) to share your phone number with \${CALLER_DISPLAY_NAME}", subscriptionHint = 2, - carrierHint = "380250", + carrierHint = "23415", androidCarrierHint = "3", - phoneNumberHint = "+16502157890", + phoneNumberHint = "+447386537913", iss = "https://example.timely-telecom.dev", icon = "iVBORw0KGgoAAAANSUhEUgAAAA4AAAAWCAYAAADwza0nAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAACBSURBVHgB7ZTBDUVQEEXvff8VoITfC0YZatABHWiH6IUOaICHBCuReXbEWd1kcmZmMRmGIinhSpABFBDoJpqccSKtA/7wY7C71FQ1NUaUyKIg4Ba8MbiJ3YPnqvcnfuI7xAfdqr0qXjU9FTXTGUnca//NIYGdoflla1BbDsPIqZgBHcEomi+uUHMAAAAASUVORK5CYII=", phoneNumberAttributeDisplayName = "Phone number", - supportedAggregatorIssNames = null, + supportedAggregatorIssNames = setOf( + "developer.vodafone.com", + "https://developer.vodafone.com", + "https://developer.vodafone.com/" + ), verifierTermsPrefix = "Provider Terms:\n" ) + val TEST_PNV_2_VERIFY_PHONE_NUMBER = TEST_PNV_2_GET_PHONE_NUMBER.copy( + tokenId = "pnv_2_verify", + vct = VCT_VERIFY_PHONE_NUMBER, + ) + + fun getConfiguredTokens(context: Context): List { + val profile = loadEditableProfile(context) + val sim1 = TEST_PNV_1_GET_PHONE_NUMBER.copy( + subtitle = profile.phoneNumberHint, + phoneNumberHint = profile.phoneNumberHint, + carrierHint = profile.carrierHint, + androidCarrierHint = profile.androidCarrierHint, + subscriptionHint = profile.subscriptionHint1, + ) + val sim1Verify = sim1.copy( + tokenId = "pnv_1_verify", + vct = VCT_VERIFY_PHONE_NUMBER, + ) + val sim2 = TEST_PNV_2_GET_PHONE_NUMBER.copy( + subtitle = profile.phoneNumberHint, + phoneNumberHint = profile.phoneNumberHint, + carrierHint = profile.carrierHint, + androidCarrierHint = profile.androidCarrierHint, + subscriptionHint = profile.subscriptionHint2, + ) + val sim2Verify = sim2.copy( + tokenId = "pnv_2_verify", + vct = VCT_VERIFY_PHONE_NUMBER, + ) + + return listOf(sim1, sim1Verify, sim2, sim2Verify) + } fun buildRegistryDatabase(items: List): ByteArray { val out = ByteArrayOutputStream() @@ -271,13 +361,17 @@ fun maybeHandlePnv( origin: String, // Either the web origin or the calling app sha callingAppInfo: CallingAppInfo ): DigitalCredentialResult? { - if (selectedID != TEST_PNV_1_GET_PHONE_NUMBER.tokenId && selectedID != TEST_PNV_2_VERIFY_PHONE_NUMBER.tokenId) { + val configuredTokens = PnvTokenRegistry.getConfiguredTokens(CmWalletApplication.appContext) + val tokenById = configuredTokens.associateBy { it.tokenId } + val validTokenIds = tokenById.keys + + if (selectedID !in validTokenIds) { return null - } else if (selectedID == TEST_PNV_1_GET_PHONE_NUMBER.tokenId) { - Log.d(TAG, "Selected number: ${TEST_PNV_1_GET_PHONE_NUMBER.title}") - } else { - Log.d(TAG, "Selected number: ${TEST_PNV_2_VERIFY_PHONE_NUMBER.title}") } + + val selectedToken = tokenById[selectedID] ?: return null + + Log.d(TAG, "Selected PNV token: ${selectedToken.title} (${selectedToken.vct})") val digitalCredentialOptions = DigitalCredentialRequestOptions.createFrom(requestJson) val requestProtocol = DigitalCredentialRequestOptions.getRequestProtocolAtIndex( digitalCredentialOptions, providerIdx @@ -302,11 +396,7 @@ fun maybeHandlePnv( throw IllegalStateException("Could not find a valid vct value for pnv") } - val selectedCred = when (selectedID) { - TEST_PNV_1_GET_PHONE_NUMBER.tokenId -> if (vct == TEST_PNV_1_GET_PHONE_NUMBER.vct) TEST_PNV_1_GET_PHONE_NUMBER else TEST_PNV_1_VERIFY_PHONE_NUMBER - TEST_PNV_2_VERIFY_PHONE_NUMBER.tokenId -> TEST_PNV_2_VERIFY_PHONE_NUMBER - else -> return null - } + val selectedCred = selectedToken val credAuthJwt = dcqlObject.getJSONObject("meta").getString("credential_authorization_jwt") val (credAuthJwtHeader, credAuthJwtpayload) = jwsDeserialization(credAuthJwt) diff --git a/app/src/main/java/com/credman/cmwallet/ui/HomeScreen.kt b/app/src/main/java/com/credman/cmwallet/ui/HomeScreen.kt index f5ef2ab..ad35098 100644 --- a/app/src/main/java/com/credman/cmwallet/ui/HomeScreen.kt +++ b/app/src/main/java/com/credman/cmwallet/ui/HomeScreen.kt @@ -23,12 +23,14 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.paint @@ -52,6 +54,7 @@ import com.credman.cmwallet.data.model.toPrivateKey import com.credman.cmwallet.decodeBase64 import com.credman.cmwallet.openid4vci.data.CredentialConfigurationMDoc import com.credman.cmwallet.openid4vci.data.CredentialConfigurationSdJwtVc +import com.credman.cmwallet.pnv.PnvTokenRegistry import com.credman.cmwallet.sdjwt.SdJwt import kotlin.io.encoding.ExperimentalEncodingApi @@ -77,6 +80,7 @@ fun HomeScreen( ) { HorizontalDivider(thickness = 2.dp) CredentialList( + viewModel = viewModel, uiState.credentials, onCredentialClick = { cred -> openCredentialDialog.value = cred @@ -98,8 +102,170 @@ fun HomeScreen( } } +@Composable +fun PnvIdentityEditor(viewModel: HomeViewModel) { + val initialProfile = remember { viewModel.getPnvProfile() } + var phoneNumber by remember { mutableStateOf(initialProfile.phoneNumberHint) } + var carrierHint by remember { mutableStateOf(initialProfile.carrierHint) } + var androidCarrierHint by remember { mutableStateOf(initialProfile.androidCarrierHint) } + var subscriptionHint1 by remember { mutableStateOf(initialProfile.subscriptionHint1.toString()) } + var subscriptionHint2 by remember { mutableStateOf(initialProfile.subscriptionHint2.toString()) } + var saveMessage by remember { mutableStateOf(null) } + var detailsVisible by remember { mutableStateOf(false) } + + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Card( + modifier = Modifier.size(350.dp, 210.dp), + shape = CardDefaults.shape, + onClick = { detailsVisible = !detailsVisible } + ) { + Box( + Modifier + .fillMaxSize() + .paint( + painterResource(id = R.drawable.card_art_dark), + contentScale = ContentScale.Crop, + ) + ) { + Row(Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) { + Column( + modifier = Modifier.padding(20.dp, 20.dp), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.Center, + ) { + Text( + text = "PNV Identity", + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = Color.White, + ) + Text( + text = "Tap to view and edit", + fontSize = 16.sp, + color = Color.White, + ) + } + } + } + } + Text( + text = if (detailsVisible) "PNV Identity selected. Edit details below." else "Tap the PNV card to view/edit details", + color = Color.Gray + ) + if (!detailsVisible) { + return@Column + } + Card( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors(containerColor = Color(0xFFF6F6F6)) + ) { + Column( + modifier = Modifier.padding(12.dp), + verticalArrangement = Arrangement.spacedBy(10.dp) + ) { + Text( + text = "PNV Details", + fontWeight = FontWeight.Bold, + fontSize = 18.sp, + ) + HorizontalDivider() + OutlinedTextField( + value = phoneNumber, + onValueChange = { phoneNumber = it.trim() }, + label = { Text("Phone Number (E.164)") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true + ) + OutlinedTextField( + value = carrierHint, + onValueChange = { carrierHint = it.trim() }, + label = { Text("Carrier Hint (MCCMNC)") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true + ) + OutlinedTextField( + value = androidCarrierHint, + onValueChange = { androidCarrierHint = it.trim() }, + label = { Text("Android Carrier Hint") }, + modifier = Modifier.fillMaxWidth(), + singleLine = true + ) + Row(horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxWidth()) { + OutlinedTextField( + value = subscriptionHint1, + onValueChange = { subscriptionHint1 = it.trim() }, + label = { Text("SIM1 Subscription") }, + modifier = Modifier.weight(1f), + singleLine = true + ) + OutlinedTextField( + value = subscriptionHint2, + onValueChange = { subscriptionHint2 = it.trim() }, + label = { Text("SIM2 Subscription") }, + modifier = Modifier.weight(1f), + singleLine = true + ) + } + Button( + onClick = { + val current = PnvTokenRegistry.Companion.EditablePnvProfile( + phoneNumberHint = phoneNumber, + carrierHint = carrierHint, + androidCarrierHint = androidCarrierHint, + subscriptionHint1 = subscriptionHint1.toIntOrNull() ?: 1, + subscriptionHint2 = subscriptionHint2.toIntOrNull() ?: 2, + ) + val (imported, message) = viewModel.importPnvProfileFromDevice(current) + phoneNumber = imported.phoneNumberHint + carrierHint = imported.carrierHint + androidCarrierHint = imported.androidCarrierHint + subscriptionHint1 = imported.subscriptionHint1.toString() + subscriptionHint2 = imported.subscriptionHint2.toString() + saveMessage = message + }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Import Device Telephony") + } + Button( + onClick = { + val s1 = subscriptionHint1.toIntOrNull() + val s2 = subscriptionHint2.toIntOrNull() + if (phoneNumber.isBlank() || carrierHint.isBlank() || androidCarrierHint.isBlank() || s1 == null || s2 == null) { + saveMessage = "Enter valid values for all fields" + return@Button + } + viewModel.savePnvProfile( + PnvTokenRegistry.Companion.EditablePnvProfile( + phoneNumberHint = phoneNumber, + carrierHint = carrierHint, + androidCarrierHint = androidCarrierHint, + subscriptionHint1 = s1, + subscriptionHint2 = s2, + ) + ) + saveMessage = "Saved. Restart CMWallet to re-register PNV tokens." + }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Save PNV Identity") + } + if (saveMessage != null) { + Text(text = saveMessage!!, color = Color.DarkGray) + } + } + } + } +} + @Composable fun CredentialList( + viewModel: HomeViewModel, credentials: List, onCredentialClick: (CredentialItem) -> Unit ) { @@ -111,6 +277,9 @@ fun CredentialList( modifier = Modifier.padding(24.dp), verticalArrangement = Arrangement.spacedBy(15.dp) ) { + item { + PnvIdentityEditor(viewModel) + } credentials.forEach { item { CredentialCard(credential = it, onCredentialClick = onCredentialClick) diff --git a/app/src/main/java/com/credman/cmwallet/ui/HomeViewModel.kt b/app/src/main/java/com/credman/cmwallet/ui/HomeViewModel.kt index 3fb8997..0b7fd2a 100644 --- a/app/src/main/java/com/credman/cmwallet/ui/HomeViewModel.kt +++ b/app/src/main/java/com/credman/cmwallet/ui/HomeViewModel.kt @@ -11,6 +11,8 @@ import com.credman.cmwallet.CmWalletApplication import com.credman.cmwallet.CmWalletApplication.Companion.TAG import com.credman.cmwallet.MainActivity import com.credman.cmwallet.data.model.CredentialItem +import com.credman.cmwallet.pnv.PnvTokenRegistry +import com.credman.cmwallet.pnv.PnvTelephonyProbe import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -41,4 +43,17 @@ class HomeViewModel : ViewModel() { fun deleteCredential(id: String) { CmWalletApplication.credentialRepo.deleteCredential(id) } + + fun getPnvProfile(): PnvTokenRegistry.Companion.EditablePnvProfile { + return PnvTokenRegistry.loadEditableProfile(CmWalletApplication.appContext) + } + + fun savePnvProfile(profile: PnvTokenRegistry.Companion.EditablePnvProfile) { + PnvTokenRegistry.saveEditableProfile(CmWalletApplication.appContext, profile) + } + + fun importPnvProfileFromDevice(current: PnvTokenRegistry.Companion.EditablePnvProfile): Pair { + val result = PnvTelephonyProbe.deriveProfile(CmWalletApplication.appContext, current) + return Pair(result.profile, result.message) + } } \ No newline at end of file