Using the Scanner Activity

The Scanner Activity (net.gini.vision.android.ScannerActivity) is the main feature of the Gini Vision library for Android. Basically your application starts this activity and gets the cropped and enhanced image when the activity returns with results.

Starting the Scanner Activity

The Intent which triggers the scanner activity must be parameterized. There are three mandatory parameters:

ScannerActivity.EXTRA_UPLOAD_ACTIVITY
The UploadActitvity must be set which is called after the document has been captured. This class must extend the net.gini.android.vision.UploadActivity and implement the uploadDocument(..) method. There is a static helper method ScannerActivity.setUploadActivityExtra in order to make setting the activity more convenient.
ScannerActivity.EXTRA_HELP_ACTIVITY
Set an Activity used for showing help messages to the user. You can also subclass the net.gini.android.vision.help.HelpActivity, if you want to customize the default help. There is a static helper method ScannerActivity.setHelpActivityExtra in order to make setting the activity more convenient.
ScannerActivity.EXTRA_DOCTYPE_BUNDLE
The desired document type must be selected by the user and must be provided as net.gini.android.vision.DocumentType instance. Furthermore, the document type parameter must be wrapped in a bundle using the key ScannerActivity.EXTRA_DOCTYPE

You can also put optional extras in the intent. All expect a boolean value.

ScannerActivity.EXTRA_SHOW_HELP
Whether the ScannerActivity should show the HelpActivity the first time it is launched during an application run.
ScannerActivity.EXTRA_STORE_ORIGINAL
Whether the ScannerActivity should store and return the original image. Please note that all images are held in the device’s memory, not in the storage. Because of that, you should use this feature only for tests or debugging since it increases memory usage significantly.
ScannerActivity.EXTRA_SET_WINDOW_FLAG_SECURE
Whether the ScannerActivity sets the secure flag on the window to prevent taking of screenshots. All activities started by the ScannerActivity (HelpActivity and UploadActivity) will also set this flag so you don’t need to set the flag in your UploadActivity subclass.
ScannerActivity.EXTRA_CREATE_JPEG_DOCUMENT_WITH_METADATA
Whether the ScannerAcitivity creates a jpeg with some exif tags from the rectified and cropped document. The jpeg is already compressed to allow the best extraction with the smallest possible upload size. The exif tags contained in the jpeg are listed at Jpeg Exif Tags.

Example

The following example starts the ScannerActivity and the ScannerActivity will return both the original image and the enhanced image. The MyUploadActivity and the optional MyHelpActivity are classes implemented by your app.

final Intent scanIntent = new Intent(this, ScannerActivity.class);
scanIntent.putExtra(ScannerActivity.EXTRA_STORE_ORIGINAL, true);
scanIntent.putExtra(ScannerActivity.EXTRA_STORE_RECTIFIED, true);

final DocumentType documentType =  // user has selected the document type already
final Bundle docTypeBundle = new Bundle();
docTypeBundle.putParcelable(ScannerActivity.EXTRA_DOCTYPE, documentType);
scanIntent.putExtra(ScannerActivity.EXTRA_DOCTYPE_BUNDLE, docTypeBundle);

ScannerActivity.setUploadActivityExtra(scanIntent, this, MyUploadActivity.class);
// Optional: if you have a custom MyHelpActivity
ScannerActivity.setHelpActivityExtra(scanIntent, this, MyHelpActivity.class);
startActivityForResult(scanIntent, IMAGE_REQUEST);

Note

Always start the activity with the startActivityForResult() method and not with the startActivity() method. Otherwise you won’t be able to obtain the captured documents.

Getting the Results of the Scanner

The ScannerActivity finishes automatically once the user has captured an image successfully. Afterwards, the Android system calls your activity’s onActivityResult method. responseCode and the Intent data will be the values that your derived UploadActivity set.

