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