mobile/android/base/NotificationHelper.java

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

michael@0 1 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
michael@0 2 * This Source Code Form is subject to the terms of the Mozilla Public
michael@0 3 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 5
michael@0 6 package org.mozilla.gecko;
michael@0 7
michael@0 8 import org.mozilla.gecko.gfx.BitmapUtils;
michael@0 9 import org.mozilla.gecko.util.GeckoEventListener;
michael@0 10
michael@0 11 import org.json.JSONArray;
michael@0 12 import org.json.JSONException;
michael@0 13 import org.json.JSONObject;
michael@0 14
michael@0 15 import android.app.NotificationManager;
michael@0 16 import android.app.PendingIntent;
michael@0 17 import android.content.BroadcastReceiver;
michael@0 18 import android.content.IntentFilter;
michael@0 19 import android.content.Context;
michael@0 20 import android.content.Intent;
michael@0 21 import android.graphics.Bitmap;
michael@0 22 import android.net.Uri;
michael@0 23 import android.support.v4.app.NotificationCompat;
michael@0 24 import android.util.Log;
michael@0 25
michael@0 26 import java.util.Iterator;
michael@0 27 import java.util.Set;
michael@0 28 import java.util.HashSet;
michael@0 29
michael@0 30 public final class NotificationHelper implements GeckoEventListener {
michael@0 31 public static final String NOTIFICATION_ID = "NotificationHelper_ID";
michael@0 32 private static final String LOGTAG = "GeckoNotificationManager";
michael@0 33 private static final String HELPER_NOTIFICATION = "helperNotif";
michael@0 34 private static final String HELPER_BROADCAST_ACTION = AppConstants.ANDROID_PACKAGE_NAME + ".helperBroadcastAction";
michael@0 35
michael@0 36 // Attributes mandatory to be used while sending a notification from js.
michael@0 37 private static final String TITLE_ATTR = "title";
michael@0 38 private static final String TEXT_ATTR = "text";
michael@0 39 private static final String ID_ATTR = "id";
michael@0 40 private static final String SMALLICON_ATTR = "smallIcon";
michael@0 41
michael@0 42 // Attributes that can be used while sending a notification from js.
michael@0 43 private static final String PROGRESS_VALUE_ATTR = "progress_value";
michael@0 44 private static final String PROGRESS_MAX_ATTR = "progress_max";
michael@0 45 private static final String PROGRESS_INDETERMINATE_ATTR = "progress_indeterminate";
michael@0 46 private static final String LIGHT_ATTR = "light";
michael@0 47 private static final String ONGOING_ATTR = "ongoing";
michael@0 48 private static final String WHEN_ATTR = "when";
michael@0 49 private static final String PRIORITY_ATTR = "priority";
michael@0 50 private static final String LARGE_ICON_ATTR = "largeIcon";
michael@0 51 private static final String EVENT_TYPE_ATTR = "eventType";
michael@0 52 private static final String ACTIONS_ATTR = "actions";
michael@0 53 private static final String ACTION_ID_ATTR = "buttonId";
michael@0 54 private static final String ACTION_TITLE_ATTR = "title";
michael@0 55 private static final String ACTION_ICON_ATTR = "icon";
michael@0 56 private static final String PERSISTENT_ATTR = "persistent";
michael@0 57
michael@0 58 private static final String NOTIFICATION_SCHEME = "moz-notification";
michael@0 59
michael@0 60 private static final String BUTTON_EVENT = "notification-button-clicked";
michael@0 61 private static final String CLICK_EVENT = "notification-clicked";
michael@0 62 private static final String CLEARED_EVENT = "notification-cleared";
michael@0 63 private static final String CLOSED_EVENT = "notification-closed";
michael@0 64
michael@0 65 private static Context mContext;
michael@0 66 private static Set<String> mClearableNotifications;
michael@0 67 private static BroadcastReceiver mReceiver;
michael@0 68 private static NotificationHelper mInstance;
michael@0 69
michael@0 70 private NotificationHelper() {
michael@0 71 }
michael@0 72
michael@0 73 public static void init(Context context) {
michael@0 74 if (mInstance != null) {
michael@0 75 Log.w(LOGTAG, "NotificationHelper.init() called twice!");
michael@0 76 return;
michael@0 77 }
michael@0 78 mInstance = new NotificationHelper();
michael@0 79 mContext = context;
michael@0 80 mClearableNotifications = new HashSet<String>();
michael@0 81 registerEventListener("Notification:Show");
michael@0 82 registerEventListener("Notification:Hide");
michael@0 83 registerReceiver(context);
michael@0 84 }
michael@0 85
michael@0 86 private static void registerEventListener(String event) {
michael@0 87 GeckoAppShell.getEventDispatcher().registerEventListener(event, mInstance);
michael@0 88 }
michael@0 89
michael@0 90 @Override
michael@0 91 public void handleMessage(String event, JSONObject message) {
michael@0 92 if (event.equals("Notification:Show")) {
michael@0 93 showNotification(message);
michael@0 94 } else if (event.equals("Notification:Hide")) {
michael@0 95 hideNotification(message);
michael@0 96 }
michael@0 97 }
michael@0 98
michael@0 99 public boolean isHelperIntent(Intent i) {
michael@0 100 return i.getBooleanExtra(HELPER_NOTIFICATION, false);
michael@0 101 }
michael@0 102
michael@0 103 private static void registerReceiver(Context context) {
michael@0 104 IntentFilter filter = new IntentFilter(HELPER_BROADCAST_ACTION);
michael@0 105 // Scheme is needed, otherwise only broadcast with no data will be catched.
michael@0 106 filter.addDataScheme(NOTIFICATION_SCHEME);
michael@0 107 mReceiver = new BroadcastReceiver() {
michael@0 108 @Override
michael@0 109 public void onReceive(Context context, Intent intent) {
michael@0 110 mInstance.handleNotificationIntent(intent);
michael@0 111 }
michael@0 112 };
michael@0 113 context.registerReceiver(mReceiver, filter);
michael@0 114 }
michael@0 115
michael@0 116
michael@0 117 private void handleNotificationIntent(Intent i) {
michael@0 118 final Uri data = i.getData();
michael@0 119 if (data == null) {
michael@0 120 Log.w(LOGTAG, "handleNotificationEvent: empty data");
michael@0 121 return;
michael@0 122 }
michael@0 123 final String id = data.getQueryParameter(ID_ATTR);
michael@0 124 final String notificationType = data.getQueryParameter(EVENT_TYPE_ATTR);
michael@0 125 if (id == null || notificationType == null) {
michael@0 126 Log.w(LOGTAG, "handleNotificationEvent: invalid intent parameters");
michael@0 127 return;
michael@0 128 }
michael@0 129
michael@0 130 // In case the user swiped out the notification, we empty the id
michael@0 131 // set.
michael@0 132 if (CLEARED_EVENT.equals(notificationType)) {
michael@0 133 mClearableNotifications.remove(id);
michael@0 134 }
michael@0 135
michael@0 136 if (GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) {
michael@0 137 JSONObject args = new JSONObject();
michael@0 138 try {
michael@0 139 args.put(ID_ATTR, id);
michael@0 140 args.put(EVENT_TYPE_ATTR, notificationType);
michael@0 141
michael@0 142 if (BUTTON_EVENT.equals(notificationType)) {
michael@0 143 final String actionName = data.getQueryParameter(ACTION_ID_ATTR);
michael@0 144 args.put(ACTION_ID_ATTR, actionName);
michael@0 145 }
michael@0 146
michael@0 147 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Notification:Event", args.toString()));
michael@0 148 } catch (JSONException e) {
michael@0 149 Log.w(LOGTAG, "Error building JSON notification arguments.", e);
michael@0 150 }
michael@0 151 }
michael@0 152 // If the notification was clicked, we are closing it. This must be executed after
michael@0 153 // sending the event to js side because when the notification is canceled no event can be
michael@0 154 // handled.
michael@0 155 if (CLICK_EVENT.equals(notificationType) && !i.getBooleanExtra(ONGOING_ATTR, false)) {
michael@0 156 hideNotification(id);
michael@0 157 }
michael@0 158
michael@0 159 }
michael@0 160
michael@0 161 private Uri.Builder getNotificationBuilder(JSONObject message, String type) {
michael@0 162 Uri.Builder b = new Uri.Builder();
michael@0 163 b.scheme(NOTIFICATION_SCHEME).appendQueryParameter(EVENT_TYPE_ATTR, type);
michael@0 164
michael@0 165 try {
michael@0 166 final String id = message.getString(ID_ATTR);
michael@0 167 b.appendQueryParameter(ID_ATTR, id);
michael@0 168 } catch (JSONException ex) {
michael@0 169 Log.i(LOGTAG, "buildNotificationPendingIntent, error parsing", ex);
michael@0 170 }
michael@0 171 return b;
michael@0 172 }
michael@0 173
michael@0 174 private Intent buildNotificationIntent(JSONObject message, Uri.Builder builder) {
michael@0 175 Intent notificationIntent = new Intent(HELPER_BROADCAST_ACTION);
michael@0 176 final boolean ongoing = message.optBoolean(ONGOING_ATTR);
michael@0 177 notificationIntent.putExtra(ONGOING_ATTR, ongoing);
michael@0 178
michael@0 179 final Uri dataUri = builder.build();
michael@0 180 notificationIntent.setData(dataUri);
michael@0 181 notificationIntent.putExtra(HELPER_NOTIFICATION, true);
michael@0 182 return notificationIntent;
michael@0 183 }
michael@0 184
michael@0 185 private PendingIntent buildNotificationPendingIntent(JSONObject message, String type) {
michael@0 186 Uri.Builder builder = getNotificationBuilder(message, type);
michael@0 187 final Intent notificationIntent = buildNotificationIntent(message, builder);
michael@0 188 PendingIntent pi = PendingIntent.getBroadcast(mContext, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
michael@0 189 return pi;
michael@0 190 }
michael@0 191
michael@0 192 private PendingIntent buildButtonClickPendingIntent(JSONObject message, JSONObject action) {
michael@0 193 Uri.Builder builder = getNotificationBuilder(message, BUTTON_EVENT);
michael@0 194 try {
michael@0 195 // Action name must be in query uri, otherwise buttons pending intents
michael@0 196 // would be collapsed.
michael@0 197 if(action.has(ACTION_ID_ATTR)) {
michael@0 198 builder.appendQueryParameter(ACTION_ID_ATTR, action.getString(ACTION_ID_ATTR));
michael@0 199 } else {
michael@0 200 Log.i(LOGTAG, "button event with no name");
michael@0 201 }
michael@0 202 } catch (JSONException ex) {
michael@0 203 Log.i(LOGTAG, "buildNotificationPendingIntent, error parsing", ex);
michael@0 204 }
michael@0 205 final Intent notificationIntent = buildNotificationIntent(message, builder);
michael@0 206 PendingIntent res = PendingIntent.getBroadcast(mContext, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT);
michael@0 207 return res;
michael@0 208 }
michael@0 209
michael@0 210 private void showNotification(JSONObject message) {
michael@0 211 NotificationCompat.Builder builder = new NotificationCompat.Builder(mContext);
michael@0 212
michael@0 213 // These attributes are required
michael@0 214 final String id;
michael@0 215 try {
michael@0 216 builder.setContentTitle(message.getString(TITLE_ATTR));
michael@0 217 builder.setContentText(message.getString(TEXT_ATTR));
michael@0 218 id = message.getString(ID_ATTR);
michael@0 219 } catch (JSONException ex) {
michael@0 220 Log.i(LOGTAG, "Error parsing", ex);
michael@0 221 return;
michael@0 222 }
michael@0 223
michael@0 224 Uri imageUri = Uri.parse(message.optString(SMALLICON_ATTR));
michael@0 225 builder.setSmallIcon(BitmapUtils.getResource(imageUri, R.drawable.ic_status_logo));
michael@0 226
michael@0 227 JSONArray light = message.optJSONArray(LIGHT_ATTR);
michael@0 228 if (light != null && light.length() == 3) {
michael@0 229 try {
michael@0 230 builder.setLights(light.getInt(0),
michael@0 231 light.getInt(1),
michael@0 232 light.getInt(2));
michael@0 233 } catch (JSONException ex) {
michael@0 234 Log.i(LOGTAG, "Error parsing", ex);
michael@0 235 }
michael@0 236 }
michael@0 237
michael@0 238 boolean ongoing = message.optBoolean(ONGOING_ATTR);
michael@0 239 builder.setOngoing(ongoing);
michael@0 240
michael@0 241 if (message.has(WHEN_ATTR)) {
michael@0 242 long when = message.optLong(WHEN_ATTR);
michael@0 243 builder.setWhen(when);
michael@0 244 }
michael@0 245
michael@0 246 if (message.has(PRIORITY_ATTR)) {
michael@0 247 int priority = message.optInt(PRIORITY_ATTR);
michael@0 248 builder.setPriority(priority);
michael@0 249 }
michael@0 250
michael@0 251 if (message.has(LARGE_ICON_ATTR)) {
michael@0 252 Bitmap b = BitmapUtils.getBitmapFromDataURI(message.optString(LARGE_ICON_ATTR));
michael@0 253 builder.setLargeIcon(b);
michael@0 254 }
michael@0 255
michael@0 256 if (message.has(PROGRESS_VALUE_ATTR) &&
michael@0 257 message.has(PROGRESS_MAX_ATTR) &&
michael@0 258 message.has(PROGRESS_INDETERMINATE_ATTR)) {
michael@0 259 try {
michael@0 260 final int progress = message.getInt(PROGRESS_VALUE_ATTR);
michael@0 261 final int progressMax = message.getInt(PROGRESS_MAX_ATTR);
michael@0 262 final boolean progressIndeterminate = message.getBoolean(PROGRESS_INDETERMINATE_ATTR);
michael@0 263 builder.setProgress(progressMax, progress, progressIndeterminate);
michael@0 264 } catch (JSONException ex) {
michael@0 265 Log.i(LOGTAG, "Error parsing", ex);
michael@0 266 }
michael@0 267 }
michael@0 268
michael@0 269 JSONArray actions = message.optJSONArray(ACTIONS_ATTR);
michael@0 270 if (actions != null) {
michael@0 271 try {
michael@0 272 for (int i = 0; i < actions.length(); i++) {
michael@0 273 JSONObject action = actions.getJSONObject(i);
michael@0 274 final PendingIntent pending = buildButtonClickPendingIntent(message, action);
michael@0 275 final String actionTitle = action.getString(ACTION_TITLE_ATTR);
michael@0 276 final Uri actionImage = Uri.parse(action.optString(ACTION_ICON_ATTR));
michael@0 277 builder.addAction(BitmapUtils.getResource(actionImage, R.drawable.ic_status_logo),
michael@0 278 actionTitle,
michael@0 279 pending);
michael@0 280 }
michael@0 281 } catch (JSONException ex) {
michael@0 282 Log.i(LOGTAG, "Error parsing", ex);
michael@0 283 }
michael@0 284 }
michael@0 285
michael@0 286 PendingIntent pi = buildNotificationPendingIntent(message, CLICK_EVENT);
michael@0 287 builder.setContentIntent(pi);
michael@0 288 PendingIntent deletePendingIntent = buildNotificationPendingIntent(message, CLEARED_EVENT);
michael@0 289 builder.setDeleteIntent(deletePendingIntent);
michael@0 290
michael@0 291 GeckoAppShell.notificationClient.add(id.hashCode(), builder.build());
michael@0 292
michael@0 293 boolean persistent = message.optBoolean(PERSISTENT_ATTR);
michael@0 294 // We add only not persistent notifications to the list since we want to purge only
michael@0 295 // them when geckoapp is destroyed.
michael@0 296 if (!persistent && !mClearableNotifications.contains(id)) {
michael@0 297 mClearableNotifications.add(id);
michael@0 298 }
michael@0 299 }
michael@0 300
michael@0 301 private void hideNotification(JSONObject message) {
michael@0 302 String id;
michael@0 303 try {
michael@0 304 id = message.getString("id");
michael@0 305 } catch (JSONException ex) {
michael@0 306 Log.i(LOGTAG, "Error parsing", ex);
michael@0 307 return;
michael@0 308 }
michael@0 309
michael@0 310 hideNotification(id);
michael@0 311 }
michael@0 312
michael@0 313 private void sendNotificationWasClosed(String id) {
michael@0 314 if (!GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) {
michael@0 315 return;
michael@0 316 }
michael@0 317 JSONObject args = new JSONObject();
michael@0 318 try {
michael@0 319 args.put(ID_ATTR, id);
michael@0 320 args.put(EVENT_TYPE_ATTR, CLOSED_EVENT);
michael@0 321 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Notification:Event", args.toString()));
michael@0 322 } catch (JSONException ex) {
michael@0 323 Log.w(LOGTAG, "sendNotificationWasClosed: error building JSON notification arguments.", ex);
michael@0 324 }
michael@0 325 }
michael@0 326
michael@0 327 private void closeNotification(String id) {
michael@0 328 GeckoAppShell.notificationClient.remove(id.hashCode());
michael@0 329 sendNotificationWasClosed(id);
michael@0 330 }
michael@0 331
michael@0 332 public void hideNotification(String id) {
michael@0 333 mClearableNotifications.remove(id);
michael@0 334 closeNotification(id);
michael@0 335 }
michael@0 336
michael@0 337 private void clearAll() {
michael@0 338 for (Iterator<String> i = mClearableNotifications.iterator(); i.hasNext();) {
michael@0 339 final String id = i.next();
michael@0 340 i.remove();
michael@0 341 closeNotification(id);
michael@0 342 }
michael@0 343 }
michael@0 344
michael@0 345 public static void destroy() {
michael@0 346 if (mInstance != null) {
michael@0 347 mInstance.clearAll();
michael@0 348 }
michael@0 349 }
michael@0 350 }

mercurial