зеркало из https://github.com/mozilla/gecko-dev.git
Merge mozilla-central to mozilla-inbound
--HG-- extra : rebase_source : ce63e63e1b27245eddc3f65e784abd96580c47f7
This commit is contained in:
Коммит
61766bf62f
|
@ -32,5 +32,5 @@ exports.locale = function locale() {
|
|||
// Returns the short locale code: ja, en, fr
|
||||
exports.language = function language() {
|
||||
return bestMatchingLocale ? bestMatchingLocale.split("-")[0].toLowerCase()
|
||||
: null;
|
||||
: "en";
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<project name="platform_build" path="build" remote="b2g" revision="e935894ef5f27e2f04b9e929a45a958e6288a223">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="891798f1e345bc2b69e71de42bd524a90b1745c4"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="b81185d30e548f782770b852473ffb53c641a490"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<project name="platform_build" path="build" remote="b2g" revision="e935894ef5f27e2f04b9e929a45a958e6288a223">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="891798f1e345bc2b69e71de42bd524a90b1745c4"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="b81185d30e548f782770b852473ffb53c641a490"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
|
||||
|
|
|
@ -19,11 +19,11 @@
|
|||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="891798f1e345bc2b69e71de42bd524a90b1745c4"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="b81185d30e548f782770b852473ffb53c641a490"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
|
||||
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="2d58f4b9206b50b8fda0d5036da6f0c62608db7c"/>
|
||||
<project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="d70e4bfdcb65e7514de0f9315b74aea1c811678d"/>
|
||||
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="27eb2f04e149fc2c9976d881b1b5984bbe7ee089"/>
|
||||
<project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="067c08fb3e5744b42b68d1f861245f7d507109bc"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
|
||||
<!-- Stock Android things -->
|
||||
|
@ -98,7 +98,7 @@
|
|||
<project name="platform/system/vold" path="system/vold" revision="919829940468066a32f403980b43f6ebfee5d314"/>
|
||||
<!-- Emulator specific things -->
|
||||
<project name="android-development" path="development" remote="b2g" revision="2bdf22305b523af644e1891b4ddfd9229336d0ce"/>
|
||||
<project name="device_generic_goldfish" path="device/generic/goldfish" remote="b2g" revision="0d5c43228006bae775c4cb57a6d3908484d41718"/>
|
||||
<project name="device_generic_goldfish" path="device/generic/goldfish" remote="b2g" revision="fc5f390fa314385e2a84629ea88284a60b40f7c4"/>
|
||||
<project name="platform/external/iproute2" path="external/iproute2" revision="c66c5716d5335e450f7a7b71ccc6a604fb2f41d2"/>
|
||||
<project name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="d2685281e2e54ca14d1df304867aa82c37b27162"/>
|
||||
<project name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="627f9b20fc518937b93747a7ff1ed4f5ed46e06f"/>
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</project>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="891798f1e345bc2b69e71de42bd524a90b1745c4"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="b81185d30e548f782770b852473ffb53c641a490"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="c6cace53426b5be7e56c0fd202118009689bc707"/>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<project name="platform_build" path="build" remote="b2g" revision="e935894ef5f27e2f04b9e929a45a958e6288a223">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="891798f1e345bc2b69e71de42bd524a90b1745c4"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="b81185d30e548f782770b852473ffb53c641a490"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
|
||||
|
|
|
@ -12,10 +12,10 @@
|
|||
<!--original fetch url was https://git.mozilla.org/releases-->
|
||||
<remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
|
||||
<!-- B2G specific things. -->
|
||||
<project name="platform_build" path="build" remote="b2g" revision="05a36844c1046a1eb07d5b1325f85ed741f961ea">
|
||||
<project name="platform_build" path="build" remote="b2g" revision="c9d4fe680662ee44a4bdea42ae00366f5df399cf">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="891798f1e345bc2b69e71de42bd524a90b1745c4"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="b81185d30e548f782770b852473ffb53c641a490"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
|
||||
|
@ -141,7 +141,7 @@
|
|||
<default remote="caf" revision="refs/tags/android-5.1.0_r1" sync-j="4"/>
|
||||
<!-- Emulator specific things -->
|
||||
<project name="device/generic/armv7-a-neon" path="device/generic/armv7-a-neon" revision="fe7df1bc8dd0fd71571505d7be1c31a4ad1e40fb"/>
|
||||
<project name="device_generic_goldfish" path="device/generic/goldfish" remote="b2g" revision="48132ec0b0dfe9fc29c7c3f0e799066be8999198"/>
|
||||
<project name="device_generic_goldfish" path="device/generic/goldfish" remote="b2g" revision="dc8c7896562bf63190befb3e6b21310a4b7144fa"/>
|
||||
<project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="59e434cbecc02653f44cedeb2ef5cc88dc8bb61b"/>
|
||||
<project name="platform_external_wpa_supplicant_8" path="external/wpa_supplicant_8" remote="b2g" revision="cbda29a58abc4ea1f7f4611fe354ab67b606219d"/>
|
||||
<project name="platform/development" path="development" revision="0c51f6e0aa2ee57fcb75ec3b2ff6bf754cece63e"/>
|
||||
|
|
|
@ -19,11 +19,11 @@
|
|||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="891798f1e345bc2b69e71de42bd524a90b1745c4"/>
|
||||
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="b81185d30e548f782770b852473ffb53c641a490"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
|
||||
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="2d58f4b9206b50b8fda0d5036da6f0c62608db7c"/>
|
||||
<project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="d70e4bfdcb65e7514de0f9315b74aea1c811678d"/>
|
||||
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="27eb2f04e149fc2c9976d881b1b5984bbe7ee089"/>
|
||||
<project name="platform_external_qemu" path="external/qemu" remote="b2g" revision="067c08fb3e5744b42b68d1f861245f7d507109bc"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="34ea6163f9f0e0122fb0bb03607eccdca31ced7a"/>
|
||||
<!-- Stock Android things -->
|
||||
|
@ -98,7 +98,7 @@
|
|||
<project name="platform/system/vold" path="system/vold" revision="919829940468066a32f403980b43f6ebfee5d314"/>
|
||||
<!-- Emulator specific things -->
|
||||
<project name="android-development" path="development" remote="b2g" revision="2bdf22305b523af644e1891b4ddfd9229336d0ce"/>
|
||||
<project name="device_generic_goldfish" path="device/generic/goldfish" remote="b2g" revision="0d5c43228006bae775c4cb57a6d3908484d41718"/>
|
||||
<project name="device_generic_goldfish" path="device/generic/goldfish" remote="b2g" revision="fc5f390fa314385e2a84629ea88284a60b40f7c4"/>
|
||||
<project name="platform/external/iproute2" path="external/iproute2" revision="c66c5716d5335e450f7a7b71ccc6a604fb2f41d2"/>
|
||||
<project name="platform/prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/i686-linux-glibc2.7-4.6" revision="d2685281e2e54ca14d1df304867aa82c37b27162"/>
|
||||
<project name="platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" path="prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.7-4.6" revision="627f9b20fc518937b93747a7ff1ed4f5ed46e06f"/>
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<project name="platform_build" path="build" remote="b2g" revision="e935894ef5f27e2f04b9e929a45a958e6288a223">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="891798f1e345bc2b69e71de42bd524a90b1745c4"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="b81185d30e548f782770b852473ffb53c641a490"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"git": {
|
||||
"git_revision": "891798f1e345bc2b69e71de42bd524a90b1745c4",
|
||||
"git_revision": "b81185d30e548f782770b852473ffb53c641a490",
|
||||
"remote": "https://git.mozilla.org/releases/gaia.git",
|
||||
"branch": ""
|
||||
},
|
||||
"revision": "8c6b450b2f9f4606f164c33513a023e59271e1e4",
|
||||
"revision": "d0a1f4d22e1a40cb89f9d592334e7506ee1317bd",
|
||||
"repo_path": "integration/gaia-central"
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
</project>
|
||||
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="891798f1e345bc2b69e71de42bd524a90b1745c4"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="b81185d30e548f782770b852473ffb53c641a490"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
|
||||
<project name="moztt" path="external/moztt" remote="b2g" revision="f5d65f5b17d9766d7925aefd0486a1e526ae9bf0"/>
|
||||
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="c6cace53426b5be7e56c0fd202118009689bc707"/>
|
||||
|
|
|
@ -12,10 +12,10 @@
|
|||
<!--original fetch url was https://git.mozilla.org/releases-->
|
||||
<remote fetch="https://git.mozilla.org/releases" name="mozillaorg"/>
|
||||
<!-- B2G specific things. -->
|
||||
<project name="platform_build" path="build" remote="b2g" revision="05a36844c1046a1eb07d5b1325f85ed741f961ea">
|
||||
<project name="platform_build" path="build" remote="b2g" revision="c9d4fe680662ee44a4bdea42ae00366f5df399cf">
|
||||
<copyfile dest="Makefile" src="core/root.mk"/>
|
||||
</project>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="891798f1e345bc2b69e71de42bd524a90b1745c4"/>
|
||||
<project name="gaia" path="gaia" remote="mozillaorg" revision="b81185d30e548f782770b852473ffb53c641a490"/>
|
||||
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
|
||||
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="e9e2923fd6cab93cf88b4b9ada82225e44fe6635"/>
|
||||
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>
|
||||
|
|
|
@ -103,9 +103,11 @@ let gSyncUI = {
|
|||
// We want to treat "account needs verification" as "needs setup". So
|
||||
// "reach in" to Weave.Status._authManager to check whether we the signed-in
|
||||
// user is verified.
|
||||
// Referencing Weave.Status spins a nested event loop to initialize the
|
||||
// authManager, so this should always return a value directly.
|
||||
// This only applies to fxAccounts-based Sync.
|
||||
// NOTE: We used to have this _authManager hack to avoid a nested
|
||||
// event-loop from querying Weave.Status.checkSetup() - while that's no
|
||||
// longer true, we do still have the FxA-specific requirement of checking
|
||||
// the verified state - so the hack remains. We should consider refactoring
|
||||
// Sync's "check setup" capabilities to take this into account at some point...
|
||||
if (Weave.Status._authManager._signedInUser !== undefined) {
|
||||
// If we have a signed in user already, and that user is not verified,
|
||||
// revert to the "needs setup" state.
|
||||
|
|
|
@ -340,7 +340,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|||
let engine = Services.search.getEngineByName(action.params.engineName);
|
||||
let query = action.params.searchSuggestion ||
|
||||
action.params.searchQuery;
|
||||
let submission = engine.getSubmission(query);
|
||||
let submission = engine.getSubmission(query, null, "keyword");
|
||||
|
||||
url = submission.uri.spec;
|
||||
postData = submission.postData;
|
||||
|
@ -1390,7 +1390,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|||
let engine = Services.search.getEngineByName(action.params.engineName);
|
||||
let query = action.params.searchSuggestion ||
|
||||
action.params.searchQuery;
|
||||
let submission = engine.getSubmission(query);
|
||||
let submission = engine.getSubmission(query, null, "keyword");
|
||||
url = submission.uri.spec;
|
||||
options.postData = submission.postData;
|
||||
break;
|
||||
|
|
|
@ -293,6 +293,15 @@ html[dir="rtl"] .tab-view li:nth-child(2).selected ~ .slide-bar {
|
|||
font-size: 2.2rem;
|
||||
}
|
||||
|
||||
.room-list-loading {
|
||||
margin: 5rem 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.room-list-loading > img {
|
||||
width: 66px;
|
||||
}
|
||||
|
||||
|
||||
/* Rooms */
|
||||
.rooms {
|
||||
|
@ -321,10 +330,10 @@ html[dir="rtl"] .tab-view li:nth-child(2).selected ~ .slide-bar {
|
|||
}
|
||||
|
||||
.new-room-view > .context {
|
||||
border-top: 1px solid #ebebeb;
|
||||
flex: 1;
|
||||
border-radius: 3px 3px 0 0;
|
||||
margin: 1rem 0 .5rem;
|
||||
padding: 1rem 15px;
|
||||
margin: .5rem 0;
|
||||
padding: .5rem 15px 1rem;
|
||||
}
|
||||
|
||||
.new-room-view > .context > .context-enabled {
|
||||
|
|
|
@ -809,6 +809,21 @@ loop.panel = (function(_, mozL10n) {
|
|||
mozL10n.get("display_name_guest");
|
||||
},
|
||||
|
||||
/**
|
||||
* Let the user know we're loading rooms
|
||||
* @returns {Object} React render
|
||||
*/
|
||||
_renderLoadingRoomsView: function() {
|
||||
return (
|
||||
React.createElement("div", {className: "room-list"},
|
||||
React.createElement("div", {className: "room-list-loading"},
|
||||
React.createElement("img", {src: "loop/shared/img/animated-spinner.svg"})
|
||||
),
|
||||
this._renderNewRoomButton()
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
_renderNoRoomsView: function() {
|
||||
return (
|
||||
React.createElement("div", {className: "room-list"},
|
||||
|
@ -841,6 +856,10 @@ loop.panel = (function(_, mozL10n) {
|
|||
console.error("RoomList error", this.state.error);
|
||||
}
|
||||
|
||||
if (this.state.pendingInitialRetrieval) {
|
||||
return this._renderLoadingRoomsView();
|
||||
}
|
||||
|
||||
if (!this.state.rooms.length) {
|
||||
return this._renderNoRoomsView();
|
||||
}
|
||||
|
|
|
@ -809,6 +809,21 @@ loop.panel = (function(_, mozL10n) {
|
|||
mozL10n.get("display_name_guest");
|
||||
},
|
||||
|
||||
/**
|
||||
* Let the user know we're loading rooms
|
||||
* @returns {Object} React render
|
||||
*/
|
||||
_renderLoadingRoomsView: function() {
|
||||
return (
|
||||
<div className="room-list">
|
||||
<div className="room-list-loading">
|
||||
<img src="loop/shared/img/animated-spinner.svg" />
|
||||
</div>
|
||||
{this._renderNewRoomButton()}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
_renderNoRoomsView: function() {
|
||||
return (
|
||||
<div className="room-list">
|
||||
|
@ -841,6 +856,10 @@ loop.panel = (function(_, mozL10n) {
|
|||
console.error("RoomList error", this.state.error);
|
||||
}
|
||||
|
||||
if (this.state.pendingInitialRetrieval) {
|
||||
return this._renderLoadingRoomsView();
|
||||
}
|
||||
|
||||
if (!this.state.rooms.length) {
|
||||
return this._renderNoRoomsView();
|
||||
}
|
||||
|
|
|
@ -114,7 +114,7 @@ loop.store = loop.store || {};
|
|||
activeRoom: this.activeRoomStore ? this.activeRoomStore.getStoreState() : {},
|
||||
error: null,
|
||||
pendingCreation: false,
|
||||
pendingInitialRetrieval: false,
|
||||
pendingInitialRetrieval: true,
|
||||
rooms: [],
|
||||
savingContext: false
|
||||
};
|
||||
|
|
|
@ -593,8 +593,20 @@ loop.roomViews = (function(mozL10n) {
|
|||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the invitation controls should be shown.
|
||||
*
|
||||
* @return {Boolean} True if there's no guests.
|
||||
*/
|
||||
_shouldRenderInvitationOverlay: function() {
|
||||
return (this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS);
|
||||
var hasGuests = typeof this.state.participants === "object" &&
|
||||
this.state.participants.filter(function(participant) {
|
||||
return !participant.owner;
|
||||
}).length > 0;
|
||||
|
||||
// Don't show if the room has participants whether from the room state or
|
||||
// there being non-owner guests in the participants array.
|
||||
return this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS && !hasGuests;
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -593,8 +593,20 @@ loop.roomViews = (function(mozL10n) {
|
|||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine if the invitation controls should be shown.
|
||||
*
|
||||
* @return {Boolean} True if there's no guests.
|
||||
*/
|
||||
_shouldRenderInvitationOverlay: function() {
|
||||
return (this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS);
|
||||
var hasGuests = typeof this.state.participants === "object" &&
|
||||
this.state.participants.filter(function(participant) {
|
||||
return !participant.owner;
|
||||
}).length > 0;
|
||||
|
||||
// Don't show if the room has participants whether from the room state or
|
||||
// there being non-owner guests in the participants array.
|
||||
return this.state.roomState !== ROOM_STATES.HAS_PARTICIPANTS && !hasGuests;
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill="#0095FF"><path opacity=".15" d="M4.9 11.8c.2-.4.7-.5 1.1-.3.4.2.5.7.3 1.1l-1.2 2.1c-.2.3-.7.5-1.1.2-.4-.2-.5-.7-.3-1.1l1.2-2z"><animate attributeType="CSS" attributeName="opacity" from="1" to=".15" dur="0.96s" repeatCount="indefinite"/></path><path opacity=".2" d="M3.4 9.7c.4-.2.9-.1 1.1.3.2.4.1.9-.3 1.1l-2.1 1.2c-.4.2-.9.1-1.1-.3-.2-.4-.1-.9.3-1.1l2.1-1.2z"><animate attributeType="CSS" attributeName="opacity" from="1" to=".15" dur="0.96s" repeatCount="indefinite" begin="0.08s"/></path><path opacity=".25" d="M3.2 7.2c.4 0 .8.4.8.8s-.4.8-.8.8H.8C.4 8.8 0 8.4 0 8s.4-.8.8-.8h2.4z"><animate attributeType="CSS" attributeName="opacity" from="1" to=".15" dur="0.96s" repeatCount="indefinite" begin="0.16s"/></path><path opacity=".35" d="M1.4 5.1C1 4.9.8 4.4 1.1 4c.2-.4.7-.5 1.1-.3l2.1 1.2c.3.2.5.7.2 1.1-.2.4-.7.5-1.1.3l-2-1.2z"><animate attributeType="CSS" attributeName="opacity" from="1" to=".15" dur="0.96s" repeatCount="indefinite" begin="0.24s"/></path><path opacity=".45" d="M3.7 2.2c-.2-.4-.1-.9.3-1.1.4-.2.9-.1 1.1.3l1.2 2.1c.2.3.1.8-.3 1-.4.3-.9.1-1.1-.3l-1.2-2z"><animate attributeType="CSS" attributeName="opacity" from="1" to=".15" dur="0.96s" repeatCount="indefinite" begin="0.32s"/></path><path opacity=".5" d="M8.8 3.2c0 .4-.4.8-.8.8s-.8-.4-.8-.8V.8c0-.4.4-.8.8-.8s.8.4.8.8v2.4z"><animate attributeType="CSS" attributeName="opacity" from="1" to=".15" dur="0.96s" repeatCount="indefinite" begin="0.40s"/></path><path opacity=".55" d="M10.9 1.4c.2-.4.7-.6 1.1-.3.4.2.5.7.3 1.1l-1.2 2.1c-.2.4-.7.5-1.1.3-.4-.3-.5-.8-.3-1.2l1.2-2z"><animate attributeType="CSS" attributeName="opacity" from="1" to=".15" dur="0.96s" repeatCount="indefinite" begin="0.48s"/></path><path opacity=".6" d="M13.8 3.7c.4-.2.9-.1 1.1.3.2.4.1.9-.3 1.1l-2.1 1.2c-.4.2-.9.1-1.1-.3-.2-.4-.1-.9.3-1.1l2.1-1.2z"><animate attributeType="CSS" attributeName="opacity" from="1" to=".15" dur="0.96s" repeatCount="indefinite" begin="0.56s"/></path><path opacity=".65" d="M15.2 7.2c.4 0 .8.4.8.8s-.4.8-.8.8h-2.4c-.4 0-.8-.4-.8-.8s.4-.8.8-.8h2.4z"><animate attributeType="CSS" attributeName="opacity" from="1" to=".15" dur="0.96s" repeatCount="indefinite" begin="0.64s"/></path><path opacity=".7" d="M11.8 11.1c-.4-.2-.5-.7-.3-1.1.2-.4.7-.5 1.1-.3l2.1 1.2c.4.2.5.7.3 1.1-.2.4-.7.5-1.1.3l-2.1-1.2z"><animate attributeType="CSS" attributeName="opacity" from="1" to=".15" dur="0.96s" repeatCount="indefinite" begin="0.70s"/></path><path opacity=".8" d="M9.7 12.6c-.2-.4-.1-.9.3-1.1.4-.2.9-.1 1.1.3l1.2 2.1c.2.4.1.9-.3 1.1-.4.2-.9.1-1.1-.3l-1.2-2.1z"><animate attributeType="CSS" attributeName="opacity" from="1" to=".15" dur="0.96s" repeatCount="indefinite" begin="0.78s"/></path><path d="M8.8 15.2c0 .4-.4.8-.8.8s-.8-.4-.8-.8v-2.4c0-.4.4-.8.8-.8s.8.4.8.8v2.4z"><animate attributeType="CSS" attributeName="opacity" from="1" to=".15" dur="0.96s" repeatCount="indefinite" begin="0.86s"/></path></g><path fill-rule="evenodd" clip-rule="evenodd" fill="#0095FF" d="M8 8.7c-1.5 0-2.6-.5-2.6-.5S5.9 10 8 10s2.6-1.9 2.6-1.9-1.1.6-2.6.6zM9.3 6c.4 0 .7.3.7.7s-.3.7-.7.7-.7-.3-.7-.7.3-.7.7-.7zM6.7 6c.4 0 .7.3.7.7s-.3.7-.7.7-.7-.3-.7-.7.3-.7.7-.7z"/></svg>
|
После Ширина: | Высота: | Размер: 3.1 KiB |
|
@ -51,6 +51,7 @@ loop.store.ActiveRoomStore = (function() {
|
|||
var OPTIONAL_ROOMINFO_FIELDS = {
|
||||
urls: "roomContextUrls",
|
||||
description: "roomDescription",
|
||||
participants: "participants",
|
||||
roomInfoFailure: "roomInfoFailure",
|
||||
roomName: "roomName",
|
||||
roomState: "roomState"
|
||||
|
@ -296,6 +297,7 @@ loop.store.ActiveRoomStore = (function() {
|
|||
}
|
||||
|
||||
this.dispatchAction(new sharedActions.SetupRoomInfo({
|
||||
participants: roomData.participants,
|
||||
roomToken: actionData.roomToken,
|
||||
roomContextUrls: roomData.decryptedContext.urls,
|
||||
roomDescription: roomData.decryptedContext.description,
|
||||
|
@ -418,6 +420,7 @@ loop.store.ActiveRoomStore = (function() {
|
|||
}
|
||||
|
||||
this.setStoreState({
|
||||
participants: actionData.participants,
|
||||
roomContextUrls: actionData.roomContextUrls,
|
||||
roomDescription: actionData.roomDescription,
|
||||
roomName: actionData.roomName,
|
||||
|
@ -449,7 +452,7 @@ loop.store.ActiveRoomStore = (function() {
|
|||
// Iterate over the optional fields that _may_ be present on the actionData
|
||||
// object.
|
||||
Object.keys(OPTIONAL_ROOMINFO_FIELDS).forEach(function(field) {
|
||||
if (actionData[field]) {
|
||||
if (actionData[field] !== undefined) {
|
||||
newState[OPTIONAL_ROOMINFO_FIELDS[field]] = actionData[field];
|
||||
}
|
||||
});
|
||||
|
@ -478,6 +481,7 @@ loop.store.ActiveRoomStore = (function() {
|
|||
this.dispatchAction(new sharedActions.UpdateRoomInfo({
|
||||
urls: roomData.decryptedContext.urls,
|
||||
description: roomData.decryptedContext.description,
|
||||
participants: roomData.participants,
|
||||
roomName: roomData.decryptedContext.roomName,
|
||||
roomUrl: roomData.roomUrl
|
||||
}));
|
||||
|
@ -792,7 +796,16 @@ loop.store.ActiveRoomStore = (function() {
|
|||
* one participantleaves.
|
||||
*/
|
||||
remotePeerDisconnected: function() {
|
||||
// Update the participants to just the owner.
|
||||
var participants = this.getStoreState("participants");
|
||||
if (participants) {
|
||||
participants = participants.filter(function(participant) {
|
||||
return participant.owner;
|
||||
});
|
||||
}
|
||||
|
||||
this.setStoreState({
|
||||
participants: participants,
|
||||
roomState: ROOM_STATES.SESSION_CONNECTED,
|
||||
remoteSrcVideoObject: null
|
||||
});
|
||||
|
|
|
@ -94,6 +94,7 @@ browser.jar:
|
|||
content/browser/loop/shared/img/empty_contacts.svg (content/shared/img/empty_contacts.svg)
|
||||
content/browser/loop/shared/img/empty_conversations.svg (content/shared/img/empty_conversations.svg)
|
||||
content/browser/loop/shared/img/empty_search.svg (content/shared/img/empty_search.svg)
|
||||
content/browser/loop/shared/img/animated-spinner.svg (content/shared/img/animated-spinner.svg)
|
||||
content/browser/loop/shared/img/avatars.svg (content/shared/img/avatars.svg)
|
||||
content/browser/loop/shared/img/firefox-avatar.svg (content/shared/img/firefox-avatar.svg)
|
||||
|
||||
|
|
|
@ -850,6 +850,13 @@ describe("loop.panel", function() {
|
|||
sinon.assert.calledWithExactly(document.mozL10n.get,
|
||||
"no_conversations_message_heading");
|
||||
});
|
||||
|
||||
it("should display a loading animation when rooms are pending", function() {
|
||||
var view = createTestComponent();
|
||||
roomStore.setStoreState({pendingInitialRetrieval: true});
|
||||
|
||||
expect(view.getDOMNode().querySelectorAll(".room-list-loading").length).to.eql(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("loop.panel.NewRoomView", function() {
|
||||
|
|
|
@ -69,7 +69,7 @@ describe("loop.store.RoomStore", function () {
|
|||
var defaultStoreState = {
|
||||
error: undefined,
|
||||
pendingCreation: false,
|
||||
pendingInitialRetrieval: false,
|
||||
pendingInitialRetrieval: true,
|
||||
rooms: [],
|
||||
activeRoom: {}
|
||||
};
|
||||
|
|
|
@ -470,8 +470,51 @@ describe("loop.roomViews", function () {
|
|||
|
||||
view = mountTestComponent();
|
||||
|
||||
expect(TestUtils.findRenderedComponentWithType(view,
|
||||
loop.roomViews.DesktopRoomInvitationView).getDOMNode()).to.not.eql(null);
|
||||
});
|
||||
|
||||
it("should render the DesktopRoomInvitationView if roomState is `JOINED` with just owner",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({
|
||||
participants: [{owner: true}],
|
||||
roomState: ROOM_STATES.JOINED
|
||||
});
|
||||
|
||||
view = mountTestComponent();
|
||||
|
||||
expect(TestUtils.findRenderedComponentWithType(view,
|
||||
loop.roomViews.DesktopRoomInvitationView).getDOMNode()).to.not.eql(null);
|
||||
});
|
||||
|
||||
it("should render the DesktopRoomConversationView if roomState is `JOINED` with remote participant",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({
|
||||
participants: [{}],
|
||||
roomState: ROOM_STATES.JOINED
|
||||
});
|
||||
|
||||
view = mountTestComponent();
|
||||
|
||||
TestUtils.findRenderedComponentWithType(view,
|
||||
loop.roomViews.DesktopRoomInvitationView);
|
||||
loop.roomViews.DesktopRoomConversationView);
|
||||
expect(TestUtils.findRenderedComponentWithType(view,
|
||||
loop.roomViews.DesktopRoomInvitationView).getDOMNode()).to.eql(null);
|
||||
});
|
||||
|
||||
it("should render the DesktopRoomConversationView if roomState is `JOINED` with participants",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({
|
||||
participants: [{owner: true}, {}],
|
||||
roomState: ROOM_STATES.JOINED
|
||||
});
|
||||
|
||||
view = mountTestComponent();
|
||||
|
||||
TestUtils.findRenderedComponentWithType(view,
|
||||
loop.roomViews.DesktopRoomConversationView);
|
||||
expect(TestUtils.findRenderedComponentWithType(view,
|
||||
loop.roomViews.DesktopRoomInvitationView).getDOMNode()).to.eql(null);
|
||||
});
|
||||
|
||||
it("should render the DesktopRoomConversationView if roomState is `HAS_PARTICIPANTS`",
|
||||
|
@ -482,6 +525,8 @@ describe("loop.roomViews", function () {
|
|||
|
||||
TestUtils.findRenderedComponentWithType(view,
|
||||
loop.roomViews.DesktopRoomConversationView);
|
||||
expect(TestUtils.findRenderedComponentWithType(view,
|
||||
loop.roomViews.DesktopRoomInvitationView).getDOMNode()).to.eql(null);
|
||||
});
|
||||
|
||||
it("should call onCallTerminated when the call ended", function() {
|
||||
|
|
|
@ -310,6 +310,7 @@ describe("loop.store.ActiveRoomStore", function () {
|
|||
decryptedContext: {
|
||||
roomName: "Monkeys"
|
||||
},
|
||||
participants: [],
|
||||
roomUrl: "http://invalid"
|
||||
};
|
||||
|
||||
|
@ -350,6 +351,7 @@ describe("loop.store.ActiveRoomStore", function () {
|
|||
new sharedActions.SetupRoomInfo({
|
||||
roomContextUrls: undefined,
|
||||
roomDescription: undefined,
|
||||
participants: [],
|
||||
roomToken: fakeToken,
|
||||
roomName: fakeRoomData.decryptedContext.roomName,
|
||||
roomUrl: fakeRoomData.roomUrl,
|
||||
|
@ -1277,6 +1279,30 @@ describe("loop.store.ActiveRoomStore", function () {
|
|||
|
||||
expect(store.getStoreState().remoteSrcVideoObject).eql(null);
|
||||
});
|
||||
|
||||
it("should remove non-owner participants", function() {
|
||||
store.setStoreState({
|
||||
participants: [{owner: true}, {}]
|
||||
});
|
||||
|
||||
store.remotePeerDisconnected();
|
||||
|
||||
var participants = store.getStoreState().participants;
|
||||
expect(participants).to.have.length.of(1);
|
||||
expect(participants[0].owner).eql(true);
|
||||
});
|
||||
|
||||
it("should keep the owner participant", function() {
|
||||
store.setStoreState({
|
||||
participants: [{owner: true}]
|
||||
});
|
||||
|
||||
store.remotePeerDisconnected();
|
||||
|
||||
var participants = store.getStoreState().participants;
|
||||
expect(participants).to.have.length.of(1);
|
||||
expect(participants[0].owner).eql(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#connectionStatus", function() {
|
||||
|
@ -1518,6 +1544,7 @@ describe("loop.store.ActiveRoomStore", function () {
|
|||
sinon.assert.calledWithExactly(dispatcher.dispatch,
|
||||
new sharedActions.UpdateRoomInfo({
|
||||
description: "fakeDescription",
|
||||
participants: undefined,
|
||||
roomName: fakeRoomData.decryptedContext.roomName,
|
||||
roomUrl: fakeRoomData.roomUrl,
|
||||
urls: {
|
||||
|
|
|
@ -186,6 +186,17 @@ let gSyncPane = {
|
|||
}
|
||||
},
|
||||
|
||||
_closeSyncStatusMessageBox: function() {
|
||||
document.getElementById("syncStatusMessage").removeAttribute("message-type");
|
||||
document.getElementById("syncStatusMessageTitle").textContent = "";
|
||||
document.getElementById("syncStatusMessageDescription").textContent = "";
|
||||
let learnMoreLink = document.getElementById("learnMoreLink");
|
||||
if (learnMoreLink) {
|
||||
learnMoreLink.parentNode.removeChild(learnMoreLink);
|
||||
}
|
||||
document.getElementById("sync-migration-buttons-deck").hidden = true;
|
||||
},
|
||||
|
||||
_setupEventListeners: function() {
|
||||
function setEventListener(aId, aEventType, aCallback)
|
||||
{
|
||||
|
@ -193,6 +204,9 @@ let gSyncPane = {
|
|||
.addEventListener(aEventType, aCallback.bind(gSyncPane));
|
||||
}
|
||||
|
||||
setEventListener("syncStatusMessageClose", "command", function () {
|
||||
gSyncPane._closeSyncStatusMessageBox();
|
||||
});
|
||||
setEventListener("noAccountSetup", "click", function (aEvent) {
|
||||
aEvent.stopPropagation();
|
||||
gSyncPane.openSetup(null);
|
||||
|
@ -431,16 +445,17 @@ let gSyncPane = {
|
|||
},
|
||||
|
||||
updateMigrationState: function(subject, state) {
|
||||
this._closeSyncStatusMessageBox();
|
||||
let selIndex;
|
||||
let sb = this._accountsStringBundle;
|
||||
switch (state) {
|
||||
case fxaMigrator.STATE_USER_FXA: {
|
||||
let sb = this._accountsStringBundle;
|
||||
// There are 2 cases here - no email address means it is an offer on
|
||||
// the first device (so the user is prompted to create an account).
|
||||
// If there is an email address it is the "join the party" flow, so the
|
||||
// user is prompted to sign in with the address they previously used.
|
||||
let email = subject ? subject.QueryInterface(Components.interfaces.nsISupportsString).data : null;
|
||||
let elt = document.getElementById("sync-migrate-upgrade-description");
|
||||
let elt = document.getElementById("syncStatusMessageDescription");
|
||||
elt.textContent = email ?
|
||||
sb.formatStringFromName("signInAfterUpgradeOnOtherDevice.description",
|
||||
[email], 1) :
|
||||
|
@ -449,11 +464,12 @@ let gSyncPane = {
|
|||
// The "Learn more" link.
|
||||
if (!email) {
|
||||
let learnMoreLink = document.createElement("label");
|
||||
learnMoreLink.id = "learnMoreLink";
|
||||
learnMoreLink.className = "text-link";
|
||||
let { text, href } = fxaMigrator.learnMoreLink;
|
||||
learnMoreLink.setAttribute("value", text);
|
||||
learnMoreLink.href = href;
|
||||
elt.appendChild(learnMoreLink);
|
||||
elt.parentNode.insertBefore(learnMoreLink, elt.nextSibling);
|
||||
}
|
||||
|
||||
// The "upgrade" button.
|
||||
|
@ -481,7 +497,7 @@ let gSyncPane = {
|
|||
let sb = this._accountsStringBundle;
|
||||
let email = subject.QueryInterface(Components.interfaces.nsISupportsString).data;
|
||||
let label = sb.formatStringFromName("needVerifiedUserLong", [email], 1);
|
||||
let elt = document.getElementById("sync-migrate-verify-label");
|
||||
let elt = document.getElementById("syncStatusMessageDescription");
|
||||
elt.setAttribute("value", label);
|
||||
// The "resend" button.
|
||||
let button = document.getElementById("sync-migrate-resend");
|
||||
|
@ -501,8 +517,8 @@ let gSyncPane = {
|
|||
document.getElementById("sync-migration").hidden = true;
|
||||
return;
|
||||
}
|
||||
document.getElementById("sync-migration").hidden = false;
|
||||
document.getElementById("sync-migration-deck").selectedIndex = selIndex;
|
||||
document.getElementById("sync-migration-buttons-deck").selectedIndex = selIndex;
|
||||
document.getElementById("syncStatusMessage").setAttribute("message-type", "migration");
|
||||
},
|
||||
|
||||
// Called whenever one of the sync engine preferences is changed.
|
||||
|
@ -678,23 +694,39 @@ let gSyncPane = {
|
|||
},
|
||||
|
||||
verifyFirefoxAccount: function() {
|
||||
fxAccounts.resendVerificationEmail().then(() => {
|
||||
fxAccounts.getSignedInUser().then(data => {
|
||||
let sb = this._accountsStringBundle;
|
||||
let title = sb.GetStringFromName("verificationSentTitle");
|
||||
let heading = sb.formatStringFromName("verificationSentHeading",
|
||||
[data.email], 1);
|
||||
let description = sb.GetStringFromName("verificationSentDescription");
|
||||
this._closeSyncStatusMessageBox();
|
||||
let changesyncStatusMessage = (data) => {
|
||||
let isError = !data;
|
||||
let syncStatusMessage = document.getElementById("syncStatusMessage");
|
||||
let syncStatusMessageTitle = document.getElementById("syncStatusMessageTitle");
|
||||
let syncStatusMessageDescription = document.getElementById("syncStatusMessageDescription");
|
||||
let maybeNot = isError ? "Not" : "";
|
||||
let sb = this._accountsStringBundle;
|
||||
let title = sb.GetStringFromName("verification" + maybeNot + "SentTitle");
|
||||
let email = !isError && data ? data.email : "";
|
||||
let description = sb.formatStringFromName("verification" + maybeNot + "SentFull", [email], 1)
|
||||
|
||||
let factory = Cc["@mozilla.org/prompter;1"]
|
||||
.getService(Ci.nsIPromptFactory);
|
||||
let prompt = factory.getPrompt(window, Ci.nsIPrompt);
|
||||
let bag = prompt.QueryInterface(Ci.nsIWritablePropertyBag2);
|
||||
bag.setPropertyAsBool("allowTabModal", true);
|
||||
syncStatusMessageTitle.textContent = title;
|
||||
syncStatusMessageDescription.textContent = description;
|
||||
let messageType = isError ? "verify-error" : "verify-success";
|
||||
syncStatusMessage.setAttribute("message-type", messageType);
|
||||
}
|
||||
|
||||
prompt.alert(title, heading + "\n\n" + description);
|
||||
});
|
||||
});
|
||||
let onError = () => {
|
||||
changesyncStatusMessage();
|
||||
};
|
||||
|
||||
let onSuccess = data => {
|
||||
if (data) {
|
||||
changesyncStatusMessage(data);
|
||||
} else {
|
||||
onError();
|
||||
}
|
||||
};
|
||||
|
||||
fxAccounts.resendVerificationEmail()
|
||||
.then(fxAccounts.getSignedInUser, onError)
|
||||
.then(onSuccess, onError);
|
||||
},
|
||||
|
||||
openOldSyncSupportPage: function() {
|
||||
|
|
|
@ -38,31 +38,29 @@
|
|||
<label class="header-name">&paneSync.title;</label>
|
||||
</hbox>
|
||||
|
||||
<hbox id="sync-migration-container"
|
||||
data-category="paneSync"
|
||||
hidden="true">
|
||||
|
||||
<vbox id="sync-migration" flex="1" hidden="true">
|
||||
|
||||
<deck id="sync-migration-deck">
|
||||
<!-- When we are in the "need FxA user" state -->
|
||||
<hbox align="center">
|
||||
<description id="sync-migrate-upgrade-description" flex="1"/>
|
||||
<spacer flex="1"/>
|
||||
<button id="sync-migrate-unlink"/>
|
||||
<button id="sync-migrate-upgrade"/>
|
||||
</hbox>
|
||||
|
||||
<!-- When we are in the "need the user to be verified" state -->
|
||||
<hbox align="center">
|
||||
<label id="sync-migrate-verify-label"/>
|
||||
<spacer flex="1"/>
|
||||
<button id="sync-migrate-forget"/>
|
||||
<button id="sync-migrate-resend"/>
|
||||
</hbox>
|
||||
</deck>
|
||||
</vbox>
|
||||
</hbox>
|
||||
<vbox id="syncStatusMessage-container" data-category="paneSync" hidden="true">
|
||||
<hbox id="syncStatusMessage">
|
||||
<vbox id="syncStatusMessageWrapper">
|
||||
<label id="syncStatusMessageTitle"></label>
|
||||
<description id="syncStatusMessageDescription"></description>
|
||||
<deck id="sync-migration-buttons-deck">
|
||||
<!-- When we are in the "need FxA user" state -->
|
||||
<hbox>
|
||||
<button id="sync-migrate-unlink"/>
|
||||
<button id="sync-migrate-upgrade"/>
|
||||
</hbox>
|
||||
<!-- When we are in the "need the user to be verified" state -->
|
||||
<hbox>
|
||||
<button id="sync-migrate-forget"/>
|
||||
<button id="sync-migrate-resend"/>
|
||||
</hbox>
|
||||
</deck>
|
||||
</vbox>
|
||||
<vbox>
|
||||
<button id="syncStatusMessageClose" class="close-icon"/>
|
||||
</vbox>
|
||||
</hbox>
|
||||
</vbox>
|
||||
|
||||
<deck id="weavePrefsDeck" data-category="paneSync" hidden="true">
|
||||
<!-- These panels are for the "legacy" sync provider -->
|
||||
|
|
|
@ -19,10 +19,11 @@ function checkElements(expectedPane) {
|
|||
continue;
|
||||
}
|
||||
let attributeValue = element.getAttribute("data-category");
|
||||
let suffix = " (id=" + element.id + ")";
|
||||
if (attributeValue == "pane" + expectedPane) {
|
||||
is_element_visible(element, expectedPane + " elements should be visible");
|
||||
is_element_visible(element, expectedPane + " elements should be visible" + suffix);
|
||||
} else {
|
||||
is_element_hidden(element, "Elements not in " + expectedPane + " should be hidden");
|
||||
is_element_hidden(element, "Elements not in " + expectedPane + " should be hidden" + suffix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ loader.lazyRequireGetter(this, "AnimationsFront",
|
|||
|
||||
const STRINGS_URI = "chrome://browser/locale/devtools/animationinspector.properties";
|
||||
const L10N = new ViewHelpers.L10N(STRINGS_URI);
|
||||
const V3_UI_PREF = "devtools.inspector.animationInspectorV3";
|
||||
|
||||
// Global toolbox/inspector, set when startup is called.
|
||||
let gToolbox, gInspector;
|
||||
|
@ -76,19 +77,20 @@ function destroy() {
|
|||
* @return {Object} An object with boolean properties.
|
||||
*/
|
||||
let getServerTraits = Task.async(function*(target) {
|
||||
let config = [{
|
||||
name: "hasToggleAll", actor: "animations", method: "toggleAll"
|
||||
}, {
|
||||
name: "hasSetCurrentTime", actor: "animationplayer", method: "setCurrentTime"
|
||||
}, {
|
||||
name: "hasMutationEvents", actor: "animations", method: "stopAnimationPlayerUpdates"
|
||||
}, {
|
||||
name: "hasSetPlaybackRate", actor: "animationplayer", method: "setPlaybackRate"
|
||||
}, {
|
||||
name: "hasTargetNode", actor: "domwalker", method: "getNodeFromActor"
|
||||
}, {
|
||||
name: "hasSetCurrentTimes", actor: "animations", method: "setCurrentTimes"
|
||||
}];
|
||||
let config = [
|
||||
{ name: "hasToggleAll", actor: "animations",
|
||||
method: "toggleAll" },
|
||||
{ name: "hasSetCurrentTime", actor: "animationplayer",
|
||||
method: "setCurrentTime" },
|
||||
{ name: "hasMutationEvents", actor: "animations",
|
||||
method: "stopAnimationPlayerUpdates" },
|
||||
{ name: "hasSetPlaybackRate", actor: "animationplayer",
|
||||
method: "setPlaybackRate" },
|
||||
{ name: "hasTargetNode", actor: "domwalker",
|
||||
method: "getNodeFromActor" },
|
||||
{ name: "hasSetCurrentTimes", actor: "animations",
|
||||
method: "setCurrentTimes" }
|
||||
];
|
||||
|
||||
let traits = {};
|
||||
for (let {name, actor, method} of config) {
|
||||
|
@ -96,7 +98,7 @@ let getServerTraits = Task.async(function*(target) {
|
|||
}
|
||||
|
||||
// Special pref-based UI trait.
|
||||
traits.isNewUI = Services.prefs.getBoolPref("devtools.inspector.animationInspectorV3");
|
||||
traits.isNewUI = Services.prefs.getBoolPref(V3_UI_PREF);
|
||||
|
||||
return traits;
|
||||
});
|
||||
|
@ -114,7 +116,8 @@ let getServerTraits = Task.async(function*(target) {
|
|||
*
|
||||
* Usage example:
|
||||
*
|
||||
* AnimationsController.on(AnimationsController.PLAYERS_UPDATED_EVENT, onPlayers);
|
||||
* AnimationsController.on(AnimationsController.PLAYERS_UPDATED_EVENT,
|
||||
* onPlayers);
|
||||
* function onPlayers() {
|
||||
* for (let player of AnimationsController.animationPlayers) {
|
||||
* // do something with player
|
||||
|
@ -126,7 +129,8 @@ let AnimationsController = {
|
|||
|
||||
initialize: Task.async(function*() {
|
||||
if (this.initialized) {
|
||||
return this.initialized.promise;
|
||||
yield this.initialized.promise;
|
||||
return;
|
||||
}
|
||||
this.initialized = promise.defer();
|
||||
|
||||
|
@ -157,7 +161,8 @@ let AnimationsController = {
|
|||
}
|
||||
|
||||
if (this.destroyed) {
|
||||
return this.destroyed.promise;
|
||||
yield this.destroyed.promise;
|
||||
return;
|
||||
}
|
||||
this.destroyed = promise.defer();
|
||||
|
||||
|
@ -272,7 +277,8 @@ let AnimationsController = {
|
|||
refreshAnimationPlayers: Task.async(function*(nodeFront) {
|
||||
yield this.destroyAnimationPlayers();
|
||||
|
||||
this.animationPlayers = yield this.animationsFront.getAnimationPlayersForNode(nodeFront);
|
||||
this.animationPlayers = yield this.animationsFront
|
||||
.getAnimationPlayersForNode(nodeFront);
|
||||
this.startAllAutoRefresh();
|
||||
|
||||
// Start listening for animation mutations only after the first method call
|
||||
|
@ -308,6 +314,25 @@ let AnimationsController = {
|
|||
this.emit(this.PLAYERS_UPDATED_EVENT, this.animationPlayers);
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get the latest known current time of document.timeline.
|
||||
* This value is sent along with all AnimationPlayerActors' states, but it
|
||||
* isn't updated after that, so this function loops over all know animations
|
||||
* to find the highest value.
|
||||
* @return {Number|Boolean} False is returned if this server version doesn't
|
||||
* provide document's current time.
|
||||
*/
|
||||
get documentCurrentTime() {
|
||||
let time = 0;
|
||||
for (let {state} of this.animationPlayers) {
|
||||
if (!state.documentCurrentTime) {
|
||||
return false;
|
||||
}
|
||||
time = Math.max(time, state.documentCurrentTime);
|
||||
}
|
||||
return time;
|
||||
},
|
||||
|
||||
startAllAutoRefresh: function() {
|
||||
if (this.traits.isNewUI) {
|
||||
return;
|
||||
|
|
|
@ -30,7 +30,8 @@ let AnimationsPanel = {
|
|||
return;
|
||||
}
|
||||
if (this.initialized) {
|
||||
return this.initialized.promise;
|
||||
yield this.initialized.promise;
|
||||
return;
|
||||
}
|
||||
this.initialized = promise.defer();
|
||||
|
||||
|
@ -74,7 +75,8 @@ let AnimationsPanel = {
|
|||
}
|
||||
|
||||
if (this.destroyed) {
|
||||
return this.destroyed.promise;
|
||||
yield this.destroyed.promise;
|
||||
return;
|
||||
}
|
||||
this.destroyed = promise.defer();
|
||||
|
||||
|
@ -157,7 +159,8 @@ let AnimationsPanel = {
|
|||
currentWidgetStateChange.push(btnClass.contains("paused")
|
||||
? widget.play() : widget.pause());
|
||||
}
|
||||
yield promise.all(currentWidgetStateChange).catch(e => console.error(e));
|
||||
yield promise.all(currentWidgetStateChange)
|
||||
.catch(error => console.error(error));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -170,7 +173,8 @@ let AnimationsPanel = {
|
|||
},
|
||||
|
||||
onTimelineTimeChanged: function(e, time) {
|
||||
AnimationsController.setCurrentTimeAll(time, true).catch(e => console.error(e));
|
||||
AnimationsController.setCurrentTimeAll(time, true)
|
||||
.catch(error => console.error(error));
|
||||
},
|
||||
|
||||
refreshAnimations: Task.async(function*() {
|
||||
|
@ -183,7 +187,8 @@ let AnimationsPanel = {
|
|||
// Re-render the timeline component.
|
||||
if (this.animationsTimelineComponent) {
|
||||
this.animationsTimelineComponent.render(
|
||||
AnimationsController.animationPlayers);
|
||||
AnimationsController.animationPlayers,
|
||||
AnimationsController.documentCurrentTime);
|
||||
}
|
||||
|
||||
// If there are no players to show, show the error message instead and
|
||||
|
|
|
@ -567,7 +567,12 @@ let TimeScale = {
|
|||
addAnimation: function(state) {
|
||||
let {startTime, delay, duration, iterationCount, playbackRate} = state;
|
||||
|
||||
this.minStartTime = Math.min(this.minStartTime, startTime);
|
||||
// Negative-delayed animations have their startTimes set such that we would
|
||||
// be displaying the delay outside the time window if we didn't take it into
|
||||
// account here.
|
||||
let relevantDelay = delay < 0 ? delay / playbackRate : 0;
|
||||
|
||||
this.minStartTime = Math.min(this.minStartTime, startTime + relevantDelay);
|
||||
let length = (delay / playbackRate) +
|
||||
((duration / playbackRate) *
|
||||
(!iterationCount ? 1 : iterationCount));
|
||||
|
@ -791,7 +796,7 @@ AnimationsTimeline.prototype = {
|
|||
this.emit("current-time-changed", time);
|
||||
},
|
||||
|
||||
render: function(animations) {
|
||||
render: function(animations, documentCurrentTime) {
|
||||
this.unrender();
|
||||
|
||||
this.animations = animations;
|
||||
|
@ -849,12 +854,11 @@ AnimationsTimeline.prototype = {
|
|||
// doesn't provide it, hide the scrubber entirely).
|
||||
// Note that because the currentTime was sent via the protocol, some time
|
||||
// may have gone by since then, and so the scrubber might be a bit late.
|
||||
let time = this.animations[0].state.documentCurrentTime;
|
||||
if (!time) {
|
||||
if (!documentCurrentTime) {
|
||||
this.scrubberEl.style.display = "none";
|
||||
} else {
|
||||
this.scrubberEl.style.display = "block";
|
||||
this.startAnimatingScrubber(time);
|
||||
this.startAnimatingScrubber(documentCurrentTime);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -953,23 +957,31 @@ AnimationsTimeline.prototype = {
|
|||
});
|
||||
|
||||
// The animation name is displayed over the iterations.
|
||||
// Note that in case of negative delay, we push the name towards the right
|
||||
// so the delay can be shown.
|
||||
createNode({
|
||||
parent: iterations,
|
||||
attributes: {
|
||||
"class": "name",
|
||||
"title": this.getAnimationTooltipText(state)
|
||||
"title": this.getAnimationTooltipText(state),
|
||||
"style": delay < 0
|
||||
? "margin-left:" +
|
||||
TimeScale.durationToDistance(Math.abs(delay), width) + "px"
|
||||
: ""
|
||||
},
|
||||
textContent: state.name
|
||||
});
|
||||
|
||||
// Delay.
|
||||
if (delay) {
|
||||
let w = TimeScale.durationToDistance(delay / rate, width);
|
||||
// Negative delays need to start at 0.
|
||||
let x = TimeScale.durationToDistance((delay < 0 ? 0 : delay) / rate, width);
|
||||
let w = TimeScale.durationToDistance(Math.abs(delay) / rate, width);
|
||||
createNode({
|
||||
parent: iterations,
|
||||
attributes: {
|
||||
"class": "delay",
|
||||
"style": `left:-${w}px;
|
||||
"style": `left:-${x}px;
|
||||
width:${w}px;`
|
||||
}
|
||||
});
|
||||
|
|
|
@ -22,18 +22,23 @@ add_task(function*() {
|
|||
let timeBlocks = timelineEl.querySelectorAll(".time-block");
|
||||
is(timeBlocks.length, 2, "2 animations are displayed");
|
||||
|
||||
info("The first animation has its rate to 1, let's measure it");
|
||||
info("The first animation has its rate set to 1, let's measure it");
|
||||
|
||||
let el = timeBlocks[0];
|
||||
let duration = el.querySelector(".iterations").getBoundingClientRect().width;
|
||||
let delay = el.querySelector(".delay").getBoundingClientRect().width;
|
||||
let duration = parseInt(el.querySelector(".iterations").style.width, 10);
|
||||
let delay = parseInt(el.querySelector(".delay").style.width, 10);
|
||||
|
||||
info("The second animation has its rate set to 2, so should be shorter");
|
||||
|
||||
let el2 = timeBlocks[1];
|
||||
let duration2 = el2.querySelector(".iterations").getBoundingClientRect().width;
|
||||
let delay2 = el2.querySelector(".delay").getBoundingClientRect().width;
|
||||
let duration2 = parseInt(el2.querySelector(".iterations").style.width, 10);
|
||||
let delay2 = parseInt(el2.querySelector(".delay").style.width, 10);
|
||||
|
||||
is(duration, 2 * duration2, "The duration width is correct");
|
||||
is(delay, 2 * delay2, "The delay width is correct");
|
||||
// The width are calculated by the animation-inspector dynamically depending
|
||||
// on the size of the panel, and therefore depends on the test machine/OS.
|
||||
// Let's not try to be too precise here and compare numbers.
|
||||
let durationDelta = (2 * duration2) - duration;
|
||||
ok(durationDelta <= 1, "The duration width is correct");
|
||||
let delayDelta = (2 * delay2) - delay;
|
||||
ok(delayDelta <= 1, "The delay width is correct");
|
||||
});
|
||||
|
|
|
@ -46,7 +46,10 @@ verificationSentTitle = Verification Sent
|
|||
# LOCALIZATION NOTE (verificationSentHeading) - %S = Email address of user's Firefox Account
|
||||
verificationSentHeading = A verification link has been sent to %S
|
||||
verificationSentDescription = Please check your email and click the link to begin syncing.
|
||||
# LOCALIZATION NOTE (verificationSentFull) - %S = Email address of user's Firefox Account
|
||||
verificationSentFull = A verification link has been sent to %S. Please check your email and click the link to begin syncing.
|
||||
|
||||
verificationNotSentTitle = Unable to Send Verification
|
||||
verificationNotSentHeading = We are unable to send a verification mail at this time
|
||||
verificationNotSentDescription = Please try again later.
|
||||
verificationNotSentFull = We are unable to send a verification mail at this time, please try again later.
|
||||
|
|
|
@ -1614,7 +1614,6 @@ toolbarbutton[constrain-size="true"][cui-areatype="toolbar"] > .toolbarbutton-ba
|
|||
}
|
||||
|
||||
#urlbar {
|
||||
-moz-padding-end: 4px;
|
||||
border-radius: @toolbarbuttonCornerRadius@;
|
||||
}
|
||||
|
||||
|
@ -1906,6 +1905,7 @@ richlistitem[type~="action"][actiontype="switchtab"][selected="true"] > .ac-url-
|
|||
var(--urlbar-separator-color) 15%,
|
||||
var(--urlbar-separator-color) 85%,
|
||||
transparent 85%);
|
||||
border-image-slice: 1;
|
||||
}
|
||||
|
||||
#urlbar-go-button {
|
||||
|
|
|
@ -238,7 +238,7 @@ body {
|
|||
border: 1px solid var(--timelime-border-color);
|
||||
|
||||
/* The background color is set independently */
|
||||
background: var(--timeline-background-color);
|
||||
background-color: var(--timeline-background-color);
|
||||
}
|
||||
|
||||
.animation-timeline .animation .cssanimation {
|
||||
|
@ -287,25 +287,16 @@ body {
|
|||
.animation-timeline .animation .delay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
/* Make sure the delay covers up the animation border */
|
||||
transform: translate(-1px, -1px);
|
||||
height: 100%;
|
||||
background-image: linear-gradient(to bottom,
|
||||
transparent,
|
||||
transparent 9px,
|
||||
var(--timelime-border-color) 9px,
|
||||
var(--timelime-border-color) 11px,
|
||||
transparent 11px,
|
||||
transparent);
|
||||
}
|
||||
|
||||
.animation-timeline .animation .delay::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
left: 0;
|
||||
width: 2px;
|
||||
height: 8px;
|
||||
top: 50%;
|
||||
margin-top: -4px;
|
||||
background: var(--timelime-border-color);
|
||||
background-image: repeating-linear-gradient(45deg,
|
||||
transparent,
|
||||
transparent 1px,
|
||||
var(--theme-selection-color) 1px,
|
||||
var(--theme-selection-color) 4px);
|
||||
background-color: var(--timelime-border-color);
|
||||
border: 1px solid var(--timelime-border-color);
|
||||
}
|
||||
|
||||
/* Animation target node gutter, contains a preview of the dom node */
|
||||
|
|
|
@ -415,6 +415,101 @@ description > html|a {
|
|||
-moz-box-align: start;
|
||||
}
|
||||
|
||||
#syncStatusMessage {
|
||||
visibility: collapse;
|
||||
opacity: 0;
|
||||
transition: opacity 1s linear;
|
||||
padding: 14px 8px 14px 14px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
#syncStatusMessage[message-type] {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#syncStatusMessage[message-type="verify-success"] {
|
||||
background-color: #74BF43;
|
||||
}
|
||||
|
||||
#syncStatusMessage[message-type="verify-error"] {
|
||||
background-color: #D74345;
|
||||
}
|
||||
|
||||
#syncStatusMessage[message-type="migration"] {
|
||||
background-color: #FF9500;
|
||||
}
|
||||
|
||||
#sync-migration-buttons-deck {
|
||||
visibility: collapse;
|
||||
}
|
||||
|
||||
#learnMoreLink {
|
||||
margin: 0;
|
||||
color: #FBFBFB;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#syncStatusMessage[message-type="migration"] #sync-migration-buttons-deck {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
#sync-migration-buttons-deck {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#sync-migration-buttons-deck button {
|
||||
margin: 0 10px 0 0;
|
||||
border: 0;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
#sync-migrate-upgrade,
|
||||
#sync-migrate-resend {
|
||||
background-color: #0095DD;
|
||||
color: #FBFBFB;
|
||||
}
|
||||
|
||||
#sync-migrate-upgrade:hover,
|
||||
#sync-migrate-resend:hover {
|
||||
background-color: #008ACB;
|
||||
}
|
||||
|
||||
#sync-migrate-upgrade:hover:active,
|
||||
#sync-migrate-resend:hover:active {
|
||||
background-color: #006B9D;
|
||||
}
|
||||
|
||||
#syncStatusMessageWrapper {
|
||||
-moz-box-flex: 1;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
#syncStatusMessageTitle, #syncStatusMessageDescription {
|
||||
color: #FBFBFB;
|
||||
}
|
||||
|
||||
#syncStatusMessage[message-type="migration"] #syncStatusMessageTitle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#syncStatusMessageTitle {
|
||||
font-weight: bold !important;
|
||||
font-size: 16px;
|
||||
line-height: 157%;
|
||||
margin: 0 0 20px;
|
||||
}
|
||||
|
||||
#syncStatusMessageDescription {
|
||||
font-size: 14px;
|
||||
line-height: 158%;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
#syncStatusMessageClose {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
#fxaSyncEngines > vbox:first-child {
|
||||
margin-right: 80px;
|
||||
}
|
||||
|
|
|
@ -1230,10 +1230,6 @@ toolbarbutton[constrain-size="true"][cui-areatype="toolbar"] > .toolbarbutton-ba
|
|||
border: 1px solid ThreeDShadow;
|
||||
}
|
||||
|
||||
#urlbar {
|
||||
-moz-padding-end: 2px;
|
||||
}
|
||||
|
||||
/* overlap the urlbar's border */
|
||||
#PopupAutoCompleteRichResult {
|
||||
margin-top: -1px;
|
||||
|
@ -1267,7 +1263,6 @@ toolbarbutton[constrain-size="true"][cui-areatype="toolbar"] > .toolbarbutton-ba
|
|||
.searchbar-textbox:not(:-moz-lwtheme) {
|
||||
border-color: hsl(0,0%,90%);
|
||||
padding: 1px;
|
||||
-moz-padding-end: 3px;
|
||||
transition-property: border-color, box-shadow;
|
||||
transition-duration: .1s;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
#include "base/basictypes.h"
|
||||
#include "BluetoothMapFolder.h"
|
||||
|
||||
BEGIN_BLUETOOTH_NAMESPACE
|
||||
|
||||
BluetoothMapFolder::~BluetoothMapFolder()
|
||||
{ }
|
||||
|
||||
BluetoothMapFolder::BluetoothMapFolder(const nsAString& aFolderName,
|
||||
BluetoothMapFolder* aParent)
|
||||
: mName(aFolderName)
|
||||
, mParent(aParent)
|
||||
{
|
||||
}
|
||||
|
||||
BluetoothMapFolder*
|
||||
BluetoothMapFolder::AddSubFolder(const nsAString& aFolderName)
|
||||
{
|
||||
nsRefPtr<BluetoothMapFolder> folder = new BluetoothMapFolder(aFolderName,
|
||||
this);
|
||||
mSubFolders.Put(nsString(aFolderName), folder);
|
||||
|
||||
return folder;
|
||||
}
|
||||
|
||||
BluetoothMapFolder*
|
||||
BluetoothMapFolder::GetSubFolder(const nsAString& aFolderName)
|
||||
{
|
||||
BluetoothMapFolder* subfolder;
|
||||
mSubFolders.Get(aFolderName, &subfolder);
|
||||
|
||||
return subfolder;
|
||||
}
|
||||
|
||||
BluetoothMapFolder*
|
||||
BluetoothMapFolder::GetParentFolder()
|
||||
{
|
||||
return mParent;
|
||||
}
|
||||
|
||||
int
|
||||
BluetoothMapFolder::GetSubFolderCount()
|
||||
{
|
||||
return mSubFolders.Count();
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapFolder::GetFolderListingObjectString(nsAString& aString,
|
||||
uint16_t aMaxListCount,
|
||||
uint16_t aStartOffset)
|
||||
{
|
||||
const char* folderListingPrefix =
|
||||
"<?xml version=\"1.0\"?>\n"
|
||||
"<!DOCTYPE folder-listing SYSTEM \" obex-folder-listing.dtd\">\n"
|
||||
"<folder-listing version=\"1.0\">\n";
|
||||
const char* folderListingSuffix = "</folder-listing>";
|
||||
|
||||
// Based on Element Specification, 9.1.1, IrObex 1.2
|
||||
nsAutoCString folderListingObejct(folderListingPrefix);
|
||||
|
||||
int count = 0;
|
||||
for (auto iter = mSubFolders.Iter(); !iter.Done(); iter.Next()) {
|
||||
if (count < aStartOffset) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count > aMaxListCount) {
|
||||
break;
|
||||
}
|
||||
|
||||
const nsAString& key = iter.Key();
|
||||
folderListingObejct.Append("<folder name=\"");
|
||||
folderListingObejct.Append(NS_ConvertUTF16toUTF8(key).get());
|
||||
folderListingObejct.Append("\">");
|
||||
count++;
|
||||
}
|
||||
|
||||
folderListingObejct.Append(folderListingSuffix);
|
||||
aString = NS_ConvertUTF8toUTF16(folderListingObejct);
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapFolder::DumpFolderInfo()
|
||||
{
|
||||
BT_LOGR("Folder name: %s, subfolder counts: %d",
|
||||
NS_ConvertUTF16toUTF8(mName).get(), mSubFolders.Count());
|
||||
}
|
||||
|
||||
END_BLUETOOTH_NAMESPACE
|
|
@ -0,0 +1,41 @@
|
|||
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#ifndef mozilla_dom_bluetooth_bluedroid_BluetoothMapFolder_h
|
||||
#define mozilla_dom_bluetooth_bluedroid_BluetoothMapFolder_h
|
||||
|
||||
#include "BluetoothCommon.h"
|
||||
#include "nsAutoPtr.h"
|
||||
#include "nsRefPtrHashtable.h"
|
||||
#include "nsTArray.h"
|
||||
|
||||
BEGIN_BLUETOOTH_NAMESPACE
|
||||
|
||||
/* This class maps MAP virtual folder structures */
|
||||
class BluetoothMapFolder
|
||||
{
|
||||
public:
|
||||
NS_INLINE_DECL_THREADSAFE_REFCOUNTING(BluetoothMapFolder)
|
||||
|
||||
BluetoothMapFolder(const nsAString& aFolderName, BluetoothMapFolder* aParent);
|
||||
// Add virtual folder to subfolders
|
||||
BluetoothMapFolder* AddSubFolder(const nsAString& aFolderName);
|
||||
BluetoothMapFolder* GetSubFolder(const nsAString& aFolderName);
|
||||
BluetoothMapFolder* GetParentFolder();
|
||||
int GetSubFolderCount();
|
||||
// Format folder listing object string
|
||||
void GetFolderListingObjectString(nsAString& aString, uint16_t aMaxListCount,
|
||||
uint16_t aStartOffset);
|
||||
void DumpFolderInfo();
|
||||
private:
|
||||
~BluetoothMapFolder();
|
||||
nsString mName;
|
||||
nsRefPtr<BluetoothMapFolder> mParent;
|
||||
nsRefPtrHashtable<nsStringHashKey, BluetoothMapFolder> mSubFolders;
|
||||
};
|
||||
|
||||
END_BLUETOOTH_NAMESPACE
|
||||
#endif //mozilla_dom_bluetooth_bluedroid_BluetoothMapFolder_h
|
|
@ -0,0 +1,915 @@
|
|||
/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#include "base/basictypes.h"
|
||||
#include "BluetoothMapSmsManager.h"
|
||||
|
||||
#include "BluetoothService.h"
|
||||
#include "BluetoothSocket.h"
|
||||
#include "BluetoothUuid.h"
|
||||
#include "ObexBase.h"
|
||||
|
||||
#include "mozilla/RefPtr.h"
|
||||
#include "mozilla/Services.h"
|
||||
#include "mozilla/StaticPtr.h"
|
||||
#include "nsAutoPtr.h"
|
||||
#include "nsIObserver.h"
|
||||
#include "nsIObserverService.h"
|
||||
|
||||
USING_BLUETOOTH_NAMESPACE
|
||||
using namespace mozilla;
|
||||
using namespace mozilla::dom;
|
||||
using namespace mozilla::ipc;
|
||||
|
||||
namespace {
|
||||
// UUID of Map Mas
|
||||
static const BluetoothUuid kMapMas = {
|
||||
{
|
||||
0x00, 0x00, 0x11, 0x32, 0x00, 0x00, 0x10, 0x00,
|
||||
0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB
|
||||
}
|
||||
};
|
||||
// UUID of Map Mns
|
||||
static const BluetoothUuid kMapMns = {
|
||||
{
|
||||
0x00, 0x00, 0x11, 0x33, 0x00, 0x00, 0x10, 0x00,
|
||||
0x80, 0x00, 0x00, 0x80, 0x5F, 0x9B, 0x34, 0xFB
|
||||
}
|
||||
};
|
||||
// UUID used in Map OBEX MAS target header
|
||||
static const BluetoothUuid kMapMasObexTarget = {
|
||||
{
|
||||
0xBB, 0x58, 0x2B, 0x40, 0x42, 0x0C, 0x11, 0xDB,
|
||||
0xB0, 0xDE, 0x08, 0x00, 0x20, 0x0C, 0x9A, 0x66
|
||||
}
|
||||
};
|
||||
|
||||
// UUID used in Map OBEX MNS target header
|
||||
static const BluetoothUuid kMapMnsObexTarget = {
|
||||
{
|
||||
0xBB, 0x58, 0x2B, 0x41, 0x42, 0x0C, 0x11, 0xDB,
|
||||
0xB0, 0xDE, 0x08, 0x00, 0x20, 0x0C, 0x9A, 0x66
|
||||
}
|
||||
};
|
||||
|
||||
StaticRefPtr<BluetoothMapSmsManager> sMapSmsManager;
|
||||
static bool sInShutdown = false;
|
||||
}
|
||||
|
||||
BEGIN_BLUETOOTH_NAMESPACE
|
||||
|
||||
NS_IMETHODIMP
|
||||
BluetoothMapSmsManager::Observe(nsISupports* aSubject,
|
||||
const char* aTopic,
|
||||
const char16_t* aData)
|
||||
{
|
||||
MOZ_ASSERT(sMapSmsManager);
|
||||
|
||||
if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
|
||||
HandleShutdown();
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
MOZ_ASSERT(false, "MapSmsManager got unexpected topic!");
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::HandleShutdown()
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
||||
sInShutdown = true;
|
||||
Disconnect(nullptr);
|
||||
sMapSmsManager = nullptr;
|
||||
}
|
||||
|
||||
BluetoothMapSmsManager::BluetoothMapSmsManager() : mMasConnected(false),
|
||||
mMnsConnected(false),
|
||||
mNtfRequired(false)
|
||||
{
|
||||
mDeviceAddress.AssignLiteral(BLUETOOTH_ADDRESS_NONE);
|
||||
BuildDefaultFolderStructure();
|
||||
}
|
||||
|
||||
BluetoothMapSmsManager::~BluetoothMapSmsManager()
|
||||
{
|
||||
nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
|
||||
if (NS_WARN_IF(!obs)) {
|
||||
return;
|
||||
}
|
||||
|
||||
NS_WARN_IF(NS_FAILED(
|
||||
obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID)));
|
||||
}
|
||||
|
||||
bool
|
||||
BluetoothMapSmsManager::Init()
|
||||
{
|
||||
nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
|
||||
if (NS_WARN_IF(!obs)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (NS_WARN_IF(NS_FAILED(
|
||||
obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false)))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* We don't start listening here as BluetoothServiceBluedroid calls Listen()
|
||||
* immediately when BT stops.
|
||||
*
|
||||
* If we start listening here, the listening fails when device boots up since
|
||||
* Listen() is called again and restarts server socket. The restart causes
|
||||
* absence of read events when device boots up.
|
||||
*/
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//static
|
||||
BluetoothMapSmsManager*
|
||||
BluetoothMapSmsManager::Get()
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
||||
// Exit early if sMapSmsManager already exists
|
||||
if (sMapSmsManager) {
|
||||
return sMapSmsManager;
|
||||
}
|
||||
|
||||
// Do not create a new instance if we're in shutdown
|
||||
if (NS_WARN_IF(sInShutdown)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Create a new instance, register, and return
|
||||
BluetoothMapSmsManager *manager = new BluetoothMapSmsManager();
|
||||
if (NS_WARN_IF(!manager->Init())) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
sMapSmsManager = manager;
|
||||
|
||||
return sMapSmsManager;
|
||||
}
|
||||
|
||||
bool
|
||||
BluetoothMapSmsManager::Listen()
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
||||
// Fail to listen if |mMasSocket| already exists
|
||||
if (NS_WARN_IF(mMasSocket)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart server socket since its underlying fd becomes invalid when
|
||||
* BT stops; otherwise no more read events would be received even if
|
||||
* BT restarts.
|
||||
*/
|
||||
if (mMasServerSocket) {
|
||||
mMasServerSocket->Close();
|
||||
mMasServerSocket = nullptr;
|
||||
}
|
||||
|
||||
mMasServerSocket = new BluetoothSocket(this);
|
||||
|
||||
nsString sdpString;
|
||||
#if ANDROID_VERSION >= 21
|
||||
/**
|
||||
* The way bluedroid handles MAP SDP record is very hacky.
|
||||
* In Lollipop version, SDP string format would be instanceId + msg type
|
||||
* + msg name. See add_maps_sdp in btif/src/btif_sock_sdp.c
|
||||
*/
|
||||
// MAS instance id
|
||||
sdpString.AppendPrintf("%02x", SDP_SMS_MMS_INSTANCE_ID);
|
||||
// Supported message type
|
||||
sdpString.AppendPrintf("%02x", SDP_MESSAGE_TYPE_SMS_GSM |
|
||||
SDP_MESSAGE_TYPE_SMS_CDMA |
|
||||
SDP_MESSAGE_TYPE_MMS);
|
||||
#endif
|
||||
/**
|
||||
* SDP service name, we don't assign RFCOMM channel directly
|
||||
* bluedroid automatically assign channel number randomly.
|
||||
*/
|
||||
sdpString.AppendLiteral("SMS/MMS Message Access");
|
||||
nsresult rv = mMasServerSocket->Listen(sdpString, kMapMas,
|
||||
BluetoothSocketType::RFCOMM, -1, false,
|
||||
true);
|
||||
if (NS_FAILED(rv)) {
|
||||
mMasServerSocket = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::MnsDataHandler(UnixSocketBuffer* aMessage)
|
||||
{
|
||||
// Ensure valid access to data[0], i.e., opCode
|
||||
int receivedLength = aMessage->GetSize();
|
||||
if (receivedLength < 1) {
|
||||
BT_LOGR("Receive empty response packet");
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t* data = aMessage->GetData();
|
||||
uint8_t opCode = data[0];
|
||||
if (opCode != ObexResponseCode::Success) {
|
||||
BT_LOGR("Unexpected OpCode: %x", opCode);
|
||||
if (mLastCommand == ObexRequestCode::Put ||
|
||||
mLastCommand == ObexRequestCode::Abort ||
|
||||
mLastCommand == ObexRequestCode::PutFinal) {
|
||||
SendMnsDisconnectRequest();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::MasDataHandler(UnixSocketBuffer* aMessage)
|
||||
{
|
||||
/**
|
||||
* Ensure
|
||||
* - valid access to data[0], i.e., opCode
|
||||
* - received packet length smaller than max packet length
|
||||
*/
|
||||
int receivedLength = aMessage->GetSize();
|
||||
if (receivedLength < 1 || receivedLength > MAX_PACKET_LENGTH) {
|
||||
ReplyError(ObexResponseCode::BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint8_t* data = aMessage->GetData();
|
||||
uint8_t opCode = data[0];
|
||||
ObexHeaderSet pktHeaders;
|
||||
nsString type;
|
||||
switch (opCode) {
|
||||
case ObexRequestCode::Connect:
|
||||
// Section 3.3.1 "Connect", IrOBEX 1.2
|
||||
// [opcode:1][length:2][version:1][flags:1][MaxPktSizeWeCanReceive:2]
|
||||
// [Headers:var]
|
||||
if (receivedLength < 7 ||
|
||||
!ParseHeaders(&data[7], receivedLength - 7, &pktHeaders)) {
|
||||
ReplyError(ObexResponseCode::BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
// "Establishing an OBEX Session"
|
||||
// The OBEX header target shall equal to MAS obex target UUID.
|
||||
if (!CompareHeaderTarget(pktHeaders)) {
|
||||
ReplyError(ObexResponseCode::BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
mRemoteMaxPacketLength = ((static_cast<int>(data[5]) << 8) | data[6]);
|
||||
|
||||
if (mRemoteMaxPacketLength < 255) {
|
||||
BT_LOGR("Remote maximum packet length %d", mRemoteMaxPacketLength);
|
||||
mRemoteMaxPacketLength = 0;
|
||||
ReplyError(ObexResponseCode::BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
ReplyToConnect();
|
||||
AfterMapSmsConnected();
|
||||
break;
|
||||
case ObexRequestCode::Disconnect:
|
||||
case ObexRequestCode::Abort:
|
||||
// Section 3.3.2 "Disconnect" and Section 3.3.5 "Abort", IrOBEX 1.2
|
||||
// The format of request packet of "Disconnect" and "Abort" are the same
|
||||
// [opcode:1][length:2][Headers:var]
|
||||
if (receivedLength < 3 ||
|
||||
!ParseHeaders(&data[3], receivedLength - 3, &pktHeaders)) {
|
||||
ReplyError(ObexResponseCode::BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
ReplyToDisconnectOrAbort();
|
||||
AfterMapSmsDisconnected();
|
||||
break;
|
||||
case ObexRequestCode::SetPath: {
|
||||
// Section 3.3.6 "SetPath", IrOBEX 1.2
|
||||
// [opcode:1][length:2][flags:1][contants:1][Headers:var]
|
||||
if (receivedLength < 5 ||
|
||||
!ParseHeaders(&data[5], receivedLength - 5, &pktHeaders)) {
|
||||
ReplyError(ObexResponseCode::BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t response = SetPath(data[3], pktHeaders);
|
||||
if (response != ObexResponseCode::Success) {
|
||||
ReplyError(response);
|
||||
return;
|
||||
}
|
||||
|
||||
ReplyToSetPath();
|
||||
}
|
||||
break;
|
||||
case ObexRequestCode::Put:
|
||||
case ObexRequestCode::PutFinal:
|
||||
// Section 3.3.3 "Put", IrOBEX 1.2
|
||||
// [opcode:1][length:2][Headers:var]
|
||||
if (receivedLength < 3 ||
|
||||
!ParseHeaders(&data[3], receivedLength - 3, &pktHeaders)) {
|
||||
ReplyError(ObexResponseCode::BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pktHeaders.Has(ObexHeaderId::Type)) {
|
||||
pktHeaders.GetContentType(type);
|
||||
BT_LOGR("Type: %s", NS_ConvertUTF16toUTF8(type).get());
|
||||
ReplyToPut();
|
||||
|
||||
if (type.EqualsLiteral("x-bt/MAP-NotificationRegistration")) {
|
||||
HandleNotificationRegistration(pktHeaders);
|
||||
} else if (type.EqualsLiteral("x-bt/MAP-event-report")) {
|
||||
HandleEventReport(pktHeaders);
|
||||
} else if (type.EqualsLiteral("x-bt/messageStatus")) {
|
||||
HandleMessageStatus(pktHeaders);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ObexRequestCode::Get:
|
||||
case ObexRequestCode::GetFinal: {
|
||||
// [opcode:1][length:2][Headers:var]
|
||||
if (receivedLength < 3 ||
|
||||
!ParseHeaders(&data[3], receivedLength - 3, &pktHeaders)) {
|
||||
ReplyError(ObexResponseCode::BadRequest);
|
||||
return;
|
||||
}
|
||||
pktHeaders.GetContentType(type);
|
||||
if (type.EqualsLiteral("x-obex/folder-listing")) {
|
||||
HandleSmsMmsFolderListing(pktHeaders);
|
||||
} else if (type.EqualsLiteral("x-bt/MAP-msg-listing")) {
|
||||
// TODO: Implement this feature in Bug 1166675
|
||||
} else if (type.EqualsLiteral("x-bt/message")) {
|
||||
// TODO: Implement this feature in Bug 1166679
|
||||
} else {
|
||||
BT_LOGR("Unknown MAP request type: %s",
|
||||
NS_ConvertUTF16toUTF8(type).get());
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
ReplyError(ObexResponseCode::NotImplemented);
|
||||
BT_LOGR("Unrecognized ObexRequestCode %x", opCode);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Virtual function of class SocketConsumer
|
||||
void
|
||||
BluetoothMapSmsManager::ReceiveSocketData(BluetoothSocket* aSocket,
|
||||
nsAutoPtr<UnixSocketBuffer>& aMessage)
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
||||
if (aSocket == mMnsSocket) {
|
||||
MnsDataHandler(aMessage);
|
||||
} else {
|
||||
MasDataHandler(aMessage);
|
||||
}
|
||||
}
|
||||
|
||||
bool
|
||||
BluetoothMapSmsManager::CompareHeaderTarget(const ObexHeaderSet& aHeader)
|
||||
{
|
||||
if (!aHeader.Has(ObexHeaderId::Target)) {
|
||||
BT_LOGR("No ObexHeaderId::Target in header");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t* targetPtr;
|
||||
int targetLength;
|
||||
aHeader.GetTarget(&targetPtr, &targetLength);
|
||||
|
||||
if (targetLength != sizeof(BluetoothUuid)) {
|
||||
BT_LOGR("Length mismatch: %d != 16", targetLength);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < sizeof(BluetoothUuid); i++) {
|
||||
if (targetPtr[i] != kMapMasObexTarget.mUuid[i]) {
|
||||
BT_LOGR("UUID mismatch: received target[%d]=0x%x != 0x%x",
|
||||
i, targetPtr[i], kMapMasObexTarget.mUuid[i]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t
|
||||
BluetoothMapSmsManager::SetPath(uint8_t flags,
|
||||
const ObexHeaderSet& aHeader)
|
||||
{
|
||||
// Section 5.2 "SetPath Function", MapSms 1.2
|
||||
// flags bit 1 must be 1 and bit 2~7 be 0
|
||||
if ((flags >> 1) != 1) {
|
||||
BT_LOGR("Illegal flags [0x%x]: bits 1~7 must be 0x01", flags);
|
||||
return ObexResponseCode::BadRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Three cases:
|
||||
* 1) Go up 1 level - flags bit 0 is 1
|
||||
* 2) Go back to root - flags bit 0 is 0 AND name header is empty
|
||||
* 3) Go down 1 level - flags bit 0 is 0 AND name header is not empty,
|
||||
* where name header is the name of child folder
|
||||
*/
|
||||
if (flags & 1) {
|
||||
// Go up 1 level
|
||||
BluetoothMapFolder* parent = mCurrentFolder->GetParentFolder();
|
||||
if (!parent) {
|
||||
mCurrentFolder = parent;
|
||||
BT_LOGR("MAS SetPath Go up 1 level");
|
||||
}
|
||||
} else {
|
||||
MOZ_ASSERT(aHeader.Has(ObexHeaderId::Name));
|
||||
|
||||
nsString childFolderName;
|
||||
aHeader.GetName(childFolderName);
|
||||
|
||||
if (childFolderName.IsEmpty()) {
|
||||
// Go back to root
|
||||
mCurrentFolder = mRootFolder;
|
||||
BT_LOGR("MAS SetPath Go back to root");
|
||||
} else {
|
||||
// Go down 1 level
|
||||
BluetoothMapFolder* child = mCurrentFolder->GetSubFolder(childFolderName);
|
||||
if (!child) {
|
||||
BT_LOGR("Illegal sub-folder name [%s]",
|
||||
NS_ConvertUTF16toUTF8(childFolderName).get());
|
||||
return ObexResponseCode::NotFound;
|
||||
}
|
||||
|
||||
mCurrentFolder = child;
|
||||
BT_LOGR("MAS SetPath Go down to 1 level");
|
||||
}
|
||||
}
|
||||
|
||||
mCurrentFolder->DumpFolderInfo();
|
||||
|
||||
return ObexResponseCode::Success;
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::AfterMapSmsConnected()
|
||||
{
|
||||
mMasConnected = true;
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::AfterMapSmsDisconnected()
|
||||
{
|
||||
mMasConnected = false;
|
||||
// To ensure we close MNS connection
|
||||
DestroyMnsObexConnection();
|
||||
}
|
||||
|
||||
bool
|
||||
BluetoothMapSmsManager::IsConnected()
|
||||
{
|
||||
return mMasConnected;
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::GetAddress(nsAString& aDeviceAddress)
|
||||
{
|
||||
return mMasSocket->GetAddress(aDeviceAddress);
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::ReplyToConnect()
|
||||
{
|
||||
if (mMasConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Section 3.3.1 "Connect", IrOBEX 1.2
|
||||
// [opcode:1][length:2][version:1][flags:1][MaxPktSizeWeCanReceive:2]
|
||||
// [Headers:var]
|
||||
uint8_t req[255];
|
||||
int index = 7;
|
||||
|
||||
req[3] = 0x10; // version=1.0
|
||||
req[4] = 0x00; // flag=0x00
|
||||
req[5] = BluetoothMapSmsManager::MAX_PACKET_LENGTH >> 8;
|
||||
req[6] = (uint8_t)BluetoothMapSmsManager::MAX_PACKET_LENGTH;
|
||||
|
||||
// Section 6.4 "Establishing an OBEX Session", MapSms 1.2
|
||||
// Headers: [Who:16][Connection ID]
|
||||
index += AppendHeaderWho(&req[index], 255, kMapMasObexTarget.mUuid,
|
||||
sizeof(BluetoothUuid));
|
||||
index += AppendHeaderConnectionId(&req[index], 0x01);
|
||||
SendMasObexData(req, ObexResponseCode::Success, index);
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::ReplyToDisconnectOrAbort()
|
||||
{
|
||||
if (!mMasConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Section 3.3.2 "Disconnect" and Section 3.3.5 "Abort", IrOBEX 1.2
|
||||
// The format of response packet of "Disconnect" and "Abort" are the same
|
||||
// [opcode:1][length:2][Headers:var]
|
||||
uint8_t req[255];
|
||||
int index = 3;
|
||||
|
||||
SendMasObexData(req, ObexResponseCode::Success, index);
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::ReplyToSetPath()
|
||||
{
|
||||
if (!mMasConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Section 3.3.6 "SetPath", IrOBEX 1.2
|
||||
// [opcode:1][length:2][Headers:var]
|
||||
uint8_t req[255];
|
||||
int index = 3;
|
||||
|
||||
SendMasObexData(req, ObexResponseCode::Success, index);
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::ReplyToPut()
|
||||
{
|
||||
if (!mMasConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Section 3.3.3.2 "PutResponse", IrOBEX 1.2
|
||||
// [opcode:1][length:2][Headers:var]
|
||||
uint8_t req[255];
|
||||
int index = 3;
|
||||
|
||||
SendMasObexData(req, ObexResponseCode::Success, index);
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::CreateMnsObexConnection()
|
||||
{
|
||||
if (mMnsSocket) {
|
||||
return;
|
||||
}
|
||||
|
||||
mMnsSocket = new BluetoothSocket(this);
|
||||
// Already encrypted in previous session
|
||||
mMnsSocket->Connect(mDeviceAddress, kMapMns,
|
||||
BluetoothSocketType::RFCOMM, -1, false, false);
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::DestroyMnsObexConnection()
|
||||
{
|
||||
if (!mMnsSocket) {
|
||||
return;
|
||||
}
|
||||
|
||||
mMnsSocket->Close();
|
||||
mMnsSocket = nullptr;
|
||||
mNtfRequired = false;
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::SendMnsConnectRequest()
|
||||
{
|
||||
MOZ_ASSERT(mMnsSocket);
|
||||
|
||||
// Section 3.3.1 "Connect", IrOBEX 1.2
|
||||
// [opcode:1][length:2][version:1][flags:1][MaxPktSizeWeCanReceive:2]
|
||||
// [Headers:var]
|
||||
uint8_t req[255];
|
||||
int index = 7;
|
||||
|
||||
req[3] = 0x10; // version=1.0
|
||||
req[4] = 0x00; // flag=0x00
|
||||
req[5] = BluetoothMapSmsManager::MAX_PACKET_LENGTH >> 8;
|
||||
req[6] = (uint8_t)BluetoothMapSmsManager::MAX_PACKET_LENGTH;
|
||||
|
||||
index += AppendHeaderTarget(&req[index], 255, kMapMnsObexTarget.mUuid,
|
||||
sizeof(BluetoothUuid));
|
||||
SendMnsObexData(req, ObexRequestCode::Connect, index);
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::SendMnsDisconnectRequest()
|
||||
{
|
||||
MOZ_ASSERT(mMnsSocket);
|
||||
|
||||
if (!mMasConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Section 3.3.2 "Disconnect", IrOBEX 1.2
|
||||
// [opcode:1][length:2][Headers:var]
|
||||
uint8_t req[255];
|
||||
int index = 3;
|
||||
|
||||
SendMnsObexData(req, ObexRequestCode::Disconnect, index);
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::HandleSmsMmsFolderListing(const ObexHeaderSet& aHeader)
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
||||
uint8_t buf[64];
|
||||
uint16_t maxListCount = 0;
|
||||
|
||||
if (aHeader.GetAppParameter(Map::AppParametersTagId::MaxListCount,
|
||||
buf, 64)) {
|
||||
maxListCount = *((uint16_t *)buf);
|
||||
// convert big endian to little endian
|
||||
maxListCount = (maxListCount >> 8) | (maxListCount << 8);
|
||||
}
|
||||
|
||||
uint16_t startOffset = 0;
|
||||
if (aHeader.GetAppParameter(Map::AppParametersTagId::StartOffset,
|
||||
buf, 64)) {
|
||||
startOffset = *((uint16_t *)buf);
|
||||
// convert big endian to little endian
|
||||
startOffset = (startOffset >> 8) | (startOffset << 8);
|
||||
}
|
||||
|
||||
// Folder listing size
|
||||
int foldersize = mCurrentFolder->GetSubFolderCount();
|
||||
|
||||
// Convert little endian to big endian
|
||||
uint8_t folderListingSizeValue[2];
|
||||
folderListingSizeValue[0] = (foldersize & 0xFF00) >> 8;
|
||||
folderListingSizeValue[1] = (foldersize & 0x00FF);
|
||||
|
||||
// Section 3.3.4 "GetResponse", IrOBEX 1.2
|
||||
// [opcode:1][length:2][FolderListingSize:4][Headers:var] where
|
||||
// Application Parameter [FolderListingSize:4] = [tagId:1][length:1][value: 2]
|
||||
uint8_t appParameter[4];
|
||||
AppendAppParameter(appParameter, sizeof(appParameter),
|
||||
(uint8_t)Map::AppParametersTagId::FolderListingSize,
|
||||
folderListingSizeValue, sizeof(folderListingSizeValue));
|
||||
|
||||
uint8_t resp[255];
|
||||
int index = 3;
|
||||
index += AppendHeaderAppParameters(&resp[index], 255, appParameter,
|
||||
sizeof(appParameter));
|
||||
|
||||
/*
|
||||
* MCE wants to query sub-folder size FolderListingSize AppParameter shall
|
||||
* be used in the response if the value of MaxListCount in the request is 0.
|
||||
* If MaxListCount = 0, the MSE shall ignore all other applications
|
||||
* parameters that may be presented in the request. The response shall
|
||||
* contain any Body header.
|
||||
*/
|
||||
if (maxListCount) {
|
||||
nsString output;
|
||||
mCurrentFolder->GetFolderListingObjectString(output, maxListCount,
|
||||
startOffset);
|
||||
index += AppendHeaderBody(&resp[index],
|
||||
mRemoteMaxPacketLength - index,
|
||||
reinterpret_cast<const uint8_t*>(
|
||||
NS_ConvertUTF16toUTF8(output).get()),
|
||||
NS_ConvertUTF16toUTF8(output).Length());
|
||||
|
||||
index += AppendHeaderEndOfBody(&resp[index]);
|
||||
}
|
||||
|
||||
SendMasObexData(resp, ObexResponseCode::Success, index);
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::BuildDefaultFolderStructure()
|
||||
{
|
||||
/* MAP specification defines virtual folders structure
|
||||
* /
|
||||
* /telecom
|
||||
* /telecom/msg
|
||||
* /telecom/msg/inbox
|
||||
* /telecom/msg/draft
|
||||
* /telecom/msg/outbox
|
||||
* /telecom/msg/sent
|
||||
* /telecom/msg/deleted
|
||||
*/
|
||||
mRootFolder = new BluetoothMapFolder(NS_LITERAL_STRING("root"), nullptr);
|
||||
BluetoothMapFolder* folder =
|
||||
mRootFolder->AddSubFolder(NS_LITERAL_STRING("telecom"));
|
||||
folder = folder->AddSubFolder(NS_LITERAL_STRING("msg"));
|
||||
|
||||
// Add mandatory folders
|
||||
folder->AddSubFolder(NS_LITERAL_STRING("inbox"));
|
||||
folder->AddSubFolder(NS_LITERAL_STRING("sent"));
|
||||
folder->AddSubFolder(NS_LITERAL_STRING("deleted"));
|
||||
folder->AddSubFolder(NS_LITERAL_STRING("outbox"));
|
||||
folder->AddSubFolder(NS_LITERAL_STRING("draft"));
|
||||
mCurrentFolder = mRootFolder;
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::HandleNotificationRegistration(
|
||||
const ObexHeaderSet& aHeader)
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
||||
uint8_t buf[64];
|
||||
if (!aHeader.GetAppParameter(Map::AppParametersTagId::NotificationStatus,
|
||||
buf, 64)) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool ntfRequired = static_cast<bool>(buf[0]);
|
||||
if (mNtfRequired == ntfRequired) {
|
||||
// Ignore request
|
||||
return;
|
||||
}
|
||||
|
||||
mNtfRequired = ntfRequired;
|
||||
/*
|
||||
* Initialization sequence for a MAP session that uses both the Messsage
|
||||
* Access service and the Message Notification service. The MNS connection
|
||||
* shall be established by the first SetNotificationRegistration set to ON
|
||||
* during MAP session. Only one MNS connection per device pair.
|
||||
* Section 6.4.2, MAP
|
||||
* If the Message Access connection is disconnected after Message Notification
|
||||
* connection establishment, this will automatically indicate a MAS
|
||||
* Notification-Deregistration for this MAS instance.
|
||||
*/
|
||||
if (mNtfRequired) {
|
||||
CreateMnsObexConnection();
|
||||
} else {
|
||||
/*
|
||||
* TODO: we shall check multiple MAS instances unregister notification to
|
||||
* drop MNS connection, but now we only support SMS/MMS, so drop connection
|
||||
* directly.
|
||||
*/
|
||||
DestroyMnsObexConnection();
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::HandleEventReport(const ObexHeaderSet& aHeader)
|
||||
{
|
||||
// TODO: Handle event report in Bug 1166666
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::HandleMessageStatus(const ObexHeaderSet& aHeader)
|
||||
{
|
||||
// TODO: Handle MessageStatus update in Bug 1186836
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::ReplyError(uint8_t aError)
|
||||
{
|
||||
BT_LOGR("[0x%x]", aError);
|
||||
|
||||
// Section 3.2 "Response Format", IrOBEX 1.2
|
||||
// [opcode:1][length:2][Headers:var]
|
||||
uint8_t req[255];
|
||||
int index = 3;
|
||||
|
||||
SendMasObexData(req, aError, index);
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::SendMasObexData(uint8_t* aData, uint8_t aOpcode,
|
||||
int aSize)
|
||||
{
|
||||
SetObexPacketInfo(aData, aOpcode, aSize);
|
||||
mMasSocket->SendSocketData(new UnixSocketRawData(aData, aSize));
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::SendMnsObexData(uint8_t* aData, uint8_t aOpcode,
|
||||
int aSize)
|
||||
{
|
||||
mLastCommand = aOpcode;
|
||||
SetObexPacketInfo(aData, aOpcode, aSize);
|
||||
mMnsSocket->SendSocketData(new UnixSocketRawData(aData, aSize));
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::OnSocketConnectSuccess(BluetoothSocket* aSocket)
|
||||
{
|
||||
MOZ_ASSERT(aSocket);
|
||||
|
||||
// MNS socket is connected
|
||||
if (aSocket == mMnsSocket) {
|
||||
mMnsConnected = true;
|
||||
SendMnsConnectRequest();
|
||||
return;
|
||||
}
|
||||
// MAS socket is connected
|
||||
// Close server socket as only one session is allowed at a time
|
||||
mMasServerSocket.swap(mMasSocket);
|
||||
|
||||
// Cache device address since we can't get socket address when a remote
|
||||
// device disconnect with us.
|
||||
mMasSocket->GetAddress(mDeviceAddress);
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::OnSocketConnectError(BluetoothSocket* aSocket)
|
||||
{
|
||||
// MNS socket connection error
|
||||
if (aSocket == mMnsSocket) {
|
||||
mMnsConnected = false;
|
||||
mMnsSocket = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
// MAS socket connection error
|
||||
mMasServerSocket = nullptr;
|
||||
mMasSocket = nullptr;
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::OnSocketDisconnect(BluetoothSocket* aSocket)
|
||||
{
|
||||
MOZ_ASSERT(aSocket);
|
||||
|
||||
// MNS socket is disconnected
|
||||
if (aSocket == mMnsSocket) {
|
||||
mMnsConnected = false;
|
||||
mMnsSocket = nullptr;
|
||||
BT_LOGR("MNS socket disconnected");
|
||||
return;
|
||||
}
|
||||
|
||||
// MAS server socket is closed
|
||||
if (aSocket != mMasSocket) {
|
||||
// Do nothing when a listening server socket is closed.
|
||||
return;
|
||||
}
|
||||
|
||||
// MAS socket is disconnected
|
||||
AfterMapSmsDisconnected();
|
||||
mDeviceAddress.AssignLiteral(BLUETOOTH_ADDRESS_NONE);
|
||||
mMasSocket = nullptr;
|
||||
|
||||
Listen();
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::Disconnect(BluetoothProfileController* aController)
|
||||
{
|
||||
if (!mMasSocket) {
|
||||
BT_WARNING("%s: No ongoing connection to disconnect", __FUNCTION__);
|
||||
return;
|
||||
}
|
||||
|
||||
mMasSocket->Close();
|
||||
}
|
||||
|
||||
NS_IMPL_ISUPPORTS(BluetoothMapSmsManager, nsIObserver)
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::Connect(const nsAString& aDeviceAddress,
|
||||
BluetoothProfileController* aController)
|
||||
{
|
||||
MOZ_ASSERT(false);
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::OnGetServiceChannel(const nsAString& aDeviceAddress,
|
||||
const nsAString& aServiceUuid,
|
||||
int aChannel)
|
||||
{
|
||||
MOZ_ASSERT(false);
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::OnUpdateSdpRecords(const nsAString& aDeviceAddress)
|
||||
{
|
||||
MOZ_ASSERT(false);
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::OnConnect(const nsAString& aErrorStr)
|
||||
{
|
||||
MOZ_ASSERT(false);
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::OnDisconnect(const nsAString& aErrorStr)
|
||||
{
|
||||
MOZ_ASSERT(false);
|
||||
}
|
||||
|
||||
void
|
||||
BluetoothMapSmsManager::Reset()
|
||||
{
|
||||
MOZ_ASSERT(false);
|
||||
}
|
||||
|
||||
END_BLUETOOTH_NAMESPACE
|
|
@ -0,0 +1,150 @@
|
|||
/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
|
||||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
#ifndef mozilla_dom_bluetooth_bluedroid_BluetoothMapSmsManager_h
|
||||
#define mozilla_dom_bluetooth_bluedroid_BluetoothMapSmsManager_h
|
||||
|
||||
#include "BluetoothCommon.h"
|
||||
#include "BluetoothMapFolder.h"
|
||||
#include "BluetoothProfileManagerBase.h"
|
||||
#include "BluetoothSocketObserver.h"
|
||||
#include "mozilla/ipc/SocketBase.h"
|
||||
|
||||
BEGIN_BLUETOOTH_NAMESPACE
|
||||
|
||||
struct Map {
|
||||
enum AppParametersTagId {
|
||||
MaxListCount = 0x1,
|
||||
StartOffset = 0x2,
|
||||
FilterMessageType = 0x3,
|
||||
FilterPeriodBegin = 0x4,
|
||||
FilterPeriodEnd = 0x5,
|
||||
FilterReadStatus = 0x6,
|
||||
FilterRecipient = 0x7,
|
||||
FilterOriginator = 0x8,
|
||||
FilterPriority = 0x9,
|
||||
Attachment = 0x0A,
|
||||
Transparent = 0x0B,
|
||||
Retry = 0x0C,
|
||||
NewMessage = 0x0D,
|
||||
NotificationStatus = 0x0E,
|
||||
MASInstanceId = 0x0F,
|
||||
ParameterMask = 0x10,
|
||||
FolderListingSize = 0x11,
|
||||
MessagesListingSize = 0x12,
|
||||
SubjectLength = 0x13,
|
||||
Charset = 0x14,
|
||||
FractionRequest = 0x15,
|
||||
FractionDeliver = 0x16,
|
||||
StatusIndicator = 0x17,
|
||||
StatusValue = 0x18,
|
||||
MSETime = 0x19
|
||||
};
|
||||
};
|
||||
|
||||
class BluetoothNamedValue;
|
||||
class BluetoothSocket;
|
||||
class ObexHeaderSet;
|
||||
|
||||
/*
|
||||
* BluetoothMapSmsManager acts as Message Server Equipment (MSE) and runs both
|
||||
* MAS server and MNS client to exchange SMS/MMS message.
|
||||
*/
|
||||
|
||||
class BluetoothMapSmsManager : public BluetoothSocketObserver
|
||||
, public BluetoothProfileManagerBase
|
||||
{
|
||||
public:
|
||||
BT_DECL_PROFILE_MGR_BASE
|
||||
BT_DECL_SOCKET_OBSERVER
|
||||
virtual void GetName(nsACString& aName)
|
||||
{
|
||||
aName.AssignLiteral("MapSms");
|
||||
}
|
||||
|
||||
static const int MAX_PACKET_LENGTH = 0xFFFE;
|
||||
static const int MAX_INSTANCE_ID = 255;
|
||||
// SDP record for SupportedMessageTypes
|
||||
static const int SDP_MESSAGE_TYPE_EMAIL = 0x01;
|
||||
static const int SDP_MESSAGE_TYPE_SMS_GSM = 0x02;
|
||||
static const int SDP_MESSAGE_TYPE_SMS_CDMA = 0x04;
|
||||
static const int SDP_MESSAGE_TYPE_MMS = 0x08;
|
||||
// By defualt SMS/MMS is default supported
|
||||
static const int SDP_SMS_MMS_INSTANCE_ID = 0;
|
||||
|
||||
static BluetoothMapSmsManager* Get();
|
||||
bool Listen();
|
||||
|
||||
protected:
|
||||
virtual ~BluetoothMapSmsManager();
|
||||
|
||||
private:
|
||||
BluetoothMapSmsManager();
|
||||
bool Init();
|
||||
void HandleShutdown();
|
||||
|
||||
void ReplyToConnect();
|
||||
void ReplyToDisconnectOrAbort();
|
||||
void ReplyToSetPath();
|
||||
void ReplyToPut();
|
||||
void ReplyError(uint8_t aError);
|
||||
|
||||
void HandleNotificationRegistration(const ObexHeaderSet& aHeader);
|
||||
void HandleEventReport(const ObexHeaderSet& aHeader);
|
||||
void HandleMessageStatus(const ObexHeaderSet& aHeader);
|
||||
void HandleSmsMmsFolderListing(const ObexHeaderSet& aHeader);
|
||||
void SendMasObexData(uint8_t* aData, uint8_t aOpcode, int aSize);
|
||||
void SendMnsObexData(uint8_t* aData, uint8_t aOpcode, int aSize);
|
||||
|
||||
uint8_t SetPath(uint8_t flags, const ObexHeaderSet& aHeader);
|
||||
bool CompareHeaderTarget(const ObexHeaderSet& aHeader);
|
||||
void AfterMapSmsConnected();
|
||||
void AfterMapSmsDisconnected();
|
||||
void CreateMnsObexConnection();
|
||||
void DestroyMnsObexConnection();
|
||||
void SendMnsConnectRequest();
|
||||
void SendMnsDisconnectRequest();
|
||||
void MnsDataHandler(mozilla::ipc::UnixSocketBuffer* aMessage);
|
||||
void MasDataHandler(mozilla::ipc::UnixSocketBuffer* aMessage);
|
||||
/*
|
||||
* Build mandatory folders
|
||||
*/
|
||||
void BuildDefaultFolderStructure();
|
||||
/**
|
||||
* Current virtual folder path
|
||||
*/
|
||||
BluetoothMapFolder* mCurrentFolder;
|
||||
nsRefPtr<BluetoothMapFolder> mRootFolder;
|
||||
|
||||
/*
|
||||
* Record the last command
|
||||
*/
|
||||
int mLastCommand;
|
||||
// MAS OBEX session status. Set when MAS OBEX session is established.
|
||||
bool mMasConnected;
|
||||
// MNS OBEX session status. Set when MNS OBEX session is established.
|
||||
bool mMnsConnected;
|
||||
bool mNtfRequired;
|
||||
nsString mDeviceAddress;
|
||||
unsigned int mRemoteMaxPacketLength;
|
||||
|
||||
// If a connection has been established, mMasSocket will be the socket
|
||||
// communicating with the remote socket. We maintain the invariant that if
|
||||
// mMasSocket is non-null, mServerSocket must be null (and vice versa).
|
||||
nsRefPtr<BluetoothSocket> mMasSocket;
|
||||
|
||||
// Server socket. Once an inbound connection is established, it will hand
|
||||
// over the ownership to mMasSocket, and get a new server socket while Listen()
|
||||
// is called.
|
||||
nsRefPtr<BluetoothSocket> mMasServerSocket;
|
||||
|
||||
// Message notification service client socket
|
||||
nsRefPtr<BluetoothSocket> mMnsSocket;
|
||||
};
|
||||
|
||||
END_BLUETOOTH_NAMESPACE
|
||||
|
||||
#endif //mozilla_dom_bluetooth_bluedroid_BluetoothMapSmsManager_h
|
|
@ -380,7 +380,6 @@ BluetoothOppManager::StartSendingNextFile()
|
|||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
||||
MOZ_ASSERT(!IsConnected());
|
||||
MOZ_ASSERT(!mBatches.IsEmpty());
|
||||
MOZ_ASSERT((int)mBatches[0].mBlobs.Length() > mCurrentBlobIndex + 1);
|
||||
|
||||
|
@ -473,6 +472,7 @@ bool
|
|||
BluetoothOppManager::ProcessNextBatch()
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
MOZ_ASSERT(!IsConnected());
|
||||
|
||||
// Remove the processed batch.
|
||||
// A batch is processed if we've incremented mCurrentBlobIndex for it.
|
||||
|
@ -605,9 +605,11 @@ BluetoothOppManager::AfterOppDisconnected()
|
|||
mOutputStream = nullptr;
|
||||
}
|
||||
|
||||
if (mReadFileThread) {
|
||||
mReadFileThread->Shutdown();
|
||||
mReadFileThread = nullptr;
|
||||
// Store local pointer of |mReadFileThread| to avoid shutdown reentry crash
|
||||
// See bug 1191715 comment 19 for more details.
|
||||
nsCOMPtr<nsIThread> thread = mReadFileThread.forget();
|
||||
if (thread) {
|
||||
thread->Shutdown();
|
||||
}
|
||||
|
||||
// Release the mount lock if file transfer completed
|
||||
|
@ -790,7 +792,7 @@ BluetoothOppManager::RetrieveSentFileName()
|
|||
}
|
||||
|
||||
/**
|
||||
* We try our best to get the file extention to avoid interoperability issues.
|
||||
* We try our best to get the file extension to avoid interoperability issues.
|
||||
* However, once we found that we are unable to get suitable extension or
|
||||
* information about the content type, sending a pre-defined file name without
|
||||
* extension would be fine.
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
#include "BluetoothGattManager.h"
|
||||
#include "BluetoothHfpManager.h"
|
||||
#include "BluetoothHidManager.h"
|
||||
#include "BluetoothMapSmsManager.h"
|
||||
#include "BluetoothOppManager.h"
|
||||
#include "BluetoothPbapManager.h"
|
||||
#include "BluetoothProfileController.h"
|
||||
|
@ -301,6 +302,7 @@ BluetoothServiceBluedroid::StopInternal(BluetoothReplyRunnable* aRunnable)
|
|||
BluetoothA2dpManager::Get(),
|
||||
BluetoothOppManager::Get(),
|
||||
BluetoothPbapManager::Get(),
|
||||
BluetoothMapSmsManager::Get(),
|
||||
BluetoothHidManager::Get()
|
||||
};
|
||||
|
||||
|
@ -1520,6 +1522,11 @@ BluetoothServiceBluedroid::AdapterStateChangedNotification(bool aState)
|
|||
if (!pbap || !pbap->Listen()) {
|
||||
BT_LOGR("Fail to start BluetoothPbapManager listening");
|
||||
}
|
||||
|
||||
BluetoothMapSmsManager* map = BluetoothMapSmsManager::Get();
|
||||
if (!map || !map->Listen()) {
|
||||
BT_LOGR("Fail to start BluetoothMapSmsManager listening");
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve promise if existed
|
||||
|
|
|
@ -367,7 +367,6 @@ BluetoothOppManager::StartSendingNextFile()
|
|||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
||||
MOZ_ASSERT(!IsConnected());
|
||||
MOZ_ASSERT(!mBatches.IsEmpty());
|
||||
MOZ_ASSERT((int)mBatches[0].mBlobs.Length() > mCurrentBlobIndex + 1);
|
||||
|
||||
|
@ -460,6 +459,7 @@ bool
|
|||
BluetoothOppManager::ProcessNextBatch()
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
MOZ_ASSERT(!IsConnected());
|
||||
|
||||
// Remove the processed batch.
|
||||
// A batch is processed if we've incremented mCurrentBlobIndex for it.
|
||||
|
@ -592,9 +592,11 @@ BluetoothOppManager::AfterOppDisconnected()
|
|||
mOutputStream = nullptr;
|
||||
}
|
||||
|
||||
if (mReadFileThread) {
|
||||
mReadFileThread->Shutdown();
|
||||
mReadFileThread = nullptr;
|
||||
// Store local pointer of |mReadFileThread| to avoid shutdown reentry crash
|
||||
// See bug 1191715 comment 19 for more details.
|
||||
nsCOMPtr<nsIThread> thread = mReadFileThread.forget();
|
||||
if (thread) {
|
||||
thread->Shutdown();
|
||||
}
|
||||
|
||||
// Release the mount lock if file transfer completed
|
||||
|
@ -777,7 +779,7 @@ BluetoothOppManager::RetrieveSentFileName()
|
|||
}
|
||||
|
||||
/**
|
||||
* We try our best to get the file extention to avoid interoperability issues.
|
||||
* We try our best to get the file extension to avoid interoperability issues.
|
||||
* However, once we found that we are unable to get suitable extension or
|
||||
* information about the content type, sending a pre-defined file name without
|
||||
* extension would be fine.
|
||||
|
|
|
@ -65,6 +65,14 @@ AppendHeaderBody(uint8_t* aRetBuf, int aBufferSize, const uint8_t* aBody,
|
|||
aBody, aLength);
|
||||
}
|
||||
|
||||
int
|
||||
AppendHeaderTarget(uint8_t* aRetBuf, int aBufferSize, const uint8_t* aTarget,
|
||||
int aLength)
|
||||
{
|
||||
return AppendHeader(ObexHeaderId::Target, aRetBuf, aBufferSize,
|
||||
aTarget, aLength);
|
||||
}
|
||||
|
||||
int
|
||||
AppendHeaderWho(uint8_t* aRetBuf, int aBufferSize, const uint8_t* aWho,
|
||||
int aLength)
|
||||
|
|
|
@ -343,6 +343,8 @@ int AppendHeaderName(uint8_t* aRetBuf, int aBufferSize, const uint8_t* aName,
|
|||
int aLength);
|
||||
int AppendHeaderBody(uint8_t* aRetBuf, int aBufferSize, const uint8_t* aBody,
|
||||
int aLength);
|
||||
int AppendHeaderTarget(uint8_t* aRetBuf, int aBufferSize, const uint8_t* aTarget,
|
||||
int aLength);
|
||||
int AppendHeaderWho(uint8_t* aRetBuf, int aBufferSize, const uint8_t* aWho,
|
||||
int aLength);
|
||||
int AppendHeaderAppParameters(uint8_t* aRetBuf, int aBufferSize,
|
||||
|
|
|
@ -81,6 +81,8 @@ if CONFIG['MOZ_B2G_BT']:
|
|||
'bluedroid/BluetoothDaemonSetupInterface.cpp',
|
||||
'bluedroid/BluetoothDaemonSocketInterface.cpp',
|
||||
'bluedroid/BluetoothGattManager.cpp',
|
||||
'bluedroid/BluetoothMapFolder.cpp',
|
||||
'bluedroid/BluetoothMapSmsManager.cpp',
|
||||
'bluedroid/BluetoothOppManager.cpp',
|
||||
'bluedroid/BluetoothPbapManager.cpp',
|
||||
'bluedroid/BluetoothServiceBluedroid.cpp',
|
||||
|
|
|
@ -381,6 +381,7 @@ SettingsManager.prototype = {
|
|||
debug("WARNING: MORE THAN " + kObserverSoftLimit + " OBSERVERS FOR " +
|
||||
aName + ": " + length + " FROM" + (new Error).stack);
|
||||
#ifdef DEBUG
|
||||
debug("JS STOPS EXECUTING AT THIS POINT IN DEBUG BUILDS!");
|
||||
throw Components.results.NS_ERROR_ABORT;
|
||||
#endif
|
||||
}
|
||||
|
|
|
@ -776,7 +776,12 @@ let gTestSuite = (function() {
|
|||
.then(() => runEmulatorShellSafe(['hostapd', '-B', configFileName]))
|
||||
.then(function (reply) {
|
||||
// It may fail at the first time due to the previous ungracefully terminated one.
|
||||
if (reply[0] === 'bind(PF_UNIX): Address already in use') {
|
||||
if (reply.length === 0) {
|
||||
// The hostapd starts successfully
|
||||
return;
|
||||
}
|
||||
|
||||
if (reply[0].indexOf('bind(PF_UNIX): Address already in use') !== -1) {
|
||||
return startOneHostapd(aIndex);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -14,6 +14,8 @@ const STATIC_IP_CONFIG = {
|
|||
dns2: "8.8.4.4",
|
||||
};
|
||||
|
||||
const TESTING_HOSTAPD = [{ ssid: 'ap0' }];
|
||||
|
||||
function testAssociateWithStaticIp(aNetwork, aStaticIpConfig) {
|
||||
return gTestSuite.setStaticIpMode(aNetwork, aStaticIpConfig)
|
||||
.then(() => gTestSuite.testAssociate(aNetwork))
|
||||
|
@ -32,10 +34,32 @@ function testAssociateWithStaticIp(aNetwork, aStaticIpConfig) {
|
|||
});
|
||||
}
|
||||
|
||||
function findDesireNetwork(aNetworks) {
|
||||
let i = gTestSuite.getFirstIndexBySsid(TESTING_HOSTAPD[0].ssid, aNetworks);
|
||||
|
||||
if (-1 !== i) {
|
||||
return aNetworks[i];
|
||||
}
|
||||
|
||||
return aNetworks[0];
|
||||
}
|
||||
|
||||
// Start test.
|
||||
gTestSuite.doTest(function() {
|
||||
gTestSuite.doTestWithoutStockAp(function() {
|
||||
return gTestSuite.ensureWifiEnabled(true)
|
||||
|
||||
// Start custom hostapd for testing.
|
||||
.then(() => gTestSuite.startHostapds(TESTING_HOSTAPD))
|
||||
.then(() => gTestSuite.verifyNumOfProcesses('hostapd',
|
||||
TESTING_HOSTAPD.length))
|
||||
|
||||
// Perform a wifi scan, and then run the static ip test
|
||||
.then(() => gTestSuite.requestWifiScan())
|
||||
.then((aNetworks) => testAssociateWithStaticIp(aNetworks[0],
|
||||
STATIC_IP_CONFIG));
|
||||
.then((aNetworks) => findDesireNetwork(aNetworks))
|
||||
.then((aNetwork) => testAssociateWithStaticIp(aNetwork,
|
||||
STATIC_IP_CONFIG))
|
||||
|
||||
// Kill running hostapd.
|
||||
.then(gTestSuite.killAllHostapd)
|
||||
.then(() => gTestSuite.verifyNumOfProcesses('hostapd', 0));
|
||||
});
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<resources>
|
||||
<dimen name="tab_panel_column_width">156dip</dimen>
|
||||
<dimen name="tab_thumbnail_height">110dip</dimen>
|
||||
<dimen name="tab_thumbnail_width">148dip</dimen>
|
||||
</resources>
|
|
@ -4,7 +4,7 @@
|
|||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
|
||||
<resources>
|
||||
<dimen name="tab_panel_column_width">174dip</dimen>
|
||||
<dimen name="tab_panel_column_width">176dip</dimen>
|
||||
<dimen name="tab_thumbnail_height">120dip</dimen>
|
||||
<dimen name="tab_thumbnail_width">168dip</dimen>
|
||||
</resources>
|
||||
|
|
|
@ -104,12 +104,6 @@ this.BrowserIDManager.prototype = {
|
|||
// we don't consider the lack of a keybundle as a failure state.
|
||||
_shouldHaveSyncKeyBundle: false,
|
||||
|
||||
get readyToAuthenticate() {
|
||||
// We are finished initializing when we *should* have a sync key bundle,
|
||||
// although we might not actually have one due to auth failures etc.
|
||||
return this._shouldHaveSyncKeyBundle;
|
||||
},
|
||||
|
||||
get needsCustomization() {
|
||||
try {
|
||||
return Services.prefs.getBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION);
|
||||
|
@ -122,7 +116,19 @@ this.BrowserIDManager.prototype = {
|
|||
for (let topic of OBSERVER_TOPICS) {
|
||||
Services.obs.addObserver(this, topic, false);
|
||||
}
|
||||
return this.initializeWithCurrentIdentity();
|
||||
// and a background fetch of account data just so we can set this.account,
|
||||
// so we have a username available before we've actually done a login.
|
||||
// XXX - this is actually a hack just for tests and really shouldn't be
|
||||
// necessary. Also, you'd think it would be safe to allow this.account to
|
||||
// be set to null when there's no user logged in, but argue with the test
|
||||
// suite, not with me :)
|
||||
this._fxaService.getSignedInUser().then(accountData => {
|
||||
if (accountData) {
|
||||
this.account = accountData.email;
|
||||
}
|
||||
}).catch(err => {
|
||||
// As above, this is only for tests so it is safe to ignore.
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -130,7 +136,7 @@ this.BrowserIDManager.prototype = {
|
|||
* the user is logged in, or is rejected if the login attempt has failed.
|
||||
*/
|
||||
ensureLoggedIn: function() {
|
||||
if (!this._shouldHaveSyncKeyBundle) {
|
||||
if (!this._shouldHaveSyncKeyBundle && this.whenReadyToAuthenticate) {
|
||||
// We are already in the process of logging in.
|
||||
return this.whenReadyToAuthenticate.promise;
|
||||
}
|
||||
|
@ -160,7 +166,6 @@ this.BrowserIDManager.prototype = {
|
|||
}
|
||||
this.resetCredentials();
|
||||
this._signedInUser = null;
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
offerSyncOptions: function () {
|
||||
|
@ -294,7 +299,8 @@ this.BrowserIDManager.prototype = {
|
|||
// reauth with the server - in that case we will also get here, but
|
||||
// should have the same identity.
|
||||
// initializeWithCurrentIdentity will throw and log if these constraints
|
||||
// aren't met, so just go ahead and do the init.
|
||||
// aren't met (indirectly, via _updateSignedInUser()), so just go ahead
|
||||
// and do the init.
|
||||
this.initializeWithCurrentIdentity(true);
|
||||
break;
|
||||
|
||||
|
@ -636,7 +642,6 @@ this.BrowserIDManager.prototype = {
|
|||
// that there is no authentication dance still under way.
|
||||
this._shouldHaveSyncKeyBundle = true;
|
||||
Weave.Status.login = this._authFailureReason;
|
||||
Services.obs.notifyObservers(null, "weave:ui:login:error", null);
|
||||
throw err;
|
||||
});
|
||||
},
|
||||
|
|
|
@ -122,7 +122,6 @@ LOGIN_FAILED_NETWORK_ERROR: "error.login.reason.network",
|
|||
LOGIN_FAILED_SERVER_ERROR: "error.login.reason.server",
|
||||
LOGIN_FAILED_INVALID_PASSPHRASE: "error.login.reason.recoverykey",
|
||||
LOGIN_FAILED_LOGIN_REJECTED: "error.login.reason.account",
|
||||
LOGIN_FAILED_NOT_READY: "error.login.reason.initializing",
|
||||
|
||||
// sync failure status codes
|
||||
METARECORD_DOWNLOAD_FAIL: "error.sync.reason.metarecord_download_fail",
|
||||
|
|
|
@ -85,18 +85,14 @@ IdentityManager.prototype = {
|
|||
_syncKeyBundle: null,
|
||||
|
||||
/**
|
||||
* Initialize the identity provider. Returns a promise that is resolved
|
||||
* when initialization is complete and the provider can be queried for
|
||||
* its state
|
||||
* Initialize the identity provider.
|
||||
*/
|
||||
initialize: function() {
|
||||
// Nothing to do for this identity provider.
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
finalize: function() {
|
||||
// Nothing to do for this identity provider.
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -115,14 +111,6 @@ IdentityManager.prototype = {
|
|||
return Promise.resolve();
|
||||
},
|
||||
|
||||
/**
|
||||
* Indicates if the identity manager is still initializing
|
||||
*/
|
||||
get readyToAuthenticate() {
|
||||
// We initialize in a fully sync manner, so we are always finished.
|
||||
return true;
|
||||
},
|
||||
|
||||
get account() {
|
||||
return Svc.Prefs.get("account", this.username);
|
||||
},
|
||||
|
|
|
@ -690,13 +690,6 @@ Sync11Service.prototype = {
|
|||
},
|
||||
|
||||
verifyLogin: function verifyLogin(allow40XRecovery = true) {
|
||||
// If the identity isn't ready it might not know the username...
|
||||
if (!this.identity.readyToAuthenticate) {
|
||||
this._log.info("Not ready to authenticate in verifyLogin.");
|
||||
this.status.login = LOGIN_FAILED_NOT_READY;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.identity.username) {
|
||||
this._log.warn("No username in verifyLogin.");
|
||||
this.status.login = LOGIN_FAILED_NO_USERNAME;
|
||||
|
@ -943,25 +936,22 @@ Sync11Service.prototype = {
|
|||
return;
|
||||
}
|
||||
|
||||
this.identity.finalize().then(
|
||||
() => {
|
||||
// an observer so the FxA migration code can take some action before
|
||||
// the new identity is created.
|
||||
Svc.Obs.notify("weave:service:start-over:init-identity");
|
||||
this.identity.username = "";
|
||||
this.status.__authManager = null;
|
||||
this.identity = Status._authManager;
|
||||
this._clusterManager = this.identity.createClusterManager(this);
|
||||
Svc.Obs.notify("weave:service:start-over:finish");
|
||||
}
|
||||
).then(null,
|
||||
err => {
|
||||
this._log.error("startOver failed to re-initialize the identity manager: " + err);
|
||||
// Still send the observer notification so the current state is
|
||||
// reflected in the UI.
|
||||
Svc.Obs.notify("weave:service:start-over:finish");
|
||||
}
|
||||
);
|
||||
try {
|
||||
this.identity.finalize();
|
||||
// an observer so the FxA migration code can take some action before
|
||||
// the new identity is created.
|
||||
Svc.Obs.notify("weave:service:start-over:init-identity");
|
||||
this.identity.username = "";
|
||||
this.status.__authManager = null;
|
||||
this.identity = Status._authManager;
|
||||
this._clusterManager = this.identity.createClusterManager(this);
|
||||
Svc.Obs.notify("weave:service:start-over:finish");
|
||||
} catch (err) {
|
||||
this._log.error("startOver failed to re-initialize the identity manager: " + err);
|
||||
// Still send the observer notification so the current state is
|
||||
// reflected in the UI.
|
||||
Svc.Obs.notify("weave:service:start-over:finish");
|
||||
}
|
||||
},
|
||||
|
||||
persistLogin: function persistLogin() {
|
||||
|
|
|
@ -30,10 +30,7 @@ this.Status = {
|
|||
.wrappedJSObject;
|
||||
let idClass = service.fxAccountsEnabled ? BrowserIDManager : IdentityManager;
|
||||
this.__authManager = new idClass();
|
||||
// .initialize returns a promise, so we need to spin until it resolves.
|
||||
let cb = Async.makeSpinningCallback();
|
||||
this.__authManager.initialize().then(cb, cb);
|
||||
cb.wait();
|
||||
this.__authManager.initialize();
|
||||
return this.__authManager;
|
||||
},
|
||||
|
||||
|
|
|
@ -652,8 +652,8 @@ Livemark.prototype = {
|
|||
let nodes = this._nodes.get(container);
|
||||
for (let node of nodes) {
|
||||
// Workaround for bug 449811.
|
||||
localObserver = observer;
|
||||
localNode = node;
|
||||
let localObserver = observer;
|
||||
let localNode = node;
|
||||
if (!aURI || node.uri == aURI.spec) {
|
||||
Services.tm.mainThread.dispatch(() => {
|
||||
localObserver.nodeHistoryDetailsChanged(localNode, 0, aVisitedStatus);
|
||||
|
|
|
@ -75,12 +75,13 @@ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12
|
|||
* This is thrown by |TelemetryStorage.loadPingFile| when reading the ping
|
||||
* from the disk fails.
|
||||
*/
|
||||
function PingReadError(message="Error reading the ping file") {
|
||||
function PingReadError(message="Error reading the ping file", becauseNoSuchFile = false) {
|
||||
Error.call(this, message);
|
||||
let error = new Error();
|
||||
this.name = "PingReadError";
|
||||
this.message = message;
|
||||
this.stack = error.stack;
|
||||
this.becauseNoSuchFile = becauseNoSuchFile;
|
||||
}
|
||||
PingReadError.prototype = Object.create(Error.prototype);
|
||||
PingReadError.prototype.constructor = PingReadError;
|
||||
|
@ -1445,7 +1446,7 @@ let TelemetryStorageImpl = {
|
|||
array = yield OS.File.read(aFilePath, options);
|
||||
} catch(e) {
|
||||
this._log.trace("loadPingfile - unreadable ping " + aFilePath, e);
|
||||
throw new PingReadError(e.message);
|
||||
throw new PingReadError(e.message, e.becauseNoSuchFile);
|
||||
}
|
||||
|
||||
let decoder = new TextDecoder();
|
||||
|
|
|
@ -324,7 +324,14 @@ let AnimationPlayerActor = ActorClass({
|
|||
*/
|
||||
onAnimationMutation: function(mutations) {
|
||||
let hasChanged = false;
|
||||
for (let {changedAnimations} of mutations) {
|
||||
for (let {removedAnimations, changedAnimations} of mutations) {
|
||||
if (removedAnimations.length) {
|
||||
// Reset the local copy of the state on removal, since the animation can
|
||||
// be kept on the client and re-added, its state needs to be sent in
|
||||
// full.
|
||||
this.currentState = null;
|
||||
}
|
||||
|
||||
if (!changedAnimations.length) {
|
||||
return;
|
||||
}
|
||||
|
@ -689,11 +696,14 @@ let AnimationsActor = exports.AnimationsActor = ActorClass({
|
|||
// already have, it means it's a transition that's re-starting. So send
|
||||
// a "removed" event for the one we already have.
|
||||
let index = this.actors.findIndex(a => {
|
||||
return a.player.constructor === player.constructor &&
|
||||
((a.isAnimation() &&
|
||||
a.player.animationName === player.animationName) ||
|
||||
(a.isTransition() &&
|
||||
a.player.transitionProperty === player.transitionProperty));
|
||||
let isSameType = a.player.constructor === player.constructor;
|
||||
let isSameName = (a.isAnimation() &&
|
||||
a.player.animationName === player.animationName) ||
|
||||
(a.isTransition() &&
|
||||
a.player.transitionProperty === player.transitionProperty);
|
||||
let isSameNode = a.player.effect.target === player.effect.target;
|
||||
|
||||
return isSameType && isSameNode && isSameName;
|
||||
});
|
||||
if (index !== -1) {
|
||||
eventData.push({
|
||||
|
|
Загрузка…
Ссылка в новой задаче