diff options
Diffstat (limited to 'mobile/android/base/java/org/mozilla/gecko/notifications/NotificationHelper.java')
-rw-r--r-- | mobile/android/base/java/org/mozilla/gecko/notifications/NotificationHelper.java | 366 |
1 files changed, 366 insertions, 0 deletions
diff --git a/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationHelper.java b/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationHelper.java new file mode 100644 index 000000000..1e33031b5 --- /dev/null +++ b/mobile/android/base/java/org/mozilla/gecko/notifications/NotificationHelper.java @@ -0,0 +1,366 @@ +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package org.mozilla.gecko.notifications; + +import java.util.HashMap; +import java.util.Iterator; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.mozilla.gecko.AppConstants; +import org.mozilla.gecko.EventDispatcher; +import org.mozilla.gecko.GeckoAppShell; +import org.mozilla.gecko.R; +import org.mozilla.gecko.gfx.BitmapUtils; +import org.mozilla.gecko.mozglue.SafeIntent; +import org.mozilla.gecko.util.GeckoEventListener; + +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.net.Uri; +import android.support.v4.app.NotificationCompat; +import android.util.Log; + +public final class NotificationHelper implements GeckoEventListener { + public static final String HELPER_BROADCAST_ACTION = AppConstants.ANDROID_PACKAGE_NAME + ".helperBroadcastAction"; + + public static final String NOTIFICATION_ID = "NotificationHelper_ID"; + private static final String LOGTAG = "GeckoNotificationHelper"; + private static final String HELPER_NOTIFICATION = "helperNotif"; + + // Attributes mandatory to be used while sending a notification from js. + private static final String TITLE_ATTR = "title"; + private static final String TEXT_ATTR = "text"; + /* package */ static final String ID_ATTR = "id"; + private static final String SMALLICON_ATTR = "smallIcon"; + + // Attributes that can be used while sending a notification from js. + private static final String PROGRESS_VALUE_ATTR = "progress_value"; + private static final String PROGRESS_MAX_ATTR = "progress_max"; + private static final String PROGRESS_INDETERMINATE_ATTR = "progress_indeterminate"; + private static final String LIGHT_ATTR = "light"; + private static final String ONGOING_ATTR = "ongoing"; + private static final String WHEN_ATTR = "when"; + private static final String PRIORITY_ATTR = "priority"; + private static final String LARGE_ICON_ATTR = "largeIcon"; + private static final String ACTIONS_ATTR = "actions"; + private static final String ACTION_ID_ATTR = "buttonId"; + private static final String ACTION_TITLE_ATTR = "title"; + private static final String ACTION_ICON_ATTR = "icon"; + private static final String PERSISTENT_ATTR = "persistent"; + private static final String HANDLER_ATTR = "handlerKey"; + private static final String COOKIE_ATTR = "cookie"; + static final String EVENT_TYPE_ATTR = "eventType"; + + private static final String NOTIFICATION_SCHEME = "moz-notification"; + + private static final String BUTTON_EVENT = "notification-button-clicked"; + private static final String CLICK_EVENT = "notification-clicked"; + static final String CLEARED_EVENT = "notification-cleared"; + + static final String ORIGINAL_EXTRA_COMPONENT = "originalComponent"; + + private final Context mContext; + + // Holds a list of notifications that should be cleared if the Fennec Activity is shut down. + // Will not include ongoing or persistent notifications that are tied to Gecko's lifecycle. + private HashMap<String, String> mClearableNotifications; + + private boolean mInitialized; + private static NotificationHelper sInstance; + + private NotificationHelper(Context context) { + mContext = context; + } + + public void init() { + if (mInitialized) { + return; + } + + mClearableNotifications = new HashMap<String, String>(); + EventDispatcher.getInstance().registerGeckoThreadListener(this, + "Notification:Show", + "Notification:Hide"); + mInitialized = true; + } + + public static NotificationHelper getInstance(Context context) { + // If someone else created this singleton, but didn't initialize it, something has gone wrong. + if (sInstance != null && !sInstance.mInitialized) { + throw new IllegalStateException("NotificationHelper was created by someone else but not initialized"); + } + + if (sInstance == null) { + sInstance = new NotificationHelper(context.getApplicationContext()); + } + return sInstance; + } + + @Override + public void handleMessage(String event, JSONObject message) { + if (event.equals("Notification:Show")) { + showNotification(message); + } else if (event.equals("Notification:Hide")) { + hideNotification(message); + } + } + + public boolean isHelperIntent(Intent i) { + return i.getBooleanExtra(HELPER_NOTIFICATION, false); + } + + public static void getArgsAndSendNotificationIntent(SafeIntent intent) { + final JSONObject args = new JSONObject(); + final Uri data = intent.getData(); + + final String notificationType = data.getQueryParameter(EVENT_TYPE_ATTR); + + try { + args.put(ID_ATTR, data.getQueryParameter(ID_ATTR)); + args.put(EVENT_TYPE_ATTR, notificationType); + args.put(HANDLER_ATTR, data.getQueryParameter(HANDLER_ATTR)); + args.put(COOKIE_ATTR, intent.getStringExtra(COOKIE_ATTR)); + + if (BUTTON_EVENT.equals(notificationType)) { + final String actionName = data.getQueryParameter(ACTION_ID_ATTR); + args.put(ACTION_ID_ATTR, actionName); + } + + Log.i(LOGTAG, "Send " + args.toString()); + GeckoAppShell.notifyObservers("Notification:Event", args.toString()); + } catch (JSONException e) { + Log.e(LOGTAG, "Error building JSON notification arguments.", e); + } + } + + public void handleNotificationIntent(SafeIntent i) { + final Uri data = i.getData(); + final String notificationType = data.getQueryParameter(EVENT_TYPE_ATTR); + final String id = data.getQueryParameter(ID_ATTR); + if (id == null || notificationType == null) { + Log.e(LOGTAG, "handleNotificationEvent: invalid intent parameters"); + return; + } + + getArgsAndSendNotificationIntent(i); + + // If the notification was clicked, we are closing it. This must be executed after + // sending the event to js side because when the notification is canceled no event can be + // handled. + if (CLICK_EVENT.equals(notificationType) && !i.getBooleanExtra(ONGOING_ATTR, false)) { + // The handler and cookie parameters are optional. + final String handler = data.getQueryParameter(HANDLER_ATTR); + final String cookie = i.getStringExtra(COOKIE_ATTR); + hideNotification(id, handler, cookie); + } + } + + private Uri.Builder getNotificationBuilder(JSONObject message, String type) { + Uri.Builder b = new Uri.Builder(); + b.scheme(NOTIFICATION_SCHEME).appendQueryParameter(EVENT_TYPE_ATTR, type); + + try { + final String id = message.getString(ID_ATTR); + b.appendQueryParameter(ID_ATTR, id); + } catch (JSONException ex) { + Log.i(LOGTAG, "buildNotificationPendingIntent, error parsing", ex); + } + + try { + final String id = message.getString(HANDLER_ATTR); + b.appendQueryParameter(HANDLER_ATTR, id); + } catch (JSONException ex) { + Log.i(LOGTAG, "Notification doesn't have a handler"); + } + + return b; + } + + private Intent buildNotificationIntent(JSONObject message, Uri.Builder builder) { + Intent notificationIntent = new Intent(HELPER_BROADCAST_ACTION); + final boolean ongoing = message.optBoolean(ONGOING_ATTR); + notificationIntent.putExtra(ONGOING_ATTR, ongoing); + + final Uri dataUri = builder.build(); + notificationIntent.setData(dataUri); + notificationIntent.putExtra(HELPER_NOTIFICATION, true); + notificationIntent.putExtra(COOKIE_ATTR, message.optString(COOKIE_ATTR)); + + // All intents get routed through the notificationReceiver. That lets us bail if we don't want to start Gecko + final ComponentName name = new ComponentName(mContext, GeckoAppShell.getGeckoInterface().getActivity().getClass()); + notificationIntent.putExtra(ORIGINAL_EXTRA_COMPONENT, name); + + return notificationIntent; + } + + private PendingIntent buildNotificationPendingIntent(JSONObject message, String type) { + Uri.Builder builder = getNotificationBuilder(message, type); + final Intent notificationIntent = buildNotificationIntent(message, builder); + return PendingIntent.getBroadcast(mContext, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + private PendingIntent buildButtonClickPendingIntent(JSONObject message, JSONObject action) { + Uri.Builder builder = getNotificationBuilder(message, BUTTON_EVENT); + try { + // Action name must be in query uri, otherwise buttons pending intents + // would be collapsed. + if (action.has(ACTION_ID_ATTR)) { + builder.appendQueryParameter(ACTION_ID_ATTR, action.getString(ACTION_ID_ATTR)); + } else { + Log.i(LOGTAG, "button event with no name"); + } + } catch (JSONException ex) { + Log.i(LOGTAG, "buildNotificationPendingIntent, error parsing", ex); + } + final Intent notificationIntent = buildNotificationIntent(message, builder); + PendingIntent res = PendingIntent.getBroadcast(mContext, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); + return res; + } + + private void showNotification(JSONObject message) { + NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext); + + // These attributes are required + final String id; + try { + builder.setContentTitle(message.getString(TITLE_ATTR)); + builder.setContentText(message.getString(TEXT_ATTR)); + id = message.getString(ID_ATTR); + } catch (JSONException ex) { + Log.i(LOGTAG, "Error parsing", ex); + return; + } + + Uri imageUri = Uri.parse(message.optString(SMALLICON_ATTR)); + builder.setSmallIcon(BitmapUtils.getResource(mContext, imageUri)); + + JSONArray light = message.optJSONArray(LIGHT_ATTR); + if (light != null && light.length() == 3) { + try { + builder.setLights(light.getInt(0), + light.getInt(1), + light.getInt(2)); + } catch (JSONException ex) { + Log.i(LOGTAG, "Error parsing", ex); + } + } + + boolean ongoing = message.optBoolean(ONGOING_ATTR); + builder.setOngoing(ongoing); + + if (message.has(WHEN_ATTR)) { + long when = message.optLong(WHEN_ATTR); + builder.setWhen(when); + } + + if (message.has(PRIORITY_ATTR)) { + int priority = message.optInt(PRIORITY_ATTR); + builder.setPriority(priority); + } + + if (message.has(LARGE_ICON_ATTR)) { + Bitmap b = BitmapUtils.getBitmapFromDataURI(message.optString(LARGE_ICON_ATTR)); + builder.setLargeIcon(b); + } + + if (message.has(PROGRESS_VALUE_ATTR) && + message.has(PROGRESS_MAX_ATTR) && + message.has(PROGRESS_INDETERMINATE_ATTR)) { + try { + final int progress = message.getInt(PROGRESS_VALUE_ATTR); + final int progressMax = message.getInt(PROGRESS_MAX_ATTR); + final boolean progressIndeterminate = message.getBoolean(PROGRESS_INDETERMINATE_ATTR); + builder.setProgress(progressMax, progress, progressIndeterminate); + } catch (JSONException ex) { + Log.i(LOGTAG, "Error parsing", ex); + } + } + + JSONArray actions = message.optJSONArray(ACTIONS_ATTR); + if (actions != null) { + try { + for (int i = 0; i < actions.length(); i++) { + JSONObject action = actions.getJSONObject(i); + final PendingIntent pending = buildButtonClickPendingIntent(message, action); + final String actionTitle = action.getString(ACTION_TITLE_ATTR); + final Uri actionImage = Uri.parse(action.optString(ACTION_ICON_ATTR)); + builder.addAction(BitmapUtils.getResource(mContext, actionImage), + actionTitle, + pending); + } + } catch (JSONException ex) { + Log.i(LOGTAG, "Error parsing", ex); + } + } + + PendingIntent pi = buildNotificationPendingIntent(message, CLICK_EVENT); + builder.setContentIntent(pi); + PendingIntent deletePendingIntent = buildNotificationPendingIntent(message, CLEARED_EVENT); + builder.setDeleteIntent(deletePendingIntent); + + ((NotificationClient) GeckoAppShell.getNotificationListener()).add(id, builder.build()); + + boolean persistent = message.optBoolean(PERSISTENT_ATTR); + // We add only not persistent notifications to the list since we want to purge only + // them when geckoapp is destroyed. + if (!persistent && !mClearableNotifications.containsKey(id)) { + mClearableNotifications.put(id, message.toString()); + } + } + + private void hideNotification(JSONObject message) { + final String id; + final String handler; + final String cookie; + try { + id = message.getString("id"); + handler = message.optString("handlerKey"); + cookie = message.optString("cookie"); + } catch (JSONException ex) { + Log.i(LOGTAG, "Error parsing", ex); + return; + } + + hideNotification(id, handler, cookie); + } + + private void closeNotification(String id, String handlerKey, String cookie) { + ((NotificationClient) GeckoAppShell.getNotificationListener()).remove(id); + } + + public void hideNotification(String id, String handlerKey, String cookie) { + mClearableNotifications.remove(id); + closeNotification(id, handlerKey, cookie); + } + + private void clearAll() { + for (Iterator<String> i = mClearableNotifications.keySet().iterator(); i.hasNext();) { + final String id = i.next(); + final String json = mClearableNotifications.get(id); + i.remove(); + + JSONObject obj; + try { + obj = new JSONObject(json); + } catch (JSONException ex) { + obj = new JSONObject(); + } + + closeNotification(id, obj.optString(HANDLER_ATTR), obj.optString(COOKIE_ATTR)); + } + } + + public static void destroy() { + if (sInstance != null) { + sInstance.clearAll(); + } + } +} |