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

15
gaia/.editorconfig Executable file
View file

@ -0,0 +1,15 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[{Makefile,*.mk}]
indent_style = tab

175
gaia/.eslintrc Executable file
View file

@ -0,0 +1,175 @@
{
"parser": "babel-eslint",
"plugins": [
"no-unsafe-innerhtml",
"no-wildcard-postmessage"
],
"rules": {
"no-unsafe-innerhtml/no-unsafe-innerhtml": 2,
"no-wildcard-postmessage/no-wildcard-postmessage": 2,
"block-scoped-var": 0,
"brace-style": 0,
"camelcase": 0,
"comma-dangle": 0,
"comma-spacing": 0,
"comma-style": 0,
"complexity": 0,
"consistent-return": 0,
"consistent-this": 0,
"curly": 0,
"default-case": 0,
"dot-location": 0,
"dot-notation": 0,
"eol-last": 0,
"eqeqeq": 0,
"func-names": 0,
"func-style": 0,
"generator-star": 0,
"generator-star-spacing": 0,
"global-strict": 0,
"guard-for-in": 0,
"handle-callback-err": 0,
"indent": 0,
"key-spacing": 0,
"linebreak-style": 0,
"max-depth": 0,
"max-len": 0,
"max-nested-callbacks": 0,
"max-params": 0,
"max-statements": 0,
"new-cap": 0,
"newline-after-var": 0,
"new-parens": 0,
"no-alert": 0,
"no-array-constructor": 0,
"no-bitwise": 0,
"no-caller": 0,
"no-catch-shadow": 0,
"no-comma-dangle": 0,
"no-cond-assign": 0,
"no-console": 0,
"no-constant-condition": 0,
"no-continue": 0,
"no-control-regex": 0,
"no-debugger": 0,
"no-delete-var": 0,
"no-div-regex": 0,
"no-dupe-args": 0,
"no-dupe-keys": 0,
"no-duplicate-case": 0,
"no-else-return": 0,
"no-empty-class": 0,
"no-empty": 0,
"no-empty-label": 0,
"no-eq-null": 0,
"no-eval": 0,
"no-ex-assign": 0,
"no-extend-native": 0,
"no-extra-bind": 0,
"no-extra-boolean-cast": 0,
"no-extra-parens": 0,
"no-extra-semi": 0,
"no-extra-strict": 0,
"no-fallthrough": 0,
"no-floating-decimal": 0,
"no-func-assign": 0,
"no-implied-eval": 0,
"no-inline-comments": 0,
"no-inner-declarations": 0,
"no-invalid-regexp": 0,
"no-irregular-whitespace": 0,
"no-iterator": 0,
"no-labels": 0,
"no-label-var": 0,
"no-lone-blocks": 0,
"no-lonely-if": 0,
"no-loop-func": 0,
"no-mixed-requires": 0,
"no-mixed-spaces-and-tabs": 0,
"no-multiple-empty-lines": 0,
"no-multi-spaces": 0,
"no-multi-str": 0,
"no-native-reassign": 0,
"no-negated-in-lhs": 0,
"no-nested-ternary": 0,
"no-new-func": 0,
"no-new": 0,
"no-new-object": 0,
"no-new-require": 0,
"no-new-wrappers": 0,
"no-obj-calls": 0,
"no-octal-escape": 0,
"no-octal": 0,
"no-param-reassign": 0,
"no-path-concat": 0,
"no-plusplus": 0,
"no-process-env": 0,
"no-process-exit": 0,
"no-proto": 0,
"no-redeclare": 0,
"no-regex-spaces": 0,
"no-reserved-keys": 0,
"no-restricted-modules": 0,
"no-return-assign": 0,
"no-script-url": 0,
"no-self-compare": 0,
"no-sequences": 0,
"no-shadow": 0,
"no-shadow-restricted-names": 0,
"no-space-before-semi": 0,
"no-spaced-func": 0,
"no-sparse-arrays": 0,
"no-sync": 0,
"no-ternary": 0,
"no-throw-literal": 0,
"no-trailing-spaces": 0,
"no-undefined": 0,
"no-undef-init": 0,
"no-undef": 0,
"no-underscore-dangle": 0,
"no-unneeded-ternary": 0,
"no-unreachable": 0,
"no-unused-expressions": 0,
"no-unused-vars": 0,
"no-use-before-define": 0,
"no-var": 0,
"no-void": 0,
"no-warning-comments": 0,
"no-with": 0,
"no-wrap-func": 0,
"object-shorthand": 0,
"one-var": 0,
"operator-assignment": 0,
"operator-linebreak": 0,
"padded-blocks": 0,
"quote-props": 0,
"quotes": 0,
"radix": 0,
"semi": 0,
"semi-spacing": 0,
"sort-vars": 0,
"space-after-function-name": 0,
"space-after-keywords": 0,
"space-before-blocks": 0,
"space-before-function-paren": 0,
"space-before-function-parentheses": 0,
"spaced-line-comment": 0,
"space-in-brackets": 0,
"space-infix-ops": 0,
"space-in-parens": 0,
"space-return-throw-case": 0,
"space-unary-ops": 0,
"strict": 0,
"use-isnan": 0,
"validdoc.js": 0,
"valid-typeof": 0,
"vars-on-top": 0,
"wrap-iife": 0,
"wrap-regex": 0,
"yoda": 0
},
"env": {
"browser": true,
"es6": true
}
}

87
gaia/.gitignore vendored Executable file
View file

@ -0,0 +1,87 @@
.DS_Store
*.rej
*.log
*.orig
*.pyc
*.swp
*.swn
*.swo
.svn
*~
manifest.appcache
/apps/calendar/js/common/presets.js
/apps/camera/js/config.js
/apps/gallery/js/config.js
/apps/findmydevice/js/config.js
/apps/homescreen/js/init.json
/apps/homescreen/everything.me/config/local.json
/apps/costcontrol/js/config.json
/apps/sms/js/blacklist.json
/apps/settings/resources/network.json
/apps/settings/resources/support.json
/apps/settings/resources/device-features.json
/apps/settings/resources/findmydevice.json
/apps/system/resources/icc.json
/apps/system/resources/wapuaprof.json
/apps/*/test/unit/_sandbox.html
/apps/*/test/unit/_proxy.html
/apps/*/locales-obj
/dev_apps/*/test/unit/_sandbox.html
/dev_apps/*/test/unit/_proxy.html
/dev_apps/*/locales-obj
/shared/test/integration/node_modules
/shared/resources/keyboard_layouts.json
/shared/pages/import/js/parameters.js
/shared/js/search_providers.json
/b2g
/firefox
/profile/
/profile-debug/
/profile-test/
/profile-gaia-test-b2g/
/profile-gaia-test-firefox/
/profile-raptor/
/profile.tar.gz
/b2g_sdk/
/tools/test-agent/node_modules
/test-output.xml
/mocha-test-results.html
/build/config/custom-extensions.json
/build/config/custom-prefs.js
/build/config/custom-settings.json
/build/eslint/xfail.list.tmp
/build_stage/
/coverage/
/local.mk
/npm-debug.log
/raptor.log
/metrics.ldjson
/distros/raptor/outoftree_apps/
/tools/xpcwindow/vendor/
/outoftree_apps/
/distros/spark/outoftree_apps/
/distribution/
/.modules.tar
/modules_tar_cachedir
ENV/
.env
modules.tar
mozilla-b2g-gaia-node-modules-*
/git-gaia-node-modules/
/docs/
/.cproject
/.project
/.settings
node_modules/
venv/

9
gaia/.jsdocrc Executable file
View file

@ -0,0 +1,9 @@
{
"plugins": ["plugins/markdown", "build/jsdoc/nostrict"],
"markdown": {
"parser": "gfm",
"hardwrap": true,
"githubRepoName": "gaia",
"githubRepoOwner": "mozilla-b2g"
}
}

98
gaia/.jshintignore Executable file
View file

@ -0,0 +1,98 @@
apps/pdfjs/content/**
apps/pdfjs/test/**
apps/email/build/**
apps/email/built/**
apps/email/js/ext/**
apps/calendar/build/**
apps/calendar/js/ext/**
apps/camera/js/vendor/**
apps/camera/test/**
apps/**/*.esp.js
apps/system/camera/**
build/r.js
build/csslinter.js
build/escodegen.js
build/esprima.js
build/jsmin.js
build/l10n/l20n.js
apps/communications/contacts/oauth2/js/parameters.js
apps/calendar/js/common/presets.js
apps/email/js/alameda.js
apps/email/js/tmpl_builder.js
apps/clock/js/alameda.js
apps/clock/js/ext/**
apps/clock/js/text_builder.js
shared/js/uuid.js
shared/js/intl/l20n*.js
shared/js/media/jpeg-exif.js
apps/keyboard/js/imes/jspinyin/libpinyin.js
apps/keyboard/js/imes/jszhuyin/lib/**
apps/keyboard/js/imes/handwriting/hwr/**
shared/js/setImmediate.js
shared/test/unit/test-agent-server.js
shared/elements/font-fit/font-fit.js
shared/elements/gaia-header/dist/gaia-header.js
shared/elements/gaia-component/gaia-component.js
shared/elements/gaia-icons/gaia-icons.js
shared/elements/gaia-toast/gaia-toast.js
shared/elements/gaia-dialog/*.js
shared/elements/gaia-text-input/*.js
shared/elements/gaia-button/gaia-button.js
shared/pages/import/js/parameters.js
apps/sharedtest/test/unit/setImmediate_test.js
apps/wappush/js/ext/**
apps/keyboard/js/imes/jsavrophonetic/jsavrophonetic.js
apps/keyboard/js/imes/latin/dictionaries/node_modules/**
apps/camera/js/vendor/alameda.js
apps/camera/js/config.js
apps/gallery/js/config.js
apps/gallery/js/frame_scripts.js
apps/gallery/js/metadata_scripts.js
apps/keyboard/js/layouts/**
apps/music/components/**
apps/music/js/metadata_scripts.js
apps/music/node_modules/**
apps/keyboard/js/imes/jszhuyin/tools/cook.js
apps/keyboard/build/keyboard-config.js
apps/settings/js/vendor/alameda.js
apps/settings/js/vendor/jszip.js
apps/sms/lib/**
apps/findmydevice/js/lib/*
apps/findmydevice/js/config.js
apps/system/js/uuid.js
apps/camera/bower_components/**
apps/bluetooth/js/vendor/alameda.js
apps/bluetooth/js/settings.js
apps/homescreen/bower_components/**
shared/js/sync/ext/kinto.min.js
tv_apps/app-deck/bower_components/**
tv_apps/browser/js/authentication_dialog.js
tv_apps/browser/js/browser_db.js
tv_apps/browser/js/index.js
tv_apps/browser/js/search_result.js
tv_apps/browser/js/search_util.js
tv_apps/browser/js/tooltip.js
tv_apps/browser/test/unit/browser_db_test.js
tv_apps/browser/bower_components/**
tv_apps/dashboard/bower_components/**
tv_apps/device-deck/bower_components/**
tv_apps/fling-player/bower_components/**
tv_apps/notification-sender/bower_components/**
tv_apps/smart-home/bower_components/**
tv_apps/smart-settings/bower_components/**
tv_apps/smart-system/bower_components/**
tv_apps/notification/bower_components/**
tv_apps/fling-tutorial/bower_components/**
tv_apps/tv-deck/bower_components/**
tv_apps/tv-epg/bower_components/**
tv_apps/remote-control/bower_components/**
tv_apps/remote-control-client/bower_components/**
tv_apps/tv-ftu/bower_components/**
tests/atoms/remote_date.js
tests/atoms/screenshot.js
tests/jsmarionette/client/marionette-client/docs/**
tests/jsmarionette/plugins/marionette-js-logger/lib/log_grabber.js
tests/jsmarionette/runner/marionette-js-runner/test/fixtures/**
tests/jsmarionette/runner/marionette-js-runner/test/bin/fixtures/**
tests/jsmarionette/runner/marionette-js-runner/venv/**
tests/jsmarionette/runner/mozilla-profile-builder/test/fixtures/**

32
gaia/.jshintrc Executable file
View file

@ -0,0 +1,32 @@
{
"camelcase": false,
"curly": true,
"forin": false,
"latedef": "nofunc",
"newcap": false,
"noarg": true,
"nonew": true,
"quotmark": "single",
"undef": true,
"unused": "vars",
"strict": true,
"trailing": true,
"maxlen": 80,
"eqnull": true,
"esnext": true,
"expr": true,
"globalstrict": true,
"maxerr": 1000,
"regexdash": true,
"laxcomma": true,
"proto": true,
"browser": true,
"devel": true,
"nonstandard": true,
"worker": true,
"-W078": true
}

98
gaia/Android.mk Executable file
View file

@ -0,0 +1,98 @@
LOCAL_PATH:= $(call my-dir)
#
# Gaia glue
#
include $(CLEAR_VARS)
GAIA_PATH := $(abspath $(LOCAL_PATH))
LOCAL_MODULE := gaia
LOCAL_MODULE_CLASS := DATA
LOCAL_MODULE_TAGS := optional eng
LOCAL_SRC_FILES := profile.tar.gz
LOCAL_MODULE_PATH := $(TARGET_OUT_DATA)/local
include $(BUILD_PREBUILT)
# Collect all flags we need to pass to Gaia Makefile
GAIA_MAKE_FLAGS := $(BOARD_GAIA_MAKE_FLAGS)
GAIA_PROFILE_INSTALL_PARENT := $(TARGET_OUT_DATA)/local
GAIA_APP_INSTALL_PARENT := $(GAIA_PROFILE_INSTALL_PARENT)
CLEAN_PROFILE := 0
# In user (production) builds we put gaia apps in /system/b2g/webapps
ifneq ($(filter user userdebug, $(TARGET_BUILD_VARIANT)),)
GAIA_MAKE_FLAGS += PRODUCTION=1
B2G_SYSTEM_APPS := 1
endif
# Gaia currently supports different builds by giving a specific device type.
# GAIA_DEVICE_TYPE:
# phone - default
# tablet
# tv
ifneq (,$(GAIA_DEVICE_TYPE))
GAIA_MAKE_FLAGS += GAIA_DEVICE_TYPE=$(GAIA_DEVICE_TYPE)
endif
# Gaia currently needs to specify the default scale value manually or pictures
# with correct resolution will not be applied.
ifneq (,$(GAIA_DEV_PIXELS_PER_PX))
GAIA_MAKE_FLAGS += GAIA_DEV_PIXELS_PER_PX=$(GAIA_DEV_PIXELS_PER_PX)
endif
ifeq ($(B2G_SYSTEM_APPS),1)
GAIA_APP_INSTALL_PARENT := $(TARGET_OUT)/b2g
CLEAN_PROFILE := 1
endif
GAIA_APP_INSTALL_PATH := $(GAIA_APP_INSTALL_PARENT)/webapps
$(LOCAL_INSTALLED_MODULE):
@echo Gaia install path: $(GAIA_APP_INSTALL_PATH)
mkdir -p $(GAIA_PROFILE_INSTALL_PARENT) $(GAIA_APP_INSTALL_PARENT)
rm -rf $(GAIA_APP_INSTALL_PATH)
@rm -rf $(GAIA_APP_INSTALL_PARENT)/svoperapps
cd $(GAIA_PROFILE_INSTALL_PARENT) && tar xfz $(abspath $<)
mkdir -p $(TARGET_OUT)/b2g
cp $(GAIA_PATH)/profile/user.js $(TARGET_OUT)/b2g/user.js
ifneq ($(GAIA_PROFILE_INSTALL_PARENT), $(GAIA_APP_INSTALL_PARENT))
mv $(GAIA_PROFILE_INSTALL_PARENT)/webapps $(GAIA_APP_INSTALL_PARENT)
endif
GAIA_TESTS_STAGE := $(GAIA_PATH)/tests-stage
.PHONY: gaia-tests-zip
gaia-tests-zip:
rm -rf $(GAIA_TESTS_STAGE)
mkdir -p $(GAIA_TESTS_STAGE)/gaiatest/gaiatest/atoms
@(cd $(GAIA_PATH)/tests/python && tar -chf - *) | (cd $(GAIA_TESTS_STAGE)/gaiatest && tar -xf -)
@(cd $(GAIA_PATH)/tests/atoms && tar -chf - *) | (cd $(GAIA_TESTS_STAGE)/gaiatest/gaiatest/atoms && tar -xf -)
(cd $(GAIA_TESTS_STAGE) && zip -r $(GAIA_PATH)/gaia-tests.zip *)
.PHONY: gaia-prefs
gaia-prefs:
$(MAKE) -C $(GAIA_PATH) $(GAIA_MAKE_FLAGS) preferences settings
.PHONY: $(LOCAL_PATH)/profile.tar.gz
$(LOCAL_PATH)/profile.tar.gz: gaia-prefs
ifeq ($(CLEAN_PROFILE), 1)
rm -rf $(GAIA_PATH)/profile $(GAIA_PATH)/profile.tar.gz
endif
$(MAKE) -C $(GAIA_PATH) $(GAIA_MAKE_FLAGS) LD_LIBRARY_PATH=$(LD_LIBRARY_PATH) profile
@FOLDERS='webapps'; \
if [ -d $(GAIA_PATH)/profile/indexedDB ]; then \
FOLDERS="indexedDB $${FOLDERS}"; \
fi; \
if [ -d $(GAIA_PATH)/profile/svoperapps ]; then \
FOLDERS="svoperapps $${FOLDERS}"; \
fi; \
cd $(GAIA_PATH)/profile && tar cfz $(abspath $@) $${FOLDERS}; \
if [ -d $(GAIA_PATH)/profile/indexedDB ]; then \
rm -rf $(GAIA_PATH)/profile/indexedDB; \
fi; \
if [ -d $(GAIA_PATH)/profile/svoperapps ]; then \
rm -rf $(GAIA_PATH)/profile/svoperapps; \
fi

13
gaia/LICENSE Executable file
View file

@ -0,0 +1,13 @@
Copyright 2015, Mozilla Foundation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

1139
gaia/Makefile Executable file

File diff suppressed because it is too large Load diff

10
gaia/apps/bluetooth/.jshintrc Executable file
View file

@ -0,0 +1,10 @@
{
"extends": "../../.jshintrc",
"predef": [
"define",
"require",
"Promise",
"Notification",
"console"
]
}

View file

@ -0,0 +1,49 @@
{
appDir: '..',
baseUrl: 'js',
mainConfigFile: '../js/config/require.js',
dir: '../../../build_stage/bluetooth',
// Set the path to "empty" to prevent the scripts defining global objects
// from being merged or they are removed after the optimization process,
// which makes the objects inaccessible by reference.
// If the inquiries to the object are all performed by requirejs, we can
// remove the path of the object from the following list.
paths: {
'shared/bluetooth_helper': 'empty:'
},
findNestedDependencies: true,
// Be sure to normalize all define() calls by extracting
// dependencies so Function toString is not needed, and
// lower capability devices like Tarako can optimize
// memory by discarding function sources. This is
// automatically done when an 'optimize' value other than
// 'none' is used. This setting makes sure it happens for
// builds where 'none' is used for 'optimize'.
normalizeDirDefines: 'all',
// optimize is now passed via Makefile's GAIA_SETTINGS_MINIFY
// default is none if not passed at all.
// optimize: 'none',
// Just strip comments, no code compression or mangling.
// Only active if optimize: 'uglify2'
uglify2: {
// Comment out the output section to get rid of line
// returns and tabs spacing.
output: {
beautify: false
},
compress: true,
mangle: true
},
fileExclusionRegExp: /^\.|^test$|^build$/,
// Keeping build dir since Makefile cleans it up and
// preps build dir with the shared directory
keepBuildDir: true,
removeCombined: true
}

View file

@ -0,0 +1,43 @@
'use strict';
/* jshint node: true */
var utils = require('utils');
exports.execute = function(options) {
var appName = utils.basename(options.APP_DIR);
var config = utils.getFile(options.APP_DIR, 'build',
'bluetooth.build.jslike');
var rjsPath = utils.joinPath(options.GAIA_DIR, 'build', 'r.js');
var requirejs;
if (utils.isNode()) {
requirejs = require(rjsPath);
} else {
var sandbox = utils.createSandbox();
sandbox.arguments = [];
sandbox.requirejsAsLib = true;
sandbox.print = function() {
utils.log(appName, Array.prototype.join.call(arguments, ' '));
};
utils.runScriptInSandbox(rjsPath, sandbox);
requirejs = sandbox.requirejs;
}
// logLevel set 4 for silent, set 0 for all
var log = 'logLevel=' + (options.VERBOSE === '1' ? '0' : '4');
var optimize = 'optimize=' +
(options.GAIA_OPTIMIZE === '1' ? 'uglify2' : 'none');
var build = new Promise(function(resolve, reject) {
requirejs.optimize([config.path, optimize, log], resolve, reject);
});
return build
.then(function() {
utils.log(appName, 'require.js optimize done');
})
.catch(function(err) {
utils.log(appName, 'require.js optimize failed');
utils.log(appName, err);
});
};

View file

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

View file

