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  * 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 }
    388