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,21 @@
{
"name": "device-deck",
"version": "0.0.0",
"homepage": "https://github.com/mozilla-b2g/gaia/tree/master/tv_apps/device-deck",
"description": "Smart Screen Apps Deck",
"main": "index.html",
"license": "MPL",
"private": true,
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"evt": "fxos-components/evt#~0.4.0",
"smart-button": "fxos-components/smart-button",
"smart-icons": "smart-components/smart-icons"
}
}

View file

@ -0,0 +1,28 @@
{
"name": "evt",
"main": "index.js",
"version": "0.4.0",
"homepage": "https://github.com/wilsonpage/evt",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"package.json",
"test",
"tests",
"README.md"
],
"_release": "0.4.0",
"_resolution": {
"type": "version",
"tag": "v0.4.0",
"commit": "3593dd6b4dd19f683d00fd0eaf2581c11a128da6"
},
"_source": "git://github.com/gaia-components/evt.git",
"_target": "~0.4.0",
"_originalSource": "gaia-components/evt"
}

View file

@ -0,0 +1,19 @@
{
"name": "evt",
"main": "index.js",
"version": "0.4.0",
"homepage": "https://github.com/wilsonpage/evt",
"authors": [
"Wilson Page <wilsonpage@me.com>"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"package.json",
"test",
"tests",
"README.md"
]
}

View file

@ -0,0 +1,154 @@
/**
* Evt
*
* A super lightweight
* event emitter library.
*
* @version 0.3.3
* @author Wilson Page <wilson.page@me.com>
*/
;(function() {
/**
* Locals
*/
var proto = Events.prototype;
var slice = [].slice;
/**
* Creates a new event emitter
* instance, or if passed an
* object, mixes the event logic
* into it.
*
* @param {Object} obj
* @return {Object}
*/
function Events(obj) {
if (!(this instanceof Events)) return new Events(obj);
if (obj) return mixin(obj, proto);
}
/**
* Registers a callback
* with an event name.
*
* @param {String} name
* @param {Function} cb
* @return {Event}
*/
proto.on = function(name, cb) {
this._cbs = this._cbs || {};
(this._cbs[name] || (this._cbs[name] = [])).push(cb);
return this;
};
/**
* Attach a callback that once
* called, detaches itself.
*
* TODO: Implement `.off()` to work
* with `once()` callbacks.
*
* @param {String} name
* @param {Function} cb
* @public
*/
proto.once = function(name, cb) {
this.on(name, one);
function one() {
cb.apply(this, arguments);
this.off(name, one);
}
};
/**
* Removes a single callback,
* or all callbacks associated
* with the passed event name.
*
* @param {String} name
* @param {Function} cb
* @return {Event}
*/
proto.off = function(name, cb) {
this._cbs = this._cbs || {};
if (!name) { this._cbs = {}; return; }
if (!cb) { return delete this._cbs[name]; }
var cbs = this._cbs[name] || [];
var i;
while (cbs && ~(i = cbs.indexOf(cb))) { cbs.splice(i, 1); }
return this;
};
/**
* Fires an event, triggering
* all callbacks registered on this
* event name.
*
* @param {String} name
* @return {Event}
*/
proto.fire = proto.emit = function(options) {
var cbs = this._cbs = this._cbs || {};
var name = options.name || options;
var batch = (cbs[name] || []).concat(cbs['*'] || []);
var ctx = options.ctx || this;
if (batch.length) {
this._fireArgs = arguments;
var args = slice.call(arguments, 1);
while (batch.length) {
batch.shift().apply(ctx, args);
}
}
return this;
};
proto.firer = function(name) {
var self = this;
return function() {
var args = slice.call(arguments);
args.unshift(name);
self.fire.apply(self, args);
};
};
/**
* Util
*/
/**
* Mixes in the properties
* of the second object into
* the first.
*
* @param {Object} a
* @param {Object} b
* @return {Object}
*/
function mixin(a, b) {
for (var key in b) a[key] = b[key];
return a;
}
/**
* Expose 'Event' (UMD)
*/
if (typeof exports === 'object') {
module.exports = Events;
} else if (typeof define === 'function' && define.amd) {
define(function(){ return Events; });
} else {
window.evt = Events;
}
})();

View file

@ -0,0 +1,25 @@
{
"name": "smart-button",
"homepage": "https://github.com/gaia-components/smart-button",
"main": "script.js",
"license": "MPL",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"examples",
"LICENSE",
"README.md"
],
"_release": "a13686c562",
"_resolution": {
"type": "branch",
"branch": "master",
"commit": "a13686c5620b996e254764eb9b6993c9555da862"
},
"_source": "git://github.com/gaia-components/smart-button.git",
"_target": "*",
"_originalSource": "gaia-components/smart-button"
}

View file

@ -0,0 +1,17 @@
{
"name": "smart-button",
"version": "0.0.0",
"homepage": "https://github.com/gaia-components/smart-button",
"main": "script.js",
"license": "MPL",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests",
"examples",
"LICENSE",
"README.md"
]
}

View file