If taking the picture was not successful due to some user interaction (e.g. the application user pressed the back button), responseCode will be Activity.RESULT_CANCELED.

If some error happened during the picture taking, responseCode will be ScannerActivity.RESULT_ERROR. The result intent has an extra ScannerActivity.EXTRA_ERROR that contains an error enumeration. See the example for details on how to extract the error enumeration. The following errors are possible:

LOW_MEMORY
There was not enough memory available to process the photo.
NO_CAMERA
The Gini Vision library was unable to connect to the camera service.

BitmapFuture

The ScannerActivity uses BitmapFuture (net.gini.android.vision.BitmapFuture) instances to pass bitmaps between activities. The BitmapFuture implements the conventional Future interface. You can always use the get() method on a BitmapFuture directly; it does return immediately and returns a Bitmap. Unfortunately, it is not possible to pass the result images directly in the result intent since Android has some hard limitations on the size of transactions.

There are two extras in the result intent which is given to the method; both are BitmapFuture instances:

ScannerActivity.EXTRA_DOCUMENT
A BitmapFuture that will get the rectified image of the document.
ScannerActivity.EXTRA_ORIGINAL
A BitmapFuture that will get the original image without any enhancements. Only available when the activity was started with the ScannerActivity.EXTRA_STORE_ORIGINAL extra.

Warning

It is important that your application always extracts the BitmapFuture instances out of the result intent. Otherwise, memory leaks are possible.

JpegFuture

Like the BitmapFuture, but instead of a Bitmap it contains a jpeg byte array. If the ScannerActivity.EXTRA_CREATE_JPEG_DOCUMENT_WITH_METADATA is set to true the jpeg will be returned using this object.

The extra containing the JpegFuture in the result intent:

ScannerActivity.EXTRA_DOCUMENT_JPEG
A JpegFuture that will get the rectified jpeg image of the document. Will be set only if ScannerActivity.EXTRA_CREATE_JPEG_DOCUMENT_WITH_METADATA was true.

Warning

It is important that your application always extracts the JpegFuture instances out of the result intent, if jpeg documents were enabled. Otherwise, memory leaks are possible.

Example

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    Bundle documentBundle;
    BitmapFuture originalFuture = null;
    BitmapFuture rectifiedFuture = null;
    BitmapFuture jpegFuture = null;

    // we always try to get the document to prevent memory leaks
    if (requestCode == IMAGE_REQUEST && data != null) {
        documentBundle = data.getBundleExtra(ScannerActivity.EXTRA_DOCUMENT_BUNDLE);
        if (documentBundle != null) {
            originalFuture = documentBundle.getParcelable(ScannerActivity.EXTRA_ORIGINAL);
            rectifiedFuture = documentBundle.getParcelable(CaptureActivity.EXTRA_DOCUMENT);
            jpegFuture = documentBundle.getParcelable(CaptureActivity.EXTRA_DOCUMENT_JPEG); // use only if ScannerActivity.EXTRA_CREATE_JPEG_DOCUMENT_WITH_METADATA was true
        }
    }

    if (requestCode == IMAGE_REQUEST && resultCode == RESULT_OK) {
        // everything ok, do something with the document
    } else if (requestCode == IMAGE_REQUEST && resultCode == ScannerActivity.RESULT_ERROR) {
        final ScannerActivity.Error error = data.getParcelableExtra(ScannerActivity.EXTRA_ERROR);
        // unfortunately there was an error we must handle
    }
 }

Jpeg Exif Tags

In order to improve our service we would like to receive a few exif tags about the circumstances in which the document was photographed.

Only the following exif tags are kept in the jpeg file. No personal, geographical or otherwise sensitive information is kept.

