Android SDK Integration 0.0.8
Introduction
Learn how to implement Lens in an Android application.
Requirements
- API Key
- Lens SDK (
LensSdk-release-X.X.X.X.zip) - Android SDK version 23 (6.0) or higher (
minSdkVersion 23) - SHA256 checksum (
checksum_release.txt)
Getting Started
Validate the SHA256 release checksum
Before using the Lens SDK, make sure that its SHA256 checksum is correct. For
Mac OS run: shasum -c checksum_release.txt and result should be
LensSdk-release-X.X.X.X.zip: OK)
Importing the Library
Extract files from the LensSDK zip into a folder inside your Android project,
for example libs/lens. Afterwards, specify this folder as repository in your
project build.gradle:
allprojects {
repositories {
// LensSDK
maven {
url rootProject.file("libs/lens")
}
// other repositories
google()
mavenCentral()
}
}
Then it is possible to use LensSDK as any other dependency in your module
build.gradle:
implementation 'com.truepic:lens:X.X.X.X'
Proguard
Lens SDK comes with a built-in consumers-rules.pro file that contains all the
necessary rules for the proguard to work correctly.
Manifest
Permissions
Before opening a camera following permissions need to be defined in the project’s manifest file:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
The following permissions are required to be granted:
Manifest.permission.CAMERA,
Manifest.permission.ACCESS_FINE_LOCATION
The camera view will not work without these and the LensError object would be
raised with additional details.
Application
In order for the SecuredSharedPreferences and LensSDK timestamp to work correctly, application elements in the manifest needs to have these parameters set up:
<application
...
android:allowBackup="false"
android:fullBackupContent="false"
android:usesCleartextTraffic="true" />
Gradle
Depends on your Gradle version, add this line in gradle.properties:
android.jetifier.blacklist = bcprov-jdk18on
or
android.jetifier.ignorelist = bcprov-jdk18on
Missing this line will cause this build error:
Unsupported class file major version 59
Recommened settings
Use API version 32:
compileSdkVersion 32
targetSdkVersion 32
Manifest changes: See developer docs https://developer.android.com/about/versions/12/behavior-changes-12#exported:
<activity
...
android:exported="true">
Gradle changes - use version 4.2.2 or above:
...
dependencies {
classpath 'com.android.tools.build:gradle:4.2.2'
...
React Native - in case it’s used inside View: after camera setup steps above, call setupLayoutHack() (https://stackoverflow.com/questions/63896520/integrating-native-code-with-camerax-to-custom-react-native-component-fails/64100451#64100451):
fun setupLayoutHack() {
Choreographer.getInstance().postFrameCallback(object : Choreographer.FrameCallback {
override fun doFrame(frameTimeNanos: Long) {
manuallyLayoutChildren()
viewTreeObserver.dispatchOnGlobalLayout()
Choreographer.getInstance().postFrameCallback(this)
}
})
}
fun manuallyLayoutChildren() {
for (i in 0 until childCount) {
val child = getChildAt(i)
child.measure(MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY))
child.layout(0, 0, child.measuredWidth, child.measuredHeight)
}
}
Implementation
Camera Preview Setup
LensCameraView is provided to facilitate camera preview:
<com.truepic.lenssdk.LensCameraView
android:id="@+id/lensCameraView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
Once LensCameraView is present in one of the layouts, it is necessary to set
up a valid API key and listeners for it. This could be done in an activity’s
onCreate or fragment’s onCreateView methods (depending on where the view
from above is being used).
// Kotlin
lensCameraView.setApiKey("api key here", object : LensHandler {
override fun onError(error: LensError) {
// handle errors here
}
override fun enableCapture(cameraEnabled: Boolean) {
// use cameraEnabled boolean
}
}, strongbox)
// Java
lensCameraView.setApiKey("api key here", new LensHandler() {
@Override
public void onError(LensError lensError) {
// handle errors here
}
@Override
public void enableCapture(boolean cameraEnabled) {
// use cameraEnabled boolean
}
}, strongbox);
It is recommended to enable camera capture only when cameraEnabled is true.
If there’s any error that would prevent the camera from working, the app will be
notified using the LensError object that contains the message and type of the
error.
strongbox - Boolean - Enable or Disable hardware security StrongBox for supported devices.
Note: API key should not be stored in plain text. It is recommended to store it obfuscated using NDK (see this gradle plugin or this article for additional details).
Error Handling
LensError object is provided whenever an error occurs while initializing and
working with the camera. Each error contains three values: message, error type,
and exception (optional)
Error Types:
Permissions_Camera- check permissions for accessing the cameraPermissions_Location- check location permissionsEnv_AirplaneMode- airplane mode is enabled and no internet connection is availableEnv_DevMode- development mode or usb debugging is enabledEnv_Network- no internet connection is availableEnv_Rooted- device is rootedEnv_PlayStoreServices- out of date or no play store services available on the deviceEnv_Location- location services are not available/enabledEnv_Location_Not_Ready- temporary while device is determining current locationCamera- errors associated with using the cameraAttestation- device integrity is compromised, more details are provided within the messageEnrollment- device enrollment errorsKeyGeneration- errors occuring while working and generating cryptographic keysNative- errors associated with native codeUndefined- all other errors that might occur, please check associated message/exception
Capturing a Photo
In order to initiate secure capture, application needs to call startPhotoCapture
method of the LensCamera interface:
// Kotlin
lensCamera.startPhotoCapture(object : LensPhotoCaptureHandler {
override fun onTruepicCaptured(uuid: UUID, truepic: Truepic) {
// truepic captured
}
override fun onThumbnailCaptured(uuid: UUID, truepic: Truepic) {
// thumbnail captured
}
override fun onBlurScoreCalculated(uuid: UUID, blurScore: float) {
// Evaluate blur score (or ignore it)
}
})
// Java
lensCamera.startPhotoCapture(new LensPhotoCaptureHandler() {
@Override
public void onTruepicCaptured(UUID uuid, Truepic truepic) {
// truepic captured
}
@Override
public void onThumbnailCaptured(UUID uuid, Truepic truepic) {
// thumbnail captured
}
@Override
public void onBlurScoreCalculated(UUID uuid, float blurScore) {
// Evaluate blur score (or ignore it)
}
});
First, onThumbnailCaptured is going to be called containing a low resolution
image that could be used as a preview. This image does not contain image
provenance. Then, onTruepicCaptured will return the C2PA signed image. UUID
could be used to update/remove previously created low resolution thumbnails.
Capturing a Video
To capture a video the SDK will need to switch capture modes from LensCaptureMode.Photo to LensCaptureMode.Video
using LensCamera.setCaptureMode(LensCaptureMode lensCaptureMode). After switching the capture mode your application
will need to call LensCamera.startRecordingVideo to start recording by providing it a LensVideoCaptureHandler like
below:
// kotlin
lensCamera.startRecordingVideo(object : LensVideoCaptureHandler {
override fun onTruepicVideoCaptured(uuid: UUID, truepicVideo: TruepicVideo) {
// This is called when recording has finished.
}
override fun onThumbnailCaptured(uuid: UUID, thumbnailAsJpeg: ByteArray) {
// This is called when a video thumbnail has been generated. The thumbnail is stored as a JPEG in the byte array.
}
override fun onRecordingStarted() {
// This is called when recording has started.
}
override fun onUpdate(recordedDurationNanos: Long) {
// While recording this is called periodically, use this to update your duration timer. If your user experience
// does not include a duration timer this method can be left empty.
}
override fun onRecordingFinished() {
// This is called when recording has finished. If the recording finished prematurely please check for any recent
// errors.
}
})
// Java
lensCamera.startRecordingVideo(new LensVideoCaptureHandler() {
@Override
public void onTruepicVideoCaptured(UUID uuid, TruepicVideo truepicVideo) {
// This is called when recording has finished.
}
@Override
public void onThumbnailCaptured(UUID uuid, byte[] thumbnailAsJpeg) {
// This is called when a video thumbnail has been generated. The thumbnail is stored as a JPEG in the byte array.
}
@Override
public void onRecordingStarted() {
// This is called when recording has started.
}
@Override
public void onUpdate(long recordedDurationNanos) {
// While recording this is called periodically, use this to update your duration timer. If your user experience
// does not include a duration timer this method can be left empty.
}
@Override
public void onRecordingFinished() {
// This is called when recording has finished. If the recording finished prematurely please check for any recent
// errors.
}
});
Once recording has started you must call LensCamera.stopRecordingVideo() to stop recording. Please note that it is
possible that an error was encountered and the recording stopped prematurely. Please be sure to check for any errors if
LensVideoCaptureHandler.onRecordingFinished() is raised without explicitly telling the SDK to stop the recording.
While there are no restrictions on file size or duration to record verified videos and upload them to your server, there is currently a file size limit of 100 MB per video file uploaded to the Lens API for processing. This may not be necessary for your use case, but if so, we recommend limiting recording to under 45 seconds per video.
Additional Camera Functionality
The Lens SDK also provides common camera functions such as pinch to zoom setZoomRatio, tap to focus / meter focusMeteringOnTap, camera facing setLensFacing, and flash mode setLensFacing. Handling the standard Android motion events is left for the implementor to determine however we do provide an example implementation in the Examples section below.
| Return Type | Method Name and Description |
|---|---|
| void | setZoomRatio(float zoomRatio) Use this method to tell the camera how much to zoom. |
| void | focusMeteringOnTap(MotionEvent motionEvent, int meteringMode, @Nullable Runnable focusMeteringOnTapCallBack) Use this method to tell the camera which metering mode to use. |
| void | setIsFrontFacingPreviewMirrored(boolean mirrored) Set front facing preview mirroring (this does not impact the captured image, or rear facing* camera). |
| boolean | getIsFrontFacingPreviewMirrored() Gets the current mirroring status |
| void | setFlashMode(LensFlashMode lensFlashMode) Sets the flash mode Off, Auto, and On |
| void | setLensFacing(LensCameraFacing lensCameraFacing) Sets which camera to use Front (aka selfie) or Back. |
| void | setPreviewScale(LensPreviewScale LensPreviewScale) Sets preview scaling: fit (default) or fill. |
| LensPreviewAspectRatio | getPreviewAspectRatio() Gets current aspect ratio of a preview. Answer might differ based on the capture mode. |
| Bitmap | getPreviewBitmap() Extracts bitmap from preview. |
| float | getLocationAccuracyThreshold() Gets the location accuracy threshold in meters. The default value is 1500m |
| void | setLocationAccuracyThreshold(float) Sets the location accuracy threshold in meters. Use caution when setting this value. The default value is 1500m. |
Examples
Below you can find two example implementations for Java and Kotlin including sample XML layout.
Layout
XML layout contains two views: LensCameraView for preview and
AppCompatButtton to initiate image capture.
<xml version="1.0" encoding="utf-8">
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Camera View -->
<com.truepic.lenssdk.LensCameraView
android:id="@+id/lens_camera"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<!-- Capture Button -->
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/capture_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Take Picture"
android:layout_margin="15dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</xml>
Java
public class CameraActivityExample extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_camera);
LensCameraView lensCamera = findViewById(R.id.lens_camera);
AppCompatButton captureButton = findViewById(R.id.capture_button);
lensCamera.setApiKey("api key here", new LensHandler() {
@Override
public void onError(LensError lensError) {
handleError(lensError);
}
@Override
public void enableCapture(boolean enabled) {
captureButton.setEnabled(enabled);
if(!enabled) {
// optional: notify user
}
}
});
captureButton.setOnClickListener(view -> lensCamera.startCapture(new LensPhotoCaptureHandler() {
@Override
public void onTruepicCaptured(UUID uuid, Truepic truepic) {
// save C2PA image
}
@Override
public void onThumbnailCaptured(UUID uuid, Truepic truepic) {
// optional
// save thumbnail while C2PA image is being created
}
}));
}
private void handleError(LensError error) {
switch (error.errorType()) {
case Env_PlayStoreServices:
case Env_AirplaneMode:
case Env_DevMode:
case Env_Network:
case Permissions_Location:
case Permissions_Camera:
case Enrollment:
case Attestation:
case Camera:
case KeyGeneration:
case Native:
case Undefined:
Log.d("Camera", error.errorMessage())
}
}
}
Kotlin
class CameraActivityExample : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_camera)
val lensCamera: LensCameraView = findViewById(R.id.lens_camera)
val captureButton: AppCompatButton = findViewById(R.id.capture_button)
lensCamera.setApiKey("api key here", object : LensHandler {
override fun onError(error: LensError) {
handleError(error)
}
override fun enableCapture(enable: Boolean) {
captureButton.isEnabled = enabled
if(!enabled) {
// optional: notify user
}
}
})
captureButton.setOnClickLenstener {
lensCamera.startCapture(object : LensPhotoCaptureHandler {
override fun onTruepicCaptured(uuid: UUID, truepic: Truepic) {
// save C2PA image
}
override fun onThumbnailCaptured(uuid: UUID, truepic: Truepic) {
// optional
// save thumbnail while C2PA image is being created
}
})
}
}
private fun handleError(error: LensError) {
when (error.errorType()) {
ErrorType.Env_PlayStoreServices,
ErrorType.Env_AirplaneMode,
ErrorType.Env_DevMode,
ErrorType.Env_Network,
ErrorType.Permissions_Location,
ErrorType.Permissions_Camera,
ErrorType.Enrollment,
ErrorType.Attestation,
ErrorType.Camera,
ErrorType.KeyGeneration,
ErrorType.Native,
ErrorType.Undefined ->
Log.d("Camera", error.errorMessage())
}
}
}
Motion Handler Example
In your main activity
Java
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_start);
LensCameraView lensCameraView = findViewById(R.id.lens_camera);
lensCamera = lensCameraView.getCamera();
//Note set your API key here
lensCameraView.setOnTouchListener(new CameraTouchListener(getApplicationContext(), lensCamera, findViewById(R.id.reticle)));
FloatingActionButton fab = findViewById(R.id.fab);
FloatingActionButton flash = findViewById(R.id.flash);
FloatingActionButton facing = findViewById(R.id.camera_facing);
FloatingActionButton mirroring = findViewById(R.id.preview_mirroring);
fab.setEnabled(false);
fab.setOnClickListener(view -> lensCamera.startCapture(this));
flash.setEnabled(false);
flash.setOnClickListener(view -> {
switch (lensFlashMode) {
case On:
lensFlashMode = LensFlashMode.Off;
flash.setImageResource(R.drawable.flash_off);
break;
case Off:
lensFlashMode = LensFlashMode.Auto;
flash.setImageResource(R.drawable.flash_auto);
break;
case Auto:
lensFlashMode = LensFlashMode.On;
flash.setImageResource(R.drawable.flash_on);
break;
}
lensCamera.setFlashMode(lensFlashMode);
});
facing.setEnabled(false);
facing.setOnClickListener(view -> {
if (lensCameraFacing == LensCameraFacing.Rear) {
lensCameraFacing = LensCameraFacing.Front;
mirroring.setEnabled(true);
}
else {
lensCameraFacing = LensCameraFacing.Rear;
mirroring.setEnabled(false);
}
lensCamera.setLensFacing(lensCameraFacing);
});
mirroring.setOnClickListener(view -> {
if (lensCameraFacing == LensCameraFacing.Front)
lensCamera.setIsFrontFacingPreviewMirrored(!lensCamera.getIsFrontFacingPreviewMirrored());
});
}
Kotlin
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_start)
val lensCameraView = findViewById<LensCameraView>(R.id.lens_camera)
lensCamera = lensCameraView.camera
lensCameraView.setOnTouchListener(
CameraTouchListener(applicationContext, lensCamera, findViewById(R.id.reticle))
)
val fab = findViewById<FloatingActionButton>(R.id.fab)
val flash = findViewById<FloatingActionButton>(R.id.flash)
val facing = findViewById<FloatingActionButton>(R.id.camera_facing)
val mirroring = findViewById<FloatingActionButton>(R.id.preview_mirroring)
fab.isEnabled = false
fab.setOnClickListener { lensCamera.startCapture(this) }
flash.isEnabled = false
flash.setOnClickListener {
when (lensFlashMode) {
LensFlashMode.On -> {
lensFlashMode = LensFlashMode.Off
flash.setImageResource(R.drawable.flash_off)
}
LensFlashMode.Off -> {
lensFlashMode = LensFlashMode.Auto
flash.setImageResource(R.drawable.flash_auto)
}
LensFlashMode.Auto -> {
lensFlashMode = LensFlashMode.On
flash.setImageResource(R.drawable.flash_on)
}
}
lensCamera.setFlashMode(lensFlashMode)
}
facing.isEnabled = false
facing.setOnClickListener {
if (lensCameraFacing == LensCameraFacing.Rear) {
lensCameraFacing = LensCameraFacing.Front
mirroring.isEnabled = true
} else {
lensCameraFacing = LensCameraFacing.Rear
mirroring.isEnabled = false
}
lensCamera.setLensFacing(lensCameraFacing)
}
mirroring.setOnClickListener {
if (lensCameraFacing == LensCameraFacing.Front)
lensCamera.isFrontFacingPreviewMirrored = !lensCamera.isFrontFacingPreviewMirrored
}
}
A customized touch listener for camera related events
Java
public class CameraTouchListener implements View.OnTouchListener, ScaleGestureDetector.OnScaleGestureListener {
private final ScaleGestureDetector gestureScale;
float scaleFactor = 1;
boolean isScaling = false;
LensCamera camera;
ImageView focusReticle;
public CameraTouchListener(Context context, LensCamera camera, ImageView focusReticle) {
this.camera = camera;
gestureScale = new ScaleGestureDetector(context, this);
this.focusReticle = focusReticle;
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
scaleFactor *= detector.getScaleFactor();
scaleFactor = scaleFactor = Math.max(scaleFactor, 1);
scaleFactor = ((float)((int)(scaleFactor * 100))) / 100;
camera.setZoomRatio(scaleFactor);
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
isScaling = true;
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
isScaling = false;
}
@Override
public boolean onTouch(View v, MotionEvent event) {
gestureScale.onTouchEvent(event);
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
return true;
case MotionEvent.ACTION_UP:
if (!isScaling)
focusReticle.setX(event.getX() - focusReticle.getWidth() / 2.0F);
focusReticle.setY(event.getY() - focusReticle.getHeight() / 2.0F);
focusReticle.setVisibility(View.VISIBLE);
camera.focusMeteringOnTap(event, LensCamera.MODE_AE | LensCamera.MODE_AF | LensCamera.MODE_AWB, () -> {
if ((focusReticle != null)&&(focusReticle.getVisibility() == View.VISIBLE)) {
focusReticle.setVisibility(View.INVISIBLE);
}
});
v.performClick();
return true;
default:
return false;
}
}
}
Kotlin
class CameraTouchListenerKt(context: Context, private var camera: LensCamera, private var focusReticle: ImageView) : View.OnTouchListener,
ScaleGestureDetector.OnScaleGestureListener {
private val gestureScale: ScaleGestureDetector = ScaleGestureDetector(context, this)
private var scaleFactor = 1f
private var isScaling = false
override fun onScale(detector: ScaleGestureDetector): Boolean {
scaleFactor *= detector.scaleFactor
scaleFactor = scaleFactor.coerceAtLeast(1f)
scaleFactor = (scaleFactor * 100).toInt().toFloat() / 100
camera.setZoomRatio(scaleFactor)
return true
}
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
isScaling = true
return true
}
override fun onScaleEnd(detector: ScaleGestureDetector) {
isScaling = false
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View, event: MotionEvent): Boolean {
gestureScale.onTouchEvent(event)
return when (event.action) {
MotionEvent.ACTION_DOWN -> true
MotionEvent.ACTION_UP -> {
if (!isScaling) focusReticle.x = event.x - focusReticle.width / 2.0f
focusReticle.y = event.y - focusReticle.height / 2.0f
focusReticle.visibility = View.VISIBLE
camera.focusMeteringOnTap(event, LensCamera.MODE_AE or LensCamera.MODE_AF or LensCamera.MODE_AWB) {
if (focusReticle.visibility == View.VISIBLE) {
focusReticle.visibility = View.INVISIBLE
}
}
v.performClick()
true
}
else -> false
}
}
}
Offline Capture
Lens offers out of the box support for capturing and signing photos and videos offline, which can be uploaded to your server or directly to the Lens API when the device regains connectivity. To learn more about the differences between online and offline capture, visit the Offline Capture Documentation.