 Chromium Code Reviews
 Chromium Code Reviews Issue 107033002:
  Combining cards instead of merging  (Closed) 
  Base URL: svn://svn.chromium.org/chrome/trunk/src
    
  
    Issue 107033002:
  Combining cards instead of merging  (Closed) 
  Base URL: svn://svn.chromium.org/chrome/trunk/src| Index: chrome/browser/resources/google_now/background.js | 
| diff --git a/chrome/browser/resources/google_now/background.js b/chrome/browser/resources/google_now/background.js | 
| index 1441dc5d0b6e2a8219c4a77552532e49427ce0ef..82263a8790ba82ea5698b5df178dc096860e538f 100644 | 
| --- a/chrome/browser/resources/google_now/background.js | 
| +++ b/chrome/browser/resources/google_now/background.js | 
| @@ -102,19 +102,26 @@ var ON_PUSH_MESSAGE_START_TASK_NAME = 'on-push-message'; | 
| var LOCATION_WATCH_NAME = 'location-watch'; | 
| /** | 
| - * Notification as it's sent by the server. | 
| + * Group as received from the server. | 
| * | 
| * @typedef {{ | 
| - * notificationId: string, | 
| - * chromeNotificationId: string, | 
| - * trigger: Object=, | 
| - * version: number, | 
| - * chromeNotificationOptions: Object, | 
| - * actionUrls: Object=, | 
| - * dismissal: Object | 
| + * nextPollSeconds: (string|undefined), | 
| + * rank: (number|undefined), | 
| + * requested: (boolean|undefined) | 
| * }} | 
| */ | 
| -var UnmergedNotification; | 
| +var ReceivedGroup; | 
| + | 
| +/** | 
| + * Server response with notifications and groups. | 
| + * | 
| + * @typedef {{ | 
| + * googleNowDisabled: (boolean|undefined), | 
| + * groups: Object.<string, ReceivedGroup>, | 
| + * notifications: Array.<ReceivedNotification> | 
| + * }} | 
| + */ | 
| +var ServerResponse; | 
| /** | 
| * Notification group as the client stores it. |cardsTimestamp| and |rank| are | 
| @@ -124,13 +131,25 @@ var UnmergedNotification; | 
| * cards update and all the times after that. | 
| * | 
| * @typedef {{ | 
| - * cards: Array.<UnmergedNotification>, | 
| - * cardsTimestamp: number=, | 
| - * nextPollTime: number=, | 
| - * rank: number= | 
| + * cards: Array.<ReceivedNotification>, | 
| + * cardsTimestamp: (number|undefined), | 
| + * nextPollTime: (number|undefined), | 
| + * rank: (number|undefined) | 
| * }} | 
| */ | 
| -var StorageGroup; | 
| +var StoredNotificationGroup; | 
| + | 
| +/** | 
| + * Pending (not yet successfully sent) dismissal for a received notification. | 
| + * |time| is the moment when the user requested dismissal. | 
| + * | 
| + * @typedef {{ | 
| + * chromeNotificationId: ChromeNotificationId, | 
| + * time: number, | 
| + * dismissalData: DismissalData | 
| + * }} | 
| + */ | 
| +var PendingDismissal; | 
| /** | 
| * Checks if a new task can't be scheduled when another task is already | 
| @@ -273,74 +292,38 @@ function setAuthorization(request, callbackBoolean) { | 
| } | 
| /** | 
| - * Shows parsed and merged cards as notifications. | 
| - * @param {Object.<string, MergedCard>} cards Map from chromeNotificationId to | 
| - * the merged card, containing cards to show. | 
| - * @param {function(CardCreateInfo)=} onCardShown Optional parameter called when | 
| - * each card is shown. | 
| + * Shows parsed and combined cards as notifications. | 
| + * @param {Object.<ChromeNotificationId, CombinedCard>} cards Map from | 
| + * chromeNotificationId to the combined card, containing cards to show. | 
| + * @param {function(ReceivedNotification)=} onCardShown Optional parameter | 
| + * called when each card is shown. | 
| */ | 
| function showNotificationCards(cards, onCardShown) { | 
| console.log('showNotificationCards ' + JSON.stringify(cards)); | 
| - instrumented.storage.local.get(['notificationsData', 'recentDismissals'], | 
| - function(items) { | 
| - console.log('showNotificationCards-get ' + | 
| - JSON.stringify(items)); | 
| - items = items || {}; | 
| - items.notificationsData = items.notificationsData || {}; | 
| - items.recentDismissals = items.recentDismissals || {}; | 
| - | 
| - instrumented.notifications.getAll(function(notifications) { | 
| - console.log('showNotificationCards-getAll ' + | 
| - JSON.stringify(notifications)); | 
| - notifications = notifications || {}; | 
| - | 
| - // Build a set of non-expired recent dismissals. It will be used for | 
| - // client-side filtering of cards. | 
| - var updatedRecentDismissals = {}; | 
| - var currentTimeMs = Date.now(); | 
| - for (var chromeNotificationId in items.recentDismissals) { | 
| - if (currentTimeMs - items.recentDismissals[chromeNotificationId] < | 
| - DISMISS_RETENTION_TIME_MS) { | 
| - updatedRecentDismissals[chromeNotificationId] = | 
| - items.recentDismissals[chromeNotificationId]; | 
| - delete cards[chromeNotificationId]; | 
| - } | 
| - } | 
| + instrumented.notifications.getAll(function(notifications) { | 
| + console.log('showNotificationCards-getAll ' + | 
| + JSON.stringify(notifications)); | 
| + notifications = notifications || {}; | 
| - // Delete notifications that didn't receive an update. | 
| - for (var chromeNotificationId in notifications) { | 
| - console.log('showNotificationCards-delete-check ' + | 
| - chromeNotificationId); | 
| - if (!(chromeNotificationId in cards)) { | 
| - console.log( | 
| - 'showNotificationCards-delete ' + chromeNotificationId); | 
| - cardSet.clear(chromeNotificationId, false); | 
| - } | 
| - } | 
| + // Mark notifications that didn't receive an update as having received | 
| + // an empty update. | 
| + for (var chromeNotificationId in notifications) { | 
| + cards[chromeNotificationId] = cards[chromeNotificationId] || []; | 
| + } | 
| - // Create/update notifications and store their new properties. | 
| - var newNotificationsData = {}; | 
| - for (var chromeNotificationId in cards) { | 
| - var notificationData = | 
| - items.notificationsData[chromeNotificationId]; | 
| - var previousVersion = notifications[chromeNotificationId] && | 
| - notificationData && | 
| - notificationData.cardCreateInfo && | 
| - notificationData.cardCreateInfo.version; | 
| - newNotificationsData[chromeNotificationId] = cardSet.update( | 
| - chromeNotificationId, | 
| - cards[chromeNotificationId], | 
| - previousVersion, | 
| - onCardShown); | 
| - } | 
| + /** @type {Object.<string, NotificationDataEntry>} */ | 
| + var notificationsData = {}; | 
| - chrome.storage.local.set({ | 
| - notificationsData: newNotificationsData, | 
| - recentDismissals: updatedRecentDismissals | 
| - }); | 
| - }); | 
| - }); | 
| + // Create/update/delete notifications. | 
| + for (var chromeNotificationId in cards) { | 
| + notificationsData[chromeNotificationId] = cardSet.update( | 
| + chromeNotificationId, | 
| + cards[chromeNotificationId], | 
| + onCardShown); | 
| + } | 
| + chrome.storage.local.set({notificationsData: notificationsData}); | 
| + }); | 
| } | 
| /** | 
| @@ -364,89 +347,39 @@ function removeAllCards() { | 
| } | 
| /** | 
| - * Merges an unmerged notification into a merged card with same ID. | 
| - * @param {MergedCard=} mergedCard Existing merged card or undefined if a merged | 
| - * card doesn't exist (i.e. we see this ID for the first time while | 
| - * merging). | 
| - * @param {UnmergedNotification} unmergedNotification Notification as it was | 
| - * received from the server. | 
| - * @param {number} notificationTimestamp The moment the unmerged card was | 
| - * received. | 
| - * @param {number} notificationGroupRank Rank of the group of the unmerged card. | 
| - * @return {MergedCard} Result of merging |unmergedNotification| into | 
| - * |mergedCard|. | 
| + * Adds a card group into a set of combined cards. | 
| + * @param {Object.<ChromeNotificationId, CombinedCard>} combinedCards Map from | 
| + * chromeNotificationId to a combined card. | 
| + * This is an input/output parameter. | 
| + * @param {StoredNotificationGroup} storedGroup Group to combine into the | 
| + * combined card set. | 
| */ | 
| -function mergeCards( | 
| - mergedCard, | 
| - unmergedNotification, | 
| - notificationTimestamp, | 
| - notificationGroupRank) { | 
| - var result = mergedCard || {dismissals: []}; | 
| - | 
| - var priority = mergedCard ? | 
| - Math.max( | 
| - mergedCard.notification.priority, | 
| - unmergedNotification.chromeNotificationOptions.priority) : | 
| - unmergedNotification.chromeNotificationOptions.priority; | 
| - | 
| - if (!mergedCard || notificationGroupRank > mergedCard.groupRank) { | 
| - result.groupRank = notificationGroupRank; | 
| - var showTime = unmergedNotification.trigger && | 
| - unmergedNotification.trigger.showTimeSec && | 
| - (notificationTimestamp + | 
| - unmergedNotification.trigger.showTimeSec * MS_IN_SECOND); | 
| - var hideTime = unmergedNotification.trigger && | 
| - unmergedNotification.trigger.hideTimeSec && | 
| - (notificationTimestamp + | 
| - unmergedNotification.trigger.hideTimeSec * MS_IN_SECOND); | 
| - result.trigger = { | 
| - showTime: showTime, | 
| - hideTime: hideTime | 
| +function combineGroup(combinedCards, storedGroup) { | 
| + for (var i = 0; i < storedGroup.cards.length; i++) { | 
| + /** @type {ReceivedNotification} */ | 
| + var receivedNotification = storedGroup.cards[i]; | 
| + | 
| + /** @type {UncombinedNotification} */ | 
| + var uncombinedNotification = { | 
| + receivedNotification: receivedNotification, | 
| + showTime: receivedNotification.trigger.showTimeSec && | 
| + (storedGroup.cardsTimestamp + | 
| + receivedNotification.trigger.showTimeSec * MS_IN_SECOND), | 
| + hideTime: storedGroup.cardsTimestamp + | 
| + receivedNotification.trigger.hideTimeSec * MS_IN_SECOND | 
| }; | 
| - } | 
| - if (!mergedCard || notificationTimestamp > mergedCard.timestamp) { | 
| - result.timestamp = notificationTimestamp; | 
| - result.notification = unmergedNotification.chromeNotificationOptions; | 
| - result.actionUrls = unmergedNotification.actionUrls; | 
| - result.version = unmergedNotification.version; | 
| - } | 
| - | 
| - result.locationBased = | 
| - result.locationBased || unmergedNotification.locationBased; | 
| - | 
| - result.notification.priority = priority; | 
| - var dismissalData = { | 
| - notificationId: unmergedNotification.notificationId, | 
| - parameters: unmergedNotification.dismissal | 
| - }; | 
| - result.dismissals.push(dismissalData); | 
| - | 
| - return result; | 
| -} | 
| - | 
| -/** | 
| - * Merges a card group into a set of merged cards. | 
| - * @param {Object.<string, MergedCard>} mergedCards Map from | 
| - * chromeNotificationId to a merged card. | 
| - * This is an input/output parameter. | 
| - * @param {StorageGroup} storageGroup Group to merge into the merged card set. | 
| - */ | 
| -function mergeGroup(mergedCards, storageGroup) { | 
| - for (var i = 0; i < storageGroup.cards.length; i++) { | 
| - var card = storageGroup.cards[i]; | 
| - mergedCards[card.chromeNotificationId] = mergeCards( | 
| - mergedCards[card.chromeNotificationId], | 
| - card, | 
| - storageGroup.cardsTimestamp, | 
| - storageGroup.rank); | 
| + var combinedCard = | 
| + combinedCards[receivedNotification.chromeNotificationId] || []; | 
| + combinedCard.push(uncombinedNotification); | 
| + combinedCards[receivedNotification.chromeNotificationId] = combinedCard; | 
| } | 
| } | 
| /** | 
| * Schedules next cards poll. | 
| - * @param {Object.<string, StorageGroup>} groups Map from group name to group | 
| - * information. | 
| + * @param {Object.<string, StoredNotificationGroup>} groups Map from group name | 
| + * to group information. | 
| * @param {boolean} isOptedIn True if the user is opted in to Google Now. | 
| */ | 
| function scheduleNextPoll(groups, isOptedIn) { | 
| @@ -480,30 +413,35 @@ function scheduleNextPoll(groups, isOptedIn) { | 
| } | 
| /** | 
| - * Merges notification groups into a set of Chrome notifications and shows them. | 
| - * @param {Object.<string, StorageGroup>} notificationGroups Map from group name | 
| - * to group information. | 
| - * @param {function(CardCreateInfo)=} onCardShown Optional parameter called when | 
| - * each card is shown. | 
| + * Combines notification groups into a set of Chrome notifications and shows | 
| + * them. | 
| + * @param {Object.<string, StoredNotificationGroup>} notificationGroups Map from | 
| + * group name to group information. | 
| + * @param {function(ReceivedNotification)=} onCardShown Optional parameter | 
| + * called when each card is shown. | 
| */ | 
| -function mergeAndShowNotificationCards(notificationGroups, onCardShown) { | 
| - var mergedCards = {}; | 
| +function combineAndShowNotificationCards(notificationGroups, onCardShown) { | 
| + console.log('combineAndShowNotificationCards ' + | 
| + JSON.stringify(notificationGroups)); | 
| + /** @type {Object.<ChromeNotificationId, CombinedCard>} */ | 
| + var combinedCards = {}; | 
| for (var groupName in notificationGroups) | 
| - mergeGroup(mergedCards, notificationGroups[groupName]); | 
| + combineGroup(combinedCards, notificationGroups[groupName]); | 
| - showNotificationCards(mergedCards, onCardShown); | 
| + showNotificationCards(combinedCards, onCardShown); | 
| } | 
| /** | 
| * Parses JSON response from the notification server, shows notifications and | 
| * schedules next update. | 
| * @param {string} response Server response. | 
| - * @param {function(CardCreateInfo)=} onCardShown Optional parameter called when | 
| - * each card is shown. | 
| + * @param {function(ReceivedNotification)=} onCardShown Optional parameter | 
| + * called when each card is shown. | 
| */ | 
| function parseAndShowNotificationCards(response, onCardShown) { | 
| console.log('parseAndShowNotificationCards ' + response); | 
| + /** @type {ServerResponse} */ | 
| var parsedResponse = JSON.parse(response); | 
| if (parsedResponse.googleNowDisabled) { | 
| @@ -517,29 +455,47 @@ function parseAndShowNotificationCards(response, onCardShown) { | 
| var receivedGroups = parsedResponse.groups; | 
| - // Populate groups with corresponding cards. | 
| - if (parsedResponse.notifications) { | 
| - for (var i = 0; i != parsedResponse.notifications.length; ++i) { | 
| - var card = parsedResponse.notifications[i]; | 
| - var group = receivedGroups[card.groupName]; | 
| - group.cards = group.cards || []; | 
| - group.cards.push(card); | 
| - } | 
| - } | 
| - | 
| - instrumented.storage.local.get('notificationGroups', function(items) { | 
| + instrumented.storage.local.get(['notificationGroups', 'recentDismissals'], | 
| + function(items) { | 
| console.log('parseAndShowNotificationCards-get ' + JSON.stringify(items)); | 
| 
robliao
2013/12/07 00:51:29
Style: These need to be tabbed over two spaces.
 
vadimt
2013/12/07 01:04:24
Done.
 | 
| items = items || {}; | 
| + /** @type {Object.<string, StoredNotificationGroup>} */ | 
| items.notificationGroups = items.notificationGroups || {}; | 
| + /** @type {Object.<NotificationId, number>} */ | 
| + items.recentDismissals = items.recentDismissals || {}; | 
| + | 
| + // Build a set of non-expired recent dismissals. It will be used for | 
| + // client-side filtering of cards. | 
| + /** @type {Object.<NotificationId, number>} */ | 
| + var updatedRecentDismissals = {}; | 
| + var now = Date.now(); | 
| + for (var notificationId in items.recentDismissals) { | 
| + var dismissalAge = now - items.recentDismissals[notificationId]; | 
| + if (dismissalAge < DISMISS_RETENTION_TIME_MS) { | 
| + updatedRecentDismissals[notificationId] = | 
| + items.recentDismissals[notificationId]; | 
| + } | 
| + } | 
| - var now = Date.now(); | 
| + // Populate groups with corresponding cards. | 
| + if (parsedResponse.notifications) { | 
| + for (var i = 0; i < parsedResponse.notifications.length; ++i) { | 
| + /** @type {ReceivedNotification} */ | 
| + var card = parsedResponse.notifications[i]; | 
| + if (!(card.notificationId in updatedRecentDismissals)) { | 
| + var group = receivedGroups[card.groupName]; | 
| + group.cards = group.cards || []; | 
| + group.cards.push(card); | 
| + } | 
| + } | 
| + } | 
| // Build updated set of groups. | 
| var updatedGroups = {}; | 
| for (var groupName in receivedGroups) { | 
| var receivedGroup = receivedGroups[groupName]; | 
| - var storageGroup = items.notificationGroups[groupName] || { | 
| + var storedGroup = items.notificationGroups[groupName] || { | 
| cards: [], | 
| cardsTimestamp: undefined, | 
| nextPollTime: undefined, | 
| @@ -552,10 +508,10 @@ function parseAndShowNotificationCards(response, onCardShown) { | 
| if (receivedGroup.cards) { | 
| // If the group contains a cards update, all its fields will get new | 
| // values. | 
| - storageGroup.cards = receivedGroup.cards; | 
| - storageGroup.cardsTimestamp = now; | 
| - storageGroup.rank = receivedGroup.rank; | 
| - storageGroup.nextPollTime = undefined; | 
| + storedGroup.cards = receivedGroup.cards; | 
| + storedGroup.cardsTimestamp = now; | 
| + storedGroup.rank = receivedGroup.rank; | 
| + storedGroup.nextPollTime = undefined; | 
| // The code below assigns nextPollTime a defined value if | 
| // nextPollSeconds is specified in the received group. | 
| // If the group's cards are not updated, and nextPollSeconds is | 
| @@ -565,26 +521,30 @@ function parseAndShowNotificationCards(response, onCardShown) { | 
| // 'nextPollSeconds' may be sent even for groups that don't contain cards | 
| // updates. | 
| if (receivedGroup.nextPollSeconds !== undefined) { | 
| - storageGroup.nextPollTime = | 
| + storedGroup.nextPollTime = | 
| now + receivedGroup.nextPollSeconds * MS_IN_SECOND; | 
| } | 
| - updatedGroups[groupName] = storageGroup; | 
| + updatedGroups[groupName] = storedGroup; | 
| } | 
| scheduleNextPoll(updatedGroups, !parsedResponse.googleNowDisabled); | 
| - chrome.storage.local.set({notificationGroups: updatedGroups}); | 
| - mergeAndShowNotificationCards(updatedGroups, onCardShown); | 
| + chrome.storage.local.set({ | 
| + notificationGroups: updatedGroups, | 
| + recentDismissals: updatedRecentDismissals | 
| + }); | 
| + combineAndShowNotificationCards(updatedGroups, onCardShown); | 
| recordEvent(GoogleNowEvent.CARDS_PARSE_SUCCESS); | 
| }); | 
| } | 
| /** | 
| * Update Location Cards Shown Count. | 
| - * @param {Object} cardCreateInfo Card Create Info | 
| + * @param {ReceivedNotification} receivedNotification Notification as it was | 
| + * received from the server. | 
| */ | 
| -function countLocationCard(cardCreateInfo) { | 
| - if (cardCreateInfo.locationBased) { | 
| +function countLocationCard(receivedNotification) { | 
| + if (receivedNotification.locationBased) { | 
| localStorage['locationCardsShown']++; | 
| } | 
| } | 
| @@ -620,7 +580,7 @@ function requestNotificationGroups(groupNames) { | 
| console.log('requestNotificationGroups-onloadend ' + request.status); | 
| if (request.status == HTTP_OK) { | 
| recordEvent(GoogleNowEvent.REQUEST_FOR_CARDS_SUCCESS); | 
| - parseAndShowNotificationCards(request.response, cardShownCallback); | 
| + parseAndShowNotificationCards(request.responseText, cardShownCallback); | 
| } | 
| }; | 
| @@ -644,7 +604,7 @@ function requestOptedIn(optedInCallback) { | 
| console.log( | 
| 'requestOptedIn-onloadend ' + request.status + ' ' + request.response); | 
| if (request.status == HTTP_OK) { | 
| - var parsedResponse = JSON.parse(request.response); | 
| + var parsedResponse = JSON.parse(request.responseText); | 
| if (parsedResponse.value) { | 
| chrome.storage.local.set({googleNowEnabled: true}); | 
| optedInCallback(); | 
| @@ -664,7 +624,7 @@ function requestOptedIn(optedInCallback) { | 
| /** | 
| * Requests notification cards from the server. | 
| - * @param {Location} position Location of this computer. | 
| + * @param {Location=} position Location of this computer. | 
| */ | 
| function requestNotificationCards(position) { | 
| console.log('requestNotificationCards ' + JSON.stringify(position)); | 
| @@ -674,17 +634,17 @@ function requestNotificationCards(position) { | 
| console.log('requestNotificationCards-storage-get ' + | 
| JSON.stringify(items)); | 
| items = items || {}; | 
| + /** @type {Object.<string, StoredNotificationGroup>} */ | 
| + items.notificationGroups = items.notificationGroups || {}; | 
| var groupsToRequest = []; | 
| - if (items.notificationGroups) { | 
| - var now = Date.now(); | 
| + var now = Date.now(); | 
| - for (var groupName in items.notificationGroups) { | 
| - var group = items.notificationGroups[groupName]; | 
| - if (group.nextPollTime !== undefined && group.nextPollTime <= now) | 
| - groupsToRequest.push(groupName); | 
| - } | 
| + for (var groupName in items.notificationGroups) { | 
| + var group = items.notificationGroups[groupName]; | 
| + if (group.nextPollTime !== undefined && group.nextPollTime <= now) | 
| + groupsToRequest.push(groupName); | 
| } | 
| if (items.googleNowEnabled) { | 
| @@ -736,7 +696,7 @@ function stopRequestLocation() { | 
| /** | 
| * Obtains new location; requests and shows notification cards based on this | 
| * location. | 
| - * @param {Location} position Location of this computer. | 
| + * @param {Location=} position Location of this computer. | 
| */ | 
| function updateNotificationsCards(position) { | 
| console.log('updateNotificationsCards ' + JSON.stringify(position) + | 
| @@ -760,7 +720,8 @@ function updateNotificationsCards(position) { | 
| /** | 
| * Sends a server request to dismiss a card. | 
| - * @param {string} chromeNotificationId chrome.notifications ID of the card. | 
| + * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of | 
| + * the card. | 
| * @param {number} dismissalTimeMs Time of the user's dismissal of the card in | 
| * milliseconds since epoch. | 
| * @param {DismissalData} dismissalData Data to build a dismissal request. | 
| @@ -769,8 +730,9 @@ function updateNotificationsCards(position) { | 
| */ | 
| function requestCardDismissal( | 
| chromeNotificationId, dismissalTimeMs, dismissalData, callbackBoolean) { | 
| - console.log('requestDismissingCard ' + chromeNotificationId + ' from ' + | 
| - NOTIFICATION_CARDS_URL); | 
| + console.log('requestDismissingCard ' + chromeNotificationId + | 
| + ' from ' + NOTIFICATION_CARDS_URL + | 
| + ', dismissalData=' + JSON.stringify(dismissalData)); | 
| var dismissalAge = Date.now() - dismissalTimeMs; | 
| @@ -781,16 +743,17 @@ function requestCardDismissal( | 
| recordEvent(GoogleNowEvent.DISMISS_REQUEST_TOTAL); | 
| - var request = 'notifications/' + dismissalData.notificationId + | 
| + var requestParameters = 'notifications/' + dismissalData.notificationId + | 
| '?age=' + dismissalAge + | 
| '&chromeNotificationId=' + chromeNotificationId; | 
| for (var paramField in dismissalData.parameters) | 
| - request += ('&' + paramField + '=' + dismissalData.parameters[paramField]); | 
| + requestParameters += ('&' + paramField + | 
| + '=' + dismissalData.parameters[paramField]); | 
| - console.log('requestCardDismissal: request=' + request); | 
| + console.log('requestCardDismissal: requestParameters=' + requestParameters); | 
| - var request = buildServerRequest('DELETE', request); | 
| + var request = buildServerRequest('DELETE', requestParameters); | 
| request.onloadend = function(event) { | 
| console.log('requestDismissingCard-onloadend ' + request.status); | 
| if (request.status == HTTP_NOCONTENT) | 
| @@ -823,7 +786,9 @@ function processPendingDismissals(callbackBoolean) { | 
| console.log('processPendingDismissals-storage-get ' + | 
| JSON.stringify(items)); | 
| items = items || {}; | 
| + /** @type {Array.<PendingDismissal>} */ | 
| items.pendingDismissals = items.pendingDismissals || []; | 
| + /** @type {Object.<NotificationId, number>} */ | 
| items.recentDismissals = items.recentDismissals || {}; | 
| var dismissalsChanged = false; | 
| @@ -847,6 +812,7 @@ function processPendingDismissals(callbackBoolean) { | 
| // Send dismissal for the first card, and if successful, repeat | 
| // recursively with the rest. | 
| + /** @type {PendingDismissal} */ | 
| var dismissal = items.pendingDismissals[0]; | 
| requestCardDismissal( | 
| dismissal.chromeNotificationId, | 
| @@ -856,7 +822,8 @@ function processPendingDismissals(callbackBoolean) { | 
| if (done) { | 
| dismissalsChanged = true; | 
| items.pendingDismissals.splice(0, 1); | 
| - items.recentDismissals[dismissal.chromeNotificationId] = | 
| + items.recentDismissals[ | 
| + dismissal.dismissalData.notificationId] = | 
| Date.now(); | 
| doProcessDismissals(); | 
| } else { | 
| @@ -895,12 +862,15 @@ function openUrl(url) { | 
| /** | 
| * Opens URL corresponding to the clicked part of the notification. | 
| - * @param {string} chromeNotificationId chrome.notifications ID of the card. | 
| - * @param {function(Object): string} selector Function that extracts the url for | 
| - * the clicked area from the button action URLs info. | 
| + * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of | 
| + * the card. | 
| + * @param {function((ActionUrls|undefined)): (string|undefined)} selector | 
| + * Function that extracts the url for the clicked area from the button | 
| + * action URLs info. | 
| */ | 
| function onNotificationClicked(chromeNotificationId, selector) { | 
| instrumented.storage.local.get('notificationsData', function(items) { | 
| + /** @type {(NotificationDataEntry|undefined)} */ | 
| var notificationData = items && | 
| items.notificationsData && | 
| items.notificationsData[chromeNotificationId]; | 
| @@ -918,7 +888,8 @@ function onNotificationClicked(chromeNotificationId, selector) { | 
| /** | 
| * Callback for chrome.notifications.onClosed event. | 
| - * @param {string} chromeNotificationId chrome.notifications ID of the card. | 
| + * @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID of | 
| + * the card. | 
| * @param {boolean} byUser Whether the notification was closed by the user. | 
| */ | 
| function onNotificationClosed(chromeNotificationId, byUser) { | 
| @@ -934,28 +905,37 @@ function onNotificationClosed(chromeNotificationId, byUser) { | 
| instrumented.storage.local.get( | 
| ['pendingDismissals', 'notificationsData'], function(items) { | 
| items = items || {}; | 
| + /** @type {Array.<PendingDismissal>} */ | 
| items.pendingDismissals = items.pendingDismissals || []; | 
| + /** @type {Object.<string, NotificationDataEntry>} */ | 
| items.notificationsData = items.notificationsData || {}; | 
| - // Deleting the notification in case it was re-added while this task was | 
| - // scheduled, waiting for execution; also cleaning notification's data | 
| - // from storage. | 
| - cardSet.clear(chromeNotificationId, true); | 
| + /** @type {NotificationDataEntry} */ | 
| + var notificationData = items.notificationsData[chromeNotificationId] || { | 
| + timestamp: Date.now(), | 
| + combinedCard: [] | 
| + }; | 
| - var notificationData = items.notificationsData[chromeNotificationId]; | 
| + var dismissalResult = | 
| + cardSet.onDismissal(chromeNotificationId, notificationData); | 
| + | 
| + for (var i = 0; i < dismissalResult.dismissals.length; i++) { | 
| + /** @type {PendingDismissal} */ | 
| + var dismissal = { | 
| + chromeNotificationId: chromeNotificationId, | 
| + time: Date.now(), | 
| + dismissalData: dismissalResult.dismissals[i] | 
| + }; | 
| + items.pendingDismissals.push(dismissal); | 
| + } | 
| - if (notificationData && notificationData.dismissals) { | 
| - for (var i = 0; i < notificationData.dismissals.length; i++) { | 
| - var dismissal = { | 
| - chromeNotificationId: chromeNotificationId, | 
| - time: Date.now(), | 
| - dismissalData: notificationData.dismissals[i] | 
| - }; | 
| - items.pendingDismissals.push(dismissal); | 
| - } | 
| + items.notificationsData[chromeNotificationId] = | 
| + dismissalResult.notificationData; | 
| - chrome.storage.local.set({pendingDismissals: items.pendingDismissals}); | 
| - } | 
| + chrome.storage.local.set({ | 
| + pendingDismissals: items.pendingDismissals, | 
| + notificationsData: items.notificationsData | 
| + }); | 
| processPendingDismissals(function(success) {}); | 
| }); | 
| @@ -1140,9 +1120,10 @@ instrumented.runtime.onStartup.addListener(function() { | 
| instrumented.storage.local.get('notificationGroups', function(items) { | 
| console.log('onStartup-get ' + JSON.stringify(items)); | 
| items = items || {}; | 
| + /** @type {Object.<string, StoredNotificationGroup>} */ | 
| items.notificationGroups = items.notificationGroups || {}; | 
| - mergeAndShowNotificationCards(items.notificationGroups); | 
| + combineAndShowNotificationCards(items.notificationGroups); | 
| }); | 
| }); | 
| @@ -1178,7 +1159,7 @@ instrumented.notifications.onButtonClicked.addListener( | 
| 'GoogleNow.ButtonClicked' + buttonIndex); | 
| onNotificationClicked(chromeNotificationId, function(actionUrls) { | 
| var url = actionUrls.buttonUrls[buttonIndex]; | 
| - verify(url, 'onButtonClicked: no url for a button'); | 
| + verify(url !== undefined, 'onButtonClicked: no url for a button'); | 
| return url; | 
| }); | 
| }); | 
| @@ -1222,6 +1203,7 @@ instrumented.pushMessaging.onMessage.addListener(function(message) { | 
| message.payload) { | 
| items.lastPollNowPayloads[message.subchannelId] = message.payload; | 
| + /** @type {Object.<string, StoredNotificationGroup>} */ | 
| items.notificationGroups = items.notificationGroups || {}; | 
| items.notificationGroups['PUSH' + message.subchannelId] = { | 
| cards: [], |