@ -0,0 +1,62 @@
'use strict';
/* global KeyEvent */
window.SmartButton = (function(win) {
// Extend from the HTMLButtonElement prototype
var proto = Object.create(HTMLButtonElement.prototype);
proto.createdCallback = function() {
this.addEventListener('mousedown', this);
this.addEventListener('mouseup', this);
this.addEventListener('touchstart', this);
this.addEventListener('touchend', this);
this.addEventListener('keydown', this);
this.addEventListener('keyup', this);
this.addEventListener('focus', this);
this.addEventListener('blur', this);
this.addEventListener('transitionend', this);
this.tabIndex = 0;
};
proto.handleEvent = function(evt) {
switch(evt.type) {
case 'mousedown':
case 'touchstart':
this.classList.add('pressed');
break;
case 'keydown':
if (evt.keyCode === KeyEvent.DOM_VK_RETURN) {
this.classList.add('pressed');
}
break;
case 'mouseup':
case 'touchend':
this.classList.remove('pressed');
this.classList.add('released');
break;
case 'keyup':
if (evt.keyCode === KeyEvent.DOM_VK_RETURN) {
this.classList.remove('pressed');
this.classList.add('released');
this.click();
}
break;
case 'transitionend':
if (this.classList.contains('released')) {
this.classList.remove('released');
}
break;
case 'focus':
this.classList.add('focused');
break;
case 'blur':
this.classList.remove('pressed');
this.classList.remove('released');
this.classList.remove('focused');
break;
}
};
// Register and return the constructor
return document.registerElement('smart-button', { prototype: proto });
})(window);

View file

