Skip to content

Commit

Permalink
Presentation Activity
Browse files Browse the repository at this point in the history
Adding PresentationActivity to wallet, which will appear on-screen
with related presentation UI once the NfcEngagementHandler starts the
activity. This addition allows for in-person presentation with NFC
engagement.

Tested manually with 0/1/2 active credentials in wallet.

Signed-off-by: Suzanna Jiwani <[email protected]>
  • Loading branch information
suzannajiwani committed Feb 6, 2024
1 parent afab57f commit 4a1cf1c
Show file tree
Hide file tree
Showing 9 changed files with 714 additions and 81 deletions.
39 changes: 38 additions & 1 deletion wallet/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android">

<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="true" />
<uses-feature
android:name="android.hardware.nfc"
android:required="true" />

<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.NFC" />

<uses-permission android:name="android.permission.NFC"/>
<uses-feature android:name="android.hardware.nfc" android:required="true"/>
Expand Down Expand Up @@ -27,6 +42,28 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity
android:name=".PresentationActivity"
android:exported="true"
android:label="@string/app_name_presentation"
android:theme="@style/Theme.IdentityCredential">
</activity>

<service
android:name=".NfcEngagementHandler"
android:exported="true"
android:label="@string/nfc_engagement_service_desc"
android:permission="android.permission.BIND_NFC_SERVICE">
<intent-filter>
<action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
</intent-filter>

<meta-data
android:name="android.nfc.cardemulation.host_apdu_service"
android:resource="@xml/nfc_engagement_apdu_service" />
</service>

</application>

</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,10 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.android.identity.credential.Credential
import com.android.identity.issuance.CredentialExtensions.housekeeping
import com.android.identity.issuance.CredentialExtensions.refreshState
import com.android.identity.util.Logger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

