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. 15var MSG_NO_TITLE = chrome.i18n.getMessage('noTitle'); 16 17// Time between server polls = 30 minutes. 18var POLL_INTERVAL = 30 * 60 * 1000; 19 20// Redraw interval is 1 min. 21var DRAW_INTERVAL = 60 * 1000; 22 23// The time when we last polled. 24var lastPollTime_ = 0; 25 26// Object for BadgeAnimation 27var badgeAnimation_; 28 29//Object for CanvasAnimation 30var canvasAnimation_; 31 32// Object containing the event. 33var nextEvent_ = null; 34 35// Storing events. 36var eventList = []; 37var nextEvents = []; 38 39// Storing calendars. 40var calendars = []; 41 42var pollUnderProgress = false; 43var defaultAuthor = ''; 44var isMultiCalendar = false; 45 46//URL for getting feed of individual calendar support. 47var 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. 51var MULTIPLE_CALENDAR_SUPPORT_URL = 'https://www.google.com/calendar/feeds' + 52 '/default/allcalendars/full'; 53 54//URL for opening Google Calendar in new tab. 55var GOOGLE_CALENDAR_URL = 'http://www.google.com/calendar/render'; 56 57//URL for declining invitation of the event. 58var 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. 62var 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 */ 70function 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 */ 80BadgeAnimation.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 */ 96BadgeAnimation.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 */ 110BadgeAnimation.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 */ 124function 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 */ 141CanvasAnimation.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 */ 157CanvasAnimation.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 */ 175CanvasAnimation.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 */ 182CanvasAnimation.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 */ 233CanvasAnimation.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 */ 246CanvasAnimation.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 */ 270CalendarManager = {}; 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 */ 279CalendarManager.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 */ 321CalendarManager.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 */ 359CalendarManager.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 */ 410CalendarManager.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 */ 439CalendarManager.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 */ 459CalendarManager.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 */ 502CalendarManager.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 533var DATE_TIME_REGEX = 534 /^(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)\.\d+(\+|-)(\d\d):(\d\d)$/; 535var DATE_TIME_REGEX_Z = /^(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)\.\d+Z$/; 536var 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*/ 546function 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 */ 584function 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 */ 594function 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 */ 607function 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 */ 628function 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*/ 637function filterSpecialChar(data) { 638 if (data) { 639 data = data.replace(/</g, '<'); 640 data = data.replace(/>/g, '>'); 641 data = data.replace(/&/g, '&'); 642 data = data.replace(/%7B/g, '{'); 643 data = data.replace(/%7D/g, '}'); 644 data = data.replace(/"/g, '"'); 645 data = data.replace(/'/g, '\''); 646 } 647 return data; 648}; 649 650/** 651 * Called from options.js page on saving the settings 652 */ 653function 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 */ 665function 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 */ 694function 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 */ 714function isCalendarUrl(url) { 715 return url.indexOf('www.google.com/calendar') != -1 ? true : false; 716}; 717 718/** 719 * Initializes everything. 720 */ 721function 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. 740window.addEventListener('load', init, false); 741