@ -0,0 +1,444 @@
/**
* Button state:
* 1. normal
* In this state, we don't add any class.
* 2. focused (class name: focused)
* The focused state may be triggered by keyboard or touch/mouse.
* 3. pressed (class name: pressed)
* This state is triggered by user pressed confirm button, mousedown,
* and touchdown.
* 4. released (class name: released)
* This state is only for changing duration of transition. We enter this
* state when user release the button. After transitionend, we goes back
* to focused state.
**/
smart-button {
font-family: "Fira Sans";
display: inline-block;
width: 8rem;
height: 8rem;
/* we need to set border to none to remove the button border */
border: none;
/* the radius is still make button as round even if we don't have border */
border-radius: 50%;
/* 50% is not a real circle because of rounding, we have to use a value larger
than 50% */
-moz-outline-radius: 60%;
outline: transparent 0 solid;
background-color: rgba(0, 0, 0, 0.5);
background-repeat: no-repeat;
background-size: 5.4rem auto;
background-position: center center;
font-style: italic;
transition-property: all;
transition-timing-function: cubic-bezier(0.25, 0, 0, 1.0);
transition-duration: 0.42s;
}
smart-button.focused {
outline: 0;
background-color: #ffffff;
transform: scale(1.2);
transition-duration: 0.42s;
}
smart-button.released {
transition-duration: 0.16s;
}
smart-button.toggled {
background-color: #00caf2;
}
smart-button.pressed {
background-color: #00caf2;
transform: scale(0.8);
transition-duration: 0.06s;
}
smart-button.disabled {
background-color: rgba(0, 0, 0, 0.5);
}
smart-button[data-icon]:before {
width: 5.4rem;
left: 1.3rem;
font-size: 5.4rem;
line-height: 8rem;
text-align: center;
color: #ffffff;
white-space: normal;
top: 0;
position: absolute;
}
smart-button[data-icon].focused:before {
color: #2d2d2d;
}
smart-button[data-icon].toggled:before {
color: #00caf2;
}
smart-button[data-icon].pressed:before {
color: #ffffff;
}
smart-button[data-icon].disabled:before {
color: #ffffff;
opacity: 0.3;
}
/**
* Simple Text Button
**/
smart-button[type="simple-text"] {
color: #dddddd;
background: none;
border: none;
border-radius: 0;
width: auto;
height: 8rem;
line-height: 8rem;
font-size: 2.4rem;
-moz-user-select: none;
padding-left: 2.6rem;
padding-right: 2.6rem;
}
smart-button[type="simple-text"].focused {
color: #ffffff;
background: none;
}
smart-button[type="simple-text"].toggled {
color: #00caf2;
}
smart-button[type="simple-text"].pressed {
color: #00caf2;
background: none;
}
/**
* End of Simple Text Button
**/
/**
* underlined text button
*/
smart-button[type="underlined-text"] {
position: relative;
color: white;
font-size: 2.4rem;
padding-left: 3.8rem;
padding-right: 3.8rem;
border-radius: 4rem;
width: auto;
line-height: 8rem;
margin-right: 1.4rem;
overflow: hidden;
}
smart-button[type="underlined-text"]:after {
position: absolute;
width: 100%;
height: 100%;
left: 0;
content: ' ';
border-bottom: 0.6rem #61acd7 solid;
box-sizing: border-box;
}
smart-button[type="underlined-text"].focused {
transform: scale(1.2);
color: #2d2d2d;
}
smart-button[type="underlined-text"].pressed {
transform: scale(1);
color: white;
background-color: #00caf2;
}
/**
* End of underlined text button
*/
/**
* App Icons
**/
smart-button[type="app-button"] {
/* To use 33.4rem instead of to use 33.6rem is to work around the anti-alias
pixel. So, the circle is filled by the background almost totally. */
width: 33.4rem;
height: 33.4rem;
position: relative;
overflow: hidden;
background-color: transparent;
}
smart-button[type="app-button"].focused {
transform: scale(1.4);
/* To use outline will not change the size of button and make the whole button
thrilling. */
outline: 0.8rem solid #ffffff;
}
smart-button[type="app-button"].pressed {
/* While scaling, we may have a gap between outline and icon which may be
anti-alias pixel. So, we use scale(1.22) to prevent such kind of case */
transform: scale(1.22);
/* To use outline will not change the size of button and make the whole button
thrilling. */
outline: 1.0rem solid #ffffff;
}
smart-button[type="app-button"].fullsized {
background-size: 33.6rem auto;
}
smart-button[type="app-button"].fitted {
background-size: 22rem auto;
}
/** we use :after as label */
smart-button[type="app-button"]:after {
content: attr(label);
position: absolute;
bottom: 0;
left: auto;
right: auto;
width: 33.6rem;
height: 8rem;
padding-top: 1rem;
line-height: 3rem;
text-align: center;
font-size: 2.8rem;
font-style: italic;
color: #ffffff;
}
/**
* text backgorund color
**/
smart-button[type="app-button"][app-type="deck"]:after {
background-color: transparent;
}
smart-button[type="app-button"][app-type="tv"]:after {
background-color: #d90c83;
}
smart-button[type="app-button"][app-type="app"]:after {
background-color: rgba(0, 0, 0, 0.4);
}
smart-button[type="app-button"][app-type="device"]:after {
background-color: #4a82ea;
}
smart-button[type="app-button"][app-type="folder"] {
background-color: #595959;
}
smart-button[type="app-button"]:before {
left: 0;
line-height: 30rem;
font-size: 16rem;
width: 100%;
}
smart-button[type="app-button"][app-type="folder"]:before {
color: #CCCCCC;
}
/**
* End of App Icons
**/
/**
* text and icon button
*/
smart-button[type="text-and-icon"] {
position: relative;
color: white;
font-size: 2.4rem;
padding-left: 9.6rem;
padding-right: 4.6rem;
border-radius: 4rem;
width: auto;
line-height: 8rem;
margin-right: 1.4rem;
overflow: hidden;
}
smart-button[type="text-and-icon"][data-icon]:before {
left: 3.8rem;
}
smart-button[type="text-and-icon"].focused {
transform: scale(1.2);
color: #2d2d2d;
}
smart-button[type="text-and-icon"].pressed {
transform: scale(1);
color: white;
background-color: #00caf2;
}
/**
* End of text and icon button
*/
/**
* banner button
**/
smart-button[type="banner-button"] {
position: relative;
color: #ffffff;
font-size: 2rem;
width: 36rem;
height: 20rem;
overflow: hidden;
border-radius: 0;
border: 0 solid #ffffff;
background-color: #5f5f5f;
background-repeat: no-repeat;
background-size: 120%;
box-sizing: border-box;
transition-property: all;
transition-timing-function: cubic-bezier(0.25, 0, 0.5, 1.0);
transition-duration: 0.1s;
}
smart-button[type="banner-button"][label]:before {
content: attr(label);
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 5rem;
line-height: 5rem;
text-align: center;
background-color: rgba(0, 0, 0, 0.4);
}
smart-button[type="banner-button"]:after {
content: " ";
position: absolute;
bottom: 0;
left: 0;
right: 0;
top: 0;
transition-property: all;
transition-timing-function: cubic-bezier(0.25, 0, 0, 1.0);
transition-duration: 0.06s;
background-color: rgba(255, 255, 255, 0);
}
smart-button[type="banner-button"].focused {
transform: none;
border-width: 1.2rem;
background-size: 100%;
transition-duration: 0.24s;
}
smart-button[type="banner-button"].pressed {
transform: scale(0.945, 0.9);
}
smart-button[type="banner-button"].pressed:after {
background-color: rgba(255, 255, 255, 0.5);
}
/**
* banner button
**/
/**
* circle text button
*/
smart-button[type="circle-text"] {
width: 21rem;
height: 21rem;
color: white;
background-color: #5f6368;
line-height: 21rem;
padding: 2.5rem 2rem 1.5rem 2rem;
text-align: center;
font-size: 3.2rem;
font-weight: bolder;
overflow: hidden;
transition-property: transform, color, background-color;
}
smart-button[type="circle-text"].focused {
transform: scale(1.2);
color: #2d2d2d;
background-color: white;
transition-property: transform, color, background-color;
}
smart-button[type="circle-text"].pressed {
transform: scale(1);
color: white;
background-color: #00caf2;
transition-property: transform, color, background-color;
}
smart-button[type="circle-text"].primary {
background-color: #00caf2;
transition-property: transform, color, background-color;
}
smart-button[type="circle-text"].primary.focused {
color: #ffffff;
background-color: #00caf2;
transition-property: transform, color, background-color;
}
smart-button[type="circle-text"].primary.pressed {
background-color: #0097b5;
transition-property: transform, color, background-color;
}
smart-button[type="circle-text"].danger {
background-color: #e63d2f;
transition-property: transform, color, background-color;
}
smart-button[type="circle-text"].danger.focused {
color: #ffffff;
background-color: #e63d2f;
transition-property: transform, color, background-color;
}
smart-button[type="circle-text"].danger.pressed {
background-color: #ac2e23;
transition-property: transform, color, background-color;
}
/**
* End of circle text button
*/

View file

