Mobile Capture 2 SDK for Android

Downloading the SDK

Download OpenCV for Android

 

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.

 

Including the SDK

 

Tip: Set your Android Studio project panel (View > Tool Windows > Project) to show the entire 'Project' directory instead of only 'Android'.
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
}

 

 

Using the SDK

Instantiate the ClientConnector

<application
    android:name=".MyApplication"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:theme="@style/AppTheme">
Tip: Access the ClientConnector from a class that extends Activity as followed: ((MyApplication)getApplication()).clientConnector;

public ClientConnector(Context context);

 

 

 

AndroidManifest

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.

 

 

Login

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.

  1. Using login credentials (Username and Password)
  2. Using an organization's unique ID (configurationUniqueId) (In the admin panel this Unique ID can be found in Organization settings) (When logging in using a configurationUniqueId, a user logs in anonymously and all document types will be available) (The admin panel Organization settings has an option to set a default anonymous account)
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);
}

 

 

Login flow Recap:
  1. setCachePaths(..., ...) //optional
  2. login(..., ..., ..., loadCallback.loadCompleted) -> 
  3. check for document types: getDocumentTypes() & selectDocumentType(index);

 

 

 

Loading configuration data into the ClientConnector

loadFromString(String configuration) throws JSONException;

 

 

 

Capture an image - Deprecated (after version 2.0.9)

  1. SurfaceView (Provides frames to process)
  2. com.cumuluspro.mobilecapture2sdk.BoundaryView (On top of SurfaceView, document boundary lines get drawn on this view)
  3. com.cumuluspro.cropit.CropImageView (On top of SurfaceView and BoundaryView, shows the captured image with cropping lines)
<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;
    }
}

 

 

Taking the picture
  1. Automatic capture
  2. Manual capture
btnFab.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        manual = true;
        takePicture()      
    }
});
  1. protected void cameraTakePictureFocussed(ClientConnector clientConnector);
  2. protected void cameraTakePictureNoFocus(ClientConnector clientConnector);
  1. public interface CaptureActivity { void readyForCrop(); }
  2. BoundaryDetectFragment: public interface FinishedTakingPicture { void finishedTakingPicture(); }

 

Image taking picture flow Recap
  1. takePicture() is called automatically
  2. cameraTakePictureFocussed() or cameraTakePictureNoFocus() has to be called inside takePicture().
  3. pictureTaken() is called automatically
  4. callback: finishedTakingPicture();
  1. set manual = true
  2. call takePicture() manually
  3. cameraTakePictureFocussed() or cameraTakePictureNoFocus() has to be called inside takePicture().
  4. pictureTaken() is called automatically
  5. callback: readyForCrop();
  6. call crop()
  7. callback: finishedCrop()

 

 

 

Capture an image (sdk version 2100 and above)

Capturing an image has been made much easier. Using the BoundaryDetectFragment class and the CaptureCallbacks interface is all there is.

 

public class MyClass extends AppCompatActivity implements CaptureCallbacks {
 
    private BoundaryDetectFragment boundaryDetectFragment;
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super...
        setContentView....
 
        boundaryDetectFragment = BoundaryDetectFragment.newInstance(clientConnector, this);
 
        // Starting SDK version 20122, a few settings can be set on the BoundaryDetectFragment to alter the way it works:
        boundaryDetectFragment.setCropZoomScaleFactor(2f); // sets the amount of zoom when altering a point in the crop screen (set to 1f for no zooming)
        boundaryDetectFragment.showCropAfterAutosnap = false; // can be set to true to be able to crop after automatic image taking
        boundaryDetectFragment.showCropAfterManual = true; // can be set to false to automatically crop a detected document or crops to the corner indicators when no document has been found
        boundaryDetectFragment.showDarkBackgroundBehindCorners = false; // the dark background is behind the detected boundaries by default, setting to true sets the dark background statically outside the corner indicators
        boundaryDetectFragment.showCornerIndicators = true; // can be set to false to hide the corner indicators
        boundaryDetectFragment.showDetectedBoundaries = true; // can be set to false to hide the detected document boundaries
        boundaryDetectFragment.showScreenFlashOnTakePicture = true; // can be set to false to hide the screen flash when taking a picture
        boundaryDetectFragment.maxResolution = new Pair<Integer, Integer>(1500, 1100); // SDK Version 20146 and up. Values of the pair are checked separately
        
        // Crop view colors (SDK Version 20146 and up
        Paint edgesPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        edgesPaint.setColor(Color.rgb(77, 77, 204));
        edgesPaint.setStyle(Paint.Style.STROKE);
        edgesPaint.setStrokeWidth(pxFromDp(2));
        edgesPaint.setPathEffect(new DashPathEffect(new float[]{pxFromDp(5), pxFromDp(5)}, 0));
        cameraFragment.setCropEdgesPaint(edgesPaint);
       
