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,211 @@ 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
+ // Currently hiding the passkey on UnknownSignal for testing purpose
134
+ // If the business logc requires deletion, please add deletion code instead
135
+ dataSource.hidePasskey(it)
136
+ }
137
+ return true
138
+ } catch (e: Exception ) {
139
+ e.printStackTrace()
140
+ return false
64
141
}
65
142
}
66
143
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)
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 ): Boolean {
161
+ try {
162
+ val request = JSONObject (requestJson)
163
+ val userId = request.getString(USER_ID )
164
+ val listCurrentPasskeysForUser = dataSource.getAllPasskeysForUser(userId) ? : emptyList()
165
+ val listAllAcceptedCredIds = mutableListOf<String >()
166
+ when (val value = request.get(ACCEPTED_CREDENTIAL_IDS )) {
167
+ is String -> listAllAcceptedCredIds.add(value)
168
+ is JSONArray -> {
169
+ for (i in 0 until value.length()) {
170
+ val item = value.get(i)
171
+ if (item is String ) {
172
+ listAllAcceptedCredIds.add(item)
173
+ }
79
174
}
80
175
}
176
+
177
+ else -> { /* do nothing*/
178
+ }
81
179
}
82
- else -> { /* do nothing*/ }
83
- }
84
180
85
- for (key in listCurrentPasskeysForUser) {
86
- if (listAllAcceptedCredIds.contains(key.credId)) {
87
- dataSource.unhidePasskey(key)
88
- } else {
89
- dataSource.hidePasskey(key)
181
+ for (key in listCurrentPasskeysForUser) {
182
+ if (listAllAcceptedCredIds.contains(key.credId)) {
183
+ dataSource.unhidePasskey(key)
184
+ } else {
185
+ dataSource.hidePasskey(key)
186
+ }
90
187
}
188
+ return true
189
+ } catch (e: Exception ) {
190
+ e.printStackTrace()
191
+ return false
91
192
}
92
193
}
93
194
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)
195
+ /* *
196
+ * Handles a {@link SignalCurrentUserDetailsRequest} by updating the username and display name
197
+ * for all passkeys associated with a given user ID.
198
+ *
199
+ * This is useful when the user's profile information (like name or display name)
200
+ * changes elsewhere, and the credential provider needs to reflect these changes
201
+ * in its stored passkey data.
202
+ *
203
+ * @param requestJson The JSON string payload from the {@link SignalCurrentUserDetailsRequest}.
204
+ * Expected to contain {@code USER_ID}, {@code NAME}, and {@code DISPLAY_NAME}.
205
+ */
206
+ private suspend fun handleCurrentUserDetailRequest (requestJson : String ): Boolean {
207
+ try {
208
+ val request = JSONObject (requestJson)
209
+ val userId = request.getString(USER_ID )
210
+ val updatedName = request.getString(NAME )
211
+ val updatedDisplayName = request.getString(DISPLAY_NAME )
212
+ val listPasskeys = dataSource.getAllPasskeysForUser(userId) ? : emptyList()
213
+ // Update user details for each passkey
214
+ for (key in listPasskeys) {
215
+ val newPasskeyItem =
216
+ key.copy(username = updatedName, displayName = updatedDisplayName)
217
+ dataSource.updatePasskey(newPasskeyItem)
218
+ }
219
+ return true
220
+ } catch (e: Exception ) {
221
+ e.printStackTrace()
222
+ return false
104
223
}
105
224
}
106
225
}
0 commit comments