@ -0,0 +1,29 @@
{
"name": "smart-icons",
"homepage": "https://github.com/smart-components/smart-icons",
"description": "Icon font for smart-screen",
"license": "MPL",
"private": true,
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"templates",
"images",
"test",
"tests",
"index.html",
"Gruntfile.js",
"README.md",
"package.json"
],
"_release": "096856d7f3",
"_resolution": {
"type": "branch",
"branch": "master",
"commit": "096856d7f3ff97352f5b0a822497f7057de96825"
},
"_source": "git://github.com/smart-components/smart-icons.git",
"_target": "*",
"_originalSource": "smart-components/smart-icons"
}

View file

@ -0,0 +1,21 @@
{
"name": "smart-icons",
"version": "0.0.0",
"homepage": "https://github.com/smart-components/smart-icons",
"description": "Icon font for smart-screen",
"license": "MPL",
"private": true,
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"templates",
"images",
"test",
"tests",
"index.html",
"Gruntfile.js",
"README.md",
"package.json"
]
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,25 @@
/* Generated by grunt-webfont */
/* Based on https://github.com/endtwist/fontcustom/blob/master/lib/fontcustom/templates/fontcustom.css */
@font-face {
font-family: "smart-icons";
src: url("fonts/smart-icons.ttf") format("truetype");
font-weight: 500;
font-style: normal;
}
[data-icon]:before,
.ligature-icons {
font-family: "smart-icons";
content: attr(data-icon);
display: inline-block;
font-weight: 500;
font-style: normal;
text-decoration: inherit;
text-transform: none;
text-rendering: optimizeLegibility;
font-size: 30px;
-webkit-font-smoothing: antialiased;
}

View file

@ -0,0 +1,65 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<meta charset="utf-8">
<title>Devices</title>
<!-- building blocks -->
<link rel="stylesheet" type="text/css" href="bower_components/smart-icons/smart-icons.css">
<link rel="stylesheet" type="text/css" href="bower_components/smart-button/style.css">
<link rel="stylesheet" type="text/css" href="style/devices.css">
<!-- Helper libraries -->
<script defer src="bower_components/evt/index.js"></script>
<!-- building blocks -->
<script defer src="bower_components/smart-button/script.js"></script>
<!-- For test purpose on phone only -->
<script defer src="shared/js/gesture_detector.js"></script>
<script defer src="shared/js/smart-screen/init_gesture.js"></script>
<!-- Shared libraries -->
<script defer src="shared/js/smart-screen/shared_utils.js"></script>
<script defer src="shared/js/smart-screen/spatial_navigator.js"></script>
<script defer src="shared/js/smart-screen/key_navigation_adapter.js"></script>
<!-- Specific code -->
<script defer src="js/bluetooth_loader.js"></script>
<script defer src="js/bluetooth_cod_mapper.js"></script>
<script defer src="js/bluetooth_manager.js"></script>
<script defer src="js/device_deck.js"></script>
<!-- Shared L10n libraries -->
<script src="shared/js/l10n.js"></script>
<script src="shared/js/l10n_date.js"></script>
<!-- L10n properties -->
<meta name="defaultLanguage" content="en-US">
<meta name="availableLanguages" content="en-US">
<link rel="localization" href="locales/devices-deck.{locale}.properties"/>
<link rel="localization" href="shared/locales/date/date.{locale}.properties"/>
</head>
<body>
<article id="main-section">
<section role="region" class="skin-dark" id="header">
<smart-button id="refresh-button" type="text-and-icon" data-icon="refresh" data-l10n-id="refresh" class="navigable"></smart-button>
</section>
<section role="region" class="skin-dark" id="grid">
<div id="connected-devices-container" class="devices-container">
<div class="devices-caption" data-l10n-id="connected"></div>
<div id="connected-devices-list" class="devices-list"></div>
</div>
<div id="newly-found-device-container" class="devices-container">
<div class="devices-caption" data-l10n-id="newly-found"></div>
<div id="newly-found-devices-list" class="devices-list"></div>
</div>
</section>
</article>
</body>
</html>

View file