Make
The manufacturer of the device.
Model
The model name or model number of the device.
ISO
The ISO Speed and ISO Latitude of the camera as specified in ISO 12232.
Exposure Time
Exposure time, given in seconds.
Aperture
The lens aperture. The unit is the APEX value.
Flash
Status of flash when the image was shot.
Compressed Bits Per Pixel
The compression mode used for a compressed image is indicated in unit bits per pixel.
Orientation
The image orientation derived from the device orientation.
Platform
Android.
OS Version
Android OS version.
GiniVision Version
GiniVision SDK version.

Upload the Document to Gini using the Gini API SDK

First of all, we have to include the Gini API SDK as dependency in the build.gradle. Check out the Gini SDK Android documentation’s Getting Started section for details.

The Gini SDK provides a Gini class which acts as a facade to the whole functionality of the Gini API. Your app must create a single instance of this class and hold it for its complete lifecycle.

Finally, the Gini instance is used in the uploadDocument(Bitmap, byte[]) method of the MyUploadActivity which extends net.gini.android.vision.UploadActivity to upload the captured document. The example below shows a possible implementation which puts the result extractions into a new intent for further processing.

Note

The abstract method uploadDocument(Bitmap) in net.gini.android.vision.UploadActivity was deprecated. You should move the method body to the new uploadDocument(Bitmap, byte[]) method and leave the old one empty.

public void uploadDocument(final Bitmap document, final byte[] jpeg) {
   // Get the doctype, if available
   DocumentType documentType = DocumentType.INVOICE;
   final Bundle documentTypeBundle = getIntent().getBundleExtra(UploadActivity.EXTRA_DOCTYPE_BUNDLE);
   if (documentTypeBundle != null) {
       DocumentType docType = documentTypeBundle.getParcelable(UploadActivity.EXTRA_DOCTYPE);
       if (docType != null) documentType = docType;
   }

   DocumentTaskManager.DocumentType docType = null;
   switch (documentType) {
       case INVOICE:
           docType = DocumentTaskManager.DocumentType.INVOICE;
           break;
       case REMITTANCE_SLIP:
           docType = DocumentTaskManager.DocumentType.REMITTANCE_SLIP;
           break;
       case INTEGRATED_REMITTANCE_SLIP:
           docType = DocumentTaskManager.DocumentType.REMITTANCE_SLIP;
           break;
   }

   // Upload the jpeg, if available, otherwise try to upload the Bitmap
   Task<Document> createDocumentTask = null;
   if (jpeg != null) {
       createDocumentTask = documentTaskManager.createDocument(jpeg, null, docType);
   } else if (document != null) {
       createDocumentTask = documentTaskManager.createDocument(document, null, docType);
   }

   if (createDocumentTask == null) {
       final Intent resultIntent = new Intent();
       resultIntent.putExtra(EXTRA_ERROR_STRING, "No bitmap or jpeg to upload.");
       setResult(RESULT_UPLOAD_ERROR, resultIntent);
       finish();
   } else {
       createDocumentTask
           .onSuccessTask(new Continuation<Document, Task<Document>>() {
               @Override
               public Task<Document> then(Task<Document> task) throws Exception {
                   final Document document = task.getResult();
                   documentId = document.getId();
                   return documentTaskManager.pollDocument(document);
               }
           })
           .onSuccessTask(new Continuation<Document, Task<Map<String, SpecificExtraction>>>() {
               @Override
               public Task<Map<String, SpecificExtraction>> then(Task<Document> task) throws Exception {
                   return documentTaskManager.getExtractions(task.getResult());
               }
           })
           .onSuccess(new Continuation<Map<String, SpecificExtraction>, Object>() {
               @Override
               public Object then(Task<Map<String, SpecificExtraction>> task) throws Exception {
                   final Map<String, SpecificExtraction> extractions = task.getResult();
                   final Bundle extractionsBundle = new Bundle();
                   for (Map.Entry<String, SpecificExtraction> entry : extractions.entrySet()) {
                       extractionsBundle.putParcelable(entry.getKey(), entry.getValue());
                   }
                   final Intent result = new Intent();
                   result.putExtra(EXTRA_DOCUMENT_ID, documentId);
                   result.putExtra(EXTRA_EXTRACTIONS, extractionsBundle);
                   setResult(RESULT_OK, result);
                   return null;
               }
           })
           .continueWith(new Continuation<Object, Object>() {
               @Override
               public Object then(Task<Object> task) throws Exception {
                   if (task.isFaulted()) {
                       //noinspection ThrowableResultOfMethodCallIgnored
                       final Exception exception = task.getError();
                       // We could inspect the exception here -- should always be a subclass of a
                       // com.android.volley.VolleyError. For example:
                       // * AuthFailureError: Client credentials or access token was wrong
                       // * ConnectError, TimeoutError: can be caused by a flaky connection
                       // * NoConnectionError: (Likely) no Internet connection available
                       LOG.debug("Upload failed:", exception);
                       // Signal to parent activity that the uploading failed
                       final Intent resultIntent = new Intent();
                       resultIntent.putExtra(EXTRA_ERROR_STRING, exception.toString());
                       setResult(RESULT_UPLOAD_ERROR, resultIntent);
                   }
                   LOG.debug("done");
                   runOnUiThread(new Runnable() {
                       @Override
                       public void run() {
                           finish();
                       }
                   });
                   return null;
               }
           });
   }
}

