1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/NotificationHelper.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,350 @@ 1.4 +/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*- 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +package org.mozilla.gecko; 1.10 + 1.11 +import org.mozilla.gecko.gfx.BitmapUtils; 1.12 +import org.mozilla.gecko.util.GeckoEventListener; 1.13 + 1.14 +import org.json.JSONArray; 1.15 +import org.json.JSONException; 1.16 +import org.json.JSONObject; 1.17 + 1.18 +import android.app.NotificationManager; 1.19 +import android.app.PendingIntent; 1.20 +import android.content.BroadcastReceiver; 1.21 +import android.content.IntentFilter; 1.22 +import android.content.Context; 1.23 +import android.content.Intent; 1.24 +import android.graphics.Bitmap; 1.25 +import android.net.Uri; 1.26 +import android.support.v4.app.NotificationCompat; 1.27 +import android.util.Log; 1.28 + 1.29 +import java.util.Iterator; 1.30 +import java.util.Set; 1.31 +import java.util.HashSet; 1.32 + 1.33 +public final class NotificationHelper implements GeckoEventListener { 1.34 + public static final String NOTIFICATION_ID = "NotificationHelper_ID"; 1.35 + private static final String LOGTAG = "GeckoNotificationManager"; 1.36 + private static final String HELPER_NOTIFICATION = "helperNotif"; 1.37 + private static final String HELPER_BROADCAST_ACTION = AppConstants.ANDROID_PACKAGE_NAME + ".helperBroadcastAction"; 1.38 + 1.39 + // Attributes mandatory to be used while sending a notification from js. 1.40 + private static final String TITLE_ATTR = "title"; 1.41 + private static final String TEXT_ATTR = "text"; 1.42 + private static final String ID_ATTR = "id"; 1.43 + private static final String SMALLICON_ATTR = "smallIcon"; 1.44 + 1.45 + // Attributes that can be used while sending a notification from js. 1.46 + private static final String PROGRESS_VALUE_ATTR = "progress_value"; 1.47 + private static final String PROGRESS_MAX_ATTR = "progress_max"; 1.48 + private static final String PROGRESS_INDETERMINATE_ATTR = "progress_indeterminate"; 1.49 + private static final String LIGHT_ATTR = "light"; 1.50 + private static final String ONGOING_ATTR = "ongoing"; 1.51 + private static final String WHEN_ATTR = "when"; 1.52 + private static final String PRIORITY_ATTR = "priority"; 1.53 + private static final String LARGE_ICON_ATTR = "largeIcon"; 1.54 + private static final String EVENT_TYPE_ATTR = "eventType"; 1.55 + private static final String ACTIONS_ATTR = "actions"; 1.56 + private static final String ACTION_ID_ATTR = "buttonId"; 1.57 + private static final String ACTION_TITLE_ATTR = "title"; 1.58 + private static final String ACTION_ICON_ATTR = "icon"; 1.59 + private static final String PERSISTENT_ATTR = "persistent"; 1.60 + 1.61 + private static final String NOTIFICATION_SCHEME = "moz-notification"; 1.62 + 1.63 + private static final String BUTTON_EVENT = "notification-button-clicked"; 1.64 + private static final String CLICK_EVENT = "notification-clicked"; 1.65 + private static final String CLEARED_EVENT = "notification-cleared"; 1.66 + private static final String CLOSED_EVENT = "notification-closed"; 1.67 + 1.68 + private static Context mContext; 1.69 + private static Set<String> mClearableNotifications; 1.70 + private static BroadcastReceiver mReceiver; 1.71 + private static NotificationHelper mInstance; 1.72 + 1.73 + private NotificationHelper() { 1.74 + } 1.75 + 1.76 + public static void init(Context context) { 1.77 + if (mInstance != null) { 1.78 + Log.w(LOGTAG, "NotificationHelper.init() called twice!"); 1.79 + return; 1.80 + } 1.81 + mInstance = new NotificationHelper(); 1.82 + mContext = context; 1.83 + mClearableNotifications = new HashSet<String>(); 1.84 + registerEventListener("Notification:Show"); 1.85 + registerEventListener("Notification:Hide"); 1.86 + registerReceiver(context); 1.87 + } 1.88 + 1.89 + private static void registerEventListener(String event) { 1.90 + GeckoAppShell.getEventDispatcher().registerEventListener(event, mInstance); 1.91 + } 1.92 + 1.93 + @Override 1.94 + public void handleMessage(String event, JSONObject message) { 1.95 + if (event.equals("Notification:Show")) { 1.96 + showNotification(message); 1.97 + } else if (event.equals("Notification:Hide")) { 1.98 + hideNotification(message); 1.99 + } 1.100 + } 1.101 + 1.102 + public boolean isHelperIntent(Intent i) { 1.103 + return i.getBooleanExtra(HELPER_NOTIFICATION, false); 1.104 + } 1.105 + 1.106 + private static void registerReceiver(Context context) { 1.107 + IntentFilter filter = new IntentFilter(HELPER_BROADCAST_ACTION); 1.108 + // Scheme is needed, otherwise only broadcast with no data will be catched. 1.109 + filter.addDataScheme(NOTIFICATION_SCHEME); 1.110 + mReceiver = new BroadcastReceiver() { 1.111 + @Override 1.112 + public void onReceive(Context context, Intent intent) { 1.113 + mInstance.handleNotificationIntent(intent); 1.114 + } 1.115 + }; 1.116 + context.registerReceiver(mReceiver, filter); 1.117 + } 1.118 + 1.119 + 1.120 + private void handleNotificationIntent(Intent i) { 1.121 + final Uri data = i.getData(); 1.122 + if (data == null) { 1.123 + Log.w(LOGTAG, "handleNotificationEvent: empty data"); 1.124 + return; 1.125 + } 1.126 + final String id = data.getQueryParameter(ID_ATTR); 1.127 + final String notificationType = data.getQueryParameter(EVENT_TYPE_ATTR); 1.128 + if (id == null || notificationType == null) { 1.129 + Log.w(LOGTAG, "handleNotificationEvent: invalid intent parameters"); 1.130 + return; 1.131 + } 1.132 + 1.133 + // In case the user swiped out the notification, we empty the id 1.134 + // set. 1.135 + if (CLEARED_EVENT.equals(notificationType)) { 1.136 + mClearableNotifications.remove(id); 1.137 + } 1.138 + 1.139 + if (GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) { 1.140 + JSONObject args = new JSONObject(); 1.141 + try { 1.142 + args.put(ID_ATTR, id); 1.143 + args.put(EVENT_TYPE_ATTR, notificationType); 1.144 + 1.145 + if (BUTTON_EVENT.equals(notificationType)) { 1.146 + final String actionName = data.getQueryParameter(ACTION_ID_ATTR); 1.147 + args.put(ACTION_ID_ATTR, actionName); 1.148 + } 1.149 + 1.150 + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Notification:Event", args.toString())); 1.151 + } catch (JSONException e) { 1.152 + Log.w(LOGTAG, "Error building JSON notification arguments.", e); 1.153 + } 1.154 + } 1.155 + // If the notification was clicked, we are closing it. This must be executed after 1.156 + // sending the event to js side because when the notification is canceled no event can be 1.157 + // handled. 1.158 + if (CLICK_EVENT.equals(notificationType) && !i.getBooleanExtra(ONGOING_ATTR, false)) { 1.159 + hideNotification(id); 1.160 + } 1.161 + 1.162 + } 1.163 + 1.164 + private Uri.Builder getNotificationBuilder(JSONObject message, String type) { 1.165 + Uri.Builder b = new Uri.Builder(); 1.166 + b.scheme(NOTIFICATION_SCHEME).appendQueryParameter(EVENT_TYPE_ATTR, type); 1.167 + 1.168 + try { 1.169 + final String id = message.getString(ID_ATTR); 1.170 + b.appendQueryParameter(ID_ATTR, id); 1.171 + } catch (JSONException ex) { 1.172 + Log.i(LOGTAG, "buildNotificationPendingIntent, error parsing", ex); 1.173 + } 1.174 + return b; 1.175 + } 1.176 + 1.177 + private Intent buildNotificationIntent(JSONObject message, Uri.Builder builder) { 1.178 + Intent notificationIntent = new Intent(HELPER_BROADCAST_ACTION); 1.179 + final boolean ongoing = message.optBoolean(ONGOING_ATTR); 1.180 + notificationIntent.putExtra(ONGOING_ATTR, ongoing); 1.181 + 1.182 + final Uri dataUri = builder.build(); 1.183 + notificationIntent.setData(dataUri); 1.184 + notificationIntent.putExtra(HELPER_NOTIFICATION, true); 1.185 + return notificationIntent; 1.186 + } 1.187 + 1.188 + private PendingIntent buildNotificationPendingIntent(JSONObject message, String type) { 1.189 + Uri.Builder builder = getNotificationBuilder(message, type); 1.190 + final Intent notificationIntent = buildNotificationIntent(message, builder); 1.191 + PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); 1.192 + return pi; 1.193 + } 1.194 + 1.195 + private PendingIntent buildButtonClickPendingIntent(JSONObject message, JSONObject action) { 1.196 + Uri.Builder builder = getNotificationBuilder(message, BUTTON_EVENT); 1.197 + try { 1.198 + // Action name must be in query uri, otherwise buttons pending intents 1.199 + // would be collapsed. 1.200 + if(action.has(ACTION_ID_ATTR)) { 1.201 + builder.appendQueryParameter(ACTION_ID_ATTR, action.getString(ACTION_ID_ATTR)); 1.202 + } else { 1.203 + Log.i(LOGTAG, "button event with no name"); 1.204 + } 1.205 + } catch (JSONException ex) { 1.206 + Log.i(LOGTAG, "buildNotificationPendingIntent, error parsing", ex); 1.207 + } 1.208 + final Intent notificationIntent = buildNotificationIntent(message, builder); 1.209 + PendingIntent res = PendingIntent.getBroadcast(mContext, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT); 1.210 + return res; 1.211 + } 1.212 + 1.213 + private void showNotification(JSONObject message) { 1.214 + NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext); 1.215 + 1.216 + // These attributes are required 1.217 + final String id; 1.218 + try { 1.219 + builder.setContentTitle(message.getString(TITLE_ATTR)); 1.220 + builder.setContentText(message.getString(TEXT_ATTR)); 1.221 + id = message.getString(ID_ATTR); 1.222 + } catch (JSONException ex) { 1.223 + Log.i(LOGTAG, "Error parsing", ex); 1.224 + return; 1.225 + } 1.226 + 1.227 + Uri imageUri = Uri.parse(message.optString(SMALLICON_ATTR)); 1.228 + builder.setSmallIcon(BitmapUtils.getResource(imageUri, R.drawable.ic_status_logo)); 1.229 + 1.230 + JSONArray light = message.optJSONArray(LIGHT_ATTR); 1.231 + if (light != null && light.length() == 3) { 1.232 + try { 1.233 + builder.setLights(light.getInt(0), 1.234 + light.getInt(1), 1.235 + light.getInt(2)); 1.236 + } catch (JSONException ex) { 1.237 + Log.i(LOGTAG, "Error parsing", ex); 1.238 + } 1.239 + } 1.240 + 1.241 + boolean ongoing = message.optBoolean(ONGOING_ATTR); 1.242 + builder.setOngoing(ongoing); 1.243 + 1.244 + if (message.has(WHEN_ATTR)) { 1.245 + long when = message.optLong(WHEN_ATTR); 1.246 + builder.setWhen(when); 1.247 + } 1.248 + 1.249 + if (message.has(PRIORITY_ATTR)) { 1.250 + int priority = message.optInt(PRIORITY_ATTR); 1.251 + builder.setPriority(priority); 1.252 + } 1.253 + 1.254 + if (message.has(LARGE_ICON_ATTR)) { 1.255 + Bitmap b = BitmapUtils.getBitmapFromDataURI(message.optString(LARGE_ICON_ATTR)); 1.256 + builder.setLargeIcon(b); 1.257 + } 1.258 + 1.259 + if (message.has(PROGRESS_VALUE_ATTR) && 1.260 + message.has(PROGRESS_MAX_ATTR) && 1.261 + message.has(PROGRESS_INDETERMINATE_ATTR)) { 1.262 + try { 1.263 + final int progress = message.getInt(PROGRESS_VALUE_ATTR); 1.264 + final int progressMax = message.getInt(PROGRESS_MAX_ATTR); 1.265 + final boolean progressIndeterminate = message.getBoolean(PROGRESS_INDETERMINATE_ATTR); 1.266 + builder.setProgress(progressMax, progress, progressIndeterminate); 1.267 + } catch (JSONException ex) { 1.268 + Log.i(LOGTAG, "Error parsing", ex); 1.269 + } 1.270 + } 1.271 + 1.272 + JSONArray actions = message.optJSONArray(ACTIONS_ATTR); 1.273 + if (actions != null) { 1.274 + try { 1.275 + for (int i = 0; i < actions.length(); i++) { 1.276 + JSONObject action = actions.getJSONObject(i); 1.277 + final PendingIntent pending = buildButtonClickPendingIntent(message, action); 1.278 + final String actionTitle = action.getString(ACTION_TITLE_ATTR); 1.279 + final Uri actionImage = Uri.parse(action.optString(ACTION_ICON_ATTR)); 1.280 + builder.addAction(BitmapUtils.getResource(actionImage, R.drawable.ic_status_logo), 1.281 + actionTitle, 1.282 + pending); 1.283 + } 1.284 + } catch (JSONException ex) { 1.285 + Log.i(LOGTAG, "Error parsing", ex); 1.286 + } 1.287 + } 1.288 + 1.289 + PendingIntent pi = buildNotificationPendingIntent(message, CLICK_EVENT); 1.290 + builder.setContentIntent(pi); 1.291 + PendingIntent deletePendingIntent = buildNotificationPendingIntent(message, CLEARED_EVENT); 1.292 + builder.setDeleteIntent(deletePendingIntent); 1.293 + 1.294 + GeckoAppShell.notificationClient.add(id.hashCode(), builder.build()); 1.295 + 1.296 + boolean persistent = message.optBoolean(PERSISTENT_ATTR); 1.297 + // We add only not persistent notifications to the list since we want to purge only 1.298 + // them when geckoapp is destroyed. 1.299 + if (!persistent && !mClearableNotifications.contains(id)) { 1.300 + mClearableNotifications.add(id); 1.301 + } 1.302 + } 1.303 + 1.304 + private void hideNotification(JSONObject message) { 1.305 + String id; 1.306 + try { 1.307 + id = message.getString("id"); 1.308 + } catch (JSONException ex) { 1.309 + Log.i(LOGTAG, "Error parsing", ex); 1.310 + return; 1.311 + } 1.312 + 1.313 + hideNotification(id); 1.314 + } 1.315 + 1.316 + private void sendNotificationWasClosed(String id) { 1.317 + if (!GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) { 1.318 + return; 1.319 + } 1.320 + JSONObject args = new JSONObject(); 1.321 + try { 1.322 + args.put(ID_ATTR, id); 1.323 + args.put(EVENT_TYPE_ATTR, CLOSED_EVENT); 1.324 + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Notification:Event", args.toString())); 1.325 + } catch (JSONException ex) { 1.326 + Log.w(LOGTAG, "sendNotificationWasClosed: error building JSON notification arguments.", ex); 1.327 + } 1.328 + } 1.329 + 1.330 + private void closeNotification(String id) { 1.331 + GeckoAppShell.notificationClient.remove(id.hashCode()); 1.332 + sendNotificationWasClosed(id); 1.333 + } 1.334 + 1.335 + public void hideNotification(String id) { 1.336 + mClearableNotifications.remove(id); 1.337 + closeNotification(id); 1.338 + } 1.339 + 1.340 + private void clearAll() { 1.341 + for (Iterator<String> i = mClearableNotifications.iterator(); i.hasNext();) { 1.342 + final String id = i.next(); 1.343 + i.remove(); 1.344 + closeNotification(id); 1.345 + } 1.346 + } 1.347 + 1.348 + public static void destroy() { 1.349 + if (mInstance != null) { 1.350 + mInstance.clearAll(); 1.351 + } 1.352 + } 1.353 +}