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