Home | History | Annotate | Download | only in google_now
      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  *
     20  * @typedef {string}
     21  */
     22 var 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  */
     32 var 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  */
     42 var ActionUrls;
     43 
     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;
     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  */
     66 var 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  */
     78 var UncombinedNotification;
     79 
     80 /**
     81  * Card combined from potentially multiple groups.
     82  *
     83  * @typedef {Array.<UncombinedNotification>}
     84  */
     85 var 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  */
    104 var UPDATE_CARD_TASK_NAME = 'update-card';
    105 
    106 /**
    107  * Builds an object to manage notification card set.
    108  * @return {Object} Card set interface.
    109  */
    110 function 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