@ -0,0 +1,175 @@
(function(exports) {
'use strict';
// XXX: this module is copied and modified from
// apps/settings/js/modules/bluetooth/bluetooth_cod_mapper.js
// because it is not exposed as shared library and is in AMD style.
// We might need to consolidate them in the future.
var MinorDeviceClasses = {
Computer: {
0: 'computer', // Uncategorized, code for device not assigned
1: 'computer', // Desktop workstation
2: 'computer', // Server-class computer
3: 'computer', // Laptop
4: 'pda', // Handheld PC/PDA (clamshell)
5: 'pda', // Palm-size PC/PDA
6: 'computer', // Wearable computer (watch size)
7: 'computer', // Tablet
reserved: 'computer' // All other values reserved. Default type
// 'computer' given for reserved case.
},
Phone: {
0: 'phone', // Uncategorized, code for device not assigned
1: 'phone', // Cellular
2: 'phone', // Cordless
3: 'phone', // Smartphone
4: 'modem', // Wired modem or voice gateway
5: 'phone', // Common ISDN access
reserved: 'phone' // All other values reserved. Default type
// 'phone' given for reserved case.
},
AudioVideo: {
0: 'audio-card', // Uncategorized, code not assigned
1: 'audio-card', // Wearable Headset Device
2: 'audio-card', // Hands-free Device
4: 'audio-input-microphone', // Microphone
5: 'audio-card', // Loudspeaker
6: 'audio-card', // Headphones
7: 'audio-card', // Portable Audio
8: 'audio-card', // Car audio
9: 'audio-card', // Set-top box
10: 'audio-card', // HiFi Audio Device
11: 'camera-video', // VCR
12: 'camera-video', // Video Camera
13: 'camera-video', // Camcorder
14: 'video-display', // Video Monitor
15: 'video-display', // Video Display and Loudspeaker
16: 'video-display', // Video Conferencing
18: 'audio-card', // Gameing/Toy
reserved: 'audio-card' // All other values reserved. Default type
// 'audio-card' given for reserved case.
},
Peripheral: {
1: 'input-gaming', // Joystick
2: 'input-gaming', // Gamepad
16: 'input-keyboard', // Keyboard
17: 'input-keyboard', // Keyboard
18: 'input-keyboard', // Keyboard
19: 'input-keyboard', // Keyboard
20: 'input-keyboard', // Keyboard
21: 'input-keyboard', // Keyboard
22: 'input-keyboard', // Keyboard
23: 'input-keyboard', // Keyboard
24: 'input-keyboard', // Keyboard
25: 'input-keyboard', // Keyboard
26: 'input-keyboard', // Keyboard
27: 'input-keyboard', // Keyboard
28: 'input-keyboard', // Keyboard
29: 'input-keyboard', // Keyboard
30: 'input-keyboard', // Keyboard
31: 'input-keyboard', // Keyboard
32: 'input-mouse', // Pointing device
33: 'input-mouse', // Pointing device
34: 'input-mouse', // Pointing device
35: 'input-mouse', // Pointing device
36: 'input-mouse', // Pointing device
37: 'input-tablet', // Digitizer tablet
38: 'input-mouse', // Pointing device
39: 'input-mouse', // Pointing device
40: 'input-mouse', // Pointing device
41: 'input-mouse', // Pointing device
42: 'input-mouse', // Pointing device
43: 'input-mouse', // Pointing device
44: 'input-mouse', // Pointing device
45: 'input-mouse', // Pointing device
46: 'input-mouse', // Pointing device
47: 'input-mouse' // Pointing device
},
Imaging: {
4: 'video-display', // Display, bit: XXX1XX
8: 'camera-photo', // Camera, bit: XX1XXX
16: 'scanner', // Scanner, bit: X1XXXX
32: 'printer' // Printer, bit: 1XXXXX
}
};
var MajorDeviceClasses = {
1: MinorDeviceClasses.Computer,
2: MinorDeviceClasses.Phone,
3: 'network-wireless', // LAN/Network Access Point Major Class
4: MinorDeviceClasses.AudioVideo,
5: MinorDeviceClasses.Peripheral,
6: MinorDeviceClasses.Imaging
};
var DeviceIconTable = {
'computer': 'laptop',
'pda': 'phone',
'phone': 'phone',
'modem': 'bluetooth', // need icon
'audio-card': 'speaker',
'audio-input-microphone': 'mic',
'camera-video': 'recorder',
'video-display': 'tv',
'input-gaming': 'game',
'input-keyboard': 'keyboard',
'input-mouse': 'mouse',
'input-tablet': 'bluetooth', // need icon
'camera-photo': 'bluetooth', // need icon
'scanner': 'bluetooth', // need icon
'printer': 'bluetooth' // need icon
};
exports.BluetoothCodMapper = {
getDeviceType: function btcodm_getDeviceType(cod) {
// Given an empty string to be default type.
// Then, we won't show any icon for empty type.
var deviceType = '';
var majorDeviceClass = MajorDeviceClasses[cod.majorDeviceClass];
if (typeof(majorDeviceClass) === 'object') {
// drop in other Major Class
deviceType = majorDeviceClass[cod.minorDeviceClass] ||
majorDeviceClass.reserved || '';
return deviceType;
} else if (typeof(majorDeviceClass) === 'string') {
// drop in LAN/Network Access Point Major Class
return majorDeviceClass;
} else if (cod.majorServiceClass & 0x100) {
// Not in any Major Device Class which is defined in Gaia.
// Ex: Wearable, Toy, Health.
// Since there is no icon to display wearable, toy, health devices,
// file a bug 1163479[2] to define them for specification needed.
// But some of these devices probably service 'Audio' per Bluetooth
// specification[1].
// Property 'type' may be missed due to CoD of major class is TOY(0x08).
// But we need to assign 'type' as 'audio-card' if service class
// is 'Audio'. This is for PTS test case TC_AG_COD_BV_02_I.
// As HFP specification defines that service class is 'Audio' can
// be considered as HFP HF.
// [1]: HFP_SPEC_V16.pdf: A device implementing the HF role of HFP shall
// set the "Audio" bit in the Service Class field.
// [2]: Bug 1163479 - [Gaia][Bluetooth] Device icon definition for
// wearable, toy, health(Major Device Class) devices.
// (https://bugzilla.mozilla.org/show_bug.cgi?id=1163479)
// Major Service Class: Audio(Speaker, Microphone, Headset service, ...)
deviceType = 'audio-card';
return deviceType;
} else {
// Not in any Class which is defined in Gaia.
return deviceType;
}
},
getIconName: function btcodm_getIconName(cod) {
var humanReadableCod = this.getDeviceType(cod);
return (DeviceIconTable[humanReadableCod] ?
DeviceIconTable[humanReadableCod] : 'bluetooth');
}
};
}(window));