Full Example

Below is a full example of an activity which displays a button. When the user clicks on the button, the activity starts the document scanner. Once the user has successfully scanned a document, the ScannerActivity returns and the example activity can use the rectified bitmap of the document.

package net.gini.android.visiontest;

import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Bundle;

import net.gini.android.vision.BitmapFuture;
import net.gini.android.vision.ScannerActivity;


public class ExampleActivity extends Activity {

    protected static final int IMAGE_REQUEST = 1;

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

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        Bundle documentBundle;
        BitmapFuture originalFuture = null;
        BitmapFuture rectifiedFuture = null;
        BitmapFuture jpegFuture = null;

        // we always try to get the document to prevent memory leaks
        if (requestCode == IMAGE_REQUEST && data != null) {
            documentBundle = data.getBundleExtra(ScannerActivity.EXTRA_DOCUMENT_BUNDLE);
            if (documentBundle != null) {
                originalFuture = documentBundle.getParcelable(ScannerActivity.EXTRA_ORIGINAL);
                rectifiedFuture = documentBundle.getParcelable(CaptureActivity.EXTRA_DOCUMENT);
                jpegFuture = documentBundle.getParcelable(CaptureActivity.EXTRA_DOCUMENT_JPEG); // use only if ScannerActivity.EXTRA_CREATE_JPEG_DOCUMENT_WITH_METADATA was true
            }
        }

        if (requestCode == IMAGE_REQUEST && resultCode == RESULT_OK) {
            // everything ok, do something with the document
        } else if (requestCode == IMAGE_REQUEST && resultCode == ScannerActivity.RESULT_ERROR) {
            final ScannerActivity.Error error = data.getParcelableExtra(ScannerActivity.EXTRA_ERROR);
            // unfortunately there was an error we must handle
        }
    }


    /**
     * Callback which is called when the user clicked on a button. Starts the document scanner.
     */
    public void onClick(View view) {
        Intent scanIntent = new Intent(this, ScannerActivity.class);
        scanIntent.putExtra(ScannerActivity.EXTRA_STORE_RECTIFIED, true);
            // MyUploadActivity is your Activity that implements the upload or processing logic.
            // It needs to be derived from Gini Vision library's UploadActivity.
        ScannerActivity.setUploadActivityExtra(scanIntent, this, MyUploadActivity.class);
        // Optional: if you have a custom MyHelpActivity
        ScannerActivity.setUploadActivityExtra(scanIntent, this, MyHelpActivity.class);
        startActivityForResult(scanIntent, IMAGE_REQUEST);
    }
}

See also

The Android documentation on Getting a result from an activity.