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.
15
gaia/.editorconfig
Executable 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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
10
gaia/apps/bluetooth/.jshintrc
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "../../.jshintrc",
|
||||
"predef": [
|
||||
"define",
|
||||
"require",
|
||||
"Promise",
|
||||
"Notification",
|
||||
"console"
|
||||
]
|
||||
}
|
||||
49
gaia/apps/bluetooth/build/bluetooth.build.jslike
Executable 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
|
||||
}
|
||||
43
gaia/apps/bluetooth/build/build.js
Executable 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);
|
||||
});
|
||||
};
|
||||
13
gaia/apps/bluetooth/js/config/require.js
Executable file
|
|
@ -0,0 +1,13 @@
|
|||
require.config({
|
||||
baseUrl: '/js',
|
||||
paths: {
|
||||
'modules': 'modules',
|
||||
'views': 'views',
|
||||
'shared': '../shared/js'
|
||||
},
|
||||
shim: {
|
||||
'shared/async_storage': {
|
||||
exports: 'asyncStorage'
|
||||
}
|
||||
}
|
||||
});
|
||||
398
gaia/apps/bluetooth/js/deviceList.js
Executable 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
|
||||
};
|
||||
})();
|
||||
});
|
||||
111
gaia/apps/bluetooth/js/modules/bluetooth/bluetooth_adapter_manager.js
Executable 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;
|
||||
});
|
||||
198
gaia/apps/bluetooth/js/modules/bluetooth/bluetooth_cod_mapper.js
Executable 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;
|
||||
});
|
||||
953
gaia/apps/bluetooth/js/modules/bluetooth/bluetooth_connection_manager.js
Executable 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;
|
||||
});
|
||||
1026
gaia/apps/bluetooth/js/modules/bluetooth/bluetooth_context.js
Executable file
158
gaia/apps/bluetooth/js/modules/bluetooth/bluetooth_device.js
Executable 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;
|
||||
};
|
||||
});
|
||||
242
gaia/apps/bluetooth/js/modules/mvvm/list_view.js
Executable 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;
|
||||
});
|
||||
102
gaia/apps/bluetooth/js/modules/mvvm/observable.js
Executable 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;
|
||||
});
|
||||
153
gaia/apps/bluetooth/js/modules/mvvm/observable_array.js
Executable 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;
|
||||
});
|
||||
12
gaia/apps/bluetooth/js/modules/navigator/mozBluetooth.js
Executable 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;
|
||||
}
|
||||
});
|
||||
430
gaia/apps/bluetooth/js/modules/pair_manager.js
Executable 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;
|
||||
});
|
||||
349
gaia/apps/bluetooth/js/modules/transfer_manager.js
Executable 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;
|
||||
});
|
||||
901
gaia/apps/bluetooth/js/settings.js
Executable 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
|
||||
});
|
||||
});
|
||||
16
gaia/apps/bluetooth/js/startup_pair.js
Executable 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());
|
||||
});
|
||||
});
|
||||
16
gaia/apps/bluetooth/js/startup_transfer.js
Executable 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
|
|
@ -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
159
gaia/apps/bluetooth/js/views/bt_template_factory.js
Executable 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);
|
||||
};
|
||||
});
|
||||
37
gaia/apps/bluetooth/js/views/cannot_transfer_dialog.js
Executable 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;
|
||||
});
|
||||
413
gaia/apps/bluetooth/js/views/device_picker_panel.js
Executable 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;
|
||||
});
|
||||
39
gaia/apps/bluetooth/js/views/pair_expired_dialog.js
Executable 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;
|
||||
});
|
||||
|
||||
219
gaia/apps/bluetooth/js/views/pair_view.js
Executable 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
51
gaia/apps/bluetooth/js/views/turn_bluetooth_on_dialog.js
Executable 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
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
81
gaia/apps/bluetooth/locales/bluetooth.en-US.properties
Executable 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.
|
||||
81
gaia/apps/bluetooth/manifest.webapp
Executable 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": {}
|
||||
}
|
||||
}
|
||||
36
gaia/apps/bluetooth/message.html
Executable 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
|
|
@ -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>
|
||||
10
gaia/apps/bluetooth/package.json
Executable 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
|
|
@ -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>
|
||||
134
gaia/apps/bluetooth/style/icons.css
Executable 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;
|
||||
}
|
||||
BIN
gaia/apps/bluetooth/style/icons/Bluetooth_120.png
Executable file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
gaia/apps/bluetooth/style/icons/Bluetooth_135.png
Executable file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
gaia/apps/bluetooth/style/icons/Bluetooth_60.png
Executable file
|
After Width: | Height: | Size: 1,018 B |
BIN
gaia/apps/bluetooth/style/icons/Bluetooth_90.png
Executable file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
gaia/apps/bluetooth/style/images/bluetooth_sprite.png
Executable file
|
After Width: | Height: | Size: 14 KiB |
BIN
gaia/apps/bluetooth/style/images/bluetooth_sprite@1.5x.png
Executable file
|
After Width: | Height: | Size: 28 KiB |
BIN
gaia/apps/bluetooth/style/images/bluetooth_sprite@2.25x.png
Executable file
|
After Width: | Height: | Size: 16 KiB |
BIN
gaia/apps/bluetooth/style/images/bluetooth_sprite@2x.png
Executable file
|
After Width: | Height: | Size: 36 KiB |
BIN
gaia/apps/bluetooth/style/images/icon_bluetooth.png
Executable file
|
After Width: | Height: | Size: 622 B |
BIN
gaia/apps/bluetooth/style/images/icon_bluetooth@1.5x.png
Executable file
|
After Width: | Height: | Size: 898 B |
BIN
gaia/apps/bluetooth/style/images/icon_bluetooth@2.25x.png
Executable file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
gaia/apps/bluetooth/style/images/icon_bluetooth@2x.png
Executable file
|
After Width: | Height: | Size: 1.2 KiB |
14
gaia/apps/bluetooth/style/images/settings_bluetooth.svg
Executable 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 |
222
gaia/apps/bluetooth/style/lists.css
Executable 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);
|
||||
}
|
||||
28
gaia/apps/bluetooth/style/message.css
Executable 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;
|
||||
}
|
||||
78
gaia/apps/bluetooth/style/onpair.css
Executable 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;
|
||||
}
|
||||
}
|
||||
132
gaia/apps/bluetooth/style/settings.css
Executable 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;
|
||||
}
|
||||
32
gaia/apps/bluetooth/style/transfer.css
Executable 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;
|
||||
}
|
||||
27
gaia/apps/bluetooth/test/build/integration/bluetooth_test.js
Executable 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
20
gaia/apps/bluetooth/test/unit/.jshintrc
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"extends": "../../../../.jshintrc",
|
||||
"predef": [
|
||||
"assert",
|
||||
"suite",
|
||||
"test",
|
||||
"define",
|
||||
"setup",
|
||||
"teardown",
|
||||
"suiteSetup",
|
||||
"suiteTeardown",
|
||||
"require",
|
||||
"requireApp",
|
||||
"testRequire",
|
||||
"sinon",
|
||||
"mocha",
|
||||
"loadBodyHTML",
|
||||
"Promise"
|
||||
]
|
||||
}
|
||||
14
gaia/apps/bluetooth/test/unit/_message.html
Executable 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>
|
||||
29
gaia/apps/bluetooth/test/unit/_onpair.html
Executable 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>
|
||||
64
gaia/apps/bluetooth/test/unit/_transfer.html
Executable 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>
|
||||
76
gaia/apps/bluetooth/test/unit/deviceList_test.js
Executable 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
12
gaia/apps/bluetooth/test/unit/mock_cannot_transfer_dialog.js
Executable 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;
|
||||
});
|
||||
10
gaia/apps/bluetooth/test/unit/mock_gDeviceList.js
Executable 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() {}
|
||||
};
|
||||
12
gaia/apps/bluetooth/test/unit/mock_pair_expired_dialog.js
Executable 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;
|
||||
});
|
||||
8
gaia/apps/bluetooth/test/unit/mock_pair_manager.js
Executable 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) {}
|
||||
};
|
||||
10
gaia/apps/bluetooth/test/unit/mock_pair_view.js
Executable 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;
|
||||
});
|
||||
12
gaia/apps/bluetooth/test/unit/mock_turn_bluetooth_on_dialog.js
Executable 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;
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
126
gaia/apps/bluetooth/test/unit/modules/bluetooth/bluetooth_cod_mapper_test.js
Executable 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
1682
gaia/apps/bluetooth/test/unit/modules/bluetooth/bluetooth_context_test.js
Executable file
195
gaia/apps/bluetooth/test/unit/modules/bluetooth/bluetooth_device_test.js
Executable 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
618
gaia/apps/bluetooth/test/unit/modules/mvvm/list_view_test.js
Executable 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
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
275
gaia/apps/bluetooth/test/unit/modules/mvvm/observable_array_test.js
Executable 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
110
gaia/apps/bluetooth/test/unit/modules/mvvm/observable_test.js
Executable 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
623
gaia/apps/bluetooth/test/unit/modules/pair_manager_test.js
Executable 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
701
gaia/apps/bluetooth/test/unit/modules/transfer_manager_test.js
Executable 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
43
gaia/apps/bluetooth/test/unit/setup.js
Executable 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));
|
||||
92
gaia/apps/bluetooth/test/unit/views/cannot_transfer_dialog_test.js
Executable 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
310
gaia/apps/bluetooth/test/unit/views/device_picker_panel_test.js
Executable 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
97
gaia/apps/bluetooth/test/unit/views/pair_expired_dialog_test.js
Executable 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
461
gaia/apps/bluetooth/test/unit/views/pair_view_test.js
Executable 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
113
gaia/apps/bluetooth/test/unit/views/turn_bluetooth_on_dialog_test.js
Executable 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
88
gaia/apps/bluetooth/transfer.html
Executable 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>
|
||||
54
gaia/apps/bookmark/js/activity_handler.js
Executable 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');
|
||||
}
|
||||
});
|
||||
175
gaia/apps/bookmark/js/bookmark_editor.js
Executable 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));
|
||||
}
|
||||
};
|
||||
39
gaia/apps/bookmark/js/bookmark_remover.js
Executable 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);
|
||||
}
|
||||
};
|
||||
32
gaia/apps/bookmark/js/components/status-behavior.css
Executable 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%)
|
||||
}
|
||||
}
|
||||
177
gaia/apps/bookmark/js/components/status.js
Executable 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
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
19
gaia/apps/bookmark/locales/bookmark.en-US.properties
Executable 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
|
||||
59
gaia/apps/bookmark/manifest.webapp
Executable 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
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"webide": {
|
||||
"prepackage": {
|
||||
"command": "make",
|
||||
"env": ["APP=bookmark"],
|
||||
"cwd": "../.."
|
||||
},
|
||||
"packageDir": "../../build_stage/bookmark/"
|
||||
}
|
||||
}
|
||||