Lens Documentation

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 camera
  • Permissions_Location - check location permissions
  • Env_AirplaneMode - airplane mode is enabled and no internet connection is available
  • Env_DevMode - development mode or usb debugging is enabled
  • Env_Network - no internet connection is available
  • Env_Rooted - device is rooted
  • Env_PlayStoreServices - out of date or no play store services available on the device
  • Env_Location - location services are not available/enabled
  • Env_Location_Not_Ready - temporary while device is determining current location
  • Camera - errors associated with using the camera
  • Attestation - device integrity is compromised, more details are provided within the message
  • Enrollment - device enrollment errors
  • KeyGeneration - errors occuring while working and generating cryptographic keys
  • Native - errors associated with native code
  • Undefined - 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 TypeMethod Name and Description
voidsetZoomRatio(float zoomRatio)
Use this method to tell the camera how much to zoom.
voidfocusMeteringOnTap(MotionEvent motionEvent, int meteringMode, @Nullable Runnable focusMeteringOnTapCallBack)
Use this method to tell the camera which metering mode to use.
voidsetIsFrontFacingPreviewMirrored(boolean mirrored)
Set front facing preview mirroring (this does not impact the captured image, or rear facing* camera).
booleangetIsFrontFacingPreviewMirrored()
Gets the current mirroring status
voidsetFlashMode(LensFlashMode lensFlashMode)
Sets the flash mode Off, Auto, and On
voidsetLensFacing(LensCameraFacing lensCameraFacing)
Sets which camera to use Front (aka selfie) or Back.
voidsetPreviewScale(LensPreviewScale LensPreviewScale)
Sets preview scaling: fit (default) or fill.
LensPreviewAspectRatiogetPreviewAspectRatio()
Gets current aspect ratio of a preview. Answer might differ based on the capture mode.
BitmapgetPreviewBitmap()
Extracts bitmap from preview.
floatgetLocationAccuracyThreshold()
Gets the location accuracy threshold in meters. The default value is 1500m
voidsetLocationAccuracyThreshold(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
        }
    }

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.

Code Copied

Glossary

Search Terms

  • API

  • C2PA

  • CAI

  • Controlled Capture

  • Entropy

  • EXIF

  • Geocoded

  • IDE

  • JPEG

  • JSON

  • Lens SDK

  • Provenance

  • Recapture

  • Reverse Image Search

  • XMP