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