// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/**
* Show/hide trigger in a card.
*
* @typedef {{
* 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: NotificationId,
* parameters: Object
* }}
*/
var DismissalData;
/**
* 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,
* 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 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 UPDATE_CARD_TASK_NAME = 'update-card';
/**
* Builds an object to manage notification card set.
* @return {Object} Card set interface.
*/
function buildCardSet() {
var alarmPrefix = 'card-';
/**
* 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 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;
}
if (onCardShown !== undefined)
onCardShown(receivedNotification);
});
}
});
}
/**
* 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 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);
}
}
/**
* 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));
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;
// 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;
}
}
// 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 || null) &&
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;
}
}
/**
* 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 onDismissal(cardId, notificationData, notificationGroups) {
var dismissals = [];
var newCombinedCard = [];
// 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 {
dismissals: dismissals,
notificationData: update(cardId, newCombinedCard, notificationGroups)
};
}
/**
* Removes card information from |notificationGroups|.
* @param {ChromeNotificationId} cardId Card ID.
* @param {Object.<string, StoredNotificationGroup>} notificationGroups
* Map from group name to group information.
*/
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(alarmPrefix) == 0) {
// Alarm to show the card.
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 || {};
var combinedCard =
(items.notificationsData[cardId] &&
items.notificationsData[cardId].combinedCard) || [];
var cardShownCallback = undefined;
if (localStorage['locationCardsShown'] <
LOCATION_CARDS_LINK_THRESHOLD) {
cardShownCallback = countLocationCard;
}
items.notificationsData[cardId] =
update(
cardId,
combinedCard,
items.notificationGroups,
cardShownCallback);
chrome.storage.local.set(items);
});
});
}
});
return {
update: update,
onDismissal: onDismissal
};
}