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

Side by Side 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 unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright 2013 The Chromium Authors. All rights reserved. 1 // Copyright 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 'use strict'; 5 'use strict';
6 6
7 /** 7 /**
8 * Show/hide trigger in a card. 8 * Show/hide trigger in a card.
9 * 9 *
10 * @typedef {{ 10 * @typedef {{
11 * showTime: number=, 11 * showTimeSec: (string|undefined),
12 * hideTime: number= 12 * hideTimeSec: string
13 * }} 13 * }}
14 */ 14 */
15 var Trigger; 15 var Trigger;
16 16
17 /** 17 /**
18 * ID of an individual (uncombined) notification.
19 *
20 * @typedef {string}
21 */
22 var NotificationId;
23
24 /**
18 * Data to build a dismissal request for a card from a specific group. 25 * Data to build a dismissal request for a card from a specific group.
19 * 26 *
20 * @typedef {{ 27 * @typedef {{
21 * notificationId: string, 28 * notificationId: NotificationId,
22 * parameters: Object 29 * parameters: Object
23 * }} 30 * }}
24 */ 31 */
25 var DismissalData; 32 var DismissalData;
26 33
27 /** 34 /**
28 * Card merged from potentially multiple groups. 35 * Urls that need to be opened when clicking a notification or its buttons.
29 * 36 *
30 * @typedef {{ 37 * @typedef {{
38 * messageUrl: (string|undefined),
39 * buttonUrls: (Array.<string>|undefined)
40 * }}
41 */
42 var ActionUrls;
43
44 /**
45 * ID of a combined notification. This is the ID used with chrome.notifications
46 * API.
47 *
48 * @typedef {string}
49 */
50 var ChromeNotificationId;
51
52 /**
53 * Notification as sent by the server.
54 *
55 * @typedef {{
56 * notificationId: NotificationId,
57 * chromeNotificationId: ChromeNotificationId,
31 * trigger: Trigger, 58 * trigger: Trigger,
32 * version: number, 59 * chromeNotificationOptions: Object,
60 * actionUrls: (ActionUrls|undefined),
61 * dismissal: Object,
62 * locationBased: (boolean|undefined),
63 * groupName: string
64 * }}
65 */
66 var ReceivedNotification;
67
68 /**
69 * Received notification in a self-sufficient form that doesn't require group's
70 * timestamp to calculate show and hide times.
71 *
72 * @typedef {{
73 * receivedNotification: ReceivedNotification,
74 * showTime: (number|undefined),
75 * hideTime: number
76 * }}
77 */
78 var UncombinedNotification;
79
80 /**
81 * Card combined from potentially multiple groups.
82 *
83 * @typedef {Array.<UncombinedNotification>}
84 */
85 var CombinedCard;
86
87 /**
88 * Data entry that we store for every Chrome notification.
89 * |timestamp| is the time when corresponding Chrome notification was created or
90 * updated last time by cardSet.update().
91 *
92 * @typedef {{
93 * actionUrls: (ActionUrls|undefined),
33 * timestamp: number, 94 * timestamp: number,
34 * notification: Object, 95 * combinedCard: CombinedCard
35 * actionUrls: Object=, 96 * }}
36 * groupRank: number, 97 *
37 * dismissals: Array.<DismissalData>, 98 */
38 * locationBased: boolean= 99 var NotificationDataEntry;
39 * }}
40 */
41 var MergedCard;
42
43 /**
44 * Set of parameters for creating card notification.
45 *
46 * @typedef {{
47 * notification: Object,
48 * hideTime: number=,
49 * version: number,
50 * previousVersion: number=,
51 * locationBased: boolean=
52 * }}
53 */
54 var CardCreateInfo;
55 100
56 /** 101 /**
57 * Names for tasks that can be created by the this file. 102 * Names for tasks that can be created by the this file.
58 */ 103 */
59 var SHOW_CARD_TASK_NAME = 'show-card'; 104 var UPDATE_CARD_TASK_NAME = 'update-card';
60 var CLEAR_CARD_TASK_NAME = 'clear-card';
61 105
62 /** 106 /**
63 * Builds an object to manage notification card set. 107 * Builds an object to manage notification card set.
64 * @return {Object} Card set interface. 108 * @return {Object} Card set interface.
65 */ 109 */
66 function buildCardSet() { 110 function buildCardSet() {
67 var cardShowPrefix = 'card-show-'; 111 var alarmPrefix = 'card-';
68 var cardHidePrefix = 'card-hide-'; 112
69 113 /**
70 /** 114 * Creates/updates/deletes a Chrome notification.
71 * Schedules hiding a notification. 115 * @param {ChromeNotificationId} cardId Card ID.
72 * @param {string} cardId Card ID. 116 * @param {?ReceivedNotification} receivedNotification Google Now card
73 * @param {number=} opt_timeHide If specified, epoch time to hide the card. If 117 * represented as a set of parameters for showing a Chrome notification,
74 * undefined, the card will be kept shown at least until next update. 118 * or null if the notification needs to be deleted.
75 */ 119 * @param {function(ReceivedNotification)=} onCardShown Optional parameter
76 function scheduleHiding(cardId, opt_timeHide) { 120 * called when each card is shown.
77 if (opt_timeHide === undefined) 121 */
78 return; 122 function updateNotification(cardId, receivedNotification, onCardShown) {
79 123 console.log('cardManager.updateNotification ' + cardId + ' ' +
80 var alarmName = cardHidePrefix + cardId; 124 JSON.stringify(receivedNotification));
81 var alarmInfo = {when: opt_timeHide}; 125
82 chrome.alarms.create(alarmName, alarmInfo); 126 if (!receivedNotification) {
83 } 127 instrumented.notifications.clear(cardId, function() {});
84
85 /**
86 * Shows a notification.
87 * @param {string} cardId Card ID.
88 * @param {CardCreateInfo} cardCreateInfo Google Now card represented as a set
89 * of parameters for showing a Chrome notification.
90 * @param {function(CardCreateInfo)=} onCardShown Optional parameter called
91 * when each card is shown.
92 */
93 function showNotification(cardId, cardCreateInfo, onCardShown) {
94 console.log('cardManager.showNotification ' + cardId + ' ' +
95 JSON.stringify(cardCreateInfo));
96
97 if (cardCreateInfo.hideTime <= Date.now()) {
98 console.log('cardManager.showNotification ' + cardId + ': expired');
99 // Card has expired. Schedule hiding to delete asociated information.
100 scheduleHiding(cardId, cardCreateInfo.hideTime);
101 return; 128 return;
102 } 129 }
103 130
104 if (cardCreateInfo.previousVersion !== cardCreateInfo.version) { 131 // Try updating the notification.
105 // Delete a notification with the specified id if it already exists, and 132 instrumented.notifications.update(
106 // then create a notification. 133 cardId,
107 instrumented.notifications.create( 134 receivedNotification.chromeNotificationOptions,
108 cardId, 135 function(wasUpdated) {
109 cardCreateInfo.notification, 136 if (!wasUpdated) {
110 function(newNotificationId) { 137 // If the notification wasn't updated, it probably didn't exist.
111 if (!newNotificationId || chrome.runtime.lastError) { 138 // Create it.
112 var errorMessage = chrome.runtime.lastError && 139 console.log('cardManager.updateNotification ' + cardId +
113 chrome.runtime.lastError.message; 140 ' failed to update, creating');
114 console.error('notifications.create: ID=' + newNotificationId + 141 instrumented.notifications.create(
115 ', ERROR=' + errorMessage); 142 cardId,
116 return; 143 receivedNotification.chromeNotificationOptions,
144 function(newNotificationId) {
145 if (!newNotificationId || chrome.runtime.lastError) {
146 var errorMessage = chrome.runtime.lastError &&
147 chrome.runtime.lastError.message;
148 console.error('notifications.create: ID=' +
149 newNotificationId + ', ERROR=' + errorMessage);
150 return;
151 }
152
153 if (onCardShown !== undefined)
154 onCardShown(receivedNotification);
155 });
156 }
157 });
158 }
159
160 /**
161 * Iterates uncombined notifications in a combined card, determining for
162 * each whether it's visible at the specified moment.
163 * @param {CombinedCard} combinedCard The combined card in question.
164 * @param {number} timestamp Time for which to calculate visibility.
165 * @param {function(UncombinedNotification, boolean)} callback Function
166 * invoked for every uncombined notification in |combinedCard|.
167 * The boolean parameter indicates whether the uncombined notification is
168 * visible at |timestamp|.
169 */
170 function iterateUncombinedNotifications(combinedCard, timestamp, callback) {
171 for (var i = 0; i != combinedCard.length; ++i) {
172 var uncombinedNotification = combinedCard[i];
173 var shouldShow = !uncombinedNotification.showTime ||
174 uncombinedNotification.showTime <= timestamp;
175 var shouldHide = uncombinedNotification.hideTime <= timestamp;
176
177 callback(uncombinedNotification, shouldShow && !shouldHide);
178 }
179 }
180
181 /**
182 * Refreshes (shows/hides) the notification corresponding to the combined card
183 * based on the current time and show-hide intervals in the combined card.
184 * @param {ChromeNotificationId} cardId Card ID.
185 * @param {CombinedCard} combinedCard Combined cards with |cardId|.
186 * @param {function(ReceivedNotification)=} onCardShown Optional parameter
187 * called when each card is shown.
188 * @return {(NotificationDataEntry|undefined)} Notification data entry for
189 * this card. It's 'undefined' if the card's life is over.
190 */
191 function update(cardId, combinedCard, onCardShown) {
192 console.log('cardManager.update ' + JSON.stringify(combinedCard));
193
194 chrome.alarms.clear(alarmPrefix + cardId);
195 var now = Date.now();
196 /** @type {?UncombinedNotification} */
197 var winningCard = null;
198 // Next moment of time when winning notification selection algotithm can
199 // potentially return a different notification.
200 /** @type {?number} */
201 var nextEventTime = null;
202
203 // Find a winning uncombined notification: a highest-priority notification
204 // that needs to be shown now.
205 iterateUncombinedNotifications(
206 combinedCard,
207 now,
208 function(uncombinedCard, visible) {
209 // If the uncombined notification is visible now and set the winning
210 // card to it if its priority is higher.
211 if (visible) {
212 if (!winningCard ||
213 uncombinedCard.receivedNotification.chromeNotificationOptions.
214 priority >
215 winningCard.receivedNotification.chromeNotificationOptions.
216 priority) {
217 winningCard = uncombinedCard;
117 } 218 }
118 219 }
119 if (onCardShown !== undefined) 220
120 onCardShown(cardCreateInfo); 221 // Next event time is the closest hide or show event.
121 222 if (uncombinedCard.showTime && uncombinedCard.showTime > now) {
122 scheduleHiding(cardId, cardCreateInfo.hideTime); 223 if (!nextEventTime || nextEventTime > uncombinedCard.showTime)
224 nextEventTime = uncombinedCard.showTime;
225 }
226 if (uncombinedCard.hideTime > now) {
227 if (!nextEventTime || nextEventTime > uncombinedCard.hideTime)
228 nextEventTime = uncombinedCard.hideTime;
229 }
230 });
231
232 // Show/hide the winning card.
233 updateNotification(
234 cardId, winningCard && winningCard.receivedNotification, onCardShown);
235
236 if (nextEventTime) {
237 // If we expect more events, create an alarm for the next one.
238 chrome.alarms.create(alarmPrefix + cardId, {when: nextEventTime});
239
240 // The trick with stringify/parse is to create a copy of action URLs,
241 // otherwise notifications data with 2 pointers to the same object won't
242 // be stored correctly to chrome.storage.
243 var winningActionUrls = winningCard &&
244 JSON.parse(JSON.stringify(
245 winningCard.receivedNotification.actionUrls));
246
247 return {
248 actionUrls: winningActionUrls,
249 timestamp: now,
250 combinedCard: combinedCard
251 };
252 } else {
253 // If there are no more events, we are done with this card. Note that all
254 // received notifications have hideTime.
255 verify(!winningCard, 'No events left, but card is shown.');
256 clearCardFromGroups(cardId);
257 return undefined;
258 }
259 }
260
261 /**
262 * Removes dismissed part of a card and refreshes the card. Returns remaining
263 * dismissals for the combined card and updated notification data.
264 * @param {ChromeNotificationId} cardId Card ID.
265 * @param {NotificationDataEntry} notificationData Stored notification entry
266 * for this card.
267 * @return {{
268 * dismissals: Array.<DismissalData>,
269 * notificationData: (NotificationDataEntry|undefined)
270 * }}
271 */
272 function onDismissal(cardId, notificationData) {
273 var dismissals = [];
274 var newCombinedCard = [];
275
276 // Determine which parts of the combined card need to be dismissed or to be
277 // preserved. We dismiss parts that were visible at the moment when the card
278 // was last updated.
279 iterateUncombinedNotifications(
280 notificationData.combinedCard,
281 notificationData.timestamp,
282 function(uncombinedCard, visible) {
283 if (visible) {
284 dismissals.push({
285 notificationId: uncombinedCard.receivedNotification.notificationId,
286 parameters: uncombinedCard.receivedNotification.dismissal
123 }); 287 });
124 } else { 288 } else {
125 // Update existing notification. 289 newCombinedCard.push(uncombinedCard);
126 instrumented.notifications.update( 290 }
127 cardId, 291 });
128 cardCreateInfo.notification, 292
129 function(wasUpdated) { 293 return {
130 if (!wasUpdated || chrome.runtime.lastError) { 294 dismissals: dismissals,
131 var errorMessage = chrome.runtime.lastError && 295 notificationData: update(cardId, newCombinedCard)
132 chrome.runtime.lastError.message;
133 console.error('notifications.update: UPDATED=' + wasUpdated +
134 ', ERROR=' + errorMessage);
135 return;
136 }
137
138 scheduleHiding(cardId, cardCreateInfo.hideTime);
139 });
140 }
141 }
142
143 /**
144 * Updates/creates a card notification with new data.
145 * @param {string} cardId Card ID.
146 * @param {MergedCard} card Google Now card from the server.
147 * @param {number=} previousVersion The version of the shown card with
148 * this id, if it exists, undefined otherwise.
149 * @param {function(CardCreateInfo)=} onCardShown Optional parameter called
150 * when each card is shown.
151 * @return {Object} Notification data entry for this card.
152 */
153 function update(cardId, card, previousVersion, onCardShown) {
154 console.log('cardManager.update ' + JSON.stringify(card) + ' ' +
155 previousVersion);
156
157 chrome.alarms.clear(cardHidePrefix + cardId);
158
159 var cardCreateInfo = {
160 notification: card.notification,
161 hideTime: card.trigger.hideTime,
162 version: card.version,
163 previousVersion: previousVersion,
164 locationBased: card.locationBased
165 }; 296 };
166 297 }
167 var shownImmediately = false; 298
168 var cardShowAlarmName = cardShowPrefix + cardId; 299 /**
169 if (card.trigger.showTime && card.trigger.showTime > Date.now()) { 300 * Removes card information from 'notificationGroups'.
170 // Card needs to be shown later. 301 * @param {ChromeNotificationId} cardId Card ID.
171 console.log('cardManager.update: postponed'); 302 */
172 var alarmInfo = { 303 function clearCardFromGroups(cardId) {
173 when: card.trigger.showTime 304 console.log('cardManager.clearCardFromGroups ' + cardId);
174 }; 305
175 chrome.alarms.create(cardShowAlarmName, alarmInfo); 306 instrumented.storage.local.get('notificationGroups', function(items) {
176 } else { 307 items = items || {};
177 // Card needs to be shown immediately. 308 /** @type {Object.<string, StoredNotificationGroup>} */
178 console.log('cardManager.update: immediate'); 309 items.notificationGroups = items.notificationGroups || {};
179 chrome.alarms.clear(cardShowAlarmName); 310
180 showNotification(cardId, cardCreateInfo, onCardShown); 311 for (var groupName in items.notificationGroups) {
181 } 312 var group = items.notificationGroups[groupName];
182 313 for (var i = 0; i != group.cards.length; ++i) {
183 return { 314 if (group.cards[i].chromeNotificationId == cardId) {
184 actionUrls: card.actionUrls, 315 group.cards.splice(i, 1);
185 cardCreateInfo: cardCreateInfo, 316 break;
186 dismissals: card.dismissals 317 }
187 }; 318 }
188 } 319 }
189 320
190 /** 321 chrome.storage.local.set(items);
191 * Removes a card notification. 322 });
192 * @param {string} cardId Card ID.
193 * @param {boolean} clearStorage True if the information associated with the
194 * card should be erased from chrome.storage.
195 */
196 function clear(cardId, clearStorage) {
197 console.log('cardManager.clear ' + cardId);
198
199 chrome.notifications.clear(cardId, function() {});
200 chrome.alarms.clear(cardShowPrefix + cardId);
201 chrome.alarms.clear(cardHidePrefix + cardId);
202
203 if (clearStorage) {
204 instrumented.storage.local.get(
205 ['notificationsData', 'notificationGroups'],
206 function(items) {
207 items = items || {};
208 items.notificationsData = items.notificationsData || {};
209 items.notificationGroups = items.notificationGroups || {};
210
211 delete items.notificationsData[cardId];
212
213 for (var groupName in items.notificationGroups) {
214 var group = items.notificationGroups[groupName];
215 for (var i = 0; i != group.cards.length; ++i) {
216 if (group.cards[i].chromeNotificationId == cardId) {
217 group.cards.splice(i, 1);
218 break;
219 }
220 }
221 }
222
223 chrome.storage.local.set(items);
224 });
225 }
226 } 323 }
227 324
228 instrumented.alarms.onAlarm.addListener(function(alarm) { 325 instrumented.alarms.onAlarm.addListener(function(alarm) {
229 console.log('cardManager.onAlarm ' + JSON.stringify(alarm)); 326 console.log('cardManager.onAlarm ' + JSON.stringify(alarm));
230 327
231 if (alarm.name.indexOf(cardShowPrefix) == 0) { 328 if (alarm.name.indexOf(alarmPrefix) == 0) {
232 // Alarm to show the card. 329 // Alarm to show the card.
233 tasks.add(SHOW_CARD_TASK_NAME, function() { 330 tasks.add(UPDATE_CARD_TASK_NAME, function() {
234 var cardId = alarm.name.substring(cardShowPrefix.length); 331 var cardId = alarm.name.substring(alarmPrefix.length);
235 instrumented.storage.local.get('notificationsData', function(items) { 332 instrumented.storage.local.get('notificationsData', function(items) {
236 console.log('cardManager.onAlarm.get ' + JSON.stringify(items)); 333 console.log('cardManager.onAlarm.get ' + JSON.stringify(items));
237 if (!items || !items.notificationsData) 334 items = items || {};
238 return; 335 /** @type {Object.<string, NotificationDataEntry>} */
239 var notificationData = items.notificationsData[cardId]; 336 items.notificationsData = items.notificationsData || {};
240 if (!notificationData) 337 var combinedCard =
241 return; 338 (items.notificationsData[cardId] &&
339 items.notificationsData[cardId].combinedCard) || [];
242 340
243 var cardShownCallback = undefined; 341 var cardShownCallback = undefined;
244 if (localStorage['locationCardsShown'] < 342 if (localStorage['locationCardsShown'] <
245 LOCATION_CARDS_LINK_THRESHOLD) { 343 LOCATION_CARDS_LINK_THRESHOLD) {
246 cardShownCallback = countLocationCard; 344 cardShownCallback = countLocationCard;
247 } 345 }
248 346
249 showNotification( 347 items.notificationsData[cardId] =
250 cardId, notificationData.cardCreateInfo, cardShownCallback); 348 update(cardId, combinedCard, cardShownCallback);
349
350 chrome.storage.local.set(items);
251 }); 351 });
252 }); 352 });
253 } else if (alarm.name.indexOf(cardHidePrefix) == 0) {
254 // Alarm to hide the card.
255 tasks.add(CLEAR_CARD_TASK_NAME, function() {
256 var cardId = alarm.name.substring(cardHidePrefix.length);
257 clear(cardId, true);
258 });
259 } 353 }
260 }); 354 });
261 355
262 return { 356 return {
263 update: update, 357 update: update,
264 clear: clear 358 onDismissal: onDismissal
265 }; 359 };
266 } 360 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698