@ -0,0 +1,398 @@
/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
'use strict';
var gDeviceList = null;
// handle Bluetooth settings
navigator.mozL10n.once(function deviceList() {
var _ = navigator.mozL10n.get;
var settings = window.navigator.mozSettings;
var bluetooth = window.navigator.mozBluetooth;
var defaultAdapter = null;
var _debug = false;
if (!settings || !bluetooth) {
return;
}
function debug(msg) {
if (!_debug) {
return;
}
console.log('[Bluetooth APP Device List]: ' + msg);
}
// device list
gDeviceList = (function devicesList() {
var deviceList = document.getElementById('devices-list-view');
var bluetoothSearch = document.getElementById('bluetooth-search');
var searchAgainBtn = document.getElementById('search-device');
var searchingItem = document.getElementById('bluetooth-searching');
var header = document.getElementById('devices-list-header');
var pairingAddress = null;
var connectingAddress = null;
var connectedAddress = null;
// stop discover other device after 60 seconds
var discoverTimeoutTime = 60000;
var discoverTimeout = null;
// Register handler from share activity
var onDeviceSelectedHandler = null;
var onExitBtnClickedHandler = null;
var isSendingFile = false;
var pairList = {
title: document.getElementById('bluetooth-paired-title'),
list: document.getElementById('bluetooth-paired-devices'),
index: [],
clear: function emptyList() {
while (this.list.hasChildNodes()) {
this.list.removeChild(this.list.lastChild);
}
this.index = [];
},
show: function showArea(isShown) {
if (!isShown) {
this.clear();
}
this.title.hidden = !isShown;
this.list.hidden = !isShown;
}
};
var openList = {
title: document.getElementById('bluetooth-found-title'),
list: document.getElementById('bluetooth-devices'),
index: [],
clear: function emptyList() {
while (this.list.hasChildNodes()) {
this.list.removeChild(this.list.lastChild);
}
this.index = [];
},
show: function showArea(isShown) {
if (!isShown) {
this.clear();
}
this.title.hidden = !isShown;
this.list.hidden = !isShown;
}
};
searchAgainBtn.onclick = function searchAgainClicked() {
if (isSendingFile || pairingAddress) {
return;
}
updateDeviceList(true); // reset network list
openList.clear();
startDiscovery();
};
// private DOM helper: create a device list item
function newListItem(device, descL10nId) {
var deviceName = document.createElement('span');
var aName = (device.name === '') ? _('unnamed-device') : device.name;
var aL10nId = (device.name === '') ? 'unnamed-device' : '';
deviceName.textContent = aName;
deviceName.dataset.l10nId = aL10nId;
var deviceDesc = document.createElement('small');
deviceDesc.textContent = (descL10nId === '') ? '' : _(descL10nId);
deviceDesc.dataset.l10nId = descL10nId;
var pairingProgress = document.createElement('progress');
pairingProgress.classList.add('overlapping-icon');
pairingProgress.classList.add('hidden');
var a = document.createElement('a');
a.appendChild(deviceName);
a.appendChild(deviceDesc);
var li = document.createElement('li');
li.classList.add('bluetooth-device');
li.classList.add('bluetooth-type-' + device.icon);
li.appendChild(a);
li.appendChild(pairingProgress);
return li;
}
// immediatly UI update, DOM element manipulation.
function updateDeviceList(show) {
bluetoothSearch.hidden = !show;
if (show) {
openList.show(true);
searchingItem.hidden = false;
} else {
openList.show(false);
pairList.show(false);
searchingItem.hidden = true;
pairingAddress = null;
connectingAddress = null;
connectedAddress = null;
// clear discoverTimeout
clearTimeout(discoverTimeout);
discoverTimeout = null;
}
}
// do default actions (start discover avaliable devices)
// when DefaultAdapter is ready.
function initial(adapter, deviceSelectedCallback, exitBtnClickedCallback) {
deviceList.hidden = false;
header.addEventListener('action', cancelActivity);
defaultAdapter = adapter;
defaultAdapter.onpairedstatuschanged = function bt_getPairedMessage(evt) {
showDevicePaired(evt.status, 'Authentication Failed', evt.address);
};
defaultAdapter.ondiscoverystatechanged =
function bt_discoveryStateChanged(evt) {
if (!evt.discovering) {
searchAgainBtn.disabled = false;
searchingItem.hidden = true;
clearTimeout(discoverTimeout);
discoverTimeout = null;
} else {
searchAgainBtn.disabled = true;
searchingItem.hidden = false;
}
};
onDeviceSelectedHandler = deviceSelectedCallback;
onExitBtnClickedHandler = exitBtnClickedCallback;
// get paired device and restore connection
// if we have one device connected before.
getPairedDevice();
startDiscovery();
}
function uninit() {
deviceList.hidden = true;
defaultAdapter = null;
header.removeEventListener('action', cancelActivity);
}
function getPairedDevice() {
if (!bluetooth.enabled || !defaultAdapter) {
return;
}
var req = defaultAdapter.getPairedDevices();
req.onsuccess = function bt_getPairedSuccess() {
// copy for sorting
var paired = req.result.slice();
var length = paired.length;
if (length === 0) {
pairList.show(false);
return;
}
pairList.clear();
paired.sort(function(a, b) {
return a.name > b.name;
});
function deviceStatus(){
return (function(device) {
var stateL10nId = (device.address === connectedAddress) ?
'device-status-connected' : '';
var aItem = newListItem(device, stateL10nId);
aItem.onclick = function() {
if (isSendingFile || pairingAddress) {
return;
}
stopDiscovery();
// show the description to be Connecting...
// since we do connection and send file to the device
var small = aItem.querySelector('small');
small.textContent = _('device-status-connecting');
small.dataset.l10nId = 'device-status-connecting';
readyToSendFile(device);
};
pairList.list.appendChild(aItem);
pairList.index[device.address] = [device, aItem];
});
}
for (var i = 0; i < length; i++) {
deviceStatus()(paired[i]);
}
pairList.show(true);
};
}
// callback function when an avaliable device found
function onDeviceFound(evt) {
var device = evt.device;
// Ignore duplicate and paired device. Update the name if needed.
var existingDevice = openList.index[device.address] ||
pairList.index[device.address];
if (existingDevice) {
var existingItem = existingDevice[1];
if (device.name && existingItem) {
var deviceName = existingItem.querySelector('a > span');
if (deviceName) {
deviceName.dataset.l10nId = '';
deviceName.textContent = device.name;
}
}
return;
}
var aItem = newListItem(device, 'device-status-tap-connect');
// bind paired callback
aItem.onclick = function() {
stopDiscovery();
if (isSendingFile || pairingAddress) {
return;
}
var small = aItem.querySelector('small');
small.textContent = _('device-status-pairing');
small.dataset.l10nId = 'device-status-pairing';
// hide the device icon
aItem.classList.add('icon-hidden');
// show the pairing progress
var progress = aItem.querySelector('progress');
progress.classList.remove('hidden');
var req = defaultAdapter.pair(device.address);
pairingAddress = device.address;
var msg = 'pairing with address = ' + pairingAddress;
debug(msg);
req.onerror = function bt_pairError(error) {
showDevicePaired(false, req.error.name);
};
};
openList.list.appendChild(aItem);
openList.index[device.address] = [device, aItem];
}
function showDevicePaired(paired, errorMessage,
addressFromOnpairedstatuschangedEvent) {
// If we don't know the pairing device address,
// it means the pair request is handled by interface level.
// So we need to do:
// 1. update paired list
// 2. remove the paired device from found device list
if (!pairingAddress) {
getPairedDevice();
// remove the paired device from found device list.
if (addressFromOnpairedstatuschangedEvent && paired) {
// If the device is on the list, remove it.
// The situation is coming while Settings app pairing with the device.
// And Bluetooth app is running in-line pairing in the background.
if (openList.index[addressFromOnpairedstatuschangedEvent]) {
var wantoRemovedItem =
openList.index[addressFromOnpairedstatuschangedEvent][1];
openList.list.removeChild(wantoRemovedItem);
delete openList.index[addressFromOnpairedstatuschangedEvent];
}
}
// Early return here since the event is not caused via Bluetooth app.
// Skip over following UI updating flow.
return;
}
// clear pairingAddress first to prevent execute
// the same status update twice.
var workingAddress = pairingAddress;
pairingAddress = null;
if (paired) {
// if the device is on the list, remove it.
// it will show on paired list later.
if (openList.index[workingAddress]) {
var device = openList.index[workingAddress][0];
var item = openList.index[workingAddress][1];
openList.list.removeChild(item);
delete openList.index[workingAddress];
connectingAddress = workingAddress;
// Already paired with target device. Then, it's able to send files.
readyToSendFile(device);
}
} else {
// show pair process fail.
var msg = _('error-pair-title');
if (errorMessage === 'Repeated Attempts') {
msg = msg + '\n' + _('error-pair-toofast');
} else if (errorMessage === 'Authentication Failed') {
msg = msg + '\n' + _('error-pair-pincode');
}
debug(msg);
window.alert(msg);
// rollback device status
if (openList.index[workingAddress]) {
var small = openList.index[workingAddress][1].querySelector('small');
small.textContent = _('device-status-tap-connect');
small.dataset.l10nId = 'device-status-tap-connect';
// show the device icon
var li = openList.index[workingAddress][1];
li.classList.remove('icon-hidden');
// hide the pairing progress
var progress =
openList.index[workingAddress][1].querySelector('progress');
progress.classList.add('hidden');
}
}
// acquire a new paired list no matter paired or unpaired
getPairedDevice();
}
function startDiscovery() {
if (!bluetooth.enabled || !defaultAdapter || discoverTimeout) {
return;
}
var req = defaultAdapter.startDiscovery();
req.onsuccess = function bt_discoveryStart() {
if (!discoverTimeout) {
discoverTimeout = setTimeout(stopDiscovery, discoverTimeoutTime);
}
};
req.onerror = function bt_discoveryFailed() {
console.error('Can not discover nearby device');
};
}
function stopDiscovery() {
if (!bluetooth.enabled || !defaultAdapter || !discoverTimeout) {
return;
}
var req = defaultAdapter.stopDiscovery();
req.onerror = function bt_discoveryStopFailed() {
console.error('Failed to stop discovery of nearby devices');
};
clearTimeout(discoverTimeout);
discoverTimeout = null;
}
function readyToSendFile(targetDevice) {
isSendingFile = true;
if (onDeviceSelectedHandler) {
onDeviceSelectedHandler(targetDevice);
}
}
function cancelActivity() {
if (onExitBtnClickedHandler) {
onExitBtnClickedHandler();
}
}
// API
return {
update: updateDeviceList,
initWithAdapter: initial,
uninit: uninit,
startDiscovery: startDiscovery,
onDeviceFound: onDeviceFound
};
})();
});

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(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,198 @@
/**
* BluetoothClassOfDeviceMapper:
* - BluetoothClassOfDeviceMapper is a mapper that helps settings/bluetooth
* apps to decide the icon type. Since platform exposed the 'number' type
* for class of device(Major Service Classes, Major Device Classes,
* Minor Device Classes), the helper will map the input number to specific
* device type. And the decision is referenced from Baseband.
* (Assigned numbers for Baseband identifies the Inquiry Access codes
* and Class of Device/Service (CoD) fields.)[3]
* According to the specifications described, we define the device type in
* Gaia side entirely. And the naming of device type we given is also
* mapping to CSS style sheet.
*
* - [1] Bluetooth API v1, BluetoothDevice icon property:
* https://developer.mozilla.org/en-US/docs/Web/API/BluetoothDevice.icon
* - [2] Bluetooth API v2, BluetoothClassOfDevice object:
* https://wiki.mozilla.org/B2G/Bluetooth/WebBluetooth-v2/
* BluetoothClassOfDevice
* - [3] Bluetooth Class of Device Reference:
* https://www.bluetooth.org/en-us/specification/assigned-numbers/baseband
* - We can update the mapper if the specifications Baseband is updated.
*
* @module BluetoothClassOfDeviceMapper
*/
define(function() {
'use strict';
var _debug = false;
var debug = function() {};
if (_debug) {
debug = function btcodm_debug(msg) {
console.log('--> [BluetoothCODMapper]: ' + msg);
};
}
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 BluetoothClassOfDeviceMapper = {
/**
* The string indicating which kind of icon could be used to represent
* the device.
*
* @access public
* @memberOf BluetoothClassOfDeviceMapper
* @param {BluetoothClassOfDevice} cod
* @return {String}
*/
getDeviceType: function btcodm_getDeviceType(cod) {
debug('cod.majorDeviceClass = ' + cod.majorDeviceClass);
debug('cod.majorServiceClass = ' + cod.majorServiceClass);
debug('cod.minorDeviceClass = ' + cod.minorDeviceClass);
// 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];
debug('majorDeviceClass = ' + majorDeviceClass);
if (typeof(majorDeviceClass) === 'object') {
// drop in other Major Class
deviceType = majorDeviceClass[cod.minorDeviceClass] ||
majorDeviceClass.reserved || '';
debug('return in minor class, type = ' + deviceType);
return deviceType;
} else if (typeof(majorDeviceClass) === 'string') {
// drop in LAN/Network Access Point Major Class
debug('return in major class, type = network-wireless');
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';
debug('return in major service class, type = audio-card');
return deviceType;
} else {
// Not in any Class which is defined in Gaia.
debug('not mapping in any class, return type = ' + deviceType);
return deviceType;
}
}
};
return BluetoothClassOfDeviceMapper;
});

View file

@ -0,0 +1,953 @@
/**
* BluetoothConnectionManager:
* BluetoothConnectionManager only update state and does not involve in any UI
* logic.
*
* @module BluetoothConnectionManager
*/
define(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,158 @@
/**
* BluetoothDevice:
* - BluetoothDevice is an Observable that wraps the platform
* BluetoothDevice object.
* - It has some observable properties: name, paired, cod
* BluetoothDevice only update device information and does not involve in any
* UI logic.
*
* @module BluetoothDevice
*/
define(function(require) {
'use strict';
var BtClassOfDeviceMapper = require('modules/bluetooth/bluetooth_cod_mapper');
var Observable = require('modules/mvvm/observable');
var _debug = false;
var debug = function() {};
if (_debug) {
debug = function btd_debug(msg) {
console.log('--> [BluetoothDevice]: ' + msg);
};
}
/**
* Provide a function to update connection info.
*
* @access public
* @memberOf BluetoothDevice
* @param {Object} options
* @param {Boolean} options.connectionStatus - is connected or not
* @param {Object} options.profiles - profiles of the connection type
*/
var updateConnectionInfo = function(options) {
debug('updateConnectionInfo(): this.address = ' + this.address +
', this.name = ' + this.name +
', options = ' + JSON.stringify(options));
if (options.connectionStatus) {
this.connectionStatus = options.connectionStatus;
}
if (options.profiles) {
this.profiles = Object.create(options.profiles);
}
debug('updateConnectionInfo(): this.connectionStatus = ' +
this.connectionStatus + ', this.profiles = ' +
JSON.stringify(this.profiles));
};
/**
* Provide a function to update description text.
*
* @access public
* @memberOf BluetoothDevice
*/
var updateDescriptionText = function() {
debug('updateDescriptionText():');
// Define description for found device.
if (this.paired === false) {
this.descriptionText = 'tapToConnect';
} else if (this.paired === 'pairing') {
this.descriptionText = 'pairing';
} else if (this.paired === true) {
this.descriptionText = 'paired';
// Define description for paired device.
if (this.connectionStatus === 'connecting') {
this.descriptionText = 'connecting';
} else if (this.connectionStatus === 'connected') {
if (this.profiles) {
var hfpConnected = this.profiles.hfp;
var a2dpConnected = this.profiles.a2dp;
if (hfpConnected && a2dpConnected) {
this.descriptionText = 'connectedWithDeviceMedia';
} else if (hfpConnected) {
this.descriptionText = 'connectedWithDevice';
} else if (a2dpConnected) {
this.descriptionText = 'connectedWithMedia';
} else {
this.descriptionText = 'connectedWithNoProfileInfo';
}
} else {
this.descriptionText = 'connectedWithNoProfileInfo';
}
} else if (this.connectionStatus === 'disconnected') {
this.descriptionText = 'disconnected';
}
}
debug('updateDescriptionText(): this.descriptionText = ' +
this.descriptionText);
};
/**
* @class BluetoothDevice
* @requires module:modules/mvvm/observable
* @param {BluetoothDevice} device
* @return {Observable} observableBluetoothDevice
*/
return function ctor_bluetooth_device(device) {
var type = BtClassOfDeviceMapper.getDeviceType(device.cod);
var connectionStatus = device.connectionStatus || 'disconnected';
var observableBluetoothDevice = Observable({
name: device.name,
paired: device.paired,
address: device.address,
type: type,
connectionStatus: connectionStatus,
profiles: null,
descriptionText: '',
get data() {return device;},
updateConnectionInfo: updateConnectionInfo,
updateDescriptionText: updateDescriptionText
});
/**
* Observe 'paired', 'connectionStatus', and 'profiles' properties changed
* event in init function. Once these properties changed, we can update
* corrected description for the device.
*/
observableBluetoothDevice._init = function btd__init() {
this.observe('paired', this.updateDescriptionText.bind(this));
this.observe('connectionStatus', this.updateDescriptionText.bind(this));
this.observe('profiles', this.updateDescriptionText.bind(this));
};
/**
* watch 'onattributechanged' event
* A function to receive device which is just found via discovery handler.
* And set a function to handle 'onattributechanged' event.
*/
device.onattributechanged = function btd_onDeviceAttributeChanged(evt) {
for (var i in evt.attrs) {
debug('onDeviceAttributeChanged(): ' + evt.attrs[i]);
switch (evt.attrs[i]) {
case 'name':
observableBluetoothDevice.name = device.name;
break;
case 'paired':
debug('onDeviceAttributeChanged(): ' +
'device.paired = ' + device.paired);
observableBluetoothDevice.paired = device.paired;
break;
case 'cod':
debug('onDeviceAttributeChanged(): ' +
'device.cod = ' + device.cod);
observableBluetoothDevice.type =
BtClassOfDeviceMapper.getDeviceType(device.cod);
break;
default:
break;
}
}
};
observableBluetoothDevice._init();
return observableBluetoothDevice;
};
});

View file

@ -0,0 +1,242 @@
'use strict';
define(function(require) {
var elements = new WeakMap();
var ObservableArray = require('modules/mvvm/observable_array');
/*
* A ListView takes an ObservableArray or an ordinary array, and generate/
* manipulate the corresponding DOM elements of the content in the array using
* the given template function. If the array is an ObservableArray, ListView
* updates the DOM elements accordingly when the array is manipulated.
*/
var ListView = function(root, observableArray, templateFunc) {
var _observableArray = null;
var _root = root;
var _templateFunc = templateFunc;
var _enabled = true;
// keep track of observeables so we can unobserve
var observerMap = new WeakMap();
function releaseObservers(element) {
var observable = observerMap.get(element);
if (!observable || typeof observable !== 'object') {
return;
}
var observers = observerMap.get(observable);
if (observers) {
observers.forEach(function eachObserver(observer) {
observable.unobserve(observer);
});
observerMap.delete(observable);
observerMap.delete(element);
}
}
// helper to observe something and automatically remove old observers
function observeAndCall(observable, map) {
var observers = observerMap.get(observable) || [];
if (observerMap.has(observable)) {
observers = observerMap.get(observable);
}
observers = Object.keys(map).map(function eachProperty(property) {
observable.observe(property, map[property]);
map[property]();
return map[property];
});
observerMap.set(observable, observers);
}
var _helper = {
observeAndCall: observeAndCall
};
var _handleEvent = function(event) {
// Ignore the change event when the list view is not enabled.
if (!_enabled) {
return;
}
var data = event.data;
switch (event.type) {
case 'insert':
_insert(data.index, data.items);
break;
case 'remove':
_remove(data.index, data.count);
break;
case 'replace':
_replace(data.index, data.newValue);
break;
case 'reset':
_reset(data.items || []);
break;
default:
break;
}
};
var _insert = function(index, items) {
if (items.length <= 0) {
return;
}
// add DOM elements
var referenceElement = _root.children[index];
for (var i = items.length - 1; i >= 0; i--) {
var curElement = _templateFunc(items[i], undefined, _helper);
observerMap.set(curElement, items[i]);
_root.insertBefore(curElement, referenceElement);
referenceElement = curElement;
}
};
var _remove = function(index, count) {
if (count === 0) {
return;
}
// remove DOM elements
if (count >= _root.childElementCount) {
// clear all
while (_root.firstElementChild) {
releaseObservers(_root.firstElementChild);
_root.removeChild(_root.firstElementChild);
}
} else {
var nextElement = _root.children[index];
for (var i = 0; i < count; i++) {
if (nextElement) {
var temp = nextElement.nextElementSibling;
releaseObservers(nextElement);
_root.removeChild(nextElement);
nextElement = temp;
} else {
break;
}
}
}
};
var _replace = function(index, value) {
var element = _root.children[index];
if (element) {
releaseObservers(element);
var newElement = _templateFunc(value, element, _helper);
observerMap.set(element, value);
if (newElement !== element) {
_root.insertBefore(newElement, element);
_root.removeChild(element);
}
}
};
var _reset = function(items) {
var itemCount = items.length;
var elementCount = _root.childElementCount;
if (itemCount === 0) {
_remove(0, elementCount);
} else if (itemCount <= elementCount) {
items.forEach(function(item, index) {
_replace(index, item);
});
// remove extra elements
_remove(itemCount, elementCount - itemCount);
} else {
var slicedItems = items.slice(0, elementCount);
var remainingItems = items.slice(elementCount);
slicedItems.forEach(function(item, index) {
_replace(index, item);
});
// add extra elements
_insert(elementCount, remainingItems);
}
};
var _enabledChanged = function() {
if (_enabled) {
_reset(_observableArray.array);
}
};
var view = {
set: function lv_set(newArray) {
if (_observableArray) {
_observableArray.unobserve(_handleEvent);
}
if (!newArray) {
// clear all existing items
if (_observableArray) {
_remove(0, _observableArray.length);
_observableArray = null;
}
return;
}
// If new array is an ordinary array, convert it to an ObservableArray.
if (Array.isArray(newArray)) {
newArray = ObservableArray(newArray);
}
_observableArray = newArray;
// An ObservableArray fires the four events for notifying its changes.
// Register to these events for updating UI.
_observableArray.observe('insert', _handleEvent);
_observableArray.observe('remove', _handleEvent);
_observableArray.observe('replace', _handleEvent);
_observableArray.observe('reset', _handleEvent);
if (this.enabled) {
_reset(_observableArray.array);
}
},
destroy: function() {
// unobserve from array and null everything out
if (_observableArray) {
_observableArray.unobserve(_handleEvent);
}
_remove(0, _observableArray.length);
elements.delete(_root);
_root = null;
_observableArray = null;
_templateFunc = null;
_enabled = false;
},
get element() {
return _root;
},
set enabled(value) {
if (_enabled !== value) {
_enabled = value;
_enabledChanged();
}
},
get enabled() {
return _enabled;
}
};
if (elements.has(_root)) {
// destroy old ListView if we setup a second one
elements.get(_root).destroy();
}
elements.set(_root, view);
// emtpy element at creation time
_root.innerHTML = '';
view.set(observableArray);
return view;
};
return ListView;
});

View file

@ -0,0 +1,102 @@
'use strict';
define(function() {
function unobserve(_eventHandlers, prop, handler) {
// arguments in reverse order to support .bind(handler) for the
// unbind from all case
function removeHandler(handler, prop) {
var handlers = _eventHandlers[prop];
if (!handlers) {
return;
}
var index = handlers.indexOf(handler);
if (index >= 0) {
handlers.splice(index, 1);
}
}
if (typeof prop === 'function') {
// (handler) -- remove from every key in _eventHandlers
Object.keys(_eventHandlers).forEach(removeHandler.bind(null, prop));
} else if (handler) {
// (prop, handler) -- remove handler from the specific prop
removeHandler(handler, prop);
} else if (prop in _eventHandlers) {
// (prop) -- otherwise remove all handlers for property
_eventHandlers[prop] = [];
}
}
/*
* An Observable is able to notify its property change. It is initialized by
* an ordinary object.
*/
function Observable(obj) {
var _eventHandlers = {};
var _observable = {
observe: function o_observe(p, handler) {
/*
* We should check if _observable[_p] exists. Since _observable[_p] is
* created along with _eventHandlers[p], here we simply check
* _eventHandlers[p].
*/
var handlers = _eventHandlers[p];
if (handlers) {
handlers.push(handler);
}
},
/**
* unobserve([prop], handler) - remove handler from observeable callbacks
*/
unobserve: unobserve.bind(null, _eventHandlers)
};
var _getFunctionTemplate = function(p) {
return function() {
return _observable['_' + p];
};
};
var _setFunctionTemplate = function(p) {
return function(value) {
var oldValue = _observable['_' + p];
if (oldValue !== value) {
_observable['_' + p] = value;
var handlers = _eventHandlers[p];
handlers.forEach(function(handler) {
handler(value, oldValue);
});
}
};
};
/*
* Iterate all properties in the object and create corresponding getter and
* setter for them.
*/
for (var p in obj) {
// If p is a function, simply add it to the observable.
if (typeof obj[p] === 'function') {
_observable[p] = obj[p];
continue;
}
_eventHandlers[p] = [];
Object.defineProperty(_observable, '_' + p, {
value: obj[p],
writable: true
});
Object.defineProperty(_observable, p, {
enumerable: true,
get: _getFunctionTemplate(p),
set: _setFunctionTemplate(p)
});
}
return _observable;
}
return Observable;
});

View file

@ -0,0 +1,153 @@
'use strict';
define(function() {
function unobserve(_eventHandlers, prop, handler) {
// arguments in reverse order to support .bind(handler) for the
// unbind from all case
function removeHandler(handler, prop) {
var handlers = _eventHandlers[prop];
if (!handlers) {
return;
}
var index = handlers.indexOf(handler);
if (index >= 0) {
handlers.splice(index, 1);
}
}
if (typeof prop === 'function') {
// (handler) -- remove from every key in _eventHandlers
Object.keys(_eventHandlers).forEach(removeHandler.bind(null, prop));
} else if (handler) {
// (prop, handler) -- remove handler from the specific prop
removeHandler(handler, prop);
} else if (prop in _eventHandlers) {
// (prop) -- otherwise remove all handlers for property
_eventHandlers[prop] = [];
}
}
/*
* An ObservableArray is able to notify its change through four basic
* operations including 'insert', 'remove', 'replace', 'reset'. It is
* initialized by an ordinary array.
*/
function ObservableArray(array) {
var _array = array || [];
var _eventHandlers = {
'insert': [],
'remove': [],
'replace': [],
'reset': []
};
var _notify = function(eventType, data) {
var handlers = _eventHandlers[eventType];
handlers.forEach(function(handler) {
handler({
type: eventType,
data: data
});
});
};
return {
get length() {
return _array.length;
},
get array() {
return _array;
},
forEach: function oa_foreach(func) {
_array.forEach(func);
},
observe: function oa_observe(eventType, handler) {
var handlers = _eventHandlers[eventType];
if (handlers) {
handlers.push(handler);
}
},
unobserve: unobserve.bind(null, _eventHandlers),
push: function oa_push(item) {
_array.push(item);
_notify('insert', {
index: _array.length - 1,
count: 1,
items: [item]
});
},
pop: function oa_pop() {
if (!_array.length) {
return;
}
var item = _array.pop();
_notify('remove', {
index: _array.length,
count: 1
});
return item;
},
splice: function oa_splice(index, count) {
if (arguments.length < 2) {
return;
}
var addedItems = Array.prototype.slice.call(arguments, 2);
_array.splice.apply(_array, arguments);
if (count) {
_notify('remove', {
index: index,
count: count
});
}
if (addedItems.length) {
_notify('insert', {
index: index,
count: addedItems.length,
items: addedItems
});
}
},
set: function oa_set(index, value) {
if (index < 0 || index >= _array.length) {
return;
}
var oldValue = _array[index];
_array[index] = value;
_notify('replace', {
index: index,
oldValue: oldValue,
newValue: value
});
},
get: function oa_get(index) {
return _array[index];
},
reset: function oa_reset(array) {
_array = array;
_notify('reset', {
items: _array
});
}
};
}
return ObservableArray;
});

View file

@ -0,0 +1,12 @@
/**
* Wraps navigator.bluetooth for replacing it in unit tests more easily.
*/
define(function() {
'use strict';
if (navigator.mozBluetooth) {
return navigator.mozBluetooth;
} else {
return 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(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(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;
});

View file

@ -0,0 +1,901 @@
'use strict';
// handle Bluetooth settings
navigator.mozL10n.once(function bluetoothSettings() {
// Service ID for profiles
var Profiles = {
'HFP': 0x111E,
'A2DP': 0x110D
};
var _ = navigator.mozL10n.get;
var l10n = navigator.mozL10n;
var settings = navigator.mozSettings;
var bluetooth = navigator.mozBluetooth;
var defaultAdapter = null;
const DEVICE_CONNECTED_KEY = 'bluetooth.device.connected';
var MAX_DEVICE_NAME_LENGTH = 20;
if (!settings || !bluetooth) {
return;
}
var header = document.querySelector('gaia-header');
header.addEventListener('action', function backToSettings() {
window.close();
});
var gBluetoothCheckBox = document.querySelector('.bluetooth-status gaia-switch');
// display Bluetooth power state
function updateBluetoothState(value) {
gBluetoothCheckBox.checked = value;
}
// activate main button
gBluetoothCheckBox.addEventListener('change', function() {
console.log("GOT CHANGE EVENT!, setting disabled");
var req = settings.createLock().set({'bluetooth.enabled': this.checked});
gBluetoothCheckBox.setAttribute('disabled', true);
req.onerror = function() {
gBluetoothCheckBox.removeAttribute('disabled');
};
});
function initialDefaultAdapter() {
if (!bluetooth.enabled)
return;
var req = bluetooth.getDefaultAdapter();
req.onsuccess = function bt_getAdapterSuccess() {
defaultAdapter = req.result;
if (defaultAdapter == null) {
// we can do nothing without DefaultAdapter, so set bluetooth disabled
settings.createLock().set({'bluetooth.enabled': false});
return;
}
defaultAdapter.ondevicefound = gDeviceList.onDeviceFound;
// initial related components that need defaultAdapter.
gMyDeviceInfo.initWithAdapter();
gDeviceList.initWithAdapter();
};
req.onerror = function bt_getAdapterFailed() {
// we can do nothing without DefaultAdapter, so set bluetooth disabled
settings.createLock().set({'bluetooth.enabled': false});
};
}
// device information
var gMyDeviceInfo = (function deviceInfo() {
var visibleItem = document.querySelector('.device-visible');
var visibleName = document.querySelector('.bluetooth-device-name');
var visibleCheckBox = document.querySelector('.device-visible gaia-switch');
var bluetoothRename = document.getElementById('bluetooth-rename');
var renameButton = document.getElementById('rename-device');
var updateNameDialog = document.getElementById('update-device-name');
var updateNameInput = document.getElementById('update-device-name-input');
var updateNameCancelButton =
document.getElementById('update-device-name-cancel');
var updateNameConfirmButton =
document.getElementById('update-device-name-confirm');
var visibleTimeout = null;
var visibleTimeoutTime = 120000; // visibility will timeout after 2 minutes
var myName = '';
visibleCheckBox.addEventListener('change', function() {
setDiscoverable(visibleCheckBox.checked);
});
renameButton.onclick = function renameBtnClicked() {
if (myName === '') {
myName = visibleName.textContent = defaultAdapter.name;
}
updateNameInput.value = myName;
updateNameDialog.hidden = false;
// Focus the input field to trigger showing the keyboard
updateNameInput.focus();
var cursorPos = updateNameInput.value.length;
updateNameInput.setSelectionRange(0, cursorPos);
};
updateNameCancelButton.onclick = function updateNameCancelClicked(evt) {
if (evt)
evt.preventDefault();
updateNameDialog.hidden = true;
};
updateNameConfirmButton.onclick = function updateNameConfirmClicked(evt) {
if (evt)
evt.preventDefault();
var nameEntered = updateNameInput.value;
nameEntered = nameEntered.replace(/^\s+|\s+$/g, '');
if (nameEntered.length > MAX_DEVICE_NAME_LENGTH) {
var wantToRetry = window.confirm(_('bluetooth-name-maxlength-alert',
{ length: MAX_DEVICE_NAME_LENGTH }));
if (!wantToRetry) {
updateNameDialog.hidden = true;
}
return;
}
if (nameEntered === myName || !bluetooth.enabled || !defaultAdapter) {
updateNameDialog.hidden = true;
return;
}
if (nameEntered !== '') {
updateDeviceName(nameEntered);
}
else {
// Bug 847459: Default name of the bluetooth device is set by bluetoothd
// to the value of the Android ro.product.model property upon first
// start. In case the user gives an empty bluetooth device name, we want
// to revert to the original ro.product.model. Gecko exposes it under
// the deviceinfo.product_model setting.
var deviceInfo = settings.createLock().get('deviceinfo.product_model');
deviceInfo.onsuccess = function bt_getProductModel() {
var productModel = deviceInfo.result['deviceinfo.product_model'];
updateDeviceName(productModel);
};
}
updateNameDialog.hidden = true;
};
function updateDeviceName(nameEntered) {
var req = defaultAdapter.setName(nameEntered);
req.onsuccess = function bt_renameSuccess() {
myName = visibleName.textContent = defaultAdapter.name;
};
}
// immediatly UI update, DOM element manipulation.
function updateDeviceInfo(show) {
bluetoothRename.hidden = !show;
if (show) {
visibleItem.hidden = false;
// get last user setting for device visible
var req = settings.createLock().get('bluetooth.visible');
req.onsuccess = function bt_getVisibleSuccess() {
var visible = req.result['bluetooth.visible'];
if (typeof visible === 'undefined') {
visible = true;
}
setDiscoverable(visible);
};
req.onerror = function bt_getVisibleError() {
visibleCheckBox.checked = true;
};
} else {
visibleItem.hidden = true;
renameButton.disabled = true;
if (visibleTimeout) {
clearTimeout(visibleTimeout);
visibleTimeout = null;
}
}
}
// initial this device information and do default actions
// when DefaultAdapter is ready.
function initial() {
visibleCheckBox.checked = defaultAdapter.discoverable;
setDiscoverable(visibleCheckBox.checked);
// we can't get device name immediately, wait a while
setTimeout(function() {
myName = visibleName.textContent = defaultAdapter.name;
renameButton.disabled = false;
}, 1000);
}
function setDiscoverable(visible) {
if (!bluetooth.enabled || !defaultAdapter)
return;
settings.createLock().set({'bluetooth.visible': visible});
defaultAdapter.setDiscoverable(visible);
// Visibility will time out after 2 mins.
if (visible) {
if (!visibleTimeout) {
visibleTimeout = setTimeout(function() {
setDiscoverable(false);
}, visibleTimeoutTime);
}
} else {
if (visibleTimeout) {
clearTimeout(visibleTimeout);
visibleTimeout = null;
}
}
visibleCheckBox.checked = visible;
}
// API
return {
update: updateDeviceInfo,
initWithAdapter: initial
};
})();
// device list
var gDeviceList = (function deviceList() {
var bluetoothSearch = document.getElementById('bluetooth-search');
var searchAgainBtn = document.getElementById('search-device');
var searchingItem = document.getElementById('bluetooth-searching');
var enableMsg = document.getElementById('bluetooth-enable-msg');
var pairingAddress = null;
var connectingAddress = null;
var connectedAddress = null;
// stop discover other device after 60 seconds
var discoverTimeoutTime = 60000;
var discoverTimeout = null;
var pairList = {
title: document.getElementById('bluetooth-paired-title'),
list: document.getElementById('bluetooth-paired-devices'),
index: [],
clear: function emptyList() {
while (this.list.hasChildNodes()) {
this.list.removeChild(this.list.lastChild);
}
this.index = [];
},
show: function showArea(isShown) {
if (!isShown) {
this.clear();
}
this.title.hidden = !isShown;
this.list.hidden = !isShown;
}
};
var openList = {
title: document.getElementById('bluetooth-found-title'),
list: document.getElementById('bluetooth-devices'),
index: [],
clear: function emptyList() {
while (this.list.hasChildNodes()) {
this.list.removeChild(this.list.lastChild);
}
this.index = [];
},
show: function showArea(isShown) {
if (!isShown) {
this.clear();
}
this.title.hidden = !isShown;
this.list.hidden = !isShown;
}
};
var optionMenu = {
menu: document.getElementById('paired-device-option'),
connectOpt: document.getElementById('connect-option'),
disconnectOpt: document.getElementById('disconnect-option'),
unpairOpt: document.getElementById('unpair-option'),
confirmDlg: document.getElementById('unpair-device'),
unpairCancel: document.getElementById('unpair-device-cancel'),
confirmOpt: document.getElementById('confirm-option'),
showActions: function showActions() {
var self = this;
if (connectedAddress && this.device.address === connectedAddress) {
this.connectOpt.style.display = 'none';
this.disconnectOpt.style.display = 'block';
this.disconnectOpt.onclick = function() {
setDeviceDisconnect(self.device);
};
} else {
this.connectOpt.style.display = 'block';
this.disconnectOpt.style.display = 'none';
this.connectOpt.onclick = function() {
setDeviceConnect(self.device);
};
}
this.unpairOpt.onclick = function() {
setDeviceUnpair(self.device);
};
this.menu.addEventListener('gaiamenu-cancel', function closeMenu() {
return self.close();
});
this.menu.show();
},
showConfirm: function showConfirm() {
var self = this;
this.unpairCancel.onclick = function() {
return self.close();
};
this.confirmOpt.onclick = function() {
setDeviceUnpair(self.device);
return self.close();
};
this.confirmDlg.hidden = false;
},
show: function showMenu(device) {
this.device = device;
// we only support audio-card device to connect atm
this[this.device.icon === 'audio-card' ?
'showActions' : 'showConfirm']();
},
close: function closeMenu() {
this.menu.hide();
this.confirmDlg.hidden = true;
return false;
}
};
searchAgainBtn.onclick = function searchAgainClicked() {
updateDeviceList(true); // reset network list
openList.clear();
startDiscovery();
};
// private DOM helper: create a device list item
function newListItem(device, descL10nId) {
var deviceName = document.createElement('span');
if (device.name !== '') {
deviceName.textContent = device.name;
} else {
deviceName.setAttribute('data-l10n-id', 'unnamed-device');
}
var deviceDesc = document.createElement('small');
if (descL10nId) {
deviceDesc.setAttribute('data-l10n-id', descL10nId);
}
var a = document.createElement('a');
a.appendChild(deviceName);
a.appendChild(deviceDesc);
var li = document.createElement('li');
li.classList.add('bluetooth-device');
li.classList.add('bluetooth-type-' + device.icon);
li.appendChild(a);
return li;
}
// immediatly UI update, DOM element manipulation.
function updateDeviceList(show) {
bluetoothSearch.hidden = !show;
if (show) {
enableMsg.hidden = true;
openList.show(true);
searchingItem.hidden = false;
document.addEventListener('visibilitychange',
stopDiscoveryWhenLeaveApp);
} else {
openList.show(false);
pairList.show(false);
enableMsg.hidden = false;
searchingItem.hidden = true;
optionMenu.close();
pairingAddress = null;
connectingAddress = null;
connectedAddress = null;
// clear discoverTimeout
clearTimeout(discoverTimeout);
discoverTimeout = null;
document.removeEventListener('visibilitychange',
stopDiscoveryWhenLeaveApp);
}
}
// do default actions (start discover avaliable devices)
// when DefaultAdapter is ready.
function initial() {
defaultAdapter.onpairedstatuschanged = function bt_getPairedMessage(evt) {
dispatchEvent(new CustomEvent('bluetooth-pairedstatuschanged'));
showDevicePaired(evt.status, 'Authentication Failed');
};
defaultAdapter.ondiscoverystatechanged =
function bt_discoveryStateChanged(evt) {
if (!evt.discovering) {
searchAgainBtn.disabled = false;
searchingItem.hidden = true;
clearTimeout(discoverTimeout);
discoverTimeout = null;
} else {
searchAgainBtn.disabled = true;
searchingItem.hidden = false;
}
};
defaultAdapter.onhfpstatuschanged = function bt_hfpStatusChanged(evt) {
showDeviceConnected(evt.address, evt.status, Profiles.HFP);
};
defaultAdapter.ona2dpstatuschanged = function bt_a2dpStatusChanged(evt) {
showDeviceConnected(evt.address, evt.status, Profiles.A2DP);
};
// Get paired device
getPairedDevice(function() {
for (var address in pairList.index) {
var deviceItem = pairList.index[address];
if (Object.keys(deviceItem.connectedProfiles).length > 0)
return;
}
// If there is no current connection and we have one device connected
// before, restore it.
restoreConnection();
});
startDiscovery();
}
function restoreConnection() {
// Reconnect the one kept in settings key.
var connectedDevice =
settings.createLock().get(DEVICE_CONNECTED_KEY);
connectedDevice.onsuccess = function bt_getConnectedDevice() {
var address = connectedDevice.result[DEVICE_CONNECTED_KEY];
if (!address || !pairList.index[address])
return;
var device = pairList.index[address].device;
setDeviceConnect(device);
};
}
function getPairedDevice(callback) {
if (!bluetooth.enabled || !defaultAdapter)
return;
var req = defaultAdapter.getPairedDevices();
req.onsuccess = function bt_getPairedSuccess() {
// copy for sorting
var paired = req.result.slice();
var length = paired.length;
if (length == 0) {
pairList.show(false);
return;
}
pairList.clear();
paired.sort(function(a, b) {
return a.name > b.name;
});
for (var i = 0; i < length; i++) {
(function(device) {
var aItem = newListItem(device, '');
aItem.onclick = function() {
optionMenu.show(device);
};
pairList.list.appendChild(aItem);
pairList.index[device.address] = {
device: device,
item: aItem,
connectedProfiles: {}
};
// if the device has to be connected when it just paired
// wait for a while so they can have time to communicate
// their connection protocol
if (device.address === connectingAddress &&
device.icon === 'audio-card') {
var small = aItem.querySelector('small');
small.setAttribute('data-l10n-id', 'device-status-connecting');
setTimeout(function() {
setDeviceConnect(device);
}, 5000);
}
})(paired[i]);
}
// update the connection status
getConnectedDeviceItems(function(connectedDeviceItems) {
if (connectedDeviceItems.length > 0) {
connectedDeviceItems.forEach(function(item) {
var connectedDevice = item.device;
var connectedProfiles = item.connectedProfiles;
for (var profile in connectedProfiles) {
showDeviceConnected(connectedDevice.address, true, profile);
}
});
}
pairList.show(true);
// the callback function now is for restoring the connected device
// when the bluetooth is turned on.
if (callback)
callback();
});
};
}
// In the callback we returns an array of connected device items.
// Each device item contains "device" and "connectedProfiles".
function getConnectedDeviceItems(callback) {
if (!callback)
return;
if (!defaultAdapter) {
callback([]);
return;
}
var getConnectedDevicesByProfile = function(profileID, gcdCallback) {
if (!gcdCallback)
return;
var req = defaultAdapter.getConnectedDevices(profileID);
req.onsuccess = function() {
gcdCallback(req.result || []);
};
req.onerror = function() {
gcdCallback(null);
};
};
var connectedDeviceItemsMap = {}; // hash by device address
var updateDeviceItemsMap = function(profileID, connectedDevices) {
if (!connectedDevices)
return;
connectedDevices.forEach(function(connectedDevice) {
var info = connectedDeviceItemsMap[connectedDevice.address];
if (info) {
info.connectedProfiles[profileID] = true;
} else {
info = {
'device': connectedDevice,
'connectedProfiles': {}
};
info.connectedProfiles[profileID] = true;
}
connectedDeviceItemsMap[connectedDevice.address] = info;
});
};
// XXX: we should have better ways of doing this.
getConnectedDevicesByProfile(Profiles.HFP, function(hfpResult) {
updateDeviceItemsMap(Profiles.HFP, hfpResult);
getConnectedDevicesByProfile(Profiles.A2DP, function(a2dpResult) {
updateDeviceItemsMap(Profiles.A2DP, a2dpResult);
var connectedDeviceItems = [];
for (var i in connectedDeviceItemsMap) {
var item = connectedDeviceItemsMap[i];
connectedDeviceItems.push(item);
}
callback(connectedDeviceItems);
});
});
}
// callback function when an avaliable device found
function onDeviceFound(evt) {
var device = evt.device;
// Ignore duplicate and paired device. Update the name if needed.
var existingDevice = openList.index[device.address] ||
pairList.index[device.address];
if (existingDevice) {
var existingItem = existingDevice.item;
if (device.name && existingItem) {
var deviceName = existingItem.querySelector('a > span');
if (deviceName) {
deviceName.removeAttribute('data-l10n-id');
deviceName.textContent = device.name;
}
}
return;
}
var aItem = newListItem(device, 'device-status-tap-connect');
// bind paired callback
aItem.onclick = function() {
// block the pairing request if there is already one.
if (pairingAddress)
return;
var small = aItem.querySelector('small');
small.setAttribute('data-l10n-id', 'device-status-pairing');
this.setAttribute('aria-disabled', true);
stopDiscovery();
// pairing dialog is handled and showing via Bluetooth app
var req = defaultAdapter.pair(device.address);
pairingAddress = device.address;
req.onerror = function bt_pairError(error) {
showDevicePaired(false, req.error.name);
};
};
openList.list.appendChild(aItem);
openList.index[device.address] = {
device: device,
item: aItem,
connectedProfiles: {}
};
}
function showDevicePaired(paired, errorMessage) {
// If we don't know the pairing device address,
// it means the pair request is handled by interface level.
// So we just need to update paired list.
if (!pairingAddress) {
getPairedDevice();
return;
}
// clear pairingAddress first to prevent execute
// the same status update twice.
var workingAddress = pairingAddress;
pairingAddress = null;
if (paired) {
// if the device is on the list, remove it.
// it will show on paired list later.
if (openList.index[workingAddress]) {
var device = openList.index[workingAddress].device;
var item = openList.index[workingAddress].item;
openList.list.removeChild(item);
delete openList.index[workingAddress];
connectingAddress = workingAddress;
}
} else {
// show pair process fail.
var msg = _('error-pair-title');
if (errorMessage === 'Repeated Attempts') {
msg = msg + '\n' + _('error-pair-toofast');
} else if (errorMessage === 'Authentication Failed') {
msg = msg + '\n' + _('error-pair-pincode');
}
window.alert(msg);
// rollback device status
if (openList.index[workingAddress]) {
var item = openList.index[workingAddress].item;
var small = item.querySelector('small');
item.removeAttribute('aria-disabled');
small.setAttribute('data-l10n-id', 'device-status-tap-connect');
}
}
// acquire a new paired list no matter paired or unpaired
getPairedDevice();
}
function setDeviceUnpair(device) {
if (device.address === connectedAddress) {
var msg = _('unpair-title') + '\n' + _('unpair-msg');
if (!window.confirm(msg))
return;
connectedAddress = null;
}
// backend takes responsibility to disconnect first.
var req = defaultAdapter.unpair(device.address);
req.onerror = function bt_pairError() {
showDevicePaired(true, null);
};
}
function setDeviceDisconnect(device, callback) {
if (!bluetooth.enabled || !defaultAdapter ||
device.address !== connectedAddress) {
if (callback)
callback();
return;
}
var req = defaultAdapter.disconnect(device);
req.onsuccess = req.onerror = function() {
if (callback)
callback();
};
}
function setDeviceConnect(device) {
// we only support audio-card device to connect now
if (!bluetooth.enabled || !defaultAdapter ||
device.icon !== 'audio-card' ||
device.address === connectedAddress) {
connectingAddress = null;
return;
}
var doConnect = function() {
var connectSuccess = function bt_connectSuccess() {
if (connectingAddress) {
connectingAddress = null;
}
};
var connectError = function bt_connectError() {
// Connection state might be changed before DOM request response.
if (connectingAddress) {
// Clear the text of connecting status.
var small =
pairList.index[connectingAddress].item.querySelector('small');
small.removeAttribute('data-l10n-id');
small.textContent = '';
connectingAddress = null;
window.alert(_('error-connect-msg'));
}
};
stopDiscovery();
var req = defaultAdapter.connect(device);
req.onsuccess = connectSuccess; // At least one profile is connected.
req.onerror = connectError; // No available profiles are connected.
connectingAddress = device.address;
if (!pairList.index[connectingAddress]) {
return;
}
var small =
pairList.index[connectingAddress].item.querySelector('small');
small.setAttribute('data-l10n-id', 'device-status-connecting');
};
// disconnect current connected device first
if (connectedAddress && pairList.index[connectedAddress]) {
setDeviceDisconnect(pairList.index[connectedAddress].device, doConnect);
} else {
doConnect();
}
}
function showDeviceConnected(deviceAddress, connected, profile) {
var deviceItem = pairList.index[deviceAddress];
if (!deviceItem)
return;
deviceItem.connectedProfiles[profile] = connected;
var existConnectedProfile = false;
if (connected) {
existConnectedProfile = true;
} else {
// Check if there are other connected profiles
for (var profile in deviceItem.connectedProfiles) {
existConnectedProfile = existConnectedProfile ||
deviceItem.connectedProfiles[profile];
}
}
if (existConnectedProfile) {
connectedAddress = deviceAddress;
// record connected device so if Bluetooth is turned off and then on
// we can restore the connection
settings.createLock().set({
'bluetooth.device.connected': connectedAddress
});
} else {
if (connectedAddress === deviceAddress) {
connectedAddress = null;
settings.createLock().set({'bluetooth.device.connected': null});
}
}
var l10nId = '';
var hfpConnected = deviceItem.connectedProfiles[Profiles.HFP];
var a2dpConnected = deviceItem.connectedProfiles[Profiles.A2DP];
if (hfpConnected && a2dpConnected) {
l10nId = 'device-status-connected-phone-media';
} else if (hfpConnected) {
l10nId = 'device-status-connected-phone';
} else if (a2dpConnected) {
l10nId = 'device-status-connected-media';
} else {
l10nId = null;
}
var small = pairList.index[deviceAddress].item.querySelector('small');
if (l10nId) {
small.setAttribute('data-l10n-id', l10nId);
} else {
small.removeAttribute('data-l10n-id');
small.textContent = '';
}
}
function startDiscovery() {
if (!bluetooth.enabled || !defaultAdapter ||
discoverTimeout || document.hidden) {
return;
}
var req = defaultAdapter.startDiscovery();
req.onsuccess = function bt_discoveryStart() {
if (!discoverTimeout)
discoverTimeout = setTimeout(stopDiscovery, discoverTimeoutTime);
};
req.onerror = function bt_discoveryFailed() {
console.error('Can not discover nearby device');
};
}
function stopDiscoveryWhenLeaveApp() {
//only stop discovery when Settings app is hidden
if (!document.hidden)
return;
stopDiscovery();
}
function stopDiscovery() {
if (!bluetooth.enabled || !defaultAdapter || !discoverTimeout)
return;
var req = defaultAdapter.stopDiscovery();
req.onerror = function bt_discoveryStopFailed() {
console.error('Can not stop discover nearby device');
};
clearTimeout(discoverTimeout);
discoverTimeout = null;
}
// API
return {
update: updateDeviceList,
initWithAdapter: initial,
startDiscovery: startDiscovery,
onDeviceFound: onDeviceFound
};
})();
var lastMozSettingValue = false;
// enable Bluetooth if the related settings says so
// register an observer to monitor bluetooth.enabled changes
settings.addObserver('bluetooth.enabled', function(event) {
var enabled = event.settingValue;
if (lastMozSettingValue == enabled)
return;
// lock UI toggle
gBluetoothCheckBox.setAttribute('disabled', true);
lastMozSettingValue = enabled;
updateBluetoothState(enabled);
gDeviceList.update(enabled);
gMyDeviceInfo.update(enabled);
// clear defaultAdapter, we have to acquire it again when enabled.
if (!enabled)
defaultAdapter = null;
});
// startup, update status
var req = settings.createLock().get('bluetooth.enabled');
req.onsuccess = function bt_getSettingsSuccess() {
lastMozSettingValue = req.result['bluetooth.enabled'];
// if bluetooth is on when booting, the adapter probably is ready.
if (lastMozSettingValue)
initialDefaultAdapter();
updateBluetoothState(lastMozSettingValue);
gDeviceList.update(lastMozSettingValue);
gMyDeviceInfo.update(lastMozSettingValue);
};
bluetooth.addEventListener('adapteradded', function() {
// enable UI toggle
gBluetoothCheckBox.removeAttribute('disabled');
initialDefaultAdapter();
});
bluetooth.addEventListener('disabled', function() {
gBluetoothCheckBox.removeAttribute('disabled'); // enable UI toggle
defaultAdapter = null; // clear defaultAdapter
});
});

View file

@ -0,0 +1,16 @@
'use strict';
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 @@
'use strict';
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());
});
});

