Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,7 @@
;var Pairview={_debug:false,_remoteDeviceName:null,_pairMethod:null,_options:null,_passkey:'',pairview:document.getElementById('pair-view'),nameLabel:document.getElementById('label-name'),pairDescription:document.getElementById('pair-description'),pairButton:document.getElementById('button-pair'),closeButton:document.getElementById('button-close'),comfirmationItem:document.getElementById('confirmation-method'),pinInputItem:document.getElementById('pin-input-method'),passkey:document.getElementById('passkey'),pinInput:document.getElementById('pin-input'),get isFullAttentionMode(){return(window.innerHeight>200);},show:function pv_show(){if(!this.isFullAttentionMode){this.close();return;}
var _=navigator.mozL10n.get;this.pairButton.addEventListener('click',this);this.closeButton.addEventListener('click',this);window.addEventListener('resize',this);this._remoteDeviceName=this._remoteDeviceName||_('unnamed-device');var truncatedDeviceName=getTruncated(this._remoteDeviceName,{node:this.nameLabel,maxLine:2,ellipsisIndex:3});this.nameLabel.textContent=truncatedDeviceName;this.pairview.hidden=false;var stringName='passive-pair-'+this._pairMethod;this.pairDescription.textContent=_(stringName,{device:truncatedDeviceName});switch(this._pairMethod){case'displaypasskey':case'confirmation':this.passkey.textContent=this._passkey;this.comfirmationItem.hidden=false;this.pinInputItem.hidden=true;break;case'enterpincode':this.pinInputItem.hidden=false;this.comfirmationItem.hidden=true;this.pinInput.focus();break;case'consent':this.comfirmationItem.hidden=false;this.pinInputItem.hidden=true;break;}},init:function pv_init(method,options){this._pairMethod=method;this._options=options;this._remoteDeviceName=options.deviceName;if(options.handle&&options.handle.passkey){var passkey=options.handle.passkey;var len=passkey.toString().length;var zeros=(len<6)?(new Array((6-len)+1)).join('0'):'';this._passkey=zeros+passkey;}
navigator.mozL10n.once(Pairview.show.bind(Pairview));},close:function pv_close(){this._options.handle.reject().then(()=>{this.debug('Resolved in reject '+this._pairMethod+' request.');window.close();},(aReason)=>{this.debug('Rejected in reject '+this._pairMethod+' request with reason: '+aReason);window.close();});},closeInput:function pv_closeInput(){if(!this.pinInputItem.hidden){this.pinInput.blur();}},handleEvent:function pv_handleEvent(evt){var _=navigator.mozL10n.get;if(!evt.target){return;}
switch(evt.type){case'click':evt.preventDefault();switch(evt.target.id){case'button-pair':this.pairDescription.textContent=_('device-status-waiting');this.pairButton.disabled=true;this.closeButton.disabled=true;switch(this._pairMethod){case'displaypasskey':window.close();break;case'enterpincode':var pinCode=this.pinInput.value;this._options.handle.setPinCode(pinCode).then(()=>{this.debug('Resolved setPinCode operation');window.close();},(aReason)=>{this.debug('Rejected setPinCode with reason: '+aReason);window.close();});break;case'confirmation':case'consent':this._options.handle.accept().then(()=>{this.debug('Resolved in '+this._pairMethod+' request');window.close();},(aReason)=>{this.debug('Rejected in '+this._pairMethod+' request '+' with reason: '+aReason);window.close();});break;}
break;case'button-close':this.close();break;}
break;case'resize':if(!this.isFullAttentionMode){this.close();}
break;default:break;}},debug:function(msg){if(this._debug){console.log('Pairview(): '+msg);}}};;function getTruncated(t,e){function i(t,e){return t!==e}var n=e.maxLine||2,l=e.node,o=e.ellipsisIndex||3,r=e.ellipsisCharacter||"...";if(null===l)return t;var s,a,c,u,d,y,h="",v=!0,p=[],f=t.slice(-o),g=l.style.visibility,x=l.style.wordBreak;l.style.visibility="hidden",l.style.wordBreak="break-all",l.textContent=".",c=l.clientHeight,l.textContent="";var C=t.split("").some(function(t,e){if(p.push(t),y=p.join(""),s=l.clientHeight,l.textContent=y+r+f,a=l.clientHeight,0===e&&(u=Math.floor(a/c)),i(s,a)&&0!==e){var o;l.textContent=y,o=l.clientHeight,i(s,o)?(v=!0,u+=1):v&&(d=e,v=!1)}return u>n?(0===e&&console.log("Your string is in a overflowed situation, please check your options"),p.pop(),l.textContent="",!0):void 0});return l.style.visibility=g,l.style.wordBreak=x,C?(h+=p.join("").slice(0,d),h+=r,h+=f):h=t,h}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,373 @@
;window.COMPONENTS_BASE_URL="/shared/elements/";;(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.GaiaHeader=f()}})(function(){var define,module,exports;return(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){;(function(define){define(function(require,exports,module){'use strict';var debug=0?console.log.bind(console):function(){};var cache={};var MIN=16;var MAX=24;var BUFFER=3;module.exports=function(config){debug('font fit',config);var space=config.space-BUFFER;var min=config.min||MIN;var max=config.max||MAX;var text=trim(config.text);var fontSize=max;var textWidth;var font;do{font=config.font.replace(/\d+px/,fontSize+'px');textWidth=getTextWidth(text,font);}while(textWidth>space&&fontSize!==min&&fontSize--);return{textWidth:textWidth,fontSize:fontSize,overflowing:textWidth>space};};function getTextWidth(text,font){var ctx=getCanvasContext(font);var width=ctx.measureText(text).width;debug('got text width',width);return width;}
function getCanvasContext(font){debug('get canvas context',font);var cached=cache[font];if(cached){return cached;}
var canvas=document.createElement('canvas');canvas.setAttribute('moz-opaque','true');canvas.setAttribute('width','1px');canvas.setAttribute('height','1px');debug('created canvas',canvas);var ctx=canvas.getContext('2d',{willReadFrequently:true});ctx.font=font;return cache[font]=ctx;}
function trim(text){return text.replace(/\s+/g,' ').trim();}});})(typeof define=='function'&&define.amd?define:(function(n,w){'use strict';return typeof module=='object'?function(c){c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){return w[n];},m.exports,m);w[n]=m.exports;};})('font-fit',this));},{}],2:[function(require,module,exports){;(function(define){'use strict';define(function(require,exports,module){var textContent=Object.getOwnPropertyDescriptor(Node.prototype,'textContent');var innerHTML=Object.getOwnPropertyDescriptor(Element.prototype,'innerHTML');var removeAttribute=Element.prototype.removeAttribute;var setAttribute=Element.prototype.setAttribute;var noop=function(){};exports.register=function(name,props){var baseProto=getBaseProto(props.extends);var template=props.template||baseProto.templateString;var extensible=props.extensible=props.hasOwnProperty('extensible')?props.extensible:true;delete props.extends;if(template){if(extensible&&props.template){props.templateString=props.template;}
var output=processCss(template,name);props.template=document.createElement('template');props.template.innerHTML=output.template;props.lightCss=output.lightCss;props.globalCss=props.globalCss||'';props.globalCss+=output.globalCss;}
injectGlobalCss(props.globalCss);delete props.globalCss;var descriptors=mixin(props.attrs||{},base.descriptors);props._attrs=props.attrs;delete props.attrs;var proto=createProto(baseProto,props);Object.defineProperties(proto,descriptors);try{return document.registerElement(name,{prototype:proto});}catch(e){if(e.name!=='NotSupportedError'){throw e;}}};var base={properties:{GaiaComponent:true,attributeChanged:noop,attached:noop,detached:noop,created:noop,createdCallback:function(){if(this.dirObserver){addDirObserver();}
injectLightCss(this);this.created();},attributeChangedCallback:function(name,from,to){var prop=toCamelCase(name);if(this._attrs&&this._attrs[prop]){this[prop]=to;}
this.attributeChanged(name,from,to);},attachedCallback:function(){if(this.dirObserver){this.setInnerDirAttributes=setInnerDirAttributes.bind(null,this);document.addEventListener('dirchanged',this.setInnerDirAttributes);}
this.attached();},detachedCallback:function(){if(this.dirObserver){document.removeEventListener('dirchanged',this.setInnerDirAttributes);}
this.detached();},setupShadowRoot:function(){if(!this.template){return;}
var node=document.importNode(this.template.content,true);this.createShadowRoot().appendChild(node);if(this.dirObserver){setInnerDirAttributes(this);}
return this.shadowRoot;},setAttr:function(name,value){var internal=this.shadowRoot.firstElementChild;setAttribute.call(internal,name,value);setAttribute.call(this,name,value);},removeAttr:function(name){var internal=this.shadowRoot.firstElementChild;removeAttribute.call(internal,name);removeAttribute.call(this,name);}},descriptors:{textContent:{set:function(value){textContent.set.call(this,value);if(this.lightStyle){this.appendChild(this.lightStyle);}},get:function(){return textContent.get();}},innerHTML:{set:function(value){innerHTML.set.call(this,value);if(this.lightStyle){this.appendChild(this.lightStyle);}},get:innerHTML.get}}};var defaultPrototype=createProto(HTMLElement.prototype,base.properties);function getBaseProto(proto){if(!proto){return defaultPrototype;}
proto=proto.prototype||proto;return!proto.GaiaComponent?createProto(proto,base.properties):proto;}
function createProto(proto,props){return mixin(Object.create(proto),props);}
var hasShadowCSS=(function(){var div=document.createElement('div');try{div.querySelector(':host');div.querySelector('::content');return true;}
catch(e){return false;}})();var regex={shadowCss:/(?:\:host|\:\:content)[^{]*\{[^}]*\}/g,':host':/(?:\:host)/g,':host()':/\:host\((.+)\)(?: \:\:content)?/g,':host-context':/\:host-context\((.+)\)([^{,]+)?/g,'::content':/(?:\:\:content)/g};function processCss(template,name){var globalCss='';var lightCss='';if(!hasShadowCSS){template=template.replace(regex.shadowCss,function(match){var hostContext=regex[':host-context'].exec(match);if(hostContext){globalCss+=match.replace(regex['::content'],'').replace(regex[':host-context'],'$1 '+name+'$2').replace(/ +/g,' ');}else{lightCss+=match.replace(regex[':host()'],name+'$1').replace(regex[':host'],name).replace(regex['::content'],name);}
return'';});}
return{template:template,lightCss:lightCss,globalCss:globalCss};}
function injectGlobalCss(css){if(!css){return;}
var style=document.createElement('style');style.innerHTML=css.trim();headReady().then(function(){document.head.appendChild(style);});}
function headReady(){return new Promise(function(resolve){if(document.head){return resolve();}
window.addEventListener('load',function fn(){window.removeEventListener('load',fn);resolve();});});}
function injectLightCss(el){if(hasShadowCSS){return;}
var stylesheet=el.querySelector('style');if(!stylesheet){stylesheet=document.createElement('style');stylesheet.setAttribute('scoped','');stylesheet.appendChild(document.createTextNode(el.lightCss));el.appendChild(stylesheet);}
el.lightStyle=stylesheet;}
function toCamelCase(string){return string.replace(/-(.)/g,function replacer(string,p1){return p1.toUpperCase();});}
var dirObserver;function setInnerDirAttributes(component){var dir=component.dir||document.dir;Array.from(component.shadowRoot.children).forEach(element=>{if(element.nodeName!=='STYLE'){element.dir=dir;}});}
function addDirObserver(){if(dirObserver){return;}
dirObserver=new MutationObserver(onChanged);dirObserver.observe(document.documentElement,{attributeFilter:['dir'],attributes:true});function onChanged(mutations){document.dispatchEvent(new Event('dirchanged'));}}
function mixin(target,source){for(var key in source){target[key]=source[key];}
return target;}});})(typeof define=='function'&&define.amd?define:(function(n,w){'use strict';return typeof module=='object'?function(c){c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-component',this));},{}],3:[function(require,module,exports){(function(define){'use strict';define(function(require,exports,module){var base=window.GAIA_ICONS_BASE_URL||window.COMPONENTS_BASE_URL||'bower_components/';if(!document.documentElement){window.addEventListener('load',load);}else{load();}
function load(){if(isLoaded()){return;}
var link=document.createElement('link');link.rel='stylesheet';link.type='text/css';link.href=base+'gaia-icons/gaia-icons.css';document.head.appendChild(link);exports.loaded=true;}
function isLoaded(){return exports.loaded||document.querySelector('link[href*=gaia-icons]')||document.documentElement.classList.contains('gaia-icons-loaded');}});})(typeof define=='function'&&define.amd?define:(function(n,w){'use strict';return typeof module=='object'?function(c){c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-icons',this));},{}],4:[function(require,module,exports){;(function(define){'use strict';define(function(require,exports,module){var component=require('gaia-component');var fontFit=require('font-fit');require('gaia-icons');var debug=0?console.log.bind(console):function(){};const KNOWN_ACTIONS={menu:'menu',back:'back',close:'close'};const TITLE_FONT='italic 300 24px FiraSans';const TITLE_PADDING=10;const MINIMUM_FONT_SIZE_CENTERED=20;const MINIMUM_FONT_SIZE_UNCENTERED=16;const MAXIMUM_FONT_SIZE=23;module.exports=component.register('gaia-header',{extensible:false,dirObserver:true,created:function(){debug('created');this.setupShadowRoot();this.els={actionButton:this.shadowRoot.querySelector('.action-button'),titles:this.getElementsByTagName('h1')};this.els.actionButton.addEventListener('click',e=>this.onActionButtonClick(e));this.observer=new MutationObserver(this.onMutation.bind(this));this.ignoreDir=this.hasAttribute('ignore-dir');this.titleEnd=this.getAttribute('title-end');this.titleStart=this.getAttribute('title-start');this.noFontFit=this.getAttribute('no-font-fit');this.notFlush=this.hasAttribute('not-flush');this.action=this.getAttribute('action');this.unresolved={};this.pending={};this._resizeThrottlingId=null;this.onResize=this.onResize.bind(this);},attached:function(){debug('attached');this.runFontFitSoon();this.observerStart();window.addEventListener('resize',this.onResize);},detached:function(){debug('detached');window.removeEventListener('resize',this.onResize);this.observerStop();this.clearPending();},clearPending:function(){for(var key in this.pending){this.pending[key].clear();delete this.pending[key];}
window.cancelAnimationFrame(this._resizeThrottlingId);this._resizeThrottlingId=null;},runFontFit:function(){debug('run font-fit');if(this.noFontFit){return Promise.resolve();}
var titles=this.els.titles;var space=this.getTitleSpace();var styles=[].map.call(titles,el=>this.getTitleStyle(el,space));return this.setTitleStylesSoon(styles);},runFontFitSoon:function(){debug('run font-fit soon');if(this.pending.runFontFitSoon){return;}
this.pending.runFontFitSoon=this.nextTick(()=>{delete this.pending.runFontFitSoon;this.runFontFit();});},getTitleStyle:function(el,space){debug('get el style',el,space);var text=el.textContent;var styleId=space.start+text+space.end+'#'+space.value;if(!text||!text.trim()){return debug('exit: no text');}
if(getStyleId(el)===styleId){return debug('exit: no change');}
var marginStart=this.getTitleMarginStart();var textSpace=space.value-Math.abs(marginStart);var fontFitResult=this.fontFit(text,textSpace,{min:MINIMUM_FONT_SIZE_CENTERED});var overflowing=fontFitResult.overflowing;var padding={start:0,end:0};if(overflowing){debug('title overflowing');padding.start=!space.start?TITLE_PADDING:0;padding.end=!space.end?TITLE_PADDING:0;textSpace=space.value-padding.start-padding.end;fontFitResult=this.fontFit(text,textSpace);marginStart=0;}
return{id:styleId,fontSize:fontFitResult.fontSize,marginStart:marginStart,overflowing:overflowing,padding:padding};},setTitleStylesSoon:function(styles){debug('set title styles soon',styles);var key='setStyleTitlesSoon';this._titleStyles=styles;if(this.unresolved[key]){return this.unresolved[key];}
this.unresolved[key]=new Promise((resolve)=>{this.pending[key]=this.nextTick(()=>{var styles=this._titleStyles;var els=this.els.titles;[].forEach.call(els,(el,i)=>{if(!styles[i]){return debug('exit');}
this.setTitleStyle(el,styles[i]);});delete this._titleStyles;delete this.unresolved[key];delete this.pending[key];resolve();});});},setTitleStyle:function(el,style){debug('set title style',style);this.observerStop();if(this.ignoreDir){el.style.marginLeft=style.marginStart+'px';el.style.paddingLeft=style.padding.start+'px';el.style.paddingRight=style.padding.end+'px';}else{el.style.marginInlineStart=style.marginStart+'px';el.style.paddingInlineStart=style.padding.start+'px';el.style.paddingInlineEnd=style.padding.end+'px';}
el.style.fontSize=style.fontSize+'px';setStyleId(el,style.id);this.observerStart();},fontFit:function(text,space,opts={}){debug('font fit:',text,space,opts);var fontFitArgs={font:TITLE_FONT,min:opts.min||MINIMUM_FONT_SIZE_UNCENTERED,max:MAXIMUM_FONT_SIZE,text:text,space:space};return fontFit(fontFitArgs);},observerStart:function(){if(this.observing){return;}
this.observer.observe(this,{childList:true,attributes:true,subtree:true});this.observing=true;debug('observer started');},observerStop:function(){if(!this.observing){return;}
this.observer.disconnect();this.observing=false;debug('observer stopped');},onResize:function(e){debug('onResize',this._resizeThrottlingId);if(this._resizeThrottlingId!==null){return;}
this._resizeThrottlingId=window.requestAnimationFrame(()=>{this._resizeThrottlingId=null;this.runFontFitSoon();});},onMutation:function(mutations){debug('on mutation',mutations);if(!this.pending.runFontFitSoon){this.runFontFit();}},getTitleSpace:function(){var start=this.titleStart;var end=this.titleEnd;var space=this.getWidth()-start-end;var result={value:space,start:start,end:end};debug('get title space',result);return result;},getWidth:function(){var value=this.notFlush?this.clientWidth:window.innerWidth;debug('get width',value);return value;},triggerAction:function(){if(this.action){this.els.actionButton.click();}},onActionButtonClick:function(){debug('action button click');var config={detail:{type:this.action}};var e=new CustomEvent('action',config);setTimeout(()=>this.dispatchEvent(e));},getTitleMarginStart:function(){var start=this.titleStart;var end=this.titleEnd;var marginStart=end-start;debug('get title margin start',marginStart);return marginStart;},getButtonsBeforeTitle:function(){var children=this.children;var l=children.length;var els=[];for(var i=0;i<l;i++){var el=children[i];if(el.tagName==='H1'){break;}
if(!contributesToLayout(el)){continue;}
els.push(el);}
if(this.action){els.push(this.els.actionButton);}
return els;},getButtonsAfterTitle:function(){var children=this.children;var els=[];for(var i=children.length-1;i>=0;i--){var el=children[i];if(el.tagName==='H1'){break;}
if(!contributesToLayout(el)){continue;}
els.push(el);}
return els;},sumButtonWidths:function(buttons){var defaultWidth=50;var sum=buttons.reduce((prev,button)=>{var isStandardButton=button===this.els.actionButton;var width=isStandardButton?defaultWidth:button.clientWidth;return prev+width;},0);debug('sum button widths',buttons,sum);return sum;},attrs:{action:{get:function(){return this._action;},set:function(value){var action=KNOWN_ACTIONS[value];if(action===this._action){return;}
this.setAttr('action',action);this._action=action;}},titleStart:{get:function(){debug('get title-start');if('_titleStart'in this){return this._titleStart;}
var buttons=this.getButtonsBeforeTitle();var value=this.sumButtonWidths(buttons);debug('get title-start',buttons,value);return value;},set:function(value){debug('set title-start',value);value=parseInt(value,10);if(value===this._titleStart||isNaN(value)){return;}
this.setAttr('title-start',value);this._titleStart=value;debug('set');}},titleEnd:{get:function(){debug('get title-end');if('_titleEnd'in this){return this._titleEnd;}
var buttons=this.getButtonsAfterTitle();return this.sumButtonWidths(buttons);},set:function(value){debug('set title-end',value);value=parseInt(value,10);if(value===this._titleEnd||isNaN(value)){return;}
this.setAttr('title-end',value);this._titleEnd=value;}},noFontFit:{get:function(){return this._noFontFit||false;},set:function(value){debug('set no-font-fit',value);value=!!(value||value==='');if(value===this.noFontFit){return;}
this._noFontFit=value;if(value){this.setAttr('no-font-fit','');}
else{this.removeAttr('no-font-fit');}}},ignoreDir:{get:function(){return this._ignoreDir||false;},set:function(value){debug('set ignore-dir',value);value=!!(value||value==='');if(value===this.ignoreDir){return;}
this._ignoreDir=value;this.dirObserver=!value;if(value){this.setAttr('ignore-dir','');}
else{this.removeAttr('ignore-dir');}}}},template:`<div class="inner">
<button class="action-button">
<content select="[l10n-action]"></content>
</button>
<content></content>
</div>
<style>
:host {
display: block;
-moz-user-select: none;
--gaia-header-button-color:
var(--header-button-color,
var(--header-color,
var(--link-color,
inherit)));
}
/**
* [hidden]
*/
:host[hidden] {
display: none;
}
/** Reset
---------------------------------------------------------*/
::-moz-focus-inner { border: 0; }
/** Inner
---------------------------------------------------------*/
.inner {
display: flex;
min-height: 50px;
-moz-user-select: none;
background:
var(--header-background,
var(--background,
#fff));
}
/** Action Button
---------------------------------------------------------*/
/**
* 1. Hidden by default
*/
.action-button {
position: relative;
display: none; /* 1 */
width: 50px;
font-size: 30px;
margin: 0;
padding: 0;
border: 0;
outline: 0;
align-items: center;
background: none;
cursor: pointer;
transition: opacity 200ms 280ms;
color:
var(--header-action-button-color,
var(--header-icon-color,
var(--gaia-header-button-color)));
}
/**
* [action=back]
* [action=menu]
* [action=close]
*
* 1. For icon vertical-alignment
*/
[action=back] .action-button,
[action=menu] .action-button,
[action=close] .action-button {
display: flex; /* 1 */
}
/**
* :active
*/
.action-button:active {
transition: none;
opacity: 0.2;
}
/** Action Button Icon
---------------------------------------------------------*/
.action-button:before {
font-family: 'gaia-icons';
font-style: normal;
text-rendering: optimizeLegibility;
font-weight: 500;
}
[action=close] .action-button:before { content: 'close' }
[action=menu] .action-button:before { content: 'menu' }
[action=back]:-moz-dir(ltr) .action-button:before { content: 'left' }
[action=back]:-moz-dir(rtl) .action-button:before { content: 'right' }
/** Action Button Icon
---------------------------------------------------------*/
/**
* 1. To enable vertical alignment.
*/
.action-button:before {
display: block;
}
/** Action Button Text
---------------------------------------------------------*/
/**
* To provide custom localized content for
* the action-button, we allow the user
* to provide an element with the class
* .l10n-action. This node is then
* pulled inside the real action-button.
*
* Example:
*
* <gaia-header action="back">
* <span l10n-action aria-label="Back">Localized text</span>
* <h1>title</h1>
* </gaia-header>
*/
::content [l10n-action] {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
font-size: 0;
}
/** Title
---------------------------------------------------------*/
/**
* 1. Vertically center text. We can't use flexbox
* here as it breaks text-overflow ellipsis
* without an inner div.
*/
::content h1 {
flex: 1;
margin: 0;
padding: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: center;
line-height: 50px; /* 1 */
font-weight: 300;
font-style: italic;
font-size: 24px;
color:
var(--header-title-color,
var(--header-color,
var(--title-color,
var(--text-color,
inherit))));
}
/**
* [ignore-dir]
*
* When the <gaia-header> component has an [ignore-dir] attribute, header
* direction is forced to LTR but we still want the <h1> text to be reversed
* so that strings like '1 selected' become 'selected 1'.
*
* When we're happy for <gaia-header> to be fully RTL responsive we won't need
* these rules anymore, but this depends on all Gaia apps being ready.
*
* This should be safe to remove when bug 1179459 lands.
*/
:host[ignore-dir] {
direction: ltr;
}
:host[ignore-dir]:-moz-dir(rtl) h1 {
direction: rtl;
}
/** Buttons
---------------------------------------------------------*/
::content a,
::content button {
position: relative;
z-index: 1;
box-sizing: border-box;
display: flex;
width: auto;
height: auto;
min-width: 50px;
margin: 0;
padding: 0 10px;
outline: 0;
border: 0;
font-size: 14px;
line-height: 1;
align-items: center;
justify-content: center;
text-decoration: none;
text-align: center;
background: none;
border-radius: 0;
font-style: italic;
cursor: pointer;
transition: opacity 200ms 280ms;
color: var(--gaia-header-button-color);
}
/**
* :active
*/
::content a:active,
::content button:active {
transition: none;
opacity: 0.2;
}
/**
* [hidden]
*/
::content a[hidden],
::content button[hidden] {
display: none;
}
/**
* [disabled]
*/
::content a[disabled],
::content button[disabled] {
pointer-events: none;
color: var(--header-disabled-button-color);
}
/** Icon Buttons
---------------------------------------------------------*/
/**
* Icons are a different color to text
*/
::content .icon,
::content [data-icon] {
color:
var(--header-icon-color,
var(--gaia-header-button-color));
}
/**
* If users want their action button
* to be in the component's light-dom
* they can add an .action class
* to make it look like the
* shadow action button.
*/
::content .action {
color:
var(--header-action-button-color,
var(--header-icon-color,
var(--gaia-header-button-color)));
}
/**
* [data-icon]:empty
*
* Icon buttons with no textContent,
* should always be 50px.
*
* This is to prevent buttons being
* larger than they should be before
* icon-font has loaded.
*/
::content [data-icon]:empty {
width: 50px;
}
</style>`,nextTick:nextTick});function contributesToLayout(el){return el.localName!=='style'&&!el.hasAttribute('l10n-action');}
function setStyleId(el,id){el._styleId=id;}
function getStyleId(el){return el._styleId;}
function nextTick(fn){var cleared;Promise.resolve().then(()=>{if(!cleared){fn();}});return{clear:function(){cleared=true;}};}});})(typeof define=='function'&&define.amd?define:(function(n,w){'use strict';return typeof module=='object'?function(c){c(require,exports,module);}:function(c){var m={exports:{}};c(function(n){return w[n];},m.exports,m);w[n]=m.exports;};})('gaia-header',this));},{"font-fit":1,"gaia-component":2,"gaia-icons":3}]},{},[4])(4)});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
require.config({baseUrl:"/js",paths:{modules:"modules",views:"views",shared:"../shared/js"},shim:{"shared/async_storage":{exports:"asyncStorage"}}});

