Download the Mobile Capture 2 SDK (version 20118) + OpenCV here.
Download the Mobile Capture 2 SDK (version 20120) + OpenCV here.
Download the Mobile Capture 2 SDK (version 20122) + OpenCV here.
Download the Mobile Capture 2 SDK (version 20138) + OpenCV here.
Download the Mobile Capture 2 SDK (version 20143) + OpenCV here.
Download the Mobile Capture 2 SDK (version 20146) + OpenCV here.
Download the Mobile Capture 2 SDK (version 20161) + OpenCV here.
apply plugin: ...
android {
...
}
allprojects {
repositories {
jcenter()
flatDir {
dirs 'libs'
}
}
}
dependencies {
...
}
dependencies {
...
compile(name: 'MobileCapture2SDK-release', ext: 'aar')
compile(name: 'OpenCV-Android-release', ext: 'aar')
compile 'com.commit451:PhotoView:1.2.4@aar'
compile 'com.squareup.okhttp3:okhttp:3.4.1' // Required starting sdk version 20120
compile 'com.squareup.okhttp3:logging-interceptor:3.4.1' // Required starting sdk version 20130
}
<application
android:name=".MyApplication"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
public ClientConnector(Context context);
The AndroidManifest.xml file requires several permissions:
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" android:required="true" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <!-- Only if you want to import images from storage -->
Google Cloud messaging requires extra AndroidManifest adjustments, please see the seperate section about Google Cloud Messaging.
public void setCachePaths(Context context, String value);
((MyApplication)getApplication()).clientConnector.setCachePaths(getApplicationContext(), email);
In this example we set the cache path to the login name to make sure all users have their own data and documents on the same device.
public void login(Context context, String loginName, String password, LoadCallback loadCallback);
public void login(Context context, String configurationUniqueId, LoadCallback loadCallback);
public interface LoadCallback {
public void loadCompleted(boolean success, Throwable exception);
}
((MyApplication)getApplication()).clientConnector.login(getApplicationContext(), mEmail, mPassword, new ClientConnector.LoadCallback() {
@Override
public void loadCompleted(boolean success, Throwable throwable) {
if (success) {
// *Check document type*
} else {
if (throwable != null) {
// Login error
} else {
// No internet connection
}
}
}
});
if (((MyApplication)getApplication()).clientConnector.getDocumentTypes().length() > 1) {
int documentTypeIndex = 0;
// Show user which document types are available and set documentTypeIndex based on user input
((MyApplication)getApplication()).clientConnector.selectDocumentType(documentTypeIndex);
} else {
// Only 1 document type available
((MyApplication)getApplication()).clientConnector.selectDocumentType(0);
}
loadFromString(String configuration) throws JSONException;
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<SurfaceView
android:id="@+id/surface_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center" />
<com.cumuluspro.mobilecapture2sdk.BoundaryView
android:id="@+id/boundary_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
<com.cumuluspro.cropit.CropImageView
android:id="@+id/crop_image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="invisible"
android:background="@android:color/black" />
</FrameLayout>
public class CameraFragment extends BoundaryDetectFragment implements SurfaceHolder.Callback,
Camera.PreviewCallback, com.cumuluspro.mobilecapture2sdk.CaptureActivity {
}
void setCaptureToast(String title);
void setCaptureToast(String title, int removeDelay);
void disableCaptureToast();
void setIsCropping(boolean state); // state is true when manually clicking on a capture button, state is false after the image has been cropped.
void toggleImportState(boolean state);
void readyForCrop();
- setCaptureToast(String title) gets called a few times in a row to show what is happening in the background (Please hold still, Detecting page, Saving page, etc)
- setCaptureToast(String title, int removeDelay) is the same above but only for error messages (Could not find page, etc). The removeDelay is an optional value to remove the message after miliSeconds.
- disableCaptureToast() gets called when the message needs to be removed from view (After manual capture - before cropping, after cropping)
- setIsCropping(state) is true when manually clicking on a capture button, state is false after the image has been cropped
- toggleImportState(state) is true when a page could not be found. resumeCapture() gets called with it so this can also be used.
- readyForCrop gets called when a manual image has been captured and cropped, ready for the user to make adjustments in CropImageView.
@Override
public void onPreviewFrame(byte[] bytes, Camera camera) {
if (!isProcessing) {
frameData = bytes;
previewHandler.post(processFrame);
}
}
- When the BoundaryDetectFragment class is not busy processing a frame, set frameData to this new frame.
- processFrame is a Runnable inside BoundaryDetectFragment that processes the frameData. previewHandler is a Handler that makes processFrame do it's work on the main thread since it also needs to draw document boundaries:
Handler previewHandler = new Handler(Looper.getMainLooper());
1. surfaceCreated:
@Override
public void surfaceCreated(SurfaceHolder holder) {
if (camera == null) return;
ParentActivity parentActivity = (ParentActivity) getActivity();
setCameraDisplayOrientation(parentActivity, camera);
}
2. surfaceChanged:
See the example code
3. surfaceDestroyed:
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
if (camera != null) {
camera.setPreviewCallback(null);
camera.stopPreview();
camera.release();
camera = null;
}
}
btnFab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
manual = true;
takePicture()
}
});
Capturing an image has been made much easier. Using the BoundaryDetectFragment class and the CaptureCallbacks interface is all there is.
private float pxFromDp(float dp) { return dp * getResources().getDisplayMetrics().density; }
<FrameLayout android:id="@+id/content_frame" android:layout_width="match_ parent" android:layout_height="match_ parent"> <!--Capture fragment is inserted here--> </FrameLayout>
The SDK provides the method "importImage":
public void importImage(byte[] img, File filesDir)
@param img: Image data as a byte array
@param filesDir: Where to store a temporary crop file. (Usually the default getFilesDir())
You can, for example, use Android's default image picking capabilities as followed:
Intent intent = new Intent(); intent.setType("image/*"); // If you have some specific collection (identified by a Uri) that you want the user to pick from, use ACTION_PICK intent.setAction(Intent.ACTION_GET_CONTENT); startActivityForResult(Intent.createChooser(intent, "Select Picture"), PICK_IMAGE_INTENT_ID);
Use onActivityResult to get the image data when picked and insert this into the importImage method as a byte array.
@Override public void onActivityResult(int requestCode, int resultCode, Intent result) { super.onActivityResult(requestCode, resultCode, result); switch (requestCode) { case PICK_IMAGE_INTENT_ID: if (resultCode == RESULT_OK) { if (cameraFragment != null) { Uri uri = result.getData(); ByteArrayOutputStream byteArrayOutStream = new ByteArrayOutputStream(); InputStream inputStream; try { inputStream = getContentResolver().openInputStream(uri); byte[] buffer = new byte[1024]; int n; while (-1 != (n = inputStream.read(buffer))) { byteArrayOutStream.write(buffer, 0, n); } } catch (Exception e) { e.printStackTrace(); } cameraFragment.importImage(byteArrayOutStream.toByteArray(), getFilesDir()); } } break; } }
The importImage method will always open the crop view which will in turn call the callback "cropStarted". When the image has been cropped, the "imageTaken" callback is called.
File thumbnail = clientConnector.getThumbnail(captureIndex);
Bitmap bitmap = null;
if (thumbnail.exists()) {
bitmap = BitmapFactory.decodeFile(thumbnail.getAbsolutePath());
}
if (bitmap != null) {
// insert bitmap into an ImageView. Get a reference to your layout's ImageView
imageView.setImageBitmap(bitmap);
}
<com.cumuluspro.mobilecapture2sdk.CustomPhotoView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
CustomPhotoView customPhotoView = (CustomPhotoView) view.findViewById(R.id.image);
customPhotoView.loadPicture(clientConnector, captureIndex);
clientConnector.rotateCaptureImage(captureIndex);
clientConnector.removeCaptureImage(captureIndex);
clientConnector.moveCaptureImage(fromIndex, toIndex);
if (clientConnector.getFieldDefinitionsCount() > 0) {
// Start indexing
} else {
// Start uploading (see past this indexing section)
}
JSONArray fieldDefinitions = clientConnector.getFieldDefinitions();
JSONObject fieldDefinition = fieldDefinitions.getJSONObject(index);
// What kind of field definition is it?
boolean isString = clientConnector.isString(fieldDefinition);
boolean isLookup = clientConnector.isLookup(fieldDefinition);
... ... = ... .isBoolean(...), isNumber, isAmount, isDate, isDatetime, isPassword, isReadOnly, isRequired
String fieldName = fieldDefinition.getString("Name");
String fieldValue = clientConnector.getFieldValue(fieldName);
String fieldDisplayName = fieldDefinition.getString("DisplayName");
String fieldDefaultValue = fieldDefinition.getString("DefaultValue");
int fieldMaxLength = fieldDefinition.getInt("MaximumLength");
String errorMessage = clientConnector.getErrorMessage(fieldName);
// Lookup field values
JSONArray lookupValues = fieldDefinition.getJSONArray("LookupValues");
// Update a field value
clientConnector.setFieldValue(fieldName, "value");
if (clientConnector.isConnected(getApplicationContext()) {
// Save uniqueId before finishing current session (and thus refreshing the captureUniqueId)
String uniqueId = clientConnector.getCaptureUniqueId();
// Finish the current capture session and set it's status to NEW (as a new upload)
clientConnector.finishCaptureSession(ClientConnector.UploadStatus.NEW);
try {
//clientConnector.uploadHistory(clientConnector.getHistoryInfo(uniqueId)); // Deprecated
clientConnector.upload(clientConnector.getHistoryInfo(uniqueId));
} catch (JSONException ex) {
ex.printStackTrace();
}
} else {
clientConnector.finishCaptureSession(ClientConnector.UploadStatus.WAITING);
}
public enum UploadStatus {
NEW(R.string.upload_status_new),
WIFIPENDING(R.string.upload_status_wifi_pending),
WAITING(R.string.upload_status_waiting),
UPLOADING(R.string.upload_status_uploading),
RETRY(R.string.upload_status_retry),
UPLOADED(R.string.upload_status_uploaded),
EDIT(R.string.upload_status_edit),
FAILED(R.string.upload_status_failed),
UNKNOWN(R.string.upload_status_unknown),
INPROGRESS(R.string.upload_status_in_progress),
SAVEDSESSION(R.string.upload_status_saved_session);
private int resourceId;
UploadStatus(int resourceId) {
this.resourceId = resourceId;
}
public int getResourceId() { return resourceId; }
public static UploadStatus safeValueOf(String value) {
for (UploadStatus status : values()) {
if (status.name().equals(value)) {
return status;
}
}
return UploadStatus.UNKNOWN;
}
}
public interface UploadListCallback {
void uploadStatusChanged(String uniqueId, boolean success, Throwable exception);
void uploadMetadataFinished(String uniqueId);
void uploadProgress(String uniqueId, int page, long pageBytes, long pageWrittenBytes);
}
clientConnector.finishCaptureSession(ClientConnector.UploadStatus.NEW); try { clientConnector.addUploadListCallback(new ClientConnector.UploadListCallback() { @Override public void uploadStatusChanged(String uniqueId, boolean success, Throwable throwable) { if (app.clientConnector.getHistoryStatus(uniqueId).equals(ClientConnector.UploadStatus.UPLOADED)) { app.clientConnector.removeUploadListCallback(this); // Remove the entry after upload? or keep it to view it later. Returns a string resource id if failed to remove. Integer errorResource = app.clientConnector.removeHistory(uniqueId); if (errorResource != null) { Toast.makeText(getApplicationContext(), context.getResources().getString(errorResource);, Toast.LENGTH_SHORT).show(); } } @Override public void uploadMetadataFinished(String uniqueId) { // Metadata gets uploaded first } @Override public void uploadProgress(String uniqueId, int page, long pageBytes, long pageWrittenBytes) { // progressBar.setProgress((int) (pageWrittenByes * 100 / pageBytes));} }); } catch (JSONException e) { e.printStackTrace(); }clientConnector.uploadHistory(clientConnector.getHistoryInfo(uniqueId));
public JSONObject getHistoryInfo(int index);
public JSONObject getHistoryInfo(String uniqueId);
clientConnector.fetchNotificationMessages();
public interface NotificationsCallback {
public void notificationsFetched(int notificationsCount, Throwable throwable);
public void notificationRemoved(String uniqueId);
}
ClientConnector.NotificationsCallback notificationsCallback = new ClientConnector.NotificationsCallback() {
@Override
public void notificationsFetched(int newNotificationsCount, Throwable throwable) {
}
@Override
public void notificationRemoved(String uniqueId) {
}
};
clientConnector.setNotificationsCallback(notificationsCallback);
JSONArray historyEntries = clientConnector.historyEntries();
JSONObject info = clientConnector.getHistoryInfo(index);
String uniqueId = info.getString("uniqueId")
ClientConnector.UploadStatus status = clientConnector.getHistoryStatus(uniqueId);
String specificValue = clientConnector.getHistoryDataFieldString(uniqueId, String *KEY*);
public interface NotificationsUnreadCallback {
void onCountChanged(int unreadCount);
}
Set this interface anywhere in your application by using the following
ClientConnector.NotificationsUnreadCallback notificationsUnreadCallback = new ClientConnector.NotificationsUnreadCallback() {
@Override
public void onCountChanged(int unreadCount) {
}
}
clientConnector.setNotificationsUnreadCallback(notificationsUnreadCallback);
The following functions are overloaded to allow additional headers to be set in the HttpPost of the methods:
HashMap<String, String> additionalHeaders = new HashMap<>();
additionalHeaders.put("Accept", "application/json");
clientConnector.upload(clientConnector.getHistoryInfo(uniqueId), additionalHeaders);
https://developers.google.com/cloud-messaging/android/start
Use the following link to get started: https://developers.google.com/mobile/add?platform=android
- Enter your application and package name and click on continue.
- Enable the services you want to use. ‘Cloud Messaging’ is required.
- Save the ‘Server API key’ inside the ‘Cloud Messaging’ tab/icon for later use. Example key: AIzaSyApj62Xv4F4B52xNd4aegBccSxwzptkNr0
- Click on Continue to ‘Generate configuration files’
- Download the ‘google-services.json’ file and copy it to the app/ or mobile/ module directory in your Android project.
Use the following link for a complete overview: https://developers.google.com/cloud-messaging/android/client
- Add the dependency to your project-level build.gradle: classpath 'com.google.gms:google-services:2.0.0-alpha6'
- Add the plugin to your app-level build.gradle: apply plugin: 'com.google.gms.google-services'
- Add the dependency to your app-level build.gradle: compile "com.google.android.gms:play-services-gcm:8.4.0"
- *If you want to use Analytics or other play-services, check the following link: https://developers.google.com/android/guides/setup#add_google_play_services_to_your_project *
- Your manifest must contain the following:
<manifest package="com.example.gcm" ...>
<uses-sdk android:minSdkVersion="8" android:targetSdkVersion="17"/>
<uses-permission android:name="android.permission.WAKE_LOCK" />
<permission android:name="<your-package-name>.permission.C2D_MESSAGE"
android:protectionLevel="signature" />
<uses-permission android:name="<your-package-name>.permission.C2D_MESSAGE" />
<application ...>
<receiver
android:name="com.google.android.gms.gcm.GcmReceiver"
android:exported="true"
android:permission="com.google.android.c2dm.permission.SEND" >
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<category android:name="com.example.gcm" />
</intent-filter>
</receiver>
<service
android:name="com.example.MyGcmListenerService"
android:exported="false" >
<intent-filter>
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
</intent-filter>
</service>
<service
android:name="com.example.MyInstanceIDListenerService"
android:exported="false">
<intent-filter>
<action android:name="com.google.android.gms.iid.InstanceID" />
</intent-filter>
</service>
</application>
</manifest>
- The permission ‘WAKE_LOCK’ is optional but needed if you want your device to wake up the processor when a message is received.
- the permission and uses-permission <package-name>. permission.C2D_MESSAGE is prevents other Android application from registering and receiving the Android application’s messages.
- The <application> tag holds 1 receiver and 2 services. The receiver only requires you to change the category android:name to your package name. Both the service tags point to classes you have to create ‘MyGcmListenerService’ and ‘MyInstanceIDListenerService’.
*Note: If you want to support pre-4.4 KitKat devices, add the following action to the intent filter declaration for the receiver: <action android:name="com.google.android.c2dm.intent.REGISTRATION" /> (Below the other <action> tag inside the receiver)*
- Create a class for obtaining a registration token / Instance ID. This class will be the class inserted in the second <service> tag in our manifest of step 2.2 (MyInstanceIDListenerService).
- The class will look like the following: https://github.com/googlesamples/google-services/blob/master/android/gcm/app/src/main/java/gcm/play/android/samples/com/gcmquickstart/RegistrationIntentService.java
- The only change needed is in the sendRegistrationToServer(String token) function: 1. Get a reference to the CumulusPro SDK’s ClientConnector class. Our demo application stores a reference in a custom Application class. This way, the ClientConnector can be reached by using: CustomApplicationClass app = (CustomApplicationClass) getApplication(); 2. Set the device unique token in the ClientConnector: app.getClientConnector().setDeviceUniqueToken(token);
- The actual sending of the token to the server will be handled by the ClientConnector when uploading
- Create a class for refreshing the token. This class will be the class inserted in the first <service> tag in our manifest of step 2.2 (MyGcmListenerService).
- The class will look like the following: https://github.com/googlesamples/google-services/blob/master/android/gcm/app/src/main/java/gcm/play/android/samples/com/gcmquickstart/MyInstanceIDListenerService.java
- The only change needed is changing RegistrationIntentService.class to your own class created at the beginning of this step (Your own MyInstanceIDListenerService).
- All applications that rely on the Play Services SDK should always check the device for a compatible Google Play services APK before accessing Google Play service features.
- See our demo application’s PreLoginActivity class
- In your Application’s first Activity:
@Override
protected void onResume() {
super.onResume();
startGcmRegistrationService();
}
private void startGcmRegistrationService() {
GoogleApiAvailability api = GoogleApiAvailability.getInstance();
int code = api.isGooglePlayServicesAvailable(this);
if (code == ConnectionResult.SUCCESS) {
// Google services up to date
onActivityResult(1234, Activity.RESULT_OK, null);
} // Google services out of date but user can resolve that
else if (api.isUserResolvableError(code) && api.showErrorDialogFragment(this, code, 1234, new DialogInterface.OnCancelListener() {
// If the user cancels the api.showErrorDialogFragment -> show a warning dialog
@Override
public void onCancel(DialogInterface dialogInterface) {
final Dialog dialogWarning = new Dialog(PreLoginActivity.this);
dialogWarning.requestWindowFeature(Window.FEATURE_NO_TITLE);
dialogWarning.setContentView(R.layout.dialog_warning_confirm);
TextView title = (TextView) dialogWarning.findViewById(R.id.txt_dialog_warning_title);
title.setText(getString(R.string.dialog_warning_title));
TextView text = (TextView) dialogWarning.findViewById(R.id.txt_dialog_warning_content);
text.setText(getString(R.string.dialog_warning_google_services_update));
TextView btnCancel = (TextView) dialogWarning.findViewById(R.id.cancel);
btnCancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
dialogWarning.cancel();
}
});
dialogWarning.setOnCancelListener(new DialogInterface.OnCancelListener() {
@Override
public void onCancel(DialogInterface dialogInterface) {
finish();
}
});
TextView btnOk = (TextView) dialogWarning.findViewById(R.id.ok);
btnOk.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
dialogWarning.dismiss();
startGcmRegistrationService();
}
});
dialogWarning.show();
}
})) {
// wait for onActivityResult call
} else {
String str = GoogleApiAvailability.getInstance().getErrorString(code);
Toast.makeText(this, str, Toast.LENGTH_LONG).show();
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch(requestCode) {
case 1234:
if (resultCode == Activity.RESULT_OK) {
Intent i = new Intent(this, RegistrationIntentService.class); // Replace this to your custom MyInstanceIDListenerService class created in step 2.3 (The class that has: sendRegistrationToServer)
startService(i); // OK, init GCM
}
break;
default:
super.onActivityResult(requestCode, resultCode, data);
}
}
- The startGcmRegistrationService function contains code from the demo application. When a user cancels the api.showErrorDialogFragment a custom warning dialog will either show the api dialog again or finish the activity.
- Receiving messages happen in two stages: 1. Android system Notification 2. Application Notification. They do not depend on eachother. The Android system notification is received using Google Cloud Messaging and the application notification will receive data from CumulusPro’s NotificationService.
- To handle Android system notifications, create a class that extends GcmListenerService. See the demo application’s GcmService class.
- Create a service tag in the Manifest file as followed:
<service android:name="com.example.yourapplication.GcmService" android:exported="false"> <intent-filter> <action android:name="com.google.android.c2dm.intent.RECEIVE" /> </intent-filter> </service>
- The GcmService class now receives Notifications in it’s onMessageReceived(String from, final Bundle data) function.
- The demo application uses this notification to immediately handle application notifications, it will either create a system notification or delete an application notification.
- At any point after logging in, the function fetchNotificationMessages() can be called in the ClientConnector to update the Application’s Notifications.