Upstream version 5.34.104.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / google_now / cards.js
index 865db8a..4c40ba6 100644 (file)
  * Show/hide trigger in a card.
  *
  * @typedef {{
- *   showTime: number=,
- *   hideTime: number=
+ *   showTimeSec: (string|undefined),
+ *   hideTimeSec: string
  * }}
  */
 var Trigger;
 
 /**
+ * ID of an individual (uncombined) notification.
+ *
+ * @typedef {string}
+ */
+var NotificationId;
+
+/**
  * Data to build a dismissal request for a card from a specific group.
  *
  * @typedef {{
- *   notificationId: string,
+ *   notificationId: NotificationId,
  *   parameters: Object
  * }}
  */
 var DismissalData;
 
 /**
- * Card merged from potentially multiple groups.
+ * Urls that need to be opened when clicking a notification or its buttons.
  *
  * @typedef {{
+ *   messageUrl: (string|undefined),
+ *   buttonUrls: (Array.<string>|undefined)
+ * }}
+ */
+var ActionUrls;
+
+/**
+ * ID of a combined notification. This is the ID used with chrome.notifications
+ * API.
+ *
+ * @typedef {string}
+ */
+var ChromeNotificationId;
+
+/**
+ * Notification as sent by the server.
+ *
+ * @typedef {{
+ *   notificationId: NotificationId,
+ *   chromeNotificationId: ChromeNotificationId,
  *   trigger: Trigger,
- *   version: number,
- *   timestamp: number,
- *   notification: Object,
- *   actionUrls: Object=,
- *   groupRank: number,
- *   dismissals: Array.<DismissalData>,
- *   locationBased: boolean=
+ *   chromeNotificationOptions: Object,
+ *   actionUrls: (ActionUrls|undefined),
+ *   dismissal: Object,
+ *   locationBased: (boolean|undefined),
+ *   groupName: string
+ * }}
+ */
+var ReceivedNotification;
+
+/**
+ * Received notification in a self-sufficient form that doesn't require group's
+ * timestamp to calculate show and hide times.
+ *
+ * @typedef {{
+ *   receivedNotification: ReceivedNotification,
+ *   showTime: (number|undefined),
+ *   hideTime: number
  * }}
  */
-var MergedCard;
+var UncombinedNotification;
 
 /**
- * Set of parameters for creating card notification.
+ * Card combined from potentially multiple groups.
+ *
+ * @typedef {Array.<UncombinedNotification>}
+ */
+var CombinedCard;
+
+/**
+ * Data entry that we store for every Chrome notification.
+ * |timestamp| is the time when corresponding Chrome notification was created or
+ * updated last time by cardSet.update().
  *
  * @typedef {{
- *   notification: Object,
- *   hideTime: number=,
- *   version: number,
- *   previousVersion: number=,
- *   locationBased: boolean=
+ *   actionUrls: (ActionUrls|undefined),
+ *   timestamp: number,
+ *   combinedCard: CombinedCard
  * }}
+ *
  */
-var CardCreateInfo;
+ var NotificationDataEntry;
 
 /**
  * Names for tasks that can be created by the this file.
  */