View file

@ -0,0 +1 @@
var gDeviceList=null;navigator.mozL10n.once(function(){function e(e){o&&console.log("[Bluetooth APP Device List]: "+e)}var t=navigator.mozL10n.get,n=window.navigator.mozSettings,i=window.navigator.mozBluetooth,d=null,o=!1;n&&i&&(gDeviceList=function(){function n(e,n){var i=document.createElement("span"),d=""===e.name?t("unnamed-device"):e.name,o=""===e.name?"unnamed-device":"";i.textContent=d,i.dataset.l10nId=o;var a=document.createElement("small");a.textContent=""===n?"":t(n),a.dataset.l10nId=n;var s=document.createElement("progress");s.classList.add("overlapping-icon"),s.classList.add("hidden");var l=document.createElement("a");l.appendChild(i),l.appendChild(a);var r=document.createElement("li");return r.classList.add("bluetooth-device"),r.classList.add("bluetooth-type-"+e.icon),r.appendChild(l),r.appendChild(s),r}function o(e){p.hidden=!e,e?(q.show(!0),x.hidden=!1):(q.show(!1),S.show(!1),x.hidden=!0,C=null,E=null,b=null,clearTimeout(I),I=null)}function a(e,t,n){f.hidden=!1,y.addEventListener("action",m),d=e,d.onpairedstatuschanged=function(e){c(e.status,"Authentication Failed",e.address)},d.ondiscoverystatechanged=function(e){e.discovering?(g.disabled=!0,x.hidden=!1):(g.disabled=!1,x.hidden=!0,clearTimeout(I),I=null)},L=t,B=n,l(),u()}function s(){f.hidden=!0,d=null,y.removeEventListener("action",m)}function l(){if(i.enabled&&d){var e=d.getPairedDevices();e.onsuccess=function(){function i(){return function(e){var i=e.address===b?"device-status-connected":"",d=n(e,i);d.onclick=function(){if(!D&&!C){v();var n=d.querySelector("small");n.textContent=t("device-status-connecting"),n.dataset.l10nId="device-status-connecting",h(e)}},S.list.appendChild(d),S.index[e.address]=[e,d]}}var d=e.result.slice(),o=d.length;if(0===o)return void S.show(!1);S.clear(),d.sort(function(e,t){return e.name>t.name});for(var a=0;o>a;a++)i()(d[a]);S.show(!0)}}}function r(i){var o=i.device,a=q.index[o.address]||S.index[o.address];if(a){var s=a[1];if(o.name&&s){var l=s.querySelector("a > span");l&&(l.dataset.l10nId="",l.textContent=o.name)}}else{var r=n(o,"device-status-tap-connect");r.onclick=function(){if(v(),!D&&!C){var n=r.querySelector("small");n.textContent=t("device-status-pairing"),n.dataset.l10nId="device-status-pairing",r.classList.add("icon-hidden");var i=r.querySelector("progress");i.classList.remove("hidden");var a=d.pair(o.address);C=o.address;var s="pairing with address = "+C;e(s),a.onerror=function(e){c(!1,a.error.name)}}},q.list.appendChild(r),q.index[o.address]=[o,r]}}function c(n,i,d){if(C){var o=C;if(C=null,n){if(q.index[o]){var a=q.index[o][0],s=q.index[o][1];q.list.removeChild(s),delete q.index[o],E=o,h(a)}}else{var r=t("error-pair-title");if("Repeated Attempts"===i?r=r+"\n"+t("error-pair-toofast"):"Authentication Failed"===i&&(r=r+"\n"+t("error-pair-pincode")),e(r),window.alert(r),q.index[o]){var c=q.index[o][1].querySelector("small");c.textContent=t("device-status-tap-connect"),c.dataset.l10nId="device-status-tap-connect";var u=q.index[o][1];u.classList.remove("icon-hidden");var v=q.index[o][1].querySelector("progress");v.classList.add("hidden")}}l()}else if(l(),d&&n&&q.index[d]){var m=q.index[d][1];q.list.removeChild(m),delete q.index[d]}}function u(){if(i.enabled&&d&&!I){var e=d.startDiscovery();e.onsuccess=function(){I||(I=setTimeout(v,w))},e.onerror=function(){console.error("Can not discover nearby device")}}}function v(){if(i.enabled&&d&&I){var e=d.stopDiscovery();e.onerror=function(){console.error("Failed to stop discovery of nearby devices")},clearTimeout(I),I=null}}function h(e){D=!0,L&&L(e)}function m(){B&&B()}var f=document.getElementById("devices-list-view"),p=document.getElementById("bluetooth-search"),g=document.getElementById("search-device"),x=document.getElementById("bluetooth-searching"),y=document.getElementById("devices-list-header"),C=null,E=null,b=null,w=6e4,I=null,L=null,B=null,D=!1,S={title:document.getElementById("bluetooth-paired-title"),list:document.getElementById("bluetooth-paired-devices"),index:[],clear:function(){for(;this.list.hasChildNodes();)this.list.removeChild(this.list.lastChild);this.index=[]},show:function(e){e||this.clear(),this.title.hidden=!e,this.list.hidden=!e}},q={title:document.getElementById("bluetooth-found-title"),list:document.getElementById("bluetooth-devices"),index:[],clear:function(){for(;this.list.hasChildNodes();)this.list.removeChild(this.list.lastChild);this.index=[]},show:function(e){e||this.clear(),this.title.hidden=!e,this.list.hidden=!e}};return g.onclick=function(){D||C||(o(!0),q.clear(),u())},{update:o,initWithAdapter:a,uninit:s,startDiscovery:u,onDeviceFound:r}}())});

