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