blob: d14479aca2052cbb73e260c7e989610b92a0d311 [file] [log] [blame]
// 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.
* This ID comes directly from the server.
*
* @typedef {string}
*/
var ServerNotificationId;
/**
* Data to build a dismissal request for a card from a specific group.
*
* @typedef {{
* notificationId: ServerNotificationId,
* 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: ServerNotificationId,
* chromeNotificationId: ChromeNotificationId,
* trigger: Trigger,
* chromeNotificationOptions: Object,
* actionUrls: (ActionUrls|undefined),
* dismissal: Object,
* locationBased: (boolean|undefined),
* groupName: string,
* cardTypeId: (number|undefined)
* }}
*/
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),
* cardTypeId: (number|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} chromeNotificationId chrome.notifications ID
* of the card.
* @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 updateNotification(
chromeNotificationId, receivedNotification, onCardShown) {
console.log(
'cardManager.updateNotification ' + chromeNotificationId + ' ' +
JSON.stringify(receivedNotification));
if (!receivedNotification) {
instrumented.notifications.clear(chromeNotificationId, function() {});
return;
}
// Try updating the notification.
instrumented.notifications.update(
chromeNotificationId,
receivedNotification.chromeNotificationOptions,
function(wasUpdated) {
if (!wasUpdated) {
// If the notification wasn't updated, it probably didn't exist.
// Create it.
console.log(
'cardManager.updateNotification ' + chromeNotificationId +
' not updated, creating instead');
instrumented.notifications.create(
chromeNotificationId,
receivedNotification.chromeNotificationOptions,
function(newChromeNotificationId) {
if (!newChromeNotificationId || chrome.runtime.lastError) {
var errorMessage = chrome.runtime.lastError &&
chrome.runtime.lastError.message;
console.error('notifications.create: ID=' +
newChromeNotificationId + ', 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} chromeNotificationId chrome.notifications ID
* of the card.
* @param {CombinedCard} combinedCard Combined cards with
* |chromeNotificationId|.
* @param {Object<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(
chromeNotificationId, combinedCard, notificationGroups, onCardShown) {
console.log('cardManager.update ' + JSON.stringify(combinedCard));
chrome.alarms.clear(alarmPrefix + chromeNotificationId);
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;
// 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(
chromeNotificationId,
winningCard && winningCard.receivedNotification,
onCardShown);
if (nextEventTime) {
// If we expect more events, create an alarm for the next one.
chrome.alarms.create(
alarmPrefix + chromeNotificationId, {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));
var winningCardTypeId = winningCard &&
winningCard.receivedNotification.cardTypeId;
return {
actionUrls: winningActionUrls,
cardTypeId: winningCardTypeId,
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(chromeNotificationId, 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} chromeNotificationId chrome.notifications ID
* of the card.
* @param {NotificationDataEntry} notificationData Stored notification entry
* for this card.
* @param {Object<StoredNotificationGroup>} notificationGroups Map from group
* name to group information.
* @return {{
* dismissals: Array<DismissalData>,
* notificationData: (NotificationDataEntry|undefined)
* }}
*/
function onDismissal(
chromeNotificationId, notificationData, notificationGroups) {
/** @type {Array<DismissalData>} */
var dismissals = [];
/** @type {Array<UncombinedNotification>} */
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(
chromeNotificationId, newCombinedCard, notificationGroups)
};
}
/**
* Removes card information from |notificationGroups|.
* @param {ChromeNotificationId} chromeNotificationId chrome.notifications ID
* of the card.
* @param {Object<StoredNotificationGroup>} notificationGroups Map from group
* name to group information.
*/
function clearCardFromGroups(chromeNotificationId, notificationGroups) {
console.log('cardManager.clearCardFromGroups ' + chromeNotificationId);
for (var groupName in notificationGroups) {
var group = notificationGroups[groupName];
for (var i = 0; i != group.cards.length; ++i) {
if (group.cards[i].chromeNotificationId == chromeNotificationId) {
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() {
/** @type {ChromeNotificationId} */
var chromeNotificationId = alarm.name.substring(alarmPrefix.length);
fillFromChromeLocalStorage({
/** @type {Object<ChromeNotificationId, NotificationDataEntry>} */
notificationsData: {},
/** @type {Object<StoredNotificationGroup>} */
notificationGroups: {}
}).then(function(items) {
console.log('cardManager.onAlarm.get ' + JSON.stringify(items));
var combinedCard =
(items.notificationsData[chromeNotificationId] &&
items.notificationsData[chromeNotificationId].combinedCard) || [];
var cardShownCallback = undefined;
if (localStorage['explanatoryCardsShown'] <
EXPLANATORY_CARDS_LINK_THRESHOLD) {
cardShownCallback = countExplanatoryCard;
}
items.notificationsData[chromeNotificationId] =
update(
chromeNotificationId,
combinedCard,
items.notificationGroups,
cardShownCallback);
chrome.storage.local.set(items);
});
});
}
});
return {
update: update,
onDismissal: onDismissal
};
}