View file

@ -0,0 +1,111 @@
/**
* BluetoothAdapterManager:
* - BluetoothAdapterManager is an Observable that wraps the platform
* Bluetooth object for construct default adapter.
* - It has only one observable properties: defaultAdapter
* BluetoothAdapterManager only update state and does not involve in any UI
* logic.
*
* @module BluetoothAdapterManager
*/
define(['require','modules/navigator/mozBluetooth','modules/mvvm/observable'],function(require) {
'use strict';
var NavigatorBluetooth = require('modules/navigator/mozBluetooth');
var Observable = require('modules/mvvm/observable');
var _debug = false;
var debug = function() {};
if (_debug) {
debug = function btam_debug(msg) {
console.log('--> [BluetoothAdapterManager]: ' + msg);
};
}
/**
* @alias module:modules/bluetooth/BluetoothAdapterManager
* @requires module:modules/navigator/mozBluetooth
* @requires module:modules/mvvm/observable
* @return {BluetoothAdapterManager}
*/
var BluetoothAdapterManager = {
/**
* The default adapter used to connect to the remote bluetooth devices.
*
* @readonly
* @memberOf BluetoothAdapterManager
* @type {Object}
*/
defaultAdapter: null,
/**
* Init Bluetooth module.
*
* @access private
* @memberOf BluetoothAdapterManager
*/
_init: function btam__init() {
// Early return while there is no navigator.mozBluetooth module.
if (!NavigatorBluetooth) {
return;
}
// watch BluetoothManager event
this._watchMozBluetoothAttributechanged();
// init default adapter
this._initDefaultAdapter();
},
/**
* Watch 'attributechanged' event from mozBluetooth for updating default
* adapter information.
*
* 'attributechanged' event description:
* A handler to trigger when bluetooth manager's only property
* defaultAdapter has changed.
*
* @access private
* @memberOf BluetoothAdapterManager
*/
_watchMozBluetoothAttributechanged:
function btam__watchMozBluetoothAttributechanged() {
NavigatorBluetooth.addEventListener('attributechanged', (evt) => {
for (var i in evt.attrs) {
switch (evt.attrs[i]) {
case 'defaultAdapter':
// Default adapter attribute change.
// Usually, it means that we reach new default adapter.
this.defaultAdapter = NavigatorBluetooth.defaultAdapter;
debug('_watchMozBluetoothAttributechanged(): ' +
'this.defaultAdapter = ' + this.defaultAdapter);
break;
default:
break;
}
}
});
},
/* Reach default adapter from platform Bluetooth.
*
* If the property is accessed on platform in the first time, it will
* trigger Gecko::Bluetooth to get default adapter from the chipset.
* So the value would be null.
* Then we will receive 'attributechanged' event later.
*
* @access private
* @memberOf BluetoothAdapterManager
*/
_initDefaultAdapter: function btam__initDefaultAdapter() {
this.defaultAdapter = NavigatorBluetooth.defaultAdapter;
debug('_initDefaultAdapter(): this.defaultAdapter = ' +
this.defaultAdapter);
}
};
// Create the observable object using the prototype.
var bluetoothAdapterManager = Observable(BluetoothAdapterManager);
bluetoothAdapterManager._init();
return bluetoothAdapterManager;
});

View file

@ -0,0 +1 @@
define([],function(){"use strict";var e=!1,o=function(){};e&&(o=function(e){console.log("--> [BluetoothCODMapper]: "+e)});var i={Computer:{0:"computer",1:"computer",2:"computer",3:"computer",4:"pda",5:"pda",6:"computer",7:"computer",reserved:"computer"},Phone:{0:"phone",1:"phone",2:"phone",3:"phone",4:"modem",5:"phone",reserved:"phone"},AudioVideo:{0:"audio-card",1:"audio-card",2:"audio-card",4:"audio-input-microphone",5:"audio-card",6:"audio-card",7:"audio-card",8:"audio-card",9:"audio-card",10:"audio-card",11:"camera-video",12:"camera-video",13:"camera-video",14:"video-display",15:"video-display",16:"video-display",18:"audio-card",reserved:"audio-card"},Peripheral:{1:"input-gaming",2:"input-gaming",16:"input-keyboard",17:"input-keyboard",18:"input-keyboard",19:"input-keyboard",20:"input-keyboard",21:"input-keyboard",22:"input-keyboard",23:"input-keyboard",24:"input-keyboard",25:"input-keyboard",26:"input-keyboard",27:"input-keyboard",28:"input-keyboard",29:"input-keyboard",30:"input-keyboard",31:"input-keyboard",32:"input-mouse",33:"input-mouse",34:"input-mouse",35:"input-mouse",36:"input-mouse",37:"input-tablet",38:"input-mouse",39:"input-mouse",40:"input-mouse",41:"input-mouse",42:"input-mouse",43:"input-mouse",44:"input-mouse",45:"input-mouse",46:"input-mouse",47:"input-mouse"},Imaging:{4:"video-display",8:"camera-photo",16:"scanner",32:"printer"}},r={1:i.Computer,2:i.Phone,3:"network-wireless",4:i.AudioVideo,5:i.Peripheral,6:i.Imaging},a={getDeviceType:function(e){o("cod.majorDeviceClass = "+e.majorDeviceClass),o("cod.majorServiceClass = "+e.majorServiceClass),o("cod.minorDeviceClass = "+e.minorDeviceClass);var i="",a=r[e.majorDeviceClass];return o("majorDeviceClass = "+a),"object"==typeof a?(i=a[e.minorDeviceClass]||a.reserved||"",o("return in minor class, type = "+i),i):"string"==typeof a?(o("return in major class, type = network-wireless"),a):256&e.majorServiceClass?(i="audio-card",o("return in major service class, type = audio-card"),i):(o("not mapping in any class, return type = "+i),i)}};return a});

View file

