Home | History | Annotate | Download | only in javascript
      1 /**
      2  * Copyright (c) 2011 The Chromium Authors. All rights reserved.
      3  * Use of this source code is governed by a BSD-style license that can be
      4  * found in the LICENSE file.
      5  */
      6 
      7 /**
      8  * PHASES
      9  * 1) Load next event from server refresh every 30 minutes or every time
     10  *   you go to calendar or every time you logout drop in a data object.
     11  * 2) Display on screen periodically once per minute or on demand.
     12  */
     13 
     14 // Message shown in badge title when no title is given to an event.
     15 var MSG_NO_TITLE = chrome.i18n.getMessage('noTitle');
     16 
     17 // Time between server polls = 30 minutes.
     18 var POLL_INTERVAL = 30 * 60 * 1000;
     19 
     20 // Redraw interval is 1 min.
     21 var DRAW_INTERVAL = 60 * 1000;
     22 
     23 // The time when we last polled.
     24 var lastPollTime_ = 0;
     25 
     26 // Object for BadgeAnimation
     27 var badgeAnimation_;
     28 
     29 //Object for CanvasAnimation
     30 var canvasAnimation_;
     31 
     32 // Object containing the event.
     33 var nextEvent_ = null;
     34 
     35 // Storing events.
     36 var eventList = [];
     37 var nextEvents = [];
     38 
     39 // Storing calendars.
     40 var calendars = [];
     41 
     42 var pollUnderProgress = false;
     43 var defaultAuthor = '';
     44 var isMultiCalendar = false;
     45 
     46 //URL for getting feed of individual calendar support.
     47 var SINGLE_CALENDAR_SUPPORT_URL = 'https://www.google.com/calendar/feeds' +
     48     '/default/private/embed?toolbar=true&max-results=10';
     49 
     50 //URL for getting feed of multiple calendar support.
     51 var MULTIPLE_CALENDAR_SUPPORT_URL = 'https://www.google.com/calendar/feeds' +
     52     '/default/allcalendars/full';
     53 
     54 //URL for opening Google Calendar in new tab.
     55 var GOOGLE_CALENDAR_URL = 'http://www.google.com/calendar/render';
     56 
     57 //URL for declining invitation of the event.
     58 var DECLINED_URL = 'http://schemas.google.com/g/2005#event.declined';
     59 
     60 //This is used to poll only once per second at most, and delay that if
     61 //we keep hitting pages that would otherwise force a load.
     62 var pendingLoadId_ = null;
     63 
     64 /**
     65  * A "loading" animation displayed while we wait for the first response from
     66  * Calendar. This animates the badge text with a dot that cycles from left to
     67  * right.
     68  * @constructor
     69  */
     70 function BadgeAnimation() {
     71   this.timerId_ = 0;
     72   this.maxCount_ = 8;  // Total number of states in animation
     73   this.current_ = 0;  // Current state
     74   this.maxDot_ = 4;  // Max number of dots in animation
     75 };
     76 
     77 /**
     78  * Paints the badge text area while loading the data.
     79  */
     80 BadgeAnimation.prototype.paintFrame = function() {
     81   var text = '';
     82   for (var i = 0; i < this.maxDot_; i++) {
     83     text += (i == this.current_) ? '.' : ' ';
     84   }
     85 
     86   chrome.browserAction.setBadgeText({text: text});
     87   this.current_++;
     88   if (this.current_ == this.maxCount_) {
     89     this.current_ = 0;
     90   }
     91 };
     92 
     93 /**
     94  * Starts the animation process.
     95  */
     96 BadgeAnimation.prototype.start = function() {
     97   if (this.timerId_) {
     98     return;
     99   }
    100 
    101   var self = this;
    102   this.timerId_ = window.setInterval(function() {
    103     self.paintFrame();
    104   }, 100);
    105 };
    106 
    107 /**
    108  * Stops the animation process.
    109  */
    110 BadgeAnimation.prototype.stop = function() {
    111   if (!this.timerId_) {
    112     return;
    113   }
    114 
    115   window.clearInterval(this.timerId_);
    116   this.timerId_ = 0;
    117 };
    118 
    119 /**
    120  * Animates the canvas after loading the data from all the calendars. It
    121  * rotates the icon and defines the badge text and title.
    122  * @constructor
    123  */
    124 function CanvasAnimation() {
    125   this.animationFrames_ = 36;  // The number of animation frames
    126   this.animationSpeed_ = 10;  // Time between each frame(in ms).
    127   this.canvas_ = $('canvas');  // The canvas width + height.
    128   this.canvasContext_ = this.canvas_.getContext('2d');  // Canvas context.
    129   this.loggedInImage_ = $('logged_in');
    130   this.rotation_ = 0;  //Keeps count of rotation angle of extension icon.
    131   this.w = this.canvas_.width;  // Setting canvas width.
    132   this.h = this.canvas_.height;  // Setting canvas height.
    133   this.RED = [208, 0, 24, 255];  //Badge color of extension icon in RGB format.
    134   this.BLUE = [0, 24, 208, 255];
    135   this.currentBadge_ = null;  // The text in the current badge.
    136 };
    137 
    138 /**
    139  * Flips the icon around and draws it.
    140  */
    141 CanvasAnimation.prototype.animate = function() {
    142   this.rotation_ += (1 / this.animationFrames_);
    143   this.drawIconAtRotation();
    144   var self = this;
    145   if (this.rotation_ <= 1) {
    146     setTimeout(function() {
    147       self.animate();
    148     }, self.animationSpeed_);
    149   } else {
    150     this.drawFinal();
    151   }
    152 };
    153 
    154 /**
    155  * Renders the icon.
    156  */
    157 CanvasAnimation.prototype.drawIconAtRotation = function() {
    158   this.canvasContext_.save();
    159   this.canvasContext_.clearRect(0, 0, this.w, this.h);
    160   this.canvasContext_.translate(Math.ceil(this.w / 2), Math.ceil(this.h / 2));
    161   this.canvasContext_.rotate(2 * Math.PI * this.getSector(this.rotation_));
    162   this.canvasContext_.drawImage(this.loggedInImage_, -Math.ceil(this.w / 2),
    163     -Math.ceil(this.h / 2));
    164   this.canvasContext_.restore();
    165   chrome.browserAction.setIcon(
    166       {imageData: this.canvasContext_.getImageData(0, 0, this.w, this.h)});
    167 };
    168 
    169 /**
    170  * Calculates the sector which has to be traversed in a single call of animate
    171  * function(360/animationFrames_ = 360/36 = 10 radians).
    172  * @param {integer} sector angle to be rotated(in radians).
    173  * @return {integer} value in radian of the sector which it has to cover.
    174  */
    175 CanvasAnimation.prototype.getSector = function(sector) {
    176   return (1 - Math.sin(Math.PI / 2 + sector * Math.PI)) / 2;
    177 };
    178 
    179 /**
    180  * Draws the event icon and determines the badge title and icon title.
    181  */
    182 CanvasAnimation.prototype.drawFinal = function() {
    183   badgeAnimation_.stop();
    184 
    185   if (!nextEvent_) {
    186     this.showLoggedOut();
    187   } else {
    188     this.drawIconAtRotation();
    189     this.rotation_ = 0;
    190 
    191     var ms = nextEvent_.startTime.getTime() - getCurrentTime();
    192     var nextEventMin = ms / (1000 * 60);
    193     var bgColor = (nextEventMin < 60) ? this.RED : this.BLUE;
    194 
    195     chrome.browserAction.setBadgeBackgroundColor({color: bgColor});
    196     currentBadge_ = this.getBadgeText(nextEvent_);
    197     chrome.browserAction.setBadgeText({text: currentBadge_});
    198 
    199     if (nextEvents.length > 0) {
    200       var text = '';
    201       for (var i = 0, event; event = nextEvents[i]; i++) {
    202         text += event.title;
    203         if (event.author || event.location) {
    204           text += '\n';
    205         }
    206         if (event.location) {
    207           text += event.location + ' ';
    208         }
    209         if (event.author) {
    210           text += event.author;
    211         }
    212         if (i < (nextEvents.length - 1)) {
    213           text += '\n----------\n';
    214         }
    215       }
    216       text = filterSpecialChar(text);
    217       chrome.browserAction.setTitle({'title' : text});
    218     }
    219   }
    220   pollUnderProgress = false;
    221 
    222   chrome.extension.sendRequest({
    223     message: 'enableSave'
    224   }, function() {
    225   });
    226 
    227   return;
    228 };
    229 
    230 /**
    231  * Shows the user logged out.
    232  */
    233 CanvasAnimation.prototype.showLoggedOut = function() {
    234   currentBadge_ = '?';
    235   chrome.browserAction.setIcon({path: '../images/icon-16_bw.gif'});
    236   chrome.browserAction.setBadgeBackgroundColor({color: [190, 190, 190, 230]});
    237   chrome.browserAction.setBadgeText({text: '?'});
    238   chrome.browserAction.setTitle({ 'title' : ''});
    239 };
    240 
    241 /**
    242  * Gets the badge text.
    243  * @param {Object} nextEvent_ next event in the calendar.
    244  * @return {String} text Badge text to be shown in extension icon.
    245  */
    246 CanvasAnimation.prototype.getBadgeText = function(nextEvent_) {
    247   if (!nextEvent_) {
    248     return '';
    249   }
    250 
    251   var ms = nextEvent_.startTime.getTime() - getCurrentTime();
    252   var nextEventMin = Math.ceil(ms / (1000 * 60));
    253 
    254   var text = '';
    255   if (nextEventMin < 60) {
    256     text = chrome.i18n.getMessage('minutes', nextEventMin.toString());
    257   } else if (nextEventMin < 1440) {
    258     text = chrome.i18n.getMessage('hours',
    259                Math.round(nextEventMin / 60).toString());
    260   } else if (nextEventMin < (1440 * 10)) {
    261     text = chrome.i18n.getMessage('days',
    262                Math.round(nextEventMin / 60 / 24).toString());
    263   }
    264   return text;
    265 };
    266 
    267 /**
    268  * Provides all the calendar related utils.
    269  */
    270 CalendarManager = {};
    271 
    272 /**
    273  * Extracts event from the each entry of the calendar.
    274  * @param {Object} elem The XML node to extract the event from.
    275  * @param {Object} mailId email of the owner of calendar in multiple calendar
    276  *     support.
    277  * @return {Object} out An object containing the event properties.
    278  */
    279 CalendarManager.extractEvent = function(elem, mailId) {
    280   var out = {};
    281 
    282   for (var node = elem.firstChild; node != null; node = node.nextSibling) {
    283     if (node.nodeName == 'title') {
    284         out.title = node.firstChild ? node.firstChild.nodeValue : MSG_NO_TITLE;
    285     } else if (node.nodeName == 'link' &&
    286                node.getAttribute('rel') == 'alternate') {
    287       out.url = node.getAttribute('href');
    288     } else if (node.nodeName == 'gd:where') {
    289       out.location = node.getAttribute('valueString');
    290     } else if (node.nodeName == 'gd:who') {
    291       if (node.firstChild) {
    292         if ((!isMultiCalendar) || (isMultiCalendar && mailId &&
    293             node.getAttribute('email') == mailId)) {
    294           out.attendeeStatus = node.firstChild.getAttribute('value');
    295         }
    296       }
    297     } else if (node.nodeName == 'gd:eventStatus') {
    298       out.status = node.getAttribute('value');
    299     } else if (node.nodeName == 'gd:when') {
    300       var startTimeStr = node.getAttribute('startTime');
    301       var endTimeStr = node.getAttribute('endTime');
    302 
    303       startTime = rfc3339StringToDate(startTimeStr);
    304       endTime = rfc3339StringToDate(endTimeStr);
    305 
    306       if (startTime == null || endTime == null) {
    307         continue;
    308       }
    309 
    310       out.isAllDay = (startTimeStr.length <= 11);
    311       out.startTime = startTime;
    312       out.endTime = endTime;
    313     }
    314   }
    315   return out;
    316 };
    317 
    318 /**
    319  * Polls the server to get the feed of the user.
    320  */
    321 CalendarManager.pollServer = function() {
    322   if (! pollUnderProgress) {
    323     eventList = [];
    324     pollUnderProgress = true;
    325     pendingLoadId_ = null;
    326     calendars = [];
    327     lastPollTime_ = getCurrentTime();
    328     var url;
    329     var xhr = new XMLHttpRequest();
    330     try {
    331       xhr.onreadystatechange = CalendarManager.genResponseChangeFunc(xhr);
    332       xhr.onerror = function(error) {
    333         console.log('error: ' + error);
    334         nextEvent_ = null;
    335         canvasAnimation_.drawFinal();
    336       };
    337       if (isMultiCalendar) {
    338         url = MULTIPLE_CALENDAR_SUPPORT_URL;
    339       } else {
    340         url = SINGLE_CALENDAR_SUPPORT_URL;
    341       }
    342 
    343       xhr.open('GET', url);
    344       xhr.send(null);
    345     } catch (e) {
    346       console.log('ex: ' + e);
    347       nextEvent_ = null;
    348       canvasAnimation_.drawFinal();
    349     }
    350   }
    351 };
    352 
    353 /**
    354  * Gathers the list of all calendars of a specific user for multiple calendar
    355  * support and event entries in single calendar.
    356  * @param {xmlHttpRequest} xhr xmlHttpRequest object containing server response.
    357  * @return {Object} anonymous function which returns to onReadyStateChange.
    358  */
    359 CalendarManager.genResponseChangeFunc = function(xhr) {
    360   return function() {
    361     if (xhr.readyState != 4) {
    362       return;
    363     }
    364     if (!xhr.responseXML) {
    365       console.log('No responseXML');
    366       nextEvent_ = null;
    367       canvasAnimation_.drawFinal();
    368       return;
    369     }
    370     if (isMultiCalendar) {
    371       var entry_ = xhr.responseXML.getElementsByTagName('entry');
    372       if (entry_ && entry_.length > 0) {
    373         calendars = [];
    374         for (var i = 0, entry; entry = entry_[i]; ++i) {
    375           if (!i) {
    376             defaultAuthor = entry.querySelector('title').textContent;
    377           }
    378           // Include only those calendars which are not hidden and selected
    379           var isHidden = entry.querySelector('hidden');
    380           var isSelected = entry.querySelector('selected');
    381           if (isHidden && isHidden.getAttribute('value') == 'false') {
    382             if (isSelected && isSelected.getAttribute('value') == 'true') {
    383               var calendar_content = entry.querySelector('content');
    384               var cal_src = calendar_content.getAttribute('src');
    385               cal_src += '?toolbar=true&max-results=10';
    386               calendars.push(cal_src);
    387             }
    388           }
    389         }
    390         CalendarManager.getCalendarFeed(0);
    391         return;
    392       }
    393     } else {
    394       calendars = [];
    395       calendars.push(SINGLE_CALENDAR_SUPPORT_URL);
    396       CalendarManager.parseCalendarEntry(xhr.responseXML, 0);
    397       return;
    398     }
    399 
    400     console.error('Error: feed retrieved, but no event found');
    401     nextEvent_ = null;
    402     canvasAnimation_.drawFinal();
    403   };
    404 };
    405 
    406 /**
    407  * Retrieves feed for a calendar
    408  * @param {integer} calendarId Id of the calendar in array of calendars.
    409  */
    410 CalendarManager.getCalendarFeed = function(calendarId) {
    411   var xmlhttp = new XMLHttpRequest();
    412   try {
    413     xmlhttp.onreadystatechange = CalendarManager.onCalendarResponse(xmlhttp,
    414                                      calendarId);
    415     xmlhttp.onerror = function(error) {
    416       console.log('error: ' + error);
    417       nextEvent_ = null;
    418       canvasAnimation_.drawFinal();
    419     };
    420 
    421     xmlhttp.open('GET', calendars[calendarId]);
    422     xmlhttp.send(null);
    423   }
    424   catch (e) {
    425     console.log('ex: ' + e);
    426     nextEvent_ = null;
    427     canvasAnimation_.drawFinal();
    428   }
    429 };
    430 
    431 /**
    432  * Gets the event entries of every calendar subscribed in default user calendar.
    433  * @param {xmlHttpRequest} xmlhttp xmlHttpRequest containing server response
    434  *     for the feed of a specific calendar.
    435  * @param {integer} calendarId Variable for storing the no of calendars
    436  *     processed.
    437  * @return {Object} anonymous function which returns to onReadyStateChange.
    438  */
    439 CalendarManager.onCalendarResponse = function(xmlhttp, calendarId) {
    440   return function() {
    441     if (xmlhttp.readyState != 4) {
    442       return;
    443     }
    444     if (!xmlhttp.responseXML) {
    445       console.log('No responseXML');
    446       nextEvent_ = null;
    447       canvasAnimation_.drawFinal();
    448       return;
    449     }
    450     CalendarManager.parseCalendarEntry(xmlhttp.responseXML, calendarId);
    451   };
    452 };
    453 
    454 /**
    455  * Parses events from calendar response XML
    456  * @param {string} responseXML Response XML for calendar.
    457  * @param {integer} calendarId  Id of the calendar in array of calendars.
    458  */
    459 CalendarManager.parseCalendarEntry = function(responseXML, calendarId) {
    460   var entry_ = responseXML.getElementsByTagName('entry');
    461   var mailId = null;
    462   var author = null;
    463 
    464   if (responseXML.querySelector('author name')) {
    465     author = responseXML.querySelector('author name').textContent;
    466   }
    467   if (responseXML.querySelector('author email')) {
    468     mailId = responseXML.querySelector('author email').textContent;
    469   }
    470 
    471   if (entry_ && entry_.length > 0) {
    472     for (var i = 0, entry; entry = entry_[i]; ++i) {
    473      var event_ = CalendarManager.extractEvent(entry, mailId);
    474 
    475       // Get the time from then to now
    476       if (event_.startTime) {
    477         var t = event_.startTime.getTime() - getCurrentTime();
    478         if (t >= 0 && (event_.attendeeStatus != DECLINED_URL)) {
    479             if (isMultiCalendar && author) {
    480               event_.author = author;
    481             }
    482             eventList.push(event_);
    483         }
    484       }
    485     }
    486   }
    487 
    488   calendarId++;
    489   //get the next calendar
    490   if (calendarId < calendars.length) {
    491     CalendarManager.getCalendarFeed(calendarId);
    492   } else {
    493     CalendarManager.populateLatestEvent(eventList);
    494   }
    495 };
    496 
    497 /**
    498  * Fills the event list with the events acquired from the calendar(s).
    499  * Parses entire event list and prepares an array of upcoming events.
    500  * @param {Array} eventList List of all events.
    501  */
    502 CalendarManager.populateLatestEvent = function(eventList) {
    503   nextEvents = [];
    504   if (isMultiCalendar) {
    505     eventList.sort(sortByDate);
    506   }
    507 
    508   //populating next events array.
    509   if (eventList.length > 0) {
    510     nextEvent_ = eventList[0];
    511     nextEvents.push(nextEvent_);
    512     var startTime = nextEvent_.startTime.setSeconds(0, 0);
    513     for (var i = 1, event; event = eventList[i]; i++) {
    514       var time = event.startTime.setSeconds(0, 0);
    515       if (time == startTime) {
    516         nextEvents.push(event);
    517       } else {
    518         break;
    519       }
    520     }
    521     if (nextEvents.length > 1 && isMultiCalendar) {
    522       nextEvents.sort(sortByAuthor);
    523     }
    524     canvasAnimation_.animate();
    525     return;
    526   } else {
    527     console.error('Error: feed retrieved, but no event found');
    528     nextEvent_ = null;
    529     canvasAnimation_.drawFinal();
    530   }
    531 };
    532 
    533 var DATE_TIME_REGEX =
    534   /^(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)\.\d+(\+|-)(\d\d):(\d\d)$/;
    535 var DATE_TIME_REGEX_Z = /^(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)\.\d+Z$/;
    536 var DATE_REGEX = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
    537 
    538 /**
    539 * Convert the incoming date into a javascript date.
    540 * @param {String} rfc3339 The rfc date in string format as following
    541 *     2006-04-28T09:00:00.000-07:00
    542 *     2006-04-28T09:00:00.000Z
    543 *     2006-04-19.
    544 * @return {Date} The javascript date format of the incoming date.
    545 */
    546 function rfc3339StringToDate(rfc3339) {
    547   var parts = DATE_TIME_REGEX.exec(rfc3339);
    548 
    549   // Try out the Z version
    550   if (!parts) {
    551     parts = DATE_TIME_REGEX_Z.exec(rfc3339);
    552   }
    553 
    554   if (parts && parts.length > 0) {
    555     var d = new Date();
    556     d.setUTCFullYear(parts[1], parseInt(parts[2], 10) - 1, parts[3]);
    557     d.setUTCHours(parts[4]);
    558     d.setUTCMinutes(parts[5]);
    559     d.setUTCSeconds(parts[6]);
    560 
    561     var tzOffsetFeedMin = 0;
    562     if (parts.length > 7) {
    563       tzOffsetFeedMin = parseInt(parts[8], 10) * 60 + parseInt(parts[9], 10);
    564       if (parts[7] != '-') { // This is supposed to be backwards.
    565         tzOffsetFeedMin = -tzOffsetFeedMin;
    566       }
    567     }
    568     return new Date(d.getTime() + tzOffsetFeedMin * 60 * 1000);
    569   }
    570 
    571   parts = DATE_REGEX.exec(rfc3339);
    572   if (parts && parts.length > 0) {
    573     return new Date(parts[1], parseInt(parts[2], 10) - 1, parts[3]);
    574   }
    575   return null;
    576 };
    577 
    578 /**
    579  * Sorts all the events by date and time.
    580  * @param {object} event_1 Event object.
    581  * @param {object} event_2 Event object.
    582  * @return {integer} timeDiff Difference in time.
    583  */
    584 function sortByDate(event_1, event_2) {
    585   return (event_1.startTime.getTime() - event_2.startTime.getTime());
    586 };
    587 
    588 /**
    589  * Sorts all the events by author name.
    590  * @param {object} event_1 Event object.
    591  * @param {object} event_2 Event object.
    592  * @return {integer} nameDiff Difference in default author and others.
    593  */
    594 function sortByAuthor(event_1, event_2) {
    595   var nameDiff;
    596   if (event_1.author && event_2.author && event_2.author == defaultAuthor) {
    597     nameDiff = 1;
    598   } else {
    599     return 0;
    600   }
    601   return nameDiff;
    602 };
    603 
    604 /**
    605  * Fires once per minute to redraw extension icon.
    606  */
    607 function redraw() {
    608   // If the next event just passed, re-poll.
    609   if (nextEvent_) {
    610     var t = nextEvent_.startTime.getTime() - getCurrentTime();
    611     if (t <= 0) {
    612       CalendarManager.pollServer();
    613       return;
    614     }
    615   }
    616   canvasAnimation_.animate();
    617 
    618   // if 30 minutes have passed re-poll
    619   if (getCurrentTime() - lastPollTime_ >= POLL_INTERVAL) {
    620     CalendarManager.pollServer();
    621   }
    622 };
    623 
    624 /**
    625  * Returns the current time in milliseconds.
    626  * @return {Number} Current time in milliseconds.
    627  */
    628 function getCurrentTime() {
    629   return (new Date()).getTime();
    630 };
    631 
    632 /**
    633 * Replaces ASCII characters from the title.
    634 * @param {String} data String containing ASCII code for special characters.
    635 * @return {String} data ASCII characters replaced with actual characters.
    636 */
    637 function filterSpecialChar(data) {
    638   if (data) {
    639     data = data.replace(/&lt;/g, '<');
    640     data = data.replace(/&gt;/g, '>');
    641     data = data.replace(/&amp;/g, '&');
    642     data = data.replace(/%7B/g, '{');
    643     data = data.replace(/%7D/g, '}');
    644     data = data.replace(/&quot;/g, '"');
    645     data = data.replace(/&#39;/g, '\'');
    646   }
    647   return data;
    648 };
    649 
    650 /**
    651  * Called from options.js page on saving the settings
    652  */
    653 function onSettingsChange() {
    654   isMultiCalendar = JSON.parse(localStorage.multiCalendar);
    655   badgeAnimation_.start();
    656   CalendarManager.pollServer();
    657 };
    658 
    659 /**
    660  * Function runs on updating a tab having url of google applications.
    661  * @param {integer} tabId Id of the tab which is updated.
    662  * @param {String} changeInfo Gives the information of change in url.
    663  * @param {String} tab Gives the url of the tab updated.
    664  */
    665 function onTabUpdated(tabId, changeInfo, tab) {
    666   var url = tab.url;
    667   if (!url) {
    668     return;
    669   }
    670 
    671   if ((url.indexOf('www.google.com/calendar/') != -1) ||
    672       ((url.indexOf('www.google.com/a/') != -1) &&
    673       (url.lastIndexOf('/acs') == url.length - 4)) ||
    674       (url.indexOf('www.google.com/accounts/') != -1)) {
    675 
    676     // The login screen isn't helpful
    677     if (url.indexOf('https://www.google.com/accounts/ServiceLogin?') == 0) {
    678       return;
    679     }
    680 
    681     if (pendingLoadId_) {
    682       clearTimeout(pendingLoadId_);
    683       pendingLoadId_ = null;
    684     }
    685 
    686     // try to poll in 2 second [which makes the redirects settle down]
    687     pendingLoadId_ = setTimeout(CalendarManager.pollServer, 2000);
    688   }
    689 };
    690 
    691 /**
    692  * Called when the user clicks on extension icon and opens calendar page.
    693  */
    694 function onClickAction() {
    695   chrome.tabs.getAllInWindow(null, function(tabs) {
    696     for (var i = 0, tab; tab = tabs[i]; i++) {
    697       if (tab.url && isCalendarUrl(tab.url)) {
    698         chrome.tabs.update(tab.id, {selected: true});
    699         CalendarManager.pollServer();
    700         return;
    701       }
    702     }
    703     chrome.tabs.create({url: GOOGLE_CALENDAR_URL});
    704     CalendarManager.pollServer();
    705   });
    706 };
    707 
    708 /**
    709  * Checks whether an instance of Google calendar is already open.
    710  * @param {String} url Url of the tab visited.
    711  * @return {boolean} true if the url is a Google calendar relative url, false
    712  *     otherwise.
    713  */
    714 function isCalendarUrl(url) {
    715   return url.indexOf('www.google.com/calendar') != -1 ? true : false;
    716 };
    717 
    718 /**
    719  * Initializes everything.
    720  */
    721 function init() {
    722   badgeAnimation_ = new BadgeAnimation();
    723   canvasAnimation_ = new CanvasAnimation();
    724 
    725   isMultiCalendar = JSON.parse(localStorage.multiCalendar || false);
    726 
    727   chrome.browserAction.setIcon({path: '../images/icon-16.gif'});
    728   badgeAnimation_.start();
    729   CalendarManager.pollServer();
    730   window.setInterval(redraw, DRAW_INTERVAL);
    731 
    732   chrome.tabs.onUpdated.addListener(onTabUpdated);
    733 
    734   chrome.browserAction.onClicked.addListener(function(tab) {
    735     onClickAction();
    736   });
    737 };
    738 
    739 //Adding listener when body is loaded to call init function.
    740 window.addEventListener('load', init, false);
    741