Simplify app naming#205
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a sample Android application demonstrating the integration of the Digital Credentials API using Credential Manager, including custom CBOR and SD-JWT parsing utilities and a Jetpack Compose UI. The review feedback highlights several critical improvements: resolving potential memory leaks caused by capturing the Activity context in viewModelScope by shifting coroutine launching to the Composable's lifecycle, replacing unsafe casts on untrusted CBOR data with safe casts to prevent crashes, and addressing robustness issues in the CBOR encoder/decoder such as unhandled Float types, unsafe double-bang operators on nullable map entries, and integer overflow vulnerabilities for large array/map sizes.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| MainContent( | ||
| uiState = uiState, | ||
| onGetDigitalCredentialClick = { | ||
| val activity = context.findActivity() | ||
| if (activity != null) { | ||
| viewModel.getDigitalCredential { | ||
| CredentialManagerUtil.getDigitalCredential(activity) | ||
| } | ||
| } | ||
| }, | ||
| onGetVerifiedEmailClick = { | ||
| val activity = context.findActivity() | ||
| if (activity != null) { | ||
| viewModel.getVerifiedEmailCredential { | ||
| CredentialManagerUtil.getVerifiedEmailCredential(activity) | ||
| } | ||
| } | ||
| }, | ||
| modifier = modifier | ||
| ) |
There was a problem hiding this comment.
Capturing the Activity context inside a lambda passed to the ViewModel's viewModelScope causes a memory leak. Since the ViewModel survives configuration changes (like screen rotation) but the Activity is destroyed and recreated, the running coroutine in viewModelScope will hold a strong reference to the destroyed Activity instance.
To fix this, launch the coroutine within the Composable's lifecycle using rememberCoroutineScope(). This ensures that the coroutine is cancelled when the Composable is disposed, preventing any memory leaks.
val coroutineScope = rememberCoroutineScope()
MainContent(
uiState = uiState,
onGetDigitalCredentialClick = {
val activity = context.findActivity()
if (activity != null) {
coroutineScope.launch {
viewModel.updateUiState(MainUiState.Loading)
val result = CredentialManagerUtil.getDigitalCredential(activity)
viewModel.updateUiState(result)
}
}
},
onGetVerifiedEmailClick = {
val activity = context.findActivity()
if (activity != null) {
coroutineScope.launch {
viewModel.updateUiState(MainUiState.Loading)
val result = CredentialManagerUtil.getVerifiedEmailCredential(activity)
viewModel.updateUiState(result)
}
}
},
modifier = modifier
)| /** | ||
| * Orchestrates a Digital Credential (e.g., mDL) request. | ||
| * | ||
| * @param requestBlock A suspend lambda that performs the retrieval and returns a [MainUiState]. | ||
| */ | ||
| fun getDigitalCredential(requestBlock: suspend () -> MainUiState) { | ||
| viewModelScope.launch { | ||
| _uiState.value = MainUiState.Loading | ||
| _uiState.value = requestBlock() | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Orchestrates a Verified Email credential request. | ||
| * | ||
| * @param requestBlock A suspend lambda that performs the retrieval and returns a [MainUiState]. | ||
| */ | ||
| fun getVerifiedEmailCredential(requestBlock: suspend () -> MainUiState) { | ||
| viewModelScope.launch { | ||
| _uiState.value = MainUiState.Loading | ||
| _uiState.value = requestBlock() | ||
| } | ||
| } |
There was a problem hiding this comment.
To prevent memory leaks from capturing Activity contexts in viewModelScope, simplify the ViewModel to only manage the UI state. The actual coroutines should be launched from the Composable's lifecycle.
/**
* Updates the current UI state.
*/
fun updateUiState(state: MainUiState) {
_uiState.value = state
}| if (mdlNamespace is List<*>) { | ||
| for (item in mdlNamespace) { | ||
| val decodedItem = if (item is CborTag && item.tag == 24L) { | ||
| cborDecode(item.item as ByteArray) | ||
| } else { | ||
| item | ||
| } | ||
| (decodedItem as? Map<*, *>)?.let { extractClaim(it, claims) } | ||
| } | ||
| } else if (mdlNamespace is Map<*, *>) { | ||
| mdlNamespace.forEach { (k, v) -> | ||
| val value = if (v is CborTag && v.tag == 24L) { | ||
| cborDecode(v.item as ByteArray) | ||
| } else { | ||
| v | ||
| } | ||
| claims.add(CredentialClaim(formatLabel(k.toString()), value.toString())) | ||
| } | ||
| } |
There was a problem hiding this comment.
Unsafe casts (as ByteArray) on untrusted CBOR data can cause the application to crash with a ClassCastException if the parsed structure does not match expectations. Use safe casts (as? ByteArray) instead.
| if (mdlNamespace is List<*>) { | |
| for (item in mdlNamespace) { | |
| val decodedItem = if (item is CborTag && item.tag == 24L) { | |
| cborDecode(item.item as ByteArray) | |
| } else { | |
| item | |
| } | |
| (decodedItem as? Map<*, *>)?.let { extractClaim(it, claims) } | |
| } | |
| } else if (mdlNamespace is Map<*, *>) { | |
| mdlNamespace.forEach { (k, v) -> | |
| val value = if (v is CborTag && v.tag == 24L) { | |
| cborDecode(v.item as ByteArray) | |
| } else { | |
| v | |
| } | |
| claims.add(CredentialClaim(formatLabel(k.toString()), value.toString())) | |
| } | |
| } | |
| if (mdlNamespace is List<*>) { | |
| for (item in mdlNamespace) { | |
| val decodedItem = if (item is CborTag && item.tag == 24L) { | |
| (item.item as? ByteArray)?.let { cborDecode(it) } ?: item | |
| } else { | |
| item | |
| } | |
| (decodedItem as? Map<*, *>)?.let { extractClaim(it, claims) } | |
| } | |
| } else if (mdlNamespace is Map<*, *>) { | |
| mdlNamespace.forEach { (k, v) -> | |
| val value = if (v is CborTag && v.tag == 24L) { | |
| (v.item as? ByteArray)?.let { cborDecode(it) } ?: v | |
| } else { | |
| v | |
| } | |
| claims.add(CredentialClaim(formatLabel(k.toString()), value.toString())) | |
| } | |
| } |
| if (data is Number) { | ||
| if (data is Double) { | ||
| throw IllegalArgumentException("Don't support doubles yet") | ||
| } else { | ||
| val value = data.toLong() | ||
| if (value >= 0) { | ||
| out.write(createArg(TYPE_UNSIGNED_INT, value)) | ||
| return | ||
| } else { | ||
| out.write(createArg(TYPE_NEGATIVE_INT, -1 - value)) | ||
| return | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Since floats are not supported by this CBOR encoder, passing a Float will currently fall through to the else block, convert the float to a Long via toLong(), and silently encode it as an integer. This causes silent data loss. Explicitly throw an exception for Float as well.
| if (data is Number) { | |
| if (data is Double) { | |
| throw IllegalArgumentException("Don't support doubles yet") | |
| } else { | |
| val value = data.toLong() | |
| if (value >= 0) { | |
| out.write(createArg(TYPE_UNSIGNED_INT, value)) | |
| return | |
| } else { | |
| out.write(createArg(TYPE_NEGATIVE_INT, -1 - value)) | |
| return | |
| } | |
| } | |
| } | |
| if (data is Number) { | |
| if (data is Double || data is Float) { | |
| throw IllegalArgumentException("Don't support floats or doubles yet") | |
| } else { | |
| val value = data.toLong() | |
| if (value >= 0) { | |
| out.write(createArg(TYPE_UNSIGNED_INT, value)) | |
| return | |
| } else { | |
| out.write(createArg(TYPE_NEGATIVE_INT, -1 - value)) | |
| return | |
| } | |
| } | |
| } |
| if (data is Map<*, *>) { | ||
| out.write(createArg(TYPE_MAP, data.size.toLong())) | ||
| for (i in data) { | ||
| encodeInternal(i.key!!, out, depth + 1) | ||
| encodeInternal(i.value!!, out, depth + 1) | ||
| } | ||
| return | ||
| } |
There was a problem hiding this comment.
Using the double-bang operator (!!) on map keys and values will throw a NullPointerException if any key or value is null. Since CBOR supports null values (encoded as major type 7), use safe destructuring and pass nullable values directly to encodeInternal.
| if (data is Map<*, *>) { | |
| out.write(createArg(TYPE_MAP, data.size.toLong())) | |
| for (i in data) { | |
| encodeInternal(i.key!!, out, depth + 1) | |
| encodeInternal(i.value!!, out, depth + 1) | |
| } | |
| return | |
| } | |
| if (data is Map<*, *>) { | |
| out.write(createArg(TYPE_MAP, data.size.toLong())) | |
| for ((key, value) in data) { | |
| encodeInternal(key, out, depth + 1) | |
| encodeInternal(value, out, depth + 1) | |
| } | |
| return | |
| } |
| TYPE_ARRAY -> { | ||
| val ret = mutableListOf<Any?>() | ||
| var consumed = arg.len | ||
| for (i in 0 until arg.arg.toInt()) { | ||
| if (offset + consumed >= data.size) throw IllegalArgumentException("Unexpected end of data for array") | ||
| val item = parseItem(data, offset + consumed, depth + 1) | ||
| ret.add(item.item) | ||
| consumed += item.len | ||
| } | ||
| return Item(ret.toList(), consumed, TYPE_ARRAY) | ||
| } | ||
|
|
||
| TYPE_MAP -> { | ||
| val ret = mutableMapOf<Any?, Any?>() | ||
| var consumed = arg.len | ||
| for (i in 0 until arg.arg.toInt()) { | ||
| if (offset + consumed >= data.size) throw IllegalArgumentException("Unexpected end of data for map key") | ||
| val key = parseItem(data, offset + consumed, depth + 1) | ||
| consumed += key.len | ||
| if (offset + consumed >= data.size) throw IllegalArgumentException("Unexpected end of data for map value") | ||
| val value = parseItem(data, offset + consumed, depth + 1) | ||
| consumed += value.len | ||
| ret[key.item] = value.item | ||
| } | ||
| return Item(ret.toMap(), consumed, TYPE_MAP) | ||
| } |
There was a problem hiding this comment.
If the CBOR data specifies an array or map size (arg.arg) larger than Int.MAX_VALUE, casting it to an Int via toInt() will overflow and become negative. This results in an empty range 0 until negative_value, causing the parser to silently return an empty list or map instead of throwing an error. Explicitly validate that the size does not exceed Int.MAX_VALUE.
TYPE_ARRAY -> {
if (arg.arg > Int.MAX_VALUE) throw IllegalArgumentException("Array size too large")
val ret = mutableListOf<Any?>()
var consumed = arg.len
for (i in 0 until arg.arg.toInt()) {
if (offset + consumed >= data.size) throw IllegalArgumentException("Unexpected end of data for array")
val item = parseItem(data, offset + consumed, depth + 1)
ret.add(item.item)
consumed += item.len
}
return Item(ret.toList(), consumed, TYPE_ARRAY)
}
TYPE_MAP -> {
if (arg.arg > Int.MAX_VALUE) throw IllegalArgumentException("Map size too large")
val ret = mutableMapOf<Any?, Any?>()
var consumed = arg.len
for (i in 0 until arg.arg.toInt()) {
if (offset + consumed >= data.size) throw IllegalArgumentException("Unexpected end of data for map key")
val key = parseItem(data, offset + consumed, depth + 1)
consumed += key.len
if (offset + consumed >= data.size) throw IllegalArgumentException("Unexpected end of data for map value")
val value = parseItem(data, offset + consumed, depth + 1)
consumed += value.len
ret[key.item] = value.item
}
return Item(ret.toMap(), consumed, TYPE_MAP)
}
No description provided.