Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(3686)

Unified Diff: chrome/browser/resources/google_now/cards.js

Issue 107033002: Combining cards instead of merging (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: rgistafson's verbal comment Created 7 years ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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
};
}

Powered by Google App Engine
This is Rietveld 408576698