Skip to content

Commit 228dbfd

Browse files
committed
Fix code review changes
Change-Id: I0571c151e62851634783b2aa8943b47fb84eea36
1 parent 1ac8dfc commit 228dbfd

File tree

9 files changed

+188
-29
lines changed

9 files changed

+188
-29
lines changed

CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/AppDependencies.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ import com.example.android.authentication.myvault.data.CredentialsDataSource
2323
import com.example.android.authentication.myvault.data.CredentialsRepository
2424
import com.example.android.authentication.myvault.data.RPIconDataSource
2525
import com.example.android.authentication.myvault.data.room.MyVaultDatabase
26+
import kotlinx.coroutines.CoroutineScope
27+
import kotlinx.coroutines.Dispatchers
28+
import kotlinx.coroutines.SupervisorJob
2629

2730
/**
2831
* This class is an application-level singleton object which is providing dependencies required for the app to function.
@@ -42,6 +45,8 @@ object AppDependencies {
4245

4346
lateinit var rpIconDataSource: RPIconDataSource
4447

48+
lateinit var coroutineScope: CoroutineScope
49+
4550
/**
4651
* Initializes the core components required for the application's data storage and icon handling.
4752
* This includes:
@@ -72,5 +77,7 @@ object AppDependencies {
7277
credentialsDataSource,
7378
context,
7479
)
80+
81+
coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob())
7582
}
7683
}

CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/NotificationUtils.kt

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,24 @@ import androidx.core.app.NotificationCompat
1313
import androidx.core.app.NotificationManagerCompat
1414
import com.example.android.authentication.myvault.ui.MainActivity
1515

16+
/**
17+
* Creates and registers a notification channel with the system.
18+
*
19+
* This method is intended to be called when your application starts up or before
20+
* you attempt to post any notifications. On Android 8.0 (API level 26) and higher,
21+
* all notifications must be assigned to a channel. If you don't specify a channel,
22+
* your notifications won't appear.
23+
*
24+
* The created channel will have {@link NotificationManager#IMPORTANCE_HIGH}.
25+
*
26+
* @param channelName The user-visible name of the channel.
27+
* This is displayed in the system's notification settings.
28+
* @param channelDescription The user-visible description of the channel.
29+
* This is displayed in the system's notification settings.
30+
* @see NotificationChannel
31+
* @see NotificationManager#createNotificationChannel(NotificationChannel)
32+
* @see Context#NOTIFICATION_SERVICE
33+
*/
1634
fun Context.createNotificationChannel(
1735
channelName: String,
1836
channelDescription: String,
@@ -29,6 +47,33 @@ fun Context.createNotificationChannel(
2947
notificationManager.createNotificationChannel(channel)
3048
}
3149

50+
/**
51+
* Displays a system notification with the given title and content.
52+
*
53+
* This notification will navigate to {@link MainActivity} when tapped.
54+
* It uses the channel ID defined by {@code NOTIFICATION_CHANNEL_ID} and will have
55+
* {@link NotificationCompat#PRIORITY_MAX}. The notification is automatically canceled
56+
* when the user taps it.
57+
*
58+
* Before posting the notification, this method checks for the
59+
* {@link android.Manifest.permission#POST_NOTIFICATIONS} permission. If the permission
60+
* is not granted, the notification will not be shown. This check is primarily for
61+
* Android 13 (API level 33) and higher, where this permission is required.
62+
*
63+
* **Note:** It's assumed that {@code NOTIFICATION_CHANNEL_ID} is a predefined constant
64+
* representing the ID of a channel that has already been created (e.g., using
65+
* {@code createNotificationChannel}). Also, {@code NOTIFICATION_ID} should be a unique
66+
* integer for this notification, allowing you to update or cancel it later.
67+
* The small icon {@code R.drawable.android_secure} should exist in your project's drawables.
68+
*
69+
* @param title The title of the notification.
70+
* @param content The main content text of the notification.
71+
*
72+
* @see NotificationCompat.Builder
73+
* @see NotificationManagerCompat
74+
* @see PendingIntent
75+
* @see android.Manifest.permission#POST_NOTIFICATIONS
76+
*/
3277
fun Context.showNotification(
3378
title: String,
3479
content: String,

CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/CredentialProviderService.kt

Lines changed: 123 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.example.android.authentication.myvault.data
22

33
import android.annotation.SuppressLint
4-
import android.util.Log
54
import androidx.credentials.SignalAllAcceptedCredentialIdsRequest
65
import androidx.credentials.SignalCurrentUserDetailsRequest
76
import androidx.credentials.SignalUnknownCredentialRequest
@@ -16,58 +15,152 @@ import com.example.android.authentication.myvault.NAME
1615
import com.example.android.authentication.myvault.R
1716
import com.example.android.authentication.myvault.USER_ID
1817
import com.example.android.authentication.myvault.showNotification
19-
import kotlinx.coroutines.runBlocking
18+
import kotlinx.coroutines.Dispatchers
19+
import kotlinx.coroutines.launch
20+
import kotlinx.coroutines.withContext
2021
import org.json.JSONArray
2122
import org.json.JSONObject
2223

24+
/**
25+
* A service that listens to credential provider events triggered by the Android system
26+
* or other credential providers.
27+
*
28+
* This service is responsible for handling signals related to credential state changes,
29+
* such as when a credential is no longer valid, when a list of accepted credentials
30+
* is updated, or when current user details change. It updates the local data source
31+
* accordingly and notifies the user about these changes via system notifications.
32+
*
33+
* This service directly interacts with {@link AppDependencies#credentialsDataSource}
34+
* to persist changes and uses {@link AppDependencies#coroutineScope} for performing
35+
* background operations.
36+
*
37+
* @see CredentialProviderEventsService
38+
* @see ProviderSignalCredentialStateRequest
39+
* @see AppDependencies
40+
*/
2341
class CredentialProviderService: CredentialProviderEventsService() {
2442
private val dataSource = AppDependencies.credentialsDataSource
43+
private val coroutineScope = AppDependencies.coroutineScope
2544

45+
/**
46+
* Called when the system or another credential provider signals a change in credential state.
47+
*
48+
* This method inspects the type of [ProviderSignalCredentialStateRequest] and delegates
49+
* to the appropriate handler function to update the local data store and show a notification.
50+
* After processing the signal, {@link ProviderSignalCredentialStateCallback#onSignalConsumed()}
51+
* is called to acknowledge receipt of the signal.
52+
*
53+
* The {@link SuppressLint("RestrictedApi")} annotation is used because this method
54+
* interacts with APIs from the {@code androidx.credentials} library that might be
55+
* marked as restricted for extension by library developers.
56+
*
57+
* @param request The request containing details about the credential state signal.
58+
* @param callback The callback to be invoked after the signal has been processed.
59+
*/
2660
@SuppressLint("RestrictedApi")
2761
override fun onSignalCredentialStateRequest(
2862
request: ProviderSignalCredentialStateRequest,
2963
callback: ProviderSignalCredentialStateCallback,
3064
) {
3165
when (request.callingRequest) {
3266
is SignalUnknownCredentialRequest -> {
33-
handleUnknownCredentialRequest(request.callingRequest.requestJson)
34-
showNotification(
35-
getString(R.string.credential_deletion),
36-
getString(R.string.unknown_signal_message)
67+
updateDataOnSignalAndShowNotification(
68+
handleRequest = ::handleUnknownCredentialRequest,
69+
requestJson = request.callingRequest.requestJson,
70+
notificationTitle = getString(R.string.credential_deletion),
71+
notificationContent = getString(R.string.unknown_signal_message)
3772
)
3873
}
3974

4075
is SignalAllAcceptedCredentialIdsRequest -> {
41-
handleAcceptedCredentialsRequest(request.callingRequest.requestJson)
42-
showNotification(
43-
getString(R.string.credentials_list_updation),
44-
getString(R.string.all_accepted_signal_message)
76+
updateDataOnSignalAndShowNotification(
77+
handleRequest = ::handleAcceptedCredentialsRequest,
78+
requestJson = request.callingRequest.requestJson,
79+
notificationTitle = getString(R.string.credentials_list_updation),
80+
notificationContent = getString(R.string.all_accepted_signal_message)
4581
)
4682
}
83+
4784
is SignalCurrentUserDetailsRequest -> {
48-
handleCurrentUserDetailRequest(request.callingRequest.requestJson)
49-
showNotification(
50-
getString(R.string.user_details_updation),
51-
getString(R.string.current_user_signal_message)
85+
updateDataOnSignalAndShowNotification(
86+
handleRequest = ::handleCurrentUserDetailRequest,
87+
requestJson = request.callingRequest.requestJson,
88+
notificationTitle = getString(R.string.user_details_updation),
89+
notificationContent = getString(R.string.current_user_signal_message)
5290
)
5391
}
92+
5493
else -> { }
5594
}
5695

5796
callback.onSignalConsumed()
5897
}
5998

60-
private fun handleUnknownCredentialRequest(requestJson: String) = runBlocking {
99+
/**
100+
* A helper function to asynchronously handle a credential state update request,
101+
* update the data source, and then show a system notification on the main thread.
102+
*
103+
* @param handleRequest A suspend function that takes the request JSON string and processes it.
104+
* This function is responsible for interacting with the data source.
105+
* @param requestJson The JSON string payload from the original credential signal request.
106+
* @param notificationTitle The title to be used for the system notification.
107+
* @param notificationContent The content text for the system notification.
108+
*/
109+
private fun updateDataOnSignalAndShowNotification(
110+
handleRequest: suspend (String) -> Unit,
111+
requestJson: String,
112+
notificationTitle: String,
113+
notificationContent: String,
114+
) {
115+
coroutineScope.launch {
116+
handleRequest(requestJson)
117+
withContext(Dispatchers.Main) {
118+
showNotification(
119+
title = notificationTitle,
120+
content = notificationContent,
121+
)
122+
}
123+
}
124+
}
125+
126+
/**
127+
* Handles a [SignalUnknownCredentialRequest] by parsing the credential ID
128+
* from the request JSON and attempting to hide the corresponding passkey in the data source.
129+
*
130+
* "Hiding" a passkey typically means marking it as inactive or not to be suggested
131+
* for autofill, often because the system has indicated it's no longer valid
132+
* (e.g., deleted from the authenticator).
133+
*
134+
* @param requestJson The JSON string payload from the [SignalUnknownCredentialRequest].
135+
* Expected to contain a {@code CREDENTIAL_ID}.
136+
*/
137+
private suspend fun handleUnknownCredentialRequest(requestJson: String) {
61138
val credentialId = JSONObject(requestJson).getString(CREDENTIAL_ID)
62139
dataSource.getPasskey(credentialId)?.let {
63140
dataSource.hidePasskey(it)
64141
}
65142
}
66143

67-
private fun handleAcceptedCredentialsRequest(requestJson: String) = runBlocking {
144+
/**
145+
* Handles a {@link SignalAllAcceptedCredentialIdsRequest} by synchronizing the visibility
146+
* state of passkeys for a specific user.
147+
*
148+
* It retrieves all current passkeys for the user from the data source. Then, it compares
149+
* this list against the list of accepted credential IDs provided in the signal.
150+
* Passkeys whose IDs are in the accepted list are unhidden (made active).
151+
* Passkeys whose IDs are not in the accepted list are hidden (made inactive).
152+
*
153+
* This is useful for scenarios where the system provides an authoritative list of
154+
* credentials that are currently valid or preferred for a user.
155+
*
156+
* @param requestJson The JSON string payload from the {@link SignalAllAcceptedCredentialIdsRequest}.
157+
* Expected to contain a {@code USER_ID} and {@code ACCEPTED_CREDENTIAL_IDS}
158+
* (which can be a string or a JSON array of strings).
159+
*/
160+
private suspend fun handleAcceptedCredentialsRequest(requestJson: String) {
68161
val request = JSONObject(requestJson)
69162
val userId = request.getString(USER_ID)
70-
val listCurrentPasskeysForUser = dataSource.getPasskeyForUser(userId) ?: emptyList()
163+
val listCurrentPasskeysForUser = dataSource.getAllPasskeysForUser(userId) ?: emptyList()
71164
val listAllAcceptedCredIds = mutableListOf<String>()
72165
when (val value = request.get(ACCEPTED_CREDENTIAL_IDS)) {
73166
is String -> listAllAcceptedCredIds.add(value)
@@ -91,12 +184,23 @@ class CredentialProviderService: CredentialProviderEventsService() {
91184
}
92185
}
93186

94-
private fun handleCurrentUserDetailRequest(requestJson: String) = runBlocking {
187+
/**
188+
* Handles a {@link SignalCurrentUserDetailsRequest} by updating the username and display name
189+
* for all passkeys associated with a given user ID.
190+
*
191+
* This is useful when the user's profile information (like name or display name)
192+
* changes elsewhere, and the credential provider needs to reflect these changes
193+
* in its stored passkey data.
194+
*
195+
* @param requestJson The JSON string payload from the {@link SignalCurrentUserDetailsRequest}.
196+
* Expected to contain {@code USER_ID}, {@code NAME}, and {@code DISPLAY_NAME}.
197+
*/
198+
private suspend fun handleCurrentUserDetailRequest(requestJson: String) {
95199
val request = JSONObject(requestJson)
96200
val userId = request.getString(USER_ID)
97201
val updatedName = request.getString(NAME)
98202
val updatedDisplayName = request.getString(DISPLAY_NAME)
99-
val listPasskeys = dataSource.getPasskeyForUser(userId) ?: emptyList()
203+
val listPasskeys = dataSource.getAllPasskeysForUser(userId) ?: emptyList()
100204
// Update user details for each passkey
101205
for (key in listPasskeys) {
102206
val newPasskeyItem = key.copy(username = updatedName, displayName = updatedDisplayName)

CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/CredentialsDataSource.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,8 +127,8 @@ class CredentialsDataSource(
127127
return myVaultDao.getPasskey(credId)
128128
}
129129

130-
fun getPasskeyForUser(userId: String): List<PasskeyItem>? {
131-
return myVaultDao.getPasskeysForUser(userId)
130+
fun getAllPasskeysForUser(userId: String): List<PasskeyItem>? {
131+
return myVaultDao.getAllPasskeysForUser(userId)
132132
}
133133

134134
suspend fun hidePasskey(passkey: PasskeyItem) {

CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/CredentialsRepository.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,9 @@ class CredentialsRepository(
228228
val credentials = credentialsDataSource.credentialsForSite(request.rpId) ?: return false
229229

230230
val passkeys = credentials.passkeys
231-
for (passkey in passkeys) {
232-
if (!passkey.hidden) {
231+
passkeys
232+
.filter { !it.hidden }
233+
.forEach { passkey ->
233234
val data = Bundle()
234235
data.putString("requestJson", option.requestJson)
235236
data.putString("credId", passkey.credId)
@@ -259,7 +260,6 @@ class CredentialsRepository(
259260
// Add the entry to the response builder.
260261
responseBuilder.addCredentialEntry(entry)
261262
}
262-
}
263263
} catch (e: IOException) {
264264
return false
265265
}

CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/room/MyVaultDatabase.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,6 @@ interface MyVaultDao {
8989
@Query("SELECT * from passkeys WHERE credId = :credId")
9090
fun getPasskey(credId: String): PasskeyItem?
9191

92-
@Query("SELECT * from passkeys WHERE uid = :userId and hidden = false")
93-
fun getPasskeysForUser(userId: String): List<PasskeyItem>?
92+
@Query("SELECT * from passkeys WHERE uid = :userId")
93+
fun getAllPasskeysForUser(userId: String): List<PasskeyItem>?
9494
}

CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/ui/MainActivity.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import androidx.activity.ComponentActivity
2020
import androidx.activity.compose.setContent
2121
import androidx.activity.enableEdgeToEdge
2222
import androidx.core.view.WindowCompat
23+
import com.example.android.authentication.myvault.R
2324
import com.example.android.authentication.myvault.createNotificationChannel
2425
import com.example.android.authentication.myvault.ui.theme.MyVaultTheme
2526

@@ -28,8 +29,8 @@ class MainActivity : ComponentActivity() {
2829
enableEdgeToEdge()
2930
super.onCreate(savedInstanceState)
3031
createNotificationChannel(
31-
"Signal API notification channel",
32-
"Notification channel used for testing Signal APIs. Apps pushes a notification if a Signal from RP is received"
32+
getString(R.string.notification_channel_name),
33+
getString(R.string.notification_channel_description),
3334
)
3435

3536
WindowCompat.setDecorFitsSystemWindows(window, false)

CredentialProvider/MyVault/app/src/main/res/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,6 @@
7575
<string name="all_accepted_signal_message">The provider received the AcceptedCredentialIds request and thus updated the list of user credentials</string>
7676
<string name="user_details_updation">User details updated successfully</string>
7777
<string name="current_user_signal_message">The provider received the CurrentUserDetails request and thus updated the user details in the credential</string>
78+
<string name="notification_channel_description">Notification channel used for testing Signal APIs. Apps pushes a notification if a Signal from RP is received</string>
79+
<string name="notification_channel_name">Signal API notification channel</string>
7880
</resources>

CredentialProvider/MyVault/gradle/libs.versions.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ espressoCore = "3.6.1"
99
lifecycleRuntime = "2.8.7"
1010
activityCompose = "1.10.0"
1111
composeBom = "2025.02.00"
12-
credentials = "1.6.0-alpha04"
12+
credentials = "1.6.0-alpha05"
1313
room = "2.7.2"
1414
biometrics = "1.2.0-alpha05"
1515
accompanist = "0.28.0"

0 commit comments

Comments
 (0)