Skip to content

Best practices

SDK initialization and permissions granting

It is recommended to initialize and check and request for the required permissions in a previous activity, before using the SDK for device engagement and transferring data.

For example, if your application has already a launcher activity, you can take advantage of it for SDK initialization.

The following example demonstrates how to request for permissions and initialize SDK using a license file.

Example

LauncherActivity.java
package com.example.app;

import static android.content.pm.PackageManager.PERMISSION_GRANTED;

import android.Manifest;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;

import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;

import com.scytales.mvalid.sdk.VerifierSDK;
import com.scytales.mvalid.sdk.VerifierSDKStatus;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;

public class LauncherActivity extends AppCompatActivity {

    private static final String TAG = "LauncherActivity";

    private static final String[] REQUIRED_PERMISSIONS = new ArrayList<String>() {{
        add(Manifest.permission.CAMERA);
        if (Build.VERSION.SDK_INT >= 31) {
            add(Manifest.permission.BLUETOOTH_ADVERTISE);
            add(Manifest.permission.BLUETOOTH_SCAN);
            add(Manifest.permission.BLUETOOTH_CONNECT);
        } else {
            add(Manifest.permission.ACCESS_FINE_LOCATION);
            add(Manifest.permission.ACCESS_COARSE_LOCATION);
        }
    }}.toArray(new String[]{});

    private final ActivityResultLauncher<String[]> requestPermissionsLauncher =
            registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> {
                if (result.entrySet().stream().allMatch(Map.Entry::getValue)) {
                    gotoMainActivity();
                }
            });

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_launcher);
        // activity creation  
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (VerifierSDK.getStatus() == VerifierSDKStatus.INITIALIZED) {
            gotoMainActivity();
            return;
        }
        VerifierSDKStatus verifierSDKStatus =
                VerifierSDK.initWithLicense(getApplicationContext(), R.raw.license);

        if (verifierSDKStatus != VerifierSDKStatus.INITIALIZED) {
            // handle initialization failure
            Log.e(TAG, verifierSDKStatus.getMessage());
        } else {
            if (Arrays.stream(REQUIRED_PERMISSIONS)
                    .allMatch(p -> ContextCompat.checkSelfPermission(this, p)
                            == PERMISSION_GRANTED)) {
                gotoMainActivity();
            } else {
                requestPermissionsLauncher.launch(REQUIRED_PERMISSIONS);
            }

        }
    }

    private void gotoMainActivity() {
        // navigate to BleActivity
        Intent intent =
                new Intent(this, MainActivity.class);
        startActivity(intent);
    }
}

The following example demonstrates how to request for permissions and initialize SDK using a license key.

Example

LauncherActivity.java
package com.example.app;

import static android.content.pm.PackageManager.PERMISSION_GRANTED;

import android.Manifest;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;

import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;

import com.scytales.mvalid.sdk.InitSDKCallback;
import com.scytales.mvalid.sdk.VerifierSDK;
import com.scytales.mvalid.sdk.VerifierSDKStatus;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;

public class LauncherActivity extends AppCompatActivity
        implements InitSDKCallback {

    private static final String TAG = "LauncherActivity";
    private static final String LICENSE_KEY = "XXXXX-XXXXX-XXXXX-XXXXX";

    private static final String[] REQUIRED_PERMISSIONS = new ArrayList<String>() {{
        add(Manifest.permission.CAMERA);
        if (Build.VERSION.SDK_INT >= 31) {
            add(Manifest.permission.BLUETOOTH_ADVERTISE);
            add(Manifest.permission.BLUETOOTH_SCAN);
            add(Manifest.permission.BLUETOOTH_CONNECT);
        } else {
            add(Manifest.permission.ACCESS_FINE_LOCATION);
            add(Manifest.permission.ACCESS_COARSE_LOCATION);
        }
    }}.toArray(new String[]{});

    private final ActivityResultLauncher<String[]> requestPermissionsLauncher =
            registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> {
                if (result.entrySet().stream().allMatch(Map.Entry::getValue)) {
                    gotoMainActivity();
                }
            });

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_launcher);
        // activity creation  
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (VerifierSDK.getStatus() == VerifierSDKStatus.INITIALIZED) {
            gotoMainActivity();
            return;
        }
        VerifierSDK.initWithLicenseKey(getApplicationContext(), LICENSE_KEY, this);
    }


    @Override
    public void onInitSDK(@NonNull VerifierSDKStatus verifierSDKStatus) {
        if (verifierSDKStatus != VerifierSDKStatus.INITIALIZED) {
            // handle initialization failure
            Log.e(TAG, verifierSDKStatus.getMessage());
        } else {
            if (Arrays.stream(REQUIRED_PERMISSIONS)
                    .allMatch(p -> ContextCompat.checkSelfPermission(this, p)
                            == PERMISSION_GRANTED)) {
                gotoMainActivity();
            } else {
                requestPermissionsLauncher.launch(REQUIRED_PERMISSIONS);
            }

        }
    }

    private void gotoMainActivity() {
        // navigate to BleActivity
        Intent intent =
                new Intent(this, Main.class);
        startActivity(intent);
    }

}

