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
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
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
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 +
'}';
}
}
}
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
}
}