149
gaia/apps/bluetooth/js/utils.js Executable file
View file

@ -0,0 +1,149 @@
/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* exported getTruncated */
'use strict';
function getTruncated(oldName, options) {
// options
var maxLine = options.maxLine || 2;
var node = options.node;
var ellipsisIndex = options.ellipsisIndex || 3;
var ellipsisCharacter = options.ellipsisCharacter || '...';
if (node === null) {
return oldName;
}
// used variables and functions
function hitsNewline(oldHeight, newHeight) {
return oldHeight !== newHeight;
}
var newName = '';
var oldHeight;
var newHeight;
var baseHeight;
var currentLine;
var ellipsisAt;
var hasNewEllipsisPoint = true;
var nameBeforeEllipsis = [];
var nameBeforeEllipsisString;
var nameAfterEllipsis = oldName.slice(-ellipsisIndex);
var realVisibility = node.style.visibility;
var realWordBreak = node.style.wordBreak;
/*
* Hide UI, because we are manipulating DOM
*/
node.style.visibility = 'hidden';
/*
* Force breaking on boundaries
*/
node.style.wordBreak = 'break-all';
/*
* Get the base height to count the currentLine at first
*/
node.textContent = '.';
baseHeight = node.clientHeight;
node.textContent = '';
var needEllipsis = oldName.split('').some(function(character, index) {
nameBeforeEllipsis.push(character);
nameBeforeEllipsisString = nameBeforeEllipsis.join('');
oldHeight = node.clientHeight;
node.textContent = nameBeforeEllipsisString +
ellipsisCharacter + nameAfterEllipsis;
newHeight = node.clientHeight;
/*
* When index is 0, we have to update currentLine according to
* the first assignment (it is possible that at first the currentLine
* is not 0 if the width of node is too small)
*/
if (index === 0) {
currentLine = Math.floor(newHeight / baseHeight);
}
if (hitsNewline(oldHeight, newHeight) && index !== 0) {
/*
* The reason why we have to check twice is because there is a
* situation that truncated string is overflowed but there is
* still room for original string.
*
* In this way, we have to memorize the ellipsis index and
* slice `nameBeforeEllipsis` to the index in the end.
*/
var testHeight;
node.textContent = nameBeforeEllipsisString;
testHeight = node.clientHeight;
if (hitsNewline(oldHeight, testHeight)) {
/*
* We have to make it true again to keep the ellipsisAt
* up to date.
*/
hasNewEllipsisPoint = true;
currentLine += 1;
} else {
/*
* This is the situation that we still have room, so we have
* to keep the ellipsisAt value for later use.
*/
if (hasNewEllipsisPoint) {
ellipsisAt = index;
hasNewEllipsisPoint = false;
}
}
}
if (currentLine > maxLine) {
if (index === 0) {
/*
* It means that at first, the whole string is already in
* an overflowed situation, you have to avoid this situation.
* And we will bypass oldName back to you.
*
* There are some options for you :
*
* 1. Check options.ellipsisCharacter
* 2. Check options.maxLine
* 3. Check node's width (maybe too narrow)
*/
console.log(
'Your string is in a overflowed situation, ' +
'please check your options');
}
/*
* Remove the last character, because it causes the overflow
*/
nameBeforeEllipsis.pop();
node.textContent = '';
return true;
}
});
// restore UI
node.style.visibility = realVisibility;
node.style.wordBreak = realWordBreak;
if (!needEllipsis) {
newName = oldName;
} else {
newName += nameBeforeEllipsis.join('').slice(0, ellipsisAt);
newName += ellipsisCharacter;
newName += nameAfterEllipsis;
}
return newName;
}

1224
gaia/apps/bluetooth/js/vendor/alameda.js vendored Executable file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,159 @@
/**
* The template function for generating an UI element for an item of Bluetooth
* paired/remote device.
*
* @module bluetooth/bt_template_factory
*/
define(function() {
'use strict';
var _debug = false;
var debug = function() {};
if (_debug) {
debug = function bttf_debug(msg) {
console.log('--> [BluetoothTemplateFactory]: ' + msg);
};
}
function btTemplate(deviceType, onItemClick, observableItem) {
var device = observableItem;
var nameSpan = document.createElement('span');
_updateItemName(nameSpan, device.name);
var descSmall = document.createElement('small');
if (deviceType === 'remote') {
descSmall.setAttribute('data-l10n-id', 'device-status-tap-connect');
}
var li = document.createElement('li');
var anchor = document.createElement('a');
li.classList.add('bluetooth-device');
// According to Bluetooth class of device to give icon style.
debug('device.type = ' + device.type);
li.classList.add('bluetooth-type-' + device.type);
// According to 'descriptionText' property to give description.
_updateItemDescriptionText(li, descSmall, device.descriptionText,
deviceType);
anchor.appendChild(nameSpan);
anchor.appendChild(descSmall); // should append this first
li.appendChild(anchor);
// Add pairing progress in remote device.
if (deviceType === 'remote') {
var pairingProgress = document.createElement('progress');
pairingProgress.classList.add('overlapping-icon');
pairingProgress.classList.add('hidden');
li.appendChild(pairingProgress);
}
// Register the handler for the click event.
if (typeof onItemClick === 'function') {
li.onclick = function() {
onItemClick(observableItem);
};
}
// Observe name property for update device name
// while device 'onattributechanged' event is coming.
device.observe('name', function(newName) {
_updateItemName(nameSpan, newName);
});
// Observe descriptionText property for update device description
// while the connection status changed.
device.observe('descriptionText', function(descriptionText) {
_updateItemDescriptionText(li, descSmall, descriptionText, deviceType);
});
return li;
}
function _updateItemName(element, name) {
if (name !== '') {
element.removeAttribute('data-l10n-id');
element.textContent = name;
} else {
element.setAttribute('data-l10n-id', 'unnamed-device');
}
}
function _updateItemDescriptionText(li, element, descriptionText,
deviceType) {
debug('_updateItemDescriptionText(): descriptionText = ' + descriptionText);
switch (descriptionText) {
case 'tapToConnect':
li.removeAttribute('aria-disabled');
element.setAttribute('data-l10n-id', 'device-status-tap-connect');
// Rollback icon layout for remote device.
if (deviceType === 'remote') {
// hide the pairing progress
li.querySelector('progress').classList.add('hidden');
// show the device icon
li.classList.remove('icon-hidden');
}
break;
case 'pairing':
li.setAttribute('aria-disabled', true);
element.setAttribute('data-l10n-id', 'device-status-pairing');
// The remote device has different icon layout
// while it's in inline pairing progress.
if (deviceType === 'remote') {
// hide the device icon
li.classList.add('icon-hidden');
// show the pairing progress
li.querySelector('progress').classList.remove('hidden');
}
break;
case 'paired':
li.removeAttribute('aria-disabled');
element.setAttribute('data-l10n-id', 'device-status-paired');
// Rollback icon layout for remote device.
if (deviceType === 'remote') {
// hide the pairing progress
li.querySelector('progress').classList.add('hidden');
// show the device icon
li.classList.remove('icon-hidden');
}
break;
case 'connecting':
li.setAttribute('aria-disabled', true);
element.setAttribute('data-l10n-id', 'device-status-connecting');
break;
case 'connectedWithDeviceMedia':
li.removeAttribute('aria-disabled');
element.setAttribute('data-l10n-id',
'device-status-connected-device-media');
break;
case 'connectedWithDevice':
li.removeAttribute('aria-disabled');
element.setAttribute('data-l10n-id',
'device-status-connected-device');
break;
case 'connectedWithMedia':
li.removeAttribute('aria-disabled');
element.setAttribute('data-l10n-id',
'device-status-connected-media');
break;
case 'connectedWithNoProfileInfo':
li.removeAttribute('aria-disabled');
element.setAttribute('data-l10n-id',
'device-status-connected');
break;
case 'disconnected':
li.removeAttribute('aria-disabled');
element.removeAttribute('data-l10n-id');
element.textContent = '';
break;
default:
break;
}
}
return function ctor_btTemplate(deviceType, onItemClick) {
return btTemplate.bind(null, deviceType, onItemClick);
};
});

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(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,39 @@
/*
* PairExpiredDialog is responsible for:
* Notify a user that the earlier pairing request which fired in
* notification center is expired. There is a prompt handling via here.
*/
define(function(require) {
'use strict';
var PairExpiredDialog = {
init: function() {
this.dialog = document.getElementById('pairing-request-timeout');
this.confirmBtn =
document.getElementById('incoming-pairing-timeout-confirm');
},
showConfirm: function(callback) {
var self = this;
this.confirmBtn.onclick = function() {
self.close();
if (callback) {
callback();
}
};
this.dialog.hidden = false;
},
get isVisible() {
return (!this.dialog.hidden);
},
close: function() {
this.dialog.hidden = true;
}
};
PairExpiredDialog.init();
return PairExpiredDialog;
});

View file

@ -0,0 +1,219 @@
/* -*- Mode: js; js-indent-level: 2; indent-tabs-mode: nil -*- */
/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
/* global getTruncated */
/* exported Pairview */
'use strict';
var Pairview = {
/**
* flag for debugging.
*/
_debug: false,
/**
* remote device name to pair with.
*/
_remoteDeviceName: null,
/**
* device authentication method
*/
_pairMethod: null,
/**
* event handler of pairing request
*/
_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;
// Since pairing process is migrated from Settings app to Bluetooth app,
// there is no way to identify the pairing request in active/passive mode.
// In order to let the pairing messsage consistency,
// given the pairing mode to be passive.
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;
}
},
/**
* It is used to init pairing information in this pair view.
*
* @memberOf Pairview
* @access public
* @param {String} method - method of this pairing request
* @param {Object} options
* @param {String} options.deviceName - name of the remote bluetooth device
* @param {BluetoothPairingHandle} options.handle - property handle that
carries specific method
to reply by user.
*/
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;
}
// show() only until the page is localized.
navigator.mozL10n.once(Pairview.show.bind(Pairview));
},
close: function pv_close() {
// Since user clicked close button, we reject pairing request.
// Because the reject() interface of pairing requests are same, we do same
// operation here.
this._options.handle.reject().then(() => {
this.debug('Resolved in reject ' + this._pairMethod + ' request.');
// Close window by self.
window.close();
}, (aReason) => {
this.debug('Rejected in reject ' + this._pairMethod +
' request with reason: ' + aReason);
// Close window by self.
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':
// Do nothing here since the pairing method display passkey only
// Close window by self.
window.close();
break;
case 'enterpincode':
var pinCode = this.pinInput.value;
this._options.handle.setPinCode(pinCode).then(() => {
this.debug('Resolved setPinCode operation');
// Close window by self.
window.close();
}, (aReason) => {
this.debug('Rejected setPinCode with reason: ' + aReason);
// Close window by self.
window.close();
});
break;
case 'confirmation':
case 'consent':
this._options.handle.accept().then(() => {
this.debug('Resolved in ' + this._pairMethod + ' request');
// Close window by self.
window.close();
}, (aReason) => {
this.debug('Rejected in ' + this._pairMethod + ' request ' +
' with reason: ' + aReason);
// Close window by self.
window.close();
});
break;
}
break;
case 'button-close':
this.close();
break;
}
break;
case 'resize':
// XXX: this is hack that we have to close the attention in this case,
// while in most other cases, we would just change it into an active
// status bar
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;
});

18
gaia/apps/bluetooth/jsdoc.json Executable file
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"
}
}

View file

@ -0,0 +1,81 @@
# Bluetooth :: Settings
# Bluetooth :: Devices Panel (reuse the string is partially)
# LOCALIZATION NOTE (bluetooth-header): if you leave the original
# en-US value "{{bluetooth}}" in the following string, the localization
# will be automatically picked from the existing string with ID "bluetooth".
# If you need to adapt your localization to the reduced space available
# in headers, you can use a shorter string here.
bluetooth-header = {{bluetooth}}
bluetooth-enable-msg = Turn Bluetooth on to view devices in the area.
bluetooth-paired-devices = Paired devices
bluetooth-devices-in-area = Devices in the area
bluetooth-visible-to-all = Visible to all
bluetooth-name-maxlength-alert = Your device name exceeds {{length}} characters. Please retry again.
bluetooth-address = Bluetooth Address
bluetooth-address-unavailable = Not available
bt-status-nopaired = No devices paired
bt-status-turnoff = Turned off
bt-status-paired = {[ plural(n) ]}
bt-status-paired[zero] = {{name}}
bt-status-paired[one] = {{name}}, +{{n}} more
bt-status-paired[two] = {{name}}, +{{n}} more
bt-status-paired[few] = {{name}}, +{{n}} more
bt-status-paired[many] = {{name}}, +{{n}} more
bt-status-paired[other] = {{name}}, +{{n}} more
device-status-tap-connect = Tap to connect
device-status-pairing = Pairing with device…
device-status-waiting = Waiting for other device…
device-status-paired = Paired
device-status-connecting = Connecting…
device-status-connected = Connected
device-status-connected-phone = Connected to phone audio
device-status-connected-media = Connected to media audio
device-status-connected-phone-media = Connected to phone/media audio
device-option-unpair = Unpair
device-option-confirm = Confirm
device-option-unpair-confirmation = Confirmation
device-option-unpair-device = Unpair device?
device-option-connect = Connect
device-option-disconnect = Disconnect
search-for-device = Searching for devices…
search-device = Search for devices
rename-device = Rename my device
unnamed-device = Unknown device
change-btdevice-name = Change device name
change-btdevice-name-desc = This name will appear on your other Bluetooth devices
bluetooth-new-name = New name
error-pair-title = Unable to pair devices
error-pair-pincode = Unable to pair with the device. Check that the PIN is correct.
error-pair-toofast = Unable to pair with the device. Too many requests.
error-connect-title = Unable to connect devices
error-connect-msg = Check that the device you’re trying to connect with is still in range and has Bluetooth turned on.
unpair-title = Unpair a connected device?
unpair-msg = You are currently connected to this device. Unpairing will also disconnect from it.
# Bluetooth :: Confirmation Dialog
bluetooth = Bluetooth
confirmation = Confirmation
bluetooth-status-description = Bluetooth is disabled
turn-bluetooth-on = Do you want to turn Bluetooth on?
cancel = Cancel
turn-on = Turn On
error-transfer-title = Bluetooth file transfer failed
error-transfer-settings = Bluetooth file transfer failed. Check that the Bluetooth settings are correct.
ok = OK
# Bluetooth :: Pair View
pair=Pair
active-pair = Confirm devices to pair
active-pair-confirmation = To pair with {{device}}, make sure that both devices currently have the following PIN displayed:
active-pair-pincode = To pair with {{device}}, enter the PIN on the device.
active-pair-passkey = To pair with {{device}}, enter the passkey on the device.
passive-pair = Bluetooth pairing request
passive-pair-confirmation = {{device}} would like to pair with this phone. To accept, make sure that both devices currently have the following PIN displayed:
passive-pair-pincode = {{device}} would like to pair with this phone. To accept, enter the PIN on the device.
passive-pair-enterpincode = {{device}} would like to pair with this phone. To accept, enter the PIN on the device.
passive-pair-passkey = {{device}} would like to pair with this phone. To accept, enter the passkey on the device.
passive-pair-displaypasskey = {{device}} would like to pair with this phone. To accept, input the passkey and then press Return or Enter on the device.
passive-pair-consent = {{device}} would like to pair with this phone.
pair-view-title = Bluetooth Request From
# Bluetooth :: Pair Manager
bluetooth-pairing-request-now-title = Bluetooth pairing request
pairing-request-timeout-title = Bluetooth Pairing Request
pairing-request-timeout-description = Bluetooth pairing request expired. Try to pair again.

View file

@ -0,0 +1,81 @@
{
"name": "Bluetooth Manager",
"type": "certified",
"role": "system",
"launch_path": "/settings.html",
"description": "Gaia Bluetooth Manager",
"developer": {
"name": "The Gaia Team",
"url": "https://github.com/mozilla-b2g/gaia"
},
"locales": {
"ar": {
"name": "Bluetooth",
"description": "Gaia Bluetooth"
},
"en-US": {
"name": "Bluetooth",
"description": "Gaia Bluetooth"
},
"fr": {
"name": "Bluetooth",
"description": "Bluetooth de Gaia"
},
"zh-TW": {
"name": "Bluetooth",
"description": "Gaia Bluetooth"
}
},
"default_locale": "en-US",
"icons": {
"60": "/style/icons/Bluetooth_60.png",
"90": "/style/icons/Bluetooth_90.png",
"120": "/style/icons/Bluetooth_120.png",
"135": "/style/icons/Bluetooth_135.png"
},
"orientation": "default",
"activities": {
"share": {
"filters": {
"blobs": {
"required": true
}
},
"disposition": "inline",
"returnValue": true,
"href": "/transfer.html"
},
"share-via-bluetooth-only": {
"filters": {
"type": "text/vcard",
"blobs": {
"required": true
}
},
"disposition": "inline",
"returnValue": true,
"href": "/transfer.html"
}
},
"messages": [
{
"bluetooth-pairing-request": "/message.html"
},
{
"bluetooth-cancel": "/message.html"
}
],
"permissions": {
"themeable": {},
"attention": {},
"bluetooth": {},
"device-storage:sdcard": {
"access": "readonly"
},
"settings": {
"access": "readwrite"
},
"desktop-notification": {},
"moz-extremely-unstable-and-will-change-webcomponents": {}
}
}

View file

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<meta charset="utf-8">
<title>Bluetooth Pair Message</title>
<meta name="description" content="Manage Bluetooth Pair Message">
<!-- Specific style -->
<link rel="stylesheet" type="text/css" href="shared/style/confirm.css"/>
<link rel="stylesheet" type="text/css" href="style/message.css">
<!-- Localization -->
<meta name="defaultLanguage" content="en-US">
<meta name="availableLanguages" content="en-US">
<link rel="localization" href="locales/bluetooth.{locale}.properties">
<script type="application/javascript" src="shared/js/l10n.js"></script>
<script type="application/javascript" src="shared/js/notification_helper.js"></script>
<!-- Specific code -->
<!-- <script type="application/javascript" defer src="shared/js/async_storage.js"></script> -->
<!-- <script type="application/javascript" defer src="shared/js/bluetooth_helper.js"></script> -->
</head>
<body role="application">
<form role="dialog" data-type="confirm" id="pairing-request-timeout" hidden>
<section>
<h1 data-l10n-id="pairing-request-timeout-title"></h1>
<p data-l10n-id="pairing-request-timeout-description"></p>
</section>
<menu>
<button data-l10n-id="ok" class="full recommend" id="incoming-pairing-timeout-confirm"></button>
</menu>
</form>
<script data-main="js/startup_pair.js" src="js/vendor/alameda.js"></script>
</body>
</html>

50
gaia/apps/bluetooth/onpair.html Executable file
View file

@ -0,0 +1,50 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<meta charset="utf-8">
<title>Bluetooth Pair Device</title>
<meta name="description" content="Prompts user to take a device pair request">
<!-- Common style -->
<link rel="stylesheet" href="shared/style/confirm.css">
<link rel="stylesheet" href="shared/style/input_areas.css">
<!-- Localization -->
<meta name="defaultLanguage" content="en-US">
<meta name="availableLanguages" content="en-US">
<link rel="localization" href="locales/bluetooth.{locale}.properties">
<script type="application/javascript" src="shared/js/l10n.js"></script>
<script type="application/javascript" src="shared/js/notification_helper.js"></script>
<!-- Specific style -->
<link rel="stylesheet" type="text/css" href="style/onpair.css"/>
<link rel="stylesheet" type="text/css" href="style/icons.css"/>
<!-- Specific code -->
<script type="application/javascript" defer src="js/views/pair_view.js"></script>
<script type="application/javascript" defer src="js/utils.js"></script>
</head>
<body role="application">
<form role="dialog" data-type="confirm" id="pair-view">
<section>
<h2 data-l10n-id="pair-view-title"></h2>
<h1 id="label-name"></h1>
<p id="pair-description"></p>
<span id="confirmation-method" >
<span id="passkey"></span>
</span>
<span id="pin-input-method" hidden>
<input type="text" id="pin-input" size="16" maxlength="16" dir="auto" />
</span>
</section>
<menu>
<button type="reset" id="button-close" data-l10n-id="cancel"></button>
<button type="submit" id="button-pair" class="recommend" data-l10n-id="pair"></button>
</menu>
</form>
</body>
</html>

View file

@ -0,0 +1,10 @@
{
"webide": {
"prepackage": {
"command": "make",
"env": ["APP=bluetooth"],
"cwd": "../.."
},
"packageDir": "../../build_stage/bluetooth/"
}
}

119
gaia/apps/bluetooth/settings.html Executable file
View file

@ -0,0 +1,119 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<meta charset="utf-8" />
<title>Bluetooth</title>
<!-- Localization -->
<meta name="defaultLanguage" content="en-US">
<meta name="availableLanguages" content="en-US">
<link rel="localization" href="locales/bluetooth.{locale}.properties">
<link rel="localization" href="/shared/elements/gaia_menu/locales/gaia_menu.{locale}.properties">
<script type="application/javascript" src="shared/js/l10n.js"></script>
<!-- Common style -->
<link rel="stylesheet" type="text/css" href="shared/style/headers.css"/>
<link rel="stylesheet" type="text/css" href="shared/style/confirm.css"/>
<link rel="stylesheet" type="text/css" href="shared/style/buttons.css"/>
<link rel="stylesheet" type="text/css" href="shared/style/progress_activity.css"/>
<link rel="stylesheet" type="text/css" href="shared/style/input_areas.css"/>
<link rel="stylesheet" type="text/css" href="/shared/elements/gaia_menu/style.css">
<link rel="stylesheet" type="text/css" href="/shared/elements/gaia-icons/gaia-icons.css">
<link rel="stylesheet" type="text/css" href="app://theme.gaiamobile.org/shared/elements/gaia-theme/gaia-theme.css">
<!-- Shared code -->
<script type="text/javascript" src="shared/js/l10n.js"></script>
<script defer src="/shared/elements/gaia-header/dist/gaia-header.js"></script>
<script defer src="/shared/js/component_utils.js"></script>
<script defer src="/shared/elements/gaia_switch/script.js"></script>
<script defer src="/shared/elements/gaia_menu/script.js"></script>
<!-- Specific style -->
<link rel="stylesheet" type="text/css" href="style/lists.css">
<link rel="stylesheet" type="text/css" href="style/icons.css">
<link rel="stylesheet" type="text/css" href="style/settings.css">
<!-- Specific code -->
<script type="text/javascript" defer src="js/settings.js"></script>
</head>
<body role="application" class="skin-organic uninit theme-settings">
<section role="region">
<gaia-header action="back">
<h1 data-l10n-id="bluetooth-header"></h1>
</gaia-header>
<div>
<ul>
<li id="bluetooth-status" class="hint">
<gaia-switch data-ignore>
<label data-l10n-id="bluetooth"></label>
</gaia-switch>
<p data-l10n-id="bluetooth-enable-msg" id="bluetooth-enable-msg" class="explanation"></p>
</li>
<li hidden class="device-visible hint">
<gaia-switch data-ignore>
<label data-l10n-id="bluetooth-visible-to-all"></label>
</gaia-switch>
<p class="explanation bluetooth-device-name"></p>
</li>
<li hidden id="bluetooth-rename">
<button id="rename-device" data-l10n-id="rename-device" disabled></button>
</li>
</ul>
<!-- Devices List View :: Paired Devices List -->
<header id="bluetooth-paired-title">
<h2 data-l10n-id="bluetooth-paired-devices"></h2>
</header>
<ul id="bluetooth-paired-devices" class="devices">
</ul>
<!-- Devices List View :: Found Devices List -->
<header id="bluetooth-found-title">
<h2 data-l10n-id="bluetooth-devices-in-area"></h2>
</header>
<ul id="bluetooth-devices" class="devices">
</ul>
<!-- Devices List View :: Search Button -->
<div id="bluetooth-searching" data-l10n-id="search-for-device" class="explanation"></div>
<ul id="bluetooth-search">
<li>
<button id="search-device" data-l10n-id="search-device" disabled></button>
</li>
</ul>
</div>
</section>
<form role="dialog" data-type="confirm" id="update-device-name" hidden>
<section>
<h1 data-l10n-id="change-btdevice-name"></h1>
<p>
<input type="text" id="update-device-name-input" value=""/>
<span data-l10n-id="change-btdevice-name-desc"></span>
</p>
</section>
<menu>
<button id="update-device-name-cancel" data-l10n-id="cancel"></button>
<button data-l10n-id="ok" id="update-device-name-confirm" class="recommend"></button>
</menu>
</form>
<gaia-menu paired-device-option hidden>
<button data-l10n-id="device-option-connect" id="connect-option"></button>
<button data-l10n-id="device-option-disconnect" id="disconnect-option"></button>
<button data-l10n-id="device-option-unpair" id="unpair-option"></button>
</form>
<form role="dialog" data-type="confirm" id="unpair-device" hidden>
<section>
<h1 data-l10n-id="device-option-unpair-confirmation"></h1>
<p data-l10n-id="device-option-unpair-device"></p>
</section>
<menu>
<button data-l10n-id="cancel" id="unpair-device-cancel"></button>
<button data-l10n-id="device-option-confirm" id="confirm-option" class="danger"></button>
</menu>
</form>
</body>
</html>

View file

