BLE device retrieval¶
According to ISO 18013-5 one of the device retrieval methods is via Bluetooth LE. To accomplish that SDK provides an out of the box implementation for retrieving data using BLE.
Permissions¶
For android SDK version 31 and later the following permissions required to be granted for BLE transfer.
- Manifest.permission.BLUETOOTH_ADVERTISE
- Manifest.permission.BLUETOOTH_SCAN
- Manifest.permission.BLUETOOTH_CONNECT
For android SDK prior to version 31 the required permissions are:
- Manifest.permission.ACCESS_FINE_LOCATION
- Manifest.permission.ACCESS_COARSE_LOCATION
Prepare and send request¶
When receiving the device engagement a SessionManager
must be instantiated in order to encrypt the
request to be send via BLE. This SessionManager
instance must be kept as it will be used later to
decrypt the received data. For detailed information see
in Session Encryption section.
Create a BLETransfer
object and add the TransferProgressListener
and
the TransferReceiveCallback
.
When device engagement is complete set the current DeviceEngagement object using
the BLETransfer#forDeviceEngagement(DeviceEngagement)
method. Then, create a request
using RequestBuilder
by setting the docRequests map and the SessionManager
instance and finally
send the request
.
Example
package com.example.app;
import ...
public class MainActivity extends AppCompatActivity
implements DeviceEngagementCallback, TransferProgressListener,
TransferReceiveCallback {
// ...
private final Map<String, Map<String, Map<String, Boolean>>> docRequests
= new HashMap<>();
private BLETransfer bleTransfer;
private SessionManager sessionManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Map<String, Boolean> itemsRequest = new HashMap<>();
itemsRequest.put("age_over_21", true);
itemsRequest.put("portrait", true);
Map<String, Map<String, Boolean>> nameSpaces = new HashMap<>();
nameSpaces.put("org.iso.18013.5.1", itemsRequest);
docRequests.put("org.iso.18013.5.1.mDL", nameSpaces);
try {
bleTransfer = new BLETransfer(getApplicationContext())
.setTransferProgressListener(this)
.setTransferReceiveCallback(this);
} catch (SDKException e) {
// handle failed BLETransfer initialization
}
// ...
}
@Override
public void onEngage(@NonNull Received<EngagementReceived> received) {
try {
received.runCatching(
failure -> {
// handle failed device engagement
},
success -> {
Handover handover = success.getHandover();
DeviceEngagement deviceEngagement
= success.getDeviceEngagement();
sessionManager = new SessionManager(deviceEngagement, handover);
Request request = new RequestBuilder()
.setSessionManager(sessionManager)
.setDocRequests(docRequests)
.build();
bleTransfer
.forDeviceEngagement(deviceEngagement)
.send(request);
}
);
} catch (Exception e) {
// handle exception sending request
}
}
@Override
public void onProgressEvent(@NonNull TransferProgressEvent transferProgressEvent) {
// update UI with progress information
}
@Override
public void onReceive(@NonNull Received<TransferReceived> received) {
// handle received response
}
}
Receiving response¶
When receiving the response the onReceive(Received<TransferReceived>)
method is called with the
received response.
There are two possible outcomes. Received object is either a Failure or Success.
When the received object is Success, use the SessionManager, created after device engagement, to decrypt the received bytes.
See also, Failure Types for details when receive fails.
Example
package com.example.app;
import ...
public class MainActivity extends AppCompatActivity
implements DeviceEngagementCallback, TransferProgressListener,
TransferReceiveCallback {
private SessionManager sessionManager;
// ...
@Override
public void onReceive(@NonNull Received<TransferReceived> received) {
try {
received.runCatching(
failure -> {
// handle failed data received
},
success -> success.runCatchingForDevice(bytesReceived -> {
SessionData sessionData = sessionManager
.decryptResponse(bytesReceived);
// do stuff with sessionData
})
);
} catch (Exception e) {
Log.e(TAG, "onReceive", e);
// handle exception
}
}
}
Failure Types¶
Device State
- BLE_NOT_ENABLED
- BLE_NOT_SUPPORTED
- BLE_CENTRAL_MODE_NOT_SUPPORTED
- BLE_PERIPHERAL_MODE_NOT_SUPPORTED
- BLE_LOCATION_PROVIDER_NOT_ENABLED
Missing permissions
- BLE_BLUETOOTH_CONNECT_PERMISSION_NOT_GRANTED
- BLE_BLUETOOTH_ADVERTISE_PERMISSION_NOT_GRANTED
- BLE_BLUETOOTH_SCAN_PERMISSION_NOT_GRANTED
- BLE_LOCATION_PERMISSIONS_NOT_GRANTED
Connection
- BLE_ADVERTISE_ERROR
- BLE_DEVICE_CONNECTION_LOST
- BLE_SERVICE_NOT_STARTED
- BLE_CONNECTION_ERROR
- BLE_TRANSFER_ERROR
- BLE_TRY_TO_CONNECT_UNAVAILABLE_SERVICE
- BLE_GATT_SERVER_NOT_OPENED
Other
- UNSPECIFIED_ERROR
- EMPTY_REQUEST
- BLE_READER_VERIFICATION_UNAVAILABLE
Sample code¶
The following example demonstrates a typical implementation for retrieving data using QR device engagement and BLE.
Example
package com.example.app;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import android.Manifest.permission;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.widget.Button;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import com.scytales.mvalid.sdk.FailureType;
import com.scytales.mvalid.sdk.Received;
import com.scytales.mvalid.sdk.SDKException;
import com.scytales.mvalid.sdk.data.DeviceResponse;
import com.scytales.mvalid.sdk.data.Request;
import com.scytales.mvalid.sdk.data.RequestBuilder;
import com.scytales.mvalid.sdk.engagement.DeviceEngagement;
import com.scytales.mvalid.sdk.engagement.DeviceEngagementCallback;
import com.scytales.mvalid.sdk.engagement.EngagementReceived;
import com.scytales.mvalid.sdk.engagement.qr.QRDeviceEngagement;
import com.scytales.mvalid.sdk.retrieval.TransferProgressEvent;
import com.scytales.mvalid.sdk.retrieval.TransferProgressListener;
import com.scytales.mvalid.sdk.retrieval.TransferReceiveCallback;
import com.scytales.mvalid.sdk.retrieval.TransferReceived;
import com.scytales.mvalid.sdk.retrieval.device.BLETransfer;
import com.scytales.mvalid.sdk.session.Handover;
import com.scytales.mvalid.sdk.session.SessionData;
import com.scytales.mvalid.sdk.session.SessionManager;
import com.scytales.mvalid.sdk.verify.DeviceVerifierResult;
import com.scytales.mvalid.sdk.verify.Verifier;
import com.scytales.mvalid.sdk.verify.VerifierResult;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
public class MainActivity extends AppCompatActivity
implements DeviceEngagementCallback, TransferProgressListener,
TransferReceiveCallback {
private static final String TAG = "MainActivity";
private static final String[] REQUIRED_PERMISSIONS = new ArrayList<String>() {{
add(permission.CAMERA);
if (Build.VERSION.SDK_INT >= 31) {
add(permission.BLUETOOTH_ADVERTISE);
add(permission.BLUETOOTH_SCAN);
add(permission.BLUETOOTH_CONNECT);
} else {
add(permission.ACCESS_FINE_LOCATION);
add(permission.ACCESS_COARSE_LOCATION);
}
}}.toArray(new String[]{});
private final QRDeviceEngagement qrDeviceEngagement = new QRDeviceEngagement();
private BLETransfer bleTransfer;
private final ActivityResultLauncher<String[]> requestPermissionsLauncher =
registerForActivityResult(new RequestMultiplePermissions(), result -> {
if (result.entrySet().stream().allMatch(Map.Entry::getValue)) {
enableScanQrBtn();
}
});
private SessionManager sessionManager;
private Collection<X509Certificate> rootCertificates;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
rootCertificates = Verifier.getRootCertificates(this);
// see more about requesting permissions in
// https://developer.android.com/training/permissions/requesting
if (Arrays.stream(REQUIRED_PERMISSIONS)
.allMatch(p -> ContextCompat.checkSelfPermission(this, p)
== PERMISSION_GRANTED)) {
enableScanQrBtn();
} else {
requestPermissionsLauncher.launch(REQUIRED_PERMISSIONS);
}
try {
qrDeviceEngagement.enableDeviceEngagement(this, this);
bleTransfer = new BLETransfer(getApplicationContext())
.setTransferProgressListener(this)
.setTransferReceiveCallback(this);
} catch (SDKException e) {
Log.e(TAG, "onCreate", e);
// handle failed initialization
}
}
private void enableScanQrBtn() {
Button scanQrBtn = findViewById(R.id.scan_qr_btn);
scanQrBtn.setOnClickListener(v -> qrDeviceEngagement.scanQR());
}
@Override
public void onEngage(@NonNull Received<EngagementReceived> received) {
Log.d(TAG, received.toString());
try {
received.runCatching(
failure -> {
Log.e(TAG, failure.toString());
FailureType type = failure.getType();
// handle failed device engagement
},
success -> {
Handover handover = success.getHandover();
DeviceEngagement deviceEngagement = success.getDeviceEngagement();
sessionManager = new SessionManager(deviceEngagement, handover);
// request for age_over_21
Request request = new RequestBuilder()
.setSessionManager(sessionManager)
.forIsoDocTypeAgeOver(21, true)
.build();
bleTransfer
.forDeviceEngagement(deviceEngagement)
.send(request);
}
);
} catch (Exception e) {
// handle exception sending request
}
}
@Override
public void onProgressEvent(@NonNull TransferProgressEvent transferProgressEvent) {
Log.i(TAG, transferProgressEvent.toString());
// update UI with progress information
}
@Override
public void onReceive(@NonNull Received<TransferReceived> received) {
try {
received.runCatching(
failure -> {
Log.e(TAG, failure.toString());
FailureType type = failure.getType();
// handle failed data received
},
success -> success.runCatchingForDevice(bytesReceived -> {
SessionData sessionData = sessionManager
.decryptResponse(bytesReceived);
sessionData.runOnError(status -> {
Log.e(TAG, status.toString());
// handle session encryption error
});
sessionData.runCatchingOnData(decryptedBytes -> {
// create a DeviceResponse to manipulate received data
DeviceResponse deviceResponse =
DeviceResponse.fromBytes(decryptedBytes);
// verify received data; a DeviceVerifierResult for each
// requested document
List<DeviceVerifierResult> verifierResults =
sessionManager.getVerifier(rootCertificates)
.verify(decryptedBytes);
// check verification for all verified documents
boolean isVerified = verifierResults.stream()
.allMatch(VerifierResult::isValid);
});
})
);
} catch (Exception e) {
Log.e(TAG, "onReceive", e);
// handle exception
}
}
}