Skip to content

Commit e3ade7e

Browse files
author
Neelansh Sahai
committed
Add Signal API implementation (Provider)
Change-Id: I5284a17da9791cd64e8d7ea8befe96dd5c770e76
1 parent 851a04f commit e3ade7e

File tree

17 files changed

+281
-34
lines changed

17 files changed

+281
-34
lines changed

CredentialProvider/MyVault/app/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ plugins {
1717
alias(libs.plugins.android.application)
1818
alias(libs.plugins.jetbrains.kotlin.android)
1919
alias(libs.plugins.devtools.ksp)
20+
alias(libs.plugins.compose.compiler)
2021
}
2122

2223
android {
@@ -77,7 +78,11 @@ dependencies {
7778
implementation(libs.androidx.ui.graphics)
7879
implementation(libs.androidx.ui.tooling.preview)
7980
implementation(libs.androidx.material3)
81+
8082
implementation(libs.androidx.credential.manager)
83+
implementation(libs.provider.events)
84+
implementation(libs.provider.events.ps)
85+
8186
implementation(libs.androidx.room.ktx)
8287
implementation(libs.androidx.room.runtime)
8388
ksp(libs.androidx.room.compiler)

CredentialProvider/MyVault/app/src/main/AndroidManifest.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
xmlns:tools="http://schemas.android.com/tools">
1919

2020
<uses-permission android:name="android.permission.INTERNET" />
21+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
2122
<application
2223
android:name="com.example.android.authentication.myvault.MyVaultApplication"
2324
android:icon="@drawable/android_secure"
@@ -105,5 +106,16 @@
105106
android:name="android.credentials.provider"
106107
android:resource="@xml/provider" />
107108
</service>
109+
110+
<service android:name=".data.CredentialProviderService"
111+
android:enabled="true"
112+
android:exported="true"
113+
android:label="My Credential Provider"
114+
android:icon="@drawable/android_secure">
115+
<intent-filter>
116+
<action android:name="android.credentials.EVENTS_SERVICE_ACTION"/>
117+
</intent-filter>
118+
</service>
119+
108120
</application>
109121
</manifest>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.example.android.authentication.myvault
2+
3+
const val NOTIFICATION_CHANNEL_ID = "channel_id"
4+
const val NOTIFICATION_ID = 135
5+
const val CREDENTIAL_ID = "credentialId"
6+
const val USER_ID = "userId"
7+
const val ACCEPTED_CREDENTIAL_IDS = "allAcceptedCredentialIds"
8+
const val NAME = "name"
9+
const val DISPLAY_NAME = "displayName"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.example.android.authentication.myvault
2+
3+
import android.Manifest
4+
import android.app.NotificationChannel
5+
import android.app.NotificationManager
6+
import android.app.PendingIntent
7+
import android.content.ComponentName
8+
import android.content.Context
9+
import android.content.Intent
10+
import android.content.pm.PackageManager
11+
import androidx.core.app.ActivityCompat
12+
import androidx.core.app.NotificationCompat
13+
import androidx.core.app.NotificationManagerCompat
14+
import com.example.android.authentication.myvault.ui.MainActivity
15+
16+
fun Context.createNotificationChannel(
17+
channelName: String,
18+
channelDescription: String,
19+
) {
20+
val channel = NotificationChannel(
21+
NOTIFICATION_CHANNEL_ID,
22+
channelName,
23+
NotificationManager.IMPORTANCE_HIGH
24+
).apply {
25+
description = channelDescription
26+
}
27+
// Register the channel with the system.
28+
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
29+
notificationManager.createNotificationChannel(channel)
30+
}
31+
32+
fun Context.showNotification(
33+
title: String,
34+
content: String,
35+
) {
36+
val intent = Intent(this, MainActivity::class.java)
37+
val pendingIntent: PendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE)
38+
39+
val builder = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
40+
.setSmallIcon(R.drawable.android_secure)
41+
.setContentTitle(title)
42+
.setContentText(content)
43+
.setPriority(NotificationCompat.PRIORITY_MAX)
44+
.setContentIntent(pendingIntent)
45+
.setAutoCancel(true)
46+
47+
with(NotificationManagerCompat.from(this)) {
48+
if (ActivityCompat.checkSelfPermission(
49+
this@showNotification,
50+
Manifest.permission.POST_NOTIFICATIONS
51+
) != PackageManager.PERMISSION_GRANTED) {
52+
return@with
53+
}
54+
notify(NOTIFICATION_ID, builder.build())
55+
}
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package com.example.android.authentication.myvault.data
2+
3+
import android.annotation.SuppressLint
4+
import android.util.Log
5+
import androidx.credentials.SignalAllAcceptedCredentialIdsRequest
6+
import androidx.credentials.SignalCurrentUserDetailsRequest
7+
import androidx.credentials.SignalUnknownCredentialRequest
8+
import androidx.credentials.providerevents.service.CredentialProviderEventsService
9+
import androidx.credentials.providerevents.signal.ProviderSignalCredentialStateCallback
10+
import androidx.credentials.providerevents.signal.ProviderSignalCredentialStateRequest
11+
import com.example.android.authentication.myvault.ACCEPTED_CREDENTIAL_IDS
12+
import com.example.android.authentication.myvault.AppDependencies
13+
import com.example.android.authentication.myvault.CREDENTIAL_ID
14+
import com.example.android.authentication.myvault.DISPLAY_NAME
15+
import com.example.android.authentication.myvault.NAME
16+
import com.example.android.authentication.myvault.R
17+
import com.example.android.authentication.myvault.USER_ID
18+
import com.example.android.authentication.myvault.showNotification
19+
import kotlinx.coroutines.runBlocking
20+
import org.json.JSONArray
21+
import org.json.JSONObject
22+
23+
class CredentialProviderService: CredentialProviderEventsService() {
24+
private val dataSource = AppDependencies.credentialsDataSource
25+
26+
@SuppressLint("RestrictedApi")
27+
override fun onSignalCredentialStateRequest(
28+
request: ProviderSignalCredentialStateRequest,
29+
callback: ProviderSignalCredentialStateCallback,
30+
) {
31+
when (request.callingRequest) {
32+
is SignalUnknownCredentialRequest -> {
33+
handleUnknownCredentialRequest(request.callingRequest.requestJson)
34+
showNotification(
35+
getString(R.string.credential_deletion),
36+
getString(R.string.unknown_signal_message)
37+
)
38+
}
39+
40+
is SignalAllAcceptedCredentialIdsRequest -> {
41+
handleAcceptedCredentialsRequest(request.callingRequest.requestJson)
42+
showNotification(
43+
getString(R.string.credentials_list_updation),
44+
getString(R.string.all_accepted_signal_message)
45+
)
46+
}
47+
is SignalCurrentUserDetailsRequest -> {
48+
handleCurrentUserDetailRequest(request.callingRequest.requestJson)
49+
showNotification(
50+
getString(R.string.user_details_updation),
51+
getString(R.string.current_user_signal_message)
52+
)
53+
}
54+
else -> { }
55+
}
56+
57+
callback.onSignalConsumed()
58+
}
59+
60+
private fun handleUnknownCredentialRequest(requestJson: String) = runBlocking {
61+
val credentialId = JSONObject(requestJson).getString(CREDENTIAL_ID)
62+
dataSource.getPasskey(credentialId)?.let {
63+
dataSource.hidePasskey(it)
64+
}
65+
}
66+
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)
79+
}
80+
}
81+
}
82+
else -> { /*do nothing*/ }
83+
}
84+
85+
for (key in listCurrentPasskeysForUser) {
86+
if (listAllAcceptedCredIds.contains(key.credId)) {
87+
dataSource.unhidePasskey(key)
88+
} else {
89+
dataSource.hidePasskey(key)
90+
}
91+
}
92+
}
93+
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)
104+
}
105+
}
106+
}

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,26 @@ class CredentialsDataSource(
126126
fun getPasskey(credId: String): PasskeyItem? {
127127
return myVaultDao.getPasskey(credId)
128128
}
129+
130+
fun getPasskeyForUser(userId: String): List<PasskeyItem>? {
131+
return myVaultDao.getPasskeysForUser(userId)
132+
}
133+
134+
suspend fun hidePasskey(passkey: PasskeyItem) {
135+
myVaultDao.updatePasskey(passkey.copy(hidden = true))
136+
}
137+
138+
suspend fun unhidePasskey(passkey: PasskeyItem) {
139+
myVaultDao.updatePasskey(passkey.copy(hidden = false))
140+
}
141+
142+
fun updateListOfPasskeys() {
143+
144+
}
145+
146+
fun updateUserDetails() {
147+
148+
}
129149
}
130150