Use in ViewModel

Create a ViewModel class and place the device engagement handling and the data receiving there. This will eliminate problems that have to do with Activity lifecycle, such as destroying activity while rotating the device.

Below you can see an example for QR device engagement using BLETransfer for device data retrieval that are used via a ViewModel.

Example

MainViewModel.java
package com.example.app;

import static androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY;

import android.annotation.SuppressLint;
import android.app.Application;

import androidx.activity.ComponentActivity;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.viewmodel.ViewModelInitializer;

import com.scytales.mvalid.sdk.NotInitializedException;
import com.scytales.mvalid.sdk.Received;
import com.scytales.mvalid.sdk.SDKException;
import com.scytales.mvalid.sdk.UnsupportedFeatureException;
import com.scytales.mvalid.sdk.data.Request;
import com.scytales.mvalid.sdk.data.RequestBuilder;
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.TransferProgressListenable;
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.SessionData;
import com.scytales.mvalid.sdk.session.SessionManager;

import java.util.Map;

public class MainViewModel extends ViewModel
        implements DeviceEngagementCallback,
        TransferReceiveCallback,
        TransferProgressListenable {

    public final static ViewModelInitializer<MainViewModel> INITIALIZER =
            new ViewModelInitializer<>(
                    MainViewModel.class,
                    creationExtras -> {
                        Application app = creationExtras.get(APPLICATION_KEY);
                        assert app != null;
                        try {
                            return new MainViewModel(new BLETransfer(app));
                        } catch (NotInitializedException | UnsupportedFeatureException e) {
                            throw new RuntimeException(e);
                        }
                    }
            );
    @NonNull
    private final BLETransfer bleTransfer;
    @NonNull
    private final QRDeviceEngagement qrDeviceEngagement;
    private final MutableLiveData<byte[]> responseBytesLiveData = new MutableLiveData<>();
    private final MutableLiveData<Error<?>> errorLiveData = new MutableLiveData<>();
    private SessionManager sessionManager;
    private Map<String, Map<String, Map<String, Boolean>>> docRequests;

    public MainViewModel(@NonNull BLETransfer bleTransfer) {

        this.bleTransfer = bleTransfer
                .setTransferReceiveCallback(this);
        this.qrDeviceEngagement = new QRDeviceEngagement();
    }

    public void setDocRequests(@NonNull Map<String, Map<String, Map<String, Boolean>>> docRequests) {
        this.docRequests = docRequests;
    }

    public void scanQR() {
        this.qrDeviceEngagement.scanQR();
    }

    public LiveData<byte[]> getResponseBytesLiveData() {
        return responseBytesLiveData;
    }

    public LiveData<Error<?>> getErrorLiveData() {
        return errorLiveData;
    }

    public SessionManager getSessionManager() {
        return sessionManager;
    }

    @SuppressLint("MissingPermission")
    public void enableDeviceEngagement(@NonNull ComponentActivity componentActivity) throws SDKException {
        qrDeviceEngagement.enableDeviceEngagement(componentActivity, this);
    }

    @Override
    public void onEngage(@NonNull Received<EngagementReceived> received) {
        try {
            received.runOnSuccessCatching(engagementReceived -> {
                sessionManager = new SessionManager(
                        engagementReceived.getDeviceEngagement(),
                        engagementReceived.getHandover()
                );
                Request request = new RequestBuilder()
                        .setSessionManager(sessionManager)
                        .setDocRequests(docRequests)
                        .build();
                bleTransfer.forDeviceEngagement(engagementReceived.getDeviceEngagement());
                bleTransfer.send(request);
            }).runOnFailure(failure -> {
                Error<EngagementReceived> error = new Error<>();
                error.failure = failure;
                errorLiveData.postValue(error);
            });
        } catch (Exception e) {
            Error<EngagementReceived> error = new Error<>();
            error.exception = e;
            errorLiveData.postValue(error);
        }
    }

    @Override
    public void onReceive(@NonNull Received<TransferReceived> received) {
        try {
            received.runOnSuccessCatching(transferReceived -> transferReceived
                    .runCatchingForDevice(bytes -> {
                        SessionData sessionData = sessionManager.decryptResponse(bytes);
                        sessionData.runOnData(responseBytesLiveData::postValue);
                        sessionData.runOnError(status -> {
                            Error<TransferReceived> error = new Error<>();
                            error.status = status;
                            errorLiveData.postValue(error);
                        });
                    })
            ).runOnFailure(failure -> {
                Error<TransferReceived> error = new Error<>();
                error.failure = failure;
                errorLiveData.postValue(error);
            });
        } catch (Exception e) {
            Error<TransferReceived> error = new Error<>();
            error.exception = e;
            errorLiveData.postValue(error);
        }
    }

    @NonNull
    @Override
    public MainViewModel setTransferProgressListener(
            @NonNull TransferProgressListener transferProgressListener) {
        this.bleTransfer.setTransferProgressListener(transferProgressListener);
        return this;
    }

    @NonNull
    @Override
    public MainViewModel unsetTransferProgressListener() {
        this.bleTransfer.unsetTransferProgressListener();
        return this;
    }

    public static class Error<D> {
        private Received.Failure<D> failure;
        private Exception exception;
        private SessionData.Status status;

        @Nullable
        public Received.Failure<D> getFailure() {
            return failure;
        }

        @Nullable
        public Exception getException() {
            return exception;
        }

        @Nullable
        public SessionData.Status getStatus() {
            return status;
        }

        @NonNull
        @Override
        public String toString() {
            return "Error{" +
                    "failure=" + failure +
                    ", exception=" + exception +
                    ", status=" + status +
                    '}';
        }
    }
}
MainActivity.java
package com.example.app;