@ -0,0 +1,134 @@
/**
* Reference these style sheet from Settings app
*/
/******************************************************************************
* Bluetooth devices
*/
.bluetooth-device {
background-repeat: no-repeat;
background-position: right 3rem top 50%; /* same position as for Wi-Fi signal levels */
-moz-padding-end: 2rem;
}
.bluetooth-device:before {
content: "";
width: 3rem;
height: 3rem;
background: url(images/bluetooth_sprite.png) no-repeat 3rem 3rem / 39rem;
position: absolute;
offset-inline-end: 0;
top: 50%;
margin-top: -1.5rem;
}
.bluetooth-device.icon-hidden:before {
display: none;
}
/* device icons */
.bluetooth-type-audio-card:before {
background-position: 0 0;
}
.bluetooth-type-audio-input-microphone:before {
background-position: -3rem 0;
}
.bluetooth-type-battery:before {
background-position: -6rem 0;
}
.bluetooth-type-camera-photo:before {
background-position: -9rem 0;
}
.bluetooth-type-camera-video:before {
background-position: -12rem 0;
}
.bluetooth-type-camera-web:before {
background-position: -15rem 0;
}
.bluetooth-type-computer:before {
background-position: -18rem 0;
}
.bluetooth-type-drive-harddisk:before {
background-position: -21rem 0;
}
.bluetooth-type-drive-optical:before {
background-position: -24rem 0;
}
.bluetooth-type-drive-removable-media:before {
background-position: -27rem 0;
}
.bluetooth-type-input-gaming:before {
background-position: -30rem 0;
}
.bluetooth-type-input-keyboard:before {
background-position: -33rem 0;
}
.bluetooth-type-input-mouse:before {
background-position: -36rem 0;
}
.bluetooth-type-input-tablet:before {
background-position: 0 -3rem;
}
.bluetooth-type-media-flash:before {
background-position: -3rem -3rem;
}
.bluetooth-type-media-floppy:before {
background-position: -6rem -3rem;
}
.bluetooth-type-media-optical:before {
background-position: -9rem -3rem;
}
.bluetooth-type-media-tape:before {
background-position: -12rem -3rem;
}
.bluetooth-type-modem:before {
background-position: -15rem -3rem;
}
.bluetooth-type-multimedia-player:before {
background-position: -18rem -3rem;
}
.bluetooth-type-network-wired:before {
background-position: -21rem -3rem;
}
.bluetooth-type-network-wireless:before {
background-position: -24rem -3rem;
}
.bluetooth-type-pda:before,
.bluetooth-type-phone:before {
background-position: -27rem -3rem;
}
.bluetooth-type-printer:before {
background-position: -30rem -3rem;
}
.bluetooth-type-scanner:before {
background-position: -33rem -3rem;
}
.bluetooth-type-video-display:before {
background-position: -36rem -3rem;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1,018 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 898 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="-223 225 64 64" enable-background="new -223 225 64 64" xml:space="preserve">
<g>
<g>
<path fill="#02C5ED" d="M-188,256.9l6.4-6.4c1.4-1.4,1.4-3.7,0-5.1L-193,234l-0.2,17.8l-7.6-7.5c-0.1-0.1-0.1-0.1-0.2-0.2h-0.4
c-0.4,1.2-0.8,2.4-1.1,3.7l9.2,9.2l-9.2,9.1c0.3,1.3,0.7,2.5,1.1,3.7h0.4c0.1-0.1,0.1-0.1,0.2-0.2l7.6-7.5V280l11.6-11.5
c1.4-1.4,1.4-3.7,0-5.1L-188,256.9z M-189.4,242.9l4.2,4.2l0,0c0.5,0.5,0.5,1.2,0,1.7l0,0l-4.3,4.3v-10.2H-189.4z M-185,266.7
l-4.3,4.3v-10.2l4.2,4.2l0,0C-184.6,265.5-184.6,266.2-185,266.7L-185,266.7z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 945 B

View file

@ -0,0 +1,222 @@
/**
* Reference these style sheet from Settings app
*/
/**
* Table View
* line height = 6rem, border included
*/
ul {
margin: 0;
padding: 0;
overflow: hidden;
list-style: none;
}
ul li {
box-sizing: border-box;
position: relative;
margin: 0 1.5rem;
border-top: 0.1rem solid #e6e6e3;
color: #000;
}
ul li:first-child {
border-top: none;
}
ul+ul li:first-child {
border-top: 0.1rem solid #e6e6e3;
}
ul li.sound-setting {
min-height: 7.9rem;
}
ul li label:not([for]) {
background: none;
}
ul li > a,
ul li > span,
ul li > small,
ul li > label > span,
ul li > label > small {
text-decoration: none;
outline: 0;
color: #000;
box-sizing: border-box;
font-size: 1.9rem;
padding: 0 3rem;
margin: 0 -1.5rem;
}
ul li,
ul li > a,
ul li > label {
min-height: 6rem;
display: flex;
flex-direction: column;
justify-content: center;
}
ul li > a,
ul li > label {
min-height: 6rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
ul li[hidden] {
display: none;
}
ul li > a > span {
pointer-events: none;
}
ul li.active a,
ul li.active span,
ul li:not([aria-disabled="true"]) > small + a:active,
ul li:not([aria-disabled="true"]) > a:active,
ul li:not([aria-disabled="true"]) > small + a:focus,
ul li:not([aria-disabled="true"]) > a:focus {
background-color: #b2f2ff;
color: #222;
}
ul li[aria-disabled="true"] > a {
color: #797e80;
opacity: 0.6;
pointer-events: none;
}
ul > li > progress {
display: inline-block;
margin: 0;
padding: 0;
}
ul > li > progress + span {
display: inline-block;
vertical-align: middle;
margin: 0;
padding: 0;
padding-inline-start: 1rem;
color: #505b5b;
font-size: 1.4rem;
line-height: 6rem;
}
/* Customization style for overlapping .bluetooth-device:before */
ul > li > progress.overlapping-icon {
position: absolute;
right: 3rem;
top: 50%;
margin-top: -1.5rem;
}
ul > li > progress.hidden {
display: none;
}
/* description + value on the same line */
ul li > a span:nth-of-type(2):not(.button) {
position: absolute;
offset-inline-end: 1.5rem;
color: #505859;
line-height: 6rem;
height: 6rem;
top: 0;
}
/* text ellipsis */
ul li > *,
ul li > label > *,
ul li > a > * {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* sublines */
ul li > small,
ul li > label > small,
ul li > a > small {
display: block;
font-size: 1.4rem;
color: #505859;
/* click/tap events should be caught by the <a> element below the <small> */
pointer-events: none;
}
small.multiline > span {
display: block;
}
ul li > label {
font-size: 1.9rem;
line-height: 1.9rem;
font-weight: 400;
margin: 0;
}
ul li > label ~ input {
margin-bottom: 1.5rem;
}
/******************************************************************************
* Boolean buttons
*/
ul li button,
ul li a.bb-button {
display: inline-block;
margin: 1rem 0;
white-space: normal;
}
/******************************************************************************
* Boolean inputs
*/
/* custom styles for boolean inputs (see /shared/style/switches.css) */
ul li label {
text-transform: none;
}
ul li > label:not([for]) {
margin-inline-start: -1.5rem;
margin-inline-end: -1.5rem;
padding-inline-start: 1.5rem;
padding-inline-end: 1.5rem;
width: 100%;
height: calc(100% - 1rem);
min-height: 5rem;
overflow: visible;
display: flex;
}
ul li > label:not([for]) > span {
line-height: normal;
height: auto;
}
/******************************************************************************
* Field inputs
*/
ul li p {
font-size: 1.9rem;
line-height: 1.9rem;
font-weight: 400;
margin: 0;
padding: 1.5rem 1.5rem 1rem;
}
ul li p + label:not([for]) {
top: 2.5rem;
height: calc(100% - 2.5rem);
}

View file

@ -0,0 +1,28 @@
html, body {
margin: 0;
padding: 0;
font-size: 10px;
background-color: #26272c;
}
body {
width: 100%;
height: 100%;
overflow: hidden;
font-family: sans-serif;
color: #fff;
}
/**
* Disable selection by default except for input, text area, and elements with
* "contenteditable".
*/
* {
-moz-user-select: none;
}
input[type="text"],
textarea,
[contenteditable="true"] {
-moz-user-select: text;
}

View file

@ -0,0 +1,78 @@
/**
* Settings > onpair style & layout
*/
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
font-size: 10px;
overflow: hidden;
}
/**
* Disable selection by default except for input, text area, and elements with
* "contenteditable" .
*/
* {
-moz-user-select: none;
}
input[type="text"],
textarea,
[contenteditable="true"] {
-moz-user-select: text;
}
#pair-view {
padding: 0;
}
#pair-view h2 {
font-size: 1.6rem;
font-weight: 400;
border: none;
margin: 2.2rem 0 1.4rem;
line-height: 1.4rem;
padding: 0 1.5rem;
}
#pair-view > section {
height: 100%;
padding: 0 1.5rem;
}
#label-name {
border-top: 0.1rem solid #696969;
font-size: 2.5rem;
line-height: 5rem;
padding: 0 1.5rem;
word-break: break-all;
}
#pair-description {
margin: 0;
padding: 1rem 1.5rem 0;
border-top-color: #696969;
font-size: 2rem;
line-height: 2.7rem;
font-weight: 300;
}
#passkey {
position: absolute;
bottom: 11rem;
offset-inline-start: 1.5rem;
width: calc(100% - 3rem);
font-size: 3rem;
font-weight: 400;
text-align: center;
}
/* Hide the description area when screen height is too small. */
@media screen and (max-height: 400px) {
#pair-description {
display: none;
}
}

View file

@ -0,0 +1,132 @@
/**
* Reference these style sheet from Settings app
*/
/**
* Settings style & layout
* note: `lists.css' must be loaded before this stylesheet
*/
html, body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
font-size: 10px;
overflow: hidden;
}
/******************************************************************************
* Setting panels: full-size <section role="region"> blocks
*/
/* Setting panels: full-size <section role="region"> blocks */
section[role="region"] {
position: absolute;
top: 0;
offset-inline-start: 0;
width: 100%;
height: 100%;
border: 0;
background-color: #f4f4f4;
overflow: hidden;
}
/**
* Headers should not scroll with the rest of the page, except for #root.
*/
section[role="region"] > header {
position: absolute;
}
section[role="region"] > div {
position: absolute;
top: 5rem;
right: 0;
bottom: 0;
left: 0;
width: 100%;
height: calc(100% - 5rem);
overflow-y: scroll;
will-change: scroll-position;
}
/******************************************************************************
* Explanation texts
*/
.explanation {
padding: 0.5rem 3rem;
font-style: italic;
font-size: 1.4rem;
color: #505859;
}
.description {
font-size: 1.7rem;
line-height: 2.3rem;
white-space: normal;
-moz-hyphens: auto;
}
.description p,
.description a,
.description label {
font-size: 1.7rem;
line-height: 2.3rem;
white-space: normal;
}
.description label {
padding: 0;
}
.description label span {
height: auto;
min-height: 6rem;
padding-top: 1.2rem;
padding-bottom: 1.2rem;
font-size: 1.7rem;
line-height: 2.3rem;
white-space: normal;
}
.description small {
font-size: 1.7rem;
color: #505859;
}
.hint label span {
top: 2.9rem !important;
}
.hint span {
line-height: 6rem;
}
.hint .explanation {
font-style: normal;
position: relative;
margin-top: -2rem;
padding: 0.5rem 4rem 1.3rem;
padding-inline-start: 1.5rem;
white-space: normal;
}
.hint > gaia-switch ~ .explanation,
.hint > label:not([for]) ~ .explanation {
padding-inline-end: 8rem;
}
.hint .explanation[hidden] {
display: none;
}
/******************************************************************************
* Update device name dialog
*/
#update-device-name > section input {
margin: 1rem 0;
}

View file

@ -0,0 +1,32 @@
html, body {
margin: 0;
padding: 0;
font-size: 10px;
background-color: #26272c;
}
body {
width: 100%;
height: 100%;
overflow: hidden;
font-family: sans-serif;
color: #fff;
}
/**
* Disable selection by default except for input, text area, and elements with
* "contenteditable".
*/
* {
-moz-user-select: none;
}
input[type="text"],
textarea,
[contenteditable="true"] {
-moz-user-select: text;
}
#icon {
width: 6.4rem;
}

View file

@ -0,0 +1,27 @@
'use strict';
/* jshint node: true, mocha: true */
/* global suiteSetup */
var helper = require('helper');
suite('Bluetooth tests', function() {
suiteSetup(helper.cleanupWorkspace);
teardown(helper.cleanupWorkspace);
test('APP=bluetooth make', function(done) {
helper.exec('APP=bluetooth make', function(error, stdout, stderr) {
helper.checkError(error, stdout, stderr);
var zipPath = process.cwd() +
'/profile/webapps/bluetooth.gaiamobile.org/application.zip';
var checkList = ['manifest.webapp'];
checkList.forEach(function(filePath) {
helper.checkFilePathInZip(zipPath, filePath);
});
done();
});
});
});

View file

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

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<body role="application">
<form role="dialog" data-type="confirm" id="pairing-request-timeout" hidden>
<section>
<h1 data-l10n-id="pairing-request-timeout-title">Bluetooth Pairing Request</h1>
<p data-l10n-id="pairing-request-timeout-description">Bluetooth pairing request expired. Try to pair again.</p>
</section>
<menu>
<button data-l10n-id="ok" class="full recommend" id="incoming-pairing-timeout-confirm">OK</button>
</menu>
</form>
</body>
</html>

View file

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<body role="application">
<form role="dialog" data-type="confirm" id="pair-view">
<section>
<h2 data-l10n-id="pair-view-title"></h2>
<h1 id="label-name"></h1>
<p id="pair-description"></p>
<span id="confirmation-method" >
<span id="passkey"></span>
</span>
<span id="pin-input-method" hidden>
<input type="text" id="pin-input" size="16" maxlength="16" />
</span>
<span id="passkey-input-method" hidden>
<input type="number" id="passkey-input" x-inputmode="digits" size="6" maxlength="6" />
</span>
</section>
<menu>
<button id="button-close" data-l10n-id="cancel">Cancel</button>
<button id="button-pair" class="recommend" data-l10n-id="pair">Pair</button>
</menu>
</form>
</body>
</html>

View file

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html>
<body role="application" class="skin-organic theme-settings uninit">
<!-- Enable Bluetooth View -->
<form role="dialog" data-type="confirm" id="enable-bluetooth-view" hidden>
<section>
<h1 data-l10n-id="confirmation">Confirmation</h1>
<p>
<img id="icon" src="style/images/settings_bluetooth.png" alt="Bluetooth Icon"/>
<strong data-l10n-id="bluetooth">Bluetooth</strong>
<small data-l10n-id="bluetooth-status-description">Bluetooth is disabled</small>
</p>
<p data-l10n-id="turn-bluetooth-on">Do you want to turn Bluetooth on?</p>
</section>
<menu>
<button type="reset" id="enable-bluetooth-button-cancel" data-l10n-id="cancel">Cancel</button>
<button type="submit" class="recommend" id="enable-bluetooth-button-turn-on" data-l10n-id="turn-on">Turn On</button>
</menu>
</form>
<!-- Bluetooth File Transfer Failed View -->
<form role="dialog" data-type="confirm" id="alert-view" hidden>
<section>
<h1 data-l10n-id="error-transfer-title">Bluetooth file transfer failed</h1>
<p data-l10n-id="error-transfer-settings">
Bluetooth file transfer failed. Check that the Bluetooth settings are correct.
</p>
</section>
<menu>
<button class="full danger" id="alert-button-ok" data-l10n-id="ok">OK</button>
</menu>
</form>
<!-- Devices List View -->
<section role="region" id="devices-list-view" class="devices-list-view current" data-rendered="true" hidden>
<gaia-header ignore-dir id="devices-list-header" class="devices-list-header" action="close">
<h1 data-l10n-id="bluetooth"></h1>
</gaia-header>
<div>
<!-- Devices List View :: Paired Devices List -->
<gaia-subheader id="bluetooth-paired-title" class="bluetooth-paired-title" skin="organic">
<span data-l10n-id="bluetooth-paired-devices">Paired Devices</span>
</gaia-subheader>
<ul id="bluetooth-paired-devices" class="bluetooth-paired-devices devices">
</ul>
<!-- Devices List View :: Found Devices List -->
<gaia-subheader id="bluetooth-found-title" class="bluetooth-found-title" skin="organic">
<span data-l10n-id="bluetooth-devices-in-area">Devices found</span>
</gaia-subheader>
<ul id="bluetooth-devices" class="bluetooth-devices devices">
</ul>
<!-- Devices List View :: Search Button -->
<div id="bluetooth-searching" data-l10n-id="search-for-device" class="bluetooth-searching explanation">
Searching for devices…
</div>
<ul id="bluetooth-search" class="bluetooth-search">
<li>
<label>
<button id="search-device" class="search-device" data-l10n-id="search-device" disabled>Search for Devices</button>
</label>
</li>
</ul>
</div>
</section>
</body>
</html>

View file

@ -0,0 +1,76 @@
/* global MocksHelper, MockL10n, MockNavigatorSettings,
MockMozBluetooth, gDeviceList */
'use strict';
require('/shared/test/unit/mocks/mocks_helper.js');
require('/shared/test/unit/load_body_html_helper.js');
require('/shared/test/unit/mocks/mock_l10n.js');
require('/shared/test/unit/mocks/mock_navigator_moz_settings.js');
require('/shared/test/unit/mocks/mock_navigator_moz_bluetooth.js');
function switchReadOnlyProperty(originObject, propName, targetObj) {
Object.defineProperty(originObject, propName, {
configurable: true,
get: function() { return targetObj; }
});
}
var mocksForDeviceListHelper = new MocksHelper([
'NavigatorSettings'
]).init();
suite('Bluetooth app > deviceList ', function() {
var realL10n;
var realMozSettings;
var realMozBluetooth;
var deviceList;
var bluetoothSearch;
var searchAgainBtn;
var searchingItem;
var exitButton;
mocksForDeviceListHelper.attachTestHelpers();
suiteSetup(function(done) {
realL10n = window.navigator.mozL10n;
window.navigator.mozL10n = MockL10n;
realMozSettings = navigator.mozSettings;
navigator.mozSettings = MockNavigatorSettings;
realMozBluetooth = navigator.mozBluetooth;
switchReadOnlyProperty(navigator, 'mozBluetooth', MockMozBluetooth);
loadBodyHTML('./_transfer.html');
requireApp('bluetooth/js/deviceList.js', done);
});
suiteTeardown(function() {
navigator.mozSettings = realMozSettings;
switchReadOnlyProperty(navigator, 'mozBluetooth', realMozBluetooth);
window.navigator.mozL10n = realL10n;
document.body.innerHTML = '';
});
setup(function() {
deviceList = document.getElementById('devices-list-view');
bluetoothSearch = document.getElementById('bluetooth-search');
searchAgainBtn = document.getElementById('search-device');
searchingItem = document.getElementById('bluetooth-searching');
exitButton = document.getElementById('cancel-activity');
});
suite('deviceList > ', function() {
test('list is updated with arg "true" ', function() {
gDeviceList.update(true);
assert.isFalse(bluetoothSearch.hidden);
assert.isFalse(searchingItem.hidden);
});
test('list is updated with arg "false" ', function() {
gDeviceList.update(false);
assert.isTrue(bluetoothSearch.hidden);
assert.isTrue(searchingItem.hidden);
});
});
});

View file

@ -0,0 +1,12 @@
define(function() {
'use strict';
var MockCannotTransferDialog = {
init: function mctd_init() {},
showConfirm: function mctd_showConfirm() {},
get isVisible() {},
close: function mctd_close() {}
};
return MockCannotTransferDialog;
});

View file

@ -0,0 +1,10 @@
/* exported MockgDeviceList */
'use strict';
var MockgDeviceList = {
update: function mdl_updateDeviceList() {},
initWithAdapter: function mdl_initial() {},
uninit: function mdl_uninit() {},
startDiscovery: function mdl_startDiscovery() {},
onDeviceFound: function mdl_onDeviceFound() {}
};

View file

@ -0,0 +1,12 @@
define(function() {
'use strict';
var MockPairExpiredDialog = {
init: function mped_init() {},
showConfirm: function mped_showConfirm() {},
get isVisible() {},
close: function mped_close() {}
};
return MockPairExpiredDialog;
});

View file

@ -0,0 +1,8 @@
/* exported MockPairManager */
'use strict';
var MockPairManager = {
setConfirmation: function mpm_setConfirmation(address, confirmed) {},
setPinCode: function mpm_setPinCode(address, pincode) {},
setPasskey: function mpm_setPasskey(address, passkey) {}
};

View file

@ -0,0 +1,10 @@
define(function() {
'use strict';
var MockPairview = {
init: function mpv_init(mode, method, device, passkey) {},
closeInput: function mpv_closeInput() {}
};
return MockPairview;
});

View file

@ -0,0 +1,12 @@
define(function() {
'use strict';
var MockTurnBluetoothOnDialog = {
init: function mtbod_init() {},
showConfirm: function mtbod_showConfirm() {},
get isVisible() {},
close: function mtbod_close() {}
};
return MockTurnBluetoothOnDialog;
});

View file

@ -0,0 +1,84 @@
'use strict';
suite('BluetoothAdapterManager', function() {
var adapterManager;
setup(function(done) {
var modules = [
'modules/bluetooth/bluetooth_adapter_manager'
];
var map = {
'modules/bluetooth': {
'modules/navigator/mozBluetooth': 'MockNavigatorBluetooth'
}
};
this.MockNavigatorBluetooth = {
defaultAdapter: {},
callbacks: {
'attributechanged': []
},
addEventListener: function(eventName, callback) {
this.callbacks[eventName].push(callback);
}
};
define('MockNavigatorBluetooth', function() {
return this.MockNavigatorBluetooth;
}.bind(this));
testRequire(modules, map, function(AdapterManager) {
adapterManager = AdapterManager;
done();
});
});
suite('_init > ', function() {
setup(function() {
this.sinon.stub(adapterManager, '_watchMozBluetoothAttributechanged');
this.sinon.stub(adapterManager, '_initDefaultAdapter');
});
test('mozBluetooth attributechanged should be watched, ' +
'defaultAdapter should be inited ', function() {
adapterManager._init();
assert.isTrue(adapterManager._watchMozBluetoothAttributechanged.called);
assert.isTrue(adapterManager._initDefaultAdapter.called);
});
});
suite('_watchMozBluetoothAttributechanged > ', function() {
var eventName, mockEvent;
setup(function() {
this.sinon.spy(this.MockNavigatorBluetooth, 'addEventListener');
eventName = 'attributechanged';
mockEvent = {
attrs: ['defaultAdapter']
};
});
test('"attributechanged" event should be watched ' +
'defaultAdapter should be inited while event is coming ', function() {
adapterManager._watchMozBluetoothAttributechanged();
assert.isTrue(
this.MockNavigatorBluetooth.addEventListener.calledWith(eventName));
// test 'attributechanged' event coming
this.MockNavigatorBluetooth.callbacks[eventName][0](mockEvent);
assert.equal(adapterManager.defaultAdapter,
this.MockNavigatorBluetooth.defaultAdapter);
});
});
suite('_initDefaultAdapter > ', function() {
setup(function() {
adapterManager.defaultAdapter = null;
});
test('defaultAdapter should be defined ', function() {
adapterManager._initDefaultAdapter();
assert.equal(adapterManager.defaultAdapter,
this.MockNavigatorBluetooth.defaultAdapter);
});
});
});

View file

@ -0,0 +1,126 @@
'use strict';
suite('BluetoothClassOfDeviceMapper > ', function() {
setup(function(done) {
var module = [
'modules/bluetooth/bluetooth_cod_mapper'
];
var map = {};
var requireCtx = testRequire([], map, function() {});
requireCtx(module, function(BtClassOfDeviceMapper) {
this.BtClassOfDeviceMapper = BtClassOfDeviceMapper;
done();
}.bind(this));
});
suite('getDeviceType > major device class: computer, phone, audio/video, ' +
'peripheral, imaging > ', function() {
var mockCodInput, expectedType;
suite('mapping in minor device class which is defined ', function() {
setup(function() {
mockCodInput = {
majorDeviceClass: 4,
majorServiceClass: 555,
minorDeviceClass: 3
};
expectedType = 'audio-card';
});
test('should return device type = "audio-card" ', function() {
assert.equal(this.BtClassOfDeviceMapper.getDeviceType(mockCodInput),
expectedType);
});
});
suite('mapping in minor device class which is reserved ', function() {
setup(function() {
mockCodInput = {
majorDeviceClass: 4,
majorServiceClass: 555,
minorDeviceClass: 23
};
expectedType = 'audio-card';
});
test('should return device type = "audio-card" ', function() {
assert.equal(this.BtClassOfDeviceMapper.getDeviceType(mockCodInput),
expectedType);
});
});
suite('mapping in minor device class which is not defined ', function() {
setup(function() {
mockCodInput = {
majorDeviceClass: 6,
majorServiceClass: 555,
minorDeviceClass: 2
};
expectedType = '';
});
test('should return device type = "" ', function() {
assert.equal(this.BtClassOfDeviceMapper.getDeviceType(mockCodInput),
expectedType);
});
});
});
suite('getDeviceType > major device class: network-wireless ', function() {
var mockCodInput, expectedType;
suite('mapping in major device class ', function() {
setup(function() {
mockCodInput = {
majorDeviceClass: 3,
majorServiceClass: 555,
minorDeviceClass: 10
};
expectedType = 'network-wireless';
});
test('should return device type = "network-wireless" ', function() {
assert.equal(this.BtClassOfDeviceMapper.getDeviceType(mockCodInput),
expectedType);
});
});
});
suite('getDeviceType > major device class: wearable, toy, health > ',
function() {
var mockCodInput, expectedType;
suite('not mapping in major device class, but mapping in ' +
'major service class: audio ', function() {
setup(function() {
mockCodInput = {
majorDeviceClass: 8,
majorServiceClass: 256,
minorDeviceClass: 3
};
expectedType = 'audio-card';
});
test('should return device type = "audio-card" ', function() {
assert.equal(this.BtClassOfDeviceMapper.getDeviceType(mockCodInput),
expectedType);
});
});
});
suite('getDeviceType > not mapping in any class ', function() {
var mockCodInput, expectedType;
suite('not mapping in any class ', function() {
setup(function() {
mockCodInput = {
majorDeviceClass: 8,
majorServiceClass: 555,
minorDeviceClass: 10
};
expectedType = '';
});
test('should return device type = "" ', function() {
assert.equal(this.BtClassOfDeviceMapper.getDeviceType(mockCodInput),
expectedType);
});
});
});
});

View file

