OLD | NEW |
1 // Copyright (c) 2013 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 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 * @fileoverview The event page for Google Now for Chrome implementation. | 8 * @fileoverview The event page for Google Now for Chrome implementation. |
9 * The Google Now event page gets Google Now cards from the server and shows | 9 * The Google Now event page gets Google Now cards from the server and shows |
10 * them as Chrome notifications. | 10 * them as Chrome notifications. |
(...skipping 77 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
88 */ | 88 */ |
89 var DEFAULT_OPTIN_CHECK_PERIOD_SECONDS = 60 * 60 * 24 * 7; // 1 week | 89 var DEFAULT_OPTIN_CHECK_PERIOD_SECONDS = 60 * 60 * 24 * 7; // 1 week |
90 | 90 |
91 /** | 91 /** |
92 * URL to open when the user clicked on a link for the our notification | 92 * URL to open when the user clicked on a link for the our notification |
93 * settings. | 93 * settings. |
94 */ | 94 */ |
95 var SETTINGS_URL = 'https://support.google.com/chrome/?p=ib_google_now_welcome'; | 95 var SETTINGS_URL = 'https://support.google.com/chrome/?p=ib_google_now_welcome'; |
96 | 96 |
97 /** | 97 /** |
| 98 * GCM registration URL. |
| 99 */ |
| 100 var GCM_REGISTRATION_URL = |
| 101 'https://android.googleapis.com/gcm/googlenotification'; |
| 102 |
| 103 /** |
| 104 * DevConsole project ID for GCM API use. |
| 105 */ |
| 106 var GCM_PROJECT_ID = '437902709571'; |
| 107 |
| 108 /** |
98 * Number of cards that need an explanatory link. | 109 * Number of cards that need an explanatory link. |
99 */ | 110 */ |
100 var EXPLANATORY_CARDS_LINK_THRESHOLD = 4; | 111 var EXPLANATORY_CARDS_LINK_THRESHOLD = 4; |
101 | 112 |
102 /** | 113 /** |
103 * Names for tasks that can be created by the extension. | 114 * Names for tasks that can be created by the extension. |
104 */ | 115 */ |
105 var UPDATE_CARDS_TASK_NAME = 'update-cards'; | 116 var UPDATE_CARDS_TASK_NAME = 'update-cards'; |
106 var DISMISS_CARD_TASK_NAME = 'dismiss-card'; | 117 var DISMISS_CARD_TASK_NAME = 'dismiss-card'; |
107 var RETRY_DISMISS_TASK_NAME = 'retry-dismiss'; | 118 var RETRY_DISMISS_TASK_NAME = 'retry-dismiss'; |
(...skipping 74 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
182 // send dismissals is scheduled. | 193 // send dismissals is scheduled. |
183 return true; | 194 return true; |
184 } | 195 } |
185 | 196 |
186 return false; | 197 return false; |
187 } | 198 } |
188 | 199 |
189 var tasks = buildTaskManager(areTasksConflicting); | 200 var tasks = buildTaskManager(areTasksConflicting); |
190 | 201 |
191 // Add error processing to API calls. | 202 // Add error processing to API calls. |
| 203 wrapper.instrumentChromeApiFunction('gcm.onMessage.addListener', 0); |
| 204 wrapper.instrumentChromeApiFunction('gcm.register', 1); |
192 wrapper.instrumentChromeApiFunction('metricsPrivate.getVariationParams', 1); | 205 wrapper.instrumentChromeApiFunction('metricsPrivate.getVariationParams', 1); |
193 wrapper.instrumentChromeApiFunction('notifications.clear', 1); | 206 wrapper.instrumentChromeApiFunction('notifications.clear', 1); |
194 wrapper.instrumentChromeApiFunction('notifications.create', 2); | 207 wrapper.instrumentChromeApiFunction('notifications.create', 2); |
195 wrapper.instrumentChromeApiFunction('notifications.getPermissionLevel', 0); | 208 wrapper.instrumentChromeApiFunction('notifications.getPermissionLevel', 0); |
196 wrapper.instrumentChromeApiFunction('notifications.update', 2); | 209 wrapper.instrumentChromeApiFunction('notifications.update', 2); |
197 wrapper.instrumentChromeApiFunction('notifications.getAll', 0); | 210 wrapper.instrumentChromeApiFunction('notifications.getAll', 0); |
198 wrapper.instrumentChromeApiFunction( | 211 wrapper.instrumentChromeApiFunction( |
199 'notifications.onButtonClicked.addListener', 0); | 212 'notifications.onButtonClicked.addListener', 0); |
200 wrapper.instrumentChromeApiFunction('notifications.onClicked.addListener', 0); | 213 wrapper.instrumentChromeApiFunction('notifications.onClicked.addListener', 0); |
201 wrapper.instrumentChromeApiFunction('notifications.onClosed.addListener', 0); | 214 wrapper.instrumentChromeApiFunction('notifications.onClosed.addListener', 0); |
202 wrapper.instrumentChromeApiFunction( | 215 wrapper.instrumentChromeApiFunction( |
203 'notifications.onPermissionLevelChanged.addListener', 0); | 216 'notifications.onPermissionLevelChanged.addListener', 0); |
204 wrapper.instrumentChromeApiFunction( | 217 wrapper.instrumentChromeApiFunction( |
205 'notifications.onShowSettings.addListener', 0); | 218 'notifications.onShowSettings.addListener', 0); |
206 wrapper.instrumentChromeApiFunction('permissions.contains', 1); | 219 wrapper.instrumentChromeApiFunction('permissions.contains', 1); |
207 wrapper.instrumentChromeApiFunction('pushMessaging.onMessage.addListener', 0); | |
208 wrapper.instrumentChromeApiFunction('storage.onChanged.addListener', 0); | |
209 wrapper.instrumentChromeApiFunction('runtime.onInstalled.addListener', 0); | 220 wrapper.instrumentChromeApiFunction('runtime.onInstalled.addListener', 0); |
210 wrapper.instrumentChromeApiFunction('runtime.onStartup.addListener', 0); | 221 wrapper.instrumentChromeApiFunction('runtime.onStartup.addListener', 0); |
| 222 wrapper.instrumentChromeApiFunction('storage.onChanged.addListener', 0); |
211 wrapper.instrumentChromeApiFunction('tabs.create', 1); | 223 wrapper.instrumentChromeApiFunction('tabs.create', 1); |
212 | 224 |
213 var updateCardsAttempts = buildAttemptManager( | 225 var updateCardsAttempts = buildAttemptManager( |
214 'cards-update', | 226 'cards-update', |
215 requestCards, | 227 requestCards, |
216 INITIAL_POLLING_PERIOD_SECONDS, | 228 INITIAL_POLLING_PERIOD_SECONDS, |
217 MAXIMUM_POLLING_PERIOD_SECONDS); | 229 MAXIMUM_POLLING_PERIOD_SECONDS); |
218 var optInPollAttempts = buildAttemptManager( | 230 var optInPollAttempts = buildAttemptManager( |
219 'optin', | 231 'optin', |
220 pollOptedInNoImmediateRecheck, | 232 pollOptedInNoImmediateRecheck, |
(...skipping 787 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1008 // We don't clear localStorage since those values are still relevant | 1020 // We don't clear localStorage since those values are still relevant |
1009 // across Google Now start-stop events. | 1021 // across Google Now start-stop events. |
1010 chrome.storage.local.clear(); | 1022 chrome.storage.local.clear(); |
1011 } | 1023 } |
1012 | 1024 |
1013 /** | 1025 /** |
1014 * Initializes the event page on install or on browser startup. | 1026 * Initializes the event page on install or on browser startup. |
1015 */ | 1027 */ |
1016 function initialize() { | 1028 function initialize() { |
1017 recordEvent(GoogleNowEvent.EXTENSION_START); | 1029 recordEvent(GoogleNowEvent.EXTENSION_START); |
| 1030 registerForGcm(); |
1018 onStateChange(); | 1031 onStateChange(); |
1019 } | 1032 } |
1020 | 1033 |
1021 /** | 1034 /** |
1022 * Starts or stops the main pipeline for polling cards. | 1035 * Starts or stops the main pipeline for polling cards. |
1023 * @param {boolean} shouldPollCardsRequest true to start and | 1036 * @param {boolean} shouldPollCardsRequest true to start and |
1024 * false to stop polling cards. | 1037 * false to stop polling cards. |
1025 */ | 1038 */ |
1026 function setShouldPollCards(shouldPollCardsRequest) { | 1039 function setShouldPollCards(shouldPollCardsRequest) { |
1027 updateCardsAttempts.isRunning(function(currentValue) { | 1040 updateCardsAttempts.isRunning(function(currentValue) { |
(...skipping 169 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1197 * opt-in state. | 1210 * opt-in state. |
1198 */ | 1211 */ |
1199 function isGoogleNowEnabled() { | 1212 function isGoogleNowEnabled() { |
1200 return fillFromChromeLocalStorage({googleNowEnabled: false}) | 1213 return fillFromChromeLocalStorage({googleNowEnabled: false}) |
1201 .then(function(items) { | 1214 .then(function(items) { |
1202 return items.googleNowEnabled; | 1215 return items.googleNowEnabled; |
1203 }); | 1216 }); |
1204 } | 1217 } |
1205 | 1218 |
1206 /** | 1219 /** |
| 1220 * Ensures the extension is ready to listen for GCM messages. |
| 1221 */ |
| 1222 function registerForGcm() { |
| 1223 // We don't need to use the key yet, just ensure the channel is set up. |
| 1224 getGcmNotificationKey(); |
| 1225 } |
| 1226 |
| 1227 /** |
| 1228 * Returns a Promise resolving to either a cached or new GCM notification key. |
| 1229 * Rejects if registration fails. |
| 1230 * @return {Promise} A Promise that resolves to a potentially-cached GCM key. |
| 1231 */ |
| 1232 function getGcmNotificationKey() { |
| 1233 return fillFromChromeLocalStorage({gcmNotificationKey: undefined}) |
| 1234 .then(function(items) { |
| 1235 if (items.gcmNotificationKey) { |
| 1236 console.log('Reused gcm key from storage.'); |
| 1237 return Promise.resolve(items.gcmNotificationKey); |
| 1238 } |
| 1239 return requestNewGcmNotificationKey(); |
| 1240 }); |
| 1241 } |
| 1242 |
| 1243 /** |
| 1244 * Returns a promise resolving to a GCM Notificaiton Key. May call |
| 1245 * chrome.gcm.register() first if required. Rejects on registration failure. |
| 1246 * @return {Promise} A Promise that resolves to a fresh GCM Notification key. |
| 1247 */ |
| 1248 function requestNewGcmNotificationKey() { |
| 1249 return getGcmRegistrationId().then(function(gcmId) { |
| 1250 authenticationManager.getAuthToken().then(function(token) { |
| 1251 authenticationManager.getLogin().then(function(username) { |
| 1252 return new Promise(function(resolve, reject) { |
| 1253 var xhr = new XMLHttpRequest(); |
| 1254 xhr.responseType = 'application/json'; |
| 1255 xhr.open('POST', GCM_REGISTRATION_URL, true); |
| 1256 xhr.setRequestHeader('Content-Type', 'application/json'); |
| 1257 xhr.setRequestHeader('Authorization', 'Bearer ' + token); |
| 1258 xhr.setRequestHeader('project_id', GCM_PROJECT_ID); |
| 1259 var payload = { |
| 1260 'operation': 'add', |
| 1261 'notification_key_name': username, |
| 1262 'registration_ids': [gcmId] |
| 1263 }; |
| 1264 xhr.onloadend = function() { |
| 1265 if (xhr.status != 200) { |
| 1266 reject(); |
| 1267 } |
| 1268 var obj = JSON.parse(xhr.responseText); |
| 1269 var key = obj && obj.notification_key; |
| 1270 if (!key) { |
| 1271 reject(); |
| 1272 } |
| 1273 console.log('gcm notification key POST: ' + key); |
| 1274 chrome.storage.local.set({gcmNotificationKey: key}); |
| 1275 resolve(key); |
| 1276 }; |
| 1277 xhr.send(JSON.stringify(payload)); |
| 1278 }); |
| 1279 }); |
| 1280 }).catch(function() { |
| 1281 // Couldn't obtain a GCM ID. Ignore and fallback to polling. |
| 1282 }); |
| 1283 }); |
| 1284 } |
| 1285 |
| 1286 /** |
| 1287 * Returns a promise resolving to either a cached or new GCM registration ID. |
| 1288 * Rejects if registration fails. |
| 1289 * @return {Promise} A Promise that resolves to a GCM registration ID. |
| 1290 */ |
| 1291 function getGcmRegistrationId() { |
| 1292 return fillFromChromeLocalStorage({gcmRegistrationId: undefined}) |
| 1293 .then(function(items) { |
| 1294 if (items.gcmRegistrationId) { |
| 1295 console.log('Reused gcm registration id from storage.'); |
| 1296 return Promise.resolve(items.gcmRegistrationId); |
| 1297 } |
| 1298 |
| 1299 return new Promise(function(resolve, reject) { |
| 1300 instrumented.gcm.register([GCM_PROJECT_ID], function(registrationId) { |
| 1301 console.log('gcm.register(): ' + registrationId); |
| 1302 if (registrationId) { |
| 1303 chrome.storage.local.set({gcmRegistrationId: registrationId}); |
| 1304 resolve(registrationId); |
| 1305 } else { |
| 1306 reject(); |
| 1307 } |
| 1308 }); |
| 1309 }); |
| 1310 }); |
| 1311 } |
| 1312 |
| 1313 /** |
1207 * Polls the optin state. | 1314 * Polls the optin state. |
1208 * Sometimes we get the response to the opted in result too soon during | 1315 * Sometimes we get the response to the opted in result too soon during |
1209 * push messaging. We'll recheck the optin state a few times before giving up. | 1316 * push messaging. We'll recheck the optin state a few times before giving up. |
1210 */ | 1317 */ |
1211 function pollOptedInWithRecheck() { | 1318 function pollOptedInWithRecheck() { |
1212 /** | 1319 /** |
1213 * Cleans up any state used to recheck the opt-in poll. | 1320 * Cleans up any state used to recheck the opt-in poll. |
1214 */ | 1321 */ |
1215 function clearPollingState() { | 1322 function clearPollingState() { |
1216 localStorage.removeItem('optedInCheckCount'); | 1323 localStorage.removeItem('optedInCheckCount'); |
(...skipping 110 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
1327 | 1434 |
1328 // Handles state change notifications for the Google Now enabled bit. | 1435 // Handles state change notifications for the Google Now enabled bit. |
1329 instrumented.storage.onChanged.addListener(function(changes, areaName) { | 1436 instrumented.storage.onChanged.addListener(function(changes, areaName) { |
1330 if (areaName === 'local') { | 1437 if (areaName === 'local') { |
1331 if ('googleNowEnabled' in changes) { | 1438 if ('googleNowEnabled' in changes) { |
1332 onStateChange(); | 1439 onStateChange(); |
1333 } | 1440 } |
1334 } | 1441 } |
1335 }); | 1442 }); |
1336 | 1443 |
1337 instrumented.pushMessaging.onMessage.addListener(function(message) { | 1444 instrumented.gcm.onMessage.addListener(function(message) { |
1338 // message.payload will be '' when the extension first starts. | 1445 console.log('gcm.onMessage ' + JSON.stringify(message)); |
1339 // Each time after signing in, we'll get latest payload for all channels. | 1446 if (!message || !message.data) { |
1340 // So, we need to poll the server only when the payload is non-empty and has | 1447 return; |
1341 // changed. | 1448 } |
1342 console.log('pushMessaging.onMessage ' + JSON.stringify(message)); | 1449 |
1343 if (message.payload.indexOf('REQUEST_CARDS') == 0) { | 1450 var payload = message.data.payload; |
| 1451 var tag = message.data.tag; |
| 1452 if (payload.indexOf('REQUEST_CARDS') == 0) { |
1344 tasks.add(ON_PUSH_MESSAGE_START_TASK_NAME, function() { | 1453 tasks.add(ON_PUSH_MESSAGE_START_TASK_NAME, function() { |
1345 // Accept promise rejection on failure since it's safer to do nothing, | 1454 // Accept promise rejection on failure since it's safer to do nothing, |
1346 // preventing polling the server when the payload really didn't change. | 1455 // preventing polling the server when the payload really didn't change. |
1347 fillFromChromeLocalStorage({ | 1456 fillFromChromeLocalStorage({ |
1348 lastPollNowPayloads: {}, | 1457 lastPollNowPayloads: {}, |
1349 /** @type {Object<string, StoredNotificationGroup>} */ | 1458 /** @type {Object<string, StoredNotificationGroup>} */ |
1350 notificationGroups: {} | 1459 notificationGroups: {} |
1351 }, PromiseRejection.ALLOW).then(function(items) { | 1460 }, PromiseRejection.ALLOW).then(function(items) { |
1352 if (items.lastPollNowPayloads[message.subchannelId] != | 1461 if (items.lastPollNowPayloads[tag] != payload) { |
1353 message.payload) { | 1462 items.lastPollNowPayloads[tag] = payload; |
1354 items.lastPollNowPayloads[message.subchannelId] = message.payload; | |
1355 | 1463 |
1356 items.notificationGroups['PUSH' + message.subchannelId] = { | 1464 items.notificationGroups['PUSH' + tag] = { |
1357 cards: [], | 1465 cards: [], |
1358 nextPollTime: Date.now() | 1466 nextPollTime: Date.now() |
1359 }; | 1467 }; |
1360 | 1468 |
1361 chrome.storage.local.set({ | 1469 chrome.storage.local.set({ |
1362 lastPollNowPayloads: items.lastPollNowPayloads, | 1470 lastPollNowPayloads: items.lastPollNowPayloads, |
1363 notificationGroups: items.notificationGroups | 1471 notificationGroups: items.notificationGroups |
1364 }); | 1472 }); |
1365 | 1473 |
1366 pollOptedInWithRecheck(); | 1474 pollOptedInWithRecheck(); |
1367 } | 1475 } |
1368 }); | 1476 }); |
1369 }); | 1477 }); |
1370 } | 1478 } |
1371 }); | 1479 }); |
OLD | NEW |