View file

@ -0,0 +1,38 @@
(function (exports) {
'use strict';
exports.BluetoothLoader = {
// on b2g-desktop, we don't have mozBluetooth API, so we inject fake one
// just to make sure we won't break TV build on b2g-desktop
_fakeMozBluetooth: {
addEventListener: function() {},
defaultAdapter: {
_discoveryHandle: {
addEventListener: function() {}
},
state: 'disabled',
enable: function() {
return Promise.resolve();
},
disable: function() {
return Promise.resolve();
},
startDiscovery: function() {
return Promise.resolve(this._discoveryHandle);
},
stopDiscovery: function() {
return Promise.resolve();
}
}
},
getMozBluetooth: function() {
if (navigator.mozBluetooth) {
return navigator.mozBluetooth;
} else {
return this._fakeMozBluetooth;
}
}
};
}(window));

View file

@ -0,0 +1,97 @@
/* globals evt, BluetoothLoader */
(function(exports) {
'use strict';
var BluetoothManager = function() {};
BluetoothManager.prototype = evt({
_mozBluetooth: undefined,
_defaultAdapter: undefined,
_discoveryHandle: undefined,
_handleDeviceFound: undefined,
_handleBeforeUnload: undefined,
_deviceDeck: undefined,
init: function bm_init() {
this._mozBluetooth = BluetoothLoader.getMozBluetooth();
this._handleDeviceFound = this.onDeviceFound.bind(this);
this._mozBluetooth.addEventListener('attributechanged',
this.onAttributeChanged.bind(this));
this._defaultAdapter = this._mozBluetooth.defaultAdapter;
},
onAttributeChanged: function bm_onAttributeChanged(evt) {
var that = this;
[].forEach.call(evt.attrs, function(attr, index) {
if (attr === 'defaultAdapter') {
that._defaultAdapter = that._mozBluetooth.defaultAdapter;
that.fire('default-adapter-ready');
}
});
},
_keepDiscoveryHandle: function bm_keepDiscoveryHandle(handle) {
this._discoveryHandle = handle;
this._discoveryHandle.addEventListener('devicefound',
this._handleDeviceFound);
},
_startDiscovery: function bm_startDiscovery() {
var that = this;
if (!this._defaultAdapter) {
return Promise.reject('default adapter doesn\'t exist');
}
if (this._defaultAdapter.state !== 'enabled') {
return this._defaultAdapter.enable().then(function() {
return that._defaultAdapter.startDiscovery();
}).then(this._keepDiscoveryHandle.bind(this));
}
return this._defaultAdapter.startDiscovery().then(
this._keepDiscoveryHandle.bind(this));
},
_stopDiscovery: function bm_stopDiscovery() {
if (!this._defaultAdapter) {
return Promise.reject('default adapter doesn\'t exist');
}
// force to stop anyway
return this._defaultAdapter.stopDiscovery();
},
safelyStartDiscovery: function bm_safelyStartDiscovery() {
var that = this;
if (!this._defaultAdapter) {
return new Promise(function(resolve, reject) {
that.on('default-adapter-ready', function() {
resolve(that._startDiscovery());
});
});
}
return this._startDiscovery();
},
safelyStopDiscovery: function bn_safelyStopDiscovery() {
return this._stopDiscovery().catch(function(reason) {
console.warn('failed to stop discovery: ' + reason);
});
},
onDeviceFound: function bm_onDeviceFound(evt) {
var device = evt.device;
this.fire('device-found', device);
}
});
exports.BluetoothManager = BluetoothManager;
}(window));

View file