@ -0,0 +1,553 @@
'use strict';
suite('BluetoothContext', function() {
var adapterManager;
var asyncStorage;
var connectionManager;
setup(function(done) {
var modules = [
'modules/bluetooth/bluetooth_adapter_manager',
'shared/async_storage',
'modules/bluetooth/bluetooth_connection_manager'
];
var map = {
'*': {
'modules/bluetooth/bluetooth_adapter_manager': 'MockAdapterManager',
'shared/async_storage': 'MockAsyncStorage'
}
};
this.MockAdapterManager = {
defaultAdapter: {
state: 'enabled',
addEventListener: function() {},
removeEventListener: function() {},
getPairedDevices: function() {return [];},
stopDiscovery: function() {},
connect: function() {},
disconnect: function() {},
getConnectedDevices: function() {}
},
observe: function() {}
};
define('MockAdapterManager', function() {
return this.MockAdapterManager;
}.bind(this));
this.MockAsyncStorage = {
getItem: function() {},
setItem: function() {},
removeItem: function() {}
};
define('MockAsyncStorage', function() {
return this.MockAsyncStorage;
}.bind(this));
testRequire(modules, map, function(AdapterManager, AsyncStorage,
ConnectionManager) {
adapterManager = AdapterManager;
asyncStorage = AsyncStorage;
connectionManager = ConnectionManager;
done();
});
});
suite('Init > ', function() {
setup(function() {
this.sinon.stub(adapterManager, 'observe');
this.sinon.stub(connectionManager, '_onDefaultAdapterChanged');
});
test('AdapterManager "defaultAdapter" property should be observed, ' +
'and access defaultAdapter from AdapterManager manually ', function() {
connectionManager._init();
assert.isTrue(adapterManager.observe.calledWith('defaultAdapter'));
assert.isTrue(connectionManager._onDefaultAdapterChanged.calledWith(
adapterManager.defaultAdapter));
});
});
suite('_onDefaultAdapterChanged > ', function() {
var newAdapter, oldAdapter;
setup(function() {
oldAdapter = {};
connectionManager._defaultAdapter = null;
this.sinon.stub(connectionManager, '_unwatchProfilesStatuschanged');
this.sinon.stub(connectionManager,
'_unwatchDefaultAdapterOnattributechanged');
this.sinon.stub(connectionManager, '_watchProfilesStatuschanged');
this.sinon.stub(connectionManager,
'_watchDefaultAdapterOnattributechanged');
this.sinon.stub(connectionManager, '_restoreConnection');
this.sinon.stub(connectionManager, '_resetConnectionInfo');
});
suite('changed with new/old adapter, ' +
'in case of having new adapter > ', function() {
suiteSetup(function() {
newAdapter = {
state: 'enabled'
};
});
test('should unwatch events from old adapter, ' +
'then watch events from new adapter ', function() {
connectionManager._onDefaultAdapterChanged(newAdapter, oldAdapter);
// test default adapter updated
assert.equal(connectionManager._defaultAdapter, newAdapter);
// test unwatch events
assert.isTrue(
connectionManager._unwatchProfilesStatuschanged.calledWith(
oldAdapter));
assert.isTrue(
connectionManager._unwatchDefaultAdapterOnattributechanged.calledWith(
oldAdapter));
// test watch events
assert.isTrue(
connectionManager._watchProfilesStatuschanged.calledWith(newAdapter));
assert.isTrue(
connectionManager._watchDefaultAdapterOnattributechanged.calledWith(
newAdapter));
// test restore connection
assert.isTrue(connectionManager._restoreConnection.called);
// test reset properties
assert.isFalse(connectionManager._resetConnectionInfo.called);
});
});
suite('changed with new/old adapter, ' +
'in case of new adapter is null > ', function() {
suiteSetup(function() {
newAdapter = null;
});
test('should unwatch events from old adapter, ' +
'then watch events from new adapter ', function() {
connectionManager._onDefaultAdapterChanged(newAdapter, oldAdapter);
// test default adapter updated
assert.isNull(connectionManager._defaultAdapter);
// test unwatch events
assert.isTrue(
connectionManager._unwatchProfilesStatuschanged.calledWith(
oldAdapter));
assert.isTrue(
connectionManager._unwatchDefaultAdapterOnattributechanged.calledWith(
oldAdapter));
// test watch events
assert.isFalse(
connectionManager._watchProfilesStatuschanged.calledWith(newAdapter));
assert.isFalse(
connectionManager._watchDefaultAdapterOnattributechanged.calledWith(
newAdapter));
// test restore connection
assert.isFalse(connectionManager._restoreConnection.called);
// test reset properties
assert.isTrue(connectionManager._resetConnectionInfo.called);
});
});
});
suite('_initConnectedDevicesInfo > ', function() {
suite('has default adapter, init connected devices info ', function() {
var mockAdapter, mockGetConnectedDevicesFromPlatformPromise,
mockConnectedDevicesByProfile;
setup(function() {
mockAdapter = {};
connectionManager._defaultAdapter = mockAdapter;
mockConnectedDevicesByProfile = {};
mockGetConnectedDevicesFromPlatformPromise =
Promise.resolve(mockConnectedDevicesByProfile);
this.sinon.stub(connectionManager,
'_getConnectedDevicesFromPlatform').returns(
mockGetConnectedDevicesFromPlatformPromise);
this.sinon.stub(connectionManager, '_constructDeviceItemsMap');
});
test('_constructDeviceItemsMap() should be called while ' +
'_getConnectedDevicesFromPlatform is resolved ',
function(done) {
connectionManager._initConnectedDevicesInfo().then(function() {
assert.isTrue(
connectionManager._getConnectedDevicesFromPlatform.called);
mockGetConnectedDevicesFromPlatformPromise.then(() => {
assert.isTrue(
connectionManager._constructDeviceItemsMap.calledWith(
mockConnectedDevicesByProfile));
}, () => {
//reject case
});
}, function() {
// reject case
}).then(done, done);
});
});
suite('has no default adapter, init connected devices info ', function() {
setup(function() {
connectionManager._defaultAdapter = null;
this.sinon.stub(connectionManager, '_getConnectedDevicesFromPlatform');
this.sinon.stub(connectionManager, '_constructDeviceItemsMap');
});
test('_getConnectedDevicesFromPlatform() should not be called, ' +
'_constructDeviceItemsMap() should not be called',
function(done) {
connectionManager._initConnectedDevicesInfo().then(function() {
// resolve case
}, function() {
assert.isFalse(
connectionManager._getConnectedDevicesFromPlatform.called);
assert.isFalse(
connectionManager._constructDeviceItemsMap.called);
}).then(done, done);
});
});
});
suite('_constructDeviceItemsMap > ', function() {
var mockConnectedDevices, expectedConectionDeviceInfo;
setup(function() {
mockConnectedDevices = {
'hfp': [{address: 'AA:BB:CC:00:11:22'}],
'a2dp': [{address: 'AA:BB:CC:00:11:22'}]
};
expectedConectionDeviceInfo = {
address: mockConnectedDevices.hfp[0].address,
connected: true,
profileID: 'hfp',
device: mockConnectedDevices.hfp[0]
};
this.sinon.stub(connectionManager, '_initConnectedDevicesCache');
});
test('_initConnectedDevicesCache should be called ', function() {
connectionManager._constructDeviceItemsMap(mockConnectedDevices);
assert.isTrue(connectionManager._initConnectedDevicesCache.called);
assert.deepEqual(connectionManager._initConnectedDevicesCache.args[0][0],
expectedConectionDeviceInfo);
});
});
suite('_initConnectedDevicesCache > ', function() {
var mockConnectedDevicesInfo, options;
suite('the updating device has info > ', function() {
setup(function() {
mockConnectedDevicesInfo = {
'AA:BB:CC:00:11:22': {
'device': {},
'connectedProfiles': {
'hfp': false,
'a2dp': true
}
}
};
options = {
address: 'AA:BB:CC:00:11:22',
connected: true,
profileID: 'hfp',
device: {}
};
connectionManager._connectedDevicesInfo = mockConnectedDevicesInfo;
this.sinon.stub(connectionManager,
'_hasConnectedProfileByAddress').returns(true);
});
test('the specific profile of connected device info should be ' +
'updated, Ex: "hfp": true ', function() {
var newCache = connectionManager._initConnectedDevicesCache(options);
assert.isTrue(newCache[options.address].connectedProfiles.hfp);
});
});
suite('the updating device has no info > ', function() {
setup(function() {
mockConnectedDevicesInfo = {};
options = {
address: 'AA:BB:CC:00:11:22',
connected: true,
profileID: 'hfp',
device: {}
};
connectionManager._connectedDevicesInfo = mockConnectedDevicesInfo;
this.sinon.stub(connectionManager,
'_hasConnectedProfileByAddress').returns(true);
});
test('the specific profile of connected device info should be ' +
'updated, Ex: "hfp": true, "device": options.device ', function() {
var newCache = connectionManager._initConnectedDevicesCache(options);
assert.isTrue(newCache[options.address].connectedProfiles.hfp);
assert.equal(newCache[options.address].device, options.device);
});
});
suite('the updating device has no connected profile > ', function() {
setup(function() {
mockConnectedDevicesInfo = {
'AA:BB:CC:00:11:22': {
'device': {},
'connectedProfiles': {
'hfp': true,
'a2dp': false
}
}
};
options = {
address: 'AA:BB:CC:00:11:22',
connected: false,
profileID: 'hfp',
device: {}
};
connectionManager._connectedDevicesInfo = mockConnectedDevicesInfo;
this.sinon.stub(connectionManager,
'_hasConnectedProfileByAddress').returns(false);
});
test('the device info should be removed from cache ', function() {
var newCache = connectionManager._initConnectedDevicesCache(options);
assert.isUndefined(newCache[options.address]);
});
});
});
suite('getConnectedDevices > ', function() {
suite('already requested getConnectedDevices() with promise > ',
function() {
var mockPromise;
setup(function() {
mockPromise = new Promise(function() {});
connectionManager._getConnectedDevicesPromise = mockPromise;
});
test('should return the requested promise..', function() {
assert.equal(connectionManager.getConnectedDevices(), mockPromise);
});
});
suite('have not requested getConnectedDevices() with promise > ',
function() {
var mockConnectedDevicesInfoPromise, mockConnectedDevicesInfo;
setup(function() {
connectionManager._getConnectedDevicesPromise = null;
mockConnectedDevicesInfo = {'AA:BB:CC:00:11:22': {}};
connectionManager._connectedDevicesInfo = mockConnectedDevicesInfo;
mockConnectedDevicesInfoPromise = Promise.resolve();
this.sinon.stub(connectionManager,
'_initConnectedDevicesInfo').returns(mockConnectedDevicesInfoPromise);
});
test('should return connected device cached while promise resolve ',
function(done) {
connectionManager.getConnectedDevices().then((connectedDevicesInfo) => {
assert.equal(connectedDevicesInfo, mockConnectedDevicesInfo);
}, () => {
// reject case
}).then(done, done);
});
});
suite('have not requested getConnectedDevices() with promise > ',
function() {
var mockConnectedDevicesInfoPromise;
setup(function() {
connectionManager._getConnectedDevicesPromise = null;
mockConnectedDevicesInfoPromise = Promise.reject();
this.sinon.stub(connectionManager,
'_initConnectedDevicesInfo').returns(mockConnectedDevicesInfoPromise);
});
test('should clean up instance of cache while promise reject ',
function(done) {
connectionManager.getConnectedDevices().then(() => {
assert.isNull(connectionManager._getConnectedDevicesPromise);
}, () => {
// reject case
}).then(done, done);
});
});
});
suite('_resetConnectionInfo > ', function() {
setup(function() {
connectionManager.connectingAddress = 'AA:BB:CC:00:11:22';
connectionManager._getConnectedDevicesPromise = Promise.resolve();
});
test('connecting address, instance of cache should be reset ', function() {
connectionManager._resetConnectionInfo();
assert.isNull(connectionManager.connectingAddress);
assert.isNull(connectionManager._getConnectedDevicesPromise);
});
});
suite('_watchDefaultAdapterOnattributechanged > ', function() {
var mockAdapter;
setup(function() {
mockAdapter = {
addEventListener: function() {}
};
this.sinon.stub(mockAdapter, 'addEventListener');
this.sinon.stub(connectionManager, '_onAdapterAttributeChanged');
});
test('attributechanged should be registered callback function', function() {
connectionManager._watchDefaultAdapterOnattributechanged(mockAdapter);
assert.equal(mockAdapter.addEventListener.args[0][0], 'attributechanged');
mockAdapter.addEventListener.args[0][1]();
assert.isTrue(connectionManager._onAdapterAttributeChanged.calledWith(
mockAdapter));
});
});
suite('_unwatchDefaultAdapterOnattributechanged > ', function() {
var mockAdapter;
setup(function() {
mockAdapter = {
removeEventListener: function() {}
};
this.sinon.stub(mockAdapter, 'removeEventListener');
this.sinon.stub(connectionManager, '_onAdapterAttributeChanged');
});
test('onattributechanged should be unregistered ', function() {
connectionManager._unwatchDefaultAdapterOnattributechanged(mockAdapter);
assert.equal(mockAdapter.removeEventListener.args[0][0],
'attributechanged');
assert.equal(mockAdapter.removeEventListener.args[0][1],
connectionManager._onAdapterAttributeChanged);
});
});
suite('_onAdapterAttributeChanged > ', function() {
var mockAdapter, evt;
setup(function() {
mockAdapter = {
state: 'enabled'
};
});
suite('state changed in enabled ', function() {
setup(function() {
evt = {
attrs: ['state']
};
this.sinon.stub(connectionManager, '_restoreConnection');
});
test('_restoreConnection() should be called ', function() {
connectionManager._onAdapterAttributeChanged(mockAdapter, evt);
assert.isTrue(connectionManager._restoreConnection.called);
});
['enabling', 'disabling', 'disabled'].forEach(function(state) {
test('_restoreConnection() should not be called when ' + state,
function() {
mockAdapter.state = state;
connectionManager._onAdapterAttributeChanged(mockAdapter, evt);
assert.isTrue(connectionManager._restoreConnection.notCalled);
});
});
});
});
suite('_watchProfilesStatuschanged > ', function() {
var mockAdapter;
setup(function() {
mockAdapter = {};
});
test('profile status callback of adapter should be registered', function() {
connectionManager._watchProfilesStatuschanged(mockAdapter);
assert.isDefined(mockAdapter.onhfpstatuschanged);
assert.isDefined(mockAdapter.ona2dpstatuschanged);
});
});
suite('_unwatchProfilesStatuschanged > ', function() {
var mockAdapter;
setup(function() {
mockAdapter = {
onhfpstatuschanged: function() {},
ona2dpstatuschanged: function() {}
};
});
test('profile status callback of adapter should be null ', function() {
connectionManager._unwatchProfilesStatuschanged(mockAdapter);
assert.isNull(mockAdapter.onhfpstatuschanged);
assert.isNull(mockAdapter.ona2dpstatuschanged);
});
});
suite('_onProfileStatuschangeHandler > ', function() {
var mockProfileID, mockEvt, resultOptions;
setup(function() {
mockProfileID = 'hfp';
mockEvt = {
address: 'AA:BB:CC:00:11:22',
status: true
};
resultOptions = {
address: mockEvt.address,
connected: mockEvt.status,
profileID: mockProfileID
};
this.sinon.stub(connectionManager, '_updateConnectionStatus');
});
test('_updateConnectionStatus() should be called with options', function() {
connectionManager._onProfileStatuschangeHandler(mockProfileID, mockEvt);
assert.isTrue(
connectionManager._updateConnectionStatus.calledWith(resultOptions));
});
});
suite('_recordConnection > ', function() {
var action, address;
var itemKey = 'device.connected';
suite('action: set > ', function() {
setup(function() {
action = 'set';
address = 'AA:BB:CC:00:11:22';
this.sinon.stub(asyncStorage, 'setItem');
this.sinon.stub(asyncStorage, 'removeItem');
});
test('should set item in async storage ', function() {
connectionManager._recordConnection(action, address);
assert.equal(asyncStorage.setItem.args[0][0], itemKey);
assert.equal(asyncStorage.setItem.args[0][1], address);
assert.isFalse(asyncStorage.removeItem.called);
});
});
suite('action: remove > ', function() {
var mockAdapter;
setup(function() {
action = 'remove';
address = 'AA:BB:CC:00:11:22';
mockAdapter = {
state: 'enabled'
};
connectionManager._defaultAdapter = mockAdapter;
this.sinon.stub(asyncStorage, 'setItem');
this.sinon.stub(asyncStorage, 'removeItem');
});
test('should remove item in async storage ', function() {
connectionManager._recordConnection(action, address);
assert.isTrue(asyncStorage.removeItem.calledWith(itemKey));
assert.isFalse(asyncStorage.setItem.called);
});
});
});
});

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,195 @@
'use strict';
suite('BluetoothDevice', function() {
var bluetoothDevice, mockBtClassOfDeviceMapper, mockDeviceObj,
defaultConnectionStatus;
setup(function(done) {
var modules = [
'modules/bluetooth/bluetooth_device',
'modules/bluetooth/bluetooth_cod_mapper'
];
var map = {
'modules/bluetooth': {
'modules/bluetooth/bluetooth_cod_mapper': 'MockBtClassOfDeviceMapper'
}
};
mockBtClassOfDeviceMapper = {
getDeviceType: function() {return '';}
};
define('MockBtClassOfDeviceMapper', function() {
return mockBtClassOfDeviceMapper;
});
mockDeviceObj = {
onattributechanged: {},
name: 'device-01',
paired: false,
address: 'AA:BB:CC:00:11:22',
cod: {}
};
defaultConnectionStatus = 'disconnected';
testRequire(modules, map, function(BluetoothDevice) {
bluetoothDevice = BluetoothDevice;
done();
});
});
suite('ctor > ', function() {
var bluetoothDeviceCreated;
setup(function() {
bluetoothDeviceCreated = bluetoothDevice(mockDeviceObj);
});
suite('create > ', function() {
test('Will create BluetoothDevice with "name", "paired", ' +
'"address", "type", "connectionStatus", "profiles", ' +
'and "descriptionText" properties. Define getter "data", ' +
'function "updateConnectionInfo", "updateDescriptionText".',
function() {
assert.equal(bluetoothDeviceCreated.name, mockDeviceObj.name);
assert.equal(bluetoothDeviceCreated.paired, mockDeviceObj.paired);
assert.equal(bluetoothDeviceCreated.address, mockDeviceObj.address);
assert.isDefined(bluetoothDeviceCreated.type);
assert.equal(bluetoothDeviceCreated.connectionStatus,
defaultConnectionStatus);
assert.isNull(bluetoothDeviceCreated.profiles);
assert.isDefined(bluetoothDeviceCreated.descriptionText);
assert.isDefined(bluetoothDeviceCreated.data);
assert.isDefined(bluetoothDeviceCreated.updateConnectionInfo);
assert.isDefined(bluetoothDeviceCreated.updateDescriptionText);
});
});
suite('updateConnectionInfo > ', function() {
var mockOptions;
setup(function() {
mockOptions = {
connectionStatus: 'connected',
profiles: {
'hfp': true,
'a2dp': false
}
};
});
test('"connectionStatus" property should be "connected"', function() {
bluetoothDeviceCreated.updateConnectionInfo(mockOptions);
assert.equal(bluetoothDeviceCreated.connectionStatus,
mockOptions.connectionStatus);
assert.deepEqual(bluetoothDeviceCreated.profiles,
mockOptions.profiles);
});
});
suite('updateDescriptionText > ', function() {
suite('"paired" property changed > ', function() {
var newPairedState;
setup(function() {
newPairedState = 'pairing';
bluetoothDeviceCreated.paired = newPairedState;
});
test('"descriptionText" property should be "pairing" ', function() {
assert.equal(bluetoothDeviceCreated.descriptionText, newPairedState);
});
});
suite('"connectionStatus" property changed > ', function() {
var newConnectionStatus;
setup(function() {
newConnectionStatus = 'connecting';
bluetoothDeviceCreated.paired = true;
bluetoothDeviceCreated.connectionStatus = newConnectionStatus;
});
test('"descriptionText" property should be "connecting" ', function() {
assert.equal(bluetoothDeviceCreated.descriptionText,
newConnectionStatus);
});
});
suite('"profiles" property changed > ', function() {
var newProfiles, expectedDescriptionText;
setup(function() {
newProfiles = {
'hfp': true,
'a2dp': true
};
expectedDescriptionText = 'connectedWithDeviceMedia';
bluetoothDeviceCreated.paired = true;
bluetoothDeviceCreated.connectionStatus = 'connected';
bluetoothDeviceCreated.profiles = newProfiles;
});
test('"descriptionText" property should be "connectedWithDeviceMedia" ',
function() {
assert.equal(bluetoothDeviceCreated.descriptionText,
expectedDescriptionText);
});
});
});
suite('event > name ', function() {
var mockEvt, newName;
setup(function() {
mockEvt = {
attrs: ['name']
};
newName = 'device-02';
mockDeviceObj.name = newName;
});
test('"onattributechanged" event coming with ' +
'name property changed ', function() {
mockDeviceObj.onattributechanged(mockEvt);
assert.equal(bluetoothDeviceCreated.name, newName);
});
});
suite('event > paired ', function() {
var mockEvt, newPairedState;
setup(function() {
mockEvt = {
attrs: ['paired']
};
newPairedState = true;
mockDeviceObj.paired = newPairedState;
});
test('"onattributechanged" event coming with ' +
'paired property changed ', function() {
mockDeviceObj.onattributechanged(mockEvt);
assert.equal(bluetoothDeviceCreated.paired, newPairedState);
});
});
suite('event > cod ', function() {
var mockEvt, newCODObject, mockType;
setup(function() {
mockEvt = {
attrs: ['cod']
};
mockType = 'audio-card';
newCODObject = {};
mockDeviceObj.cod = newCODObject;
this.sinon.stub(mockBtClassOfDeviceMapper, 'getDeviceType').returns(
mockType);
});
test('"onattributechanged" event coming with ' +
'cod property changed ', function() {
mockDeviceObj.onattributechanged(mockEvt);
assert.isTrue(mockBtClassOfDeviceMapper.getDeviceType.calledWith(
newCODObject));
assert.equal(bluetoothDeviceCreated.type, mockType);
});
});
});
});

View file

@ -0,0 +1,618 @@
/* global define */
'use strict';
// The views are very tightly coupled to models, rather than reimplement model
// behavior in a mock, rely on tested interfaces on the model instead
suite('ListView', function() {
// Define a shim for ObservableArray that is to be loaded in ListView.
// Then we are able to creat a stub on it.
suiteSetup(function(done) {
// Use the shim in ListView.
var ObservableArrayShimName = 'observable_array_shim' + Date.now();
var map = {
'modules/mvvm/list_view': {
'modules/mvvm/observable_array': ObservableArrayShimName
}
};
// XXX: Create a new require context.
// We can only define the mock module after retrieving the context.
var requireCtx = testRequire([], map, function() {});
// Define the mock module.
define(ObservableArrayShimName, function() {
var ctor = function observable_array_shim() {
return ctor.mInnerFunction.apply(this, arguments);
};
ctor.mInnerFunction = function() {};
ctor.mTeardown = function observable_array_shim_teardown() {
ctor.mInnerFunction = function() {};
};
return ctor;
});
requireCtx([
'modules/mvvm/observable',
'modules/mvvm/observable_array',
'modules/mvvm/list_view',
ObservableArrayShimName
], (function(Observable, ObservableArray, ListView, ObservableArrayShim) {
this.Observable = Observable;
this.ListView = ListView;
this.ObservableArrayShim = ObservableArrayShim;
// stub the real ObservableArray to create spies on observe method
// Redirect the shim to the ObservableArray stub.
this.ObservableArray = sinon.stub(ObservableArrayShim,
'mInnerFunction', function() {
var result = ObservableArray.apply(this, arguments);
sinon.spy(result, 'observe');
sinon.spy(result, 'unobserve');
return result;
});
done();
}).bind(this));
});
teardown(function() {
this.ObservableArray.reset();
});
suiteTeardown(function() {
this.ObservableArray.restore();
});
setup(function() {
this.array = [];
this.observableArray = this.ObservableArray(this.array);
});
suite('ListView(<ul>, ObservableArray, function)', function() {
setup(function() {
this.container = document.createElement('ul');
this.template = (function(item, recycle) {
if (recycle && this.template.recycle) {
return recycle;
}
return document.createElement('li');
}).bind(this);
this.sinon.spy(this, 'template');
this.listView = this.ListView(
this.container, this.observableArray, this.template
);
});
test('.element', function() {
assert.equal(this.listView.element, this.container);
});
test('Listens to ObservableArray', function() {
var observe = this.observableArray.observe;
assert.ok(observe.calledWith('insert'));
assert.ok(observe.calledWith('remove'));
assert.ok(observe.calledWith('replace'));
assert.ok(observe.calledWith('reset'));
});
suite('Add Multiple Elements', function() {
setup(function() {
// add 3 items
for (var count = 0; count < 3; count++) {
this.observableArray.push({ count: count });
}
});
// use forEach instead of for to keep 'count' in a closure
[0, 1, 2].forEach(function(count) {
suite('item ' + count, function() {
setup(function() {
this.item = this.array[count];
});
test('kept order', function() {
assert.equal(this.item.count, count);
});
test('called template', function() {
assert.ok(this.template.calledWith(this.item));
});
test('template returned element is in correct position', function() {
var li = this.template.withArgs(this.item).returnValues[0];
assert.equal(this.container.children[count], li);
});
});
});
suite('Replace Element', function() {
setup(function() {
this.template.reset();
this.item = { replacement: true };
this.originalElements =
Array.prototype.slice.call(this.container.children);
this.observableArray.set(1, this.item);
});
test('called template with recycled element', function() {
assert.ok(
this.template.calledWith(this.item, this.originalElements[1])
);
});
test('only called template method once', function() {
assert.equal(this.template.callCount, 1);
});
test('replaced element', function() {
var li = this.template.withArgs(this.item).returnValues[0];
assert.equal(this.container.children[1], li);
});
test('leaves other elements alone', function() {
assert.equal(this.container.children[0], this.originalElements[0]);
assert.equal(this.container.children[2], this.originalElements[2]);
});
});
suite('Replace Element (w/recycle)', function() {
setup(function() {
this.template.recycle = true;
this.template.reset();
this.item = { replacement: true };
this.originalElements =
Array.prototype.slice.call(this.container.children);
this.observableArray.set(1, this.item);
});
test('called template with recycled element', function() {
assert.ok(
this.template.calledWith(this.item, this.originalElements[1])
);
});
test('only called template method once', function() {
assert.equal(this.template.callCount, 1);
});
test('same element still in place', function() {
var li = this.template.withArgs(this.item).returnValues[0];
assert.equal(li, this.originalElements[1], 'returned original');
assert.equal(this.container.children[1], this.originalElements[1]);
});
test('leaves other elements alone', function() {
assert.equal(this.container.children[0], this.originalElements[0]);
assert.equal(this.container.children[2], this.originalElements[2]);
});
});
suite('Reset With Shorter Array', function() {
setup(function() {
this.template.reset();
this.originalElements =
Array.prototype.slice.call(this.container.children);
this.originalArray = this.array;
this.array = [{ reset: 1 }, { reset: 2 }];
this.observableArray.reset(this.array);
});
// use forEach instead of for to store 'index' in closure
[0, 1].forEach(function(index) {
suite('item ' + index, function() {
setup(function() {
this.item = this.array[index];
this.originalElement = this.originalElements[index];
});
test('called template with recycled element', function() {
assert.ok(
this.template.calledWith(this.item, this.originalElement)
);
});
test('replaced element', function() {
var li = this.template.withArgs(this.item).returnValues[0];
assert.equal(this.container.children[index], li);
});
});
});
test('removed extra elements', function() {
assert.equal(this.container.children.length, 2);
assert.equal(this.originalElements[2].parentNode, null);
});
});
suite('Reset With Longer Array', function() {
setup(function() {
this.template.reset();
this.originalElements =
Array.prototype.slice.call(this.container.children);
this.originalArray = this.array;
this.array = [{ reset: 1 }, { reset: 2 }, { reset: 3 }, { reset: 4 }];
this.observableArray.reset(this.array);
});
// use forEach instead of for to store 'index' in closure
[0, 1, 2].forEach(function(index) {
suite('item ' + index, function() {
setup(function() {
this.item = this.array[index];
this.originalElement = this.originalElements[index];
});
test('called template with recycled element', function() {
assert.ok(
this.template.calledWith(this.item, this.originalElement)
);
});
test('replaced element', function() {
var li = this.template.withArgs(this.item).returnValues[0];
assert.equal(this.container.children[index], li);
});
});
});
suite('item 3', function() {
setup(function() {
this.item = this.array[3];
});
test('called template without recycled element', function() {
assert.ok(this.template.calledWith(this.item));
assert.equal(
this.template.withArgs(this.item).args[0][1], undefined
);
});
test('added extra element', function() {
assert.equal(this.container.children.length, 4);
var li = this.template.withArgs(this.item).returnValues[0];
assert.equal(this.container.children[3], li);
});
});
});
suite('reset with .set([{},{}])', function() {
setup(function() {
this.template.reset();
this.originalElements =
Array.prototype.slice.call(this.container.children);
this.originalArray = this.array;
this.array = [{ reset: 1 }, { reset: 2 }];
this.listView.set(this.array);
});
test('creates and watches an ObservableArray', function() {
var withNew = this.ObservableArray.withArgs(this.array);
assert.ok(withNew.called);
var observe = withNew.returnValues[0].observe;
assert.ok(observe.calledWith('insert'));
assert.ok(observe.calledWith('remove'));
assert.ok(observe.calledWith('replace'));
assert.ok(observe.calledWith('reset'));
});
// use forEach instead of for to store 'index' in closure
[0, 1].forEach(function(index) {
suite('item ' + index, function() {
setup(function() {
this.item = this.array[index];
this.originalElement = this.originalElements[index];
});
test('called template with recycled element', function() {
assert.ok(
this.template.calledWith(this.item, this.originalElement)
);
});
test('replaced element', function() {
var li = this.template.withArgs(this.item).returnValues[0];
assert.equal(this.container.children[index], li);
});
});
});
test('removed extra elements', function() {
assert.equal(this.container.children.length, 2);
assert.equal(this.originalElements[2].parentNode, null);
});
});
suite('.enabled = false', function() {
setup(function() {
this.template.reset();
this.originalElements =
Array.prototype.slice.call(this.container.children);
this.listView.enabled = false;
});
// test a bunch of things that modify the array
[function pop() {
this.observableArray.pop();
}, function push() {
this.observableArray.push({});
}, function replace() {
this.observableArray.set(1, {});
}, function reset() {
this.array = [{}];
this.observableArray.reset(this.array);
}, function set() {
this.array = [{}];
this.listView.set(this.array);
}].forEach(function(method) {
suite(method.name, function() {
setup(method);
test('does not call template', function() {
assert.equal(this.template.called, 0);
});
test('does not change elements', function() {
for (var i = 0; i < this.container.children.length; i++) {
assert.equal(this.container.children[i],
this.originalElements[i]);
}
});
suite('.enabled = true', function() {
setup(function() {
this.listView.enabled = true;
});
test('calls template for each item in array', function() {
this.array.forEach(function(item, index) {
assert.ok(this.template.calledWith(
item, this.originalElements[index]
), 'array[' + index + ']');
}, this);
});
test('elements are put in the right place', function() {
this.array.forEach(function(item, index) {
var template = this.template.withArgs(item);
assert.equal(
this.container.children[index], template.returnValues[0],
'element[' + index + ']'
);
}, this);
});
});
});
});
});
});
});
suite('ListView(<div>, ObservableArray, function)', function() {
setup(function() {
this.container = document.createElement('div');
// ensure content inside the container at creation doesn't interfere
this.container.appendChild(document.createElement('span'));
this.template = (function(item, recycle) {
if (recycle && this.template.recycle) {
return recycle;
}
return document.createElement('div');
}).bind(this);
this.sinon.spy(this, 'template');
this.observableArray.push({ count: 0 });
this.listView = this.ListView(
this.container, this.observableArray, this.template
);
});
test('.element', function() {
assert.equal(this.listView.element, this.container);
});
test('Listens to ObservableArray', function() {
var observe = this.observableArray.observe;
assert.ok(observe.calledWith('insert'));
assert.ok(observe.calledWith('remove'));
assert.ok(observe.calledWith('replace'));
assert.ok(observe.calledWith('reset'));
});
test('Initial Element doesnt recycle', function() {
var item = this.array[0];
var filtered = this.template.withArgs(item);
// ensures that if there happens to be an element in the creation
// of the ListView, it is not used as a "recycled" element
assert.ok(filtered.callCount, 1);
assert.equal(filtered.args[0][1], undefined);
});
suite('Add Multiple Elements', function() {
setup(function() {
// add 2 more items
for (var count = 1; count < 3; count++) {
this.observableArray.push({ count: count });
}
});
// use forEach instead of for to keep 'count' in a closure
[1, 2].forEach(function(count) {
suite('item ' + count, function() {
setup(function() {
this.item = this.array[count];
});
test('kept order', function() {
assert.equal(this.item.count, count);
});
test('called template', function() {
assert.ok(this.template.calledWith(this.item));
});
test('template returned element is in correct position', function() {
var div = this.template.withArgs(this.item).returnValues[0];
assert.equal(this.container.children[count], div);
});
});
});
suite('Replace Element', function() {
setup(function() {
this.template.reset();
this.item = { replacement: true };
this.originalElements = [].slice.call(this.container.children);
this.observableArray.set(1, this.item);
});
test('called template with recycled element', function() {
assert.ok(
this.template.calledWith(this.item, this.originalElements[1])
);
});
test('only called template method once', function() {
assert.equal(this.template.callCount, 1);
});
test('replaced element', function() {
var div = this.template.withArgs(this.item).returnValues[0];
assert.equal(this.container.children[1], div);
});
test('leaves other elements alone', function() {
assert.equal(this.container.children[0], this.originalElements[0]);
assert.equal(this.container.children[2], this.originalElements[2]);
});
});
suite('Remove Element', function() {
setup(function() {
this.template.reset();
this.originalElements = [].slice.call(this.container.children);
this.observableArray.pop();
});
test('removed last element', function() {
assert.equal(this.originalElements[2].parentNode, null);
});
test('leaves other elements alone', function() {
assert.equal(this.container.children[0], this.originalElements[0]);
assert.equal(this.container.children[1], this.originalElements[1]);
});
});
suite('Insert Element', function() {
setup(function() {
this.originalElements = [].slice.call(this.container.children);
this.item = {};
this.template.reset();
this.observableArray.splice(0, 0, this.item);
});
test('only called template method once', function() {
assert.equal(this.template.callCount, 1);
});
test('inserted element', function() {
var div = this.template.withArgs(this.item).returnValues[0];
assert.equal(this.container.children[0], div);
});
test('shifts other elements', function() {
assert.equal(this.container.children[1], this.originalElements[0]);
assert.equal(this.container.children[2], this.originalElements[1]);
assert.equal(this.container.children[3], this.originalElements[2]);
});
});
});
});
suite('observable hooks', function() {
setup(function() {
this.observable = this.Observable({ test: 1 });
this.observableArray = this.ObservableArray([this.observable]);
this.hook = this.sinon.spy();
this.template = (function(data, recycle, helper) {
helper.observeAndCall(data, {
test: this.hook
});
return recycle || document.createElement('li');
}).bind(this);
this.container = document.createElement('ul');
this.sinon.spy(this, 'template');
this.listView = this.ListView(
this.container, this.observableArray, this.template
);
});
test('hook called at creation', function() {
assert.equal(this.hook.callCount, 1);
});
test('hook called when observable changes', function() {
this.hook.reset();
this.observable.test = false;
assert.equal(this.hook.callCount, 1);
});
test('unobserves when removing', function() {
this.sinon.spy(this.observable, 'unobserve');
this.observableArray.splice(0, 1);
assert.ok(this.observable.unobserve.calledWith(this.hook));
});
test('unobserves when replacing', function() {
this.sinon.spy(this.observable, 'unobserve');
this.sinon.spy(this.observable, 'observe');
this.observableArray.set(0, this.observable);
// should unobserve
assert.ok(this.observable.unobserve.calledWith(this.hook));
// and also observe
assert.ok(this.observable.observe.calledWith('test', this.hook));
// but should unobserve first
assert.ok(
this.observable.unobserve.calledBefore(this.observable.observe)
);
});
});
suite('destroy', function() {
setup(function() {
this.observableArray = this.ObservableArray([1, 2, 3]);
this.container = document.createElement('div');
this.listView = this.ListView(
this.container, this.observableArray, function() {
return document.createElement('div');
}
);
});
test('removes children', function() {
this.listView.destroy();
assert.equal(this.container.children.length, 0);
});
test('sanity check', function() {
assert.equal(this.observableArray.unobserve.callCount, 0);
assert.equal(this.container.children.length, 3);
});
test('.element', function() {
this.listView.destroy();
assert.equal(this.listView.element, null);
});
test('unbinds from observeable', function() {
this.listView.destroy();
// called unobserve with a method
assert.equal(this.observableArray.unobserve.callCount, 1);
var arg = this.observableArray.unobserve.args[0][0];
assert.isFunction(arg);
});
test('creating another ListView destroys old', function() {
this.observableArray.observe.reset();
this.sinon.spy(this.listView, 'destroy');
// create a new one, we don't care about what happens to it.
this.ListView(
this.container, this.observableArray, function() {
return document.createElement('div');
}
);
// destroys old view
assert.ok(this.listView.destroy.called);
// unbinds from observable
assert.ok(this.observableArray.unobserve.called);
// re-observes
assert.ok(this.observableArray.observe.called);
// and make sure its done in the right order
assert.ok(
this.observableArray.unobserve.calledBefore(
this.observableArray.observe
)
);
});
});
});

View file

