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

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

Powered by Google App Engine
This is Rietveld 408576698