Skip to content

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

MainActivity.java
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

MainActivity.java
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

MainActivity.java
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
        }
    }
}