class CredentialInformationViewModel : ViewModel() {
class CredentialInformationViewModel : ViewModel() {

companion object {
private const val TAG = "CredentialInformationViewModel"
Expand All @@ -27,7 +25,7 @@ class CredentialInformationViewModel : ViewModel() {
3,
30*24*3600,
application.androidKeystoreSecureArea,
"mdoc/MSO")
WalletApplication.AUTH_KEY_DOMAIN)
lastHousekeepingAt.value = System.currentTimeMillis()
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
/*
* Copyright (C) 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)

package com.android.identity_credential.wallet

import android.Manifest
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build
import android.os.Bundle
import android.view.ViewGroup
import android.widget.LinearLayout
Expand Down Expand Up @@ -114,44 +131,70 @@ class MainActivity : ComponentActivity() {
Manifest.permission.NFC to "NFC permission is required to operate"
))

private val blePermissionTracker: PermissionTracker = if (Build.VERSION.SDK_INT >= 31) {
PermissionTracker(this, mapOf(
Manifest.permission.BLUETOOTH_ADVERTISE to "This application requires Bluetooth " +
"advertising to send credential data",
Manifest.permission.BLUETOOTH_SCAN to "This application requires Bluetooth " +
"scanning to send credential data",
Manifest.permission.BLUETOOTH_CONNECT to "This application requires Bluetooth " +
"connection to send credential data"
))
} else {
PermissionTracker(this, mapOf(
Manifest.permission.ACCESS_FINE_LOCATION to "This application requires Bluetooth " +
"to send credential data"
))
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

application = getApplication() as WalletApplication

permissionTracker.updatePermissions()
blePermissionTracker.updatePermissions()
val requiredPermissions: List<String> = if (Build.VERSION.SDK_INT >= 31) {
listOf(Manifest.permission.BLUETOOTH_ADVERTISE, Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT)
} else {
listOf(Manifest.permission.ACCESS_FINE_LOCATION)
}

setContent {
IdentityCredentialTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "MainScreen") {
composable("MainScreen") {
MainScreen(navController)
}
composable("AboutScreen") {
AboutScreen(navController)
}
composable("AddToWalletScreen") {
AddToWalletScreen(navController)
}
composable("CredentialInfo/{credentialId}") { backStackEntry ->
CredentialInfoScreen(navController,
backStackEntry.arguments?.getString("credentialId")!!)
}
composable("CredentialInfo/{credentialId}/Details") { backStackEntry ->
CredentialDetailsScreen(navController,
backStackEntry.arguments?.getString("credentialId")!!)
}
composable("ProvisionCredentialScreen") {
ProvisionCredentialScreen(navController)
blePermissionTracker.PermissionCheck(permissions = requiredPermissions) {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "MainScreen") {
composable("MainScreen") {
MainScreen(navController)
}
composable("AboutScreen") {
AboutScreen(navController)
}
composable("AddToWalletScreen") {
AddToWalletScreen(navController)
}
composable("CredentialInfo/{credentialId}") { backStackEntry ->
CredentialInfoScreen(navController,
backStackEntry.arguments?.getString("credentialId")!!)
}
composable("CredentialInfo/{credentialId}/Details") { backStackEntry ->
CredentialDetailsScreen(navController,
backStackEntry.arguments?.getString("credentialId")!!)
}
composable("ProvisionCredentialScreen") {
ProvisionCredentialScreen(navController)
}
}
}
}

}
}
}
Expand Down Expand Up @@ -289,7 +332,6 @@ class MainActivity : ComponentActivity() {
fun MainScreenCredentialPager(navigation: NavHostController) {

Column() {

val credentialIds = application.credentialStore.listCredentials()
val pagerState = rememberPagerState(pageCount = {
credentialIds.size
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* Copyright (C) 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.android.identity_credential.wallet

import android.content.Context
import android.content.Intent
import android.nfc.cardemulation.HostApduService
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Vibrator
import androidx.core.content.ContextCompat
import com.android.identity.android.mdoc.engagement.NfcEngagementHelper
import com.android.identity.android.mdoc.transport.DataTransport
import com.android.identity.android.mdoc.transport.DataTransportOptions
import com.android.identity.internal.Util
import com.android.identity.securearea.SecureArea
import com.android.identity.util.Logger

class NfcEngagementHandler : HostApduService() {
companion object {
private val TAG = "NfcEngagementHandler"
}

private var engagementHelper: NfcEngagementHelper? = null

private val eDeviceKeyCurve = SecureArea.EC_CURVE_P256
private val eDeviceKeyPair by lazy {
Util.createEphemeralKeyPair(eDeviceKeyCurve)
}
private val nfcEngagementListener = object : NfcEngagementHelper.Listener {

override fun onTwoWayEngagementDetected() {
Logger.i(TAG, "onTwoWayEngagementDetected")
}

override fun onHandoverSelectMessageSent() {
Logger.i(TAG, "onHandoverSelectMessageSent")
// This is invoked _just_ before the NFC tag reader will do a READ_BINARY
// for the Handover Select message. Vibrate the device to indicate to the
// user they can start removing the device from the reader.
val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
val vibrationPattern = longArrayOf(0, 500, 50, 300)
val indexInPatternToRepeat = -1
vibrator.vibrate(vibrationPattern, indexInPatternToRepeat)
PresentationActivity.setState(PresentationActivity.State.ENGAGEMENT_SENT)
}

override fun onDeviceConnecting() {
Logger.i(TAG, "onDeviceConnecting")
}

override fun onDeviceConnected(transport: DataTransport) {
Logger.i(TAG, "onDeviceConnected")

if (!PresentationActivity.setEngagementInfo(transport, engagementHelper!!.handover,
eDeviceKeyCurve, eDeviceKeyPair, engagementHelper!!.deviceEngagement)) {
Logger.i(TAG, "Presentation already in progress")
} else {
PresentationActivity.setState(PresentationActivity.State.CONNECTED)
}

engagementHelper?.close()
engagementHelper = null
}

override fun onError(error: Throwable) {
Logger.i(TAG, "Engagement Listener: onError -> ${error.message}")
engagementHelper?.close()
engagementHelper = null
}
}

override fun onCreate() {
super.onCreate()
Logger.i(TAG, "onCreate")

val application: WalletApplication = application as WalletApplication

if (application.credentialStore.listCredentials().size > 0
&& !PresentationActivity.isPresentationActive()) {

PresentationActivity.setState(PresentationActivity.State.ENGAGING)

val options = DataTransportOptions.Builder().build()
val builder = NfcEngagementHelper.Builder(
applicationContext,
eDeviceKeyPair.public,
eDeviceKeyCurve,
options,
nfcEngagementListener,
ContextCompat.getMainExecutor(applicationContext)
)
builder.useNegotiatedHandover()
engagementHelper = builder.build()

val launchAppIntent = Intent(applicationContext, PresentationActivity::class.java)
launchAppIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or
Intent.FLAG_ACTIVITY_NO_HISTORY or
Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
applicationContext.startActivity(launchAppIntent)
}
}

override fun processCommandApdu(commandApdu: ByteArray, extras: Bundle?): ByteArray? {
Logger.dHex(TAG, "processCommandApdu", commandApdu)
return engagementHelper?.nfcProcessCommandApdu(commandApdu)
}

override fun onDeactivated(reason: Int) {
Logger.i(TAG, "onDeactivated: reason-> $reason ")
engagementHelper?.nfcOnDeactivated(reason)

// We need to close the NfcEngagementHelper but if we're doing it as the reader moves
// out of the field, it's too soon as it may take a couple of seconds to establish
// the connection, triggering onDeviceConnected() callback above.
//
// In fact, the reader _could_ actually take a while to establish the connection...
// for example the UI in the mdoc doc reader might have the operator pick the
// transport if more than one is offered. In fact this is exactly what we do in
// our mdoc reader.
//
// So we give the reader 15 seconds to do this...
//
val timeoutSeconds = 15
Handler(Looper.getMainLooper()).postDelayed({
if (engagementHelper != null) {
Logger.w(TAG, "Reader didn't connect inside $timeoutSeconds seconds, closing")
engagementHelper!!.close()
}
}, timeoutSeconds * 1000L)
}
}
Loading

0 comments on commit 4a1cf1c

Please sign in to comment.