1/**
2 * Copyright (c) 2010 The Chromium Authors. All rights reserved.  Use of this
3 * source code is governed by a BSD-style license that can be found in the
4 * 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 * @return {Object} out An object containing the event properties.
276 */
277CalendarManager.extractEvent = function(elem) {
278  var out = {};
279
280  for (var node = elem.firstChild; node != null; node = node.nextSibling) {
281    if (node.nodeName == 'title') {
282        out.title = node.firstChild ? node.firstChild.nodeValue : MSG_NO_TITLE;
283    } else if (node.nodeName == 'link' &&
284               node.getAttribute('rel') == 'alternate') {
285      out.url = node.getAttribute('href');
286    } else if (node.nodeName == 'gd:where') {
287      out.location = node.getAttribute('valueString');
288    } else if (node.nodeName == 'gd:who') {
289      if (node.firstChild) {
290        out.attendeeStatus = node.firstChild.getAttribute('value');
291      }
292    } else if (node.nodeName == 'gd:eventStatus') {
293      out.status = node.getAttribute('value');
294    } else if (node.nodeName == 'gd:when') {
295      var startTimeStr = node.getAttribute('startTime');
296      var endTimeStr = node.getAttribute('endTime');
297
298      startTime = rfc3339StringToDate(startTimeStr);
299      endTime = rfc3339StringToDate(endTimeStr);
300
301      if (startTime == null || endTime == null) {
302        continue;
303      }
304
305      out.isAllDay = (startTimeStr.length <= 11);
306      out.startTime = startTime;
307      out.endTime = endTime;
308    }
309  }
310  return out;
311};
312
313/**
314 * Polls the server to get the feed of the user.
315 */
316CalendarManager.pollServer = function() {
317  if (! pollUnderProgress) {
318    eventList = [];
319    pollUnderProgress = true;
320    pendingLoadId_ = null;
321    calendars = [];
322    lastPollTime_ = getCurrentTime();
323    var url;
324    var xhr = new XMLHttpRequest();
325    try {
326      xhr.onreadystatechange = CalendarManager.genResponseChangeFunc(xhr);
327      xhr.onerror = function(error) {
328        console.log('error: ' + error);
329        nextEvent_ = null;
330        canvasAnimation_.drawFinal();
331      };
332      if (isMultiCalendar) {
333        url = MULTIPLE_CALENDAR_SUPPORT_URL;
334      } else {
335        url = SINGLE_CALENDAR_SUPPORT_URL;
336      }
337
338      xhr.open('GET', url);
339      xhr.send(null);
340    } catch (e) {
341      console.log('ex: ' + e);
342      nextEvent_ = null;
343      canvasAnimation_.drawFinal();
344    }
345  }
346};
347
348/**
349 * Gathers the list of all calendars of a specific user for multiple calendar
350 * support and event entries in single calendar.
351 * @param {xmlHttpRequest} xhr xmlHttpRequest object containing server response.
352 * @return {Object} anonymous function which returns to onReadyStateChange.
353 */
354CalendarManager.genResponseChangeFunc = function(xhr) {
355  return function() {
356    if (xhr.readyState != 4) {
357      return;
358    }
359    if (!xhr.responseXML) {
360      console.log('No responseXML');
361      nextEvent_ = null;
362      canvasAnimation_.drawFinal();
363      return;
364    }
365    if (isMultiCalendar) {
366      var entry_ = xhr.responseXML.getElementsByTagName('entry');
367      if (entry_ && entry_.length > 0) {
368        calendars = [];
369        for (var i = 0, entry; entry = entry_[i]; ++i) {
370          if (!i) {
371            defaultAuthor = entry.querySelector('title').textContent;
372          }
373          // Include only those calendars which are not hidden and selected
374          var isHidden = entry.querySelector('hidden');
375          var isSelected = entry.querySelector('selected');
376          if (isHidden && isHidden.getAttribute('value') == 'false') {
377            if (isSelected && isSelected.getAttribute('value') == 'true') {
378              var calendar_content = entry.querySelector('content');
379              var cal_src = calendar_content.getAttribute('src');
380              cal_src += '?toolbar=true&max-results=10';
381              calendars.push(cal_src);
382            }
383          }
384        }
385        CalendarManager.getCalendarFeed(0);
386        return;
387      }
388    } else {
389      calendars = [];
390      calendars.push(SINGLE_CALENDAR_SUPPORT_URL);
391      CalendarManager.parseCalendarEntry(xhr.responseXML, 0);
392      return;
393    }
394
395    console.error('Error: feed retrieved, but no event found');
396    nextEvent_ = null;
397    canvasAnimation_.drawFinal();
398  };
399};
400
401/**
402 * Retrieves feed for a calendar
403 * @param {integer} calendarId Id of the calendar in array of calendars.
404 */
405CalendarManager.getCalendarFeed = function(calendarId) {
406  var xmlhttp = new XMLHttpRequest();
407  try {
408    xmlhttp.onreadystatechange = CalendarManager.onCalendarResponse(xmlhttp,
409                                     calendarId);
410    xmlhttp.onerror = function(error) {
411      console.log('error: ' + error);
412      nextEvent_ = null;
413      canvasAnimation_.drawFinal();
414    };
415
416    xmlhttp.open('GET', calendars[calendarId]);
417    xmlhttp.send(null);
418  }
419  catch (e) {
420    console.log('ex: ' + e);
421    nextEvent_ = null;
422    canvasAnimation_.drawFinal();
423  }
424};
425
426/**
427 * Gets the event entries of every calendar subscribed in default user calendar.
428 * @param {xmlHttpRequest} xmlhttp xmlHttpRequest containing server response
429 *     for the feed of a specific calendar.
430 * @param {integer} calendarId Variable for storing the no of calendars
431 *     processed.
432 * @return {Object} anonymous function which returns to onReadyStateChange.
433 */
434CalendarManager.onCalendarResponse = function(xmlhttp, calendarId) {
435  return function() {
436    if (xmlhttp.readyState != 4) {
437      return;
438    }
439    if (!xmlhttp.responseXML) {
440      console.log('No responseXML');
441      nextEvent_ = null;
442      canvasAnimation_.drawFinal();
443      return;
444    }
445    CalendarManager.parseCalendarEntry(xmlhttp.responseXML, calendarId);
446  };
447};
448
449/**
450 * Parses events from calendar response XML
451 * @param {string} responseXML Response XML for calendar.
452 * @param {integer} calendarId  Id of the calendar in array of calendars.
453 */
454CalendarManager.parseCalendarEntry = function(responseXML, calendarId) {
455  var entry_ = responseXML.getElementsByTagName('entry');
456  var author = responseXML.querySelector('author name').textContent;
457
458  if (entry_ && entry_.length > 0) {
459    for (var i = 0, entry; entry = entry_[i]; ++i) {
460      var event_ = CalendarManager.extractEvent(entry);
461
462      // Get the time from then to now
463      if (event_.startTime) {
464        var t = event_.startTime.getTime() - getCurrentTime();
465        if (t >= 0 && (event_.attendeeStatus != DECLINED_URL)) {
466            if (isMultiCalendar) {
467              event_.author = author;
468            }
469            eventList.push(event_);
470        }
471      }
472    }
473  }
474
475  calendarId++;
476  //get the next calendar
477  if (calendarId < calendars.length) {
478    CalendarManager.getCalendarFeed(calendarId);
479  } else {
480    CalendarManager.populateLatestEvent(eventList);
481  }
482};
483
484/**
485 * Fills the event list with the events acquired from the calendar(s).
486 * Parses entire event list and prepares an array of upcoming events.
487 * @param {Array} eventList List of all events.
488 */
489CalendarManager.populateLatestEvent = function(eventList) {
490  nextEvents = [];
491  if (isMultiCalendar) {
492    eventList.sort(sortByDate);
493  }
494
495  //populating next events array.
496  if (eventList.length > 0) {
497    nextEvent_ = eventList[0];
498    nextEvent_.startTime.setSeconds(0, 0);
499    nextEvents.push(nextEvent_);
500    var startTime = nextEvent_.startTime;
501    for (var i = 1, event; event = eventList[i]; i++) {
502      var time = event.startTime.setSeconds(0, 0);
503      if (time == startTime) {
504        nextEvents.push(event);
505      } else {
506        break;
507      }
508    }
509    if (nextEvents.length > 1) {
510      nextEvents.sort(sortByAuthor);
511    }
512    canvasAnimation_.animate();
513    return;
514  } else {
515    console.error('Error: feed retrieved, but no event found');
516    nextEvent_ = null;
517    canvasAnimation_.drawFinal();
518  }
519};
520
521var DATE_TIME_REGEX =
522  /^(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)\.\d+(\+|-)(\d\d):(\d\d)$/;
523var DATE_TIME_REGEX_Z = /^(\d\d\d\d)-(\d\d)-(\d\d)T(\d\d):(\d\d):(\d\d)\.\d+Z$/;
524var DATE_REGEX = /^(\d\d\d\d)-(\d\d)-(\d\d)$/;
525
526/**
527* Convert the incoming date into a javascript date.
528* @param {String} rfc3339 The rfc date in string format as following
529*     2006-04-28T09:00:00.000-07:00
530*     2006-04-28T09:00:00.000Z
531*     2006-04-19.
532* @return {Date} The javascript date format of the incoming date.
533*/
534function rfc3339StringToDate(rfc3339) {
535  var parts = DATE_TIME_REGEX.exec(rfc3339);
536
537  // Try out the Z version
538  if (!parts) {
539    parts = DATE_TIME_REGEX_Z.exec(rfc3339);
540  }
541
542  if (parts && parts.length > 0) {
543    var d = new Date();
544    d.setUTCFullYear(parts[1], parseInt(parts[2], 10) - 1, parts[3]);
545    d.setUTCHours(parts[4]);
546    d.setUTCMinutes(parts[5]);
547    d.setUTCSeconds(parts[6]);
548
549    var tzOffsetFeedMin = 0;
550    if (parts.length > 7) {
551      tzOffsetFeedMin = parseInt(parts[8], 10) * 60 + parseInt(parts[9], 10);
552      if (parts[7] != '-') { // This is supposed to be backwards.
553        tzOffsetFeedMin = -tzOffsetFeedMin;
554      }
555    }
556    return new Date(d.getTime() + tzOffsetFeedMin * 60 * 1000);
557  }
558
559  parts = DATE_REGEX.exec(rfc3339);
560  if (parts && parts.length > 0) {
561    return new Date(parts[1], parseInt(parts[2], 10) - 1, parts[3]);
562  }
563  return null;
564};
565
566/**
567 * Sorts all the events by date and time.
568 * @param {object} event_1 Event object.
569 * @param {object} event_2 Event object.
570 * @return {integer} timeDiff Difference in time.
571 */
572function sortByDate(event_1, event_2) {
573  return (event_1.startTime.getTime() - event_2.startTime.getTime());
574};
575
576/**
577 * Sorts all the events by author name.
578 * @param {object} event_1 Event object.
579 * @param {object} event_2 Event object.
580 * @return {integer} nameDiff Difference in default author and others.
581 */
582function sortByAuthor(event_1, event_2) {
583  var nameDiff;
584  if (event_2.author == defaultAuthor) {
585    nameDiff = 1;
586  } else {
587    return 0;
588  }
589  return nameDiff;
590};
591
592/**
593 * Fires once per minute to redraw extension icon.
594 */
595function redraw() {
596  // If the next event just passed, re-poll.
597  if (nextEvent_) {
598    var t = nextEvent_.startTime.getTime() - getCurrentTime();
599    if (t <= 0) {
600      CalendarManager.pollServer();
601      return;
602    }
603  }
604  canvasAnimation_.animate();
605
606  // if ((we are logged in) && (30 minutes have passed)) re-poll
607  if (nextEvent_ && (getCurrentTime() - lastPollTime_ >= POLL_INTERVAL)) {
608    CalendarManager.pollServer();
609  }
610};
611
612/**
613 * Returns the current time in milliseconds.
614 * @return {Number} Current time in milliseconds.
615 */
616function getCurrentTime() {
617  return (new Date()).getTime();
618};
619
620/**
621* Replaces ASCII characters from the title.
622* @param {String} data String containing ASCII code for special characters.
623* @return {String} data ASCII characters replaced with actual characters.
624*/
625function filterSpecialChar(data) {
626  if (data) {
627    data = data.replace(/&lt;/g, '<');
628    data = data.replace(/&gt;/g, '>');
629    data = data.replace(/&amp;/g, '&');
630    data = data.replace(/%7B/g, '{');
631    data = data.replace(/%7D/g, '}');
632    data = data.replace(/&quot;/g, '"');
633    data = data.replace(/&#39;/g, '\'');
634  }
635  return data;
636};
637
638/**
639 * Called from options.js page on saving the settings
640 */
641function onSettingsChange() {
642  isMultiCalendar = JSON.parse(localStorage.multiCalendar);
643  badgeAnimation_.start();
644  CalendarManager.pollServer();
645};
646
647/**
648 * Function runs on updating a tab having url of google applications.
649 * @param {integer} tabId Id of the tab which is updated.
650 * @param {String} changeInfo Gives the information of change in url.
651 * @param {String} tab Gives the url of the tab updated.
652 */
653function onTabUpdated(tabId, changeInfo, tab) {
654  var url = tab.url;
655  if (!url) {
656    return;
657  }
658
659  if ((url.indexOf('www.google.com/calendar/') != -1) ||
660      ((url.indexOf('www.google.com/a/') != -1) &&
661      (url.lastIndexOf('/acs') == url.length - 4)) ||
662      (url.indexOf('www.google.com/accounts/') != -1)) {
663
664    // The login screen isn't helpful
665    if (url.indexOf('https://www.google.com/accounts/ServiceLogin?') == 0) {
666      return;
667    }
668
669    if (pendingLoadId_) {
670      clearTimeout(pendingLoadId_);
671      pendingLoadId_ = null;
672    }
673
674    // try to poll in 2 second [which makes the redirects settle down]
675    pendingLoadId_ = setTimeout(CalendarManager.pollServer, 2000);
676  }
677};
678
679/**
680 * Called when the user clicks on extension icon and opens calendar page.
681 */
682function onClickAction() {
683  chrome.tabs.getAllInWindow(null, function(tabs) {
684    for (var i = 0, tab; tab = tabs[i]; i++) {
685      if (tab.url && isCalendarUrl(tab.url)) {
686        chrome.tabs.update(tab.id, {selected: true});
687        CalendarManager.pollServer();
688        return;
689      }
690    }
691    chrome.tabs.create({url: GOOGLE_CALENDAR_URL});
692    CalendarManager.pollServer();
693  });
694};
695
696/**
697 * Checks whether an instance of Google calendar is already open.
698 * @param {String} url Url of the tab visited.
699 * @return {boolean} true if the url is a Google calendar relative url, false
700 *     otherwise.
701 */
702function isCalendarUrl(url) {
703  return url.indexOf('www.google.com/calendar') != -1 ? true : false;
704};
705
706/**
707 * Initializes everything.
708 */
709function init() {
710  badgeAnimation_ = new BadgeAnimation();
711  canvasAnimation_ = new CanvasAnimation();
712
713  isMultiCalendar = JSON.parse(localStorage.multiCalendar || false);
714
715  chrome.browserAction.setIcon({path: '../images/icon-16.gif'});
716  badgeAnimation_.start();
717  CalendarManager.pollServer();
718  window.setInterval(redraw, DRAW_INTERVAL);
719
720  chrome.tabs.onUpdated.addListener(onTabUpdated);
721
722  chrome.browserAction.onClicked.addListener(function(tab) {
723    onClickAction();
724  });
725};
726
727//Adding listener when body is loaded to call init function.
728window.addEventListener('load', init, false);
729