diff options
| author | Jules Laplace <jules@okfoc.us> | 2017-02-16 01:24:12 +0100 |
|---|---|---|
| committer | Jules Laplace <jules@okfoc.us> | 2017-02-16 01:24:12 +0100 |
| commit | 30c49550c89c1b69c680170d2dc247eac76bd463 (patch) | |
| tree | 8732652298b630b9ba15def97e59738f1c9bf7b6 /StoneIsland/plugins/phonegap-plugin-push/src | |
| parent | 8f1f626384e6ba75f4fb24c27e0973260a74421b (diff) | |
push plugin
Diffstat (limited to 'StoneIsland/plugins/phonegap-plugin-push/src')
15 files changed, 2818 insertions, 0 deletions
diff --git a/StoneIsland/plugins/phonegap-plugin-push/src/android/com/adobe/phonegap/push/BackgroundActionButtonHandler.java b/StoneIsland/plugins/phonegap-plugin-push/src/android/com/adobe/phonegap/push/BackgroundActionButtonHandler.java new file mode 100644 index 00000000..3ccea6cb --- /dev/null +++ b/StoneIsland/plugins/phonegap-plugin-push/src/android/com/adobe/phonegap/push/BackgroundActionButtonHandler.java @@ -0,0 +1,41 @@ +package com.adobe.phonegap.push; + +import android.app.NotificationManager; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.support.v4.app.RemoteInput; + +public class BackgroundActionButtonHandler extends BroadcastReceiver implements PushConstants { + private static String LOG_TAG = "PushPlugin_BackgroundActionButtonHandler"; + + @Override + public void onReceive(Context context, Intent intent) { + Bundle extras = intent.getExtras(); + Log.d(LOG_TAG, "BackgroundActionButtonHandler = " + extras); + + int notId = intent.getIntExtra(NOT_ID, 0); + Log.d(LOG_TAG, "not id = " + notId); + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(GCMIntentService.getAppName(context), notId); + + if (extras != null) { + Bundle originalExtras = extras.getBundle(PUSH_BUNDLE); + + originalExtras.putBoolean(FOREGROUND, false); + originalExtras.putBoolean(COLDSTART, false); + originalExtras.putString(ACTION_CALLBACK, extras.getString(CALLBACK)); + + Bundle remoteInput = RemoteInput.getResultsFromIntent(intent); + if (remoteInput != null) { + String inputString = remoteInput.getCharSequence(INLINE_REPLY).toString(); + Log.d(LOG_TAG, "response: " + inputString); + originalExtras.putString(INLINE_REPLY, inputString); + } + + PushPlugin.sendExtras(originalExtras); + } + } +} diff --git a/StoneIsland/plugins/phonegap-plugin-push/src/android/com/adobe/phonegap/push/GCMIntentService.java b/StoneIsland/plugins/phonegap-plugin-push/src/android/com/adobe/phonegap/push/GCMIntentService.java new file mode 100644 index 00000000..e1a2b75c --- /dev/null +++ b/StoneIsland/plugins/phonegap-plugin-push/src/android/com/adobe/phonegap/push/GCMIntentService.java @@ -0,0 +1,802 @@ +package com.adobe.phonegap.push; + +import android.annotation.SuppressLint; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.app.NotificationCompat; +import android.support.v4.app.NotificationManagerCompat; +import android.support.v4.app.NotificationCompat.WearableExtender; +import android.support.v4.app.RemoteInput; +import android.text.Html; +import android.text.Spanned; +import android.util.Log; + +import com.google.android.gms.gcm.GcmListenerService; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Random; + +@SuppressLint("NewApi") +public class GCMIntentService extends GcmListenerService implements PushConstants { + + private static final String LOG_TAG = "PushPlugin_GCMIntentService"; + private static HashMap<Integer, ArrayList<String>> messageMap = new HashMap<Integer, ArrayList<String>>(); + + public void setNotification(int notId, String message){ + ArrayList<String> messageList = messageMap.get(notId); + if(messageList == null) { + messageList = new ArrayList<String>(); + messageMap.put(notId, messageList); + } + + if(message.isEmpty()){ + messageList.clear(); + }else{ + messageList.add(message); + } + } + + @Override + public void onMessageReceived(String from, Bundle extras) { + Log.d(LOG_TAG, "onMessage - from: " + from); + + if (extras != null) { + Context applicationContext = getApplicationContext(); + + SharedPreferences prefs = applicationContext.getSharedPreferences(PushPlugin.COM_ADOBE_PHONEGAP_PUSH, Context.MODE_PRIVATE); + boolean forceShow = prefs.getBoolean(FORCE_SHOW, false); + boolean clearBadge = prefs.getBoolean(CLEAR_BADGE, false); + + extras = normalizeExtras(applicationContext, extras); + + if (clearBadge) { + PushPlugin.setApplicationIconBadgeNumber(getApplicationContext(), 0); + } + + // if we are in the foreground and forceShow is `false` only send data + if (!forceShow && PushPlugin.isInForeground()) { + Log.d(LOG_TAG, "foreground"); + extras.putBoolean(FOREGROUND, true); + extras.putBoolean(COLDSTART, false); + PushPlugin.sendExtras(extras); + } + // if we are in the foreground and forceShow is `true`, force show the notification if the data has at least a message or title + else if (forceShow && PushPlugin.isInForeground()) { + Log.d(LOG_TAG, "foreground force"); + extras.putBoolean(FOREGROUND, true); + extras.putBoolean(COLDSTART, false); + + showNotificationIfPossible(applicationContext, extras); + } + // if we are not in the foreground always send notification if the data has at least a message or title + else { + Log.d(LOG_TAG, "background"); + extras.putBoolean(FOREGROUND, false); + extras.putBoolean(COLDSTART, PushPlugin.isActive()); + + showNotificationIfPossible(applicationContext, extras); + } + } + } + + /* + * Change a values key in the extras bundle + */ + private void replaceKey(Context context, String oldKey, String newKey, Bundle extras, Bundle newExtras) { + Object value = extras.get(oldKey); + if ( value != null ) { + if (value instanceof String) { + value = localizeKey(context, newKey, (String) value); + + newExtras.putString(newKey, (String) value); + } else if (value instanceof Boolean) { + newExtras.putBoolean(newKey, (Boolean) value); + } else if (value instanceof Number) { + newExtras.putDouble(newKey, ((Number) value).doubleValue()); + } else { + newExtras.putString(newKey, String.valueOf(value)); + } + } + } + + /* + * Normalize localization for key + */ + private String localizeKey(Context context, String key, String value) { + if (key.equals(TITLE) || key.equals(MESSAGE) || key.equals(SUMMARY_TEXT)) { + try { + JSONObject localeObject = new JSONObject(value); + + String localeKey = localeObject.getString(LOC_KEY); + + ArrayList<String> localeFormatData = new ArrayList<String>(); + if (!localeObject.isNull(LOC_DATA)) { + String localeData = localeObject.getString(LOC_DATA); + JSONArray localeDataArray = new JSONArray(localeData); + for (int i = 0 ; i < localeDataArray.length(); i++) { + localeFormatData.add(localeDataArray.getString(i)); + } + } + + String packageName = context.getPackageName(); + Resources resources = context.getResources(); + + int resourceId = resources.getIdentifier(localeKey, "string", packageName); + + if (resourceId != 0) { + return resources.getString(resourceId, localeFormatData.toArray()); + } + else { + Log.d(LOG_TAG, "can't find resource for locale key = " + localeKey); + + return value; + } + } + catch(JSONException e) { + Log.d(LOG_TAG, "no locale found for key = " + key + ", error " + e.getMessage()); + + return value; + } + } + + return value; + } + + /* + * Replace alternate keys with our canonical value + */ + private String normalizeKey(String key) { + if (key.equals(BODY) || key.equals(ALERT) || key.equals(GCM_NOTIFICATION_BODY) || key.equals(TWILIO_BODY)) { + return MESSAGE; + } else if (key.equals(TWILIO_TITLE)) { + return TITLE; + }else if (key.equals(MSGCNT) || key.equals(BADGE)) { + return COUNT; + } else if (key.equals(SOUNDNAME) || key.equals(TWILIO_SOUND)) { + return SOUND; + } else if (key.startsWith(GCM_NOTIFICATION)) { + return key.substring(GCM_NOTIFICATION.length()+1, key.length()); + } else if (key.startsWith(GCM_N)) { + return key.substring(GCM_N.length()+1, key.length()); + } else if (key.startsWith(UA_PREFIX)) { + key = key.substring(UA_PREFIX.length()+1, key.length()); + return key.toLowerCase(); + } else { + return key; + } + } + + /* + * Parse bundle into normalized keys. + */ + private Bundle normalizeExtras(Context context, Bundle extras) { + Log.d(LOG_TAG, "normalize extras"); + Iterator<String> it = extras.keySet().iterator(); + Bundle newExtras = new Bundle(); + + while (it.hasNext()) { + String key = it.next(); + + Log.d(LOG_TAG, "key = " + key); + + // If normalizeKeythe key is "data" or "message" and the value is a json object extract + // This is to support parse.com and other services. Issue #147 and pull #218 + if (key.equals(PARSE_COM_DATA) || key.equals(MESSAGE)) { + Object json = extras.get(key); + // Make sure data is json object stringified + if ( json instanceof String && ((String) json).startsWith("{") ) { + Log.d(LOG_TAG, "extracting nested message data from key = " + key); + try { + // If object contains message keys promote each value to the root of the bundle + JSONObject data = new JSONObject((String) json); + if ( data.has(ALERT) || data.has(MESSAGE) || data.has(BODY) || data.has(TITLE) ) { + Iterator<String> jsonIter = data.keys(); + while (jsonIter.hasNext()) { + String jsonKey = jsonIter.next(); + + Log.d(LOG_TAG, "key = data/" + jsonKey); + + String value = data.getString(jsonKey); + jsonKey = normalizeKey(jsonKey); + value = localizeKey(context, jsonKey, value); + + newExtras.putString(jsonKey, value); + } + } + } catch( JSONException e) { + Log.e(LOG_TAG, "normalizeExtras: JSON exception"); + } + } + } else if (key.equals(("notification"))) { + Bundle value = extras.getBundle(key); + Iterator<String> iterator = value.keySet().iterator(); + while (iterator.hasNext()) { + String notifkey = iterator.next(); + + Log.d(LOG_TAG, "notifkey = " + notifkey); + String newKey = normalizeKey(notifkey); + Log.d(LOG_TAG, "replace key " + notifkey + " with " + newKey); + + String valueData = value.getString(notifkey); + valueData = localizeKey(context, newKey, valueData); + + newExtras.putString(newKey, valueData); + } + continue; + } + + String newKey = normalizeKey(key); + Log.d(LOG_TAG, "replace key " + key + " with " + newKey); + replaceKey(context, key, newKey, extras, newExtras); + + } // while + + return newExtras; + } + + private int extractBadgeCount(Bundle extras) { + int count = -1; + String msgcnt = extras.getString(COUNT); + + try { + if (msgcnt != null) { + count = Integer.parseInt(msgcnt); + } + } catch (NumberFormatException e) { + Log.e(LOG_TAG, e.getLocalizedMessage(), e); + } + + return count; + } + + private void showNotificationIfPossible (Context context, Bundle extras) { + + // Send a notification if there is a message or title, otherwise just send data + String message = extras.getString(MESSAGE); + String title = extras.getString(TITLE); + String contentAvailable = extras.getString(CONTENT_AVAILABLE); + String forceStart = extras.getString(FORCE_START); + int badgeCount = extractBadgeCount(extras); + if (badgeCount >= 0) { + Log.d(LOG_TAG, "count =[" + badgeCount + "]"); + PushPlugin.setApplicationIconBadgeNumber(context, badgeCount); + } + + Log.d(LOG_TAG, "message =[" + message + "]"); + Log.d(LOG_TAG, "title =[" + title + "]"); + Log.d(LOG_TAG, "contentAvailable =[" + contentAvailable + "]"); + Log.d(LOG_TAG, "forceStart =[" + forceStart + "]"); + + if ((message != null && message.length() != 0) || + (title != null && title.length() != 0)) { + + Log.d(LOG_TAG, "create notification"); + + if(title == null || title.isEmpty()){ + extras.putString(TITLE, getAppName(this)); + } + + createNotification(context, extras); + } + + if(!PushPlugin.isActive() && "1".equals(forceStart)){ + Log.d(LOG_TAG, "app is not running but we should start it and put in background"); + Intent intent = new Intent(this, PushHandlerActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(PUSH_BUNDLE, extras); + intent.putExtra(START_IN_BACKGROUND, true); + intent.putExtra(FOREGROUND, false); + startActivity(intent); + } else if ("1".equals(contentAvailable)) { + Log.d(LOG_TAG, "app is not running and content available true"); + Log.d(LOG_TAG, "send notification event"); + PushPlugin.sendExtras(extras); + } + } + + public void createNotification(Context context, Bundle extras) { + NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + String appName = getAppName(this); + String packageName = context.getPackageName(); + Resources resources = context.getResources(); + + int notId = parseInt(NOT_ID, extras); + Intent notificationIntent = new Intent(this, PushHandlerActivity.class); + notificationIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); + notificationIntent.putExtra(PUSH_BUNDLE, extras); + notificationIntent.putExtra(NOT_ID, notId); + + int requestCode = new Random().nextInt(); + PendingIntent contentIntent = PendingIntent.getActivity(this, requestCode, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + NotificationCompat.Builder mBuilder = + new NotificationCompat.Builder(context) + .setWhen(System.currentTimeMillis()) + .setContentTitle(fromHtml(extras.getString(TITLE))) + .setTicker(fromHtml(extras.getString(TITLE))) + .setContentIntent(contentIntent) + .setAutoCancel(true); + + SharedPreferences prefs = context.getSharedPreferences(PushPlugin.COM_ADOBE_PHONEGAP_PUSH, Context.MODE_PRIVATE); + String localIcon = prefs.getString(ICON, null); + String localIconColor = prefs.getString(ICON_COLOR, null); + boolean soundOption = prefs.getBoolean(SOUND, true); + boolean vibrateOption = prefs.getBoolean(VIBRATE, true); + Log.d(LOG_TAG, "stored icon=" + localIcon); + Log.d(LOG_TAG, "stored iconColor=" + localIconColor); + Log.d(LOG_TAG, "stored sound=" + soundOption); + Log.d(LOG_TAG, "stored vibrate=" + vibrateOption); + + /* + * Notification Vibration + */ + + setNotificationVibration(extras, vibrateOption, mBuilder); + + /* + * Notification Icon Color + * + * Sets the small-icon background color of the notification. + * To use, add the `iconColor` key to plugin android options + * + */ + setNotificationIconColor(extras.getString("color"), mBuilder, localIconColor); + + /* + * Notification Icon + * + * Sets the small-icon of the notification. + * + * - checks the plugin options for `icon` key + * - if none, uses the application icon + * + * The icon value must be a string that maps to a drawable resource. + * If no resource is found, falls + * + */ + setNotificationSmallIcon(context, extras, packageName, resources, mBuilder, localIcon); + + /* + * Notification Large-Icon + * + * Sets the large-icon of the notification + * + * - checks the gcm data for the `image` key + * - checks to see if remote image, loads it. + * - checks to see if assets image, Loads It. + * - checks to see if resource image, LOADS IT! + * - if none, we don't set the large icon + * + */ + setNotificationLargeIcon(extras, packageName, resources, mBuilder); + + /* + * Notification Sound + */ + if (soundOption) { + setNotificationSound(context, extras, mBuilder); + } + + /* + * LED Notification + */ + setNotificationLedColor(extras, mBuilder); + + /* + * Priority Notification + */ + setNotificationPriority(extras, mBuilder); + + /* + * Notification message + */ + setNotificationMessage(notId, extras, mBuilder); + + /* + * Notification count + */ + setNotificationCount(context, extras, mBuilder); + + /* + * Notification count + */ + setVisibility(context, extras, mBuilder); + + /* + * Notification add actions + */ + createActions(extras, mBuilder, resources, packageName, notId); + + mNotificationManager.notify(appName, notId, mBuilder.build()); + } + + private void updateIntent(Intent intent, String callback, Bundle extras, boolean foreground, int notId) { + intent.putExtra(CALLBACK, callback); + intent.putExtra(PUSH_BUNDLE, extras); + intent.putExtra(FOREGROUND, foreground); + intent.putExtra(NOT_ID, notId); + } + + private void createActions(Bundle extras, NotificationCompat.Builder mBuilder, Resources resources, String packageName, int notId) { + Log.d(LOG_TAG, "create actions: with in-line"); + String actions = extras.getString(ACTIONS); + if (actions != null) { + try { + JSONArray actionsArray = new JSONArray(actions); + ArrayList<NotificationCompat.Action> wActions = new ArrayList<NotificationCompat.Action>(); + for (int i=0; i < actionsArray.length(); i++) { + int min = 1; + int max = 2000000000; + Random random = new Random(); + int uniquePendingIntentRequestCode = random.nextInt((max - min) + 1) + min; + Log.d(LOG_TAG, "adding action"); + JSONObject action = actionsArray.getJSONObject(i); + Log.d(LOG_TAG, "adding callback = " + action.getString(CALLBACK)); + boolean foreground = action.optBoolean(FOREGROUND, true); + boolean inline = action.optBoolean("inline", false); + Intent intent = null; + PendingIntent pIntent = null; + if (inline) { + Log.d(LOG_TAG, "Version: " + android.os.Build.VERSION.SDK_INT + " = " + android.os.Build.VERSION_CODES.M); + if (android.os.Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.M) { + Log.d(LOG_TAG, "push activity"); + intent = new Intent(this, PushHandlerActivity.class); + } else { + Log.d(LOG_TAG, "push receiver"); + intent = new Intent(this, BackgroundActionButtonHandler.class); + } + + updateIntent(intent, action.getString(CALLBACK), extras, foreground, notId); + + if (android.os.Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.M) { + Log.d(LOG_TAG, "push activity for notId " + notId); + pIntent = PendingIntent.getActivity(this, uniquePendingIntentRequestCode, intent, PendingIntent.FLAG_ONE_SHOT); + } else { + Log.d(LOG_TAG, "push receiver for notId " + notId); + pIntent = PendingIntent.getBroadcast(this, uniquePendingIntentRequestCode, intent, PendingIntent.FLAG_ONE_SHOT); + } + } else if (foreground) { + intent = new Intent(this, PushHandlerActivity.class); + updateIntent(intent, action.getString(CALLBACK), extras, foreground, notId); + pIntent = PendingIntent.getActivity(this, uniquePendingIntentRequestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } else { + intent = new Intent(this, BackgroundActionButtonHandler.class); + updateIntent(intent, action.getString(CALLBACK), extras, foreground, notId); + pIntent = PendingIntent.getBroadcast(this, uniquePendingIntentRequestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + NotificationCompat.Action.Builder actionBuilder = + new NotificationCompat.Action.Builder(resources.getIdentifier(action.optString(ICON, ""), DRAWABLE, packageName), + action.getString(TITLE), pIntent); + + RemoteInput remoteInput = null; + if (inline) { + Log.d(LOG_TAG, "create remote input"); + String replyLabel = "Enter your reply here"; + remoteInput = + new RemoteInput.Builder(INLINE_REPLY) + .setLabel(replyLabel) + .build(); + actionBuilder.addRemoteInput(remoteInput); + } + + NotificationCompat.Action wAction = actionBuilder.build(); + wActions.add(actionBuilder.build()); + + if (inline) { + mBuilder.addAction(wAction); + } else { + mBuilder.addAction(resources.getIdentifier(action.optString(ICON, ""), DRAWABLE, packageName), + action.getString(TITLE), pIntent); + } + wAction = null; + pIntent = null; + } + mBuilder.extend(new WearableExtender().addActions(wActions)); + wActions.clear(); + } catch(JSONException e) { + // nope + } + } + } + + private void setNotificationCount(Context context, Bundle extras, NotificationCompat.Builder mBuilder) { + int count = extractBadgeCount(extras); + if (count >= 0) { + Log.d(LOG_TAG, "count =[" + count + "]"); + mBuilder.setNumber(count); + } + } + + + private void setVisibility(Context context, Bundle extras, NotificationCompat.Builder mBuilder) { + String visibilityStr = extras.getString(VISIBILITY); + if (visibilityStr != null) { + try { + Integer visibility = Integer.parseInt(visibilityStr); + if (visibility >= NotificationCompat.VISIBILITY_SECRET && visibility <= NotificationCompat.VISIBILITY_PUBLIC) { + mBuilder.setVisibility(visibility); + } else { + Log.e(LOG_TAG, "Visibility parameter must be between -1 and 1"); + } + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } + } + + private void setNotificationVibration(Bundle extras, Boolean vibrateOption, NotificationCompat.Builder mBuilder) { + String vibrationPattern = extras.getString(VIBRATION_PATTERN); + if (vibrationPattern != null) { + String[] items = vibrationPattern.replaceAll("\\[", "").replaceAll("\\]", "").split(","); + long[] results = new long[items.length]; + for (int i = 0; i < items.length; i++) { + try { + results[i] = Long.parseLong(items[i].trim()); + } catch (NumberFormatException nfe) {} + } + mBuilder.setVibrate(results); + } else { + if (vibrateOption) { + mBuilder.setDefaults(Notification.DEFAULT_VIBRATE); + } + } + } + + private void setNotificationMessage(int notId, Bundle extras, NotificationCompat.Builder mBuilder) { + String message = extras.getString(MESSAGE); + + String style = extras.getString(STYLE, STYLE_TEXT); + if(STYLE_INBOX.equals(style)) { + setNotification(notId, message); + + mBuilder.setContentText(fromHtml(message)); + + ArrayList<String> messageList = messageMap.get(notId); + Integer sizeList = messageList.size(); + if (sizeList > 1) { + String sizeListMessage = sizeList.toString(); + String stacking = sizeList + " more"; + if (extras.getString(SUMMARY_TEXT) != null) { + stacking = extras.getString(SUMMARY_TEXT); + stacking = stacking.replace("%n%", sizeListMessage); + } + NotificationCompat.InboxStyle notificationInbox = new NotificationCompat.InboxStyle() + .setBigContentTitle(fromHtml(extras.getString(TITLE))) + .setSummaryText(fromHtml(stacking)); + + for (int i = messageList.size() - 1; i >= 0; i--) { + notificationInbox.addLine(fromHtml(messageList.get(i))); + } + + mBuilder.setStyle(notificationInbox); + } else { + NotificationCompat.BigTextStyle bigText = new NotificationCompat.BigTextStyle(); + if (message != null) { + bigText.bigText(fromHtml(message)); + bigText.setBigContentTitle(fromHtml(extras.getString(TITLE))); + mBuilder.setStyle(bigText); + } + } + } else if (STYLE_PICTURE.equals(style)) { + setNotification(notId, ""); + + NotificationCompat.BigPictureStyle bigPicture = new NotificationCompat.BigPictureStyle(); + bigPicture.bigPicture(getBitmapFromURL(extras.getString(PICTURE))); + bigPicture.setBigContentTitle(fromHtml(extras.getString(TITLE))); + bigPicture.setSummaryText(fromHtml(extras.getString(SUMMARY_TEXT))); + + mBuilder.setContentTitle(fromHtml(extras.getString(TITLE))); + mBuilder.setContentText(fromHtml(message)); + + mBuilder.setStyle(bigPicture); + } else { + setNotification(notId, ""); + + NotificationCompat.BigTextStyle bigText = new NotificationCompat.BigTextStyle(); + + if (message != null) { + mBuilder.setContentText(fromHtml(message)); + + bigText.bigText(fromHtml(message)); + bigText.setBigContentTitle(fromHtml(extras.getString(TITLE))); + + String summaryText = extras.getString(SUMMARY_TEXT); + if (summaryText != null) { + bigText.setSummaryText(fromHtml(summaryText)); + } + + mBuilder.setStyle(bigText); + } + /* + else { + mBuilder.setContentText("<missing message content>"); + } + */ + } + } + + private void setNotificationSound(Context context, Bundle extras, NotificationCompat.Builder mBuilder) { + String soundname = extras.getString(SOUNDNAME); + if (soundname == null) { + soundname = extras.getString(SOUND); + } + if (SOUND_RINGTONE.equals(soundname)) { + mBuilder.setSound(android.provider.Settings.System.DEFAULT_RINGTONE_URI); + } else if (soundname != null && !soundname.contentEquals(SOUND_DEFAULT)) { + Uri sound = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + + "://" + context.getPackageName() + "/raw/" + soundname); + Log.d(LOG_TAG, sound.toString()); + mBuilder.setSound(sound); + } else { + mBuilder.setSound(android.provider.Settings.System.DEFAULT_NOTIFICATION_URI); + } + } + + private void setNotificationLedColor(Bundle extras, NotificationCompat.Builder mBuilder) { + String ledColor = extras.getString(LED_COLOR); + if (ledColor != null) { + // Converts parse Int Array from ledColor + String[] items = ledColor.replaceAll("\\[", "").replaceAll("\\]", "").split(","); + int[] results = new int[items.length]; + for (int i = 0; i < items.length; i++) { + try { + results[i] = Integer.parseInt(items[i].trim()); + } catch (NumberFormatException nfe) {} + } + if (results.length == 4) { + mBuilder.setLights(Color.argb(results[0], results[1], results[2], results[3]), 500, 500); + } else { + Log.e(LOG_TAG, "ledColor parameter must be an array of length == 4 (ARGB)"); + } + } + } + + private void setNotificationPriority(Bundle extras, NotificationCompat.Builder mBuilder) { + String priorityStr = extras.getString(PRIORITY); + if (priorityStr != null) { + try { + Integer priority = Integer.parseInt(priorityStr); + if (priority >= NotificationCompat.PRIORITY_MIN && priority <= NotificationCompat.PRIORITY_MAX) { + mBuilder.setPriority(priority); + } else { + Log.e(LOG_TAG, "Priority parameter must be between -2 and 2"); + } + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } + } + + private void setNotificationLargeIcon(Bundle extras, String packageName, Resources resources, NotificationCompat.Builder mBuilder) { + String gcmLargeIcon = extras.getString(IMAGE); // from gcm + if (gcmLargeIcon != null && !"".equals(gcmLargeIcon)) { + if (gcmLargeIcon.startsWith("http://") || gcmLargeIcon.startsWith("https://")) { + mBuilder.setLargeIcon(getBitmapFromURL(gcmLargeIcon)); + Log.d(LOG_TAG, "using remote large-icon from gcm"); + } else { + AssetManager assetManager = getAssets(); + InputStream istr; + try { + istr = assetManager.open(gcmLargeIcon); + Bitmap bitmap = BitmapFactory.decodeStream(istr); + mBuilder.setLargeIcon(bitmap); + Log.d(LOG_TAG, "using assets large-icon from gcm"); + } catch (IOException e) { + int largeIconId = 0; + largeIconId = resources.getIdentifier(gcmLargeIcon, DRAWABLE, packageName); + if (largeIconId != 0) { + Bitmap largeIconBitmap = BitmapFactory.decodeResource(resources, largeIconId); + mBuilder.setLargeIcon(largeIconBitmap); + Log.d(LOG_TAG, "using resources large-icon from gcm"); + } else { + Log.d(LOG_TAG, "Not setting large icon"); + } + } + } + } + } + + private void setNotificationSmallIcon(Context context, Bundle extras, String packageName, Resources resources, NotificationCompat.Builder mBuilder, String localIcon) { + int iconId = 0; + String icon = extras.getString(ICON); + if (icon != null && !"".equals(icon)) { + iconId = resources.getIdentifier(icon, DRAWABLE, packageName); + Log.d(LOG_TAG, "using icon from plugin options"); + } + else if (localIcon != null && !"".equals(localIcon)) { + iconId = resources.getIdentifier(localIcon, DRAWABLE, packageName); + Log.d(LOG_TAG, "using icon from plugin options"); + } + if (iconId == 0) { + Log.d(LOG_TAG, "no icon resource found - using application icon"); + iconId = context.getApplicationInfo().icon; + } + mBuilder.setSmallIcon(iconId); + } + + private void setNotificationIconColor(String color, NotificationCompat.Builder mBuilder, String localIconColor) { + int iconColor = 0; + if (color != null && !"".equals(color)) { + try { + iconColor = Color.parseColor(color); + } catch (IllegalArgumentException e) { + Log.e(LOG_TAG, "couldn't parse color from android options"); + } + } + else if (localIconColor != null && !"".equals(localIconColor)) { + try { + iconColor = Color.parseColor(localIconColor); + } catch (IllegalArgumentException e) { + Log.e(LOG_TAG, "couldn't parse color from android options"); + } + } + if (iconColor != 0) { + mBuilder.setColor(iconColor); + } + } + + public Bitmap getBitmapFromURL(String strURL) { + try { + URL url = new URL(strURL); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setDoInput(true); + connection.connect(); + InputStream input = connection.getInputStream(); + return BitmapFactory.decodeStream(input); + } catch (IOException e) { + e.printStackTrace(); + return null; + } + } + + public static String getAppName(Context context) { + CharSequence appName = context.getPackageManager().getApplicationLabel(context.getApplicationInfo()); + return (String)appName; + } + + private int parseInt(String value, Bundle extras) { + int retval = 0; + + try { + retval = Integer.parseInt(extras.getString(value)); + } + catch(NumberFormatException e) { + Log.e(LOG_TAG, "Number format exception - Error parsing " + value + ": " + e.getMessage()); + } + catch(Exception e) { + Log.e(LOG_TAG, "Number format exception - Error parsing " + value + ": " + e.getMessage()); + } + + return retval; + } + + private Spanned fromHtml(String source) { + if (source != null) + return Html.fromHtml(source); + else + return null; + } +} diff --git a/StoneIsland/plugins/phonegap-plugin-push/src/android/com/adobe/phonegap/push/PermissionUtils.java b/StoneIsland/plugins/phonegap-plugin-push/src/android/com/adobe/phonegap/push/PermissionUtils.java new file mode 100644 index 00000000..6aa5c9bf --- /dev/null +++ b/StoneIsland/plugins/phonegap-plugin-push/src/android/com/adobe/phonegap/push/PermissionUtils.java @@ -0,0 +1,55 @@ +package com.adobe.phonegap.push; + +import android.content.Context; +import android.content.pm.ApplicationInfo; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +public class PermissionUtils { + + private static final String CHECK_OP_NO_THROW = "checkOpNoThrow"; + + public static boolean hasPermission(Context appContext, String appOpsServiceId) throws UnknownError { + + ApplicationInfo appInfo = appContext.getApplicationInfo(); + + String pkg = appContext.getPackageName(); + int uid = appInfo.uid; + Class appOpsClass = null; + Object appOps = appContext.getSystemService("appops"); + + try { + + appOpsClass = Class.forName("android.app.AppOpsManager"); + + Method checkOpNoThrowMethod = appOpsClass.getMethod( + CHECK_OP_NO_THROW, + Integer.TYPE, + Integer.TYPE, + String.class + ); + + Field opValue = appOpsClass.getDeclaredField(appOpsServiceId); + + int value = (int) opValue.getInt(Integer.class); + Object result = checkOpNoThrowMethod.invoke(appOps, value, uid, pkg); + + return Integer.parseInt(result.toString()) == 0; // AppOpsManager.MODE_ALLOWED + + } catch (ClassNotFoundException e) { + throw new UnknownError("class not found"); + } catch (NoSuchMethodException e) { + throw new UnknownError("no such method"); + } catch (NoSuchFieldException e) { + throw new UnknownError("no such field"); + } catch (InvocationTargetException e) { + throw new UnknownError("invocation target"); + } catch (IllegalAccessException e) { + throw new UnknownError("illegal access"); + } + + } + +} diff --git a/StoneIsland/plugins/phonegap-plugin-push/src/android/com/adobe/phonegap/push/PushConstants.java b/StoneIsland/plugins/phonegap-plugin-push/src/android/com/adobe/phonegap/push/PushConstants.java new file mode 100644 index 00000000..37874e04 --- /dev/null +++ b/StoneIsland/plugins/phonegap-plugin-push/src/android/com/adobe/phonegap/push/PushConstants.java @@ -0,0 +1,72 @@ +package com.adobe.phonegap.push; + +public interface PushConstants { + public static final String COM_ADOBE_PHONEGAP_PUSH = "com.adobe.phonegap.push"; + public static final String REGISTRATION_ID = "registrationId"; + public static final String FOREGROUND = "foreground"; + public static final String TITLE = "title"; + public static final String NOT_ID = "notId"; + public static final String PUSH_BUNDLE = "pushBundle"; + public static final String ICON = "icon"; + public static final String ICON_COLOR = "iconColor"; + public static final String SOUND = "sound"; + public static final String SOUND_DEFAULT = "default"; + public static final String SOUND_RINGTONE = "ringtone"; + public static final String VIBRATE = "vibrate"; + public static final String ACTIONS = "actions"; + public static final String CALLBACK = "callback"; + public static final String ACTION_CALLBACK = "actionCallback"; + public static final String DRAWABLE = "drawable"; + public static final String MSGCNT = "msgcnt"; + public static final String VIBRATION_PATTERN = "vibrationPattern"; + public static final String STYLE = "style"; + public static final String SUMMARY_TEXT = "summaryText"; + public static final String PICTURE = "picture"; + public static final String GCM_N = "gcm.n."; + public static final String GCM_NOTIFICATION = "gcm.notification"; + public static final String GCM_NOTIFICATION_BODY = "gcm.notification.body"; + public static final String UA_PREFIX = "com.urbanairship.push"; + public static final String PARSE_COM_DATA = "data"; + public static final String ALERT = "alert"; + public static final String MESSAGE = "message"; + public static final String BODY = "body"; + public static final String SOUNDNAME = "soundname"; + public static final String LED_COLOR = "ledColor"; + public static final String PRIORITY = "priority"; + public static final String IMAGE = "image"; + public static final String STYLE_INBOX = "inbox"; + public static final String STYLE_PICTURE = "picture"; + public static final String STYLE_TEXT = "text"; + public static final String BADGE = "badge"; + public static final String INITIALIZE = "init"; + public static final String SUBSCRIBE = "subscribe"; + public static final String UNSUBSCRIBE = "unsubscribe"; + public static final String UNREGISTER = "unregister"; + public static final String EXIT = "exit"; + public static final String FINISH = "finish"; + public static final String HAS_PERMISSION = "hasPermission"; + public static final String ANDROID = "android"; + public static final String SENDER_ID = "senderID"; + public static final String CLEAR_BADGE = "clearBadge"; + public static final String CLEAR_NOTIFICATIONS = "clearNotifications"; + public static final String COLDSTART = "coldstart"; + public static final String ADDITIONAL_DATA = "additionalData"; + public static final String COUNT = "count"; + public static final String FROM = "from"; + public static final String COLLAPSE_KEY = "collapse_key"; + public static final String FORCE_SHOW = "forceShow"; + public static final String GCM = "GCM"; + public static final String CONTENT_AVAILABLE = "content-available"; + public static final String TOPICS = "topics"; + public static final String SET_APPLICATION_ICON_BADGE_NUMBER = "setApplicationIconBadgeNumber"; + public static final String CLEAR_ALL_NOTIFICATIONS = "clearAllNotifications"; + public static final String VISIBILITY = "visibility"; + public static final String INLINE_REPLY = "inlineReply"; + public static final String LOC_KEY = "locKey"; + public static final String LOC_DATA = "locData"; + public static final String TWILIO_BODY = "twi_body"; + public static final String TWILIO_TITLE = "twi_title"; + public static final String TWILIO_SOUND = "twi_sound"; + public static final String START_IN_BACKGROUND = "cdvStartInBackground"; + public static final String FORCE_START = "force-start"; +} diff --git a/StoneIsland/plugins/phonegap-plugin-push/src/android/com/adobe/phonegap/push/PushHandlerActivity.java b/StoneIsland/plugins/phonegap-plugin-push/src/android/com/adobe/phonegap/push/PushHandlerActivity.java new file mode 100644 index 00000000..23682ac8 --- /dev/null +++ b/StoneIsland/plugins/phonegap-plugin-push/src/android/com/adobe/phonegap/push/PushHandlerActivity.java @@ -0,0 +1,120 @@ +package com.adobe.phonegap.push; + +import android.app.Activity; +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Bundle; +import android.util.Log; +import android.support.v4.app.RemoteInput; + + +public class PushHandlerActivity extends Activity implements PushConstants { + private static String LOG_TAG = "PushPlugin_PushHandlerActivity"; + + /* + * this activity will be started if the user touches a notification that we own. + * We send it's data off to the push plugin for processing. + * If needed, we boot up the main activity to kickstart the application. + * @see android.app.Activity#onCreate(android.os.Bundle) + */ + @Override + public void onCreate(Bundle savedInstanceState) { + GCMIntentService gcm = new GCMIntentService(); + + Intent intent = getIntent(); + + int notId = intent.getExtras().getInt(NOT_ID, 0); + Log.d(LOG_TAG, "not id = " + notId); + gcm.setNotification(notId, ""); + super.onCreate(savedInstanceState); + Log.v(LOG_TAG, "onCreate"); + String callback = getIntent().getExtras().getString("callback"); + Log.d(LOG_TAG, "callback = " + callback); + boolean foreground = getIntent().getExtras().getBoolean("foreground", true); + boolean startOnBackground = getIntent().getExtras().getBoolean(START_IN_BACKGROUND, false); + + if(!startOnBackground){ + NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(GCMIntentService.getAppName(this), notId); + } + + boolean isPushPluginActive = PushPlugin.isActive(); + boolean inline = processPushBundle(isPushPluginActive, intent); + + if(inline && android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.N){ + foreground = true; + } + + Log.d(LOG_TAG, "bringToForeground = " + foreground); + + finish(); + + Log.d(LOG_TAG, "isPushPluginActive = " + isPushPluginActive); + if (!isPushPluginActive && foreground && inline) { + Log.d(LOG_TAG, "forceMainActivityReload"); + forceMainActivityReload(false); + } else if(startOnBackground) { + Log.d(LOG_TAG, "startOnBackgroundTrue"); + forceMainActivityReload(true); + } else { + Log.d(LOG_TAG, "don't want main activity"); + } + } + + /** + * Takes the pushBundle extras from the intent, + * and sends it through to the PushPlugin for processing. + */ + private boolean processPushBundle(boolean isPushPluginActive, Intent intent) { + Bundle extras = getIntent().getExtras(); + Bundle remoteInput = null; + + if (extras != null) { + Bundle originalExtras = extras.getBundle(PUSH_BUNDLE); + + originalExtras.putBoolean(FOREGROUND, false); + originalExtras.putBoolean(COLDSTART, !isPushPluginActive); + originalExtras.putString(ACTION_CALLBACK, extras.getString(CALLBACK)); + + remoteInput = RemoteInput.getResultsFromIntent(intent); + if (remoteInput != null) { + String inputString = remoteInput.getCharSequence(INLINE_REPLY).toString(); + Log.d(LOG_TAG, "response: " + inputString); + originalExtras.putString(INLINE_REPLY, inputString); + } + + PushPlugin.sendExtras(originalExtras); + } + return remoteInput == null; + } + + /** + * Forces the main activity to re-launch if it's unloaded. + */ + private void forceMainActivityReload(boolean startOnBackground) { + PackageManager pm = getPackageManager(); + Intent launchIntent = pm.getLaunchIntentForPackage(getApplicationContext().getPackageName()); + + Bundle extras = getIntent().getExtras(); + if (extras != null) { + Bundle originalExtras = extras.getBundle(PUSH_BUNDLE); + if (originalExtras != null) { + launchIntent.putExtras(originalExtras); + } + launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + launchIntent.addFlags(Intent.FLAG_FROM_BACKGROUND); + launchIntent.putExtra(START_IN_BACKGROUND, startOnBackground); + } + + startActivity(launchIntent); + } + + @Override + protected void onResume() { + super.onResume(); + final NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancelAll(); + } +} diff --git a/StoneIsland/plugins/phonegap-plugin-push/src/android/com/adobe/phonegap/push/PushInstanceIDListenerService.java b/StoneIsland/plugins/phonegap-plugin-push/src/android/com/adobe/phonegap/push/PushInstanceIDListenerService.java new file mode 100644 index 00000000..eaa39a48 --- /dev/null +++ b/StoneIsland/plugins/phonegap-plugin-push/src/android/com/adobe/phonegap/push/PushInstanceIDListenerService.java @@ -0,0 +1,27 @@ +package com.adobe.phonegap.push; + +import android.content.Intent; +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import com.google.android.gms.iid.InstanceID; +import com.google.android.gms.iid.InstanceIDListenerService; + +import org.json.JSONException; + +import java.io.IOException; + +public class PushInstanceIDListenerService extends InstanceIDListenerService implements PushConstants { + public static final String LOG_TAG = "PushPlugin_PushInstanceIDListenerService"; + + @Override + public void onTokenRefresh() { + SharedPreferences sharedPref = getApplicationContext().getSharedPreferences(COM_ADOBE_PHONEGAP_PUSH, Context.MODE_PRIVATE); + String senderID = sharedPref.getString(SENDER_ID, ""); + if (!"".equals(senderID)) { + Intent intent = new Intent(this, RegistrationIntentService.class); + startService(intent); + } + } +} diff --git a/StoneIsland/plugins/phonegap-plugin-push/src/android/com/adobe/phonegap/push/PushPlugin.java b/StoneIsland/plugins/phonegap-plugin-push/src/android/com/adobe/phonegap/push/PushPlugin.java new file mode 100644 index 00000000..f6faaa2b --- /dev/null +++ b/StoneIsland/plugins/phonegap-plugin-push/src/android/com/adobe/phonegap/push/PushPlugin.java @@ -0,0 +1,458 @@ +package com.adobe.phonegap.push; + +import android.app.NotificationManager; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.util.Log; + +import com.google.android.gms.gcm.GcmPubSub; +import com.google.android.gms.iid.InstanceID; + +import org.apache.cordova.CallbackContext; +import org.apache.cordova.CordovaInterface; +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.CordovaWebView; +import org.apache.cordova.PluginResult; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.ArrayList; +import java.util.List; + +import me.leolin.shortcutbadger.ShortcutBadger; + +public class PushPlugin extends CordovaPlugin implements PushConstants { + + public static final String LOG_TAG = "PushPlugin"; + + private static CallbackContext pushContext; + private static CordovaWebView gWebView; + private static List<Bundle> gCachedExtras = Collections.synchronizedList(new ArrayList<Bundle>()); + private static boolean gForeground = false; + + private static String registration_id = ""; + + /** + * Gets the application context from cordova's main activity. + * @return the application context + */ + private Context getApplicationContext() { + return this.cordova.getActivity().getApplicationContext(); + } + + @Override + public boolean execute(final String action, final JSONArray data, final CallbackContext callbackContext) { + Log.v(LOG_TAG, "execute: action=" + action); + gWebView = this.webView; + + if (INITIALIZE.equals(action)) { + cordova.getThreadPool().execute(new Runnable() { + public void run() { + pushContext = callbackContext; + JSONObject jo = null; + + Log.v(LOG_TAG, "execute: data=" + data.toString()); + SharedPreferences sharedPref = getApplicationContext().getSharedPreferences(COM_ADOBE_PHONEGAP_PUSH, Context.MODE_PRIVATE); + String senderID = null; + + try { + jo = data.getJSONObject(0).getJSONObject(ANDROID); + + Log.v(LOG_TAG, "execute: jo=" + jo.toString()); + + senderID = jo.getString(SENDER_ID); + + Log.v(LOG_TAG, "execute: senderID=" + senderID); + + String savedSenderID = sharedPref.getString(SENDER_ID, ""); + registration_id = InstanceID.getInstance(getApplicationContext()).getToken(senderID, GCM); + + if (!"".equals(registration_id)) { + JSONObject json = new JSONObject().put(REGISTRATION_ID, registration_id); + + Log.v(LOG_TAG, "onRegistered: " + json.toString()); + + JSONArray topics = jo.optJSONArray(TOPICS); + subscribeToTopics(topics, registration_id); + + PushPlugin.sendEvent( json ); + } else { + callbackContext.error("Empty registration ID received from GCM"); + return; + } + } catch (JSONException e) { + Log.e(LOG_TAG, "execute: Got JSON Exception " + e.getMessage()); + callbackContext.error(e.getMessage()); + } catch (IOException e) { + Log.e(LOG_TAG, "execute: Got JSON Exception " + e.getMessage()); + callbackContext.error(e.getMessage()); + } + + if (jo != null) { + SharedPreferences.Editor editor = sharedPref.edit(); + try { + editor.putString(ICON, jo.getString(ICON)); + } catch (JSONException e) { + Log.d(LOG_TAG, "no icon option"); + } + try { + editor.putString(ICON_COLOR, jo.getString(ICON_COLOR)); + } catch (JSONException e) { + Log.d(LOG_TAG, "no iconColor option"); + } + + boolean clearBadge = jo.optBoolean(CLEAR_BADGE, false); + if (clearBadge) { + setApplicationIconBadgeNumber(getApplicationContext(), 0); + } + + editor.putBoolean(SOUND, jo.optBoolean(SOUND, true)); + editor.putBoolean(VIBRATE, jo.optBoolean(VIBRATE, true)); + editor.putBoolean(CLEAR_BADGE, clearBadge); + editor.putBoolean(CLEAR_NOTIFICATIONS, jo.optBoolean(CLEAR_NOTIFICATIONS, true)); + editor.putBoolean(FORCE_SHOW, jo.optBoolean(FORCE_SHOW, false)); + editor.putString(SENDER_ID, senderID); + editor.commit(); + + } + + if (!gCachedExtras.isEmpty()) { + Log.v(LOG_TAG, "sending cached extras"); + synchronized(gCachedExtras) { + Iterator<Bundle> gCachedExtrasIterator = gCachedExtras.iterator(); + while (gCachedExtrasIterator.hasNext()) { + sendExtras(gCachedExtrasIterator.next()); + } + } + gCachedExtras.clear(); + } + } + }); + } else if (UNREGISTER.equals(action)) { + cordova.getThreadPool().execute(new Runnable() { + public void run() { + try { + SharedPreferences sharedPref = getApplicationContext().getSharedPreferences(COM_ADOBE_PHONEGAP_PUSH, Context.MODE_PRIVATE); + JSONArray topics = data.optJSONArray(0); + if (topics != null && !"".equals(registration_id)) { + unsubscribeFromTopics(topics, registration_id); + } else { + InstanceID.getInstance(getApplicationContext()).deleteInstanceID(); + Log.v(LOG_TAG, "UNREGISTER"); + + // Remove shared prefs + SharedPreferences.Editor editor = sharedPref.edit(); + editor.remove(SOUND); + editor.remove(VIBRATE); + editor.remove(CLEAR_BADGE); + editor.remove(CLEAR_NOTIFICATIONS); + editor.remove(FORCE_SHOW); + editor.remove(SENDER_ID); + editor.commit(); + } + + callbackContext.success(); + } catch (IOException e) { + Log.e(LOG_TAG, "execute: Got JSON Exception " + e.getMessage()); + callbackContext.error(e.getMessage()); + } + } + }); + } else if (FINISH.equals(action)) { + callbackContext.success(); + } else if (HAS_PERMISSION.equals(action)) { + cordova.getThreadPool().execute(new Runnable() { + public void run() { + JSONObject jo = new JSONObject(); + try { + jo.put("isEnabled", PermissionUtils.hasPermission(getApplicationContext(), "OP_POST_NOTIFICATION")); + PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, jo); + pluginResult.setKeepCallback(true); + callbackContext.sendPluginResult(pluginResult); + } catch (UnknownError e) { + callbackContext.error(e.getMessage()); + } catch (JSONException e) { + callbackContext.error(e.getMessage()); + } + } + }); + } else if (SET_APPLICATION_ICON_BADGE_NUMBER.equals(action)) { + cordova.getThreadPool().execute(new Runnable() { + public void run() { + Log.v(LOG_TAG, "setApplicationIconBadgeNumber: data=" + data.toString()); + try { + setApplicationIconBadgeNumber(getApplicationContext(), data.getJSONObject(0).getInt(BADGE)); + } catch (JSONException e) { + callbackContext.error(e.getMessage()); + } + callbackContext.success(); + } + }); + } else if (CLEAR_ALL_NOTIFICATIONS.equals(action)) { + cordova.getThreadPool().execute(new Runnable() { + public void run() { + Log.v(LOG_TAG, "clearAllNotifications"); + clearAllNotifications(); + callbackContext.success(); + } + }); + } else if (SUBSCRIBE.equals(action)){ + // Subscribing for a topic + cordova.getThreadPool().execute(new Runnable() { + public void run() { + try { + String topic = data.getString(0); + subscribeToTopic(topic, registration_id); + callbackContext.success(); + } catch (JSONException e) { + callbackContext.error(e.getMessage()); + } catch (IOException e) { + callbackContext.error(e.getMessage()); + } + } + }); + } else if (UNSUBSCRIBE.equals(action)){ + // un-subscribing for a topic + cordova.getThreadPool().execute(new Runnable(){ + public void run() { + try { + String topic = data.getString(0); + unsubscribeFromTopic(topic, registration_id); + callbackContext.success(); + } catch (JSONException e) { + callbackContext.error(e.getMessage()); + } catch (IOException e) { + callbackContext.error(e.getMessage()); + } + } + }); + } else { + Log.e(LOG_TAG, "Invalid action : " + action); + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.INVALID_ACTION)); + return false; + } + + return true; + } + + public static void sendEvent(JSONObject _json) { + PluginResult pluginResult = new PluginResult(PluginResult.Status.OK, _json); + pluginResult.setKeepCallback(true); + if (pushContext != null) { + pushContext.sendPluginResult(pluginResult); + } + } + + public static void sendError(String message) { + PluginResult pluginResult = new PluginResult(PluginResult.Status.ERROR, message); + pluginResult.setKeepCallback(true); + if (pushContext != null) { + pushContext.sendPluginResult(pluginResult); + } + } + + /* + * 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 (gWebView != null) { + sendEvent(convertBundleToJson(extras)); + } else { + Log.v(LOG_TAG, "sendExtras: caching extras to send at a later time."); + gCachedExtras.add(extras); + } + } + } + + public static void setApplicationIconBadgeNumber(Context context, int badgeCount) { + if (badgeCount > 0) { + ShortcutBadger.applyCount(context, badgeCount); + } else { + ShortcutBadger.removeCount(context); + } + } + + @Override + public void initialize(CordovaInterface cordova, CordovaWebView webView) { + super.initialize(cordova, webView); + gForeground = true; + } + + @Override + public void onPause(boolean multitasking) { + super.onPause(multitasking); + gForeground = false; + + SharedPreferences prefs = getApplicationContext().getSharedPreferences(COM_ADOBE_PHONEGAP_PUSH, Context.MODE_PRIVATE); + if (prefs.getBoolean(CLEAR_NOTIFICATIONS, true)) { + clearAllNotifications(); + } + } + + @Override + public void onResume(boolean multitasking) { + super.onResume(multitasking); + gForeground = true; + } + + @Override + public void onDestroy() { + super.onDestroy(); + gForeground = false; + gWebView = null; + } + + private void clearAllNotifications() { + final NotificationManager notificationManager = (NotificationManager) cordova.getActivity().getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancelAll(); + } + + /** + * Transform `topic name` to `topic path` + * Normally, the `topic` inputed from end-user is `topic name` only. + * We should convert them to GCM `topic path` + * Example: + * when topic name = 'my-topic' + * then topic path = '/topics/my-topic' + * + * @param String topic The topic name + * @return The topic path + */ + private String getTopicPath(String topic) + { + return "/topics/" + topic; + } + + private void subscribeToTopics(JSONArray topics, String registrationToken) throws IOException { + if (topics != null) { + String topic = null; + for (int i=0; i<topics.length(); i++) { + topic = topics.optString(i, null); + subscribeToTopic(topic, registrationToken); + } + } + } + + private void subscribeToTopic(String topic, String registrationToken) throws IOException + { + try { + if (topic != null) { + Log.d(LOG_TAG, "Subscribing to topic: " + topic); + GcmPubSub.getInstance(getApplicationContext()).subscribe(registrationToken, getTopicPath(topic), null); + } + } catch (IOException e) { + Log.e(LOG_TAG, "Failed to subscribe to topic: " + topic, e); + throw e; + } + } + + private void unsubscribeFromTopics(JSONArray topics, String registrationToken) { + if (topics != null) { + String topic = null; + for (int i=0; i<topics.length(); i++) { + try { + topic = topics.optString(i, null); + if (topic != null) { + Log.d(LOG_TAG, "Unsubscribing to topic: " + topic); + GcmPubSub.getInstance(getApplicationContext()).unsubscribe(registrationToken, getTopicPath(topic)); + } + } catch (IOException e) { + Log.e(LOG_TAG, "Failed to unsubscribe to topic: " + topic, e); + } + } + } + } + + private void unsubscribeFromTopic(String topic, String registrationToken) throws IOException + { + try { + if (topic != null) { + Log.d(LOG_TAG, "Unsubscribing to topic: " + topic); + GcmPubSub.getInstance(getApplicationContext()).unsubscribe(registrationToken, getTopicPath(topic)); + } + } catch (IOException e) { + Log.e(LOG_TAG, "Failed to unsubscribe to topic: " + topic, e); + throw e; + } + } + + /* + * serializes a bundle to JSON. + */ + private static JSONObject convertBundleToJson(Bundle extras) { + Log.d(LOG_TAG, "convert extras to json"); + try { + JSONObject json = new JSONObject(); + JSONObject additionalData = new JSONObject(); + + // Add any keys that need to be in top level json to this set + HashSet<String> jsonKeySet = new HashSet(); + Collections.addAll(jsonKeySet, TITLE,MESSAGE,COUNT,SOUND,IMAGE); + + Iterator<String> it = extras.keySet().iterator(); + while (it.hasNext()) { + String key = it.next(); + Object value = extras.get(key); + + Log.d(LOG_TAG, "key = " + key); + + if (jsonKeySet.contains(key)) { + json.put(key, value); + } + else if (key.equals(COLDSTART)) { + additionalData.put(key, extras.getBoolean(COLDSTART)); + } + else if (key.equals(FOREGROUND)) { + additionalData.put(key, extras.getBoolean(FOREGROUND)); + } + else if ( value instanceof String ) { + String strValue = (String)value; + try { + // Try to figure out if the value is another JSON object + if (strValue.startsWith("{")) { + additionalData.put(key, new JSONObject(strValue)); + } + // Try to figure out if the value is another JSON array + else if (strValue.startsWith("[")) { + additionalData.put(key, new JSONArray(strValue)); + } + else { + additionalData.put(key, value); + } + } catch (Exception e) { + additionalData.put(key, value); + } + } + } // while + + json.put(ADDITIONAL_DATA, additionalData); + Log.v(LOG_TAG, "extrasToJSON: " + json.toString()); + + return json; + } + catch( JSONException e) { + Log.e(LOG_TAG, "extrasToJSON: JSON exception"); + } + return null; + } + + public static boolean isInForeground() { + return gForeground; + } + + public static boolean isActive() { + return gWebView != null; + } + + protected static void setRegistrationID(String token) { + registration_id = token; + } +} diff --git a/StoneIsland/plugins/phonegap-plugin-push/src/android/com/adobe/phonegap/push/RegistrationIntentService.java b/StoneIsland/plugins/phonegap-plugin-push/src/android/com/adobe/phonegap/push/RegistrationIntentService.java new file mode 100644 index 00000000..b181e88e --- /dev/null +++ b/StoneIsland/plugins/phonegap-plugin-push/src/android/com/adobe/phonegap/push/RegistrationIntentService.java @@ -0,0 +1,38 @@ +package com.adobe.phonegap.push; + +import android.content.Context; + +import android.app.IntentService; +import android.content.Intent; +import android.content.SharedPreferences; +import android.util.Log; + +import com.google.android.gms.gcm.GoogleCloudMessaging; +import com.google.android.gms.iid.InstanceID; + +import java.io.IOException; + +public class RegistrationIntentService extends IntentService implements PushConstants { + public static final String LOG_TAG = "PushPlugin_RegistrationIntentService"; + + public RegistrationIntentService() { + super(LOG_TAG); + } + + @Override + protected void onHandleIntent(Intent intent) { + SharedPreferences sharedPreferences = getApplicationContext().getSharedPreferences(COM_ADOBE_PHONEGAP_PUSH, Context.MODE_PRIVATE); + + try { + InstanceID instanceID = InstanceID.getInstance(this); + String senderID = sharedPreferences.getString(SENDER_ID, ""); + String token = instanceID.getToken(senderID, + GoogleCloudMessaging.INSTANCE_ID_SCOPE, null); + PushPlugin.setRegistrationID(token); + Log.i(LOG_TAG, "new GCM Registration Token: " + token); + + } catch (Exception e) { + Log.d(LOG_TAG, "Failed to complete token refresh", e); + } + } +} diff --git a/StoneIsland/plugins/phonegap-plugin-push/src/browser/ServiceWorker.js b/StoneIsland/plugins/phonegap-plugin-push/src/browser/ServiceWorker.js new file mode 100644 index 00000000..0fc06d09 --- /dev/null +++ b/StoneIsland/plugins/phonegap-plugin-push/src/browser/ServiceWorker.js @@ -0,0 +1,51 @@ +var messageChannel; + +self.addEventListener('install', function(event) { + self.skipWaiting(); +}); + +self.addEventListener('push', function(event) { + // parse incoming message + var obj = {}; + var pushData = { + image: 'https://avatars1.githubusercontent.com/u/60365?v=3&s=200', + additionalData: {} + }; + if (event.data) { + obj = event.data.json(); + } + + console.log(obj); + + // convert to push plugin API + for (var key in obj) { + if (key === 'title') { + pushData.title = obj[key]; + } else if (key === 'message' || key === 'body') { + pushData.message = obj[key]; + } else if (key === 'count' || key === 'msgcnt' || key === 'badge') { + pushData.count = obj[key]; + } else if (key === 'sound' || key === 'soundname') { + pushData.sound = obj[key]; + } else if (key === 'image') { + pushData.image = obj[key]; + } else { + pushData.additionalData[key] = obj[key]; + } + } + + event.waitUntil( + self.registration.showNotification(pushData.title, { + body: pushData.message, + icon: pushData.image, + tag: 'simple-push-demo-notification-tag' + }) + ); + + messageChannel.ports[0].postMessage(pushData); + +}); + +self.addEventListener('message', function(event) { + messageChannel = event; +}); diff --git a/StoneIsland/plugins/phonegap-plugin-push/src/browser/manifest.json b/StoneIsland/plugins/phonegap-plugin-push/src/browser/manifest.json new file mode 100644 index 00000000..ce8390ab --- /dev/null +++ b/StoneIsland/plugins/phonegap-plugin-push/src/browser/manifest.json @@ -0,0 +1,4 @@ +{ + "name": "Push Demo", + "gcm_sender_id": "85075801930" +} diff --git a/StoneIsland/plugins/phonegap-plugin-push/src/ios/AppDelegate+notification.h b/StoneIsland/plugins/phonegap-plugin-push/src/ios/AppDelegate+notification.h new file mode 100644 index 00000000..9970762b --- /dev/null +++ b/StoneIsland/plugins/phonegap-plugin-push/src/ios/AppDelegate+notification.h @@ -0,0 +1,22 @@ +// +// AppDelegate+notification.h +// pushtest +// +// Created by Robert Easterday on 10/26/12. +// +// + +#import "AppDelegate.h" + +@interface AppDelegate (notification) +- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken; +- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error; +- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:( void (^)(UIBackgroundFetchResult))completionHandler; +- (void)pushPluginOnApplicationDidBecomeActive:(UIApplication *)application; +- (void)application:(UIApplication *)application handleActionWithIdentifier:(NSString *)identifier forRemoteNotification:(NSDictionary *)userInfo completionHandler:(void(^)())completionHandler; +- (id) getCommandInstance:(NSString*)className; + +@property (nonatomic, retain) NSDictionary *launchNotification; +@property (nonatomic, retain) NSNumber *coldstart; + +@end diff --git a/StoneIsland/plugins/phonegap-plugin-push/src/ios/AppDelegate+notification.m b/StoneIsland/plugins/phonegap-plugin-push/src/ios/AppDelegate+notification.m new file mode 100644 index 00000000..fc18dd79 --- /dev/null +++ b/StoneIsland/plugins/phonegap-plugin-push/src/ios/AppDelegate+notification.m @@ -0,0 +1,278 @@ +// +// AppDelegate+notification.m +// pushtest +// +// Created by Robert Easterday on 10/26/12. +// +// + +#import "AppDelegate+notification.h" +#import "PushPlugin.h" +#import <objc/runtime.h> + +static char launchNotificationKey; +static char coldstartKey; + +@implementation AppDelegate (notification) + +- (id) getCommandInstance:(NSString*)className +{ + return [self.viewController getCommandInstance:className]; +} + +// its dangerous to override a method from within a category. +// Instead we will use method swizzling. we set this up in the load call. ++ (void)load +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + Class class = [self class]; + + SEL originalSelector = @selector(init); + SEL swizzledSelector = @selector(pushPluginSwizzledInit); + + Method original = class_getInstanceMethod(class, originalSelector); + Method swizzled = class_getInstanceMethod(class, swizzledSelector); + + BOOL didAddMethod = + class_addMethod(class, + originalSelector, + method_getImplementation(swizzled), + method_getTypeEncoding(swizzled)); + + if (didAddMethod) { + class_replaceMethod(class, + swizzledSelector, + method_getImplementation(original), + method_getTypeEncoding(original)); + } else { + method_exchangeImplementations(original, swizzled); + } + }); +} + +- (AppDelegate *)pushPluginSwizzledInit +{ + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(createNotificationChecker:) + name:UIApplicationDidFinishLaunchingNotification + object:nil]; + [[NSNotificationCenter defaultCenter]addObserver:self + selector:@selector(pushPluginOnApplicationDidBecomeActive:) + name:UIApplicationDidBecomeActiveNotification + object:nil]; + + // This actually calls the original init method over in AppDelegate. Equivilent to calling super + // on an overrided method, this is not recursive, although it appears that way. neat huh? + return [self pushPluginSwizzledInit]; +} + +// This code will be called immediately after application:didFinishLaunchingWithOptions:. We need +// to process notifications in cold-start situations +- (void)createNotificationChecker:(NSNotification *)notification +{ + NSLog(@"createNotificationChecker"); + if (notification) + { + NSDictionary *launchOptions = [notification userInfo]; + if (launchOptions) { + NSLog(@"coldstart"); + self.launchNotification = [launchOptions objectForKey: @"UIApplicationLaunchOptionsRemoteNotificationKey"]; + self.coldstart = [NSNumber numberWithBool:YES]; + } else { + NSLog(@"not coldstart"); + self.coldstart = [NSNumber numberWithBool:NO]; + } + } +} + +- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { + PushPlugin *pushHandler = [self getCommandInstance:@"PushNotification"]; + [pushHandler didRegisterForRemoteNotificationsWithDeviceToken:deviceToken]; +} + +- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error { + PushPlugin *pushHandler = [self getCommandInstance:@"PushNotification"]; + [pushHandler didFailToRegisterForRemoteNotificationsWithError:error]; +} + +- (void) application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo { + NSLog(@"clicked on the shade"); + PushPlugin *pushHandler = [self getCommandInstance:@"PushNotification"]; + pushHandler.notificationMessage = userInfo; + pushHandler.isInline = NO; + [pushHandler notificationReceived]; +} + +- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { + NSLog(@"didReceiveNotification with fetchCompletionHandler"); + + // app is in the foreground so call notification callback + if (application.applicationState == UIApplicationStateActive) { + NSLog(@"app active"); + PushPlugin *pushHandler = [self getCommandInstance:@"PushNotification"]; + pushHandler.notificationMessage = userInfo; + pushHandler.isInline = YES; + [pushHandler notificationReceived]; + + completionHandler(UIBackgroundFetchResultNewData); + } + // app is in background or in stand by + else { + NSLog(@"app in-active"); + + // do some convoluted logic to find out if this should be a silent push. + long silent = 0; + id aps = [userInfo objectForKey:@"aps"]; + id contentAvailable = [aps objectForKey:@"content-available"]; + if ([contentAvailable isKindOfClass:[NSString class]] && [contentAvailable isEqualToString:@"1"]) { + silent = 1; + } else if ([contentAvailable isKindOfClass:[NSNumber class]]) { + silent = [contentAvailable integerValue]; + } + + if (silent == 1) { + NSLog(@"this should be a silent push"); + void (^safeHandler)(UIBackgroundFetchResult) = ^(UIBackgroundFetchResult result){ + dispatch_async(dispatch_get_main_queue(), ^{ + completionHandler(result); + }); + }; + + PushPlugin *pushHandler = [self getCommandInstance:@"PushNotification"]; + + if (pushHandler.handlerObj == nil) { + pushHandler.handlerObj = [NSMutableDictionary dictionaryWithCapacity:2]; + } + + id notId = [userInfo objectForKey:@"notId"]; + if (notId != nil) { + NSLog(@"Push Plugin notId %@", notId); + [pushHandler.handlerObj setObject:safeHandler forKey:notId]; + } else { + NSLog(@"Push Plugin notId handler"); + [pushHandler.handlerObj setObject:safeHandler forKey:@"handler"]; + } + + pushHandler.notificationMessage = userInfo; + pushHandler.isInline = NO; + [pushHandler notificationReceived]; + } else { + NSLog(@"just put it in the shade"); + //save it for later + self.launchNotification = userInfo; + + completionHandler(UIBackgroundFetchResultNewData); + } + } +} + +- (BOOL)userHasRemoteNotificationsEnabled { + UIApplication *application = [UIApplication sharedApplication]; + if ([[UIApplication sharedApplication] respondsToSelector:@selector(registerUserNotificationSettings:)]) { + return application.currentUserNotificationSettings.types != UIUserNotificationTypeNone; + } else { +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" + return application.enabledRemoteNotificationTypes != UIRemoteNotificationTypeNone; +#pragma GCC diagnostic pop + } +} + +- (void)pushPluginOnApplicationDidBecomeActive:(NSNotification *)notification { + + NSLog(@"active"); + + UIApplication *application = notification.object; + + PushPlugin *pushHandler = [self getCommandInstance:@"PushNotification"]; + if (pushHandler.clearBadge) { + NSLog(@"PushPlugin clearing badge"); + //zero badge + application.applicationIconBadgeNumber = 0; + } else { + NSLog(@"PushPlugin skip clear badge"); + } + + if (self.launchNotification) { + pushHandler.isInline = NO; + pushHandler.coldstart = [self.coldstart boolValue]; + pushHandler.notificationMessage = self.launchNotification; + self.launchNotification = nil; + self.coldstart = [NSNumber numberWithBool:NO]; + [pushHandler performSelectorOnMainThread:@selector(notificationReceived) withObject:pushHandler waitUntilDone:NO]; + } +} + + +- (void)application:(UIApplication *) application handleActionWithIdentifier: (NSString *) identifier +forRemoteNotification: (NSDictionary *) notification completionHandler: (void (^)()) completionHandler { + + NSLog(@"Push Plugin handleActionWithIdentifier %@", identifier); + NSMutableDictionary *userInfo = [notification mutableCopy]; + [userInfo setObject:identifier forKey:@"actionCallback"]; + NSLog(@"Push Plugin userInfo %@", userInfo); + + if (application.applicationState == UIApplicationStateActive) { + PushPlugin *pushHandler = [self getCommandInstance:@"PushNotification"]; + pushHandler.notificationMessage = userInfo; + pushHandler.isInline = NO; + [pushHandler notificationReceived]; + } else { + void (^safeHandler)() = ^(void){ + dispatch_async(dispatch_get_main_queue(), ^{ + completionHandler(); + }); + }; + + PushPlugin *pushHandler = [self getCommandInstance:@"PushNotification"]; + + if (pushHandler.handlerObj == nil) { + pushHandler.handlerObj = [NSMutableDictionary dictionaryWithCapacity:2]; + } + + id notId = [userInfo objectForKey:@"notId"]; + if (notId != nil) { + NSLog(@"Push Plugin notId %@", notId); + [pushHandler.handlerObj setObject:safeHandler forKey:notId]; + } else { + NSLog(@"Push Plugin notId handler"); + [pushHandler.handlerObj setObject:safeHandler forKey:@"handler"]; + } + + pushHandler.notificationMessage = userInfo; + pushHandler.isInline = NO; + + [pushHandler performSelectorOnMainThread:@selector(notificationReceived) withObject:pushHandler waitUntilDone:NO]; + } +} + +// The accessors use an Associative Reference since you can't define a iVar in a category +// http://developer.apple.com/library/ios/#documentation/cocoa/conceptual/objectivec/Chapters/ocAssociativeReferences.html +- (NSMutableArray *)launchNotification +{ + return objc_getAssociatedObject(self, &launchNotificationKey); +} + +- (void)setLaunchNotification:(NSDictionary *)aDictionary +{ + objc_setAssociatedObject(self, &launchNotificationKey, aDictionary, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (NSNumber *)coldstart +{ + return objc_getAssociatedObject(self, &coldstartKey); +} + +- (void)setColdstart:(NSNumber *)aNumber +{ + objc_setAssociatedObject(self, &coldstartKey, aNumber, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (void)dealloc +{ + self.launchNotification = nil; // clear the association and release the object + self.coldstart = nil; +} + +@end diff --git a/StoneIsland/plugins/phonegap-plugin-push/src/ios/PushPlugin.h b/StoneIsland/plugins/phonegap-plugin-push/src/ios/PushPlugin.h new file mode 100644 index 00000000..276a0080 --- /dev/null +++ b/StoneIsland/plugins/phonegap-plugin-push/src/ios/PushPlugin.h @@ -0,0 +1,80 @@ +/* + Copyright 2009-2011 Urban Airship Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binaryform must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided withthe distribution. + + THIS SOFTWARE IS PROVIDED BY THE URBAN AIRSHIP INC``AS IS'' AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + EVENT SHALL URBAN AIRSHIP INC OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#import <Foundation/Foundation.h> +#import <Cordova/CDV.h> +#import <Cordova/CDVPlugin.h> + +@protocol GGLInstanceIDDelegate; +@protocol GCMReceiverDelegate; +@interface PushPlugin : CDVPlugin<GGLInstanceIDDelegate, GCMReceiverDelegate> +{ + NSDictionary *notificationMessage; + BOOL isInline; + NSString *notificationCallbackId; + NSString *callback; + BOOL clearBadge; + + NSMutableDictionary *handlerObj; + void (^completionHandler)(UIBackgroundFetchResult); + + BOOL ready; +} + +@property (nonatomic, copy) NSString *callbackId; +@property (nonatomic, copy) NSString *notificationCallbackId; +@property (nonatomic, copy) NSString *callback; + +@property (nonatomic, strong) NSDictionary *notificationMessage; +@property BOOL isInline; +@property BOOL coldstart; +@property BOOL clearBadge; +@property (nonatomic, strong) NSMutableDictionary *handlerObj; + +- (void)init:(CDVInvokedUrlCommand*)command; +- (void)unregister:(CDVInvokedUrlCommand*)command; +- (void)subscribe:(CDVInvokedUrlCommand*)command; +- (void)unsubscribe:(CDVInvokedUrlCommand*)command; + +- (void)didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken; +- (void)didFailToRegisterForRemoteNotificationsWithError:(NSError *)error; + +- (void)setNotificationMessage:(NSDictionary *)notification; +- (void)notificationReceived; + +- (void)willSendDataMessageWithID:(NSString *)messageID error:(NSError *)error; +- (void)didSendDataMessageWithID:(NSString *)messageID; +- (void)didDeleteMessagesOnServer; + +// GCM Features +@property(nonatomic, assign) BOOL usesGCM; +@property(nonatomic, strong) NSNumber* gcmSandbox; +@property(nonatomic, strong) NSString *gcmSenderId; +@property(nonatomic, strong) NSDictionary *gcmRegistrationOptions; +@property(nonatomic, strong) void (^gcmRegistrationHandler) (NSString *registrationToken, NSError *error); +@property(nonatomic, strong) NSString *gcmRegistrationToken; +@property(nonatomic, strong) NSArray *gcmTopics; + +@end diff --git a/StoneIsland/plugins/phonegap-plugin-push/src/ios/PushPlugin.m b/StoneIsland/plugins/phonegap-plugin-push/src/ios/PushPlugin.m new file mode 100644 index 00000000..a176b9af --- /dev/null +++ b/StoneIsland/plugins/phonegap-plugin-push/src/ios/PushPlugin.m @@ -0,0 +1,677 @@ +/* + Copyright 2009-2011 Urban Airship Inc. All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binaryform must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided withthe distribution. + + THIS SOFTWARE IS PROVIDED BY THE URBAN AIRSHIP INC``AS IS'' AND ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + EVENT SHALL URBAN AIRSHIP INC OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +// See GGLInstanceID.h +#define GMP_NO_MODULES true + +#import "PushPlugin.h" +#import "GoogleCloudMessaging.h" +#import "GGLInstanceIDHeaders.h" + +@implementation PushPlugin : CDVPlugin + +@synthesize notificationMessage; +@synthesize isInline; +@synthesize coldstart; + +@synthesize callbackId; +@synthesize notificationCallbackId; +@synthesize callback; +@synthesize clearBadge; +@synthesize handlerObj; + +@synthesize usesGCM; +@synthesize gcmSandbox; +@synthesize gcmSenderId; +@synthesize gcmRegistrationOptions; +@synthesize gcmRegistrationHandler; +@synthesize gcmRegistrationToken; +@synthesize gcmTopics; + +-(void)initGCMRegistrationHandler; +{ + __weak __block PushPlugin *weakSelf = self; + gcmRegistrationHandler = ^(NSString *registrationToken, NSError *error){ + if (registrationToken != nil) { + NSLog(@"GCM Registration Token: %@", registrationToken); + [weakSelf setGcmRegistrationToken: registrationToken]; + + id topics = [weakSelf gcmTopics]; + if (topics != nil) { + for (NSString *topic in topics) { + NSLog(@"subscribe from topic: %@", topic); + id pubSub = [GCMPubSub sharedInstance]; + [pubSub subscribeWithToken: [weakSelf gcmRegistrationToken] + topic:[NSString stringWithFormat:@"/topics/%@", topic] + options:nil + handler:^void(NSError *error) { + if (error) { + if (error.code == 3001) { + NSLog(@"Already subscribed to %@", topic); + } else { + NSLog(@"Failed to subscribe to topic %@: %@", topic, error); + } + } + else { + NSLog(@"Successfully subscribe to topic %@", topic); + } + }]; + } + } + + [weakSelf registerWithToken:registrationToken]; + } else { + NSLog(@"Registration to GCM failed with error: %@", error.localizedDescription); + [weakSelf failWithMessage:self.callbackId withMsg:@"" withError:error]; + } + }; +} + +// GCM refresh token +// Unclear how this is testable under normal circumstances +- (void)onTokenRefresh { +#if !TARGET_IPHONE_SIMULATOR + // A rotation of the registration tokens is happening, so the app needs to request a new token. + NSLog(@"The GCM registration token needs to be changed."); + [[GGLInstanceID sharedInstance] tokenWithAuthorizedEntity:[self gcmSenderId] + scope:kGGLInstanceIDScopeGCM + options:[self gcmRegistrationOptions] + handler:[self gcmRegistrationHandler]]; +#endif +} + +- (void)willSendDataMessageWithID:(NSString *)messageID error:(NSError *)error { + NSLog(@"willSendDataMessageWithID"); + if (error) { + // Failed to send the message. + } else { + // Will send message, you can save the messageID to track the message + } +} + +- (void)didSendDataMessageWithID:(NSString *)messageID { + NSLog(@"willSendDataMessageWithID"); + // Did successfully send message identified by messageID +} + +- (void)didDeleteMessagesOnServer { + NSLog(@"didDeleteMessagesOnServer"); + // Some messages sent to this device were deleted on the GCM server before reception, likely + // because the TTL expired. The client should notify the app server of this, so that the app + // server can resend those messages. +} + +- (void)unregister:(CDVInvokedUrlCommand*)command; +{ + NSArray* topics = [command argumentAtIndex:0]; + + if (topics != nil) { + id pubSub = [GCMPubSub sharedInstance]; + for (NSString *topic in topics) { + NSLog(@"unsubscribe from topic: %@", topic); + [pubSub unsubscribeWithToken: [self gcmRegistrationToken] + topic:[NSString stringWithFormat:@"/topics/%@", topic] + options:nil + handler:^void(NSError *error) { + if (error) { + NSLog(@"Failed to unsubscribe from topic %@: %@", topic, error); + } + else { + NSLog(@"Successfully unsubscribe from topic %@", topic); + } + }]; + } + } else { + [[UIApplication sharedApplication] unregisterForRemoteNotifications]; + [self successWithMessage:command.callbackId withMsg:@"unregistered"]; + } +} + +- (void)subscribe:(CDVInvokedUrlCommand*)command; +{ + NSString* topic = [command argumentAtIndex:0]; + + if (topic != nil) { + NSLog(@"subscribe from topic: %@", topic); + id pubSub = [GCMPubSub sharedInstance]; + [pubSub subscribeWithToken: [self gcmRegistrationToken] + topic:[NSString stringWithFormat:@"/topics/%@", topic] + options:nil + handler:^void(NSError *error) { + if (error) { + if (error.code == 3001) { + NSLog(@"Already subscribed to %@", topic); + [self successWithMessage:command.callbackId withMsg:[NSString stringWithFormat:@"Already subscribed to %@", topic]]; + } else { + NSLog(@"Failed to subscribe to topic %@: %@", topic, error); + [self failWithMessage:command.callbackId withMsg:[NSString stringWithFormat:@"Failed to subscribe to topic %@", topic] withError:error]; + } + } + else { + NSLog(@"Successfully subscribe to topic %@", topic); + [self successWithMessage:command.callbackId withMsg:[NSString stringWithFormat:@"Successfully subscribe to topic %@", topic]]; + } + }]; + } else { + NSLog(@"There is no topic to subscribe"); + [self successWithMessage:command.callbackId withMsg:@"There is no topic to subscribe"]; + } +} + +- (void)unsubscribe:(CDVInvokedUrlCommand*)command; +{ + NSString* topic = [command argumentAtIndex:0]; + + if (topic != nil) { + NSLog(@"unsubscribe from topic: %@", topic); + id pubSub = [GCMPubSub sharedInstance]; + [pubSub unsubscribeWithToken: [self gcmRegistrationToken] + topic:[NSString stringWithFormat:@"/topics/%@", topic] + options:nil + handler:^void(NSError *error) { + if (error) { + NSLog(@"Failed to unsubscribe to topic %@: %@", topic, error); + [self failWithMessage:command.callbackId withMsg:[NSString stringWithFormat:@"Failed to unsubscribe to topic %@", topic] withError:error]; + } else { + NSLog(@"Successfully unsubscribe to topic %@", topic); + [self successWithMessage:command.callbackId withMsg:[NSString stringWithFormat:@"Successfully unsubscribe to topic %@", topic]]; + } + }]; + } else { + NSLog(@"There is no topic to unsubscribe"); + [self successWithMessage:command.callbackId withMsg:@"There is no topic to unsubscribe"]; + } +} + +- (void)init:(CDVInvokedUrlCommand*)command; +{ + [self.commandDelegate runInBackground:^ { + + NSLog(@"Push Plugin register called"); + self.callbackId = command.callbackId; + + NSMutableDictionary* options = [command.arguments objectAtIndex:0]; + NSMutableDictionary* iosOptions = [options objectForKey:@"ios"]; + + NSArray* topics = [iosOptions objectForKey:@"topics"]; + [self setGcmTopics:topics]; + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000 + UIUserNotificationType UserNotificationTypes = UIUserNotificationTypeNone; +#endif + UIRemoteNotificationType notificationTypes = UIRemoteNotificationTypeNone; + + id badgeArg = [iosOptions objectForKey:@"badge"]; + id soundArg = [iosOptions objectForKey:@"sound"]; + id alertArg = [iosOptions objectForKey:@"alert"]; + id clearBadgeArg = [iosOptions objectForKey:@"clearBadge"]; + + if (([badgeArg isKindOfClass:[NSString class]] && [badgeArg isEqualToString:@"true"]) || [badgeArg boolValue]) + { + notificationTypes |= UIRemoteNotificationTypeBadge; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000 + UserNotificationTypes |= UIUserNotificationTypeBadge; +#endif + } + + if (([soundArg isKindOfClass:[NSString class]] && [soundArg isEqualToString:@"true"]) || [soundArg boolValue]) + { + notificationTypes |= UIRemoteNotificationTypeSound; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000 + UserNotificationTypes |= UIUserNotificationTypeSound; +#endif + } + + if (([alertArg isKindOfClass:[NSString class]] && [alertArg isEqualToString:@"true"]) || [alertArg boolValue]) + { + notificationTypes |= UIRemoteNotificationTypeAlert; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000 + UserNotificationTypes |= UIUserNotificationTypeAlert; +#endif + } + + notificationTypes |= UIRemoteNotificationTypeNewsstandContentAvailability; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000 + UserNotificationTypes |= UIUserNotificationActivationModeBackground; +#endif + + if (clearBadgeArg == nil || ([clearBadgeArg isKindOfClass:[NSString class]] && [clearBadgeArg isEqualToString:@"false"]) || ![clearBadgeArg boolValue]) { + NSLog(@"PushPlugin.register: setting badge to false"); + clearBadge = NO; + } else { + NSLog(@"PushPlugin.register: setting badge to true"); + clearBadge = YES; + [[UIApplication sharedApplication] setApplicationIconBadgeNumber:0]; + } + NSLog(@"PushPlugin.register: clear badge is set to %d", clearBadge); + + if (notificationTypes == UIRemoteNotificationTypeNone) + NSLog(@"PushPlugin.register: Push notification type is set to none"); + + isInline = NO; + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000 + NSLog(@"PushPlugin.register: better button setup"); + // setup action buttons + NSMutableSet *categories = [[NSMutableSet alloc] init]; + id categoryOptions = [iosOptions objectForKey:@"categories"]; + if (categoryOptions != nil && [categoryOptions isKindOfClass:[NSDictionary class]]) { + for (id key in categoryOptions) { + NSLog(@"categories: key %@", key); + id category = [categoryOptions objectForKey:key]; + + id yesButton = [category objectForKey:@"yes"]; + UIMutableUserNotificationAction *yesAction; + if (yesButton != nil && [yesButton isKindOfClass:[NSDictionary class]]) { + yesAction = [self createAction: yesButton]; + } + id noButton = [category objectForKey:@"no"]; + UIMutableUserNotificationAction *noAction; + if (noButton != nil && [noButton isKindOfClass:[NSDictionary class]]) { + noAction = [self createAction: noButton]; + } + id maybeButton = [category objectForKey:@"maybe"]; + UIMutableUserNotificationAction *maybeAction; + if (maybeButton != nil && [maybeButton isKindOfClass:[NSDictionary class]]) { + maybeAction = [self createAction: maybeButton]; + } + + // First create the category + UIMutableUserNotificationCategory *notificationCategory = [[UIMutableUserNotificationCategory alloc] init]; + + // Identifier to include in your push payload and local notification + notificationCategory.identifier = key; + + NSMutableArray *categoryArray = [[NSMutableArray alloc] init]; + NSMutableArray *minimalCategoryArray = [[NSMutableArray alloc] init]; + if (yesButton != nil) { + [categoryArray addObject:yesAction]; + [minimalCategoryArray addObject:yesAction]; + } + if (noButton != nil) { + [categoryArray addObject:noAction]; + [minimalCategoryArray addObject:noAction]; + } + if (maybeButton != nil) { + [categoryArray addObject:maybeAction]; + } + + // Add the actions to the category and set the action context + [notificationCategory setActions:categoryArray forContext:UIUserNotificationActionContextDefault]; + + // Set the actions to present in a minimal context + [notificationCategory setActions:minimalCategoryArray forContext:UIUserNotificationActionContextMinimal]; + + NSLog(@"Adding category %@", key); + [categories addObject:notificationCategory]; + } + + } +#else + NSLog(@"PushPlugin.register: action buttons only supported on iOS8 and above"); +#endif + + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 80000 + if ([[UIApplication sharedApplication]respondsToSelector:@selector(registerUserNotificationSettings:)]) { + UIUserNotificationSettings *settings = [UIUserNotificationSettings settingsForTypes:UserNotificationTypes categories:categories]; + [[UIApplication sharedApplication] registerUserNotificationSettings:settings]; + [[UIApplication sharedApplication] registerForRemoteNotifications]; + } else { + [[UIApplication sharedApplication] registerForRemoteNotificationTypes: + (UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound | UIRemoteNotificationTypeAlert)]; + } +#else + [[UIApplication sharedApplication] registerForRemoteNotificationTypes: + (UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound | UIRemoteNotificationTypeAlert)]; +#endif + + // GCM options + [self setGcmSenderId: [iosOptions objectForKey:@"senderID"]]; + NSLog(@"GCM Sender ID %@", gcmSenderId); + if([[self gcmSenderId] length] > 0) { + NSLog(@"Using GCM Notification"); + [self setUsesGCM: YES]; + [self initGCMRegistrationHandler]; + } else { + NSLog(@"Using APNS Notification"); + [self setUsesGCM:NO]; + } + id gcmSandBoxArg = [iosOptions objectForKey:@"gcmSandbox"]; + + [self setGcmSandbox:@NO]; + if ([self usesGCM] && + (([gcmSandBoxArg isKindOfClass:[NSString class]] && [gcmSandBoxArg isEqualToString:@"true"]) || + [gcmSandBoxArg boolValue])) + { + NSLog(@"Using GCM Sandbox"); + [self setGcmSandbox:@YES]; + } + + if (notificationMessage) { // if there is a pending startup notification + dispatch_async(dispatch_get_main_queue(), ^{ + // delay to allow JS event handlers to be setup + [self performSelector:@selector(notificationReceived) withObject:nil afterDelay: 0.5]; + }); + } + }]; +} + +- (UIMutableUserNotificationAction *)createAction:(NSDictionary *)dictionary { + + UIMutableUserNotificationAction *myAction = [[UIMutableUserNotificationAction alloc] init]; + + myAction = [[UIMutableUserNotificationAction alloc] init]; + myAction.identifier = [dictionary objectForKey:@"callback"]; + myAction.title = [dictionary objectForKey:@"title"]; + id mode =[dictionary objectForKey:@"foreground"]; + if (mode == nil || ([mode isKindOfClass:[NSString class]] && [mode isEqualToString:@"false"]) || ![mode boolValue]) { + myAction.activationMode = UIUserNotificationActivationModeBackground; + } else { + myAction.activationMode = UIUserNotificationActivationModeForeground; + } + id destructive = [dictionary objectForKey:@"destructive"]; + if (destructive == nil || ([destructive isKindOfClass:[NSString class]] && [destructive isEqualToString:@"false"]) || ![destructive boolValue]) { + myAction.destructive = NO; + } else { + myAction.destructive = YES; + } + myAction.authenticationRequired = NO; + + return myAction; +} + +- (void)didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { + if (self.callbackId == nil) { + NSLog(@"Unexpected call to didRegisterForRemoteNotificationsWithDeviceToken, ignoring: %@", deviceToken); + return; + } + NSLog(@"Push Plugin register success: %@", deviceToken); + + NSMutableDictionary *results = [NSMutableDictionary dictionary]; + NSString *token = [[[[deviceToken description] stringByReplacingOccurrencesOfString:@"<"withString:@""] + stringByReplacingOccurrencesOfString:@">" withString:@""] + stringByReplacingOccurrencesOfString: @" " withString: @""]; + [results setValue:token forKey:@"deviceToken"]; + +#if !TARGET_IPHONE_SIMULATOR + // Get Bundle Info for Remote Registration (handy if you have more than one app) + [results setValue:[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleDisplayName"] forKey:@"appName"]; + [results setValue:[[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"] forKey:@"appVersion"]; + + // Check what Notifications the user has turned on. We registered for all three, but they may have manually disabled some or all of them. +#define SYSTEM_VERSION_LESS_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending) + + NSUInteger rntypes; + if (!SYSTEM_VERSION_LESS_THAN(@"8.0")) { + rntypes = [[[UIApplication sharedApplication] currentUserNotificationSettings] types]; + } else { + rntypes = [[UIApplication sharedApplication] enabledRemoteNotificationTypes]; + } + + // Set the defaults to disabled unless we find otherwise... + NSString *pushBadge = @"disabled"; + NSString *pushAlert = @"disabled"; + NSString *pushSound = @"disabled"; + + // Check what Registered Types are turned on. This is a bit tricky since if two are enabled, and one is off, it will return a number 2... not telling you which + // one is actually disabled. So we are literally checking to see if rnTypes matches what is turned on, instead of by number. The "tricky" part is that the + // single notification types will only match if they are the ONLY one enabled. Likewise, when we are checking for a pair of notifications, it will only be + // true if those two notifications are on. This is why the code is written this way + if(rntypes & UIRemoteNotificationTypeBadge){ + pushBadge = @"enabled"; + } + if(rntypes & UIRemoteNotificationTypeAlert) { + pushAlert = @"enabled"; + } + if(rntypes & UIRemoteNotificationTypeSound) { + pushSound = @"enabled"; + } + + [results setValue:pushBadge forKey:@"pushBadge"]; + [results setValue:pushAlert forKey:@"pushAlert"]; + [results setValue:pushSound forKey:@"pushSound"]; + + // Get the users Device Model, Display Name, Token & Version Number + UIDevice *dev = [UIDevice currentDevice]; + [results setValue:dev.name forKey:@"deviceName"]; + [results setValue:dev.model forKey:@"deviceModel"]; + [results setValue:dev.systemVersion forKey:@"deviceSystemVersion"]; + + if([self usesGCM]) { + GGLInstanceIDConfig *instanceIDConfig = [GGLInstanceIDConfig defaultConfig]; + instanceIDConfig.delegate = self; + [[GGLInstanceID sharedInstance] startWithConfig:instanceIDConfig]; + + [self setGcmRegistrationOptions: @{kGGLInstanceIDRegisterAPNSOption:deviceToken, + kGGLInstanceIDAPNSServerTypeSandboxOption:[self gcmSandbox]}]; + + [[GGLInstanceID sharedInstance] tokenWithAuthorizedEntity:[self gcmSenderId] + scope:kGGLInstanceIDScopeGCM + options:[self gcmRegistrationOptions] + handler:[self gcmRegistrationHandler]]; + + GCMConfig *gcmConfig = [GCMConfig defaultConfig]; + gcmConfig.receiverDelegate = self; + [[GCMService sharedInstance] startWithConfig:gcmConfig]; + + } else { + [self registerWithToken: token]; + } +#endif +} + +- (void)didFailToRegisterForRemoteNotificationsWithError:(NSError *)error +{ + if (self.callbackId == nil) { + NSLog(@"Unexpected call to didFailToRegisterForRemoteNotificationsWithError, ignoring: %@", error); + return; + } + NSLog(@"Push Plugin register failed"); + [self failWithMessage:self.callbackId withMsg:@"" withError:error]; +} + +- (void)notificationReceived { + NSLog(@"Notification received"); + + if (notificationMessage && self.callbackId != nil) + { + NSMutableDictionary* message = [NSMutableDictionary dictionaryWithCapacity:4]; + NSMutableDictionary* additionalData = [NSMutableDictionary dictionaryWithCapacity:4]; + + + for (id key in notificationMessage) { + if ([key isEqualToString:@"aps"]) { + id aps = [notificationMessage objectForKey:@"aps"]; + + for(id key in aps) { + NSLog(@"Push Plugin key: %@", key); + id value = [aps objectForKey:key]; + + if ([key isEqualToString:@"alert"]) { + if ([value isKindOfClass:[NSDictionary class]]) { + for (id messageKey in value) { + id messageValue = [value objectForKey:messageKey]; + if ([messageKey isEqualToString:@"body"]) { + [message setObject:messageValue forKey:@"message"]; + } else if ([messageKey isEqualToString:@"title"]) { + [message setObject:messageValue forKey:@"title"]; + } else { + [additionalData setObject:messageValue forKey:messageKey]; + } + } + } + else { + [message setObject:value forKey:@"message"]; + } + } else if ([key isEqualToString:@"title"]) { + [message setObject:value forKey:@"title"]; + } else if ([key isEqualToString:@"badge"]) { + [message setObject:value forKey:@"count"]; + } else if ([key isEqualToString:@"sound"]) { + [message setObject:value forKey:@"sound"]; + } else if ([key isEqualToString:@"image"]) { + [message setObject:value forKey:@"image"]; + } else { + [additionalData setObject:value forKey:key]; + } + } + } else { + [additionalData setObject:[notificationMessage objectForKey:key] forKey:key]; + } + } + + if (isInline) { + [additionalData setObject:[NSNumber numberWithBool:YES] forKey:@"foreground"]; + } else { + [additionalData setObject:[NSNumber numberWithBool:NO] forKey:@"foreground"]; + } + + if (coldstart) { + [additionalData setObject:[NSNumber numberWithBool:YES] forKey:@"coldstart"]; + } else { + [additionalData setObject:[NSNumber numberWithBool:NO] forKey:@"coldstart"]; + } + + [message setObject:additionalData forKey:@"additionalData"]; + + // send notification message + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:message]; + [pluginResult setKeepCallbackAsBool:YES]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; + + self.coldstart = NO; + self.notificationMessage = nil; + } +} + +- (void)setApplicationIconBadgeNumber:(CDVInvokedUrlCommand *)command +{ + NSMutableDictionary* options = [command.arguments objectAtIndex:0]; + int badge = [[options objectForKey:@"badge"] intValue] ?: 0; + + [[UIApplication sharedApplication] setApplicationIconBadgeNumber:badge]; + + NSString* message = [NSString stringWithFormat:@"app badge count set to %d", badge]; + CDVPluginResult *commandResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:message]; + [self.commandDelegate sendPluginResult:commandResult callbackId:command.callbackId]; +} + +- (void)getApplicationIconBadgeNumber:(CDVInvokedUrlCommand *)command +{ + NSInteger badge = [UIApplication sharedApplication].applicationIconBadgeNumber; + + CDVPluginResult *commandResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsInt:(int)badge]; + [self.commandDelegate sendPluginResult:commandResult callbackId:command.callbackId]; +} + +- (void)clearAllNotifications:(CDVInvokedUrlCommand *)command +{ + [[UIApplication sharedApplication] setApplicationIconBadgeNumber:0]; + + NSString* message = [NSString stringWithFormat:@"cleared all notifications"]; + CDVPluginResult *commandResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:message]; + [self.commandDelegate sendPluginResult:commandResult callbackId:command.callbackId]; +} + +- (void)hasPermission:(CDVInvokedUrlCommand *)command +{ + BOOL enabled = NO; + id<UIApplicationDelegate> appDelegate = [UIApplication sharedApplication].delegate; + if ([appDelegate respondsToSelector:@selector(userHasRemoteNotificationsEnabled)]) { + enabled = [appDelegate performSelector:@selector(userHasRemoteNotificationsEnabled)]; + } + + NSMutableDictionary* message = [NSMutableDictionary dictionaryWithCapacity:1]; + [message setObject:[NSNumber numberWithBool:enabled] forKey:@"isEnabled"]; + CDVPluginResult *commandResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:message]; + [self.commandDelegate sendPluginResult:commandResult callbackId:command.callbackId]; +} + +-(void)successWithMessage:(NSString *)callbackId withMsg:(NSString *)message +{ + if (callbackId != nil) + { + CDVPluginResult *commandResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:message]; + [self.commandDelegate sendPluginResult:commandResult callbackId:callbackId]; + } +} + +-(void)registerWithToken:(NSString*)token; { + // Send result to trigger 'registration' event but keep callback + NSMutableDictionary* message = [NSMutableDictionary dictionaryWithCapacity:1]; + [message setObject:token forKey:@"registrationId"]; + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:message]; + [pluginResult setKeepCallbackAsBool:YES]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId]; +} + + +-(void)failWithMessage:(NSString *)callbackId withMsg:(NSString *)message withError:(NSError *)error +{ + NSString *errorMessage = (error) ? [NSString stringWithFormat:@"%@ - %@", message, [error localizedDescription]] : message; + CDVPluginResult *commandResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:errorMessage]; + + [self.commandDelegate sendPluginResult:commandResult callbackId:callbackId]; +} + +-(void) finish:(CDVInvokedUrlCommand*)command +{ + NSLog(@"Push Plugin finish called"); + + [self.commandDelegate runInBackground:^ { + NSString* notId = [command.arguments objectAtIndex:0]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [NSTimer scheduledTimerWithTimeInterval:0.1 + target:self + selector:@selector(stopBackgroundTask:) + userInfo:notId + repeats:NO]; + }); + + CDVPluginResult* pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK]; + [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId]; + }]; +} + +-(void)stopBackgroundTask:(NSTimer*)timer +{ + UIApplication *app = [UIApplication sharedApplication]; + + NSLog(@"Push Plugin stopBackgroundTask called"); + + if (handlerObj) { + NSLog(@"Push Plugin handlerObj"); + completionHandler = [handlerObj[[timer userInfo]] copy]; + if (completionHandler) { + NSLog(@"Push Plugin: stopBackgroundTask (remaining t: %f)", app.backgroundTimeRemaining); + completionHandler(UIBackgroundFetchResultNewData); + completionHandler = nil; + } + } +} + +@end diff --git a/StoneIsland/plugins/phonegap-plugin-push/src/windows/PushPluginProxy.js b/StoneIsland/plugins/phonegap-plugin-push/src/windows/PushPluginProxy.js new file mode 100644 index 00000000..ac04f39d --- /dev/null +++ b/StoneIsland/plugins/phonegap-plugin-push/src/windows/PushPluginProxy.js @@ -0,0 +1,93 @@ +var myApp = {}; +var pushNotifications = Windows.Networking.PushNotifications; + +var createNotificationJSON = function (e) { + var result = { message: '' }; //Added to identify callback as notification type in the API in case where notification has no message + var notificationPayload; + + switch (e.notificationType) { + case pushNotifications.PushNotificationType.toast: + case pushNotifications.PushNotificationType.tile: + if (e.notificationType === pushNotifications.PushNotificationType.toast) { + notificationPayload = e.toastNotification.content; + } + else { + notificationPayload = e.tileNotification.content; + } + var texts = notificationPayload.getElementsByTagName("text"); + if (texts.length > 1) { + result.title = texts[0].innerText; + result.message = texts[1].innerText; + } + else if(texts.length === 1) { + result.message = texts[0].innerText; + } + var images = notificationPayload.getElementsByTagName("image"); + if (images.length > 0) { + result.image = images[0].getAttribute("src"); + } + var soundFile = notificationPayload.getElementsByTagName("audio"); + if (soundFile.length > 0) { + result.sound = soundFile[0].getAttribute("src"); + } + break; + + case pushNotifications.PushNotificationType.badge: + notificationPayload = e.badgeNotification.content; + result.count = notificationPayload.getElementsByTagName("badge")[0].getAttribute("value"); + break; + + case pushNotifications.PushNotificationType.raw: + result.message = e.rawNotification.content; + break; + } + + result.additionalData = { coldstart: false }; // this gets called only when the app is running + result.additionalData.pushNotificationReceivedEventArgs = e; + return result; +} + +module.exports = { + init: function (onSuccess, onFail, args) { + + var onNotificationReceived = function (e) { + var result = createNotificationJSON(e); + onSuccess(result, { keepCallback: true }); + } + + try { + pushNotifications.PushNotificationChannelManager.createPushNotificationChannelForApplicationAsync().done( + function (channel) { + var result = {}; + result.registrationId = channel.uri; + myApp.channel = channel; + channel.addEventListener("pushnotificationreceived", onNotificationReceived); + myApp.notificationEvent = onNotificationReceived; + onSuccess(result, { keepCallback: true }); + + var context = cordova.require('cordova/platform').activationContext; + var launchArgs = context ? context.args : null; + if (launchArgs) { //If present, app launched through push notification + var result = { message: '' }; //Added to identify callback as notification type in the API + result.launchArgs = launchArgs; + result.additionalData = { coldstart: true }; + onSuccess(result, { keepCallback: true }); + } + }, function (error) { + onFail(error); + }); + } catch (ex) { + onFail(ex); + } + }, + unregister: function (onSuccess, onFail, args) { + try { + myApp.channel.removeEventListener("pushnotificationreceived", myApp.notificationEvent); + myApp.channel.close(); + onSuccess(); + } catch(ex) { + onFail(ex); + } + } +}; +require("cordova/exec/proxy").add("PushNotification", module.exports); |
