/*
* Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.amazon.cordova.plugin;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.CordovaActivity;
import org.apache.cordova.LOG;
import org.json.JSONArray;
import org.json.JSONException;
import com.amazon.device.messaging.ADM;
import android.app.Activity;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import java.util.Iterator;
import org.json.JSONObject;
public class PushPlugin extends CordovaPlugin {
private static String TAG = "PushPlugin";
/**
* @uml.property name="adm"
* @uml.associationEnd
*/
private ADM adm = null;
/**
* @uml.property name="activity"
* @uml.associationEnd
*/
private Activity activity = null;
private static CordovaWebView webview = null;
private static String notificationHandlerCallBack;
private static boolean isForeground = false;
private static Bundle gCachedExtras = null;
public static final String REGISTER = "register";
public static final String UNREGISTER = "unregister";
public static final String REGISTER_EVENT = "registered";
public static final String UNREGISTER_EVENT = "unregistered";
public static final String MESSAGE = "message";
public static final String ECB = "ecb";
public static final String EVENT = "event";
public static final String PAYLOAD = "payload";
public static final String FOREGROUND = "foreground";
public static final String REG_ID = "regid";
public static final String COLDSTART = "coldstart";
private static final String NON_AMAZON_DEVICE_ERROR = "PushNotifications using Amazon Device Messaging is only supported on Kindle Fire devices (2nd Generation and Later only).";
private static final String ADM_NOT_SUPPORTED_ERROR = "Amazon Device Messaging is not supported on this device.";
private static final String REGISTER_OPTIONS_NULL = "Register options are not specified.";
private static final String ECB_NOT_SPECIFIED = "ecb(eventcallback) option is not specified in register().";
private static final String ECB_NAME_NOT_SPECIFIED = "ecb(eventcallback) value is missing in options for register().";
private static final String REGISTRATION_SUCCESS_RESPONSE = "Registration started...";
private static final String UNREGISTRATION_SUCCESS_RESPONSE = "Unregistration started...";
private static final String MODEL_FIRST_GEN = "Kindle Fire";
public enum ADMReadiness {
INITIALIZED, NON_AMAZON_DEVICE, ADM_NOT_SUPPORTED
}
/**
* Sets the context of the Command. This can then be used to do things like get file paths associated with the
* Activity.
*
* @param cordova
* The context of the main Activity.
* @param webView
* The associated CordovaWebView.
*/
@Override
public void initialize(CordovaInterface cordova, CordovaWebView webView) {
super.initialize(cordova, webView);
// Initialize only for Amazon devices 2nd Generation and later
if (this.isAmazonDevice() && !isFirstGenKindleFireDevice()) {
adm = new ADM(cordova.getActivity());
activity = (CordovaActivity) cordova.getActivity();
webview = this.webView;
isForeground = true;
ADMMessageHandler.saveConfigOptions(activity);
} else {
LOG.e(TAG, NON_AMAZON_DEVICE_ERROR);
}
}
/**
* Checks if current device manufacturer is Amazon by using android.os.Build.MANUFACTURER property
*
* @return returns true for all Kindle Fire OS devices.
*/
private boolean isAmazonDevice() {
String deviceMaker = android.os.Build.MANUFACTURER;
return deviceMaker.equalsIgnoreCase("Amazon");
}
/**
* Check if device is First generation Kindle
*
* @return if device is First generation Kindle
*/
private static boolean isFirstGenKindleFireDevice() {
return android.os.Build.MODEL.equals(MODEL_FIRST_GEN);
}
/**
* Checks if ADM is available and supported - could be one of three 1. Non Amazon device, hence no ADM support 2.
* ADM is not supported on this Kindle device (1st generation) 3. ADM is successfully initialized and ready to be
* used
*
* @return returns true for all Kindle Fire OS devices.
*/
public ADMReadiness isPushPluginReady() {
if (adm == null) {
return ADMReadiness.NON_AMAZON_DEVICE;
} else if (!adm.isSupported()) {
return ADMReadiness.ADM_NOT_SUPPORTED;
}
return ADMReadiness.INITIALIZED;
}
/**
* @see Plugin#execute(String, JSONArray, String)
*/
@Override
public boolean execute(final String request, final JSONArray args,
CallbackContext callbackContext) throws JSONException {
try {
// check ADM readiness
ADMReadiness ready = isPushPluginReady();
if (ready == ADMReadiness.NON_AMAZON_DEVICE) {
callbackContext.error(NON_AMAZON_DEVICE_ERROR);
return false;
} else if (ready == ADMReadiness.ADM_NOT_SUPPORTED) {
callbackContext.error(ADM_NOT_SUPPORTED_ERROR);
return false;
} else if (callbackContext == null) {
LOG.e(TAG,
"CallbackConext is null. Notification to WebView is not possible. Can not proceed.");
return false;
}
// Process the request here
if (REGISTER.equals(request)) {
if (args == null) {
LOG.e(TAG, REGISTER_OPTIONS_NULL);
callbackContext.error(REGISTER_OPTIONS_NULL);
return false;
}
// parse args to get eventcallback name
if (args.isNull(0)) {
LOG.e(TAG, ECB_NOT_SPECIFIED);
callbackContext.error(ECB_NOT_SPECIFIED);
return false;
}
JSONObject jo = args.getJSONObject(0);
if (jo.getString("ecb").isEmpty()) {
LOG.e(TAG, ECB_NAME_NOT_SPECIFIED);
callbackContext.error(ECB_NAME_NOT_SPECIFIED);
return false;
}
callbackContext.success(REGISTRATION_SUCCESS_RESPONSE);
notificationHandlerCallBack = jo.getString(ECB);
String regId = adm.getRegistrationId();
LOG.d(TAG, "regId = " + regId);
if (regId == null) {
adm.startRegister();
} else {
sendRegistrationIdWithEvent(REGISTER_EVENT, regId);
}
// see if there are any messages while app was in background and
// launched via app icon
LOG.d(TAG,"checking for offline message..");
deliverPendingMessageAndCancelNotifiation();
return true;
} else if (UNREGISTER.equals(request)) {
adm.startUnregister();
callbackContext.success(UNREGISTRATION_SUCCESS_RESPONSE);
return true;
} else {
LOG.e(TAG, "Invalid action : " + request);
callbackContext.error("Invalid action : " + request);
return false;
}
} catch (final Exception e) {
callbackContext.error(e.getMessage());
}
return false;
}
/**
* Checks if any bundle extras were cached while app was not running
*
* @return returns tru if cached Bundle is not null otherwise true.
*/
public boolean cachedExtrasAvailable() {
return (gCachedExtras != null);
}
/**
* Checks if offline message was pending to be delivered from notificationIntent. Sends it to webView(JS) if it is
* and also clears notification from the NotificaitonCenter.
*/
private boolean deliverOfflineMessages() {
LOG.d(TAG,"deliverOfflineMessages()");
Bundle pushBundle = ADMMessageHandler.getOfflineMessage();
if (pushBundle != null) {
LOG.d(TAG,"Sending offline message...");
sendExtras(pushBundle);
ADMMessageHandler.cleanupNotificationIntent();
return true;
}
return false;
}
// lifecyle callback to set the isForeground
@Override
public void onPause(boolean multitasking) {
LOG.d(TAG, "onPause");
super.onPause(multitasking);
isForeground = false;
}
@Override
public void onResume(boolean multitasking) {
LOG.d(TAG, "onResume");
super.onResume(multitasking);
isForeground = true;
//Check if there are any offline messages?
deliverPendingMessageAndCancelNotifiation();
}
@Override
public void onDestroy() {
LOG.d(TAG, "onDestroy");
super.onDestroy();
isForeground = false;
webview = null;
notificationHandlerCallBack = null;
}
/**
* Indicates if app is in foreground or not.
*
* @return returns true if app is running otherwise false.
*/
public static boolean isInForeground() {
return isForeground;
}
/**
* Indicates if app is killed or not
*
* @return returns true if app is killed otherwise false.
*/
public static boolean isActive() {
return webview != null;
}
/**
* Delivers pending/offline messages if any
*
* @return returns true if there were any pending messages otherwise false.
*/
public boolean deliverPendingMessageAndCancelNotifiation() {
boolean delivered = false;
LOG.d(TAG,"deliverPendingMessages()");
if (cachedExtrasAvailable()) {
LOG.v(TAG, "sending cached extras");
sendExtras(gCachedExtras);
gCachedExtras = null;
delivered = true;
} else {
delivered = deliverOfflineMessages();
}
// Clear the notification if any exists
ADMMessageHandler.cancelNotification(activity);
return delivered;
}
/**
* Sends register/unregiste events to JS
*
* @param String
* - eventName - "register", "unregister"
* @param String
* - valid registrationId
*/
public static void sendRegistrationIdWithEvent(String event,
String registrationId) {
if (TextUtils.isEmpty(event) || TextUtils.isEmpty(registrationId)) {
return;
}
try {
JSONObject json;
json = new JSONObject().put(EVENT, event);
json.put(REG_ID, registrationId);
sendJavascript(json);
} catch (Exception e) {
Log.getStackTraceString(e);
}
}
/**
* Sends events to JS using cordova nativeToJS bridge.
*
* @param JSONObject
*/
public static boolean sendJavascript(JSONObject json) {
if (json == null) {
LOG.i(TAG, "JSON object is empty. Nothing to send to JS.");
return true;
}
if (notificationHandlerCallBack != null && webview != null) {
String jsToSend = "javascript:" + notificationHandlerCallBack + "("
+ json.toString() + ")";
LOG.v(TAG, "sendJavascript: " + jsToSend);
webview.sendJavascript(jsToSend);
return true;
}
return false;
}
/*
* Sends the pushbundle extras to the client application. If the client application isn't currently active, it is
* cached for later processing.
*/
public static void sendExtras(Bundle extras) {
if (extras != null) {
if (!sendJavascript(convertBundleToJson(extras))) {
LOG.v(TAG,
"sendExtras: could not send to JS. Caching extras to send at a later time.");
gCachedExtras = extras;
}
}
}
// serializes a bundle to JSON.
private static JSONObject convertBundleToJson(Bundle extras) {
if (extras == null) {
return null;
}
try {
JSONObject json;
json = new JSONObject().put(EVENT, MESSAGE);
JSONObject jsondata = new JSONObject();
Iterator<String> it = extras.keySet().iterator();
while (it.hasNext()) {
String key = it.next();
Object value = extras.get(key);
// System data from Android
if (key.equals(FOREGROUND)) {
json.put(key, extras.getBoolean(FOREGROUND));
} else if (key.equals(COLDSTART)) {
json.put(key, extras.getBoolean(COLDSTART));
} else {
// we encourage put the message content into message value
// when server send out notification
if (key.equals(MESSAGE)) {
json.put(key, value);
}
if (value instanceof String) {
// Try to figure out if the value is another JSON object
String strValue = (String) value;
if (strValue.startsWith("{")) {
try {
JSONObject json2 = new JSONObject(strValue);
jsondata.put(key, json2);
} catch (Exception e) {
jsondata.put(key, value);
}
// Try to figure out if the value is another JSON
// array
} else if (strValue.startsWith("[")) {
try {
JSONArray json2 = new JSONArray(strValue);
jsondata.put(key, json2);
} catch (Exception e) {
jsondata.put(key, value);
}
} else {
jsondata.put(key, value);
}
}
} // while
}
json.put(PAYLOAD, jsondata);
LOG.v(TAG, "extrasToJSON: " + json.toString());
return json;
} catch (JSONException e) {
LOG.e(TAG, "extrasToJSON: JSON exception");
}
return null;
}
}