@ -0,0 +1,143 @@
/* global SpatialNavigator, SharedUtils, KeyNavigationAdapter,
BluetoothManager, evt, BluetoothCodMapper */
(function(exports) {
'use strict';
/**
* DeviceDeck is the controller class of device-deck.
*
* @class DeviceDeck
* @requires BluetoothManager
* @requires KeyNavigationAdapter
* @requires SpatialNavigator
* @requires {@link http://bit.ly/1Ld0WYX|SmartButton}
*/
var DeviceDeck = function() {
};
DeviceDeck.prototype = evt({
_navigableElements: [],
_spatialNavigator: undefined,
_keyNavigationAdapter: undefined,
connectedDevicesList:
document.getElementById('connected-devices-list'),
newlyFoundDeviceList:
document.getElementById('newly-found-devices-list'),
_bluetoothManager: undefined,
/**
* Initialize DeviceDeck. It means:
* 1. initialize BlutoothManager and start discovery immediately
* 2. initialize KeyNavigationAdapter
* 3. Find all navigable elements and initialize SpatialNavigator
*
* And when it receives `device-found` event from BluetoothManager, it will
* create {@link http://bit.ly/1Ld0WYX|SmartButton} for the device.
*
* @public
* @method DeviceDeck#init
*/
init: function() {
this._bluetoothManager = new BluetoothManager();
this._bluetoothManager.init();
this._bluetoothManager.safelyStartDiscovery();
this._bluetoothManager.on('device-found',
this.onBluetoothDeviceFound.bind(this));
this._keyNavigationAdapter = new KeyNavigationAdapter();
this._keyNavigationAdapter.init();
this._navigableElements =
SharedUtils.nodeListToArray(
document.querySelectorAll('.navigable:not(.app-banner)'));
this._spatialNavigator = new SpatialNavigator(this._navigableElements);
this._spatialNavigator.on('focus', this.onFocus.bind(this));
this._keyNavigationAdapter.on('move', this.onMove.bind(this));
this._keyNavigationAdapter.on('enter-keyup', this.onEnter.bind(this));
this._spatialNavigator.focus();
},
/**
* refresh list of devices in div#connected-devices-list and
* div#newly-found-devices-list
*
* @public
* @method DeviceDeck#refreshDevices
*/
refreshDevices: function() {
var that = this;
this._bluetoothManager.safelyStopDiscovery().then(function() {
[].forEach.call(that.connectedDevicesList.children, function(elem) {
elem.remove();
});
[].forEach.call(that.newlyFoundDeviceList.children, function(elem) {
elem.remove();
});
}).then(function() {
that._bluetoothManager.safelyStartDiscovery();
});
},
/**
* Create {@link http://bit.ly/1Ld0WYX|SmartButton} for each device found
* and append it to div#connected-devices-list or
* div#newly-found-devices-list
*
* @public
* @method DeviceDeck#onBluetoothDeviceFound
* @param {BluetoothDevice} device - see
* {@link http://mzl.la/1UQllr3|BluetoothDevice}
*/
onBluetoothDeviceFound: function(device) {
var name = device.name || device.address;
var paired = device.paired;
// TODO: we should validate whether there is duplicate button of device
var button = document.createElement('smart-button');
button.setAttribute('type', 'app-button');
button.setAttribute('app-type', 'device');
button.classList.add('navigable');
button.setAttribute('label', name);
button.dataset.type = 'bluetooth';
button.dataset.address = device.address;
button.dataset.icon = BluetoothCodMapper.getIconName(device.cod);
button.dataset.bluetoothDeviceType = device.type;
if (paired) {
this.connectedDevicesList.appendChild(button);
} else {
this.newlyFoundDeviceList.appendChild(button);
}
this._spatialNavigator.add(button);
},
onMove: function(key) {
this._spatialNavigator.move(key);
},
onEnter: function() {
var focused = this._spatialNavigator.getFocusedElement();
if (focused && focused.id === 'refresh-button') {
this.refreshDevices();
}
},
onFocus: function(elem) {
elem.focus();
}
});
exports.deviceDeck = new DeviceDeck();
exports.deviceDeck.init();
} (window));

View file

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

View file

@ -0,0 +1,3 @@
refresh=Refresh
connected=Connected
newly-found=Newly Found

View file

@ -0,0 +1,27 @@
{
"name": "Devices",
"description": "Smart Screen Devices Deck",
"launch_path": "/index.html",
"type": "certified",
"role": "deck",
"developer": {
"name": "The Gaia Team",
"url": "https://github.com/mozilla-b2g/gaia"
},
"permissions": {
"bluetooth":{},
"moz-extremely-unstable-and-will-change-webcomponents":{}
},
"locales": {
"en-US": {
"name": "Devices",
"description": "Smart Screen Devices Deck"
}
},
"default_locale": "en-US",
"icons": {
"336": "/style/icons/devices-deck_336.png",
"672": "/style/icons/devices-deck_672.png"
},
"orientation": "landscape"
}

View file

