• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1var cookie_namespace = 'android_developer';
2var isMobile = false; // true if mobile, so we can adjust some layout
3var mPagePath; // initialized in ready() function
4
5var basePath = getBaseUri(location.pathname);
6var SITE_ROOT = toRoot + basePath.substring(1, basePath.indexOf("/", 1));
7
8// TODO(akassay) generate this var in the reference doc build.
9var API_LEVELS = ['1', '2', '3', '4', '5', '6', '7', '8', '9',
10      '10', '11', '12', '13', '14', '15', '16',
11      '17', '18', '19', '20', '21', '22', '23', '24', '25', 'O'];
12var METADATA = METADATA || {};
13var RESERVED_METADATA_CATEGORY_NAMES = ['extras', 'carousel', 'collections',
14                                        'searchHeroCollections'];
15
16// Ensure that all ajax getScript() requests allow caching
17$.ajaxSetup({
18  cache: true
19});
20
21/******  ON LOAD SET UP STUFF *********/
22
23$(document).ready(function() {
24
25  // prep nav expandos
26  var pagePath = location.href.replace(location.hash, '');
27  // account for intl docs by removing the intl/*/ path
28  if (pagePath.indexOf("/intl/") == 0) {
29    pagePath = pagePath.substr(pagePath.indexOf("/", 6)); // start after intl/ to get last /
30  }
31
32  if (pagePath.indexOf(SITE_ROOT) == 0) {
33    if (pagePath == '' || pagePath.charAt(pagePath.length - 1) == '/') {
34      pagePath += 'index.html';
35    }
36  }
37
38  // Need a copy of the pagePath before it gets changed in the next block;
39  // it's needed to perform proper tab highlighting in offline docs (see rootDir below)
40  var pagePathOriginal = pagePath;
41  if (SITE_ROOT.match(/\.\.\//) || SITE_ROOT == '') {
42    // If running locally, SITE_ROOT will be a relative path, so account for that by
43    // finding the relative URL to this page. This will allow us to find links on the page
44    // leading back to this page.
45    var pathParts = pagePath.split('/');
46    var relativePagePathParts = [];
47    var upDirs = (SITE_ROOT.match(/(\.\.\/)+/) || [''])[0].length / 3;
48    for (var i = 0; i < upDirs; i++) {
49      relativePagePathParts.push('..');
50    }
51    for (var i = 0; i < upDirs; i++) {
52      relativePagePathParts.push(pathParts[pathParts.length - (upDirs - i) - 1]);
53    }
54    relativePagePathParts.push(pathParts[pathParts.length - 1]);
55    pagePath = relativePagePathParts.join('/');
56  } else {
57    // Otherwise the page path is already an absolute URL
58  }
59
60  // set global variable so we can highlight the sidenav a bit later (such as for google reference)
61  // and highlight the sidenav
62  mPagePath = pagePath;
63
64  // Check for params and remove them.
65  mPagePath = mPagePath.split('?')[0];
66  highlightSidenav();
67
68  // set up prev/next links if they exist
69  var $selNavLink = $('#nav').find('a[href="' + pagePath + '"]');
70  var $selListItem;
71  if ($selNavLink.length) {
72    $selListItem = $selNavLink.closest('li');
73
74    // set up prev links
75    var $prevLink = [];
76    var $prevListItem = $selListItem.prev('li');
77
78    var crossBoundaries = ($("body.design").length > 0) || ($("body.guide").length > 0) ? true :
79false; // navigate across topic boundaries only in design docs
80    if ($prevListItem.length) {
81      if ($prevListItem.hasClass('nav-section') || crossBoundaries) {
82        // jump to last topic of previous section
83        $prevLink = $prevListItem.find('a:last');
84      } else if (!$selListItem.hasClass('nav-section')) {
85        // jump to previous topic in this section
86        $prevLink = $prevListItem.find('a:eq(0)');
87      }
88    } else {
89      // jump to this section's index page (if it exists)
90      var $parentListItem = $selListItem.parents('li');
91      $prevLink = $selListItem.parents('li').find('a');
92
93      // except if cross boundaries aren't allowed, and we're at the top of a section already
94      // (and there's another parent)
95      if (!crossBoundaries && $parentListItem.hasClass('nav-section') &&
96                           $selListItem.hasClass('nav-section')) {
97        $prevLink = [];
98      }
99    }
100
101    // set up next links
102    var $nextLink = [];
103    var startClass = false;
104    var isCrossingBoundary = false;
105
106    if ($selListItem.hasClass('nav-section') && $selListItem.children('div.empty').length == 0) {
107      // we're on an index page, jump to the first topic
108      $nextLink = $selListItem.find('ul:eq(0)').find('a:eq(0)');
109
110      // if there aren't any children, go to the next section (required for About pages)
111      if ($nextLink.length == 0) {
112        $nextLink = $selListItem.next('li').find('a');
113      } else if ($('.topic-start-link').length) {
114        // as long as there's a child link and there is a "topic start link" (we're on a landing)
115        // then set the landing page "start link" text to be the first doc title
116        $('.topic-start-link').text($nextLink.text().toUpperCase());
117      }
118
119      // If the selected page has a description, then it's a class or article homepage
120      if ($selListItem.find('a[description]').length) {
121        // this means we're on a class landing page
122        startClass = true;
123      }
124    } else {
125      // jump to the next topic in this section (if it exists)
126      $nextLink = $selListItem.next('li').find('a:eq(0)');
127      if ($nextLink.length == 0) {
128        isCrossingBoundary = true;
129        // no more topics in this section, jump to the first topic in the next section
130        $nextLink = $selListItem.parents('li:eq(0)').next('li').find('a:eq(0)');
131        if (!$nextLink.length) {  // Go up another layer to look for next page (lesson > class > course)
132          $nextLink = $selListItem.parents('li:eq(1)').next('li.nav-section').find('a:eq(0)');
133          if ($nextLink.length == 0) {
134            // if that doesn't work, we're at the end of the list, so disable NEXT link
135            $('.next-page-link').attr('href', '').addClass("disabled")
136                                .click(function() { return false; });
137            // and completely hide the one in the footer
138            $('.content-footer .next-page-link').hide();
139          }
140        }
141      }
142    }
143
144    if (startClass) {
145      $('.start-class-link').attr('href', $nextLink.attr('href')).removeClass("hide");
146
147      // if there's no training bar (below the start button),
148      // then we need to add a bottom border to button
149      if (!$("#tb").length) {
150        $('.start-class-link').css({'border-bottom':'1px solid #DADADA'});
151      }
152    } else if (isCrossingBoundary && !$('body.design').length) {  // Design always crosses boundaries
153      $('.content-footer.next-class').show();
154      $('.next-page-link').attr('href', '')
155                          .removeClass("hide").addClass("disabled")
156                          .click(function() { return false; });
157      // and completely hide the one in the footer
158      $('.content-footer .next-page-link').hide();
159      $('.content-footer .prev-page-link').hide();
160
161      if ($nextLink.length) {
162        $('.next-class-link').attr('href', $nextLink.attr('href'))
163                             .removeClass("hide");
164
165        $('.content-footer .next-class-link').append($nextLink.html());
166
167        $('.next-class-link').find('.new').empty();
168      }
169    } else {
170      $('.next-page-link').attr('href', $nextLink.attr('href'))
171                          .removeClass("hide");
172      // for the footer link, also add the previous and next page titles
173      if ($prevLink.length) {
174        $('.content-footer .prev-page-link').append($prevLink.html());
175      }
176      if ($nextLink.length) {
177        $('.content-footer .next-page-link').append($nextLink.html());
178      }
179    }
180
181    if (!startClass && $prevLink.length) {
182      var prevHref = $prevLink.attr('href');
183      if (prevHref == SITE_ROOT + 'index.html') {
184        // Don't show Previous when it leads to the homepage
185      } else {
186        $('.prev-page-link').attr('href', $prevLink.attr('href')).removeClass("hide");
187      }
188    }
189  }
190
191  // Set up the course landing pages for Training with class names and descriptions
192  if ($('body.trainingcourse').length) {
193    var $classLinks = $selListItem.find('ul li a').not('#nav .nav-section .nav-section ul a');
194
195    // create an array for all the class descriptions
196    var $classDescriptions = new Array($classLinks.length);
197    var lang = getLangPref();
198    $classLinks.each(function(index) {
199      var langDescr = $(this).attr(lang + "-description");
200      if (typeof langDescr !== 'undefined' && langDescr !== false) {
201        // if there's a class description in the selected language, use that
202        $classDescriptions[index] = langDescr;
203      } else {
204        // otherwise, use the default english description
205        $classDescriptions[index] = $(this).attr("description");
206      }
207    });
208
209    var $olClasses  = $('<ol class="class-list"></ol>');
210    var $liClass;
211    var $h2Title;
212    var $pSummary;
213    var $olLessons;
214    var $liLesson;
215    $classLinks.each(function(index) {
216      $liClass  = $('<li class="clearfix"></li>');
217      $h2Title  = $('<a class="title" href="' + $(this).attr('href') + '"><h2 class="norule">' + $(this).html() + '</h2><span></span></a>');
218      $pSummary = $('<p class="description">' + $classDescriptions[index] + '</p>');
219
220      $olLessons  = $('<ol class="lesson-list"></ol>');
221
222      $lessons = $(this).closest('li').find('ul li a');
223
224      if ($lessons.length) {
225        $lessons.each(function(index) {
226          $olLessons.append('<li><a href="' + $(this).attr('href') + '">' + $(this).html() + '</a></li>');
227        });
228      } else {
229        $pSummary.addClass('article');
230      }
231
232      $liClass.append($h2Title).append($pSummary).append($olLessons);
233      $olClasses.append($liClass);
234    });
235    $('#classes').append($olClasses);
236  }
237
238  // Set up expand/collapse behavior
239  initExpandableNavItems("#nav");
240
241  // Set up play-on-hover <video> tags.
242  $('video.play-on-hover').bind('click', function() {
243    $(this).get(0).load(); // in case the video isn't seekable
244    $(this).get(0).play();
245  });
246
247  // Set up play-on-click for <video> tags with a "video-wrapper".
248  $('.video-wrapper > video').bind('click', function() {
249    this.play();
250    $(this.parentElement).addClass('playing');
251  });
252
253  // Set up tooltips
254  var TOOLTIP_MARGIN = 10;
255  $('acronym,.tooltip-link').each(function() {
256    var $target = $(this);
257    var $tooltip = $('<div>')
258        .addClass('tooltip-box')
259        .append($target.attr('title'))
260        .hide()
261        .appendTo('body');
262    $target.removeAttr('title');
263
264    $target.hover(function() {
265      // in
266      var targetRect = $target.offset();
267      targetRect.width = $target.width();
268      targetRect.height = $target.height();
269
270      $tooltip.css({
271        left: targetRect.left,
272        top: targetRect.top + targetRect.height + TOOLTIP_MARGIN
273      });
274      $tooltip.addClass('below');
275      $tooltip.show();
276    }, function() {
277      // out
278      $tooltip.hide();
279    });
280  });
281
282  // Set up <h2> deeplinks
283  $('h2').click(function() {
284    var id = $(this).attr('id');
285    if (id) {
286      if (history && history.replaceState) {
287        // Change url without scrolling.
288        history.replaceState({}, '', '#' + id);
289      } else {
290        document.location.hash = id;
291      }
292    }
293  });
294
295  //Loads the +1 button
296  //var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true;
297  //po.src = 'https://apis.google.com/js/plusone.js';
298  //var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s);
299});
300// END of the onload event
301
302function initExpandableNavItems(rootTag) {
303  var toggleIcon = $(
304      rootTag + ' li.nav-section .nav-section-header .toggle-icon, ' +
305      rootTag + ' li.nav-section .nav-section-header a[href="#"]');
306
307  toggleIcon.on('click keypress', function(e) {
308    if (e.type == 'keypress' && e.which == 13 || e.type == 'click') {
309      doNavToggle(this);
310    }
311  });
312
313  // Stop expand/collapse behavior when clicking on nav section links
314  // (since we're navigating away from the page)
315  // This selector captures the first instance of <a>, but not those with "#" as the href.
316  $('.nav-section-header').find('a:eq(0)').not('a[href="#"]').click(function(evt) {
317    window.location.href = $(this).attr('href');
318    return false;
319  });
320}
321
322function doNavToggle(el) {
323  var section = $(el).closest('li.nav-section');
324  if (section.hasClass('expanded')) {
325    /* hide me and descendants */
326    section.find('ul').slideUp(250, function() {
327      // remove 'expanded' class from my section and any children
328      section.closest('li').removeClass('expanded');
329      $('li.nav-section', section).removeClass('expanded');
330    });
331  } else {
332    /* show me */
333    // first hide all other siblings
334    var $others = $('li.nav-section.expanded', $(el).closest('ul')).not('.sticky');
335    $others.removeClass('expanded').children('ul').slideUp(250);
336
337    // now expand me
338    section.closest('li').addClass('expanded');
339    section.children('ul').slideDown(250);
340  }
341}
342
343/** Highlight the current page in sidenav, expanding children as appropriate */
344function highlightSidenav() {
345  // if something is already highlighted, undo it. This is for dynamic navigation (Samples index)
346  if ($("ul#nav li.selected").length) {
347    unHighlightSidenav();
348  }
349  // look for URL in sidenav, including the hash
350  var $selNavLink = $('#nav').find('a[href="' + mPagePath + location.hash + '"]');
351
352  // If the selNavLink is still empty, look for it without the hash
353  if ($selNavLink.length == 0) {
354    $selNavLink = $('#nav').find('a[href="' + mPagePath + '"]');
355  }
356
357  var $selListItem;
358  var breadcrumb = [];
359
360  if ($selNavLink.length) {
361    // Find this page's <li> in sidenav and set selected
362    $selListItem = $selNavLink.closest('li');
363    $selListItem.addClass('selected');
364
365    // Traverse up the tree and expand all parent nav-sections
366    $selNavLink.parents('li.nav-section').each(function() {
367      $(this).addClass('expanded');
368      $(this).children('ul').show();
369
370      var link = $(this).find('a').first();
371
372      if (!$(this).is($selListItem)) {
373        breadcrumb.unshift(link)
374      }
375    });
376
377    $('#nav').scrollIntoView($selNavLink);
378  }
379
380  breadcrumb.forEach(function(link) {
381    link.dacCrumbs();
382  });
383}
384
385function unHighlightSidenav() {
386  $("ul#nav li.selected").removeClass("selected");
387  $('ul#nav li.nav-section.expanded').removeClass('expanded').children('ul').hide();
388}
389
390var agent = navigator['userAgent'].toLowerCase();
391// If a mobile phone, set flag and do mobile setup
392if ((agent.indexOf("mobile") != -1) ||      // android, iphone, ipod
393    (agent.indexOf("blackberry") != -1) ||
394    (agent.indexOf("webos") != -1) ||
395    (agent.indexOf("mini") != -1)) {        // opera mini browsers
396  isMobile = true;
397}
398
399$(document).ready(function() {
400  $("pre:not(.no-pretty-print)").addClass("prettyprint");
401  prettyPrint();
402});
403
404/* Show popup dialogs */
405function showDialog(id) {
406  $dialog = $("#" + id);
407  $dialog.prepend('<div class="box-border"><div class="top"> <div class="left"></div> <div class="right"></div></div><div class="bottom"> <div class="left"></div> <div class="right"></div> </div> </div>');
408  $dialog.wrapInner('<div/>');
409  $dialog.removeClass("hide");
410}
411
412/* #########    COOKIES!     ########## */
413
414function readCookie(cookie) {
415  var myCookie = cookie_namespace + "_" + cookie + "=";
416  if (document.cookie) {
417    var index = document.cookie.indexOf(myCookie);
418    if (index != -1) {
419      var valStart = index + myCookie.length;
420      var valEnd = document.cookie.indexOf(";", valStart);
421      if (valEnd == -1) {
422        valEnd = document.cookie.length;
423      }
424      var val = document.cookie.substring(valStart, valEnd);
425      return val;
426    }
427  }
428  return 0;
429}
430
431function writeCookie(cookie, val, section) {
432  if (val == undefined) return;
433  section = section == null ? "_" : "_" + section + "_";
434  var age = 2 * 365 * 24 * 60 * 60; // set max-age to 2 years
435  var cookieValue = cookie_namespace + section + cookie + "=" + val +
436                    "; max-age=" + age + "; path=/";
437  document.cookie = cookieValue;
438}
439
440/* #########     END COOKIES!     ########## */
441
442/*
443 * Manages secion card states and nav resize to conclude loading
444 */
445(function() {
446  $(document).ready(function() {
447
448    // Stack hover states
449    $('.section-card-menu').each(function(index, el) {
450      var height = $(el).height();
451      $(el).css({height:height + 'px', position:'relative'});
452      var $cardInfo = $(el).find('.card-info');
453
454      $cardInfo.css({position: 'absolute', bottom:'0px', left:'0px', right:'0px', overflow:'visible'});
455    });
456
457  });
458
459})();
460
461/*      MISC LIBRARY FUNCTIONS     */
462
463function toggle(obj, slide) {
464  var ul = $("ul:first", obj);
465  var li = ul.parent();
466  if (li.hasClass("closed")) {
467    if (slide) {
468      ul.slideDown("fast");
469    } else {
470      ul.show();
471    }
472    li.removeClass("closed");
473    li.addClass("open");
474    $(".toggle-img", li).attr("title", "hide pages");
475  } else {
476    ul.slideUp("fast");
477    li.removeClass("open");
478    li.addClass("closed");
479    $(".toggle-img", li).attr("title", "show pages");
480  }
481}
482
483function buildToggleLists() {
484  $(".toggle-list").each(
485    function(i) {
486      $("div:first", this).append("<a class='toggle-img' href='#' title='show pages' onClick='toggle(this.parentNode.parentNode, true); return false;'></a>");
487      $(this).addClass("closed");
488    });
489}
490
491function hideNestedItems(list, toggle) {
492  $list = $(list);
493  // hide nested lists
494  if ($list.hasClass('showing')) {
495    $("li ol", $list).hide('fast');
496    $list.removeClass('showing');
497  // show nested lists
498  } else {
499    $("li ol", $list).show('fast');
500    $list.addClass('showing');
501  }
502  $(".more,.less", $(toggle)).toggle();
503}
504
505/* Call this to add listeners to a <select> element for Studio/Eclipse/Other docs */
506function setupIdeDocToggle() {
507  $("select.ide").change(function() {
508    var selected = $(this).find("option:selected").attr("value");
509    $(".select-ide").hide();
510    $(".select-ide." + selected).show();
511
512    $("select.ide").val(selected);
513  });
514}
515
516/* Used to hide and reveal supplemental content, such as long code samples.
517   See the companion CSS in android-developer-docs.css */
518function toggleContent(obj) {
519  var div = $(obj).closest(".toggle-content");
520  var toggleMe = $(".toggle-content-toggleme:eq(0)", div);
521  if (div.hasClass("closed")) { // if it's closed, open it
522    toggleMe.slideDown();
523    $(".toggle-content-text:eq(0)", obj).toggle();
524    div.removeClass("closed").addClass("open");
525    $(".toggle-content-img:eq(0)", div).attr("title", "hide").attr("src", toRoot +
526                  "assets/images/styles/disclosure_up.png");
527  } else { // if it's open, close it
528    toggleMe.slideUp('fast', function() {  // Wait until the animation is done before closing arrow
529      $(".toggle-content-text:eq(0)", obj).toggle();
530      div.removeClass("open").addClass("closed");
531      div.find(".toggle-content").removeClass("open").addClass("closed")
532              .find(".toggle-content-toggleme").hide();
533      $(".toggle-content-img", div).attr("title", "show").attr("src", toRoot +
534                  "assets/images/styles/disclosure_down.png");
535    });
536  }
537  return false;
538}
539
540/* New version of expandable content */
541function toggleExpandable(link, id) {
542  if ($(id).is(':visible')) {
543    $(id).slideUp();
544    $(link).removeClass('expanded');
545  } else {
546    $(id).slideDown();
547    $(link).addClass('expanded');
548  }
549}
550
551function hideExpandable(ids) {
552  $(ids).slideUp();
553  $(ids).prev('h4').find('a.expandable').removeClass('expanded');
554}
555
556/*
557 *  Slideshow 1.0
558 *  Used on /index.html and /develop/index.html for carousel
559 *
560 *  Sample usage:
561 *  HTML -
562 *  <div class="slideshow-container">
563 *   <a href="" class="slideshow-prev">Prev</a>
564 *   <a href="" class="slideshow-next">Next</a>
565 *   <ul>
566 *       <li class="item"><img src="images/marquee1.jpg"></li>
567 *       <li class="item"><img src="images/marquee2.jpg"></li>
568 *       <li class="item"><img src="images/marquee3.jpg"></li>
569 *       <li class="item"><img src="images/marquee4.jpg"></li>
570 *   </ul>
571 *  </div>
572 *
573 *   <script type="text/javascript">
574 *   $('.slideshow-container').dacSlideshow({
575 *       auto: true,
576 *       btnPrev: '.slideshow-prev',
577 *       btnNext: '.slideshow-next'
578 *   });
579 *   </script>
580 *
581 *  Options:
582 *  btnPrev:    optional identifier for previous button
583 *  btnNext:    optional identifier for next button
584 *  btnPause:   optional identifier for pause button
585 *  auto:       whether or not to auto-proceed
586 *  speed:      animation speed
587 *  autoTime:   time between auto-rotation
588 *  easing:     easing function for transition
589 *  start:      item to select by default
590 *  scroll:     direction to scroll in
591 *  pagination: whether or not to include dotted pagination
592 *
593 */
594
595(function($) {
596  $.fn.dacSlideshow = function(o) {
597
598    //Options - see above
599    o = $.extend({
600      btnPrev:   null,
601      btnNext:   null,
602      btnPause:  null,
603      auto:      true,
604      speed:     500,
605      autoTime:  12000,
606      easing:    null,
607      start:     0,
608      scroll:    1,
609      pagination: true
610
611    }, o || {});
612
613    //Set up a carousel for each
614    return this.each(function() {
615
616      var running = false;
617      var animCss = o.vertical ? "top" : "left";
618      var sizeCss = o.vertical ? "height" : "width";
619      var div = $(this);
620      var ul = $("ul", div);
621      var tLi = $("li", ul);
622      var tl = tLi.size();
623      var timer = null;
624
625      var li = $("li", ul);
626      var itemLength = li.size();
627      var curr = o.start;
628
629      li.css({float: o.vertical ? "none" : "left"});
630      ul.css({margin: "0", padding: "0", position: "relative", "list-style-type": "none", "z-index": "1"});
631      div.css({position: "relative", "z-index": "2", left: "0px"});
632
633      var liSize = o.vertical ? height(li) : width(li);
634      var ulSize = liSize * itemLength;
635      var divSize = liSize;
636
637      li.css({width: li.width(), height: li.height()});
638      ul.css(sizeCss, ulSize + "px").css(animCss, -(curr * liSize));
639
640      div.css(sizeCss, divSize + "px");
641
642      //Pagination
643      if (o.pagination) {
644        var pagination = $("<div class='pagination'></div>");
645        var pag_ul = $("<ul></ul>");
646        if (tl > 1) {
647          for (var i = 0; i < tl; i++) {
648            var li = $("<li>" + i + "</li>");
649            pag_ul.append(li);
650            if (i == o.start) li.addClass('active');
651            li.click(function() {
652              go(parseInt($(this).text()));
653            })
654          }
655          pagination.append(pag_ul);
656          div.append(pagination);
657        }
658      }
659
660      //Previous button
661      if (o.btnPrev)
662             $(o.btnPrev).click(function(e) {
663               e.preventDefault();
664               return go(curr - o.scroll);
665             });
666
667      //Next button
668      if (o.btnNext)
669             $(o.btnNext).click(function(e) {
670               e.preventDefault();
671               return go(curr + o.scroll);
672             });
673
674      //Pause button
675      if (o.btnPause)
676             $(o.btnPause).click(function(e) {
677               e.preventDefault();
678               if ($(this).hasClass('paused')) {
679                 startRotateTimer();
680               } else {
681                 pauseRotateTimer();
682               }
683             });
684
685      //Auto rotation
686      if (o.auto) startRotateTimer();
687
688      function startRotateTimer() {
689        clearInterval(timer);
690        timer = setInterval(function() {
691          if (curr == tl - 1) {
692            go(0);
693          } else {
694            go(curr + o.scroll);
695          }
696        }, o.autoTime);
697        $(o.btnPause).removeClass('paused');
698      }
699
700      function pauseRotateTimer() {
701        clearInterval(timer);
702        $(o.btnPause).addClass('paused');
703      }
704
705      //Go to an item
706      function go(to) {
707        if (!running) {
708
709          if (to < 0) {
710            to = itemLength - 1;
711          } else if (to > itemLength - 1) {
712            to = 0;
713          }
714          curr = to;
715
716          running = true;
717
718          ul.animate(
719              animCss == "left" ? {left: -(curr * liSize)} : {top: -(curr * liSize)} , o.speed, o.easing,
720                     function() {
721                       running = false;
722                     }
723                 );
724
725          $(o.btnPrev + "," + o.btnNext).removeClass("disabled");
726          $((curr - o.scroll < 0 && o.btnPrev)              ||
727             (curr + o.scroll > itemLength && o.btnNext)              ||
728             []
729           ).addClass("disabled");
730
731          var nav_items = $('li', pagination);
732          nav_items.removeClass('active');
733          nav_items.eq(to).addClass('active');
734
735        }
736        if (o.auto) startRotateTimer();
737        return false;
738      };
739    });
740  };
741
742  function css(el, prop) {
743    return parseInt($.css(el[0], prop)) || 0;
744  };
745  function width(el) {
746    return el[0].offsetWidth + css(el, 'marginLeft') + css(el, 'marginRight');
747  };
748  function height(el) {
749    return el[0].offsetHeight + css(el, 'marginTop') + css(el, 'marginBottom');
750  };
751
752})(jQuery);
753
754/*
755 *  dacSlideshow 1.0
756 *  Used on develop/index.html for side-sliding tabs
757 *
758 *  Sample usage:
759 *  HTML -
760 *  <div class="slideshow-container">
761 *   <a href="" class="slideshow-prev">Prev</a>
762 *   <a href="" class="slideshow-next">Next</a>
763 *   <ul>
764 *       <li class="item"><img src="images/marquee1.jpg"></li>
765 *       <li class="item"><img src="images/marquee2.jpg"></li>
766 *       <li class="item"><img src="images/marquee3.jpg"></li>
767 *       <li class="item"><img src="images/marquee4.jpg"></li>
768 *   </ul>
769 *  </div>
770 *
771 *   <script type="text/javascript">
772 *   $('.slideshow-container').dacSlideshow({
773 *       auto: true,
774 *       btnPrev: '.slideshow-prev',
775 *       btnNext: '.slideshow-next'
776 *   });
777 *   </script>
778 *
779 *  Options:
780 *  btnPrev:    optional identifier for previous button
781 *  btnNext:    optional identifier for next button
782 *  auto:       whether or not to auto-proceed
783 *  speed:      animation speed
784 *  autoTime:   time between auto-rotation
785 *  easing:     easing function for transition
786 *  start:      item to select by default
787 *  scroll:     direction to scroll in
788 *  pagination: whether or not to include dotted pagination
789 *
790 */
791(function($) {
792  $.fn.dacTabbedList = function(o) {
793
794    //Options - see above
795    o = $.extend({
796      speed : 250,
797      easing: null,
798      nav_id: null,
799      frame_id: null
800    }, o || {});
801
802    //Set up a carousel for each
803    return this.each(function() {
804
805      var curr = 0;
806      var running = false;
807      var animCss = "margin-left";
808      var sizeCss = "width";
809      var div = $(this);
810
811      var nav = $(o.nav_id, div);
812      var nav_li = $("li", nav);
813      var nav_size = nav_li.size();
814      var frame = div.find(o.frame_id);
815      var content_width = $(frame).find('ul').width();
816      //Buttons
817      $(nav_li).click(function(e) {
818           go($(nav_li).index($(this)));
819         })
820
821      //Go to an item
822      function go(to) {
823        if (!running) {
824          curr = to;
825          running = true;
826
827          frame.animate({'margin-left' : -(curr * content_width)}, o.speed, o.easing,
828                     function() {
829                       running = false;
830                     }
831                 );
832
833          nav_li.removeClass('active');
834          nav_li.eq(to).addClass('active');
835
836        }
837        return false;
838      };
839    });
840  };
841
842  function css(el, prop) {
843    return parseInt($.css(el[0], prop)) || 0;
844  };
845  function width(el) {
846    return el[0].offsetWidth + css(el, 'marginLeft') + css(el, 'marginRight');
847  };
848  function height(el) {
849    return el[0].offsetHeight + css(el, 'marginTop') + css(el, 'marginBottom');
850  };
851
852})(jQuery);
853
854/* ######################################################## */
855/* #################  JAVADOC REFERENCE ################### */
856/* ######################################################## */
857
858
859
860var API_LEVEL_COOKIE = "api_level";
861var minLevel = 1;
862var maxLevel = 1;
863
864function buildApiLevelSelector() {
865  maxLevel = API_LEVELS.length;
866  var userApiLevel = parseInt(readCookie(API_LEVEL_COOKIE));
867  userApiLevel = userApiLevel == 0 ? maxLevel : userApiLevel; // If there's no cookie (zero), use the max by default
868
869  minLevel = parseInt($("#doc-api-level").attr("class"));
870  // Handle provisional api levels; the provisional level will always be the highest possible level
871  // Provisional api levels will also have a length; other stuff that's just missing a level won't,
872  // so leave those kinds of entities at the default level of 1 (for example, the R.styleable class)
873  if (isNaN(minLevel) && minLevel.length) {
874    minLevel = maxLevel;
875  }
876  var select = $("#apiLevelSelector").html("").change(changeApiLevel);
877  for (var i = maxLevel - 1; i >= 0; i--) {
878    var option = $("<option />").attr("value", "" + API_LEVELS[i]).append("" + API_LEVELS[i]);
879    //  if (API_LEVELS[i] < minLevel) option.addClass("absent"); // always false for strings (codenames)
880    select.append(option);
881  }
882
883  // get the DOM element and use setAttribute cuz IE6 fails when using jquery .attr('selected',true)
884  var selectedLevelItem = $("#apiLevelSelector option[value='" + userApiLevel + "']").get(0);
885  selectedLevelItem.setAttribute('selected', true);
886}
887
888function changeApiLevel() {
889  maxLevel = API_LEVELS.length;
890  minLevel = parseInt($('#doc-api-level').attr('class'));
891  var selectedLevel = maxLevel;
892
893  selectedLevel = parseInt($("#apiLevelSelector option:selected").val());
894  toggleVisisbleApis(selectedLevel, "body");
895
896  writeCookie(API_LEVEL_COOKIE, selectedLevel, null);
897
898  if (selectedLevel < minLevel) {
899      // Show the API notice dialog, set number values and button event
900      $('#api-unavailable').trigger('modal-open');
901      $('#api-unavailable .selected-level').text(selectedLevel);
902      $('#api-unavailable .api-level').text(minLevel);
903      $('#api-unavailable button.ok').attr('onclick','$("#apiLevelSelector").val("' + minLevel + '");changeApiLevel();');
904  }
905}
906
907function toggleVisisbleApis(selectedLevel, context) {
908  var apis = $(".api", context);
909  apis.each(function(i) {
910    var obj = $(this);
911    var className = obj.attr("class");
912    var apiLevelIndex = className.lastIndexOf("-") + 1;
913    var apiLevelEndIndex = className.indexOf(" ", apiLevelIndex);
914    apiLevelEndIndex = apiLevelEndIndex != -1 ? apiLevelEndIndex : className.length;
915    var apiLevel = className.substring(apiLevelIndex, apiLevelEndIndex);
916    if (apiLevel.length == 0) { // for odd cases when the since data is actually missing, just bail
917      return;
918    }
919    apiLevel = parseInt(apiLevel);
920
921    // Handle provisional api levels; if this item's level is the provisional one, set it to the max
922    var selectedLevelNum = parseInt(selectedLevel)
923    var apiLevelNum = parseInt(apiLevel);
924    if (isNaN(apiLevelNum)) {
925      apiLevelNum = maxLevel;
926    }
927
928    // Grey things out that aren't available and give a tooltip title
929    if (apiLevelNum > selectedLevelNum) {
930      obj.addClass("absent").attr("title", "Requires API Level \"" +
931            apiLevel + "\" or higher. To reveal, change the target API level " +
932              "above the left navigation.");
933    } else obj.removeClass("absent").removeAttr("title");
934  });
935}
936
937/* #################  SIDENAV TREE VIEW ################### */
938/* TODO: eliminate redundancy with non-google functions */
939function init_google_navtree(navtree_id, toroot, root_nodes) {
940  var me = new Object();
941  me.toroot = toroot;
942  me.node = new Object();
943
944  me.node.li = document.getElementById(navtree_id);
945  if (!me.node.li) {
946    return;
947  }
948
949  me.node.children_data = root_nodes;
950  me.node.children = new Array();
951  me.node.children_ul = document.createElement("ul");
952  me.node.get_children_ul = function() { return me.node.children_ul; };
953  //me.node.children_ul.className = "children_ul";
954  me.node.li.appendChild(me.node.children_ul);
955  me.node.depth = 0;
956
957  get_google_node(me, me.node);
958}
959
960function new_google_node(me, mom, text, link, children_data, api_level) {
961  var node = new Object();
962  var child;
963  node.children = Array();
964  node.children_data = children_data;
965  node.depth = mom.depth + 1;
966  node.get_children_ul = function() {
967      if (!node.children_ul) {
968        node.children_ul = document.createElement("ul");
969        node.children_ul.className = "tree-list-children";
970        node.li.appendChild(node.children_ul);
971      }
972      return node.children_ul;
973    };
974  node.li = document.createElement("li");
975
976  mom.get_children_ul().appendChild(node.li);
977
978  if (link) {
979    child = document.createElement("a");
980
981  } else {
982    child = document.createElement("span");
983    child.className = "tree-list-subtitle";
984
985  }
986  if (children_data != null) {
987    node.li.className = "nav-section";
988    node.label_div = document.createElement("div");
989    node.label_div.className = "nav-section-header-ref";
990    node.li.appendChild(node.label_div);
991    get_google_node(me, node);
992    node.label_div.appendChild(child);
993  } else {
994    node.li.appendChild(child);
995  }
996  if (link) {
997    child.href = me.toroot + link;
998  }
999  node.label = document.createTextNode(text);
1000  child.appendChild(node.label);
1001
1002  node.children_ul = null;
1003
1004  return node;
1005}
1006
1007function get_google_node(me, mom) {
1008  mom.children_visited = true;
1009  var linkText;
1010  for (var i in mom.children_data) {
1011    var node_data = mom.children_data[i];
1012    linkText = node_data[0];
1013
1014    if (linkText.match("^" + "com.google.android") == "com.google.android") {
1015      linkText = linkText.substr(19, linkText.length);
1016    }
1017    mom.children[i] = new_google_node(me, mom, linkText, node_data[1],
1018        node_data[2], node_data[3]);
1019  }
1020}
1021
1022/****** NEW version of script to build google and sample navs dynamically ******/
1023// TODO: update Google reference docs to tolerate this new implementation
1024
1025var NODE_NAME = 0;
1026var NODE_HREF = 1;
1027var NODE_GROUP = 2;
1028var NODE_TAGS = 3;
1029var NODE_CHILDREN = 4;
1030
1031function init_google_navtree2(navtree_id, data) {
1032  var $containerUl = $("#" + navtree_id);
1033  for (var i in data) {
1034    var node_data = data[i];
1035    $containerUl.append(new_google_node2(node_data));
1036  }
1037
1038  // Make all third-generation list items 'sticky' to prevent them from collapsing
1039  $containerUl.find('li li li.nav-section').addClass('sticky');
1040
1041  initExpandableNavItems("#" + navtree_id);
1042}
1043
1044function new_google_node2(node_data) {
1045  var linkText = node_data[NODE_NAME];
1046  if (linkText.match("^" + "com.google.android") == "com.google.android") {
1047    linkText = linkText.substr(19, linkText.length);
1048  }
1049  var $li = $('<li>');
1050  var $a;
1051  if (node_data[NODE_HREF] != null) {
1052    $a = $('<a href="' + toRoot + node_data[NODE_HREF] + '" title="' + linkText + '" >' +
1053        linkText + '</a>');
1054  } else {
1055    $a = $('<a href="#" onclick="return false;" title="' + linkText + '" >' +
1056        linkText + '/</a>');
1057  }
1058  var $childUl = $('<ul>');
1059  if (node_data[NODE_CHILDREN] != null) {
1060    $li.addClass("nav-section");
1061    $a = $('<div class="nav-section-header">').append($a);
1062    if (node_data[NODE_HREF] == null) $a.addClass('empty');
1063
1064    for (var i in node_data[NODE_CHILDREN]) {
1065      var child_node_data = node_data[NODE_CHILDREN][i];
1066      $childUl.append(new_google_node2(child_node_data));
1067    }
1068    $li.append($childUl);
1069  }
1070  $li.prepend($a);
1071
1072  return $li;
1073}
1074
1075function showGoogleRefTree() {
1076  init_default_google_navtree(toRoot);
1077  init_default_gcm_navtree(toRoot);
1078}
1079
1080function init_default_google_navtree(toroot) {
1081  // load json file for navtree data
1082  $.getScript(toRoot + 'gms_navtree_data.js', function(data, textStatus, jqxhr) {
1083    // when the file is loaded, initialize the tree
1084    if (jqxhr.status === 200) {
1085      init_google_navtree("gms-tree-list", toroot, GMS_NAVTREE_DATA);
1086      highlightSidenav();
1087    }
1088  });
1089}
1090
1091function init_default_gcm_navtree(toroot) {
1092  // load json file for navtree data
1093  $.getScript(toRoot + 'gcm_navtree_data.js', function(data, textStatus, jqxhr) {
1094    // when the file is loaded, initialize the tree
1095    if (jqxhr.status === 200) {
1096      init_google_navtree("gcm-tree-list", toroot, GCM_NAVTREE_DATA);
1097      highlightSidenav();
1098    }
1099  });
1100}
1101
1102/* TOGGLE INHERITED MEMBERS */
1103
1104/* Toggle an inherited class (arrow toggle)
1105 * @param linkObj  The link that was clicked.
1106 * @param expand  'true' to ensure it's expanded. 'false' to ensure it's closed.
1107 *                'null' to simply toggle.
1108 */
1109function toggleInherited(linkObj, expand) {
1110  var base = linkObj.getAttribute("id");
1111  var list = document.getElementById(base + "-list");
1112  var summary = document.getElementById(base + "-summary");
1113  var trigger = document.getElementById(base + "-trigger");
1114  var a = $(linkObj);
1115  if ((expand == null && a.hasClass("closed")) || expand) {
1116    list.style.display = "none";
1117    summary.style.display = "block";
1118    trigger.src = toRoot + "assets/images/styles/disclosure_up.png";
1119    a.removeClass("closed");
1120    a.addClass("opened");
1121  } else if ((expand == null && a.hasClass("opened")) || (expand == false)) {
1122    list.style.display = "block";
1123    summary.style.display = "none";
1124    trigger.src = toRoot + "assets/images/styles/disclosure_down.png";
1125    a.removeClass("opened");
1126    a.addClass("closed");
1127  }
1128  return false;
1129}
1130
1131/* Toggle all inherited classes in a single table (e.g. all inherited methods)
1132 * @param linkObj  The link that was clicked.
1133 * @param expand  'true' to ensure it's expanded. 'false' to ensure it's closed.
1134 *                'null' to simply toggle.
1135 */
1136function toggleAllInherited(linkObj, expand) {
1137  var a = $(linkObj);
1138  var table = $(a.parent().parent().parent()); // ugly way to get table/tbody
1139  var expandos = $(".jd-expando-trigger", table);
1140  if ((expand == null && a.text() == "[Expand]") || expand) {
1141    expandos.each(function(i) {
1142      toggleInherited(this, true);
1143    });
1144    a.text("[Collapse]");
1145  } else if ((expand == null && a.text() == "[Collapse]") || (expand == false)) {
1146    expandos.each(function(i) {
1147      toggleInherited(this, false);
1148    });
1149    a.text("[Expand]");
1150  }
1151  return false;
1152}
1153
1154/* Toggle all inherited members in the class (link in the class title)
1155 */
1156function toggleAllClassInherited() {
1157  var a = $("#toggleAllClassInherited"); // get toggle link from class title
1158  var toggles = $(".toggle-all", $("#body-content"));
1159  if (a.text() == "[Expand All]") {
1160    toggles.each(function(i) {
1161      toggleAllInherited(this, true);
1162    });
1163    a.text("[Collapse All]");
1164  } else {
1165    toggles.each(function(i) {
1166      toggleAllInherited(this, false);
1167    });
1168    a.text("[Expand All]");
1169  }
1170  return false;
1171}
1172
1173/* Expand all inherited members in the class. Used when initiating page search */
1174function ensureAllInheritedExpanded() {
1175  var toggles = $(".toggle-all", $("#body-content"));
1176  toggles.each(function(i) {
1177    toggleAllInherited(this, true);
1178  });
1179  $("#toggleAllClassInherited").text("[Collapse All]");
1180}
1181
1182/* HANDLE KEY EVENTS
1183 * - Listen for Ctrl+F (Cmd on Mac) and expand all inherited members (to aid page search)
1184 */
1185var agent = navigator['userAgent'].toLowerCase();
1186var mac = agent.indexOf("macintosh") != -1;
1187
1188$(document).keydown(function(e) {
1189  var control = mac ? e.metaKey && !e.ctrlKey : e.ctrlKey; // get ctrl key
1190  if (control && e.which == 70) {  // 70 is "F"
1191    ensureAllInheritedExpanded();
1192  }
1193});
1194
1195/* On-demand functions */
1196
1197/** Move sample code line numbers out of PRE block and into non-copyable column */
1198function initCodeLineNumbers() {
1199  var numbers = $("#codesample-block a.number");
1200  if (numbers.length) {
1201    $("#codesample-line-numbers").removeClass("hidden").append(numbers);
1202  }
1203
1204  $(document).ready(function() {
1205    // select entire line when clicked
1206    $("span.code-line").click(function() {
1207      if (!shifted) {
1208        selectText(this);
1209      }
1210    });
1211    // invoke line link on double click
1212    $(".code-line").dblclick(function() {
1213      document.location.hash = $(this).attr('id');
1214    });
1215    // highlight the line when hovering on the number
1216    $("#codesample-line-numbers a.number").mouseover(function() {
1217      var id = $(this).attr('href');
1218      $(id).css('background', '#e7e7e7');
1219    });
1220    $("#codesample-line-numbers a.number").mouseout(function() {
1221      var id = $(this).attr('href');
1222      $(id).css('background', 'none');
1223    });
1224  });
1225}
1226
1227// create SHIFT key binder to avoid the selectText method when selecting multiple lines
1228var shifted = false;
1229$(document).bind('keyup keydown', function(e) {
1230  shifted = e.shiftKey; return true;
1231});
1232
1233// courtesy of jasonedelman.com
1234function selectText(element) {
1235  var doc = document      ,
1236        range, selection
1237  ;
1238  if (doc.body.createTextRange) { //ms
1239    range = doc.body.createTextRange();
1240    range.moveToElementText(element);
1241    range.select();
1242  } else if (window.getSelection) { //all others
1243    selection = window.getSelection();
1244    range = doc.createRange();
1245    range.selectNodeContents(element);
1246    selection.removeAllRanges();
1247    selection.addRange(range);
1248  }
1249}
1250
1251/** Display links and other information about samples that match the
1252    group specified by the URL */
1253function showSamples() {
1254  var group = $("#samples").attr('class');
1255  $("#samples").html("<p>Here are some samples for <b>" + group + "</b> apps:</p>");
1256
1257  var $ul = $("<ul>");
1258  $selectedLi = $("#nav li.selected");
1259
1260  $selectedLi.children("ul").children("li").each(function() {
1261    var $li = $("<li>").append($(this).find("a").first().clone());
1262    var $samplesLink = $li.find("a");
1263    if ($samplesLink.text().endsWith('/')) {
1264      $samplesLink.text($samplesLink.text().slice(0,-1));
1265    }
1266    $ul.append($li);
1267  });
1268
1269  $("#samples").append($ul);
1270
1271}
1272
1273/* ########################################################## */
1274/* ###################  RESOURCE CARDS  ##################### */
1275/* ########################################################## */
1276
1277/** Handle resource queries, collections, and grids (sections). Requires
1278    jd_tag_helpers.js and the *_unified_data.js to be loaded. */
1279
1280(function() {
1281  $(document).ready(function() {
1282    // Need to initialize hero carousel before other sections for dedupe
1283    // to work correctly.
1284    $('[data-carousel-query]').dacCarouselQuery();
1285
1286    // Iterate over all instances and initialize a resource widget.
1287    $('.resource-widget').resourceWidget();
1288  });
1289
1290  $.fn.widgetOptions = function() {
1291    return {
1292      cardSizes: (this.data('cardsizes') || '').split(','),
1293      maxResults: parseInt(this.data('maxresults'), 10) || Infinity,
1294      initialResults: this.data('initialResults'),
1295      itemsPerPage: this.data('itemsPerPage'),
1296      sortOrder: this.data('sortorder'),
1297      query: this.data('query'),
1298      section: this.data('section'),
1299      /* Added by LFL 6/6/14 */
1300      resourceStyle: this.data('resourcestyle') || 'card',
1301      stackSort: this.data('stacksort') || 'true',
1302      // For filter based resources
1303      allowDuplicates: this.data('allow-duplicates') || 'false'
1304    };
1305  };
1306
1307  $.fn.deprecateOldGridStyles = function() {
1308    var m = this.get(0).className.match(/\bcol-(\d+)\b/);
1309    if (m && !this.is('.cols > *')) {
1310      this.removeClass('col-' + m[1]);
1311    }
1312    return this;
1313  }
1314
1315  /*
1316   * Three types of resource layouts:
1317   * Flow - Uses a fixed row-height flow using float left style.
1318   * Carousel - Single card slideshow all same dimension absolute.
1319   * Stack - Uses fixed columns and flexible element height.
1320   */
1321  function initResourceWidget(widget, resources, opts) {
1322    var $widget = $(widget).deprecateOldGridStyles();
1323    var isFlow = $widget.hasClass('resource-flow-layout');
1324    var isCarousel = $widget.hasClass('resource-carousel-layout');
1325    var isStack = $widget.hasClass('resource-stack-layout');
1326
1327    opts = opts || $widget.widgetOptions();
1328    resources = resources || metadata.query(opts);
1329
1330    if (opts.maxResults !== undefined) {
1331      resources = resources.slice(0, opts.maxResults);
1332    }
1333
1334    if (isFlow) {
1335      drawResourcesFlowWidget($widget, opts, resources);
1336    } else if (isCarousel) {
1337      drawResourcesCarouselWidget($widget, opts, resources);
1338    } else if (isStack) {
1339      opts.numStacks = $widget.data('numstacks');
1340      drawResourcesStackWidget($widget, opts, resources);
1341    }
1342  }
1343
1344  $.fn.resourceWidget = function(resources, options) {
1345    return this.each(function() {
1346      initResourceWidget(this, resources, options);
1347    });
1348  };
1349
1350  /* Initializes a Resource Carousel Widget */
1351  function drawResourcesCarouselWidget($widget, opts, resources) {
1352    $widget.empty();
1353    var plusone = false; // stop showing plusone buttons on cards
1354
1355    $widget.addClass('resource-card slideshow-container')
1356      .append($('<a>').addClass('slideshow-prev').text('Prev'))
1357      .append($('<a>').addClass('slideshow-next').text('Next'));
1358
1359    var css = {'width': $widget.width() + 'px',
1360                'height': $widget.height() + 'px'};
1361
1362    var $ul = $('<ul>');
1363
1364    for (var i = 0; i < resources.length; ++i) {
1365      var $card = $('<a>')
1366        .attr('href', cleanUrl(resources[i].url))
1367        .decorateResourceCard(resources[i], plusone);
1368
1369      $('<li>').css(css)
1370          .append($card)
1371          .appendTo($ul);
1372    }
1373
1374    $('<div>').addClass('frame')
1375      .append($ul)
1376      .appendTo($widget);
1377
1378    $widget.dacSlideshow({
1379      auto: true,
1380      btnPrev: '.slideshow-prev',
1381      btnNext: '.slideshow-next'
1382    });
1383  }
1384
1385  /* Initializes a Resource Card Stack Widget (column-based layout)
1386     Modified by LFL 6/6/14
1387   */
1388  function drawResourcesStackWidget($widget, opts, resources, sections) {
1389    // Don't empty widget, grab all items inside since they will be the first
1390    // items stacked, followed by the resource query
1391    var plusone = false; // stop showing plusone buttons on cards
1392    var cards = $widget.find('.resource-card').detach().toArray();
1393    var numStacks = opts.numStacks || 1;
1394    var $stacks = [];
1395
1396    for (var i = 0; i < numStacks; ++i) {
1397      $stacks[i] = $('<div>').addClass('resource-card-stack')
1398          .appendTo($widget);
1399    }
1400
1401    var sectionResources = [];
1402
1403    // Extract any subsections that are actually resource cards
1404    if (sections) {
1405      for (i = 0; i < sections.length; ++i) {
1406        if (!sections[i].sections || !sections[i].sections.length) {
1407          // Render it as a resource card
1408          sectionResources.push(
1409            $('<a>')
1410              .addClass('resource-card section-card')
1411              .attr('href', cleanUrl(sections[i].resource.url))
1412              .decorateResourceCard(sections[i].resource, plusone)[0]
1413          );
1414
1415        } else {
1416          cards.push(
1417            $('<div>')
1418              .addClass('resource-card section-card-menu')
1419              .decorateResourceSection(sections[i], plusone)[0]
1420          );
1421        }
1422      }
1423    }
1424
1425    cards = cards.concat(sectionResources);
1426
1427    for (i = 0; i < resources.length; ++i) {
1428      var $card = createResourceElement(resources[i], opts);
1429
1430      if (opts.resourceStyle.indexOf('related') > -1) {
1431        $card.addClass('related-card');
1432      }
1433
1434      cards.push($card[0]);
1435    }
1436
1437    if (opts.stackSort !== 'false') {
1438      for (i = 0; i < cards.length; ++i) {
1439        // Find the stack with the shortest height, but give preference to
1440        // left to right order.
1441        var minHeight = $stacks[0].height();
1442        var minIndex = 0;
1443
1444        for (var j = 1; j < numStacks; ++j) {
1445          var height = $stacks[j].height();
1446          if (height < minHeight - 45) {
1447            minHeight = height;
1448            minIndex = j;
1449          }
1450        }
1451
1452        $stacks[minIndex].append($(cards[i]));
1453      }
1454    }
1455  }
1456
1457  /*
1458    Create a resource card using the given resource object and a list of html
1459     configured options. Returns a jquery object containing the element.
1460  */
1461  function createResourceElement(resource, opts, plusone) {
1462    var $el;
1463
1464    // The difference here is that generic cards are not entirely clickable
1465    // so its a div instead of an a tag, also the generic one is not given
1466    // the resource-card class so it appears with a transparent background
1467    // and can be styled in whatever way the css setup.
1468    if (opts.resourceStyle === 'generic') {
1469      $el = $('<div>')
1470        .addClass('resource')
1471        .attr('href', cleanUrl(resource.url))
1472        .decorateResource(resource, opts);
1473    } else {
1474      var cls = 'resource resource-card';
1475
1476      $el = $('<a>')
1477        .addClass(cls)
1478        .attr('href', cleanUrl(resource.url))
1479        .decorateResourceCard(resource, plusone);
1480    }
1481
1482    return $el;
1483  }
1484
1485  function createResponsiveFlowColumn(cardSize) {
1486    var cardWidth = parseInt(cardSize.match(/(\d+)/)[1], 10);
1487    var column = $('<div>').addClass('col-' + (cardWidth / 3) + 'of6');
1488    if (cardWidth < 9) {
1489      column.addClass('col-tablet-1of2');
1490    } else if (cardWidth > 9 && cardWidth < 18) {
1491      column.addClass('col-tablet-1of1');
1492    }
1493    if (cardWidth < 18) {
1494      column.addClass('col-mobile-1of1');
1495    }
1496    return column;
1497  }
1498
1499  /* Initializes a flow widget, see distribute.scss for generating accompanying css */
1500  function drawResourcesFlowWidget($widget, opts, resources) {
1501    // We'll be doing our own modifications to opts.
1502    opts = $.extend({}, opts);
1503
1504    $widget.empty().addClass('cols');
1505    if (opts.itemsPerPage) {
1506      $('<div class="col-1of1 dac-section-links dac-text-center">')
1507        .append(
1508          $('<div class="dac-section-link dac-show-less" data-toggle="show-less">Less<i class="dac-sprite dac-auto-unfold-less"></i></div>'),
1509          $('<div class="dac-section-link dac-show-more" data-toggle="show-more">More<i class="dac-sprite dac-auto-unfold-more"></i></div>')
1510        )
1511        .appendTo($widget);
1512    }
1513
1514    $widget.data('options.resourceflow', opts);
1515    $widget.data('resources.resourceflow', resources);
1516
1517    drawResourceFlowPage($widget, opts, resources);
1518  }
1519
1520  function drawResourceFlowPage($widget, opts, resources) {
1521    var cardSizes = opts.cardSizes || ['6x6']; // 2015-08-09: dynamic card sizes are deprecated
1522    var i = opts.currentIndex || 0;
1523    var j = 0;
1524    var plusone = false; // stop showing plusone buttons on cards
1525    var firstPage = i === 0;
1526    var initialResults = opts.initialResults || opts.itemsPerPage || resources.length;
1527    var max = firstPage ? initialResults : i + opts.itemsPerPage;
1528    max = Math.min(resources.length, max);
1529
1530    var page = $('<div class="resource-flow-page">');
1531    if (opts.itemsPerPage) {
1532      $widget.find('.dac-section-links').before(page);
1533    } else {
1534      $widget.append(page);
1535    }
1536
1537    while (i < max) {
1538      var cardSize = cardSizes[j++ % cardSizes.length];
1539      cardSize = cardSize.replace(/^\s+|\s+$/, '');
1540
1541      var column = createResponsiveFlowColumn(cardSize).appendTo(page);
1542
1543      // A stack has a third dimension which is the number of stacked items
1544      var isStack = cardSize.match(/(\d+)x(\d+)x(\d+)/);
1545      var stackCount = 0;
1546      var $stackDiv = null;
1547
1548      if (isStack) {
1549        // Create a stack container which should have the dimensions defined
1550        // by the product of the items inside.
1551        $stackDiv = $('<div>').addClass('resource-card-stack resource-card-' + isStack[1] +
1552          'x' + isStack[2] * isStack[3]) .appendTo(column);
1553      }
1554
1555      // Build each stack item or just a single item
1556      do {
1557        var resource = resources[i];
1558
1559        var $card = createResourceElement(resources[i], opts, plusone);
1560
1561        $card.addClass('resource-card-' + cardSize +
1562          ' resource-card-' + resource.type.toLowerCase());
1563
1564        if (isStack) {
1565          $card.addClass('resource-card-' + isStack[1] + 'x' + isStack[2]);
1566          if (++stackCount === parseInt(isStack[3])) {
1567            $card.addClass('resource-card-row-stack-last');
1568            stackCount = 0;
1569          }
1570        } else {
1571          stackCount = 0;
1572        }
1573
1574        $card.appendTo($stackDiv || column);
1575
1576      } while (++i < max && stackCount > 0);
1577
1578      // Record number of pages viewed in analytics.
1579      if (!firstPage) {
1580        var clicks = Math.ceil((i - initialResults) / opts.itemsPerPage);
1581        devsite.analytics.trackAnalyticsEvent('event',
1582            'Cards', 'Click More', clicks);
1583      }
1584    }
1585
1586    opts.currentIndex = i;
1587    $widget.toggleClass('dac-has-more', i < resources.length);
1588    $widget.toggleClass('dac-has-less', !firstPage);
1589
1590    $widget.trigger('dac:domchange');
1591    if (opts.onRenderPage) {
1592      opts.onRenderPage(page);
1593    }
1594  }
1595
1596  function drawResourceFlowReset($widget, opts, resources) {
1597    $widget.find('.resource-flow-page')
1598        .slice(1)
1599        .remove();
1600    $widget.toggleClass('dac-has-more', true);
1601    $widget.toggleClass('dac-has-less', false);
1602
1603    opts.currentIndex = Math.min(opts.initialResults, resources.length);
1604    devsite.analytics.trackAnalyticsEvent('event', 'Cards', 'Click Less');
1605  }
1606
1607  /* A decorator for event functions which finds the surrounding widget and it's options */
1608  function wrapWithWidget(func) {
1609    return function(e) {
1610      if (e) e.preventDefault();
1611
1612      var $widget = $(this).closest('.resource-flow-layout');
1613      var opts = $widget.data('options.resourceflow');
1614      var resources = $widget.data('resources.resourceflow');
1615      func($widget, opts, resources);
1616    };
1617  }
1618
1619  /* Build a site map of resources using a section as a root. */
1620  function buildSectionList(opts) {
1621    if (opts.section && SECTION_BY_ID[opts.section]) {
1622      return SECTION_BY_ID[opts.section].sections || [];
1623    }
1624    return [];
1625  }
1626
1627  function cleanUrl(url) {
1628    if (url && url.indexOf('//') === -1) {
1629      url = toRoot + url;
1630    }
1631
1632    return url;
1633  }
1634
1635  // Delegated events for resources.
1636  $(document).on('click', '.resource-flow-layout [data-toggle="show-more"]', wrapWithWidget(drawResourceFlowPage));
1637  $(document).on('click', '.resource-flow-layout [data-toggle="show-less"]', wrapWithWidget(drawResourceFlowReset));
1638})();
1639
1640(function($) {
1641  // A mapping from category and type values to new values or human presentable strings.
1642  var SECTION_MAP = {
1643    googleplay: 'google play'
1644  };
1645
1646  /*
1647    Utility method for creating dom for the description area of a card.
1648    Used in decorateResourceCard and decorateResource.
1649  */
1650  function buildResourceCardDescription(resource, plusone) {
1651    var $description = $('<div>').addClass('description ellipsis');
1652
1653    $description.append($('<div>').addClass('text').html(resource.summary));
1654
1655    if (resource.cta) {
1656      $description.append($('<a>').addClass('cta').html(resource.cta));
1657    }
1658
1659    if (plusone) {
1660      var plusurl = resource.url.indexOf("//") > -1 ? resource.url :
1661        "//developer.android.com/" + resource.url;
1662
1663      $description.append($('<div>').addClass('util')
1664        .append($('<div>').addClass('g-plusone')
1665          .attr('data-size', 'small')
1666          .attr('data-align', 'right')
1667          .attr('data-href', plusurl)));
1668    }
1669
1670    return $description;
1671  }
1672
1673  /* Simple jquery function to create dom for a standard resource card */
1674  $.fn.decorateResourceCard = function(resource, plusone) {
1675    var section = resource.category || resource.type;
1676    section = (SECTION_MAP[section] || section).toLowerCase();
1677    var imgUrl = resource.image ||
1678      'assets/images/resource-card-default-android.jpg';
1679
1680    if (imgUrl.indexOf('//') === -1) {
1681      imgUrl = toRoot + imgUrl;
1682    }
1683
1684    if (resource.type === 'youtube' || resource.type === 'video') {
1685      $('<div>').addClass('play-button')
1686        .append($('<i class="dac-sprite dac-play-white">'))
1687        .appendTo(this);
1688    }
1689
1690    $('<div>').addClass('card-bg')
1691      .css('background-image', 'url(' + (imgUrl || toRoot +
1692        'assets/images/resource-card-default-android.jpg') + ')')
1693      .appendTo(this);
1694
1695    $('<div>').addClass('card-info' + (!resource.summary ? ' empty-desc' : ''))
1696      .append($('<div>').addClass('section').text(section))
1697      .append($('<div>').addClass('title' + (resource.title_highlighted ? ' highlighted' : ''))
1698        .html(resource.title_highlighted || resource.title))
1699      .append(buildResourceCardDescription(resource, plusone))
1700      .appendTo(this);
1701
1702    return this;
1703  };
1704
1705  /* Simple jquery function to create dom for a resource section card (menu) */
1706  $.fn.decorateResourceSection = function(section, plusone) {
1707    var resource = section.resource;
1708    //keep url clean for matching and offline mode handling
1709    var urlPrefix = resource.image.indexOf("//") > -1 ? "" : toRoot;
1710    var $base = $('<a>')
1711        .addClass('card-bg')
1712        .attr('href', resource.url)
1713        .append($('<div>').addClass('card-section-icon')
1714          .append($('<div>').addClass('icon'))
1715          .append($('<div>').addClass('section').html(resource.title)))
1716      .appendTo(this);
1717
1718    var $cardInfo = $('<div>').addClass('card-info').appendTo(this);
1719
1720    if (section.sections && section.sections.length) {
1721      // Recurse the section sub-tree to find a resource image.
1722      var stack = [section];
1723
1724      while (stack.length) {
1725        if (stack[0].resource.image) {
1726          $base.css('background-image', 'url(' + urlPrefix + stack[0].resource.image + ')');
1727          break;
1728        }
1729
1730        if (stack[0].sections) {
1731          stack = stack.concat(stack[0].sections);
1732        }
1733
1734        stack.shift();
1735      }
1736
1737      var $ul = $('<ul>')
1738        .appendTo($cardInfo);
1739
1740      var max = section.sections.length > 3 ? 3 : section.sections.length;
1741
1742      for (var i = 0; i < max; ++i) {
1743
1744        var subResource = section.sections[i];
1745        if (!plusone) {
1746          $('<li>')
1747            .append($('<a>').attr('href', subResource.url)
1748              .append($('<div>').addClass('title').html(subResource.title))
1749              .append($('<div>').addClass('description ellipsis')
1750                .append($('<div>').addClass('text').html(subResource.summary))
1751                .append($('<div>').addClass('util'))))
1752          .appendTo($ul);
1753        } else {
1754          $('<li>')
1755            .append($('<a>').attr('href', subResource.url)
1756              .append($('<div>').addClass('title').html(subResource.title))
1757              .append($('<div>').addClass('description ellipsis')
1758                .append($('<div>').addClass('text').html(subResource.summary))
1759                .append($('<div>').addClass('util')
1760                  .append($('<div>').addClass('g-plusone')
1761                    .attr('data-size', 'small')
1762                    .attr('data-align', 'right')
1763                    .attr('data-href', resource.url)))))
1764          .appendTo($ul);
1765        }
1766      }
1767
1768      // Add a more row
1769      if (max < section.sections.length) {
1770        $('<li>')
1771          .append($('<a>').attr('href', resource.url)
1772            .append($('<div>')
1773              .addClass('title')
1774              .text('More')))
1775        .appendTo($ul);
1776      }
1777    } else {
1778      // No sub-resources, just render description?
1779    }
1780
1781    return this;
1782  };
1783
1784  /* Render other types of resource styles that are not cards. */
1785  $.fn.decorateResource = function(resource, opts) {
1786    var imgUrl = resource.image ||
1787      'assets/images/resource-card-default-android.jpg';
1788    var linkUrl = resource.url;
1789
1790    if (imgUrl.indexOf('//') === -1) {
1791      imgUrl = toRoot + imgUrl;
1792    }
1793
1794    if (linkUrl && linkUrl.indexOf('//') === -1) {
1795      linkUrl = toRoot + linkUrl;
1796    }
1797
1798    $(this).append(
1799      $('<div>').addClass('image')
1800        .css('background-image', 'url(' + imgUrl + ')'),
1801      $('<div>').addClass('info').append(
1802        $('<h4>').addClass('title').html(resource.title_highlighted || resource.title),
1803        $('<p>').addClass('summary').html(resource.summary),
1804        $('<a>').attr('href', linkUrl).addClass('cta').html('Learn More')
1805      )
1806    );
1807
1808    return this;
1809  };
1810})(jQuery);
1811
1812/*
1813  Fullscreen Carousel
1814
1815  The following allows for an area at the top of the page that takes over the
1816  entire browser height except for its top offset and an optional bottom
1817  padding specified as a data attribute.
1818
1819  HTML:
1820
1821  <div class="fullscreen-carousel">
1822    <div class="fullscreen-carousel-content">
1823      <!-- content here -->
1824    </div>
1825    <div class="fullscreen-carousel-content">
1826      <!-- content here -->
1827    </div>
1828
1829    etc ...
1830
1831  </div>
1832
1833  Control over how the carousel takes over the screen can mostly be defined in
1834  a css file. Setting min-height on the .fullscreen-carousel-content elements
1835  will prevent them from shrinking to far vertically when the browser is very
1836  short, and setting max-height on the .fullscreen-carousel itself will prevent
1837  the area from becoming to long in the case that the browser is stretched very
1838  tall.
1839
1840  There is limited functionality for having multiple sections since that request
1841  was removed, but it is possible to add .next-arrow and .prev-arrow elements to
1842  scroll between multiple content areas.
1843*/
1844
1845(function() {
1846  $(document).ready(function() {
1847    $('.fullscreen-carousel').each(function() {
1848      initWidget(this);
1849    });
1850  });
1851
1852  function initWidget(widget) {
1853    var $widget = $(widget);
1854
1855    var topOffset = $widget.offset().top;
1856    var padBottom = parseInt($widget.data('paddingbottom')) || 0;
1857    var maxHeight = 0;
1858    var minHeight = 0;
1859    var $content = $widget.find('.fullscreen-carousel-content');
1860    var $nextArrow = $widget.find('.next-arrow');
1861    var $prevArrow = $widget.find('.prev-arrow');
1862    var $curSection = $($content[0]);
1863
1864    if ($content.length <= 1) {
1865      $nextArrow.hide();
1866      $prevArrow.hide();
1867    } else {
1868      $nextArrow.click(function() {
1869        var index = ($content.index($curSection) + 1);
1870        $curSection.hide();
1871        $curSection = $($content[index >= $content.length ? 0 : index]);
1872        $curSection.show();
1873      });
1874
1875      $prevArrow.click(function() {
1876        var index = ($content.index($curSection) - 1);
1877        $curSection.hide();
1878        $curSection = $($content[index < 0 ? $content.length - 1 : 0]);
1879        $curSection.show();
1880      });
1881    }
1882
1883    // Just hide all content sections except first.
1884    $content.each(function(index) {
1885      if ($(this).height() > minHeight) minHeight = $(this).height();
1886      $(this).css({position: 'absolute',  display: index > 0 ? 'none' : ''});
1887    });
1888
1889    // Register for changes to window size, and trigger.
1890    $(window).resize(resizeWidget);
1891    resizeWidget();
1892
1893    function resizeWidget() {
1894      var height = $(window).height() - topOffset - padBottom;
1895      $widget.width($(window).width());
1896      $widget.height(height < minHeight ? minHeight :
1897        (maxHeight && height > maxHeight ? maxHeight : height));
1898    }
1899  }
1900})();
1901
1902/*
1903  Tab Carousel
1904
1905  The following allows tab widgets to be installed via the html below. Each
1906  tab content section should have a data-tab attribute matching one of the
1907  nav items'. Also each tab content section should have a width matching the
1908  tab carousel.
1909
1910  HTML:
1911
1912  <div class="tab-carousel">
1913    <ul class="tab-nav">
1914      <li><a href="#" data-tab="handsets">Handsets</a>
1915      <li><a href="#" data-tab="wearable">Wearable</a>
1916      <li><a href="#" data-tab="tv">TV</a>
1917    </ul>
1918
1919    <div class="tab-carousel-content">
1920      <div data-tab="handsets">
1921        <!--Full width content here-->
1922      </div>
1923
1924      <div data-tab="wearable">
1925        <!--Full width content here-->
1926      </div>
1927
1928      <div data-tab="tv">
1929        <!--Full width content here-->
1930      </div>
1931    </div>
1932  </div>
1933
1934*/
1935(function() {
1936  $(document).ready(function() {
1937    $('.tab-carousel').each(function() {
1938      initWidget(this);
1939    });
1940  });
1941
1942  function initWidget(widget) {
1943    var $widget = $(widget);
1944    var $nav = $widget.find('.tab-nav');
1945    var $anchors = $nav.find('[data-tab]');
1946    var $li = $nav.find('li');
1947    var $contentContainer = $widget.find('.tab-carousel-content');
1948    var $tabs = $contentContainer.find('[data-tab]');
1949    var $curTab = $($tabs[0]); // Current tab is first tab.
1950    var width = $widget.width();
1951
1952    // Setup nav interactivity.
1953    $anchors.click(function(evt) {
1954      evt.preventDefault();
1955      var query = '[data-tab=' + $(this).data('tab') + ']';
1956      transitionWidget($tabs.filter(query));
1957    });
1958
1959    // Add highlight for navigation on first item.
1960    var $highlight = $('<div>').addClass('highlight')
1961      .css({left:$li.position().left + 'px', width:$li.outerWidth() + 'px'})
1962      .appendTo($nav);
1963
1964    // Store height since we will change contents to absolute.
1965    $contentContainer.height($contentContainer.height());
1966
1967    // Absolutely position tabs so they're ready for transition.
1968    $tabs.each(function(index) {
1969      $(this).css({position: 'absolute', left: index > 0 ? width + 'px' : '0'});
1970    });
1971
1972    function transitionWidget($toTab) {
1973      if (!$curTab.is($toTab)) {
1974        var curIndex = $tabs.index($curTab[0]);
1975        var toIndex = $tabs.index($toTab[0]);
1976        var dir = toIndex > curIndex ? 1 : -1;
1977
1978        // Animate content sections.
1979        $toTab.css({left:(width * dir) + 'px'});
1980        $curTab.animate({left:(width * -dir) + 'px'});
1981        $toTab.animate({left:'0'});
1982
1983        // Animate navigation highlight.
1984        $highlight.animate({left:$($li[toIndex]).position().left + 'px',
1985          width:$($li[toIndex]).outerWidth() + 'px'})
1986
1987        // Store new current section.
1988        $curTab = $toTab;
1989      }
1990    }
1991  }
1992})();
1993
1994/**
1995 * Auto TOC
1996 *
1997 * Upgrades h2s on the page to have a rule and be toggle-able on mobile.
1998 */
1999(function($) {
2000  var upgraded = false;
2001  var h2Titles;
2002
2003  function initWidget() {
2004    // add HRs below all H2s (except for a few other h2 variants)
2005    // Consider doing this with css instead.
2006    h2Titles = $('h2').not('#qv h2, #tb h2, .sidebox h2, #devdoc-nav h2, h2.norule');
2007    h2Titles.css({paddingBottom:0}).after('<hr/>');
2008
2009    // Exit early if on older browser.
2010    if (!window.matchMedia) {
2011      return;
2012    }
2013
2014    // Only run logic in mobile layout.
2015    var query = window.matchMedia('(max-width: 719px)');
2016    if (query.matches) {
2017      makeTogglable();
2018    } else {
2019      query.addListener(makeTogglable);
2020    }
2021  }
2022
2023  function makeTogglable() {
2024    // Only run this logic once.
2025    if (upgraded) { return; }
2026    upgraded = true;
2027
2028    // Only make content h2s togglable.
2029    var contentTitles = h2Titles.filter('#jd-content *');
2030
2031    // If there are more than 1
2032    if (contentTitles.size() < 2) {
2033      return;
2034    }
2035
2036    contentTitles.each(function() {
2037      // Find all the relevant nodes.
2038      var $title = $(this);
2039      var $hr = $title.next();
2040      var $contents = allNextUntil($hr[0], 'h2, .next-docs');
2041      var $section = $($title)
2042        .add($hr)
2043        .add($title.prev('a[name]'))
2044        .add($contents);
2045      var $anchor = $section.first().prev();
2046      var anchorMethod = 'after';
2047      if ($anchor.length === 0) {
2048        $anchor = $title.parent();
2049        anchorMethod = 'prepend';
2050      }
2051
2052      // Some h2s are in their own container making it pretty hard to find the end, so skip.
2053      if ($contents.length === 0) {
2054        return;
2055      }
2056
2057      // Remove from DOM before messing with it. DOM is slow!
2058      $section.detach();
2059
2060      // Add mobile-only expand arrows.
2061      $title.prepend('<span class="dac-visible-mobile-inline-block">' +
2062          '<i class="dac-toggle-expand dac-sprite dac-expand-more-black"></i>' +
2063          '<i class="dac-toggle-collapse dac-sprite dac-expand-less-black"></i>' +
2064          '</span>')
2065        .attr('data-toggle', 'section');
2066
2067      // Wrap in magic markup.
2068      $section = $section.wrapAll('<div class="dac-toggle dac-mobile">').parent();
2069
2070      // extra div used for max-height calculation.
2071      $contents.wrapAll('<div class="dac-toggle-content dac-expand"><div>');
2072
2073      // Pre-expand section if requested.
2074      if ($title.hasClass('is-expanded')) {
2075        $section.addClass('is-expanded');
2076      }
2077
2078      // Pre-expand section if targetted by hash.
2079      if (location.hash && $section.find(location.hash).length) {
2080        $section.addClass('is-expanded');
2081      }
2082
2083      // Add it back to the dom.
2084      $anchor[anchorMethod].call($anchor, $section);
2085    });
2086  }
2087
2088  // Similar to $.fn.nextUntil() except we need all nodes, jQuery skips text nodes.
2089  function allNextUntil(elem, until) {
2090    var matched = [];
2091
2092    while ((elem = elem.nextSibling) && elem.nodeType !== 9) {
2093      if (elem.nodeType === 1 && jQuery(elem).is(until)) {
2094        break;
2095      }
2096      matched.push(elem);
2097    }
2098    return $(matched);
2099  }
2100
2101  $(function() {
2102    initWidget();
2103  });
2104})(jQuery);
2105
2106(function($, window) {
2107  'use strict';
2108
2109  // Blogger API info
2110  var apiUrl = 'https://www.googleapis.com/blogger/v3';
2111  var apiKey = 'AIzaSyCFhbGnjW06dYwvRCU8h_zjdpS4PYYbEe8';
2112
2113  // Blog IDs can be found in the markup of the blog posts
2114  var blogs = {
2115    'android-developers': {
2116      id: '6755709643044947179',
2117      title: 'Android Developers Blog'
2118    }
2119  };
2120  var monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
2121      'July', 'August', 'September', 'October', 'November', 'December'];
2122
2123  var BlogReader = (function() {
2124    var reader;
2125
2126    function BlogReader() {
2127      this.doneSetup = false;
2128    }
2129
2130    /**
2131     * Initialize the blog reader and modal.
2132     */
2133    BlogReader.prototype.setup = function() {
2134      $('#jd-content').append(
2135          '<div id="blog-reader" data-modal="blog-reader" class="dac-modal dac-has-small-header">' +
2136            '<div class="dac-modal-container">' +
2137              '<div class="dac-modal-window">' +
2138                '<header class="dac-modal-header">' +
2139                  '<div class="dac-modal-header-actions">' +
2140                    '<a href="" class="dac-modal-header-open" target="_blank">' +
2141                      '<i class="dac-sprite dac-open-in-new"></i>' +
2142                    '</a>' +
2143                    '<button class="dac-modal-header-close" data-modal-toggle>' +
2144                    '</button>' +
2145                  '</div>' +
2146                  '<h2 class="norule dac-modal-header-title"></h2>' +
2147                '</header>' +
2148                '<div class="dac-modal-content dac-blog-reader">' +
2149                  '<time class="dac-blog-reader-date" pubDate></time>' +
2150                  '<h3 class="dac-blog-reader-title"></h3>' +
2151                  '<div class="dac-blog-reader-text clearfix"></div>' +
2152                '</div>' +
2153              '</div>' +
2154            '</div>' +
2155          '</div>');
2156
2157      this.blogReader = $('#blog-reader').dacModal();
2158
2159      this.doneSetup = true;
2160    };
2161
2162    BlogReader.prototype.openModal_ = function(blog, post) {
2163      var published = new Date(post.published);
2164      var formattedDate = monthNames[published.getMonth()] + ' ' + published.getDate() + ' ' + published.getFullYear();
2165      this.blogReader.find('.dac-modal-header-open').attr('href', post.url);
2166      this.blogReader.find('.dac-modal-header-title').text(blog.title);
2167      this.blogReader.find('.dac-blog-reader-title').html(post.title);
2168      this.blogReader.find('.dac-blog-reader-date').html(formattedDate);
2169      this.blogReader.find('.dac-blog-reader-text').html(post.content);
2170      this.blogReader.trigger('modal-open');
2171    };
2172
2173    /**
2174     * Show a blog post in a modal
2175     * @param  {string} blogName - The name of the Blogspot blog.
2176     * @param  {string} postPath - The path to the blog post.
2177     * @param  {bool} secondTry - Has it failed once?
2178     */
2179    BlogReader.prototype.showPost = function(blogName, postPath, secondTry) {
2180      var blog = blogs[blogName];
2181      var postUrl = 'https://' + blogName + '.blogspot.com' + postPath;
2182
2183      var url = apiUrl + '/blogs/' + blog.id + '/posts/bypath?path=' + encodeURIComponent(postPath) + '&key=' + apiKey;
2184      $.ajax(url, {timeout: 650}).done(this.openModal_.bind(this, blog)).fail(function(error) {
2185        // Retry once if we get an error
2186        if (error.status === 500 && !secondTry) {
2187          this.showPost(blogName, postPath, true);
2188        } else {
2189          window.location.href = postUrl;
2190        }
2191      }.bind(this));
2192    };
2193
2194    return {
2195      getReader: function() {
2196        if (!reader) {
2197          reader = new BlogReader();
2198        }
2199        return reader;
2200      }
2201    };
2202  })();
2203
2204  var blogReader = BlogReader.getReader();
2205
2206  function wrapLinkWithReader(e) {
2207    var el = $(e.currentTarget);
2208    if (el.hasClass('dac-modal-header-open')) {
2209      return;
2210    }
2211
2212    // Only catch links on blogspot.com
2213    var matches = el.attr('href').match(/https?:\/\/([^\.]*).blogspot.com([^$]*)/);
2214    if (matches && matches.length === 3) {
2215      var blogName = matches[1];
2216      var postPath = matches[2];
2217
2218      // Check if we have information about the blog
2219      if (!blogs[blogName]) {
2220        return;
2221      }
2222
2223      // Setup the first time it's used
2224      if (!blogReader.doneSetup) {
2225        blogReader.setup();
2226      }
2227
2228      e.preventDefault();
2229      blogReader.showPost(blogName, postPath);
2230    }
2231  }
2232
2233  $(document).on('click.blog-reader', 'a.resource-card[href*="blogspot.com/"]',
2234      wrapLinkWithReader);
2235})(jQuery, window);
2236
2237(function($) {
2238  $.fn.debounce = function(func, wait, immediate) {
2239    var timeout;
2240
2241    return function() {
2242      var context = this;
2243      var args = arguments;
2244
2245      var later = function() {
2246        timeout = null;
2247        if (!immediate) {
2248          func.apply(context, args);
2249        }
2250      };
2251
2252      var callNow = immediate && !timeout;
2253      clearTimeout(timeout);
2254      timeout = setTimeout(later, wait);
2255
2256      if (callNow) {
2257        func.apply(context, args);
2258      }
2259    };
2260  };
2261})(jQuery);
2262
2263/* Calculate the vertical area remaining */
2264(function($) {
2265  $.fn.ellipsisfade = function() {
2266    // Only fetch line-height of first element to avoid recalculate style.
2267    // Will be NaN if no elements match, which is ok.
2268    var lineHeight = parseInt(this.css('line-height'), 10);
2269
2270    this.each(function() {
2271      // get element text
2272      var $this = $(this);
2273      var remainingHeight = $this.parent().parent().height();
2274      $this.parent().siblings().each(function() {
2275        var elHeight;
2276        if ($(this).is(':visible')) {
2277          elHeight = $(this).outerHeight(true);
2278          remainingHeight = remainingHeight - elHeight;
2279        }
2280      });
2281
2282      var adjustedRemainingHeight = ((remainingHeight) / lineHeight >> 0) * lineHeight;
2283      $this.parent().css({height: adjustedRemainingHeight});
2284      $this.css({height: 'auto'});
2285    });
2286
2287    return this;
2288  };
2289
2290  /* Pass the line height to ellipsisfade() to adjust the height of the
2291   text container to show the max number of lines possible, without
2292   showing lines that are cut off. This works with the css ellipsis
2293   classes to fade last text line and apply an ellipsis char. */
2294  function updateEllipsis(context) {
2295    if (!(context instanceof jQuery)) {
2296      context = $('html');
2297    }
2298
2299    context.find('.card-info .text').ellipsisfade();
2300  }
2301
2302  $(window).on('resize', $.fn.debounce(updateEllipsis, 500));
2303  $(updateEllipsis);
2304  $('html').on('dac:domchange', function(e) { updateEllipsis($(e.target)); });
2305})(jQuery);
2306
2307/* Filter */
2308(function($) {
2309  'use strict';
2310
2311  /**
2312   * A single filter item content.
2313   * @type {string} - Element template.
2314   * @private
2315   */
2316  var ITEM_STR_ = '<input type="checkbox" value="{{value}}" class="dac-form-checkbox" id="{{id}}">' +
2317      '<label for="{{id}}" class="dac-form-checkbox-button"></label>' +
2318      '<label for="{{id}}" class="dac-form-label">{{name}}</label>';
2319
2320  /**
2321   * Template for a chip element.
2322   * @type {*|HTMLElement}
2323   * @private
2324   */
2325  var CHIP_BASE_ = $('<li class="dac-filter-chip">' +
2326    '<button class="dac-filter-chip-close">' +
2327      '<i class="dac-sprite dac-close-black dac-filter-chip-close-icon"></i>' +
2328    '</button>' +
2329  '</li>');
2330
2331  /**
2332   * Component to handle narrowing down resources.
2333   * @param {HTMLElement} el - The DOM element.
2334   * @param {Object} options
2335   * @constructor
2336   */
2337  function Filter(el, options) {
2338    this.el = $(el);
2339    this.options = $.extend({}, Filter.DEFAULTS_, options);
2340    this.init();
2341  }
2342
2343  Filter.DEFAULTS_ = {
2344    activeClass: 'dac-active',
2345    chipsDataAttr: 'filter-chips',
2346    nameDataAttr: 'filter-name',
2347    countDataAttr: 'filter-count',
2348    tabViewDataAttr: 'tab-view',
2349    valueDataAttr: 'filter-value'
2350  };
2351
2352  /**
2353   * Draw resource cards.
2354   * @param {Array} resources
2355   * @private
2356   */
2357  Filter.prototype.draw_ = function(resources) {
2358    var that = this;
2359
2360    if (resources.length === 0) {
2361      this.containerEl_.html('<p class="dac-filter-message">Nothing matches selected filters.</p>');
2362      return;
2363    }
2364
2365    // Draw resources.
2366    that.containerEl_.resourceWidget(resources, that.data_.options);
2367  };
2368
2369  /**
2370   * Initialize a Filter component.
2371   */
2372  Filter.prototype.init = function() {
2373    this.containerEl_ = $(this.options.filter);
2374
2375    // Setup data settings
2376    this.data_ = {};
2377    this.data_.chips = {};
2378    this.data_.options = this.containerEl_.widgetOptions();
2379    this.data_.all = window.metadata.query(this.data_.options);
2380
2381    // Initialize filter UI
2382    this.initUi();
2383  };
2384
2385  /**
2386   * Generate a chip for a given filter item.
2387   * @param {Object} item - A single filter option (checkbox container).
2388   * @returns {HTMLElement} A new Chip element.
2389   */
2390  Filter.prototype.chipForItem = function(item) {
2391    var chip = CHIP_BASE_.clone();
2392    chip.prepend(this.data_.chips[item.data('filter-value')]);
2393    chip.data('item.dac-filter', item);
2394    item.data('chip.dac-filter', chip);
2395    this.addToItemValue(item, 1);
2396    return chip[0];
2397  };
2398
2399  /**
2400   * Update count of checked filter items.
2401   * @param {Object} item - A single filter option (checkbox container).
2402   * @param {Number} value - Either -1 or 1.
2403   */
2404  Filter.prototype.addToItemValue = function(item, value) {
2405    var tab = item.parent().data(this.options.tabViewDataAttr);
2406    var countEl = this.countEl_.filter('[data-' + this.options.countDataAttr + '="' + tab + '"]');
2407    var count = value + parseInt(countEl.text(), 10);
2408    countEl.text(count);
2409    countEl.toggleClass('dac-disabled', count === 0);
2410  };
2411
2412  /**
2413   * Set event listeners.
2414   * @private
2415   */
2416  Filter.prototype.setEventListeners_ = function() {
2417    this.chipsEl_.on('click.dac-filter', '.dac-filter-chip-close', this.closeChipHandler_.bind(this));
2418    this.tabViewEl_.on('change.dac-filter', ':checkbox', this.toggleCheckboxHandler_.bind(this));
2419  };
2420
2421  /**
2422   * Check filter items that are active by default.
2423   */
2424  Filter.prototype.activateInitialFilters_ = function() {
2425    var id = (new Date()).getTime();
2426    var initiallyCheckedValues = this.data_.options.query.replace(/,\s*/g, '+').split('+');
2427    var chips = document.createDocumentFragment();
2428    var that = this;
2429
2430    this.items_.each(function(i) {
2431      var item = $(this);
2432      var opts = item.data();
2433      that.data_.chips[opts.filterValue] = opts.filterName;
2434
2435      var checkbox = $(ITEM_STR_.replace(/\{\{name\}\}/g, opts.filterName)
2436        .replace(/\{\{value\}\}/g, opts.filterValue)
2437        .replace(/\{\{id\}\}/g, 'filter-' + id + '-' + (i + 1)));
2438
2439      if (initiallyCheckedValues.indexOf(opts.filterValue) > -1) {
2440        checkbox[0].checked = true;
2441        chips.appendChild(that.chipForItem(item));
2442      }
2443
2444      item.append(checkbox);
2445    });
2446
2447    this.chipsEl_.append(chips);
2448  };
2449
2450  /**
2451   * Initialize the Filter view
2452   */
2453  Filter.prototype.initUi = function() {
2454    // Cache DOM elements
2455    this.chipsEl_ = this.el.find('[data-' + this.options.chipsDataAttr + ']');
2456    this.countEl_ = this.el.find('[data-' + this.options.countDataAttr + ']');
2457    this.tabViewEl_ = this.el.find('[data-' + this.options.tabViewDataAttr + ']');
2458    this.items_ = this.el.find('[data-' + this.options.nameDataAttr + ']');
2459
2460    // Setup UI
2461    this.draw_(this.data_.all);
2462    this.activateInitialFilters_();
2463    this.setEventListeners_();
2464  };
2465
2466  /**
2467   * @returns {[types|Array, tags|Array, category|Array]}
2468   */
2469  Filter.prototype.getActiveClauses = function() {
2470    var tags = [];
2471    var types = [];
2472    var categories = [];
2473
2474    this.items_.find(':checked').each(function(i, checkbox) {
2475      // Currently, there is implicit business logic here that `tag` is AND'ed together
2476      // while `type` is OR'ed. So , and + do the same thing here. It would be great to
2477      // reuse the same query engine for filters, but it would need more powerful syntax.
2478      // Probably parenthesis, to support "tag:dog + tag:cat + (type:video, type:blog)"
2479      var expression = $(checkbox).val();
2480      var regex = /(\w+):(\w+)/g;
2481      var match;
2482
2483      while (match = regex.exec(expression)) {
2484        switch (match[1]) {
2485          case 'category':
2486            categories.push(match[2]);
2487            break;
2488          case 'tag':
2489            tags.push(match[2]);
2490            break;
2491          case 'type':
2492            types.push(match[2]);
2493            break;
2494        }
2495      }
2496    });
2497
2498    return [types, tags, categories];
2499  };
2500
2501  /**
2502   * Actual filtering logic.
2503   * @returns {Array}
2504   */
2505  Filter.prototype.filteredResources = function() {
2506    var data = this.getActiveClauses();
2507    var types = data[0];
2508    var tags = data[1];
2509    var categories = data[2];
2510    var resources = [];
2511    var resource = {};
2512    var tag = '';
2513    var shouldAddResource = true;
2514
2515    for (var resourceIndex = 0; resourceIndex < this.data_.all.length; resourceIndex++) {
2516      resource = this.data_.all[resourceIndex];
2517      shouldAddResource = types.indexOf(resource.type) > -1;
2518
2519      if (categories && categories.length > 0) {
2520        shouldAddResource = shouldAddResource && categories.indexOf(resource.category) > -1;
2521      }
2522
2523      for (var tagIndex = 0; shouldAddResource && tagIndex < tags.length; tagIndex++) {
2524        tag = tags[tagIndex];
2525        shouldAddResource = resource.tags.indexOf(tag) > -1;
2526      }
2527
2528      if (shouldAddResource) {
2529        resources.push(resource);
2530      }
2531    }
2532
2533    return resources;
2534  };
2535
2536  /**
2537   * Close Chip Handler
2538   * @param {Event} event - Click event
2539   * @private
2540   */
2541  Filter.prototype.closeChipHandler_ = function(event) {
2542    var chip = $(event.currentTarget).parent();
2543    var checkbox = chip.data('item.dac-filter').find(':first-child')[0];
2544    checkbox.checked = false;
2545    this.changeStateForCheckbox(checkbox);
2546  };
2547
2548  /**
2549   * Handle filter item state change.
2550   * @param {Event} event - Change event
2551   * @private
2552   */
2553  Filter.prototype.toggleCheckboxHandler_ = function(event) {
2554    this.changeStateForCheckbox(event.currentTarget);
2555  };
2556
2557  /**
2558   * Redraw resource view based on new state.
2559   * @param checkbox
2560   */
2561  Filter.prototype.changeStateForCheckbox = function(checkbox) {
2562    var item = $(checkbox).parent();
2563
2564    if (checkbox.checked) {
2565      this.chipsEl_.append(this.chipForItem(item));
2566      devsite.analytics.trackAnalyticsEvent('event',
2567          'Filters', 'Check', $(checkbox).val());
2568    } else {
2569      item.data('chip.dac-filter').remove();
2570      this.addToItemValue(item, -1);
2571      devsite.analytics.trackAnalyticsEvent('event',
2572          'Filters', 'Uncheck', $(checkbox).val());
2573    }
2574
2575    this.draw_(this.filteredResources());
2576  };
2577
2578  /**
2579   * jQuery plugin
2580   */
2581  $.fn.dacFilter = function() {
2582    return this.each(function() {
2583      var el = $(this);
2584      new Filter(el, el.data());
2585    });
2586  };
2587
2588  /**
2589   * Data Attribute API
2590   */
2591  $(function() {
2592    $('[data-filter]').dacFilter();
2593  });
2594})(jQuery);
2595
2596(function($) {
2597  'use strict';
2598
2599  /**
2600   * Toggle Floating Label state.
2601   * @param {HTMLElement} el - The DOM element.
2602   * @param options
2603   * @constructor
2604   */
2605  function FloatingLabel(el, options) {
2606    this.el = $(el);
2607    this.options = $.extend({}, FloatingLabel.DEFAULTS_, options);
2608    this.group = this.el.closest('.dac-form-input-group');
2609    this.input = this.group.find('.dac-form-input');
2610
2611    this.checkValue_ = this.checkValue_.bind(this);
2612    this.checkValue_();
2613
2614    this.input.on('focus', function() {
2615      this.group.addClass('dac-focused');
2616    }.bind(this));
2617    this.input.on('blur', function() {
2618      this.group.removeClass('dac-focused');
2619      this.checkValue_();
2620    }.bind(this));
2621    this.input.on('keyup', this.checkValue_);
2622  }
2623
2624  /**
2625   * The label is moved out of the textbox when it has a value.
2626   */
2627  FloatingLabel.prototype.checkValue_ = function() {
2628    if (this.input.val().length) {
2629      this.group.addClass('dac-has-value');
2630    } else {
2631      this.group.removeClass('dac-has-value');
2632    }
2633  };
2634
2635  /**
2636   * jQuery plugin
2637   * @param  {object} options - Override default options.
2638   */
2639  $.fn.dacFloatingLabel = function(options) {
2640    return this.each(function() {
2641      new FloatingLabel(this, options);
2642    });
2643  };
2644
2645  $(document).on('ready.aranja', function() {
2646    $('.dac-form-floatlabel').each(function() {
2647      $(this).dacFloatingLabel($(this).data());
2648    });
2649  });
2650})(jQuery);
2651
2652(function($) {
2653  'use strict';
2654
2655  /**
2656   * @param {HTMLElement} el - The DOM element.
2657   * @param {Object} options
2658   * @constructor
2659   */
2660  function Crumbs(selected, options) {
2661    this.options = $.extend({}, Crumbs.DEFAULTS_, options);
2662    this.el = $(this.options.container);
2663
2664    // Do not build breadcrumbs for landing site.
2665    if (!selected || location.pathname === '/index.html' || location.pathname === '/') {
2666      return;
2667    }
2668
2669    // Cache navigation resources
2670    this.selected = $(selected);
2671    this.selectedParent = this.selected.closest('.dac-nav-secondary').siblings('a');
2672
2673    // Build the breadcrumb list.
2674    this.init();
2675  }
2676
2677  Crumbs.DEFAULTS_ = {
2678    container: '.dac-header-crumbs',
2679    crumbItem: $('<li class="dac-header-crumbs-item">'),
2680    linkClass: 'dac-header-crumbs-link'
2681  };
2682
2683  Crumbs.prototype.init = function() {
2684    Crumbs.buildCrumbForLink(this.selected.clone()).appendTo(this.el);
2685
2686    if (this.selectedParent.length) {
2687      Crumbs.buildCrumbForLink(this.selectedParent.clone()).prependTo(this.el);
2688    }
2689
2690    // Reveal the breadcrumbs
2691    this.el.addClass('dac-has-content');
2692  };
2693
2694  /**
2695   * Build a HTML structure for a breadcrumb.
2696   * @param {string} link
2697   * @return {jQuery}
2698   */
2699  Crumbs.buildCrumbForLink = function(link) {
2700    link.find('br').replaceWith(' ');
2701
2702    var crumbLink = $('<a>')
2703      .attr('class', Crumbs.DEFAULTS_.linkClass)
2704      .attr('href', link.attr('href'))
2705      .text(link.text());
2706
2707    return Crumbs.DEFAULTS_.crumbItem.clone().append(crumbLink);
2708  };
2709
2710  /**
2711   * jQuery plugin
2712   */
2713  $.fn.dacCrumbs = function(options) {
2714    return this.each(function() {
2715      new Crumbs(this, options);
2716    });
2717  };
2718})(jQuery);
2719
2720(function($) {
2721  'use strict';
2722
2723  /**
2724   * @param {HTMLElement} el - The DOM element.
2725   * @param {Object} options
2726   * @constructor
2727   */
2728  function SearchInput(el, options) {
2729    this.el = $(el);
2730    this.options = $.extend({}, SearchInput.DEFAULTS_, options);
2731    this.body = $('body');
2732    this.input = this.el.find('input');
2733    this.close = this.el.find(this.options.closeButton);
2734    this.clear = this.el.find(this.options.clearButton);
2735    this.icon = this.el.find('.' + this.options.iconClass);
2736    this.init();
2737  }
2738
2739  SearchInput.DEFAULTS_ = {
2740    activeClass: 'dac-active',
2741    activeIconClass: 'dac-search',
2742    closeButton: '[data-search-close]',
2743    clearButton: '[data-search-clear]',
2744    hiddenClass: 'dac-hidden',
2745    iconClass: 'dac-header-search-icon',
2746    searchModeClass: 'dac-search-mode',
2747    transitionDuration: 250
2748  };
2749
2750  SearchInput.prototype.init = function() {
2751    this.input.on('focus.dac-search', this.setActiveState.bind(this))
2752              .on('input.dac-search', this.checkInputValue.bind(this));
2753    this.close.on('click.dac-search', this.unsetActiveStateHandler_.bind(this));
2754    this.clear.on('click.dac-search', this.clearInput.bind(this));
2755  };
2756
2757  SearchInput.prototype.setActiveState = function() {
2758    var that = this;
2759
2760    this.clear.addClass(this.options.hiddenClass);
2761    this.body.addClass(this.options.searchModeClass);
2762    this.checkInputValue();
2763
2764    // Set icon to black after background has faded to white.
2765    setTimeout(function() {
2766      that.icon.addClass(that.options.activeIconClass);
2767    }, this.options.transitionDuration);
2768  };
2769
2770  SearchInput.prototype.unsetActiveStateHandler_ = function(event) {
2771    event.preventDefault();
2772    this.unsetActiveState();
2773  };
2774
2775  SearchInput.prototype.unsetActiveState = function() {
2776    this.icon.removeClass(this.options.activeIconClass);
2777    this.clear.addClass(this.options.hiddenClass);
2778    this.body.removeClass(this.options.searchModeClass);
2779  };
2780
2781  SearchInput.prototype.clearInput = function(event) {
2782    event.preventDefault();
2783    this.input.val('');
2784    this.clear.addClass(this.options.hiddenClass);
2785  };
2786
2787  SearchInput.prototype.checkInputValue = function() {
2788    if (this.input.val().length) {
2789      this.clear.removeClass(this.options.hiddenClass);
2790    } else {
2791      this.clear.addClass(this.options.hiddenClass);
2792    }
2793  };
2794
2795  /**
2796   * jQuery plugin
2797   * @param {object} options - Override default options.
2798   */
2799  $.fn.dacSearchInput = function() {
2800    return this.each(function() {
2801      var el = $(this);
2802      el.data('search-input.dac', new SearchInput(el, el.data()));
2803    });
2804  };
2805
2806  /**
2807   * Data Attribute API
2808   */
2809  $(function() {
2810    $('[data-search]').dacSearchInput();
2811  });
2812})(jQuery);
2813
2814/* global METADATA */
2815(function($) {
2816  function DacCarouselQuery(el) {
2817    el = $(el);
2818
2819    var opts = el.data();
2820    opts.maxResults = parseInt(opts.maxResults || '100', 10);
2821    opts.query = opts.carouselQuery;
2822    var resources = window.metadata.query(opts);
2823
2824    el.empty();
2825    $(resources).each(function() {
2826      var resource = $.extend({}, this, METADATA.carousel[this.url]);
2827      el.dacHero(resource);
2828    });
2829
2830    // Pagination element.
2831    el.append('<div class="dac-hero-carousel-pagination"><div class="wrap" data-carousel-pagination>');
2832
2833    el.dacCarousel();
2834  }
2835
2836  // jQuery plugin
2837  $.fn.dacCarouselQuery = function() {
2838    return this.each(function() {
2839      var el = $(this);
2840      var data = el.data('dac.carouselQuery');
2841
2842      if (!data) { el.data('dac.carouselQuery', (data = new DacCarouselQuery(el))); }
2843    });
2844  };
2845
2846  // Data API
2847  $(function() {
2848    $('[data-carousel-query]').dacCarouselQuery();
2849  });
2850})(jQuery);
2851
2852(function($) {
2853  /**
2854   * A CSS based carousel, inspired by SequenceJS.
2855   * @param {jQuery} el
2856   * @param {object} options
2857   * @constructor
2858   */
2859  function DacCarousel(el, options) {
2860    this.el = $(el);
2861    this.options = options = $.extend({}, DacCarousel.OPTIONS, this.el.data(), options || {});
2862    this.frames = this.el.find(options.frameSelector);
2863    this.count = this.frames.size();
2864    this.current = options.start;
2865
2866    this.initPagination();
2867    this.initEvents();
2868    this.initFrame();
2869  }
2870
2871  DacCarousel.OPTIONS = {
2872    auto:      true,
2873    autoTime:  10000,
2874    autoMinTime: 5000,
2875    btnPrev:   '[data-carousel-prev]',
2876    btnNext:   '[data-carousel-next]',
2877    frameSelector: 'article',
2878    loop:      true,
2879    start:     0,
2880    swipeThreshold: 160,
2881    pagination: '[data-carousel-pagination]'
2882  };
2883
2884  DacCarousel.prototype.initPagination = function() {
2885    this.pagination = $([]);
2886    if (!this.options.pagination) { return; }
2887
2888    var pagination = $('<ul class="dac-pagination">');
2889    var parent = this.el;
2890    if (typeof this.options.pagination === 'string') { parent = this.el.find(this.options.pagination); }
2891
2892    if (this.count > 1) {
2893      for (var i = 0; i < this.count; i++) {
2894        var li = $('<li class="dac-pagination-item">').text(i);
2895        if (i === this.options.start) { li.addClass('active'); }
2896        li.click(this.go.bind(this, i));
2897
2898        pagination.append(li);
2899      }
2900      this.pagination = pagination.children();
2901      parent.append(pagination);
2902    }
2903  };
2904
2905  DacCarousel.prototype.initEvents = function() {
2906    var that = this;
2907
2908    this.touch = {
2909      start: {x: 0, y: 0},
2910      end:   {x: 0, y: 0}
2911    };
2912
2913    this.el.on('touchstart', this.touchstart_.bind(this));
2914    this.el.on('touchend', this.touchend_.bind(this));
2915    this.el.on('touchmove', this.touchmove_.bind(this));
2916
2917    this.el.hover(function() {
2918      that.pauseRotateTimer();
2919    }, function() {
2920      that.startRotateTimer();
2921    });
2922
2923    $(this.options.btnPrev).click(function(e) {
2924      e.preventDefault();
2925      that.prev();
2926    });
2927
2928    $(this.options.btnNext).click(function(e) {
2929      e.preventDefault();
2930      that.next();
2931    });
2932  };
2933
2934  DacCarousel.prototype.touchstart_ = function(event) {
2935    var t = event.originalEvent.touches[0];
2936    this.touch.start = {x: t.screenX, y: t.screenY};
2937  };
2938
2939  DacCarousel.prototype.touchend_ = function() {
2940    var deltaX = this.touch.end.x - this.touch.start.x;
2941    var deltaY = Math.abs(this.touch.end.y - this.touch.start.y);
2942    var shouldSwipe = (deltaY < Math.abs(deltaX)) && (Math.abs(deltaX) >= this.options.swipeThreshold);
2943
2944    if (shouldSwipe) {
2945      if (deltaX > 0) {
2946        this.prev();
2947      } else {
2948        this.next();
2949      }
2950    }
2951  };
2952
2953  DacCarousel.prototype.touchmove_ = function(event) {
2954    var t = event.originalEvent.touches[0];
2955    this.touch.end = {x: t.screenX, y: t.screenY};
2956  };
2957
2958  DacCarousel.prototype.initFrame = function() {
2959    this.frames.removeClass('active').eq(this.options.start).addClass('active');
2960  };
2961
2962  DacCarousel.prototype.startRotateTimer = function() {
2963    if (!this.options.auto || this.rotateTimer) { return; }
2964    this.rotateTimer = setTimeout(this.next.bind(this), this.options.autoTime);
2965  };
2966
2967  DacCarousel.prototype.pauseRotateTimer = function() {
2968    clearTimeout(this.rotateTimer);
2969    this.rotateTimer = null;
2970  };
2971
2972  DacCarousel.prototype.prev = function() {
2973    this.go(this.current - 1);
2974  };
2975
2976  DacCarousel.prototype.next = function() {
2977    this.go(this.current + 1);
2978  };
2979
2980  DacCarousel.prototype.go = function(next) {
2981    // Figure out what the next slide is.
2982    while (this.count > 0 && next >= this.count) { next -= this.count; }
2983    while (next < 0) { next += this.count; }
2984
2985    // Cancel if we're already on that slide.
2986    if (next === this.current) { return; }
2987
2988    // Prepare next slide.
2989    this.frames.eq(next).removeClass('out');
2990
2991    // Recalculate styles before starting slide transition.
2992    this.el.resolveStyles();
2993    // Update pagination
2994    this.pagination.removeClass('active').eq(next).addClass('active');
2995
2996    // Transition out current frame
2997    this.frames.eq(this.current).toggleClass('active out');
2998
2999    // Transition in a new frame
3000    this.frames.eq(next).toggleClass('active');
3001
3002    this.current = next;
3003  };
3004
3005  // Helper which resolves new styles for an element, so it can start transitioning
3006  // from the new values.
3007  $.fn.resolveStyles = function() {
3008    /*jshint expr:true*/
3009    this[0] && this[0].offsetTop;
3010    return this;
3011  };
3012
3013  // jQuery plugin
3014  $.fn.dacCarousel = function() {
3015    this.each(function() {
3016      var $el = $(this);
3017      $el.data('dac-carousel', new DacCarousel(this));
3018    });
3019    return this;
3020  };
3021
3022  // Data API
3023  $(function() {
3024    $('[data-carousel]').dacCarousel();
3025  });
3026})(jQuery);
3027
3028/* global toRoot */
3029
3030(function($) {
3031  // Ordering matters
3032  var TAG_MAP = [
3033    {from: 'developerstory', to: 'Android Developer Story'},
3034    {from: 'googleplay', to: 'Google Play'}
3035  ];
3036
3037  function DacHero(el, resource, isSearch) {
3038    var slide = $('<article>');
3039    slide.addClass(isSearch ? 'dac-search-hero' : 'dac-expand dac-hero');
3040    var image = cleanUrl(resource.heroImage || resource.image);
3041    var fullBleed = image && !resource.heroColor;
3042
3043    if (!isSearch) {
3044      // Configure background
3045      slide.css({
3046        backgroundImage: fullBleed ? 'url(' + image + ')' : '',
3047        backgroundColor: resource.heroColor || ''
3048      });
3049
3050      // Should copy be inverted
3051      slide.toggleClass('dac-invert', resource.heroInvert || fullBleed);
3052      slide.toggleClass('dac-darken', fullBleed);
3053
3054      // Should be clickable
3055      slide.append($('<a class="dac-hero-carousel-action">').attr('href', cleanUrl(resource.url)));
3056    }
3057
3058    var cols = $('<div class="cols dac-hero-content">');
3059
3060    // inline image column
3061    var rightCol = $('<div class="col-1of2 col-push-1of2 dac-hero-figure">')
3062      .appendTo(cols);
3063
3064    if ((!fullBleed || isSearch) && image) {
3065      rightCol.append($('<img>').attr('src', image));
3066    }
3067
3068    // info column
3069    $('<div class="col-1of2 col-pull-1of2">')
3070      .append($('<div class="dac-hero-tag">').text(formatTag(resource)))
3071      .append($('<h1 class="dac-hero-title">').text(formatTitle(resource)))
3072      .append($('<p class="dac-hero-description">').text(resource.summary))
3073      .append($('<a class="dac-hero-cta">')
3074        .text(formatCTA(resource))
3075        .attr('href', cleanUrl(resource.url))
3076        .prepend($('<span class="dac-sprite dac-auto-chevron">'))
3077      )
3078      .appendTo(cols);
3079
3080    slide.append(cols.wrap('<div class="wrap">').parent());
3081    el.append(slide);
3082  }
3083
3084  function cleanUrl(url) {
3085    if (url && url.indexOf('//') === -1) {
3086      url = toRoot + url;
3087    }
3088    return url;
3089  }
3090
3091  function formatTag(resource) {
3092    // Hmm, need a better more scalable solution for this.
3093    for (var i = 0, mapping; mapping = TAG_MAP[i]; i++) {
3094      if (resource.tags.indexOf(mapping.from) > -1) {
3095        return mapping.to;
3096      }
3097    }
3098    return resource.type;
3099  }
3100
3101  function formatTitle(resource) {
3102    return resource.title.replace(/android developer story: /i, '');
3103  }
3104
3105  function formatCTA(resource) {
3106    return resource.type === 'youtube' ? 'Watch the video' : 'Learn more';
3107  }
3108
3109  // jQuery plugin
3110  $.fn.dacHero = function(resource, isSearch) {
3111    return this.each(function() {
3112      var el = $(this);
3113      return new DacHero(el, resource, isSearch);
3114    });
3115  };
3116})(jQuery);
3117
3118(function($) {
3119  'use strict';
3120
3121  function highlightString(label, query) {
3122    query = query || '';
3123    //query = query.replace('<wbr>', '').replace('.', '\\.');
3124    var queryRE = new RegExp('(' + query + ')', 'ig');
3125    return label.replace(queryRE, '<em>$1</em>');
3126  }
3127
3128  $.fn.highlightMatches = function(query) {
3129    return this.each(function() {
3130      var el = $(this);
3131      var label = el.html();
3132      var highlighted = highlightString(label, query);
3133      el.html(highlighted);
3134      el.addClass('highlighted');
3135    });
3136  };
3137})(jQuery);
3138
3139/**
3140 * History tracking.
3141 * Track visited urls in localStorage.
3142 */
3143(function($) {
3144  var PAGES_TO_STORE_ = 100;
3145  var MIN_NUMBER_OF_PAGES_TO_DISPLAY_ = 6;
3146  var CONTAINER_SELECTOR_ = '.dac-search-results-history-wrap';
3147
3148  /**
3149   * Generate resource cards for visited pages.
3150   * @param {HTMLElement} el
3151   * @constructor
3152   */
3153  function HistoryQuery(el) {
3154    this.el = $(el);
3155
3156    // Only show history component if enough pages have been visited.
3157    if (getVisitedPages().length < MIN_NUMBER_OF_PAGES_TO_DISPLAY_) {
3158      this.el.closest(CONTAINER_SELECTOR_).addClass('dac-hidden');
3159      return;
3160    }
3161
3162    // Rename query
3163    this.el.data('query', this.el.data('history-query'));
3164
3165    // jQuery method to populate cards.
3166    this.el.resourceWidget();
3167  }
3168
3169  /**
3170   * Fetch from localStorage an array of visted pages
3171   * @returns {Array}
3172   */
3173  function getVisitedPages() {
3174    var visited = localStorage.getItem('visited-pages');
3175    return visited ? JSON.parse(visited) : [];
3176  }
3177
3178  /**
3179   * Return a page corresponding to cuurent pathname. If none exists, create one.
3180   * @param {Array} pages
3181   * @param {String} path
3182   * @returns {Object} Page
3183   */
3184  function getPageForPath(pages, path) {
3185    var page;
3186
3187    // Backwards lookup for current page, last pages most likely to be visited again.
3188    for (var i = pages.length - 1; i >= 0; i--) {
3189      if (pages[i].path === path) {
3190        page = pages[i];
3191
3192        // Remove page object from pages list to ensure correct ordering.
3193        pages.splice(i, 1);
3194
3195        return page;
3196      }
3197    }
3198
3199    // If storage limit is exceeded, remove last visited path.
3200    if (pages.length >= PAGES_TO_STORE_) {
3201      pages.shift();
3202    }
3203
3204    return {path: path};
3205  }
3206
3207  /**
3208   * Add current page to back of visited array, increase hit count by 1.
3209   */
3210  function addCurrectPage() {
3211    var path = location.pathname;
3212
3213    // Do not track frontpage visits.
3214    if (path === '/' || path === '/index.html') {return;}
3215
3216    var pages = getVisitedPages();
3217    var page = getPageForPath(pages, path);
3218
3219    // New page visits have no hit count.
3220    page.hit = ~~page.hit + 1;
3221
3222    // Most recently visted pages are located at the end of the visited array.
3223    pages.push(page);
3224
3225    localStorage.setItem('visited-pages', JSON.stringify(pages));
3226  }
3227
3228  /**
3229   * Hit count compare function.
3230   * @param {Object} a - page
3231   * @param {Object} b - page
3232   * @returns {number}
3233   */
3234  function byHit(a, b) {
3235    if (a.hit > b.hit) {
3236      return -1;
3237    } else if (a.hit < b.hit) {
3238      return 1;
3239    }
3240
3241    return 0;
3242  }
3243
3244  /**
3245   * Return a list of visited urls in a given order.
3246   * @param {String} order - (recent|most-visited)
3247   * @returns {Array}
3248   */
3249  $.dacGetVisitedUrls = function(order) {
3250    var pages = getVisitedPages();
3251
3252    if (order === 'recent') {
3253      pages.reverse();
3254    } else {
3255      pages.sort(byHit);
3256    }
3257
3258    return pages.map(function(page) {
3259      return page.path.replace(/^\//, '');
3260    });
3261  };
3262
3263  // jQuery plugin
3264  $.fn.dacHistoryQuery = function() {
3265    return this.each(function() {
3266      var el = $(this);
3267      var data = el.data('dac.recentlyVisited');
3268
3269      if (!data) {
3270        el.data('dac.recentlyVisited', (data = new HistoryQuery(el)));
3271      }
3272    });
3273  };
3274
3275  $(function() {
3276    $('[data-history-query]').dacHistoryQuery();
3277    // Do not block page rendering.
3278    setTimeout(addCurrectPage, 0);
3279  });
3280})(jQuery);
3281
3282/* ############################################ */
3283/* ##########     LOCALIZATION     ############ */
3284/* ############################################ */
3285/**
3286 * Global helpers.
3287 */
3288function getBaseUri(uri) {
3289  var intlUrl = (uri.substring(0, 6) === '/intl/');
3290  if (intlUrl) {
3291    var base = uri.substring(uri.indexOf('intl/') + 5, uri.length);
3292    base = base.substring(base.indexOf('/') + 1, base.length);
3293    return '/' + base;
3294  } else {
3295    return uri;
3296  }
3297}
3298
3299function changeLangPref(targetLang, submit) {
3300  window.writeCookie('pref_lang', targetLang, null);
3301  $('#language').find('option[value="' + targetLang + '"]').attr('selected', true);
3302  if (submit) {
3303    $('#setlang').submit();
3304  }
3305}
3306// Redundant usage to appease jshint.
3307window.changeLangPref = changeLangPref;
3308
3309(function() {
3310  /**
3311   * Whitelisted locales. Should match choices in language dropdown. Repeated here
3312   * as a lot of i18n logic happens before page load and dropdown is ready.
3313   */
3314  var LANGUAGES = [
3315    'en',
3316    'es',
3317    'id',
3318    'ja',
3319    'ko',
3320    'pt-br',
3321    'ru',
3322    'vi',
3323    'zh-cn',
3324    'zh-tw'
3325  ];
3326
3327  /**
3328   * Master list of translated strings for template files.
3329   */
3330  var PHRASES = {
3331    'newsletter': {
3332      'title': 'Get the latest Android developer news and tips that will help you find success on Google Play.',
3333      'requiredHint': '* Required Fields',
3334      'name': 'Full name',
3335      'email': 'Email address',
3336      'company': 'Company / developer name',
3337      'appUrl': 'One of your Play Store app URLs',
3338      'business': {
3339        'label': 'Which best describes your business:',
3340        'apps': 'Apps',
3341        'games': 'Games',
3342        'both': 'Apps & Games'
3343      },
3344      'confirmMailingList': 'Add me to the mailing list for the monthly newsletter and occasional emails about ' +
3345                            'development and Google Play opportunities.',
3346      'privacyPolicy': 'I acknowledge that the information provided in this form will be subject to Google\'s ' +
3347                       '<a href="https://www.google.com/policies/privacy/" target="_blank">privacy policy</a>.',
3348      'languageVal': 'English',
3349      'successTitle': 'Hooray!',
3350      'successDetails': 'You have successfully signed up for the latest Android developer news and tips.',
3351      'languageValTarget': {
3352        'en': 'English',
3353        'ar': 'Arabic (العربيّة)',
3354        'id': 'Indonesian (Bahasa)',
3355        'fr': 'French (français)',
3356        'de': 'German (Deutsch)',
3357        'ja': 'Japanese (日本語)',
3358        'ko': 'Korean (한국어)',
3359        'ru': 'Russian (Русский)',
3360        'es': 'Spanish (español)',
3361        'th': 'Thai (ภาษาไทย)',
3362        'tr': 'Turkish (Türkçe)',
3363        'vi': 'Vietnamese (tiếng Việt)',
3364        'pt-br': 'Brazilian Portuguese (Português Brasileiro)',
3365        'zh-cn': 'Simplified Chinese (简体中文)',
3366        'zh-tw': 'Traditional Chinese (繁體中文)',
3367      },
3368      'resetLangTitle': "Browse this site in %{targetLang}?",
3369      'resetLangTextIntro': 'You requested a page in %{targetLang}, but your language preference for this site is %{lang}.',
3370      'resetLangTextCta': 'Would you like to change your language preference and browse this site in %{targetLang}? ' +
3371                          'If you want to change your language preference later, use the language menu at the bottom of each page.',
3372      'resetLangButtonYes': 'Change Language',
3373      'resetLangButtonNo': 'Not Now'
3374    }
3375  };
3376
3377  /**
3378   * Current locale.
3379   */
3380  var locale = (function() {
3381    var lang = window.readCookie('pref_lang');
3382    if (lang === 0 || LANGUAGES.indexOf(lang) === -1) {
3383      lang = 'en';
3384    }
3385    return lang;
3386  })();
3387  var localeTarget = (function() {
3388    var lang = getQueryVariable('hl');
3389    if (lang === false || LANGUAGES.indexOf(lang) === -1) {
3390      lang = locale;
3391    }
3392    return lang;
3393  })();
3394
3395  /**
3396   * Global function shims for backwards compatibility
3397   */
3398  window.changeNavLang = function() {
3399    // Already done.
3400  };
3401
3402  window.loadLangPref = function() {
3403    // Languages pref already loaded.
3404  };
3405
3406  window.getLangPref = function() {
3407    return locale;
3408  };
3409
3410  window.getLangTarget = function() {
3411    return localeTarget;
3412  };
3413
3414  // Expose polyglot instance for advanced localization.
3415  var polyglot = window.polyglot = new window.Polyglot({
3416    locale: locale,
3417    phrases: PHRASES
3418  });
3419
3420  // When DOM is ready.
3421  $(function() {
3422    // Mark current locale in language picker.
3423    $('#language').find('option[value="' + locale + '"]').attr('selected', true);
3424
3425    $('html').dacTranslate().on('dac:domchange', function(e) {
3426      $(e.target).dacTranslate();
3427    });
3428  });
3429
3430  $.fn.dacTranslate = function() {
3431    // Translate strings in template markup:
3432
3433    // OLD
3434    // Having all translations in HTML does not scale well and bloats every page.
3435    // Need to migrate this to data-l JS translations below.
3436    if (locale !== 'en') {
3437      var $links = this.find('a[' + locale + '-lang]');
3438      $links.each(function() { // for each link with a translation
3439        var $link = $(this);
3440        // put the desired language from the attribute as the text
3441        $link.text($link.attr(locale + '-lang'));
3442      });
3443    }
3444
3445    // NEW
3446    // A simple declarative api for JS translations. Feel free to extend as appropriate.
3447
3448    // Miscellaneous string compilations
3449    // Build full strings from localized substrings:
3450    var myLocaleTarget = window.getLangTarget();
3451    var myTargetLang = window.polyglot.t("newsletter.languageValTarget." + myLocaleTarget);
3452    var myLang = window.polyglot.t("newsletter.languageVal");
3453    var myTargetLangTitleString = window.polyglot.t("newsletter.resetLangTitle", {targetLang: myTargetLang});
3454    var myResetLangTextIntro = window.polyglot.t("newsletter.resetLangTextIntro", {targetLang: myTargetLang, lang: myLang});
3455    var myResetLangTextCta = window.polyglot.t("newsletter.resetLangTextCta", {targetLang: myTargetLang});
3456    //var myResetLangButtonYes = window.polyglot.t("newsletter.resetLangButtonYes", {targetLang: myTargetLang});
3457
3458    // Inject strings as text values in dialog components:
3459    $("#langform .dac-modal-header-title").text(myTargetLangTitleString);
3460    $("#langform #resetLangText").text(myResetLangTextIntro);
3461    $("#langform #resetLangCta").text(myResetLangTextCta);
3462    //$("#resetLangButtonYes").attr("data-t", window.polyglot.t(myResetLangButtonYes));
3463
3464    // Text: <div data-t="nav.home"></div>
3465    // HTML: <div data-t="privacy" data-t-html></html>
3466    this.find('[data-t]').each(function() {
3467      var el = $(this);
3468      var data = el.data();
3469      if (data.t) {
3470        el[data.tHtml === '' ? 'html' : 'text'](polyglot.t(data.t));
3471      }
3472    });
3473
3474    return this;
3475  };
3476})();
3477/* ##########     END LOCALIZATION     ############ */
3478
3479// Translations. These should eventually be moved into language-specific files and loaded on demand.
3480// jshint nonbsp:false
3481switch (window.getLangPref()) {
3482  case 'ar':
3483    window.polyglot.extend({
3484      'newsletter': {
3485        'title': 'Google Play. يمكنك الحصول على آخر الأخبار والنصائح من مطوّري تطبيقات Android، مما يساعدك ' +
3486          'على تحقيق النجاح على',
3487        'requiredHint': '* حقول مطلوبة',
3488        'name': '. الاسم بالكامل ',
3489        'email': '. عنوان البريد الإلكتروني ',
3490        'company': '. اسم الشركة / اسم مطوّر البرامج',
3491        'appUrl': '. أحد عناوين URL لتطبيقاتك في متجر Play',
3492        'business': {
3493          'label': '. ما العنصر الذي يوضح طبيعة نشاطك التجاري بدقة؟ ',
3494          'apps': 'التطبيقات',
3495          'games': 'الألعاب',
3496          'both': 'التطبيقات والألعاب'
3497        },
3498        'confirmMailingList': 'إضافتي إلى القائمة البريدية للنشرة الإخبارية الشهرية والرسائل الإلكترونية التي يتم' +
3499          ' إرسالها من حين لآخر بشأن التطوير وفرص Google Play.',
3500        'privacyPolicy': 'أقر بأن المعلومات المقدَّمة في هذا النموذج تخضع لسياسة خصوصية ' +
3501          '<a href="https://www.google.com/intl/ar/policies/privacy/" target="_blank">Google</a>.',
3502        'languageVal': 'Arabic (العربيّة)',
3503        'successTitle': 'رائع!',
3504        'successDetails': 'لقد اشتركت بنجاح للحصول على آخر الأخبار والنصائح من مطوّري برامج Android.'
3505      }
3506    });
3507    break;
3508  case 'zh-cn':
3509    window.polyglot.extend({
3510      'newsletter': {
3511        'title': '获取最新的 Android 开发者资讯和提示,助您在 Google Play 上取得成功。',
3512        'requiredHint': '* 必填字段',
3513        'name': '全名',
3514        'email': '电子邮件地址',
3515        'company': '公司/开发者名称',
3516        'appUrl': '您的某个 Play 商店应用网址',
3517        'business': {
3518          'label': '哪一项能够最准确地描述您的业务?',
3519          'apps': '应用',
3520          'games': '游戏',
3521          'both': '应用和游戏'
3522        },
3523        'confirmMailingList': '将我添加到邮寄名单,以便接收每月简报以及不定期发送的关于开发和 Google Play 商机的电子邮件。',
3524        'privacyPolicy': '我确认自己了解在此表单中提供的信息受 <a href="https://www.google.com/intl/zh-CN/' +
3525        'policies/privacy/" target="_blank">Google</a> 隐私权政策的约束。',
3526        'languageVal': 'Simplified Chinese (简体中文)',
3527        'successTitle': '太棒了!',
3528        'successDetails': '您已成功订阅最新的 Android 开发者资讯和提示。'
3529      }
3530    });
3531    break;
3532  case 'zh-tw':
3533    window.polyglot.extend({
3534      'newsletter': {
3535        'title': '獲得 Android 開發人員的最新消息和各項秘訣,讓您在 Google Play 上輕鬆邁向成功之路。',
3536        'requiredHint': '* 必要欄位',
3537        'name': '全名',
3538        'email': '電子郵件地址',
3539        'company': '公司/開發人員名稱',
3540        'appUrl': '您其中一個 Play 商店應用程式的網址',
3541        'business': {
3542          'label': '為您的商家選取最合適的產品類別。',
3543          'apps': '應用程式',
3544          'games': '遊戲',
3545          'both': '應用程式和遊戲'
3546        },
3547        'confirmMailingList': '我想加入 Google Play 的郵寄清單,以便接收每月電子報和 Google Play 不定期寄送的電子郵件,' +
3548          '瞭解關於開發和 Google Play 商機的資訊。',
3549        'privacyPolicy': '我瞭解,我在這張表單中提供的資訊將受到 <a href="' +
3550        'https://www.google.com/intl/zh-TW/policies/privacy/" target="_blank">Google</a> 隱私權政策.',
3551        'languageVal': 'Traditional Chinese (繁體中文)',
3552        'successTitle': '太棒了!',
3553        'successDetails': '您已經成功訂閱 Android 開發人員的最新消息和各項秘訣。'
3554      }
3555    });
3556    break;
3557  case 'fr':
3558    window.polyglot.extend({
3559      'newsletter': {
3560        'title': 'Recevez les dernières actualités destinées aux développeurs Android, ainsi que des conseils qui ' +
3561          'vous mèneront vers le succès sur Google Play.',
3562        'requiredHint': '* Champs obligatoires',
3563        'name': 'Nom complet',
3564        'email': 'Adresse e-mail',
3565        'company': 'Nom de la société ou du développeur',
3566        'appUrl': 'Une de vos URL Play Store',
3567        'business': {
3568          'label': 'Quelle option décrit le mieux votre activité ?',
3569          'apps': 'Applications',
3570          'games': 'Jeux',
3571          'both': 'Applications et jeux'
3572        },
3573        'confirmMailingList': 'Ajoutez-moi à la liste de diffusion de la newsletter mensuelle et tenez-moi informé ' +
3574          'par des e-mails occasionnels de l\'évolution et des opportunités de Google Play.',
3575        'privacyPolicy': 'Je comprends que les renseignements fournis dans ce formulaire seront soumis aux <a href="' +
3576        'https://www.google.com/intl/fr/policies/privacy/" target="_blank">règles de confidentialité</a> de Google.',
3577        'languageVal': 'French (français)',
3578        'successTitle': 'Super !',
3579        'successDetails': 'Vous êtes bien inscrit pour recevoir les actualités et les conseils destinés aux ' +
3580          'développeurs Android.'
3581      }
3582    });
3583    break;
3584  case 'de':
3585    window.polyglot.extend({
3586      'newsletter': {
3587        'title': 'Abonniere aktuelle Informationen und Tipps für Android-Entwickler und werde noch erfolgreicher ' +
3588          'bei Google Play.',
3589        'requiredHint': '* Pflichtfelder',
3590        'name': 'Vollständiger Name',
3591        'email': 'E-Mail-Adresse',
3592        'company': 'Unternehmens-/Entwicklername',
3593        'appUrl': 'Eine der URLs deiner Play Store App',
3594        'business': {
3595          'label': 'Welche der folgenden Kategorien beschreibt dein Unternehmen am besten?',
3596          'apps': 'Apps',
3597          'games': 'Spiele',
3598          'both': 'Apps und Spiele'
3599        },
3600        'confirmMailingList': 'Meine E-Mail-Adresse soll zur Mailingliste hinzugefügt werden, damit ich den ' +
3601          'monatlichen Newsletter sowie gelegentlich E-Mails zu Entwicklungen und Optionen bei Google Play erhalte.',
3602        'privacyPolicy': 'Ich bestätige, dass die in diesem Formular bereitgestellten Informationen gemäß der ' +
3603          '<a href="https://www.google.com/intl/de/policies/privacy/" target="_blank">Datenschutzerklärung</a> von ' +
3604          'Google verwendet werden dürfen.',
3605        'languageVal': 'German (Deutsch)',
3606        'successTitle': 'Super!',
3607        'successDetails': 'Du hast dich erfolgreich angemeldet und erhältst jetzt aktuelle Informationen und Tipps ' +
3608          'für Android-Entwickler.'
3609      }
3610    });
3611    break;
3612  case 'id':
3613    window.polyglot.extend({
3614      'newsletter': {
3615        'title': 'Receba as dicas e as notícias mais recentes para os desenvolvedores Android e seja bem-sucedido ' +
3616        'no Google Play.',
3617        'requiredHint': '* Bidang Wajib Diisi',
3618        'name': 'Nama lengkap',
3619        'email': 'Alamat email',
3620        'company': 'Nama pengembang / perusahaan',
3621        'appUrl': 'Salah satu URL aplikasi Play Store Anda',
3622        'business': {
3623          'label': 'Dari berikut ini, mana yang paling cocok dengan bisnis Anda?',
3624          'apps': 'Aplikasi',
3625          'games': 'Game',
3626          'both': 'Aplikasi dan Game'
3627        },
3628        'confirmMailingList': 'Tambahkan saya ke milis untuk mendapatkan buletin bulanan dan email sesekali mengenai ' +
3629          'perkembangan dan kesempatan yang ada di Google Play.',
3630        'privacyPolicy': 'Saya memahami bahwa informasi yang diberikan dalam formulir ini tunduk pada <a href="' +
3631        'https://www.google.com/intl/in/policies/privacy/" target="_blank">kebijakan privasi</a> Google.',
3632        'languageVal': 'Indonesian (Bahasa)',
3633        'successTitle': 'Hore!',
3634        'successDetails': 'Anda berhasil mendaftar untuk kiat dan berita pengembang Android terbaru.'
3635      }
3636    });
3637    break;
3638  case 'it':
3639    //window.polyglot.extend({
3640    //  'newsletter': {
3641    //    'title': 'Receba as dicas e as notícias mais recentes para os desenvolvedores Android e seja bem-sucedido ' +
3642    //    'no Google Play.',
3643    //    'requiredHint': '* Campos obrigatórios',
3644    //    'name': 'Nome completo',
3645    //    'email': 'Endereço de Email',
3646    //    'company': 'Nome da empresa / do desenvolvedor',
3647    //    'appUrl': 'URL de um dos seus apps da Play Store',
3648    //    'business': {
3649    //      'label': 'Qual das seguintes opções melhor descreve sua empresa?',
3650    //      'apps': 'Apps',
3651    //      'games': 'Jogos',
3652    //      'both': 'Apps e Jogos'
3653    //    },
3654    //    'confirmMailingList': 'Inscreva-me na lista de e-mails para que eu receba o boletim informativo mensal, ' +
3655    //    'bem como e-mails ocasionais sobre o desenvolvimento e as oportunidades do Google Play.',
3656    //    'privacyPolicy': 'Reconheço que as informações fornecidas neste formulário estão sujeitas à <a href="' +
3657    //    'https://www.google.com.br/policies/privacy/" target="_blank">Política de Privacidade</a> do Google.',
3658    //    'languageVal': 'Italian (italiano)',
3659    //    'successTitle': 'Uhu!',
3660    //    'successDetails': 'Você se inscreveu para receber as notícias e as dicas mais recentes para os ' +
3661    //    'desenvolvedores Android.',
3662    //  }
3663    //});
3664    break;
3665  case 'ja':
3666    window.polyglot.extend({
3667      'newsletter': {
3668        'title': 'Google Play での成功に役立つ Android デベロッパー向けの最新ニュースやおすすめの情報をお届けします。',
3669        'requiredHint': '* 必須',
3670        'name': '氏名',
3671        'email': 'メールアドレス',
3672        'company': '会社名 / デベロッパー名',
3673        'appUrl': 'Play ストア アプリの URL(いずれか 1 つ)',
3674        'business': {
3675          'label': 'お客様のビジネスに最もよく当てはまるものをお選びください。',
3676          'apps': 'アプリ',
3677          'games': 'ゲーム',
3678          'both': 'アプリとゲーム'
3679        },
3680        'confirmMailingList': '開発や Google Play の最新情報に関する毎月発行のニュースレターや不定期発行のメールを受け取る',
3681        'privacyPolicy': 'このフォームに入力した情報に <a href="https://www.google.com/intl/ja/policies/privacy/" ' +
3682          'target="_blank">Google</a> のプライバシー ポリシーが適用',
3683        'languageVal': 'Japanese (日本語)',
3684        'successTitle': '完了です!',
3685        'successDetails': 'Android デベロッパー向けの最新ニュースやおすすめの情報の配信登録が完了しました。'
3686      }
3687    });
3688    break;
3689  case 'ko':
3690    window.polyglot.extend({
3691      'newsletter': {
3692        'title': 'Google Play에서 성공을 거두는 데 도움이 되는 최신 Android 개발자 소식 및 도움말을 받아 보세요.',
3693        'requiredHint': '* 필수 입력란',
3694        'name': '이름',
3695        'email': '이메일 주소',
3696        'company': '회사/개발자 이름',
3697        'appUrl': 'Play 스토어 앱 URL 중 1개',
3698        'business': {
3699          'label': '다음 중 내 비즈니스를 가장 잘 설명하는 단어는 무엇인가요?',
3700          'apps': '앱',
3701          'games': '게임',
3702          'both': '앱 및 게임'
3703        },
3704        'confirmMailingList': '개발 및 Google Play 관련 소식에 관한 월별 뉴스레터 및 비정기 이메일을 받아보겠습니다.',
3705        'privacyPolicy': '이 양식에 제공한 정보는 <a href="https://www.google.com/intl/ko/policies/privacy/" ' +
3706          'target="_blank">Google의</a> 개인정보취급방침에 따라 사용됨을',
3707        'languageVal':'Korean (한국어)',
3708        'successTitle': '축하합니다!',
3709        'successDetails': '최신 Android 개발자 뉴스 및 도움말을 받아볼 수 있도록 가입을 완료했습니다.'
3710      }
3711    });
3712    break;
3713  case 'pt-br':
3714    window.polyglot.extend({
3715      'newsletter': {
3716        'title': 'Receba as dicas e as notícias mais recentes para os desenvolvedores Android e seja bem-sucedido ' +
3717        'no Google Play.',
3718        'requiredHint': '* Campos obrigatórios',
3719        'name': 'Nome completo',
3720        'email': 'Endereço de Email',
3721        'company': 'Nome da empresa / do desenvolvedor',
3722        'appUrl': 'URL de um dos seus apps da Play Store',
3723        'business': {
3724          'label': 'Qual das seguintes opções melhor descreve sua empresa?',
3725          'apps': 'Apps',
3726          'games': 'Jogos',
3727          'both': 'Apps e Jogos'
3728        },
3729        'confirmMailingList': 'Inscreva-me na lista de e-mails para que eu receba o boletim informativo mensal, ' +
3730        'bem como e-mails ocasionais sobre o desenvolvimento e as oportunidades do Google Play.',
3731        'privacyPolicy': 'Reconheço que as informações fornecidas neste formulário estão sujeitas à <a href="' +
3732        'https://www.google.com.br/policies/privacy/" target="_blank">Política de Privacidade</a> do Google.',
3733        'languageVal': 'Brazilian Portuguese (Português Brasileiro)',
3734        'successTitle': 'Uhu!',
3735        'successDetails': 'Você se inscreveu para receber as notícias e as dicas mais recentes para os ' +
3736        'desenvolvedores Android.'
3737      }
3738    });
3739    break;
3740  case 'ru':
3741    window.polyglot.extend({
3742      'newsletter': {
3743        'title': 'Хотите получать последние новости и советы для разработчиков Google Play? Заполните эту форму.',
3744        'requiredHint': '* Обязательные поля',
3745        'name': 'Полное имя',
3746        'email': 'Адрес электронной почты',
3747        'company': 'Название компании или имя разработчика',
3748        'appUrl': 'Ссылка на любое ваше приложение в Google Play',
3749        'business': {
3750          'label': 'Что вы создаете?',
3751          'apps': 'Приложения',
3752          'games': 'Игры',
3753          'both': 'Игры и приложения'
3754        },
3755        'confirmMailingList': 'Я хочу получать ежемесячную рассылку для разработчиков и другие полезные новости ' +
3756          'Google Play.',
3757        'privacyPolicy': 'Я предоставляю эти данные в соответствии с <a href="' +
3758          'https://www.google.com/intl/ru/policies/privacy/" target="_blank">Политикой конфиденциальности</a> Google.',
3759        'languageVal': 'Russian (Русский)',
3760        'successTitle': 'Поздравляем!',
3761        'successDetails': 'Теперь вы подписаны на последние новости и советы для разработчиков Android.'
3762      }
3763    });
3764    break;
3765  case 'es':
3766    window.polyglot.extend({
3767      'newsletter': {
3768        'title': 'Recibe las últimas noticias y sugerencias para programadores de Android y logra tener éxito en ' +
3769          'Google Play.',
3770        'requiredHint': '* Campos obligatorios',
3771        'name': 'Dirección de correo electrónico',
3772        'email': 'Endereço de Email',
3773        'company': 'Nombre de la empresa o del programador',
3774        'appUrl': 'URL de una de tus aplicaciones de Play Store',
3775        'business': {
3776          'label': '¿Qué describe mejor a tu empresa?',
3777          'apps': 'Aplicaciones',
3778          'games': 'Juegos',
3779          'both': 'Juegos y aplicaciones'
3780        },
3781        'confirmMailingList': 'Deseo unirme a la lista de distribución para recibir el boletín informativo mensual ' +
3782          'y correos electrónicos ocasionales sobre desarrollo y oportunidades de Google Play.',
3783        'privacyPolicy': 'Acepto que la información que proporcioné en este formulario cumple con la <a href="' +
3784        'https://www.google.com/intl/es/policies/privacy/" target="_blank">política de privacidad</a> de Google.',
3785        'languageVal': 'Spanish (español)',
3786        'successTitle': '¡Felicitaciones!',
3787        'successDetails': 'El registro para recibir las últimas noticias y sugerencias para programadores de Android ' +
3788          'se realizó correctamente.'
3789      }
3790    });
3791    break;
3792  case 'th':
3793    window.polyglot.extend({
3794      'newsletter': {
3795        'title': 'รับข่าวสารล่าสุดสำหรับนักพัฒนาซอฟต์แวร์ Android ตลอดจนเคล็ดลับที่จะช่วยให้คุณประสบความสำเร็จบน ' +
3796          'Google Play',
3797        'requiredHint': '* ช่องที่ต้องกรอก',
3798        'name': 'ชื่อและนามสกุล',
3799        'email': 'ที่อยู่อีเมล',
3800        'company': 'ชื่อบริษัท/นักพัฒนาซอฟต์แวร์',
3801        'appUrl': 'URL แอปใดแอปหนึ่งของคุณใน Play สโตร์',
3802        'business': {
3803          'label': 'ข้อใดตรงกับธุรกิจของคุณมากที่สุด',
3804          'apps': 'แอป',
3805          'games': 'เกม',
3806          'both': 'แอปและเกม'
3807        },
3808        'confirmMailingList': 'เพิ่มฉันลงในรายชื่ออีเมลเพื่อรับจดหมายข่าวรายเดือนและอีเมลเป็นครั้งคราวเกี่ยวกับก' +
3809          'ารพัฒนาซอฟต์แวร์และโอกาสใน Google Play',
3810        'privacyPolicy': 'ฉันรับทราบว่าข้อมูลที่ให้ไว้ในแบบฟอร์มนี้จะเป็นไปตามนโยบายส่วนบุคคลของ ' +
3811          '<a href="https://www.google.com/intl/th/policies/privacy/" target="_blank">Google</a>',
3812        'languageVal': 'Thai (ภาษาไทย)',
3813        'successTitle': 'ไชโย!',
3814        'successDetails': 'คุณลงชื่อสมัครรับข่าวสารและเคล็ดลับล่าสุดสำหรับนักพัฒนาซอฟต์แวร์ Android เสร็จเรียบร้อยแล้ว'
3815      }
3816    });
3817    break;
3818  case 'tr':
3819    window.polyglot.extend({
3820      'newsletter': {
3821        'title': 'Google Play\'de başarılı olmanıza yardımcı olacak en son Android geliştirici haberleri ve ipuçları.',
3822        'requiredHint': '* Zorunlu Alanlar',
3823        'name': 'Tam ad',
3824        'email': 'E-posta adresi',
3825        'company': 'Şirket / geliştirici adı',
3826        'appUrl': 'Play Store uygulama URL\'lerinizden biri',
3827        'business': {
3828          'label': 'İşletmenizi en iyi hangisi tanımlar?',
3829          'apps': 'Uygulamalar',
3830          'games': 'Oyunlar',
3831          'both': 'Uygulamalar ve Oyunlar'
3832        },
3833        'confirmMailingList': 'Beni, geliştirme ve Google Play fırsatlarıyla ilgili ara sıra gönderilen e-posta ' +
3834          'iletilerine ilişkin posta listesine ve aylık haber bültenine ekle.',
3835        'privacyPolicy': 'Bu formda sağlanan bilgilerin Google\'ın ' +
3836          '<a href="https://www.google.com/intl/tr/policies/privacy/" target="_blank">Gizlilik Politikası\'na</a> ' +
3837          'tabi olacağını kabul ediyorum.',
3838        'languageVal': 'Turkish (Türkçe)',
3839        'successTitle': 'Yaşasın!',
3840        'successDetails': 'En son Android geliştirici haberleri ve ipuçlarına başarıyla kaydoldunuz.'
3841      }
3842    });
3843    break;
3844  case 'vi':
3845    window.polyglot.extend({
3846      'newsletter': {
3847        'title': 'Nhận tin tức và mẹo mới nhất dành cho nhà phát triển Android sẽ giúp bạn tìm thấy thành công trên ' +
3848          'Google Play.',
3849        'requiredHint': '* Các trường bắt buộc',
3850        'name': 'Tên đầy đủ',
3851        'email': 'Địa chỉ email',
3852        'company': 'Tên công ty/nhà phát triển',
3853        'appUrl': 'Một trong số các URL ứng dụng trên cửa hàng Play của bạn',
3854        'business': {
3855          'label': 'Lựa chọn nào sau đây mô tả chính xác nhất doanh nghiệp của bạn?',
3856          'apps': 'Ứng dụng',
3857          'games': 'Trò chơi',
3858          'both': 'Ứng dụng và trò chơi'
3859        },
3860        'confirmMailingList': 'Thêm tôi vào danh sách gửi thư cho bản tin hàng tháng và email định kỳ về việc phát ' +
3861          'triển và cơ hội của Google Play.',
3862        'privacyPolicy': 'Tôi xác nhận rằng thông tin được cung cấp trong biểu mẫu này tuân thủ chính sách bảo mật ' +
3863          'của <a href="https://www.google.com/intl/vi/policies/privacy/" target="_blank">Google</a>.',
3864        'languageVal': 'Vietnamese (tiếng Việt)',
3865        'successTitle': 'Thật tuyệt!',
3866        'successDetails': 'Bạn đã đăng ký thành công nhận tin tức và mẹo mới nhất dành cho nhà phát triển của Android.'
3867      }
3868    });
3869    break;
3870}
3871
3872(function($) {
3873  'use strict';
3874
3875  function Modal(el, options) {
3876    this.el = $(el);
3877    this.options = $.extend({}, options);
3878    this.isOpen = false;
3879
3880    this.el.on('click', function(event) {
3881      if (!$.contains(this.el.find('.dac-modal-window')[0], event.target)) {
3882        return this.el.trigger('modal-close');
3883      }
3884    }.bind(this));
3885
3886    this.el.on('modal-open', this.open_.bind(this));
3887    this.el.on('modal-close', this.close_.bind(this));
3888    this.el.on('modal-toggle', this.toggle_.bind(this));
3889  }
3890
3891  Modal.prototype.toggle_ = function() {
3892    this.el.trigger('modal-' + (this.isOpen ? 'close' : 'open'));
3893  };
3894
3895  Modal.prototype.close_ = function() {
3896    // When closing the modal for Android Studio downloads, reload the page
3897    // because otherwise we might get stuck with post-download dialog state
3898    if ($("[data-modal='studio_tos'].dac-active").length) {
3899      location.reload();
3900    }
3901    this.el.removeClass('dac-active');
3902    $('body').removeClass('dac-modal-open');
3903    this.isOpen = false;
3904  };
3905
3906  Modal.prototype.open_ = function() {
3907    this.el.addClass('dac-active');
3908    $('body').addClass('dac-modal-open');
3909    this.isOpen = true;
3910  };
3911
3912  function onClickToggleModal(event) {
3913    event.preventDefault();
3914    var toggle = $(event.currentTarget);
3915    var options = toggle.data();
3916    var modal = options.modalToggle ? $('[data-modal="' + options.modalToggle + '"]') :
3917      toggle.closest('[data-modal]');
3918    modal.trigger('modal-toggle');
3919  }
3920
3921  /**
3922   * jQuery plugin
3923   * @param  {object} options - Override default options.
3924   */
3925  $.fn.dacModal = function(options) {
3926    return this.each(function() {
3927      new Modal(this, options);
3928    });
3929  };
3930
3931  $.fn.dacToggleModal = function(options) {
3932    return this.each(function() {
3933      new ToggleModal(this, options);
3934    });
3935  };
3936
3937  /**
3938   * Data Attribute API
3939   */
3940  $(document).on('ready.aranja', function() {
3941    $('[data-modal]').each(function() {
3942      $(this).dacModal($(this).data());
3943    });
3944
3945    $('html').on('click.modal', '[data-modal-toggle]', onClickToggleModal);
3946
3947    // Check if url anchor is targetting a toggle to open the modal.
3948    if (location.hash) {
3949      var $elem = $(document.getElementById(location.hash.substr(1)));
3950      if ($elem.attr('data-modal-toggle')) {
3951        $elem.trigger('click');
3952      }
3953    }
3954
3955    var isTargetLangValid = false;
3956    $(ANDROID_LANGUAGES).each(function(index, langCode) {
3957      if (langCode == window.getLangTarget()) {
3958        isTargetLangValid = true;
3959        return;
3960      }
3961    });
3962    if (window.getLangTarget() !== window.getLangPref() && isTargetLangValid) {
3963        $('#langform').trigger('modal-open');
3964        $("#langform button.yes").attr("onclick","window.changeLangPref('" + window.getLangTarget() + "', true);  return false;");
3965        $("#langform button.no").attr("onclick","window.changeLangPref('" + window.getLangPref() + "', true); return false;");
3966    }
3967
3968    /* Check the current API level, but only if we're in the reference */
3969    if (location.pathname.indexOf('/reference') == 0) {
3970      // init available apis based on user pref
3971      changeApiLevel();
3972    }
3973  });
3974})(jQuery);
3975
3976/* Fullscreen - Toggle fullscreen mode for reference pages */
3977(function($) {
3978  'use strict';
3979
3980  /**
3981   * @param {HTMLElement} el - The DOM element.
3982   * @constructor
3983   */
3984  function Fullscreen(el) {
3985    this.el = $(el);
3986    this.html = $('html');
3987    this.icon = this.el.find('.dac-sprite');
3988    this.isFullscreen = window.readCookie(Fullscreen.COOKIE_) === 'true';
3989    this.activate_();
3990    this.el.on('click.dac-fullscreen', this.toggleHandler_.bind(this));
3991  }
3992
3993  /**
3994   * Cookie name for storing the state
3995   * @type {string}
3996   * @private
3997   */
3998  Fullscreen.COOKIE_ = 'fullscreen';
3999
4000  /**
4001   * Classes to modify the DOM
4002   * @type {{mode: string, fullscreen: string, fullscreenExit: string}}
4003   * @private
4004   */
4005  Fullscreen.CLASSES_ = {
4006    mode: 'dac-fullscreen-mode',
4007    fullscreen: 'dac-fullscreen',
4008    fullscreenExit: 'dac-fullscreen-exit'
4009  };
4010
4011  /**
4012   * Event listener for toggling fullscreen mode
4013   * @param {MouseEvent} event
4014   * @private
4015   */
4016  Fullscreen.prototype.toggleHandler_ = function(event) {
4017    event.stopPropagation();
4018    this.toggle(!this.isFullscreen, true);
4019  };
4020
4021  /**
4022   * Change the DOM based on current state.
4023   * @private
4024   */
4025  Fullscreen.prototype.activate_ = function() {
4026    this.icon.toggleClass(Fullscreen.CLASSES_.fullscreen, !this.isFullscreen);
4027    this.icon.toggleClass(Fullscreen.CLASSES_.fullscreenExit, this.isFullscreen);
4028    this.html.toggleClass(Fullscreen.CLASSES_.mode, this.isFullscreen);
4029  };
4030
4031  /**
4032   * Toggle fullscreen mode and store the state in a cookie.
4033   */
4034  Fullscreen.prototype.toggle = function() {
4035    this.isFullscreen = !this.isFullscreen;
4036    window.writeCookie(Fullscreen.COOKIE_, this.isFullscreen, null);
4037    this.activate_();
4038  };
4039
4040  /**
4041   * jQuery plugin
4042   */
4043  $.fn.dacFullscreen = function() {
4044    return this.each(function() {
4045      new Fullscreen($(this));
4046    });
4047  };
4048})(jQuery);
4049
4050(function($) {
4051  'use strict';
4052
4053  /**
4054   * @param {HTMLElement} selected - The link that is selected in the nav.
4055   * @constructor
4056   */
4057  function HeaderTabs(selected) {
4058
4059    // Don't highlight any tabs on the index page
4060    if (location.pathname === '/index.html' || location.pathname === '/') {
4061      //return;
4062    }
4063
4064    this.selected = $(selected);
4065    this.selectedParent = this.selected.closest('.dac-nav-secondary').siblings('a');
4066    this.links = $('.dac-header-tabs a');
4067
4068    this.selectActiveTab();
4069  }
4070
4071  HeaderTabs.prototype.selectActiveTab = function() {
4072    var section = null;
4073
4074    if (this.selectedParent.length) {
4075      section = this.selectedParent.text();
4076    } else {
4077      section = this.selected.text();
4078    }
4079
4080    if (section) {
4081      this.links.removeClass('selected');
4082
4083      this.links.filter(function() {
4084        return $(this).text() === $.trim(section);
4085      }).addClass('selected');
4086    }
4087  };
4088
4089  /**
4090   * jQuery plugin
4091   */
4092  $.fn.dacHeaderTabs = function() {
4093    return this.each(function() {
4094      new HeaderTabs(this);
4095    });
4096  };
4097})(jQuery);
4098
4099(function($) {
4100  'use strict';
4101  var icon = $('<i/>').addClass('dac-sprite dac-nav-forward');
4102  var config = JSON.parse(window.localStorage.getItem('global-navigation') || '{}');
4103  var forwardLink = $('<span/>')
4104    .addClass('dac-nav-link-forward')
4105    .html(icon)
4106    .attr('tabindex', 0)
4107    .on('click keypress', function(e) {
4108      if (e.type == 'keypress' && e.which == 13 || e.type == 'click') {
4109        swap_(e);
4110      }
4111    });
4112
4113  /**
4114   * @constructor
4115   */
4116  function Nav(navigation) {
4117    $('.dac-nav-list').dacCurrentPage().dacHeaderTabs().dacSidebarToggle($('body'));
4118
4119    navigation.find('[data-reference-tree]').dacReferenceNav();
4120
4121    setupViews_(navigation.children().eq(0).children());
4122
4123    initCollapsedNavs(navigation.find('.dac-nav-sub-slider'));
4124
4125    $('#dac-main-navigation').scrollIntoView('.selected')
4126  }
4127
4128  function updateStore(icon) {
4129    var navClass = getCurrentLandingPage_(icon);
4130    var isExpanded = icon.hasClass('dac-expand-less-black');
4131    var expandedNavs = config.expanded || [];
4132    if (isExpanded) {
4133      expandedNavs.push(navClass);
4134    } else {
4135      expandedNavs = expandedNavs.filter(function(item) {
4136        return item !== navClass;
4137      });
4138    }
4139    config.expanded = expandedNavs;
4140    window.localStorage.setItem('global-navigation', JSON.stringify(config));
4141  }
4142
4143  function toggleSubNav_(icon) {
4144    var isExpanded = icon.hasClass('dac-expand-less-black');
4145    icon.toggleClass('dac-expand-less-black', !isExpanded);
4146    icon.toggleClass('dac-expand-more-black', isExpanded);
4147    icon.data('sub-navigation.dac').slideToggle(200);
4148
4149    updateStore(icon);
4150  }
4151
4152  function handleSubNavToggle_(event) {
4153    event.preventDefault();
4154    var icon = $(event.target);
4155    toggleSubNav_(icon);
4156  }
4157
4158  function getCurrentLandingPage_(icon) {
4159    return icon.closest('li')[0].className.replace('dac-nav-item ', '');
4160  }
4161
4162  // Setup sub navigation collapse/expand
4163  function initCollapsedNavs(toggleIcons) {
4164    toggleIcons.each(setInitiallyActive_($('body')));
4165    toggleIcons.on('click keypress', function(e) {
4166      if (e.type == 'keypress' && e.which == 13 || e.type == 'click') {
4167        handleSubNavToggle_(e);
4168      }
4169    });
4170  }
4171
4172  function setInitiallyActive_(body) {
4173    var expandedNavs = config.expanded || [];
4174    return function(i, icon) {
4175      icon = $(icon);
4176      var subNav = icon.next();
4177
4178      if (!subNav.length) {
4179        return;
4180      }
4181
4182      var landingPageClass = getCurrentLandingPage_(icon);
4183      var expanded = expandedNavs.indexOf(landingPageClass) >= 0;
4184      landingPageClass = landingPageClass === 'home' ? 'about' : landingPageClass;
4185
4186      if (landingPageClass == 'about' && location.pathname == '/index.html') {
4187        expanded = true;
4188      }
4189
4190      // TODO: Should read from localStorage
4191      var visible = body.hasClass(landingPageClass) || expanded;
4192
4193      icon.data('sub-navigation.dac', subNav);
4194      icon.toggleClass('dac-expand-less-black', visible);
4195      icon.toggleClass('dac-expand-more-black', !visible);
4196      subNav.toggle(visible);
4197    };
4198  }
4199
4200  function setupViews_(views) {
4201    if (views.length === 1) {
4202      // Active tier 1 nav.
4203      views.addClass('dac-active');
4204    } else {
4205      // Activate back button and tier 2 nav.
4206      views.slice(0, 2).addClass('dac-active');
4207      var selectedNav = views.eq(2).find('.selected').after(forwardLink);
4208      var langAttr = selectedNav.attr(window.getLangPref() + '-lang');
4209      //form the label from locale attr if possible, else set to selectedNav text value
4210      if ((typeof langAttr !== typeof undefined &&  langAttr !== false) && (langAttr !== '')) {
4211        $('.dac-nav-back-title').text(langAttr);
4212      } else {
4213        $('.dac-nav-back-title').text(selectedNav.text());
4214      }
4215    }
4216
4217    // Navigation should animate.
4218    setTimeout(function() {
4219      views.removeClass('dac-no-anim');
4220    }, 10);
4221  }
4222
4223  function swap_(event) {
4224    event.preventDefault();
4225    $(event.currentTarget).trigger('swap-content');
4226  }
4227
4228  /**
4229   * jQuery plugin
4230   */
4231  $.fn.dacNav = function() {
4232    return this.each(function() {
4233      new Nav($(this));
4234    });
4235  };
4236})(jQuery);
4237
4238/* global NAVTREE_DATA */
4239(function($) {
4240  /**
4241   * Build the reference navigation with namespace dropdowns.
4242   * @param {jQuery} el - The DOM element.
4243   */
4244  function buildReferenceNav(el) {
4245    var supportLibraryPath = '/reference/android/support/';
4246    var currPath = location.pathname;
4247
4248    if (currPath.indexOf(supportLibraryPath) > -1) {
4249      updateSupportLibrariesNav(supportLibraryPath, currPath);
4250    }
4251    var namespaceList = el.find('[data-reference-namespaces]');
4252    var resources = $('[data-reference-resources]').detach();
4253    var selected = namespaceList.find('.selected');
4254    resources.appendTo(el);
4255
4256    // Links should be toggleable.
4257    namespaceList.find('a').addClass('dac-reference-nav-toggle dac-closed');
4258
4259    // Set the path for the navtree data to use.
4260    var navtree_filepath = getNavtreeFilePath(supportLibraryPath, currPath);
4261
4262    // Load in all resources
4263    $.getScript(navtree_filepath, function(data, textStatus, xhr) {
4264      if (xhr.status === 200) {
4265        namespaceList.on(
4266            'click', 'a.dac-reference-nav-toggle', toggleResourcesHandler);
4267      }
4268    });
4269
4270    // No setup required if no resources are present
4271    if (!resources.length) {
4272      return;
4273    }
4274
4275    // The resources should be a part of selected namespace.
4276    var overview = addResourcesToView(resources, selected);
4277
4278    // Currently viewing Overview
4279    if (location.href === overview.attr('href')) {
4280      overview.parent().addClass('selected');
4281    }
4282
4283    // Open currently selected resource
4284    var listsToOpen = selected.children().eq(1);
4285    listsToOpen = listsToOpen.add(
4286        listsToOpen.find('.selected').parent()).show();
4287
4288    // Mark dropdowns as open
4289    listsToOpen.prev().removeClass('dac-closed');
4290
4291    // Scroll into view
4292    namespaceList.scrollIntoView(selected);
4293  }
4294
4295  function getNavtreeFilePath(supportLibraryPath, currPath) {
4296    var navtree_filepath = '';
4297    var navtree_filename = 'navtree_data.js';
4298    if (currPath.indexOf(supportLibraryPath + 'test') > -1) {
4299      navtree_filepath = supportLibraryPath + 'test/' + navtree_filename;
4300    } else if (currPath.indexOf(supportLibraryPath + 'wearable') > -1) {
4301      navtree_filepath = supportLibraryPath + 'wearable/' + navtree_filename;
4302    } else {
4303      navtree_filepath = '/' + navtree_filename;
4304    }
4305    return navtree_filepath;
4306  }
4307
4308  function updateSupportLibrariesNav(supportLibraryPath, currPath) {
4309    var navTitle = '';
4310    if (currPath.indexOf(supportLibraryPath + 'test') > -1) {
4311      navTitle = 'Test Support APIs';
4312    } else if (currPath.indexOf(supportLibraryPath + 'wearable') > -1) {
4313      navTitle = 'Wearable Support APIs';
4314    }
4315    $('#api-nav-title').text(navTitle);
4316    $('#api-level-toggle').hide();
4317  }
4318
4319  /**
4320   * Handles the toggling of resources.
4321   * @param {Event} event
4322   */
4323  function toggleResourcesHandler(event) {
4324    event.preventDefault();
4325    if (event.type == 'click' || event.type == 'keypress' && event.which == 13) {
4326      var el = $(this);
4327      // If resources for given namespace is not present, fetch correct data.
4328      if (this.tagName === 'A' && !this.hasResources) {
4329        addResourcesToView(buildResourcesViewForData(getDataForNamespace(el.text())), el.parent());
4330      }
4331
4332      el.toggleClass('dac-closed').next().slideToggle(200);
4333    }
4334  }
4335
4336  /**
4337   * @param {String} namespace
4338   * @returns {Array} namespace data
4339   */
4340  function getDataForNamespace(namespace) {
4341    var namespaceData = NAVTREE_DATA.filter(function(data) {
4342      return data[0] === namespace;
4343    });
4344
4345    return namespaceData.length ? namespaceData[0][2] : [];
4346  }
4347
4348  /**
4349   * Build a list item for a resource
4350   * @param {Array} resource
4351   * @returns {String}
4352   */
4353  function buildResourceItem(resource) {
4354    return '<li class="api apilevel-' + resource[3] + '"><a href="/' + resource[1] + '">' + resource[0] + '</a></li>';
4355  }
4356
4357  /**
4358   * Build resources list items.
4359   * @param {Array} resources
4360   * @returns {String}
4361   */
4362  function buildResourceList(resources) {
4363    return '<li><h2>' + resources[0] + '</h2><ul>' + resources[2].map(buildResourceItem).join('') + '</ul>';
4364  }
4365
4366  /**
4367   * Build a resources view
4368   * @param {Array} data
4369   * @returns {jQuery} resources in an unordered list.
4370   */
4371  function buildResourcesViewForData(data) {
4372    return $('<ul>' + data.map(buildResourceList).join('') + '</ul>');
4373  }
4374
4375  /**
4376   * Add resources to a containing view.
4377   * @param {jQuery} resources
4378   * @param {jQuery} view
4379   * @returns {jQuery} the overview link.
4380   */
4381  function addResourcesToView(resources, view) {
4382    var namespace = view.children().eq(0);
4383    var overview = $('<a href="' + namespace.attr('href') + '">Overview</a>');
4384
4385    // Mark namespace with content;
4386    namespace[0].hasResources = true;
4387
4388    // Add correct classes / event listeners to resources.
4389    resources.prepend($('<li>').html(overview))
4390      .find('a')
4391        .addClass('dac-reference-nav-resource')
4392      .end()
4393        .find('h2').attr('tabindex', 0)
4394        .addClass('dac-reference-nav-toggle dac-closed')
4395        .on('click keypress', toggleResourcesHandler)
4396      .end()
4397        .add(resources.find('ul'))
4398        .addClass('dac-reference-nav-resources')
4399      .end()
4400        .appendTo(view);
4401
4402    return overview;
4403  }
4404
4405  function setActiveReferencePackage(el) {
4406    var packageLinkEls = el.find('[data-reference-namespaces] a');
4407    var selected = null;
4408    var highestMatchCount = 0;
4409    packageLinkEls.each(function(index, linkEl) {
4410      var matchCount = 0;
4411      $(location.pathname.split('/')).each(function(index, subpath) {
4412        if (linkEl.href.indexOf('/' + subpath + '/') > -1) {
4413          matchCount++;
4414        }
4415      });
4416      if (matchCount > highestMatchCount) {
4417        selected = linkEl;
4418        highestMatchCount = matchCount;
4419      }
4420    });
4421    $(selected).parent().addClass('selected');
4422  }
4423
4424  /**
4425   * jQuery plugin
4426   */
4427  $.fn.dacReferenceNav = function() {
4428    return this.each(function() {
4429      setActiveReferencePackage($(this));
4430      buildReferenceNav($(this));
4431    });
4432  };
4433})(jQuery);
4434
4435/** Scroll a container to make a target element visible
4436 This is called when the page finished loading. */
4437$.fn.scrollIntoView = function(target) {
4438  if ('string' === typeof target) {
4439    target = this.find(target);
4440  }
4441  if (this.is(':visible')) {
4442    if (target.length == 0) {
4443      // If no selected item found, exit
4444      return;
4445    }
4446
4447    // get the target element's offset from its container nav by measuring the element's offset
4448    // relative to the document then subtract the container nav's offset relative to the document
4449    var targetOffset = target.offset().top - this.offset().top;
4450    var containerHeight = this.height();
4451    if (targetOffset > containerHeight * .8) { // multiply nav height by .8 so we move up the item
4452      // if it's more than 80% down the nav
4453      // scroll the item up by an amount equal to 80% the container height
4454      this.scrollTop(targetOffset - (containerHeight * .8));
4455    }
4456  }
4457};
4458
4459(function($) {
4460  $.fn.dacCurrentPage = function() {
4461    // Highlight the header tabs...
4462    // highlight Design tab
4463    var baseurl = getBaseUri(window.location.pathname);
4464    var urlSegments = baseurl.split('/');
4465    var navEl = this;
4466    var body = $('body');
4467    var subNavEl = navEl.find('.dac-nav-secondary');
4468    var parentNavEl;
4469    var selected;
4470    // In NDK docs, highlight appropriate sub-nav
4471    if (body.hasClass('dac-ndk')) {
4472      if (body.hasClass('guide')) {
4473        selected = navEl.find('> li.guides > a').addClass('selected');
4474      } else if (body.hasClass('reference')) {
4475        selected = navEl.find('> li.reference > a').addClass('selected');
4476      } else if (body.hasClass('samples')) {
4477        selected = navEl.find('> li.samples > a').addClass('selected');
4478      } else if (body.hasClass('downloads')) {
4479        selected = navEl.find('> li.downloads > a').addClass('selected');
4480      }
4481    } else if (body.hasClass('dac-studio')) {
4482      if (body.hasClass('download')) {
4483        selected = navEl.find('> li.download > a').addClass('selected');
4484      } else if (body.hasClass('features')) {
4485        selected = navEl.find('> li.features > a').addClass('selected');
4486      } else if (body.hasClass('guide')) {
4487        selected = navEl.find('> li.guide > a').addClass('selected');
4488      } else if (body.hasClass('preview')) {
4489        selected = navEl.find('> li.preview > a').addClass('selected');
4490      }
4491    } else if (body.hasClass('design')) {
4492      selected = navEl.find('> li.design > a').addClass('selected');
4493      // highlight Home nav
4494    } else if (body.hasClass('about') || location.pathname == '/index.html') {
4495      parentNavEl = navEl.find('> li.home > a');
4496      parentNavEl.addClass('has-subnav');
4497      // In Home docs, also highlight appropriate sub-nav
4498      if (urlSegments[1] === 'wear' || urlSegments[1] === 'tv' ||
4499        urlSegments[1] === 'auto') {
4500        selected = subNavEl.find('li.' + urlSegments[1] + ' > a').addClass('selected');
4501      } else if (urlSegments[1] === 'about') {
4502        selected = subNavEl.find('li.versions > a').addClass('selected');
4503      } else {
4504        selected = parentNavEl.removeClass('has-subnav').addClass('selected');
4505      }
4506      // highlight Develop nav
4507    } else if (body.hasClass('develop') || body.hasClass('google')) {
4508      parentNavEl = navEl.find('> li.develop > a');
4509      parentNavEl.addClass('has-subnav');
4510      // In Develop docs, also highlight appropriate sub-nav
4511      if (urlSegments[1] === 'training') {
4512        selected = subNavEl.find('li.training > a').addClass('selected');
4513      } else if (urlSegments[1] === 'guide') {
4514        selected = subNavEl.find('li.guide > a').addClass('selected');
4515      } else if (urlSegments[1] === 'reference') {
4516        // If the root is reference, but page is also part of Google Services, select Google
4517        if (body.hasClass('google')) {
4518          selected = subNavEl.find('li.google > a').addClass('selected');
4519        } else {
4520          selected = subNavEl.find('li.reference > a').addClass('selected');
4521        }
4522      } else if (body.hasClass('google')) {
4523        selected = subNavEl.find('li.google > a').addClass('selected');
4524      } else if (body.hasClass('samples')) {
4525        selected = subNavEl.find('li.samples > a').addClass('selected');
4526      } else {
4527        selected = parentNavEl.removeClass('has-subnav').addClass('selected');
4528      }
4529      // highlight Distribute nav
4530    } else if (body.hasClass('distribute')) {
4531      parentNavEl = navEl.find('> li.distribute > a');
4532      parentNavEl.addClass('has-subnav');
4533      // In Distribute docs, also highlight appropriate sub-nav
4534      if (urlSegments[2] === 'users') {
4535        selected = subNavEl.find('li.users > a').addClass('selected');
4536      } else if (urlSegments[2] === 'engage') {
4537        selected = subNavEl.find('li.engage > a').addClass('selected');
4538      } else if (urlSegments[2] === 'monetize') {
4539        selected = subNavEl.find('li.monetize > a').addClass('selected');
4540      } else if (urlSegments[2] === 'analyze') {
4541        selected = subNavEl.find('li.analyze > a').addClass('selected');
4542      } else if (urlSegments[2] === 'tools') {
4543        selected = subNavEl.find('li.disttools > a').addClass('selected');
4544      } else if (urlSegments[2] === 'stories') {
4545        selected = subNavEl.find('li.stories > a').addClass('selected');
4546      } else if (urlSegments[2] === 'essentials') {
4547        selected = subNavEl.find('li.essentials > a').addClass('selected');
4548      } else if (urlSegments[2] === 'googleplay') {
4549        selected = subNavEl.find('li.googleplay > a').addClass('selected');
4550      } else {
4551        selected = parentNavEl.removeClass('has-subnav').addClass('selected');
4552      }
4553    } else if (body.hasClass('preview')) {
4554      selected = navEl.find('> li.preview > a').addClass('selected');
4555    }
4556    return $(selected);
4557  };
4558})(jQuery);
4559
4560(function($) {
4561  'use strict';
4562
4563  /**
4564   * Toggle the visabilty of the mobile navigation.
4565   * @param {HTMLElement} el - The DOM element.
4566   * @param {Object} options
4567   * @constructor
4568   */
4569  function ToggleNav(el, options) {
4570    this.el = $(el);
4571    this.options = $.extend({}, ToggleNav.DEFAULTS_, options);
4572    this.body = $(document.body);
4573    this.navigation_ = this.body.find(this.options.navigation);
4574    this.el.on('click', this.clickHandler_.bind(this));
4575  }
4576
4577  ToggleNav.BREAKPOINT_ = 980;
4578
4579  /**
4580   * Open on correct sizes
4581   */
4582  function toggleSidebarVisibility(body) {
4583    var wasClosed = ('' + localStorage.getItem('navigation-open')) === 'false';
4584    // Override the local storage setting for navigation-open for child sites
4585    // with no-subnav class.
4586    if (document.body.classList.contains('no-subnav')) {
4587      wasClosed = false;
4588    }
4589
4590    if (wasClosed) {
4591      body.removeClass(ToggleNav.DEFAULTS_.activeClass);
4592    } else if (window.innerWidth >= ToggleNav.BREAKPOINT_) {
4593      body.addClass(ToggleNav.DEFAULTS_.activeClass);
4594    } else {
4595      body.removeClass(ToggleNav.DEFAULTS_.activeClass);
4596    }
4597  }
4598
4599  /**
4600   * ToggleNav Default Settings
4601   * @type {{body: boolean, dimmer: string, navigation: string, activeClass: string}}
4602   * @private
4603   */
4604  ToggleNav.DEFAULTS_ = {
4605    body: true,
4606    dimmer: '.dac-nav-dimmer',
4607    animatingClass: 'dac-nav-animating',
4608    navigation: '[data-dac-nav]',
4609    activeClass: 'dac-nav-open'
4610  };
4611
4612  /**
4613   * The actual toggle logic.
4614   * @param {Event} event
4615   * @private
4616   */
4617  ToggleNav.prototype.clickHandler_ = function(event) {
4618    event.preventDefault();
4619    var animatingClass = this.options.animatingClass;
4620    var body = this.body;
4621
4622    body.addClass(animatingClass);
4623    body.toggleClass(this.options.activeClass);
4624
4625    setTimeout(function() {
4626      body.removeClass(animatingClass);
4627    }, this.navigation_.transitionDuration());
4628
4629    if (window.innerWidth >= ToggleNav.BREAKPOINT_) {
4630      localStorage.setItem('navigation-open', body.hasClass(this.options.activeClass));
4631    }
4632  };
4633
4634  /**
4635   * jQuery plugin
4636   * @param  {object} options - Override default options.
4637   */
4638  $.fn.dacToggleMobileNav = function() {
4639    return this.each(function() {
4640      var el = $(this);
4641      new ToggleNav(el, el.data());
4642    });
4643  };
4644
4645  $.fn.dacSidebarToggle = function(body) {
4646    toggleSidebarVisibility(body);
4647    $(window).on('resize', toggleSidebarVisibility.bind(null, body));
4648  };
4649
4650  /**
4651   * Data Attribute API
4652   */
4653  $(function() {
4654    $('[data-dac-toggle-nav]').dacToggleMobileNav();
4655  });
4656})(jQuery);
4657
4658(function($) {
4659  'use strict';
4660
4661  /**
4662   * Submit the newsletter form to a Google Form.
4663   * @param {HTMLElement} el - The Form DOM element.
4664   * @constructor
4665   */
4666  function NewsletterForm(el) {
4667    this.el = $(el);
4668    this.form = this.el.find('form');
4669    $('<iframe/>').hide()
4670      .attr('name', 'dac-newsletter-iframe')
4671      .attr('src', '')
4672      .insertBefore(this.form);
4673    this.el.find('[data-newsletter-language]').val(window.polyglot.t('newsletter.languageVal'));
4674    this.form.on('submit', this.submitHandler_.bind(this));
4675  }
4676
4677  /**
4678   * Milliseconds until modal has vanished after modal-close is triggered.
4679   * @type {number}
4680   * @private
4681   */
4682  NewsletterForm.CLOSE_DELAY_ = 300;
4683
4684  /**
4685   * Switch view to display form after close.
4686   * @private
4687   */
4688  NewsletterForm.prototype.closeHandler_ = function() {
4689    setTimeout(function() {
4690      this.el.trigger('swap-reset');
4691    }.bind(this), NewsletterForm.CLOSE_DELAY_);
4692  };
4693
4694  /**
4695   * Reset the modal to initial state.
4696   * @private
4697   */
4698  NewsletterForm.prototype.reset_ = function() {
4699    this.form.trigger('reset');
4700    this.el.one('modal-close', this.closeHandler_.bind(this));
4701  };
4702
4703  /**
4704   * Display a success view on submit.
4705   * @private
4706   */
4707  NewsletterForm.prototype.submitHandler_ = function() {
4708    this.el.one('swap-complete', this.reset_.bind(this));
4709    this.el.trigger('swap-content');
4710  };
4711
4712  /**
4713   * jQuery plugin
4714   * @param  {object} options - Override default options.
4715   */
4716  $.fn.dacNewsletterForm = function(options) {
4717    return this.each(function() {
4718      new NewsletterForm(this, options);
4719    });
4720  };
4721
4722  /**
4723   * Data Attribute API
4724   */
4725  $(document).on('ready.aranja', function() {
4726    $('[data-newsletter]').each(function() {
4727      $(this).dacNewsletterForm();
4728    });
4729  });
4730})(jQuery);
4731
4732/* globals METADATA, YOUTUBE_RESOURCES, BLOGGER_RESOURCES */
4733window.metadata = {};
4734
4735/**
4736 * Prepare metadata and indices for querying.
4737 */
4738window.metadata.prepare = (function() {
4739  // Helper functions.
4740  function mergeArrays() {
4741    return Array.prototype.concat.apply([], arguments);
4742  }
4743
4744  /**
4745   * Creates lookup maps for a resource index.
4746   * I.e. where MAP['some tag'][resource.id] === true when that resource has 'some tag'.
4747   * @param resourceDict
4748   * @returns {{}}
4749   */
4750  function buildResourceLookupMap(resourceDict) {
4751    var map = {};
4752    for (var key in resourceDict) {
4753      var dictForKey = {};
4754      var srcArr = resourceDict[key];
4755      for (var i = 0; i < srcArr.length; i++) {
4756        dictForKey[srcArr[i].index] = true;
4757      }
4758      map[key] = dictForKey;
4759    }
4760    return map;
4761  }
4762
4763  /**
4764   * Merges metadata maps for english and the current language into the global store.
4765   */
4766  function mergeMetadataMap(name, locale) {
4767    if (locale && locale !== 'en' && METADATA[locale]) {
4768      METADATA[name] = $.extend(METADATA.en[name], METADATA[locale][name]);
4769    } else {
4770      METADATA[name] = METADATA.en[name];
4771    }
4772  }
4773
4774  /**
4775   * Index all resources by type, url, tag and category.
4776   * @param resources
4777   */
4778  function createIndices(resources) {
4779    // URL, type, tag and category lookups
4780    var byType = METADATA.byType = {};
4781    var byUrl = METADATA.byUrl = {};
4782    var byTag = METADATA.byTag = {};
4783    var byCategory = METADATA.byCategory = {};
4784
4785    for (var i = 0; i < resources.length; i++) {
4786      var res = resources[i];
4787
4788      // Store index.
4789      res.index = i;
4790
4791      // Index by type.
4792      var type = res.type;
4793      if (type) {
4794        byType[type] = byType[type] || [];
4795        byType[type].push(res);
4796      }
4797
4798      // Index by tag.
4799      var tags = res.tags || [];
4800      for (var j = 0; j < tags.length; j++) {
4801        var tag = tags[j];
4802        if (tag) {
4803          byTag[tag] = byTag[tag] || [];
4804          byTag[tag].push(res);
4805        }
4806      }
4807
4808      // Index by category.
4809      var category = res.category;
4810      if (category) {
4811        byCategory[category] = byCategory[category] || [];
4812        byCategory[category].push(res);
4813      }
4814
4815      // Index by url.
4816      var url = res.url;
4817      if (url) {
4818        res.baseUrl = url.replace(/^intl\/\w+[\/]/, '');
4819        byUrl[res.baseUrl] = res;
4820      }
4821    }
4822    METADATA.hasType = buildResourceLookupMap(byType);
4823    METADATA.hasTag = buildResourceLookupMap(byTag);
4824    METADATA.hasCategory = buildResourceLookupMap(byCategory);
4825  }
4826
4827  return function() {
4828    // Only once.
4829    if (METADATA.all) { return; }
4830
4831    // Get current language.
4832    var locale = getLangPref();
4833    // Merge english resources.
4834    if (useDevsiteMetadata) {
4835      var all_keys = Object.keys(METADATA['en']);
4836      METADATA.all = []
4837
4838      $(all_keys).each(function(index, category) {
4839        if (RESERVED_METADATA_CATEGORY_NAMES.indexOf(category) == -1) {
4840          METADATA.all = mergeArrays(
4841            METADATA.all,
4842            METADATA.en[category]
4843          );
4844        }
4845      });
4846
4847      METADATA.all = mergeArrays(
4848        METADATA.all,
4849        YOUTUBE_RESOURCES,
4850        BLOGGER_RESOURCES,
4851        METADATA.en.extras
4852      );
4853    } else {
4854      METADATA.all = mergeArrays(
4855        METADATA.en.about,
4856        METADATA.en.design,
4857        METADATA.en.distribute,
4858        METADATA.en.develop,
4859        YOUTUBE_RESOURCES,
4860        BLOGGER_RESOURCES,
4861        METADATA.en.extras
4862      );
4863    }
4864
4865    // Merge local language resources.
4866    if (locale !== 'en' && METADATA[locale]) {
4867      if (useDevsiteMetadata) {
4868        all_keys = Object.keys(METADATA[locale]);
4869        $(all_keys).each(function(index, category) {
4870          if (RESERVED_METADATA_CATEGORY_NAMES.indexOf(category) == -1) {
4871            METADATA.all = mergeArrays(
4872              METADATA.all,
4873              METADATA.en[category]
4874            );
4875          }
4876        });
4877
4878        METADATA.all = mergeArrays(
4879          METADATA.all,
4880          METADATA[locale].extras
4881        );
4882      } else {
4883        METADATA.all = mergeArrays(
4884          METADATA.all,
4885          METADATA[locale].about,
4886          METADATA[locale].design,
4887          METADATA[locale].distribute,
4888          METADATA[locale].develop,
4889          METADATA[locale].extras
4890        );
4891
4892      }
4893    }
4894
4895    mergeMetadataMap('collections', locale);
4896    mergeMetadataMap('searchHeroCollections', locale);
4897    mergeMetadataMap('carousel', locale);
4898
4899    // Create query indicies for resources.
4900    createIndices(METADATA.all, locale);
4901
4902    // Reference metadata.
4903    METADATA.androidReference = mergeArrays(
4904        window.DATA, window.SUPPORT_WEARABLE_DATA, window.SUPPORT_TEST_DATA);
4905    METADATA.googleReference = mergeArrays(window.GMS_DATA, window.GCM_DATA);
4906  };
4907})();
4908
4909/* global METADATA, util */
4910window.metadata.query = (function($) {
4911  var pageMap = {};
4912
4913  function buildResourceList(opts) {
4914    window.metadata.prepare();
4915    var expressions = parseResourceQuery(opts.query || '');
4916    var instanceMap = {};
4917    var results = [];
4918
4919    for (var i = 0; i < expressions.length; i++) {
4920      var clauses = expressions[i];
4921
4922      // Get all resources for first clause
4923      var resources = getResourcesForClause(clauses.shift());
4924
4925      // Concat to final results list
4926      results = results.concat(resources.map(filterResources(clauses, i > 0, instanceMap)).filter(filterEmpty));
4927    }
4928
4929    // Set correct order
4930    if (opts.sortOrder && results.length) {
4931      results = opts.sortOrder === 'random' ? util.shuffle(results) : results.sort(sortResultsByKey(opts.sortOrder));
4932    }
4933
4934    // Slice max results.
4935    if (opts.maxResults !== Infinity) {
4936      results = results.slice(0, opts.maxResults);
4937    }
4938
4939    // Remove page level duplicates
4940    if (opts.allowDuplicates === undefined || opts.allowDuplicates === 'false') {
4941      results = results.filter(removePageLevelDuplicates);
4942
4943      for (var index = 0; index < results.length; ++index) {
4944        pageMap[results[index].index] = 1;
4945      }
4946    }
4947
4948    return results;
4949  }
4950
4951  function filterResources(clauses, removeDuplicates, map) {
4952    return function(resource) {
4953      var resourceIsAllowed = true;
4954
4955      // References must be defined.
4956      if (resource === undefined) {
4957        return;
4958      }
4959
4960      // Get canonical (localized) version of resource if possible.
4961      resource = METADATA.byUrl[resource.baseUrl] || METADATA.byUrl[resource.url] || resource;
4962
4963      // Filter out resources already used
4964      if (removeDuplicates) {
4965        resourceIsAllowed = !map[resource.index];
4966      }
4967
4968      // Must fulfill all criteria
4969      if (clauses.length > 0) {
4970        resourceIsAllowed = resourceIsAllowed && doesResourceMatchClauses(resource, clauses);
4971      }
4972
4973      // Mark resource as used.
4974      if (resourceIsAllowed) {
4975        map[resource.index] = 1;
4976      }
4977
4978      return resourceIsAllowed && resource;
4979    };
4980  }
4981
4982  function filterEmpty(resource) {
4983    return resource;
4984  }
4985
4986  function sortResultsByKey(key) {
4987    var desc = key.charAt(0) === '-';
4988
4989    if (desc) {
4990      key = key.substring(1);
4991    }
4992
4993    return function(x, y) {
4994      return (desc ? -1 : 1) * (parseInt(x[key], 10) - parseInt(y[key], 10));
4995    };
4996  }
4997
4998  function getResourcesForClause(clause) {
4999    switch (clause.attr) {
5000      case 'type':
5001        return METADATA.byType[clause.value];
5002      case 'tag':
5003        return METADATA.byTag[clause.value];
5004      case 'collection':
5005        var resources = METADATA.collections[clause.value] || {};
5006        return getResourcesByUrlCollection(resources.resources);
5007      case 'history':
5008        return getResourcesByUrlCollection($.dacGetVisitedUrls(clause.value));
5009      case 'section':
5010        return getResourcesByUrlCollection([clause.value].sections);
5011      default:
5012        return [];
5013    }
5014  }
5015
5016  function getResourcesByUrlCollection(resources) {
5017    return (resources || []).map(function(url) {
5018      return METADATA.byUrl[url];
5019    });
5020  }
5021
5022  function removePageLevelDuplicates(resource) {
5023    return resource && !pageMap[resource.index];
5024  }
5025
5026  function doesResourceMatchClauses(resource, clauses) {
5027    for (var i = 0; i < clauses.length; i++) {
5028      var map;
5029      switch (clauses[i].attr) {
5030        case 'type':
5031          map = METADATA.hasType[clauses[i].value];
5032          break;
5033        case 'tag':
5034          map = METADATA.hasTag[clauses[i].value];
5035          break;
5036      }
5037
5038      if (!map || (!!clauses[i].negative ? map[resource.index] : !map[resource.index])) {
5039        return clauses[i].negative;
5040      }
5041    }
5042
5043    return true;
5044  }
5045
5046  function parseResourceQuery(query) {
5047    // Parse query into array of expressions (expression e.g. 'tag:foo + type:video')
5048    var expressions = [];
5049    var expressionStrs = query.split(',') || [];
5050    for (var i = 0; i < expressionStrs.length; i++) {
5051      var expr = expressionStrs[i] || '';
5052
5053      // Break expression into clauses (clause e.g. 'tag:foo')
5054      var clauses = [];
5055      var clauseStrs = expr.split(/(?=[\+\-])/);
5056      for (var j = 0; j < clauseStrs.length; j++) {
5057        var clauseStr = clauseStrs[j] || '';
5058
5059        // Get attribute and value from clause (e.g. attribute='tag', value='foo')
5060        var parts = clauseStr.split(':');
5061        var clause = {};
5062
5063        clause.attr = parts[0].replace(/^\s+|\s+$/g, '');
5064        if (clause.attr) {
5065          if (clause.attr.charAt(0) === '+') {
5066            clause.attr = clause.attr.substring(1);
5067          } else if (clause.attr.charAt(0) === '-') {
5068            clause.negative = true;
5069            clause.attr = clause.attr.substring(1);
5070          }
5071        }
5072
5073        if (parts.length > 1) {
5074          clause.value = parts[1].replace(/^\s+|\s+$/g, '');
5075        }
5076
5077        clauses.push(clause);
5078      }
5079
5080      if (!clauses.length) {
5081        continue;
5082      }
5083
5084      expressions.push(clauses);
5085    }
5086
5087    return expressions;
5088  }
5089
5090  return buildResourceList;
5091})(jQuery);
5092
5093/* global METADATA, getLangPref */
5094
5095window.metadata.search = (function() {
5096  'use strict';
5097
5098  var currentLang = getLangPref();
5099
5100  function search(query) {
5101    window.metadata.prepare();
5102    return {
5103      android: findDocsMatches(query, METADATA.androidReference),
5104      docs: findDocsMatches(query, METADATA.googleReference),
5105      resources: findResourceMatches(query)
5106    };
5107  }
5108
5109  function findDocsMatches(query, data) {
5110    var results = [];
5111
5112    for (var i = 0; i < data.length; i++) {
5113      var s = data[i];
5114      if (query.length !== 0 && s.label.toLowerCase().indexOf(query.toLowerCase()) !== -1) {
5115        results.push(s);
5116      }
5117    }
5118
5119    rankAutocompleteApiResults(query, results);
5120
5121    return results;
5122  }
5123
5124  function findResourceMatches(query) {
5125    var results = [];
5126
5127    // Search for matching JD docs
5128    if (query.length >= 2) {
5129      /* In some langs, spaces may be optional between certain non-Ascii word-glyphs. For
5130       * those langs, only match query at word boundaries if query includes Ascii chars only.
5131       */
5132      var NO_BOUNDARY_LANGUAGES = ['ja','ko','vi','zh-cn','zh-tw'];
5133      var isAsciiOnly = /^[\u0000-\u007f]*$/.test(query);
5134      var noBoundaries = (NO_BOUNDARY_LANGUAGES.indexOf(window.getLangPref()) !== -1);
5135      var exprBoundary = (!isAsciiOnly && noBoundaries) ? '' : '(?:^|\\s)';
5136      var queryRegex = new RegExp(exprBoundary + query.toLowerCase(), 'g');
5137
5138      var all = METADATA.all;
5139      for (var i = 0; i < all.length; i++) {
5140        // current search comparison, with counters for tag and title,
5141        // used later to improve ranking
5142        var s = all[i];
5143        s.matched_tag = 0;
5144        s.matched_title = 0;
5145        var matched = false;
5146
5147        // Check if query matches any tags; work backwards toward 1 to assist ranking
5148        if (s.keywords) {
5149          for (var j = s.keywords.length - 1; j >= 0; j--) {
5150            // it matches a tag
5151            if (s.keywords[j].toLowerCase().match(queryRegex)) {
5152              matched = true;
5153              s.matched_tag = j + 1; // add 1 to index position
5154            }
5155          }
5156        }
5157
5158        // Check if query matches doc title
5159        if (s.title.toLowerCase().match(queryRegex)) {
5160          matched = true;
5161          s.matched_title = 1;
5162        }
5163
5164        // Remember the doc if it matches either
5165        if (matched) {
5166          results.push(s);
5167        }
5168      }
5169
5170      // Improve the current results
5171      results = lookupBetterResult(results);
5172
5173      // Rank/sort all the matched pages
5174      rankAutocompleteDocResults(results);
5175
5176      return results;
5177    }
5178  }
5179
5180  // Replaces a match with another resource by url, if it exists.
5181  function lookupReplacementByUrl(match, url) {
5182    var replacement = METADATA.byUrl[url];
5183
5184    // Replacement resource does not exists.
5185    if (!replacement) { return; }
5186
5187    replacement.matched_title = Math.max(replacement.matched_title, match.matched_title);
5188    replacement.matched_tag = Math.max(replacement.matched_tag, match.matched_tag);
5189
5190    return replacement;
5191  }
5192
5193  // Find the localized version of a page if it exists.
5194  function lookupLocalizedVersion(match) {
5195    return METADATA.byUrl[match.baseUrl] || METADATA.byUrl[match.url];
5196  }
5197
5198  // Find the main page for a tutorial when matching a subpage.
5199  function lookupTutorialIndex(match) {
5200    // Guard for non index tutorial pages.
5201    if (match.type !== 'training' || match.url.indexOf('index.html') >= 0) { return; }
5202
5203    var indexUrl = match.url.replace(/[^\/]+$/, 'index.html');
5204    return lookupReplacementByUrl(match, indexUrl);
5205  }
5206
5207  // Find related results which are a better match for the user.
5208  function lookupBetterResult(matches) {
5209    var newMatches = [];
5210
5211    matches = matches.filter(function(match) {
5212      var newMatch = match;
5213      newMatch = lookupTutorialIndex(newMatch) || newMatch;
5214      newMatch = lookupLocalizedVersion(newMatch) || newMatch;
5215
5216      if (newMatch !== match) {
5217        newMatches.push(newMatch);
5218      }
5219
5220      return newMatch === match;
5221    });
5222
5223    return toUnique(newMatches.concat(matches));
5224  }
5225
5226  /* Order the jd doc result list based on match quality */
5227  function rankAutocompleteDocResults(matches) {
5228    if (!matches || !matches.length) {
5229      return;
5230    }
5231
5232    var _resultScoreFn = function(match) {
5233      var score = 1.0;
5234
5235      // if the query matched a tag
5236      if (match.matched_tag > 0) {
5237        // multiply score by factor relative to position in tags list (max of 3)
5238        score *= 3 / match.matched_tag;
5239
5240        // if it also matched the title
5241        if (match.matched_title > 0) {
5242          score *= 2;
5243        }
5244      } else if (match.matched_title > 0) {
5245        score *= 3;
5246      }
5247
5248      if (match.lang === currentLang) {
5249        score *= 5;
5250      }
5251
5252      return score;
5253    };
5254
5255    for (var i = 0; i < matches.length; i++) {
5256      matches[i].__resultScore = _resultScoreFn(matches[i]);
5257    }
5258
5259    matches.sort(function(a, b) {
5260      var n = b.__resultScore - a.__resultScore;
5261
5262      if (n === 0) {
5263        // lexicographical sort if scores are the same
5264        n = (a.title < b.title) ? -1 : 1;
5265      }
5266
5267      return n;
5268    });
5269  }
5270
5271  /* Order the result list based on match quality */
5272  function rankAutocompleteApiResults(query, matches) {
5273    query = query || '';
5274    if (!matches || !matches.length) {
5275      return;
5276    }
5277
5278    // helper function that gets the last occurence index of the given regex
5279    // in the given string, or -1 if not found
5280    var _lastSearch = function(s, re) {
5281      if (s === '') {
5282        return -1;
5283      }
5284      var l = -1;
5285      var tmp;
5286      while ((tmp = s.search(re)) >= 0) {
5287        if (l < 0) {
5288          l = 0;
5289        }
5290        l += tmp;
5291        s = s.substr(tmp + 1);
5292      }
5293      return l;
5294    };
5295
5296    // helper function that counts the occurrences of a given character in
5297    // a given string
5298    var _countChar = function(s, c) {
5299      var n = 0;
5300      for (var i = 0; i < s.length; i++) {
5301        if (s.charAt(i) === c) {
5302          ++n;
5303        }
5304      }
5305      return n;
5306    };
5307
5308    var queryLower = query.toLowerCase();
5309    var queryAlnum = (queryLower.match(/\w+/) || [''])[0];
5310    var partPrefixAlnumRE = new RegExp('\\b' + queryAlnum);
5311    var partExactAlnumRE = new RegExp('\\b' + queryAlnum + '\\b');
5312
5313    var _resultScoreFn = function(result) {
5314      // scores are calculated based on exact and prefix matches,
5315      // and then number of path separators (dots) from the last
5316      // match (i.e. favoring classes and deep package names)
5317      var score = 1.0;
5318      var labelLower = result.label.toLowerCase();
5319      var t;
5320      var partsAfter;
5321      t = _lastSearch(labelLower, partExactAlnumRE);
5322      if (t >= 0) {
5323        // exact part match
5324        partsAfter = _countChar(labelLower.substr(t + 1), '.');
5325        score *= 200 / (partsAfter + 1);
5326      } else {
5327        t = _lastSearch(labelLower, partPrefixAlnumRE);
5328        if (t >= 0) {
5329          // part prefix match
5330          partsAfter = _countChar(labelLower.substr(t + 1), '.');
5331          score *= 20 / (partsAfter + 1);
5332        }
5333      }
5334
5335      return score;
5336    };
5337
5338    for (var i = 0; i < matches.length; i++) {
5339      // if the API is deprecated, default score is 0; otherwise, perform scoring
5340      if (matches[i].deprecated === 'true') {
5341        matches[i].__resultScore = 0;
5342      } else {
5343        matches[i].__resultScore = _resultScoreFn(matches[i]);
5344      }
5345    }
5346
5347    matches.sort(function(a, b) {
5348      var n = b.__resultScore - a.__resultScore;
5349
5350      if (n === 0) {
5351        // lexicographical sort if scores are the same
5352        n = (a.label < b.label) ? -1 : 1;
5353      }
5354
5355      return n;
5356    });
5357  }
5358
5359  // Destructive but fast toUnique.
5360  // http://stackoverflow.com/a/25082874
5361  function toUnique(array) {
5362    var c;
5363    var b = array.length || 1;
5364
5365    while (c = --b) {
5366      while (c--) {
5367        if (array[b] === array[c]) {
5368          array.splice(c, 1);
5369        }
5370      }
5371    }
5372    return array;
5373  }
5374
5375  return search;
5376})();
5377
5378(function($) {
5379  'use strict';
5380
5381  /**
5382   * Smoothly scroll to location on current page.
5383   * @param el
5384   * @param options
5385   * @constructor
5386   */
5387  function ScrollButton(el, options) {
5388    this.el = $(el);
5389    this.target = $(this.el.attr('href'));
5390    this.options = $.extend({}, ScrollButton.DEFAULTS_, options);
5391
5392    if (typeof this.options.offset === 'string') {
5393      this.options.offset = $(this.options.offset).height();
5394    }
5395
5396    this.el.on('click', this.clickHandler_.bind(this));
5397  }
5398
5399  /**
5400   * Default options
5401   * @type {{duration: number, easing: string, offset: number, scrollContainer: string}}
5402   * @private
5403   */
5404  ScrollButton.DEFAULTS_ = {
5405    duration: 300,
5406    easing: 'swing',
5407    offset: '.dac-header',
5408    scrollContainer: 'html, body'
5409  };
5410
5411  /**
5412   * Scroll logic
5413   * @param event
5414   * @private
5415   */
5416  ScrollButton.prototype.clickHandler_ = function(event) {
5417    if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
5418      return;
5419    }
5420
5421    event.preventDefault();
5422
5423    var position = this.getTargetPosition();
5424    $(this.options.scrollContainer).animate({
5425      scrollTop: position - this.options.offset
5426    }, this.options);
5427  };
5428
5429  ScrollButton.prototype.getTargetPosition = function() {
5430    if (this.options.scrollContainer === ScrollButton.DEFAULTS_.scrollContainer) {
5431      return this.target.offset().top;
5432    }
5433    var scrollContainer = $(this.options.scrollContainer)[0];
5434    var currentEl = this.target[0];
5435    var pos = 0;
5436    while (currentEl !== scrollContainer && currentEl !== null) {
5437      pos += currentEl.offsetTop;
5438      currentEl = currentEl.offsetParent;
5439    }
5440    return pos;
5441  };
5442
5443  /**
5444   * jQuery plugin
5445   * @param  {object} options - Override default options.
5446   */
5447  $.fn.dacScrollButton = function(options) {
5448    return this.each(function() {
5449      new ScrollButton(this, options);
5450    });
5451  };
5452
5453  /**
5454   * Data Attribute API
5455   */
5456  $(document).on('ready.aranja', function() {
5457    $('[data-scroll-button]').each(function() {
5458      $(this).dacScrollButton($(this).data());
5459    });
5460  });
5461})(jQuery);
5462
5463/* global getLangPref */
5464(function($) {
5465  var LANG;
5466
5467  function getSearchLang() {
5468    if (!LANG) {
5469      LANG = getLangPref();
5470
5471      // Fix zh-cn to be zh-CN.
5472      LANG = LANG.replace(/-\w+/, function(m) { return m.toUpperCase(); });
5473    }
5474    return LANG;
5475  }
5476
5477  function customSearch(query, start) {
5478    var searchParams = {
5479      // current cse instance:
5480      //cx: '001482626316274216503:zu90b7s047u',
5481      // new cse instance:
5482      cx: '000521750095050289010:zpcpi1ea4s8',
5483      key: 'AIzaSyCFhbGnjW06dYwvRCU8h_zjdpS4PYYbEe8',
5484      q: query,
5485      start: start || 1,
5486      num: 9,
5487      hl: getSearchLang(),
5488      fields: 'queries,items(pagemap,link,title,htmlSnippet,formattedUrl)'
5489    };
5490
5491    return $.get('https://content.googleapis.com/customsearch/v1?' +  $.param(searchParams));
5492  }
5493
5494  function renderResults(el, results, searchAppliance) {
5495    var referenceResults = searchAppliance.getReferenceResults();
5496    if (!results.items) {
5497      el.append($('<div>').text('No results'));
5498      return;
5499    }
5500
5501    for (var i = 0; i < results.items.length; i++) {
5502      var item = results.items[i];
5503      var isDuplicate = false;
5504      $(referenceResults.android).each(function(index, result) {
5505        if (item.link.indexOf(result.link) > -1) {
5506          isDuplicate = true;
5507          return false;
5508        }
5509      });
5510
5511      if (!isDuplicate) {
5512        var hasImage = item.pagemap && item.pagemap.cse_thumbnail;
5513        var sectionMatch = item.link.match(/developer\.android\.com\/(\w*)/);
5514        var section = (sectionMatch && sectionMatch[1]) || 'blog';
5515
5516        var entry = $('<div>').addClass('dac-custom-search-entry cols');
5517
5518        if (hasImage) {
5519          var image = item.pagemap.cse_thumbnail[0];
5520          entry.append($('<div>').addClass('dac-custom-search-image-wrapper')
5521            .append($('<div>').addClass('dac-custom-search-image').css('background-image', 'url(' + image.src + ')')));
5522        }
5523
5524        entry.append($('<div>').addClass('dac-custom-search-text-wrapper')
5525          .append($('<p>').addClass('dac-custom-search-section').text(section))
5526          .append(
5527            $('<a>').text(item.title).attr('href', item.link).wrap('<h2>').parent().addClass('dac-custom-search-title')
5528          )
5529          .append($('<p>').addClass('dac-custom-search-snippet').html(item.htmlSnippet.replace(/<br>/g, '')))
5530          .append($('<a>').addClass('dac-custom-search-link').text(item.formattedUrl).attr('href', item.link)));
5531
5532        el.append(entry);
5533      }
5534    }
5535
5536    if (results.queries.nextPage) {
5537      var loadMoreButton = $('<button id="dac-custom-search-load-more">')
5538        .addClass('dac-custom-search-load-more')
5539        .text('Load more')
5540        .click(function() {
5541          loadMoreResults(el, results, searchAppliance);
5542        });
5543
5544      el.append(loadMoreButton);
5545    }
5546  };
5547
5548  function loadMoreResults(el, results, searchAppliance) {
5549    var query = results.queries.request[0].searchTerms;
5550    var start = results.queries.nextPage[0].startIndex;
5551    var loadMoreButton = el.find('#dac-custom-search-load-more');
5552
5553    loadMoreButton.text('Loading more...');
5554
5555    customSearch(query, start).then(function(results) {
5556      loadMoreButton.remove();
5557      renderResults(el, results, searchAppliance);
5558    });
5559  }
5560
5561  $.fn.customSearch = function(query, searchAppliance) {
5562    var el = $(this);
5563
5564    customSearch(query).then(function(results) {
5565      el.empty();
5566      renderResults(el, results, searchAppliance);
5567    });
5568  };
5569})(jQuery);
5570
5571/* global METADATA */
5572
5573(function($) {
5574  $.fn.dacSearchRenderHero = function(resources, query) {
5575    var el = $(this);
5576    el.empty();
5577
5578    var resource = METADATA.searchHeroCollections[query];
5579
5580    if (resource) {
5581      el.dacHero(resource, true);
5582      el.show();
5583
5584      return true;
5585    } else {
5586      el.hide();
5587    }
5588  };
5589})(jQuery);
5590
5591(function($) {
5592  $.fn.dacSearchRenderReferences = function(results, query) {
5593    var referenceCard = $('.suggest-card.reference');
5594    referenceCard.data('searchreferences.dac', {results: results, query: query});
5595    renderResults(referenceCard, results, query, false);
5596  };
5597
5598  var ROW_COUNT_COLLAPSED = 20;
5599  var ROW_COUNT_EXPANDED = 40;
5600  var ROW_COUNT_GOOGLE_COLLAPSED = 1;
5601  var ROW_COUNT_GOOGLE_EXPANDED = 8;
5602
5603  function onSuggestionClick(e) {
5604    devsite.analytics.trackAnalyticsEvent('event',
5605        'Suggestion Click', 'clicked: ' + $(e.currentTarget).attr('href'),
5606        'query: ' + $('#search_autocomplete').val().toLowerCase());
5607  }
5608
5609  function buildLink(match) {
5610    var link = $('<a>').attr('href', window.toRoot + match.link);
5611
5612    var label = match.label;
5613    var classNameStart = label.match(/[A-Z]/) ? label.search(/[A-Z]/) : label.lastIndexOf('.') + 1;
5614    var newLink = '<span class="namespace">' +
5615      label.substr(0, classNameStart) +
5616      '</span>' +
5617      label.substr(classNameStart, label.length);
5618
5619    link.html(newLink);
5620    return link;
5621  }
5622
5623  function buildSuggestion(match, query) {
5624    var li = $('<li>').addClass('dac-search-results-reference-entry');
5625
5626    var link = buildLink(match);
5627    link.highlightMatches(query);
5628    li.append(link);
5629    return li[0];
5630  }
5631
5632  function buildResults(results, query) {
5633    return results.map(function(match) {
5634      return buildSuggestion(match, query);
5635    });
5636  }
5637
5638  function renderAndroidResults(list, gMatches, query) {
5639    list.empty();
5640
5641    var header = $('<li class="dac-search-results-reference-header">android APIs</li>');
5642    list.append(header);
5643
5644    if (gMatches.length > 0) {
5645      list.removeClass('no-results');
5646
5647      var resources = buildResults(gMatches, query);
5648      list.append(resources);
5649      return true;
5650    } else {
5651      list.append('<li class="dac-search-results-reference-entry-empty">No results</li>');
5652    }
5653  }
5654
5655  function renderGoogleDocsResults(list, gGoogleMatches, query) {
5656    list = $('.suggest-card.reference ul');
5657
5658    if (gGoogleMatches.length > 0) {
5659      list.append('<li class="dac-search-results-reference-header">in Google Services</li>');
5660
5661      var resources = buildResults(gGoogleMatches, query);
5662      list.append(resources);
5663
5664      return true;
5665    }
5666  }
5667
5668  function renderResults(referenceCard, results, query, expanded) {
5669    var list = referenceCard.find('ul');
5670    list.toggleClass('is-expanded', !!expanded);
5671
5672    // Figure out how many results we can show in our fixed size box.
5673    var total = expanded ? ROW_COUNT_EXPANDED : ROW_COUNT_COLLAPSED;
5674    var googleCount = expanded ? ROW_COUNT_GOOGLE_EXPANDED : ROW_COUNT_GOOGLE_COLLAPSED;
5675    googleCount = Math.max(googleCount, total - results.android.length);
5676    googleCount = Math.min(googleCount, results.docs.length);
5677
5678    if (googleCount > 0) {
5679      // If there are google results, reserve space for its header.
5680      googleCount++;
5681    }
5682
5683    var androidCount = Math.max(0, total - googleCount);
5684    if (androidCount === 0) {
5685      // Reserve space for "No reference results"
5686      googleCount--;
5687    }
5688
5689    renderAndroidResults(list, results.android.slice(0, androidCount), query);
5690    renderGoogleDocsResults(list, results.docs.slice(0, googleCount - 1), query);
5691
5692    var totalResults = results.android.length + results.docs.length;
5693    if (totalResults === 0) {
5694      list.addClass('no-results');
5695    }
5696
5697    // Tweak see more logic to account for references.
5698    var hasMore = totalResults > ROW_COUNT_COLLAPSED && !util.matchesMedia('mobile');
5699    if (hasMore) {
5700      // We can't actually show all matches, only as many as the expanded list
5701      // will fit, so we actually lie if the total results count is more
5702      var moreCount = Math.min(totalResults, ROW_COUNT_EXPANDED + ROW_COUNT_GOOGLE_EXPANDED);
5703      var $moreLink = $('<li class="dac-search-results-reference-entry-empty " data-toggle="show-more">see more matches</li>');
5704      list.append($moreLink.on('click', onToggleMore));
5705    }
5706    var searchEl = $('#search-resources');
5707    searchEl.toggleClass('dac-has-more', searchEl.hasClass('dac-has-more') || (hasMore && !expanded));
5708    searchEl.toggleClass('dac-has-less', searchEl.hasClass('dac-has-less') || (hasMore && expanded));
5709  }
5710
5711  function onToggleMore(e) {
5712    var link = $(e.currentTarget);
5713    var referenceCard = $('.suggest-card.reference');
5714    var data = referenceCard.data('searchreferences.dac');
5715
5716    if (util.matchesMedia('mobile')) { return; }
5717
5718    renderResults(referenceCard, data.results, data.query, link.data('toggle') === 'show-more');
5719  }
5720
5721  $(document).on('click', '.dac-search-results-resources [data-toggle="show-more"]', onToggleMore);
5722  $(document).on('click', '.dac-search-results-resources [data-toggle="show-less"]', onToggleMore);
5723  $(document).on('click', '.suggest-card.reference a', onSuggestionClick);
5724})(jQuery);
5725
5726(function($) {
5727  function highlightPage(query, page) {
5728    page.find('.title').highlightMatches(query);
5729  }
5730
5731  $.fn.dacSearchRenderResources = function(gDocsMatches, query) {
5732    this.resourceWidget(gDocsMatches, {
5733      itemsPerPage: 18,
5734      initialResults: 6,
5735      cardSizes: ['6x2'],
5736      onRenderPage: highlightPage.bind(null, query)
5737    });
5738
5739    return this;
5740  };
5741})(jQuery);
5742
5743/*global metadata */
5744
5745(function($, metadata) {
5746  'use strict';
5747
5748  function Search() {
5749    this.body = $('body');
5750    this.lastQuery = null;
5751    this.searchResults = $('#search-results');
5752    this.searchClose = $('[data-search-close]');
5753    this.searchClear = $('[data-search-clear]');
5754    this.searchInput = $('#search_autocomplete');
5755    this.searchResultsContent = $('#dac-search-results-content');
5756    this.searchResultsFor = $('#search-results-for');
5757    this.searchResultsHistory = $('#dac-search-results-history');
5758    this.searchResultsResources = $('#search-resources');
5759    this.searchResultsHero = $('#dac-search-results-hero');
5760    this.searchResultsReference = $('#dac-search-results-reference');
5761    this.searchHeader = $('[data-search]').data('search-input.dac');
5762    this.pageNav = $('a[name=navigation]');
5763    this.currQueryReferenceResults = {};
5764    this.isOpen = false;
5765  }
5766
5767  Search.prototype.init = function() {
5768    this.searchHistory = window.dacStore('search-history');
5769
5770    this.searchInput.focus(this.onSearchChanged.bind(this));
5771    this.searchInput.keypress(this.handleKeyboardShortcut.bind(this));
5772    this.pageNav.keyup(this.handleTabbedToNav.bind(this));
5773    this.searchResults.keyup(this.handleKeyboardShortcut.bind(this));
5774    this.searchInput.on('input', this.onSearchChanged.bind(this));
5775    this.searchClear.click(this.clear.bind(this));
5776    this.searchClose.click(this.close.bind(this));
5777
5778    this.customSearch = $.fn.debounce(function(query) {
5779      $('#dac-custom-search-results').customSearch(query, this);
5780    }.bind(this), 1000);
5781    // Start search shortcut (/)
5782    $('body').keyup(function(event) {
5783      if (event.which === 191 && $(event.target).is(':not(:input)')) {
5784        this.searchInput.focus();
5785      }
5786    }.bind(this));
5787
5788    $(window).on('popstate', this.onPopState.bind(this));
5789    $(window).hashchange(this.onHashChange.bind(this));
5790    this.onHashChange();
5791  };
5792
5793  Search.prototype.checkRedirectToIndex = function() {
5794    var query = this.getUrlQuery();
5795    var target = window.getLangTarget();
5796    var prefix = (target !== 'en') ? '/intl/' + target : '';
5797    var pathname = location.pathname.slice(prefix.length);
5798    if (query != null && pathname !== '/index.html') {
5799      location.href = prefix + '/index.html' + location.hash;
5800      return true;
5801    }
5802  };
5803
5804  Search.prototype.handleKeyboardShortcut = function(event) {
5805    // Close (esc)
5806    if (event.which === 27) {
5807      this.searchClose.trigger('click');
5808      event.preventDefault();
5809    }
5810
5811    // Previous result (up arrow)
5812    if (event.which === 38) {
5813      this.previousResult();
5814      event.preventDefault();
5815    }
5816
5817    // Next result (down arrow)
5818    if (event.which === 40) {
5819      this.nextResult();
5820      event.preventDefault();
5821    }
5822
5823    // Navigate to result (enter)
5824    if (event.which === 13) {
5825      this.navigateToResult();
5826      event.preventDefault();
5827    }
5828  };
5829
5830  Search.prototype.handleTabbedToNav = function(event) {
5831    if (this.isOpen) {
5832      this.searchClose.trigger('click');
5833    }
5834  }
5835
5836  Search.prototype.goToResult = function(relativeIndex) {
5837    var links = this.searchResults.find('a').filter(':visible');
5838    var selectedLink = this.searchResults.find('.dac-selected');
5839
5840    if (selectedLink.length) {
5841      var found = $.inArray(selectedLink[0], links);
5842
5843      selectedLink.removeClass('dac-selected');
5844      links.eq(found + relativeIndex).addClass('dac-selected');
5845      return true;
5846    } else {
5847      if (relativeIndex > 0) {
5848        links.first().addClass('dac-selected');
5849      }
5850    }
5851  };
5852
5853  Search.prototype.previousResult = function() {
5854    this.goToResult(-1);
5855  };
5856
5857  Search.prototype.nextResult = function() {
5858    this.goToResult(1);
5859  };
5860
5861  Search.prototype.navigateToResult = function() {
5862    var query = this.getQuery();
5863    var selectedLink = this.searchResults.find('.dac-selected');
5864
5865    if (selectedLink.length) {
5866      selectedLink[0].click();
5867    } else {
5868      this.searchHistory.push(query);
5869      this.addQueryToUrl(query);
5870
5871      var isMobileOrTablet = typeof window.orientation !== 'undefined';
5872
5873      if (isMobileOrTablet) {
5874        this.searchInput.blur();
5875      }
5876    }
5877  };
5878
5879  Search.prototype.onHashChange = function() {
5880    var query = this.getUrlQuery();
5881    if (query != null && query !== this.getQuery()) {
5882      this.searchInput.val(query);
5883      this.onSearchChanged();
5884    }
5885  };
5886
5887  Search.prototype.clear = function() {
5888    this.searchInput.val('');
5889    window.location.hash = '';
5890    this.onSearchChanged();
5891    this.searchInput.focus();
5892  };
5893
5894  Search.prototype.close = function() {
5895    this.removeQueryFromUrl();
5896    this.searchInput.blur();
5897    this.hideOverlay();
5898    this.pageNav.focus();
5899    this.isOpen = false;
5900  };
5901
5902  Search.prototype.getUrlQuery = function() {
5903    var queryMatch = location.hash.match(/q=(.*)&?/);
5904    return queryMatch && queryMatch[1] && decodeURI(queryMatch[1]);
5905  };
5906
5907  Search.prototype.getQuery = function() {
5908    return this.searchInput.val().replace(/(^ +)|( +$)/g, '');
5909  };
5910
5911  Search.prototype.getReferenceResults = function() {
5912    return this.currQueryReferenceResults;
5913  };
5914
5915  Search.prototype.onSearchChanged = function() {
5916    var query = this.getQuery();
5917
5918    this.showOverlay();
5919    this.render(query);
5920  };
5921
5922  Search.prototype.render = function(query) {
5923    if (this.lastQuery === query) { return; }
5924
5925    if (query.length < 2) {
5926      query = '';
5927    }
5928
5929    this.lastQuery = query;
5930    this.searchResultsFor.text(query);
5931
5932    // CSE results lag behind the metadata/reference results. We need to empty
5933    // the CSE results and add 'Loading' text so user's aren't looking at two
5934    // different sets of search results at one time.
5935    var $loadingEl =
5936        $('<div class="loadingCustomSearchResults">Loading Results...</div>');
5937    $('#dac-custom-search-results').empty().prepend($loadingEl);
5938
5939    this.customSearch(query);
5940    var metadataResults = metadata.search(query);
5941    this.searchResultsResources.dacSearchRenderResources(metadataResults.resources, query);
5942    this.searchResultsReference.dacSearchRenderReferences(metadataResults, query);
5943    this.currQueryReferenceResults = metadataResults;
5944    var hasHero = this.searchResultsHero.dacSearchRenderHero(metadataResults.resources, query);
5945    var hasQuery = !!query;
5946
5947    this.searchResultsReference.toggle(!hasHero);
5948    this.searchResultsContent.toggle(hasQuery);
5949    this.searchResultsHistory.toggle(!hasQuery);
5950    this.addQueryToUrl(query);
5951    this.pushState();
5952  };
5953
5954  Search.prototype.addQueryToUrl = function(query) {
5955    var hash = 'q=' + encodeURI(query);
5956
5957    if (query) {
5958      if (window.history.replaceState) {
5959        window.history.replaceState(null, '', '#' + hash);
5960      } else {
5961        window.location.hash = hash;
5962      }
5963    }
5964  };
5965
5966  Search.prototype.onPopState = function() {
5967    if (!this.getUrlQuery()) {
5968      this.hideOverlay();
5969      this.searchHeader.unsetActiveState();
5970    }
5971  };
5972
5973  Search.prototype.removeQueryFromUrl = function() {
5974    window.location.hash = '';
5975  };
5976
5977  Search.prototype.pushState = function() {
5978    if (window.history.pushState && !this.lastQuery.length) {
5979      window.history.pushState(null, '');
5980    }
5981  };
5982
5983  Search.prototype.showOverlay = function() {
5984    this.isOpen = true;
5985    this.body.addClass('dac-modal-open dac-search-open');
5986  };
5987
5988  Search.prototype.hideOverlay = function() {
5989    this.body.removeClass('dac-modal-open dac-search-open');
5990  };
5991
5992  $(document).on('ready.aranja', function() {
5993    var search = new Search();
5994    search.init();
5995  });
5996})(jQuery, metadata);
5997
5998window.dacStore = (function(window) {
5999  /**
6000   * Creates a new persistent store.
6001   * If localStorage is unavailable, the items are stored in memory.
6002   *
6003   * @constructor
6004   * @param {string} name    The name of the store
6005   * @param {number} maxSize The maximum number of items the store can hold.
6006   */
6007  var Store = function(name, maxSize) {
6008    var content = [];
6009
6010    var hasLocalStorage = !!window.localStorage;
6011
6012    if (hasLocalStorage) {
6013      try {
6014        content = JSON.parse(window.localStorage.getItem(name) || []);
6015      } catch (e) {
6016        // Store contains invalid data
6017        window.localStorage.removeItem(name);
6018      }
6019    }
6020
6021    function push(item) {
6022      if (content[0] === item) {
6023        return;
6024      }
6025
6026      content.unshift(item);
6027
6028      if (maxSize) {
6029        content.splice(maxSize, content.length);
6030      }
6031
6032      if (hasLocalStorage) {
6033        window.localStorage.setItem(name, JSON.stringify(content));
6034      }
6035    }
6036
6037    function all() {
6038      // Return a copy
6039      return content.slice();
6040    }
6041
6042    return {
6043      push: push,
6044      all: all
6045    };
6046  };
6047
6048  var stores = {
6049    'search-history': new Store('search-history', 3)
6050  };
6051
6052  /**
6053   * Get a named persistent store.
6054   * @param  {string} name
6055   * @return {Store}
6056   */
6057  return function getStore(name) {
6058    return stores[name];
6059  };
6060})(window);
6061
6062(function($) {
6063  'use strict';
6064
6065  /**
6066   * A component that swaps two dynamic height views with an animation.
6067   * Listens for the following events:
6068   * * swap-content: triggers SwapContent.swap_()
6069   * * swap-reset: triggers SwapContent.reset()
6070   * @param el
6071   * @param options
6072   * @constructor
6073   */
6074  function SwapContent(el, options) {
6075    this.el = $(el);
6076    this.options = $.extend({}, SwapContent.DEFAULTS_, options);
6077    this.options.dynamic = this.options.dynamic === 'true';
6078    this.containers = this.el.find(this.options.container);
6079    this.initiallyActive = this.containers.children('.' + this.options.activeClass).eq(0);
6080    this.el.on('swap-content', this.swap.bind(this));
6081    this.el.on('swap-reset', this.reset.bind(this));
6082    this.el.find(this.options.swapButton).on('click keypress', function(e) {
6083      if (e.type == 'keypress' && e.which == 13 || e.type == 'click') {
6084        this.swap();
6085      }
6086    }.bind(this));
6087  }
6088
6089  /**
6090   * SwapContent's default settings.
6091   * @type {{activeClass: string, container: string, transitionSpeed: number}}
6092   * @private
6093   */
6094  SwapContent.DEFAULTS_ = {
6095    activeClass: 'dac-active',
6096    container: '[data-swap-container]',
6097    dynamic: 'true',
6098    swapButton: '[data-swap-button]',
6099    transitionSpeed: 500
6100  };
6101
6102  /**
6103   * Returns container's visible height.
6104   * @param container
6105   * @returns {number}
6106   */
6107  SwapContent.prototype.currentHeight = function(container) {
6108    return container.children('.' + this.options.activeClass).outerHeight();
6109  };
6110
6111  /**
6112   * Reset to show initial content
6113   */
6114  SwapContent.prototype.reset = function() {
6115    if (!this.initiallyActive.hasClass(this.initiallyActive)) {
6116      this.containers.children().toggleClass(this.options.activeClass);
6117    }
6118  };
6119
6120  /**
6121   * Complete the swap.
6122   */
6123  SwapContent.prototype.complete = function() {
6124    this.containers.height('auto');
6125    this.containers.trigger('swap-complete');
6126  };
6127
6128  /**
6129   * Perform the swap of content.
6130   */
6131  SwapContent.prototype.swap = function() {
6132    this.containers.each(function(index, container) {
6133      container = $(container);
6134
6135      if (!this.options.dynamic) {
6136        container.children().toggleClass(this.options.activeClass);
6137        this.complete.bind(this);
6138        $('.' + this.options.activeClass).focus();
6139        return;
6140      }
6141
6142      container.height(this.currentHeight(container)).children().toggleClass(this.options.activeClass);
6143      container.animate({height: this.currentHeight(container)}, this.options.transitionSpeed,
6144        this.complete.bind(this));
6145    }.bind(this));
6146  };
6147
6148  /**
6149   * jQuery plugin
6150   * @param  {object} options - Override default options.
6151   */
6152  $.fn.dacSwapContent = function(options) {
6153    return this.each(function() {
6154      new SwapContent(this, options);
6155    });
6156  };
6157
6158  /**
6159   * Data Attribute API
6160   */
6161  $(document).on('ready.aranja', function() {
6162    $('[data-swap]').each(function() {
6163      $(this).dacSwapContent($(this).data());
6164    });
6165  });
6166})(jQuery);
6167
6168/* Tabs */
6169(function($) {
6170  'use strict';
6171
6172  /**
6173   * @param {HTMLElement} el - The DOM element.
6174   * @param {Object} options
6175   * @constructor
6176   */
6177  function Tabs(el, options) {
6178    this.el = $(el);
6179    this.options = $.extend({}, Tabs.DEFAULTS_, options);
6180    this.init();
6181  }
6182
6183  Tabs.DEFAULTS_ = {
6184    activeClass: 'dac-active',
6185    viewDataAttr: 'tab-view',
6186    itemDataAttr: 'tab-item'
6187  };
6188
6189  Tabs.prototype.init = function() {
6190    var itemDataAttribute = '[data-' + this.options.itemDataAttr + ']';
6191    this.tabEl_ = this.el.find(itemDataAttribute);
6192    this.tabViewEl_ = this.el.find('[data-' + this.options.viewDataAttr + ']');
6193    this.el.on('click.dac-tabs', itemDataAttribute, this.changeTabs.bind(this));
6194  };
6195
6196  Tabs.prototype.changeTabs = function(event) {
6197    var current = $(event.currentTarget);
6198    var index = current.index();
6199
6200    if (current.hasClass(this.options.activeClass)) {
6201      current.add(this.tabViewEl_.eq(index)).removeClass(this.options.activeClass);
6202    } else {
6203      this.tabEl_.add(this.tabViewEl_).removeClass(this.options.activeClass);
6204      current.add(this.tabViewEl_.eq(index)).addClass(this.options.activeClass);
6205    }
6206  };
6207
6208  /**
6209   * jQuery plugin
6210   */
6211  $.fn.dacTabs = function() {
6212    return this.each(function() {
6213      var el = $(this);
6214      new Tabs(el, el.data());
6215    });
6216  };
6217
6218  /**
6219   * Data Attribute API
6220   */
6221  $(function() {
6222    $('[data-tabs]').dacTabs();
6223  });
6224})(jQuery);
6225
6226/* Toast Component */
6227(function($) {
6228  'use strict';
6229  /**
6230   * @constant
6231   * @type {String}
6232   */
6233  var LOCAL_STORAGE_KEY = 'toast-closed-index';
6234
6235  /**
6236   * Dictionary from local storage.
6237   */
6238  var toastDictionary = localStorage.getItem(LOCAL_STORAGE_KEY);
6239  toastDictionary = toastDictionary ? JSON.parse(toastDictionary) : {};
6240
6241  /**
6242   * Variable used for caching the body.
6243   */
6244  var bodyCached;
6245
6246  /**
6247   * @param {HTMLElement} el - The DOM element.
6248   * @param {Object} options
6249   * @constructor
6250   */
6251  function Toast(el, options) {
6252    this.el = $(el);
6253    this.options = $.extend({}, Toast.DEFAULTS_, options);
6254    this.init();
6255  }
6256
6257  Toast.DEFAULTS_ = {
6258    closeBtnClass: 'dac-toast-close-btn',
6259    closeDuration: 200,
6260    visibleClass: 'dac-visible',
6261    wrapClass: 'dac-toast-wrap'
6262  };
6263
6264  /**
6265   * Generate a close button.
6266   * @returns {*|HTMLElement}
6267   */
6268  Toast.prototype.closeBtn = function() {
6269    this.closeBtnEl = this.closeBtnEl || $('<button class="' + this.options.closeBtnClass + '">' +
6270      '<span class="dac-button dac-raised dac-primary">OK</span>' +
6271    '</button>');
6272    return this.closeBtnEl;
6273  };
6274
6275  /**
6276   * Initialize a new toast element
6277   */
6278  Toast.prototype.init = function() {
6279    this.hash = this.el.text().replace(/[\s\n\t]/g, '').split('').slice(0, 128).join('');
6280
6281    if (toastDictionary[this.hash]) {
6282      return;
6283    }
6284
6285    this.closeBtn().on('click', this.onClickHandler.bind(this));
6286    this.el.find('.' + this.options.wrapClass).append(this.closeBtn());
6287    this.el.addClass(this.options.visibleClass);
6288    this.dynamicPadding(this.el.outerHeight());
6289  };
6290
6291  /**
6292   * Add padding to make sure all page is visible.
6293   */
6294  Toast.prototype.dynamicPadding = function(val) {
6295    var currentPadding = parseInt(bodyCached.css('padding-bottom') || 0);
6296    bodyCached.css('padding-bottom', val + currentPadding);
6297  };
6298
6299  /**
6300   * Remove a toast from the DOM
6301   */
6302  Toast.prototype.remove = function() {
6303    this.dynamicPadding(-this.el.outerHeight());
6304    this.el.remove();
6305  };
6306
6307  /**
6308   * Handle removal of the toast.
6309   */
6310  Toast.prototype.onClickHandler = function() {
6311    // Only fadeout toasts from top of stack. Others are removed immediately.
6312    var duration = this.el.index() === 0 ? this.options.closeDuration : 0;
6313    this.el.fadeOut(duration, this.remove.bind(this));
6314
6315    // Save closed state.
6316    toastDictionary[this.hash] = 1;
6317    localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(toastDictionary));
6318  };
6319
6320  /**
6321   * jQuery plugin
6322   * @param  {object} options - Override default options.
6323   */
6324  $.fn.dacToast = function() {
6325    return this.each(function() {
6326      var el = $(this);
6327      new Toast(el, el.data());
6328    });
6329  };
6330
6331  /**
6332   * Data Attribute API
6333   */
6334  $(function() {
6335    bodyCached = $('#body-content');
6336    $('[data-toast]').dacToast();
6337  });
6338})(jQuery);
6339
6340(function($) {
6341  function Toggle(el) {
6342    $(el).on('click.dac.togglesection', this.toggle);
6343  }
6344
6345  Toggle.prototype.toggle = function() {
6346    var $this = $(this);
6347
6348    var $parent = getParent($this);
6349    var isExpanded = $parent.hasClass('is-expanded');
6350
6351    transitionMaxHeight($parent.find('.dac-toggle-content'), !isExpanded);
6352    $parent.toggleClass('is-expanded');
6353
6354    return false;
6355  };
6356
6357  function getParent($this) {
6358    var selector = $this.attr('data-target');
6359
6360    if (!selector) {
6361      selector = $this.attr('href');
6362      selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '');
6363    }
6364
6365    var $parent = selector && $(selector);
6366
6367    $parent = $parent && $parent.length ? $parent : $this.closest('.dac-toggle');
6368
6369    return $parent.length ? $parent : $this.parent();
6370  }
6371
6372  /**
6373   * Runs a transition of max-height along with responsive styles which hide or expand the element.
6374   * @param $el
6375   * @param visible
6376   */
6377  function transitionMaxHeight($el, visible) {
6378    var contentHeight = $el.prop('scrollHeight');
6379    var targetHeight = visible ? contentHeight : 0;
6380    var duration = $el.transitionDuration();
6381
6382    // If we're hiding, first set the maxHeight we're transitioning from.
6383    if (!visible) {
6384      $el.css({
6385          transitionDuration: '0s',
6386          maxHeight: contentHeight + 'px'
6387        })
6388        .resolveStyles()
6389        .css('transitionDuration', '');
6390    }
6391
6392    // Transition to new state
6393    $el.css('maxHeight', targetHeight);
6394
6395    // Reset maxHeight to css value after transition.
6396    setTimeout(function() {
6397      $el.css({
6398          transitionDuration: '0s',
6399          maxHeight: ''
6400        })
6401        .resolveStyles()
6402        .css('transitionDuration', '');
6403    }, duration);
6404  }
6405
6406  // Utility to get the transition duration for the element.
6407  $.fn.transitionDuration = function() {
6408    var d = $(this).css('transitionDuration') || '0s';
6409
6410    return +(parseFloat(d) * (/ms/.test(d) ? 1 : 1000)).toFixed(0);
6411  };
6412
6413  // jQuery plugin
6414  $.fn.toggleSection = function(option) {
6415    return this.each(function() {
6416      var $this = $(this);
6417      var data = $this.data('dac.togglesection');
6418      if (!data) {$this.data('dac.togglesection', (data = new Toggle(this)));}
6419      if (typeof option === 'string') {data[option].call($this);}
6420    });
6421  };
6422
6423  // Data api
6424  $(document)
6425    .on('click.toggle', '[data-toggle="section"]', Toggle.prototype.toggle);
6426})(jQuery);
6427
6428(function(window) {
6429  /**
6430   * Media query breakpoints. Should match CSS.
6431   */
6432  var BREAKPOINTS = {
6433    mobile: [0, 719],
6434    tablet: [720, 959],
6435    desktop: [960, 9999]
6436  };
6437
6438  /**
6439   * Fisher-Yates Shuffle (Knuth shuffle).
6440   * @param {Array} input
6441   * @returns {Array} shuffled array.
6442   */
6443  function shuffle(input) {
6444    for (var i = input.length; i >= 0; i--) {
6445      var randomIndex = Math.floor(Math.random() * (i + 1));
6446      var randomItem = input[randomIndex];
6447      input[randomIndex] = input[i];
6448      input[i] = randomItem;
6449    }
6450
6451    return input;
6452  }
6453
6454  /**
6455   * Matches media breakpoints like in CSS.
6456   * @param {string} form of either mobile, tablet or desktop.
6457   */
6458  function matchesMedia(form) {
6459    var breakpoint = BREAKPOINTS[form];
6460    return window.innerWidth >= breakpoint[0] && window.innerWidth <= breakpoint[1];
6461  }
6462
6463  window.util = {
6464    shuffle: shuffle,
6465    matchesMedia: matchesMedia
6466  };
6467})(window);
6468
6469(function($, window) {
6470  'use strict';
6471
6472  var YouTubePlayer = (function() {
6473    var player;
6474
6475    function VideoPlayer() {
6476      this.mPlayerPaused = false;
6477      this.doneSetup = false;
6478    }
6479
6480    VideoPlayer.prototype.setup = function() {
6481      // loads the IFrame Player API code asynchronously.
6482      $.getScript('https://www.youtube.com/iframe_api');
6483
6484      // Add the shadowbox HTML to the body
6485      $('body').prepend(
6486'<div id="video-player" class="Video">' +
6487  '<div id="video-overlay" class="Video-overlay" />' +
6488  '<div class="Video-container">' +
6489    '<div class="Video-frame">' +
6490      '<span class="Video-loading">Loading&hellip;</span>' +
6491      '<div id="youTubePlayer"></div>' +
6492    '</div>' +
6493    '<div class="Video-controls">' +
6494      '<button id="picture-in-picture" class="Video-button Video-button--picture-in-picture">' +
6495      '<button id="close-video" class="Video-button Video-button--close" />' +
6496    '</div>' +
6497  '</div>' +
6498'</div>');
6499
6500      this.videoPlayer = $('#video-player');
6501
6502      var pictureInPictureButton = this.videoPlayer.find('#picture-in-picture');
6503      pictureInPictureButton.on('click.aranja', this.toggleMinimizeVideo.bind(this));
6504
6505      var videoOverlay = this.videoPlayer.find('#video-overlay');
6506      var closeButton = this.videoPlayer.find('#close-video');
6507      var closeVideo = this.closeVideo.bind(this);
6508      videoOverlay.on('click.aranja', closeVideo);
6509      closeButton.on('click.aranja', closeVideo);
6510
6511      this.doneSetup = true;
6512    };
6513
6514    VideoPlayer.prototype.startYouTubePlayer = function(videoId) {
6515      this.videoPlayer.show();
6516
6517      if (!this.isLoaded) {
6518        this.queueVideo = videoId;
6519        return;
6520      }
6521
6522      this.mPlayerPaused = false;
6523      // check if we've already created this player
6524      if (!this.youTubePlayer) {
6525        // check if there's a start time specified
6526        var idAndHash = videoId.split('#');
6527        var startTime = 0;
6528        if (idAndHash.length > 1) {
6529          startTime = idAndHash[1].split('t=')[1] !== undefined ? idAndHash[1].split('t=')[1] : 0;
6530        }
6531        // enable localized player
6532        var lang = getLangPref();
6533        var captionsOn = lang === 'en' ? 0 : 1;
6534
6535        this.youTubePlayer = new YT.Player('youTubePlayer', {
6536          height: 720,
6537          width: 1280,
6538          videoId: idAndHash[0],
6539          // jscs:disable requireCamelCaseOrUpperCaseIdentifiers
6540          playerVars: {start: startTime, hl: lang, cc_load_policy: captionsOn},
6541          // jscs:enable
6542          events: {
6543            'onReady': this.onPlayerReady.bind(this),
6544            'onStateChange': this.onPlayerStateChange.bind(this)
6545          }
6546        });
6547      } else {
6548        // if a video different from the one already playing was requested, cue it up
6549        if (videoId !== this.getVideoId()) {
6550          this.youTubePlayer.cueVideoById(videoId);
6551        }
6552        this.youTubePlayer.playVideo();
6553      }
6554    };
6555
6556    VideoPlayer.prototype.onPlayerReady = function(event) {
6557      if (!isMobile) {
6558        event.target.playVideo();
6559        this.mPlayerPaused = false;
6560      }
6561    };
6562
6563    VideoPlayer.prototype.toggleMinimizeVideo = function(event) {
6564      event.stopPropagation();
6565      this.videoPlayer.toggleClass('Video--picture-in-picture');
6566    };
6567
6568    VideoPlayer.prototype.closeVideo = function() {
6569      try {
6570        this.youTubePlayer.pauseVideo();
6571      } catch (e) {
6572      }
6573      this.videoPlayer.fadeOut(200, function() {
6574        this.videoPlayer.removeClass('Video--picture-in-picture');
6575      }.bind(this));
6576    };
6577
6578    VideoPlayer.prototype.getVideoId = function() {
6579      // jscs:disable requireCamelCaseOrUpperCaseIdentifiers
6580      return this.youTubePlayer && this.youTubePlayer.getVideoData().video_id;
6581      // jscs:enable
6582    };
6583
6584    /* Track youtube playback for analytics */
6585    VideoPlayer.prototype.onPlayerStateChange = function(event) {
6586      var videoId = this.getVideoId();
6587      var currentTime = this.youTubePlayer && this.youTubePlayer.getCurrentTime();
6588
6589      // Video starts, send the video ID
6590      if (event.data === YT.PlayerState.PLAYING) {
6591        if (this.mPlayerPaused) {
6592          devsite.analytics.trackAnalyticsEvent('event',
6593              'Videos', 'Resume', videoId);
6594        } else {
6595          // track the start playing event so we know from which page the video was selected
6596          devsite.analytics.trackAnalyticsEvent('event',
6597              'Videos', 'Start: ' + videoId, 'on: ' + document.location.href);
6598        }
6599        this.mPlayerPaused = false;
6600      }
6601
6602      // Video paused, send video ID and video elapsed time
6603      if (event.data === YT.PlayerState.PAUSED) {
6604        devsite.analytics.trackAnalyticsEvent('event',
6605            'Videos', 'Paused: ' + videoId, 'on: ' + currentTime);
6606        this.mPlayerPaused = true;
6607      }
6608
6609      // Video finished, send video ID and video elapsed time
6610      if (event.data === YT.PlayerState.ENDED) {
6611        devsite.analytics.trackAnalyticsEvent('event',
6612            'Videos', 'Finished: ' + videoId, 'on: ' + currentTime);
6613        this.mPlayerPaused = true;
6614      }
6615    };
6616
6617    return {
6618      getPlayer: function() {
6619        if (!player) {
6620          player = new VideoPlayer();
6621        }
6622
6623        return player;
6624      }
6625    };
6626  })();
6627
6628  var videoPlayer = YouTubePlayer.getPlayer();
6629
6630  window.onYouTubeIframeAPIReady = function() {
6631    videoPlayer.isLoaded = true;
6632
6633    if (videoPlayer.queueVideo) {
6634      videoPlayer.startYouTubePlayer(videoPlayer.queueVideo);
6635    }
6636  };
6637
6638  function wrapLinkInPlayer(e) {
6639    e.preventDefault();
6640
6641    if (!videoPlayer.doneSetup) {
6642      videoPlayer.setup();
6643    }
6644
6645    var videoIdMatches = $(e.currentTarget).attr('href').match(/(?:youtu.be\/|v=)([^&]*)/);
6646    var videoId = videoIdMatches && videoIdMatches[1];
6647
6648    if (videoId) {
6649      videoPlayer.startYouTubePlayer(videoId);
6650    }
6651  }
6652
6653  $(document).on('click.video', 'a[href*="youtube.com/watch"], a[href*="youtu.be"]', wrapLinkInPlayer);
6654})(jQuery, window);
6655
6656/**
6657 * Wide table
6658 *
6659 * Wraps tables in a scrollable area so you can read them on mobile.
6660 */
6661(function($) {
6662  function initWideTable() {
6663    $('table.jd-sumtable').each(function(i, table) {
6664      $(table).wrap('<div class="dac-expand wide-table">');
6665    });
6666  }
6667
6668  $(function() {
6669    initWideTable();
6670  });
6671})(jQuery);
6672
6673/** Utilities */
6674
6675/* returns the given string with all HTML brackets converted to entities
6676    TODO: move this to the site's JS library */
6677function escapeHTML(string) {
6678  return string.replace(/</g,"&lt;")
6679                .replace(/>/g,"&gt;");
6680};
6681
6682function getQueryVariable(variable) {
6683  var query = window.location.search.substring(1);
6684  var vars = query.split("&");
6685  for (var i=0;i<vars.length;i++) {
6686    var pair = vars[i].split("=");
6687    if(pair[0] == variable){return pair[1];}
6688  }
6689  return(false);
6690};
6691