1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5var animationFrames = 36; 6var animationSpeed = 10; // ms 7var canvas = document.getElementById('canvas'); 8var loggedInImage = document.getElementById('logged_in'); 9var canvasContext = canvas.getContext('2d'); 10var pollIntervalMin = 1; // 1 minute 11var pollIntervalMax = 60; // 1 hour 12var requestTimeout = 1000 * 2; // 2 seconds 13var rotation = 0; 14var loadingAnimation = new LoadingAnimation(); 15 16// Legacy support for pre-event-pages. 17var oldChromeVersion = !chrome.runtime; 18var requestTimerId; 19 20function getGmailUrl() { 21 return "https://mail.google.com/mail/"; 22} 23 24// Identifier used to debug the possibility of multiple instances of the 25// extension making requests on behalf of a single user. 26function getInstanceId() { 27 if (!localStorage.hasOwnProperty("instanceId")) 28 localStorage.instanceId = 'gmc' + parseInt(Date.now() * Math.random(), 10); 29 return localStorage.instanceId; 30} 31 32function getFeedUrl() { 33 // "zx" is a Gmail query parameter that is expected to contain a random 34 // string and may be ignored/stripped. 35 return getGmailUrl() + "feed/atom?zx=" + encodeURIComponent(getInstanceId()); 36} 37 38function isGmailUrl(url) { 39 // Return whether the URL starts with the Gmail prefix. 40 return url.indexOf(getGmailUrl()) == 0; 41} 42 43// A "loading" animation displayed while we wait for the first response from 44// Gmail. This animates the badge text with a dot that cycles from left to 45// right. 46function LoadingAnimation() { 47 this.timerId_ = 0; 48 this.maxCount_ = 8; // Total number of states in animation 49 this.current_ = 0; // Current state 50 this.maxDot_ = 4; // Max number of dots in animation 51} 52 53LoadingAnimation.prototype.paintFrame = function() { 54 var text = ""; 55 for (var i = 0; i < this.maxDot_; i++) { 56 text += (i == this.current_) ? "." : " "; 57 } 58 if (this.current_ >= this.maxDot_) 59 text += ""; 60 61 chrome.browserAction.setBadgeText({text:text}); 62 this.current_++; 63 if (this.current_ == this.maxCount_) 64 this.current_ = 0; 65} 66 67LoadingAnimation.prototype.start = function() { 68 if (this.timerId_) 69 return; 70 71 var self = this; 72 this.timerId_ = window.setInterval(function() { 73 self.paintFrame(); 74 }, 100); 75} 76 77LoadingAnimation.prototype.stop = function() { 78 if (!this.timerId_) 79 return; 80 81 window.clearInterval(this.timerId_); 82 this.timerId_ = 0; 83} 84 85function updateIcon() { 86 if (!localStorage.hasOwnProperty('unreadCount')) { 87 chrome.browserAction.setIcon({path:"gmail_not_logged_in.png"}); 88 chrome.browserAction.setBadgeBackgroundColor({color:[190, 190, 190, 230]}); 89 chrome.browserAction.setBadgeText({text:"?"}); 90 } else { 91 chrome.browserAction.setIcon({path: "gmail_logged_in.png"}); 92 chrome.browserAction.setBadgeBackgroundColor({color:[208, 0, 24, 255]}); 93 chrome.browserAction.setBadgeText({ 94 text: localStorage.unreadCount != "0" ? localStorage.unreadCount : "" 95 }); 96 } 97} 98 99function scheduleRequest() { 100 console.log('scheduleRequest'); 101 var randomness = Math.random() * 2; 102 var exponent = Math.pow(2, localStorage.requestFailureCount || 0); 103 var multiplier = Math.max(randomness * exponent, 1); 104 var delay = Math.min(multiplier * pollIntervalMin, pollIntervalMax); 105 delay = Math.round(delay); 106 console.log('Scheduling for: ' + delay); 107 108 if (oldChromeVersion) { 109 if (requestTimerId) { 110 window.clearTimeout(requestTimerId); 111 } 112 requestTimerId = window.setTimeout(onAlarm, delay*60*1000); 113 } else { 114 console.log('Creating alarm'); 115 // Use a repeating alarm so that it fires again if there was a problem 116 // setting the next alarm. 117 chrome.alarms.create('refresh', {periodInMinutes: delay}); 118 } 119} 120 121// ajax stuff 122function startRequest(params) { 123 // Schedule request immediately. We want to be sure to reschedule, even in the 124 // case where the extension process shuts down while this request is 125 // outstanding. 126 if (params && params.scheduleRequest) scheduleRequest(); 127 128 function stopLoadingAnimation() { 129 if (params && params.showLoadingAnimation) loadingAnimation.stop(); 130 } 131 132 if (params && params.showLoadingAnimation) 133 loadingAnimation.start(); 134 135 getInboxCount( 136 function(count) { 137 stopLoadingAnimation(); 138 updateUnreadCount(count); 139 }, 140 function() { 141 stopLoadingAnimation(); 142 delete localStorage.unreadCount; 143 updateIcon(); 144 } 145 ); 146} 147 148function getInboxCount(onSuccess, onError) { 149 var xhr = new XMLHttpRequest(); 150 var abortTimerId = window.setTimeout(function() { 151 xhr.abort(); // synchronously calls onreadystatechange 152 }, requestTimeout); 153 154 function handleSuccess(count) { 155 localStorage.requestFailureCount = 0; 156 window.clearTimeout(abortTimerId); 157 if (onSuccess) 158 onSuccess(count); 159 } 160 161 var invokedErrorCallback = false; 162 function handleError() { 163 ++localStorage.requestFailureCount; 164 window.clearTimeout(abortTimerId); 165 if (onError && !invokedErrorCallback) 166 onError(); 167 invokedErrorCallback = true; 168 } 169 170 try { 171 xhr.onreadystatechange = function() { 172 if (xhr.readyState != 4) 173 return; 174 175 if (xhr.responseXML) { 176 var xmlDoc = xhr.responseXML; 177 var fullCountSet = xmlDoc.evaluate("/gmail:feed/gmail:fullcount", 178 xmlDoc, gmailNSResolver, XPathResult.ANY_TYPE, null); 179 var fullCountNode = fullCountSet.iterateNext(); 180 if (fullCountNode) { 181 handleSuccess(fullCountNode.textContent); 182 return; 183 } else { 184 console.error(chrome.i18n.getMessage("gmailcheck_node_error")); 185 } 186 } 187 188 handleError(); 189 }; 190 191 xhr.onerror = function(error) { 192 handleError(); 193 }; 194 195 xhr.open("GET", getFeedUrl(), true); 196 xhr.send(null); 197 } catch(e) { 198 console.error(chrome.i18n.getMessage("gmailcheck_exception", e)); 199 handleError(); 200 } 201} 202 203function gmailNSResolver(prefix) { 204 if(prefix == 'gmail') { 205 return 'http://purl.org/atom/ns#'; 206 } 207} 208 209function updateUnreadCount(count) { 210 var changed = localStorage.unreadCount != count; 211 localStorage.unreadCount = count; 212 updateIcon(); 213 if (changed) 214 animateFlip(); 215} 216 217 218function ease(x) { 219 return (1-Math.sin(Math.PI/2+x*Math.PI))/2; 220} 221 222function animateFlip() { 223 rotation += 1/animationFrames; 224 drawIconAtRotation(); 225 226 if (rotation <= 1) { 227 setTimeout(animateFlip, animationSpeed); 228 } else { 229 rotation = 0; 230 updateIcon(); 231 } 232} 233 234function drawIconAtRotation() { 235 canvasContext.save(); 236 canvasContext.clearRect(0, 0, canvas.width, canvas.height); 237 canvasContext.translate( 238 Math.ceil(canvas.width/2), 239 Math.ceil(canvas.height/2)); 240 canvasContext.rotate(2*Math.PI*ease(rotation)); 241 canvasContext.drawImage(loggedInImage, 242 -Math.ceil(canvas.width/2), 243 -Math.ceil(canvas.height/2)); 244 canvasContext.restore(); 245 246 chrome.browserAction.setIcon({imageData:canvasContext.getImageData(0, 0, 247 canvas.width,canvas.height)}); 248} 249 250function goToInbox() { 251 console.log('Going to inbox...'); 252 chrome.tabs.getAllInWindow(undefined, function(tabs) { 253 for (var i = 0, tab; tab = tabs[i]; i++) { 254 if (tab.url && isGmailUrl(tab.url)) { 255 console.log('Found Gmail tab: ' + tab.url + '. ' + 256 'Focusing and refreshing count...'); 257 chrome.tabs.update(tab.id, {selected: true}); 258 startRequest({scheduleRequest:false, showLoadingAnimation:false}); 259 return; 260 } 261 } 262 console.log('Could not find Gmail tab. Creating one...'); 263 chrome.tabs.create({url: getGmailUrl()}); 264 }); 265} 266 267function onInit() { 268 console.log('onInit'); 269 localStorage.requestFailureCount = 0; // used for exponential backoff 270 startRequest({scheduleRequest:true, showLoadingAnimation:true}); 271 if (!oldChromeVersion) { 272 // TODO(mpcomplete): We should be able to remove this now, but leaving it 273 // for a little while just to be sure the refresh alarm is working nicely. 274 chrome.alarms.create('watchdog', {periodInMinutes:5}); 275 } 276} 277 278function onAlarm(alarm) { 279 console.log('Got alarm', alarm); 280 // |alarm| can be undefined because onAlarm also gets called from 281 // window.setTimeout on old chrome versions. 282 if (alarm && alarm.name == 'watchdog') { 283 onWatchdog(); 284 } else { 285 startRequest({scheduleRequest:true, showLoadingAnimation:false}); 286 } 287} 288 289function onWatchdog() { 290 chrome.alarms.get('refresh', function(alarm) { 291 if (alarm) { 292 console.log('Refresh alarm exists. Yay.'); 293 } else { 294 console.log('Refresh alarm doesn\'t exist!? ' + 295 'Refreshing now and rescheduling.'); 296 startRequest({scheduleRequest:true, showLoadingAnimation:false}); 297 } 298 }); 299} 300 301if (oldChromeVersion) { 302 updateIcon(); 303 onInit(); 304} else { 305 chrome.runtime.onInstalled.addListener(onInit); 306 chrome.alarms.onAlarm.addListener(onAlarm); 307} 308 309var filters = { 310 // TODO(aa): Cannot use urlPrefix because all the url fields lack the protocol 311 // part. See crbug.com/140238. 312 url: [{urlContains: getGmailUrl().replace(/^https?\:\/\//, '')}] 313}; 314 315function onNavigate(details) { 316 if (details.url && isGmailUrl(details.url)) { 317 console.log('Recognized Gmail navigation to: ' + details.url + '.' + 318 'Refreshing count...'); 319 startRequest({scheduleRequest:false, showLoadingAnimation:false}); 320 } 321} 322if (chrome.webNavigation && chrome.webNavigation.onDOMContentLoaded && 323 chrome.webNavigation.onReferenceFragmentUpdated) { 324 chrome.webNavigation.onDOMContentLoaded.addListener(onNavigate, filters); 325 chrome.webNavigation.onReferenceFragmentUpdated.addListener( 326 onNavigate, filters); 327} else { 328 chrome.tabs.onUpdated.addListener(function(_, details) { 329 onNavigate(details); 330 }); 331} 332 333chrome.browserAction.onClicked.addListener(goToInbox); 334 335if (chrome.runtime && chrome.runtime.onStartup) { 336 chrome.runtime.onStartup.addListener(function() { 337 console.log('Starting browser... updating icon.'); 338 startRequest({scheduleRequest:false, showLoadingAnimation:false}); 339 updateIcon(); 340 }); 341} else { 342 // This hack is needed because Chrome 22 does not persist browserAction icon 343 // state, and also doesn't expose onStartup. So the icon always starts out in 344 // wrong state. We don't actually use onStartup except as a clue that we're 345 // in a version of Chrome that has this problem. 346 chrome.windows.onCreated.addListener(function() { 347 console.log('Window created... updating icon.'); 348 startRequest({scheduleRequest:false, showLoadingAnimation:false}); 349 updateIcon(); 350 }); 351} 352