131151
data class PasswordMetaData(

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

Lines changed: 27 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -229,34 +229,36 @@ class CredentialsRepository(
229229

230230
val passkeys = credentials.passkeys
231231
for (passkey in passkeys) {
232-
val data = Bundle()
233-
data.putString("requestJson", option.requestJson)
234-
data.putString("credId", passkey.credId)
235-
236-
// Create a PendingIntent to launch the activity that will handle the passkey retrieval
237-
val pendingIntent = createNewPendingIntent(
238-
"",
239-
GET_PASSKEY_INTENT,
240-
data,
241-
)
232+
if (!passkey.hidden) {
233+
val data = Bundle()
234+
data.putString("requestJson", option.requestJson)
235+
data.putString("credId", passkey.credId)
236+
237+
// Create a PendingIntent to launch the activity that will handle the passkey retrieval
238+
val pendingIntent = createNewPendingIntent(
239+
"",
240+
GET_PASSKEY_INTENT,
241+
data,
242+
)
242243

243-
// Create a PublicKeyCredentialEntry object to represent the passkey
244-
val entryBuilder =
245-
configurePublicKeyCredentialEntryBuilder(passkey, pendingIntent, option)
244+
// Create a PublicKeyCredentialEntry object to represent the passkey
245+
val entryBuilder =
246+
configurePublicKeyCredentialEntryBuilder(passkey, pendingIntent, option)
247+
248+
// Configure biometric prompt data
249+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
250+
entryBuilder.setBiometricPromptData(
251+
BiometricPromptData(
252+
cryptoObject = null,
253+
allowedAuthenticators = allowedAuthenticator,
254+
),
255+
)
256+
}
246257

247-
// Configure biometric prompt data
248-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
249-
entryBuilder.setBiometricPromptData(
250-
BiometricPromptData(
251-
cryptoObject = null,
252-
allowedAuthenticators = allowedAuthenticator,
253-
),
254-
)
258+
val entry = entryBuilder.build()
259+
// Add the entry to the response builder.
260+
responseBuilder.addCredentialEntry(entry)
255261
}
256-
257-
val entry = entryBuilder.build()
258-
// Add the entry to the response builder.
259-
responseBuilder.addCredentialEntry(entry)
260262
}
261263
} catch (e: IOException) {
262264
return false

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import androidx.room.PrimaryKey
3131
* @property credPrivateKey The private key
3232
* @property siteId The ID of the site
3333
* @property lastUsedTimeMs The last time the passkey item was used
34+
* @property hidden Whether a passkey is hidden from the end user or not
3435
*/
3536
@Entity(
3637
tableName = "passkeys",
@@ -47,4 +48,5 @@ data class PasskeyItem(
4748
@ColumnInfo(name = "credPrivateKey") val credPrivateKey: String,
4849
@ColumnInfo(name = "siteId") val siteId: Long,
4950
@ColumnInfo(name = "lastUsedTimeMs") val lastUsedTimeMs: Long,
51+
@ColumnInfo(name = "hidden") val hidden: Boolean = false,
5052
)

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import kotlinx.coroutines.flow.Flow
3434
PasswordItem::class,
3535
PasskeyItem::class,
3636
],
37-
version = 7,
37+
version = 8,
3838
)
3939
abstract class MyVaultDatabase : RoomDatabase() {
4040
abstract fun myVaultDao(): MyVaultDao
@@ -88,4 +88,7 @@ interface MyVaultDao {
8888

8989
@Query("SELECT * from passkeys WHERE credId = :credId")
9090
fun getPasskey(credId: String): PasskeyItem?
91+
92+
@Query("SELECT * from passkeys WHERE uid = :userId and hidden = false")
93+
fun getPasskeysForUser(userId: String): List<PasskeyItem>?
9194
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,18 @@ 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.createNotificationChannel
2324
import com.example.android.authentication.myvault.ui.theme.MyVaultTheme
2425

2526
class MainActivity : ComponentActivity() {
2627
override fun onCreate(savedInstanceState: Bundle?) {
2728
enableEdgeToEdge()
2829
super.onCreate(savedInstanceState)
30+
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"
33+
)
34+
2935
WindowCompat.setDecorFitsSystemWindows(window, false)
3036
setContent {
3137
MyVaultTheme {

0 commit comments

Comments
 (0)