| Index: chrome/browser/resources/google_now/cards.js
|
| diff --git a/chrome/browser/resources/google_now/cards.js b/chrome/browser/resources/google_now/cards.js
|
| index 865db8a36b649f97608886f4f016e306370326e7..3295012bff040517139b41692d2da5d6d6cc40ec 100644
|
| --- a/chrome/browser/resources/google_now/cards.js
|
| +++ b/chrome/browser/resources/google_now/cards.js
|
| @@ -8,237 +8,335 @@
|
| * 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 MergedCard;
|
| +var ReceivedNotification;
|
|
|
| /**
|
| - * Set of parameters for creating card notification.
|
| + * Received notification in a self-sufficient form that doesn't require group's
|
| + * timestamp to calculate show and hide times.
|
| *
|
| * @typedef {{
|
| - * notification: Object,
|
| - * hideTime: number=,
|
| - * version: number,
|
| - * previousVersion: number=,
|
| - * locationBased: boolean=
|
| + * receivedNotification: ReceivedNotification,
|
| + * showTime: (number|undefined),
|
| + * hideTime: number
|
| * }}
|
| */
|
| -var CardCreateInfo;
|
| +var UncombinedNotification;
|
| +
|
| +/**
|
| + * 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 {{
|
| + * actionUrls: (ActionUrls|undefined),
|
| + * timestamp: number,
|
| + * combinedCard: CombinedCard
|
| + * }}
|
| + *
|
| + */
|
| + 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} 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;
|
| + }
|
|
|
| - var alarmName = cardHidePrefix + cardId;
|
| - var alarmInfo = {when: opt_timeHide};
|
| - chrome.alarms.create(alarmName, alarmInfo);
|
| + // 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;
|
| + }
|
| +
|
| + 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 {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, onCardShown) {
|
| + console.log('cardManager.update ' + JSON.stringify(combinedCard));
|
|
|
| - if (onCardShown !== undefined)
|
| - onCardShown(cardCreateInfo);
|
| + chrome.alarms.clear(alarmPrefix + cardId);
|
| + var now = Date.now();
|
| + /** @type {?UncombinedNotification} */
|
| + var winningCard = null;
|
| + // 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 &&
|
| + 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);
|
| + 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.
|
| + * @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) {
|
| + 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)
|
| };
|
| }
|
|
|
| /**
|
| - * 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.
|
| */
|
| - 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;
|
| - }
|
| - }
|
| - }
|
| + function clearCardFromGroups(cardId) {
|
| + console.log('cardManager.clearCardFromGroups ' + cardId);
|
|
|
| - chrome.storage.local.set(items);
|
| - });
|
| - }
|
| + instrumented.storage.local.get('notificationGroups', function(items) {
|
| + items = items || {};
|
| + /** @type {Object.<string, StoredNotificationGroup>} */
|
| + items.notificationGroups = items.notificationGroups || {};
|
| +
|
| + 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);
|
| + });
|
| }
|
|
|
| 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);
|
| + tasks.add(UPDATE_CARD_TASK_NAME, function() {
|
| + var cardId = alarm.name.substring(alarmPrefix.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;
|
| + items = items || {};
|
| + /** @type {Object.<string, NotificationDataEntry>} */
|
| + items.notificationsData = items.notificationsData || {};
|
| + var combinedCard =
|
| + (items.notificationsData[cardId] &&
|
| + items.notificationsData[cardId].combinedCard) || [];
|
|
|
| var cardShownCallback = undefined;
|
| if (localStorage['locationCardsShown'] <
|
| @@ -246,21 +344,17 @@ function buildCardSet() {
|
| cardShownCallback = countLocationCard;
|
| }
|
|
|
| - showNotification(
|
| - cardId, notificationData.cardCreateInfo, cardShownCallback);
|
| + items.notificationsData[cardId] =
|
| + update(cardId, combinedCard, cardShownCallback);
|
| +
|
| + chrome.storage.local.set(items);
|
| });
|
| });
|
| - } 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);
|
| - });
|
| }
|
| });
|
|
|
| return {
|
| update: update,
|
| - clear: clear
|
| + onDismissal: onDismissal
|
| };
|
| }
|
|
|