• 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 * This ID comes directly from the server.
20 *
21 * @typedef {string}
22 */
23var 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 */
33var 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 */
43var ActionUrls;
44
45/**
46 * ID of a combined notification.
47 * This is the ID used with chrome.notifications API.
48 *
49 * @typedef {string}
50 */
51var 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 */
68var 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 */
80var UncombinedNotification;
81
82/**
83 * Card combined from potentially multiple groups.
84 *
85 * @typedef {Array.<UncombinedNotification>}
86 */
87var 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 */
102var NotificationDataEntry;
103
104/**
105 * Names for tasks that can be created by the this file.
106 */
107var UPDATE_CARD_TASK_NAME = 'update-card';
108
109/**
110 * Builds an object to manage notification card set.
111 * @return {Object} Card set interface.
112 */
113function 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                ' not updated, creating instead');
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}
388