Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / google_now / cards.js
1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 'use strict';
6
7 /**
8  * Show/hide trigger in a card.
9  *
10  * @typedef {{
11  *   showTimeSec: (string|undefined),
12  *   hideTimeSec: string
13  * }}
14  */
15 var Trigger;
16
17 /**
18  * ID of an individual (uncombined) notification.
19  * This ID comes directly from the server.
20  *
21  * @typedef {string}
22  */
23 var ServerNotificationId;
24
25 /**
26  * Data to build a dismissal request for a card from a specific group.
27  *
28  * @typedef {{
29  *   notificationId: ServerNotificationId,
30  *   parameters: Object
31  * }}
32  */
33 var DismissalData;
34
35 /**
36  * Urls that need to be opened when clicking a notification or its buttons.
37  *
38  * @typedef {{
39  *   messageUrl: (string|undefined),
40  *   buttonUrls: (Array.<string>|undefined)
41  * }}
42  */
43 var ActionUrls;
44
45 /**
46  * ID of a combined notification.
47  * This is the ID used with chrome.notifications API.
48  *
49  * @typedef {string}
50  */
51 var ChromeNotificationId;
52
53 /**
54  * Notification as sent by the server.
55  *
56  * @typedef {{
57  *   notificationId: ServerNotificationId,
58  *   chromeNotificationId: ChromeNotificationId,
59  *   trigger: Trigger,
60  *   chromeNotificationOptions: Object,
61  *   actionUrls: (ActionUrls|undefined),
62  *   dismissal: Object,
63  *   locationBased: (boolean|undefined),
64  *   groupName: string,
65  *   cardTypeId: (number|undefined)
66  * }}
67  */
68 var ReceivedNotification;
69
70 /**
71  * Received notification in a self-sufficient form that doesn't require group's
72  * timestamp to calculate show and hide times.
73  *
74  * @typedef {{
75  *   receivedNotification: ReceivedNotification,
76  *   showTime: (number|undefined),
77  *   hideTime: number
78  * }}
79  */
80 var UncombinedNotification;
81
82 /**
83  * Card combined from potentially multiple groups.
84  *
85  * @typedef {Array.<UncombinedNotification>}
86  */
87 var CombinedCard;
88
89 /**
90  * Data entry that we store for every Chrome notification.
91  * |timestamp| is the time when corresponding Chrome notification was created or
92  * updated last time by cardSet.update().
93  *
94  * @typedef {{
95  *   actionUrls: (ActionUrls|undefined),
96  *   cardTypeId: (number|undefined),
97  *   timestamp: number,
98  *   combinedCard: CombinedCard
99  * }}
100  *
101  */
102 var NotificationDataEntry;
103
104 /**
105  * Names for tasks that can be created by the this file.
106  */
107 var UPDATE_CARD_TASK_NAME = 'update-card';
108
109 /**
110  * Builds an object to manage notification card set.
111  * @return {Object} Card set interface.
112  */
113 function buildCardSet() {
114   var alarmPrefix = 'card-';
115
116   /**
117    * Creates/updates/deletes a Chrome notification.
118    * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID
119    *     of the card.
120    * @param {(ReceivedNotification|undefined)} receivedNotification Google Now
121    *     card represented as a set of parameters for showing a Chrome
122    *     notification, or null if the notification needs to be deleted.
123    * @param {function(ReceivedNotification)=} onCardShown Optional parameter
124    *     called when each card is shown.
125    */
126   function updateNotification(
127       chromeNotificationId, receivedNotification, onCardShown) {
128     console.log(
129         'cardManager.updateNotification ' + chromeNotificationId + ' ' +
130         JSON.stringify(receivedNotification));
131
132     if (!receivedNotification) {
133       instrumented.notifications.clear(chromeNotificationId, function() {});
134       return;
135     }
136
137     // Try updating the notification.
138     instrumented.notifications.update(
139         chromeNotificationId,
140         receivedNotification.chromeNotificationOptions,
141         function(wasUpdated) {
142           if (!wasUpdated) {
143             // If the notification wasn't updated, it probably didn't exist.
144             // Create it.
145             console.log(
146                 'cardManager.updateNotification ' + chromeNotificationId +
147                 ' failed to update, creating');
148             instrumented.notifications.create(
149                 chromeNotificationId,
150                 receivedNotification.chromeNotificationOptions,
151                 function(newChromeNotificationId) {
152                   if (!newChromeNotificationId || chrome.runtime.lastError) {
153                     var errorMessage = chrome.runtime.lastError &&
154                                        chrome.runtime.lastError.message;
155                     console.error('notifications.create: ID=' +
156                         newChromeNotificationId + ', ERROR=' + errorMessage);
157                     return;
158                   }
159
160                   if (onCardShown !== undefined)
161                     onCardShown(receivedNotification);
162                 });
163           }
164         });
165   }
166
167   /**
168    * Iterates uncombined notifications in a combined card, determining for
169    * each whether it's visible at the specified moment.
170    * @param {CombinedCard} combinedCard The combined card in question.
171    * @param {number} timestamp Time for which to calculate visibility.
172    * @param {function(UncombinedNotification, boolean)} callback Function
173    *     invoked for every uncombined notification in |combinedCard|.
174    *     The boolean parameter indicates whether the uncombined notification is
175    *     visible at |timestamp|.
176    */
177   function iterateUncombinedNotifications(combinedCard, timestamp, callback) {
178     for (var i = 0; i != combinedCard.length; ++i) {
179       var uncombinedNotification = combinedCard[i];
180       var shouldShow = !uncombinedNotification.showTime ||
181           uncombinedNotification.showTime <= timestamp;
182       var shouldHide = uncombinedNotification.hideTime <= timestamp;
183
184       callback(uncombinedNotification, shouldShow && !shouldHide);
185     }
186   }
187
188   /**
189    * Refreshes (shows/hides) the notification corresponding to the combined card
190    * based on the current time and show-hide intervals in the combined card.
191    * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID
192    *     of the card.
193    * @param {CombinedCard} combinedCard Combined cards with
194    *     |chromeNotificationId|.
195    * @param {Object.<string, StoredNotificationGroup>} notificationGroups
196    *     Map from group name to group information.
197    * @param {function(ReceivedNotification)=} onCardShown Optional parameter
198    *     called when each card is shown.
199    * @return {(NotificationDataEntry|undefined)} Notification data entry for
200    *     this card. It's 'undefined' if the card's life is over.
201    */
202   function update(
203       chromeNotificationId, combinedCard, notificationGroups, onCardShown) {
204     console.log('cardManager.update ' + JSON.stringify(combinedCard));
205
206     chrome.alarms.clear(alarmPrefix + chromeNotificationId);
207     var now = Date.now();
208     /** @type {(UncombinedNotification|undefined)} */
209     var winningCard = undefined;
210     // Next moment of time when winning notification selection algotithm can
211     // potentially return a different notification.
212     /** @type {?number} */
213     var nextEventTime = null;
214
215     // Find a winning uncombined notification: a highest-priority notification
216     // that needs to be shown now.
217     iterateUncombinedNotifications(
218         combinedCard,
219         now,
220         function(uncombinedCard, visible) {
221           // If the uncombined notification is visible now and set the winning
222           // card to it if its priority is higher.
223           if (visible) {
224             if (!winningCard ||
225                 uncombinedCard.receivedNotification.chromeNotificationOptions.
226                     priority >
227                 winningCard.receivedNotification.chromeNotificationOptions.
228                     priority) {
229               winningCard = uncombinedCard;
230             }
231           }
232
233           // Next event time is the closest hide or show event.
234           if (uncombinedCard.showTime && uncombinedCard.showTime > now) {
235             if (!nextEventTime || nextEventTime > uncombinedCard.showTime)
236               nextEventTime = uncombinedCard.showTime;
237           }
238           if (uncombinedCard.hideTime > now) {
239             if (!nextEventTime || nextEventTime > uncombinedCard.hideTime)
240               nextEventTime = uncombinedCard.hideTime;
241           }
242         });
243
244     // Show/hide the winning card.
245     updateNotification(
246         chromeNotificationId,
247         winningCard && winningCard.receivedNotification,
248         onCardShown);
249
250     if (nextEventTime) {
251       // If we expect more events, create an alarm for the next one.
252       chrome.alarms.create(
253           alarmPrefix + chromeNotificationId, {when: nextEventTime});
254
255       // The trick with stringify/parse is to create a copy of action URLs,
256       // otherwise notifications data with 2 pointers to the same object won't
257       // be stored correctly to chrome.storage.
258       var winningActionUrls = winningCard &&
259           winningCard.receivedNotification.actionUrls &&
260           JSON.parse(JSON.stringify(
261               winningCard.receivedNotification.actionUrls));
262       var winningCardTypeId = winningCard &&
263           winningCard.receivedNotification.cardTypeId;
264       return {
265         actionUrls: winningActionUrls,
266         cardTypeId: winningCardTypeId,
267         timestamp: now,
268         combinedCard: combinedCard
269       };
270     } else {
271       // If there are no more events, we are done with this card. Note that all
272       // received notifications have hideTime.
273       verify(!winningCard, 'No events left, but card is shown.');
274       clearCardFromGroups(chromeNotificationId, notificationGroups);
275       return undefined;
276     }
277   }
278
279   /**
280    * Removes dismissed part of a card and refreshes the card. Returns remaining
281    * dismissals for the combined card and updated notification data.
282    * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID
283    *     of the card.
284    * @param {NotificationDataEntry} notificationData Stored notification entry
285    *     for this card.
286    * @param {Object.<string, StoredNotificationGroup>} notificationGroups
287    *     Map from group name to group information.
288    * @return {{
289    *   dismissals: Array.<DismissalData>,
290    *   notificationData: (NotificationDataEntry|undefined)
291    * }}
292    */
293   function onDismissal(
294       chromeNotificationId, notificationData, notificationGroups) {
295     /** @type {Array.<DismissalData>} */
296     var dismissals = [];
297     /** @type {Array.<UncombinedNotification>} */
298     var newCombinedCard = [];
299
300     // Determine which parts of the combined card need to be dismissed or to be
301     // preserved. We dismiss parts that were visible at the moment when the card
302     // was last updated.
303     iterateUncombinedNotifications(
304       notificationData.combinedCard,
305       notificationData.timestamp,
306       function(uncombinedCard, visible) {
307         if (visible) {
308           dismissals.push({
309             notificationId: uncombinedCard.receivedNotification.notificationId,
310             parameters: uncombinedCard.receivedNotification.dismissal
311           });
312         } else {
313           newCombinedCard.push(uncombinedCard);
314         }
315       });
316
317     return {
318       dismissals: dismissals,
319       notificationData: update(
320           chromeNotificationId, newCombinedCard, notificationGroups)
321     };
322   }
323
324   /**
325    * Removes card information from |notificationGroups|.
326    * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID
327    *     of the card.
328    * @param {Object.<string, StoredNotificationGroup>} notificationGroups
329    *     Map from group name to group information.
330    */
331   function clearCardFromGroups(chromeNotificationId, notificationGroups) {
332     console.log('cardManager.clearCardFromGroups ' + chromeNotificationId);
333     for (var groupName in notificationGroups) {
334       var group = notificationGroups[groupName];
335       for (var i = 0; i != group.cards.length; ++i) {
336         if (group.cards[i].chromeNotificationId == chromeNotificationId) {
337           group.cards.splice(i, 1);
338           break;
339         }
340       }
341     }
342   }
343
344   instrumented.alarms.onAlarm.addListener(function(alarm) {
345     console.log('cardManager.onAlarm ' + JSON.stringify(alarm));
346
347     if (alarm.name.indexOf(alarmPrefix) == 0) {
348       // Alarm to show the card.
349       tasks.add(UPDATE_CARD_TASK_NAME, function() {
350         /** @type {ChromeNotificationId} */
351         var chromeNotificationId = alarm.name.substring(alarmPrefix.length);
352         fillFromChromeLocalStorage({
353           /** @type {Object.<ChromeNotificationId, NotificationDataEntry>} */
354           notificationsData: {},
355           /** @type {Object.<string, StoredNotificationGroup>} */
356           notificationGroups: {}
357         }).then(function(items) {
358           console.log('cardManager.onAlarm.get ' + JSON.stringify(items));
359
360           var combinedCard =
361             (items.notificationsData[chromeNotificationId] &&
362              items.notificationsData[chromeNotificationId].combinedCard) || [];
363
364           var cardShownCallback = undefined;
365           if (localStorage['explanatoryCardsShown'] <
366               EXPLANATORY_CARDS_LINK_THRESHOLD) {
367              cardShownCallback = countExplanatoryCard;
368           }
369
370           items.notificationsData[chromeNotificationId] =
371               update(
372                   chromeNotificationId,
373                   combinedCard,
374                   items.notificationGroups,
375                   cardShownCallback);
376
377           chrome.storage.local.set(items);
378         });
379       });
380     }
381   });
382
383   return {
384     update: update,
385     onDismissal: onDismissal
386   };
387 }