1
1
package com.example.android.authentication.myvault.data
2
2
3
3
import android.annotation.SuppressLint
4
- import android.util.Log
5
4
import androidx.credentials.SignalAllAcceptedCredentialIdsRequest
6
5
import androidx.credentials.SignalCurrentUserDetailsRequest
7
6
import androidx.credentials.SignalUnknownCredentialRequest
@@ -16,91 +15,209 @@ import com.example.android.authentication.myvault.NAME
16
15
import com.example.android.authentication.myvault.R
17
16
import com.example.android.authentication.myvault.USER_ID
18
17
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
20
21
import org.json.JSONArray
21
22
import org.json.JSONObject
22
23
24
+ /* *
25
+ * A service that listens to credential provider events triggered by the relying parties
26
+ *
27
+ * This service is responsible for handling signals related to credential state changes in the RPs,
28
+ * such as when a credential is no longer valid, when a list of accepted credentials is accepted,
29
+ * or when current user details change for credentials
30
+ */
23
31
class CredentialProviderService : CredentialProviderEventsService () {
24
32
private val dataSource = AppDependencies .credentialsDataSource
33
+ private val coroutineScope = AppDependencies .coroutineScope
25
34
35
+ /* *
36
+ * Called when the system or another credential provider signals a change in credential state.
37
+ *
38
+ * This method inspects the type of [ProviderSignalCredentialStateRequest] and delegates
39
+ * to the appropriate handler function to update the local data store and show a notification.
40
+ * After processing the signal, {@link ProviderSignalCredentialStateCallback#onSignalConsumed()}
41
+ * is called to acknowledge receipt of the signal.
42
+ *
43
+ * The {@link SuppressLint("RestrictedApi")} annotation is used because this method
44
+ * interacts with APIs from the {@code androidx.credentials} library that might be
45
+ * marked as restricted for extension by library developers.
46
+ *
47
+ * @param request The request containing details about the credential state signal.
48
+ * @param callback The callback to be invoked after the signal has been processed.
49
+ */
26
50
@SuppressLint(" RestrictedApi" )
27
51
override fun onSignalCredentialStateRequest (
28
52
request : ProviderSignalCredentialStateRequest ,
29
53
callback : ProviderSignalCredentialStateCallback ,
30
54
) {
31
55
when (request.callingRequest) {
32
56
is SignalUnknownCredentialRequest -> {
33
- handleUnknownCredentialRequest(request.callingRequest.requestJson)
34
- showNotification(
35
- getString(R .string.credential_deletion),
36
- getString(R .string.unknown_signal_message)
57
+ updateDataOnSignalAndShowNotification(
58
+ handleRequest = ::handleUnknownCredentialRequest,
59
+ requestJson = request.callingRequest.requestJson,
60
+ notificationTitle = getString(R .string.credential_deletion),
61
+ notificationContent = getString(R .string.unknown_signal_message)
37
62
)
38
63
}
39
64
40
65
is SignalAllAcceptedCredentialIdsRequest -> {
41
- handleAcceptedCredentialsRequest(request.callingRequest.requestJson)
42
- showNotification(
43
- getString(R .string.credentials_list_updation),
44
- getString(R .string.all_accepted_signal_message)
66
+ updateDataOnSignalAndShowNotification(
67
+ handleRequest = ::handleAcceptedCredentialsRequest,
68
+ requestJson = request.callingRequest.requestJson,
69
+ notificationTitle = getString(R .string.credentials_list_updation),
70
+ notificationContent = getString(R .string.all_accepted_signal_message)
45
71
)
46
72
}
73
+
47
74
is SignalCurrentUserDetailsRequest -> {
48
- handleCurrentUserDetailRequest(request.callingRequest.requestJson)
49
- showNotification(
50
- getString(R .string.user_details_updation),
51
- getString(R .string.current_user_signal_message)
75
+ updateDataOnSignalAndShowNotification(
76
+ handleRequest = ::handleCurrentUserDetailRequest,
77
+ requestJson = request.callingRequest.requestJson,
78
+ notificationTitle = getString(R .string.user_details_updation),
79
+ notificationContent = getString(R .string.current_user_signal_message)
52
80
)
53
81
}
82
+
54
83
else -> { }
55
84
}
56
85
57
86
callback.onSignalConsumed()
58
87
}
59
88
60
- private fun handleUnknownCredentialRequest (requestJson : String ) = runBlocking {
61
- val credentialId = JSONObject (requestJson).getString(CREDENTIAL_ID )
62
- dataSource.getPasskey(credentialId)?.let {
63
- dataSource.hidePasskey(it)
89
+ /* *
90
+ * A helper function to asynchronously handle a credential state update request,
91
+ * update the data source, and then show a system notification on the main thread.
92
+ *
93
+ * @param handleRequest A suspend function that takes the request JSON string and processes it.
94
+ * This function is responsible for interacting with the data source.
95
+ * @param requestJson The JSON string payload from the original credential signal request.
96
+ * @param notificationTitle The title to be used for the system notification.
97
+ * @param notificationContent The content text for the system notification.
98
+ */
99
+ private fun updateDataOnSignalAndShowNotification (
100
+ handleRequest : suspend (String ) -> Boolean ,
101
+ requestJson : String ,
102
+ notificationTitle : String ,
103
+ notificationContent : String ,
104
+ ) {
105
+ coroutineScope.launch {
106
+ val success = handleRequest(requestJson)
107
+ withContext(Dispatchers .Main ) {
108
+ if (success) {
109
+ showNotification(
110
+ title = notificationTitle,
111
+ content = notificationContent,
112
+ )
113
+ }
114
+ }
115
+ }
116
+ }
117
+
118
+ /* *
119
+ * Handles a [SignalUnknownCredentialRequest] by parsing the credential ID
120
+ * from the request JSON and attempting to hide the corresponding passkey in the data source.
121
+ *
122
+ * "Hiding" a passkey typically means marking it as inactive or not to be suggested
123
+ * for autofill, often because the system has indicated it's no longer valid
124
+ * (e.g., deleted from the authenticator).
125
+ *
126
+ * @param requestJson The JSON string payload from the [SignalUnknownCredentialRequest].
127
+ * Expected to contain a {@code CREDENTIAL_ID}.
128
+ */
129
+ private suspend fun handleUnknownCredentialRequest (requestJson : String ): Boolean {
130
+ try {
131
+ val credentialId = JSONObject (requestJson).getString(CREDENTIAL_ID )
132
+ dataSource.getPasskey(credentialId)?.let {
133
+ dataSource.hidePasskey(it)
134
+ }
135
+ return true
136
+ } catch (e: Exception ) {
137
+ e.printStackTrace()
138
+ return false
64
139
}
65
140
}
66
141
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)
142
+ /* *
143
+ * Handles a {@link SignalAllAcceptedCredentialIdsRequest} by synchronizing the visibility
144
+ * state of passkeys for a specific user.
145
+ *
146
+ * It retrieves all current passkeys for the user from the data source. Then, it compares
147
+ * this list against the list of accepted credential IDs provided in the signal.
148
+ * Passkeys whose IDs are in the accepted list are unhidden (made active).
149
+ * Passkeys whose IDs are not in the accepted list are hidden (made inactive).
150
+ *
151
+ * This is useful for scenarios where the system provides an authoritative list of
152
+ * credentials that are currently valid or preferred for a user.
153
+ *
154
+ * @param requestJson The JSON string payload from the {@link SignalAllAcceptedCredentialIdsRequest}.
155
+ * Expected to contain a {@code USER_ID} and {@code ACCEPTED_CREDENTIAL_IDS}
156
+ * (which can be a string or a JSON array of strings).
157
+ */
158
+ private suspend fun handleAcceptedCredentialsRequest (requestJson : String ): Boolean {
159
+ try {
160
+ val request = JSONObject (requestJson)
161
+ val userId = request.getString(USER_ID )
162
+ val listCurrentPasskeysForUser = dataSource.getAllPasskeysForUser(userId) ? : emptyList()
163
+ val listAllAcceptedCredIds = mutableListOf<String >()
164
+ when (val value = request.get(ACCEPTED_CREDENTIAL_IDS )) {
165
+ is String -> listAllAcceptedCredIds.add(value)
166
+ is JSONArray -> {
167
+ for (i in 0 until value.length()) {
168
+ val item = value.get(i)
169
+ if (item is String ) {
170
+ listAllAcceptedCredIds.add(item)
171
+ }
79
172
}
80
173
}
174
+
175
+ else -> { /* do nothing*/
176
+ }
81
177
}
82
- else -> { /* do nothing*/ }
83
- }
84
178
85
- for (key in listCurrentPasskeysForUser) {
86
- if (listAllAcceptedCredIds.contains(key.credId)) {
87
- dataSource.unhidePasskey(key)
88
- } else {
89
- dataSource.hidePasskey(key)
179
+ for (key in listCurrentPasskeysForUser) {
180
+ if (listAllAcceptedCredIds.contains(key.credId)) {
181
+ dataSource.unhidePasskey(key)
182
+ } else {
183
+ dataSource.hidePasskey(key)
184
+ }
90
185
}
186
+ return true
187
+ } catch (e: Exception ) {
188
+ e.printStackTrace()
189
+ return false
91
190
}
92
191
}
93
192
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)
193
+ /* *
194
+ * Handles a {@link SignalCurrentUserDetailsRequest} by updating the username and display name
195
+ * for all passkeys associated with a given user ID.
196
+ *
197
+ * This is useful when the user's profile information (like name or display name)
198
+ * changes elsewhere, and the credential provider needs to reflect these changes
199
+ * in its stored passkey data.
200
+ *
201
+ * @param requestJson The JSON string payload from the {@link SignalCurrentUserDetailsRequest}.
202
+ * Expected to contain {@code USER_ID}, {@code NAME}, and {@code DISPLAY_NAME}.
203
+ */
204
+ private suspend fun handleCurrentUserDetailRequest (requestJson : String ): Boolean {
205
+ try {
206
+ val request = JSONObject (requestJson)
207
+ val userId = request.getString(USER_ID )
208
+ val updatedName = request.getString(NAME )
209
+ val updatedDisplayName = request.getString(DISPLAY_NAME )
210
+ val listPasskeys = dataSource.getAllPasskeysForUser(userId) ? : emptyList()
211
+ // Update user details for each passkey
212
+ for (key in listPasskeys) {
213
+ val newPasskeyItem =
214
+ key.copy(username = updatedName, displayName = updatedDisplayName)
215
+ dataSource.updatePasskey(newPasskeyItem)
216
+ }
217
+ return true
218
+ } catch (e: Exception ) {
219
+ e.printStackTrace()
220
+ return false
104
221
}
105
222
}
106
223
}
0 commit comments