/* * 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; } }