@ -0,0 +1,275 @@
'use strict';
suite('ObservableArray', function() {
var methods = [
'forEach', 'push', 'pop', 'splice', 'set', 'get', 'reset', 'observe'
];
suiteSetup(function(done) {
testRequire(['modules/mvvm/observable_array'], (function(ObservableArray) {
this.ObservableArray = ObservableArray;
done();
}).bind(this));
});
setup(function() {
this.array = methods.slice();
this.observable = this.ObservableArray(this.array);
});
suite('shape: ObservableArray(array)', function() {
methods.forEach(function(method) {
test('.' + method + ' is a function', function() {
assert.equal(typeof this.observable[method], 'function');
});
});
test('.length is a number', function() {
assert.equal(typeof this.observable.length, 'number');
});
test('.array is the original array', function() {
assert.equal(this.observable.array, this.array);
});
});
test('get()', function() {
assert.ok(this.array.length);
for (var index = 0; index < this.array.length; index++) {
assert.equal(this.observable.get(index), this.array[index],
'index: ' + index);
}
});
suite('observable actions', function() {
var events = ['insert', 'remove', 'replace', 'reset'];
function checkSpies(expected) {
events.forEach(function(event) {
var expectedCount = expected[event] || 0;
test(event + ' called ' + expectedCount + ' times', function() {
assert.equal(this.spies[event].args.length, expectedCount);
});
});
}
function checkArgs(event) {
var args = [].slice.call(arguments, 1);
test(event + ' called with correct arguments', function() {
var calls = this.spies[event].args;
assert.deepEqual(calls[calls.length - 1], args);
});
}
function resetSpies(environment) {
events.forEach(function(event) {
this.spies[event].reset();
}, environment);
}
setup(function() {
this.spies = {};
events.forEach(function(event) {
this.spies[event] = this.sinon.spy();
this.observable.observe(event, this.spies[event]);
}, this);
});
suite('pop()', function() {
setup(function() {
this.observable.pop();
});
checkSpies({ remove: 1 });
checkArgs('remove', {
type: 'remove',
data: {
index: methods.length - 1,
count: 1
}
});
test('modifies array', function() {
var expected = methods.slice();
expected.pop();
assert.deepEqual(this.array, expected);
});
});
suite('push()', function() {
setup(function() {
this.observable.push('test');
});
checkSpies({ insert: 1 });
checkArgs('insert', {
type: 'insert',
data: {
index: methods.length,
count: 1,
items: ['test']
}
});
test('modifies array', function() {
var expected = methods.slice();
expected.push('test');
assert.deepEqual(this.array, expected);
});
});
suite('splice(1, 2)', function() {
setup(function() {
this.observable.splice(1, 2);
});
checkSpies({ remove: 1 });
checkArgs('remove', {
type: 'remove',
data: {
index: 1,
count: 2
}
});
test('modifies array', function() {
var expect = methods.slice();
expect.splice(1, 2);
assert.deepEqual(this.array, expect);
});
});
suite('splice(1, 2, 3, 4)', function() {
setup(function() {
this.observable.splice(1, 2, 3, 4);
});
checkSpies({ remove: 1, insert: 1 });
checkArgs('remove', {
type: 'remove',
data: {
index: 1,
count: 2
}
});
checkArgs('insert', {
type: 'insert',
data: {
index: 1,
count: 2,
items: [3, 4]
}
});
test('modifies array', function() {
var expected = methods.slice();
expected.splice(1, 2, 3, 4);
assert.deepEqual(this.array, expected);
});
});
suite('splice(2, 0, 2)', function() {
setup(function() {
this.observable.splice(2, 0, 2);
});
checkSpies({ insert: 1 });
checkArgs('insert', {
type: 'insert',
data: {
index: 2,
count: 1,
items: [2]
}
});
test('modifies array', function() {
var expected = methods.slice();
expected.splice(2, 0, 2);
assert.deepEqual(this.array, expected);
});
});
suite('set(7, true)', function() {
setup(function() {
this.observable.set(7, true);
});
checkSpies({ replace: 1 });
checkArgs('replace', {
type: 'replace',
data: {
index: 7,
oldValue: methods[7],
newValue: true
}
});
test('modifies array', function() {
var expected = methods.slice();
expected[7] = true;
assert.deepEqual(this.array, expected);
});
});
suite('reset([])', function() {
var testArray = [true, false];
setup(function() {
this.observable.reset(testArray);
});
checkSpies({ reset: 1 });
checkArgs('reset', {
type: 'reset',
data: {
items: testArray
}
});
test('changes array', function() {
assert.equal(this.observable.array, testArray);
});
});
suite('pop() on empty', function() {
setup(function() {
this.observable.reset([]);
resetSpies(this);
this.observable.pop();
});
// no events expected
checkSpies({});
});
suite('unobserve', function() {
setup(function() {
// bind some listeners
this.spies = [this.sinon.spy(), this.sinon.spy()];
events.forEach(function(event) {
this.observable.observe(event, this.spies[0]);
this.observable.observe(event, this.spies[1]);
}, this);
});
test('(property, handler)', function() {
// remove handler for property
this.observable.unobserve('insert', this.spies[0]);
// insert, only other handler should be called
this.observable.push({});
assert.equal(this.spies[0].callCount, 0);
assert.equal(this.spies[1].callCount, 1);
// remove, both handlers should be called
this.observable.pop();
assert.equal(this.spies[0].callCount, 1);
assert.equal(this.spies[1].callCount, 2);
});
test('(handler)', function() {
// remove handler for all properties
this.observable.unobserve(this.spies[0]);
// insert, only other handler should be called
this.observable.push({});
assert.equal(this.spies[0].callCount, 0);
assert.equal(this.spies[1].callCount, 1);
// remove, only other handler should be called
this.observable.pop();
assert.equal(this.spies[0].callCount, 0);
assert.equal(this.spies[1].callCount, 2);
});
test('(property)', function() {
// remove handler for all properties
this.observable.unobserve('insert');
// insert, neither handler should be called
this.observable.push({});
assert.equal(this.spies[0].callCount, 0);
assert.equal(this.spies[1].callCount, 0);
// remove, both handlers should be called
this.observable.pop({});
assert.equal(this.spies[0].callCount, 1);
assert.equal(this.spies[1].callCount, 1);
});
});
});
});

View file

@ -0,0 +1,110 @@
'use strict';
suite('Observable', function() {
suiteSetup(function(done) {
testRequire(['modules/mvvm/observable'], (function(Observable) {
this.Observable = Observable;
done();
}).bind(this));
});
test('shape: Observable({}).observe is a function', function() {
var observable = this.Observable({});
assert.equal(typeof observable.observe, 'function');
});
test('shape: Observable({}).unobserve is a function', function() {
var observable = this.Observable({});
assert.equal(typeof observable.observe, 'function');
});
suite('Creates observable properties', function() {
var template = {
testBoolean: true,
testNumber: 10,
testString: 'test',
'test object': {}
// currently disabled - we don't handle functions
// testFunction: function() {}
};
var props = Object.keys(template);
setup(function() {
this.observable = this.Observable(template);
});
// some tests for each property
props.forEach(function(prop) {
test('prop: "' + prop + '" copied value', function() {
assert.equal(this.observable[prop], template[prop]);
});
// test some stuff for observing
suite('prop: "' + prop + '" observe', function() {
setup(function() {
this.spy = this.sinon.spy();
this.observable.observe(prop, this.spy);
});
test('does not generate any callbacks when attaching', function() {
assert.isFalse(this.spy.called);
});
test('does not notify when set to same value', function() {
this.observable[prop] = template[prop];
assert.isFalse(this.spy.called);
});
test('notifies when set to new value', function() {
this.newValue = {};
this.observable[prop] = this.newValue;
assert.isTrue(this.spy.calledWith(this.newValue, template[prop]));
});
});
});
suite('unobserve', function() {
setup(function() {
// bind some listeners
this.spies = [this.sinon.spy(), this.sinon.spy()];
props.forEach(function(prop) {
this.observable.observe(prop, this.spies[0]);
this.observable.observe(prop, this.spies[1]);
}, this);
});
test('(property, handler)', function() {
// remove handler for property
this.observable.unobserve('testNumber', this.spies[0]);
// change property - only other handler should be called
this.observable.testNumber = {};
assert.equal(this.spies[0].callCount, 0);
assert.equal(this.spies[1].callCount, 1);
// change a different property, both handlers should be called
this.observable.testString = {};
assert.equal(this.spies[0].callCount, 1);
assert.equal(this.spies[1].callCount, 2);
});
test('(handler)', function() {
// remove handler for all properties
this.observable.unobserve(this.spies[0]);
// change property - only other handler should be called
this.observable.testNumber = {};
assert.equal(this.spies[0].callCount, 0);
assert.equal(this.spies[1].callCount, 1);
// change a different property, only other handler should be called
this.observable.testString = {};
assert.equal(this.spies[0].callCount, 0);
assert.equal(this.spies[1].callCount, 2);
});
test('(property)', function() {
// remove handler for all properties
this.observable.unobserve('testNumber');
// change property - neither handler should be called
this.observable.testNumber = {};
assert.equal(this.spies[0].callCount, 0);
assert.equal(this.spies[1].callCount, 0);
// change a different property, both handlers should be called
this.observable.testString = {};
assert.equal(this.spies[0].callCount, 1);
assert.equal(this.spies[1].callCount, 1);
});
});
});
});

View file

@ -0,0 +1,623 @@
/* global MockL10n, MockNavigatormozSetMessageHandler, MockNavigatorSettings,
MockNavigatormozApps, MockMozBluetooth, MockNotificationHelper */
'use strict';
require('/shared/test/unit/mocks/mocks_helper.js');
require('/shared/test/unit/mocks/mock_l10n.js');
require('/shared/test/unit/mocks/mock_navigator_moz_settings.js');
require('/shared/test/unit/mocks/mock_navigator_moz_set_message_handler.js');
require('/shared/test/unit/mocks/mock_navigator_moz_apps.js');
require('/shared/test/unit/mocks/mock_navigator_moz_bluetooth.js');
require('/shared/test/unit/mocks/mock_notification_helper.js');
function switchReadOnlyProperty(originObject, propName, targetObj) {
Object.defineProperty(originObject, propName, {
configurable: true,
get: function() { return targetObj; }
});
}
suite('Bluetooth app > PairManager ', function() {
var realL10n;
var realNotificationHelper;
var realSetMessageHandler;
var realMozSettings;
var realMozApps;
var realMozBluetooth;
var AdapterManager;
var BtContext;
var PairManager;
var PairExpiredDialog;
var Pairview;
suiteSetup(function(done) {
realL10n = window.navigator.mozL10n;
navigator.mozL10n = MockL10n;
realNotificationHelper = window.NotificationHelper;
window.NotificationHelper = MockNotificationHelper;
realSetMessageHandler = navigator.mozSetMessageHandler;
navigator.mozSetMessageHandler = MockNavigatormozSetMessageHandler;
realMozSettings = navigator.mozSettings;
navigator.mozSettings = MockNavigatorSettings;
realMozApps = navigator.mozApps;
navigator.mozApps = MockNavigatormozApps;
realMozBluetooth = navigator.mozBluetooth;
switchReadOnlyProperty(navigator, 'mozBluetooth', MockMozBluetooth);
MockNavigatormozSetMessageHandler.mSetup();
var modules = [
'unit/mock_pair_expired_dialog',
'unit/mock_pair_view',
'modules/bluetooth/bluetooth_adapter_manager',
'modules/bluetooth/bluetooth_context',
'modules/pair_manager'
];
var maps = {
'*': {
'views/pair_expired_dialog': 'unit/mock_pair_expired_dialog',
'modules/bluetooth/bluetooth_adapter_manager': 'MockAdapterManager',
'modules/bluetooth/bluetooth_context': 'MockBluetoothContext'
}
};
this.MockAdapterManager = {
defaultAdapter: {},
observe: function() {}
};
define('MockAdapterManager', function() {
return this.MockAdapterManager;
}.bind(this));
this.MockBluetoothContext = {
callbacks: {
'enabled': []
},
observe: function(eventName, callback) {
this.callbacks[eventName].push(callback);
}
};
define('MockBluetoothContext', function() {
return this.MockBluetoothContext;
}.bind(this));
var requireCtx = testRequire([], maps, function() {});
requireCtx(modules, function(pairExpiredDialog, pairview, adapterManager,
btContext, pairManager) {
PairExpiredDialog = pairExpiredDialog;
Pairview = pairview;
AdapterManager = adapterManager;
BtContext = btContext;
PairManager = pairManager;
done();
}.bind(this));
});
suiteTeardown(function() {
MockNavigatormozApps.mTeardown();
switchReadOnlyProperty(navigator, 'mozBluetooth', realMozBluetooth);
navigator.mozApps = realMozApps;
MockNavigatormozSetMessageHandler.mTeardown();
navigator.mozSettings = realMozSettings;
MockNavigatorSettings.mTeardown();
navigator.mozSetMessageHandler = realSetMessageHandler;
navigator.mozL10n = realL10n;
window.NotificationHelper = realNotificationHelper;
});
suite('init > ', function() {
setup(function() {
this.sinon.stub(AdapterManager, 'observe');
this.sinon.spy(BtContext, 'observe');
this.sinon.stub(PairManager, '_onDefaultAdapterChanged');
this.sinon.stub(PairManager, '_watchOndisplaypasskeyreq');
this.sinon.stub(PairManager, '_watchOnenterpincodereq');
this.sinon.stub(PairManager, '_watchOnpairingconfirmationreq');
this.sinon.stub(PairManager, '_watchOnpairingconsentreq');
this.sinon.stub(PairManager, '_watchOnpairingaborted');
this.sinon.stub(PairManager, '_onRequestPairingFromSystemMessage');
this.sinon.stub(PairManager, 'showPendingPairing');
this.sinon.stub(PairManager, 'onBluetoothDisabled');
PairManager.init();
});
teardown(function() {
MockNavigatorSettings.mTeardown();
});
suite('observe "defaultAdapter" from AdapterManager > ', function() {
test('AdapterManager "defaultAdapter" property should be observed, ' +
'and access defaultAdapter from AdapterManager manually ',
function() {
assert.isTrue(AdapterManager.observe.calledWith('defaultAdapter'));
assert.isTrue(PairManager._onDefaultAdapterChanged.calledWith(
AdapterManager.defaultAdapter));
});
});
suite('should watch pairing request events from dom event > ', function() {
test('"_watchOndisplaypasskeyreq", "_watchOnenterpincodereq", ' +
'"_watchOnpairingconfirmationreq", "_watchOnpairingconsentreq", ' +
'"_watchOnpairingaborted" should be called ', function() {
assert.isTrue(PairManager._watchOndisplaypasskeyreq.called);
assert.isTrue(PairManager._watchOnenterpincodereq.called);
assert.isTrue(PairManager._watchOnpairingconfirmationreq.called);
assert.isTrue(PairManager._watchOnpairingconsentreq.called);
assert.isTrue(PairManager._watchOnpairingaborted.called);
});
});
suite('fire "bluetooth-pairing-request" event > ', function() {
var message, eventName;
setup(function() {
message = {};
eventName = 'bluetooth-pairing-request';
MockNavigatormozSetMessageHandler.mTrigger(eventName, message);
});
test('handle event with _onRequestPairingFromSystemMessage()',
function() {
assert.isTrue(
PairManager._onRequestPairingFromSystemMessage.calledWith(message),
'the "_onRequestPairingFromSystemMessage" should be called with ' +
'"message" object after received "bluetooth-pairing-request" event ');
});
});
suite('screen locked status change > ', function() {
var lockscreenKey = 'lockscreen.locked';
setup(function() {
MockNavigatorSettings.mTriggerObservers(lockscreenKey,
{settingValue: false});
});
test('observes settings key "lockscreen.locked"', function() {
assert.equal(MockNavigatorSettings.mObservers[lockscreenKey].length, 1);
});
test('showPendingPairing() should be called with arg event while ' +
'lockscreen.locked settings key value changed ', function() {
assert.isTrue(PairManager.showPendingPairing.calledWith(false));
});
});
suite('observe "enabled" from BtContext > ', function() {
var property;
setup(function() {
property = 'enabled';
});
test('BtContext "enabled" property should be observed, ' +
'onBluetoothDisabled() should be called ' +
'while BtContext "enabled" property changed to false ', function() {
assert.isTrue(BtContext.observe.calledWith(property));
BtContext.callbacks[property][0](false);
assert.isTrue(PairManager.onBluetoothDisabled.called);
});
test('onBluetoothDisabled() should not be called ' +
'while BtContext "enabled" property changed to true ', function() {
BtContext.callbacks[property][0](true);
assert.isFalse(PairManager.onBluetoothDisabled.called);
});
});
});
suite('_onDefaultAdapterChanged > ', function() {
var mockNewAdapter;
setup(function() {
mockNewAdapter = {};
PairManager._defaultAdapter = null;
});
test('_defaultAdapter should be defined ', function() {
PairManager._onDefaultAdapterChanged(mockNewAdapter);
assert.equal(PairManager._defaultAdapter, mockNewAdapter);
});
});
suite('_watchOndisplaypasskeyreq > ', function() {
var mockDefaultAdapter, mockEvent, mockEventName, expectedPairingInfo;
setup(function() {
mockDefaultAdapter = {
pairingReqs: {
addEventListener: function() {}
}
};
PairManager._defaultAdapter = mockDefaultAdapter;
this.sinon.stub(PairManager._defaultAdapter.pairingReqs,
'addEventListener');
this.sinon.stub(PairManager, '_handlePairingRequest');
mockEvent = {};
mockEventName = 'displaypasskey';
expectedPairingInfo = {
method: mockEventName,
evt: mockEvent
};
});
test('pairingReqs.ondisplaypasskeyreq should be registered callback',
function() {
PairManager._watchOndisplaypasskeyreq();
// addEventListener
assert.equal(
PairManager._defaultAdapter.pairingReqs.addEventListener.args[0][0],
'displaypasskeyreq');
PairManager._defaultAdapter.pairingReqs.addEventListener.args[0][1](
mockEvent, mockEventName);
// _handlePairingRequest
assert.deepEqual(PairManager._handlePairingRequest.args[0][0],
expectedPairingInfo);
});
});
suite('_watchOnpairingaborted > ', function() {
var mockDefaultAdapter, mockEvent;
setup(function() {
mockDefaultAdapter = {
addEventListener: function() {}
};
PairManager._defaultAdapter = mockDefaultAdapter;
this.sinon.stub(PairManager._defaultAdapter, 'addEventListener');
this.sinon.stub(PairManager, '_onPairingAborted');
mockEvent = {};
});
test('_defaultAdapter.onpairingaborted should be registered callback ' +
'function ', function() {
PairManager._watchOnpairingaborted();
assert.equal(PairManager._defaultAdapter.addEventListener.args[0][0],
'pairingaborted');
PairManager._defaultAdapter.addEventListener.args[0][1](mockEvent);
assert.isTrue(PairManager._onPairingAborted.calledWith(mockEvent));
});
});
suite('_onPairingAborted > ', function() {
var mockEvent;
setup(function() {
this.sinon.stub(Pairview, 'closeInput');
PairManager.pendingPairing = {};
PairManager.childWindow = {
Pairview: Pairview,
close: this.sinon.spy()
};
switchReadOnlyProperty(PairExpiredDialog, 'isVisible', true);
this.sinon.stub(PairExpiredDialog, 'close');
this.sinon.stub(window, 'close');
});
test('Pairview.closeInput() should be called, childWindow should be close' +
'window.close() should be called, pendingPairing should be null, ' +
'PairExpiredDialog.close() should be called ', function() {
PairManager._onPairingAborted(mockEvent);
assert.isTrue(PairManager.childWindow.Pairview.closeInput.called);
assert.isTrue(PairManager.childWindow.close.called);
assert.isNull(PairManager.pendingPairing);
assert.isTrue(PairExpiredDialog.close.called);
assert.isTrue(window.close.called);
});
});
suite('_handlePairingRequest > ', function() {
var pairingInfo = {
method: 'confirmation',
evt: {}
};
suite('screen locked mode > ', function() {
setup(function(done) {
this.sinon.stub(PairManager, 'fireNotification');
this.sinon.stub(PairManager, 'cleanPendingPairing');
this.sinon.stub(PairManager, 'showPairview');
MockNavigatorSettings.mSettings['lockscreen.locked'] = true;
PairManager._handlePairingRequest(pairingInfo);
setTimeout(done);
});
test('handle showPairview() from _handlePairingRequest() ', function() {
assert.isTrue(PairManager.fireNotification.calledWith(pairingInfo),
'fireNotification() should be called after do _handlePairingRequest()' +
' in screen lock mode');
assert.isFalse(PairManager.cleanPendingPairing.called,
'cleanPendingPairing() should not be called after do ' +
'_handlePairingRequest() in screen lock mode');
assert.isFalse(PairManager.showPairview.called,
'showPairview() should not be called after do _handlePairingRequest()' +
' in screen lock mode');
});
});
suite('screen unlocked mode > ', function() {
setup(function(done) {
this.sinon.stub(PairManager, 'fireNotification');
this.sinon.stub(PairManager, 'cleanPendingPairing');
this.sinon.stub(PairManager, 'showPairview');
MockNavigatorSettings.mSettings['lockscreen.locked'] = false;
PairManager._handlePairingRequest(pairingInfo);
setTimeout(done);
});
test('handle showPairview() from _handlePairingRequest() ', function() {
assert.isFalse(PairManager.fireNotification.called,
'fireNotification() should not be called after ' +
'do _handlePairingRequest() in screen lock mode');
assert.isTrue(PairManager.cleanPendingPairing.called,
'cleanPendingPairing() should not be called after do ' +
'_handlePairingRequest() in screen lock mode');
assert.isTrue(PairManager.showPairview.calledWith(pairingInfo),
'showPairview() should be called after do onRequestPairing() ' +
'in screen unlock mode');
});
});
});
suite('fireNotification > ', function() {
var notificationStub, mockNotification, pairingInfo, titleResult,
optionsResult;
setup(function() {
this.sinon.stub(PairManager, 'showPairview');
this.sinon.stub(PairManager, 'pairingRequestExpiredNotificationHandler');
mockNotification = {};
notificationStub =
this.sinon.stub(window.NotificationHelper, 'send').returns(
Promise.resolve(mockNotification));
PairManager.pendingPairing = null;
pairingInfo = {
evt: {
deviceName: 'device-01'
}
};
titleResult = 'bluetooth-pairing-request-now-title';
optionsResult = {
bodyL10n: { raw: pairingInfo.evt.deviceName },
icon: 'app://bluetooth.gaiamobile.org/style/images/icon_bluetooth.png',
tag: 'pairing-request',
closeOnClick: false
};
});
test('pendingPairing, new and fired one notification, set notification ' +
'onclick handler ', function(done) {
PairManager.fireNotification(pairingInfo).then(() => {
assert.isDefined(PairManager.pendingPairing.showPairviewCallback);
PairManager.pendingPairing.showPairviewCallback();
assert.isTrue(PairManager.showPairview.calledWith(pairingInfo));
assert.isTrue(notificationStub.calledOnce);
assert.equal(notificationStub.firstCall.args[0], titleResult);
assert.deepEqual(notificationStub.firstCall.args[1], optionsResult);
assert.isDefined(mockNotification.onclick);
mockNotification.onclick();
assert.isTrue(
PairManager.pairingRequestExpiredNotificationHandler.called);
done();
});
});
});
suite('pairingRequestExpiredNotificationHandler > ', function() {
var mockNotification;
suite('screen locked mode > ', function() {
setup(function(done) {
mockNotification = {
close: function() {/* do something */}
};
this.sinon.spy(mockNotification, 'close');
this.sinon.stub(PairExpiredDialog, 'showConfirm');
MockNavigatorSettings.mSettings['lockscreen.locked'] = true;
PairManager.pairingRequestExpiredNotificationHandler(mockNotification);
setTimeout(done);
});
test('notification should not be close, pair expired dialog should not ' +
'be show with confirmation', function() {
assert.isFalse(mockNotification.close.called);
assert.isFalse(PairExpiredDialog.showConfirm.called);
});
});
suite('screen unlocked mode > ', function() {
setup(function(done) {
mockNotification = {
close: function() {/* do something */}
};
this.sinon.spy(mockNotification, 'close');
switchReadOnlyProperty(PairExpiredDialog, 'isVisible', false);
this.sinon.stub(PairExpiredDialog, 'showConfirm');
MockNavigatorSettings.mSettings['lockscreen.locked'] = false;
PairManager.pairingRequestExpiredNotificationHandler(mockNotification);
setTimeout(done);
});
teardown(function() {
mockNotification = null;
});
test('notification should be close, confirm dialog should not be ' +
'called if pair request not expired yet', function() {
PairManager._isExpired = false;
assert.isTrue(mockNotification.close.called);
MockNavigatormozApps.mTriggerLastRequestSuccess();
assert.isFalse(PairExpiredDialog.showConfirm.called);
assert.isFalse(MockNavigatormozApps.mAppWasLaunched);
});
test('notification should be close, confirm dialog should be called ' +
'if it is not visible', function() {
PairManager._isExpired = true;
assert.isTrue(mockNotification.close.called);
MockNavigatormozApps.mTriggerLastRequestSuccess();
assert.isTrue(PairExpiredDialog.showConfirm.called);
assert.isFalse(MockNavigatormozApps.mAppWasLaunched);
});
test('notification should be close, confirm dialog should not be called' +
' if it is visible', function() {
switchReadOnlyProperty(PairExpiredDialog, 'isVisible', true);
assert.isTrue(mockNotification.close.called);
MockNavigatormozApps.mTriggerLastRequestSuccess();
assert.isFalse(PairExpiredDialog.showConfirm.called);
assert.ok(MockNavigatormozApps.mAppWasLaunched);
});
});
});
suite('showPendingPairing > ', function() {
var cleanPendingPairingStub;
suite('screen unlocked mode > with pending pairing', function() {
setup(function() {
cleanPendingPairingStub =
sinon.stub(PairManager, 'cleanPendingPairing');
PairManager.pendingPairing.showPairviewCallback = this.sinon.spy();
});
test('showPairviewCallback() should be called, cleanPendingPairing() ' +
'should be called', function() {
PairManager.showPendingPairing(false);
assert.isTrue(PairManager.pendingPairing.showPairviewCallback.called);
assert.isTrue(cleanPendingPairingStub.called);
cleanPendingPairingStub.restore();
});
});
suite('screen unlocked mode > without pending pairing', function() {
setup(function() {
cleanPendingPairingStub =
sinon.stub(PairManager, 'cleanPendingPairing');
PairManager.pendingPairing = null;
});
test('showPairviewCallback() should not be called, ' +
'cleanPendingPairing() should not be called', function() {
PairManager.showPendingPairing(false);
assert.isFalse(cleanPendingPairingStub.called);
cleanPendingPairingStub.restore();
});
});
suite('screen locked mode > with pending pairing', function() {
setup(function() {
cleanPendingPairingStub =
sinon.stub(PairManager, 'cleanPendingPairing');
PairManager.pendingPairing = {
showPairviewCallback: this.sinon.spy()
};
});
test('showPairviewCallback() should not be called, ' +
'cleanPendingPairing() should not be called', function() {
PairManager.showPendingPairing(true);
assert.isFalse(PairManager.pendingPairing.showPairviewCallback.called);
assert.isFalse(cleanPendingPairingStub.called);
cleanPendingPairingStub.restore();
});
});
});
suite('cleanPendingPairing > ', function() {
setup(function() {
this.sinon.stub(PairManager, 'cleanNotifications');
PairManager.pendingPairing = {};
});
test('pendingPairing should be null, cleanNotifications() should be ' +
'called', function() {
PairManager.cleanPendingPairing();
assert.equal(PairManager.pendingPairing, null);
assert.isTrue(PairManager.cleanNotifications.called);
});
});
suite('cleanNotifications > ', function() {
var realNotificationGet, mockNotificationGet, mockNotis;
setup(function() {
mockNotis = [{
tag: 'pairing-request',
close: this.sinon.spy()
}, {
tag: 'email-msg',
close: this.sinon.spy()
}];
mockNotificationGet = function() {
return {
then: function(callback) {
callback(mockNotis);
}
};
};
realNotificationGet = window.Notification.get;
window.Notification.get = mockNotificationGet;
});
teardown(function() {
window.Notification.get = realNotificationGet;
mockNotificationGet = null;
mockNotis = null;
});
test('notification with tag "pairing-request" should be closed',
function() {
PairManager.cleanNotifications();
assert.isTrue(mockNotis[0].close.called);
assert.isFalse(mockNotis[1].close.called);
});
});
suite('showPairview > ', function() {
var openStub, pairviewInitSpy, pairingInfo;
suiteSetup(function() {
pairviewInitSpy = sinon.spy(Pairview, 'init');
openStub = sinon.stub(window, 'open', function() {
return {Pairview: Pairview};
});
pairingInfo = {
method: 'confirmation',
evt: {}
};
PairManager.showPairview(pairingInfo);
});
suiteTeardown(function() {
pairviewInitSpy.restore();
openStub.restore();
});
test('should init Pairview after page onloaded ', function() {
PairManager.childWindow.onload();
assert.isTrue(pairviewInitSpy.calledWith(pairingInfo.method,
pairingInfo.evt));
});
});
suite('onBluetoothDisabled > ', function() {
setup(function() {
this.sinon.stub(Pairview, 'closeInput');
PairManager.childWindow = {
Pairview: Pairview,
close: this.sinon.spy()
};
this.sinon.stub(window, 'close');
});
test('Pairview.closeInput() should be called, childWindow should be close' +
'window.close() should be called', function() {
PairManager.onBluetoothDisabled();
assert.isTrue(PairManager.childWindow.Pairview.closeInput.called);
assert.isTrue(PairManager.childWindow.close.called);
assert.isTrue(window.close.called);
});
});
});

View file

