• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
15var Trigger;
16
17/**
18 * ID of an individual (uncombined) notification.
19 *
20 * @typedef {string}
21 */
22var NotificationId;
23
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 */
32var DismissalData;
33
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 */
42var ActionUrls;
43
44/**
45 * ID of a combined notification. This is the ID used with chrome.notifications
46 * API.
47 *
48 * @typedef {string}
49 */
50var ChromeNotificationId;
51
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 */
66var ReceivedNotification;
67
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 */
78var UncombinedNotification;
79
80/**
81 * Card combined from potentially multiple groups.
82 *
83 * @typedef {Array.<UncombinedNotification>}
84 */
85var CombinedCard;
86
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;
100
101/**
102 * Names for tasks that can be created by the this file.
103 */
104var UPDATE_CARD_TASK_NAME = 'update-card';
105
106/**
107 * Builds an object to manage notification card set.
108 * @return {Object} Card set interface.
109 */
110function buildCardSet() {
111  var alarmPrefix = 'card-';
112
113  /**
114   * Creates/updates/deletes a Chrome notification.
115   * @param {ChromeNotificationId} cardId Card ID.
116   * @param {?ReceivedNotification} receivedNotification Google Now card
117   *     represented as a set of parameters for showing a Chrome notification,
118   *     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));
125
126    if (!receivedNotification) {
127      instrumented.notifications.clear(cardId, function() {});
128      return;
129    }
130
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                  }
152
153                  if (onCardShown !== undefined)
154                    onCardShown(receivedNotification);
155                });
156          }
157        });
158  }
159
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;
176
177      callback(uncombinedNotification, shouldShow && !shouldHide);
178    }
179  }
180
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));
195
196    chrome.alarms.clear(alarmPrefix + cardId);
197    var now = Date.now();
198    /** @type {?UncombinedNotification} */
199    var winningCard = null;
200    // Next moment of time when winning notification selection algotithm can
201    // potentially return a different notification.
202    /** @type {?number} */
203    var nextEventTime = null;
204
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          }
222
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        });
233
234    // Show/hide the winning card.
235    updateNotification(
236        cardId, winningCard && winningCard.receivedNotification, onCardShown);
237
238    if (nextEventTime) {
239      // If we expect more events, create an alarm for the next one.
240      chrome.alarms.create(alarmPrefix + cardId, {when: nextEventTime});
241
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 || null) &&
247          JSON.parse(JSON.stringify(
248              winningCard.receivedNotification.actionUrls));
249
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  }
263
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 = [];
280
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      });
297
298    return {
299      dismissals: dismissals,
300      notificationData: update(cardId, newCombinedCard, notificationGroups)
301    };
302  }
303
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  }
322
323  instrumented.alarms.onAlarm.addListener(function(alarm) {
324    console.log('cardManager.onAlarm ' + JSON.stringify(alarm));
325
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 || {};
339
340              var combinedCard =
341                (items.notificationsData[cardId] &&
342                 items.notificationsData[cardId].combinedCard) || [];
343
344              var cardShownCallback = undefined;
345              if (localStorage['locationCardsShown'] <
346                  LOCATION_CARDS_LINK_THRESHOLD) {
347                 cardShownCallback = countLocationCard;
348              }
349
350              items.notificationsData[cardId] =
351                  update(
352                      cardId,
353                      combinedCard,
354                      items.notificationGroups,
355                      cardShownCallback);
356
357              chrome.storage.local.set(items);
358            });
359      });
360    }
361  });
362
363  return {
364    update: update,
365    onDismissal: onDismissal
366  };
367}
368