        Paint invalidEdgesPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        invalidEdgesPaint.setColor(Color.RED);
        invalidEdgesPaint.setStyle(Paint.Style.STROKE);
        invalidEdgesPaint.setStrokeWidth(pxFromDp(2));
        invalidEdgesPaint.setPathEffect(new DashPathEffect(new float[]{pxFromDp(5), pxFromDp(5)}, 0));
        cameraFragment.setInvalidCropEdgesPaint(invalidEdgesPaint);
 
        Paint cornersPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        cornersPaint.setColor(Color.argb(102, 255, 255, 255));
        cornersPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        cameraFragment.setCropCornersPaint(cornersPaint);
       
        Paint activeCornersPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        activeCornersPaint.setColor(Color.argb(102, 255, 255, 255));
        activeCornersPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        cameraFragment.setActiveCropCornerPaint(activeCornersPaint);
 
        // utility function for the cropview options:

        private float pxFromDp(float dp) {  return dp * getResources().getDisplayMetrics().density; }

 

 

 
        boundaryDetectFragment.canvasReadyCallback = new BoundaryDetectFragment.CanvasReadyCallback() {
            @Override
            public void ready(int width, int height) {
                // ID card corner indicators example
                int horizontalMargin = width / 10;
                double factor = 0.66;
                int wantedWidth = width - (2 * horizontalMargin);
                int wantedHeight = (int)(factor * (double)wantedWidth);
                int verticalMargin = (height - wantedHeight) / 2;
 
                boundaryDetectFragment.setBorderMargins(horizontalMargin, verticalMargin);
 
                // additional settings that have to be set in the canvasReady callback
                boundaryDetectFragment.setCornerIndicatorSettings(float lineThickness, int noMatchColor, int matchColor);
                boundaryDetectFragment.setDetectedBoundarySettings(float lineThickness, int noMatchColor, int matchColor);
                boundaryDetectFragment.darkBackgroundColor = Color.argb(100, 0, 0, 0);
            }
        }
 
        // Manual picture taking:
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (boundaryDetectFragment.isCropping()) {
                    boundaryDetectFragment.crop();
                } else {
                    boundaryDetectFragment.takeManualPicture();
                }
            }
        });
    }
 
    private void openCamera() {
        FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
        fragmentTransaction.replace(R.id.content_frame, boundaryDetectFragment);
        fragmentTransaction.commit();
    }
 
// Use the callback functions from CaptureCallbacks interface
void cameraOpenFailed(String reason);
void cameraStarted();
void cameraPaused();
void cameraStopped();
void cropStarted();
void cropStopped();
void captureStatus(String status);
void captureFailed();
void autoSnap();
void imageTaken();
 
// boundaryDetectFragment has other unmentioned useful functions such as: 
// pauseCamera(); 
// resumeCamera();
// disableCropping();   
// importImage(byte[] img, File filesDir);
// setCropZoomScaleFactor(float scale); // default is 2f
}
 
Your layout should include a layout with an id that matches the fragmentTransaction.replace: content_frame.
Note that the camera frame works best with an aspect ratio of 3:4 (width, height)
 
<FrameLayout
    android:id="@+id/content_frame"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <!--Capture fragment is inserted here-->

</FrameLayout>

 

Importing an image

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.

 

Previewing images

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);

 

 

 

Indexing

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");

 

 

Uploading

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);

 

 

 

 

Notifications

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);
}
ClientConnector.NotificationsUnreadCallback notificationsUnreadCallback = new ClientConnector.NotificationsUnreadCallback() {
    @Override
    public void onCountChanged(int unreadCount) {
        
    }
}
clientConnector.setNotificationsUnreadCallback(notificationsUnreadCallback);
  1. After finishCaptureSession(...)
  2. After uploadHistory(...)
  3. After removeHistory(...)
  4. After restoreHistory(...)
  5. After cancelUpload(...)
  6. After fetchNotificationMessages()

 

 

Passing additional iConnector headers

The following functions are overloaded to allow additional headers to be set in the HttpPost of the methods:

  1. validateDocumentFields(...)
  2. validateHistoryDocumentFields(...)
  3. reloadServerLookupForFieldIndex(...)
  4. fieldDefinitionsFromServer(...)
  5. fieldValuesForInput(...)
  6. upload(...)

 

HashMap<String, String> additionalHeaders = new HashMap<>();
additionalHeaders.put("Accept", "application/json");
clientConnector.upload(clientConnector.getHistoryInfo(uniqueId), additionalHeaders);

 

 

 

 

 

 

Google Cloud Messaging Android

 

Reference:

https://developers.google.com/cloud-messaging/android/start

 

Step 1: Enable google services for your app

 

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.

 

Step 2: Implement Cloud Messaging

 

Use the following link for a complete overview: https://developers.google.com/cloud-messaging/android/client

 

Step 2.1: Add dependencies

 

- 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 *

 

Step 2.2: Edit Application Manifest

 

- 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)*

 

Step 2.3: Create service classes

 

- 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).

 

Step 2.4 Check for Google Play Services APK

 

- 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.

 

Step 2.5 Handle incomming messages

 

- 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.

Create your own Knowledge Base