@ -0,0 +1,701 @@
/* global MockNavigatormozSetMessageHandler, MockNavigatormozApps */
'use strict';
require('/shared/test/unit/mocks/mocks_helper.js');
require('/shared/test/unit/mocks/mock_navigator_moz_set_message_handler.js');
require('/shared/test/unit/mocks/mock_navigator_moz_apps.js');
function switchReadOnlyProperty(originObject, propName, targetObj) {
Object.defineProperty(originObject, propName, {
configurable: true,
get: function() { return targetObj; }
});
}
suite('Bluetooth app > PairManager ', function() {
var realSetMessageHandler;
var realMozApps;
var realGetDeviceStorage;
var AdapterManager;
var BtContext;
var CannotTransferDialog;
var DevicePickerPanel;
var TurnBluetoothOnDialog;
var TransferManager;
suiteSetup(function(done) {
realSetMessageHandler = navigator.mozSetMessageHandler;
navigator.mozSetMessageHandler = MockNavigatormozSetMessageHandler;
realMozApps = navigator.mozApps;
navigator.mozApps = MockNavigatormozApps;
MockNavigatormozSetMessageHandler.mSetup();
var modules = [
'modules/bluetooth/bluetooth_adapter_manager',
'modules/bluetooth/bluetooth_context',
'views/cannot_transfer_dialog',
'views/device_picker_panel',
'views/turn_bluetooth_on_dialog',
'modules/transfer_manager'
];
var maps = {
'*': {
'modules/bluetooth/bluetooth_adapter_manager': 'MockAdapterManager',
'modules/bluetooth/bluetooth_context': 'MockBluetoothContext',
'views/cannot_transfer_dialog': 'unit/mock_cannot_transfer_dialog',
'views/device_picker_panel': 'MockDevicePickerPanel',
'views/turn_bluetooth_on_dialog': 'unit/mock_turn_bluetooth_on_dialog'
}
};
this.MockAdapterManager = {
defaultAdapter: {},
observe: function() {}
};
define('MockAdapterManager', function() {
return this.MockAdapterManager;
}.bind(this));
this.MockBluetoothContext = {
callbacks: {
'enabled': []
},
observe: function(eventName, callback) {
this.callbacks[eventName].push(callback);
},
setEnabled: function() {},
sendFile: function() {}
};
define('MockBluetoothContext', function() {
return this.MockBluetoothContext;
}.bind(this));
this.MockDevicePickerPanel = {
visible: false,
addEventListener: function() {}
};
define('MockDevicePickerPanel', function() {
return this.MockDevicePickerPanel;
}.bind(this));
var requireCtx = testRequire([], maps, function() {});
requireCtx(modules, function(adapterManager, btContext,
cannotTransferDialog, devicePickerPanel,
turnBluetoothOnDialog, transferManager) {
AdapterManager = adapterManager;
BtContext = btContext;
CannotTransferDialog = cannotTransferDialog;
DevicePickerPanel = devicePickerPanel;
TurnBluetoothOnDialog = turnBluetoothOnDialog;
TransferManager = transferManager;
done();
}.bind(this));
});
suiteTeardown(function() {
MockNavigatormozApps.mTeardown();
navigator.mozApps = realMozApps;
MockNavigatormozSetMessageHandler.mTeardown();
navigator.mozSetMessageHandler = realSetMessageHandler;
navigator.getDeviceStorage = realGetDeviceStorage;
});
suite('init > ', function() {
setup(function() {
this.sinon.stub(TransferManager, '_watchActivityRequest');
});
test('_watchActivityRequest() should be called ', function() {
TransferManager.init();
assert.isTrue(TransferManager._watchActivityRequest.called);
});
});
suite('_watchActivityRequest > ', function() {
setup(function() {
this.sinon.stub(navigator, 'mozSetMessageHandler');
this.sinon.stub(TransferManager, '_activityHandler');
});
test('navigator.mozSetMessageHandler() should be set with activity handler',
function() {
TransferManager._watchActivityRequest();
assert.equal(navigator.mozSetMessageHandler.args[0][0], 'activity');
navigator.mozSetMessageHandler.args[0][1]();
assert.isTrue(TransferManager._activityHandler.called);
});
});
suite('_activityHandler > ', function() {
var mockActivityRequest;
suite('accepted format to service sending files > ', function() {
setup(function() {
mockActivityRequest = {
source: {
name: 'share',
data: {
blobs: [{}, {}]
}
}
};
TransferManager._activity = null;
DevicePickerPanel.visible = false;
this.sinon.stub(TransferManager, '_observeDefaultAdapter');
this.sinon.stub(TransferManager, '_observeBluetoothEnabled');
this.sinon.stub(TransferManager, '_watchEventFromDevicePicker');
this.sinon.stub(TransferManager, '_cannotTransfer');
});
test('Should service the activity request, ' +
'adapter, Bluetooth state, device picker should be observed event ',
function() {
TransferManager._activityHandler(mockActivityRequest);
assert.deepEqual(TransferManager._activity, mockActivityRequest);
assert.isTrue(TransferManager._observeDefaultAdapter.called);
assert.isTrue(TransferManager._observeBluetoothEnabled.called);
assert.isTrue(TransferManager._watchEventFromDevicePicker.called);
assert.isTrue(DevicePickerPanel.visible);
assert.isFalse(TransferManager._cannotTransfer.called);
});
});
suite('unaccepted format to cancel sending files > ', function() {
setup(function() {
TransferManager._activity = null;
DevicePickerPanel.visible = false;
this.sinon.stub(TransferManager, '_observeDefaultAdapter');
this.sinon.stub(TransferManager, '_observeBluetoothEnabled');
this.sinon.stub(TransferManager, '_watchEventFromDevicePicker');
this.sinon.stub(TransferManager, '_cannotTransfer');
});
suite('no blobs in activity.source.data > ', function() {
setup(function() {
mockActivityRequest = {
source: {
name: 'share',
data: {}
}
};
});
test('Should not service the activity request, ' +
'_cannotTransfer() should be called ',
function() {
TransferManager._activityHandler(mockActivityRequest);
assert.isNull(TransferManager._activity);
assert.isFalse(TransferManager._observeDefaultAdapter.called);
assert.isFalse(TransferManager._observeBluetoothEnabled.called);
assert.isFalse(TransferManager._watchEventFromDevicePicker.called);
assert.isFalse(DevicePickerPanel.visible);
assert.isTrue(TransferManager._cannotTransfer.called);
});
});
suite('empty array in activity.source.data.blobs > ', function() {
setup(function() {
mockActivityRequest = {
source: {
name: 'share',
data: {
blobs: []
}
}
};
});
test('Should not service the activity request, ' +
'_cannotTransfer() should be called ',
function() {
TransferManager._activityHandler(mockActivityRequest);
assert.isNull(TransferManager._activity);
assert.isFalse(TransferManager._observeDefaultAdapter.called);
assert.isFalse(TransferManager._observeBluetoothEnabled.called);
assert.isFalse(TransferManager._watchEventFromDevicePicker.called);
assert.isFalse(DevicePickerPanel.visible);
assert.isTrue(TransferManager._cannotTransfer.called);
});
});
});
});
suite('_cannotTransfer > ', function() {
suite('CannotTransferDialog is not visible > ', function() {
var mockShowConfirmPromise;
setup(function() {
switchReadOnlyProperty(CannotTransferDialog, 'isVisible', false);
mockShowConfirmPromise = Promise.resolve();
this.sinon.stub(CannotTransferDialog, 'showConfirm').returns(
mockShowConfirmPromise);
this.sinon.stub(TransferManager, '_endTransferWithReason');
});
test('CannotTransferDialog should be shown, ' +
'_endTransferWithReason() should be called with "cancelled"' +
'after a user confirm the dialog ', function(done) {
TransferManager._cannotTransfer();
assert.isTrue(CannotTransferDialog.showConfirm.called);
CannotTransferDialog.showConfirm().then(() => {
assert.isTrue(
TransferManager._endTransferWithReason.calledWith('cancelled'));
}, () => {
// reject case
assert.isTrue(false);
}).then(done, done);
});
});
suite('CannotTransferDialog is visible > ', function() {
setup(function() {
switchReadOnlyProperty(CannotTransferDialog, 'isVisible', true);
this.sinon.stub(CannotTransferDialog, 'showConfirm');
this.sinon.stub(TransferManager, '_endTransferWithReason');
});
test('_endTransferWithReason() should not be called ', function() {
TransferManager._cannotTransfer();
assert.isFalse(CannotTransferDialog.showConfirm.called);
});
});
});
suite('_endTransferWithReason > ', function() {
var mockActivityRequest, mockReason;
suite('reason is "transferred" > ', function() {
setup(function() {
mockActivityRequest = {
postResult: function() {}
};
TransferManager._activity = mockActivityRequest;
this.sinon.stub(mockActivityRequest, 'postResult');
mockReason = 'transferred';
});
test('postResult() should be call with the reason ', function() {
TransferManager._endTransferWithReason(mockReason);
assert.isTrue(mockActivityRequest.postResult.calledWith(mockReason));
assert.isNull(TransferManager._activity);
});
});
suite('reason is "cancelled" > ', function() {
setup(function() {
mockActivityRequest = {
postError: function() {}
};
TransferManager._activity = mockActivityRequest;
this.sinon.stub(mockActivityRequest, 'postError');
mockReason = 'cancelled';
});
test('postError() should be call with the reason ', function() {
TransferManager._endTransferWithReason(mockReason);
assert.isTrue(mockActivityRequest.postError.calledWith(mockReason));
assert.isNull(TransferManager._activity);
});
});
});
suite('_observeDefaultAdapter > ', function() {
var mockAdapter;
setup(function() {
mockAdapter = {};
AdapterManager.defaultAdapter = mockAdapter;
this.sinon.stub(AdapterManager, 'observe');
this.sinon.stub(TransferManager, '_onDefaultAdapterChanged');
});
test('AdapterManager should be observed with "defaultAdapter", ' +
'_onDefaultAdapterChanged() should be called ', function() {
TransferManager._observeDefaultAdapter();
assert.equal(AdapterManager.observe.args[0][0], 'defaultAdapter');
assert.isDefined(AdapterManager.observe.args[0][1]);
assert.isTrue(TransferManager._onDefaultAdapterChanged.calledWith(
mockAdapter));
});
});
suite('_observeBluetoothEnabled > ', function() {
var mockEnabledState;
setup(function() {
mockEnabledState = true;
BtContext.enabled = mockEnabledState;
this.sinon.stub(BtContext, 'observe');
this.sinon.stub(TransferManager, '_onBluetoothEnabledChanged');
});
test('BtContext should be observed with "enabled"', function() {
TransferManager._observeBluetoothEnabled();
assert.equal(BtContext.observe.args[0][0], 'enabled');
assert.isDefined(BtContext.observe.args[0][1]);
});
});
suite('_onDefaultAdapterChanged > ', function() {
suite('adapter is changed from existing instance to null > ', function() {
var mockNewAdapter, mockOldAdapter;
setup(function() {
mockNewAdapter = null;
mockOldAdapter = {};
this.sinon.stub(TransferManager, '_endTransferWithReason');
});
test('_endTransferWithReason() should be called with reason "cancelled"',
function() {
TransferManager._onDefaultAdapterChanged(
mockNewAdapter, mockOldAdapter);
assert.isTrue(TransferManager._endTransferWithReason.calledWith(
'cancelled'));
});
});
suite('adapter is changed from null to existing instance > ', function() {
var mockNewAdapter, mockOldAdapter;
setup(function() {
mockNewAdapter = {};
mockOldAdapter = null;
this.sinon.stub(TransferManager, '_endTransferWithReason');
});
test('_endTransferWithReason() should not be called', function() {
TransferManager._onDefaultAdapterChanged(
mockNewAdapter, mockOldAdapter);
assert.isFalse(TransferManager._endTransferWithReason.called);
});
});
});
suite('_onBluetoothEnabledChanged > ', function() {
var mockEnabledState;
setup(function() {
this.sinon.stub(TurnBluetoothOnDialog, 'close');
});
test('not function when passed value is undefined ', function() {
TransferManager._onBluetoothEnabledChanged(undefined);
assert.isFalse(TurnBluetoothOnDialog.close.called);
});
suite('Bluetooth "enabled" state changed to be true > ', function() {
setup(function() {
mockEnabledState = true;
this.sinon.stub(TurnBluetoothOnDialog, 'showConfirm');
});
test('TurnBluetoothOnDialog should be closed ', function() {
TransferManager._onBluetoothEnabledChanged(mockEnabledState);
assert.isTrue(TurnBluetoothOnDialog.close.called);
assert.isFalse(TurnBluetoothOnDialog.showConfirm.called);
});
});
suite('Bluetooth "enabled" state changed to be false > ', function() {
var mockUserAction, mockShowConfirmPromise;
setup(function() {
this.sinon.stub(BtContext, 'setEnabled');
this.sinon.stub(TransferManager, '_endTransferWithReason');
});
suite('A user confirmed to turn Bluetooth on > ', function() {
setup(function() {
mockEnabledState = false;
mockUserAction = 'confirm';
mockShowConfirmPromise = Promise.resolve(mockUserAction);
this.sinon.stub(TurnBluetoothOnDialog, 'showConfirm').returns(
mockShowConfirmPromise);
});
test('TurnBluetoothOnDialog should be shown with confirm dialog, ' +
'BtContext should be enabled with true ', function(done) {
TransferManager._onBluetoothEnabledChanged(mockEnabledState);
assert.isFalse(TurnBluetoothOnDialog.close.called);
assert.isTrue(TurnBluetoothOnDialog.showConfirm.called);
mockShowConfirmPromise.then((userAction) => {
assert.equal(userAction, mockUserAction);
assert.isTrue(BtContext.setEnabled.calledWith(true));
assert.isFalse(TransferManager._endTransferWithReason.called);
}, () => {
// reject case
assert.isTrue(false);
}).then(done, done);
});
});
suite('A user cancelled to turn Bluetooth on > ', function() {
setup(function() {
mockEnabledState = false;
mockUserAction = 'cancel';
mockShowConfirmPromise = Promise.resolve(mockUserAction);
this.sinon.stub(TurnBluetoothOnDialog, 'showConfirm').returns(
mockShowConfirmPromise);
});
test('TurnBluetoothOnDialog should be shown with confirm dialog, ' +
'_endTransferWithReason should be called with reason "cancelled" ',
function(done) {
TransferManager._onBluetoothEnabledChanged(mockEnabledState);
assert.isFalse(TurnBluetoothOnDialog.close.called);
assert.isTrue(TurnBluetoothOnDialog.showConfirm.called);
mockShowConfirmPromise.then((userAction) => {
assert.equal(userAction, mockUserAction);
assert.isFalse(BtContext.setEnabled.called);
assert.isTrue(
TransferManager._endTransferWithReason.calledWith('cancelled'));
}, () => {
// reject case
assert.isTrue(false);
}).then(done, done);
});
});
});
});
suite('_watchEventFromDevicePicker > ', function() {
setup(function() {
this.sinon.stub(DevicePickerPanel, 'addEventListener');
});
test('Should add "devicePicked", "cancelSelection" events, ' +
'and regited _onDevicePicked(), _onCancelSelection() handlers ' +
'from DevicePickerPanel ', function() {
TransferManager._watchEventFromDevicePicker();
assert.equal(DevicePickerPanel.addEventListener.args[0][0],
'devicePicked');
assert.isDefined(DevicePickerPanel.addEventListener.args[0][1]);
assert.equal(DevicePickerPanel.addEventListener.args[1][0],
'cancelSelection');
assert.isDefined(DevicePickerPanel.addEventListener.args[1][1]);
});
});
suite('_onDevicePicked > ', function() {
var mockActivityRequest, mockEvent, mockSchedule;
setup(function() {
mockSchedule = {};
this.sinon.stub(TransferManager,
'_produceSendingFilesSchedule').returns(mockSchedule);
this.sinon.stub(TransferManager, '_postMessageToSystemApp');
this.sinon.stub(BtContext, 'sendFile');
this.sinon.stub(TransferManager, '_endTransferWithReason');
mockEvent = {
detail: {
address: 'AA:BB:CC:00:11:22'
}
};
});
suite('blob has name > ', function() {
setup(function() {
mockActivityRequest = {
source: {
name: 'share',
data: {
blobs: [{
name: 'file_01'
}, {
name: 'file_02'
}]
}
}
};
TransferManager._activity = mockActivityRequest;
});
test('_produceSendingFilesSchedule() should be called with length, ' +
'_postMessageToSystemApp() should be called with schedule, ' +
'BtContext.sendFile() should be called with: ' +
'address of target device and blob in soure.data, ' +
'_endTransferWithReason() should be called with transferred ',
function(done) {
TransferManager._onDevicePicked(mockEvent).then(() => {
assert.isTrue(
TransferManager._produceSendingFilesSchedule.calledWith(
mockActivityRequest.source.data.blobs.length));
assert.isTrue(
TransferManager._postMessageToSystemApp.calledWith(mockSchedule));
assert.equal(BtContext.sendFile.args[0][0], mockEvent.detail.address);
assert.deepEqual(BtContext.sendFile.args[0][1],
mockActivityRequest.source.data.blobs[0]);
assert.equal(BtContext.sendFile.args[1][0], mockEvent.detail.address);
assert.deepEqual(BtContext.sendFile.args[1][1],
mockActivityRequest.source.data.blobs[1]);
assert.isTrue(TransferManager._endTransferWithReason.calledWith(
'transferred'));
}, () => {
// reject case
assert.isTrue(false);
}).then(done, done);
});
});
suite('blob has no name, has filepaths > ', function() {
var mockGetPromise, mockGotFile;
setup(function() {
mockActivityRequest = {
source: {
name: 'share',
data: {
blobs: [{}, {}],
filepaths: [
'/sdcard/DCIM/100MZLLA/IMG_0001.jpg',
'/sdcard/DCIM/100MZLLA/IMG_0002.jpg'
]
}
}
};
TransferManager._activity = mockActivityRequest;
mockGotFile = {};
mockGetPromise = Promise.resolve(mockGotFile);
var MockGetDeviceStorage = function() {
return {
get: function() {return mockGetPromise;}
};
};
realGetDeviceStorage = navigator.getDeviceStorage;
navigator.getDeviceStorage = MockGetDeviceStorage;
});
teardown(function() {
navigator.getDeviceStorage = realGetDeviceStorage;
});
test('_produceSendingFilesSchedule() should be called with length, ' +
'_postMessageToSystemApp() should be called with schedule, ' +
'BtContext.sendFile() should be called with: ' +
'address of target device and file from device storage, ' +
'_endTransferWithReason() should be called with transferred ',
function(done) {
TransferManager._onDevicePicked(mockEvent).then(() => {
assert.isTrue(
TransferManager._produceSendingFilesSchedule.calledWith(
mockActivityRequest.source.data.blobs.length));
assert.isTrue(
TransferManager._postMessageToSystemApp.calledWith(mockSchedule));
assert.equal(BtContext.sendFile.args[0][0], mockEvent.detail.address);
assert.equal(BtContext.sendFile.args[0][1], mockGotFile);
assert.equal(BtContext.sendFile.args[1][0], mockEvent.detail.address);
assert.equal(BtContext.sendFile.args[1][1], mockGotFile);
assert.isTrue(TransferManager._endTransferWithReason.calledWith(
'transferred'));
}, () => {
// reject case
assert.isTrue(false);
}).then(done, done);
});
});
suite('blob has no name, has no filepaths > ', function() {
setup(function() {
mockActivityRequest = {
source: {
name: 'share',
data: {
blobs: [{}, {}]
}
}
};
TransferManager._activity = mockActivityRequest;
});
test('_produceSendingFilesSchedule() should be called with length, ' +
'_postMessageToSystemApp() should be called with schedule, ' +
'BtContext.sendFile() should be called with: ' +
'address of target device and blob in soure.data, ' +
'_endTransferWithReason() should be called with transferred ',
function(done) {
TransferManager._onDevicePicked(mockEvent).then(() => {
assert.isTrue(
TransferManager._produceSendingFilesSchedule.calledWith(
mockActivityRequest.source.data.blobs.length));
assert.isTrue(
TransferManager._postMessageToSystemApp.calledWith(mockSchedule));
assert.equal(BtContext.sendFile.args[0][0], mockEvent.detail.address);
assert.deepEqual(BtContext.sendFile.args[0][1],
mockActivityRequest.source.data.blobs[0]);
assert.equal(BtContext.sendFile.args[1][0], mockEvent.detail.address);
assert.deepEqual(BtContext.sendFile.args[1][1],
mockActivityRequest.source.data.blobs[1]);
assert.isTrue(TransferManager._endTransferWithReason.calledWith(
'transferred'));
}, () => {
// reject case
assert.isTrue(false);
}).then(done, done);
});
});
});
suite('_onCancelSelection > ', function() {
setup(function() {
this.sinon.stub(TransferManager, '_endTransferWithReason');
});
test('_endTransferWithReason() should be called with reason "cancelled"',
function() {
TransferManager._onCancelSelection();
assert.isTrue(
TransferManager._endTransferWithReason.calledWith('cancelled'));
});
});
suite('_produceSendingFilesSchedule > ', function() {
var mockSendingFilesSchedule, mockNumberOfTasks;
setup(function() {
mockNumberOfTasks = 3;
mockSendingFilesSchedule = {
numberOfFiles: mockNumberOfTasks,
numSuccessful: 0,
numUnsuccessful: 0
};
});
test('_postMessageToSystemApp() should be called with object of ' +
'sending files schedule ', function() {
assert.deepEqual(
TransferManager._produceSendingFilesSchedule(mockNumberOfTasks),
mockSendingFilesSchedule);
});
});
suite('_postMessageToSystemApp > ', function() {
var mockMessage, mockResultOfMozAppGetSelf, mockConnectPromise, mockPorts;
setup(function() {
mockMessage = {};
mockPorts = [
{
postMessage: function() {}
}
];
this.sinon.stub(mockPorts[0], 'postMessage');
mockConnectPromise = Promise.resolve(mockPorts);
mockResultOfMozAppGetSelf = {
connect: function() {return mockConnectPromise;}
};
this.sinon.spy(mockResultOfMozAppGetSelf, 'connect');
});
test('Should post inputed message via port "bluetoothTransfercomms" ' +
'if get app.connect, ports via mozApps successfully ', function(done) {
TransferManager._postMessageToSystemApp(mockMessage);
MockNavigatormozApps.mTriggerLastRequestSuccess(
mockResultOfMozAppGetSelf);
assert.isTrue(
mockResultOfMozAppGetSelf.connect.calledWith('bluetoothTransfercomms'));
mockConnectPromise.then(() => {
assert.isTrue(
mockPorts[0].postMessage.calledWith(mockMessage));
}, () => {
// reject case
assert.isTrue(false);
}).then(done, done);
});
});
});

View file

@ -0,0 +1,43 @@
/* global requirejs */
'use strict';
require('/shared/test/unit/mocks/mocks_helper.js');
requireApp('bluetooth/js/vendor/alameda.js', (function() {
var contextId = 0;
var baseConfig = {
baseUrl: '/js',
urlArgs: 'bust=' + Date.now(),
paths: {
'modules': 'modules',
'views': 'views',
'shared': '../shared/js',
'unit': '../test/unit',
'shared_mocks': '../shared/test/unit/mocks'
},
shim: {},
modules: [
{
name: 'main'
}
]
};
this.testRequire = function(modules, map, callback) {
var ctx;
if (arguments.length === 2) {
callback = map;
map = null;
}
ctx = requirejs.config({
context: contextId++
});
ctx.config(baseConfig);
ctx.config({
map: map || {}
});
ctx(modules, callback);
return ctx;
};
}).bind(window));

View file

@ -0,0 +1,92 @@
'use strict';
require('/shared/test/unit/load_body_html_helper.js');
suite('Bluetooth app > CannotTransferDialog ', function() {
var CannotTransferDialog;
suiteSetup(function(done) {
loadBodyHTML('./_transfer.html');
var module = [
'views/cannot_transfer_dialog'
];
var map = {};
var requireCtx = testRequire([], map, function() {});
requireCtx(module, function(cannotTransferDialog) {
CannotTransferDialog = cannotTransferDialog;
done();
}.bind(this));
});
suiteTeardown(function() {
document.body.innerHTML = '';
});
suite('init > ', function() {
test('dialog, confirm button should be defined after init() ', function() {
assert.equal(CannotTransferDialog.dialog,
document.getElementById('alert-view'));
assert.equal(CannotTransferDialog.confirmBtn,
document.getElementById('alert-button-ok'));
});
});
suite('showConfirm > ', function() {
setup(function() {
CannotTransferDialog.dialog.hidden = true;
this.sinon.stub(CannotTransferDialog, 'close');
});
teardown(function() {
CannotTransferDialog.dialog.hidden = true;
});
test('dialog should be show after showConfirm() called ', function() {
CannotTransferDialog.showConfirm();
assert.isFalse(CannotTransferDialog.dialog.hidden);
});
test('close() be called and resovle promise after a user clicked ' +
'confirm button in cannot transfer dialog ', function(done) {
CannotTransferDialog.showConfirm().then(() => {
assert.ok(true);
}, () => {
// reject case
}).then(done, done);
CannotTransferDialog.confirmBtn.onclick();
assert.isTrue(CannotTransferDialog.close.called);
});
});
suite('isVisible > ', function() {
setup(function() {
CannotTransferDialog.dialog.hidden = false;
});
teardown(function() {
CannotTransferDialog.dialog.hidden = true;
});
test('getting the value of visible should be true ' +
'after isVisible() is called ', function() {
assert.isTrue(CannotTransferDialog.isVisible);
});
});
suite('close > ', function() {
setup(function() {
CannotTransferDialog.dialog.hidden = false;
});
teardown(function() {
CannotTransferDialog.dialog.hidden = true;
});
test('dialog should be hidden after close() is called ', function() {
CannotTransferDialog.close();
assert.isTrue(CannotTransferDialog.dialog.hidden);
});
});
});

View file

@ -0,0 +1,310 @@
/* global MockL10n */
'use strict';
require('/shared/test/unit/load_body_html_helper.js');
require('/shared/test/unit/mocks/mock_l10n.js');
suite('DevicePickerPanel', function() {
var realL10n;
var devicePickerPanel;
var btContext;
var btTemplateFactory;
var listView;
suiteSetup(function() {
realL10n = window.navigator.mozL10n;
window.navigator.mozL10n = MockL10n;
loadBodyHTML('./_transfer.html');
});
suiteTeardown(function() {
window.navigator.mozL10n = realL10n;
});
setup(function(done) {
var modules = [
'modules/bluetooth/bluetooth_context',
'views/bt_template_factory',
'modules/mvvm/list_view',
'views/device_picker_panel'
];
var map = {
'*': {
'modules/bluetooth/bluetooth_context': 'MockBluetoothContext',
'modules/mvvm/list_view': 'MockListView'
}
};
this.MockBluetoothContext = {
observe: function() {},
getPairedDevices: function() {},
getRemoteDevices: function() {},
hasPairedDevice: false,
state: 'disabled',
discovering: false,
startDiscovery: function() {return Promise.resolve();},
pair: function() {}
};
define('MockBluetoothContext', function() {
return this.MockBluetoothContext;
}.bind(this));
this.MockListView = function MockListView() {
return {};
};
define('MockListView', function() {
return this.MockListView;
}.bind(this));
testRequire(modules, map, function(BluetoothContext, BtTemplateFactory,
ListView, DevicePickerPanel) {
btContext = BluetoothContext;
btTemplateFactory = BtTemplateFactory;
listView = ListView;
devicePickerPanel = DevicePickerPanel;
done();
});
});
suite('_Init > ', function() {
setup(function() {
this.sinon.stub(devicePickerPanel._elements.header.headerActionClose,
'addEventListener');
this.sinon.stub(devicePickerPanel._elements.search.searchButton,
'addEventListener');
devicePickerPanel._pairedDevicesListView = null;
devicePickerPanel._foundDevicesListView = null;
this.sinon.stub(devicePickerPanel, '_initItemsState');
this.sinon.stub(btContext, 'startDiscovery').returns(Promise.resolve());
});
test('The close button of header should be regedit action callback, ' +
'search button should be regedit onclick callback, ' +
'paired/found list view should be defined, ' +
'_initItemsState() should be called ' +
'BtContext.startDiscovery() should be called ', function() {
devicePickerPanel._init();
// close button
assert.equal(
devicePickerPanel._elements.header.headerActionClose.
addEventListener.args[0][0], 'action');
assert.isDefined(
devicePickerPanel._elements.header.headerActionClose.
addEventListener.args[0][1]);
// search button
assert.equal(
devicePickerPanel._elements.search.searchButton.
addEventListener.args[0][0], 'click');
assert.isDefined(
devicePickerPanel._elements.search.searchButton.
addEventListener.args[0][1]);
// paired/found list view
assert.isDefined(devicePickerPanel._pairedDevicesListView);
assert.isDefined(devicePickerPanel._foundDevicesListView);
assert.isTrue(devicePickerPanel._initItemsState.called);
assert.isTrue(btContext.startDiscovery.called);
});
});
suite('_initItemsState > ', function() {
setup(function() {
this.sinon.stub(btContext, 'observe');
this.sinon.stub(devicePickerPanel, '_updatePairedDevicesHeader');
this.sinon.stub(devicePickerPanel, '_updatePairedDevicesList');
this.sinon.stub(devicePickerPanel, '_updateFoundDevicesList');
this.sinon.stub(devicePickerPanel, '_updateSearchItem');
this.sinon.stub(devicePickerPanel, '_updateSearchingItem');
});
test('btContext.hasPairedDevice should be observed with callback, ' +
'btContext.state should be observed with callback, ' +
'btContext.discovering should be observed with callback, ' +
'paired device header, paired/found device list, ' +
'search/searching item should be updated.. ', function() {
devicePickerPanel._initItemsState();
// paired device header
assert.equal(btContext.observe.args[0][0], 'hasPairedDevice');
assert.isDefined(btContext.observe.args[0][1]);
assert.isTrue(devicePickerPanel._updatePairedDevicesHeader.called);
// paired device list
assert.equal(btContext.observe.args[1][0], 'state');
assert.isDefined(btContext.observe.args[1][1]);
assert.isTrue(devicePickerPanel._updatePairedDevicesList.called);
// found device list
assert.equal(btContext.observe.args[2][0], 'state');
assert.isDefined(btContext.observe.args[2][1]);
assert.isTrue(devicePickerPanel._updateFoundDevicesList.called);
// search item
assert.equal(btContext.observe.args[3][0], 'state');
assert.isDefined(btContext.observe.args[3][1]);
assert.isTrue(devicePickerPanel._updateSearchItem.called);
// searching item
assert.equal(btContext.observe.args[4][0], 'discovering');
assert.isDefined(btContext.observe.args[4][1]);
assert.isTrue(devicePickerPanel._updateSearchingItem.called);
});
});
suite('_onHeaderActionCloseClick > ', function() {
var expectedEvent = {
type: 'cancelSelection'
};
setup(function() {
this.sinon.stub(devicePickerPanel, '_emitEvent');
});
test('_emitEvent() should be called with event, type = "cancelSelection" ' +
'while _onHeaderActionCloseClick() be called ', function() {
devicePickerPanel._onHeaderActionCloseClick();
assert.isTrue(devicePickerPanel._emitEvent.calledWith(expectedEvent));
});
});
suite('_onSearchButtonClick > ', function() {
setup(function() {
this.sinon.stub(btContext, 'startDiscovery').returns(Promise.resolve());
});
test('btContext.startDiscovery() should be called while ' +
'search button is clicked ', function() {
devicePickerPanel._onSearchButtonClick();
assert.isTrue(btContext.startDiscovery.called);
});
});
suite('_onPairedDeviceItemClick > ', function() {
var mockDeviceItem, expectedEvent;
setup(function() {
mockDeviceItem = {
address: 'AA:BB:CC:00:11:22'
};
expectedEvent = {
type: 'devicePicked',
detail: {
address: mockDeviceItem.address
}
};
this.sinon.stub(devicePickerPanel, '_emitEvent');
});
test('_emitEvent() should be called with event, type = "devicePicked" ' +
'while _onPairedDeviceItemClick() be called with device item ',
function() {
devicePickerPanel._onPairedDeviceItemClick(mockDeviceItem);
assert.isTrue(devicePickerPanel._emitEvent.calledWith(expectedEvent));
});
});
suite('_onFoundDeviceItemClick > ', function() {
var mockDeviceItem, mockPairPromise;
suite('btContext.pair() with resolve > ', function() {
var expectedEvent;
setup(function() {
mockDeviceItem = {
address: 'AA:BB:CC:00:11:22',
paired: false
};
expectedEvent = {
type: 'devicePicked',
detail: {
address: mockDeviceItem.address
}
};
mockPairPromise = Promise.resolve();
this.sinon.stub(btContext, 'pair').returns(mockPairPromise);
this.sinon.stub(devicePickerPanel, '_emitEvent');
this.sinon.stub(devicePickerPanel, '_alertPairFailedErrorMessage');
});
test('the "paired" property of device item should be set "pairing", ' +
'_emitEvent() should be called with event, type = "devicePicked," ' +
'_alertPairFailedErrorMessage() should not be called ' +
'while _onFoundDeviceItemClick() be called with device item ',
function(done) {
devicePickerPanel._onFoundDeviceItemClick(mockDeviceItem);
assert.equal(mockDeviceItem.paired, 'pairing');
mockPairPromise.then(() => {
assert.isTrue(devicePickerPanel._emitEvent.calledWith(expectedEvent));
assert.isFalse(devicePickerPanel._alertPairFailedErrorMessage.called);
}, () => {
// reject case
}).then(done, done);
});
});
suite('btContext.pair() with reject > ', function() {
setup(function() {
mockDeviceItem = {
address: 'AA:BB:CC:00:11:22',
paired: false
};
mockPairPromise = Promise.reject();
this.sinon.stub(btContext, 'pair').returns(mockPairPromise);
this.sinon.stub(devicePickerPanel, '_emitEvent');
this.sinon.stub(devicePickerPanel, '_alertPairFailedErrorMessage');
});
test('the "paired" property of device item should be set "pairing", ' +
'then the property will be set to false in pair rejected case, ' +
'_emitEvent() should not be called, ' +
'_alertPairFailedErrorMessage() should be called ' +
'while _onFoundDeviceItemClick() be called with device item ',
function(done) {
devicePickerPanel._onFoundDeviceItemClick(mockDeviceItem);
assert.equal(mockDeviceItem.paired, 'pairing');
mockPairPromise.then(() => {
// resolve case
}, () => {
assert.isFalse(mockDeviceItem.paired);
assert.isFalse(devicePickerPanel._emitEvent.called);
assert.isTrue(devicePickerPanel._alertPairFailedErrorMessage.called);
}).then(done, done);
});
});
});
suite('_alertPairFailedErrorMessage > ', function() {
var mockErrorReason, expectedMsg;
suite('no errorReason > ', function() {
setup(function() {
mockErrorReason = null;
expectedMsg = 'error-pair-title';
this.sinon.stub(window, 'alert');
});
test('Show default message if there is no error reason ', function() {
devicePickerPanel._alertPairFailedErrorMessage(mockErrorReason);
assert.isTrue(window.alert.calledWith(expectedMsg));
});
});
suite('has errorReason "Authentication Failed" > ', function() {
setup(function() {
mockErrorReason = 'Authentication Failed';
expectedMsg = 'error-pair-title' + '\n' + 'error-pair-pincode';
this.sinon.stub(window, 'alert');
});
test('Show message with pincode error if error reason is ' +
'"Authentication Failed" ', function() {
devicePickerPanel._alertPairFailedErrorMessage(mockErrorReason);
assert.isTrue(window.alert.calledWith(expectedMsg));
});
});
});
suite('visible > ', function() {
setup(function() {
devicePickerPanel._elements.view.viewDevicePicker.hidden = true;
});
test('device picker view should be set visible', function() {
devicePickerPanel.visible = true;
assert.isFalse(devicePickerPanel._elements.view.viewDevicePicker.hidden);
});
});
});

View file

@ -0,0 +1,97 @@
/* global MockL10n */
'use strict';
require('/shared/test/unit/load_body_html_helper.js');
require('/shared/test/unit/mocks/mock_l10n.js');
suite('Bluetooth app > PairExpiredDialog ', function() {
var realL10n, PairExpiredDialog;
suiteSetup(function(done) {
realL10n = window.navigator.mozL10n;
window.navigator.mozL10n = MockL10n;
loadBodyHTML('./_message.html');
var module = [
'views/pair_expired_dialog'
];
var map = {};
var requireCtx = testRequire([], map, function() {});
requireCtx(module, function(pairExpiredDialog) {
PairExpiredDialog = pairExpiredDialog;
done();
}.bind(this));
});
suiteTeardown(function() {
window.navigator.mozL10n = realL10n;
document.body.innerHTML = '';
});
suite('init > ', function() {
test('dialog, confirm button should be defined after init() ', function() {
assert.equal(PairExpiredDialog.dialog,
document.getElementById('pairing-request-timeout'));
assert.equal(PairExpiredDialog.confirmBtn,
document.getElementById('incoming-pairing-timeout-confirm'));
});
});
suite('showConfirm > ', function() {
var callback;
setup(function() {
callback = function() {/* do something.. */};
PairExpiredDialog.dialog.hidden = true;
this.sinon.stub(PairExpiredDialog, 'close');
});
teardown(function() {
callback = null;
PairExpiredDialog.dialog.hidden = true;
});
test('dialog should be show after showConfirm() called ', function() {
PairExpiredDialog.showConfirm(callback);
assert.isFalse(PairExpiredDialog.dialog.hidden);
});
test('close() be called with callback function after a user clicked ' +
'confirm button in pair expired dialog ', function() {
PairExpiredDialog.showConfirm(callback);
PairExpiredDialog.confirmBtn.onclick();
assert.isTrue(PairExpiredDialog.close.called);
});
});
suite('isVisible > ', function() {
setup(function() {
PairExpiredDialog.dialog.hidden = false;
});
teardown(function() {
PairExpiredDialog.dialog.hidden = true;
});
test('getting the value of visible should be true ' +
'after isVisible() is called ', function() {
assert.isTrue(PairExpiredDialog.isVisible);
});
});
suite('close > ', function() {
setup(function() {
PairExpiredDialog.dialog.hidden = false;
});
teardown(function() {
PairExpiredDialog.dialog.hidden = true;
});
test('dialog should be hidden after close() is called, and the arg of ' +
'callback should be excuted if it is given ', function() {
PairExpiredDialog.close();
assert.isTrue(PairExpiredDialog.dialog.hidden);
});
});
});

View file

