OLD | NEW |
---|---|
1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 'use strict'; | 5 'use strict'; |
6 | 6 |
7 /** | 7 /** |
8 * @fileoverview The event page for Google Now for Chrome implementation. | 8 * @fileoverview The event page for Google Now for Chrome implementation. |
9 * The Google Now event page gets Google Now cards from the server and shows | 9 * The Google Now event page gets Google Now cards from the server and shows |
10 * them as Chrome notifications. | 10 * them as Chrome notifications. |
11 * The service performs periodic updating of Google Now cards. | 11 * The service performs periodic updating of Google Now cards. |
12 * Each updating of the cards includes 3 steps: | 12 * Each updating of the cards includes 3 steps: |
13 * 1. Obtaining the location of the machine; | 13 * 1. Obtaining the location of the machine; |
14 * 2. Making a server request based on that location; | 14 * 2. Making a server request based on that location; |
15 * 3. Showing the received cards as notifications. | 15 * 3. Showing the received cards as notifications. |
16 */ | 16 */ |
17 | 17 |
18 // TODO(vadimt): Use background permission to show notifications even when all | 18 // TODO(vadimt): Use background permission to show notifications even when all |
19 // browser windows are closed. | 19 // browser windows are closed. |
20 // TODO(vadimt): Remove the C++ implementation. | 20 // TODO(vadimt): Remove the C++ implementation. |
21 // TODO(vadimt): Decide what to do in incognito mode. | 21 // TODO(vadimt): Decide what to do in incognito mode. |
22 // TODO(vadimt): Gather UMAs. | 22 // TODO(vadimt): Gather UMAs. |
23 // TODO(vadimt): Honor the flag the enables Google Now integration. | 23 // TODO(vadimt): Honor the flag the enables Google Now integration. |
24 // TODO(vadimt): Figure out the final values of the constants. | 24 // TODO(vadimt): Figure out the final values of the constants. |
25 // TODO(vadimt): Report internal and server errors. Collect UMAs on errors where | 25 // TODO(vadimt): Report internal and server errors. Collect UMAs on errors where |
26 // appropriate. Also consider logging errors or throwing exceptions. | 26 // appropriate. Also consider logging errors or throwing exceptions. |
27 | |
28 // TODO(vadimt): Consider processing errors for all storage.set calls. | 27 // TODO(vadimt): Consider processing errors for all storage.set calls. |
28 | |
29 // TODO(vadimt): Figure out the server name. Use it in the manifest and for | 29 // TODO(vadimt): Figure out the server name. Use it in the manifest and for |
30 // NOTIFICATION_CARDS_URL. Meanwhile, to use the feature, you need to manually | 30 // NOTIFICATION_CARDS_URL. Meanwhile, to use the feature, you need to manually |
31 // set the server name via local storage. | 31 // set the server name via local storage. |
32 /** | 32 /** |
33 * URL to retrieve notification cards. | 33 * URL to retrieve notification cards. |
34 */ | 34 */ |
35 var NOTIFICATION_CARDS_URL = localStorage['server_url']; | 35 var NOTIFICATION_CARDS_URL = localStorage['server_url']; |
36 | 36 |
37 /** | 37 /** |
38 * Standard response code for successful HTTP requests. This is the only success | 38 * Standard response code for successful HTTP requests. This is the only success |
39 * code the server will send. | 39 * code the server will send. |
40 */ | 40 */ |
41 var HTTP_OK = 200; | 41 var HTTP_OK = 200; |
42 | 42 |
43 /** | 43 /** |
44 * Initial period for polling for Google Now Notifications cards to use when the | 44 * Initial period for polling for Google Now Notifications cards to use when the |
45 * period from the server is not available. | 45 * period from the server is not available. |
46 */ | 46 */ |
47 var INITIAL_POLLING_PERIOD_SECONDS = 300; // 5 minutes | 47 var INITIAL_POLLING_PERIOD_SECONDS = 300; // 5 minutes |
48 | 48 |
49 /** | 49 /** |
50 * Maximal period for polling for Google Now Notifications cards to use when the | 50 * Maximal period for polling for Google Now Notifications cards to use when the |
51 * period from the server is not available. | 51 * period from the server is not available. |
52 */ | 52 */ |
53 var MAXIMUM_POLLING_PERIOD_SECONDS = 3600; // 1 hour | 53 var MAXIMUM_POLLING_PERIOD_SECONDS = 3600; // 1 hour |
54 | 54 |
55 var UPDATE_NOTIFICATIONS_ALARM_NAME = 'UPDATE'; | |
56 | |
55 var storage = chrome.storage.local; | 57 var storage = chrome.storage.local; |
56 | 58 |
57 /** | 59 /** |
58 * Show a notification and remember information associated with it. | 60 * Names for tasks that can be created by the extension. |
61 */ | |
62 var UPDATE_CARDS_TASK_NAME = 'update-cards'; | |
rgustafson
2013/03/14 19:05:20
Why did TaskName go away?
Why are these and the t
vadimt
2013/03/14 22:56:05
http://en.wikipedia.org/wiki/Layer_(object-oriente
| |
63 var DISMISS_CARD_TASK_NAME = 'dismiss-card'; | |
64 var CARD_CLICKED_TASK_NAME = 'card-clicked'; | |
65 | |
66 /** | |
67 * Checks if a new task can't be added to a task queue that contains an | |
68 * existing task. | |
69 * @param {string} newTaskName Name of the new task. | |
70 * @param {string} queuedTaskName Name of the task in the queue. | |
71 * @return {boolean} Whether the new task conflicts with the existing task. | |
72 */ | |
73 function areTasksConflicting(newTaskName, queuedTaskName) { | |
74 if (newTaskName == UPDATE_CARDS_TASK_NAME && | |
75 queuedTaskName == UPDATE_CARDS_TASK_NAME) { | |
76 // If a card update is requested while an old update is still in the | |
77 // queue, we don't need the new update. | |
78 return true; | |
79 } | |
80 | |
rgustafson
2013/03/14 19:05:20
extra line
vadimt
2013/03/14 22:56:05
It makes sense to separate groups of operators wit
| |
81 return false; | |
82 } | |
83 | |
84 var tasks = TaskManager(areTasksConflicting); | |
85 | |
86 /** | |
87 * Shows a notification and remembers information associated with it. | |
59 * @param {Object} card Google Now card represented as a set of parameters for | 88 * @param {Object} card Google Now card represented as a set of parameters for |
60 * showing a Chrome notification. | 89 * showing a Chrome notification. |
61 * @param {Object} notificationsUrlInfo Map from notification id to the | 90 * @param {Object} notificationsUrlInfo Map from notification id to the |
62 * notification's set of URLs. | 91 * notification's set of URLs. |
63 */ | 92 */ |
64 function createNotification(card, notificationsUrlInfo) { | 93 function createNotification(card, notificationsUrlInfo) { |
65 // Create a notification or quietly update if it already exists. | 94 // Create a notification or quietly update if it already exists. |
66 // TODO(vadimt): Implement non-quiet updates. | 95 // TODO(vadimt): Implement non-quiet updates. |
67 chrome.notifications.create( | 96 chrome.notifications.create( |
68 card.notificationId, | 97 card.notificationId, |
69 card.notification, | 98 card.notification, |
70 function(assignedNotificationId) {}); | 99 function() {}); |
71 | 100 |
72 notificationsUrlInfo[card.notificationId] = card.actionUrls; | 101 notificationsUrlInfo[card.notificationId] = card.actionUrls; |
73 } | 102 } |
74 | 103 |
75 /** | 104 /** |
76 * Parse JSON response from the notification server, show notifications and | 105 * Parses JSON response from the notification server, show notifications and |
77 * schedule next update. | 106 * schedule next update. |
78 * @param {string} response Server response. | 107 * @param {string} response Server response. |
79 */ | 108 */ |
80 function parseAndShowNotificationCards(response) { | 109 function parseAndShowNotificationCards(response) { |
81 try { | 110 try { |
82 var parsedResponse = JSON.parse(response); | 111 var parsedResponse = JSON.parse(response); |
83 } catch (error) { | 112 } catch (error) { |
84 // TODO(vadimt): Report errors to the user. | 113 // TODO(vadimt): Report errors to the user. |
85 return; | 114 return; |
86 } | 115 } |
87 | 116 |
88 var cards = parsedResponse.cards; | 117 var cards = parsedResponse.cards; |
89 | 118 |
90 if (!(cards instanceof Array)) { | 119 if (!(cards instanceof Array)) { |
91 // TODO(vadimt): Report errors to the user. | 120 // TODO(vadimt): Report errors to the user. |
92 return; | 121 return; |
93 } | 122 } |
94 | 123 |
95 if (typeof parsedResponse.expiration_timestamp_seconds != 'number') { | 124 if (typeof parsedResponse.expiration_timestamp_seconds != 'number') { |
96 // TODO(vadimt): Report errors to the user. | 125 // TODO(vadimt): Report errors to the user. |
97 return; | 126 return; |
98 } | 127 } |
99 | 128 |
129 tasks.debugSetStepName( | |
130 'parseAndShowNotificationCards-get-active-notifications'); | |
100 storage.get('activeNotifications', function(items) { | 131 storage.get('activeNotifications', function(items) { |
101 // Mark existing notifications that received an update in this server | 132 // Mark existing notifications that received an update in this server |
102 // response. | 133 // response. |
103 for (var i = 0; i < cards.length; ++i) { | 134 for (var i = 0; i < cards.length; ++i) { |
104 var notificationId = cards[i].notificationId; | 135 var notificationId = cards[i].notificationId; |
105 if (notificationId in items.activeNotifications) | 136 if (notificationId in items.activeNotifications) |
106 items.activeNotifications[notificationId].hasUpdate = true; | 137 items.activeNotifications[notificationId].hasUpdate = true; |
107 } | 138 } |
108 | 139 |
109 // Delete notifications that didn't receive an update. | 140 // Delete notifications that didn't receive an update. |
110 for (var notificationId in items.activeNotifications) | 141 for (var notificationId in items.activeNotifications) |
111 if (!items.activeNotifications[notificationId].hasUpdate) { | 142 if (!items.activeNotifications[notificationId].hasUpdate) { |
112 chrome.notifications.clear( | 143 chrome.notifications.clear( |
113 notificationId, | 144 notificationId, |
114 function(wasDeleted) {}); | 145 function() {}); |
115 } | 146 } |
116 | 147 |
117 // Create/update notifications and store their new properties. | 148 // Create/update notifications and store their new properties. |
118 var notificationsUrlInfo = {}; | 149 var notificationsUrlInfo = {}; |
119 | 150 |
120 for (var i = 0; i < cards.length; ++i) { | 151 for (var i = 0; i < cards.length; ++i) { |
121 try { | 152 try { |
122 createNotification(cards[i], notificationsUrlInfo); | 153 createNotification(cards[i], notificationsUrlInfo); |
123 } catch (error) { | 154 } catch (error) { |
124 // TODO(vadimt): Report errors to the user. | 155 // TODO(vadimt): Report errors to the user. |
125 } | 156 } |
126 } | 157 } |
127 storage.set({activeNotifications: notificationsUrlInfo}); | 158 storage.set({activeNotifications: notificationsUrlInfo}); |
128 | 159 |
129 scheduleNextUpdate(parsedResponse.expiration_timestamp_seconds); | 160 scheduleNextUpdate(parsedResponse.expiration_timestamp_seconds); |
130 | 161 |
131 // Now that we got a valid response from the server, reset the retry period | 162 // Now that we got a valid response from the server, reset the retry period |
132 // to the initial value. This retry period will be used the next time we | 163 // to the initial value. This retry period will be used the next time we |
133 // fail to get the server-provided period. | 164 // fail to get the server-provided period. |
134 storage.set({retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS}); | 165 storage.set({retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS}); |
166 tasks.finish(); | |
135 }); | 167 }); |
136 } | 168 } |
137 | 169 |
138 /** | 170 /** |
139 * Request notification cards from the server. | 171 * Requests notification cards from the server. |
140 * @param {string} requestParameters Query string for the request. | 172 * @param {string} requestParameters Query string for the request. |
141 */ | 173 */ |
142 function requestNotificationCards(requestParameters) { | 174 function requestNotificationCards(requestParameters) { |
143 // TODO(vadimt): Figure out how to send user's identity to the server. | 175 // TODO(vadimt): Figure out how to send user's identity to the server. |
144 var request = new XMLHttpRequest(); | 176 var request = new XMLHttpRequest(); |
145 | 177 |
146 request.responseType = 'text'; | 178 request.responseType = 'text'; |
147 request.onload = function(event) { | 179 request.onloadend = function() { |
148 if (request.status == HTTP_OK) | 180 if (request.status == HTTP_OK) |
149 parseAndShowNotificationCards(request.response); | 181 parseAndShowNotificationCards(request.response); |
182 else | |
183 tasks.finish(); | |
150 } | 184 } |
151 | 185 |
152 request.open( | 186 request.open( |
153 'GET', | 187 'GET', |
154 NOTIFICATION_CARDS_URL + '/notifications' + requestParameters, | 188 NOTIFICATION_CARDS_URL + '/notifications' + requestParameters, |
155 true); | 189 true); |
190 tasks.debugSetStepName('requestNotificationCards-send-request'); | |
156 request.send(); | 191 request.send(); |
157 } | 192 } |
158 | 193 |
159 /** | 194 /** |
160 * Request notification cards from the server when we have geolocation. | 195 * Requests notification cards from the server when we have geolocation. |
161 * @param {Geoposition} position Location of this computer. | 196 * @param {Geoposition} position Location of this computer. |
162 */ | 197 */ |
163 function requestNotificationCardsWithLocation(position) { | 198 function requestNotificationCardsWithLocation(position) { |
164 // TODO(vadimt): Should we use 'q' as the parameter name? | 199 // TODO(vadimt): Should we use 'q' as the parameter name? |
165 var requestParameters = | 200 var requestParameters = |
166 '?q=' + position.coords.latitude + | 201 '?q=' + position.coords.latitude + |
167 ',' + position.coords.longitude + | 202 ',' + position.coords.longitude + |
168 ',' + position.coords.accuracy; | 203 ',' + position.coords.accuracy; |
169 | 204 |
170 requestNotificationCards(requestParameters); | 205 requestNotificationCards(requestParameters); |
171 } | 206 } |
172 | 207 |
173 /** | 208 /** |
174 * Request notification cards from the server when we don't have geolocation. | 209 * Requests notification cards from the server when we don't have geolocation. |
175 * @param {PositionError} positionError Position error. | 210 * @param {PositionError} positionError Position error. |
176 */ | 211 */ |
177 function requestNotificationCardsWithoutLocation(positionError) { | 212 function requestNotificationCardsWithoutLocation(positionError) { |
178 requestNotificationCards(''); | 213 requestNotificationCards(''); |
179 } | 214 } |
180 | 215 |
181 /** | 216 /** |
182 * Obtain new location; request and show notification cards based on this | 217 * Obtains new location; requests and shows notification cards based on this |
183 * location. | 218 * location. |
184 */ | 219 */ |
185 function updateNotificationsCards() { | 220 function updateNotificationsCards() { |
186 storage.get('retryDelaySeconds', function(items) { | 221 tasks.submit(UPDATE_CARDS_TASK_NAME, function() { |
187 // Immediately schedule the update after the current retry period. Then, | 222 tasks.debugSetStepName('updateNotificationsCards-get-retryDelaySeconds'); |
188 // we'll use update time from the server if available. | 223 storage.get('retryDelaySeconds', function(items) { |
189 scheduleNextUpdate(items.retryDelaySeconds); | 224 // Immediately schedule the update after the current retry period. Then, |
190 | 225 // we'll use update time from the server if available. |
191 // TODO(vadimt): Consider interrupting waiting for the next update if we | 226 scheduleNextUpdate(items.retryDelaySeconds); |
192 // detect that the network conditions have changed. Also, decide whether the | 227 |
193 // exponential backoff is needed both when we are offline and when there are | 228 // TODO(vadimt): Consider interrupting waiting for the next update if we |
194 // failures on the server side. | 229 // detect that the network conditions have changed. Also, decide whether |
195 var newRetryDelaySeconds = | 230 // the exponential backoff is needed both when we are offline and when |
196 Math.min(items.retryDelaySeconds * 2 * (1 + 0.2 * Math.random()), | 231 // there are failures on the server side. |
197 MAXIMUM_POLLING_PERIOD_SECONDS); | 232 var newRetryDelaySeconds = |
198 storage.set({retryDelaySeconds: newRetryDelaySeconds}); | 233 Math.min(items.retryDelaySeconds * 2 * (1 + 0.2 * Math.random()), |
199 | 234 MAXIMUM_POLLING_PERIOD_SECONDS); |
200 navigator.geolocation.getCurrentPosition( | 235 storage.set({retryDelaySeconds: newRetryDelaySeconds}); |
201 requestNotificationCardsWithLocation, | 236 |
202 requestNotificationCardsWithoutLocation); | 237 tasks.debugSetStepName('updateNotificationsCards-get-location'); |
238 navigator.geolocation.getCurrentPosition( | |
239 requestNotificationCardsWithLocation, | |
240 requestNotificationCardsWithoutLocation); | |
241 }); | |
203 }); | 242 }); |
204 } | 243 } |
205 | 244 |
206 /** | 245 /** |
207 * Opens URL corresponding to the clicked part of the notification. | 246 * Opens URL corresponding to the clicked part of the notification. |
208 * @param {string} notificationId Unique identifier of the notification. | 247 * @param {string} notificationId Unique identifier of the notification. |
209 * @param {function(Object): string} selector Function that extracts the url for | 248 * @param {function(Object): string} selector Function that extracts the url for |
210 * the clicked area from the button action URLs info. | 249 * the clicked area from the button action URLs info. |
211 */ | 250 */ |
212 function onNotificationClicked(notificationId, selector) { | 251 function onNotificationClicked(notificationId, selector) { |
213 storage.get('activeNotifications', function(items) { | 252 tasks.submit(CARD_CLICKED_TASK_NAME, function() { |
214 var actionUrls = items.activeNotifications[notificationId]; | 253 tasks.debugSetStepName('onNotificationClicked-get-activeNotifications'); |
215 if (typeof actionUrls != 'object') { | 254 storage.get('activeNotifications', function(items) { |
216 // TODO(vadimt): report an error. | 255 var actionUrls = items.activeNotifications[notificationId]; |
217 return; | 256 if (typeof actionUrls != 'object') { |
218 } | 257 // TODO(vadimt): report an error. |
219 | 258 tasks.finish(); |
220 var url = selector(actionUrls); | 259 return; |
221 | 260 } |
222 if (typeof url != 'string') { | 261 |
223 // TODO(vadimt): report an error. | 262 var url = selector(actionUrls); |
224 return; | 263 |
225 } | 264 if (typeof url != 'string') { |
226 | 265 // TODO(vadimt): report an error. |
227 chrome.tabs.create({url: url}); | 266 tasks.finish(); |
267 return; | |
268 } | |
269 | |
270 chrome.tabs.create({url: url}); | |
271 tasks.finish(); | |
272 }); | |
228 }); | 273 }); |
229 } | 274 } |
230 | 275 |
231 /** | 276 /** |
232 * Callback for chrome.notifications.onClosed event. | 277 * Callback for chrome.notifications.onClosed event. |
233 * @param {string} notificationId Unique identifier of the notification. | 278 * @param {string} notificationId Unique identifier of the notification. |
234 * @param {boolean} byUser Whether the notification was closed by the user. | 279 * @param {boolean} byUser Whether the notification was closed by the user. |
235 */ | 280 */ |
236 function onNotificationClosed(notificationId, byUser) { | 281 function onNotificationClosed(notificationId, byUser) { |
237 if (byUser) { | 282 if (byUser) { |
238 // TODO(vadimt): Analyze possible race conditions between request for cards | 283 tasks.submit(DISMISS_CARD_TASK_NAME, function() { |
239 // and dismissal. | 284 // Deleting the notification in case it was re-added while this task was |
240 // Send a dismiss request to the server. | 285 // waiting in the queue. |
241 var requestParameters = '?id=' + notificationId; | 286 chrome.notifications.clear( |
242 var request = new XMLHttpRequest(); | 287 notificationId, |
243 request.responseType = 'text'; | 288 function() {}); |
244 // TODO(vadimt): If the request fails, for example, because there is no | 289 |
245 // internet connection, do retry with exponential backoff. | 290 // Send a dismiss request to the server. |
246 request.open( | 291 var requestParameters = '?id=' + notificationId; |
247 'GET', | 292 var request = new XMLHttpRequest(); |
248 NOTIFICATION_CARDS_URL + '/dismiss' + requestParameters, | 293 request.responseType = 'text'; |
249 true); | 294 request.onloadend = function() { |
250 request.send(); | 295 tasks.finish(); |
251 } | 296 } |
252 } | 297 // TODO(vadimt): If the request fails, for example, because there is no |
253 | 298 // internet connection, do retry with exponential backoff. |
254 /** | 299 request.open( |
255 * Schedule next update for notification cards. | 300 'GET', |
301 NOTIFICATION_CARDS_URL + '/dismiss' + requestParameters, | |
302 true); | |
303 tasks.debugSetStepName('onNotificationClosed-send-request'); | |
304 request.send(); | |
305 }); | |
306 } | |
307 } | |
308 | |
309 /** | |
310 * Schedules next update for notification cards. | |
256 * @param {int} delaySeconds Length of time in seconds after which the alarm | 311 * @param {int} delaySeconds Length of time in seconds after which the alarm |
257 * event should fire. | 312 * event should fire. |
258 */ | 313 */ |
259 function scheduleNextUpdate(delaySeconds) { | 314 function scheduleNextUpdate(delaySeconds) { |
260 // Schedule an alarm after the specified delay. 'periodInMinutes' is for the | 315 // Schedule an alarm after the specified delay. 'periodInMinutes' is for the |
261 // case when we fail to re-register the alarm. | 316 // case when we fail to re-register the alarm. |
262 chrome.alarms.create({ | 317 var alarmInfo = { |
263 delayInMinutes: delaySeconds / 60, | 318 delayInMinutes: delaySeconds / 60, |
264 periodInMinutes: MAXIMUM_POLLING_PERIOD_SECONDS / 60 | 319 periodInMinutes: MAXIMUM_POLLING_PERIOD_SECONDS / 60 |
265 }); | 320 }; |
266 } | 321 |
267 | 322 chrome.alarms.create(UPDATE_NOTIFICATIONS_ALARM_NAME, alarmInfo); |
268 /** | 323 } |
269 * Initialize the event page on install or on browser startup. | 324 |
325 /** | |
326 * Initializes the event page on install or on browser startup. | |
270 */ | 327 */ |
271 function initialize() { | 328 function initialize() { |
272 var initialStorage = { | 329 var initialStorage = { |
273 activeNotifications: {}, | 330 activeNotifications: {}, |
274 retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS | 331 retryDelaySeconds: INITIAL_POLLING_PERIOD_SECONDS |
275 }; | 332 }; |
276 storage.set(initialStorage, updateNotificationsCards); | 333 storage.set(initialStorage); |
334 updateNotificationsCards(); | |
277 } | 335 } |
278 | 336 |
279 chrome.runtime.onInstalled.addListener(function(details) { | 337 chrome.runtime.onInstalled.addListener(function(details) { |
280 if (details.reason != 'chrome_update') | 338 if (details.reason != 'chrome_update') |
281 initialize(); | 339 initialize(); |
282 }); | 340 }); |
283 | 341 |
284 chrome.runtime.onStartup.addListener(function() { | 342 chrome.runtime.onStartup.addListener(function() { |
285 initialize(); | 343 initialize(); |
286 }); | 344 }); |
287 | 345 |
288 chrome.alarms.onAlarm.addListener(function(alarm) { | 346 chrome.alarms.onAlarm.addListener(function(alarm) { |
289 updateNotificationsCards(); | 347 if (alarm.name == UPDATE_NOTIFICATIONS_ALARM_NAME) |
348 updateNotificationsCards(); | |
290 }); | 349 }); |
291 | 350 |
292 chrome.notifications.onClicked.addListener( | 351 chrome.notifications.onClicked.addListener( |
293 function(notificationId) { | 352 function(notificationId) { |
294 onNotificationClicked(notificationId, function(actionUrls) { | 353 onNotificationClicked(notificationId, function(actionUrls) { |
295 return actionUrls.messageUrl; | 354 return actionUrls.messageUrl; |
296 }); | 355 }); |
297 }); | 356 }); |
298 | 357 |
299 chrome.notifications.onButtonClicked.addListener( | 358 chrome.notifications.onButtonClicked.addListener( |
300 function(notificationId, buttonIndex) { | 359 function(notificationId, buttonIndex) { |
301 onNotificationClicked(notificationId, function(actionUrls) { | 360 onNotificationClicked(notificationId, function(actionUrls) { |
302 if (!Array.isArray(actionUrls.buttonUrls)) | 361 if (!Array.isArray(actionUrls.buttonUrls)) |
303 return undefined; | 362 return undefined; |
304 | 363 |
305 return actionUrls.buttonUrls[buttonIndex]; | 364 return actionUrls.buttonUrls[buttonIndex]; |
306 }); | 365 }); |
307 }); | 366 }); |
308 | 367 |
309 chrome.notifications.onClosed.addListener(onNotificationClosed); | 368 chrome.notifications.onClosed.addListener(onNotificationClosed); |
OLD | NEW |