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,219 @@ 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 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
+ */
23
41
class CredentialProviderService : CredentialProviderEventsService () {
24
42
private val dataSource = AppDependencies .credentialsDataSource
43
+ private val coroutineScope = AppDependencies .coroutineScope
25
44
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
+ */
26
60
@SuppressLint(" RestrictedApi" )
27
61
override fun onSignalCredentialStateRequest (
28
62
request : ProviderSignalCredentialStateRequest ,
29
63
callback : ProviderSignalCredentialStateCallback ,
30
64
) {
31
65
when (request.callingRequest) {
32
66
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)
37
72
)
38
73
}
39
74
40
75
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)
45
81
)
46
82
}
83
+
47
84
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)
52
90
)
53
91
}
92
+
54
93
else -> { }
55
94
}
56
95
57
96
callback.onSignalConsumed()
58
97
}
59
98
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
64
149
}
65
150
}
66
151
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
+ }
79
182
}
80
183
}
184
+
185
+ else -> { /* do nothing*/
186
+ }
81
187
}
82
- else -> { /* do nothing*/ }
83
- }
84
188
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
+ }
90
195
}
196
+ return true
197
+ } catch (e: Exception ) {
198
+ e.printStackTrace()
199
+ return false
91
200
}
92
201
}
93
202
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
104
231
}
105
232
}
106
233
}
0 commit comments