Skip to content

Commit 9ad3dbe

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

File tree

9 files changed

+239
-57
lines changed

9 files changed

+239
-57
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,
Lines changed: 174 additions & 47 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,91 +15,219 @@ 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 {
61-
val credentialId = JSONObject(requestJson).getString(CREDENTIAL_ID)
62-
dataSource.getPasskey(credentialId)?.let {
63-
dataSource.hidePasskey(it)
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) -> Boolean,
111+
requestJson: String,
112+
notificationTitle: String,
113+
notificationContent: String,
114+
) {
115+
coroutineScope.launch {
116+
val success = handleRequest(requestJson)
117+
withContext(Dispatchers.Main) {
118+
if (success) {
119+
showNotification(
120+
title = notificationTitle,
121+
content = notificationContent,
122+
)
123+
}
124+
}
125+
}
126+
}
127+
128+
/**
129+
* Handles a [SignalUnknownCredentialRequest] by parsing the credential ID
130+
* from the request JSON and attempting to hide the corresponding passkey in the data source.
131+
*
132+
* "Hiding" a passkey typically means marking it as inactive or not to be suggested
133+
* for autofill, often because the system has indicated it's no longer valid
134+
* (e.g., deleted from the authenticator).
135+
*
136+
* @param requestJson The JSON string payload from the [SignalUnknownCredentialRequest].
137+
* Expected to contain a {@code CREDENTIAL_ID}.
138+
*/
139+
private suspend fun handleUnknownCredentialRequest(requestJson: String): Boolean {
140+
try {
141+
val credentialId = JSONObject(requestJson).getString(CREDENTIAL_ID)
142+
dataSource.getPasskey(credentialId)?.let {
143+
dataSource.hidePasskey(it)
144+
}
145+
return true
146+
} catch (e: Exception) {
147+
e.printStackTrace()
148+
return false
64149
}
65150
}
66151

67-
private fun handleAcceptedCredentialsRequest(requestJson: String) = runBlocking {
68-
val request = JSONObject(requestJson)
69-
val userId = request.getString(USER_ID)
70-
val listCurrentPasskeysForUser = dataSource.getPasskeyForUser(userId) ?: emptyList()
71-
val listAllAcceptedCredIds = mutableListOf<String>()
72-
when (val value = request.get(ACCEPTED_CREDENTIAL_IDS)) {
73-
is String -> listAllAcceptedCredIds.add(value)
74-
is JSONArray -> {
75-
for (i in 0 until value.length()) {
76-
val item = value.get(i)
77-
if (item is String) {
78-
listAllAcceptedCredIds.add(item)
152+
/**
153+
* Handles a {@link SignalAllAcceptedCredentialIdsRequest} by synchronizing the visibility
154+
* state of passkeys for a specific user.
155+
*
156+
* It retrieves all current passkeys for the user from the data source. Then, it compares
157+
* this list against the list of accepted credential IDs provided in the signal.
158+
* Passkeys whose IDs are in the accepted list are unhidden (made active).
159+
* Passkeys whose IDs are not in the accepted list are hidden (made inactive).
160+
*
161+
* This is useful for scenarios where the system provides an authoritative list of
162+
* credentials that are currently valid or preferred for a user.
163+
*
164+
* @param requestJson The JSON string payload from the {@link SignalAllAcceptedCredentialIdsRequest}.
165+
* Expected to contain a {@code USER_ID} and {@code ACCEPTED_CREDENTIAL_IDS}
166+
* (which can be a string or a JSON array of strings).
167+
*/
168+
private suspend fun handleAcceptedCredentialsRequest(requestJson: String): Boolean {
169+
try {
170+
val request = JSONObject(requestJson)
171+
val userId = request.getString(USER_ID)
172+
val listCurrentPasskeysForUser = dataSource.getAllPasskeysForUser(userId) ?: emptyList()
173+
val listAllAcceptedCredIds = mutableListOf<String>()
174+
when (val value = request.get(ACCEPTED_CREDENTIAL_IDS)) {
175+
is String -> listAllAcceptedCredIds.add(value)
176+
is JSONArray -> {
177+
for (i in 0 until value.length()) {
178+
val item = value.get(i)
179+
if (item is String) {
180+
listAllAcceptedCredIds.add(item)
181+
}
79182
}
80183
}
184+
185+
else -> { /*do nothing*/
186+
}
81187
}
82-
else -> { /*do nothing*/ }
83-
}
84188

85-
for (key in listCurrentPasskeysForUser) {
86-
if (listAllAcceptedCredIds.contains(key.credId)) {
87-
dataSource.unhidePasskey(key)
88-
} else {
89-
dataSource.hidePasskey(key)
189+
for (key in listCurrentPasskeysForUser) {
190+
if (listAllAcceptedCredIds.contains(key.credId)) {
191+
dataSource.unhidePasskey(key)
192+
} else {
193+
dataSource.hidePasskey(key)
194+
}
90195
}
196+
return true
197+
} catch (e: Exception) {
198+
e.printStackTrace()
199+
return false
91200
}
92201
}
93202

94-
private fun handleCurrentUserDetailRequest(requestJson: String) = runBlocking {
95-
val request = JSONObject(requestJson)
96-
val userId = request.getString(USER_ID)
97-
val updatedName = request.getString(NAME)
98-
val updatedDisplayName = request.getString(DISPLAY_NAME)
99-
val listPasskeys = dataSource.getPasskeyForUser(userId) ?: emptyList()
100-
// Update user details for each passkey
101-
for (key in listPasskeys) {
102-
val newPasskeyItem = key.copy(username = updatedName, displayName = updatedDisplayName)
103-
dataSource.updatePasskey(newPasskeyItem)
203+
/**
204+
* Handles a {@link SignalCurrentUserDetailsRequest} by updating the username and display name
205+
* for all passkeys associated with a given user ID.
206+
*
207+
* This is useful when the user's profile information (like name or display name)
208+
* changes elsewhere, and the credential provider needs to reflect these changes
209+
* in its stored passkey data.
210+
*
211+
* @param requestJson The JSON string payload from the {@link SignalCurrentUserDetailsRequest}.
212+
* Expected to contain {@code USER_ID}, {@code NAME}, and {@code DISPLAY_NAME}.
213+
*/
214+
private suspend fun handleCurrentUserDetailRequest(requestJson: String): Boolean {
215+
try {
216+
val request = JSONObject(requestJson)
217+
val userId = request.getString(USER_ID)
218+
val updatedName = request.getString(NAME)
219+
val updatedDisplayName = request.getString(DISPLAY_NAME)
220+
val listPasskeys = dataSource.getAllPasskeysForUser(userId) ?: emptyList()
221+
// Update user details for each passkey
222+
for (key in listPasskeys) {
223+
val newPasskeyItem =
224+
key.copy(username = updatedName, displayName = updatedDisplayName)
225+
dataSource.updatePasskey(newPasskeyItem)
226+
}
227+
return true
228+
} catch (e: Exception) {
229+
e.printStackTrace()
230+
return false
104231
}
105232
}
106233
}

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
}

0 commit comments

Comments
 (0)