From b169925da439dcd54b32a9ed9e4bcf510a775f3a Mon Sep 17 00:00:00 2001 From: Yoshi Huang Date: Wed, 20 Aug 2014 17:35:18 +0800 Subject: [PATCH 01/88] Bug 1055560 - Part 1: DOM Change. r=smaug --- dom/nfc/MozNDEFRecord.h | 5 +++-- dom/webidl/MozNDEFRecord.webidl | 24 +++++++++++++----------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/dom/nfc/MozNDEFRecord.h b/dom/nfc/MozNDEFRecord.h index ee0591505f36..8fc2cdb8c2f8 100644 --- a/dom/nfc/MozNDEFRecord.h +++ b/dom/nfc/MozNDEFRecord.h @@ -15,6 +15,7 @@ #include "nsWrapperCache.h" #include "jsapi.h" +#include "mozilla/dom/MozNDEFRecordBinding.h" #include "mozilla/dom/TypedArray.h" #include "jsfriendapi.h" #include "js/GCAPI.h" @@ -53,7 +54,7 @@ public: const MozNDEFRecordOptions& aOptions, ErrorResult& aRv); - uint8_t Tnf() const + TNF Tnf() const { return mTnf; } @@ -88,7 +89,7 @@ private: void HoldData(); void DropData(); - uint8_t mTnf; + TNF mTnf; JS::Heap mType; JS::Heap mId; JS::Heap mPayload; diff --git a/dom/webidl/MozNDEFRecord.webidl b/dom/webidl/MozNDEFRecord.webidl index 2baf16ebd533..7e02647a6c41 100644 --- a/dom/webidl/MozNDEFRecord.webidl +++ b/dom/webidl/MozNDEFRecord.webidl @@ -5,22 +5,24 @@ /* Copyright © 2013 Deutsche Telekom, Inc. */ +enum TNF { + "empty", + "well-known", + "media-type", + "absolute-uri", + "external", + "unknown", + "unchanged" +}; + [Constructor(optional MozNDEFRecordOptions options)] interface MozNDEFRecord { /** - * Type Name Field (3-bits) - Specifies the NDEF record type in general. - * tnf_empty: 0x00 - * tnf_well_known: 0x01 - * tnf_mime_media: 0x02 - * tnf_absolute_uri: 0x03 - * tnf_external type: 0x04 - * tnf_unknown: 0x05 - * tnf_unchanged: 0x06 - * tnf_reserved: 0x07 + * Type Name Field - Specifies the NDEF record type in general. */ [Constant] - readonly attribute octet tnf; + readonly attribute TNF tnf; /** * type - Describes the content of the payload. This can be a mime type. @@ -43,7 +45,7 @@ interface MozNDEFRecord }; dictionary MozNDEFRecordOptions { - octet tnf = 0; // default to tnf_empty. + TNF tnf = "empty"; Uint8Array type; Uint8Array id; Uint8Array payload; From 76d367acf0594355d066131123879c1708cacd72 Mon Sep 17 00:00:00 2001 From: Yoshi Huang Date: Wed, 20 Aug 2014 19:50:29 +0800 Subject: [PATCH 02/88] Bug 1055560 - Part 2: NfcService changes. r=smaug --- dom/nfc/gonk/NfcMessageHandler.cpp | 6 ++++-- dom/nfc/gonk/NfcOptions.h | 3 ++- dom/nfc/gonk/NfcService.cpp | 1 + 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/dom/nfc/gonk/NfcMessageHandler.cpp b/dom/nfc/gonk/NfcMessageHandler.cpp index 76a0eae9ca3f..080a2d65e60f 100644 --- a/dom/nfc/gonk/NfcMessageHandler.cpp +++ b/dom/nfc/gonk/NfcMessageHandler.cpp @@ -4,6 +4,7 @@ #include "NfcMessageHandler.h" #include +#include "mozilla/dom/MozNDEFRecordBinding.h" #include "nsDebug.h" #include "NfcGonkMessage.h" #include "NfcOptions.h" @@ -13,6 +14,7 @@ using namespace android; using namespace mozilla; +using namespace mozilla::dom; static const char* kConfigRequest = "config"; static const char* kGetDetailsNDEF = "getDetailsNDEF"; @@ -330,7 +332,7 @@ NfcMessageHandler::ReadNDEFMessage(const Parcel& aParcel, EventOptions& aOptions for (int i = 0; i < recordCount; i++) { int32_t tnf = aParcel.readInt32(); NDEFRecordStruct record; - record.mTnf = tnf; + record.mTnf = static_cast(tnf); int32_t typeLength = aParcel.readInt32(); record.mType.AppendElements( @@ -357,7 +359,7 @@ NfcMessageHandler::WriteNDEFMessage(Parcel& aParcel, const CommandOptions& aOpti aParcel.writeInt32(recordCount); for (int i = 0; i < recordCount; i++) { const NDEFRecordStruct& record = aOptions.mRecords[i]; - aParcel.writeInt32(record.mTnf); + aParcel.writeInt32(static_cast(record.mTnf)); void* data; diff --git a/dom/nfc/gonk/NfcOptions.h b/dom/nfc/gonk/NfcOptions.h index b32213b8d6ef..fca0a75edddc 100644 --- a/dom/nfc/gonk/NfcOptions.h +++ b/dom/nfc/gonk/NfcOptions.h @@ -6,12 +6,13 @@ #define NfcOptions_h #include "mozilla/dom/NfcOptionsBinding.h" +#include "mozilla/dom/MozNDEFRecordBinding.h" namespace mozilla { struct NDEFRecordStruct { - uint8_t mTnf; + dom::TNF mTnf; nsTArray mType; nsTArray mId; nsTArray mPayload; diff --git a/dom/nfc/gonk/NfcService.cpp b/dom/nfc/gonk/NfcService.cpp index c2bd6336516e..4bb84b17c0b7 100644 --- a/dom/nfc/gonk/NfcService.cpp +++ b/dom/nfc/gonk/NfcService.cpp @@ -133,6 +133,7 @@ public: MozNDEFRecordOptions& record = *event.mRecords.Value().AppendElement(); record.mTnf = recordStruct.mTnf; + MOZ_ASSERT(record.mTnf < TNF::EndGuard_); if (recordStruct.mType.Length() > 0) { record.mType.Construct(); From 410800023168e37aff84c568640dfaca80f035ce Mon Sep 17 00:00:00 2001 From: Yoshi Huang Date: Thu, 21 Aug 2014 12:03:13 +0800 Subject: [PATCH 03/88] Bug 1055560 - Part 3: Update tests. r=dimi From 29bd27dc7ebcbc0ec4cce0681fa956eed7fad6ea Mon Sep 17 00:00:00 2001 --- dom/nfc/tests/marionette/head.js | 21 +++++++++++++++++---- dom/nfc/tests/marionette/test_ndef.js | 2 +- dom/nfc/tests/marionette/test_nfc_error_messages.js | 2 +- 3 files changed, 19 insertions(+), 6 deletions(-) --- dom/nfc/tests/marionette/head.js | 21 +++++++++++++++---- dom/nfc/tests/marionette/test_ndef.js | 2 +- .../marionette/test_nfc_error_messages.js | 2 +- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/dom/nfc/tests/marionette/head.js b/dom/nfc/tests/marionette/head.js index 5223e9d6c176..0443dd25ca24 100644 --- a/dom/nfc/tests/marionette/head.js +++ b/dom/nfc/tests/marionette/head.js @@ -126,8 +126,9 @@ let NCI = (function() { let TAG = (function() { function setData(re, flag, tnf, type, payload) { let deferred = Promise.defer(); + let tnfNum = NDEF.getTNFNum(tnf); let cmd = "nfc tag set " + re + - " [" + flag + "," + tnf + "," + type + ",," + payload + "]"; + " [" + flag + "," + tnfNum + "," + type + ",," + payload + "]"; emulator.run(cmd, function(result) { is(result.pop(), "OK", "set NDEF data of tag" + re); @@ -156,8 +157,9 @@ let TAG = (function() { let SNEP = (function() { function put(dsap, ssap, flags, tnf, type, id, payload) { let deferred = Promise.defer(); + let tnfNum = NDEF.getTNFNum(tnf); let cmd = "nfc snep put " + dsap + " " + ssap + " [" + flags + "," + - tnf + "," + + tnfNum + "," + type + "," + id + "," + payload + "]"; @@ -245,7 +247,18 @@ function runTests() { } const NDEF = { - TNF_WELL_KNOWN: 1, + TNF_WELL_KNOWN: "well-known", + + tnfValues: ["empty", "well-known", "media-type", "absolute-uri", "external", + "unknown", "unchanged", "reserved"], + + getTNFNum: function (tnfString) { + return this.tnfValues.indexOf(tnfString); + }, + + getTNFString: function(tnfNum) { + return this.tnfValues[tnfNum]; + }, // compares two NDEF messages compare: function(ndef1, ndef2) { @@ -290,7 +303,7 @@ const NDEF = { let type = NfcUtils.fromUTF8(this.atob(value.type)); let id = NfcUtils.fromUTF8(this.atob(value.id)); let payload = NfcUtils.fromUTF8(this.atob(value.payload)); - return new MozNDEFRecord({tnf: value.tnf, type: type, id: id, payload: payload}); + return new MozNDEFRecord({tnf: NDEF.getTNFString(value.tnf), type: type, id: id, payload: payload}); }, window); return ndef; } diff --git a/dom/nfc/tests/marionette/test_ndef.js b/dom/nfc/tests/marionette/test_ndef.js index b17d11a45ca0..1ca59ca637ea 100644 --- a/dom/nfc/tests/marionette/test_ndef.js +++ b/dom/nfc/tests/marionette/test_ndef.js @@ -8,7 +8,7 @@ function testConstructNDEF() { try { // omit type, id and payload. let r = new MozNDEFRecord(); - is(r.tnf, 0, "r.tnf should be 0"); + is(r.tnf, "empty", "r.tnf should be 'empty'"); is(r.type, null, "r.type should be null"); is(r.id, null, "r.id should be null"); is(r.payload, null, "r.payload should be null"); diff --git a/dom/nfc/tests/marionette/test_nfc_error_messages.js b/dom/nfc/tests/marionette/test_nfc_error_messages.js index 75ad583486da..4a91f2c758ee 100644 --- a/dom/nfc/tests/marionette/test_nfc_error_messages.js +++ b/dom/nfc/tests/marionette/test_nfc_error_messages.js @@ -10,7 +10,7 @@ const MARIONETTE_TIMEOUT = 60000; const MARIONETTE_HEAD_JS = 'head.js'; const MANIFEST_URL = 'app://system.gaiamobile.org/manifest.webapp'; -const NDEF_MESSAGE = [new MozNDEFRecord({tnf: 0x01, +const NDEF_MESSAGE = [new MozNDEFRecord({tnf: "well-known", type: new Uint8Array(0x84), payload: new Uint8Array(0x20)})]; From 8bc44bad7af6dcd825c7af02844b4732532e7070 Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Wed, 10 Sep 2014 02:50:48 -0700 Subject: [PATCH 04/88] Bumping gaia.json for 8 gaia revision(s) a=gaia-bump ======== https://hg.mozilla.org/integration/gaia-central/rev/c7b07c667231 Author: Carsten Book Desc: Merge pull request #23844 from oklang/Bug_1064735-Theme_apps_should_be_hidden_on_homescreen Bug 1064735 - Theme apps should be hidden on homescreen. r=21 ======== https://hg.mozilla.org/integration/gaia-central/rev/8ec752b23ce0 Author: Olle Klang Desc: Bug 1064735 - Theme apps should be hidden on homescreen. ======== https://hg.mozilla.org/integration/gaia-central/rev/cdcfaffc2b9b Author: Carsten Book Desc: Merge pull request #23694 from yfdyh000/937367 Bug 937367 - Fix links broken in Legal Information. r=ehung ======== https://hg.mozilla.org/integration/gaia-central/rev/dd3d243061dd Author: YFdyh000 Desc: Bug 937367 - Fix links broken in Legal Information ======== https://hg.mozilla.org/integration/gaia-central/rev/abc6f44f13fb Author: gasolin Desc: Merge pull request #23899 from gasolin/issue-1065271 Bug 1065271 - [settings] help panel code cleanup, r=arhtur ======== https://hg.mozilla.org/integration/gaia-central/rev/84eaded748e7 Author: gasolin Desc: Bug 1065271 - [settings] help panel code cleanup ======== https://hg.mozilla.org/integration/gaia-central/rev/4f44c9478754 Author: Guillaume C. Marty Desc: Merge pull request #23769 from gmarty/Bug-1063026-Music-player-refinments Bug 1063026 - [Utility Tray] Refinements to Music player on utility tray... ======== https://hg.mozilla.org/integration/gaia-central/rev/11e7c0d81618 Author: Guillaume Marty Desc: Bug 1063026 - [Utility Tray] Refinements to Music player on utility tray and lockscreen --- b2g/config/gaia.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index 6bd5dac85e81..821159a475d7 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -4,6 +4,6 @@ "remote": "", "branch": "" }, - "revision": "a4a76a4221d7d963d01377f38d68768d0e829017", + "revision": "c7b07c667231e473fb6ea4ef0c4d239ce5c881d0", "repo_path": "/integration/gaia-central" } From c72400c925d85a240aa7c3cb6a4f18149dac2a1f Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Wed, 10 Sep 2014 02:56:55 -0700 Subject: [PATCH 05/88] Bumping manifests a=b2g-bump --- b2g/config/dolphin/sources.xml | 2 +- b2g/config/emulator-ics/sources.xml | 2 +- b2g/config/emulator-jb/sources.xml | 2 +- b2g/config/emulator-kk/sources.xml | 2 +- b2g/config/emulator/sources.xml | 2 +- b2g/config/flame-kk/sources.xml | 2 +- b2g/config/flame/sources.xml | 2 +- b2g/config/hamachi/sources.xml | 2 +- b2g/config/helix/sources.xml | 2 +- b2g/config/nexus-4/sources.xml | 2 +- b2g/config/wasabi/sources.xml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/b2g/config/dolphin/sources.xml b/b2g/config/dolphin/sources.xml index d58e6ef7108b..f9e5a5f7844c 100644 --- a/b2g/config/dolphin/sources.xml +++ b/b2g/config/dolphin/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator-ics/sources.xml b/b2g/config/emulator-ics/sources.xml index 5538e57b0b7b..3000c9d4b769 100644 --- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/emulator-jb/sources.xml b/b2g/config/emulator-jb/sources.xml index 5fa9f1545de3..4e1594fd8b3c 100644 --- a/b2g/config/emulator-jb/sources.xml +++ b/b2g/config/emulator-jb/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/emulator-kk/sources.xml b/b2g/config/emulator-kk/sources.xml index 9950c2235ff9..98916c6a3b64 100644 --- a/b2g/config/emulator-kk/sources.xml +++ b/b2g/config/emulator-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator/sources.xml b/b2g/config/emulator/sources.xml index 5538e57b0b7b..3000c9d4b769 100644 --- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/flame-kk/sources.xml b/b2g/config/flame-kk/sources.xml index 819404a1c373..8254afccc7cc 100644 --- a/b2g/config/flame-kk/sources.xml +++ b/b2g/config/flame-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/flame/sources.xml b/b2g/config/flame/sources.xml index 7faf9d509080..a13bbb511e87 100644 --- a/b2g/config/flame/sources.xml +++ b/b2g/config/flame/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/hamachi/sources.xml b/b2g/config/hamachi/sources.xml index d97fc629132f..890c2aaa7833 100644 --- a/b2g/config/hamachi/sources.xml +++ b/b2g/config/hamachi/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/helix/sources.xml b/b2g/config/helix/sources.xml index c6941fbdbe7f..0ab4f244ddb4 100644 --- a/b2g/config/helix/sources.xml +++ b/b2g/config/helix/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/nexus-4/sources.xml b/b2g/config/nexus-4/sources.xml index 6e761f2125a6..bbac70a7a2ba 100644 --- a/b2g/config/nexus-4/sources.xml +++ b/b2g/config/nexus-4/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/wasabi/sources.xml b/b2g/config/wasabi/sources.xml index ad81afa22bbf..b912630e2a81 100644 --- a/b2g/config/wasabi/sources.xml +++ b/b2g/config/wasabi/sources.xml @@ -17,7 +17,7 @@ - + From f52fe75becde45ccee1be94bc297658555d4cbd1 Mon Sep 17 00:00:00 2001 From: Yoshi Huang Date: Thu, 28 Aug 2014 16:44:48 +0800 Subject: [PATCH 06/88] Bug 1007724 - Part 1: tnf validation. r=smaug From 5289124d435e3eed280c8a1728b55ab005ac0aae Mon Sep 17 00:00:00 2001 --- dom/nfc/MozNDEFRecord.cpp | 40 ++++++++++++++++++++++++++++++++++++++++ dom/nfc/MozNDEFRecord.h | 3 +++ 2 files changed, 43 insertions(+) --- dom/nfc/MozNDEFRecord.cpp | 40 +++++++++++++++++++++++++++++++++++++++ dom/nfc/MozNDEFRecord.h | 3 +++ 2 files changed, 43 insertions(+) diff --git a/dom/nfc/MozNDEFRecord.cpp b/dom/nfc/MozNDEFRecord.cpp index 913032f286c4..9a8ca3e34765 100644 --- a/dom/nfc/MozNDEFRecord.cpp +++ b/dom/nfc/MozNDEFRecord.cpp @@ -64,6 +64,42 @@ MozNDEFRecord::DropData() mozilla::DropJSObjects(this); } +/** + * Validate TNF. + * See section 3.3 THE NDEF Specification Test Requirements, + * NDEF specification 1.0 + */ +/* static */ +bool +MozNDEFRecord::ValidateTNF(const MozNDEFRecordOptions& aOptions, + ErrorResult& aRv) +{ + // * The TNF field MUST have a value between 0x00 and 0x06. + // * The TNF value MUST NOT be 0x07. + // These two requirements are already handled by WebIDL bindings. + + // If the TNF value is 0x00 (Empty), the TYPE, ID, and PAYLOAD fields MUST be + // omitted from the record. + if ((aOptions.mTnf == TNF::Empty) && + (aOptions.mType.WasPassed() || aOptions.mId.WasPassed() || + aOptions.mPayload.WasPassed())) { + NS_WARNING("tnf is empty but type/id/payload is not null."); + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return false; + } + + // If the TNF value is 0x05 (Unknown) or 0x06(Unchanged), the TYPE field MUST + // be omitted from the NDEF record. + if ((aOptions.mTnf == TNF::Unknown || aOptions.mTnf == TNF::Unchanged) && + aOptions.mType.WasPassed()) { + NS_WARNING("tnf is unknown/unchanged but type is not null."); + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return false; + } + + return true; +} + /* static */ already_AddRefed MozNDEFRecord::Constructor(const GlobalObject& aGlobal, @@ -76,6 +112,10 @@ MozNDEFRecord::Constructor(const GlobalObject& aGlobal, return nullptr; } + if (!ValidateTNF(aOptions, aRv)) { + return nullptr; + } + nsRefPtr ndefrecord = new MozNDEFRecord(aGlobal.Context(), win, aOptions); if (!ndefrecord) { diff --git a/dom/nfc/MozNDEFRecord.h b/dom/nfc/MozNDEFRecord.h index 8fc2cdb8c2f8..d5b04afa023c 100644 --- a/dom/nfc/MozNDEFRecord.h +++ b/dom/nfc/MozNDEFRecord.h @@ -89,6 +89,9 @@ private: void HoldData(); void DropData(); + static bool + ValidateTNF(const MozNDEFRecordOptions& aOptions, ErrorResult& aRv); + TNF mTnf; JS::Heap mType; JS::Heap mId; From 048eaf951c974be75771df9fb0aa32086db7fa5b Mon Sep 17 00:00:00 2001 From: Yoshi Huang Date: Fri, 29 Aug 2014 14:28:12 +0800 Subject: [PATCH 07/88] Bug 1007724 - Part 2: marionette tests. r=dimi From 84c328a6b61db42daa196f18e0f4113e1b74b2e8 Mon Sep 17 00:00:00 2001 --- dom/nfc/tests/marionette/test_ndef.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) --- dom/nfc/tests/marionette/test_ndef.js | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/dom/nfc/tests/marionette/test_ndef.js b/dom/nfc/tests/marionette/test_ndef.js index 1ca59ca637ea..e813cf896226 100644 --- a/dom/nfc/tests/marionette/test_ndef.js +++ b/dom/nfc/tests/marionette/test_ndef.js @@ -18,6 +18,34 @@ function testConstructNDEF() { ok(false, 'type, id or payload should be optional. error:' + e); } + try { + new MozNDEFRecord({type: new Uint8Array(1)}); + ok(false, "new MozNDEFRecord should fail, type should be null for empty tnf"); + } catch (e){ + ok(true); + } + + try { + new MozNDEFRecord({tnf: "unknown", type: new Uint8Array(1)}); + ok(false, "new MozNDEFRecord should fail, type should be null for unknown tnf"); + } catch (e){ + ok(true); + } + + try { + new MozNDEFRecord({tnf: "unchanged", type: new Uint8Array(1)}); + ok(false, "new MozNDEFRecord should fail, type should be null for unchanged tnf"); + } catch (e){ + ok(true); + } + + try { + new MozNDEFRecord({tnf: "illegal", type: new Uint8Array(1)}); + ok(false, "new MozNDEFRecord should fail, invalid tnf"); + } catch (e){ + ok(true); + } + runNextTest(); } From 2992c300195d410c239ed490126be2e34314f7e1 Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Wed, 10 Sep 2014 03:30:47 -0700 Subject: [PATCH 08/88] Bumping gaia.json for 2 gaia revision(s) a=gaia-bump ======== https://hg.mozilla.org/integration/gaia-central/rev/419b7ec98abf Author: Yoshi Huang Desc: Merge pull request #23396 from allstarschh/Bug1055560_enum_tnf Bug 1055560 - enum tnf in MozNDEFRecord. r=gweng ======== https://hg.mozilla.org/integration/gaia-central/rev/e7043b1e7707 Author: Yoshi Huang Desc: Bug 1055560 - enum TNF in MozNDEFRecord. --- b2g/config/gaia.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index 821159a475d7..f6b218a4b2b1 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -4,6 +4,6 @@ "remote": "", "branch": "" }, - "revision": "c7b07c667231e473fb6ea4ef0c4d239ce5c881d0", + "revision": "419b7ec98abf10d4850748fb31391b15a2b71605", "repo_path": "/integration/gaia-central" } From f7fd8bca4f70a23d28e0e9a3d3ce1245a7a71ea4 Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Wed, 10 Sep 2014 03:36:55 -0700 Subject: [PATCH 09/88] Bumping manifests a=b2g-bump --- b2g/config/dolphin/sources.xml | 2 +- b2g/config/emulator-ics/sources.xml | 2 +- b2g/config/emulator-jb/sources.xml | 2 +- b2g/config/emulator-kk/sources.xml | 2 +- b2g/config/emulator/sources.xml | 2 +- b2g/config/flame-kk/sources.xml | 2 +- b2g/config/flame/sources.xml | 2 +- b2g/config/hamachi/sources.xml | 2 +- b2g/config/helix/sources.xml | 2 +- b2g/config/nexus-4/sources.xml | 2 +- b2g/config/wasabi/sources.xml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/b2g/config/dolphin/sources.xml b/b2g/config/dolphin/sources.xml index f9e5a5f7844c..5d4dda07cf24 100644 --- a/b2g/config/dolphin/sources.xml +++ b/b2g/config/dolphin/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator-ics/sources.xml b/b2g/config/emulator-ics/sources.xml index 3000c9d4b769..430bd083962d 100644 --- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/emulator-jb/sources.xml b/b2g/config/emulator-jb/sources.xml index 4e1594fd8b3c..f2f76b1ab1cb 100644 --- a/b2g/config/emulator-jb/sources.xml +++ b/b2g/config/emulator-jb/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/emulator-kk/sources.xml b/b2g/config/emulator-kk/sources.xml index 98916c6a3b64..5ea029cfbe83 100644 --- a/b2g/config/emulator-kk/sources.xml +++ b/b2g/config/emulator-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator/sources.xml b/b2g/config/emulator/sources.xml index 3000c9d4b769..430bd083962d 100644 --- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/flame-kk/sources.xml b/b2g/config/flame-kk/sources.xml index 8254afccc7cc..ea2ca3f52839 100644 --- a/b2g/config/flame-kk/sources.xml +++ b/b2g/config/flame-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/flame/sources.xml b/b2g/config/flame/sources.xml index a13bbb511e87..265a6489759f 100644 --- a/b2g/config/flame/sources.xml +++ b/b2g/config/flame/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/hamachi/sources.xml b/b2g/config/hamachi/sources.xml index 890c2aaa7833..78746c4ada48 100644 --- a/b2g/config/hamachi/sources.xml +++ b/b2g/config/hamachi/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/helix/sources.xml b/b2g/config/helix/sources.xml index 0ab4f244ddb4..282afb63124e 100644 --- a/b2g/config/helix/sources.xml +++ b/b2g/config/helix/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/nexus-4/sources.xml b/b2g/config/nexus-4/sources.xml index bbac70a7a2ba..6ca9d6916163 100644 --- a/b2g/config/nexus-4/sources.xml +++ b/b2g/config/nexus-4/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/wasabi/sources.xml b/b2g/config/wasabi/sources.xml index b912630e2a81..6730c616ad30 100644 --- a/b2g/config/wasabi/sources.xml +++ b/b2g/config/wasabi/sources.xml @@ -17,7 +17,7 @@ - + From da2752cd4996c03a2927c61443379f4b60ece42e Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Wed, 10 Sep 2014 03:45:52 -0700 Subject: [PATCH 10/88] Bumping gaia.json for 2 gaia revision(s) a=gaia-bump MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ======== https://hg.mozilla.org/integration/gaia-central/rev/05232d016003 Author: Sergi Mansilla Desc: Merge pull request #23902 from ADLR-es/fix-bug-1061941 Bug 1061941 - [Contacts] Editing visible information (name) is not refle... ======== https://hg.mozilla.org/integration/gaia-central/rev/6c27cc807dce Author: Adrián de la Rosa Desc: Bug 1061941 - [Contacts] Editing visible information (name) is not reflected in 'Contacts' until page/app refreshes for newly created contacts --- b2g/config/gaia.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index f6b218a4b2b1..cffe0fbb755d 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -4,6 +4,6 @@ "remote": "", "branch": "" }, - "revision": "419b7ec98abf10d4850748fb31391b15a2b71605", + "revision": "05232d0160039bdaa04eb982ade9effdf52884ca", "repo_path": "/integration/gaia-central" } From e577c8e9956ad9629bb0df594ff4ea65289a3e1c Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Wed, 10 Sep 2014 03:52:03 -0700 Subject: [PATCH 11/88] Bumping manifests a=b2g-bump --- b2g/config/dolphin/sources.xml | 2 +- b2g/config/emulator-ics/sources.xml | 2 +- b2g/config/emulator-jb/sources.xml | 2 +- b2g/config/emulator-kk/sources.xml | 2 +- b2g/config/emulator/sources.xml | 2 +- b2g/config/flame-kk/sources.xml | 2 +- b2g/config/flame/sources.xml | 2 +- b2g/config/hamachi/sources.xml | 2 +- b2g/config/helix/sources.xml | 2 +- b2g/config/nexus-4/sources.xml | 2 +- b2g/config/wasabi/sources.xml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/b2g/config/dolphin/sources.xml b/b2g/config/dolphin/sources.xml index 5d4dda07cf24..d8f02b02de34 100644 --- a/b2g/config/dolphin/sources.xml +++ b/b2g/config/dolphin/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator-ics/sources.xml b/b2g/config/emulator-ics/sources.xml index 430bd083962d..da81a5e33589 100644 --- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/emulator-jb/sources.xml b/b2g/config/emulator-jb/sources.xml index f2f76b1ab1cb..01d62029e505 100644 --- a/b2g/config/emulator-jb/sources.xml +++ b/b2g/config/emulator-jb/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/emulator-kk/sources.xml b/b2g/config/emulator-kk/sources.xml index 5ea029cfbe83..d0001cf40e55 100644 --- a/b2g/config/emulator-kk/sources.xml +++ b/b2g/config/emulator-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator/sources.xml b/b2g/config/emulator/sources.xml index 430bd083962d..da81a5e33589 100644 --- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/flame-kk/sources.xml b/b2g/config/flame-kk/sources.xml index ea2ca3f52839..4d40771a112a 100644 --- a/b2g/config/flame-kk/sources.xml +++ b/b2g/config/flame-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/flame/sources.xml b/b2g/config/flame/sources.xml index 265a6489759f..4d20879f9aaa 100644 --- a/b2g/config/flame/sources.xml +++ b/b2g/config/flame/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/hamachi/sources.xml b/b2g/config/hamachi/sources.xml index 78746c4ada48..9149ec1aef03 100644 --- a/b2g/config/hamachi/sources.xml +++ b/b2g/config/hamachi/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/helix/sources.xml b/b2g/config/helix/sources.xml index 282afb63124e..ccd3d01dcc40 100644 --- a/b2g/config/helix/sources.xml +++ b/b2g/config/helix/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/nexus-4/sources.xml b/b2g/config/nexus-4/sources.xml index 6ca9d6916163..c78fbb461b5a 100644 --- a/b2g/config/nexus-4/sources.xml +++ b/b2g/config/nexus-4/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/wasabi/sources.xml b/b2g/config/wasabi/sources.xml index 6730c616ad30..77763da6e9fd 100644 --- a/b2g/config/wasabi/sources.xml +++ b/b2g/config/wasabi/sources.xml @@ -17,7 +17,7 @@ - + From 845b6b85ee5c3ee59352980031f09f86cdf0d305 Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Wed, 10 Sep 2014 05:05:44 -0700 Subject: [PATCH 12/88] Bumping gaia.json for 11 gaia revision(s) a=gaia-bump ======== https://hg.mozilla.org/integration/gaia-central/rev/d33202a7cab1 Author: Yura Zenevich Desc: Merge pull request #23348 from yzen/bug-1030346 Bug 1030346 - several improvements to Contacts accessibility. ======== https://hg.mozilla.org/integration/gaia-central/rev/98518e5c9bcb Author: Yura Zenevich Desc: Bug 1030346 - updated labels, aria labels and their aria-visibility. ======== https://hg.mozilla.org/integration/gaia-central/rev/285f580df5b9 Author: Yura Zenevich Desc: Bug 1030346 - fixed checkbox roles, state and aria markup. ======== https://hg.mozilla.org/integration/gaia-central/rev/6248f183504f Author: Yura Zenevich Desc: Bug 1030346 - fixed listbox dropdown roles and aria markup. ======== https://hg.mozilla.org/integration/gaia-central/rev/66b3c2078a8a Author: Yura Zenevich Desc: Bug 1030346 - fixed list, listbox and false list roles. ======== https://hg.mozilla.org/integration/gaia-central/rev/aaac8facb6fd Author: Yura Zenevich Desc: Bug 1030346 - make element order in markup consistent with the displayed order. ======== https://hg.mozilla.org/integration/gaia-central/rev/8de8dd55abd7 Author: Yura Zenevich Desc: Bug 1030346 - removed redundant role="menuitem" from header buttons. Button is more consistent with the rest of the UI when in screen reader mode. ======== https://hg.mozilla.org/integration/gaia-central/rev/8fe73d338957 Author: Jan Jongboom Desc: Merge pull request #23595 from comoyo/bug1048738 Bug 1048738 - Change default snooze time to 10 minutes. r=mcav ======== https://hg.mozilla.org/integration/gaia-central/rev/969614014118 Author: Jan Jongboom Desc: Bug 1048738 - Change default snooze time to 10 minutes ======== https://hg.mozilla.org/integration/gaia-central/rev/f1eca18b68fb Author: Aleh Zasypkin Desc: Merge pull request #22897 from azasypkin/bug-1053952-batch-deletion Bug 1053952 - [Messages][Refactoring] Delete all messages at once when deleting threads. r=schung ======== https://hg.mozilla.org/integration/gaia-central/rev/b15d23f5381c Author: Aleh Zasypkin Desc: Bug 1053952 - [Messages][Refactoring] Delete all messages at once when deleting threads. r=schung --- b2g/config/gaia.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index cffe0fbb755d..4d0b3ef7628f 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -4,6 +4,6 @@ "remote": "", "branch": "" }, - "revision": "05232d0160039bdaa04eb982ade9effdf52884ca", + "revision": "d33202a7cab1ee8a7a1f70839ae9c4b37efcd91c", "repo_path": "/integration/gaia-central" } From 9b08eb7cf47ec5b87465f0379e64b77aaef6c62f Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Wed, 10 Sep 2014 05:11:54 -0700 Subject: [PATCH 13/88] Bumping manifests a=b2g-bump --- b2g/config/dolphin/sources.xml | 2 +- b2g/config/emulator-ics/sources.xml | 2 +- b2g/config/emulator-jb/sources.xml | 2 +- b2g/config/emulator-kk/sources.xml | 2 +- b2g/config/emulator/sources.xml | 2 +- b2g/config/flame-kk/sources.xml | 2 +- b2g/config/flame/sources.xml | 2 +- b2g/config/hamachi/sources.xml | 2 +- b2g/config/helix/sources.xml | 2 +- b2g/config/nexus-4/sources.xml | 2 +- b2g/config/wasabi/sources.xml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/b2g/config/dolphin/sources.xml b/b2g/config/dolphin/sources.xml index d8f02b02de34..c3bc8fd42c65 100644 --- a/b2g/config/dolphin/sources.xml +++ b/b2g/config/dolphin/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator-ics/sources.xml b/b2g/config/emulator-ics/sources.xml index da81a5e33589..fb97e2697f80 100644 --- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/emulator-jb/sources.xml b/b2g/config/emulator-jb/sources.xml index 01d62029e505..b12b0b3505d4 100644 --- a/b2g/config/emulator-jb/sources.xml +++ b/b2g/config/emulator-jb/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/emulator-kk/sources.xml b/b2g/config/emulator-kk/sources.xml index d0001cf40e55..20929e302e35 100644 --- a/b2g/config/emulator-kk/sources.xml +++ b/b2g/config/emulator-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator/sources.xml b/b2g/config/emulator/sources.xml index da81a5e33589..fb97e2697f80 100644 --- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/flame-kk/sources.xml b/b2g/config/flame-kk/sources.xml index 4d40771a112a..c1fcd5e06054 100644 --- a/b2g/config/flame-kk/sources.xml +++ b/b2g/config/flame-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/flame/sources.xml b/b2g/config/flame/sources.xml index 4d20879f9aaa..0fc941158e61 100644 --- a/b2g/config/flame/sources.xml +++ b/b2g/config/flame/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/hamachi/sources.xml b/b2g/config/hamachi/sources.xml index 9149ec1aef03..d7ce5ad50b48 100644 --- a/b2g/config/hamachi/sources.xml +++ b/b2g/config/hamachi/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/helix/sources.xml b/b2g/config/helix/sources.xml index ccd3d01dcc40..bf38ce683e2f 100644 --- a/b2g/config/helix/sources.xml +++ b/b2g/config/helix/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/nexus-4/sources.xml b/b2g/config/nexus-4/sources.xml index c78fbb461b5a..78265b305e95 100644 --- a/b2g/config/nexus-4/sources.xml +++ b/b2g/config/nexus-4/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/wasabi/sources.xml b/b2g/config/wasabi/sources.xml index 77763da6e9fd..bb40cd72c008 100644 --- a/b2g/config/wasabi/sources.xml +++ b/b2g/config/wasabi/sources.xml @@ -17,7 +17,7 @@ - + From 54984255c6798393dfc4601114fd879e3e597add Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Wed, 10 Sep 2014 05:25:37 -0700 Subject: [PATCH 14/88] Bumping gaia.json for 2 gaia revision(s) a=gaia-bump ======== https://hg.mozilla.org/integration/gaia-central/rev/51177cf072f9 Author: Francisco Jordano Desc: Merge pull request #23701 from jpruden92/bug1058312-contacts-v2 Bug 1058312 - [Contacts] Individual Contact Field 'Company' cannot be removed after being updated. ======== https://hg.mozilla.org/integration/gaia-central/rev/e24aa7d99680 Author: jprudencio Desc: Bug 1058312 - [Contacts] Individual Contact Field 'Company' cannot be removed after being updated. --- b2g/config/gaia.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index 4d0b3ef7628f..d9ffec21709e 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -4,6 +4,6 @@ "remote": "", "branch": "" }, - "revision": "d33202a7cab1ee8a7a1f70839ae9c4b37efcd91c", + "revision": "51177cf072f9766b883c9adb5c2614a5f43d14f7", "repo_path": "/integration/gaia-central" } From 61a9303b34c98a6a3aeb001de708260739890fb0 Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Wed, 10 Sep 2014 05:31:48 -0700 Subject: [PATCH 15/88] Bumping manifests a=b2g-bump --- b2g/config/dolphin/sources.xml | 2 +- b2g/config/emulator-ics/sources.xml | 2 +- b2g/config/emulator-jb/sources.xml | 2 +- b2g/config/emulator-kk/sources.xml | 2 +- b2g/config/emulator/sources.xml | 2 +- b2g/config/flame-kk/sources.xml | 2 +- b2g/config/flame/sources.xml | 2 +- b2g/config/hamachi/sources.xml | 2 +- b2g/config/helix/sources.xml | 2 +- b2g/config/nexus-4/sources.xml | 2 +- b2g/config/wasabi/sources.xml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/b2g/config/dolphin/sources.xml b/b2g/config/dolphin/sources.xml index c3bc8fd42c65..2f6d53d78fdd 100644 --- a/b2g/config/dolphin/sources.xml +++ b/b2g/config/dolphin/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator-ics/sources.xml b/b2g/config/emulator-ics/sources.xml index fb97e2697f80..281a7c95e39e 100644 --- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/emulator-jb/sources.xml b/b2g/config/emulator-jb/sources.xml index b12b0b3505d4..675bb1bf1d6d 100644 --- a/b2g/config/emulator-jb/sources.xml +++ b/b2g/config/emulator-jb/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/emulator-kk/sources.xml b/b2g/config/emulator-kk/sources.xml index 20929e302e35..d1c9fb2868d6 100644 --- a/b2g/config/emulator-kk/sources.xml +++ b/b2g/config/emulator-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator/sources.xml b/b2g/config/emulator/sources.xml index fb97e2697f80..281a7c95e39e 100644 --- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/flame-kk/sources.xml b/b2g/config/flame-kk/sources.xml index c1fcd5e06054..775e5959d960 100644 --- a/b2g/config/flame-kk/sources.xml +++ b/b2g/config/flame-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/flame/sources.xml b/b2g/config/flame/sources.xml index 0fc941158e61..09df27f53852 100644 --- a/b2g/config/flame/sources.xml +++ b/b2g/config/flame/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/hamachi/sources.xml b/b2g/config/hamachi/sources.xml index d7ce5ad50b48..1afcc81e0b5d 100644 --- a/b2g/config/hamachi/sources.xml +++ b/b2g/config/hamachi/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/helix/sources.xml b/b2g/config/helix/sources.xml index bf38ce683e2f..f8da7ce5e3e7 100644 --- a/b2g/config/helix/sources.xml +++ b/b2g/config/helix/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/nexus-4/sources.xml b/b2g/config/nexus-4/sources.xml index 78265b305e95..8812d3a1c1be 100644 --- a/b2g/config/nexus-4/sources.xml +++ b/b2g/config/nexus-4/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/wasabi/sources.xml b/b2g/config/wasabi/sources.xml index bb40cd72c008..475d38636780 100644 --- a/b2g/config/wasabi/sources.xml +++ b/b2g/config/wasabi/sources.xml @@ -17,7 +17,7 @@ - + From 7abbfac9727d7f3e6bff933a674b1d2b3e6f9cf0 Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Wed, 10 Sep 2014 05:40:45 -0700 Subject: [PATCH 16/88] Bumping gaia.json for 2 gaia revision(s) a=gaia-bump ======== https://hg.mozilla.org/integration/gaia-central/rev/f7dd70f6731a Author: Ben Francis Desc: Merge pull request #23724 from benfrancis/1062329 Bug 1062329 - Active state for Rocketbar windows button r=vingtetun ======== https://hg.mozilla.org/integration/gaia-central/rev/928bfc3077b7 Author: Ben Francis Desc: Bug 1062329 - Active state for Rocketbar windows button --- b2g/config/gaia.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index d9ffec21709e..020ccc7f68b8 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -4,6 +4,6 @@ "remote": "", "branch": "" }, - "revision": "51177cf072f9766b883c9adb5c2614a5f43d14f7", + "revision": "f7dd70f6731aeae2ca2b6a9d97768611fb0fc596", "repo_path": "/integration/gaia-central" } From 4720fd29ae15a93928d6db6237a3c296b35385a7 Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Wed, 10 Sep 2014 05:51:56 -0700 Subject: [PATCH 17/88] Bumping manifests a=b2g-bump --- b2g/config/dolphin/sources.xml | 2 +- b2g/config/emulator-ics/sources.xml | 2 +- b2g/config/emulator-jb/sources.xml | 2 +- b2g/config/emulator-kk/sources.xml | 2 +- b2g/config/emulator/sources.xml | 2 +- b2g/config/flame-kk/sources.xml | 2 +- b2g/config/flame/sources.xml | 2 +- b2g/config/hamachi/sources.xml | 2 +- b2g/config/helix/sources.xml | 2 +- b2g/config/nexus-4/sources.xml | 2 +- b2g/config/wasabi/sources.xml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/b2g/config/dolphin/sources.xml b/b2g/config/dolphin/sources.xml index 2f6d53d78fdd..2d1163b33782 100644 --- a/b2g/config/dolphin/sources.xml +++ b/b2g/config/dolphin/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator-ics/sources.xml b/b2g/config/emulator-ics/sources.xml index 281a7c95e39e..3c1022a6025c 100644 --- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/emulator-jb/sources.xml b/b2g/config/emulator-jb/sources.xml index 675bb1bf1d6d..48b2961943b8 100644 --- a/b2g/config/emulator-jb/sources.xml +++ b/b2g/config/emulator-jb/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/emulator-kk/sources.xml b/b2g/config/emulator-kk/sources.xml index d1c9fb2868d6..10094826eabc 100644 --- a/b2g/config/emulator-kk/sources.xml +++ b/b2g/config/emulator-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator/sources.xml b/b2g/config/emulator/sources.xml index 281a7c95e39e..3c1022a6025c 100644 --- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/flame-kk/sources.xml b/b2g/config/flame-kk/sources.xml index 775e5959d960..abab18ec3ae4 100644 --- a/b2g/config/flame-kk/sources.xml +++ b/b2g/config/flame-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/flame/sources.xml b/b2g/config/flame/sources.xml index 09df27f53852..45b8397d18b7 100644 --- a/b2g/config/flame/sources.xml +++ b/b2g/config/flame/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/hamachi/sources.xml b/b2g/config/hamachi/sources.xml index 1afcc81e0b5d..dcedecd9e809 100644 --- a/b2g/config/hamachi/sources.xml +++ b/b2g/config/hamachi/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/helix/sources.xml b/b2g/config/helix/sources.xml index f8da7ce5e3e7..8d130a276948 100644 --- a/b2g/config/helix/sources.xml +++ b/b2g/config/helix/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/nexus-4/sources.xml b/b2g/config/nexus-4/sources.xml index 8812d3a1c1be..9a89fa8576f6 100644 --- a/b2g/config/nexus-4/sources.xml +++ b/b2g/config/nexus-4/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/wasabi/sources.xml b/b2g/config/wasabi/sources.xml index 475d38636780..cc5453dd6ad9 100644 --- a/b2g/config/wasabi/sources.xml +++ b/b2g/config/wasabi/sources.xml @@ -17,7 +17,7 @@ - + From 8bf31e2480a20f1c336ccf3627715579201034db Mon Sep 17 00:00:00 2001 From: Mike Habicher Date: Mon, 25 Aug 2014 18:06:31 -0400 Subject: [PATCH 18/88] Bug 1053966 - pass settings change notices to Observers as aSubject instead of aData, r=bz,qdot --- b2g/chrome/content/payment.js | 9 +- dom/audiochannel/AudioChannelService.cpp | 44 ++++------ dom/bindings/BindingUtils.h | 14 ++- dom/bluetooth/BluetoothService.cpp | 88 +++++-------------- dom/bluetooth/BluetoothService.h | 2 +- .../bluedroid/hfp/BluetoothHfpManager.cpp | 41 ++++----- .../bluedroid/hfp/BluetoothHfpManager.h | 2 +- dom/bluetooth/bluez/BluetoothHfpManager.cpp | 41 ++++----- dom/bluetooth/bluez/BluetoothHfpManager.h | 2 +- dom/bluetooth2/BluetoothService.cpp | 56 +++--------- dom/bluetooth2/BluetoothService.h | 2 +- .../bluedroid/hfp/BluetoothHfpManager.cpp | 39 ++++---- .../bluedroid/hfp/BluetoothHfpManager.h | 2 +- dom/bluetooth2/bluez/BluetoothHfpManager.cpp | 40 ++++----- dom/bluetooth2/bluez/BluetoothHfpManager.h | 2 +- dom/fmradio/FMRadioService.cpp | 61 +++++-------- dom/geolocation/nsGeolocation.cpp | 39 ++++---- dom/geolocation/nsGeolocation.h | 2 +- dom/settings/SettingsRequestManager.jsm | 12 +-- dom/system/NetworkGeolocationProvider.js | 9 +- dom/system/gonk/AudioManager.cpp | 41 +++------ dom/system/gonk/AutoMounterSetting.cpp | 47 ++++------ dom/system/gonk/NetworkManager.js | 3 +- dom/system/gonk/RadioInterfaceLayer.js | 6 +- dom/system/gonk/TimeZoneSettingObserver.cpp | 35 +++----- dom/webidl/SettingChangeNotification.webidl | 12 +++ dom/webidl/moz.build | 1 + dom/wifi/WifiWorker.js | 8 +- toolkit/devtools/discovery/discovery.js | 5 +- 29 files changed, 234 insertions(+), 431 deletions(-) create mode 100644 dom/webidl/SettingChangeNotification.webidl diff --git a/b2g/chrome/content/payment.js b/b2g/chrome/content/payment.js index c9d46870194b..cc529f92b3f1 100644 --- a/b2g/chrome/content/payment.js +++ b/b2g/chrome/content/payment.js @@ -178,13 +178,12 @@ PaymentSettings.prototype = { } try { - let setting = JSON.parse(aData); - if (!setting.key || - (setting.key !== kRilDefaultDataServiceId && - setting.key !== kRilDefaultPaymentServiceId)) { + if (!aSubject.key || + (aSubject.key !== kRilDefaultDataServiceId && + aSubject.key !== kRilDefaultPaymentServiceId)) { return; } - this.setServiceId(setting.key, setting.value); + this.setServiceId(aSubject.key, aSubject.value); } catch (e) { LOGE(e); } diff --git a/dom/audiochannel/AudioChannelService.cpp b/dom/audiochannel/AudioChannelService.cpp index 44cb6f5e6631..5e79a9a0bca4 100644 --- a/dom/audiochannel/AudioChannelService.cpp +++ b/dom/audiochannel/AudioChannelService.cpp @@ -20,6 +20,7 @@ #include "nsComponentManagerUtils.h" #include "nsPIDOMWindow.h" #include "nsServiceManagerUtils.h" +#include "mozilla/dom/SettingChangeNotificationBinding.h" #ifdef MOZ_WIDGET_GONK #include "nsJSUtils.h" @@ -801,48 +802,33 @@ AudioChannelService::Observe(nsISupports* aSubject, const char* aTopic, const ch // To process the volume control on each audio channel according to // change of settings else if (!strcmp(aTopic, "mozsettings-changed")) { - AutoSafeJSContext cx; - nsDependentString dataStr(aData); - JS::Rooted val(cx); - if (!JS_ParseJSON(cx, dataStr.get(), dataStr.Length(), &val) || - !val.isObject()) { + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + RootedDictionary setting(cx); + if (!WrappedJSToDictionary(cx, aSubject, setting)) { return NS_OK; } - - JS::Rooted obj(cx, &val.toObject()); - JS::Rooted key(cx); - if (!JS_GetProperty(cx, obj, "key", &key) || - !key.isString()) { + if (!StringBeginsWith(setting.mKey, NS_LITERAL_STRING("audio.volume."))) { return NS_OK; } - - JS::Rooted jsKey(cx, JS::ToString(cx, key)); - if (!jsKey) { + if (!setting.mValue.isNumber()) { return NS_OK; } - nsAutoJSString keyStr; - if (!keyStr.init(cx, jsKey) || keyStr.Find("audio.volume.", 0, false)) { - return NS_OK; - } - - JS::Rooted value(cx); - if (!JS_GetProperty(cx, obj, "value", &value) || !value.isInt32()) { - return NS_OK; - } - + nsCOMPtr audioManager = do_GetService(NS_AUDIOMANAGER_CONTRACTID); NS_ENSURE_TRUE(audioManager, NS_OK); - int32_t index = value.toInt32(); - if (keyStr.EqualsLiteral("audio.volume.content")) { + int32_t index = setting.mValue.toNumber(); + if (setting.mKey.EqualsLiteral("audio.volume.content")) { audioManager->SetAudioChannelVolume((int32_t)AudioChannel::Content, index); - } else if (keyStr.EqualsLiteral("audio.volume.notification")) { + } else if (setting.mKey.EqualsLiteral("audio.volume.notification")) { audioManager->SetAudioChannelVolume((int32_t)AudioChannel::Notification, index); - } else if (keyStr.EqualsLiteral("audio.volume.alarm")) { + } else if (setting.mKey.EqualsLiteral("audio.volume.alarm")) { audioManager->SetAudioChannelVolume((int32_t)AudioChannel::Alarm, index); - } else if (keyStr.EqualsLiteral("audio.volume.telephony")) { + } else if (setting.mKey.EqualsLiteral("audio.volume.telephony")) { audioManager->SetAudioChannelVolume((int32_t)AudioChannel::Telephony, index); - } else if (!keyStr.EqualsLiteral("audio.volume.bt_sco")) { + } else if (!setting.mKey.EqualsLiteral("audio.volume.bt_sco")) { // bt_sco is not a valid audio channel so we manipulate it in // AudioManager.cpp. And the others should not be used. // We didn't use MOZ_CRASH or MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE here diff --git a/dom/bindings/BindingUtils.h b/dom/bindings/BindingUtils.h index cf25c0917449..1a186f1200ef 100644 --- a/dom/bindings/BindingUtils.h +++ b/dom/bindings/BindingUtils.h @@ -2957,25 +2957,21 @@ CallerSubsumes(JS::Handle aValue) template inline bool -WrappedJSToDictionary(nsISupports* aObject, T& aDictionary) +WrappedJSToDictionary(JSContext* aCx, nsISupports* aObject, T& aDictionary) { nsCOMPtr wrappedObj = do_QueryInterface(aObject); if (!wrappedObj) { return false; } - AutoJSAPI jsapi; - jsapi.Init(); - - JSContext* cx = jsapi.cx(); - JS::Rooted obj(cx, wrappedObj->GetJSObject()); + JS::Rooted obj(aCx, wrappedObj->GetJSObject()); if (!obj) { return false; } - JSAutoCompartment ac(cx, obj); - JS::Rooted v(cx, OBJECT_TO_JSVAL(obj)); - return aDictionary.Init(cx, v); + JSAutoCompartment ac(aCx, obj); + JS::Rooted v(aCx, JS::ObjectValue(*obj)); + return aDictionary.Init(aCx, v); } } // namespace dom diff --git a/dom/bluetooth/BluetoothService.cpp b/dom/bluetooth/BluetoothService.cpp index 4784ae3608b1..57368d6a8314 100644 --- a/dom/bluetooth/BluetoothService.cpp +++ b/dom/bluetooth/BluetoothService.cpp @@ -34,6 +34,7 @@ #include "nsITimer.h" #include "nsServiceManagerUtils.h" #include "nsXPCOM.h" +#include "mozilla/dom/SettingChangeNotificationBinding.h" #if defined(MOZ_WIDGET_GONK) #include "cutils/properties.h" @@ -560,91 +561,44 @@ BluetoothService::HandleStartupSettingsCheck(bool aEnable) } nsresult -BluetoothService::HandleSettingsChanged(const nsAString& aData) +BluetoothService::HandleSettingsChanged(nsISupports* aSubject) { MOZ_ASSERT(NS_IsMainThread()); // The string that we're interested in will be a JSON string that looks like: // {"key":"bluetooth.enabled","value":true} - AutoSafeJSContext cx; - if (!cx) { + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + RootedDictionary setting(cx); + if (!WrappedJSToDictionary(cx, aSubject, setting)) { return NS_OK; } - - JS::Rooted val(cx); - if (!JS_ParseJSON(cx, aData.BeginReading(), aData.Length(), &val)) { - return JS_ReportPendingException(cx) ? NS_OK : NS_ERROR_OUT_OF_MEMORY; - } - - if (!val.isObject()) { - return NS_OK; - } - - JS::Rooted obj(cx, &val.toObject()); - - JS::Rooted key(cx); - if (!JS_GetProperty(cx, obj, "key", &key)) { - MOZ_ASSERT(!JS_IsExceptionPending(cx)); - return NS_ERROR_OUT_OF_MEMORY; - } - - if (!key.isString()) { - return NS_OK; - } - - // First, check if the string equals to BLUETOOTH_DEBUGGING_SETTING - bool match; - if (!JS_StringEqualsAscii(cx, key.toString(), BLUETOOTH_DEBUGGING_SETTING, &match)) { - MOZ_ASSERT(!JS_IsExceptionPending(cx)); - return NS_ERROR_OUT_OF_MEMORY; - } - - if (match) { - JS::Rooted value(cx); - if (!JS_GetProperty(cx, obj, "value", &value)) { - MOZ_ASSERT(!JS_IsExceptionPending(cx)); - return NS_ERROR_OUT_OF_MEMORY; - } - - if (!value.isBoolean()) { + if (setting.mKey.EqualsASCII(BLUETOOTH_DEBUGGING_SETTING)) { + if (!setting.mValue.isBoolean()) { MOZ_ASSERT(false, "Expecting a boolean for 'bluetooth.debugging.enabled'!"); return NS_ERROR_UNEXPECTED; } - - SWITCH_BT_DEBUG(value.toBoolean()); + + SWITCH_BT_DEBUG(setting.mValue.toBoolean()); return NS_OK; } // Second, check if the string is BLUETOOTH_ENABLED_SETTING - if (!JS_StringEqualsAscii(cx, key.toString(), BLUETOOTH_ENABLED_SETTING, &match)) { - MOZ_ASSERT(!JS_IsExceptionPending(cx)); - return NS_ERROR_OUT_OF_MEMORY; + if (!setting.mKey.EqualsASCII(BLUETOOTH_ENABLED_SETTING)) { + return NS_OK; + } + if (!setting.mValue.isBoolean()) { + MOZ_ASSERT(false, "Expecting a boolean for 'bluetooth.enabled'!"); + return NS_ERROR_UNEXPECTED; } - if (match) { - JS::Rooted value(cx); - if (!JS_GetProperty(cx, obj, "value", &value)) { - MOZ_ASSERT(!JS_IsExceptionPending(cx)); - return NS_ERROR_OUT_OF_MEMORY; - } + sToggleInProgress = true; - if (!value.isBoolean()) { - MOZ_ASSERT(false, "Expecting a boolean for 'bluetooth.enabled'!"); - return NS_ERROR_UNEXPECTED; - } - - if (sToggleInProgress || value.toBoolean() == IsEnabled()) { - // Nothing to do here. - return NS_OK; - } - - sToggleInProgress = true; - - nsresult rv = StartStopBluetooth(value.toBoolean(), false); - NS_ENSURE_SUCCESS(rv, rv); - } + nsresult rv = StartStopBluetooth(setting.mValue.toBoolean(), false); + NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } @@ -755,7 +709,7 @@ BluetoothService::Observe(nsISupports* aSubject, const char* aTopic, } if (!strcmp(aTopic, MOZSETTINGS_CHANGED_ID)) { - return HandleSettingsChanged(nsDependentString(aData)); + return HandleSettingsChanged(aSubject); } if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { diff --git a/dom/bluetooth/BluetoothService.h b/dom/bluetooth/BluetoothService.h index 8a3f006feb9b..c8b7884d5d0c 100644 --- a/dom/bluetooth/BluetoothService.h +++ b/dom/bluetooth/BluetoothService.h @@ -372,7 +372,7 @@ protected: * Called when "mozsettings-changed" observer topic fires. */ nsresult - HandleSettingsChanged(const nsAString& aData); + HandleSettingsChanged(nsISupports* aSubject); /** * Called when XPCOM is shutting down. diff --git a/dom/bluetooth/bluedroid/hfp/BluetoothHfpManager.cpp b/dom/bluetooth/bluedroid/hfp/BluetoothHfpManager.cpp index 1dcfe7783f96..10d7853c3aec 100644 --- a/dom/bluetooth/bluedroid/hfp/BluetoothHfpManager.cpp +++ b/dom/bluetooth/bluedroid/hfp/BluetoothHfpManager.cpp @@ -27,6 +27,8 @@ #include "nsRadioInterfaceLayer.h" #include "nsServiceManagerUtils.h" #include "nsThreadUtils.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/SettingChangeNotificationBinding.h" #define MOZSETTINGS_CHANGED_ID "mozsettings-changed" #define AUDIO_VOLUME_BT_SCO_ID "audio.volume.bt_sco" @@ -462,7 +464,7 @@ BluetoothHfpManager::Observe(nsISupports* aSubject, const char16_t* aData) { if (!strcmp(aTopic, MOZSETTINGS_CHANGED_ID)) { - HandleVolumeChanged(nsDependentString(aData)); + HandleVolumeChanged(aSubject); } else if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { HandleShutdown(); } else { @@ -561,39 +563,28 @@ public: }; void -BluetoothHfpManager::HandleVolumeChanged(const nsAString& aData) +BluetoothHfpManager::HandleVolumeChanged(nsISupports* aSubject) { MOZ_ASSERT(NS_IsMainThread()); // The string that we're interested in will be a JSON string that looks like: // {"key":"volumeup", "value":10} // {"key":"volumedown", "value":2} - JSContext* cx = nsContentUtils::GetSafeJSContext(); - NS_ENSURE_TRUE_VOID(cx); - - JS::Rooted val(cx); - NS_ENSURE_TRUE_VOID(JS_ParseJSON(cx, aData.BeginReading(), aData.Length(), &val)); - NS_ENSURE_TRUE_VOID(val.isObject()); - - JS::Rooted obj(cx, &val.toObject()); - JS::Rooted key(cx); - if (!JS_GetProperty(cx, obj, "key", &key) || !key.isString()) { + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + RootedDictionary setting(cx); + if (!WrappedJSToDictionary(cx, aSubject, setting)) { + return; + } + if (!setting.mKey.EqualsASCII(AUDIO_VOLUME_BT_SCO_ID)) { + return; + } + if (!setting.mValue.isNumber()) { return; } - bool match; - if (!JS_StringEqualsAscii(cx, key.toString(), AUDIO_VOLUME_BT_SCO_ID, &match) || - !match) { - return; - } - - JS::Rooted value(cx); - if (!JS_GetProperty(cx, obj, "value", &value) || - !value.isNumber()) { - return; - } - - mCurrentVgs = value.toNumber(); + mCurrentVgs = setting.mValue.toNumber(); // Adjust volume by headset and we don't have to send volume back to headset if (mReceiveVgsFlag) { diff --git a/dom/bluetooth/bluedroid/hfp/BluetoothHfpManager.h b/dom/bluetooth/bluedroid/hfp/BluetoothHfpManager.h index f69d126a41be..4cafcfe52504 100644 --- a/dom/bluetooth/bluedroid/hfp/BluetoothHfpManager.h +++ b/dom/bluetooth/bluedroid/hfp/BluetoothHfpManager.h @@ -149,7 +149,7 @@ private: void Cleanup(); void HandleShutdown(); - void HandleVolumeChanged(const nsAString& aData); + void HandleVolumeChanged(nsISupports* aSubject); void Notify(const hal::BatteryInformation& aBatteryInfo); void NotifyConnectionStateChanged(const nsAString& aType); diff --git a/dom/bluetooth/bluez/BluetoothHfpManager.cpp b/dom/bluetooth/bluez/BluetoothHfpManager.cpp index 0eebcebcbd98..bdf7ae496ceb 100644 --- a/dom/bluetooth/bluez/BluetoothHfpManager.cpp +++ b/dom/bluetooth/bluez/BluetoothHfpManager.cpp @@ -23,6 +23,8 @@ #include "nsIObserverService.h" #include "nsISettingsService.h" #include "nsServiceManagerUtils.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/SettingChangeNotificationBinding.h" #ifdef MOZ_B2G_RIL #include "nsIDOMIccInfo.h" @@ -205,7 +207,7 @@ BluetoothHfpManager::Observe(nsISupports* aSubject, const char16_t* aData) { if (!strcmp(aTopic, MOZSETTINGS_CHANGED_ID)) { - HandleVolumeChanged(nsDependentString(aData)); + HandleVolumeChanged(aSubject); } else if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { HandleShutdown(); } else { @@ -555,7 +557,7 @@ BluetoothHfpManager::NotifyDialer(const nsAString& aCommand) #endif // MOZ_B2G_RIL void -BluetoothHfpManager::HandleVolumeChanged(const nsAString& aData) +BluetoothHfpManager::HandleVolumeChanged(nsISupports* aSubject) { MOZ_ASSERT(NS_IsMainThread()); @@ -563,32 +565,21 @@ BluetoothHfpManager::HandleVolumeChanged(const nsAString& aData) // {"key":"volumeup", "value":10} // {"key":"volumedown", "value":2} - JSContext* cx = nsContentUtils::GetSafeJSContext(); - NS_ENSURE_TRUE_VOID(cx); - - JS::Rooted val(cx); - NS_ENSURE_TRUE_VOID(JS_ParseJSON(cx, aData.BeginReading(), aData.Length(), &val)); - NS_ENSURE_TRUE_VOID(val.isObject()); - - JS::Rooted obj(cx, &val.toObject()); - JS::Rooted key(cx); - if (!JS_GetProperty(cx, obj, "key", &key) || !key.isString()) { + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + RootedDictionary setting(cx); + if (!WrappedJSToDictionary(cx, aSubject, setting)) { + return; + } + if (!setting.mKey.EqualsASCII(AUDIO_VOLUME_BT_SCO_ID)) { + return; + } + if (!setting.mValue.isNumber()) { return; } - bool match; - if (!JS_StringEqualsAscii(cx, key.toString(), AUDIO_VOLUME_BT_SCO_ID, &match) || - !match) { - return; - } - - JS::Rooted value(cx); - if (!JS_GetProperty(cx, obj, "value", &value)|| - !value.isNumber()) { - return; - } - - mCurrentVgs = value.toNumber(); + mCurrentVgs = setting.mValue.toNumber(); // Adjust volume by headset and we don't have to send volume back to headset if (mReceiveVgsFlag) { diff --git a/dom/bluetooth/bluez/BluetoothHfpManager.h b/dom/bluetooth/bluez/BluetoothHfpManager.h index 5b09ba2e125c..2de35b57a22f 100644 --- a/dom/bluetooth/bluez/BluetoothHfpManager.h +++ b/dom/bluetooth/bluez/BluetoothHfpManager.h @@ -149,7 +149,7 @@ private: BluetoothHfpManager(); void HandleShutdown(); - void HandleVolumeChanged(const nsAString& aData); + void HandleVolumeChanged(nsISupports* aSubject); bool Init(); void Notify(const hal::BatteryInformation& aBatteryInfo); diff --git a/dom/bluetooth2/BluetoothService.cpp b/dom/bluetooth2/BluetoothService.cpp index 428c7c1f25c2..9482b9ca8ed6 100644 --- a/dom/bluetooth2/BluetoothService.cpp +++ b/dom/bluetooth2/BluetoothService.cpp @@ -33,6 +33,7 @@ #include "nsITimer.h" #include "nsServiceManagerUtils.h" #include "nsXPCOM.h" +#include "mozilla/dom/SettingChangeNotificationBinding.h" #if defined(MOZ_WIDGET_GONK) #include "cutils/properties.h" @@ -522,60 +523,29 @@ BluetoothService::HandleStartupSettingsCheck(bool aEnable) } nsresult -BluetoothService::HandleSettingsChanged(const nsAString& aData) +BluetoothService::HandleSettingsChanged(nsISupports* aSubject) { MOZ_ASSERT(NS_IsMainThread()); // The string that we're interested in will be a JSON string that looks like: // {"key":"bluetooth.enabled","value":true} - AutoSafeJSContext cx; - if (!cx) { + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + RootedDictionary setting(cx); + if (!WrappedJSToDictionary(cx, aSubject, setting)) { return NS_OK; } - - JS::Rooted val(cx); - if (!JS_ParseJSON(cx, aData.BeginReading(), aData.Length(), &val)) { - return JS_ReportPendingException(cx) ? NS_OK : NS_ERROR_OUT_OF_MEMORY; - } - - if (!val.isObject()) { + if (!setting.mKey.EqualsASCII(BLUETOOTH_DEBUGGING_SETTING)) { return NS_OK; } - - JS::Rooted obj(cx, &val.toObject()); - - JS::Rooted key(cx); - if (!JS_GetProperty(cx, obj, "key", &key)) { - MOZ_ASSERT(!JS_IsExceptionPending(cx)); - return NS_ERROR_OUT_OF_MEMORY; + if (!setting.mValue.isBoolean()) { + MOZ_ASSERT(false, "Expecting a boolean for 'bluetooth.debugging.enabled'!"); + return NS_ERROR_UNEXPECTED; } - if (!key.isString()) { - return NS_OK; - } - - // Check whether the string is BLUETOOTH_DEBUGGING_SETTING - bool match; - if (!JS_StringEqualsAscii(cx, key.toString(), BLUETOOTH_DEBUGGING_SETTING, &match)) { - MOZ_ASSERT(!JS_IsExceptionPending(cx)); - return NS_ERROR_OUT_OF_MEMORY; - } - - if (match) { - JS::Rooted value(cx); - if (!JS_GetProperty(cx, obj, "value", &value)) { - MOZ_ASSERT(!JS_IsExceptionPending(cx)); - return NS_ERROR_OUT_OF_MEMORY; - } - - if (!value.isBoolean()) { - MOZ_ASSERT(false, "Expecting a boolean for 'bluetooth.debugging.enabled'!"); - return NS_ERROR_UNEXPECTED; - } - - SWITCH_BT_DEBUG(value.toBoolean()); - } + SWITCH_BT_DEBUG(setting.mValue.toBoolean()); return NS_OK; } @@ -686,7 +656,7 @@ BluetoothService::Observe(nsISupports* aSubject, const char* aTopic, } if (!strcmp(aTopic, MOZSETTINGS_CHANGED_ID)) { - return HandleSettingsChanged(nsDependentString(aData)); + return HandleSettingsChanged(aSubject); } if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { diff --git a/dom/bluetooth2/BluetoothService.h b/dom/bluetooth2/BluetoothService.h index 0588d15e76c3..cca723cd7568 100644 --- a/dom/bluetooth2/BluetoothService.h +++ b/dom/bluetooth2/BluetoothService.h @@ -371,7 +371,7 @@ protected: * Called when "mozsettings-changed" observer topic fires. */ nsresult - HandleSettingsChanged(const nsAString& aData); + HandleSettingsChanged(nsISupports* aSubject); /** * Called when XPCOM is shutting down. diff --git a/dom/bluetooth2/bluedroid/hfp/BluetoothHfpManager.cpp b/dom/bluetooth2/bluedroid/hfp/BluetoothHfpManager.cpp index 8b78a7520b3f..b6809fb243d3 100644 --- a/dom/bluetooth2/bluedroid/hfp/BluetoothHfpManager.cpp +++ b/dom/bluetooth2/bluedroid/hfp/BluetoothHfpManager.cpp @@ -27,6 +27,7 @@ #include "nsRadioInterfaceLayer.h" #include "nsServiceManagerUtils.h" #include "nsThreadUtils.h" +#include "mozilla/dom/SettingChangeNotificationBinding.h" #define MOZSETTINGS_CHANGED_ID "mozsettings-changed" #define AUDIO_VOLUME_BT_SCO_ID "audio.volume.bt_sco" @@ -465,7 +466,7 @@ BluetoothHfpManager::Observe(nsISupports* aSubject, const char16_t* aData) { if (!strcmp(aTopic, MOZSETTINGS_CHANGED_ID)) { - HandleVolumeChanged(nsDependentString(aData)); + HandleVolumeChanged(aSubject); } else if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { HandleShutdown(); } else { @@ -564,39 +565,29 @@ public: }; void -BluetoothHfpManager::HandleVolumeChanged(const nsAString& aData) +BluetoothHfpManager::HandleVolumeChanged(nsISupports* aSubject) { MOZ_ASSERT(NS_IsMainThread()); // The string that we're interested in will be a JSON string that looks like: // {"key":"volumeup", "value":10} // {"key":"volumedown", "value":2} - JSContext* cx = nsContentUtils::GetSafeJSContext(); - NS_ENSURE_TRUE_VOID(cx); - JS::Rooted val(cx); - NS_ENSURE_TRUE_VOID(JS_ParseJSON(cx, aData.BeginReading(), aData.Length(), &val)); - NS_ENSURE_TRUE_VOID(val.isObject()); - - JS::Rooted obj(cx, &val.toObject()); - JS::Rooted key(cx); - if (!JS_GetProperty(cx, obj, "key", &key) || !key.isString()) { + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + RootedDictionary setting(cx); + if (!WrappedJSToDictionary(cx, aSubject, setting)) { + return; + } + if (!setting.mKey.EqualsASCII(AUDIO_VOLUME_BT_SCO_ID)) { + return; + } + if (!setting.mValue.isNumber()) { return; } - bool match; - if (!JS_StringEqualsAscii(cx, key.toString(), AUDIO_VOLUME_BT_SCO_ID, &match) || - !match) { - return; - } - - JS::Rooted value(cx); - if (!JS_GetProperty(cx, obj, "value", &value) || - !value.isNumber()) { - return; - } - - mCurrentVgs = value.toNumber(); + mCurrentVgs = setting.mValue.toNumber(); // Adjust volume by headset and we don't have to send volume back to headset if (mReceiveVgsFlag) { diff --git a/dom/bluetooth2/bluedroid/hfp/BluetoothHfpManager.h b/dom/bluetooth2/bluedroid/hfp/BluetoothHfpManager.h index 3af1a6eb5c64..1cb15259b1ed 100644 --- a/dom/bluetooth2/bluedroid/hfp/BluetoothHfpManager.h +++ b/dom/bluetooth2/bluedroid/hfp/BluetoothHfpManager.h @@ -147,7 +147,7 @@ private: bool Init(); void HandleShutdown(); - void HandleVolumeChanged(const nsAString& aData); + void HandleVolumeChanged(nsISupports* aSubject); void Notify(const hal::BatteryInformation& aBatteryInfo); void NotifyConnectionStateChanged(const nsAString& aType); diff --git a/dom/bluetooth2/bluez/BluetoothHfpManager.cpp b/dom/bluetooth2/bluez/BluetoothHfpManager.cpp index cd9d4f9044ec..d8a8b6d6c7c5 100644 --- a/dom/bluetooth2/bluez/BluetoothHfpManager.cpp +++ b/dom/bluetooth2/bluez/BluetoothHfpManager.cpp @@ -23,6 +23,7 @@ #include "nsIObserverService.h" #include "nsISettingsService.h" #include "nsServiceManagerUtils.h" +#include "mozilla/dom/SettingChangeNotificationBinding.h" #ifdef MOZ_B2G_RIL #include "nsIDOMIccInfo.h" @@ -205,7 +206,7 @@ BluetoothHfpManager::Observe(nsISupports* aSubject, const char16_t* aData) { if (!strcmp(aTopic, MOZSETTINGS_CHANGED_ID)) { - HandleVolumeChanged(nsDependentString(aData)); + HandleVolumeChanged(aSubject); } else if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { HandleShutdown(); } else { @@ -555,7 +556,7 @@ BluetoothHfpManager::NotifyDialer(const nsAString& aCommand) #endif // MOZ_B2G_RIL void -BluetoothHfpManager::HandleVolumeChanged(const nsAString& aData) +BluetoothHfpManager::HandleVolumeChanged(nsISupports* aSubject) { MOZ_ASSERT(NS_IsMainThread()); @@ -563,32 +564,21 @@ BluetoothHfpManager::HandleVolumeChanged(const nsAString& aData) // {"key":"volumeup", "value":10} // {"key":"volumedown", "value":2} - JSContext* cx = nsContentUtils::GetSafeJSContext(); - NS_ENSURE_TRUE_VOID(cx); - - JS::Rooted val(cx); - NS_ENSURE_TRUE_VOID(JS_ParseJSON(cx, aData.BeginReading(), aData.Length(), &val)); - NS_ENSURE_TRUE_VOID(val.isObject()); - - JS::Rooted obj(cx, &val.toObject()); - JS::Rooted key(cx); - if (!JS_GetProperty(cx, obj, "key", &key) || !key.isString()) { + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + RootedDictionary setting(cx); + if (!WrappedJSToDictionary(cx, aSubject, setting)) { + return; + } + if (!setting.mKey.EqualsASCII(AUDIO_VOLUME_BT_SCO_ID)) { + return; + } + if (!setting.mValue.isNumber()) { return; } - bool match; - if (!JS_StringEqualsAscii(cx, key.toString(), AUDIO_VOLUME_BT_SCO_ID, &match) || - !match) { - return; - } - - JS::Rooted value(cx); - if (!JS_GetProperty(cx, obj, "value", &value)|| - !value.isNumber()) { - return; - } - - mCurrentVgs = value.toNumber(); + mCurrentVgs = setting.mValue.toNumber(); // Adjust volume by headset and we don't have to send volume back to headset if (mReceiveVgsFlag) { diff --git a/dom/bluetooth2/bluez/BluetoothHfpManager.h b/dom/bluetooth2/bluez/BluetoothHfpManager.h index 5b09ba2e125c..2de35b57a22f 100644 --- a/dom/bluetooth2/bluez/BluetoothHfpManager.h +++ b/dom/bluetooth2/bluez/BluetoothHfpManager.h @@ -149,7 +149,7 @@ private: BluetoothHfpManager(); void HandleShutdown(); - void HandleVolumeChanged(const nsAString& aData); + void HandleVolumeChanged(nsISupports* aSubject); bool Init(); void Notify(const hal::BatteryInformation& aBatteryInfo); diff --git a/dom/fmradio/FMRadioService.cpp b/dom/fmradio/FMRadioService.cpp index d6885eebd6d3..e80a278ce951 100644 --- a/dom/fmradio/FMRadioService.cpp +++ b/dom/fmradio/FMRadioService.cpp @@ -16,6 +16,8 @@ #include "nsIObserverService.h" #include "nsISettingsService.h" #include "nsJSUtils.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/SettingChangeNotificationBinding.h" #define BAND_87500_108000_kHz 1 #define BAND_76000_108000_kHz 2 @@ -678,9 +680,9 @@ FMRadioService::CancelSeek(FMRadioReplyRunnable* aReplyRunnable) } NS_IMETHODIMP -FMRadioService::Observe(nsISupports * aSubject, - const char * aTopic, - const char16_t * aData) +FMRadioService::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(sFMRadioService); @@ -691,47 +693,26 @@ FMRadioService::Observe(nsISupports * aSubject, // The string that we're interested in will be a JSON string looks like: // {"key":"airplaneMode.enabled","value":true} - AutoSafeJSContext cx; - const nsDependentString dataStr(aData); - JS::Rooted val(cx); - if (!JS_ParseJSON(cx, dataStr.get(), dataStr.Length(), &val) || - !val.isObject()) { - NS_WARNING("Bad JSON string format."); + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + RootedDictionary setting(cx); + if (!WrappedJSToDictionary(cx, aSubject, setting)) { + return NS_OK; + } + if (!setting.mKey.EqualsASCII(SETTING_KEY_AIRPLANEMODE_ENABLED)) { + return NS_OK; + } + if (!setting.mValue.isBoolean()) { return NS_OK; } - JS::Rooted obj(cx, &val.toObject()); - JS::Rooted key(cx); - if (!JS_GetProperty(cx, obj, "key", &key) || - !key.isString()) { - NS_WARNING("Failed to get string property `key`."); - return NS_OK; - } + mAirplaneModeEnabled = setting.mValue.toBoolean(); + mHasReadAirplaneModeSetting = true; - JS::Rooted jsKey(cx, key.toString()); - nsAutoJSString keyStr; - if (!keyStr.init(cx, jsKey)) { - return NS_OK; - } - - if (keyStr.EqualsLiteral(SETTING_KEY_AIRPLANEMODE_ENABLED)) { - JS::Rooted value(cx); - if (!JS_GetProperty(cx, obj, "value", &value)) { - NS_WARNING("Failed to get property `value`."); - return NS_OK; - } - - if (!value.isBoolean()) { - return NS_OK; - } - - mAirplaneModeEnabled = value.toBoolean(); - mHasReadAirplaneModeSetting = true; - - // Disable the FM radio HW if Airplane mode is enabled. - if (mAirplaneModeEnabled) { - Disable(nullptr); - } + // Disable the FM radio HW if Airplane mode is enabled. + if (mAirplaneModeEnabled) { + Disable(nullptr); } return NS_OK; diff --git a/dom/geolocation/nsGeolocation.cpp b/dom/geolocation/nsGeolocation.cpp index c3568aa4e529..f5f7f7e61db2 100644 --- a/dom/geolocation/nsGeolocation.cpp +++ b/dom/geolocation/nsGeolocation.cpp @@ -24,6 +24,7 @@ #include "mozilla/Preferences.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/dom/PermissionMessageUtils.h" +#include "mozilla/dom/SettingChangeNotificationBinding.h" class nsIPrincipal; @@ -712,36 +713,26 @@ nsGeolocationService::~nsGeolocationService() } void -nsGeolocationService::HandleMozsettingChanged(const char16_t* aData) +nsGeolocationService::HandleMozsettingChanged(nsISupports* aSubject) { // The string that we're interested in will be a JSON string that looks like: // {"key":"gelocation.enabled","value":true} - AutoSafeJSContext cx; - - nsDependentString dataStr(aData); - JS::Rooted val(cx); - if (!JS_ParseJSON(cx, dataStr.get(), dataStr.Length(), &val) || !val.isObject()) { + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + RootedDictionary setting(cx); + if (!WrappedJSToDictionary(cx, aSubject, setting)) { + return; + } + if (!setting.mKey.EqualsASCII(GEO_SETINGS_ENABLED)) { + return; + } + if (!setting.mValue.isBoolean()) { return; } - JS::Rooted obj(cx, &val.toObject()); - JS::Rooted key(cx); - if (!JS_GetProperty(cx, obj, "key", &key) || !key.isString()) { - return; - } - - bool match; - if (!JS_StringEqualsAscii(cx, key.toString(), GEO_SETINGS_ENABLED, &match) || !match) { - return; - } - - JS::Rooted value(cx); - if (!JS_GetProperty(cx, obj, "value", &value) || !value.isBoolean()) { - return; - } - - HandleMozsettingValue(value.toBoolean()); + HandleMozsettingValue(setting.mValue.toBoolean()); } void @@ -786,7 +777,7 @@ nsGeolocationService::Observe(nsISupports* aSubject, } if (!strcmp("mozsettings-changed", aTopic)) { - HandleMozsettingChanged(aData); + HandleMozsettingChanged(aSubject); return NS_OK; } diff --git a/dom/geolocation/nsGeolocation.h b/dom/geolocation/nsGeolocation.h index 255537384ceb..131090eb0fc7 100644 --- a/dom/geolocation/nsGeolocation.h +++ b/dom/geolocation/nsGeolocation.h @@ -70,7 +70,7 @@ public: nsresult Init(); - void HandleMozsettingChanged(const char16_t* aData); + void HandleMozsettingChanged(nsISupports* aSubject); void HandleMozsettingValue(const bool aValue); // Management of the Geolocation objects diff --git a/dom/settings/SettingsRequestManager.jsm b/dom/settings/SettingsRequestManager.jsm index 415e50aee3a0..2b250770cb16 100644 --- a/dom/settings/SettingsRequestManager.jsm +++ b/dom/settings/SettingsRequestManager.jsm @@ -662,12 +662,12 @@ let SettingsRequestManager = { sendSettingsChange: function(aKey, aValue, aIsServiceLock) { this.broadcastMessage("Settings:Change:Return:OK", { key: aKey, value: aValue }); - Services.obs.notifyObservers(this, kMozSettingsChangedObserverTopic, - JSON.stringify({ - key: aKey, - value: aValue, - isInternalChange: aIsServiceLock - })); + var setting = { + key: aKey, + value: aValue, + isInternalChange: aIsServiceLock + }; + Services.obs.notifyObservers(setting, kMozSettingsChangedObserverTopic, ""); }, broadcastMessage: function broadcastMessage(aMsgName, aContent) { diff --git a/dom/system/NetworkGeolocationProvider.js b/dom/system/NetworkGeolocationProvider.js index 4bc174aa843c..04aa0f24658c 100755 --- a/dom/system/NetworkGeolocationProvider.js +++ b/dom/system/NetworkGeolocationProvider.js @@ -264,11 +264,10 @@ WifiGeoPositionProvider.prototype = { } try { - let setting = JSON.parse(aData); - if (setting.key == SETTINGS_DEBUG_ENABLED) { - gLoggingEnabled = setting.value; - } else if (setting.key == SETTINGS_WIFI_ENABLED) { - gWifiScanningEnabled = setting.value; + if (aSubject.key == SETTINGS_DEBUG_ENABLED) { + gLoggingEnabled = aSubject.value; + } else if (aSubject.key == SETTINGS_WIFI_ENABLED) { + gWifiScanningEnabled = aSubject.value; } } catch (e) { } diff --git a/dom/system/gonk/AudioManager.cpp b/dom/system/gonk/AudioManager.cpp index a86a4b47c761..e71e0b43d73d 100644 --- a/dom/system/gonk/AudioManager.cpp +++ b/dom/system/gonk/AudioManager.cpp @@ -41,6 +41,8 @@ #include "nsServiceManagerUtils.h" #include "nsComponentManagerUtils.h" #include "nsXULAppAPI.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/SettingChangeNotificationBinding.h" using namespace mozilla::dom::gonk; using namespace android; @@ -350,36 +352,21 @@ AudioManager::Observe(nsISupports* aSubject, // To process the volume control on each audio channel according to // change of settings else if (!strcmp(aTopic, MOZ_SETTINGS_CHANGE_ID)) { - AutoSafeJSContext cx; - nsDependentString dataStr(aData); - JS::Rooted val(cx); - if (!JS_ParseJSON(cx, dataStr.get(), dataStr.Length(), &val) || - !val.isObject()) { + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + RootedDictionary setting(cx); + if (!WrappedJSToDictionary(cx, aSubject, setting)) { + return NS_OK; + } + if (!setting.mKey.EqualsASCII("audio.volume.bt_sco")) { + return NS_OK; + } + if (!setting.mValue.isNumber()) { return NS_OK; } - JS::Rooted obj(cx, &val.toObject()); - JS::Rooted key(cx); - if (!JS_GetProperty(cx, obj, "key", &key) || - !key.isString()) { - return NS_OK; - } - - JS::Rooted jsKey(cx, JS::ToString(cx, key)); - if (!jsKey) { - return NS_OK; - } - nsAutoJSString keyStr; - if (!keyStr.init(cx, jsKey) || !keyStr.EqualsLiteral("audio.volume.bt_sco")) { - return NS_OK; - } - - JS::Rooted value(cx); - if (!JS_GetProperty(cx, obj, "value", &value) || !value.isInt32()) { - return NS_OK; - } - - int32_t index = value.toInt32(); + int32_t index = setting.mValue.toNumber(); SetStreamVolumeIndex(AUDIO_STREAM_BLUETOOTH_SCO, index); return NS_OK; diff --git a/dom/system/gonk/AutoMounterSetting.cpp b/dom/system/gonk/AutoMounterSetting.cpp index 876a756b02be..be14bf3e1545 100644 --- a/dom/system/gonk/AutoMounterSetting.cpp +++ b/dom/system/gonk/AutoMounterSetting.cpp @@ -20,6 +20,8 @@ #include "xpcpublic.h" #include "mozilla/dom/ScriptSettings.h" #include "mozilla/Attributes.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/SettingChangeNotificationBinding.h" #undef LOG #define LOG(args...) __android_log_print(ANDROID_LOG_INFO, "AutoMounterSetting" , ## args) @@ -31,6 +33,8 @@ #define UMS_VOLUME_ENABLED_SUFFIX ".enabled" #define MOZSETTINGS_CHANGED "mozsettings-changed" +using namespace mozilla::dom; + namespace mozilla { namespace system { @@ -235,52 +239,35 @@ AutoMounterSetting::Observe(nsISupports* aSubject, // The string that we're interested in will be a JSON string that looks like: // {"key":"ums.autoMount","value":true} - mozilla::AutoSafeJSContext cx; - nsDependentString dataStr(aData); - JS::Rooted val(cx); - if (!JS_ParseJSON(cx, dataStr.get(), dataStr.Length(), &val) || - !val.isObject()) { - return NS_OK; - } - JS::Rooted obj(cx, &val.toObject()); - JS::Rooted key(cx); - if (!JS_GetProperty(cx, obj, "key", &key) || - !key.isString()) { - return NS_OK; - } - - JSString *jsKey = JS::ToString(cx, key); - nsAutoJSString keyStr; - if (!keyStr.init(cx, jsKey)) { - return NS_OK; - } - - JS::Rooted value(cx); - if (!JS_GetProperty(cx, obj, "value", &value)) { + AutoJSAPI jsapi; + jsapi.Init(); + JSContext* cx = jsapi.cx(); + RootedDictionary setting(cx); + if (!WrappedJSToDictionary(cx, aSubject, setting)) { return NS_OK; } // Check for ums.mode changes - if (keyStr.EqualsLiteral(UMS_MODE)) { - if (!value.isInt32()) { + if (setting.mKey.EqualsASCII(UMS_MODE)) { + if (!setting.mValue.isInt32()) { return NS_OK; } - int32_t mode = value.toInt32(); + int32_t mode = setting.mValue.toInt32(); SetAutoMounterMode(mode); return NS_OK; } // Check for ums.volume.NAME.enabled - if (StringBeginsWith(keyStr, NS_LITERAL_STRING(UMS_VOLUME_ENABLED_PREFIX)) && - StringEndsWith(keyStr, NS_LITERAL_STRING(UMS_VOLUME_ENABLED_SUFFIX))) { - if (!value.isBoolean()) { + if (StringBeginsWith(setting.mKey, NS_LITERAL_STRING(UMS_VOLUME_ENABLED_PREFIX)) && + StringEndsWith(setting.mKey, NS_LITERAL_STRING(UMS_VOLUME_ENABLED_SUFFIX))) { + if (!setting.mValue.isBoolean()) { return NS_OK; } const size_t prefixLen = sizeof(UMS_VOLUME_ENABLED_PREFIX) - 1; const size_t suffixLen = sizeof(UMS_VOLUME_ENABLED_SUFFIX) - 1; nsDependentSubstring volumeName = - Substring(keyStr, prefixLen, keyStr.Length() - prefixLen - suffixLen); - bool isSharingEnabled = value.toBoolean(); + Substring(setting.mKey, prefixLen, setting.mKey.Length() - prefixLen - suffixLen); + bool isSharingEnabled = setting.mValue.toBoolean(); SetAutoMounterSharingMode(NS_LossyConvertUTF16toASCII(volumeName), isSharingEnabled); return NS_OK; } diff --git a/dom/system/gonk/NetworkManager.js b/dom/system/gonk/NetworkManager.js index d639b9287d42..fad9153d0884 100644 --- a/dom/system/gonk/NetworkManager.js +++ b/dom/system/gonk/NetworkManager.js @@ -220,8 +220,7 @@ NetworkManager.prototype = { observe: function(subject, topic, data) { switch (topic) { case TOPIC_MOZSETTINGS_CHANGED: - let setting = JSON.parse(data); - this.handle(setting.key, setting.value); + this.handle(subject.key, subject.value); break; case TOPIC_PREF_CHANGED: this._manageOfflineStatus = diff --git a/dom/system/gonk/RadioInterfaceLayer.js b/dom/system/gonk/RadioInterfaceLayer.js index ae68cb14024d..eacd459b9b66 100644 --- a/dom/system/gonk/RadioInterfaceLayer.js +++ b/dom/system/gonk/RadioInterfaceLayer.js @@ -960,8 +960,7 @@ XPCOMUtils.defineLazyGetter(this, "gDataConnectionManager", function () { observe: function(subject, topic, data) { switch (topic) { case kMozSettingsChangedObserverTopic: - let setting = JSON.parse(data); - this.handle(setting.key, setting.value); + this.handle(subject.key, subject.value); break; case NS_XPCOM_SHUTDOWN_OBSERVER_ID: this._shutdown(); @@ -3049,8 +3048,7 @@ RadioInterface.prototype = { observe: function(subject, topic, data) { switch (topic) { case kMozSettingsChangedObserverTopic: - let setting = JSON.parse(data); - this.handleSettingsChange(setting.key, setting.value, setting.isInternalChange); + this.handleSettingsChange(subject.key, subject.value, subject.isInternalChange); break; case kSysClockChangeObserverTopic: let offset = parseInt(data, 10); diff --git a/dom/system/gonk/TimeZoneSettingObserver.cpp b/dom/system/gonk/TimeZoneSettingObserver.cpp index 18306e7e0d25..edc80d953b42 100644 --- a/dom/system/gonk/TimeZoneSettingObserver.cpp +++ b/dom/system/gonk/TimeZoneSettingObserver.cpp @@ -22,6 +22,8 @@ #include "xpcpublic.h" #include "nsContentUtils.h" #include "nsPrintfCString.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/SettingChangeNotificationBinding.h" #undef LOG #define LOG(args...) __android_log_print(ANDROID_LOG_INFO, "Time Zone Setting" , ## args) @@ -31,6 +33,7 @@ #define MOZSETTINGS_CHANGED "mozsettings-changed" using namespace mozilla; +using namespace mozilla::dom; namespace { @@ -184,8 +187,8 @@ NS_IMPL_ISUPPORTS(TimeZoneSettingObserver, nsIObserver) NS_IMETHODIMP TimeZoneSettingObserver::Observe(nsISupports *aSubject, - const char *aTopic, - const char16_t *aData) + const char *aTopic, + const char16_t *aData) { if (strcmp(aTopic, MOZSETTINGS_CHANGED) != 0) { return NS_OK; @@ -199,37 +202,19 @@ TimeZoneSettingObserver::Observe(nsISupports *aSubject, // {"key":"time.timezone","value":"UTC-05:00"} AutoSafeJSContext cx; - - // Parse the JSON value. - nsDependentString dataStr(aData); - JS::Rooted val(cx); - if (!JS_ParseJSON(cx, dataStr.get(), dataStr.Length(), &val) || - !val.isObject()) { + RootedDictionary setting(cx); + if (!WrappedJSToDictionary(cx, aSubject, setting)) { return NS_OK; } - - // Get the key, which should be the JS string "time.timezone". - JS::Rooted obj(cx, &val.toObject()); - JS::Rooted key(cx); - if (!JS_GetProperty(cx, obj, "key", &key) || - !key.isString()) { + if (!setting.mKey.EqualsASCII(TIME_TIMEZONE)) { return NS_OK; } - bool match; - if (!JS_StringEqualsAscii(cx, key.toString(), TIME_TIMEZONE, &match) || - !match) { - return NS_OK; - } - - // Get the value, which should be a JS string like "America/Chicago". - JS::Rooted value(cx); - if (!JS_GetProperty(cx, obj, "value", &value) || - !value.isString()) { + if (!setting.mValue.isString()) { return NS_OK; } // Set the system timezone. - return SetTimeZone(value, cx); + return SetTimeZone(setting.mValue, cx); } } // anonymous namespace diff --git a/dom/webidl/SettingChangeNotification.webidl b/dom/webidl/SettingChangeNotification.webidl new file mode 100644 index 000000000000..11c097e3dd82 --- /dev/null +++ b/dom/webidl/SettingChangeNotification.webidl @@ -0,0 +1,12 @@ +/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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/. + */ + +// Used internally by Gecko +dictionary SettingChangeNotification { + DOMString key = ""; + any value; + boolean isInternalChange = false; +}; diff --git a/dom/webidl/moz.build b/dom/webidl/moz.build index 9e64ef1ec812..3d1f5ea6f6cb 100644 --- a/dom/webidl/moz.build +++ b/dom/webidl/moz.build @@ -336,6 +336,7 @@ WEBIDL_FILES = [ 'ServiceWorkerContainer.webidl', 'ServiceWorkerGlobalScope.webidl', 'ServiceWorkerRegistration.webidl', + 'SettingChangeNotification.webidl', 'SettingsManager.webidl', 'ShadowRoot.webidl', 'SharedWorker.webidl', diff --git a/dom/wifi/WifiWorker.js b/dom/wifi/WifiWorker.js index 9e5256a4b819..7ccb575af99a 100644 --- a/dom/wifi/WifiWorker.js +++ b/dom/wifi/WifiWorker.js @@ -3631,17 +3631,13 @@ WifiWorker.prototype = { observe: function observe(subject, topic, data) { switch (topic) { case kMozSettingsChangedObserverTopic: - // The string we're interested in will be a JSON string that looks like: - // {"key":"wifi.enabled","value":"true"}. - - let setting = JSON.parse(data); // To avoid WifiWorker setting the wifi again, don't need to deal with // the "mozsettings-changed" event fired from internal setting. - if (setting.isInternalChange) { + if (subject.isInternalChange) { return; } - this.handle(setting.key, setting.value); + this.handle(subject.key, subject.value); break; case "xpcom-shutdown": diff --git a/toolkit/devtools/discovery/discovery.js b/toolkit/devtools/discovery/discovery.js index f2583cb650d7..bdeb08d27def 100644 --- a/toolkit/devtools/discovery/discovery.js +++ b/toolkit/devtools/discovery/discovery.js @@ -205,11 +205,10 @@ LocalDevice.prototype = { if (topic !== "mozsettings-changed") { return; } - let setting = JSON.parse(data); - if (setting.key !== LocalDevice.SETTING) { + if (subject.key !== LocalDevice.SETTING) { return; } - this._name = setting.value; + this._name = subject.value; log("Device: " + this._name); }, From 113c93ef50b6cb3305e86adbfd8a59169f4abba1 Mon Sep 17 00:00:00 2001 From: Matthew Noorenberghe Date: Wed, 10 Sep 2014 00:07:03 -0700 Subject: [PATCH 19/88] Bug 1065052 - Implement modified Loop FxA registration flow using a second Loop registration. r=abr,jaws --- browser/components/loop/MozLoopAPI.jsm | 11 ++- browser/components/loop/MozLoopService.jsm | 87 ++++++++++++++----- browser/components/loop/content/js/client.js | 1 - .../loop/test/mochitest/browser_fxa_login.js | 27 +++++- .../components/loop/test/mochitest/head.js | 43 +++++++++ .../loop/test/mochitest/loop_fxa.sjs | 37 +++++++- 6 files changed, 180 insertions(+), 26 deletions(-) diff --git a/browser/components/loop/MozLoopAPI.jsm b/browser/components/loop/MozLoopAPI.jsm index dacf1e2ae1b7..5cf5e6bc8c90 100644 --- a/browser/components/loop/MozLoopAPI.jsm +++ b/browser/components/loop/MozLoopAPI.jsm @@ -338,8 +338,9 @@ function injectLoopAPI(targetWindow) { enumerable: true, writable: true, value: function(path, method, payloadObj, callback) { + // XXX: Bug 1065153 - Should take a sessionType parameter instead of hard-coding GUEST // XXX Should really return a DOM promise here. - return MozLoopService.hawkRequest(path, method, payloadObj).then((response) => { + return MozLoopService.hawkRequest(LOOP_SESSION_TYPE.GUEST, path, method, payloadObj).then((response) => { callback(null, response.body); }, (error) => { callback(Cu.cloneInto(error, targetWindow)); @@ -347,6 +348,14 @@ function injectLoopAPI(targetWindow) { } }, + LOOP_SESSION_TYPE: { + enumerable: true, + writable: false, + value: function() { + return LOOP_SESSION_TYPE; + }, + }, + logInToFxA: { enumerable: true, writable: true, diff --git a/browser/components/loop/MozLoopService.jsm b/browser/components/loop/MozLoopService.jsm index d4121e57b589..04592c6ba9f8 100644 --- a/browser/components/loop/MozLoopService.jsm +++ b/browser/components/loop/MozLoopService.jsm @@ -15,6 +15,11 @@ const INVALID_AUTH_TOKEN = 110; // serving" number of 2^24 - 1 is greater than it. const MAX_SOFT_START_TICKET_NUMBER = 16777214; +const LOOP_SESSION_TYPE = { + GUEST: 1, + FXA: 2, +}; + Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Promise.jsm"); @@ -22,7 +27,7 @@ Cu.import("resource://gre/modules/osfile.jsm", this); Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/FxAccountsOAuthClient.jsm"); -this.EXPORTED_SYMBOLS = ["MozLoopService"]; +this.EXPORTED_SYMBOLS = ["MozLoopService", "LOOP_SESSION_TYPE"]; XPCOMUtils.defineLazyModuleGetter(this, "console", "resource://gre/modules/devtools/Console.jsm"); @@ -202,6 +207,8 @@ let MozLoopServiceInternal = { /** * Performs a hawk based request to the loop server. * + * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request. + * This is one of the LOOP_SESSION_TYPE members. * @param {String} path The path to make the request to. * @param {String} method The request method, e.g. 'POST', 'GET'. * @param {Object} payloadObj An object which is converted to JSON and @@ -212,14 +219,14 @@ let MozLoopServiceInternal = { * as JSON and contains an 'error' property, the promise will be * rejected with this JSON-parsed response. */ - hawkRequest: function(path, method, payloadObj) { + hawkRequest: function(sessionType, path, method, payloadObj) { if (!gHawkClient) { gHawkClient = new HawkClient(this.loopServerUri); } let sessionToken; try { - sessionToken = Services.prefs.getCharPref("loop.hawk-session-token"); + sessionToken = Services.prefs.getCharPref(this.getSessionTokenPrefName(sessionType)); } catch (x) { // It is ok for this not to exist, we'll default to sending no-creds } @@ -237,19 +244,37 @@ let MozLoopServiceInternal = { }); }, + getSessionTokenPrefName: function(sessionType) { + let suffix; + switch (sessionType) { + case LOOP_SESSION_TYPE.GUEST: + suffix = ""; + break; + case LOOP_SESSION_TYPE.FXA: + suffix = ".fxa"; + break; + default: + throw new Error("Unknown LOOP_SESSION_TYPE"); + break; + } + return "loop.hawk-session-token" + suffix; + }, + /** * Used to store a session token from a request if it exists in the headers. * + * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request. + * One of the LOOP_SESSION_TYPE members. * @param {Object} headers The request headers, which may include a * "hawk-session-token" to be saved. * @return true on success or no token, false on failure. */ - storeSessionToken: function(headers) { + storeSessionToken: function(sessionType, headers) { let sessionToken = headers["hawk-session-token"]; if (sessionToken) { // XXX should do more validation here if (sessionToken.length === 64) { - Services.prefs.setCharPref("loop.hawk-session-token", sessionToken); + Services.prefs.setCharPref(this.getSessionTokenPrefName(sessionType), sessionToken); } else { // XXX Bubble the precise details up to the UI somehow (bug 1013248). console.warn("Loop server sent an invalid session token"); @@ -274,28 +299,35 @@ let MozLoopServiceInternal = { return; } - this.registerWithLoopServer(pushUrl); + this.registerWithLoopServer(LOOP_SESSION_TYPE.GUEST, pushUrl).then(() => { + gRegisteredDeferred.resolve(); + // No need to clear the promise here, everything was good, so we don't need + // to re-register. + }, (error) => { + Cu.reportError("Failed to register with Loop server: " + error.errno); + gRegisteredDeferred.reject(error.errno); + gRegisteredDeferred = null; + }); }, /** - * Registers with the Loop server. + * Registers with the Loop server either as a guest or a FxA user. * + * @param {LOOP_SESSION_TYPE} sessionType The type of session e.g. guest or FxA * @param {String} pushUrl The push url given by the push server. - * @param {Boolean} noRetry Optional, don't retry if authentication fails. + * @param {Boolean} [retry=true] Whether to retry if authentication fails. + * @return {Promise} */ - registerWithLoopServer: function(pushUrl, noRetry) { - this.hawkRequest("/registration", "POST", { simplePushURL: pushUrl}) + registerWithLoopServer: function(sessionType, pushUrl, retry = true) { + return this.hawkRequest(sessionType, "/registration", "POST", { simplePushURL: pushUrl}) .then((response) => { // If this failed we got an invalid token. storeSessionToken rejects // the gRegisteredDeferred promise for us, so here we just need to // early return. - if (!this.storeSessionToken(response.headers)) + if (!this.storeSessionToken(sessionType, response.headers)) return; this.clearError("registration"); - gRegisteredDeferred.resolve(); - // No need to clear the promise here, everything was good, so we don't need - // to re-register. }, (error) => { // There's other errors than invalid auth token, but we should only do the reset // as a last resort. @@ -307,16 +339,16 @@ let MozLoopServiceInternal = { } // Authorization failed, invalid token, we need to try again with a new token. - Services.prefs.clearUserPref("loop.hawk-session-token"); - this.registerWithLoopServer(pushUrl, true); - return; + Services.prefs.clearUserPref(this.getSessionTokenPrefName(sessionType)); + if (retry) { + return this.registerWithLoopServer(sessionType, pushUrl, false); + } } // XXX Bubble the precise details up to the UI somehow (bug 1013248). Cu.reportError("Failed to register with the loop server. error: " + error); this.setError("registration", error); - gRegisteredDeferred.reject(error.errno); - gRegisteredDeferred = null; + throw error; } ); }, @@ -509,7 +541,7 @@ let MozLoopServiceInternal = { * @return {Promise} resolved with the body of the hawk request for OAuth parameters. */ promiseFxAOAuthParameters: function() { - return this.hawkRequest("/fxa-oauth/params", "POST").then(response => { + return this.hawkRequest(LOOP_SESSION_TYPE.FXA, "/fxa-oauth/params", "POST").then(response => { return JSON.parse(response.body); }); }, @@ -587,7 +619,7 @@ let MozLoopServiceInternal = { code: code, state: state, }; - return this.hawkRequest("/fxa-oauth/token", "POST", payload).then(response => { + return this.hawkRequest(LOOP_SESSION_TYPE.FXA, "/fxa-oauth/token", "POST", payload).then(response => { return JSON.parse(response.body); }); }, @@ -921,6 +953,15 @@ this.MozLoopService = { }).then(tokenData => { gFxAOAuthTokenData = tokenData; return tokenData; + }).then(tokenData => { + return gRegisteredDeferred.promise.then(Task.async(function*() { + if (gPushHandler.pushUrl) { + yield MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA, gPushHandler.pushUrl); + } else { + throw new Error("No pushUrl for FxA registration"); + } + return gFxAOAuthTokenData; + })); }, error => { gFxAOAuthTokenData = null; @@ -931,6 +972,8 @@ this.MozLoopService = { /** * Performs a hawk based request to the loop server. * + * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request. + * One of the LOOP_SESSION_TYPE members. * @param {String} path The path to make the request to. * @param {String} method The request method, e.g. 'POST', 'GET'. * @param {Object} payloadObj An object which is converted to JSON and @@ -941,7 +984,7 @@ this.MozLoopService = { * as JSON and contains an 'error' property, the promise will be * rejected with this JSON-parsed response. */ - hawkRequest: function(path, method, payloadObj) { + hawkRequest: function(sessionType, path, method, payloadObj) { return MozLoopServiceInternal.hawkRequest(path, method, payloadObj); }, }; diff --git a/browser/components/loop/content/js/client.js b/browser/components/loop/content/js/client.js index 7f35ada2341b..161ccab7f720 100644 --- a/browser/components/loop/content/js/client.js +++ b/browser/components/loop/content/js/client.js @@ -104,7 +104,6 @@ loop.Client = (function($) { * -- callUrl: The url of the call * -- expiresAt: The amount of hours until expiry of the url * - * @param {String} simplepushUrl a registered Simple Push URL * @param {string} nickname the nickname of the future caller * @param {Function} cb Callback(err, callUrlData) */ diff --git a/browser/components/loop/test/mochitest/browser_fxa_login.js b/browser/components/loop/test/mochitest/browser_fxa_login.js index 1347220c671c..2e0b11336c14 100644 --- a/browser/components/loop/test/mochitest/browser_fxa_login.js +++ b/browser/components/loop/test/mochitest/browser_fxa_login.js @@ -7,8 +7,13 @@ "use strict"; -const gFxAOAuthTokenData = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}).gFxAOAuthTokenData; +const { + LOOP_SESSION_TYPE, + gFxAOAuthTokenData +} = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}); + const BASE_URL = "http://mochi.test:8888/browser/browser/components/loop/test/mochitest/loop_fxa.sjs?"; +const HAWK_TOKEN_LENGTH = 64; add_task(function* setup() { Services.prefs.setCharPref("loop.server", BASE_URL); @@ -18,6 +23,8 @@ add_task(function* setup() { yield promiseDeletedOAuthParams(BASE_URL); Services.prefs.clearUserPref("loop.server"); Services.prefs.clearUserPref("services.push.serverURL"); + Services.prefs.clearUserPref(MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.GUEST)); + Services.prefs.clearUserPref(MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA)); }); }); @@ -161,10 +168,27 @@ add_task(function* basicAuthorizationAndRegistration() { }; yield promiseOAuthParamsSetup(BASE_URL, params); + info("registering"); + mockPushHandler.pushUrl = "https://localhost/pushUrl/guest"; + yield MozLoopService.register(mockPushHandler); + let prefName = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.GUEST); + let padding = new Array(HAWK_TOKEN_LENGTH - mockPushHandler.pushUrl.length).fill("X").join(""); + ise(Services.prefs.getCharPref(prefName), mockPushHandler.pushUrl + padding, "Check guest hawk token"); + + // Normally the same pushUrl would be registered but we change it in the test + // to be able to check for success on the second registration. + mockPushHandler.pushUrl = "https://localhost/pushUrl/fxa"; + let tokenData = yield MozLoopService.logInToFxA(); ise(tokenData.access_token, "code1_access_token", "Check access_token"); ise(tokenData.scope, "profile", "Check scope"); ise(tokenData.token_type, "bearer", "Check token_type"); + + let registrationResponse = yield promiseOAuthGetRegistration(BASE_URL); + ise(registrationResponse.response.simplePushURL, "https://localhost/pushUrl/fxa", "Check registered push URL"); + prefName = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA); + padding = new Array(HAWK_TOKEN_LENGTH - mockPushHandler.pushUrl.length).fill("X").join(""); + ise(Services.prefs.getCharPref(prefName), mockPushHandler.pushUrl + padding, "Check FxA hawk token"); }); add_task(function* loginWithParams401() { @@ -178,6 +202,7 @@ add_task(function* loginWithParams401() { test_error: "params_401", }; yield promiseOAuthParamsSetup(BASE_URL, params); + yield MozLoopService.register(mockPushHandler); let loginPromise = MozLoopService.logInToFxA(); yield loginPromise.then(tokenData => { diff --git a/browser/components/loop/test/mochitest/head.js b/browser/components/loop/test/mochitest/head.js index ce652ff04986..0fe60041a29c 100644 --- a/browser/components/loop/test/mochitest/head.js +++ b/browser/components/loop/test/mochitest/head.js @@ -112,3 +112,46 @@ function promiseDeletedOAuthParams(baseURL) { return deferred.promise; } + +/** + * Get the last registration on the test server. + */ +function promiseOAuthGetRegistration(baseURL) { + let deferred = Promise.defer(); + let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. + createInstance(Ci.nsIXMLHttpRequest); + xhr.open("GET", baseURL + "/get_registration", true); + xhr.responseType = "json"; + xhr.addEventListener("load", () => deferred.resolve(xhr)); + xhr.addEventListener("error", deferred.reject); + xhr.send(); + + return deferred.promise; +} + +/** + * This is used to fake push registration and notifications for + * MozLoopService tests. There is only one object created per test instance, as + * once registration has taken place, the object cannot currently be changed. + */ +let mockPushHandler = { + // This sets the registration result to be returned when initialize + // is called. By default, it is equivalent to success. + registrationResult: null, + pushUrl: undefined, + + /** + * MozLoopPushHandler API + */ + initialize: function(registerCallback, notificationCallback) { + registerCallback(this.registrationResult, this.pushUrl); + this._notificationCallback = notificationCallback; + }, + + /** + * Test-only API to simplify notifying a push notification result. + */ + notify: function(version) { + this._notificationCallback(version); + } +}; diff --git a/browser/components/loop/test/mochitest/loop_fxa.sjs b/browser/components/loop/test/mochitest/loop_fxa.sjs index 5293f36dceb6..0eb678ba110a 100644 --- a/browser/components/loop/test/mochitest/loop_fxa.sjs +++ b/browser/components/loop/test/mochitest/loop_fxa.sjs @@ -8,6 +8,7 @@ "use strict"; const REQUIRED_PARAMS = ["client_id", "content_uri", "oauth_uri", "profile_uri", "state"]; +const HAWK_TOKEN_LENGTH = 64; Components.utils.import("resource://gre/modules/NetUtil.jsm"); @@ -17,7 +18,7 @@ Components.utils.import("resource://gre/modules/NetUtil.jsm"); function handleRequest(request, response) { // Look at the query string but ignore past the encoded ? when deciding on the handler. switch (request.queryString.replace(/%3F.*/,"")) { - case "/setup_params": + case "/setup_params": // Test-only setup_params(request, response); return; case "/fxa-oauth/params": @@ -29,6 +30,12 @@ function handleRequest(request, response) { case "/fxa-oauth/token": token(request, response); return; + case "/registration": + registration(request, response); + return; + case "/get_registration": // Test-only + get_registration(request, response); + return; } response.setStatusLine(request.httpVersion, 404, "Not Found"); } @@ -47,6 +54,7 @@ function setup_params(request, response) { response.setHeader("Content-Type", "text/plain", false); if (request.method == "DELETE") { setSharedState("/fxa-oauth/params", ""); + setSharedState("/registration", ""); response.write("Params deleted"); return; } @@ -141,3 +149,30 @@ function token(request, response) { response.setHeader("Content-Type", "application/json; charset=utf-8", false); response.write(JSON.stringify(tokenData, null, 2)); } + +/** + * POST /registration + * + * Mock Loop registration endpoint which simply returns the simplePushURL with + * padding as the hawk session token. + */ +function registration(request, response) { + let body = NetUtil.readInputStreamToString(request.bodyInputStream, + request.bodyInputStream.available()); + let payload = JSON.parse(body); + setSharedState("/registration", body); + let pushURL = payload.simplePushURL; + // Pad the pushURL with "X" to the token length to simulate a token + let padding = new Array(HAWK_TOKEN_LENGTH - pushURL.length).fill("X").join(""); + response.setHeader("hawk-session-token", pushURL + padding, false); +} + +/** + * GET /get_registration + * + * Used for testing purposes to check if registration succeeded by returning the POST body. + */ +function get_registration(request, response) { + response.setHeader("Content-Type", "application/json; charset=utf-8", false); + response.write(getSharedState("/registration")); +} From ffc2d58074e06a332228bd2a9b8002161d14149b Mon Sep 17 00:00:00 2001 From: Alexander Seleznev Date: Wed, 10 Sep 2014 00:07:10 -0700 Subject: [PATCH 20/88] Bug 1003713 - Fix icon and position of help subview anchor arrow (RTL locale). r=jaws --- .../themes/shared/customizableui/panelUIOverlay.inc.css | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/browser/themes/shared/customizableui/panelUIOverlay.inc.css b/browser/themes/shared/customizableui/panelUIOverlay.inc.css index 357311e85592..19f95d70be71 100644 --- a/browser/themes/shared/customizableui/panelUIOverlay.inc.css +++ b/browser/themes/shared/customizableui/panelUIOverlay.inc.css @@ -891,7 +891,13 @@ toolbarbutton[panel-multiview-anchor="true"] > .toolbarbutton-menubutton-button linear-gradient(rgba(255,255,255,0.3), rgba(255,255,255,0)); background-repeat: no-repeat; background-color: Highlight; - background-position: left 10px center, 0; /* this doesn't need to be changed for RTL */ + background-position: left 10px center, 0; +} + +#PanelUI-help[panel-multiview-anchor="true"]:-moz-locale-dir(rtl)::after { + background-image: url(chrome://browser/skin/customizableui/subView-arrow-back-inverted-rtl.png), + linear-gradient(rgba(255,255,255,0.3), rgba(255,255,255,0)); + background-position: right 10px center, 0; } toolbarbutton[panel-multiview-anchor="true"] { From 3b0f454701de13eae0b817d45ba3ecd738bc9c87 Mon Sep 17 00:00:00 2001 From: "Carsten \"Tomcat\" Book" Date: Wed, 10 Sep 2014 10:44:22 +0200 Subject: [PATCH 21/88] Backed out changeset cea07e54707e (bug 1065052) for xpcshell test failures --- browser/components/loop/MozLoopAPI.jsm | 11 +-- browser/components/loop/MozLoopService.jsm | 87 +++++-------------- browser/components/loop/content/js/client.js | 1 + .../loop/test/mochitest/browser_fxa_login.js | 27 +----- .../components/loop/test/mochitest/head.js | 43 --------- .../loop/test/mochitest/loop_fxa.sjs | 37 +------- 6 files changed, 26 insertions(+), 180 deletions(-) diff --git a/browser/components/loop/MozLoopAPI.jsm b/browser/components/loop/MozLoopAPI.jsm index 5cf5e6bc8c90..dacf1e2ae1b7 100644 --- a/browser/components/loop/MozLoopAPI.jsm +++ b/browser/components/loop/MozLoopAPI.jsm @@ -338,9 +338,8 @@ function injectLoopAPI(targetWindow) { enumerable: true, writable: true, value: function(path, method, payloadObj, callback) { - // XXX: Bug 1065153 - Should take a sessionType parameter instead of hard-coding GUEST // XXX Should really return a DOM promise here. - return MozLoopService.hawkRequest(LOOP_SESSION_TYPE.GUEST, path, method, payloadObj).then((response) => { + return MozLoopService.hawkRequest(path, method, payloadObj).then((response) => { callback(null, response.body); }, (error) => { callback(Cu.cloneInto(error, targetWindow)); @@ -348,14 +347,6 @@ function injectLoopAPI(targetWindow) { } }, - LOOP_SESSION_TYPE: { - enumerable: true, - writable: false, - value: function() { - return LOOP_SESSION_TYPE; - }, - }, - logInToFxA: { enumerable: true, writable: true, diff --git a/browser/components/loop/MozLoopService.jsm b/browser/components/loop/MozLoopService.jsm index 04592c6ba9f8..d4121e57b589 100644 --- a/browser/components/loop/MozLoopService.jsm +++ b/browser/components/loop/MozLoopService.jsm @@ -15,11 +15,6 @@ const INVALID_AUTH_TOKEN = 110; // serving" number of 2^24 - 1 is greater than it. const MAX_SOFT_START_TICKET_NUMBER = 16777214; -const LOOP_SESSION_TYPE = { - GUEST: 1, - FXA: 2, -}; - Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Promise.jsm"); @@ -27,7 +22,7 @@ Cu.import("resource://gre/modules/osfile.jsm", this); Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/FxAccountsOAuthClient.jsm"); -this.EXPORTED_SYMBOLS = ["MozLoopService", "LOOP_SESSION_TYPE"]; +this.EXPORTED_SYMBOLS = ["MozLoopService"]; XPCOMUtils.defineLazyModuleGetter(this, "console", "resource://gre/modules/devtools/Console.jsm"); @@ -207,8 +202,6 @@ let MozLoopServiceInternal = { /** * Performs a hawk based request to the loop server. * - * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request. - * This is one of the LOOP_SESSION_TYPE members. * @param {String} path The path to make the request to. * @param {String} method The request method, e.g. 'POST', 'GET'. * @param {Object} payloadObj An object which is converted to JSON and @@ -219,14 +212,14 @@ let MozLoopServiceInternal = { * as JSON and contains an 'error' property, the promise will be * rejected with this JSON-parsed response. */ - hawkRequest: function(sessionType, path, method, payloadObj) { + hawkRequest: function(path, method, payloadObj) { if (!gHawkClient) { gHawkClient = new HawkClient(this.loopServerUri); } let sessionToken; try { - sessionToken = Services.prefs.getCharPref(this.getSessionTokenPrefName(sessionType)); + sessionToken = Services.prefs.getCharPref("loop.hawk-session-token"); } catch (x) { // It is ok for this not to exist, we'll default to sending no-creds } @@ -244,37 +237,19 @@ let MozLoopServiceInternal = { }); }, - getSessionTokenPrefName: function(sessionType) { - let suffix; - switch (sessionType) { - case LOOP_SESSION_TYPE.GUEST: - suffix = ""; - break; - case LOOP_SESSION_TYPE.FXA: - suffix = ".fxa"; - break; - default: - throw new Error("Unknown LOOP_SESSION_TYPE"); - break; - } - return "loop.hawk-session-token" + suffix; - }, - /** * Used to store a session token from a request if it exists in the headers. * - * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request. - * One of the LOOP_SESSION_TYPE members. * @param {Object} headers The request headers, which may include a * "hawk-session-token" to be saved. * @return true on success or no token, false on failure. */ - storeSessionToken: function(sessionType, headers) { + storeSessionToken: function(headers) { let sessionToken = headers["hawk-session-token"]; if (sessionToken) { // XXX should do more validation here if (sessionToken.length === 64) { - Services.prefs.setCharPref(this.getSessionTokenPrefName(sessionType), sessionToken); + Services.prefs.setCharPref("loop.hawk-session-token", sessionToken); } else { // XXX Bubble the precise details up to the UI somehow (bug 1013248). console.warn("Loop server sent an invalid session token"); @@ -299,35 +274,28 @@ let MozLoopServiceInternal = { return; } - this.registerWithLoopServer(LOOP_SESSION_TYPE.GUEST, pushUrl).then(() => { - gRegisteredDeferred.resolve(); - // No need to clear the promise here, everything was good, so we don't need - // to re-register. - }, (error) => { - Cu.reportError("Failed to register with Loop server: " + error.errno); - gRegisteredDeferred.reject(error.errno); - gRegisteredDeferred = null; - }); + this.registerWithLoopServer(pushUrl); }, /** - * Registers with the Loop server either as a guest or a FxA user. + * Registers with the Loop server. * - * @param {LOOP_SESSION_TYPE} sessionType The type of session e.g. guest or FxA * @param {String} pushUrl The push url given by the push server. - * @param {Boolean} [retry=true] Whether to retry if authentication fails. - * @return {Promise} + * @param {Boolean} noRetry Optional, don't retry if authentication fails. */ - registerWithLoopServer: function(sessionType, pushUrl, retry = true) { - return this.hawkRequest(sessionType, "/registration", "POST", { simplePushURL: pushUrl}) + registerWithLoopServer: function(pushUrl, noRetry) { + this.hawkRequest("/registration", "POST", { simplePushURL: pushUrl}) .then((response) => { // If this failed we got an invalid token. storeSessionToken rejects // the gRegisteredDeferred promise for us, so here we just need to // early return. - if (!this.storeSessionToken(sessionType, response.headers)) + if (!this.storeSessionToken(response.headers)) return; this.clearError("registration"); + gRegisteredDeferred.resolve(); + // No need to clear the promise here, everything was good, so we don't need + // to re-register. }, (error) => { // There's other errors than invalid auth token, but we should only do the reset // as a last resort. @@ -339,16 +307,16 @@ let MozLoopServiceInternal = { } // Authorization failed, invalid token, we need to try again with a new token. - Services.prefs.clearUserPref(this.getSessionTokenPrefName(sessionType)); - if (retry) { - return this.registerWithLoopServer(sessionType, pushUrl, false); - } + Services.prefs.clearUserPref("loop.hawk-session-token"); + this.registerWithLoopServer(pushUrl, true); + return; } // XXX Bubble the precise details up to the UI somehow (bug 1013248). Cu.reportError("Failed to register with the loop server. error: " + error); this.setError("registration", error); - throw error; + gRegisteredDeferred.reject(error.errno); + gRegisteredDeferred = null; } ); }, @@ -541,7 +509,7 @@ let MozLoopServiceInternal = { * @return {Promise} resolved with the body of the hawk request for OAuth parameters. */ promiseFxAOAuthParameters: function() { - return this.hawkRequest(LOOP_SESSION_TYPE.FXA, "/fxa-oauth/params", "POST").then(response => { + return this.hawkRequest("/fxa-oauth/params", "POST").then(response => { return JSON.parse(response.body); }); }, @@ -619,7 +587,7 @@ let MozLoopServiceInternal = { code: code, state: state, }; - return this.hawkRequest(LOOP_SESSION_TYPE.FXA, "/fxa-oauth/token", "POST", payload).then(response => { + return this.hawkRequest("/fxa-oauth/token", "POST", payload).then(response => { return JSON.parse(response.body); }); }, @@ -953,15 +921,6 @@ this.MozLoopService = { }).then(tokenData => { gFxAOAuthTokenData = tokenData; return tokenData; - }).then(tokenData => { - return gRegisteredDeferred.promise.then(Task.async(function*() { - if (gPushHandler.pushUrl) { - yield MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA, gPushHandler.pushUrl); - } else { - throw new Error("No pushUrl for FxA registration"); - } - return gFxAOAuthTokenData; - })); }, error => { gFxAOAuthTokenData = null; @@ -972,8 +931,6 @@ this.MozLoopService = { /** * Performs a hawk based request to the loop server. * - * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request. - * One of the LOOP_SESSION_TYPE members. * @param {String} path The path to make the request to. * @param {String} method The request method, e.g. 'POST', 'GET'. * @param {Object} payloadObj An object which is converted to JSON and @@ -984,7 +941,7 @@ this.MozLoopService = { * as JSON and contains an 'error' property, the promise will be * rejected with this JSON-parsed response. */ - hawkRequest: function(sessionType, path, method, payloadObj) { + hawkRequest: function(path, method, payloadObj) { return MozLoopServiceInternal.hawkRequest(path, method, payloadObj); }, }; diff --git a/browser/components/loop/content/js/client.js b/browser/components/loop/content/js/client.js index 161ccab7f720..7f35ada2341b 100644 --- a/browser/components/loop/content/js/client.js +++ b/browser/components/loop/content/js/client.js @@ -104,6 +104,7 @@ loop.Client = (function($) { * -- callUrl: The url of the call * -- expiresAt: The amount of hours until expiry of the url * + * @param {String} simplepushUrl a registered Simple Push URL * @param {string} nickname the nickname of the future caller * @param {Function} cb Callback(err, callUrlData) */ diff --git a/browser/components/loop/test/mochitest/browser_fxa_login.js b/browser/components/loop/test/mochitest/browser_fxa_login.js index 2e0b11336c14..1347220c671c 100644 --- a/browser/components/loop/test/mochitest/browser_fxa_login.js +++ b/browser/components/loop/test/mochitest/browser_fxa_login.js @@ -7,13 +7,8 @@ "use strict"; -const { - LOOP_SESSION_TYPE, - gFxAOAuthTokenData -} = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}); - +const gFxAOAuthTokenData = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}).gFxAOAuthTokenData; const BASE_URL = "http://mochi.test:8888/browser/browser/components/loop/test/mochitest/loop_fxa.sjs?"; -const HAWK_TOKEN_LENGTH = 64; add_task(function* setup() { Services.prefs.setCharPref("loop.server", BASE_URL); @@ -23,8 +18,6 @@ add_task(function* setup() { yield promiseDeletedOAuthParams(BASE_URL); Services.prefs.clearUserPref("loop.server"); Services.prefs.clearUserPref("services.push.serverURL"); - Services.prefs.clearUserPref(MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.GUEST)); - Services.prefs.clearUserPref(MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA)); }); }); @@ -168,27 +161,10 @@ add_task(function* basicAuthorizationAndRegistration() { }; yield promiseOAuthParamsSetup(BASE_URL, params); - info("registering"); - mockPushHandler.pushUrl = "https://localhost/pushUrl/guest"; - yield MozLoopService.register(mockPushHandler); - let prefName = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.GUEST); - let padding = new Array(HAWK_TOKEN_LENGTH - mockPushHandler.pushUrl.length).fill("X").join(""); - ise(Services.prefs.getCharPref(prefName), mockPushHandler.pushUrl + padding, "Check guest hawk token"); - - // Normally the same pushUrl would be registered but we change it in the test - // to be able to check for success on the second registration. - mockPushHandler.pushUrl = "https://localhost/pushUrl/fxa"; - let tokenData = yield MozLoopService.logInToFxA(); ise(tokenData.access_token, "code1_access_token", "Check access_token"); ise(tokenData.scope, "profile", "Check scope"); ise(tokenData.token_type, "bearer", "Check token_type"); - - let registrationResponse = yield promiseOAuthGetRegistration(BASE_URL); - ise(registrationResponse.response.simplePushURL, "https://localhost/pushUrl/fxa", "Check registered push URL"); - prefName = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA); - padding = new Array(HAWK_TOKEN_LENGTH - mockPushHandler.pushUrl.length).fill("X").join(""); - ise(Services.prefs.getCharPref(prefName), mockPushHandler.pushUrl + padding, "Check FxA hawk token"); }); add_task(function* loginWithParams401() { @@ -202,7 +178,6 @@ add_task(function* loginWithParams401() { test_error: "params_401", }; yield promiseOAuthParamsSetup(BASE_URL, params); - yield MozLoopService.register(mockPushHandler); let loginPromise = MozLoopService.logInToFxA(); yield loginPromise.then(tokenData => { diff --git a/browser/components/loop/test/mochitest/head.js b/browser/components/loop/test/mochitest/head.js index 0fe60041a29c..ce652ff04986 100644 --- a/browser/components/loop/test/mochitest/head.js +++ b/browser/components/loop/test/mochitest/head.js @@ -112,46 +112,3 @@ function promiseDeletedOAuthParams(baseURL) { return deferred.promise; } - -/** - * Get the last registration on the test server. - */ -function promiseOAuthGetRegistration(baseURL) { - let deferred = Promise.defer(); - let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. - createInstance(Ci.nsIXMLHttpRequest); - xhr.open("GET", baseURL + "/get_registration", true); - xhr.responseType = "json"; - xhr.addEventListener("load", () => deferred.resolve(xhr)); - xhr.addEventListener("error", deferred.reject); - xhr.send(); - - return deferred.promise; -} - -/** - * This is used to fake push registration and notifications for - * MozLoopService tests. There is only one object created per test instance, as - * once registration has taken place, the object cannot currently be changed. - */ -let mockPushHandler = { - // This sets the registration result to be returned when initialize - // is called. By default, it is equivalent to success. - registrationResult: null, - pushUrl: undefined, - - /** - * MozLoopPushHandler API - */ - initialize: function(registerCallback, notificationCallback) { - registerCallback(this.registrationResult, this.pushUrl); - this._notificationCallback = notificationCallback; - }, - - /** - * Test-only API to simplify notifying a push notification result. - */ - notify: function(version) { - this._notificationCallback(version); - } -}; diff --git a/browser/components/loop/test/mochitest/loop_fxa.sjs b/browser/components/loop/test/mochitest/loop_fxa.sjs index 0eb678ba110a..5293f36dceb6 100644 --- a/browser/components/loop/test/mochitest/loop_fxa.sjs +++ b/browser/components/loop/test/mochitest/loop_fxa.sjs @@ -8,7 +8,6 @@ "use strict"; const REQUIRED_PARAMS = ["client_id", "content_uri", "oauth_uri", "profile_uri", "state"]; -const HAWK_TOKEN_LENGTH = 64; Components.utils.import("resource://gre/modules/NetUtil.jsm"); @@ -18,7 +17,7 @@ Components.utils.import("resource://gre/modules/NetUtil.jsm"); function handleRequest(request, response) { // Look at the query string but ignore past the encoded ? when deciding on the handler. switch (request.queryString.replace(/%3F.*/,"")) { - case "/setup_params": // Test-only + case "/setup_params": setup_params(request, response); return; case "/fxa-oauth/params": @@ -30,12 +29,6 @@ function handleRequest(request, response) { case "/fxa-oauth/token": token(request, response); return; - case "/registration": - registration(request, response); - return; - case "/get_registration": // Test-only - get_registration(request, response); - return; } response.setStatusLine(request.httpVersion, 404, "Not Found"); } @@ -54,7 +47,6 @@ function setup_params(request, response) { response.setHeader("Content-Type", "text/plain", false); if (request.method == "DELETE") { setSharedState("/fxa-oauth/params", ""); - setSharedState("/registration", ""); response.write("Params deleted"); return; } @@ -149,30 +141,3 @@ function token(request, response) { response.setHeader("Content-Type", "application/json; charset=utf-8", false); response.write(JSON.stringify(tokenData, null, 2)); } - -/** - * POST /registration - * - * Mock Loop registration endpoint which simply returns the simplePushURL with - * padding as the hawk session token. - */ -function registration(request, response) { - let body = NetUtil.readInputStreamToString(request.bodyInputStream, - request.bodyInputStream.available()); - let payload = JSON.parse(body); - setSharedState("/registration", body); - let pushURL = payload.simplePushURL; - // Pad the pushURL with "X" to the token length to simulate a token - let padding = new Array(HAWK_TOKEN_LENGTH - pushURL.length).fill("X").join(""); - response.setHeader("hawk-session-token", pushURL + padding, false); -} - -/** - * GET /get_registration - * - * Used for testing purposes to check if registration succeeded by returning the POST body. - */ -function get_registration(request, response) { - response.setHeader("Content-Type", "application/json; charset=utf-8", false); - response.write(getSharedState("/registration")); -} From 148244a3e6a42b6c351b6a079dcfe59901a0add8 Mon Sep 17 00:00:00 2001 From: Matthew Noorenberghe Date: Wed, 10 Sep 2014 01:48:54 -0700 Subject: [PATCH 22/88] Bug 1065052 - Implement modified Loop FxA registration flow using a second Loop registration. r=abr,jaws --- browser/components/loop/MozLoopAPI.jsm | 11 ++- browser/components/loop/MozLoopService.jsm | 91 ++++++++++++++----- browser/components/loop/content/js/client.js | 1 - .../loop/test/mochitest/browser_fxa_login.js | 27 +++++- .../components/loop/test/mochitest/head.js | 43 +++++++++ .../loop/test/mochitest/loop_fxa.sjs | 37 +++++++- 6 files changed, 184 insertions(+), 26 deletions(-) diff --git a/browser/components/loop/MozLoopAPI.jsm b/browser/components/loop/MozLoopAPI.jsm index dacf1e2ae1b7..5cf5e6bc8c90 100644 --- a/browser/components/loop/MozLoopAPI.jsm +++ b/browser/components/loop/MozLoopAPI.jsm @@ -338,8 +338,9 @@ function injectLoopAPI(targetWindow) { enumerable: true, writable: true, value: function(path, method, payloadObj, callback) { + // XXX: Bug 1065153 - Should take a sessionType parameter instead of hard-coding GUEST // XXX Should really return a DOM promise here. - return MozLoopService.hawkRequest(path, method, payloadObj).then((response) => { + return MozLoopService.hawkRequest(LOOP_SESSION_TYPE.GUEST, path, method, payloadObj).then((response) => { callback(null, response.body); }, (error) => { callback(Cu.cloneInto(error, targetWindow)); @@ -347,6 +348,14 @@ function injectLoopAPI(targetWindow) { } }, + LOOP_SESSION_TYPE: { + enumerable: true, + writable: false, + value: function() { + return LOOP_SESSION_TYPE; + }, + }, + logInToFxA: { enumerable: true, writable: true, diff --git a/browser/components/loop/MozLoopService.jsm b/browser/components/loop/MozLoopService.jsm index d4121e57b589..e234cb7cadbf 100644 --- a/browser/components/loop/MozLoopService.jsm +++ b/browser/components/loop/MozLoopService.jsm @@ -15,6 +15,11 @@ const INVALID_AUTH_TOKEN = 110; // serving" number of 2^24 - 1 is greater than it. const MAX_SOFT_START_TICKET_NUMBER = 16777214; +const LOOP_SESSION_TYPE = { + GUEST: 1, + FXA: 2, +}; + Cu.import("resource://gre/modules/Services.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/Promise.jsm"); @@ -22,7 +27,7 @@ Cu.import("resource://gre/modules/osfile.jsm", this); Cu.import("resource://gre/modules/Task.jsm"); Cu.import("resource://gre/modules/FxAccountsOAuthClient.jsm"); -this.EXPORTED_SYMBOLS = ["MozLoopService"]; +this.EXPORTED_SYMBOLS = ["MozLoopService", "LOOP_SESSION_TYPE"]; XPCOMUtils.defineLazyModuleGetter(this, "console", "resource://gre/modules/devtools/Console.jsm"); @@ -202,6 +207,8 @@ let MozLoopServiceInternal = { /** * Performs a hawk based request to the loop server. * + * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request. + * This is one of the LOOP_SESSION_TYPE members. * @param {String} path The path to make the request to. * @param {String} method The request method, e.g. 'POST', 'GET'. * @param {Object} payloadObj An object which is converted to JSON and @@ -212,14 +219,14 @@ let MozLoopServiceInternal = { * as JSON and contains an 'error' property, the promise will be * rejected with this JSON-parsed response. */ - hawkRequest: function(path, method, payloadObj) { + hawkRequest: function(sessionType, path, method, payloadObj) { if (!gHawkClient) { gHawkClient = new HawkClient(this.loopServerUri); } let sessionToken; try { - sessionToken = Services.prefs.getCharPref("loop.hawk-session-token"); + sessionToken = Services.prefs.getCharPref(this.getSessionTokenPrefName(sessionType)); } catch (x) { // It is ok for this not to exist, we'll default to sending no-creds } @@ -237,19 +244,37 @@ let MozLoopServiceInternal = { }); }, + getSessionTokenPrefName: function(sessionType) { + let suffix; + switch (sessionType) { + case LOOP_SESSION_TYPE.GUEST: + suffix = ""; + break; + case LOOP_SESSION_TYPE.FXA: + suffix = ".fxa"; + break; + default: + throw new Error("Unknown LOOP_SESSION_TYPE"); + break; + } + return "loop.hawk-session-token" + suffix; + }, + /** * Used to store a session token from a request if it exists in the headers. * + * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request. + * One of the LOOP_SESSION_TYPE members. * @param {Object} headers The request headers, which may include a * "hawk-session-token" to be saved. * @return true on success or no token, false on failure. */ - storeSessionToken: function(headers) { + storeSessionToken: function(sessionType, headers) { let sessionToken = headers["hawk-session-token"]; if (sessionToken) { // XXX should do more validation here if (sessionToken.length === 64) { - Services.prefs.setCharPref("loop.hawk-session-token", sessionToken); + Services.prefs.setCharPref(this.getSessionTokenPrefName(sessionType), sessionToken); } else { // XXX Bubble the precise details up to the UI somehow (bug 1013248). console.warn("Loop server sent an invalid session token"); @@ -274,28 +299,39 @@ let MozLoopServiceInternal = { return; } - this.registerWithLoopServer(pushUrl); + this.registerWithLoopServer(LOOP_SESSION_TYPE.GUEST, pushUrl).then(() => { + // storeSessionToken could have rejected and nulled the promise if the token was malformed. + if (!gRegisteredDeferred) { + return; + } + gRegisteredDeferred.resolve(); + // No need to clear the promise here, everything was good, so we don't need + // to re-register. + }, (error) => { + Cu.reportError("Failed to register with Loop server: " + error.errno); + gRegisteredDeferred.reject(error.errno); + gRegisteredDeferred = null; + }); }, /** - * Registers with the Loop server. + * Registers with the Loop server either as a guest or a FxA user. * + * @param {LOOP_SESSION_TYPE} sessionType The type of session e.g. guest or FxA * @param {String} pushUrl The push url given by the push server. - * @param {Boolean} noRetry Optional, don't retry if authentication fails. + * @param {Boolean} [retry=true] Whether to retry if authentication fails. + * @return {Promise} */ - registerWithLoopServer: function(pushUrl, noRetry) { - this.hawkRequest("/registration", "POST", { simplePushURL: pushUrl}) + registerWithLoopServer: function(sessionType, pushUrl, retry = true) { + return this.hawkRequest(sessionType, "/registration", "POST", { simplePushURL: pushUrl}) .then((response) => { // If this failed we got an invalid token. storeSessionToken rejects // the gRegisteredDeferred promise for us, so here we just need to // early return. - if (!this.storeSessionToken(response.headers)) + if (!this.storeSessionToken(sessionType, response.headers)) return; this.clearError("registration"); - gRegisteredDeferred.resolve(); - // No need to clear the promise here, everything was good, so we don't need - // to re-register. }, (error) => { // There's other errors than invalid auth token, but we should only do the reset // as a last resort. @@ -307,16 +343,16 @@ let MozLoopServiceInternal = { } // Authorization failed, invalid token, we need to try again with a new token. - Services.prefs.clearUserPref("loop.hawk-session-token"); - this.registerWithLoopServer(pushUrl, true); - return; + Services.prefs.clearUserPref(this.getSessionTokenPrefName(sessionType)); + if (retry) { + return this.registerWithLoopServer(sessionType, pushUrl, false); + } } // XXX Bubble the precise details up to the UI somehow (bug 1013248). Cu.reportError("Failed to register with the loop server. error: " + error); this.setError("registration", error); - gRegisteredDeferred.reject(error.errno); - gRegisteredDeferred = null; + throw error; } ); }, @@ -509,7 +545,7 @@ let MozLoopServiceInternal = { * @return {Promise} resolved with the body of the hawk request for OAuth parameters. */ promiseFxAOAuthParameters: function() { - return this.hawkRequest("/fxa-oauth/params", "POST").then(response => { + return this.hawkRequest(LOOP_SESSION_TYPE.FXA, "/fxa-oauth/params", "POST").then(response => { return JSON.parse(response.body); }); }, @@ -587,7 +623,7 @@ let MozLoopServiceInternal = { code: code, state: state, }; - return this.hawkRequest("/fxa-oauth/token", "POST", payload).then(response => { + return this.hawkRequest(LOOP_SESSION_TYPE.FXA, "/fxa-oauth/token", "POST", payload).then(response => { return JSON.parse(response.body); }); }, @@ -921,6 +957,15 @@ this.MozLoopService = { }).then(tokenData => { gFxAOAuthTokenData = tokenData; return tokenData; + }).then(tokenData => { + return gRegisteredDeferred.promise.then(Task.async(function*() { + if (gPushHandler.pushUrl) { + yield MozLoopServiceInternal.registerWithLoopServer(LOOP_SESSION_TYPE.FXA, gPushHandler.pushUrl); + } else { + throw new Error("No pushUrl for FxA registration"); + } + return gFxAOAuthTokenData; + })); }, error => { gFxAOAuthTokenData = null; @@ -931,6 +976,8 @@ this.MozLoopService = { /** * Performs a hawk based request to the loop server. * + * @param {LOOP_SESSION_TYPE} sessionType The type of session to use for the request. + * One of the LOOP_SESSION_TYPE members. * @param {String} path The path to make the request to. * @param {String} method The request method, e.g. 'POST', 'GET'. * @param {Object} payloadObj An object which is converted to JSON and @@ -941,7 +988,7 @@ this.MozLoopService = { * as JSON and contains an 'error' property, the promise will be * rejected with this JSON-parsed response. */ - hawkRequest: function(path, method, payloadObj) { + hawkRequest: function(sessionType, path, method, payloadObj) { return MozLoopServiceInternal.hawkRequest(path, method, payloadObj); }, }; diff --git a/browser/components/loop/content/js/client.js b/browser/components/loop/content/js/client.js index 7f35ada2341b..161ccab7f720 100644 --- a/browser/components/loop/content/js/client.js +++ b/browser/components/loop/content/js/client.js @@ -104,7 +104,6 @@ loop.Client = (function($) { * -- callUrl: The url of the call * -- expiresAt: The amount of hours until expiry of the url * - * @param {String} simplepushUrl a registered Simple Push URL * @param {string} nickname the nickname of the future caller * @param {Function} cb Callback(err, callUrlData) */ diff --git a/browser/components/loop/test/mochitest/browser_fxa_login.js b/browser/components/loop/test/mochitest/browser_fxa_login.js index 1347220c671c..2e0b11336c14 100644 --- a/browser/components/loop/test/mochitest/browser_fxa_login.js +++ b/browser/components/loop/test/mochitest/browser_fxa_login.js @@ -7,8 +7,13 @@ "use strict"; -const gFxAOAuthTokenData = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}).gFxAOAuthTokenData; +const { + LOOP_SESSION_TYPE, + gFxAOAuthTokenData +} = Cu.import("resource:///modules/loop/MozLoopService.jsm", {}); + const BASE_URL = "http://mochi.test:8888/browser/browser/components/loop/test/mochitest/loop_fxa.sjs?"; +const HAWK_TOKEN_LENGTH = 64; add_task(function* setup() { Services.prefs.setCharPref("loop.server", BASE_URL); @@ -18,6 +23,8 @@ add_task(function* setup() { yield promiseDeletedOAuthParams(BASE_URL); Services.prefs.clearUserPref("loop.server"); Services.prefs.clearUserPref("services.push.serverURL"); + Services.prefs.clearUserPref(MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.GUEST)); + Services.prefs.clearUserPref(MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA)); }); }); @@ -161,10 +168,27 @@ add_task(function* basicAuthorizationAndRegistration() { }; yield promiseOAuthParamsSetup(BASE_URL, params); + info("registering"); + mockPushHandler.pushUrl = "https://localhost/pushUrl/guest"; + yield MozLoopService.register(mockPushHandler); + let prefName = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.GUEST); + let padding = new Array(HAWK_TOKEN_LENGTH - mockPushHandler.pushUrl.length).fill("X").join(""); + ise(Services.prefs.getCharPref(prefName), mockPushHandler.pushUrl + padding, "Check guest hawk token"); + + // Normally the same pushUrl would be registered but we change it in the test + // to be able to check for success on the second registration. + mockPushHandler.pushUrl = "https://localhost/pushUrl/fxa"; + let tokenData = yield MozLoopService.logInToFxA(); ise(tokenData.access_token, "code1_access_token", "Check access_token"); ise(tokenData.scope, "profile", "Check scope"); ise(tokenData.token_type, "bearer", "Check token_type"); + + let registrationResponse = yield promiseOAuthGetRegistration(BASE_URL); + ise(registrationResponse.response.simplePushURL, "https://localhost/pushUrl/fxa", "Check registered push URL"); + prefName = MozLoopServiceInternal.getSessionTokenPrefName(LOOP_SESSION_TYPE.FXA); + padding = new Array(HAWK_TOKEN_LENGTH - mockPushHandler.pushUrl.length).fill("X").join(""); + ise(Services.prefs.getCharPref(prefName), mockPushHandler.pushUrl + padding, "Check FxA hawk token"); }); add_task(function* loginWithParams401() { @@ -178,6 +202,7 @@ add_task(function* loginWithParams401() { test_error: "params_401", }; yield promiseOAuthParamsSetup(BASE_URL, params); + yield MozLoopService.register(mockPushHandler); let loginPromise = MozLoopService.logInToFxA(); yield loginPromise.then(tokenData => { diff --git a/browser/components/loop/test/mochitest/head.js b/browser/components/loop/test/mochitest/head.js index ce652ff04986..0fe60041a29c 100644 --- a/browser/components/loop/test/mochitest/head.js +++ b/browser/components/loop/test/mochitest/head.js @@ -112,3 +112,46 @@ function promiseDeletedOAuthParams(baseURL) { return deferred.promise; } + +/** + * Get the last registration on the test server. + */ +function promiseOAuthGetRegistration(baseURL) { + let deferred = Promise.defer(); + let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. + createInstance(Ci.nsIXMLHttpRequest); + xhr.open("GET", baseURL + "/get_registration", true); + xhr.responseType = "json"; + xhr.addEventListener("load", () => deferred.resolve(xhr)); + xhr.addEventListener("error", deferred.reject); + xhr.send(); + + return deferred.promise; +} + +/** + * This is used to fake push registration and notifications for + * MozLoopService tests. There is only one object created per test instance, as + * once registration has taken place, the object cannot currently be changed. + */ +let mockPushHandler = { + // This sets the registration result to be returned when initialize + // is called. By default, it is equivalent to success. + registrationResult: null, + pushUrl: undefined, + + /** + * MozLoopPushHandler API + */ + initialize: function(registerCallback, notificationCallback) { + registerCallback(this.registrationResult, this.pushUrl); + this._notificationCallback = notificationCallback; + }, + + /** + * Test-only API to simplify notifying a push notification result. + */ + notify: function(version) { + this._notificationCallback(version); + } +}; diff --git a/browser/components/loop/test/mochitest/loop_fxa.sjs b/browser/components/loop/test/mochitest/loop_fxa.sjs index 5293f36dceb6..0eb678ba110a 100644 --- a/browser/components/loop/test/mochitest/loop_fxa.sjs +++ b/browser/components/loop/test/mochitest/loop_fxa.sjs @@ -8,6 +8,7 @@ "use strict"; const REQUIRED_PARAMS = ["client_id", "content_uri", "oauth_uri", "profile_uri", "state"]; +const HAWK_TOKEN_LENGTH = 64; Components.utils.import("resource://gre/modules/NetUtil.jsm"); @@ -17,7 +18,7 @@ Components.utils.import("resource://gre/modules/NetUtil.jsm"); function handleRequest(request, response) { // Look at the query string but ignore past the encoded ? when deciding on the handler. switch (request.queryString.replace(/%3F.*/,"")) { - case "/setup_params": + case "/setup_params": // Test-only setup_params(request, response); return; case "/fxa-oauth/params": @@ -29,6 +30,12 @@ function handleRequest(request, response) { case "/fxa-oauth/token": token(request, response); return; + case "/registration": + registration(request, response); + return; + case "/get_registration": // Test-only + get_registration(request, response); + return; } response.setStatusLine(request.httpVersion, 404, "Not Found"); } @@ -47,6 +54,7 @@ function setup_params(request, response) { response.setHeader("Content-Type", "text/plain", false); if (request.method == "DELETE") { setSharedState("/fxa-oauth/params", ""); + setSharedState("/registration", ""); response.write("Params deleted"); return; } @@ -141,3 +149,30 @@ function token(request, response) { response.setHeader("Content-Type", "application/json; charset=utf-8", false); response.write(JSON.stringify(tokenData, null, 2)); } + +/** + * POST /registration + * + * Mock Loop registration endpoint which simply returns the simplePushURL with + * padding as the hawk session token. + */ +function registration(request, response) { + let body = NetUtil.readInputStreamToString(request.bodyInputStream, + request.bodyInputStream.available()); + let payload = JSON.parse(body); + setSharedState("/registration", body); + let pushURL = payload.simplePushURL; + // Pad the pushURL with "X" to the token length to simulate a token + let padding = new Array(HAWK_TOKEN_LENGTH - pushURL.length).fill("X").join(""); + response.setHeader("hawk-session-token", pushURL + padding, false); +} + +/** + * GET /get_registration + * + * Used for testing purposes to check if registration succeeded by returning the POST body. + */ +function get_registration(request, response) { + response.setHeader("Content-Type", "application/json; charset=utf-8", false); + response.write(getSharedState("/registration")); +} From 27d4c79cc8d61d0015413dd7c478de632d9a08fc Mon Sep 17 00:00:00 2001 From: Alan K Date: Mon, 8 Sep 2014 07:23:00 +0200 Subject: [PATCH 23/88] Bug 1045046 - Handle manual sync event and broadcast to registered cloudsync adapters. r=ttaubert --- browser/base/content/browser-syncui.js | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/browser/base/content/browser-syncui.js b/browser/base/content/browser-syncui.js index 8746c9cc411f..623947e8fd2d 100644 --- a/browser/base/content/browser-syncui.js +++ b/browser/base/content/browser-syncui.js @@ -2,6 +2,15 @@ # 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/. +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); + +#ifdef MOZ_SERVICES_CLOUDSYNC +XPCOMUtils.defineLazyModuleGetter(this, "CloudSync", + "resource://gre/modules/CloudSync.jsm"); +#else +let CloudSync = null; +#endif + // gSyncUI handles updating the tools menu and displaying notifications. let gSyncUI = { DEFAULT_EOL_URL: "https://www.mozilla.org/firefox/?utm_source=synceol", @@ -122,7 +131,9 @@ let gSyncUI = { document.getElementById("sync-setup-state").hidden = true; document.getElementById("sync-syncnow-state").hidden = true; - if (loginFailed) { + if (CloudSync && CloudSync.ready && CloudSync().adapters.count) { + document.getElementById("sync-syncnow-state").hidden = false; + } else if (loginFailed) { document.getElementById("sync-reauth-state").hidden = false; } else if (needsSetup) { document.getElementById("sync-setup-state").hidden = false; @@ -275,7 +286,14 @@ let gSyncUI = { // Commands doSync: function SUI_doSync() { - setTimeout(function() Weave.Service.errorHandler.syncAndReportErrors(), 0); + let needsSetup = this._needsSetup(); + let loginFailed = this._loginFailed(); + + if (!(loginFailed || needsSetup)) { + setTimeout(function () Weave.Service.errorHandler.syncAndReportErrors(), 0); + } + + Services.obs.notifyObservers(null, "cloudsync:user-sync", null); }, handleToolbarButton: function SUI_handleStatusbarButton() { From c66253d900ebfe66780b847db65792e4a97c2a4e Mon Sep 17 00:00:00 2001 From: Dave Townsend Date: Tue, 9 Sep 2014 13:58:26 -0700 Subject: [PATCH 24/88] Bug 1062522 - Fix intermittent failure in browser_bug575561.js. r=ttaubert --- .../content/test/general/browser_bug575561.js | 168 +++++++++--------- browser/base/content/test/general/head.js | 14 ++ 2 files changed, 98 insertions(+), 84 deletions(-) diff --git a/browser/base/content/test/general/browser_bug575561.js b/browser/base/content/test/general/browser_bug575561.js index f9cd13dd9f7b..554cc277b4a2 100644 --- a/browser/base/content/test/general/browser_bug575561.js +++ b/browser/base/content/test/general/browser_bug575561.js @@ -1,84 +1,84 @@ -function test() { - waitForExplicitFinish(); - - // Pinned: Link to the same domain should not open a new tab - // Tests link to http://example.com/browser/browser/base/content/test/general/dummy_page.html - testLink(0, true, false, function() { - // Pinned: Link to a different subdomain should open a new tab - // Tests link to http://test1.example.com/browser/browser/base/content/test/general/dummy_page.html - testLink(1, true, true, function() { - // Pinned: Link to a different domain should open a new tab - // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html - testLink(2, true, true, function() { - // Not Pinned: Link to a different domain should not open a new tab - // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html - testLink(2, false, false, function() { - // Pinned: Targetted link should open a new tab - // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html with target="foo" - testLink(3, true, true, function() { - // Pinned: Link in a subframe should not open a new tab - // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html in subframe - testLink(0, true, false, function() { - // Pinned: Link to the same domain (with www prefix) should not open a new tab - // Tests link to http://www.example.com/browser/browser/base/content/test/general/dummy_page.html - testLink(4, true, false, function() { - // Pinned: Link to a data: URI should not open a new tab - // Tests link to data:text/html,Another Page - testLink(5, true, false, function() { - // Pinned: Link to an about: URI should not open a new tab - // Tests link to about:mozilla - testLink(6, true, false, finish); - }); - }); - }, true); - }); - }); - }); - }); - }); -} - -function testLink(aLinkIndex, pinTab, expectNewTab, nextTest, testSubFrame) { - let appTab = gBrowser.addTab("http://example.com/browser/browser/base/content/test/general/app_bug575561.html", {skipAnimation: true}); - if (pinTab) - gBrowser.pinTab(appTab); - gBrowser.selectedTab = appTab; - - waitForDocLoadComplete(appTab.linkedBrowser).then(function() { - let browser = gBrowser.getBrowserForTab(appTab); - if (testSubFrame) - browser = browser.contentDocument.getElementsByTagName("iframe")[0]; - - let links = browser.contentDocument.getElementsByTagName("a"); - - if (expectNewTab) - gBrowser.tabContainer.addEventListener("TabOpen", onTabOpen, true); - else - waitForDocLoadComplete(appTab.linkedBrowser).then(onPageLoad); - - info("Clicking " + links[aLinkIndex].textContent); - EventUtils.sendMouseEvent({type:"click"}, links[aLinkIndex], browser.contentWindow); - let linkLocation = links[aLinkIndex].href; - - function onPageLoad() { - browser.removeEventListener("load", onPageLoad, true); - is(browser.contentDocument.location.href, linkLocation, "Link should not open in a new tab"); - executeSoon(function(){ - gBrowser.removeTab(appTab); - nextTest(); - }); - } - - function onTabOpen(event) { - gBrowser.tabContainer.removeEventListener("TabOpen", onTabOpen, true); - ok(true, "Link should open a new tab"); - waitForDocLoadComplete(event.target.linkedBrowser).then(function() { - executeSoon(function(){ - gBrowser.removeTab(appTab); - gBrowser.removeCurrentTab(); - nextTest(); - }); - }); - } - }); -} +const TEST_URL = "http://example.com/browser/browser/base/content/test/general/app_bug575561.html"; + +add_task(function*() { + SimpleTest.requestCompleteLog(); + + // Pinned: Link to the same domain should not open a new tab + // Tests link to http://example.com/browser/browser/base/content/test/general/dummy_page.html + yield testLink(0, true, false); + // Pinned: Link to a different subdomain should open a new tab + // Tests link to http://test1.example.com/browser/browser/base/content/test/general/dummy_page.html + yield testLink(1, true, true); + + // Pinned: Link to a different domain should open a new tab + // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html + yield testLink(2, true, true); + + // Not Pinned: Link to a different domain should not open a new tab + // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html + yield testLink(2, false, false); + + // Pinned: Targetted link should open a new tab + // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html with target="foo" + yield testLink(3, true, true); + + // Pinned: Link in a subframe should not open a new tab + // Tests link to http://example.org/browser/browser/base/content/test/general/dummy_page.html in subframe + yield testLink(0, true, false, true); + + // Pinned: Link to the same domain (with www prefix) should not open a new tab + // Tests link to http://www.example.com/browser/browser/base/content/test/general/dummy_page.html + yield testLink(4, true, false); + + // Pinned: Link to a data: URI should not open a new tab + // Tests link to data:text/html,Another Page + yield testLink(5, true, false); + + // Pinned: Link to an about: URI should not open a new tab + // Tests link to about:mozilla + yield testLink(6, true, false); +}); + +let waitForPageLoad = Task.async(function*(browser, linkLocation) { + yield waitForDocLoadComplete(); + + is(browser.contentDocument.location.href, linkLocation, "Link should not open in a new tab"); +}); + +let waitForTabOpen = Task.async(function*() { + let event = yield promiseWaitForEvent(gBrowser.tabContainer, "TabOpen", true); + ok(true, "Link should open a new tab"); + + yield waitForDocLoadComplete(event.target.linkedBrowser); + yield Promise.resolve(); + + gBrowser.removeCurrentTab(); +}); + +let testLink = Task.async(function*(aLinkIndex, pinTab, expectNewTab, testSubFrame) { + let appTab = gBrowser.addTab(TEST_URL, {skipAnimation: true}); + if (pinTab) + gBrowser.pinTab(appTab); + gBrowser.selectedTab = appTab; + + yield waitForDocLoadComplete(); + + let browser = appTab.linkedBrowser; + if (testSubFrame) + browser = browser.contentDocument.querySelector("iframe"); + + let link = browser.contentDocument.querySelectorAll("a")[aLinkIndex]; + + let promise; + if (expectNewTab) + promise = waitForTabOpen(); + else + promise = waitForPageLoad(browser, link.href); + + info("Clicking " + link.textContent); + link.click(); + + yield promise; + + gBrowser.removeTab(appTab); +}); diff --git a/browser/base/content/test/general/head.js b/browser/base/content/test/general/head.js index ecc38d19915c..3e45642f45f8 100644 --- a/browser/base/content/test/general/head.js +++ b/browser/base/content/test/general/head.js @@ -108,6 +108,19 @@ function promiseWaitForCondition(aConditionFn) { return deferred.promise; } +function promiseWaitForEvent(object, eventName, capturing = false) { + return new Promise((resolve) => { + function listener(event) { + info("Saw " + eventName); + object.removeEventListener(eventName, listener, capturing); + resolve(event); + } + + info("Waiting for " + eventName); + object.addEventListener(eventName, listener, capturing); + }); +} + function getTestPlugin(aName) { var pluginName = aName || "Test Plug-in"; var ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost); @@ -443,6 +456,7 @@ function waitForDocLoadComplete(aBrowser=gBrowser) { onStateChange: function (webProgress, req, flags, status) { let docStart = Ci.nsIWebProgressListener.STATE_IS_NETWORK | Ci.nsIWebProgressListener.STATE_STOP; + info("Saw state " + flags.toString(16)); if ((flags & docStart) == docStart) { aBrowser.removeProgressListener(progressListener); info("Browser loaded"); From 34ef942fc93861f47b91ca1d58b942c5a7e92479 Mon Sep 17 00:00:00 2001 From: Sudheesh Singanamalla Date: Mon, 8 Sep 2014 21:55:21 +0530 Subject: [PATCH 25/88] Bug 1056490 - Fixes addons.repository WARN Unknown type id when parsing addon: 5. r=Irving --- .../extensions/internal/AddonRepository.jsm | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/toolkit/mozapps/extensions/internal/AddonRepository.jsm b/toolkit/mozapps/extensions/internal/AddonRepository.jsm index 4c64bcd3eeac..f9dfcdbd6681 100644 --- a/toolkit/mozapps/extensions/internal/AddonRepository.jsm +++ b/toolkit/mozapps/extensions/internal/AddonRepository.jsm @@ -1072,6 +1072,8 @@ this.AddonRepository = { switch (localName) { case "type": // Map AMO's type id to corresponding string + // https://github.com/mozilla/olympia/blob/master/apps/constants/base.py#L127 + // These definitions need to be updated whenever AMO adds a new type. let id = parseInt(node.getAttribute("id")); switch (id) { case 1: @@ -1083,8 +1085,29 @@ this.AddonRepository = { case 3: addon.type = "dictionary"; break; + case 4: + addon.type = "search"; + break; + case 5: + addon.type = "langpack"; + break; + case 6: + addon.type = "langpack-addon"; + break; + case 7: + addon.type = "plugin"; + break; + case 8: + addon.type = "api"; + break; + case 9: + addon.type = "lightweight-theme"; + break; + case 11: + addon.type = "webapp"; + break; default: - logger.warn("Unknown type id when parsing addon: " + id); + logger.info("Unknown type id " + id + " found when parsing response for GUID " + guid); } break; case "authors": From 29d1ced1210413ed0b0da540ef87d89c84bda337 Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Wed, 10 Sep 2014 06:10:46 -0700 Subject: [PATCH 26/88] Bumping gaia.json for 2 gaia revision(s) a=gaia-bump ======== https://hg.mozilla.org/integration/gaia-central/rev/da68670d0336 Author: vingtetun <21@vingtetun.org> Desc: Merge pull request #23864 from vingtetun/tweak-cardsview-final Bug 1061324 - Display the app chrome in the card view. r=sfoster ======== https://hg.mozilla.org/integration/gaia-central/rev/9f4ddd9f4c09 Author: Vivien Nicolas Desc: Bug 1061324 - Rework cards view into cards strip with app chrome. r=sfoster --- b2g/config/gaia.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index 020ccc7f68b8..d58e3b523d27 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -4,6 +4,6 @@ "remote": "", "branch": "" }, - "revision": "f7dd70f6731aeae2ca2b6a9d97768611fb0fc596", + "revision": "da68670d03363937c366e1c3f1d532f3b18dd216", "repo_path": "/integration/gaia-central" } From e057b1b2eab10c867682ffa9f69c7b08c343a935 Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Wed, 10 Sep 2014 06:12:38 -0700 Subject: [PATCH 27/88] Bumping manifests a=b2g-bump --- b2g/config/dolphin/sources.xml | 2 +- b2g/config/emulator-ics/sources.xml | 2 +- b2g/config/emulator-jb/sources.xml | 2 +- b2g/config/emulator-kk/sources.xml | 2 +- b2g/config/emulator/sources.xml | 2 +- b2g/config/flame-kk/sources.xml | 2 +- b2g/config/flame/sources.xml | 2 +- b2g/config/hamachi/sources.xml | 2 +- b2g/config/helix/sources.xml | 2 +- b2g/config/nexus-4/sources.xml | 2 +- b2g/config/wasabi/sources.xml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/b2g/config/dolphin/sources.xml b/b2g/config/dolphin/sources.xml index 2d1163b33782..5c7bfe169465 100644 --- a/b2g/config/dolphin/sources.xml +++ b/b2g/config/dolphin/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator-ics/sources.xml b/b2g/config/emulator-ics/sources.xml index 3c1022a6025c..a4fd0244b53f 100644 --- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/emulator-jb/sources.xml b/b2g/config/emulator-jb/sources.xml index 48b2961943b8..7454b21c724d 100644 --- a/b2g/config/emulator-jb/sources.xml +++ b/b2g/config/emulator-jb/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/emulator-kk/sources.xml b/b2g/config/emulator-kk/sources.xml index 10094826eabc..43af3ee85c27 100644 --- a/b2g/config/emulator-kk/sources.xml +++ b/b2g/config/emulator-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator/sources.xml b/b2g/config/emulator/sources.xml index 3c1022a6025c..a4fd0244b53f 100644 --- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/flame-kk/sources.xml b/b2g/config/flame-kk/sources.xml index abab18ec3ae4..fae5dc8095af 100644 --- a/b2g/config/flame-kk/sources.xml +++ b/b2g/config/flame-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/flame/sources.xml b/b2g/config/flame/sources.xml index 45b8397d18b7..5d91a7841172 100644 --- a/b2g/config/flame/sources.xml +++ b/b2g/config/flame/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/hamachi/sources.xml b/b2g/config/hamachi/sources.xml index dcedecd9e809..b54097452e66 100644 --- a/b2g/config/hamachi/sources.xml +++ b/b2g/config/hamachi/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/helix/sources.xml b/b2g/config/helix/sources.xml index 8d130a276948..5ead676ce3c8 100644 --- a/b2g/config/helix/sources.xml +++ b/b2g/config/helix/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/nexus-4/sources.xml b/b2g/config/nexus-4/sources.xml index 9a89fa8576f6..8fcfdf97d82b 100644 --- a/b2g/config/nexus-4/sources.xml +++ b/b2g/config/nexus-4/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/wasabi/sources.xml b/b2g/config/wasabi/sources.xml index cc5453dd6ad9..3db4a322c7d0 100644 --- a/b2g/config/wasabi/sources.xml +++ b/b2g/config/wasabi/sources.xml @@ -17,7 +17,7 @@ - + From 382b349fba5deb07e4283ee929752cece3c9fbe9 Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Wed, 10 Sep 2014 06:40:47 -0700 Subject: [PATCH 28/88] Bumping gaia.json for 2 gaia revision(s) a=gaia-bump ======== https://hg.mozilla.org/integration/gaia-central/rev/c2c7db737a47 Author: Anthony Ricaud Desc: Merge pull request #23813 from Rik/refresh-call-info-1060723 Bug 1060723 - Call info page isn't updated after a call dispatched from ... r=drs ======== https://hg.mozilla.org/integration/gaia-central/rev/d96dc40c623f Author: Anthony Ricaud Desc: Bug 1060723 - Call info page isn't updated after a call dispatched from it ends --- b2g/config/gaia.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index d58e3b523d27..c8cba4ea0c84 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -4,6 +4,6 @@ "remote": "", "branch": "" }, - "revision": "da68670d03363937c366e1c3f1d532f3b18dd216", + "revision": "c2c7db737a47676e53e252e1677ebe4b84c25a06", "repo_path": "/integration/gaia-central" } From f02de30fd9c9acaa1371c8efc2f5636725ba63a9 Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Wed, 10 Sep 2014 06:47:06 -0700 Subject: [PATCH 29/88] Bumping manifests a=b2g-bump --- b2g/config/dolphin/sources.xml | 2 +- b2g/config/emulator-ics/sources.xml | 2 +- b2g/config/emulator-jb/sources.xml | 2 +- b2g/config/emulator-kk/sources.xml | 2 +- b2g/config/emulator/sources.xml | 2 +- b2g/config/flame-kk/sources.xml | 2 +- b2g/config/flame/sources.xml | 2 +- b2g/config/hamachi/sources.xml | 2 +- b2g/config/helix/sources.xml | 2 +- b2g/config/nexus-4/sources.xml | 2 +- b2g/config/wasabi/sources.xml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/b2g/config/dolphin/sources.xml b/b2g/config/dolphin/sources.xml index 5c7bfe169465..afd7b904f600 100644 --- a/b2g/config/dolphin/sources.xml +++ b/b2g/config/dolphin/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator-ics/sources.xml b/b2g/config/emulator-ics/sources.xml index a4fd0244b53f..38b8fd30a756 100644 --- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/emulator-jb/sources.xml b/b2g/config/emulator-jb/sources.xml index 7454b21c724d..70bfe8b0a5ae 100644 --- a/b2g/config/emulator-jb/sources.xml +++ b/b2g/config/emulator-jb/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/emulator-kk/sources.xml b/b2g/config/emulator-kk/sources.xml index 43af3ee85c27..abc9ff4bab8d 100644 --- a/b2g/config/emulator-kk/sources.xml +++ b/b2g/config/emulator-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator/sources.xml b/b2g/config/emulator/sources.xml index a4fd0244b53f..38b8fd30a756 100644 --- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/flame-kk/sources.xml b/b2g/config/flame-kk/sources.xml index fae5dc8095af..47897740a8dd 100644 --- a/b2g/config/flame-kk/sources.xml +++ b/b2g/config/flame-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/flame/sources.xml b/b2g/config/flame/sources.xml index 5d91a7841172..645e104ef933 100644 --- a/b2g/config/flame/sources.xml +++ b/b2g/config/flame/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/hamachi/sources.xml b/b2g/config/hamachi/sources.xml index b54097452e66..79b9b85a1796 100644 --- a/b2g/config/hamachi/sources.xml +++ b/b2g/config/hamachi/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/helix/sources.xml b/b2g/config/helix/sources.xml index 5ead676ce3c8..2df2489265fc 100644 --- a/b2g/config/helix/sources.xml +++ b/b2g/config/helix/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/nexus-4/sources.xml b/b2g/config/nexus-4/sources.xml index 8fcfdf97d82b..de44a8db35cd 100644 --- a/b2g/config/nexus-4/sources.xml +++ b/b2g/config/nexus-4/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/wasabi/sources.xml b/b2g/config/wasabi/sources.xml index 3db4a322c7d0..c66bbca777c5 100644 --- a/b2g/config/wasabi/sources.xml +++ b/b2g/config/wasabi/sources.xml @@ -17,7 +17,7 @@ - + From 293edf2bd18c49a8448ccc158d8efce05118d85b Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Wed, 10 Sep 2014 06:55:46 -0700 Subject: [PATCH 30/88] Bumping gaia.json for 2 gaia revision(s) a=gaia-bump MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ======== https://hg.mozilla.org/integration/gaia-central/rev/2328937a56f9 Author: Fernando Rodríguez Sela Desc: Merge pull request #23904 from frsela/STK/Bug1059434_Tests Bug 1059434 - STK display text command doesn't support time duration, r=vingtetun ======== https://hg.mozilla.org/integration/gaia-central/rev/64cf91123003 Author: Fernando Rodriguez Sela Desc: Bug 1059434 - STK display text command doesn't support time duration --- b2g/config/gaia.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index c8cba4ea0c84..2df623eee067 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -4,6 +4,6 @@ "remote": "", "branch": "" }, - "revision": "c2c7db737a47676e53e252e1677ebe4b84c25a06", + "revision": "2328937a56f98c717a5baddd59da5c57490d9d55", "repo_path": "/integration/gaia-central" } From 464b5e986f990bf752703da8878378226de06569 Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Wed, 10 Sep 2014 07:02:20 -0700 Subject: [PATCH 31/88] Bumping manifests a=b2g-bump --- b2g/config/dolphin/sources.xml | 2 +- b2g/config/emulator-ics/sources.xml | 2 +- b2g/config/emulator-jb/sources.xml | 4 ++-- b2g/config/emulator-kk/sources.xml | 2 +- b2g/config/emulator/sources.xml | 2 +- b2g/config/flame-kk/sources.xml | 2 +- b2g/config/flame/sources.xml | 2 +- b2g/config/hamachi/sources.xml | 2 +- b2g/config/helix/sources.xml | 2 +- b2g/config/nexus-4/sources.xml | 2 +- b2g/config/wasabi/sources.xml | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/b2g/config/dolphin/sources.xml b/b2g/config/dolphin/sources.xml index afd7b904f600..e3f998cce17a 100644 --- a/b2g/config/dolphin/sources.xml +++ b/b2g/config/dolphin/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator-ics/sources.xml b/b2g/config/emulator-ics/sources.xml index 38b8fd30a756..e4e1ed9bcb02 100644 --- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/emulator-jb/sources.xml b/b2g/config/emulator-jb/sources.xml index 70bfe8b0a5ae..c0c60115eb40 100644 --- a/b2g/config/emulator-jb/sources.xml +++ b/b2g/config/emulator-jb/sources.xml @@ -17,7 +17,7 @@ - + @@ -130,7 +130,7 @@ - + diff --git a/b2g/config/emulator-kk/sources.xml b/b2g/config/emulator-kk/sources.xml index abc9ff4bab8d..aff04635b2e0 100644 --- a/b2g/config/emulator-kk/sources.xml +++ b/b2g/config/emulator-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator/sources.xml b/b2g/config/emulator/sources.xml index 38b8fd30a756..e4e1ed9bcb02 100644 --- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/flame-kk/sources.xml b/b2g/config/flame-kk/sources.xml index 47897740a8dd..9ad9b27ea012 100644 --- a/b2g/config/flame-kk/sources.xml +++ b/b2g/config/flame-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/flame/sources.xml b/b2g/config/flame/sources.xml index 645e104ef933..700eb2262255 100644 --- a/b2g/config/flame/sources.xml +++ b/b2g/config/flame/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/hamachi/sources.xml b/b2g/config/hamachi/sources.xml index 79b9b85a1796..6d77ab5d80b1 100644 --- a/b2g/config/hamachi/sources.xml +++ b/b2g/config/hamachi/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/helix/sources.xml b/b2g/config/helix/sources.xml index 2df2489265fc..d9b119df5c68 100644 --- a/b2g/config/helix/sources.xml +++ b/b2g/config/helix/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/nexus-4/sources.xml b/b2g/config/nexus-4/sources.xml index de44a8db35cd..a7a5caadd5a9 100644 --- a/b2g/config/nexus-4/sources.xml +++ b/b2g/config/nexus-4/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/wasabi/sources.xml b/b2g/config/wasabi/sources.xml index c66bbca777c5..6c4c937a3e7a 100644 --- a/b2g/config/wasabi/sources.xml +++ b/b2g/config/wasabi/sources.xml @@ -17,7 +17,7 @@ - + From c956dd00eda949c4d3928554325f291da357abc6 Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Wed, 10 Sep 2014 07:07:59 -0700 Subject: [PATCH 32/88] Bumping manifests a=b2g-bump --- b2g/config/dolphin/sources.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b2g/config/dolphin/sources.xml b/b2g/config/dolphin/sources.xml index e3f998cce17a..4c4c4474e333 100644 --- a/b2g/config/dolphin/sources.xml +++ b/b2g/config/dolphin/sources.xml @@ -38,7 +38,7 @@ - + From 878cfbf069b9486b2f3bc2ba14c1d452206c2d4f Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Wed, 10 Sep 2014 07:10:47 -0700 Subject: [PATCH 33/88] Bumping gaia.json for 2 gaia revision(s) a=gaia-bump MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ======== https://hg.mozilla.org/integration/gaia-central/rev/16366dfa0ad8 Author: Fernando Jiménez Moreno Desc: Merge pull request #23855 from empoalp/bug_1064807 Bug 1064807 - In multicall the sound alert for new SMS does not beep. r=etienne ======== https://hg.mozilla.org/integration/gaia-central/rev/5bbb4b007fe4 Author: David Garcia Desc: Bug 1064807 - In multicall the sound alert for new SMS does not beep r=etienne --- b2g/config/gaia.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/b2g/config/gaia.json b/b2g/config/gaia.json index 2df623eee067..2754fd598d83 100644 --- a/b2g/config/gaia.json +++ b/b2g/config/gaia.json @@ -4,6 +4,6 @@ "remote": "", "branch": "" }, - "revision": "2328937a56f98c717a5baddd59da5c57490d9d55", + "revision": "16366dfa0ad88916f68bd891cc6c12e5bb9c6c51", "repo_path": "/integration/gaia-central" } From b85fd6d42d3d943ed9bafc59eda606b787daeec7 Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Wed, 10 Sep 2014 07:17:15 -0700 Subject: [PATCH 34/88] Bumping manifests a=b2g-bump --- b2g/config/dolphin/sources.xml | 2 +- b2g/config/emulator-ics/sources.xml | 2 +- b2g/config/emulator-jb/sources.xml | 2 +- b2g/config/emulator-kk/sources.xml | 2 +- b2g/config/emulator/sources.xml | 2 +- b2g/config/flame-kk/sources.xml | 2 +- b2g/config/flame/sources.xml | 2 +- b2g/config/hamachi/sources.xml | 2 +- b2g/config/helix/sources.xml | 2 +- b2g/config/nexus-4/sources.xml | 2 +- b2g/config/wasabi/sources.xml | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/b2g/config/dolphin/sources.xml b/b2g/config/dolphin/sources.xml index 4c4c4474e333..28830bc6ce45 100644 --- a/b2g/config/dolphin/sources.xml +++ b/b2g/config/dolphin/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator-ics/sources.xml b/b2g/config/emulator-ics/sources.xml index e4e1ed9bcb02..62c5b4c9309e 100644 --- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/emulator-jb/sources.xml b/b2g/config/emulator-jb/sources.xml index c0c60115eb40..a5f884289b07 100644 --- a/b2g/config/emulator-jb/sources.xml +++ b/b2g/config/emulator-jb/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/emulator-kk/sources.xml b/b2g/config/emulator-kk/sources.xml index aff04635b2e0..2b7bc47cd846 100644 --- a/b2g/config/emulator-kk/sources.xml +++ b/b2g/config/emulator-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator/sources.xml b/b2g/config/emulator/sources.xml index e4e1ed9bcb02..62c5b4c9309e 100644 --- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -19,7 +19,7 @@ - + diff --git a/b2g/config/flame-kk/sources.xml b/b2g/config/flame-kk/sources.xml index 9ad9b27ea012..b8954f6eace1 100644 --- a/b2g/config/flame-kk/sources.xml +++ b/b2g/config/flame-kk/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/flame/sources.xml b/b2g/config/flame/sources.xml index 700eb2262255..ab475799de60 100644 --- a/b2g/config/flame/sources.xml +++ b/b2g/config/flame/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/hamachi/sources.xml b/b2g/config/hamachi/sources.xml index 6d77ab5d80b1..b02d0c2d9138 100644 --- a/b2g/config/hamachi/sources.xml +++ b/b2g/config/hamachi/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/helix/sources.xml b/b2g/config/helix/sources.xml index d9b119df5c68..f5cddc0fcc0f 100644 --- a/b2g/config/helix/sources.xml +++ b/b2g/config/helix/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/nexus-4/sources.xml b/b2g/config/nexus-4/sources.xml index a7a5caadd5a9..dab0f33e78fc 100644 --- a/b2g/config/nexus-4/sources.xml +++ b/b2g/config/nexus-4/sources.xml @@ -17,7 +17,7 @@ - + diff --git a/b2g/config/wasabi/sources.xml b/b2g/config/wasabi/sources.xml index 6c4c937a3e7a..5fd6efd744c4 100644 --- a/b2g/config/wasabi/sources.xml +++ b/b2g/config/wasabi/sources.xml @@ -17,7 +17,7 @@ - + From 9780f77d43599976aeb5f94e49b2a9618ad99d8b Mon Sep 17 00:00:00 2001 From: Edgar Chen Date: Wed, 10 Sep 2014 11:28:21 +0800 Subject: [PATCH 35/88] Bug 1065205 - Read IccId after we process appType and fallback to SIM configuration if appType is still unknown. r=vyang --- dom/system/gonk/ril_worker.js | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/dom/system/gonk/ril_worker.js b/dom/system/gonk/ril_worker.js index 43734a7685f8..a0f5910656e3 100644 --- a/dom/system/gonk/ril_worker.js +++ b/dom/system/gonk/ril_worker.js @@ -3470,14 +3470,6 @@ RilObject.prototype = { return; } - let ICCRecordHelper = this.context.ICCRecordHelper; - // Try to get iccId only when cardState left GECKO_CARDSTATE_UNDETECTED. - if (iccStatus.cardState === CARD_STATE_PRESENT && - (this.cardState === GECKO_CARDSTATE_UNINITIALIZED || - this.cardState === GECKO_CARDSTATE_UNDETECTED)) { - ICCRecordHelper.readICCID(); - } - if (RILQUIRKS_SUBSCRIPTION_CONTROL) { // All appIndex is -1 means the subscription is not activated yet. // Note that we don't support "ims" for now, so we don't take it into @@ -3538,6 +3530,14 @@ RilObject.prototype = { newCardState = GECKO_CARDSTATE_UNKNOWN; } + let ICCRecordHelper = this.context.ICCRecordHelper; + // Try to get iccId only when cardState left GECKO_CARDSTATE_UNDETECTED. + if (iccStatus.cardState === CARD_STATE_PRESENT && + (this.cardState === GECKO_CARDSTATE_UNINITIALIZED || + this.cardState === GECKO_CARDSTATE_UNDETECTED)) { + ICCRecordHelper.readICCID(); + } + if (this.cardState == newCardState) { return; } @@ -12705,6 +12705,7 @@ ICCIOHelperObject.prototype = { case CARD_APPTYPE_ISIM: // For SIM, this is what we want case CARD_APPTYPE_SIM: + default: options.p2 = 0x00; options.p3 = GET_RESPONSE_EF_SIZE_BYTES; break; From aa05e56e1896bf7dc5ed41518a55964c169c226c Mon Sep 17 00:00:00 2001 From: B2G Bumper Bot Date: Wed, 10 Sep 2014 07:32:42 -0700 Subject: [PATCH 36/88] Bumping manifests a=b2g-bump --- b2g/config/emulator-ics/sources.xml | 2 +- b2g/config/emulator/sources.xml | 2 +- b2g/config/hamachi/sources.xml | 2 +- b2g/config/helix/sources.xml | 2 +- b2g/config/wasabi/sources.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/b2g/config/emulator-ics/sources.xml b/b2g/config/emulator-ics/sources.xml index 62c5b4c9309e..67b4244cc5ee 100644 --- a/b2g/config/emulator-ics/sources.xml +++ b/b2g/config/emulator-ics/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/emulator/sources.xml b/b2g/config/emulator/sources.xml index 62c5b4c9309e..67b4244cc5ee 100644 --- a/b2g/config/emulator/sources.xml +++ b/b2g/config/emulator/sources.xml @@ -15,7 +15,7 @@ - + diff --git a/b2g/config/hamachi/sources.xml b/b2g/config/hamachi/sources.xml index b02d0c2d9138..096fcf33405f 100644 --- a/b2g/config/hamachi/sources.xml +++ b/b2g/config/hamachi/sources.xml @@ -13,7 +13,7 @@ - + diff --git a/b2g/config/helix/sources.xml b/b2g/config/helix/sources.xml index f5cddc0fcc0f..f690608421a8 100644 --- a/b2g/config/helix/sources.xml +++ b/b2g/config/helix/sources.xml @@ -11,7 +11,7 @@ - + diff --git a/b2g/config/wasabi/sources.xml b/b2g/config/wasabi/sources.xml index 5fd6efd744c4..e19a6232b392 100644 --- a/b2g/config/wasabi/sources.xml +++ b/b2g/config/wasabi/sources.xml @@ -13,7 +13,7 @@ - + From 808e8f9b50ba2375534330e0a3da46dc1ba597da Mon Sep 17 00:00:00 2001 From: Garner Lee Date: Mon, 8 Sep 2014 15:15:00 -0400 Subject: [PATCH 37/88] Bug 1062672 - B2G NFC: Typo for 'ESE' in HCI Event origin. r=allstars.chh --- dom/nfc/gonk/NfcService.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dom/nfc/gonk/NfcService.cpp b/dom/nfc/gonk/NfcService.cpp index 4bb84b17c0b7..1a16a4bf66b6 100644 --- a/dom/nfc/gonk/NfcService.cpp +++ b/dom/nfc/gonk/NfcService.cpp @@ -25,7 +25,7 @@ using namespace mozilla::ipc; static const nsLiteralString SEOriginString[] = { NS_LITERAL_STRING("SIM"), - NS_LITERAL_STRING("ESE"), + NS_LITERAL_STRING("eSE"), NS_LITERAL_STRING("ASSD") }; From 3397687a7ee8aacfb289bf3d9b0f39fa64209531 Mon Sep 17 00:00:00 2001 From: Krzysztof Mioduszewski Date: Tue, 9 Sep 2014 05:56:00 -0400 Subject: [PATCH 38/88] Bug 1063525 - Remove getEventType method from nsNfc.js. r=allstars.chh --- dom/nfc/nsNfc.js | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/dom/nfc/nsNfc.js b/dom/nfc/nsNfc.js index 907c11caf83c..d49435f0c496 100644 --- a/dom/nfc/nsNfc.js +++ b/dom/nfc/nsNfc.js @@ -22,8 +22,6 @@ XPCOMUtils.defineLazyServiceGetter(this, "appsService", "@mozilla.org/AppsService;1", "nsIAppsService"); -const NFC_PEER_EVENT_READY = 0x01; -const NFC_PEER_EVENT_LOST = 0x02; /** * NFCTag @@ -226,9 +224,8 @@ mozNfc.prototype = { this.__DOM_IMPL__.setEventHandler("onpeerlost", handler); }, - eventListenerWasAdded: function(evt) { - let eventType = this.getEventType(evt); - if (eventType != NFC_PEER_EVENT_READY) { + eventListenerWasAdded: function(eventType) { + if (eventType !== "peerready") { return; } @@ -236,9 +233,8 @@ mozNfc.prototype = { this._nfcContentHelper.registerTargetForPeerReady(this._window, appId); }, - eventListenerWasRemoved: function(evt) { - let eventType = this.getEventType(evt); - if (eventType != NFC_PEER_EVENT_READY) { + eventListenerWasRemoved: function(eventType) { + if (eventType !== "peerready") { return; } @@ -285,21 +281,6 @@ mozNfc.prototype = { this.__DOM_IMPL__.dispatchEvent(event); }, - getEventType: function getEventType(evt) { - let eventType = -1; - switch (evt) { - case 'peerready': - eventType = NFC_PEER_EVENT_READY; - break; - case 'peerlost': - eventType = NFC_PEER_EVENT_LOST; - break; - default: - break; - } - return eventType; - }, - hasDeadWrapper: function hasDeadWrapper() { return Cu.isDeadWrapper(this._window) || Cu.isDeadWrapper(this.__DOM_IMPL__); }, From 738b9fb2e4ef5c10d27e388399ff04aea9973a72 Mon Sep 17 00:00:00 2001 From: Sam Penrose Date: Mon, 8 Sep 2014 16:45:03 -0700 Subject: [PATCH 39/88] Bug 1064613 - Move server-side account deletion block in _handleGetAssertionError(). r=ferjm --- services/fxaccounts/FxAccountsManager.jsm | 27 +++++++++++------------ 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/services/fxaccounts/FxAccountsManager.jsm b/services/fxaccounts/FxAccountsManager.jsm index 44e5ea9b1f8b..686e73babf35 100644 --- a/services/fxaccounts/FxAccountsManager.jsm +++ b/services/fxaccounts/FxAccountsManager.jsm @@ -196,20 +196,19 @@ this.FxAccountsManager = { } ); } - } - ); - - // Otherwise, the account was deleted, so ask for Sign In/Up - return this._localSignOut().then( - () => { - return this._uiRequest(UI_REQUEST_SIGN_IN_FLOW, aAudience, - aPrincipal); - }, - (reason) => { - // reject primary problem, not signout failure - log.error("Signing out in response to server error threw: " + - reason); - return this._error(reason); + // ... otherwise, the account was deleted, so ask for Sign In/Up + return this._localSignOut().then( + () => { + return this._uiRequest(UI_REQUEST_SIGN_IN_FLOW, aAudience, + aPrincipal); + }, + (reason) => { + // reject primary problem, not signout failure + log.error("Signing out in response to server error threw: " + + reason); + return this._error(reason); + } + ); } ); } From 522bc839cc31224c44e92a4e6bf1febacfc09c71 Mon Sep 17 00:00:00 2001 From: Shawn Ku Date: Tue, 9 Sep 2014 22:25:14 +0800 Subject: [PATCH 40/88] Bug 1064647 - B2G RIL: Treat 0x90, 0x91, 0x9E and 0x9F as a success results for sw1 regarding ICC_IO. r=Edgar --- dom/system/gonk/ril_worker.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/dom/system/gonk/ril_worker.js b/dom/system/gonk/ril_worker.js index a0f5910656e3..b61771a21a43 100644 --- a/dom/system/gonk/ril_worker.js +++ b/dom/system/gonk/ril_worker.js @@ -5854,7 +5854,11 @@ RilObject.prototype[REQUEST_SIM_IO] = function REQUEST_SIM_IO(length, options) { let Buf = this.context.Buf; options.sw1 = Buf.readInt32(); options.sw2 = Buf.readInt32(); - if (options.sw1 != ICC_STATUS_NORMAL_ENDING) { + // See 3GPP TS 11.11, clause 9.4.1 for opetation success results. + if (options.sw1 !== ICC_STATUS_NORMAL_ENDING && + options.sw1 !== ICC_STATUS_NORMAL_ENDING_WITH_EXTRA && + options.sw1 !== ICC_STATUS_WITH_SIM_DATA && + options.sw1 !== ICC_STATUS_WITH_RESPONSE_DATA) { ICCIOHelper.processICCIOError(options); return; } From 9c941c186dc83e61e69ea1474ec73bf6be71be95 Mon Sep 17 00:00:00 2001 From: Matthew Noorenberghe Date: Wed, 10 Sep 2014 02:33:41 -0700 Subject: [PATCH 41/88] Bug 1065052 - Follow-up - Correctly pass the sessionType to MozLoopServiceInternal.hawkRequest. rs=mikedeboer DONTBUILD --- browser/components/loop/MozLoopService.jsm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/browser/components/loop/MozLoopService.jsm b/browser/components/loop/MozLoopService.jsm index e234cb7cadbf..4479b21a9565 100644 --- a/browser/components/loop/MozLoopService.jsm +++ b/browser/components/loop/MozLoopService.jsm @@ -989,7 +989,7 @@ this.MozLoopService = { * rejected with this JSON-parsed response. */ hawkRequest: function(sessionType, path, method, payloadObj) { - return MozLoopServiceInternal.hawkRequest(path, method, payloadObj); + return MozLoopServiceInternal.hawkRequest(sessionType, path, method, payloadObj); }, }; Object.freeze(this.MozLoopService); From 31d186652b30a34f24e2df592ce5d9657bf373d5 Mon Sep 17 00:00:00 2001 From: Margaret Leibovic Date: Wed, 10 Sep 2014 12:06:53 +0200 Subject: [PATCH 42/88] Bug 1063518 - Hide MLS "Learn More" link when MLS is disabled. r=liuche --- mobile/android/base/preferences/GeckoPreferences.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mobile/android/base/preferences/GeckoPreferences.java b/mobile/android/base/preferences/GeckoPreferences.java index 96d2eade53e1..330e56f47ebe 100644 --- a/mobile/android/base/preferences/GeckoPreferences.java +++ b/mobile/android/base/preferences/GeckoPreferences.java @@ -674,7 +674,9 @@ OnSharedPreferenceChangeListener preferences.removePreference(pref); i--; continue; - } else if (AppConstants.RELEASE_BUILD && PREFS_GEO_REPORTING.equals(key)) { + } else if (AppConstants.RELEASE_BUILD && + (PREFS_GEO_REPORTING.equals(key) || + PREFS_GEO_LEARN_MORE.equals(key))) { // We don't build wifi/cell tower collection in release builds, so hide the UI. preferences.removePreference(pref); i--; From 28b1c3e0cb8715b6de83cb867bafd563e3495458 Mon Sep 17 00:00:00 2001 From: Margaret Leibovic Date: Wed, 10 Sep 2014 12:06:55 +0200 Subject: [PATCH 43/88] Bug 1038797 - Add network error page. r=bnicholson --- .../mozilla/search/PostSearchFragment.java | 62 +++++++++++++++--- .../org/mozilla/search/PreSearchFragment.java | 7 ++ .../res/drawable-hdpi/network_error.png | Bin 0 -> 5333 bytes .../res/drawable-mdpi/network_error.png | Bin 0 -> 4446 bytes .../res/drawable-xhdpi/network_error.png | Bin 0 -> 6439 bytes .../res/drawable-xxhdpi/network_error.png | Bin 0 -> 7438 bytes .../search/res/layout/search_empty.xml | 5 +- .../layout/search_fragment_post_search.xml | 9 ++- .../search/res/values/search_colors.xml | 2 + .../android/search/strings/search_strings.dtd | 3 + .../search/strings/search_strings.xml.in | 3 + 11 files changed, 79 insertions(+), 12 deletions(-) create mode 100644 mobile/android/search/res/drawable-hdpi/network_error.png create mode 100644 mobile/android/search/res/drawable-mdpi/network_error.png create mode 100644 mobile/android/search/res/drawable-xhdpi/network_error.png create mode 100644 mobile/android/search/res/drawable-xxhdpi/network_error.png diff --git a/mobile/android/search/java/org/mozilla/search/PostSearchFragment.java b/mobile/android/search/java/org/mozilla/search/PostSearchFragment.java index 37676f2639b2..5b4945303e2f 100644 --- a/mobile/android/search/java/org/mozilla/search/PostSearchFragment.java +++ b/mobile/android/search/java/org/mozilla/search/PostSearchFragment.java @@ -9,15 +9,20 @@ import android.content.Intent; import android.graphics.Bitmap; import android.net.Uri; import android.os.Bundle; +import android.provider.Settings; import android.support.v4.app.Fragment; import android.text.TextUtils; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.ViewStub; import android.webkit.WebChromeClient; import android.webkit.WebView; import android.webkit.WebViewClient; +import android.widget.ImageView; import android.widget.ProgressBar; +import android.widget.TextView; import org.mozilla.gecko.AppConstants; import org.mozilla.gecko.Telemetry; @@ -33,6 +38,7 @@ public class PostSearchFragment extends Fragment { private SearchEngineManager searchEngineManager; private WebView webview; + private View errorView; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @@ -43,7 +49,8 @@ public class PostSearchFragment extends Fragment { webview = (WebView) mainView.findViewById(R.id.webview); webview.setWebChromeClient(new ChromeClient()); - webview.setWebViewClient(new LinkInterceptingClient()); + webview.setWebViewClient(new ResultsWebViewClient()); + // This is required for our greasemonkey terror script. webview.getSettings().setJavaScriptEnabled(true); @@ -77,11 +84,10 @@ public class PostSearchFragment extends Fragment { @Override public void execute(SearchEngine engine) { final String url = engine.resultsUriForQuery(query); - // Only load urls if the url is different than the webview's current url. - if (!TextUtils.equals(webview.getUrl(), url)) { - webview.loadUrl(Constants.ABOUT_BLANK); - webview.loadUrl(url); - } + + // Load about:blank to avoid flashing old results. + webview.loadUrl(Constants.ABOUT_BLANK); + webview.loadUrl(url); } }); } @@ -90,12 +96,18 @@ public class PostSearchFragment extends Fragment { /** * A custom WebViewClient that intercepts every page load. This allows * us to decide whether to load the url here, or send it to Android - * as an intent. + * as an intent. It also handles network errors. */ - private class LinkInterceptingClient extends WebViewClient { + private class ResultsWebViewClient extends WebViewClient { + + // Whether or not there is a network error. + private boolean networkError; @Override public void onPageStarted(WebView view, final String url, Bitmap favicon) { + // Reset the error state. + networkError = false; + searchEngineManager.getEngine(new SearchEngineManager.SearchEngineCallback() { @Override public void execute(SearchEngine engine) { @@ -119,6 +131,40 @@ public class PostSearchFragment extends Fragment { } }); } + + @Override + public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { + Log.e(LOG_TAG, "Error loading search results: " + description); + + networkError = true; + + if (errorView == null) { + final ViewStub errorViewStub = (ViewStub) getView().findViewById(R.id.error_view_stub); + errorView = errorViewStub.inflate(); + + ((ImageView) errorView.findViewById(R.id.empty_image)).setImageResource(R.drawable.network_error); + ((TextView) errorView.findViewById(R.id.empty_title)).setText(R.string.network_error_title); + + final TextView message = (TextView) errorView.findViewById(R.id.empty_message); + message.setText(R.string.network_error_message); + message.setTextColor(getResources().getColor(R.color.network_error_link)); + message.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + startActivity(new Intent(Settings.ACTION_SETTINGS)); + } + }); + } + } + + @Override + public void onPageFinished(WebView view, String url) { + // Make sure the error view is hidden if the network error was fixed. + if (errorView != null) { + errorView.setVisibility(networkError ? View.VISIBLE : View.GONE); + webview.setVisibility(networkError ? View.GONE : View.VISIBLE); + } + } } /** diff --git a/mobile/android/search/java/org/mozilla/search/PreSearchFragment.java b/mobile/android/search/java/org/mozilla/search/PreSearchFragment.java index 36ca82f0c4c2..443f32b7c170 100644 --- a/mobile/android/search/java/org/mozilla/search/PreSearchFragment.java +++ b/mobile/android/search/java/org/mozilla/search/PreSearchFragment.java @@ -20,7 +20,9 @@ import android.view.View; import android.view.ViewGroup; import android.view.ViewStub; import android.widget.AdapterView; +import android.widget.ImageView; import android.widget.ListView; +import android.widget.TextView; import org.mozilla.gecko.Telemetry; import org.mozilla.gecko.TelemetryContract; @@ -135,6 +137,11 @@ public class PreSearchFragment extends Fragment { if (emptyView == null) { final ViewStub emptyViewStub = (ViewStub) getView().findViewById(R.id.empty_view_stub); emptyView = emptyViewStub.inflate(); + + ((ImageView) emptyView.findViewById(R.id.empty_image)).setImageResource(R.drawable.search_fox); + ((TextView) emptyView.findViewById(R.id.empty_title)).setText(R.string.search_empty_title); + ((TextView) emptyView.findViewById(R.id.empty_message)).setText(R.string.search_empty_message); + listView.setEmptyView(emptyView); } } diff --git a/mobile/android/search/res/drawable-hdpi/network_error.png b/mobile/android/search/res/drawable-hdpi/network_error.png new file mode 100644 index 0000000000000000000000000000000000000000..4407781b52670a5429f5bcb9090e240fc15c00ca GIT binary patch literal 5333 zcmV;`6e{b9P)X+uL$Nkc;* zP;zf(X>4Tx07wm;mUmQB*%pV-y*Itk5+Wca^cs2zAksTX6$DXM^`x7XQc?|s+0 z08spb1j2M!0f022SQPH-!CVp(%f$Br7!UytSOLJ{W@ZFO_(THK{JlMynW#v{v-a*T zfMmPdEWc1DbJqWVks>!kBnAKqMb$PuekK>?0+ds;#ThdH1j_W4DKdsJG8Ul;qO2n0 z#IJ1jr{*iW$(WZWsE0n`c;fQ!l&-AnmjxZO1uWyz`0VP>&nP`#itsL#`S=Q!g`M=rU9)45( zJ;-|dRq-b5&z?byo>|{)?5r=n76A4nTALlSzLiw~v~31J<>9PP?;rs31pu_(obw)r zY+jPY;tVGXi|p)da{-@gE-UCa`=5eu%D;v=_nFJ?`&K)q7e9d`Nfk3?MdhZarb|T3 z%nS~f&t(1g5dY)AIcd$w!z`Siz!&j_=v7hZlnI21XuE|xfmo0(WD10T)!}~_HYW!e zew}L+XmwuzeT6wtxJd`dZ#@7*BLgIEKY9Xv>st^p3dp{^Xswa2bB{85{^$B13tWnB z;Y>jyQ|9&zk7RNsqAVGs--K+z0uqo1bf5|}fi5rtEMN^BfHQCd-XH*kfJhJnmIE$G z0%<@5vOzxB0181d*a3EfYH$G5fqKvcPJ%XY23!PJzzuK<41h;K3WmW;Fah3yX$XSw z5EY_9s*o0>51B&N5F1(uc|$=^I1~fLLy3?Ol0f;;Ca4%HgQ}rJP(Ab`bQ-z{U4#0d z2hboi2K@njgb|nm(_szR0JebHusa+GN5aeCM0gdP2N%HG;Yzp`J`T6S7vUT504#-H z!jlL<$Or?`Mpy_N@kBz9SR?@vA#0H$qyni$nvf2p8@Y{0k#Xb$28W?xm>3qu8RLgp zjNxKdVb)?wFx8l2m{v>|<~C*!GlBVnrDD~wrdTJeKXwT=5u1%I#8zOBU|X=4u>;s) z>^mF|$G{ol9B_WP7+f-LHLe7=57&&lfa}8z;U@8Tyei%l?}87(bMRt(A-)QK9Dg3) zj~~XrCy)tR1Z#p1A(kK{Y$Q|=8VKhI{e%(1G*N-5Pjn)N5P8I0VkxnX*g?EW941ba z6iJ387g8iCnY4jaNopcpCOsy-A(P2EWJhusSwLP-t|XrzUnLKcKTwn?CKOLf97RIe zPB}`sKzTrUL#0v;sBY9)s+hW+T2H-1eM)^VN0T#`^Oxhvt&^*fYnAJldnHel*Ozyf zUoM{~Um<@={-*r60#U(0!Bc^wuvVc);k3d%g-J!4qLpHZVwz%!VuRu}#Ze`^l7W)9 z5>Kf>>9Eozr6C$Z)1`URxU@~QI@)F0FdauXr2Es8>BaOP=)Lp_WhG@>R;lZ?BJkMlIuMhw8ApiF&yDYW2hFJ?fJhni{?u z85&g@mo&yT8JcdI$(rSw=QPK(Xj%)k1X|@<=e1rim6`6$RAwc!i#egKuI;BS(LSWz zt39n_sIypSqfWEV6J3%nTQ@-4i zi$R;gsG*9XzhRzXqv2yCs*$VFDx+GXJH|L;wsDH_KI2;^u!)^Xl1YupO;gy^-c(?^ z&$Q1BYvyPsG^;hc$D**@Sy`+`)}T4VJji^bd7Jqw3q6Zii=7tT7GEswEK@D(EFW1Z zSp`^awCb?>!`j4}Yh7b~$A)U-W3$et-R8BesV(1jzwLcHnq9En7Q0Tn&-M=XBKs!$ zF$X<|c!#|X_tWYh)GZit z(Q)Cp9CDE^WG;+fcyOWARoj*0TI>4EP1lX*cEoMO-Pk?Z{kZ!p4@(b`M~lalr<3Oz z&kJ6Nm#vN_+kA5{dW4@^Vjg_`q%qU1ULk& z3Fr!>1V#i_2R;ij2@(Z$1jE4r!MlPVFVbHmT+|iPIq0wy5aS{>yK?9ZAjVh%SOwMWgFjair&;wpi!{CU}&@N=Eg#~ zLQ&zpEzVmGY{hI9Z0+4-0xS$$Xe-OToc?Y*V;rTcf_ zb_jRe-RZjXSeas3UfIyD;9afd%<`i0x4T#DzE)vdabOQ=k7SRuGN`h>O0Q~1)u-yD z>VX=Mn&!Rgd$;YK+Q-}1zu#?t(*cbG#Ronf6db&N$oEidtwC+YVcg-Y!_VuY>bk#Y ze_ww@?MU&F&qswvrN_dLb=5o6*Egs)ls3YRlE$&)amR1{;Ppd$6RYV^Go!iq1UMl% z@#4q$AMc(FJlT1QeX8jv{h#)>&{~RGq1N2iiMFIRX?sk2-|2wUogK~{EkB$8eDsX= znVPf8XG_nK&J~=SIiGia@9y}|z3FhX{g&gcj=lwb=lWgyFW&aLedUh- zof`v-2Kw$UzI*>(+&$@i-u=-BsSjR1%z8NeX#HdC`Hh-Z(6xI-`hmHDqv!v)W&&nrf>M(RhcN6(D;jNN*%^u_SYjF;2ng}*8Ow)d6M ztDk;%`@Lsk$;9w$(d(H%O5UixIr`T2ZRcd@Yg-!v68` z@soP==<(Cy#fv}I9b!&SPW;fJL+^r5QbR+-8*A6DZ4zD{U4~t?-@JMA1}7vWY(~Fy z(XqX~ed&@VONyQ93-C!jd-klN-x#Csu9lXTF(oA>e|Ia;0PF%RY@5g9>1zfF;7R~) zzPT>gy8+lnXs!*Q+jaEl(d-@pmB)1vz={I_lL1jzv~c0V<#q-)bLPxsEcF9u0|Arn zmmyMVR3PBX^{3)1hBNUv=0Dzw2Dq-bSu!Bo133zP~WQt3>XCf_ncMx z{Q2{}efsqIkhUDFcBzB+Tg#R$+t95*Td`urzm1KJ*D}6cs(}OCLrm3`=5_%rE-vmv z+D4h{&F=!(-o1PCsB4ciH*|;h9Xoa$%F4>BWL>%*5YAAHY1oJnBl>OKx^>g+*|Sp; z6B9Qz!Azb5c&6}7K_{3j;wp=;0Ks6hTc+ogq54BJ z19}hZ*?rJjf*cYw<7hDlp$lQs7p9@)MzXyla^XY((^7V80W8$CG_#$~hUQ_K-b>{% zzy(DBPkB6k%<~8wVCERhccKgfN=r-su%m+jhOZ_rhpi1DkSB&>xgl7(0|+}46McuZ zYeQE8Oa_Hcwg3=UgLUlFV1NP2%jB9suzY3uP6LNmDf{8%R}f-|?nl_qyxuP5NF)^f zja)@ufmi+zGRE;7j1`@K+0&_IXR?A8M=$e!zhnsAU}lCNt{|-3^@%1 z^Uc;U{bVz{5#C#a@SMt2EvLN`tIp|ZnDVPAH6aKC$4w|CyODg&EdaBP@-~TnI%Z_E zgPr3OI;;k;Ik4>5CFT4H$eSqJI!<+fwqU`6A*@}Mbjq};TTn8TIs(CFp6dasDv}_X z6=9ahE?Lco&ob8sysu(s6u|tF07sHY$2EQprUJaz0d}4T(-RY232~ySUF#L5Khkma zp+ko%%F4f|v*j?pfB`}im96A=*)x)s3jnL-=^x4f--tZ34a&vRz1`Zr3=-v=O zq$*ZpS&R5G+f@Xyst35sx%`l*hVS%H3Sje`>LX4);qQ%LGTP|7n_c}nmYw zMCB_5i&Ff9broUeuQ^pBn)Hn2Y~)QM{|sPqv!s;(E&<^37;`eYT&Axu>I6*+ETn^& zvtm8zQZ^?#tz&^N6r|1dC*r#wXaWG{J(6`nJm~}jkOKfV1M7O{Lb;bkc^;%J_XGd(zL)np zo^?ET*4Nj6Q(j(vnD?kAOVC6E)`dy?_U)^3ODT4w)3D(24ERRaUj+!Nn<6Iw$i>Td zeAv(7yMZ<5E6N>Sz7r-+X_cTkWn@Kf98ab&pvizV0-DbAn-#qab$=miJ^3YO$4(dc zS+a--XmYs%Ncku~m1j%>mdLI__NwjMx4&IoUEN|idd*5#L1PLIWC}ixE_vigTa~RL z1D-@w9>en9qJt|_*A>u=Q20|Q_W)nlx9i#(R1}GJ^ZT+n1P{IlS7z9tVO`^~u2m?~ zKU`%Z-Jg#tmi97nyJGp>hps$r(2Pa?MFiiaD?En%WW%@{-}5sa9khM`gLPeMgpx<= z2<-xWWOK^nM=-7Y2xf~@eY6&99N*QfT^BkP)|KedKaTta{%E#Sy+U5$DHz^~dGA1I zhN9hWwRroVlfQu99&#N&%-hEe(m{Ty)8zGf6VlVuQ*d5WSvSu`UM3$b3;MC-3&>$5 z4p#h+9Vic%FQ306yfQP8+n4S0$C9&Q@IEr98H3dVK#Ys131 z>M8wW4jS%3PcEt-2B0GJNeMP60F=9cf=Gt4gUR=`#S}Ed zy4C~ORX&AmJP$)NgCo$JHfSS45;M>M%*%A#0$_3gca*R;2_{a&wRz7`N0*4TLPKuI zCTj754qzgie)6$j#|Kj#mE2HK$s;D9aqPMhWAQt7DK%6yfSVVm^%+HTM5|*08Y3z) ztR<|$!DFI?h{9r`(K+g%$!>|BH)-f<*@bm+f@MPlD|I8c+A3vcW{MBy+66M8U7@Xv zX2+;o8hU1E>ZX+uL$Nkc;* zP;zf(X>4Tx07wm;mUmQB*%pV-y*Itk5+Wca^cs2zAksTX6$DXM^`x7XQc?|s+0 z08spb1j2M!0f022SQPH-!CVp(%f$Br7!UytSOLJ{W@ZFO_(THK{JlMynW#v{v-a*T zfMmPdEWc1DbJqWVks>!kBnAKqMb$PuekK>?0+ds;#ThdH1j_W4DKdsJG8Ul;qO2n0 z#IJ1jr{*iW$(WZWsE0n`c;fQ!l&-AnmjxZO1uWyz`0VP>&nP`#itsL#`S=Q!g`M=rU9)45( zJ;-|dRq-b5&z?byo>|{)?5r=n76A4nTALlSzLiw~v~31J<>9PP?;rs31pu_(obw)r zY+jPY;tVGXi|p)da{-@gE-UCa`=5eu%D;v=_nFJ?`&K)q7e9d`Nfk3?MdhZarb|T3 z%nS~f&t(1g5dY)AIcd$w!z`Siz!&j_=v7hZlnI21XuE|xfmo0(WD10T)!}~_HYW!e zew}L+XmwuzeT6wtxJd`dZ#@7*BLgIEKY9Xv>st^p3dp{^Xswa2bB{85{^$B13tWnB z;Y>jyQ|9&zk7RNsqAVGs--K+z0uqo1bf5|}fi5rtEMN^BfHQCd-XH*kfJhJnmIE$G z0%<@5vOzxB0181d*a3EfYH$G5fqKvcPJ%XY23!PJzzuK<41h;K3WmW;Fah3yX$XSw z5EY_9s*o0>51B&N5F1(uc|$=^I1~fLLy3?Ol0f;;Ca4%HgQ}rJP(Ab`bQ-z{U4#0d z2hboi2K@njgb|nm(_szR0JebHusa+GN5aeCM0gdP2N%HG;Yzp`J`T6S7vUT504#-H z!jlL<$Or?`Mpy_N@kBz9SR?@vA#0H$qyni$nvf2p8@Y{0k#Xb$28W?xm>3qu8RLgp zjNxKdVb)?wFx8l2m{v>|<~C*!GlBVnrDD~wrdTJeKXwT=5u1%I#8zOBU|X=4u>;s) z>^mF|$G{ol9B_WP7+f-LHLe7=57&&lfa}8z;U@8Tyei%l?}87(bMRt(A-)QK9Dg3) zj~~XrCy)tR1Z#p1A(kK{Y$Q|=8VKhI{e%(1G*N-5Pjn)N5P8I0VkxnX*g?EW941ba z6iJ387g8iCnY4jaNopcpCOsy-A(P2EWJhusSwLP-t|XrzUnLKcKTwn?CKOLf97RIe zPB}`sKzTrUL#0v;sBY9)s+hW+T2H-1eM)^VN0T#`^Oxhvt&^*fYnAJldnHel*Ozyf zUoM{~Um<@={-*r60#U(0!Bc^wuvVc);k3d%g-J!4qLpHZVwz%!VuRu}#Ze`^l7W)9 z5>Kf>>9Eozr6C$Z)1`URxU@~QI@)F0FdauXr2Es8>BaOP=)Lp_WhG@>R;lZ?BJkMlIuMhw8ApiF&yDYW2hFJ?fJhni{?u z85&g@mo&yT8JcdI$(rSw=QPK(Xj%)k1X|@<=e1rim6`6$RAwc!i#egKuI;BS(LSWz zt39n_sIypSqfWEV6J3%nTQ@-4i zi$R;gsG*9XzhRzXqv2yCs*$VFDx+GXJH|L;wsDH_KI2;^u!)^Xl1YupO;gy^-c(?^ z&$Q1BYvyPsG^;hc$D**@Sy`+`)}T4VJji^bd7Jqw3q6Zii=7tT7GEswEK@D(EFW1Z zSp`^awCb?>!`j4}Yh7b~$A)U-W3$et-R8BesV(1jzwLcHnq9En7Q0Tn&-M=XBKs!$ zF$X<|c!#|X_tWYh)GZit z(Q)Cp9CDE^WG;+fcyOWARoj*0TI>4EP1lX*cEoMO-Pk?Z{kZ!p4@(b`M~lalr<3Oz z&kJ6Nm#vN_+kA5{dW4@^Vjg_`q%qU1ULk& z3Fr!>1V#i_2R;ij2@(Z$1jE4r!MlPVFVbHmT+|iPIq0wy5aS{>yK?9ZAjVh%SOwMWgFjair&;wpi!{CU}&@N=Eg#~ zLQ&zpEzVmGY{hI9Z0+4-0xS$$Xe-OToc?Y*V;rTcf_ zb_jRe-RZjXSeas3UfIyD;9afd%<`i0x4T#DzE)vdabOQ=k7SRuGN`h>O0Q~1)u-yD z>VX=Mn&!Rgd$;YK+Q-}1zu#?t(*cbG#Ronf6db&N$oEidtwC+YVcg-Y!_VuY>bk#Y ze_ww@?MU&F&qswvrN_dLb=5o6*Egs)ls3YRlE$&)amR1{;Ppd$6RYV^Go!iq1UMl% z@#4q$AMc(FJlT1QeX8jv{h#)>&{~RGq1N2iiMFIRX?sk2-|2wUogK~{EkB$8eDsX= znVPf8XG_nK&J~=SIiGia@9y}|z3FhX{g&gcj=lwb=lWgyFW&aLedUh- zof`v-2Kw$UzI*>(+&$@i-u=-BsSjR1%z8NeX#HdC`Hh-Z(6xI-`hmHDqv!v)W&&nrf>M(RhcN6(D;jNN*%^u_SYjF;2ng}*8Ow)d6M ztDk;%`@Lsk$;9w$(d(H%O5UixIr`T2ZRcd@dwnSD%FRUF589$pe2aFQb{ z4F|p0A5PcI!GIQ8_J_$zCux>uE=WW+-kHmirnY!f7BPx4DK`6q)%v5%cguoiZEZE9 z6~&g74WXh)P+t4|9=Xr%bMJGXdoNs+vz_Oh^Lzi^bI$LadmmTGG%96fWxL15#I12%sgbR%ttX89Bfk+K z&G2VY7ez-$$GeS$uLC_mDGGj?~r=_JGA*i)f57k=$l){pm3I7;Co@RUih98iTk&(6R zu@7D2I+JP&3JT_ig@ws7S-?}foi>rDJeSska|ul9F!d7j|7fc#D=S~6)s^Dn;)U2a ziQY{b<}t<(QvHB1UQ<4b@e&2+HydjLRJmvAw@=7_*bZFn32N@-7=WXn>Wvhi;G*&Q+opCbgE@t#hKC) z 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 BSm6Kw literal 0 HcmV?d00001 diff --git a/mobile/android/search/res/drawable-xxhdpi/network_error.png b/mobile/android/search/res/drawable-xxhdpi/network_error.png new file mode 100644 index 0000000000000000000000000000000000000000..0f1e2480e3aeb9067a8e497987f9845ba365fa36 GIT binary patch literal 7438 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 M07*qoM6N<$g3q8(AOHXW literal 0 HcmV?d00001 diff --git a/mobile/android/search/res/layout/search_empty.xml b/mobile/android/search/res/layout/search_empty.xml index 425a199b10ab..9177a9dde9f7 100644 --- a/mobile/android/search/res/layout/search_empty.xml +++ b/mobile/android/search/res/layout/search_empty.xml @@ -22,7 +22,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="15dp" - android:src="@drawable/search_fox" android:gravity="top|center" android:scaleType="fitCenter"/> @@ -33,17 +32,17 @@ android:layout_weight="3"> diff --git a/mobile/android/search/res/layout/search_fragment_post_search.xml b/mobile/android/search/res/layout/search_fragment_post_search.xml index 72f1ab08bac3..4bbfd36b3a6f 100644 --- a/mobile/android/search/res/layout/search_fragment_post_search.xml +++ b/mobile/android/search/res/layout/search_fragment_post_search.xml @@ -19,5 +19,12 @@ android:id="@+id/webview" android:layout_width="match_parent" android:layout_height="match_parent"/> - + + + diff --git a/mobile/android/search/res/values/search_colors.xml b/mobile/android/search/res/values/search_colors.xml index 6b8139a1333b..bc0093f5e639 100644 --- a/mobile/android/search/res/values/search_colors.xml +++ b/mobile/android/search/res/values/search_colors.xml @@ -25,4 +25,6 @@ #ADB0B1 #383E42 + + #0092DB diff --git a/mobile/android/search/strings/search_strings.dtd b/mobile/android/search/strings/search_strings.dtd index 85e0bb3224b9..02ac3bbdc87a 100644 --- a/mobile/android/search/strings/search_strings.dtd +++ b/mobile/android/search/strings/search_strings.dtd @@ -26,3 +26,6 @@ + + + diff --git a/mobile/android/search/strings/search_strings.xml.in b/mobile/android/search/strings/search_strings.xml.in index e4b40dd19912..437f573628e8 100644 --- a/mobile/android/search/strings/search_strings.xml.in +++ b/mobile/android/search/strings/search_strings.xml.in @@ -19,3 +19,6 @@ &search_widget_button_label; &default_engine_identifier; + + &network_error_title; + &network_error_message; From 681d2c0c8883385d86da028822384dd1db9647c0 Mon Sep 17 00:00:00 2001 From: Marco Bonardo Date: Wed, 10 Sep 2014 12:46:14 +0200 Subject: [PATCH 44/88] Bug 1047811 - Part 1 - Allow to commit a main-thread Storage transaction asynchronously. r=asuth --- storage/public/mozStorageHelper.h | 168 ++++++++++-------- storage/src/mozStorageConnection.cpp | 1 + storage/src/mozStorageConnection.h | 8 + storage/test/storage_test_harness.h | 167 +++++++++++++++++- storage/test/test_transaction_helper.cpp | 127 +++++++------- storage/test/test_true_async.cpp | 169 ------------------- storage/test/unit/test_storage_connection.js | 2 + 7 files changed, 335 insertions(+), 307 deletions(-) diff --git a/storage/public/mozStorageHelper.h b/storage/public/mozStorageHelper.h index a4ecda8f7c5b..5104baca87d8 100644 --- a/storage/public/mozStorageHelper.h +++ b/storage/public/mozStorageHelper.h @@ -7,10 +7,13 @@ #define MOZSTORAGEHELPER_H #include "nsAutoPtr.h" +#include "nsStringGlue.h" +#include "mozilla/DebugOnly.h" #include "mozIStorageAsyncConnection.h" #include "mozIStorageConnection.h" #include "mozIStorageStatement.h" +#include "mozIStoragePendingStatement.h" #include "nsError.h" /** @@ -18,59 +21,113 @@ * the transaction will be completed even if you have an exception or * return early. * - * aCommitOnComplete controls whether the transaction is committed or rolled - * back when it goes out of scope. A common use is to create an instance with - * commitOnComplete = FALSE (rollback), then call Commit on this object manually - * when your function completes successfully. + * A common use is to create an instance with aCommitOnComplete = false (rollback), + * then call Commit() on this object manually when your function completes + * successfully. * - * Note that nested transactions are not supported by sqlite, so if a transaction - * is already in progress, this object does nothing. Note that in this case, - * you may not get the transaction type you ask for, and you won't be able + * @note nested transactions are not supported by Sqlite, so if a transaction + * is already in progress, this object does nothing. Note that in this case, + * you may not get the transaction type you asked for, and you won't be able * to rollback. * - * Note: This class is templatized to be also usable with internal data - * structures. External users of this class should generally use - * |mozStorageTransaction| instead. + * @param aConnection + * The connection to create the transaction on. + * @param aCommitOnComplete + * Controls whether the transaction is committed or rolled back when + * this object goes out of scope. + * @param aType [optional] + * The transaction type, as defined in mozIStorageConnection. Defaults + * to TRANSACTION_DEFERRED. + * @param aAsyncCommit [optional] + * Whether commit should be executed asynchronously on the helper thread. + * This is a special option introduced as an interim solution to reduce + * main-thread fsyncs in Places. Can only be used on main-thread. + * + * WARNING: YOU SHOULD _NOT_ WRITE NEW MAIN-THREAD CODE USING THIS! + * + * Notice that async commit might cause synchronous statements to fail + * with SQLITE_BUSY. A possible mitigation strategy is to use + * PRAGMA busy_timeout, but notice that might cause main-thread jank. + * Finally, if the database is using WAL journaling mode, other + * connections won't see the changes done in async committed transactions + * until commit is complete. + * + * For all of the above reasons, this should only be used as an interim + * solution and avoided completely if possible. */ -template -class mozStorageTransactionBase +class mozStorageTransaction { public: - mozStorageTransactionBase(T* aConnection, - bool aCommitOnComplete, - int32_t aType = mozIStorageConnection::TRANSACTION_DEFERRED) + mozStorageTransaction(mozIStorageConnection* aConnection, + bool aCommitOnComplete, + int32_t aType = mozIStorageConnection::TRANSACTION_DEFERRED, + bool aAsyncCommit = false) : mConnection(aConnection), mHasTransaction(false), mCommitOnComplete(aCommitOnComplete), - mCompleted(false) + mCompleted(false), + mAsyncCommit(aAsyncCommit) { - // We won't try to get a transaction if one is already in progress. - if (mConnection) - mHasTransaction = NS_SUCCEEDED(mConnection->BeginTransactionAs(aType)); + if (mConnection) { + nsAutoCString query("BEGIN"); + switch(aType) { + case mozIStorageConnection::TRANSACTION_IMMEDIATE: + query.AppendLiteral(" IMMEDIATE"); + break; + case mozIStorageConnection::TRANSACTION_EXCLUSIVE: + query.AppendLiteral(" EXCLUSIVE"); + break; + case mozIStorageConnection::TRANSACTION_DEFERRED: + query.AppendLiteral(" DEFERRED"); + break; + default: + MOZ_ASSERT(false, "Unknown transaction type"); + } + // If a transaction is already in progress, this will fail, since Sqlite + // doesn't support nested transactions. + mHasTransaction = NS_SUCCEEDED(mConnection->ExecuteSimpleSQL(query)); + } } - ~mozStorageTransactionBase() + + ~mozStorageTransaction() { - if (mConnection && mHasTransaction && ! mCompleted) { - if (mCommitOnComplete) - mConnection->CommitTransaction(); - else - mConnection->RollbackTransaction(); + if (mConnection && mHasTransaction && !mCompleted) { + if (mCommitOnComplete) { + mozilla::DebugOnly rv = Commit(); + NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), + "A transaction didn't commit correctly"); + } + else { + mozilla::DebugOnly rv = Rollback(); + NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), + "A transaction didn't rollback correctly"); + } } } /** * Commits the transaction if one is in progress. If one is not in progress, * this is a NOP since the actual owner of the transaction outside of our - * scope is in charge of finally comitting or rolling back the transaction. + * scope is in charge of finally committing or rolling back the transaction. */ nsresult Commit() { - if (!mConnection || mCompleted) - return NS_OK; // no connection, or already done + if (!mConnection || mCompleted || !mHasTransaction) + return NS_OK; mCompleted = true; - if (! mHasTransaction) - return NS_OK; // transaction not ours, ignore - nsresult rv = mConnection->CommitTransaction(); + + // TODO (bug 559659): this might fail with SQLITE_BUSY, but we don't handle + // it, thus the transaction might stay open until the next COMMIT. + nsresult rv; + if (mAsyncCommit) { + nsCOMPtr ps; + rv = mConnection->ExecuteSimpleSQLAsync(NS_LITERAL_CSTRING("COMMIT"), + nullptr, getter_AddRefs(ps)); + } + else { + rv = mConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING("COMMIT")); + } + if (NS_SUCCEEDED(rv)) mHasTransaction = false; @@ -78,22 +135,21 @@ public: } /** - * Rolls back the transaction in progress. You should only call this function - * if this object has a real transaction (HasTransaction() = true) because - * otherwise, there is no transaction to roll back. + * Rolls back the transaction if one is in progress. If one is not in progress, + * this is a NOP since the actual owner of the transaction outside of our + * scope is in charge of finally rolling back the transaction. */ nsresult Rollback() { - if (!mConnection || mCompleted) - return NS_OK; // no connection, or already done + if (!mConnection || mCompleted || !mHasTransaction) + return NS_OK; mCompleted = true; - if (! mHasTransaction) - return NS_ERROR_FAILURE; - // It is possible that a rollback will return busy, so we busy wait... + // TODO (bug 1062823): from Sqlite 3.7.11 on, rollback won't ever return + // a busy error, so this handling can be removed. nsresult rv = NS_OK; do { - rv = mConnection->RollbackTransaction(); + rv = mConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING("ROLLBACK")); if (rv == NS_ERROR_STORAGE_BUSY) (void)PR_Sleep(PR_INTERVAL_NO_WAIT); } while (rv == NS_ERROR_STORAGE_BUSY); @@ -104,42 +160,14 @@ public: return rv; } - /** - * Returns whether this object wraps a real transaction. False means that - * this object doesn't do anything because there was already a transaction in - * progress when it was created. - */ - bool HasTransaction() - { - return mHasTransaction; - } - - /** - * This sets the default action (commit or rollback) when this object goes - * out of scope. - */ - void SetDefaultAction(bool aCommitOnComplete) - { - mCommitOnComplete = aCommitOnComplete; - } - protected: - U mConnection; + nsCOMPtr mConnection; bool mHasTransaction; bool mCommitOnComplete; bool mCompleted; + bool mAsyncCommit; }; -/** - * An instance of the mozStorageTransaction<> family dedicated - * to |mozIStorageConnection|. - */ -typedef mozStorageTransactionBase > -mozStorageTransaction; - - - /** * This class wraps a statement so that it is guaraneed to be reset when * this object goes out of scope. diff --git a/storage/src/mozStorageConnection.cpp b/storage/src/mozStorageConnection.cpp index 854c5b7770db..20459632e971 100644 --- a/storage/src/mozStorageConnection.cpp +++ b/storage/src/mozStorageConnection.cpp @@ -1218,6 +1218,7 @@ Connection::initializeClone(Connection* aClone, bool aReadOnly) "journal_size_limit", "synchronous", "wal_autocheckpoint", + "busy_timeout" }; for (uint32_t i = 0; i < ArrayLength(pragmas); ++i) { // Read-only connections just need cache_size and temp_store pragmas. diff --git a/storage/src/mozStorageConnection.h b/storage/src/mozStorageConnection.h index 4d9b782dbae6..f1f13d57f9bf 100644 --- a/storage/src/mozStorageConnection.h +++ b/storage/src/mozStorageConnection.h @@ -120,6 +120,14 @@ public: ::sqlite3_commit_hook(mDBConn, aCallbackFn, aData); }; + /** + * Gets autocommit status. + */ + bool getAutocommit() { + MOZ_ASSERT(mDBConn, "A connection must exist at this point"); + return static_cast(::sqlite3_get_autocommit(mDBConn)); + }; + /** * Lazily creates and returns a background execution thread. In the future, * the thread may be re-claimed if left idle, so you should call this diff --git a/storage/test/storage_test_harness.h b/storage/test/storage_test_harness.h index f2c767003957..1e70b4a74797 100644 --- a/storage/test/storage_test_harness.h +++ b/storage/test/storage_test_harness.h @@ -5,9 +5,13 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "TestHarness.h" + #include "nsMemory.h" +#include "prthread.h" #include "nsThreadUtils.h" #include "nsDirectoryServiceDefs.h" +#include "mozilla/ReentrantMonitor.h" + #include "mozIStorageService.h" #include "mozIStorageConnection.h" #include "mozIStorageStatementCallback.h" @@ -18,7 +22,10 @@ #include "mozIStorageStatement.h" #include "mozIStoragePendingStatement.h" #include "mozIStorageError.h" -#include "nsThreadUtils.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsIEventTarget.h" + +#include "sqlite3.h" static int gTotalTests = 0; static int gPassedTests = 0; @@ -54,6 +61,8 @@ static int gPassedTests = 0; #else #include +#define do_check_ok(aInvoc) do_check_true((aInvoc) == SQLITE_OK) + // Print nsresult as uint32_t std::ostream& operator<<(std::ostream& aStream, const nsresult aInput) { @@ -224,3 +233,159 @@ blocking_async_close(mozIStorageConnection *db) db->AsyncClose(spinner); spinner->SpinUntilCompleted(); } + +//////////////////////////////////////////////////////////////////////////////// +//// Mutex Watching + +/** + * Verify that mozIStorageAsyncStatement's life-cycle never triggers a mutex on + * the caller (generally main) thread. We do this by decorating the sqlite + * mutex logic with our own code that checks what thread it is being invoked on + * and sets a flag if it is invoked on the main thread. We are able to easily + * decorate the SQLite mutex logic because SQLite allows us to retrieve the + * current function pointers being used and then provide a new set. + */ + +sqlite3_mutex_methods orig_mutex_methods; +sqlite3_mutex_methods wrapped_mutex_methods; + +bool mutex_used_on_watched_thread = false; +PRThread *watched_thread = nullptr; +/** + * Ugly hack to let us figure out what a connection's async thread is. If we + * were MOZILLA_INTERNAL_API and linked as such we could just include + * mozStorageConnection.h and just ask Connection directly. But that turns out + * poorly. + * + * When the thread a mutex is invoked on isn't watched_thread we save it to this + * variable. + */ +PRThread *last_non_watched_thread = nullptr; + +/** + * Set a flag if the mutex is used on the thread we are watching, but always + * call the real mutex function. + */ +extern "C" void wrapped_MutexEnter(sqlite3_mutex *mutex) +{ + PRThread *curThread = ::PR_GetCurrentThread(); + if (curThread == watched_thread) + mutex_used_on_watched_thread = true; + else + last_non_watched_thread = curThread; + orig_mutex_methods.xMutexEnter(mutex); +} + +extern "C" int wrapped_MutexTry(sqlite3_mutex *mutex) +{ + if (::PR_GetCurrentThread() == watched_thread) + mutex_used_on_watched_thread = true; + return orig_mutex_methods.xMutexTry(mutex); +} + +void hook_sqlite_mutex() +{ + // We need to initialize and teardown SQLite to get it to set up the + // default mutex handlers for us so we can steal them and wrap them. + do_check_ok(sqlite3_initialize()); + do_check_ok(sqlite3_shutdown()); + do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &orig_mutex_methods)); + do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &wrapped_mutex_methods)); + wrapped_mutex_methods.xMutexEnter = wrapped_MutexEnter; + wrapped_mutex_methods.xMutexTry = wrapped_MutexTry; + do_check_ok(::sqlite3_config(SQLITE_CONFIG_MUTEX, &wrapped_mutex_methods)); +} + +/** + * Call to clear the watch state and to set the watching against this thread. + * + * Check |mutex_used_on_watched_thread| to see if the mutex has fired since + * this method was last called. Since we're talking about the current thread, + * there are no race issues to be concerned about + */ +void watch_for_mutex_use_on_this_thread() +{ + watched_thread = ::PR_GetCurrentThread(); + mutex_used_on_watched_thread = false; +} + + +//////////////////////////////////////////////////////////////////////////////// +//// Thread Wedgers + +/** + * A runnable that blocks until code on another thread invokes its unwedge + * method. By dispatching this to a thread you can ensure that no subsequent + * runnables dispatched to the thread will execute until you invoke unwedge. + * + * The wedger is self-dispatching, just construct it with its target. + */ +class ThreadWedger : public nsRunnable +{ +public: + explicit ThreadWedger(nsIEventTarget *aTarget) + : mReentrantMonitor("thread wedger") + , unwedged(false) + { + aTarget->Dispatch(this, aTarget->NS_DISPATCH_NORMAL); + } + + NS_IMETHOD Run() + { + mozilla::ReentrantMonitorAutoEnter automon(mReentrantMonitor); + + if (!unwedged) + automon.Wait(); + + return NS_OK; + } + + void unwedge() + { + mozilla::ReentrantMonitorAutoEnter automon(mReentrantMonitor); + unwedged = true; + automon.Notify(); + } + +private: + mozilla::ReentrantMonitor mReentrantMonitor; + bool unwedged; +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Async Helpers + +/** + * A horrible hack to figure out what the connection's async thread is. By + * creating a statement and async dispatching we can tell from the mutex who + * is the async thread, PRThread style. Then we map that to an nsIThread. + */ +already_AddRefed +get_conn_async_thread(mozIStorageConnection *db) +{ + // Make sure we are tracking the current thread as the watched thread + watch_for_mutex_use_on_this_thread(); + + // - statement with nothing to bind + nsCOMPtr stmt; + db->CreateAsyncStatement( + NS_LITERAL_CSTRING("SELECT 1"), + getter_AddRefs(stmt)); + blocking_async_execute(stmt); + stmt->Finalize(); + + nsCOMPtr threadMan = + do_GetService("@mozilla.org/thread-manager;1"); + nsCOMPtr asyncThread; + threadMan->GetThreadFromPRThread(last_non_watched_thread, + getter_AddRefs(asyncThread)); + + // Additionally, check that the thread we get as the background thread is the + // same one as the one we report from getInterface. + nsCOMPtr target = do_GetInterface(db); + nsCOMPtr allegedAsyncThread = do_QueryInterface(target); + PRThread *allegedPRThread; + (void)allegedAsyncThread->GetPRThread(&allegedPRThread); + do_check_eq(allegedPRThread, last_non_watched_thread); + return asyncThread.forget(); +} diff --git a/storage/test/test_transaction_helper.cpp b/storage/test/test_transaction_helper.cpp index 26b834dfd2a9..5f0a04b918ec 100644 --- a/storage/test/test_transaction_helper.cpp +++ b/storage/test/test_transaction_helper.cpp @@ -7,42 +7,19 @@ #include "storage_test_harness.h" #include "mozStorageHelper.h" +#include "mozStorageConnection.h" + +using namespace mozilla; +using namespace mozilla::storage; + +bool has_transaction(mozIStorageConnection* aDB) { + return !(static_cast(aDB)->getAutocommit()); +} /** * This file test our Transaction helper in mozStorageHelper.h. */ -void -test_HasTransaction() -{ - nsCOMPtr db(getMemoryDatabase()); - - // First test that it holds the transaction after it should have gotten one. - { - mozStorageTransaction transaction(db, false); - do_check_true(transaction.HasTransaction()); - (void)transaction.Commit(); - // And that it does not have a transaction after we have committed. - do_check_false(transaction.HasTransaction()); - } - - // Check that no transaction is had after a rollback. - { - mozStorageTransaction transaction(db, false); - do_check_true(transaction.HasTransaction()); - (void)transaction.Rollback(); - do_check_false(transaction.HasTransaction()); - } - - // Check that we do not have a transaction if one is already obtained. - mozStorageTransaction outerTransaction(db, false); - do_check_true(outerTransaction.HasTransaction()); - { - mozStorageTransaction innerTransaction(db, false); - do_check_false(innerTransaction.HasTransaction()); - } -} - void test_Commit() { @@ -52,11 +29,13 @@ test_Commit() // exists after the transaction falls out of scope. { mozStorageTransaction transaction(db, false); + do_check_true(has_transaction(db)); (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE test (id INTEGER PRIMARY KEY)" )); (void)transaction.Commit(); } + do_check_false(has_transaction(db)); bool exists = false; (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); @@ -72,11 +51,13 @@ test_Rollback() // not exists after the transaction falls out of scope. { mozStorageTransaction transaction(db, true); + do_check_true(has_transaction(db)); (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE test (id INTEGER PRIMARY KEY)" )); (void)transaction.Rollback(); } + do_check_false(has_transaction(db)); bool exists = true; (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); @@ -92,10 +73,12 @@ test_AutoCommit() // transaction falls out of scope. This means the Commit was successful. { mozStorageTransaction transaction(db, true); + do_check_true(has_transaction(db)); (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE test (id INTEGER PRIMARY KEY)" )); } + do_check_false(has_transaction(db)); bool exists = false; (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); @@ -112,68 +95,78 @@ test_AutoRollback() // successful. { mozStorageTransaction transaction(db, false); + do_check_true(has_transaction(db)); (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE test (id INTEGER PRIMARY KEY)" )); } + do_check_false(has_transaction(db)); bool exists = true; (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); do_check_false(exists); } -void -test_SetDefaultAction() -{ - nsCOMPtr db(getMemoryDatabase()); - - // First we test that rollback happens when we first set it to automatically - // commit. - { - mozStorageTransaction transaction(db, true); - (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( - "CREATE TABLE test1 (id INTEGER PRIMARY KEY)" - )); - transaction.SetDefaultAction(false); - } - bool exists = true; - (void)db->TableExists(NS_LITERAL_CSTRING("test1"), &exists); - do_check_false(exists); - - // Now we do the opposite and test that a commit happens when we first set it - // to automatically rollback. - { - mozStorageTransaction transaction(db, false); - (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( - "CREATE TABLE test2 (id INTEGER PRIMARY KEY)" - )); - transaction.SetDefaultAction(true); - } - exists = false; - (void)db->TableExists(NS_LITERAL_CSTRING("test2"), &exists); - do_check_true(exists); -} - void test_null_database_connection() { // We permit the use of the Transaction helper when passing a null database // in, so we need to make sure this still works without crashing. mozStorageTransaction transaction(nullptr, false); - - do_check_false(transaction.HasTransaction()); do_check_true(NS_SUCCEEDED(transaction.Commit())); do_check_true(NS_SUCCEEDED(transaction.Rollback())); } +void +test_async_Commit() +{ + // note this will be active for any following test. + hook_sqlite_mutex(); + + nsCOMPtr db(getMemoryDatabase()); + + // -- wedge the thread + nsCOMPtr target(get_conn_async_thread(db)); + do_check_true(target); + nsRefPtr wedger (new ThreadWedger(target)); + + { + mozStorageTransaction transaction(db, false, + mozIStorageConnection::TRANSACTION_DEFERRED, + true); + do_check_true(has_transaction(db)); + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE test (id INTEGER PRIMARY KEY)" + )); + (void)transaction.Commit(); + } + do_check_true(has_transaction(db)); + + // -- unwedge the async thread + wedger->unwedge(); + + // Ensure the transaction has done its job by enqueueing an async execution. + nsCOMPtr stmt; + (void)db->CreateAsyncStatement(NS_LITERAL_CSTRING( + "SELECT NULL" + ), getter_AddRefs(stmt)); + blocking_async_execute(stmt); + stmt->Finalize(); + do_check_false(has_transaction(db)); + bool exists = false; + (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); + do_check_true(exists); + + blocking_async_close(db); +} + void (*gTests[])(void) = { - test_HasTransaction, test_Commit, test_Rollback, test_AutoCommit, test_AutoRollback, - test_SetDefaultAction, test_null_database_connection, + test_async_Commit, }; const char *file = __FILE__; diff --git a/storage/test/test_true_async.cpp b/storage/test/test_true_async.cpp index 4782494930b7..beea9fb0a6f8 100644 --- a/storage/test/test_true_async.cpp +++ b/storage/test/test_true_async.cpp @@ -5,175 +5,6 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "storage_test_harness.h" -#include "prthread.h" -#include "nsIEventTarget.h" -#include "nsIInterfaceRequestorUtils.h" - -#include "sqlite3.h" - -#include "mozilla/ReentrantMonitor.h" - -using mozilla::ReentrantMonitor; -using mozilla::ReentrantMonitorAutoEnter; - -/** - * Verify that mozIStorageAsyncStatement's life-cycle never triggers a mutex on - * the caller (generally main) thread. We do this by decorating the sqlite - * mutex logic with our own code that checks what thread it is being invoked on - * and sets a flag if it is invoked on the main thread. We are able to easily - * decorate the SQLite mutex logic because SQLite allows us to retrieve the - * current function pointers being used and then provide a new set. - */ - -/* ===== Mutex Watching ===== */ - -sqlite3_mutex_methods orig_mutex_methods; -sqlite3_mutex_methods wrapped_mutex_methods; - -bool mutex_used_on_watched_thread = false; -PRThread *watched_thread = nullptr; -/** - * Ugly hack to let us figure out what a connection's async thread is. If we - * were MOZILLA_INTERNAL_API and linked as such we could just include - * mozStorageConnection.h and just ask Connection directly. But that turns out - * poorly. - * - * When the thread a mutex is invoked on isn't watched_thread we save it to this - * variable. - */ -PRThread *last_non_watched_thread = nullptr; - -/** - * Set a flag if the mutex is used on the thread we are watching, but always - * call the real mutex function. - */ -extern "C" void wrapped_MutexEnter(sqlite3_mutex *mutex) -{ - PRThread *curThread = ::PR_GetCurrentThread(); - if (curThread == watched_thread) - mutex_used_on_watched_thread = true; - else - last_non_watched_thread = curThread; - orig_mutex_methods.xMutexEnter(mutex); -} - -extern "C" int wrapped_MutexTry(sqlite3_mutex *mutex) -{ - if (::PR_GetCurrentThread() == watched_thread) - mutex_used_on_watched_thread = true; - return orig_mutex_methods.xMutexTry(mutex); -} - - -#define do_check_ok(aInvoc) do_check_true((aInvoc) == SQLITE_OK) - -void hook_sqlite_mutex() -{ - // We need to initialize and teardown SQLite to get it to set up the - // default mutex handlers for us so we can steal them and wrap them. - do_check_ok(sqlite3_initialize()); - do_check_ok(sqlite3_shutdown()); - do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &orig_mutex_methods)); - do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &wrapped_mutex_methods)); - wrapped_mutex_methods.xMutexEnter = wrapped_MutexEnter; - wrapped_mutex_methods.xMutexTry = wrapped_MutexTry; - do_check_ok(::sqlite3_config(SQLITE_CONFIG_MUTEX, &wrapped_mutex_methods)); -} - -/** - * Call to clear the watch state and to set the watching against this thread. - * - * Check |mutex_used_on_watched_thread| to see if the mutex has fired since - * this method was last called. Since we're talking about the current thread, - * there are no race issues to be concerned about - */ -void watch_for_mutex_use_on_this_thread() -{ - watched_thread = ::PR_GetCurrentThread(); - mutex_used_on_watched_thread = false; -} - - -//////////////////////////////////////////////////////////////////////////////// -//// Thread Wedgers - -/** - * A runnable that blocks until code on another thread invokes its unwedge - * method. By dispatching this to a thread you can ensure that no subsequent - * runnables dispatched to the thread will execute until you invoke unwedge. - * - * The wedger is self-dispatching, just construct it with its target. - */ -class ThreadWedger : public nsRunnable -{ -public: - explicit ThreadWedger(nsIEventTarget *aTarget) - : mReentrantMonitor("thread wedger") - , unwedged(false) - { - aTarget->Dispatch(this, aTarget->NS_DISPATCH_NORMAL); - } - - NS_IMETHOD Run() - { - ReentrantMonitorAutoEnter automon(mReentrantMonitor); - - if (!unwedged) - automon.Wait(); - - return NS_OK; - } - - void unwedge() - { - ReentrantMonitorAutoEnter automon(mReentrantMonitor); - unwedged = true; - automon.Notify(); - } - -private: - ReentrantMonitor mReentrantMonitor; - bool unwedged; -}; - -//////////////////////////////////////////////////////////////////////////////// -//// Async Helpers - -/** - * A horrible hack to figure out what the connection's async thread is. By - * creating a statement and async dispatching we can tell from the mutex who - * is the async thread, PRThread style. Then we map that to an nsIThread. - */ -already_AddRefed -get_conn_async_thread(mozIStorageConnection *db) -{ - // Make sure we are tracking the current thread as the watched thread - watch_for_mutex_use_on_this_thread(); - - // - statement with nothing to bind - nsCOMPtr stmt; - db->CreateAsyncStatement( - NS_LITERAL_CSTRING("SELECT 1"), - getter_AddRefs(stmt)); - blocking_async_execute(stmt); - stmt->Finalize(); - - nsCOMPtr threadMan = - do_GetService("@mozilla.org/thread-manager;1"); - nsCOMPtr asyncThread; - threadMan->GetThreadFromPRThread(last_non_watched_thread, - getter_AddRefs(asyncThread)); - - // Additionally, check that the thread we get as the background thread is the - // same one as the one we report from getInterface. - nsCOMPtr target = do_GetInterface(db); - nsCOMPtr allegedAsyncThread = do_QueryInterface(target); - PRThread *allegedPRThread; - (void)allegedAsyncThread->GetPRThread(&allegedPRThread); - do_check_eq(allegedPRThread, last_non_watched_thread); - return asyncThread.forget(); -} - //////////////////////////////////////////////////////////////////////////////// //// Tests diff --git a/storage/test/unit/test_storage_connection.js b/storage/test/unit/test_storage_connection.js index 66de13757ccf..23db1815e6db 100644 --- a/storage/test/unit/test_storage_connection.js +++ b/storage/test/unit/test_storage_connection.js @@ -736,6 +736,7 @@ add_task(function test_clone_copies_pragmas() { name: "journal_size_limit", value: 524288, copied: true }, { name: "synchronous", value: 2, copied: true }, { name: "wal_autocheckpoint", value: 16, copied: true }, + { name: "busy_timeout", value: 50, copied: true }, { name: "ignore_check_constraints", value: 1, copied: false }, ]; @@ -778,6 +779,7 @@ add_task(function test_readonly_clone_copies_pragmas() { name: "journal_size_limit", value: 524288, copied: false }, { name: "synchronous", value: 2, copied: false }, { name: "wal_autocheckpoint", value: 16, copied: false }, + { name: "busy_timeout", value: 50, copied: false }, { name: "ignore_check_constraints", value: 1, copied: false }, ]; From 00076210716d34b14aea9c31f785311c7c0ff09e Mon Sep 17 00:00:00 2001 From: Marco Bonardo Date: Wed, 10 Sep 2014 12:46:17 +0200 Subject: [PATCH 45/88] bug 1047811 - Part 2 - Use async transactions in Places. r=Mano --- toolkit/components/places/Database.cpp | 17 ++++++++++------- toolkit/components/places/nsNavHistory.cpp | 12 +++++++++--- .../test_1017502-bookmarks_foreign_count.js | 5 +++-- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/toolkit/components/places/Database.cpp b/toolkit/components/places/Database.cpp index 154196a428e1..7a263e7d65e5 100644 --- a/toolkit/components/places/Database.cpp +++ b/toolkit/components/places/Database.cpp @@ -49,6 +49,9 @@ #define BYTES_PER_KIBIBYTE 1024 +// How much time Sqlite can wait before returning a SQLITE_BUSY error. +#define DATABASE_BUSY_TIMEOUT_MS 100 + // Old Sync GUID annotation. #define SYNCGUID_ANNO NS_LITERAL_CSTRING("sync/guid") @@ -597,6 +600,10 @@ Database::InitSchema(bool* aDatabaseMigrated) (void)mMainConn->SetGrowthIncrement(growthIncrementKiB * BYTES_PER_KIBIBYTE, EmptyCString()); } + nsAutoCString busyTimeoutPragma("PRAGMA busy_timeout = "); + busyTimeoutPragma.AppendInt(DATABASE_BUSY_TIMEOUT_MS); + (void)mMainConn->ExecuteSimpleSQL(busyTimeoutPragma); + // We use our functions during migration, so initialize them now. rv = InitFunctions(); NS_ENSURE_SUCCESS(rv, rv); @@ -1210,8 +1217,6 @@ Database::MigrateV7Up() return NS_ERROR_FILE_CORRUPTED; } - mozStorageTransaction transaction(mMainConn, false); - // We need an index on lastModified to catch quickly last modified bookmark // title for tag container's children. This will be useful for Sync, too. bool lastModIndexExists = false; @@ -1391,7 +1396,7 @@ Database::MigrateV7Up() NS_ENSURE_SUCCESS(rv, rv); } - return transaction.Commit(); + return NS_OK; } @@ -1399,7 +1404,6 @@ nsresult Database::MigrateV8Up() { MOZ_ASSERT(NS_IsMainThread()); - mozStorageTransaction transaction(mMainConn, false); nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TRIGGER IF EXISTS moz_historyvisits_afterinsert_v1_trigger")); @@ -1445,7 +1449,7 @@ Database::MigrateV8Up() NS_ENSURE_SUCCESS(rv, rv); } - return transaction.Commit(); + return NS_OK; } @@ -1453,7 +1457,6 @@ nsresult Database::MigrateV9Up() { MOZ_ASSERT(NS_IsMainThread()); - mozStorageTransaction transaction(mMainConn, false); // Added in Bug 488966. The last_visit_date column caches the last // visit date, this enhances SELECT performances when we // need to sort visits by visit date. @@ -1485,7 +1488,7 @@ Database::MigrateV9Up() NS_ENSURE_SUCCESS(rv, rv); } - return transaction.Commit(); + return NS_OK; } diff --git a/toolkit/components/places/nsNavHistory.cpp b/toolkit/components/places/nsNavHistory.cpp index 9886ac651d35..6aa14e4d91d5 100644 --- a/toolkit/components/places/nsNavHistory.cpp +++ b/toolkit/components/places/nsNavHistory.cpp @@ -2267,7 +2267,9 @@ nsresult nsNavHistory::BeginUpdateBatch() { if (mBatchLevel++ == 0) { - mBatchDBTransaction = new mozStorageTransaction(mDB->MainConn(), false); + mBatchDBTransaction = new mozStorageTransaction(mDB->MainConn(), false, + mozIStorageConnection::TRANSACTION_DEFERRED, + true); NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, nsINavHistoryObserver, OnBeginUpdateBatch()); @@ -2332,7 +2334,9 @@ nsNavHistory::RemovePagesInternal(const nsCString& aPlaceIdsQueryString) if (aPlaceIdsQueryString.IsEmpty()) return NS_OK; - mozStorageTransaction transaction(mDB->MainConn(), false); + mozStorageTransaction transaction(mDB->MainConn(), false, + mozIStorageConnection::TRANSACTION_DEFERRED, + true); // Delete all visits for the specified place ids. nsresult rv = mDB->MainConn()->ExecuteSimpleSQL( @@ -2724,7 +2728,9 @@ nsNavHistory::RemoveVisitsByTimeframe(PRTime aBeginTime, PRTime aEndTime) // force a full refresh calling onEndUpdateBatch (will call Refresh()) UpdateBatchScoper batch(*this); // sends Begin/EndUpdateBatch to observers - mozStorageTransaction transaction(mDB->MainConn(), false); + mozStorageTransaction transaction(mDB->MainConn(), false, + mozIStorageConnection::TRANSACTION_DEFERRED, + true); // Delete all visits within the timeframe. nsCOMPtr deleteVisitsStmt = mDB->GetStatement( diff --git a/toolkit/components/places/tests/bookmarks/test_1017502-bookmarks_foreign_count.js b/toolkit/components/places/tests/bookmarks/test_1017502-bookmarks_foreign_count.js index d2046620d2b1..70bb9db9ad3c 100644 --- a/toolkit/components/places/tests/bookmarks/test_1017502-bookmarks_foreign_count.js +++ b/toolkit/components/places/tests/bookmarks/test_1017502-bookmarks_foreign_count.js @@ -11,7 +11,8 @@ added or removed and also the maintenance task to fix wrong counts. const T_URI = NetUtil.newURI("https://www.mozilla.org/firefox/nightly/firstrun/"); -function* getForeignCountForURL(conn, url){ +function* getForeignCountForURL(conn, url) { + yield promiseAsyncUpdates(); let url = url instanceof Ci.nsIURI ? url.spec : url; let rows = yield conn.executeCached( "SELECT foreign_count FROM moz_places WHERE url = :t_url ", { t_url: url }); @@ -106,4 +107,4 @@ add_task(function* add_remove_tags_test(){ // Check foreign count is set to 0 when all tags are removed PlacesUtils.tagging.untagURI(T_URI, ["test tag", "one", "two"]); Assert.equal((yield getForeignCountForURL(conn, T_URI)), 0); -}); \ No newline at end of file +}); From 8e15820a390e2e2bebecc62025af9aa2f9479f2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A3o=20Gottwald?= Date: Wed, 10 Sep 2014 12:55:48 +0200 Subject: [PATCH 46/88] Bug 1063594 - main-pane-loaded notification should be sent at the end of gMainPane.init. r=jaws --- browser/components/preferences/in-content/main.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/browser/components/preferences/in-content/main.js b/browser/components/preferences/in-content/main.js index ba2ffb634b26..132e7ad092ec 100644 --- a/browser/components/preferences/in-content/main.js +++ b/browser/components/preferences/in-content/main.js @@ -60,14 +60,8 @@ var gMainPane = { this.updateBrowserStartupLastSession(); - // Notify observers that the UI is now ready - Components.classes["@mozilla.org/observer-service;1"] - .getService(Components.interfaces.nsIObserverService) - .notifyObservers(window, "main-pane-loaded", null); - #ifdef XP_WIN // Functionality for "Show tabs in taskbar" on Windows 7 and up. - try { let sysInfo = Cc["@mozilla.org/system-info;1"]. getService(Ci.nsIPropertyBag2); @@ -75,7 +69,6 @@ var gMainPane = { let showTabsInTaskbar = document.getElementById("showTabsInTaskbar"); showTabsInTaskbar.hidden = ver < 6.1; } catch (ex) {} - #endif setEventListener("browser.privatebrowsing.autostart", "change", @@ -94,6 +87,11 @@ var gMainPane = { gMainPane.restoreDefaultHomePage); setEventListener("chooseFolder", "command", gMainPane.chooseFolder); + + // Notify observers that the UI is now ready + Components.classes["@mozilla.org/observer-service;1"] + .getService(Components.interfaces.nsIObserverService) + .notifyObservers(window, "main-pane-loaded", null); }, // HOME PAGE From fd40edf863487414a59c5fa2a5e00bd4bb856e6c Mon Sep 17 00:00:00 2001 From: "Carsten \"Tomcat\" Book" Date: Wed, 10 Sep 2014 13:07:12 +0200 Subject: [PATCH 47/88] Backed out changeset 925427a6d0b1 (bug 1047811) --- toolkit/components/places/Database.cpp | 17 +++++++---------- toolkit/components/places/nsNavHistory.cpp | 12 +++--------- .../test_1017502-bookmarks_foreign_count.js | 5 ++--- 3 files changed, 12 insertions(+), 22 deletions(-) diff --git a/toolkit/components/places/Database.cpp b/toolkit/components/places/Database.cpp index 7a263e7d65e5..154196a428e1 100644 --- a/toolkit/components/places/Database.cpp +++ b/toolkit/components/places/Database.cpp @@ -49,9 +49,6 @@ #define BYTES_PER_KIBIBYTE 1024 -// How much time Sqlite can wait before returning a SQLITE_BUSY error. -#define DATABASE_BUSY_TIMEOUT_MS 100 - // Old Sync GUID annotation. #define SYNCGUID_ANNO NS_LITERAL_CSTRING("sync/guid") @@ -600,10 +597,6 @@ Database::InitSchema(bool* aDatabaseMigrated) (void)mMainConn->SetGrowthIncrement(growthIncrementKiB * BYTES_PER_KIBIBYTE, EmptyCString()); } - nsAutoCString busyTimeoutPragma("PRAGMA busy_timeout = "); - busyTimeoutPragma.AppendInt(DATABASE_BUSY_TIMEOUT_MS); - (void)mMainConn->ExecuteSimpleSQL(busyTimeoutPragma); - // We use our functions during migration, so initialize them now. rv = InitFunctions(); NS_ENSURE_SUCCESS(rv, rv); @@ -1217,6 +1210,8 @@ Database::MigrateV7Up() return NS_ERROR_FILE_CORRUPTED; } + mozStorageTransaction transaction(mMainConn, false); + // We need an index on lastModified to catch quickly last modified bookmark // title for tag container's children. This will be useful for Sync, too. bool lastModIndexExists = false; @@ -1396,7 +1391,7 @@ Database::MigrateV7Up() NS_ENSURE_SUCCESS(rv, rv); } - return NS_OK; + return transaction.Commit(); } @@ -1404,6 +1399,7 @@ nsresult Database::MigrateV8Up() { MOZ_ASSERT(NS_IsMainThread()); + mozStorageTransaction transaction(mMainConn, false); nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "DROP TRIGGER IF EXISTS moz_historyvisits_afterinsert_v1_trigger")); @@ -1449,7 +1445,7 @@ Database::MigrateV8Up() NS_ENSURE_SUCCESS(rv, rv); } - return NS_OK; + return transaction.Commit(); } @@ -1457,6 +1453,7 @@ nsresult Database::MigrateV9Up() { MOZ_ASSERT(NS_IsMainThread()); + mozStorageTransaction transaction(mMainConn, false); // Added in Bug 488966. The last_visit_date column caches the last // visit date, this enhances SELECT performances when we // need to sort visits by visit date. @@ -1488,7 +1485,7 @@ Database::MigrateV9Up() NS_ENSURE_SUCCESS(rv, rv); } - return NS_OK; + return transaction.Commit(); } diff --git a/toolkit/components/places/nsNavHistory.cpp b/toolkit/components/places/nsNavHistory.cpp index 6aa14e4d91d5..9886ac651d35 100644 --- a/toolkit/components/places/nsNavHistory.cpp +++ b/toolkit/components/places/nsNavHistory.cpp @@ -2267,9 +2267,7 @@ nsresult nsNavHistory::BeginUpdateBatch() { if (mBatchLevel++ == 0) { - mBatchDBTransaction = new mozStorageTransaction(mDB->MainConn(), false, - mozIStorageConnection::TRANSACTION_DEFERRED, - true); + mBatchDBTransaction = new mozStorageTransaction(mDB->MainConn(), false); NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers, nsINavHistoryObserver, OnBeginUpdateBatch()); @@ -2334,9 +2332,7 @@ nsNavHistory::RemovePagesInternal(const nsCString& aPlaceIdsQueryString) if (aPlaceIdsQueryString.IsEmpty()) return NS_OK; - mozStorageTransaction transaction(mDB->MainConn(), false, - mozIStorageConnection::TRANSACTION_DEFERRED, - true); + mozStorageTransaction transaction(mDB->MainConn(), false); // Delete all visits for the specified place ids. nsresult rv = mDB->MainConn()->ExecuteSimpleSQL( @@ -2728,9 +2724,7 @@ nsNavHistory::RemoveVisitsByTimeframe(PRTime aBeginTime, PRTime aEndTime) // force a full refresh calling onEndUpdateBatch (will call Refresh()) UpdateBatchScoper batch(*this); // sends Begin/EndUpdateBatch to observers - mozStorageTransaction transaction(mDB->MainConn(), false, - mozIStorageConnection::TRANSACTION_DEFERRED, - true); + mozStorageTransaction transaction(mDB->MainConn(), false); // Delete all visits within the timeframe. nsCOMPtr deleteVisitsStmt = mDB->GetStatement( diff --git a/toolkit/components/places/tests/bookmarks/test_1017502-bookmarks_foreign_count.js b/toolkit/components/places/tests/bookmarks/test_1017502-bookmarks_foreign_count.js index 70bb9db9ad3c..d2046620d2b1 100644 --- a/toolkit/components/places/tests/bookmarks/test_1017502-bookmarks_foreign_count.js +++ b/toolkit/components/places/tests/bookmarks/test_1017502-bookmarks_foreign_count.js @@ -11,8 +11,7 @@ added or removed and also the maintenance task to fix wrong counts. const T_URI = NetUtil.newURI("https://www.mozilla.org/firefox/nightly/firstrun/"); -function* getForeignCountForURL(conn, url) { - yield promiseAsyncUpdates(); +function* getForeignCountForURL(conn, url){ let url = url instanceof Ci.nsIURI ? url.spec : url; let rows = yield conn.executeCached( "SELECT foreign_count FROM moz_places WHERE url = :t_url ", { t_url: url }); @@ -107,4 +106,4 @@ add_task(function* add_remove_tags_test(){ // Check foreign count is set to 0 when all tags are removed PlacesUtils.tagging.untagURI(T_URI, ["test tag", "one", "two"]); Assert.equal((yield getForeignCountForURL(conn, T_URI)), 0); -}); +}); \ No newline at end of file From c70ad543f33ec3e340deebfa37e8fda2a1266ca9 Mon Sep 17 00:00:00 2001 From: "Carsten \"Tomcat\" Book" Date: Wed, 10 Sep 2014 13:08:07 +0200 Subject: [PATCH 48/88] Backed out changeset bc1363a17e0e (bug 1047811) for bustage --- storage/public/mozStorageHelper.h | 168 ++++++++---------- storage/src/mozStorageConnection.cpp | 1 - storage/src/mozStorageConnection.h | 8 - storage/test/storage_test_harness.h | 167 +----------------- storage/test/test_transaction_helper.cpp | 127 +++++++------- storage/test/test_true_async.cpp | 169 +++++++++++++++++++ storage/test/unit/test_storage_connection.js | 2 - 7 files changed, 307 insertions(+), 335 deletions(-) diff --git a/storage/public/mozStorageHelper.h b/storage/public/mozStorageHelper.h index 5104baca87d8..a4ecda8f7c5b 100644 --- a/storage/public/mozStorageHelper.h +++ b/storage/public/mozStorageHelper.h @@ -7,13 +7,10 @@ #define MOZSTORAGEHELPER_H #include "nsAutoPtr.h" -#include "nsStringGlue.h" -#include "mozilla/DebugOnly.h" #include "mozIStorageAsyncConnection.h" #include "mozIStorageConnection.h" #include "mozIStorageStatement.h" -#include "mozIStoragePendingStatement.h" #include "nsError.h" /** @@ -21,113 +18,59 @@ * the transaction will be completed even if you have an exception or * return early. * - * A common use is to create an instance with aCommitOnComplete = false (rollback), - * then call Commit() on this object manually when your function completes - * successfully. + * aCommitOnComplete controls whether the transaction is committed or rolled + * back when it goes out of scope. A common use is to create an instance with + * commitOnComplete = FALSE (rollback), then call Commit on this object manually + * when your function completes successfully. * - * @note nested transactions are not supported by Sqlite, so if a transaction - * is already in progress, this object does nothing. Note that in this case, - * you may not get the transaction type you asked for, and you won't be able + * Note that nested transactions are not supported by sqlite, so if a transaction + * is already in progress, this object does nothing. Note that in this case, + * you may not get the transaction type you ask for, and you won't be able * to rollback. * - * @param aConnection - * The connection to create the transaction on. - * @param aCommitOnComplete - * Controls whether the transaction is committed or rolled back when - * this object goes out of scope. - * @param aType [optional] - * The transaction type, as defined in mozIStorageConnection. Defaults - * to TRANSACTION_DEFERRED. - * @param aAsyncCommit [optional] - * Whether commit should be executed asynchronously on the helper thread. - * This is a special option introduced as an interim solution to reduce - * main-thread fsyncs in Places. Can only be used on main-thread. - * - * WARNING: YOU SHOULD _NOT_ WRITE NEW MAIN-THREAD CODE USING THIS! - * - * Notice that async commit might cause synchronous statements to fail - * with SQLITE_BUSY. A possible mitigation strategy is to use - * PRAGMA busy_timeout, but notice that might cause main-thread jank. - * Finally, if the database is using WAL journaling mode, other - * connections won't see the changes done in async committed transactions - * until commit is complete. - * - * For all of the above reasons, this should only be used as an interim - * solution and avoided completely if possible. + * Note: This class is templatized to be also usable with internal data + * structures. External users of this class should generally use + * |mozStorageTransaction| instead. */ -class mozStorageTransaction +template +class mozStorageTransactionBase { public: - mozStorageTransaction(mozIStorageConnection* aConnection, - bool aCommitOnComplete, - int32_t aType = mozIStorageConnection::TRANSACTION_DEFERRED, - bool aAsyncCommit = false) + mozStorageTransactionBase(T* aConnection, + bool aCommitOnComplete, + int32_t aType = mozIStorageConnection::TRANSACTION_DEFERRED) : mConnection(aConnection), mHasTransaction(false), mCommitOnComplete(aCommitOnComplete), - mCompleted(false), - mAsyncCommit(aAsyncCommit) + mCompleted(false) { - if (mConnection) { - nsAutoCString query("BEGIN"); - switch(aType) { - case mozIStorageConnection::TRANSACTION_IMMEDIATE: - query.AppendLiteral(" IMMEDIATE"); - break; - case mozIStorageConnection::TRANSACTION_EXCLUSIVE: - query.AppendLiteral(" EXCLUSIVE"); - break; - case mozIStorageConnection::TRANSACTION_DEFERRED: - query.AppendLiteral(" DEFERRED"); - break; - default: - MOZ_ASSERT(false, "Unknown transaction type"); - } - // If a transaction is already in progress, this will fail, since Sqlite - // doesn't support nested transactions. - mHasTransaction = NS_SUCCEEDED(mConnection->ExecuteSimpleSQL(query)); - } + // We won't try to get a transaction if one is already in progress. + if (mConnection) + mHasTransaction = NS_SUCCEEDED(mConnection->BeginTransactionAs(aType)); } - - ~mozStorageTransaction() + ~mozStorageTransactionBase() { - if (mConnection && mHasTransaction && !mCompleted) { - if (mCommitOnComplete) { - mozilla::DebugOnly rv = Commit(); - NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), - "A transaction didn't commit correctly"); - } - else { - mozilla::DebugOnly rv = Rollback(); - NS_WARN_IF_FALSE(NS_SUCCEEDED(rv), - "A transaction didn't rollback correctly"); - } + if (mConnection && mHasTransaction && ! mCompleted) { + if (mCommitOnComplete) + mConnection->CommitTransaction(); + else + mConnection->RollbackTransaction(); } } /** * Commits the transaction if one is in progress. If one is not in progress, * this is a NOP since the actual owner of the transaction outside of our - * scope is in charge of finally committing or rolling back the transaction. + * scope is in charge of finally comitting or rolling back the transaction. */ nsresult Commit() { - if (!mConnection || mCompleted || !mHasTransaction) - return NS_OK; + if (!mConnection || mCompleted) + return NS_OK; // no connection, or already done mCompleted = true; - - // TODO (bug 559659): this might fail with SQLITE_BUSY, but we don't handle - // it, thus the transaction might stay open until the next COMMIT. - nsresult rv; - if (mAsyncCommit) { - nsCOMPtr ps; - rv = mConnection->ExecuteSimpleSQLAsync(NS_LITERAL_CSTRING("COMMIT"), - nullptr, getter_AddRefs(ps)); - } - else { - rv = mConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING("COMMIT")); - } - + if (! mHasTransaction) + return NS_OK; // transaction not ours, ignore + nsresult rv = mConnection->CommitTransaction(); if (NS_SUCCEEDED(rv)) mHasTransaction = false; @@ -135,21 +78,22 @@ public: } /** - * Rolls back the transaction if one is in progress. If one is not in progress, - * this is a NOP since the actual owner of the transaction outside of our - * scope is in charge of finally rolling back the transaction. + * Rolls back the transaction in progress. You should only call this function + * if this object has a real transaction (HasTransaction() = true) because + * otherwise, there is no transaction to roll back. */ nsresult Rollback() { - if (!mConnection || mCompleted || !mHasTransaction) - return NS_OK; + if (!mConnection || mCompleted) + return NS_OK; // no connection, or already done mCompleted = true; + if (! mHasTransaction) + return NS_ERROR_FAILURE; - // TODO (bug 1062823): from Sqlite 3.7.11 on, rollback won't ever return - // a busy error, so this handling can be removed. + // It is possible that a rollback will return busy, so we busy wait... nsresult rv = NS_OK; do { - rv = mConnection->ExecuteSimpleSQL(NS_LITERAL_CSTRING("ROLLBACK")); + rv = mConnection->RollbackTransaction(); if (rv == NS_ERROR_STORAGE_BUSY) (void)PR_Sleep(PR_INTERVAL_NO_WAIT); } while (rv == NS_ERROR_STORAGE_BUSY); @@ -160,14 +104,42 @@ public: return rv; } + /** + * Returns whether this object wraps a real transaction. False means that + * this object doesn't do anything because there was already a transaction in + * progress when it was created. + */ + bool HasTransaction() + { + return mHasTransaction; + } + + /** + * This sets the default action (commit or rollback) when this object goes + * out of scope. + */ + void SetDefaultAction(bool aCommitOnComplete) + { + mCommitOnComplete = aCommitOnComplete; + } + protected: - nsCOMPtr mConnection; + U mConnection; bool mHasTransaction; bool mCommitOnComplete; bool mCompleted; - bool mAsyncCommit; }; +/** + * An instance of the mozStorageTransaction<> family dedicated + * to |mozIStorageConnection|. + */ +typedef mozStorageTransactionBase > +mozStorageTransaction; + + + /** * This class wraps a statement so that it is guaraneed to be reset when * this object goes out of scope. diff --git a/storage/src/mozStorageConnection.cpp b/storage/src/mozStorageConnection.cpp index 20459632e971..854c5b7770db 100644 --- a/storage/src/mozStorageConnection.cpp +++ b/storage/src/mozStorageConnection.cpp @@ -1218,7 +1218,6 @@ Connection::initializeClone(Connection* aClone, bool aReadOnly) "journal_size_limit", "synchronous", "wal_autocheckpoint", - "busy_timeout" }; for (uint32_t i = 0; i < ArrayLength(pragmas); ++i) { // Read-only connections just need cache_size and temp_store pragmas. diff --git a/storage/src/mozStorageConnection.h b/storage/src/mozStorageConnection.h index f1f13d57f9bf..4d9b782dbae6 100644 --- a/storage/src/mozStorageConnection.h +++ b/storage/src/mozStorageConnection.h @@ -120,14 +120,6 @@ public: ::sqlite3_commit_hook(mDBConn, aCallbackFn, aData); }; - /** - * Gets autocommit status. - */ - bool getAutocommit() { - MOZ_ASSERT(mDBConn, "A connection must exist at this point"); - return static_cast(::sqlite3_get_autocommit(mDBConn)); - }; - /** * Lazily creates and returns a background execution thread. In the future, * the thread may be re-claimed if left idle, so you should call this diff --git a/storage/test/storage_test_harness.h b/storage/test/storage_test_harness.h index 1e70b4a74797..f2c767003957 100644 --- a/storage/test/storage_test_harness.h +++ b/storage/test/storage_test_harness.h @@ -5,13 +5,9 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "TestHarness.h" - #include "nsMemory.h" -#include "prthread.h" #include "nsThreadUtils.h" #include "nsDirectoryServiceDefs.h" -#include "mozilla/ReentrantMonitor.h" - #include "mozIStorageService.h" #include "mozIStorageConnection.h" #include "mozIStorageStatementCallback.h" @@ -22,10 +18,7 @@ #include "mozIStorageStatement.h" #include "mozIStoragePendingStatement.h" #include "mozIStorageError.h" -#include "nsIInterfaceRequestorUtils.h" -#include "nsIEventTarget.h" - -#include "sqlite3.h" +#include "nsThreadUtils.h" static int gTotalTests = 0; static int gPassedTests = 0; @@ -61,8 +54,6 @@ static int gPassedTests = 0; #else #include -#define do_check_ok(aInvoc) do_check_true((aInvoc) == SQLITE_OK) - // Print nsresult as uint32_t std::ostream& operator<<(std::ostream& aStream, const nsresult aInput) { @@ -233,159 +224,3 @@ blocking_async_close(mozIStorageConnection *db) db->AsyncClose(spinner); spinner->SpinUntilCompleted(); } - -//////////////////////////////////////////////////////////////////////////////// -//// Mutex Watching - -/** - * Verify that mozIStorageAsyncStatement's life-cycle never triggers a mutex on - * the caller (generally main) thread. We do this by decorating the sqlite - * mutex logic with our own code that checks what thread it is being invoked on - * and sets a flag if it is invoked on the main thread. We are able to easily - * decorate the SQLite mutex logic because SQLite allows us to retrieve the - * current function pointers being used and then provide a new set. - */ - -sqlite3_mutex_methods orig_mutex_methods; -sqlite3_mutex_methods wrapped_mutex_methods; - -bool mutex_used_on_watched_thread = false; -PRThread *watched_thread = nullptr; -/** - * Ugly hack to let us figure out what a connection's async thread is. If we - * were MOZILLA_INTERNAL_API and linked as such we could just include - * mozStorageConnection.h and just ask Connection directly. But that turns out - * poorly. - * - * When the thread a mutex is invoked on isn't watched_thread we save it to this - * variable. - */ -PRThread *last_non_watched_thread = nullptr; - -/** - * Set a flag if the mutex is used on the thread we are watching, but always - * call the real mutex function. - */ -extern "C" void wrapped_MutexEnter(sqlite3_mutex *mutex) -{ - PRThread *curThread = ::PR_GetCurrentThread(); - if (curThread == watched_thread) - mutex_used_on_watched_thread = true; - else - last_non_watched_thread = curThread; - orig_mutex_methods.xMutexEnter(mutex); -} - -extern "C" int wrapped_MutexTry(sqlite3_mutex *mutex) -{ - if (::PR_GetCurrentThread() == watched_thread) - mutex_used_on_watched_thread = true; - return orig_mutex_methods.xMutexTry(mutex); -} - -void hook_sqlite_mutex() -{ - // We need to initialize and teardown SQLite to get it to set up the - // default mutex handlers for us so we can steal them and wrap them. - do_check_ok(sqlite3_initialize()); - do_check_ok(sqlite3_shutdown()); - do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &orig_mutex_methods)); - do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &wrapped_mutex_methods)); - wrapped_mutex_methods.xMutexEnter = wrapped_MutexEnter; - wrapped_mutex_methods.xMutexTry = wrapped_MutexTry; - do_check_ok(::sqlite3_config(SQLITE_CONFIG_MUTEX, &wrapped_mutex_methods)); -} - -/** - * Call to clear the watch state and to set the watching against this thread. - * - * Check |mutex_used_on_watched_thread| to see if the mutex has fired since - * this method was last called. Since we're talking about the current thread, - * there are no race issues to be concerned about - */ -void watch_for_mutex_use_on_this_thread() -{ - watched_thread = ::PR_GetCurrentThread(); - mutex_used_on_watched_thread = false; -} - - -//////////////////////////////////////////////////////////////////////////////// -//// Thread Wedgers - -/** - * A runnable that blocks until code on another thread invokes its unwedge - * method. By dispatching this to a thread you can ensure that no subsequent - * runnables dispatched to the thread will execute until you invoke unwedge. - * - * The wedger is self-dispatching, just construct it with its target. - */ -class ThreadWedger : public nsRunnable -{ -public: - explicit ThreadWedger(nsIEventTarget *aTarget) - : mReentrantMonitor("thread wedger") - , unwedged(false) - { - aTarget->Dispatch(this, aTarget->NS_DISPATCH_NORMAL); - } - - NS_IMETHOD Run() - { - mozilla::ReentrantMonitorAutoEnter automon(mReentrantMonitor); - - if (!unwedged) - automon.Wait(); - - return NS_OK; - } - - void unwedge() - { - mozilla::ReentrantMonitorAutoEnter automon(mReentrantMonitor); - unwedged = true; - automon.Notify(); - } - -private: - mozilla::ReentrantMonitor mReentrantMonitor; - bool unwedged; -}; - -//////////////////////////////////////////////////////////////////////////////// -//// Async Helpers - -/** - * A horrible hack to figure out what the connection's async thread is. By - * creating a statement and async dispatching we can tell from the mutex who - * is the async thread, PRThread style. Then we map that to an nsIThread. - */ -already_AddRefed -get_conn_async_thread(mozIStorageConnection *db) -{ - // Make sure we are tracking the current thread as the watched thread - watch_for_mutex_use_on_this_thread(); - - // - statement with nothing to bind - nsCOMPtr stmt; - db->CreateAsyncStatement( - NS_LITERAL_CSTRING("SELECT 1"), - getter_AddRefs(stmt)); - blocking_async_execute(stmt); - stmt->Finalize(); - - nsCOMPtr threadMan = - do_GetService("@mozilla.org/thread-manager;1"); - nsCOMPtr asyncThread; - threadMan->GetThreadFromPRThread(last_non_watched_thread, - getter_AddRefs(asyncThread)); - - // Additionally, check that the thread we get as the background thread is the - // same one as the one we report from getInterface. - nsCOMPtr target = do_GetInterface(db); - nsCOMPtr allegedAsyncThread = do_QueryInterface(target); - PRThread *allegedPRThread; - (void)allegedAsyncThread->GetPRThread(&allegedPRThread); - do_check_eq(allegedPRThread, last_non_watched_thread); - return asyncThread.forget(); -} diff --git a/storage/test/test_transaction_helper.cpp b/storage/test/test_transaction_helper.cpp index 5f0a04b918ec..26b834dfd2a9 100644 --- a/storage/test/test_transaction_helper.cpp +++ b/storage/test/test_transaction_helper.cpp @@ -7,19 +7,42 @@ #include "storage_test_harness.h" #include "mozStorageHelper.h" -#include "mozStorageConnection.h" - -using namespace mozilla; -using namespace mozilla::storage; - -bool has_transaction(mozIStorageConnection* aDB) { - return !(static_cast(aDB)->getAutocommit()); -} /** * This file test our Transaction helper in mozStorageHelper.h. */ +void +test_HasTransaction() +{ + nsCOMPtr db(getMemoryDatabase()); + + // First test that it holds the transaction after it should have gotten one. + { + mozStorageTransaction transaction(db, false); + do_check_true(transaction.HasTransaction()); + (void)transaction.Commit(); + // And that it does not have a transaction after we have committed. + do_check_false(transaction.HasTransaction()); + } + + // Check that no transaction is had after a rollback. + { + mozStorageTransaction transaction(db, false); + do_check_true(transaction.HasTransaction()); + (void)transaction.Rollback(); + do_check_false(transaction.HasTransaction()); + } + + // Check that we do not have a transaction if one is already obtained. + mozStorageTransaction outerTransaction(db, false); + do_check_true(outerTransaction.HasTransaction()); + { + mozStorageTransaction innerTransaction(db, false); + do_check_false(innerTransaction.HasTransaction()); + } +} + void test_Commit() { @@ -29,13 +52,11 @@ test_Commit() // exists after the transaction falls out of scope. { mozStorageTransaction transaction(db, false); - do_check_true(has_transaction(db)); (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE test (id INTEGER PRIMARY KEY)" )); (void)transaction.Commit(); } - do_check_false(has_transaction(db)); bool exists = false; (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); @@ -51,13 +72,11 @@ test_Rollback() // not exists after the transaction falls out of scope. { mozStorageTransaction transaction(db, true); - do_check_true(has_transaction(db)); (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE test (id INTEGER PRIMARY KEY)" )); (void)transaction.Rollback(); } - do_check_false(has_transaction(db)); bool exists = true; (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); @@ -73,12 +92,10 @@ test_AutoCommit() // transaction falls out of scope. This means the Commit was successful. { mozStorageTransaction transaction(db, true); - do_check_true(has_transaction(db)); (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE test (id INTEGER PRIMARY KEY)" )); } - do_check_false(has_transaction(db)); bool exists = false; (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); @@ -95,78 +112,68 @@ test_AutoRollback() // successful. { mozStorageTransaction transaction(db, false); - do_check_true(has_transaction(db)); (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( "CREATE TABLE test (id INTEGER PRIMARY KEY)" )); } - do_check_false(has_transaction(db)); bool exists = true; (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); do_check_false(exists); } +void +test_SetDefaultAction() +{ + nsCOMPtr db(getMemoryDatabase()); + + // First we test that rollback happens when we first set it to automatically + // commit. + { + mozStorageTransaction transaction(db, true); + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE test1 (id INTEGER PRIMARY KEY)" + )); + transaction.SetDefaultAction(false); + } + bool exists = true; + (void)db->TableExists(NS_LITERAL_CSTRING("test1"), &exists); + do_check_false(exists); + + // Now we do the opposite and test that a commit happens when we first set it + // to automatically rollback. + { + mozStorageTransaction transaction(db, false); + (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( + "CREATE TABLE test2 (id INTEGER PRIMARY KEY)" + )); + transaction.SetDefaultAction(true); + } + exists = false; + (void)db->TableExists(NS_LITERAL_CSTRING("test2"), &exists); + do_check_true(exists); +} + void test_null_database_connection() { // We permit the use of the Transaction helper when passing a null database // in, so we need to make sure this still works without crashing. mozStorageTransaction transaction(nullptr, false); + + do_check_false(transaction.HasTransaction()); do_check_true(NS_SUCCEEDED(transaction.Commit())); do_check_true(NS_SUCCEEDED(transaction.Rollback())); } -void -test_async_Commit() -{ - // note this will be active for any following test. - hook_sqlite_mutex(); - - nsCOMPtr db(getMemoryDatabase()); - - // -- wedge the thread - nsCOMPtr target(get_conn_async_thread(db)); - do_check_true(target); - nsRefPtr wedger (new ThreadWedger(target)); - - { - mozStorageTransaction transaction(db, false, - mozIStorageConnection::TRANSACTION_DEFERRED, - true); - do_check_true(has_transaction(db)); - (void)db->ExecuteSimpleSQL(NS_LITERAL_CSTRING( - "CREATE TABLE test (id INTEGER PRIMARY KEY)" - )); - (void)transaction.Commit(); - } - do_check_true(has_transaction(db)); - - // -- unwedge the async thread - wedger->unwedge(); - - // Ensure the transaction has done its job by enqueueing an async execution. - nsCOMPtr stmt; - (void)db->CreateAsyncStatement(NS_LITERAL_CSTRING( - "SELECT NULL" - ), getter_AddRefs(stmt)); - blocking_async_execute(stmt); - stmt->Finalize(); - do_check_false(has_transaction(db)); - bool exists = false; - (void)db->TableExists(NS_LITERAL_CSTRING("test"), &exists); - do_check_true(exists); - - blocking_async_close(db); -} - void (*gTests[])(void) = { + test_HasTransaction, test_Commit, test_Rollback, test_AutoCommit, test_AutoRollback, + test_SetDefaultAction, test_null_database_connection, - test_async_Commit, }; const char *file = __FILE__; diff --git a/storage/test/test_true_async.cpp b/storage/test/test_true_async.cpp index beea9fb0a6f8..4782494930b7 100644 --- a/storage/test/test_true_async.cpp +++ b/storage/test/test_true_async.cpp @@ -5,6 +5,175 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "storage_test_harness.h" +#include "prthread.h" +#include "nsIEventTarget.h" +#include "nsIInterfaceRequestorUtils.h" + +#include "sqlite3.h" + +#include "mozilla/ReentrantMonitor.h" + +using mozilla::ReentrantMonitor; +using mozilla::ReentrantMonitorAutoEnter; + +/** + * Verify that mozIStorageAsyncStatement's life-cycle never triggers a mutex on + * the caller (generally main) thread. We do this by decorating the sqlite + * mutex logic with our own code that checks what thread it is being invoked on + * and sets a flag if it is invoked on the main thread. We are able to easily + * decorate the SQLite mutex logic because SQLite allows us to retrieve the + * current function pointers being used and then provide a new set. + */ + +/* ===== Mutex Watching ===== */ + +sqlite3_mutex_methods orig_mutex_methods; +sqlite3_mutex_methods wrapped_mutex_methods; + +bool mutex_used_on_watched_thread = false; +PRThread *watched_thread = nullptr; +/** + * Ugly hack to let us figure out what a connection's async thread is. If we + * were MOZILLA_INTERNAL_API and linked as such we could just include + * mozStorageConnection.h and just ask Connection directly. But that turns out + * poorly. + * + * When the thread a mutex is invoked on isn't watched_thread we save it to this + * variable. + */ +PRThread *last_non_watched_thread = nullptr; + +/** + * Set a flag if the mutex is used on the thread we are watching, but always + * call the real mutex function. + */ +extern "C" void wrapped_MutexEnter(sqlite3_mutex *mutex) +{ + PRThread *curThread = ::PR_GetCurrentThread(); + if (curThread == watched_thread) + mutex_used_on_watched_thread = true; + else + last_non_watched_thread = curThread; + orig_mutex_methods.xMutexEnter(mutex); +} + +extern "C" int wrapped_MutexTry(sqlite3_mutex *mutex) +{ + if (::PR_GetCurrentThread() == watched_thread) + mutex_used_on_watched_thread = true; + return orig_mutex_methods.xMutexTry(mutex); +} + + +#define do_check_ok(aInvoc) do_check_true((aInvoc) == SQLITE_OK) + +void hook_sqlite_mutex() +{ + // We need to initialize and teardown SQLite to get it to set up the + // default mutex handlers for us so we can steal them and wrap them. + do_check_ok(sqlite3_initialize()); + do_check_ok(sqlite3_shutdown()); + do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &orig_mutex_methods)); + do_check_ok(::sqlite3_config(SQLITE_CONFIG_GETMUTEX, &wrapped_mutex_methods)); + wrapped_mutex_methods.xMutexEnter = wrapped_MutexEnter; + wrapped_mutex_methods.xMutexTry = wrapped_MutexTry; + do_check_ok(::sqlite3_config(SQLITE_CONFIG_MUTEX, &wrapped_mutex_methods)); +} + +/** + * Call to clear the watch state and to set the watching against this thread. + * + * Check |mutex_used_on_watched_thread| to see if the mutex has fired since + * this method was last called. Since we're talking about the current thread, + * there are no race issues to be concerned about + */ +void watch_for_mutex_use_on_this_thread() +{ + watched_thread = ::PR_GetCurrentThread(); + mutex_used_on_watched_thread = false; +} + + +//////////////////////////////////////////////////////////////////////////////// +//// Thread Wedgers + +/** + * A runnable that blocks until code on another thread invokes its unwedge + * method. By dispatching this to a thread you can ensure that no subsequent + * runnables dispatched to the thread will execute until you invoke unwedge. + * + * The wedger is self-dispatching, just construct it with its target. + */ +class ThreadWedger : public nsRunnable +{ +public: + explicit ThreadWedger(nsIEventTarget *aTarget) + : mReentrantMonitor("thread wedger") + , unwedged(false) + { + aTarget->Dispatch(this, aTarget->NS_DISPATCH_NORMAL); + } + + NS_IMETHOD Run() + { + ReentrantMonitorAutoEnter automon(mReentrantMonitor); + + if (!unwedged) + automon.Wait(); + + return NS_OK; + } + + void unwedge() + { + ReentrantMonitorAutoEnter automon(mReentrantMonitor); + unwedged = true; + automon.Notify(); + } + +private: + ReentrantMonitor mReentrantMonitor; + bool unwedged; +}; + +//////////////////////////////////////////////////////////////////////////////// +//// Async Helpers + +/** + * A horrible hack to figure out what the connection's async thread is. By + * creating a statement and async dispatching we can tell from the mutex who + * is the async thread, PRThread style. Then we map that to an nsIThread. + */ +already_AddRefed +get_conn_async_thread(mozIStorageConnection *db) +{ + // Make sure we are tracking the current thread as the watched thread + watch_for_mutex_use_on_this_thread(); + + // - statement with nothing to bind + nsCOMPtr stmt; + db->CreateAsyncStatement( + NS_LITERAL_CSTRING("SELECT 1"), + getter_AddRefs(stmt)); + blocking_async_execute(stmt); + stmt->Finalize(); + + nsCOMPtr threadMan = + do_GetService("@mozilla.org/thread-manager;1"); + nsCOMPtr asyncThread; + threadMan->GetThreadFromPRThread(last_non_watched_thread, + getter_AddRefs(asyncThread)); + + // Additionally, check that the thread we get as the background thread is the + // same one as the one we report from getInterface. + nsCOMPtr target = do_GetInterface(db); + nsCOMPtr allegedAsyncThread = do_QueryInterface(target); + PRThread *allegedPRThread; + (void)allegedAsyncThread->GetPRThread(&allegedPRThread); + do_check_eq(allegedPRThread, last_non_watched_thread); + return asyncThread.forget(); +} + //////////////////////////////////////////////////////////////////////////////// //// Tests diff --git a/storage/test/unit/test_storage_connection.js b/storage/test/unit/test_storage_connection.js index 23db1815e6db..66de13757ccf 100644 --- a/storage/test/unit/test_storage_connection.js +++ b/storage/test/unit/test_storage_connection.js @@ -736,7 +736,6 @@ add_task(function test_clone_copies_pragmas() { name: "journal_size_limit", value: 524288, copied: true }, { name: "synchronous", value: 2, copied: true }, { name: "wal_autocheckpoint", value: 16, copied: true }, - { name: "busy_timeout", value: 50, copied: true }, { name: "ignore_check_constraints", value: 1, copied: false }, ]; @@ -779,7 +778,6 @@ add_task(function test_readonly_clone_copies_pragmas() { name: "journal_size_limit", value: 524288, copied: false }, { name: "synchronous", value: 2, copied: false }, { name: "wal_autocheckpoint", value: 16, copied: false }, - { name: "busy_timeout", value: 50, copied: false }, { name: "ignore_check_constraints", value: 1, copied: false }, ]; From fd72b08f1c9361606a0f5352051a894d6c96a6c3 Mon Sep 17 00:00:00 2001 From: Paul Rouget Date: Wed, 10 Sep 2014 08:47:26 -0400 Subject: [PATCH 49/88] Bug 1050386 - [timeline] build a temporary timeline panel, r=paul --HG-- rename : browser/devtools/shared/test/browser_graphs-07.js => browser/devtools/shared/test/browser_graphs-07a.js --- browser/app/profile/firefox.js | 3 +- browser/devtools/jar.mn | 2 + browser/devtools/main.js | 48 +- browser/devtools/moz.build | 3 +- .../browser_profiler_data-massaging-01.js | 2 +- browser/devtools/shared/test/browser.ini | 3 +- ...ser_graphs-07.js => browser_graphs-07a.js} | 0 .../shared/test/browser_graphs-07b.js | 69 +++ browser/devtools/shared/widgets/Graphs.jsm | 19 +- browser/devtools/timeline/moz.build | 13 + browser/devtools/timeline/panel.js | 63 +++ browser/devtools/timeline/test/browser.ini | 17 + ...browser_timeline_aaa_run_first_leaktest.js | 22 + .../test/browser_timeline_blueprint.js | 29 ++ ..._timeline_overview-initial-selection-01.js | 38 ++ ..._timeline_overview-initial-selection-02.js | 32 ++ .../test/browser_timeline_overview-update.js | 48 ++ .../timeline/test/browser_timeline_panels.js | 42 ++ .../test/browser_timeline_recording.js | 34 ++ .../browser_timeline_waterfall-background.js | 47 ++ .../browser_timeline_waterfall-generic.js | 68 +++ .../test/browser_timeline_waterfall-styles.js | 89 ++++ .../timeline/test/doc_simple-test.html | 26 + browser/devtools/timeline/test/head.js | 133 ++++++ browser/devtools/timeline/timeline.js | 281 +++++++++++ browser/devtools/timeline/timeline.xul | 68 +++ browser/devtools/timeline/widgets/global.js | 51 ++ browser/devtools/timeline/widgets/overview.js | 208 ++++++++ .../devtools/timeline/widgets/waterfall.js | 444 ++++++++++++++++++ .../chrome/browser/devtools/timeline.dtd | 30 ++ .../browser/devtools/timeline.properties | 40 ++ browser/locales/jar.mn | 2 + browser/themes/linux/devtools/timeline.css | 5 + browser/themes/linux/jar.mn | 1 + browser/themes/osx/devtools/timeline.css | 6 + browser/themes/osx/jar.mn | 1 + .../themes/shared/devtools/timeline.inc.css | 159 +++++++ browser/themes/windows/devtools/timeline.css | 5 + browser/themes/windows/jar.mn | 2 + toolkit/devtools/server/actors/timeline.js | 11 +- 40 files changed, 2146 insertions(+), 18 deletions(-) rename browser/devtools/shared/test/{browser_graphs-07.js => browser_graphs-07a.js} (100%) create mode 100644 browser/devtools/shared/test/browser_graphs-07b.js create mode 100644 browser/devtools/timeline/moz.build create mode 100644 browser/devtools/timeline/panel.js create mode 100644 browser/devtools/timeline/test/browser.ini create mode 100644 browser/devtools/timeline/test/browser_timeline_aaa_run_first_leaktest.js create mode 100644 browser/devtools/timeline/test/browser_timeline_blueprint.js create mode 100644 browser/devtools/timeline/test/browser_timeline_overview-initial-selection-01.js create mode 100644 browser/devtools/timeline/test/browser_timeline_overview-initial-selection-02.js create mode 100644 browser/devtools/timeline/test/browser_timeline_overview-update.js create mode 100644 browser/devtools/timeline/test/browser_timeline_panels.js create mode 100644 browser/devtools/timeline/test/browser_timeline_recording.js create mode 100644 browser/devtools/timeline/test/browser_timeline_waterfall-background.js create mode 100644 browser/devtools/timeline/test/browser_timeline_waterfall-generic.js create mode 100644 browser/devtools/timeline/test/browser_timeline_waterfall-styles.js create mode 100644 browser/devtools/timeline/test/doc_simple-test.html create mode 100644 browser/devtools/timeline/test/head.js create mode 100644 browser/devtools/timeline/timeline.js create mode 100644 browser/devtools/timeline/timeline.xul create mode 100644 browser/devtools/timeline/widgets/global.js create mode 100644 browser/devtools/timeline/widgets/overview.js create mode 100644 browser/devtools/timeline/widgets/waterfall.js create mode 100644 browser/locales/en-US/chrome/browser/devtools/timeline.dtd create mode 100644 browser/locales/en-US/chrome/browser/devtools/timeline.properties create mode 100644 browser/themes/linux/devtools/timeline.css create mode 100644 browser/themes/osx/devtools/timeline.css create mode 100644 browser/themes/shared/devtools/timeline.inc.css create mode 100644 browser/themes/windows/devtools/timeline.css diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 685631d90c7e..a1f8b308bad9 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1372,8 +1372,9 @@ pref("devtools.debugger.ui.variables-sorting-enabled", true); pref("devtools.debugger.ui.variables-only-enum-visible", false); pref("devtools.debugger.ui.variables-searchbox-visible", false); -// Enable the Profiler +// Enable the Profiler and the Timeline pref("devtools.profiler.enabled", true); +pref("devtools.timeline.enabled", false); // The default Profiler UI settings pref("devtools.profiler.ui.show-platform-data", false); diff --git a/browser/devtools/jar.mn b/browser/devtools/jar.mn index e64dcb24bd22..192ac9ac6af7 100644 --- a/browser/devtools/jar.mn +++ b/browser/devtools/jar.mn @@ -119,3 +119,5 @@ browser.jar: content/browser/devtools/eyedropper.xul (eyedropper/eyedropper.xul) content/browser/devtools/eyedropper/crosshairs.css (eyedropper/crosshairs.css) content/browser/devtools/eyedropper/nocursor.css (eyedropper/nocursor.css) + content/browser/devtools/timeline/timeline.xul (timeline/timeline.xul) + content/browser/devtools/timeline/timeline.js (timeline/timeline.js) diff --git a/browser/devtools/main.js b/browser/devtools/main.js index c5ba45b2c02f..9a73c393b072 100644 --- a/browser/devtools/main.js +++ b/browser/devtools/main.js @@ -31,25 +31,28 @@ loader.lazyGetter(this, "ShaderEditorPanel", () => require("devtools/shaderedito loader.lazyGetter(this, "CanvasDebuggerPanel", () => require("devtools/canvasdebugger/panel").CanvasDebuggerPanel); loader.lazyGetter(this, "WebAudioEditorPanel", () => require("devtools/webaudioeditor/panel").WebAudioEditorPanel); loader.lazyGetter(this, "ProfilerPanel", () => require("devtools/profiler/panel").ProfilerPanel); +loader.lazyGetter(this, "TimelinePanel", () => require("devtools/timeline/panel").TimelinePanel); loader.lazyGetter(this, "NetMonitorPanel", () => require("devtools/netmonitor/panel").NetMonitorPanel); -loader.lazyGetter(this, "ScratchpadPanel", () => require("devtools/scratchpad/scratchpad-panel").ScratchpadPanel); loader.lazyGetter(this, "StoragePanel", () => require("devtools/storage/panel").StoragePanel); +loader.lazyGetter(this, "ScratchpadPanel", () => require("devtools/scratchpad/scratchpad-panel").ScratchpadPanel); // Strings const toolboxProps = "chrome://browser/locale/devtools/toolbox.properties"; const inspectorProps = "chrome://browser/locale/devtools/inspector.properties"; +const webConsoleProps = "chrome://browser/locale/devtools/webconsole.properties"; const debuggerProps = "chrome://browser/locale/devtools/debugger.properties"; const styleEditorProps = "chrome://browser/locale/devtools/styleeditor.properties"; const shaderEditorProps = "chrome://browser/locale/devtools/shadereditor.properties"; const canvasDebuggerProps = "chrome://browser/locale/devtools/canvasdebugger.properties"; const webAudioEditorProps = "chrome://browser/locale/devtools/webaudioeditor.properties"; -const webConsoleProps = "chrome://browser/locale/devtools/webconsole.properties"; const profilerProps = "chrome://browser/locale/devtools/profiler.properties"; +const timelineProps = "chrome://browser/locale/devtools/timeline.properties"; const netMonitorProps = "chrome://browser/locale/devtools/netmonitor.properties"; -const scratchpadProps = "chrome://browser/locale/devtools/scratchpad.properties"; const storageProps = "chrome://browser/locale/devtools/storage.properties"; +const scratchpadProps = "chrome://browser/locale/devtools/scratchpad.properties"; loader.lazyGetter(this, "toolboxStrings", () => Services.strings.createBundle(toolboxProps)); +loader.lazyGetter(this, "profilerStrings",() => Services.strings.createBundle(profilerProps)); loader.lazyGetter(this, "webConsoleStrings", () => Services.strings.createBundle(webConsoleProps)); loader.lazyGetter(this, "debuggerStrings", () => Services.strings.createBundle(debuggerProps)); loader.lazyGetter(this, "styleEditorStrings", () => Services.strings.createBundle(styleEditorProps)); @@ -57,10 +60,10 @@ loader.lazyGetter(this, "shaderEditorStrings", () => Services.strings.createBund loader.lazyGetter(this, "canvasDebuggerStrings", () => Services.strings.createBundle(canvasDebuggerProps)); loader.lazyGetter(this, "webAudioEditorStrings", () => Services.strings.createBundle(webAudioEditorProps)); loader.lazyGetter(this, "inspectorStrings", () => Services.strings.createBundle(inspectorProps)); -loader.lazyGetter(this, "profilerStrings",() => Services.strings.createBundle(profilerProps)); +loader.lazyGetter(this, "timelineStrings", () => Services.strings.createBundle(timelineProps)); loader.lazyGetter(this, "netMonitorStrings", () => Services.strings.createBundle(netMonitorProps)); -loader.lazyGetter(this, "scratchpadStrings", () => Services.strings.createBundle(scratchpadProps)); loader.lazyGetter(this, "storageStrings", () => Services.strings.createBundle(storageProps)); +loader.lazyGetter(this, "scratchpadStrings", () => Services.strings.createBundle(scratchpadProps)); let Tools = {}; exports.Tools = Tools; @@ -78,9 +81,11 @@ Tools.options = { panelLabel: l10n("options.panelLabel", toolboxStrings), tooltip: l10n("optionsButton.tooltip", toolboxStrings), inMenu: false, + isTargetSupported: function(target) { return true; }, + build: function(iframeWindow, toolbox) { return new OptionsPanel(iframeWindow, toolbox); } @@ -113,6 +118,7 @@ Tools.webConsole = { isTargetSupported: function(target) { return true; }, + build: function(iframeWindow, toolbox) { return new WebConsolePanel(iframeWindow, toolbox); } @@ -230,11 +236,13 @@ Tools.canvasDebugger = { label: l10n("ToolboxCanvasDebugger.label", canvasDebuggerStrings), panelLabel: l10n("ToolboxCanvasDebugger.panelLabel", canvasDebuggerStrings), tooltip: l10n("ToolboxCanvasDebugger.tooltip", canvasDebuggerStrings), + // Hide the Canvas Debugger in the Add-on Debugger and Browser Toolbox // (bug 1047520). isTargetSupported: function(target) { return !target.isAddon && !target.chrome; }, + build: function (iframeWindow, toolbox) { return new CanvasDebuggerPanel(iframeWindow, toolbox); } @@ -250,9 +258,11 @@ Tools.webAudioEditor = { label: l10n("ToolboxWebAudioEditor1.label", webAudioEditorStrings), panelLabel: l10n("ToolboxWebAudioEditor1.panelLabel", webAudioEditorStrings), tooltip: l10n("ToolboxWebAudioEditor1.tooltip", webAudioEditorStrings), + isTargetSupported: function(target) { return !target.isAddon; }, + build: function(iframeWindow, toolbox) { return new WebAudioEditorPanel(iframeWindow, toolbox); } @@ -284,11 +294,32 @@ Tools.jsprofiler = { } }; +Tools.timeline = { + id: "timeline", + ordinal: 8, + visibilityswitch: "devtools.timeline.enabled", + icon: "chrome://browser/skin/devtools/tool-network.svg", + invertIconForLightTheme: true, + url: "chrome://browser/content/devtools/timeline/timeline.xul", + label: l10n("timeline.label", timelineStrings), + panelLabel: l10n("timeline.panelLabel", timelineStrings), + tooltip: l10n("timeline.tooltip", timelineStrings), + + isTargetSupported: function(target) { + return !target.isAddon; + }, + + build: function (iframeWindow, toolbox) { + let panel = new TimelinePanel(iframeWindow, toolbox); + return panel.open(); + } +}; + Tools.netMonitor = { id: "netmonitor", accesskey: l10n("netmonitor.accesskey", netMonitorStrings), key: l10n("netmonitor.commandkey", netMonitorStrings), - ordinal: 8, + ordinal: 9, modifiers: osString == "Darwin" ? "accel,alt" : "accel,shift", visibilityswitch: "devtools.netmonitor.enabled", icon: "chrome://browser/skin/devtools/tool-network.svg", @@ -312,7 +343,7 @@ Tools.netMonitor = { Tools.storage = { id: "storage", key: l10n("storage.commandkey", storageStrings), - ordinal: 9, + ordinal: 10, accesskey: l10n("storage.accesskey", storageStrings), modifiers: "shift", visibilityswitch: "devtools.storage.enabled", @@ -337,7 +368,7 @@ Tools.storage = { Tools.scratchpad = { id: "scratchpad", - ordinal: 10, + ordinal: 11, visibilityswitch: "devtools.scratchpad.enabled", icon: "chrome://browser/skin/devtools/tool-scratchpad.svg", invertIconForLightTheme: true, @@ -367,6 +398,7 @@ let defaultTools = [ Tools.canvasDebugger, Tools.webAudioEditor, Tools.jsprofiler, + Tools.timeline, Tools.netMonitor, Tools.storage, Tools.scratchpad diff --git a/browser/devtools/moz.build b/browser/devtools/moz.build index f7125f3ae757..f1327bdfefb9 100644 --- a/browser/devtools/moz.build +++ b/browser/devtools/moz.build @@ -13,11 +13,11 @@ DIRS += [ 'fontinspector', 'framework', 'inspector', - 'projecteditor', 'layoutview', 'markupview', 'netmonitor', 'profiler', + 'projecteditor', 'responsivedesign', 'scratchpad', 'shadereditor', @@ -27,6 +27,7 @@ DIRS += [ 'styleeditor', 'styleinspector', 'tilt', + 'timeline', 'webaudioeditor', 'webconsole', 'webide', diff --git a/browser/devtools/profiler/test/browser_profiler_data-massaging-01.js b/browser/devtools/profiler/test/browser_profiler_data-massaging-01.js index 2111d9d1368b..dac9da56ef0e 100644 --- a/browser/devtools/profiler/test/browser_profiler_data-massaging-01.js +++ b/browser/devtools/profiler/test/browser_profiler_data-massaging-01.js @@ -1,4 +1,4 @@ -/* Any copyright is dedicated to the Public Domain. +s/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ /** diff --git a/browser/devtools/shared/test/browser.ini b/browser/devtools/shared/test/browser.ini index b74db4b57c09..7a6f4ab54f38 100644 --- a/browser/devtools/shared/test/browser.ini +++ b/browser/devtools/shared/test/browser.ini @@ -21,7 +21,8 @@ support-files = [browser_graphs-04.js] [browser_graphs-05.js] [browser_graphs-06.js] -[browser_graphs-07.js] +[browser_graphs-07a.js] +[browser_graphs-07b.js] [browser_graphs-08.js] [browser_graphs-09.js] [browser_graphs-10a.js] diff --git a/browser/devtools/shared/test/browser_graphs-07.js b/browser/devtools/shared/test/browser_graphs-07a.js similarity index 100% rename from browser/devtools/shared/test/browser_graphs-07.js rename to browser/devtools/shared/test/browser_graphs-07a.js diff --git a/browser/devtools/shared/test/browser_graphs-07b.js b/browser/devtools/shared/test/browser_graphs-07b.js new file mode 100644 index 000000000000..54f519148bc9 --- /dev/null +++ b/browser/devtools/shared/test/browser_graphs-07b.js @@ -0,0 +1,69 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests if selections can't be added via clicking, while not allowed. + +const TEST_DATA = [{ delta: 112, value: 48 }, { delta: 213, value: 59 }, { delta: 313, value: 60 }, { delta: 413, value: 59 }, { delta: 530, value: 59 }, { delta: 646, value: 58 }, { delta: 747, value: 60 }, { delta: 863, value: 48 }, { delta: 980, value: 37 }, { delta: 1097, value: 30 }, { delta: 1213, value: 29 }, { delta: 1330, value: 23 }, { delta: 1430, value: 10 }, { delta: 1534, value: 17 }, { delta: 1645, value: 20 }, { delta: 1746, value: 22 }, { delta: 1846, value: 39 }, { delta: 1963, value: 26 }, { delta: 2080, value: 27 }, { delta: 2197, value: 35 }, { delta: 2312, value: 47 }, { delta: 2412, value: 53 }, { delta: 2514, value: 60 }, { delta: 2630, value: 37 }, { delta: 2730, value: 36 }, { delta: 2830, value: 37 }, { delta: 2946, value: 36 }, { delta: 3046, value: 40 }, { delta: 3163, value: 47 }, { delta: 3280, value: 41 }, { delta: 3380, value: 35 }, { delta: 3480, value: 27 }, { delta: 3580, value: 39 }, { delta: 3680, value: 42 }, { delta: 3780, value: 49 }, { delta: 3880, value: 55 }, { delta: 3980, value: 60 }, { delta: 4080, value: 60 }, { delta: 4180, value: 60 }]; +let {LineGraphWidget} = Cu.import("resource:///modules/devtools/Graphs.jsm", {}); +let {DOMHelpers} = Cu.import("resource:///modules/devtools/DOMHelpers.jsm", {}); +let {Promise} = devtools.require("resource://gre/modules/Promise.jsm"); +let {Hosts} = devtools.require("devtools/framework/toolbox-hosts"); + +let test = Task.async(function*() { + yield promiseTab("about:blank"); + yield performTest(); + gBrowser.removeCurrentTab(); + finish(); +}); + +function* performTest() { + let [host, win, doc] = yield createHost(); + let graph = new LineGraphWidget(doc.body, "fps"); + yield graph.once("ready"); + + testGraph(graph); + + graph.destroy(); + host.destroy(); +} + +function testGraph(graph) { + graph.setData(TEST_DATA); + graph.selectionEnabled = false; + + info("Attempting to make a selection."); + + dragStart(graph, 300); + is(graph.hasSelection() || graph.hasSelectionInProgress(), false, + "The graph shouldn't have a selection (1)."); + + hover(graph, 400); + is(graph.hasSelection() || graph.hasSelectionInProgress(), false, + "The graph shouldn't have a selection (2)."); + + dragStop(graph, 500); + is(graph.hasSelection() || graph.hasSelectionInProgress(), false, + "The graph shouldn't have a selection (3)."); +} + +// EventUtils just doesn't work! + +function hover(graph, x, y = 1) { + x /= window.devicePixelRatio; + y /= window.devicePixelRatio; + graph._onMouseMove({ clientX: x, clientY: y }); +} + +function dragStart(graph, x, y = 1) { + x /= window.devicePixelRatio; + y /= window.devicePixelRatio; + graph._onMouseMove({ clientX: x, clientY: y }); + graph._onMouseDown({ clientX: x, clientY: y }); +} + +function dragStop(graph, x, y = 1) { + x /= window.devicePixelRatio; + y /= window.devicePixelRatio; + graph._onMouseMove({ clientX: x, clientY: y }); + graph._onMouseUp({ clientX: x, clientY: y }); +} diff --git a/browser/devtools/shared/widgets/Graphs.jsm b/browser/devtools/shared/widgets/Graphs.jsm index 7ee49e8c7113..9b4b901e2207 100644 --- a/browser/devtools/shared/widgets/Graphs.jsm +++ b/browser/devtools/shared/widgets/Graphs.jsm @@ -10,7 +10,12 @@ const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise; const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js", {}); const {Task} = Cu.import("resource://gre/modules/Task.jsm", {}); -this.EXPORTED_SYMBOLS = ["LineGraphWidget", "BarGraphWidget", "CanvasGraphUtils"]; +this.EXPORTED_SYMBOLS = [ + "AbstractCanvasGraph", + "LineGraphWidget", + "BarGraphWidget", + "CanvasGraphUtils" +]; const HTML_NS = "http://www.w3.org/1999/xhtml"; const GRAPH_SRC = "chrome://browser/content/devtools/graphs-frame.xhtml"; @@ -494,6 +499,12 @@ AbstractCanvasGraph.prototype = { return this._selection.start != null && this._selection.end == null; }, + /** + * Specifies whether or not mouse selection is allowed. + * @type boolean + */ + selectionEnabled: true, + /** * Sets the selection bounds. * Use `dropCursor` to hide the cursor. @@ -955,6 +966,9 @@ AbstractCanvasGraph.prototype = { switch (this._canvas.getAttribute("input")) { case "hovering-background": case "hovering-region": + if (!this.selectionEnabled) { + break; + } this._selection.start = mouseX; this._selection.end = null; this.emit("selecting"); @@ -990,6 +1004,9 @@ AbstractCanvasGraph.prototype = { switch (this._canvas.getAttribute("input")) { case "hovering-background": case "hovering-region": + if (!this.selectionEnabled) { + break; + } if (this.getSelectionWidth() < 1) { let region = this.getHoveredRegion(); if (region) { diff --git a/browser/devtools/timeline/moz.build b/browser/devtools/timeline/moz.build new file mode 100644 index 000000000000..bd58f06caf19 --- /dev/null +++ b/browser/devtools/timeline/moz.build @@ -0,0 +1,13 @@ +# vim: set filetype=python: +# 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/. + +EXTRA_JS_MODULES.devtools.timeline += [ + 'panel.js', + 'widgets/global.js', + 'widgets/overview.js', + 'widgets/waterfall.js' +] + +BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] diff --git a/browser/devtools/timeline/panel.js b/browser/devtools/timeline/panel.js new file mode 100644 index 000000000000..a2308f25941e --- /dev/null +++ b/browser/devtools/timeline/panel.js @@ -0,0 +1,63 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript 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/. */ +"use strict"; + +const { Cc, Ci, Cu, Cr } = require("chrome"); + +Cu.import("resource://gre/modules/Task.jsm"); + +loader.lazyRequireGetter(this, "promise"); +loader.lazyRequireGetter(this, "EventEmitter", + "devtools/toolkit/event-emitter"); + +loader.lazyRequireGetter(this, "TimelineFront", + "devtools/server/actors/timeline", true); + +function TimelinePanel(iframeWindow, toolbox) { + this.panelWin = iframeWindow; + this._toolbox = toolbox; + + EventEmitter.decorate(this); +}; + +exports.TimelinePanel = TimelinePanel; + +TimelinePanel.prototype = { + /** + * Open is effectively an asynchronous constructor. + * + * @return object + * A promise that is resolved when the timeline completes opening. + */ + open: Task.async(function*() { + // Local debugging needs to make the target remote. + yield this.target.makeRemote(); + + this.panelWin.gToolbox = this._toolbox; + this.panelWin.gTarget = this.target; + this.panelWin.gFront = new TimelineFront(this.target.client, this.target.form); + yield this.panelWin.startupTimeline(); + + this.isReady = true; + this.emit("ready"); + return this; + }), + + // DevToolPanel API + + get target() this._toolbox.target, + + destroy: Task.async(function*() { + // Make sure this panel is not already destroyed. + if (this._destroyed) { + return; + } + + yield this.panelWin.shutdownTimeline(); + this.emit("destroyed"); + this._destroyed = true; + }) +}; diff --git a/browser/devtools/timeline/test/browser.ini b/browser/devtools/timeline/test/browser.ini new file mode 100644 index 000000000000..6c222a949893 --- /dev/null +++ b/browser/devtools/timeline/test/browser.ini @@ -0,0 +1,17 @@ +[DEFAULT] +skip-if = e10s # Bug 1065355 - devtools tests disabled with e10s +subsuite = devtools +support-files = + doc_simple-test.html + head.js + +[browser_timeline_aaa_run_first_leaktest.js] +[browser_timeline_blueprint.js] +[browser_timeline_overview-initial-selection-01.js] +[browser_timeline_overview-initial-selection-02.js] +[browser_timeline_overview-update.js] +[browser_timeline_panels.js] +[browser_timeline_recording.js] +[browser_timeline_waterfall-background.js] +[browser_timeline_waterfall-generic.js] +[browser_timeline_waterfall-styles.js] diff --git a/browser/devtools/timeline/test/browser_timeline_aaa_run_first_leaktest.js b/browser/devtools/timeline/test/browser_timeline_aaa_run_first_leaktest.js new file mode 100644 index 000000000000..cdde8855dc24 --- /dev/null +++ b/browser/devtools/timeline/test/browser_timeline_aaa_run_first_leaktest.js @@ -0,0 +1,22 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the timeline leaks on initialization and sudden destruction. + * You can also use this initialization format as a template for other tests. + */ + +let test = Task.async(function*() { + let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL); + + ok(target, "Should have a target available."); + ok(debuggee, "Should have a debuggee available."); + ok(panel, "Should have a panel available."); + + ok(panel.panelWin.gToolbox, "Should have a toolbox reference on the panel window."); + ok(panel.panelWin.gTarget, "Should have a target reference on the panel window."); + ok(panel.panelWin.gFront, "Should have a front reference on the panel window."); + + yield teardown(panel); + finish(); +}); diff --git a/browser/devtools/timeline/test/browser_timeline_blueprint.js b/browser/devtools/timeline/test/browser_timeline_blueprint.js new file mode 100644 index 000000000000..6964acda7bf0 --- /dev/null +++ b/browser/devtools/timeline/test/browser_timeline_blueprint.js @@ -0,0 +1,29 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the timeline blueprint has a correct structure. + */ + +function test() { + let { TIMELINE_BLUEPRINT } = devtools.require("devtools/timeline/global"); + + ok(TIMELINE_BLUEPRINT, + "A timeline blueprint should be available."); + + ok(Object.keys(TIMELINE_BLUEPRINT).length, + "The timeline blueprint has at least one entry."); + + for (let [key, value] of Iterator(TIMELINE_BLUEPRINT)) { + ok("group" in value, + "Each entry in the timeline blueprint contains a `group` key."); + ok("fill" in value, + "Each entry in the timeline blueprint contains a `fill` key."); + ok("stroke" in value, + "Each entry in the timeline blueprint contains a `stroke` key."); + ok("label" in value, + "Each entry in the timeline blueprint contains a `label` key."); + } + + finish(); +} diff --git a/browser/devtools/timeline/test/browser_timeline_overview-initial-selection-01.js b/browser/devtools/timeline/test/browser_timeline_overview-initial-selection-01.js new file mode 100644 index 000000000000..fa4cc8c7dd49 --- /dev/null +++ b/browser/devtools/timeline/test/browser_timeline_overview-initial-selection-01.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the overview has an initial selection when recording has finished + * and there is data available. + */ + +let test = Task.async(function*() { + let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL); + let { EVENTS, TimelineView, TimelineController } = panel.panelWin; + let { OVERVIEW_INITIAL_SELECTION_RATIO: selectionRatio } = panel.panelWin; + + yield TimelineController.toggleRecording(); + ok(true, "Recording has started."); + + let updated = 0; + panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++); + + ok((yield waitUntil(() => updated > 10)), + "The overview graph was updated a bunch of times."); + ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)), + "There are some markers available."); + + yield TimelineController.toggleRecording(); + ok(true, "Recording has ended."); + + let markers = TimelineController.getMarkers(); + let selection = TimelineView.overview.getSelection(); + + is(selection.start, markers[0].start * TimelineView.overview.dataScaleX, + "The initial selection start is correct."); + is(selection.end - selection.start, TimelineView.overview.width * selectionRatio, + "The initial selection end is correct."); + + yield teardown(panel); + finish(); +}); diff --git a/browser/devtools/timeline/test/browser_timeline_overview-initial-selection-02.js b/browser/devtools/timeline/test/browser_timeline_overview-initial-selection-02.js new file mode 100644 index 000000000000..fd2070218c40 --- /dev/null +++ b/browser/devtools/timeline/test/browser_timeline_overview-initial-selection-02.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the overview has no initial selection when recording has finished + * and there is no data available. + */ + +let test = Task.async(function*() { + let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL); + let { EVENTS, TimelineView, TimelineController } = panel.panelWin; + let { OVERVIEW_INITIAL_SELECTION_RATIO: selectionRatio } = panel.panelWin; + + yield TimelineController.toggleRecording(); + ok(true, "Recording has started."); + + yield TimelineController._stopRecordingAndDiscardData(); + ok(true, "Recording has ended."); + + let markers = TimelineController.getMarkers(); + let selection = TimelineView.overview.getSelection(); + + is(markers.length, 0, + "There are no markers available."); + is(selection.start, null, + "The initial selection start is correct."); + is(selection.end, null, + "The initial selection end is correct."); + + yield teardown(panel); + finish(); +}); diff --git a/browser/devtools/timeline/test/browser_timeline_overview-update.js b/browser/devtools/timeline/test/browser_timeline_overview-update.js new file mode 100644 index 000000000000..2dcc28fbca8c --- /dev/null +++ b/browser/devtools/timeline/test/browser_timeline_overview-update.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the overview graph is continuously updated. + */ + +let test = Task.async(function*() { + let [target, debuggee, panel] = yield initTimelinePanel("about:blank"); + let { EVENTS, TimelineView, TimelineController } = panel.panelWin; + + yield DevToolsUtils.waitForTime(1000); + yield TimelineController.toggleRecording(); + ok(true, "Recording has started."); + + ok("selectionEnabled" in TimelineView.overview, + "The selection should not be enabled for the overview graph (1)."); + is(TimelineView.overview.selectionEnabled, false, + "The selection should not be enabled for the overview graph (2)."); + is(TimelineView.overview.hasSelection(), false, + "The overview graph shouldn't have a selection before recording."); + + let updated = 0; + panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++); + + ok((yield waitUntil(() => updated > 10)), + "The overview graph was updated a bunch of times."); + + ok("selectionEnabled" in TimelineView.overview, + "The selection should still not be enabled for the overview graph (3)."); + is(TimelineView.overview.selectionEnabled, false, + "The selection should still not be enabled for the overview graph (4)."); + is(TimelineView.overview.hasSelection(), false, + "The overview graph should not have a selection while recording."); + + yield TimelineController.toggleRecording(); + ok(true, "Recording has ended."); + + is(TimelineController.getMarkers().length, 0, + "There are no markers available."); + is(TimelineView.overview.selectionEnabled, true, + "The selection should now be enabled for the overview graph."); + is(TimelineView.overview.hasSelection(), false, + "The overview graph should not have a selection after recording."); + + yield teardown(panel); + finish(); +}); diff --git a/browser/devtools/timeline/test/browser_timeline_panels.js b/browser/devtools/timeline/test/browser_timeline_panels.js new file mode 100644 index 000000000000..a9acf53fd016 --- /dev/null +++ b/browser/devtools/timeline/test/browser_timeline_panels.js @@ -0,0 +1,42 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the timeline panels are correctly shown and hidden when + * recording starts and stops. + */ + +let test = Task.async(function*() { + let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL); + let { $, EVENTS } = panel.panelWin; + + is($("#record-button").hasAttribute("checked"), false, + "The record button should not be checked yet."); + is($("#timeline-pane").selectedPanel, $("#empty-notice"), + "An empty notice is initially displayed instead of the waterfall view."); + + let whenRecStarted = panel.panelWin.once(EVENTS.RECORDING_STARTED); + EventUtils.synthesizeMouseAtCenter($("#record-button"), {}, panel.panelWin); + yield whenRecStarted; + + ok(true, "Recording has started."); + + is($("#record-button").getAttribute("checked"), "true", + "The record button should be checked now."); + is($("#timeline-pane").selectedPanel, $("#recording-notice"), + "A recording notice is now displayed instead of the waterfall view."); + + let whenRecEnded = panel.panelWin.once(EVENTS.RECORDING_ENDED); + EventUtils.synthesizeMouseAtCenter($("#record-button"), {}, panel.panelWin); + yield whenRecEnded; + + ok(true, "Recording has ended."); + + is($("#record-button").hasAttribute("checked"), false, + "The record button should be unchecked again."); + is($("#timeline-pane").selectedPanel, $("#timeline-waterfall"), + "A waterfall view is now displayed."); + + yield teardown(panel); + finish(); +}); diff --git a/browser/devtools/timeline/test/browser_timeline_recording.js b/browser/devtools/timeline/test/browser_timeline_recording.js new file mode 100644 index 000000000000..e55338d77d4f --- /dev/null +++ b/browser/devtools/timeline/test/browser_timeline_recording.js @@ -0,0 +1,34 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the timeline can properly start and stop a recording. + */ + +let test = Task.async(function*() { + let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL); + let { gFront, TimelineController } = panel.panelWin; + + is((yield gFront.isRecording()), false, + "The timeline actor should not be recording when the tool starts."); + is(TimelineController.getMarkers().length, 0, + "There should be no markers available when the tool starts."); + + yield TimelineController.toggleRecording(); + + is((yield gFront.isRecording()), true, + "The timeline actor should be recording now."); + ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)), + "There are some markers available now."); + + ok("startTime" in TimelineController.getMarkers(), + "A `startTime` field was set on the markers array."); + ok("endTime" in TimelineController.getMarkers(), + "An `endTime` field was set on the markers array."); + ok(TimelineController.getMarkers().endTime > + TimelineController.getMarkers().startTime, + "Some time has passed since the recording started."); + + yield teardown(panel); + finish(); +}); diff --git a/browser/devtools/timeline/test/browser_timeline_waterfall-background.js b/browser/devtools/timeline/test/browser_timeline_waterfall-background.js new file mode 100644 index 000000000000..5f7c8ea3ec33 --- /dev/null +++ b/browser/devtools/timeline/test/browser_timeline_waterfall-background.js @@ -0,0 +1,47 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the waterfall background is a 1px high canvas stretching across + * the container bounds. + */ + +let test = Task.async(function*() { + let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL); + let { $, EVENTS, TimelineView, TimelineController } = panel.panelWin; + + yield TimelineController.toggleRecording(); + ok(true, "Recording has started."); + + let updated = 0; + panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++); + + ok((yield waitUntil(() => updated > 0)), + "The overview graph was updated a bunch of times."); + ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)), + "There are some markers available."); + + yield TimelineController.toggleRecording(); + ok(true, "Recording has ended."); + + // Test the waterfall background. + + let parentWidth = $("#timeline-waterfall").getBoundingClientRect().width; + let waterfallWidth = TimelineView.waterfall._waterfallWidth; + let sidebarWidth = 150; // px + is(waterfallWidth, parentWidth - sidebarWidth, + "The waterfall width is correct.") + + ok(TimelineView.waterfall._canvas, + "A canvas should be created after the recording ended."); + ok(TimelineView.waterfall._ctx, + "A 2d context should be created after the recording ended."); + + is(TimelineView.waterfall._canvas.width, waterfallWidth, + "The canvas width is correct."); + is(TimelineView.waterfall._canvas.height, 1, + "The canvas height is correct."); + + yield teardown(panel); + finish(); +}); diff --git a/browser/devtools/timeline/test/browser_timeline_waterfall-generic.js b/browser/devtools/timeline/test/browser_timeline_waterfall-generic.js new file mode 100644 index 000000000000..a4f3c827c648 --- /dev/null +++ b/browser/devtools/timeline/test/browser_timeline_waterfall-generic.js @@ -0,0 +1,68 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the waterfall is properly built after finishing a recording. + */ + +let test = Task.async(function*() { + let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL); + let { $, $$, EVENTS, TimelineController } = panel.panelWin; + + yield TimelineController.toggleRecording(); + ok(true, "Recording has started."); + + let updated = 0; + panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++); + + ok((yield waitUntil(() => updated > 0)), + "The overview graph was updated a bunch of times."); + ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)), + "There are some markers available."); + + yield TimelineController.toggleRecording(); + ok(true, "Recording has ended."); + + // Test the header container. + + ok($(".timeline-header-container"), + "A header container should have been created."); + + // Test the header sidebar (left). + + ok($(".timeline-header-sidebar"), + "A header sidebar node should have been created."); + ok($(".timeline-header-sidebar > .timeline-header-name"), + "A header name label should have been created inside the sidebar."); + + // Test the header ticks (right). + + ok($(".timeline-header-ticks"), + "A header ticks node should have been created."); + ok($$(".timeline-header-ticks > .timeline-header-tick").length > 0, + "Some header tick labels should have been created inside the tick node."); + + // Test the markers container. + + ok($(".timeline-marker-container"), + "A marker container should have been created."); + + // Test the markers sidebar (left). + + ok($$(".timeline-marker-sidebar").length, + "Some marker sidebar nodes should have been created."); + ok($$(".timeline-marker-sidebar > .timeline-marker-bullet").length, + "Some marker color bullets should have been created inside the sidebar."); + ok($$(".timeline-marker-sidebar > .timeline-marker-name").length, + "Some marker name labels should have been created inside the sidebar."); + + // Test the markers waterfall (right). + + ok($$(".timeline-marker-waterfall").length, + "Some marker waterfall nodes should have been created."); + ok($$(".timeline-marker-waterfall > .timeline-marker-bar").length, + "Some marker color bars should have been created inside the waterfall."); + + yield teardown(panel); + finish(); +}); diff --git a/browser/devtools/timeline/test/browser_timeline_waterfall-styles.js b/browser/devtools/timeline/test/browser_timeline_waterfall-styles.js new file mode 100644 index 000000000000..682efc3933f5 --- /dev/null +++ b/browser/devtools/timeline/test/browser_timeline_waterfall-styles.js @@ -0,0 +1,89 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests if the waterfall is properly built after making a selection + * and the child nodes are styled correctly. + */ + +var gRGB_TO_HSL = { + "rgb(193, 132, 214)": "hsl(285,50%,68%)", + "rgb(152, 61, 183)": "hsl(285,50%,48%)", + "rgb(161, 223, 138)": "hsl(104,57%,71%)", + "rgb(96, 201, 58)": "hsl(104,57%,51%)", + "rgb(240, 195, 111)": "hsl(39,82%,69%)", + "rgb(227, 155, 22)": "hsl(39,82%,49%)", +}; + +let test = Task.async(function*() { + let [target, debuggee, panel] = yield initTimelinePanel(SIMPLE_URL); + let { TIMELINE_BLUEPRINT } = devtools.require("devtools/timeline/global"); + let { $, $$, EVENTS, TimelineController } = panel.panelWin; + + yield TimelineController.toggleRecording(); + ok(true, "Recording has started."); + + let updated = 0; + panel.panelWin.on(EVENTS.OVERVIEW_UPDATED, () => updated++); + + ok((yield waitUntil(() => updated > 0)), + "The overview graph was updated a bunch of times."); + ok((yield waitUntil(() => TimelineController.getMarkers().length > 0)), + "There are some markers available."); + + yield TimelineController.toggleRecording(); + ok(true, "Recording has ended."); + + // Test the table sidebars. + + for (let sidebar of [ + ...$$(".timeline-header-sidebar"), + ...$$(".timeline-marker-sidebar") + ]) { + is(sidebar.getAttribute("width"), "150", + "The table's sidebar width is correct."); + } + + // Test the table ticks. + + for (let tick of $$(".timeline-header-tick")) { + ok(tick.getAttribute("value").match(/^\d+ ms$/), + "The table's timeline ticks appear to have correct labels."); + ok(tick.style.transform.match(/^translateX\(.*px\)$/), + "The table's timeline ticks appear to have proper translations."); + } + + // Test the marker bullets. + + for (let bullet of $$(".timeline-marker-bullet")) { + let type = bullet.getAttribute("type"); + + ok(type in TIMELINE_BLUEPRINT, + "The bullet type is present in the timeline blueprint."); + is(gRGB_TO_HSL[bullet.style.backgroundColor], TIMELINE_BLUEPRINT[type].fill, + "The bullet's background color is correct."); + is(gRGB_TO_HSL[bullet.style.borderColor], TIMELINE_BLUEPRINT[type].stroke, + "The bullet's border color is correct."); + } + + // Test the marker bars. + + for (let bar of $$(".timeline-marker-bar")) { + let type = bar.getAttribute("type"); + + ok(type in TIMELINE_BLUEPRINT, + "The bar type is present in the timeline blueprint."); + is(gRGB_TO_HSL[bar.style.backgroundColor], TIMELINE_BLUEPRINT[type].fill, + "The bar's background color is correct."); + is(gRGB_TO_HSL[bar.style.borderColor], TIMELINE_BLUEPRINT[type].stroke, + "The bar's border color is correct."); + + ok(bar.getAttribute("width") > 0, + "The bar appears to have a proper width."); + ok(bar.style.transform.match(/^translateX\(.*px\)$/), + "The bar appears to have proper translations."); + } + + yield teardown(panel); + finish(); +}); diff --git a/browser/devtools/timeline/test/doc_simple-test.html b/browser/devtools/timeline/test/doc_simple-test.html new file mode 100644 index 000000000000..c37e8514d43b --- /dev/null +++ b/browser/devtools/timeline/test/doc_simple-test.html @@ -0,0 +1,26 @@ + + + + + + + Timeline test page + + + + + + + diff --git a/browser/devtools/timeline/test/head.js b/browser/devtools/timeline/test/head.js new file mode 100644 index 000000000000..e89fa5f3fc05 --- /dev/null +++ b/browser/devtools/timeline/test/head.js @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +let { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); + +// Disable logging for all the tests. Both the debugger server and frontend will +// be affected by this pref. +let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log"); +Services.prefs.setBoolPref("devtools.debugger.log", false); + +// Enable the tool while testing. +let gToolEnabled = Services.prefs.getBoolPref("devtools.timeline.enabled"); +Services.prefs.setBoolPref("devtools.timeline.enabled", true); + +let { Task } = Cu.import("resource://gre/modules/Task.jsm", {}); +let { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); +let { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {}); +let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); +let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); + +let TargetFactory = devtools.TargetFactory; +let Toolbox = devtools.Toolbox; + +const EXAMPLE_URL = "http://example.com/browser/browser/devtools/timeline/test/"; +const SIMPLE_URL = EXAMPLE_URL + "doc_simple-test.html"; + +// All tests are asynchronous. +waitForExplicitFinish(); + +registerCleanupFunction(() => { + info("finish() was called, cleaning up..."); + Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging); + Services.prefs.setBoolPref("devtools.timeline.enabled", gToolEnabled); +}); + +function addTab(url) { + info("Adding tab: " + url); + + let deferred = promise.defer(); + let tab = gBrowser.selectedTab = gBrowser.addTab(url); + let linkedBrowser = tab.linkedBrowser; + + linkedBrowser.addEventListener("load", function onLoad() { + linkedBrowser.removeEventListener("load", onLoad, true); + info("Tab added and finished loading: " + url); + deferred.resolve(tab); + }, true); + + return deferred.promise; +} + +function removeTab(tab) { + info("Removing tab."); + + let deferred = promise.defer(); + let tabContainer = gBrowser.tabContainer; + + tabContainer.addEventListener("TabClose", function onClose(aEvent) { + tabContainer.removeEventListener("TabClose", onClose, false); + info("Tab removed and finished closing."); + deferred.resolve(); + }, false); + + gBrowser.removeTab(tab); + return deferred.promise; +} + +/** + * Spawns a new tab and starts up a toolbox with the timeline panel + * automatically selected. + * + * Must be used within a task. + * + * @param string url + * The location of the new tab to spawn. + * @return object + * A promise resolved once the timeline is initialized, with the + * [target, debuggee, panel] instances. + */ +function* initTimelinePanel(url) { + info("Initializing a timeline pane."); + + let tab = yield addTab(url); + let target = TargetFactory.forTab(tab); + let debuggee = target.window.wrappedJSObject; + + yield target.makeRemote(); + + let toolbox = yield gDevTools.showToolbox(target, "timeline"); + let panel = toolbox.getCurrentPanel(); + return [target, debuggee, panel]; +} + +/** + * Closes a tab and destroys the toolbox holding a timeline panel. + * + * Must be used within a task. + * + * @param object panel + * The timeline panel, created by the toolbox. + * @return object + * A promise resolved once the timeline, toolbox and debuggee tab + * are destroyed. + */ +function* teardown(panel) { + info("Destroying the specified timeline."); + + let tab = panel.target.tab; + yield panel._toolbox.destroy(); + yield removeTab(tab); +} + +/** + * Waits until a predicate returns true. + * + * @param function predicate + * Invoked once in a while until it returns true. + * @param number interval [optional] + * How often the predicate is invoked, in milliseconds. + */ +function waitUntil(predicate, interval = 10) { + if (predicate()) { + return promise.resolve(true); + } + let deferred = promise.defer(); + setTimeout(function() { + waitUntil(predicate).then(() => deferred.resolve(true)); + }, interval); + return deferred.promise; +} diff --git a/browser/devtools/timeline/timeline.js b/browser/devtools/timeline/timeline.js new file mode 100644 index 000000000000..94e5e1711076 --- /dev/null +++ b/browser/devtools/timeline/timeline.js @@ -0,0 +1,281 @@ +/* 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/. */ +"use strict"; + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/devtools/Loader.jsm"); + +devtools.lazyRequireGetter(this, "promise"); +devtools.lazyRequireGetter(this, "EventEmitter", + "devtools/toolkit/event-emitter"); + +devtools.lazyRequireGetter(this, "Overview", + "devtools/timeline/overview", true); +devtools.lazyRequireGetter(this, "Waterfall", + "devtools/timeline/waterfall", true); + +devtools.lazyImporter(this, "PluralForm", + "resource://gre/modules/PluralForm.jsm"); + +const OVERVIEW_UPDATE_INTERVAL = 200; +const OVERVIEW_INITIAL_SELECTION_RATIO = 0.15; + +// The panel's window global is an EventEmitter firing the following events: +const EVENTS = { + // When a recording is started or stopped, via the `stopwatch` button. + RECORDING_STARTED: "Timeline:RecordingStarted", + RECORDING_ENDED: "Timeline:RecordingEnded", + + // When the overview graph is populated with new markers. + OVERVIEW_UPDATED: "Timeline:OverviewUpdated", + + // When the waterfall view is populated with new markers. + WATERFALL_UPDATED: "Timeline:WaterfallUpdated" +}; + +/** + * The current target and the timeline front, set by this tool's host. + */ +let gToolbox, gTarget, gFront; + +/** + * Initializes the timeline controller and views. + */ +let startupTimeline = Task.async(function*() { + yield TimelineView.initialize(); + yield TimelineController.initialize(); +}); + +/** + * Destroys the timeline controller and views. + */ +let shutdownTimeline = Task.async(function*() { + yield TimelineView.destroy(); + yield TimelineController.destroy(); + yield gFront.stop(); +}); + +/** + * Functions handling the timeline frontend controller. + */ +let TimelineController = { + /** + * Permanent storage for the markers streamed by the backend. + */ + _markers: [], + + /** + * Initialization function, called when the tool is started. + */ + initialize: function() { + this._onRecordingTick = this._onRecordingTick.bind(this); + this._onMarkers = this._onMarkers.bind(this); + gFront.on("markers", this._onMarkers); + }, + + /** + * Destruction function, called when the tool is closed. + */ + destroy: function() { + gFront.off("markers", this._onMarkers); + }, + + /** + * Gets the accumulated markers in this recording. + * @return array. + */ + getMarkers: function() { + return this._markers; + }, + + /** + * Starts/stops the timeline recording and streaming. + */ + toggleRecording: Task.async(function*() { + let isRecording = yield gFront.isRecording(); + if (isRecording == false) { + yield this._startRecording(); + } else { + yield this._stopRecording(); + } + }), + + /** + * Starts the recording, updating the UI as needed. + */ + _startRecording: function*() { + this._markers = []; + this._markers.startTime = performance.now(); + this._markers.endTime = performance.now(); + this._updateId = setInterval(this._onRecordingTick, OVERVIEW_UPDATE_INTERVAL); + + TimelineView.handleRecordingStarted(); + yield gFront.start(); + }, + + /** + * Stops the recording, updating the UI as needed. + */ + _stopRecording: function*() { + clearInterval(this._updateId); + + TimelineView.handleMarkersUpdate(this._markers); + TimelineView.handleRecordingEnded(); + yield gFront.stop(); + }, + + /** + * Used in tests. Stops the recording, discarding the accumulated markers and + * updating the UI as needed. + */ + _stopRecordingAndDiscardData: function*() { + this._markers.length = 0; + yield this._stopRecording(); + }, + + /** + * Callback handling the "markers" event on the timeline front. + * + * @param array markers + * A list of new markers collected since the last time this + * function was invoked. + */ + _onMarkers: function(markers) { + Array.prototype.push.apply(this._markers, markers); + }, + + /** + * Callback invoked at a fixed interval while recording. + * Updates the markers store with the current time and the timeline overview. + */ + _onRecordingTick: function() { + this._markers.endTime = performance.now(); + TimelineView.handleMarkersUpdate(this._markers); + } +}; + +/** + * Functions handling the timeline frontend view. + */ +let TimelineView = { + /** + * Initialization function, called when the tool is started. + */ + initialize: Task.async(function*() { + this.overview = new Overview($("#timeline-overview")); + this.waterfall = new Waterfall($("#timeline-waterfall")); + + this._onSelecting = this._onSelecting.bind(this); + this._onRefresh = this._onRefresh.bind(this); + this.overview.on("selecting", this._onSelecting); + this.overview.on("refresh", this._onRefresh); + + yield this.overview.ready(); + yield this.waterfall.recalculateBounds(); + }), + + /** + * Destruction function, called when the tool is closed. + */ + destroy: function() { + this.overview.off("selecting", this._onSelecting); + this.overview.off("refresh", this._onRefresh); + this.overview.destroy(); + }, + + /** + * Signals that a recording session has started and triggers the appropriate + * changes in the UI. + */ + handleRecordingStarted: function() { + $("#record-button").setAttribute("checked", "true"); + $("#timeline-pane").selectedPanel = $("#recording-notice"); + + this.overview.selectionEnabled = false; + this.overview.dropSelection(); + this.overview.setData([]); + this.waterfall.clearView(); + + window.emit(EVENTS.RECORDING_STARTED); + }, + + /** + * Signals that a recording session has ended and triggers the appropriate + * changes in the UI. + */ + handleRecordingEnded: function() { + $("#record-button").removeAttribute("checked"); + $("#timeline-pane").selectedPanel = $("#timeline-waterfall"); + + this.overview.selectionEnabled = true; + + let markers = TimelineController.getMarkers(); + if (markers.length) { + let start = markers[0].start * this.overview.dataScaleX; + let end = start + this.overview.width * OVERVIEW_INITIAL_SELECTION_RATIO; + this.overview.setSelection({ start, end }); + } else { + let duration = markers.endTime - markers.startTime; + this.waterfall.setData(markers, 0, duration); + } + + window.emit(EVENTS.RECORDING_ENDED); + }, + + /** + * Signals that a new set of markers was made available by the controller, + * or that the overview graph needs to be updated. + * + * @param array markers + * A list of new markers collected since the recording has started. + */ + handleMarkersUpdate: function(markers) { + this.overview.setData(markers); + window.emit(EVENTS.OVERVIEW_UPDATED); + }, + + /** + * Callback handling the "selecting" event on the timeline overview. + */ + _onSelecting: function() { + if (!this.overview.hasSelection() && + !this.overview.hasSelectionInProgress()) { + this.waterfall.clearView(); + return; + } + let selection = this.overview.getSelection(); + let start = selection.start / this.overview.dataScaleX; + let end = selection.end / this.overview.dataScaleX; + + let markers = TimelineController.getMarkers(); + let timeStart = Math.min(start, end); + let timeEnd = Math.max(start, end); + this.waterfall.setData(markers, timeStart, timeEnd); + }, + + /** + * Callback handling the "refresh" event on the timeline overview. + */ + _onRefresh: function() { + this.waterfall.recalculateBounds(); + this._onSelecting(); + } +}; + +/** + * Convenient way of emitting events from the panel window. + */ +EventEmitter.decorate(this); + +/** + * DOM query helpers. + */ +function $(selector, target = document) { + return target.querySelector(selector); +} +function $$(selector, target = document) { + return target.querySelectorAll(selector); +} diff --git a/browser/devtools/timeline/timeline.xul b/browser/devtools/timeline/timeline.xul new file mode 100644 index 000000000000..ce521bd15921 --- /dev/null +++ b/browser/devtools/timeline/timeline.xul @@ -0,0 +1,68 @@ + + + + + + + + %timelineDTD; +]> + + + - - - diff --git a/browser/devtools/timeline/test/head.js b/browser/devtools/timeline/test/head.js deleted file mode 100644 index e89fa5f3fc05..000000000000 --- a/browser/devtools/timeline/test/head.js +++ /dev/null @@ -1,133 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - http://creativecommons.org/publicdomain/zero/1.0/ */ -"use strict"; - -const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; - -let { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); - -// Disable logging for all the tests. Both the debugger server and frontend will -// be affected by this pref. -let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log"); -Services.prefs.setBoolPref("devtools.debugger.log", false); - -// Enable the tool while testing. -let gToolEnabled = Services.prefs.getBoolPref("devtools.timeline.enabled"); -Services.prefs.setBoolPref("devtools.timeline.enabled", true); - -let { Task } = Cu.import("resource://gre/modules/Task.jsm", {}); -let { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); -let { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {}); -let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); -let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); - -let TargetFactory = devtools.TargetFactory; -let Toolbox = devtools.Toolbox; - -const EXAMPLE_URL = "http://example.com/browser/browser/devtools/timeline/test/"; -const SIMPLE_URL = EXAMPLE_URL + "doc_simple-test.html"; - -// All tests are asynchronous. -waitForExplicitFinish(); - -registerCleanupFunction(() => { - info("finish() was called, cleaning up..."); - Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging); - Services.prefs.setBoolPref("devtools.timeline.enabled", gToolEnabled); -}); - -function addTab(url) { - info("Adding tab: " + url); - - let deferred = promise.defer(); - let tab = gBrowser.selectedTab = gBrowser.addTab(url); - let linkedBrowser = tab.linkedBrowser; - - linkedBrowser.addEventListener("load", function onLoad() { - linkedBrowser.removeEventListener("load", onLoad, true); - info("Tab added and finished loading: " + url); - deferred.resolve(tab); - }, true); - - return deferred.promise; -} - -function removeTab(tab) { - info("Removing tab."); - - let deferred = promise.defer(); - let tabContainer = gBrowser.tabContainer; - - tabContainer.addEventListener("TabClose", function onClose(aEvent) { - tabContainer.removeEventListener("TabClose", onClose, false); - info("Tab removed and finished closing."); - deferred.resolve(); - }, false); - - gBrowser.removeTab(tab); - return deferred.promise; -} - -/** - * Spawns a new tab and starts up a toolbox with the timeline panel - * automatically selected. - * - * Must be used within a task. - * - * @param string url - * The location of the new tab to spawn. - * @return object - * A promise resolved once the timeline is initialized, with the - * [target, debuggee, panel] instances. - */ -function* initTimelinePanel(url) { - info("Initializing a timeline pane."); - - let tab = yield addTab(url); - let target = TargetFactory.forTab(tab); - let debuggee = target.window.wrappedJSObject; - - yield target.makeRemote(); - - let toolbox = yield gDevTools.showToolbox(target, "timeline"); - let panel = toolbox.getCurrentPanel(); - return [target, debuggee, panel]; -} - -/** - * Closes a tab and destroys the toolbox holding a timeline panel. - * - * Must be used within a task. - * - * @param object panel - * The timeline panel, created by the toolbox. - * @return object - * A promise resolved once the timeline, toolbox and debuggee tab - * are destroyed. - */ -function* teardown(panel) { - info("Destroying the specified timeline."); - - let tab = panel.target.tab; - yield panel._toolbox.destroy(); - yield removeTab(tab); -} - -/** - * Waits until a predicate returns true. - * - * @param function predicate - * Invoked once in a while until it returns true. - * @param number interval [optional] - * How often the predicate is invoked, in milliseconds. - */ -function waitUntil(predicate, interval = 10) { - if (predicate()) { - return promise.resolve(true); - } - let deferred = promise.defer(); - setTimeout(function() { - waitUntil(predicate).then(() => deferred.resolve(true)); - }, interval); - return deferred.promise; -} diff --git a/browser/devtools/timeline/timeline.js b/browser/devtools/timeline/timeline.js deleted file mode 100644 index 94e5e1711076..000000000000 --- a/browser/devtools/timeline/timeline.js +++ /dev/null @@ -1,281 +0,0 @@ -/* 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/. */ -"use strict"; - -const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; - -Cu.import("resource://gre/modules/Task.jsm"); -Cu.import("resource://gre/modules/devtools/Loader.jsm"); - -devtools.lazyRequireGetter(this, "promise"); -devtools.lazyRequireGetter(this, "EventEmitter", - "devtools/toolkit/event-emitter"); - -devtools.lazyRequireGetter(this, "Overview", - "devtools/timeline/overview", true); -devtools.lazyRequireGetter(this, "Waterfall", - "devtools/timeline/waterfall", true); - -devtools.lazyImporter(this, "PluralForm", - "resource://gre/modules/PluralForm.jsm"); - -const OVERVIEW_UPDATE_INTERVAL = 200; -const OVERVIEW_INITIAL_SELECTION_RATIO = 0.15; - -// The panel's window global is an EventEmitter firing the following events: -const EVENTS = { - // When a recording is started or stopped, via the `stopwatch` button. - RECORDING_STARTED: "Timeline:RecordingStarted", - RECORDING_ENDED: "Timeline:RecordingEnded", - - // When the overview graph is populated with new markers. - OVERVIEW_UPDATED: "Timeline:OverviewUpdated", - - // When the waterfall view is populated with new markers. - WATERFALL_UPDATED: "Timeline:WaterfallUpdated" -}; - -/** - * The current target and the timeline front, set by this tool's host. - */ -let gToolbox, gTarget, gFront; - -/** - * Initializes the timeline controller and views. - */ -let startupTimeline = Task.async(function*() { - yield TimelineView.initialize(); - yield TimelineController.initialize(); -}); - -/** - * Destroys the timeline controller and views. - */ -let shutdownTimeline = Task.async(function*() { - yield TimelineView.destroy(); - yield TimelineController.destroy(); - yield gFront.stop(); -}); - -/** - * Functions handling the timeline frontend controller. - */ -let TimelineController = { - /** - * Permanent storage for the markers streamed by the backend. - */ - _markers: [], - - /** - * Initialization function, called when the tool is started. - */ - initialize: function() { - this._onRecordingTick = this._onRecordingTick.bind(this); - this._onMarkers = this._onMarkers.bind(this); - gFront.on("markers", this._onMarkers); - }, - - /** - * Destruction function, called when the tool is closed. - */ - destroy: function() { - gFront.off("markers", this._onMarkers); - }, - - /** - * Gets the accumulated markers in this recording. - * @return array. - */ - getMarkers: function() { - return this._markers; - }, - - /** - * Starts/stops the timeline recording and streaming. - */ - toggleRecording: Task.async(function*() { - let isRecording = yield gFront.isRecording(); - if (isRecording == false) { - yield this._startRecording(); - } else { - yield this._stopRecording(); - } - }), - - /** - * Starts the recording, updating the UI as needed. - */ - _startRecording: function*() { - this._markers = []; - this._markers.startTime = performance.now(); - this._markers.endTime = performance.now(); - this._updateId = setInterval(this._onRecordingTick, OVERVIEW_UPDATE_INTERVAL); - - TimelineView.handleRecordingStarted(); - yield gFront.start(); - }, - - /** - * Stops the recording, updating the UI as needed. - */ - _stopRecording: function*() { - clearInterval(this._updateId); - - TimelineView.handleMarkersUpdate(this._markers); - TimelineView.handleRecordingEnded(); - yield gFront.stop(); - }, - - /** - * Used in tests. Stops the recording, discarding the accumulated markers and - * updating the UI as needed. - */ - _stopRecordingAndDiscardData: function*() { - this._markers.length = 0; - yield this._stopRecording(); - }, - - /** - * Callback handling the "markers" event on the timeline front. - * - * @param array markers - * A list of new markers collected since the last time this - * function was invoked. - */ - _onMarkers: function(markers) { - Array.prototype.push.apply(this._markers, markers); - }, - - /** - * Callback invoked at a fixed interval while recording. - * Updates the markers store with the current time and the timeline overview. - */ - _onRecordingTick: function() { - this._markers.endTime = performance.now(); - TimelineView.handleMarkersUpdate(this._markers); - } -}; - -/** - * Functions handling the timeline frontend view. - */ -let TimelineView = { - /** - * Initialization function, called when the tool is started. - */ - initialize: Task.async(function*() { - this.overview = new Overview($("#timeline-overview")); - this.waterfall = new Waterfall($("#timeline-waterfall")); - - this._onSelecting = this._onSelecting.bind(this); - this._onRefresh = this._onRefresh.bind(this); - this.overview.on("selecting", this._onSelecting); - this.overview.on("refresh", this._onRefresh); - - yield this.overview.ready(); - yield this.waterfall.recalculateBounds(); - }), - - /** - * Destruction function, called when the tool is closed. - */ - destroy: function() { - this.overview.off("selecting", this._onSelecting); - this.overview.off("refresh", this._onRefresh); - this.overview.destroy(); - }, - - /** - * Signals that a recording session has started and triggers the appropriate - * changes in the UI. - */ - handleRecordingStarted: function() { - $("#record-button").setAttribute("checked", "true"); - $("#timeline-pane").selectedPanel = $("#recording-notice"); - - this.overview.selectionEnabled = false; - this.overview.dropSelection(); - this.overview.setData([]); - this.waterfall.clearView(); - - window.emit(EVENTS.RECORDING_STARTED); - }, - - /** - * Signals that a recording session has ended and triggers the appropriate - * changes in the UI. - */ - handleRecordingEnded: function() { - $("#record-button").removeAttribute("checked"); - $("#timeline-pane").selectedPanel = $("#timeline-waterfall"); - - this.overview.selectionEnabled = true; - - let markers = TimelineController.getMarkers(); - if (markers.length) { - let start = markers[0].start * this.overview.dataScaleX; - let end = start + this.overview.width * OVERVIEW_INITIAL_SELECTION_RATIO; - this.overview.setSelection({ start, end }); - } else { - let duration = markers.endTime - markers.startTime; - this.waterfall.setData(markers, 0, duration); - } - - window.emit(EVENTS.RECORDING_ENDED); - }, - - /** - * Signals that a new set of markers was made available by the controller, - * or that the overview graph needs to be updated. - * - * @param array markers - * A list of new markers collected since the recording has started. - */ - handleMarkersUpdate: function(markers) { - this.overview.setData(markers); - window.emit(EVENTS.OVERVIEW_UPDATED); - }, - - /** - * Callback handling the "selecting" event on the timeline overview. - */ - _onSelecting: function() { - if (!this.overview.hasSelection() && - !this.overview.hasSelectionInProgress()) { - this.waterfall.clearView(); - return; - } - let selection = this.overview.getSelection(); - let start = selection.start / this.overview.dataScaleX; - let end = selection.end / this.overview.dataScaleX; - - let markers = TimelineController.getMarkers(); - let timeStart = Math.min(start, end); - let timeEnd = Math.max(start, end); - this.waterfall.setData(markers, timeStart, timeEnd); - }, - - /** - * Callback handling the "refresh" event on the timeline overview. - */ - _onRefresh: function() { - this.waterfall.recalculateBounds(); - this._onSelecting(); - } -}; - -/** - * Convenient way of emitting events from the panel window. - */ -EventEmitter.decorate(this); - -/** - * DOM query helpers. - */ -function $(selector, target = document) { - return target.querySelector(selector); -} -function $$(selector, target = document) { - return target.querySelectorAll(selector); -} diff --git a/browser/devtools/timeline/timeline.xul b/browser/devtools/timeline/timeline.xul deleted file mode 100644 index ce521bd15921..000000000000 --- a/browser/devtools/timeline/timeline.xul +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - %timelineDTD; -]> - - - + + + diff --git a/browser/devtools/timeline/test/head.js b/browser/devtools/timeline/test/head.js new file mode 100644 index 000000000000..e89fa5f3fc05 --- /dev/null +++ b/browser/devtools/timeline/test/head.js @@ -0,0 +1,133 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +let { Services } = Cu.import("resource://gre/modules/Services.jsm", {}); + +// Disable logging for all the tests. Both the debugger server and frontend will +// be affected by this pref. +let gEnableLogging = Services.prefs.getBoolPref("devtools.debugger.log"); +Services.prefs.setBoolPref("devtools.debugger.log", false); + +// Enable the tool while testing. +let gToolEnabled = Services.prefs.getBoolPref("devtools.timeline.enabled"); +Services.prefs.setBoolPref("devtools.timeline.enabled", true); + +let { Task } = Cu.import("resource://gre/modules/Task.jsm", {}); +let { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); +let { DevToolsUtils } = Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm", {}); +let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); +let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); + +let TargetFactory = devtools.TargetFactory; +let Toolbox = devtools.Toolbox; + +const EXAMPLE_URL = "http://example.com/browser/browser/devtools/timeline/test/"; +const SIMPLE_URL = EXAMPLE_URL + "doc_simple-test.html"; + +// All tests are asynchronous. +waitForExplicitFinish(); + +registerCleanupFunction(() => { + info("finish() was called, cleaning up..."); + Services.prefs.setBoolPref("devtools.debugger.log", gEnableLogging); + Services.prefs.setBoolPref("devtools.timeline.enabled", gToolEnabled); +}); + +function addTab(url) { + info("Adding tab: " + url); + + let deferred = promise.defer(); + let tab = gBrowser.selectedTab = gBrowser.addTab(url); + let linkedBrowser = tab.linkedBrowser; + + linkedBrowser.addEventListener("load", function onLoad() { + linkedBrowser.removeEventListener("load", onLoad, true); + info("Tab added and finished loading: " + url); + deferred.resolve(tab); + }, true); + + return deferred.promise; +} + +function removeTab(tab) { + info("Removing tab."); + + let deferred = promise.defer(); + let tabContainer = gBrowser.tabContainer; + + tabContainer.addEventListener("TabClose", function onClose(aEvent) { + tabContainer.removeEventListener("TabClose", onClose, false); + info("Tab removed and finished closing."); + deferred.resolve(); + }, false); + + gBrowser.removeTab(tab); + return deferred.promise; +} + +/** + * Spawns a new tab and starts up a toolbox with the timeline panel + * automatically selected. + * + * Must be used within a task. + * + * @param string url + * The location of the new tab to spawn. + * @return object + * A promise resolved once the timeline is initialized, with the + * [target, debuggee, panel] instances. + */ +function* initTimelinePanel(url) { + info("Initializing a timeline pane."); + + let tab = yield addTab(url); + let target = TargetFactory.forTab(tab); + let debuggee = target.window.wrappedJSObject; + + yield target.makeRemote(); + + let toolbox = yield gDevTools.showToolbox(target, "timeline"); + let panel = toolbox.getCurrentPanel(); + return [target, debuggee, panel]; +} + +/** + * Closes a tab and destroys the toolbox holding a timeline panel. + * + * Must be used within a task. + * + * @param object panel + * The timeline panel, created by the toolbox. + * @return object + * A promise resolved once the timeline, toolbox and debuggee tab + * are destroyed. + */ +function* teardown(panel) { + info("Destroying the specified timeline."); + + let tab = panel.target.tab; + yield panel._toolbox.destroy(); + yield removeTab(tab); +} + +/** + * Waits until a predicate returns true. + * + * @param function predicate + * Invoked once in a while until it returns true. + * @param number interval [optional] + * How often the predicate is invoked, in milliseconds. + */ +function waitUntil(predicate, interval = 10) { + if (predicate()) { + return promise.resolve(true); + } + let deferred = promise.defer(); + setTimeout(function() { + waitUntil(predicate).then(() => deferred.resolve(true)); + }, interval); + return deferred.promise; +} diff --git a/browser/devtools/timeline/timeline.js b/browser/devtools/timeline/timeline.js new file mode 100644 index 000000000000..94e5e1711076 --- /dev/null +++ b/browser/devtools/timeline/timeline.js @@ -0,0 +1,281 @@ +/* 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/. */ +"use strict"; + +const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/devtools/Loader.jsm"); + +devtools.lazyRequireGetter(this, "promise"); +devtools.lazyRequireGetter(this, "EventEmitter", + "devtools/toolkit/event-emitter"); + +devtools.lazyRequireGetter(this, "Overview", + "devtools/timeline/overview", true); +devtools.lazyRequireGetter(this, "Waterfall", + "devtools/timeline/waterfall", true); + +devtools.lazyImporter(this, "PluralForm", + "resource://gre/modules/PluralForm.jsm"); + +const OVERVIEW_UPDATE_INTERVAL = 200; +const OVERVIEW_INITIAL_SELECTION_RATIO = 0.15; + +// The panel's window global is an EventEmitter firing the following events: +const EVENTS = { + // When a recording is started or stopped, via the `stopwatch` button. + RECORDING_STARTED: "Timeline:RecordingStarted", + RECORDING_ENDED: "Timeline:RecordingEnded", + + // When the overview graph is populated with new markers. + OVERVIEW_UPDATED: "Timeline:OverviewUpdated", + + // When the waterfall view is populated with new markers. + WATERFALL_UPDATED: "Timeline:WaterfallUpdated" +}; + +/** + * The current target and the timeline front, set by this tool's host. + */ +let gToolbox, gTarget, gFront; + +/** + * Initializes the timeline controller and views. + */ +let startupTimeline = Task.async(function*() { + yield TimelineView.initialize(); + yield TimelineController.initialize(); +}); + +/** + * Destroys the timeline controller and views. + */ +let shutdownTimeline = Task.async(function*() { + yield TimelineView.destroy(); + yield TimelineController.destroy(); + yield gFront.stop(); +}); + +/** + * Functions handling the timeline frontend controller. + */ +let TimelineController = { + /** + * Permanent storage for the markers streamed by the backend. + */ + _markers: [], + + /** + * Initialization function, called when the tool is started. + */ + initialize: function() { + this._onRecordingTick = this._onRecordingTick.bind(this); + this._onMarkers = this._onMarkers.bind(this); + gFront.on("markers", this._onMarkers); + }, + + /** + * Destruction function, called when the tool is closed. + */ + destroy: function() { + gFront.off("markers", this._onMarkers); + }, + + /** + * Gets the accumulated markers in this recording. + * @return array. + */ + getMarkers: function() { + return this._markers; + }, + + /** + * Starts/stops the timeline recording and streaming. + */ + toggleRecording: Task.async(function*() { + let isRecording = yield gFront.isRecording(); + if (isRecording == false) { + yield this._startRecording(); + } else { + yield this._stopRecording(); + } + }), + + /** + * Starts the recording, updating the UI as needed. + */ + _startRecording: function*() { + this._markers = []; + this._markers.startTime = performance.now(); + this._markers.endTime = performance.now(); + this._updateId = setInterval(this._onRecordingTick, OVERVIEW_UPDATE_INTERVAL); + + TimelineView.handleRecordingStarted(); + yield gFront.start(); + }, + + /** + * Stops the recording, updating the UI as needed. + */ + _stopRecording: function*() { + clearInterval(this._updateId); + + TimelineView.handleMarkersUpdate(this._markers); + TimelineView.handleRecordingEnded(); + yield gFront.stop(); + }, + + /** + * Used in tests. Stops the recording, discarding the accumulated markers and + * updating the UI as needed. + */ + _stopRecordingAndDiscardData: function*() { + this._markers.length = 0; + yield this._stopRecording(); + }, + + /** + * Callback handling the "markers" event on the timeline front. + * + * @param array markers + * A list of new markers collected since the last time this + * function was invoked. + */ + _onMarkers: function(markers) { + Array.prototype.push.apply(this._markers, markers); + }, + + /** + * Callback invoked at a fixed interval while recording. + * Updates the markers store with the current time and the timeline overview. + */ + _onRecordingTick: function() { + this._markers.endTime = performance.now(); + TimelineView.handleMarkersUpdate(this._markers); + } +}; + +/** + * Functions handling the timeline frontend view. + */ +let TimelineView = { + /** + * Initialization function, called when the tool is started. + */ + initialize: Task.async(function*() { + this.overview = new Overview($("#timeline-overview")); + this.waterfall = new Waterfall($("#timeline-waterfall")); + + this._onSelecting = this._onSelecting.bind(this); + this._onRefresh = this._onRefresh.bind(this); + this.overview.on("selecting", this._onSelecting); + this.overview.on("refresh", this._onRefresh); + + yield this.overview.ready(); + yield this.waterfall.recalculateBounds(); + }), + + /** + * Destruction function, called when the tool is closed. + */ + destroy: function() { + this.overview.off("selecting", this._onSelecting); + this.overview.off("refresh", this._onRefresh); + this.overview.destroy(); + }, + + /** + * Signals that a recording session has started and triggers the appropriate + * changes in the UI. + */ + handleRecordingStarted: function() { + $("#record-button").setAttribute("checked", "true"); + $("#timeline-pane").selectedPanel = $("#recording-notice"); + + this.overview.selectionEnabled = false; + this.overview.dropSelection(); + this.overview.setData([]); + this.waterfall.clearView(); + + window.emit(EVENTS.RECORDING_STARTED); + }, + + /** + * Signals that a recording session has ended and triggers the appropriate + * changes in the UI. + */ + handleRecordingEnded: function() { + $("#record-button").removeAttribute("checked"); + $("#timeline-pane").selectedPanel = $("#timeline-waterfall"); + + this.overview.selectionEnabled = true; + + let markers = TimelineController.getMarkers(); + if (markers.length) { + let start = markers[0].start * this.overview.dataScaleX; + let end = start + this.overview.width * OVERVIEW_INITIAL_SELECTION_RATIO; + this.overview.setSelection({ start, end }); + } else { + let duration = markers.endTime - markers.startTime; + this.waterfall.setData(markers, 0, duration); + } + + window.emit(EVENTS.RECORDING_ENDED); + }, + + /** + * Signals that a new set of markers was made available by the controller, + * or that the overview graph needs to be updated. + * + * @param array markers + * A list of new markers collected since the recording has started. + */ + handleMarkersUpdate: function(markers) { + this.overview.setData(markers); + window.emit(EVENTS.OVERVIEW_UPDATED); + }, + + /** + * Callback handling the "selecting" event on the timeline overview. + */ + _onSelecting: function() { + if (!this.overview.hasSelection() && + !this.overview.hasSelectionInProgress()) { + this.waterfall.clearView(); + return; + } + let selection = this.overview.getSelection(); + let start = selection.start / this.overview.dataScaleX; + let end = selection.end / this.overview.dataScaleX; + + let markers = TimelineController.getMarkers(); + let timeStart = Math.min(start, end); + let timeEnd = Math.max(start, end); + this.waterfall.setData(markers, timeStart, timeEnd); + }, + + /** + * Callback handling the "refresh" event on the timeline overview. + */ + _onRefresh: function() { + this.waterfall.recalculateBounds(); + this._onSelecting(); + } +}; + +/** + * Convenient way of emitting events from the panel window. + */ +EventEmitter.decorate(this); + +/** + * DOM query helpers. + */ +function $(selector, target = document) { + return target.querySelector(selector); +} +function $$(selector, target = document) { + return target.querySelectorAll(selector); +} diff --git a/browser/devtools/timeline/timeline.xul b/browser/devtools/timeline/timeline.xul new file mode 100644 index 000000000000..ce521bd15921 --- /dev/null +++ b/browser/devtools/timeline/timeline.xul @@ -0,0 +1,68 @@ + + + + + + + + %timelineDTD; +]> + + +