@ -0,0 +1,953 @@
/**
* BluetoothConnectionManager:
* BluetoothConnectionManager only update state and does not involve in any UI
* logic.
*
* @module BluetoothConnectionManager
*/
define(['require','modules/bluetooth/bluetooth_adapter_manager','shared/async_storage'],function(require) {
'use strict';
var AdapterManager = require('modules/bluetooth/bluetooth_adapter_manager');
var AsyncStorage = require('shared/async_storage');
var _debug = false;
var debug = function() {};
if (_debug) {
debug = function btam_debug(msg) {
console.log('--> [BluetoothConnectionManager]: ' + msg);
};
}
/**
* @alias module:modules/bluetooth/BluetoothConnectionManager
* @requires module:modules/bluetooth/bluetooth_adapter_manager
* @requires module:shared/async_storage
* @return {BluetoothConnectionManager}
*/
var BluetoothConnectionManager = {
/**
* The profiles of connected device that we are defined here.
*
* @public
* @memberOf BluetoothConnectionManager
* @type {Object}
*/
Profiles: {
'hfp': 0x111E, // Handsfree
'a2dp': 0x110D // Advanced Audio Distribution Devices
},
/**
* The address of device that we are trying to connect.
*
* @public
* @memberOf BluetoothConnectionManager
* @type {String}
*/
connectingAddress: null,
/**
* A object that we cache the connected devices information(address, device,
* connectedProfiles). It will be inited while default adapter is ready.
* And these information is coming from profile events.
* Each connected device is hashed by device address.
*
* EX:
* _connectedDevicesInfo = {
* 'AA:BB:CC:00:11:22': {
* 'device': DeviceObject,
* 'connectedProfiles': {
* 'hfp': true,
* 'a2dp': false
* }
* }
* };
*
* @private
* @memberOf BluetoothConnectionManager
* @type {Object}
*/
_connectedDevicesInfo: {},
/**
* An instance to maintain that we have created a promise to get connected
* devices.
*
* @access public
* @memberOf BluetoothConnectionManager
* @return {Promise}
*/
_getConnectedDevicesPromise: null,
/**
* The object maintains listeners' callback per property name.
* Each listener would be called as following definition.
* 'connecting' - be called when device is connecting.
* 'connected': - be called when device is connected.
* 'disconnected': - be called when device is disconnected.
* 'profileChanged': - be called when profile is changed.
*
* @memberOf BluetoothConnectionManager
* @type {Object}
*/
_listeners: {
'connecting': [],
'connected': [],
'disconnected': [],
'profileChanged': []
},
/**
* Default adapter of Bluetooth.
*
* @access private
* @memberOf BluetoothConnectionManager
* @type {Object} BluetoothAdapter
*/
_defaultAdapter: null,
/**
* Init BluetoothConnectionManager module.
*
* @access private
* @memberOf BluetoothConnectionManager
*/
_init: function btcm__init() {
// Observe 'defaultAdapter' property for reaching default adapter.
AdapterManager.observe('defaultAdapter',
this._onDefaultAdapterChanged.bind(this));
this._onDefaultAdapterChanged(AdapterManager.defaultAdapter);
},
/**
* 'defaultAdapter' change event handler from adapter manager for
* updating adapter immediately.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {Object} BluetoothAdapter newAdapter
* @param {Object} BluetoothAdapter oldAdapter
*/
_onDefaultAdapterChanged:
function btcm__onDefaultAdapterChanged(newAdapter, oldAdapter) {
debug('_onDefaultAdapterChanged(): newAdapter = ' + newAdapter);
debug('_onDefaultAdapterChanged(): oldAdapter = ' + oldAdapter);
// save default adapter
this._defaultAdapter = newAdapter;
if (oldAdapter) {
// unwatch event since the old adapter is no longer usefull
this._unwatchProfilesStatuschanged(oldAdapter);
this._unwatchDefaultAdapterOnattributechanged(oldAdapter);
}
if (newAdapter) {
// watch event since the new adapter is ready to access
this._watchProfilesStatuschanged(newAdapter);
this._watchDefaultAdapterOnattributechanged(newAdapter);
// restore connection
if (newAdapter.state === 'enabled') {
this._restoreConnection();
}
} else {
// reset properties only
this._resetConnectionInfo();
}
},
/**
* Return the cache of connected devices in ConnectionManager.
*
* @access public
* @memberOf BluetoothConnectionManager
* @return {Promise}
*/
getConnectedDevices: function btcm_getConnectedDevices() {
if (!this._getConnectedDevicesPromise) {
this._getConnectedDevicesPromise =
this._initConnectedDevicesInfo().then(() => {
debug('getConnectedDevices(): resolved with latest cache = ' +
JSON.stringify(this._connectedDevicesInfo));
return this._connectedDevicesInfo;
}, (reason) => {
debug('getConnectedDevices(): rejected with reason = ' + reason);
this._getConnectedDevicesPromise = null;
});
}
return this._getConnectedDevicesPromise;
},
/**
* Init cache of connected device and save it in cache.
*
* @access private
* @memberOf BluetoothConnectionManager
* @return {Promise}
*/
_initConnectedDevicesInfo: function btcm__initConnectedDevicesInfo() {
if (!this._defaultAdapter) {
return Promise.reject('default adapter is not existed!!');
}
// Init connection status and profile from platform.
// Then, save these connected device information in cache.
return this._getConnectedDevicesFromPlatform().then(
(connectedDevicesByProfile) => {
this._constructDeviceItemsMap(connectedDevicesByProfile);
}, (reason) => {
debug('_initConnectedDevicesInfo(): rejected in ' +
'_getConnectedDevicesFromPlatform');
return Promise.reject(reason);
});
},
/**
* The method will update each device item in maintaining map.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {Object} connectedDevices
*/
_constructDeviceItemsMap:
function btcm__constructDeviceItemsMap(connectedDevices) {
debug('_constructDeviceItemsMap(): connectedDevices = ' +
JSON.stringify(connectedDevices));
if (!connectedDevices) {
// Return empty object while there is no any connected devices.
debug('_constructDeviceItemsMap(): early return with empty object');
return;
}
Object.keys(this.Profiles).map((profileID) => {
connectedDevices[profileID].forEach((connectedDevice) => {
var connectionDeviceInfo = {
address: connectedDevice.address,
connected: true,
profileID: profileID,
device: connectedDevice
};
debug('_constructDeviceItemsMap(): connectionDeviceInfo = ' +
JSON.stringify(connectionDeviceInfo));
this._initConnectedDevicesCache(connectionDeviceInfo);
});
});
},
/**
* Init the cache which is maintained for connection devices.
* And the input is gotten from platform API adapter.getConnectedDevices().
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {Object} options
* @param {String} options.address - address of the device
* @param {Boolean} options.connected - is connected or not
* @param {Object} options.profileID - profile ID of the connection type
* @param {Object} options.device - connect device, Bluetooth Object
*/
_initConnectedDevicesCache:
function btcm__initConnectedDevicesCache(options) {
debug('_initConnectedDevicesCache(): options = ' +
JSON.stringify(options));
// hash by device address
var info = this._connectedDevicesInfo[options.address];
if (info) {
// Already have profiles, update it for other profile.
info.connectedProfiles[options.profileID] = options.connected;
} else {
// Not have profiles yet, create for it.
// If options.device is existed, save the connected device.
// Otherwise, given null in this property.
var connectedDevice = (options.device) ? options.device : null;
info = {
'device': connectedDevice,
'connectedProfiles': {}
};
info.connectedProfiles[options.profileID] = options.connected;
}
// Save the device/profile in map.
this._connectedDevicesInfo[options.address] = info;
// If there is no profile connected,
// remove the device item from cache since it is already disconnected.
var dataToCheckConnectedProfile = {
address: options.address,
connectedDevices: this._connectedDevicesInfo
};
if (!this._hasConnectedProfileByAddress(dataToCheckConnectedProfile)) {
delete this._connectedDevicesInfo[options.address];
}
// Return the latest cache which is just updated here.
debug('_initConnectedDevicesCache(): this._connectedDevicesInfo = ' +
JSON.stringify(this._connectedDevicesInfo));
return this._connectedDevicesInfo;
},
/**
* Update the cache which is maintained for connection devices.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {Object} options
* @param {String} options.address - address of the device
* @param {Boolean} options.connected - is connected or not
* @param {Object} options.profileID - profile ID of the connection type
* @param {Object} options.device - connect device, Bluetooth Object
*/
_updateConnectedDevices:
function btcm__updateConnectedDevices(options) {
return this.getConnectedDevices().then((connectedDevicesInfo) => {
debug('_updateConnectedDevices(): connectedDevicesInfo = ' +
JSON.stringify(connectedDevicesInfo));
// hash by device address
var info =
(connectedDevicesInfo) ? connectedDevicesInfo[options.address] : null;
if (info) {
// Already have profiles, update it for other profile.
info.connectedProfiles[options.profileID] = options.connected;
} else {
// Not have profiles yet, create for it.
// If options.device is existed, save the connected device.
// Otherwise, given null in this property.
var connectedDevice = (options.device) ? options.device : null;
info = {
'device': connectedDevice,
'connectedProfiles': {}
};
info.connectedProfiles[options.profileID] = options.connected;
}
// Save the device/profile in map.
this._connectedDevicesInfo[options.address] = info;
// If there is no profile connected,
// remove the device item from cache since it is already disconnected.
var dataToCheckConnectedProfile = {
address: options.address,
connectedDevices: this._connectedDevicesInfo
};
if (!this._hasConnectedProfileByAddress(dataToCheckConnectedProfile)) {
delete this._connectedDevicesInfo[options.address];
}
// Return the latest cache which is just updated here.
debug('_updateConnectedDevices(): this._connectedDevicesInfo = ' +
JSON.stringify(this._connectedDevicesInfo));
return Promise.resolve(this._connectedDevicesInfo);
}, () => {
debug('_updateConnectedDevices(): rejected with some exception');
return Promise.reject('rejected with some exception');
});
},
/**
* Only reset properties since there is no available default adapter.
*
* @access private
* @memberOf BluetoothConnectionManager
*/
_resetConnectionInfo: function btcm__resetConnectionInfo() {
// Reset connection status.
this.connectingAddress = null;
// Clean up the instance to get connected devices
// while new adapter is ready.
this._getConnectedDevicesPromise = null;
},
/**
* Watch 'onattributechanged' event from default adapter for watching
* adapter enabled/disabled status.
*
* Description of 'onattributechanged' event:
* A handler to trigger when one of the local bluetooth adapter's properties
* has changed. Note access to the changed property in this event handler
* would get the updated value.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {Object} BluetoothAdapter adapter
*/
_watchDefaultAdapterOnattributechanged:
function btcm__watchDefaultAdapterOnattributechanged(adapter) {
adapter.addEventListener('attributechanged',
this._onAdapterAttributeChanged.bind(this, adapter));
},
/**
* Unwatch 'onattributechanged' event from default adapter since adapter is
* removed.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {Object} BluetoothAdapter adapter
*/
_unwatchDefaultAdapterOnattributechanged:
function btcm__unwatchDefaultAdapterOnattributechanged(adapter) {
adapter.removeEventListener('attributechanged',
this._onAdapterAttributeChanged);
},
/**
* 'onattributechanged' event handler from default adapter for reaching
* adapter enabled/disabled status.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {Object} BluetoothAdapter adapter
* @param {event} evt
*/
_onAdapterAttributeChanged:
function btcm__onAdapterAttributeChanged(adapter, evt) {
for (var i in evt.attrs) {
debug('_onAdapterAttributeChanged(): ' + evt.attrs[i]);
switch (evt.attrs[i]) {
case 'state':
if (adapter.state === 'enabled') {
// Restore connection while default adapter state is enabled.
this._restoreConnection();
}
break;
default:
break;
}
}
},
/**
* Watch every of profile events('onhfpstatuschanged','ona2dpstatuschanged')
* from default adapter for updating device connected status immediately.
*
* Description of 'onhfpstatuschanged' event:
* Specifies an event listener to receive hfpstatuschanged events.
* Those events occur when an HFP connection status changes.
*
* Description of 'ona2dpstatuschanged' event:
* Specifies an event listener to receive a2dpstatuschanged events.
* Those events occur when an A2DP connection status changes.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {Object} BluetoothAdapter adapter
*/
_watchProfilesStatuschanged:
function btcm__watchProfilesStatuschanged(adapter) {
var eventName;
for (var profileID in this.Profiles) {
eventName = 'on' + profileID + 'statuschanged';
adapter[eventName] =
this._onProfileStatuschangeHandler.bind(this, profileID);
}
},
/**
* Unwatch every of profile events('onhfpstatuschanged',
* 'ona2dpstatuschanged') from default adapter since adapter is removed.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {Object} BluetoothAdapter adapter
*/
_unwatchProfilesStatuschanged:
function btc__unwatchProfilesStatuschanged(adapter) {
var eventName;
for (var profileID in this.Profiles) {
eventName = 'on' + profileID + 'statuschanged';
adapter[eventName] = null;
}
},
/**
* 'onhfpstatuschanged', 'ona2dpstatuschanged' events handler from
* default adapter for updating device connected status.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {String} profileID
* @param {event} evt
*/
_onProfileStatuschangeHandler:
function btcm__onProfileStatuschangeHandler(profileID, evt) {
debug('_onProfileStatuschangeHandler(): profileID = ' + profileID +
', evt.address = ' + evt.address + ', evt.status = ' + evt.status);
var options = {
address: evt.address,
connected: evt.status,
profileID: profileID
};
// Update connection status.
this._updateConnectionStatus(options);
},
/**
* Query async storage to restore connection.
*
* @access private
* @memberOf BluetoothConnectionManager
*/
_restoreConnection: function btcm__restoreConnection() {
if (!this._defaultAdapter) {
return;
}
// Reconnect the one kept in the async storage.
AsyncStorage.getItem('device.connected', (address) => {
if (!address) {
return;
}
// Make sure the restore device is already connected or not.
this.getConnectedDevices().then((connectedDevices) => {
if (connectedDevices && connectedDevices[address]) {
// Do an early return since the restore device is connected already.
debug('_restoreConnection(): early return cause connected already');
return;
}
// Get the device which is wanted to connect from paired devices.
var restoreDevice = this._getPairedDeviceByAddress(address);
if (restoreDevice) {
this.connectingAddress = restoreDevice.address;
// Fire 'connecting' event.
var event = {
type: 'connecting',
detail: {
address: restoreDevice.address
}
};
this._emitEvent(event);
this._connect(restoreDevice).then(() => {
debug('_restoreConnection(): restore connection successfully');
}, (reason) => {
debug('_restoreConnection(): restore connection failed, ' +
'reason = ' + reason);
// No available profiles are connected. Reset connecting address.
this.connectingAddress = null;
// Then, fire 'disconnected' event.
event = {
type: 'disconnected',
detail: {
address: restoreDevice.address
}
};
this._emitEvent(event);
});
}
});
});
},
/**
* Record connected device so if Bluetooth is turned off and then on
* we can restore the connection.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {String} action - to set or remove item for recording connection
* @param {String} address - the address of connected device
*/
_recordConnection: function btcm__recordConnection(action, address) {
if (action === 'set') {
// record connected device so if Bluetooth is turned off and then on
// we can restore the connection
AsyncStorage.setItem('device.connected', address);
debug('_recordConnection(): set item');
} else if ((action === 'remove') &&
(this._defaultAdapter.state === 'enabled')) {
// Clean up the connected device from async storage
// which is recorded before.
// Only remove the record while Bluetooth state is enabled.
// Because the request also comes while Bluetooth is turned off.
AsyncStorage.removeItem('device.connected');
debug('_recordConnection(): remove item');
}
},
/**
* Update connection status.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {Object} options
* @param {String} options.address - address of the device
* @param {Boolean} options.connected - is connected or not
* @param {Object} options.profileID - profile ID of the connection type
*/
_updateConnectionStatus: function btcm___updateConnectionStatus(options) {
debug('_updateConnectionStatus(): address = ' + options.address +
', connected = ' + options.connected +
', profileID = ' + options.profileID);
this.connectingAddress = null;
// Update the profile in the cache of connected device info.
this._updateConnectedDevices(options).then((connectedDevicesInfo) => {
debug('_updateConnectionStatus(): _updateConnectedDevices() ' +
'resolved with connectedDevicesInfo = ' +
JSON.stringify(connectedDevicesInfo));
// Prepare latest data to check connected profile.
var dataToCheckConnectedProfile = {
address: options.address,
connectedDevices: connectedDevicesInfo
};
// Fire 'connected'/'disconnected' event according to
// the connection profile. Then, record connection.
var event;
if (options.connected) {
// Fire 'connected' event.
event = {
type: 'connected',
detail: {
address: options.address
}
};
this._emitEvent(event);
// Record connection.
this._recordConnection('set', options.address);
} else {
// If there is no profile connected,
// we have to remove the record connection.
// And fire 'disconnected' event for outer modules.
if (!this._hasConnectedProfileByAddress(
dataToCheckConnectedProfile)) {
// Record connection. Only remove the record while Bluetooth state
// is enabled. Because the event also comes while Bluetooth is
// turned off.
if (this._defaultAdapter.state === 'enabled') {
this._recordConnection('remove');
}
// Fire 'disconnected' event.
event = {
type: 'disconnected',
detail: {
address: options.address
}
};
this._emitEvent(event);
}
}
// Fire 'profileChanged' event.
var newProfiles;
if (!this._hasConnectedProfileByAddress(dataToCheckConnectedProfile)) {
newProfiles = null;
} else {
newProfiles = connectedDevicesInfo[options.address].connectedProfiles;
}
event = {
type: 'profileChanged',
detail: {
address: options.address,
profiles: newProfiles
}
};
this._emitEvent(event);
}, (reason) => {
debug('_updateConnectionStatus(): miss to update in rejected case, ' +
'reason = ' + reason);
});
},
/**
* It provides a convenient function for panel to connect with a device.
* And the panel no need to care about connected device currently.
* The method will disconnect current connected device first.
* Then, it will start connecting a paired device with the device's adapter.
*
* @access public
* @memberOf BluetoothConnectionManager
* @param {BluetoothDevice} device
* @return {Promise}
*/
connect: function btcm_connect(device) {
if (!this._defaultAdapter) {
return Promise.reject('default adapter is not existed!!');
}
// Disconnect current connected device first.
debug('connect(): Want to connect device address = ' + device.address +
', name = ' + device.name);
var connectedDevices = [];
return this.getConnectedDevices().then((connectedDevicesInfo) => {
for (var address in connectedDevicesInfo) {
if (connectedDevicesInfo[address].device !== null) {
connectedDevices.push(connectedDevicesInfo[address].device);
debug('connect(): push device cache in queue = ' +
JSON.stringify(connectedDevices));
} else {
var regetPairedDevice = this._getPairedDeviceByAddress(address);
connectedDevices.push(regetPairedDevice);
debug('connect(): push _getPairedDeviceByAddress in queue = ' +
JSON.stringify(regetPairedDevice));
}
}
debug('connect(): Will disconnect these connected devices = ' +
JSON.stringify(connectedDevices));
// Disconnect these connected device before
// service to connect with new device.
return Promise.all(connectedDevices.map((connectedDevice) => {
return this.disconnect(connectedDevice);
})).then(() => {
// All connected devices is disconnected.
// We can start to connect the new request.
debug('connect(): Start to connect with wanted device address = ' +
device.address);
return this._connect(device).then(() => {
debug('connect(): Resolved');
}, (reason) => {
debug('connect(): reason = ' + reason);
return Promise.reject(reason);
});
});
});
},
/**
* The method will connect the input device with the device's adapter.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {BluetoothDevice} device
* @returns {Promise}
*/
_connect: function btcm__connect(device) {
if (!this._defaultAdapter) {
return Promise.reject('default adapter is not existed!!');
}
// Save the connecting address
this.connectingAddress = device.address;
// Fire 'connecting' event.
var event = {
type: 'connecting',
detail: {
address: device.address
}
};
this._emitEvent(event);
debug('_connect(): before start to connect, stop discovery first');
// Note on Bluedroid stack, discovery has to be stopped before connect
// (i.e., call stopDiscovery() before connect())
// otherwise stack callbacks with connect failure.
return this._defaultAdapter.stopDiscovery().then(() => {
debug('_connect(): start connecting device, ' +
'address = ' + device.address);
return this._defaultAdapter.connect(device).then(() => {
debug('_connect(): resolve, already connected with address = ' +
device.address);
}, () => {
// No available profiles are connected. Reset connecting address.
this.connectingAddress = null;
// Fire 'disconnected' event.
event = {
type: 'disconnected',
detail: {
address: device.address
}
};
this._emitEvent(event);
debug('_connect(): reject with connection failed');
return Promise.reject('connection failed');
});
}, () => {
// Cannot connect with the device since stopDiscovery failed.
this.connectingAddress = null;
// Fire 'disconnected' event.
event = {
type: 'disconnected',
detail: {
address: device.address
}
};
this._emitEvent(event);
debug('_connect(): reject with stop discovery failed');
return Promise.reject('stop discovery failed');
});
},
/**
* The method will disconnect the input device with the device's adapter.
*
* @access public
* @memberOf BluetoothConnectionManager
* @param {BluetoothDevice} device
* @returns {Promise}
*/
disconnect: function btcm_disconnect(device) {
if (!this._defaultAdapter) {
return Promise.reject('default adapter is not existed!!');
}
return this._defaultAdapter.disconnect(device).then(() => {
debug('disconnect(): onsuccess(): resolve');
}, () => {
debug('disconnect(): onerror(): reject with disconnect failed');
return Promise.reject('disconnect failed');
});
},
/**
* The method will get all connected devices profiles
* which we are interested in. Profile: HFP, A2DP.
*
* @access private
* @memberOf BluetoothConnectionManager
* @returns {Promise} resolve: connectedDevices
* @returns {Promise} reject: reason
*/
_getConnectedDevicesFromPlatform:
function btcm__getConnectedDevicesFromPlatform() {
// Get connected device via profiles HFP, A2DP
return Promise.all(Object.keys(this.Profiles).map((profile) => {
return this._getConnectedDevicesByProfile(this.Profiles[profile]);
})).then((connectedDevices) => {
// Update each connected devices in map.
var collectedConnectedDevicesByProfile = {};
Object.keys(this.Profiles).forEach((profile, index) => {
collectedConnectedDevicesByProfile[profile] = connectedDevices[index];
});
debug('_getConnectedDevicesFromPlatform(): ' +
'collectedConnectedDevicesByProfile = ' +
JSON.stringify(collectedConnectedDevicesByProfile));
return Promise.resolve(collectedConnectedDevicesByProfile);
});
},
/**
* The method will get connected device by inputed profile.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {String} profileID
* @returns {Promise} resolve: the connected devices in array
* @returns {Promise} reject: reason
*/
_getConnectedDevicesByProfile:
function btcm__getConnectedDevicesByProfile(profileID) {
if (!this._defaultAdapter) {
debug('_getConnectedDevicesByProfile(): reject with no adapter');
return Promise.reject('default adapter is not existed!!');
}
if (this._defaultAdapter.state === 'disabled') {
debug('_getConnectedDevicesByProfile(): resolve with empty array ' +
'since it is impossible to connect with any device');
return Promise.reject('getConnectedDevices in disabled state');
}
return this._defaultAdapter.getConnectedDevices(profileID).then(
(connectedDevice) => {
debug('_getConnectedDevicesByProfile(): resolved with ' +
'connectedDevice = ' + JSON.stringify(connectedDevice));
return Promise.resolve(connectedDevice || []);
}, (reason) => {
debug('_getConnectedDevicesByProfile(): rejected with ' +
'reason = ' + reason);
return Promise.reject(reason);
});
},
/**
* Get device from paired devices by address.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {String} address
*/
_getPairedDeviceByAddress:
function btcm__getPairedDeviceByAddress(address) {
if (!this._defaultAdapter) {
return null;
}
var pairedDevices = this._defaultAdapter.getPairedDevices();
if (pairedDevices.length === 0) {
return null;
} else {
for (var i in pairedDevices) {
if (pairedDevices[i].address === address) {
return pairedDevices[i];
}
}
return null;
}
},
/**
* Find out there is any profile still connected.
*
* @access private
* @memberOf BluetoothConnectionManager
* @param {Object} options
* @param {String} options.connectedDevices - connected devices info
* @param {String} options.address - the address of device
*/
_hasConnectedProfileByAddress:
function btcm__hasConnectedProfileByAddress(options) {
if (!options.connectedDevices[options.address]) {
return false;
}
var hasConnectedProfile = false;
for (var profileID in this.Profiles) {
var connectedProfiles =
options.connectedDevices[options.address].connectedProfiles;
if (connectedProfiles && (connectedProfiles[profileID] === true)) {
hasConnectedProfile = true;
}
}
return hasConnectedProfile;
},
/**
* A function to emit event to each registered listener by the input type.
*
* @memberOf BluetoothConnectionManager
* @param {Object} options
* @param {String} options.type - type of event name
* @param {Object} options.detail - the object pass to the listener
*/
_emitEvent: function btcm__emitEvent(options) {
this._listeners[options.type].forEach(function(listener) {
listener(options);
});
},
/**
* The method will provide event listener for outer modules to regist.
*
* @access public
* @memberOf BluetoothConnectionManager
* @param {String} eventName
* @param {Function} callback
*/
addEventListener: function btcm_addEventListener(eventName, callback) {
if (callback && (this._listeners.hasOwnProperty(eventName))) {
this._listeners[eventName].push(callback);
}
},
/**
* The method will provide event listener for outer modules to un-regist.
*
* @access public
* @memberOf BluetoothConnectionManager
* @param {String} eventName
* @param {Function} callback
*/
removeEventListener:
function btcm_removeEventListener(eventName, callback) {
if (callback && (this._listeners.hasOwnProperty(eventName))) {
var index = this._listeners[eventName].indexOf(callback);
if (index >= 0) {
this._listeners[eventName].splice(index, 1);
}
}
}
};
BluetoothConnectionManager._init();
return BluetoothConnectionManager;
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1 @@
define(["require","modules/bluetooth/bluetooth_cod_mapper","modules/mvvm/observable"],function(e){"use strict";var t=e("modules/bluetooth/bluetooth_cod_mapper"),i=e("modules/mvvm/observable"),n=!1,o=function(){};n&&(o=function(e){console.log("--> [BluetoothDevice]: "+e)});var s=function(e){o("updateConnectionInfo(): this.address = "+this.address+", this.name = "+this.name+", options = "+JSON.stringify(e)),e.connectionStatus&&(this.connectionStatus=e.connectionStatus),e.profiles&&(this.profiles=Object.create(e.profiles)),o("updateConnectionInfo(): this.connectionStatus = "+this.connectionStatus+", this.profiles = "+JSON.stringify(this.profiles))},c=function(){if(o("updateDescriptionText():"),this.paired===!1)this.descriptionText="tapToConnect";else if("pairing"===this.paired)this.descriptionText="pairing";else if(this.paired===!0)if(this.descriptionText="paired","connecting"===this.connectionStatus)this.descriptionText="connecting";else if("connected"===this.connectionStatus)if(this.profiles){var e=this.profiles.hfp,t=this.profiles.a2dp;e&&t?this.descriptionText="connectedWithDeviceMedia":e?this.descriptionText="connectedWithDevice":t?this.descriptionText="connectedWithMedia":this.descriptionText="connectedWithNoProfileInfo"}else this.descriptionText="connectedWithNoProfileInfo";else"disconnected"===this.connectionStatus&&(this.descriptionText="disconnected");o("updateDescriptionText(): this.descriptionText = "+this.descriptionText)};return function(e){var n=t.getDeviceType(e.cod),r=e.connectionStatus||"disconnected",d=i({name:e.name,paired:e.paired,address:e.address,type:n,connectionStatus:r,profiles:null,descriptionText:"",get data(){return e},updateConnectionInfo:s,updateDescriptionText:c});return d._init=function(){this.observe("paired",this.updateDescriptionText.bind(this)),this.observe("connectionStatus",this.updateDescriptionText.bind(this)),this.observe("profiles",this.updateDescriptionText.bind(this))},e.onattributechanged=function(i){for(var n in i.attrs)switch(o("onDeviceAttributeChanged(): "+i.attrs[n]),i.attrs[n]){case"name":d.name=e.name;break;case"paired":o("onDeviceAttributeChanged(): device.paired = "+e.paired),d.paired=e.paired;break;case"cod":o("onDeviceAttributeChanged(): device.cod = "+e.cod),d.type=t.getDeviceType(e.cod)}},d._init(),d}});

View file

@ -0,0 +1 @@
define(["require","modules/mvvm/observable_array"],function(e){var n=new WeakMap,r=e("modules/mvvm/observable_array"),t=function(e,t,i){function o(e){var n=u.get(e);if(n&&"object"==typeof n){var r=u.get(n);r&&(r.forEach(function(e){n.unobserve(e)}),u["delete"](n),u["delete"](e))}}function l(e,n){var r=u.get(e)||[];u.has(e)&&(r=u.get(e)),r=Object.keys(n).map(function(r){return e.observe(r,n[r]),n[r](),n[r]}),u.set(e,r)}var a=null,s=e,c=i,f=!0,u=new WeakMap,v={observeAndCall:l},d=function(e){if(f){var n=e.data;switch(e.type){case"insert":b(n.index,n.items);break;case"remove":h(n.index,n.count);break;case"replace":m(n.index,n.newValue);break;case"reset":g(n.items||[])}}},b=function(e,n){if(!(n.length<=0))for(var r=s.children[e],t=n.length-1;t>=0;t--){var i=c(n[t],void 0,v);u.set(i,n[t]),s.insertBefore(i,r),r=i}},h=function(e,n){if(0!==n)if(n>=s.childElementCount)for(;s.firstElementChild;)o(s.firstElementChild),s.removeChild(s.firstElementChild);else for(var r=s.children[e],t=0;n>t&&r;t++){var i=r.nextElementSibling;o(r),s.removeChild(r),r=i}},m=function(e,n){var r=s.children[e];if(r){o(r);var t=c(n,r,v);u.set(r,n),t!==r&&(s.insertBefore(t,r),s.removeChild(r))}},g=function(e){var n=e.length,r=s.childElementCount;if(0===n)h(0,r);else if(r>=n)e.forEach(function(e,n){m(n,e)}),h(n,r-n);else{var t=e.slice(0,r),i=e.slice(r);t.forEach(function(e,n){m(n,e)}),b(r,i)}},y=function(){f&&g(a.array)},C={set:function(e){return a&&a.unobserve(d),e?(Array.isArray(e)&&(e=r(e)),a=e,a.observe("insert",d),a.observe("remove",d),a.observe("replace",d),a.observe("reset",d),void(this.enabled&&g(a.array))):void(a&&(h(0,a.length),a=null))},destroy:function(){a&&a.unobserve(d),h(0,a.length),n["delete"](s),s=null,a=null,c=null,f=!1},get element(){return s},set enabled(e){f!==e&&(f=e,y())},get enabled(){return f}};return n.has(s)&&n.get(s).destroy(),n.set(s,C),s.innerHTML="",C.set(t),C};return t});

View file

@ -0,0 +1 @@
define([],function(){function n(n,e,r){function t(e,r){var t=n[r];if(t){var i=t.indexOf(e);i>=0&&t.splice(i,1)}}"function"==typeof e?Object.keys(n).forEach(t.bind(null,e)):r?t(r,e):e in n&&(n[e]=[])}function e(e){var r={},t={observe:function(n,e){var t=r[n];t&&t.push(e)},unobserve:n.bind(null,r)},i=function(n){return function(){return t["_"+n]}},f=function(n){return function(e){var i=t["_"+n];if(i!==e){t["_"+n]=e;var f=r[n];f.forEach(function(n){n(e,i)})}}};for(var u in e)"function"!=typeof e[u]?(r[u]=[],Object.defineProperty(t,"_"+u,{value:e[u],writable:!0}),Object.defineProperty(t,u,{enumerable:!0,get:i(u),set:f(u)})):t[u]=e[u];return t}return e});

View file

@ -0,0 +1 @@
define([],function(){function n(n,e,t){function r(e,t){var r=n[t];if(r){var i=r.indexOf(e);i>=0&&r.splice(i,1)}}"function"==typeof e?Object.keys(n).forEach(r.bind(null,e)):t?r(t,e):e in n&&(n[e]=[])}function e(e){var t=e||[],r={insert:[],remove:[],replace:[],reset:[]},i=function(n,e){var t=r[n];t.forEach(function(t){t({type:n,data:e})})};return{get length(){return t.length},get array(){return t},forEach:function(n){t.forEach(n)},observe:function(n,e){var t=r[n];t&&t.push(e)},unobserve:n.bind(null,r),push:function(n){t.push(n),i("insert",{index:t.length-1,count:1,items:[n]})},pop:function(){if(t.length){var n=t.pop();return i("remove",{index:t.length,count:1}),n}},splice:function(n,e){if(!(arguments.length<2)){var r=Array.prototype.slice.call(arguments,2);t.splice.apply(t,arguments),e&&i("remove",{index:n,count:e}),r.length&&i("insert",{index:n,count:r.length,items:r})}},set:function(n,e){if(!(0>n||n>=t.length)){var r=t[n];t[n]=e,i("replace",{index:n,oldValue:r,newValue:e})}},get:function(n){return t[n]},reset:function(n){t=n,i("reset",{items:t})}}}return e});

View file

@ -0,0 +1 @@
define([],function(){"use strict";return navigator.mozBluetooth?navigator.mozBluetooth:null});

View file

@ -0,0 +1,430 @@
/* -*- Mode: js; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* global NotificationHelper */
define(['require','modules/bluetooth/bluetooth_adapter_manager','modules/bluetooth/bluetooth_context','views/pair_expired_dialog'],function(require) {
'use strict';
var AdapterManager = require('modules/bluetooth/bluetooth_adapter_manager');
var BtContext = require('modules/bluetooth/bluetooth_context');
var PairExpiredDialog = require('views/pair_expired_dialog');
var _debug = false;
var debug = function() {};
if (_debug) {
debug = function btam_debug(msg) {
console.log('--> [PairManager]: ' + msg);
};
}
/*
* PairManager is responsible for:
* 1. Handling system message 'bluetooth-pairing-request' while there is an
* incoming/outgoing pairing request.
* 2. Handling dom event from BluetoothPairingListener while there is an
* incoming/outgoing pairing request.
* 3. Handling dom event 'ondeviceunpaired' while some remote devices
* request for canceling an overdue pairing request. The reason could be
* authentication fails, remote device down, and internal error happens.
*/
var PairManager = {
/**
* Default adapter of Bluetooth.
*
* @access private
* @memberOf PairManger
* @type {Object} BluetoothAdapter
*/
_defaultAdapter: null,
/**
* Indicate if the pair request is expired
* @type {Boolean}
*/
_isExpired: false,
init: function() {
// Observe 'defaultAdapter' property for reaching default adapter.
AdapterManager.observe('defaultAdapter',
this._onDefaultAdapterChanged.bind(this));
this._onDefaultAdapterChanged(AdapterManager.defaultAdapter);
// Watch pairing events.
this._watchOndisplaypasskeyreq();
this._watchOnenterpincodereq();
this._watchOnpairingconfirmationreq();
this._watchOnpairingconsentreq();
this._watchOnpairingaborted();
navigator.mozSetMessageHandler('bluetooth-pairing-request',
this._onRequestPairingFromSystemMessage.bind(this)
);
// Observe screen lockscreen locked/unlocked state for show pending
// pairing request immediately.
navigator.mozSettings.addObserver('lockscreen.locked',
function gotScreenLockedChanged(event) {
this.showPendingPairing(event.settingValue);
}.bind(this));
// Observe Bluetooth 'enabled' property from hardware side.
// Then, close pairing dialog immediately.
BtContext.observe('enabled', (enabled) => {
if (!enabled) {
this.onBluetoothDisabled();
}
});
},
/**
* 'defaultAdapter' change event handler from adapter manager for
* updating it immediately.
*
* @access private
* @memberOf PairManager
* @param {Object} BluetoothAdapter newAdapter
*/
_onDefaultAdapterChanged: function(newAdapter) {
// save default adapter
this._defaultAdapter = newAdapter;
},
/**
* Watch 'ondisplaypasskeyreq' dom event for pairing.
* A handler to trigger when a remote bluetooth device requests to display
* passkey on the screen during pairing process.
*
* @access private
* @memberOf PairManager
*/
_watchOndisplaypasskeyreq: function() {
if (!this._defaultAdapter || !this._defaultAdapter.pairingReqs) {
debug('_watchOndisplaypasskeyreq() no adapter or pairingReqs');
return;
}
this._defaultAdapter.pairingReqs.addEventListener('displaypasskeyreq',
(evt) => {
debug('_watchOndisplaypasskeyreq()');
this._handlePairingRequest({
method: 'displaypasskey',
evt: evt
});
});
},
/**
* Watch 'onenterpincodereq' dom event for pairing.
* A handler to trigger when a remote bluetooth device requests user enter
* PIN code during pairing process.
*
* @access private
* @memberOf PairManager
*/
_watchOnenterpincodereq: function() {
if (!this._defaultAdapter || !this._defaultAdapter.pairingReqs) {
debug('_watchOnenterpincodereq() no adapter or pairingReqs');
return;
}
this._defaultAdapter.pairingReqs.addEventListener('enterpincodereq',
(evt) => {
debug('_watchOnenterpincodereq()');
this._handlePairingRequest({
method: 'enterpincode',
evt: evt
});
});
},
/**
* Watch 'onpairingconfirmationreq' dom event for pairing.
* A handler to trigger when a remote bluetooth device requests user
* confirm passkey during pairing process. Applications may prompt passkey
* to user for confirmation, or confirm the passkey for user proactively.
*
* @access private
* @memberOf PairManager
*/
_watchOnpairingconfirmationreq: function() {
if (!this._defaultAdapter || !this._defaultAdapter.pairingReqs) {
debug('_watchOnpairingconfirmationreq() no adapter or pairingReqs');
return;
}
this._defaultAdapter.pairingReqs.addEventListener(
'pairingconfirmationreq', (evt) => {
debug('_watchOnpairingconfirmationreq: evt = ' + JSON.stringify(evt));
this._handlePairingRequest({
method: 'confirmation',
evt: evt
});
});
},
/**
* Watch 'onpairingconsentreq' dom event for pairing.
* A handler to trigger when a remote bluetooth device requests user
* confirm pairing during pairing process. Applications may prompt user
* for confirmation or confirm for user proactively.
*
* @access private
* @memberOf PairManager
*/
_watchOnpairingconsentreq: function() {
if (!this._defaultAdapter || !this._defaultAdapter.pairingReqs) {
debug('_watchOnpairingconsentreq() no adapter or pairingReqs');
return;
}
this._defaultAdapter.pairingReqs.addEventListener('pairingconsentreq',
(evt) => {
debug('_watchOnpairingconsentreq: evt = ' + JSON.stringify(evt));
this._handlePairingRequest({
method: 'consent',
evt: evt
});
});
},
/**
* Watch 'onpairingaborted' dom event for pairing aborted.
* A handler to trigger when pairing fails due to one of
* following conditions:
* - authentication fails
* - remote device down (bluetooth ACL becomes disconnected)
* - internal error happens
*
* @access private
* @memberOf PairManager
*/
_watchOnpairingaborted: function() {
if (!this._defaultAdapter) {
return;
}
this._defaultAdapter.addEventListener('pairingaborted',
this._onPairingAborted.bind(this));
},
/**
* A handler to handle 'onpairingaborted' event while it's coming.
*
* @access private
* @memberOf PairManager
*/
_onPairingAborted: function(evt) {
debug('_onPairingAborted(): evt = ' + JSON.stringify(evt));
// if the attention screen still open, close it
if (this.childWindow) {
this.childWindow.Pairview.closeInput();
this.childWindow.close();
}
// If "onpairingaborted" event is coming, and there is a pending pairing
// request, we have reset the object. Because the callback is useless
// since the older pairing request is expired.
// The moment is fit to notify user that the later pairing request
// is timeout or canceled. According to user story, do nothing here.
// Clear up the pending pairing request only.
if (this.pendingPairing) {
this.pendingPairing = null;
}
// Close pairing expired dialog if it is showing.
if (PairExpiredDialog.isVisible) {
PairExpiredDialog.close();
// Have to close Bluetooth app after the dialog is closed.
window.close();
}
},
/**
* Receive the system message event for launch Bluetooth app here.
*
* @access private
* @memberOf PairManager
*/
_onRequestPairingFromSystemMessage: function() {
debug('onRequestPairingFromSystemMessage():');
},
/**
* It is used to handle each pairing request from different pairing methods.
*
* @memberOf PairManager
* @access private
* @param {Object} pairingInfo
* @param {Object} pairingInfo.method - method of this pairing request
* @param {Object} pairingInfo.evt - DOM evt of this pairing request
*/
_handlePairingRequest: function(pairingInfo) {
debug('_onRequestPairing():' +
' pairingInfo.method = ' + pairingInfo.method +
' pairingInfo.evt = ' + pairingInfo.evt);
var req = navigator.mozSettings.createLock().get('lockscreen.locked');
req.onsuccess = () => {
if (req.result['lockscreen.locked']) {
// notify user that we are receiving pairing request
this.fireNotification(pairingInfo);
} else {
// We have clear up pending one before show the new pairing requst.
// Becasue the pending pairing request is no longer usefull.
this.cleanPendingPairing();
// show pair view directly while lock screen is unlocked
this.showPairview(pairingInfo);
}
};
req.onerror = () => {
// We have clear up pending one before show the new pairing requst.
// Becasue the pending pairing request is no longer usefull.
this.cleanPendingPairing();
// fallback to default value 'unlocked'
this.showPairview(pairingInfo);
};
},
fireNotification: function(pairingInfo) {
// Once we received a pairing request in screen locked mode,
// overwrite the object with the latest pairing request. Because the
// older pairing request might be timeout and useless now.
this.pendingPairing = {
showPairviewCallback: this.showPairview.bind(this, pairingInfo)
};
// Prepare notification toast.
var titleL10n = 'bluetooth-pairing-request-now-title';
var bodyL10n = pairingInfo.evt.deviceName ?
{ raw: pairingInfo.evt.deviceName } :
'unnamed-device';
var iconUrl =
'app://bluetooth.gaiamobile.org/style/images/icon_bluetooth.png';
// We always use tag "pairing-request" to manage these notifications.
var notificationId = 'pairing-request';
return NotificationHelper.send(titleL10n, {
bodyL10n: bodyL10n,
icon: iconUrl,
tag: notificationId,
closeOnClick: false
}).then(notification => {
// set onclick handler for the notification
notification.onclick =
this.pairingRequestExpiredNotificationHandler.bind(this,
notification);
});
},
// According to user story, it won't notify user again
// while the pending pairing request is just timeout or canceled.
// So we have to set onclick handler in this moment.
// The handler will pop out a pairing request expired prompt only.
pairingRequestExpiredNotificationHandler: function(notification) {
var req = navigator.mozSettings.createLock().get('lockscreen.locked');
req.onsuccess = () => {
// Avoid to do nothting while the notification toast is showing
// and a user is able to trigger onclick event. Make sure screen
// is unlocked, then show the prompt in bluetooth app.
if (!req.result['lockscreen.locked']) {
// Clean the pairing request notficiation which is expired.
notification.close();
navigator.mozApps.getSelf().onsuccess = (evt) => {
var app = evt.target.result;
// launch bluetooth app to foreground for showing the prompt
if (PairExpiredDialog.isVisible) {
app.launch();
}
// show an alert with the overdue message
if (!PairExpiredDialog.isVisible && this._isExpired) {
debug('show expired dialog');
PairExpiredDialog.showConfirm(function() {
// Have to close Bluetooth app after the dialog is closed.
window.close();
});
}
};
}
};
},
// If there is a pending pairing request while a user just unlocks screen,
// we will show pair view immediately. Then, we clear up the notification.
// If pendingPairing object is not exist, it means pair request is expired.
showPendingPairing: function(screenLocked) {
debug('showPendingPairing when screenLocked:' + screenLocked);
if (!screenLocked) {
if (this.pendingPairing) {
this._isExpired = false;
// show pair view from the callback function
if (this.pendingPairing.showPairviewCallback) {
this.pendingPairing.showPairviewCallback();
}
this.cleanPendingPairing();
} else {
this._isExpired = true;
}
}
},
cleanPendingPairing: function() {
debug('cleanPendingPairing(): has pendingPairing = ' +
(this.pendingPairing));
// Clear up the pending pairing request
if (this.pendingPairing) {
this.pendingPairing = null;
}
// Clear up the pairing request from notification.
this.cleanNotifications();
},
// Clean all notifications which are fired with tag 'pairing-request'.
cleanNotifications: function() {
Notification.get().then(function(notifications) {
if (notifications) {
notifications.forEach(function(notification) {
// Compare tags, as the tag is based on the "pairing-request" and
// we only have one notification for the request. Plus, there
// is no "id" field on the notification.
if (notification.tag === 'pairing-request' &&
notification.close) {
notification.close();
}
});
}
});
},
showPairview: function(pairingInfo) {
debug('showPairview(): pairingInfo = ' + pairingInfo);
var protocol = window.location.protocol;
var host = window.location.host;
this.childWindow = window.open(protocol + '//' + host + '/onpair.html',
'pair_screen', 'attention');
this.childWindow.onload = () => {
this.childWindow.Pairview.init(pairingInfo.method, pairingInfo.evt);
};
},
onBluetoothDisabled: function() {
debug('onBluetoothDisabled():');
// if the attention screen still open, close it
if (this.childWindow) {
this.childWindow.Pairview.closeInput();
this.childWindow.close();
}
// Since Bluetooth is off, close itself.
window.close();
}
};
return PairManager;
});

View file

@ -0,0 +1,349 @@
/*
* TransferManager is responsible for:
* - Handling system message 'activity' while there is an activity request
* incoming. Then, it will post result/error to requesting app.
* - Watching Bluetooth enabled/disabled state, then show/hide turn Bluetooth
* on dialog.
* - Watching default adapter existed or not. And will post error if we cannot
* get default adapter.
* - Watching 'devicePicked', 'cancelSelection' events from DevicePickerPanel
* module. Then, do corrected activity service flow regarding user decision.
*
* @module BluetoothContext
*/
define(['require','modules/bluetooth/bluetooth_adapter_manager','modules/bluetooth/bluetooth_context','views/cannot_transfer_dialog','views/device_picker_panel','views/turn_bluetooth_on_dialog'],function(require) {
'use strict';
var AdapterManager = require('modules/bluetooth/bluetooth_adapter_manager');
var BtContext = require('modules/bluetooth/bluetooth_context');
var CannotTransferDialog = require('views/cannot_transfer_dialog');
var DevicePickerPanel = require('views/device_picker_panel');
var TurnBluetoothOnDialog = require('views/turn_bluetooth_on_dialog');
var _debug = false;
var debug = function() {};
if (_debug) {
debug = function bttm_debug(msg) {
console.log('--> [TransferManager]: ' + msg);
};
}
var TransferManager = {
/**
* An instance to access activity source.
*
* @access private
* @memberOf TransferManager
* @type {Object}
*/
_activity: null,
/**
* The init function only watches the 'activity' event from system message.
*
* @access private
* @memberOf TransferManager
*/
init: function bttm_init() {
// Watch 'activity' event from system message.
this._watchActivityRequest();
},
/**
* Watch 'activity' event with callback function from system message.
*
* @access private
* @memberOf TransferManager
*/
_watchActivityRequest: function bttm__watchActivityRequest() {
navigator.mozSetMessageHandler('activity',
this._activityHandler.bind(this));
},
/**
* Handle 'activity' request. Then make decision to service the request or
* reject it.
*
* @access private
* @memberOf TransferManager
* @param {Object} activityRequest
*/
_activityHandler: function bttm__activityHandler(activityRequest) {
debug('_activityHandler(): activityRequest = ' +
JSON.stringify(activityRequest));
if (activityRequest.source.data.blobs &&
activityRequest.source.data.blobs.length > 0) {
this._activity = activityRequest;
// Source info is enough to servie file transfer.
// Start to init and observe Bluetooth relative modules.
this._observeDefaultAdapter();
this._observeBluetoothEnabled();
this._watchEventFromDevicePicker();
DevicePickerPanel.visible = true;
} else {
debug('_activityHandler(): Cannot transfer without blobs data!');
this._cannotTransfer();
}
},
/**
* Show alert dialog. End the activity service while a user confirm to close
* the dialog.
*
* @access private
* @memberOf TransferManager
*/
_cannotTransfer: function bttm__cannotTransfer() {
debug('_cannotTransfer(): show alert dialog!!');
// show an alert with the overdue message
if (!CannotTransferDialog.isVisible) {
CannotTransferDialog.showConfirm().then(() => {
// Post error with reason then end the activity service.
this._endTransferWithReason('cancelled');
});
}
},
/**
* Post result/error to end the activity service.
*
* @access private
* @memberOf TransferManager
*/
_endTransferWithReason: function bttm__endTransferWithReason(reason) {
debug('_endTransferWithReason(): reason = ' + reason);
if (reason === 'transferred') {
this._activity.postResult(reason);
} else if (reason === 'cancelled') {
this._activity.postError(reason);
}
this._activity = null;
},
/**
* Observe and get default adapter from bluetooth adapter manager.
*
* @access private
* @memberOf TransferManager
*/
_observeDefaultAdapter: function bttm__observeDefaultAdapter() {
// Observe 'defaultAdapter' property for reaching default adapter.
AdapterManager.observe('defaultAdapter',
this._onDefaultAdapterChanged.bind(this));
this._onDefaultAdapterChanged(AdapterManager.defaultAdapter);
},
/**
* Observe and get 'enabled' property from bluetooth context.
*
* @access private
* @memberOf TransferManager
*/
_observeBluetoothEnabled: function bttm__observeBluetoothEnabled() {
// Observe Bluetooth 'enabled' property from hardware side.
BtContext.observe('enabled', this._onBluetoothEnabledChanged.bind(this));
this._onBluetoothEnabledChanged(BtContext.enabled);
},
/**
* 'defaultAdapter' change event handler from adapter manager for
* updating it immediately.
*
* @access private
* @memberOf TransferManager
* @param {Object} BluetoothAdapter newAdapter
* @param {Object} BluetoothAdapter oldAdapter
*/
_onDefaultAdapterChanged:
function bttm__onDefaultAdapterChanged(newAdapter, oldAdapter) {
if (oldAdapter && (newAdapter === null)) {
debug('_onDefaultAdapterChanged(): Can not get bluetooth adapter!');
// Post error with reason then end the activity service.
this._endTransferWithReason('cancelled');
}
},
/**
* 'enabled' change event handler from bluetooth context for
* updating UI flow immediately.
*
* @access private
* @memberOf TransferManager
* @param {Boolean} enabled
*/
_onBluetoothEnabledChanged:
function bttm__onBluetoothEnabledChanged(enabled) {
debug('_onBluetoothEnabledChanged(): enabled = ' + enabled);
switch(enabled) {
case undefined:
break;
case true:
// Close the confirmation dialog
TurnBluetoothOnDialog.close();
break;
default:
TurnBluetoothOnDialog.showConfirm().then((result) => {
if (result === 'confirm') {
// Turn Bluetooth on.
BtContext.setEnabled(true);
} else if (result === 'cancel') {
// Post error with reason then end the activity service.
this._endTransferWithReason('cancelled');
}
});
break;
}
},
/**
* Watch 'devicePicked', 'cancelSelection' event from device picker panel.
*
* @access private
* @memberOf TransferManager
*/
_watchEventFromDevicePicker: function bttm__watchEventFromDevicePicker() {
debug('_watchEventFromDevicePicker():');
// Watch 'devicePicked' event for reaching device picked from user.
DevicePickerPanel.addEventListener('devicePicked',
this._onDevicePicked.bind(this));
// Watch 'cancelSelection' event for reaching cancel selection from user.
DevicePickerPanel.addEventListener('cancelSelection',
this._onCancelSelection.bind(this));
},
/**
* While the device is picked from user, let's send file(s) to the device.
*
* @access private
* @memberOf TransferManager
* @param {Object} event
* @param {String} event.type - type of event name
* @param {Object} event.detail - device info in this object
* @param {Object} event.detail.address - address of device
* @returns {Promise}
*/
_onDevicePicked: function bttm__onDevicePicked(event) {
// Send file to the device via address
debug('_onDevicePicked(): address of target device = ' +
event.detail.address);
var targetDeviceAddress = event.detail.address;
var blobs = this._activity.source.data.blobs;
// Produce sending files schedule.
// Then, post message to system app for sending files in queue.
var schedule = this._produceSendingFilesSchedule(blobs.length);
this._postMessageToSystemApp(schedule);
// Send each file via Bluetooth sendFile API
return Promise.all(blobs.map((blob, index) => {
/**
* Check blob.name because sendFile() API needs a "file"
* object and a filaname before sending it.
* If there is no filename in the blob, Bluetooth API will give a
* default name "Unknown.jpeg".
* So Bluetooth app has to find out the name via device storage.
*/
if (blob.name) {
// The blob has name, send the blob directly.
debug('blob is sending with name...');
return BtContext.sendFile(targetDeviceAddress, blob);
} else if (this._activity.source.data.filepaths) {
// The blob does not have name,
// browse the file via filepath from storage again.
var filepath = this._activity.source.data.filepaths[index];
var storage = navigator.getDeviceStorage('sdcard');
return storage.get(filepath).then((file) => {
debug('getFile succeed & file is sending... file = ' + file);
return BtContext.sendFile(targetDeviceAddress, file);
}, (error) => {
debug('getFile failed so that blob is sending without filename ' +
error && error.name);
return BtContext.sendFile(targetDeviceAddress, blob);
});
} else {
// The blob does not have name and filepath,
// pass the blob without name to send file directly.
debug('no filepath to get from device storage ' +
'so that blob is sending without filename');
return BtContext.sendFile(targetDeviceAddress, blob);
}
})).then(() => {
debug('all files are already sent out..');
// Post result with reason then end the activity service.
this._endTransferWithReason('transferred');
return Promise.resolve();
});
},
/**
* While a user cancel selection, let's post result then end the activity
* service.
*
* @access private
* @memberOf TransferManager
* @param {Object} event
* @param {String} event.type - type of event name
*/
_onCancelSelection: function bttm__onCancelSelection(event) {
debug('_onCancelSelection(): cancel from user decision');
// Post error with reason then end the activity service.
this._endTransferWithReason('cancelled');
},
/**
* A function is a role producer.
* Bluetooth app produce one message for each sending file request.
*
* @access private
* @memberOf TransferManager
* @param {Number} numberOfTasks - number of tasks
* @returns {Object}
*/
_produceSendingFilesSchedule:
function bttm__produceSendingFilesSchedule(numberOfTasks) {
// Construct a object to contain info of sending files schedule.
// And the info will be posted message to system app.
// The result of send files will be displayed on notification.
var sendingFilesSchedule = {
numberOfFiles: numberOfTasks,
numSuccessful: 0,
numUnsuccessful: 0
};
return sendingFilesSchedule;
},
/**
* A function to do inner app communcation.
*
* @access private
* @memberOf TransferManager
* @param {Object} message - the info of sending files schedule
*/
_postMessageToSystemApp:
function bttm__postMessageToSystemApp(message) {
// Set up Inter-App Communications
var getRequest = navigator.mozApps.getSelf();
getRequest.onsuccess = (evt) => {
var app = evt.target.result;
// If IAC doesn't exist, just bail out.
if (!app.connect) {
debug('_postMessageToSystemApp(): Cannot post message since no ' +
'connect function, message = ' + JSON.stringify(message));
return;
}
// Post message object to system app.
app.connect('bluetoothTransfercomms').then((ports) => {
ports.forEach((port) => {
port.postMessage(message);
debug('_postMessageToSystemApp(): sending files schedule = ' +
JSON.stringify(message));
});
});
};
}
};
return TransferManager;
});

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,16 @@
require(['config/require'], function() {
var _debug = false;
var debug = function() {};
if (_debug) {
debug = function btsp_debug(msg) {
console.log('--> [startup_pair]: ' + msg);
};
}
require(['modules/pair_manager'], function(PairManager) {
debug('Load pair manager with API version 2. Loaded!!');
navigator.mozL10n.once(() => PairManager.init());
});
});

View file

@ -0,0 +1,16 @@
require(['config/require'], function() {
var _debug = false;
var debug = function() {};
if (_debug) {
debug = function btst_debug(msg) {
console.log('--> [startup_transfer]: ' + msg);
};
}
require(['modules/transfer_manager'], function(TransferManager) {
debug('Load transfer manager with API version 2. Loaded!!');
navigator.mozL10n.once(() => TransferManager.init());
});
});

View file

@ -0,0 +1 @@
;function getTruncated(t,e){function i(t,e){return t!==e}var n=e.maxLine||2,l=e.node,o=e.ellipsisIndex||3,r=e.ellipsisCharacter||"...";if(null===l)return t;var s,a,c,u,d,y,h="",v=!0,p=[],f=t.slice(-o),g=l.style.visibility,x=l.style.wordBreak;l.style.visibility="hidden",l.style.wordBreak="break-all",l.textContent=".",c=l.clientHeight,l.textContent="";var C=t.split("").some(function(t,e){if(p.push(t),y=p.join(""),s=l.clientHeight,l.textContent=y+r+f,a=l.clientHeight,0===e&&(u=Math.floor(a/c)),i(s,a)&&0!==e){var o;l.textContent=y,o=l.clientHeight,i(s,o)?(v=!0,u+=1):v&&(d=e,v=!1)}return u>n?(0===e&&console.log("Your string is in a overflowed situation, please check your options"),p.pop(),l.textContent="",!0):void 0});return l.style.visibility=g,l.style.wordBreak=x,C?(h+=p.join("").slice(0,d),h+=r,h+=f):h=t,h}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
define([],function(){"use strict";function e(e,a,n){var c=n,r=document.createElement("span");t(r,c.name);var s=document.createElement("small");"remote"===e&&s.setAttribute("data-l10n-id","device-status-tap-connect");var o=document.createElement("li"),u=document.createElement("a");if(o.classList.add("bluetooth-device"),d("device.type = "+c.type),o.classList.add("bluetooth-type-"+c.type),i(o,s,c.descriptionText,e),u.appendChild(r),u.appendChild(s),o.appendChild(u),"remote"===e){var l=document.createElement("progress");l.classList.add("overlapping-icon"),l.classList.add("hidden"),o.appendChild(l)}return"function"==typeof a&&(o.onclick=function(){a(n)}),c.observe("name",function(e){t(r,e)}),c.observe("descriptionText",function(t){i(o,s,t,e)}),o}function t(e,t){""!==t?(e.removeAttribute("data-l10n-id"),e.textContent=t):e.setAttribute("data-l10n-id","unnamed-device")}function i(e,t,i,a){switch(d("_updateItemDescriptionText(): descriptionText = "+i),i){case"tapToConnect":e.removeAttribute("aria-disabled"),t.setAttribute("data-l10n-id","device-status-tap-connect"),"remote"===a&&(e.querySelector("progress").classList.add("hidden"),e.classList.remove("icon-hidden"));break;case"pairing":e.setAttribute("aria-disabled",!0),t.setAttribute("data-l10n-id","device-status-pairing"),"remote"===a&&(e.classList.add("icon-hidden"),e.querySelector("progress").classList.remove("hidden"));break;case"paired":e.removeAttribute("aria-disabled"),t.setAttribute("data-l10n-id","device-status-paired"),"remote"===a&&(e.querySelector("progress").classList.add("hidden"),e.classList.remove("icon-hidden"));break;case"connecting":e.setAttribute("aria-disabled",!0),t.setAttribute("data-l10n-id","device-status-connecting");break;case"connectedWithDeviceMedia":e.removeAttribute("aria-disabled"),t.setAttribute("data-l10n-id","device-status-connected-device-media");break;case"connectedWithDevice":e.removeAttribute("aria-disabled"),t.setAttribute("data-l10n-id","device-status-connected-device");break;case"connectedWithMedia":e.removeAttribute("aria-disabled"),t.setAttribute("data-l10n-id","device-status-connected-media");break;case"connectedWithNoProfileInfo":e.removeAttribute("aria-disabled"),t.setAttribute("data-l10n-id","device-status-connected");break;case"disconnected":e.removeAttribute("aria-disabled"),t.removeAttribute("data-l10n-id"),t.textContent=""}}var a=!1,d=function(){};return a&&(d=function(e){console.log("--> [BluetoothTemplateFactory]: "+e)}),function(t,i){return e.bind(null,t,i)}});

View file

@ -0,0 +1,37 @@
/*
* CannotTransferDialog is responsible for:
* Alert a user that the activity request is not able to transfer via
* Bluetooth with some reason. There is an alert handling via here.
*/
define([],function() {
'use strict';
var CannotTransferDialog = {
init: function() {
this.dialog = document.getElementById('alert-view');
this.confirmBtn =
document.getElementById('alert-button-ok');
},
showConfirm: function() {
return new Promise((resolve) => {
this.confirmBtn.onclick = () => {
this.close();
resolve();
};
this.dialog.hidden = false;
});
},
get isVisible() {
return (!this.dialog.hidden);
},
close : function() {
this.dialog.hidden = true;
}
};
CannotTransferDialog.init();
return CannotTransferDialog;
});

View file

@ -0,0 +1,413 @@
/*
* DevicePickerPanel is responsible for:
* - Render paired/found devices list in this panel.
* - Emit 'devicePicked' event with device address while a user click on a
* paired device.
* - Pair with the picked device while a user click on a found device.
* Emit 'devicePicked' event with device address until the picked device
* is pared.
* - Emit 'cancelSelection' event while a user click close button on the
* header for leaving.
*
* @module DevicePickerPanel
*/
define(['require','modules/bluetooth/bluetooth_context','views/bt_template_factory','modules/mvvm/list_view'],function(require) {
'use strict';
var BtContext = require('modules/bluetooth/bluetooth_context');
var BtTemplateFactory = require('views/bt_template_factory');
var ListView = require('modules/mvvm/list_view');
var _debug = false;
var debug = function() {};
if (_debug) {
debug = function btdpp_debug(msg) {
console.log('--> [DevicePickerPanel]: ' + msg);
};
}
var DevicePickerPanel = {
/**
* A instance to maintain paired devices list view.
*
* @access private
* @memberOf DevicePickerPanel
* @type {Object}
*/
_pairedDevicesListView: null,
/**
* A instance to maintain found devices list view.
*
* @access private
* @memberOf DevicePickerPanel
* @type {Object}
*/
_foundDevicesListView: null,
/**
* Construct elements.
*
* @access private
* @memberOf DevicePickerPanel
* @type {Object}
*/
_elements: {
view: {
viewDevicePicker: document.querySelector('.devices-list-view')
},
header: {
headerActionClose: document.querySelector('.devices-list-header')
},
paired: {
pairedDevicesHeader: document.querySelector('.bluetooth-paired-title'),
pairedDevicesList: document.querySelector('.bluetooth-paired-devices')
},
found: {
foundDevicesHeader: document.querySelector('.bluetooth-found-title'),
foundDevicesList: document.querySelector('.bluetooth-devices')
},
search: {
searchingItem: document.querySelector('.bluetooth-searching'),
searchItem: document.querySelector('.bluetooth-search'),
searchButton: document.querySelector('.search-device')
}
},
/**
* The object maintains listeners' callback per property name.
* Each listener would be called as following definition.
* 'devicePicked' - be called when device is picked and paired successfully.
* 'cancelSelection': - be called when user click on close button on header.
*
* @memberOf DevicePickerPanel
* @type {Object}
*/
_listeners: {
'devicePicked': [],
'cancelSelection': []
},
/**
* Default adapter of Bluetooth.
*
* @access private
* @memberOf DevicePickerPanel
* @type {Object} BluetoothAdapter
*/
_defaultAdapter: null,
/**
* The function init the paired/found devices list and search button.
*
* @access private
* @memberOf DevicePickerPanel
*/
_init: function btdpp__init() {
// Bind click event for header.
this._elements.header.headerActionClose.addEventListener('action',
this._onHeaderActionCloseClick.bind(this));
// Bind click event for search button.
this._elements.search.searchButton.addEventListener('click',
this._onSearchButtonClick.bind(this));
// Bind click event for paired device item.
var pairedDeviceTemplate =
BtTemplateFactory('paired', this._onPairedDeviceItemClick.bind(this));
// Create paired devices list view.
this._pairedDevicesListView =
ListView(this._elements.paired.pairedDevicesList,
BtContext.getPairedDevices(),
pairedDeviceTemplate);
// Bind click event for found device item.
var foundDeviceTemplate =
BtTemplateFactory('remote', this._onFoundDeviceItemClick.bind(this));
// Create found devices list view.
this._foundDevicesListView =
ListView(this._elements.found.foundDevicesList,
BtContext.getRemoteDevices(),
foundDeviceTemplate);
// Init items state.
this._initItemsState();
// search devices automatically while the panel is inited.
BtContext.startDiscovery().then(() => {
debug('_init(): startDiscovery successfully');
}, (reason) => {
debug('_init(): startDiscovery failed, ' +
'reason = ' + reason);
});
},
/**
* The function init these elements which are displayed in this panel.
*
* @access private
* @memberOf DevicePickerPanel
*/
_initItemsState: function btdpp__initItemsState() {
// paired devices header
BtContext.observe('hasPairedDevice',
this._updatePairedDevicesHeader.bind(this));
this._updatePairedDevicesHeader(BtContext.hasPairedDevice);
// paired devices list
BtContext.observe('state', this._updatePairedDevicesList.bind(this));
this._updatePairedDevicesList(BtContext.state);
// found devices list
BtContext.observe('state', this._updateFoundDevicesList.bind(this));
this._updateFoundDevicesList(BtContext.state);
this._pairedDevicesListView.enabled = true;
this._foundDevicesListView.enabled = true;
// search
BtContext.observe('state', this._updateSearchItem.bind(this));
this._updateSearchItem(BtContext.state);
BtContext.observe('discovering', this._updateSearchingItem.bind(this));
this._updateSearchingItem(BtContext.discovering);
},
/**
* Start discovery devices while the search button is clicked.
*
* @access private
* @memberOf DevicePickerPanel
*/
_onHeaderActionCloseClick: function btdpp__onHeaderActionCloseClick() {
debug('_onHeaderActionCloseClick():');
// Emit 'cancelSelection' event to outer modules.
var event = {
type: 'cancelSelection'
};
this._emitEvent(event);
},
/**
* Start discovery devices while the search button is clicked.
*
* @access private
* @memberOf DevicePickerPanel
*/
_onSearchButtonClick: function btdpp__onSearchButtonClick() {
debug('_onSearchButtonClick():');
BtContext.startDiscovery().then(() => {
debug('_onSearchButtonClick(): startDiscovery successfully');
}, (reason) => {
debug('_onSearchButtonClick(): startDiscovery failed, ' +
'reason = ' + reason);
});
},
/**
* While the paired device is clicked, emit event to outer modules with
* the device item.
*
* @access private
* @memberOf DevicePickerPanel
*/
_onPairedDeviceItemClick:
function btdpp__onPairedDeviceItemClick(deviceItem) {
debug('_onPairedDeviceItemClick(): deviceItem.address = ' +
deviceItem.address);
debug('_onPairedDeviceItemClick(): deviceItem.paired = ' +
deviceItem.paired);
// Emit 'devicePicked' event to outer modules with the device item.
var event = {
type: 'devicePicked',
detail: {
address: deviceItem.address
}
};
this._emitEvent(event);
},
/**
* Pair with the clicked device. While the device is paired successfully,
* emit event to outer modules with the device item.
*
* @access private
* @memberOf DevicePickerPanel
*/
_onFoundDeviceItemClick:
function btdpp__onFoundDeviceItemClick(deviceItem) {
debug('_onFoundDeviceItemClick(): deviceItem.address = ' +
deviceItem.address);
// Update device pairing status first.
deviceItem.paired = 'pairing';
// Pair with the remote device.
BtContext.pair(deviceItem.address).then(() => {
debug('_onFoundDeviceItemClick(): pair successfully');
// Emit 'devicePicked' event to outer modules with the
// paired device item.
var event = {
type: 'devicePicked',
detail: {
address: deviceItem.address
}
};
this._emitEvent(event);
}, (reason) => {
debug('_onFoundDeviceItemClick(): pair failed, ' +
'reason = ' + reason);
// Reset the paired status back to false,
// since the 'pairing' status is given in Gaia side.
deviceItem.paired = false;
// Show alert message while pair device failed.
this._alertPairFailedErrorMessage(reason);
});
},
/**
* Show an alert dialog since the device is failed in pairing process.
*
* @access private
* @memberOf DevicePickerPanel
*/
_alertPairFailedErrorMessage:
function btdpp__alertPairFailedErrorMessage(errorReason) {
var _ = navigator.mozL10n.get;
var msg = _('error-pair-title'); // This is a default message.
if (errorReason === 'Repeated Attempts') {
msg = msg + '\n' + _('error-pair-toofast');
} else if (errorReason === 'Authentication Failed') {
msg = msg + '\n' + _('error-pair-pincode');
}
window.alert(msg);
},
/**
* A function to show/hide paired devices header.
*
* @access private
* @memberOf DevicePickerPanel
*/
_updatePairedDevicesHeader:
function btdpp__updatePairedDevicesHeader(hasPairedDevice) {
debug('_updatePairedDevicesHeader(): ' +
'callback from observe "hasPairedDevice" = ' + hasPairedDevice);
this._elements.paired.pairedDevicesHeader.hidden =
!(hasPairedDevice && (BtContext.state === 'enabled'));
},
/**
* A function to show/hide paired devices list.
*
* @access private
* @memberOf DevicePickerPanel
*/
_updatePairedDevicesList: function btdpp__updatePairedDevicesList(state) {
debug('_updatePairedDevicesList(): ' +
'callback from observe "state" = ' + state);
this._elements.paired.pairedDevicesHeader.hidden =
!((state === 'enabled') && (BtContext.hasPairedDevice));
this._elements.paired.pairedDevicesList.hidden = (state !== 'enabled');
},
/**
* A function to show/hide found devices list.
*
* @access private
* @memberOf DevicePickerPanel
*/
_updateFoundDevicesList: function btdpp__updateFoundDevicesList(state) {
debug('_updateFoundDevicesList(): ' +
'callback from observe "state" = ' + state);
this._elements.found.foundDevicesHeader.hidden = (state !== 'enabled');
this._elements.found.foundDevicesList.hidden = (state !== 'enabled');
},
/**
* A function to show/hide search item.
*
* @access private
* @memberOf DevicePickerPanel
*/
_updateSearchItem: function btdpp__updateSearchItem(state) {
debug('_updateSearchItem(): ' +
'callback from observe "state" = ' + state);
this._elements.search.searchItem.hidden = (state !== 'enabled');
},
/**
* A function to show/hide searching description item.
*
* @access private
* @memberOf DevicePickerPanel
*/
_updateSearchingItem: function btdpp__updateSearchingItem(discovering) {
debug('_updateSearchingItem(): ' +
'callback from observe "discovering" = ' + discovering);
this._elements.search.searchingItem.hidden = !discovering;
this._elements.search.searchButton.disabled = discovering;
},
/**
* A function to emit event to each registered listener by the input type.
*
* @memberOf DevicePickerPanel
* @param {Object} options
* @param {String} options.type - type of event name
* @param {Object} options.detail - the object pass to the listener
*/
_emitEvent: function btdpp__emitEvent(options) {
this._listeners[options.type].forEach(function(listener) {
listener(options);
});
},
/**
* The method will provide event listener for outer modules to regist.
*
* @access public
* @memberOf DevicePickerPanel
* @param {String} eventName
* @param {Function} callback
*/
addEventListener: function btdpp_addEventListener(eventName, callback) {
if (callback && (this._listeners.hasOwnProperty(eventName))) {
this._listeners[eventName].push(callback);
}
},
/**
* The method will provide event listener for outer modules to un-regist.
*
* @access public
* @memberOf DevicePickerPanel
* @param {String} eventName
* @param {Function} callback
*/
removeEventListener:
function btdpp_removeEventListener(eventName, callback) {
if (callback && (this._listeners.hasOwnProperty(eventName))) {
var index = this._listeners[eventName].indexOf(callback);
if (index >= 0) {
this._listeners[eventName].splice(index, 1);
}
}
},
/**
* The setter will show/hide the device picker view for outer modules.
*
* @access public
* @memberOf DevicePickerPanel
* @param {Boolean} enabled
*/
set visible(enabled) {
// display device picker view
this._elements.view.viewDevicePicker.hidden = !enabled;
}
};
DevicePickerPanel._init();
return DevicePickerPanel;
});

View file

@ -0,0 +1 @@
define(["require"],function(i){"use strict";var n={init:function(){this.dialog=document.getElementById("pairing-request-timeout"),this.confirmBtn=document.getElementById("incoming-pairing-timeout-confirm")},showConfirm:function(i){var n=this;this.confirmBtn.onclick=function(){n.close(),i&&i()},this.dialog.hidden=!1},get isVisible(){return!this.dialog.hidden},close:function(){this.dialog.hidden=!0}};return n.init(),n});

View file

@ -0,0 +1,7 @@
;var Pairview={_debug:false,_remoteDeviceName:null,_pairMethod:null,_options:null,_passkey:'',pairview:document.getElementById('pair-view'),nameLabel:document.getElementById('label-name'),pairDescription:document.getElementById('pair-description'),pairButton:document.getElementById('button-pair'),closeButton:document.getElementById('button-close'),comfirmationItem:document.getElementById('confirmation-method'),pinInputItem:document.getElementById('pin-input-method'),passkey:document.getElementById('passkey'),pinInput:document.getElementById('pin-input'),get isFullAttentionMode(){return(window.innerHeight>200);},show:function pv_show(){if(!this.isFullAttentionMode){this.close();return;}
var _=navigator.mozL10n.get;this.pairButton.addEventListener('click',this);this.closeButton.addEventListener('click',this);window.addEventListener('resize',this);this._remoteDeviceName=this._remoteDeviceName||_('unnamed-device');var truncatedDeviceName=getTruncated(this._remoteDeviceName,{node:this.nameLabel,maxLine:2,ellipsisIndex:3});this.nameLabel.textContent=truncatedDeviceName;this.pairview.hidden=false;var stringName='passive-pair-'+this._pairMethod;this.pairDescription.textContent=_(stringName,{device:truncatedDeviceName});switch(this._pairMethod){case'displaypasskey':case'confirmation':this.passkey.textContent=this._passkey;this.comfirmationItem.hidden=false;this.pinInputItem.hidden=true;break;case'enterpincode':this.pinInputItem.hidden=false;this.comfirmationItem.hidden=true;this.pinInput.focus();break;case'consent':this.comfirmationItem.hidden=false;this.pinInputItem.hidden=true;break;}},init:function pv_init(method,options){this._pairMethod=method;this._options=options;this._remoteDeviceName=options.deviceName;if(options.handle&&options.handle.passkey){var passkey=options.handle.passkey;var len=passkey.toString().length;var zeros=(len<6)?(new Array((6-len)+1)).join('0'):'';this._passkey=zeros+passkey;}
navigator.mozL10n.once(Pairview.show.bind(Pairview));},close:function pv_close(){this._options.handle.reject().then(()=>{this.debug('Resolved in reject '+this._pairMethod+' request.');window.close();},(aReason)=>{this.debug('Rejected in reject '+this._pairMethod+' request with reason: '+aReason);window.close();});},closeInput:function pv_closeInput(){if(!this.pinInputItem.hidden){this.pinInput.blur();}},handleEvent:function pv_handleEvent(evt){var _=navigator.mozL10n.get;if(!evt.target){return;}
switch(evt.type){case'click':evt.preventDefault();switch(evt.target.id){case'button-pair':this.pairDescription.textContent=_('device-status-waiting');this.pairButton.disabled=true;this.closeButton.disabled=true;switch(this._pairMethod){case'displaypasskey':window.close();break;case'enterpincode':var pinCode=this.pinInput.value;this._options.handle.setPinCode(pinCode).then(()=>{this.debug('Resolved setPinCode operation');window.close();},(aReason)=>{this.debug('Rejected setPinCode with reason: '+aReason);window.close();});break;case'confirmation':case'consent':this._options.handle.accept().then(()=>{this.debug('Resolved in '+this._pairMethod+' request');window.close();},(aReason)=>{this.debug('Rejected in '+this._pairMethod+' request '+' with reason: '+aReason);window.close();});break;}
break;case'button-close':this.close();break;}
break;case'resize':if(!this.isFullAttentionMode){this.close();}
break;default:break;}},debug:function(msg){if(this._debug){console.log('Pairview(): '+msg);}}};

View file

@ -0,0 +1,51 @@
/*
* TurnBluetoothOnDialog is responsible for:
* A confirmation dialog to ask user to turn Bluetooth on or not.
* There is a confirmation handling via here.
*/
define([],function() {
'use strict';
var TurnBluetoothOnDialog = {
init: function() {
this.dialog = document.getElementById('enable-bluetooth-view');
this.cancelBtn =
document.getElementById('enable-bluetooth-button-cancel');
this.confirmBtn =
document.getElementById('enable-bluetooth-button-turn-on');
},
showConfirm: function() {
return new Promise((resolve) => {
this.cancelBtn.onclick = (evt) => {
if (evt) {
evt.preventDefault();
}
this.close();
resolve('cancel');
};
this.confirmBtn.onclick = (evt) => {
if (evt) {
evt.preventDefault();
}
this.close();
resolve('confirm');
};
this.dialog.hidden = false;
});
},
get isVisible() {
return (!this.dialog.hidden);
},
close: function() {
this.dialog.hidden = true;
}
};
TurnBluetoothOnDialog.init();
return TurnBluetoothOnDialog;
});

View file

@ -0,0 +1,18 @@
{
"name": "bluetooth",
"tags": {
"dictionaries": ["jsdoc"]
},
"source": {
"include": ["apps/bluetooth/js/**/*.js"]
},
"plugins": [
"plugins/markdown",
"plugins/summarize"
],
"opts": {
"private": true,
"recurse": true,
"destination": "docs/bluetooth"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show more