-var SHOW_CARD_TASK_NAME = 'show-card';
-var CLEAR_CARD_TASK_NAME = 'clear-card';
+var UPDATE_CARD_TASK_NAME = 'update-card';
 
 /**
  * Builds an object to manage notification card set.
  * @return {Object} Card set interface.
  */
 function buildCardSet() {
-  var cardShowPrefix = 'card-show-';
-  var cardHidePrefix = 'card-hide-';
+  var alarmPrefix = 'card-';
 
   /**
-   * Schedules hiding a notification.
-   * @param {string} cardId Card ID.
-   * @param {number=} opt_timeHide If specified, epoch time to hide the card. If
-   *      undefined, the card will be kept shown at least until next update.
+   * Creates/updates/deletes a Chrome notification.
+   * @param {ChromeNotificationId} cardId Card ID.
+   * @param {(ReceivedNotification|undefined)} receivedNotification Google Now
+   *     card represented as a set of parameters for showing a Chrome
+   *     notification, or null if the notification needs to be deleted.
+   * @param {function(ReceivedNotification)=} onCardShown Optional parameter
+   *     called when each card is shown.
    */
-  function scheduleHiding(cardId, opt_timeHide) {
-    if (opt_timeHide === undefined)
+  function updateNotification(cardId, receivedNotification, onCardShown) {
+    console.log('cardManager.updateNotification ' + cardId + ' ' +
+                JSON.stringify(receivedNotification));
+
+    if (!receivedNotification) {
+      instrumented.notifications.clear(cardId, function() {});
       return;
+    }
+
+    // Try updating the notification.
+    instrumented.notifications.update(
+        cardId,
+        receivedNotification.chromeNotificationOptions,
+        function(wasUpdated) {
+          if (!wasUpdated) {
+            // If the notification wasn't updated, it probably didn't exist.
+            // Create it.
+            console.log('cardManager.updateNotification ' + cardId +
+                        ' failed to update, creating');
+            instrumented.notifications.create(
+                cardId,
+                receivedNotification.chromeNotificationOptions,
+                function(newNotificationId) {
+                  if (!newNotificationId || chrome.runtime.lastError) {
+                    var errorMessage = chrome.runtime.lastError &&
+                                       chrome.runtime.lastError.message;
+                    console.error('notifications.create: ID=' +
+                        newNotificationId + ', ERROR=' + errorMessage);
+                    return;
+                  }
 
-    var alarmName = cardHidePrefix + cardId;
-    var alarmInfo = {when: opt_timeHide};
-    chrome.alarms.create(alarmName, alarmInfo);
+                  if (onCardShown !== undefined)
+                    onCardShown(receivedNotification);
+                });
+          }
+        });
   }
 
   /**
-   * Shows a notification.
-   * @param {string} cardId Card ID.
-   * @param {CardCreateInfo} cardCreateInfo Google Now card represented as a set
-   *     of parameters for showing a Chrome notification.
-   * @param {function(CardCreateInfo)=} onCardShown Optional parameter called
-   *     when each card is shown.
+   * Iterates uncombined notifications in a combined card, determining for
+   * each whether it's visible at the specified moment.
+   * @param {CombinedCard} combinedCard The combined card in question.
+   * @param {number} timestamp Time for which to calculate visibility.
+   * @param {function(UncombinedNotification, boolean)} callback Function
+   *     invoked for every uncombined notification in |combinedCard|.
+   *     The boolean parameter indicates whether the uncombined notification is
+   *     visible at |timestamp|.
    */
-  function showNotification(cardId, cardCreateInfo, onCardShown) {
-    console.log('cardManager.showNotification ' + cardId + ' ' +
-                JSON.stringify(cardCreateInfo));
-
-    if (cardCreateInfo.hideTime <= Date.now()) {
-      console.log('cardManager.showNotification ' + cardId + ': expired');
-      // Card has expired. Schedule hiding to delete asociated information.
-      scheduleHiding(cardId, cardCreateInfo.hideTime);
-      return;
+  function iterateUncombinedNotifications(combinedCard, timestamp, callback) {
+    for (var i = 0; i != combinedCard.length; ++i) {
+      var uncombinedNotification = combinedCard[i];
+      var shouldShow = !uncombinedNotification.showTime ||
+          uncombinedNotification.showTime <= timestamp;
+      var shouldHide = uncombinedNotification.hideTime <= timestamp;
+
+      callback(uncombinedNotification, shouldShow && !shouldHide);
     }
+  }
 
-    if (cardCreateInfo.previousVersion !== cardCreateInfo.version) {
-      // Delete a notification with the specified id if it already exists, and
-      // then create a notification.
-      instrumented.notifications.create(
-          cardId,
-          cardCreateInfo.notification,
-          function(newNotificationId) {
-            if (!newNotificationId || chrome.runtime.lastError) {
-              var errorMessage = chrome.runtime.lastError &&
-                                 chrome.runtime.lastError.message;
-              console.error('notifications.create: ID=' + newNotificationId +
-                            ', ERROR=' + errorMessage);
-              return;
-            }
+  /**
+   * Refreshes (shows/hides) the notification corresponding to the combined card
+   * based on the current time and show-hide intervals in the combined card.
+   * @param {ChromeNotificationId} cardId Card ID.
+   * @param {CombinedCard} combinedCard Combined cards with |cardId|.
+   * @param {Object.<string, StoredNotificationGroup>} notificationGroups
+   *     Map from group name to group information.
+   * @param {function(ReceivedNotification)=} onCardShown Optional parameter
+   *     called when each card is shown.
+   * @return {(NotificationDataEntry|undefined)} Notification data entry for
+   *     this card. It's 'undefined' if the card's life is over.
+   */
+  function update(cardId, combinedCard, notificationGroups, onCardShown) {
+    console.log('cardManager.update ' + JSON.stringify(combinedCard));
 
-            if (onCardShown !== undefined)
-              onCardShown(cardCreateInfo);
+    chrome.alarms.clear(alarmPrefix + cardId);
+    var now = Date.now();
+    /** @type {(UncombinedNotification|undefined)} */
+    var winningCard = undefined;
+    // Next moment of time when winning notification selection algotithm can
+    // potentially return a different notification.
+    /** @type {?number} */
+    var nextEventTime = null;
 
-            scheduleHiding(cardId, cardCreateInfo.hideTime);
-          });
-    } else {
-      // Update existing notification.
-      instrumented.notifications.update(
-          cardId,
-          cardCreateInfo.notification,
-          function(wasUpdated) {
-            if (!wasUpdated || chrome.runtime.lastError) {
-              var errorMessage = chrome.runtime.lastError &&
-                                 chrome.runtime.lastError.message;
-              console.error('notifications.update: UPDATED=' + wasUpdated +
-                            ', ERROR=' + errorMessage);
-              return;
+    // Find a winning uncombined notification: a highest-priority notification
+    // that needs to be shown now.
+    iterateUncombinedNotifications(
+        combinedCard,
+        now,
+        function(uncombinedCard, visible) {
+          // If the uncombined notification is visible now and set the winning
+          // card to it if its priority is higher.
+          if (visible) {
+            if (!winningCard ||
+                uncombinedCard.receivedNotification.chromeNotificationOptions.
+                    priority >
+                winningCard.receivedNotification.chromeNotificationOptions.
+                    priority) {
+              winningCard = uncombinedCard;
             }
+          }
 
-            scheduleHiding(cardId, cardCreateInfo.hideTime);
-          });
+          // Next event time is the closest hide or show event.
+          if (uncombinedCard.showTime && uncombinedCard.showTime > now) {
+            if (!nextEventTime || nextEventTime > uncombinedCard.showTime)
+              nextEventTime = uncombinedCard.showTime;
+          }
+          if (uncombinedCard.hideTime > now) {
+            if (!nextEventTime || nextEventTime > uncombinedCard.hideTime)
+              nextEventTime = uncombinedCard.hideTime;
+          }
+        });
+
+    // Show/hide the winning card.
+    updateNotification(
+        cardId, winningCard && winningCard.receivedNotification, onCardShown);
+
+    if (nextEventTime) {
+      // If we expect more events, create an alarm for the next one.
+      chrome.alarms.create(alarmPrefix + cardId, {when: nextEventTime});
+
+      // The trick with stringify/parse is to create a copy of action URLs,
+      // otherwise notifications data with 2 pointers to the same object won't
+      // be stored correctly to chrome.storage.
+      var winningActionUrls = winningCard &&
+          winningCard.receivedNotification.actionUrls &&
+          JSON.parse(JSON.stringify(
+              winningCard.receivedNotification.actionUrls));
+
+      return {
+        actionUrls: winningActionUrls,
+        timestamp: now,
+        combinedCard: combinedCard
+      };
+    } else {
+      // If there are no more events, we are done with this card. Note that all
+      // received notifications have hideTime.
+      verify(!winningCard, 'No events left, but card is shown.');
+      clearCardFromGroups(cardId, notificationGroups);
+      return undefined;
     }
   }
 
   /**
-   * Updates/creates a card notification with new data.
-   * @param {string} cardId Card ID.
-   * @param {MergedCard} card Google Now card from the server.
-   * @param {number=} previousVersion The version of the shown card with
-   *     this id, if it exists, undefined otherwise.
-   * @param {function(CardCreateInfo)=} onCardShown Optional parameter called
-   *     when each card is shown.
-   * @return {Object} Notification data entry for this card.
+   * Removes dismissed part of a card and refreshes the card. Returns remaining
+   * dismissals for the combined card and updated notification data.
+   * @param {ChromeNotificationId} cardId Card ID.
+   * @param {NotificationDataEntry} notificationData Stored notification entry
+   *     for this card.
+   * @param {Object.<string, StoredNotificationGroup>} notificationGroups
+   *     Map from group name to group information.
+   * @return {{
+   *   dismissals: Array.<DismissalData>,
+   *   notificationData: (NotificationDataEntry|undefined)
+   * }}
    */
-  function update(cardId, card, previousVersion, onCardShown) {
-    console.log('cardManager.update ' + JSON.stringify(card) + ' ' +
-        previousVersion);
-
-    chrome.alarms.clear(cardHidePrefix + cardId);
-
-    var cardCreateInfo = {
-      notification: card.notification,
-      hideTime: card.trigger.hideTime,
-      version: card.version,
-      previousVersion: previousVersion,
-      locationBased: card.locationBased
-    };
+  function onDismissal(cardId, notificationData, notificationGroups) {
+    var dismissals = [];
+    var newCombinedCard = [];
 
-    var shownImmediately = false;
-    var cardShowAlarmName = cardShowPrefix + cardId;
-    if (card.trigger.showTime && card.trigger.showTime > Date.now()) {
-      // Card needs to be shown later.
-      console.log('cardManager.update: postponed');
-      var alarmInfo = {
-        when: card.trigger.showTime
-      };
-      chrome.alarms.create(cardShowAlarmName, alarmInfo);
-    } else {
-      // Card needs to be shown immediately.
-      console.log('cardManager.update: immediate');
-      chrome.alarms.clear(cardShowAlarmName);
-      showNotification(cardId, cardCreateInfo, onCardShown);
-    }
+    // Determine which parts of the combined card need to be dismissed or to be
+    // preserved. We dismiss parts that were visible at the moment when the card
+    // was last updated.
+    iterateUncombinedNotifications(
+      notificationData.combinedCard,
+      notificationData.timestamp,
+      function(uncombinedCard, visible) {
+        if (visible) {
+          dismissals.push({
+            notificationId: uncombinedCard.receivedNotification.notificationId,
+            parameters: uncombinedCard.receivedNotification.dismissal
+          });
+        } else {
+          newCombinedCard.push(uncombinedCard);
+        }
+      });
 
     return {
-      actionUrls: card.actionUrls,
-      cardCreateInfo: cardCreateInfo,
-      dismissals: card.dismissals
+      dismissals: dismissals,
+      notificationData: update(cardId, newCombinedCard, notificationGroups)
     };
   }
 
   /**
-   * Removes a card notification.
-   * @param {string} cardId Card ID.
-   * @param {boolean} clearStorage True if the information associated with the
-   *     card should be erased from chrome.storage.
+   * Removes card information from |notificationGroups|.
+   * @param {ChromeNotificationId} cardId Card ID.
+   * @param {Object.<string, StoredNotificationGroup>} notificationGroups
+   *     Map from group name to group information.
    */
-  function clear(cardId, clearStorage) {
-    console.log('cardManager.clear ' + cardId);
-
-    chrome.notifications.clear(cardId, function() {});
-    chrome.alarms.clear(cardShowPrefix + cardId);
-    chrome.alarms.clear(cardHidePrefix + cardId);
-
-    if (clearStorage) {
-      instrumented.storage.local.get(
-          ['notificationsData', 'notificationGroups'],
-          function(items) {
-            items = items || {};
-            items.notificationsData = items.notificationsData || {};
-            items.notificationGroups = items.notificationGroups || {};
-
-            delete items.notificationsData[cardId];
-
-            for (var groupName in items.notificationGroups) {
-              var group = items.notificationGroups[groupName];
-              for (var i = 0; i != group.cards.length; ++i) {
-                if (group.cards[i].chromeNotificationId == cardId) {
-                  group.cards.splice(i, 1);
-                  break;
-                }
-              }
-            }
-
-            chrome.storage.local.set(items);
-          });
+  function clearCardFromGroups(cardId, notificationGroups) {
+    console.log('cardManager.clearCardFromGroups ' + cardId);
+    for (var groupName in notificationGroups) {
+      var group = notificationGroups[groupName];
+      for (var i = 0; i != group.cards.length; ++i) {
+        if (group.cards[i].chromeNotificationId == cardId) {
+          group.cards.splice(i, 1);
+          break;
+        }
+      }
     }
   }
 
   instrumented.alarms.onAlarm.addListener(function(alarm) {
     console.log('cardManager.onAlarm ' + JSON.stringify(alarm));
 
-    if (alarm.name.indexOf(cardShowPrefix) == 0) {
+    if (alarm.name.indexOf(alarmPrefix) == 0) {
       // Alarm to show the card.
-      tasks.add(SHOW_CARD_TASK_NAME, function() {
-        var cardId = alarm.name.substring(cardShowPrefix.length);
-        instrumented.storage.local.get('notificationsData', function(items) {
-          console.log('cardManager.onAlarm.get ' + JSON.stringify(items));
-          if (!items || !items.notificationsData)
-            return;
-          var notificationData = items.notificationsData[cardId];
-          if (!notificationData)
-            return;
-
-          var cardShownCallback = undefined;
-          if (localStorage['locationCardsShown'] <
-              LOCATION_CARDS_LINK_THRESHOLD) {
-             cardShownCallback = countLocationCard;
-          }
+      tasks.add(UPDATE_CARD_TASK_NAME, function() {
+        var cardId = alarm.name.substring(alarmPrefix.length);
+        instrumented.storage.local.get(
+            ['notificationsData', 'notificationGroups'],
+            function(items) {
+              console.log('cardManager.onAlarm.get ' + JSON.stringify(items));
+              items = items || {};
+              /** @type {Object.<string, NotificationDataEntry>} */
+              items.notificationsData = items.notificationsData || {};
+              /** @type {Object.<string, StoredNotificationGroup>} */
+              items.notificationGroups = items.notificationGroups || {};
 
-          showNotification(
-              cardId, notificationData.cardCreateInfo, cardShownCallback);
-        });
-      });
-    } else if (alarm.name.indexOf(cardHidePrefix) == 0) {
-      // Alarm to hide the card.
-      tasks.add(CLEAR_CARD_TASK_NAME, function() {
-        var cardId = alarm.name.substring(cardHidePrefix.length);
-        clear(cardId, true);
+              var combinedCard =
+                (items.notificationsData[cardId] &&
+                 items.notificationsData[cardId].combinedCard) || [];
+
+              var cardShownCallback = undefined;
+              if (localStorage['explanatoryCardsShown'] <
+                  EXPLANATORY_CARDS_LINK_THRESHOLD) {
+                 cardShownCallback = countExplanatoryCard;
+              }
+
+              items.notificationsData[cardId] =
+                  update(
+                      cardId,
+                      combinedCard,
+                      items.notificationGroups,
+                      cardShownCallback);
+
+              chrome.storage.local.set(items);
+            });
       });
     }
   });
 
   return {
     update: update,
-    clear: clear
+    onDismissal: onDismissal
   };
 }