import android.os.Bundle;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;

import com.scytales.mvalid.sdk.SDKException;
import com.scytales.mvalid.sdk.data.DeviceResponse;
import com.scytales.mvalid.sdk.data.IsoDocRequests;
import com.scytales.mvalid.sdk.retrieval.TransferProgressEvent;
import com.scytales.mvalid.sdk.retrieval.TransferProgressListener;
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.Collection;
import java.util.List;

public class MainActivity extends AppCompatActivity
        implements TransferProgressListener {

    private static final String TAG = "MainActivity";

    private MainViewModel viewModel;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        viewModel = new ViewModelProvider(this,
                ViewModelProvider.Factory.from(MainViewModel.INITIALIZER)
        ).get(MainViewModel.class);

        viewModel.setDocRequests(IsoDocRequests.getAgeOverDocRequest(21, true));
        viewModel.setTransferProgressListener(this);
        viewModel.getErrorLiveData().observe(this, error -> {
            Log.e(TAG, error.toString());
            // handle error
        });
        viewModel.getResponseBytesLiveData().observe(this, bytes -> {
            try {
                // create a DeviceResponse to manipulate received data
                DeviceResponse response = DeviceResponse.fromBytes(bytes);
                Log.i(TAG, response.toString());

                // verify received data; a DeviceVerifierResult for each
                // requested document
                Collection<X509Certificate> rootCertificates =
                        Verifier.getRootCertificates(MainActivity.this);
                List<DeviceVerifierResult> verifierResults = viewModel
                        .getSessionManager()
                        .getVerifier(rootCertificates)
                        .verify(bytes);

                Log.i(TAG, verifierResults.toString());

                // check verification for all verified documents
                boolean isVerified = verifierResults.stream()
                        .allMatch(VerifierResult::isValid);
            } catch (SDKException e) {
                Log.e(TAG, e.getMessage());
            }
        });

        try {
            viewModel.enableDeviceEngagement(this);
            findViewById(R.id.scan_qr_btn).setOnClickListener(v -> viewModel.scanQR());
        } catch (SDKException e) {
            Log.e(TAG, e.getMessage());
        }
    }

    @Override
    public void onProgressEvent(@NonNull TransferProgressEvent transferProgressEvent) {
        Log.d(TAG, transferProgressEvent.toString());
        // update UI with event
    }
}