@ -0,0 +1,461 @@
/* global MocksHelper, MockL10n, Pairview */
'use strict';
require('/shared/test/unit/mocks/mocks_helper.js');
require('/shared/test/unit/load_body_html_helper.js');
require('/shared/test/unit/mocks/mock_l10n.js');
requireApp('bluetooth/test/unit/mock_pair_manager.js');
function switchReadOnlyProperty(originObject, propName, targetObj) {
Object.defineProperty(originObject, propName, {
configurable: true,
get: function() { return targetObj; }
});
}
var mocksForPairviewHelper = new MocksHelper([
'PairManager'
]).init();
suite('Bluetooth app > Pairview ', function() {
var realL10n;
mocksForPairviewHelper.attachTestHelpers();
suiteSetup(function(done) {
realL10n = window.navigator.mozL10n;
window.navigator.mozL10n = MockL10n;
loadBodyHTML('./_onpair.html');
requireApp('bluetooth/js/views/pair_view.js', done);
});
suiteTeardown(function() {
window.navigator.mozL10n = realL10n;
document.body.innerHTML = '';
});
suite('isFullAttentionMode > ', function() {
var realWindowInnerHeight;
suiteSetup(function() {
realWindowInnerHeight = window.innerHeight;
});
suiteTeardown(function() {
switchReadOnlyProperty(window, 'innerHeight', realWindowInnerHeight);
});
suite('window.innerHeight is 200 > ', function() {
setup(function() {
switchReadOnlyProperty(window, 'innerHeight', 200);
});
test('getting the mode of full attention should be false ' +
'after isFullAttentionMode() is called ', function() {
assert.isFalse(Pairview.isFullAttentionMode);
});
});
suite('window.innerHeight is 201 > ', function() {
setup(function() {
switchReadOnlyProperty(window, 'innerHeight', 201);
});
test('getting the mode of full attention should be true ' +
'after isFullAttentionMode() is called ', function() {
assert.isTrue(Pairview.isFullAttentionMode);
});
});
});
suite('init > ', function() {
var mockPairMethod, mockOptions;
setup(function(done) {
mockPairMethod = 'displaypasskey';
mockOptions = {
deviceName: 'device-01',
handle: {
passkey: 123456
}
};
this.sinon.stub(Pairview, 'show');
this.sinon.stub(Pairview, '_pairMethod');
this.sinon.stub(Pairview, '_remoteDeviceName');
this.sinon.stub(Pairview, '_passkey');
Pairview.init(mockPairMethod, mockOptions);
setTimeout(done);
});
test('pairing info should be inited after init() ', function() {
assert.equal(Pairview._pairMethod, mockPairMethod);
assert.equal(Pairview._remoteDeviceName, mockOptions.deviceName);
assert.equal(Pairview._passkey, 123456);
});
test('pair view should be showing after mozL10n.once callback', function() {
assert.isTrue(Pairview.show.called);
});
});
suite('show > pair method in three methods > ', function() {
var realGetTruncated;
var mockDeviceName = 'device-01';
var mockPasskey = 123456;
setup(function() {
realGetTruncated = window.getTruncated;
window.getTruncated = this.sinon.stub();
window.getTruncated.returns({
addEventListener: this.sinon.stub()
});
});
suite('early return > ', function() {
var realWindowInnerHeight;
setup(function() {
realWindowInnerHeight = window.innerHeight;
switchReadOnlyProperty(window, 'innerHeight', 200);
this.sinon.stub(Pairview, 'close');
Pairview.show();
});
teardown(function() {
switchReadOnlyProperty(window, 'innerHeight', realWindowInnerHeight);
});
test('isFullAttentionMode() should be false', function() {
assert.isFalse(Pairview.isFullAttentionMode);
});
test('close() should be called while attention sceen is not in full ' +
'screen mode ', function() {
assert.isTrue(Pairview.close.called);
});
test('getTruncated() should not be called since early return',
function() {
assert.isFalse(window.getTruncated.called);
});
});
suite('confirmation, displaypasskey > ', function() {
['confirmation', 'displaypasskey'].forEach((mockPairMethod) => {
setup(function() {
this.sinon.stub(Pairview, '_pairMethod', mockPairMethod);
this.sinon.stub(Pairview, '_remoteDeviceName', mockDeviceName);
this.sinon.stub(Pairview, '_passkey', mockPasskey);
Pairview.show();
});
teardown(function() {
Pairview.nameLabel.textContent = undefined;
Pairview.pairview.hidden = true;
Pairview.pairDescription.textContent = undefined;
Pairview.passkey.textContent = undefined;
Pairview.comfirmationItem.hidden = true;
Pairview.pinInputItem.hidden = true;
});
test('nameLabel should be defined after show() ', function() {
assert.isDefined(Pairview.nameLabel.textContent);
});
test('view should not be hidden after show() ', function() {
assert.isFalse(Pairview.pairview.hidden);
});
test('description should be defined after show() ', function() {
assert.isDefined(Pairview.pairDescription.textContent);
});
test('passkey textContent should be displayed and ' +
'same with pairing info from system message ', function() {
assert.equal(Pairview.passkey.textContent, mockPasskey);
});
test('comfirmation method should not be hidden after show()',
function() {
assert.isFalse(Pairview.comfirmationItem.hidden);
});
test('pin input field should be hidden after show() ', function() {
assert.isTrue(Pairview.pinInputItem.hidden);
});
});
});
suite('enterpincode > ', function() {
setup(function() {
this.sinon.stub(Pairview, '_pairMethod', 'enterpincode');
this.sinon.stub(Pairview, '_remoteDeviceName', mockDeviceName);
Pairview.show();
});
teardown(function() {
Pairview.nameLabel.textContent = undefined;
Pairview.pairview.hidden = true;
Pairview.pairDescription.textContent = undefined;
Pairview.comfirmationItem.hidden = true;
Pairview.pinInputItem.hidden = true;
});
test('nameLabel should be defined after show() ', function() {
assert.isDefined(Pairview.nameLabel.textContent);
});
test('view should not be hidden after show() ', function() {
assert.isFalse(Pairview.pairview.hidden);
});
test('description should be defined after show() ', function() {
assert.isDefined(Pairview.pairDescription.textContent);
});
test('pin input field should not be hidden after show() ', function() {
assert.isFalse(Pairview.pinInputItem.hidden);
});
test('comfirmation method should be hidden after show() ', function() {
assert.isTrue(Pairview.comfirmationItem.hidden);
});
});
suite('consent > ', function() {
setup(function() {
this.sinon.stub(Pairview, '_pairMethod', 'consent');
this.sinon.stub(Pairview, '_remoteDeviceName', mockDeviceName);
Pairview.show();
});
teardown(function() {
Pairview.nameLabel.textContent = undefined;
Pairview.pairview.hidden = true;
Pairview.pairDescription.textContent = undefined;
Pairview.comfirmationItem.hidden = true;
Pairview.pinInputItem.hidden = true;
});
test('nameLabel should be defined after show() ', function() {
assert.isDefined(Pairview.nameLabel.textContent);
});
test('view should not be hidden after show() ', function() {
assert.isFalse(Pairview.pairview.hidden);
});
test('description should be defined after show() ', function() {
assert.isDefined(Pairview.pairDescription.textContent);
});
test('comfirmation method should not be hidden after show()', function() {
assert.isFalse(Pairview.comfirmationItem.hidden);
});
test('pin input field should be hidden after show() ', function() {
assert.isTrue(Pairview.pinInputItem.hidden);
});
});
});
suite('handleEvent > ', function() {
var fakePreventDefault = function() {/* do nothing here.. */};
var stubWindowClose;
suiteSetup(function() {
stubWindowClose = sinon.stub(window, 'close');
Pairview.pairButton.disabled = false;
Pairview.closeButton.disabled = false;
Pairview.pairDescription.textContent = undefined;
});
suiteTeardown(function() {
stubWindowClose.restore();
Pairview.pairButton.disabled = false;
Pairview.closeButton.disabled = false;
Pairview.pairDescription.textContent = undefined;
});
suite('click pair button > pair method: displaypasskey ', function() {
setup(function() {
this.sinon.stub(Pairview, '_pairMethod', 'displaypasskey');
Pairview.handleEvent({target: {id: 'button-pair'},
type: 'click',
preventDefault: fakePreventDefault});
});
test('description should be defined after clicked pair button',
function() {
assert.isDefined(Pairview.pairDescription.textContent);
});
test('pair button should be disabled after clicked event ', function() {
assert.isTrue(Pairview.pairButton.disabled);
});
test('close button should be disabled after clicked event ', function() {
assert.isTrue(Pairview.closeButton.disabled);
});
test('window should be closed after clicked event', function() {
assert.isTrue(stubWindowClose.called);
});
});
suite('click pair button > pair method: enterpincode ', function() {
var mockOptions, pincode = 'SixteenTxtLength';
setup(function() {
this.sinon.stub(Pairview, '_pairMethod', 'enterpincode');
Pairview.pinInput.value = pincode;
mockOptions = {
handle: {
setPinCode: function() {return Promise.resolve();}
}
};
this.sinon.spy(mockOptions.handle, 'setPinCode');
this.sinon.stub(Pairview, '_options', mockOptions);
Pairview.handleEvent({target: {id: 'button-pair'},
type: 'click',
preventDefault: fakePreventDefault});
});
teardown(function() {
Pairview.pinInput.value = '';
});
test('description should be defined after clicked pair button',
function() {
assert.isDefined(Pairview.pairDescription.textContent);
});
test('pair button should be disabled after clicked event ', function() {
assert.isTrue(Pairview.pairButton.disabled);
});
test('close button should be disabled after clicked event ', function() {
assert.isTrue(Pairview.closeButton.disabled);
});
test('set pin code should be set after clicked event', function() {
assert.isTrue(Pairview._options.handle.setPinCode.calledWith(pincode));
});
test('window should be closed after setPinCode resolve ', function(done) {
mockOptions.handle.setPinCode(pincode).then(() => {
assert.isTrue(stubWindowClose.called);
}, () => {
// reject case
assert.isTrue(false);
}).then(done, done);
});
});
suite('click pair button > pair method: confirmation, consent ',
function() {
var mockOptions;
['confirmation', 'consent'].forEach((mockPairMethod) => {
setup(function() {
this.sinon.stub(Pairview, '_pairMethod', mockPairMethod);
mockOptions = {
handle: {
accept: function() {return Promise.resolve();}
}
};
this.sinon.spy(mockOptions.handle, 'accept');
this.sinon.stub(Pairview, '_options', mockOptions);
Pairview.handleEvent({target: {id: 'button-pair'},
type: 'click',
preventDefault: fakePreventDefault});
});
test('description should be defined after clicked pair button',
function() {
assert.isDefined(Pairview.pairDescription.textContent);
});
test('pair button should be disabled after clicked event ', function() {
assert.isTrue(Pairview.pairButton.disabled);
});
test('close button should be disabled after clicked event', function() {
assert.isTrue(Pairview.closeButton.disabled);
});
test('handle.accept should be set after clicked event', function() {
assert.isTrue(Pairview._options.handle.accept.called);
});
test('window should be closed after accept resolve', function(done) {
mockOptions.handle.accept().then(() => {
assert.isTrue(stubWindowClose.called);
}, () => {
// reject case
assert.isTrue(false);
}).then(done, done);
});
});
});
suite('click close button > ', function() {
var pairviewCloseSpy, mockOptions;
setup(function() {
pairviewCloseSpy = this.sinon.spy(Pairview, 'close');
mockOptions = {
handle: {
reject: function() {return Promise.resolve();}
}
};
this.sinon.spy(mockOptions.handle, 'reject');
this.sinon.stub(Pairview, '_options', mockOptions);
Pairview.handleEvent({target: {id: 'button-close'},
type: 'click',
preventDefault: fakePreventDefault});
});
test('close() method should be called after clicked close ', function() {
assert.isTrue(pairviewCloseSpy.called);
});
test('handle.reject should be set after clicked close ', function(done) {
mockOptions.handle.reject().then(() => {
assert.isTrue(stubWindowClose.called);
}, () => {
// reject case
assert.isTrue(false);
}).then(done, done);
});
});
suite('received "resize" event > ', function() {
var pairviewCloseSpy, mockOptions;
setup(function() {
pairviewCloseSpy = this.sinon.spy(Pairview, 'close');
mockOptions = {
handle: {
reject: function() {return Promise.resolve();}
}
};
this.sinon.spy(mockOptions.handle, 'reject');
this.sinon.stub(Pairview, '_options', mockOptions);
switchReadOnlyProperty(window, 'innerHeight', 150);
Pairview.handleEvent({target: {},
type: 'resize',
preventDefault: fakePreventDefault});
});
teardown(function() {
switchReadOnlyProperty(window, 'innerHeight', null);
});
test('close() method should be called after clicked close ', function() {
assert.isTrue(pairviewCloseSpy.called);
});
test('handle.reject should be set after clicked close ', function(done) {
mockOptions.handle.reject().then(() => {
assert.isTrue(stubWindowClose.called);
}, () => {
// reject case
assert.isTrue(false);
}).then(done, done);
});
});
});
suite('closeInput > ', function() {
var pairviewPinInputSpy;
setup(function() {
pairviewPinInputSpy = this.sinon.spy(Pairview.pinInput, 'blur');
Pairview.pinInputItem.hidden = false;
Pairview.closeInput();
});
teardown(function() {
pairviewPinInputSpy.restore();
Pairview.pinInputItem.hidden = true;
});
test('pin input should be blurred after closeInput() ', function() {
assert.isTrue(pairviewPinInputSpy.called);
});
});
});

View file

@ -0,0 +1,113 @@
'use strict';
require('/shared/test/unit/load_body_html_helper.js');
suite('Bluetooth app > TurnBluetoothOnDialog ', function() {
var TurnBluetoothOnDialog;
suiteSetup(function(done) {
loadBodyHTML('./_transfer.html');
var module = [
'views/turn_bluetooth_on_dialog'
];
var map = {};
var requireCtx = testRequire([], map, function() {});
requireCtx(module, function(turnBluetoothOnDialog) {
TurnBluetoothOnDialog = turnBluetoothOnDialog;
done();
}.bind(this));
});
suiteTeardown(function() {
document.body.innerHTML = '';
});
suite('init > ', function() {
test('dialog, cancel/confirm button should be defined after init() ',
function() {
assert.equal(TurnBluetoothOnDialog.dialog,
document.getElementById('enable-bluetooth-view'));
assert.equal(TurnBluetoothOnDialog.cancelBtn,
document.getElementById('enable-bluetooth-button-cancel'));
assert.equal(TurnBluetoothOnDialog.confirmBtn,
document.getElementById('enable-bluetooth-button-turn-on'));
});
});
suite('showConfirm > ', function() {
var confirmedAction;
setup(function() {
confirmedAction = {
cancel: 'cancel',
confirm: 'confirm'
};
TurnBluetoothOnDialog.dialog.hidden = true;
this.sinon.stub(TurnBluetoothOnDialog, 'close');
});
teardown(function() {
TurnBluetoothOnDialog.dialog.hidden = true;
});
test('dialog should be show after showConfirm() called ', function() {
TurnBluetoothOnDialog.showConfirm();
assert.isFalse(TurnBluetoothOnDialog.dialog.hidden);
});
test('close() be called and resovled promise with arg "cancel" after a ' +
'user clicked cancel button in turn Bluetooth on dialog ',
function(done) {
TurnBluetoothOnDialog.showConfirm().then((result) => {
assert.equal(result, confirmedAction.cancel);
}, () => {
// reject case
}).then(done, done);
TurnBluetoothOnDialog.cancelBtn.onclick();
assert.isTrue(TurnBluetoothOnDialog.close.called);
});
test('close() be called and resolved promise with arg "confirm" after a ' +
'user clicked confirm button in turn Bluetooth on dialog ',
function(done) {
TurnBluetoothOnDialog.showConfirm().then((result) => {
assert.equal(result, confirmedAction.confirm);
}, () => {
// reject case
}).then(done, done);
TurnBluetoothOnDialog.confirmBtn.onclick();
assert.isTrue(TurnBluetoothOnDialog.close.called);
});
});
suite('isVisible > ', function() {
setup(function() {
TurnBluetoothOnDialog.dialog.hidden = false;
});
teardown(function() {
TurnBluetoothOnDialog.dialog.hidden = true;
});
test('getting the value of visible should be true ' +
'after isVisible() is called ', function() {
assert.isTrue(TurnBluetoothOnDialog.isVisible);
});
});
suite('close > ', function() {
setup(function() {
TurnBluetoothOnDialog.dialog.hidden = false;
});
teardown(function() {
TurnBluetoothOnDialog.dialog.hidden = true;
});
test('dialog should be hidden after close() is called ', function() {
TurnBluetoothOnDialog.close();
assert.isTrue(TurnBluetoothOnDialog.dialog.hidden);
});
});
});

View file

@ -0,0 +1,88 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1">
<meta charset="utf-8" />
<title>Bluetooth</title>
<!-- Localization -->
<meta name="defaultLanguage" content="en-US">
<meta name="availableLanguages" content="en-US">
<link rel="localization" href="locales/bluetooth.{locale}.properties">
<script type="application/javascript" src="shared/js/l10n.js"></script>
<!-- Common style -->
<!-- <link rel="stylesheet" type="text/css" href="shared/elements/gaia-icons/gaia-icons.css" /> -->
<link rel="stylesheet" type="text/css" href="app://theme.gaiamobile.org/shared/elements/gaia-theme/gaia-theme.css" />
<link rel="stylesheet" type="text/css" href="shared/style/confirm.css"/>
<link rel="stylesheet" type="text/css" href="shared/style/buttons.css" />
<link rel="stylesheet" type="text/css" href="shared/style/progress_activity.css"/>
<!-- Shared code -->
<script type="text/javascript" src="shared/js/l10n.js"></script>
<!-- Web Components -->
<script defer src="shared/elements/config.js"></script>
<script defer src="shared/elements/gaia-header/dist/gaia-header.js"></script>
<script type="text/javascript" src="shared/js/component_utils.js"></script>
<script type="text/javascript" src="shared/elements/gaia_subheader/script.js"></script>
<!-- Specific style -->
<link rel="stylesheet" type="text/css" href="style/transfer.css" />
<link rel="stylesheet" type="text/css" href="style/lists.css">
<link rel="stylesheet" type="text/css" href="style/icons.css">
<link rel="stylesheet" type="text/css" href="style/settings.css">
</head>
<body role="application" class="skin-organic theme-settings uninit">
<!-- Enable Bluetooth View -->
<form role="dialog" data-type="confirm" id="enable-bluetooth-view" hidden>
<section>
<h1 data-l10n-id="confirmation"></h1>
<p>
<img id="icon" src="style/images/settings_bluetooth.svg" alt="Bluetooth Icon"/>
<strong data-l10n-id="bluetooth"></strong>
<small data-l10n-id="bluetooth-status-description"></small>
</p>
<p data-l10n-id="turn-bluetooth-on"></p>
</section>
<menu>
<button type="reset" id="enable-bluetooth-button-cancel" data-l10n-id="cancel">Cancel</button>
<button type="submit" class="recommend" id="enable-bluetooth-button-turn-on" data-l10n-id="turn-on"></button>
</menu>
</form>
<!-- Bluetooth File Transfer Failed View -->
<form role="dialog" data-type="confirm" id="alert-view" hidden>
<section>
<h1 data-l10n-id="error-transfer-title"></h1>
<p data-l10n-id="error-transfer-settings"></p>
</section>
<menu>
<button class="full danger" id="alert-button-ok" data-l10n-id="ok"></button>
</menu>
</form>
<!-- Devices List View -->
<section role="region" id="devices-list-view" class="devices-list-view current" data-rendered="true" hidden>
<gaia-header ignore-dir id="devices-list-header" class="devices-list-header" action="close">
<h1 data-l10n-id="bluetooth"></h1>
</gaia-header>
<div>
<!-- Devices List View :: Paired Devices List -->
<gaia-subheader id="bluetooth-paired-title" class="bluetooth-paired-title" skin="organic">
<span data-l10n-id="bluetooth-paired-devices"></span>
</gaia-subheader>
<ul id="bluetooth-paired-devices" class="bluetooth-paired-devices devices">
</ul>
<!-- Devices List View :: Found Devices List -->
<gaia-subheader id="bluetooth-found-title" class="bluetooth-found-title" skin="organic">
<span data-l10n-id="bluetooth-devices-in-area"></span>
</gaia-subheader>
<ul id="bluetooth-devices" class="bluetooth-devices devices">
</ul>
<!-- Devices List View :: Search Button -->
<div id="bluetooth-searching" data-l10n-id="search-for-device" class="bluetooth-searching explanation"></div>
<ul id="bluetooth-search" class="bluetooth-search">
<li>
<button id="search-device" class="search-device" data-l10n-id="search-device" disabled></button>
</li>
</ul>
</div>
</section>
<script data-main="js/startup_transfer.js" src="js/vendor/alameda.js"></script>
</body>
</html>

View file

@ -0,0 +1,54 @@
'use strict';
/* global utils, BookmarkEditor, BookmarkRemover */
var ActivityHandler = {
'save-bookmark': function ah_save(activity) {
BookmarkEditor.init({
data: activity.source.data,
onsaved: function onsaved(saved) {
window.addEventListener('status-hidden', function hidden() {
window.removeEventListener('status-hidden', hidden);
activity.postResult(saved ? 'saved' : 'updated');
});
var msg = saved ?
'added-to-home-screen-message' : 'updated-pinned-site';
navigator.mozL10n.formatValue(msg)
.then(msg => utils.status.show(msg));
},
oncancelled: function oncancelled() {
activity.postError('cancelled');
}
});
},
'remove-bookmark': function ah_remove(activity) {
BookmarkRemover.init({
id: activity.source.data.url,
onremoved: function onremoved() {
activity.postResult('removed');
},
oncancelled: function oncancelled(e) {
activity.postError(e);
}
});
}
};
navigator.mozSetMessageHandler('activity', function onActivity(activity) {
var name = activity.source.name;
switch (name) {
case 'save-bookmark':
case 'remove-bookmark':
if (activity.source.data.type === 'url') {
ActivityHandler[name](activity);
} else {
activity.postError('type not supported');
}
break;
default:
activity.postError('name not supported');
}
});

View file

@ -0,0 +1,175 @@
'use strict';
/* global UrlHelper, BookmarksDatabase, Icon */
/* exported BookmarkEditor */
var BookmarkEditor = {
BOOKMARK_ICON_SIZE: 60,
APP_ICON_SIZE: 60,
init: function bookmarkEditor_show(options) {
this.data = options.data;
this.onsaved = options.onsaved;
this.oncancelled = options.oncancelled;
var mode = 'add';
BookmarksDatabase.get(this.data.url).then((function got(bookmark) {
if (bookmark) {
this.data = bookmark;
mode = 'put';
}
this._init(mode);
}).bind(this), this._init.bind(this, mode));
},
_init: function bookmarkEditor_init(mode) {
this.mode = document.body.dataset.mode = mode;
this.bookmarkTitle = document.getElementById('bookmark-title');
this.bookmarkURL = document.getElementById('bookmark-url');
this.bookmarkIcon = document.getElementById('bookmark-icon');
this.header = document.getElementById('header');
this.saveButton = document.getElementById('done-button');
this.appInstallationSection = document.getElementById('app-installation');
this.appIcon = document.getElementById('app-icon');
this.appIconPlaceholder = document.getElementById('app-icon-placeholder');
this.appNameText = document.getElementById('app-name');
this.installAppButton = document.getElementById('install-app-button');
this.header.addEventListener('action', this.close.bind(this));
this.saveListener = this.save.bind(this);
this.saveButton.addEventListener('click', this.saveListener);
this.bookmarkTitle.value = this.data.name || '';
this.bookmarkURL.textContent = this.data.url;
this._renderIcon();
if (this.data.manifestURL) {
this.manifestURL = this.data.manifestURL;
this._fetchManifest(this.manifestURL);
}
this._checkDoneButton();
this.form = document.getElementById('bookmark-form');
this.form.addEventListener('input', this._checkDoneButton.bind(this));
this.form.addEventListener('submit', this._submit.bind(this));
var touchstart = 'ontouchstart' in window ? 'touchstart' : 'mousedown';
this.clearButton = document.getElementById('bookmark-title-clear');
this.clearButton.addEventListener(touchstart, this._clearTitle.bind(this));
if (mode === 'put') {
this._onEditMode();
this.saveButton.setAttribute('data-l10n-id', 'done-action');
} else {
this.saveButton.setAttribute('data-l10n-id', 'add-action');
}
// We're appending new elements to DOM so to make sure headers are
// properly resized and centered, we emmit a lazyload event.
// This will be removed when the gaia-header web component lands.
window.dispatchEvent(new CustomEvent('lazyload', {
detail: document.body
}));
},
_renderIcon: function renderIcon() {
var icon = new Icon(this.bookmarkIcon, this.data.icon);
icon.render({'size': this.BOOKMARK_ICON_SIZE});
},
_renderAppIcon: function renderAppIcon(manifest, size) {
// Parse icon URL from app manifest
var iconURL = window.IconsHelper.getBestIconFromWebManifest(manifest, size);
if (!iconURL) {
return;
}
// Switch out placeholder for real icon when it loads
this.appIconListener = this._handleAppIconLoad.bind(this);
this.appIcon.addEventListener('load', this.appIconListener);
this.appIcon.setAttribute('src', iconURL);
},
_handleAppIconLoad: function _handleAppIconLoad() {
this.appIconPlaceholder.classList.add('hidden');
this.appIcon.classList.remove('hidden');
this.appIcon.removeEventListener('load', this.appIconListener);
},
_fetchManifest: function bookmarkEditor_fetchManifest(manifestURL) {
var manifestPromise = window.WebManifestHelper.getManifest(manifestURL);
manifestPromise.then((function(manifestData) {
if (manifestData) {
this.installAppButtonListener = this._installApp.bind(this);
this.installAppButton.addEventListener('click',
this.installAppButtonListener);
this.appNameText.textContent = manifestData.short_name ||
manifestData.name;
this.appInstallationSection.classList.remove('hidden');
this._renderAppIcon(manifestData, manifestURL, this.APP_ICON_SIZE);
}
}).bind(this)).catch(function(error) {
console.error('Unable to get web manifest: ' + error);
});
return manifestPromise;
},
_onEditMode: function bookmarkEditor_onEditMode() {
// Done button will be disabled on edit mode once it is displayed
this.saveButton.disabled = true;
},
close: function bookmarkEditor_close() {
this.oncancelled();
},
/**
* Handles the submit case for the form when the user presses the enter key.
* @param {Event} event The form submit event.
*/
_submit: function(event) {
event.preventDefault();
if (!this.saveButton.disabled) {
this.save();
}
},
_clearTitle: function bookmarkEditor_clearTitle(event) {
event.preventDefault();
this.bookmarkTitle.value = '';
this._checkDoneButton();
},
_checkDoneButton: function bookmarkEditor_checkDoneButton() {
// If one of the fields is blank, the “Done” button should be dimmed and
// inactive
var title = this.bookmarkTitle.value.trim();
this.saveButton.disabled = title === '';
},
_installApp: function bookmarkEditor_installApp() {
window.navigator.mozApps.install(this.manifestURL);
},
save: function bookmarkEditor_save(evt) {
this.saveButton.removeEventListener('click', this.saveListener);
if (this.installAppButtonListener) {
this.installAppButton.removeEventListener('click',
this.installAppButtonListener);
}
// Only allow urls to be bookmarked.
// This is defensive check - callers should filter out non-URLs.
var url = this.data.url.trim();
if (UrlHelper.isNotURL(url)) {
this.oncancelled();
return;
}
this.data.name = this.bookmarkTitle.value;
this.data.url = url;
BookmarksDatabase[this.mode](this.data).then(this.onsaved.bind(this),
this.close.bind(this));
}
};

View file

@ -0,0 +1,39 @@
'use strict';
/* global BookmarksDatabase */
/* exported BookmarkRemover */
var BookmarkRemover = {
init: function bookmarkRemover_init(data) {
var id = data.id;
BookmarksDatabase.get(id).then(function got(bookmark) {
if (!bookmark) {
data.oncancelled('bookmark does not exist');
return;
}
// Title and message
var ttl = document.getElementById('title');
var msg = document.getElementById('message');
ttl.setAttribute('data-l10n-id', 'remove-title');
ttl.setAttribute('data-l10n-args',
JSON.stringify({ name: bookmark.name }));
msg.setAttribute('data-l10n-id', 'remove-message');
msg.setAttribute('data-l10n-args',
JSON.stringify({ name: bookmark.name }));
// Action handlers
document.getElementById('cancel-action').addEventListener('click',
function onCancel() {
data.oncancelled('cancelled');
}
);
document.getElementById('remove-action').addEventListener('click',
function onConfirm(evt) {
evt.target.removeEventListener('click', onConfirm);
BookmarksDatabase.remove(id).then(data.onremoved, data.oncancelled);
}
);
}, data.oncancelled);
}
};

View file

@ -0,0 +1,32 @@
section[role="status"] {
animation-name: hide;
animation-duration: .3s;
animation-fill-mode: forwards;
}
section[role="status"].onviewport {
animation-name: show;
}
section[role="status"].hidden {
display: none;
}
@keyframes show {
from {
transform: translateY(100%)
}
to {
transform: translateY(0)
}
}
@keyframes hide {
from {
transform: translateY(0)
}
to {
transform: translateY(100%)
}
}

View file

@ -0,0 +1,177 @@
'use strict';
window.utils = window.utils || {};
window.utils.status = (function() {
// How many milliseconds is displayed the status component by default
var DISPLAYED_TIME = 1500;
// References to the DOMElement(s) that renders the status UI component
var section, content;
// The numerical ID of the timeout in order to hide UI component
var timeoutID;
/*
* Clears the callback in charge of hiding the component after timeout
*/
function clearHideTimeout() {
if (timeoutID === null) {
return;
}
window.clearTimeout(timeoutID);
timeoutID = null;
}
/*
* Shows the status component
*
* @param{Object} Message. It could be a string or a DOMFragment that
* represents the normal and strong strings
*
* @param{int} It defines the time that the status is displayed in ms. This
* parameter is optional
*
*/
function show(message, duration) {
clearHideTimeout();
content.innerHTML = '';
if (typeof message === 'string') {
content.textContent = message;
} else {
try {
// Here we should have a DOMFragment
content.appendChild(message);
} catch (ex) {
console.error('DOMException: ' + ex.message);
}
}
section.classList.remove('hidden');
section.classList.add('onviewport');
timeoutID = window.setTimeout(hide, duration || DISPLAYED_TIME);
}
/*
* This function is invoked when some animation is ended
*/
function animationEnd(evt) {
var eventName = 'status-showed';
if (evt.animationName === 'hide') {
clearHideTimeout();
section.classList.add('hidden');
eventName = 'status-hidden';
}
window.dispatchEvent(new CustomEvent(eventName));
}
/*
* Hides the status component
*/
function hide() {
section.classList.remove('onviewport');
}
/*
* Releases memory
*/
function destroy() {
section.removeEventListener('animationend', animationEnd);
document.body.removeChild(section);
clearHideTimeout();
section = content = null;
}
function getPath() {
return '/js/components/';
}
/*
* Initializes the library. Basically it creates the markup:
*
* <section role="status">
* <p>xxx</p>
* </section>
*/
function initialize() {
if (section) {
return;
}
section = document.createElement('section');
var link = document.createElement('link');
link.type = 'text/css';
link.rel = 'stylesheet';
link.href = getPath() + 'status-behavior.css';
document.head.appendChild(link);
section.setAttribute('role', 'status');
section.classList.add('hidden');
content = document.createElement('p');
section.appendChild(content);
section.addEventListener('animationend', animationEnd);
// to prevent flickering
setTimeout(function append() {
document.body.appendChild(section);
});
}
// Initializing the library
if (document.readyState === 'complete') {
initialize();
} else {
document.addEventListener('DOMContentLoaded', function loaded() {
document.removeEventListener('DOMContentLoaded', loaded);
initialize();
});
}
return {
/*
* The library is auto-initialized but it is for unit testing purposes
*/
init: initialize,
/*
* Shows the status component
*
* @param{Object} Message. It could be a string or a DOMFragment that
* represents the normal and strong strings
*
* @param{int} It defines the time that the status is displayed in ms
*
*/
show: show,
/*
* Hides the status component
*/
hide: hide,
/*
* Releases memory
*/
destroy: destroy,
/*
* Sets up the duration in milliseconds that a status is displayed
*
* @param{int} The time in milliseconds
*
*/
setDuration: function setDuration(time) {
DISPLAYED_TIME = time || DISPLAYED_TIME;
}
};
})();

18
gaia/apps/bookmark/jsdoc.json Executable file
View file

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

View file

@ -0,0 +1,19 @@
# Add and update pinned websites UIs
add-to-home-screen-title=Add to Home Screen
edit-pinned-site-title=Edit Pinned Site
added-to-home-screen-message=Added to home screen
updated-pinned-site=Updated pinned site
done-action=Done
add-action=Add
cancel-action=Cancel
add-application=Add Application
add=Add
application-available=An application is available for this web site.
# Remove pinned websites UI
remove-title=Remove {{name}}
remove-message=Remove {{name}} from system?
remove-action=Remove

View file

@ -0,0 +1,59 @@
{
"name": "Bookmark",
"description": "Bookmark Manager",
"type": "certified",
"role": "system",
"developer": {
"name": "The Gaia Team",
"url": "https://github.com/mozilla-b2g/gaia"
},
"permissions": {
"themeable": {},
"systemXHR": {},
"moz-extremely-unstable-and-will-change-webcomponents": {}
},
"activities": {
"save-bookmark": {
"filters": {
"type": "url",
"url": {
"required": true,
"pattern": "(https?|rtsp|data):.{1,16384}"
}
},
"disposition": "inline",
"href": "/save.html",
"returnValue": true
},
"remove-bookmark": {
"filters": {
"type": "url",
"url": {
"required": true,
"pattern": "(https?|rtsp|data):.{1,16384}"
}
},
"disposition": "inline",
"href": "/remove.html",
"returnValue": true
}
},
"locales": {
"en-US": {
"name": "Bookmark",
"description": "Bookmark Manager"
},
"fr": {
"name": "Marque-pages",
"description": "Gestionnaire de marque-pages"
}
},
"default_locale": "en-US",
"datastores-owned": {
"bookmarks_store": {
"access": "readwrite",
"certified-only": false,
"description": "Stores bookmarks"
}
}
}

10
gaia/apps/bookmark/package.json Executable file
View file

@ -0,0 +1,10 @@
{
"webide": {
"prepackage": {
"command": "make",
"env": ["APP=bookmark"],
"cwd": "../.."
},
"packageDir": "../../build_stage/bookmark/"
}
}

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