@ -0,0 +1,80 @@
html {
font-size: 10px;
/* We have Fira Sans as default font in real device. But it isn't default in
* * browser. That's the main reason this line is here.*/
font-family: "Fira Sans";
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
margin: 0;
padding: 0;
}
#main-section {
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
}
section#header {
height: 16rem;
background-image: url(images/img_device_bg.png);
position: relative;
}
section#grid {
height: calc(100% - 16rem);
background-color: #d1d1d1;
}
#refresh-button {
position: absolute;
right: 5.6rem;
top: 25%;
}
.devices-container {
width: 100%;
height: 50%;
}
.devices-caption {
width: 163.6rem;
height: 12.6rem;
line-height: 12.6rem;
color: #323232;
font-size: 4.2rem;
font-style: italic;
padding-left: 14.2rem;
}
.devices-list {
padding-left: 14.2rem;
padding-right: 14.2rem;
}
.devices-list > smart-button[type="app-button"] {
background-color: #000;
margin: 5.8rem 2.6rem 2.9rem 2.6rem;
width: 18rem;
height: 18rem;
}
.devices-list > smart-button[type="app-button"]:before {
transform: scale(0.536);
line-height: 13rem;
}
.devices-list > smart-button[type="app-button"].focused:before {
color: #ffffff;
}
/** we use :after as label */
.devices-list > smart-button[type="app-button"]:after {
width: 18rem;
height: 5rem;
font-size: 1.6rem;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 KiB

View file

@ -0,0 +1,18 @@
{
"extends": "../../../../.jshintrc",
"predef": [
"assert",
"suite",
"test",
"setup",
"teardown",
"suiteSetup",
"suiteTeardown",
"require",
"requireApp",
"sinon",
"mocha",
"loadBodyHTML"
]
}

View file

@ -0,0 +1,175 @@
/* global MockMozBluetooth, MockBTAdapter, BluetoothManager, MockEventTarget,
MockDiscoveryHandle */
'use strict';
require('/bower_components/evt/index.js');
require('/shared/test/unit/mocks/mock_event_target.js');
require('/shared/test/unit/mocks/mock_navigator_moz_bluetooth_v2.js');
require('/js/bluetooth_loader.js');
require('/js/bluetooth_manager.js');
suite('device-deck/BluetoothManager', function() {
var realMozBluetooth;
var bluetoothManager;
suiteSetup(function() {
var MockDiscoveryHandle = function() {
this.ondevicefound = null;
};
MockDiscoveryHandle.prototype = Object.create(MockEventTarget.prototype);
window.MockDiscoveryHandle = MockDiscoveryHandle;
realMozBluetooth = navigator.mozBluetooth;
switchReadOnlyProperty(navigator, 'mozBluetooth', MockMozBluetooth);
});
suiteTeardown(function() {
window.MockDiscoveryHandle = undefined;
switchReadOnlyProperty(navigator, 'mozBluetooth', realMozBluetooth);
});
var switchReadOnlyProperty = function (originObject, propName, targetObj) {
Object.defineProperty(originObject, propName, {
configurable: true,
get: function() { return targetObj; }
});
};
var setupMockAdapterAndGetHandle = function(context, mockDiscoveryHandle) {
mockDiscoveryHandle = new MockDiscoveryHandle();
MockBTAdapter.state = 'enabled';
MockBTAdapter.startDiscovery = function() {};
MockBTAdapter.stopDiscovery = function() {};
context.sinon.stub(MockBTAdapter, 'startDiscovery', function() {
return Promise.resolve(mockDiscoveryHandle);
});
context.sinon.stub(MockBTAdapter, 'stopDiscovery', function() {
return Promise.resolve();
});
return mockDiscoveryHandle;
};
var teardownMockAdapter = function(mockDiscoveryHandle) {
MockBTAdapter.startDiscovery.restore();
MockBTAdapter.stopDiscovery.restore();
if (mockDiscoveryHandle) {
mockDiscoveryHandle = undefined;
}
};
suite('init()', function() {
setup(function() {
bluetoothManager = new BluetoothManager();
});
teardown(function() {
bluetoothManager = undefined;
});
test('should handle "attributechanged" event after init', function() {
var onAttributeChangedSpy =
this.sinon.spy(bluetoothManager, 'onAttributeChanged');
bluetoothManager.init();
MockMozBluetooth.triggerEventListeners('attributechanged', {
type: 'attributechanged',
attrs: []
});
assert.isTrue(onAttributeChangedSpy.calledOnce);
assert.equal(
onAttributeChangedSpy.firstCall.args[0].type, 'attributechanged');
});
test('should fire "default-adapter-ready" event when evt.attr of ' +
' attributechanged contains defaultAdapter', function() {
var stub = this.sinon.stub();
bluetoothManager.init();
bluetoothManager.on('default-adapter-ready', stub);
MockMozBluetooth.triggerEventListeners('attributechanged', {
type: 'attributechanged',
attrs: ['defaultAdapter']
});
assert.isTrue(stub.calledOnce);
});
});
suite('safelyStartDiscovery()', function() {
setup(function() {
setupMockAdapterAndGetHandle(this);
bluetoothManager = new BluetoothManager();
bluetoothManager.init();
});
teardown(function() {
teardownMockAdapter();
bluetoothManager = undefined;
});
test('should invoke startDiscovery() of defaultAdapter', function(done) {
bluetoothManager.safelyStartDiscovery().then(function() {
assert.isTrue(MockBTAdapter.startDiscovery.calledOnce);
}).then(done, done);
});
});
suite('safelyStopDiscovery()', function() {
setup(function(done) {
setupMockAdapterAndGetHandle(this);
bluetoothManager = new BluetoothManager();
bluetoothManager.init();
bluetoothManager.safelyStartDiscovery().then(done);
});
teardown(function() {
teardownMockAdapter();
bluetoothManager = undefined;
});
test('should invoke stopDiscovery() of defaultAdapter', function(done) {
bluetoothManager.safelyStopDiscovery().then(function() {
assert.isTrue(MockBTAdapter.stopDiscovery.calledOnce);
}).then(done, done);
});
});
suite('onDeviceFound()', function() {
var mockDiscoveryHandle;
var onDeviceFoundSpy;
setup(function() {
mockDiscoveryHandle = setupMockAdapterAndGetHandle(this);
bluetoothManager = new BluetoothManager();
onDeviceFoundSpy = this.sinon.spy(bluetoothManager, 'onDeviceFound');
bluetoothManager.init();
});
teardown(function() {
teardownMockAdapter();
onDeviceFoundSpy.restore();
bluetoothManager = undefined;
});
test('should fire "device-found" event when receiving "devicefound" from ' +
'mozBluetooth', function(done) {
var deviceFoundEvent = {
type: 'devicefound',
device: 'fake-device-data'
};
var stub = this.sinon.stub();
bluetoothManager.on('device-found', stub);
bluetoothManager.safelyStartDiscovery().then(function() {
mockDiscoveryHandle.dispatchEvent(deviceFoundEvent);
assert.isTrue(onDeviceFoundSpy.calledWithExactly(deviceFoundEvent));
assert.isTrue(
stub.calledWithExactly(deviceFoundEvent.device));
}).then(done, done);
});
});
});