зеркало из https://github.com/mozilla/gecko-dev.git
Merge latest green inbound changeset and mozilla-central
This commit is contained in:
Коммит
2f579f2c8b
|
@ -860,6 +860,8 @@ pref("browser.sessionstore.restore_pinned_tabs_on_demand", false);
|
|||
pref("browser.sessionstore.upgradeBackup.latestBuildID", "");
|
||||
// End-users should not run sessionstore in debug mode
|
||||
pref("browser.sessionstore.debug", false);
|
||||
// Enable asynchronous data collection by default.
|
||||
pref("browser.sessionstore.async", true);
|
||||
|
||||
// allow META refresh by default
|
||||
pref("accessibility.blockautorefresh", false);
|
||||
|
|
|
@ -524,7 +524,8 @@
|
|||
observes="devtoolsMenuBroadcaster_DevAppMgr"
|
||||
accesskey="&devAppMgrMenu.accesskey;"/>
|
||||
<menuitem id="menu_chromeDebugger"
|
||||
observes="devtoolsMenuBroadcaster_ChromeDebugger"/>
|
||||
observes="devtoolsMenuBroadcaster_ChromeDebugger"
|
||||
accesskey="&chromeDebuggerMenu.accesskey;"/>
|
||||
<menuitem id="menu_browserConsole"
|
||||
observes="devtoolsMenuBroadcaster_BrowserConsole"
|
||||
accesskey="&browserConsoleCmd.accesskey;"/>
|
||||
|
|
|
@ -215,13 +215,20 @@ SocialUI = {
|
|||
// enabled == true means we at least have a defaultProvider
|
||||
let provider = Social.provider || Social.defaultProvider;
|
||||
// We only need to update the command itself - all our menu items use it.
|
||||
let label = gNavigatorBundle.getFormattedString(Social.provider ?
|
||||
"social.turnOff.label" :
|
||||
"social.turnOn.label",
|
||||
[provider.name]);
|
||||
let accesskey = gNavigatorBundle.getString(Social.provider ?
|
||||
"social.turnOff.accesskey" :
|
||||
"social.turnOn.accesskey");
|
||||
let label;
|
||||
if (Social.providers.length == 1) {
|
||||
label = gNavigatorBundle.getFormattedString(Social.provider
|
||||
? "social.turnOff.label"
|
||||
: "social.turnOn.label",
|
||||
[provider.name]);
|
||||
} else {
|
||||
label = gNavigatorBundle.getString(Social.provider
|
||||
? "social.turnOffAll.label"
|
||||
: "social.turnOnAll.label");
|
||||
}
|
||||
let accesskey = gNavigatorBundle.getString(Social.provider
|
||||
? "social.turnOff.accesskey"
|
||||
: "social.turnOn.accesskey");
|
||||
toggleCommand.setAttribute("label", label);
|
||||
toggleCommand.setAttribute("accesskey", accesskey);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ function test() {
|
|||
|
||||
Services.prefs.setBoolPref("social.allowMultipleWorkers", true);
|
||||
runSocialTestWithProvider(gProviders, function (finishcb) {
|
||||
Social.enabled = true;
|
||||
runSocialTests(tests, undefined, undefined, function() {
|
||||
Services.prefs.clearUserPref("social.allowMultipleWorkers");
|
||||
finishcb();
|
||||
|
|
|
@ -24,7 +24,11 @@
|
|||
<content>
|
||||
<xul:stringbundle src="chrome://browser/locale/search.properties"
|
||||
anonid="searchbar-stringbundle"/>
|
||||
|
||||
<!--
|
||||
There is a dependency between "maxrows" attribute and
|
||||
"SuggestAutoComplete._historyLimit" (nsSearchSuggestions.js). Changing
|
||||
one of them requires changing the other one.
|
||||
-->
|
||||
<xul:textbox class="searchbar-textbox"
|
||||
anonid="searchbar-textbox"
|
||||
type="autocomplete"
|
||||
|
|
|
@ -2,12 +2,19 @@
|
|||
* 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";
|
||||
|
||||
function debug(msg) {
|
||||
Services.console.logStringMessage("SessionStoreContent: " + msg);
|
||||
}
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory",
|
||||
"resource:///modules/sessionstore/SessionHistory.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "SessionStorage",
|
||||
"resource:///modules/sessionstore/SessionStorage.jsm");
|
||||
|
||||
/**
|
||||
* Listens for and handles content events that we need for the
|
||||
* session store service to be notified of state changes in content.
|
||||
|
@ -55,7 +62,37 @@ let EventListener = {
|
|||
}
|
||||
}
|
||||
};
|
||||
EventListener.init();
|
||||
|
||||
/**
|
||||
* Listens for and handles messages sent by the session store service.
|
||||
*/
|
||||
let MessageListener = {
|
||||
|
||||
MESSAGES: [
|
||||
"SessionStore:collectSessionHistory",
|
||||
"SessionStore:collectSessionStorage"
|
||||
],
|
||||
|
||||
init: function () {
|
||||
this.MESSAGES.forEach(m => addMessageListener(m, this));
|
||||
},
|
||||
|
||||
receiveMessage: function ({name, data: {id}}) {
|
||||
switch (name) {
|
||||
case "SessionStore:collectSessionHistory":
|
||||
let history = SessionHistory.read(docShell);
|
||||
sendAsyncMessage(name, {id: id, data: history});
|
||||
break;
|
||||
case "SessionStore:collectSessionStorage":
|
||||
let storage = SessionStorage.serialize(docShell);
|
||||
sendAsyncMessage(name, {id: id, data: storage});
|
||||
break;
|
||||
default:
|
||||
debug("received unknown message '" + name + "'");
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let ProgressListener = {
|
||||
init: function() {
|
||||
|
@ -74,4 +111,7 @@ let ProgressListener = {
|
|||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
|
||||
Ci.nsISupportsWeakReference])
|
||||
};
|
||||
|
||||
EventListener.init();
|
||||
MessageListener.init();
|
||||
ProgressListener.init();
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/* 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";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["Messenger"];
|
||||
|
||||
const Cu = Components.utils;
|
||||
|
||||
Cu.import("resource://gre/modules/Promise.jsm", this);
|
||||
Cu.import("resource://gre/modules/Timer.jsm", this);
|
||||
|
||||
/**
|
||||
* The external API exported by this module.
|
||||
*/
|
||||
this.Messenger = Object.freeze({
|
||||
send: function (tab, type, options = {}) {
|
||||
return MessengerInternal.send(tab, type, options);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* A module that handles communication between the main and content processes.
|
||||
*/
|
||||
let MessengerInternal = {
|
||||
// The id of the last message we sent. This is used to assign a unique ID to
|
||||
// every message we send to handle multiple responses from the same browser.
|
||||
_latestMessageID: 0,
|
||||
|
||||
/**
|
||||
* Sends a message to the given tab and waits for a response.
|
||||
*
|
||||
* @param tab
|
||||
* tabbrowser tab
|
||||
* @param type
|
||||
* {string} the type of the message
|
||||
* @param options (optional)
|
||||
* {timeout: int} to set the timeout in milliseconds
|
||||
* @return {Promise} A promise that will resolve to the response message or
|
||||
* be reject when timing out.
|
||||
*/
|
||||
send: function (tab, type, options = {}) {
|
||||
let browser = tab.linkedBrowser;
|
||||
let mm = browser.messageManager;
|
||||
let deferred = Promise.defer();
|
||||
let id = ++this._latestMessageID;
|
||||
let timeout;
|
||||
|
||||
function onMessage({data: {id: mid, data}}) {
|
||||
if (mid == id) {
|
||||
mm.removeMessageListener(type, onMessage);
|
||||
clearTimeout(timeout);
|
||||
deferred.resolve(data);
|
||||
}
|
||||
}
|
||||
|
||||
mm.addMessageListener(type, onMessage);
|
||||
mm.sendAsyncMessage(type, {id: id});
|
||||
|
||||
function onTimeout() {
|
||||
mm.removeMessageListener(type, onMessage);
|
||||
deferred.reject(new Error("Timed out while waiting for a " + type + " " +
|
||||
"response message."));
|
||||
}
|
||||
|
||||
let delay = (options && options.timeout) || 5000;
|
||||
timeout = setTimeout(onTimeout, delay);
|
||||
return deferred.promise;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,82 @@
|
|||
/* 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";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["PrivacyLevel"];
|
||||
|
||||
const Cu = Components.utils;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
const PREF_NORMAL = "browser.sessionstore.privacy_level";
|
||||
const PREF_DEFERRED = "browser.sessionstore.privacy_level_deferred";
|
||||
|
||||
// The following constants represent the different possible privacy levels that
|
||||
// can be set by the user and that we need to consider when collecting text
|
||||
// data, cookies, and POSTDATA.
|
||||
//
|
||||
// Collect data from all sites (http and https).
|
||||
const PRIVACY_NONE = 0;
|
||||
// Collect data from unencrypted sites (http), only.
|
||||
const PRIVACY_ENCRYPTED = 1;
|
||||
// Collect no data.
|
||||
const PRIVACY_FULL = 2;
|
||||
|
||||
/**
|
||||
* Returns whether we will resume the session automatically on next startup.
|
||||
*/
|
||||
function willResumeAutomatically() {
|
||||
return Services.prefs.getIntPref("browser.startup.page") == 3 ||
|
||||
Services.prefs.getBoolPref("browser.sessionstore.resume_session_once");
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the current privacy level as set by the user.
|
||||
*
|
||||
* @param isPinned
|
||||
* Whether to return the privacy level for pinned tabs.
|
||||
* @return {int} The privacy level as read from the user's preferences.
|
||||
*/
|
||||
function getCurrentLevel(isPinned) {
|
||||
let pref = PREF_NORMAL;
|
||||
|
||||
// If we're in the process of quitting and we're not autoresuming the session
|
||||
// then we will use the deferred privacy level for non-pinned tabs.
|
||||
if (!isPinned && Services.startup.shuttingDown && !willResumeAutomatically()) {
|
||||
pref = PREF_DEFERRED;
|
||||
}
|
||||
|
||||
return Services.prefs.getIntPref(pref);
|
||||
}
|
||||
|
||||
/**
|
||||
* The external API as exposed by this module.
|
||||
*/
|
||||
let PrivacyLevel = Object.freeze({
|
||||
/**
|
||||
* Checks whether we're allowed to save data for a specific site.
|
||||
*
|
||||
* @param {isHttps: boolean, isPinned: boolean}
|
||||
* An object that must have two properties: 'isHttps' and 'isPinned'.
|
||||
* 'isHttps' tells whether the site us secure communication (HTTPS).
|
||||
* 'isPinned' tells whether the site is loaded in a pinned tab.
|
||||
* @return {bool} Whether we can save data for the specified site.
|
||||
*/
|
||||
canSave: function ({isHttps, isPinned}) {
|
||||
let level = getCurrentLevel(isPinned);
|
||||
|
||||
// Never save any data when full privacy is requested.
|
||||
if (level == PRIVACY_FULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't save data for encrypted sites when requested.
|
||||
if (isHttps && level == PRIVACY_ENCRYPTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
|
@ -12,8 +12,8 @@ const Ci = Components.interfaces;
|
|||
Cu.import("resource://gre/modules/Services.jsm", this);
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
|
||||
"resource:///modules/sessionstore/SessionStore.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
|
||||
"resource:///modules/sessionstore/PrivacyLevel.jsm");
|
||||
|
||||
// MAX_EXPIRY should be 2^63-1, but JavaScript can't handle that precision.
|
||||
const MAX_EXPIRY = Math.pow(2, 62);
|
||||
|
@ -67,8 +67,8 @@ let SessionCookiesInternal = {
|
|||
for (let cookie of CookieStore.getCookiesForHost(host)) {
|
||||
// _getCookiesForHost() will only return hosts with the right privacy
|
||||
// rules, so there is no need to do anything special with this call
|
||||
// to checkPrivacyLevel().
|
||||
if (SessionStore.checkPrivacyLevel(cookie.secure, isPinned)) {
|
||||
// to PrivacyLevel.canSave().
|
||||
if (PrivacyLevel.canSave({isHttps: cookie.secure, isPinned: isPinned})) {
|
||||
cookies.push(cookie);
|
||||
}
|
||||
}
|
||||
|
@ -209,7 +209,7 @@ let SessionCookiesInternal = {
|
|||
// case testing scheme will be sufficient.
|
||||
if (/https?/.test(scheme) && !hosts[host] &&
|
||||
(!checkPrivacy ||
|
||||
SessionStore.checkPrivacyLevel(scheme == "https", isPinned))) {
|
||||
PrivacyLevel.canSave({isHttps: scheme == "https", isPinned: isPinned}))) {
|
||||
// By setting this to true or false, we can determine when looking at
|
||||
// the host in update() if we should check for privacy.
|
||||
hosts[host] = isPinned;
|
||||
|
|
|
@ -0,0 +1,272 @@
|
|||
/* 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";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["SessionHistory"];
|
||||
|
||||
const Cu = Components.utils;
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
|
||||
"resource:///modules/sessionstore/PrivacyLevel.jsm");
|
||||
|
||||
function debug(msg) {
|
||||
Services.console.logStringMessage("SessionHistory: " + msg);
|
||||
}
|
||||
|
||||
// The preference value that determines how much post data to save.
|
||||
XPCOMUtils.defineLazyGetter(this, "gPostData", function () {
|
||||
const PREF = "browser.sessionstore.postdata";
|
||||
|
||||
// Observer that updates the cached value when the preference changes.
|
||||
Services.prefs.addObserver(PREF, () => {
|
||||
this.gPostData = Services.prefs.getIntPref(PREF);
|
||||
}, false);
|
||||
|
||||
return Services.prefs.getIntPref(PREF);
|
||||
});
|
||||
|
||||
/**
|
||||
* The external API exported by this module.
|
||||
*/
|
||||
this.SessionHistory = Object.freeze({
|
||||
read: function (docShell, includePrivateData) {
|
||||
return SessionHistoryInternal.read(docShell, includePrivateData);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* The internal API for the SessionHistory module.
|
||||
*/
|
||||
let SessionHistoryInternal = {
|
||||
/**
|
||||
* Collects session history data for a given docShell.
|
||||
*
|
||||
* @param docShell
|
||||
* The docShell that owns the session history.
|
||||
* @param includePrivateData (optional)
|
||||
* True to always include private data and skip any privacy checks.
|
||||
*/
|
||||
read: function (docShell, includePrivateData = false) {
|
||||
let data = {entries: []};
|
||||
let isPinned = docShell.isAppTab;
|
||||
let webNavigation = docShell.QueryInterface(Ci.nsIWebNavigation);
|
||||
let history = webNavigation.sessionHistory;
|
||||
|
||||
if (history && history.count > 0) {
|
||||
try {
|
||||
for (let i = 0; i < history.count; i++) {
|
||||
let shEntry = history.getEntryAtIndex(i, false);
|
||||
let entry = this._serializeEntry(shEntry, includePrivateData, isPinned);
|
||||
data.entries.push(entry);
|
||||
}
|
||||
} catch (ex) {
|
||||
// In some cases, getEntryAtIndex will throw. This seems to be due to
|
||||
// history.count being higher than it should be. By doing this in a
|
||||
// try-catch, we'll update history to where it breaks, print an error
|
||||
// message, and still save sessionstore.js.
|
||||
debug("SessionStore failed gathering complete history " +
|
||||
"for the focused window/tab. See bug 669196.");
|
||||
}
|
||||
data.index = history.index + 1;
|
||||
} else {
|
||||
let uri = webNavigation.currentURI.spec;
|
||||
// We landed here because the history is inaccessible or there are no
|
||||
// history entries. In that case we should at least record the docShell's
|
||||
// current URL as a single history entry. If the URL is not about:blank
|
||||
// or it's a blank tab that was modified (like a custom newtab page),
|
||||
// record it. For about:blank we explicitly want an empty array without
|
||||
// an 'index' property to denote that there are no history entries.
|
||||
if (uri != "about:blank" || webNavigation.document.body.hasChildNodes()) {
|
||||
data.entries.push({ url: uri });
|
||||
data.index = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Get an object that is a serialized representation of a History entry.
|
||||
*
|
||||
* @param shEntry
|
||||
* nsISHEntry instance
|
||||
* @param includePrivateData
|
||||
* Always return privacy sensitive data (use with care).
|
||||
* @param isPinned
|
||||
* The tab is pinned and should be treated differently for privacy.
|
||||
* @return object
|
||||
*/
|
||||
_serializeEntry: function (shEntry, includePrivateData, isPinned) {
|
||||
let entry = { url: shEntry.URI.spec };
|
||||
|
||||
// Save some bytes and don't include the title property
|
||||
// if that's identical to the current entry's URL.
|
||||
if (shEntry.title && shEntry.title != entry.url) {
|
||||
entry.title = shEntry.title;
|
||||
}
|
||||
if (shEntry.isSubFrame) {
|
||||
entry.subframe = true;
|
||||
}
|
||||
|
||||
let cacheKey = shEntry.cacheKey;
|
||||
if (cacheKey && cacheKey instanceof Ci.nsISupportsPRUint32 &&
|
||||
cacheKey.data != 0) {
|
||||
// XXXbz would be better to have cache keys implement
|
||||
// nsISerializable or something.
|
||||
entry.cacheKey = cacheKey.data;
|
||||
}
|
||||
entry.ID = shEntry.ID;
|
||||
entry.docshellID = shEntry.docshellID;
|
||||
|
||||
// We will include the property only if it's truthy to save a couple of
|
||||
// bytes when the resulting object is stringified and saved to disk.
|
||||
if (shEntry.referrerURI)
|
||||
entry.referrer = shEntry.referrerURI.spec;
|
||||
|
||||
if (shEntry.srcdocData)
|
||||
entry.srcdocData = shEntry.srcdocData;
|
||||
|
||||
if (shEntry.isSrcdocEntry)
|
||||
entry.isSrcdocEntry = shEntry.isSrcdocEntry;
|
||||
|
||||
if (shEntry.contentType)
|
||||
entry.contentType = shEntry.contentType;
|
||||
|
||||
let x = {}, y = {};
|
||||
shEntry.getScrollPosition(x, y);
|
||||
if (x.value != 0 || y.value != 0)
|
||||
entry.scroll = x.value + "," + y.value;
|
||||
|
||||
// Collect post data for the current history entry.
|
||||
try {
|
||||
let postdata = this._serializePostData(shEntry, isPinned);
|
||||
if (postdata) {
|
||||
entry.postdata_b64 = postdata;
|
||||
}
|
||||
} catch (ex) {
|
||||
// POSTDATA is tricky - especially since some extensions don't get it right
|
||||
debug("Failed serializing post data: " + ex);
|
||||
}
|
||||
|
||||
// Collect owner data for the current history entry.
|
||||
try {
|
||||
let owner = this._serializeOwner(shEntry);
|
||||
if (owner) {
|
||||
entry.owner_b64 = owner;
|
||||
}
|
||||
} catch (ex) {
|
||||
// Not catching anything specific here, just possible errors
|
||||
// from writeCompoundObject() and the like.
|
||||
debug("Failed serializing owner data: " + ex);
|
||||
}
|
||||
|
||||
entry.docIdentifier = shEntry.BFCacheEntry.ID;
|
||||
|
||||
if (shEntry.stateData != null) {
|
||||
entry.structuredCloneState = shEntry.stateData.getDataAsBase64();
|
||||
entry.structuredCloneVersion = shEntry.stateData.formatVersion;
|
||||
}
|
||||
|
||||
if (!(shEntry instanceof Ci.nsISHContainer)) {
|
||||
return entry;
|
||||
}
|
||||
|
||||
if (shEntry.childCount > 0) {
|
||||
let children = [];
|
||||
for (let i = 0; i < shEntry.childCount; i++) {
|
||||
let child = shEntry.GetChildAt(i);
|
||||
|
||||
if (child) {
|
||||
// Don't try to restore framesets containing wyciwyg URLs.
|
||||
// (cf. bug 424689 and bug 450595)
|
||||
if (child.URI.schemeIs("wyciwyg")) {
|
||||
children.length = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
children.push(this._serializeEntry(child, includePrivateData, isPinned));
|
||||
}
|
||||
}
|
||||
|
||||
if (children.length) {
|
||||
entry.children = children;
|
||||
}
|
||||
}
|
||||
|
||||
return entry;
|
||||
},
|
||||
|
||||
/**
|
||||
* Serialize post data contained in the given session history entry.
|
||||
*
|
||||
* @param shEntry
|
||||
* The session history entry.
|
||||
* @param isPinned
|
||||
* Whether the docShell is owned by a pinned tab.
|
||||
* @return The base64 encoded post data.
|
||||
*/
|
||||
_serializePostData: function (shEntry, isPinned) {
|
||||
let isHttps = shEntry.URI.schemeIs("https");
|
||||
if (!shEntry.postData || !gPostData ||
|
||||
!PrivacyLevel.canSave({isHttps: isHttps, isPinned: isPinned})) {
|
||||
return null;
|
||||
}
|
||||
|
||||
shEntry.postData.QueryInterface(Ci.nsISeekableStream)
|
||||
.seek(Ci.nsISeekableStream.NS_SEEK_SET, 0);
|
||||
let stream = Cc["@mozilla.org/binaryinputstream;1"]
|
||||
.createInstance(Ci.nsIBinaryInputStream);
|
||||
stream.setInputStream(shEntry.postData);
|
||||
let postBytes = stream.readByteArray(stream.available());
|
||||
let postdata = String.fromCharCode.apply(null, postBytes);
|
||||
if (gPostData != -1 &&
|
||||
postdata.replace(/^(Content-.*\r\n)+(\r\n)*/, "").length > gPostData) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// We can stop doing base64 encoding once our serialization into JSON
|
||||
// is guaranteed to handle all chars in strings, including embedded
|
||||
// nulls.
|
||||
return btoa(postdata);
|
||||
},
|
||||
|
||||
/**
|
||||
* Serialize owner data contained in the given session history entry.
|
||||
*
|
||||
* @param shEntry
|
||||
* The session history entry.
|
||||
* @return The base64 encoded owner data.
|
||||
*/
|
||||
_serializeOwner: function (shEntry) {
|
||||
if (!shEntry.owner) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let binaryStream = Cc["@mozilla.org/binaryoutputstream;1"].
|
||||
createInstance(Ci.nsIObjectOutputStream);
|
||||
let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
|
||||
pipe.init(false, false, 0, 0xffffffff, null);
|
||||
binaryStream.setOutputStream(pipe.outputStream);
|
||||
binaryStream.writeCompoundObject(shEntry.owner, Ci.nsISupports, true);
|
||||
binaryStream.close();
|
||||
|
||||
// Now we want to read the data from the pipe's input end and encode it.
|
||||
let scriptableStream = Cc["@mozilla.org/binaryinputstream;1"].
|
||||
createInstance(Ci.nsIBinaryInputStream);
|
||||
scriptableStream.setInputStream(pipe.inputStream);
|
||||
let ownerBytes =
|
||||
scriptableStream.readByteArray(scriptableStream.available());
|
||||
|
||||
// We can stop doing base64 encoding once our serialization into JSON
|
||||
// is guaranteed to handle all chars in strings, including embedded
|
||||
// nulls.
|
||||
return btoa(String.fromCharCode.apply(null, ownerBytes));
|
||||
}
|
||||
};
|
|
@ -151,7 +151,7 @@ let SessionSaverInternal = {
|
|||
delay = Math.max(this._lastSaveTime + gInterval - Date.now(), delay, 0);
|
||||
|
||||
// Schedule a state save.
|
||||
this._timeoutID = setTimeout(() => this._saveState(), delay);
|
||||
this._timeoutID = setTimeout(() => this._saveStateAsync(), delay);
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -186,8 +186,7 @@ let SessionSaverInternal = {
|
|||
* update the corresponding caches.
|
||||
*/
|
||||
_saveState: function (forceUpdateAllWindows = false) {
|
||||
// Cancel any pending timeouts or just clear
|
||||
// the timeout if this is why we've been called.
|
||||
// Cancel any pending timeouts.
|
||||
this.cancel();
|
||||
|
||||
stopWatchStart("COLLECT_DATA_MS", "COLLECT_DATA_LONGEST_OP_MS");
|
||||
|
@ -246,6 +245,33 @@ let SessionSaverInternal = {
|
|||
this._writeState(state);
|
||||
},
|
||||
|
||||
/**
|
||||
* Saves the current session state. Collects data asynchronously and calls
|
||||
* _saveState() to collect data again (with a cache hit rate of hopefully
|
||||
* 100%) and write to disk afterwards.
|
||||
*/
|
||||
_saveStateAsync: function () {
|
||||
// Allow scheduling delayed saves again.
|
||||
this._timeoutID = null;
|
||||
|
||||
// Check whether asynchronous data collection is disabled.
|
||||
if (!Services.prefs.getBoolPref("browser.sessionstore.async")) {
|
||||
this._saveState();
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the last save time to make sure we wait at least another interval
|
||||
// length until we call _saveStateAsync() again.
|
||||
this.updateLastSaveTime();
|
||||
|
||||
// Save state synchronously after all tab caches have been filled. The data
|
||||
// for the tab caches is collected asynchronously. We will reuse this
|
||||
// cached data if the tab hasn't been invalidated in the meantime. In that
|
||||
// case we will just fall back to synchronous data collection for single
|
||||
// tabs.
|
||||
SessionStore.fillTabCachesAsynchronously().then(() => this._saveState());
|
||||
},
|
||||
|
||||
/**
|
||||
* Write the given state object to disk.
|
||||
*/
|
||||
|
|
|
@ -7,12 +7,13 @@
|
|||
this.EXPORTED_SYMBOLS = ["SessionStorage"];
|
||||
|
||||
const Cu = Components.utils;
|
||||
const Ci = Components.interfaces;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
|
||||
"resource:///modules/sessionstore/SessionStore.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "PrivacyLevel",
|
||||
"resource:///modules/sessionstore/PrivacyLevel.jsm");
|
||||
|
||||
this.SessionStorage = {
|
||||
/**
|
||||
|
@ -51,16 +52,17 @@ let DomStorage = {
|
|||
read: function DomStorage_read(aDocShell, aFullData) {
|
||||
let data = {};
|
||||
let isPinned = aDocShell.isAppTab;
|
||||
let shistory = aDocShell.sessionHistory;
|
||||
let webNavigation = aDocShell.QueryInterface(Ci.nsIWebNavigation);
|
||||
let shistory = webNavigation.sessionHistory;
|
||||
|
||||
for (let i = 0; i < shistory.count; i++) {
|
||||
for (let i = 0; shistory && i < shistory.count; i++) {
|
||||
let principal = History.getPrincipalForEntry(shistory, i, aDocShell);
|
||||
if (!principal)
|
||||
continue;
|
||||
|
||||
// Check if we're allowed to store sessionStorage data.
|
||||
let isHTTPS = principal.URI && principal.URI.schemeIs("https");
|
||||
if (aFullData || SessionStore.checkPrivacyLevel(isHTTPS, isPinned)) {
|
||||
let isHttps = principal.URI && principal.URI.schemeIs("https");
|
||||
if (aFullData || PrivacyLevel.canSave({isHttps: isHttps, isPinned: isPinned})) {
|
||||
let origin = principal.jarPrefix + principal.origin;
|
||||
|
||||
// Don't read a host twice.
|
||||
|
@ -90,8 +92,8 @@ let DomStorage = {
|
|||
let storageManager = aDocShell.QueryInterface(Components.interfaces.nsIDOMStorageManager);
|
||||
|
||||
// There is no need to pass documentURI, it's only used to fill documentURI property of
|
||||
// domstorage event, which in this case has no consumer. Prevention of events in case
|
||||
// of missing documentURI will be solved in a followup bug to bug 600307.
|
||||
// domstorage event, which in this case has no consumer. Prevention of events in case
|
||||
// of missing documentURI will be solved in a followup bug to bug 600307.
|
||||
let storage = storageManager.createStorage(principal, "", aDocShell.usePrivateBrowsing);
|
||||
|
||||
for (let [key, value] in Iterator(data)) {
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -14,7 +14,10 @@ JS_MODULES_PATH = 'modules/sessionstore'
|
|||
|
||||
EXTRA_JS_MODULES = [
|
||||
'DocumentUtils.jsm',
|
||||
'Messenger.jsm',
|
||||
'PrivacyLevel.jsm',
|
||||
'SessionCookies.jsm',
|
||||
'SessionHistory.jsm',
|
||||
'SessionMigration.jsm',
|
||||
'SessionStorage.jsm',
|
||||
'SessionWorker.js',
|
||||
|
|
|
@ -58,7 +58,6 @@ function test() {
|
|||
ss.getBrowserState();
|
||||
|
||||
is(gBrowser.tabs[1], tab, "newly created tab should exist by now");
|
||||
ok(tab.linkedBrowser.__SS_data, "newly created tab should be in save state");
|
||||
|
||||
// Start a load and interrupt it by closing the tab
|
||||
tab.linkedBrowser.loadURI(URI_TO_LOAD);
|
||||
|
|
|
@ -17,7 +17,7 @@ const COLLAPSE_DATA_URL_LENGTH = 60;
|
|||
const {UndoStack} = require("devtools/shared/undo");
|
||||
const {editableField, InplaceEditor} = require("devtools/shared/inplace-editor");
|
||||
const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
|
||||
const {colorUtils} = require("devtools/shared/css-color");
|
||||
const {colorUtils} = require("devtools/css-color");
|
||||
const promise = require("sdk/core/promise");
|
||||
|
||||
Cu.import("resource://gre/modules/devtools/LayoutHelpers.jsm");
|
||||
|
|
|
@ -6,15 +6,9 @@ const COLOR_UNIT_PREF = "devtools.defaultColorUnit";
|
|||
let origColorUnit;
|
||||
let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
|
||||
let {Loader} = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
|
||||
let {colorUtils} = devtools.require("devtools/shared/css-color");
|
||||
let {colorUtils} = devtools.require("devtools/css-color");
|
||||
|
||||
function test() {
|
||||
// FIXME: Enable this test on Linux once bug 916544 is fixed
|
||||
if (Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS === "Linux") {
|
||||
Services = colorUtils.CssColor = Loader = null;
|
||||
return;
|
||||
}
|
||||
|
||||
waitForExplicitFinish();
|
||||
|
||||
gBrowser.selectedTab = gBrowser.addTab();
|
||||
|
@ -302,7 +296,7 @@ function getTestData() {
|
|||
{authored: "hsla(0, 0%, 0%, 0)", name: "transparent", hex: "transparent", hsl: "transparent", rgb: "transparent"},
|
||||
{authored: "rgba(50, 60, 70, 0.5)", name: "rgba(50, 60, 70, 0.5)", hex: "rgba(50, 60, 70, 0.5)", hsl: "hsla(210, 17%, 24%, 0.5)", rgb: "rgba(50, 60, 70, 0.5)"},
|
||||
{authored: "rgba(0, 0, 0, 0.3)", name: "rgba(0, 0, 0, 0.3)", hex: "rgba(0, 0, 0, 0.3)", hsl: "hsla(0, 0%, 0%, 0.3)", rgb: "rgba(0, 0, 0, 0.3)"},
|
||||
{authored: "rgba(255, 255, 255, 0.7)", name: "rgba(255, 255, 255, 0.7)", hex: "rgba(255, 255, 255, 0.7)", hsl: "hsla(0, 0%, 100%, 0.7)", rgb: "rgba(255, 255, 255, 0.7)"},
|
||||
{authored: "rgba(255, 255, 255, 0.6)", name: "rgba(255, 255, 255, 0.6)", hex: "rgba(255, 255, 255, 0.6)", hsl: "hsla(0, 0%, 100%, 0.6)", rgb: "rgba(255, 255, 255, 0.6)"},
|
||||
{authored: "rgba(127, 89, 45, 1)", name: "#7F592D", hex: "#7F592D", hsl: "hsl(32.195, 48%, 34%)", rgb: "rgb(127, 89, 45)"},
|
||||
{authored: "hsla(19.304, 56%, 40%, 1)", name: "#9F512C", hex: "#9F512C", hsl: "hsl(19.304, 57%, 40%)", rgb: "rgb(159, 81, 44)"},
|
||||
{authored: "invalidColor", name: "", hex: "", hsl: "", rgb: ""}
|
||||
|
|
|
@ -11,7 +11,7 @@ let {CssLogic} = require("devtools/styleinspector/css-logic");
|
|||
let {ELEMENT_STYLE} = require("devtools/server/actors/styles");
|
||||
let promise = require("sdk/core/promise");
|
||||
let {EventEmitter} = require("devtools/shared/event-emitter");
|
||||
let {colorUtils} = require("devtools/shared/css-color");
|
||||
let {colorUtils} = require("devtools/css-color");
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/PluralForm.jsm");
|
||||
|
|
|
@ -12,7 +12,7 @@ const promise = require("sdk/core/promise");
|
|||
let {CssLogic} = require("devtools/styleinspector/css-logic");
|
||||
let {InplaceEditor, editableField, editableItem} = require("devtools/shared/inplace-editor");
|
||||
let {ELEMENT_STYLE, PSEUDO_ELEMENTS} = require("devtools/server/actors/styles");
|
||||
let {colorUtils} = require("devtools/shared/css-color");
|
||||
let {colorUtils} = require("devtools/css-color");
|
||||
let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
|
|
@ -6,6 +6,10 @@
|
|||
|
||||
const TEST_URI = "http://example.com/browser/browser/devtools/webconsole/test/test-error.html";
|
||||
|
||||
let containsValue;
|
||||
let Sources;
|
||||
let containsValueInvoked = false;
|
||||
|
||||
function test() {
|
||||
addTab(TEST_URI);
|
||||
browser.addEventListener("load", function onLoad() {
|
||||
|
@ -15,21 +19,32 @@ function test() {
|
|||
}
|
||||
|
||||
function testViewSource(hud) {
|
||||
info("console opened");
|
||||
|
||||
let button = content.document.querySelector("button");
|
||||
ok(button, "we have the button on the page");
|
||||
|
||||
expectUncaughtException();
|
||||
EventUtils.sendMouseEvent({ type: "click" }, button, content);
|
||||
|
||||
waitForMessages({
|
||||
webconsole: hud,
|
||||
messages: [{
|
||||
text: "fooBazBaz is not defined",
|
||||
category: CATEGORY_JS,
|
||||
severity: SEVERITY_ERROR,
|
||||
}],
|
||||
}).then(([result]) => {
|
||||
Cu.forceGC();
|
||||
openDebugger().then(({panelWin: { DebuggerView }}) => {
|
||||
info("debugger openeed");
|
||||
Sources = DebuggerView.Sources;
|
||||
openConsole(null, (hud) => {
|
||||
info("console opened again");
|
||||
|
||||
waitForMessages({
|
||||
webconsole: hud,
|
||||
messages: [{
|
||||
text: "fooBazBaz is not defined",
|
||||
category: CATEGORY_JS,
|
||||
severity: SEVERITY_ERROR,
|
||||
}],
|
||||
}).then(onMessage);
|
||||
});
|
||||
});
|
||||
|
||||
function onMessage([result]) {
|
||||
let msg = [...result.matched][0];
|
||||
ok(msg, "error message");
|
||||
let locationNode = msg.querySelector(".location");
|
||||
|
@ -37,8 +52,14 @@ function testViewSource(hud) {
|
|||
|
||||
Services.ww.registerNotification(observer);
|
||||
|
||||
containsValue = Sources.containsValue;
|
||||
Sources.containsValue = () => {
|
||||
containsValueInvoked = true;
|
||||
return false;
|
||||
};
|
||||
|
||||
EventUtils.sendMouseEvent({ type: "click" }, locationNode);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let observer = {
|
||||
|
@ -53,6 +74,9 @@ let observer = {
|
|||
// executeSoon() is necessary to avoid crashing Firefox. See bug 611543.
|
||||
executeSoon(function() {
|
||||
aSubject.close();
|
||||
ok(containsValueInvoked, "custom containsValue() was invoked");
|
||||
Sources.containsValue = containsValue;
|
||||
Sources = containsValue = null;
|
||||
finishTest();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -246,6 +246,7 @@ These should match what Safari and other Apple applications use on OS X Lion. --
|
|||
<!-- LOCALIZATION NOTE (chromeDebuggerMenu.label): This is the label for the
|
||||
- application menu item that opens the browser debugger UI in the Tools menu. -->
|
||||
<!ENTITY chromeDebuggerMenu.label "Browser Debugger">
|
||||
<!ENTITY chromeDebuggerMenu.accesskey "e">
|
||||
|
||||
<!ENTITY devToolbarCloseButton.tooltiptext "Close Developer Toolbar">
|
||||
<!ENTITY devToolbarMenu.label "Developer Toolbar">
|
||||
|
|
|
@ -428,6 +428,8 @@ social.turnOff.accesskey=T
|
|||
# LOCALIZATION NOTE (social.turnOn.label): %S is the name of the social provider
|
||||
social.turnOn.label=Turn on %S
|
||||
social.turnOn.accesskey=T
|
||||
social.turnOffAll.label=Turn off all Services
|
||||
social.turnOnAll.label=Turn on all Services
|
||||
|
||||
# LOCALIZATION NOTE (social.markpageMenu.label): %S is the name of the social provider
|
||||
social.markpageMenu.label=Save Page to %S
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
<!-- 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/. -->
|
||||
|
||||
<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
|
||||
<ShortName>Bing</ShortName>
|
||||
<Description>Bing. Search by Microsoft.</Description>
|
||||
<InputEncoding>UTF-8</InputEncoding>
|
||||
<Image width="16" height="16">data:image/x-icon;base64,AAABAAIAEBAAAAAAAAD/AAAAJgAAACAgAAAAAAAA+AMAACUBAACJUE5HDQoaCgAAAA1JSERSAAAAEAAAABAIBgAAAB/z/2EAAADGSURBVDjLY/i/TPQ/JZiBOgZsd/z/f08AhCbLgJdH/4MBiKaaAWtUIK4i4DJUA95d/v//xsz//788+o8VPN4GMRCnATAAMuByF8IFx3NR1dxbjscAkCTI+dicDDIIBkAuxWoALs0wDLIABH5+QDIA5DeY0wmFPMhrMAA34Gw1QhAtkFAwyHWwAIZahkiJoBiAOQ1kILpmkMHIaqBRy4BiOihgYACkCBQ2IIwcrSBDkNIFZl7YaARxAShcYAaAMCxaaZOZKMAAOzkOfhyf540AAAAASUVORK5CYIKJUE5HDQoaCgAAAA1JSERSAAAAIAAAACAIBgAAAHN6evQAAAO/SURBVFjDxZRJTxRBFMfrJiC7enKJ28nEGL+AXo2J+gFcrkr0C4gLKjCAoILKIgKyjSZG40FFMephHERcLm4XVzTRi7I4DHRPL8/3qqq7q6d7ZIgRO/mFzlT1+/3fq6ZZ8lzB9smq3JGxQwtgPhmvyHk1XpO3kZF8/EgOjB2eX8hJbkY3/4WjOTBRkQuMbuaVCsHEsVwOoxTzhpROHs8TnMjDAMe8hX/KcQFJOZVIFQaYPEE//mMqBb9QyKlGInmQiFCASpkG0WPVoF7m5xio63OmSvCreqEgIkjUILVEPjCeRhIWQF2fCwmCZI5QSqeIunyYOilg7iZEf5QWYDQG6nrW1OQLaj08aQFM1RdAskHApmhRkopHAgHU9ayp84RJop6Q0lMFME2cRs4UYoA674HQAHIt2bgUtNt7+R76PYzUUAS0gb2QbF8npA35rjTpSKV4urEQZhopQL0YCUEFfAG+xEC7s4//netFz+iDZZA8XSiEUsrFTUUwc1bAnHRE6nEk6+K012U4AtbX8JD25Cjo17e6wplzRaAR55FmCnAG5RIqlOnihQb3wXTLMnf/tAp11roc9HtlfG/6lbpf5ko5LcWgI2xGngVhPKkJ7/hNP2hYXN3LaZLjVEZKaO0rwHwbDdQx7uzkUr2VKIFUWwkGOItFJMZIMID54SaoewRFvpH6xup2WQzWx1v+YtoE6G1CnLqAtGMA9yHEfBoMoPesB3WPdr5Y0OyhN4txujgdRjcEG7q3i4uNi6UcphYyn9YGHlDXOS0enkzSVsw71J0OkfTLelEHRkepoBMDqMnN+MHgBMJEUpZSueBJeYe8y5AAT8rBRDGniwLIxDx12MiGyr11pTNV5iLHysEOreHyYL2rG1G8CMxLApZe0PqU9uLoE2Bc2+TvTOlQFTpQd9aNzfxZ37/y6G0utYhuAfN1QHSvBPvHy0AI6kYIJYqQztJwxkrykfKg/OcrsC6vdsVWj4D5Cjn0rgL7Wzz4QUiMgv26FayBbWD2r+JnyOnHwgPbwX7TyvcEPmLf42BdWe1KrZ7FYPUKmJM+DCu2P7Rg1lfiC9jxA5641xPbRB8FwBeCMDNB5/VgN9jvLmcXhqTvr4D9cI9P6Ir7/DDnbcyEe2YOeI72XRz3YDo7cMxrgl2GSDn9AmZiUZWAsOcPhHWYSahIVRh/IWYjrKvZZBmlSwTRJQAISxdk7CobWYYuXXHUA5wARlfJkD1XyawyD1Bk6Rhdpc+Z1lG4Fm+G/1bkEhX8SczlnaXP8Juz5TddEmZvDz4eOQAAAABJRU5ErkJggg==</Image>
|
||||
<Url type="application/x-suggestions+json" template="http://api.bing.com/osjson.aspx">
|
||||
<Param name="query" value="{searchTerms}"/>
|
||||
<Param name="form" value="MOZW"/>
|
||||
</Url>
|
||||
<Url type="text/html" method="GET" template="http://www.bing.com/search">
|
||||
<Param name="q" value="{searchTerms}"/>
|
||||
<MozParam name="pc" condition="pref" pref="ms-pc"/>
|
||||
<Param name="form" value="MOZW"/>
|
||||
</Url>
|
||||
<SearchForm>http://www.bing.com/search</SearchForm>
|
||||
</SearchPlugin>
|
|
@ -1,4 +1,4 @@
|
|||
bing
|
||||
bingmetrofx
|
||||
googlemetrofx
|
||||
wikipedia
|
||||
yahoo
|
||||
|
|
|
@ -88,12 +88,6 @@ var Appbar = {
|
|||
this._updateStarButton();
|
||||
},
|
||||
|
||||
onDownloadButton: function() {
|
||||
let notificationBox = Browser.getNotificationBox();
|
||||
notificationBox.notificationsHidden = !notificationBox.notificationsHidden;
|
||||
ContextUI.dismiss();
|
||||
},
|
||||
|
||||
onPinButton: function() {
|
||||
if (this.pinButton.checked) {
|
||||
Browser.pinSite();
|
||||
|
|
|
@ -254,7 +254,7 @@
|
|||
<observes element="bcast_urlbarState" attribute="*"/>
|
||||
<hbox id="toolbar-context-page" pack="end">
|
||||
<circularprogressindicator id="download-progress"
|
||||
oncommand="Appbar.onDownloadButton()"/>
|
||||
oncommand="Downloads.onDownloadButton()"/>
|
||||
<toolbarbutton id="star-button" class="appbar-primary"
|
||||
type="checkbox"
|
||||
oncommand="Appbar.onStarButton()"/>
|
||||
|
|
|
@ -88,7 +88,7 @@ var Downloads = {
|
|||
case 0: // Downloading
|
||||
case 5: // Queued
|
||||
this.watchDownload(dl);
|
||||
this.updateInfobar(dl);
|
||||
this.updateInfobar();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -194,7 +194,6 @@ var Downloads = {
|
|||
},
|
||||
|
||||
showNotification: function dh_showNotification(title, msg, buttons, priority) {
|
||||
this._notificationBox.notificationsHidden = false;
|
||||
let notification = this._notificationBox.appendNotification(msg,
|
||||
title,
|
||||
URI_GENERIC_ICON_DOWNLOAD,
|
||||
|
@ -335,9 +334,10 @@ var Downloads = {
|
|||
this._downloadProgressIndicator.updateProgress(percentComplete);
|
||||
},
|
||||
|
||||
_computeDownloadProgressString: function dv_computeDownloadProgressString(aDownload) {
|
||||
_computeDownloadProgressString: function dv_computeDownloadProgressString() {
|
||||
let totTransferred = 0, totSize = 0, totSecondsLeft = 0;
|
||||
for (let [guid, info] of this._progressNotificationInfo) {
|
||||
let guid, info;
|
||||
for ([guid, info] of this._progressNotificationInfo) {
|
||||
let size = info.download.size;
|
||||
let amountTransferred = info.download.amountTransferred;
|
||||
let speed = info.download.speed;
|
||||
|
@ -358,7 +358,7 @@ var Downloads = {
|
|||
|
||||
if (this._downloadCount == 1) {
|
||||
return Strings.browser.GetStringFromName("alertDownloadsStart2")
|
||||
.replace("#1", aDownload.displayName)
|
||||
.replace("#1", info.download.displayName)
|
||||
.replace("#2", progress)
|
||||
.replace("#3", timeLeft)
|
||||
}
|
||||
|
@ -380,12 +380,20 @@ var Downloads = {
|
|||
this._progressNotificationInfo.set(aDownload.guid, infoObj);
|
||||
},
|
||||
|
||||
updateInfobar: function dv_updateInfobar(aDownload) {
|
||||
let message = this._computeDownloadProgressString(aDownload);
|
||||
this._updateCircularProgressMeter();
|
||||
onDownloadButton: function dv_onDownloadButton() {
|
||||
if (this._progressNotification) {
|
||||
let progressBar = this._notificationBox.getNotificationWithValue("download-progress");
|
||||
if (progressBar) {
|
||||
this._notificationBox.removeNotification(progressBar);
|
||||
} else {
|
||||
this.updateInfobar();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
if (this._notificationBox && this._notificationBox.notificationsHidden)
|
||||
this._notificationBox.notificationsHidden = false;
|
||||
updateInfobar: function dv_updateInfobar() {
|
||||
let message = this._computeDownloadProgressString();
|
||||
this._updateCircularProgressMeter();
|
||||
|
||||
if (this._progressNotification == null ||
|
||||
!this._notificationBox.getNotificationWithValue("download-progress")) {
|
||||
|
@ -457,7 +465,7 @@ var Downloads = {
|
|||
case "dl-start":
|
||||
let download = aSubject.QueryInterface(Ci.nsIDownload);
|
||||
this.watchDownload(download);
|
||||
this.updateInfobar(download);
|
||||
this.updateInfobar();
|
||||
break;
|
||||
case "dl-done":
|
||||
this._downloadsInProgress--;
|
||||
|
|
|
@ -124,7 +124,6 @@ HelperAppLauncherDialog.prototype = {
|
|||
className: "download-host-text"
|
||||
}
|
||||
);
|
||||
notificationBox.notificationsHidden = false;
|
||||
let newBar = notificationBox.appendNotification("",
|
||||
"save-download",
|
||||
URI_GENERIC_ICON_DOWNLOAD,
|
||||
|
|
|
@ -34,9 +34,8 @@ function prefObserver(subject, topic, data) {
|
|||
if (enable && !Social.provider) {
|
||||
// this will result in setting Social.provider
|
||||
SocialService.getOrderedProviderList(function(providers) {
|
||||
Social._updateProviderCache(providers);
|
||||
Social.enabled = true;
|
||||
Services.obs.notifyObservers(null, "social:providers-changed", null);
|
||||
Social._updateProviderCache(providers);
|
||||
});
|
||||
} else if (!enable && Social.provider) {
|
||||
Social.provider = null;
|
||||
|
@ -168,7 +167,7 @@ this.Social = {
|
|||
// Retrieve the current set of providers, and set the current provider.
|
||||
SocialService.getOrderedProviderList(function (providers) {
|
||||
Social._updateProviderCache(providers);
|
||||
Social._updateWorkerState(true);
|
||||
Social._updateWorkerState(SocialService.enabled);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -181,10 +180,16 @@ this.Social = {
|
|||
Services.obs.notifyObservers(null, "social:" + topic, origin);
|
||||
return;
|
||||
}
|
||||
if (topic == "provider-enabled" || topic == "provider-disabled") {
|
||||
if (topic == "provider-enabled") {
|
||||
Social._updateProviderCache(providers);
|
||||
Social._updateWorkerState(Social.enabled);
|
||||
Services.obs.notifyObservers(null, "social:" + topic, origin);
|
||||
return;
|
||||
}
|
||||
if (topic == "provider-disabled") {
|
||||
// a provider was removed from the list of providers, that does not
|
||||
// affect worker state for other providers
|
||||
Social._updateProviderCache(providers);
|
||||
Social._updateWorkerState(true);
|
||||
Services.obs.notifyObservers(null, "social:providers-changed", null);
|
||||
Services.obs.notifyObservers(null, "social:" + topic, origin);
|
||||
return;
|
||||
}
|
||||
|
@ -194,7 +199,6 @@ this.Social = {
|
|||
Social._updateProviderCache(providers);
|
||||
let provider = Social._getProviderFromOrigin(origin);
|
||||
provider.reload();
|
||||
Services.obs.notifyObservers(null, "social:providers-changed", null);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -210,6 +214,7 @@ this.Social = {
|
|||
// Called to update our cache of providers and set the current provider
|
||||
_updateProviderCache: function (providers) {
|
||||
this.providers = providers;
|
||||
Services.obs.notifyObservers(null, "social:providers-changed", null);
|
||||
|
||||
// If social is currently disabled there's nothing else to do other than
|
||||
// to notify about the lack of a provider.
|
||||
|
|
|
@ -4,4 +4,4 @@
|
|||
# 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/.
|
||||
|
||||
DIRS += ['chrome']
|
||||
DIRS += ['chrome', 'unit']
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# 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/.
|
||||
|
||||
DIRS += ['social']
|
||||
|
||||
XPCSHELL_TESTS_MANIFESTS += ['social/xpcshell.ini']
|
|
@ -0,0 +1,11 @@
|
|||
# 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/.
|
||||
|
||||
XPCSHELL_RESOURCES = \
|
||||
xpcshell.ini \
|
||||
head.js \
|
||||
blocklist.xml \
|
||||
test_social.js \
|
||||
test_socialDisabledStartup.js \
|
||||
$(NULL)
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0"?>
|
||||
<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
|
||||
<emItems>
|
||||
<emItem blockID="s1" id="bad.com@services.mozilla.org"></emItem>
|
||||
</emItems>
|
||||
</blocklist>
|
|
@ -0,0 +1,146 @@
|
|||
/* 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/. */
|
||||
|
||||
const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
var Social, SocialService;
|
||||
|
||||
let manifests = [
|
||||
{
|
||||
name: "provider 1",
|
||||
origin: "https://example1.com",
|
||||
sidebarURL: "https://example1.com/sidebar/",
|
||||
},
|
||||
{
|
||||
name: "provider 2",
|
||||
origin: "https://example2.com",
|
||||
sidebarURL: "https://example1.com/sidebar/",
|
||||
}
|
||||
];
|
||||
|
||||
const MANIFEST_PREFS = Services.prefs.getBranch("social.manifest.");
|
||||
|
||||
// SocialProvider class relies on blocklisting being enabled. To enable
|
||||
// blocklisting, we have to setup an app and initialize the blocklist (see
|
||||
// initApp below).
|
||||
const gProfD = do_get_profile();
|
||||
|
||||
const XULAPPINFO_CONTRACTID = "@mozilla.org/xre/app-info;1";
|
||||
const XULAPPINFO_CID = Components.ID("{c763b610-9d49-455a-bbd2-ede71682a1ac}");
|
||||
|
||||
function createAppInfo(id, name, version, platformVersion) {
|
||||
gAppInfo = {
|
||||
// nsIXULAppInfo
|
||||
vendor: "Mozilla",
|
||||
name: name,
|
||||
ID: id,
|
||||
version: version,
|
||||
appBuildID: "2007010101",
|
||||
platformVersion: platformVersion ? platformVersion : "1.0",
|
||||
platformBuildID: "2007010101",
|
||||
|
||||
// nsIXULRuntime
|
||||
inSafeMode: false,
|
||||
logConsoleErrors: true,
|
||||
OS: "XPCShell",
|
||||
XPCOMABI: "noarch-spidermonkey",
|
||||
invalidateCachesOnRestart: function invalidateCachesOnRestart() {
|
||||
// Do nothing
|
||||
},
|
||||
|
||||
// nsICrashReporter
|
||||
annotations: {},
|
||||
|
||||
annotateCrashReport: function(key, data) {
|
||||
this.annotations[key] = data;
|
||||
},
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIXULAppInfo,
|
||||
Ci.nsIXULRuntime,
|
||||
Ci.nsICrashReporter,
|
||||
Ci.nsISupports])
|
||||
};
|
||||
|
||||
var XULAppInfoFactory = {
|
||||
createInstance: function (outer, iid) {
|
||||
if (outer != null)
|
||||
throw Components.results.NS_ERROR_NO_AGGREGATION;
|
||||
return gAppInfo.QueryInterface(iid);
|
||||
}
|
||||
};
|
||||
var registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
|
||||
registrar.registerFactory(XULAPPINFO_CID, "XULAppInfo",
|
||||
XULAPPINFO_CONTRACTID, XULAppInfoFactory);
|
||||
}
|
||||
|
||||
function initApp() {
|
||||
createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
|
||||
// prepare a blocklist file for the blocklist service
|
||||
var blocklistFile = gProfD.clone();
|
||||
blocklistFile.append("blocklist.xml");
|
||||
if (blocklistFile.exists())
|
||||
blocklistFile.remove(false);
|
||||
var source = do_get_file("blocklist.xml");
|
||||
source.copyTo(gProfD, "blocklist.xml");
|
||||
}
|
||||
|
||||
function setManifestPref(manifest) {
|
||||
let string = Cc["@mozilla.org/supports-string;1"].
|
||||
createInstance(Ci.nsISupportsString);
|
||||
string.data = JSON.stringify(manifest);
|
||||
Services.prefs.setComplexValue("social.manifest." + manifest.origin, Ci.nsISupportsString, string);
|
||||
}
|
||||
|
||||
function do_wait_observer(topic, cb) {
|
||||
function observer(subject, topic, data) {
|
||||
Services.obs.removeObserver(observer, topic);
|
||||
cb();
|
||||
}
|
||||
Services.obs.addObserver(observer, topic, false);
|
||||
}
|
||||
|
||||
function do_initialize_social(enabledOnStartup, cb) {
|
||||
initApp();
|
||||
|
||||
manifests.forEach(function (manifest) {
|
||||
setManifestPref(manifest);
|
||||
});
|
||||
// Set both providers active and flag the first one as "current"
|
||||
let activeVal = Cc["@mozilla.org/supports-string;1"].
|
||||
createInstance(Ci.nsISupportsString);
|
||||
let active = {};
|
||||
for (let m of manifests)
|
||||
active[m.origin] = 1;
|
||||
activeVal.data = JSON.stringify(active);
|
||||
Services.prefs.setComplexValue("social.activeProviders",
|
||||
Ci.nsISupportsString, activeVal);
|
||||
Services.prefs.setCharPref("social.provider.current", manifests[0].origin);
|
||||
Services.prefs.setBoolPref("social.enabled", enabledOnStartup);
|
||||
|
||||
do_register_cleanup(function() {
|
||||
manifests.forEach(function (manifest) {
|
||||
Services.prefs.clearUserPref("social.manifest." + manifest.origin);
|
||||
});
|
||||
Services.prefs.clearUserPref("social.enabled");
|
||||
Services.prefs.clearUserPref("social.provider.current");
|
||||
Services.prefs.clearUserPref("social.activeProviders");
|
||||
});
|
||||
|
||||
// expecting 2 providers installed
|
||||
do_wait_observer("social:providers-changed", function() {
|
||||
do_check_eq(Social.providers.length, 2, "2 providers installed");
|
||||
cb();
|
||||
});
|
||||
|
||||
// import and initialize everything
|
||||
SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
|
||||
do_check_eq(SocialService.enabled, enabledOnStartup, "service is doing its thing");
|
||||
do_check_true(SocialService.hasEnabledProviders, "Service has enabled providers");
|
||||
Social = Cu.import("resource:///modules/Social.jsm", {}).Social;
|
||||
do_check_false(Social.initialized, "Social is not initialized");
|
||||
Social.init();
|
||||
do_check_true(Social.initialized, "Social is initialized");
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||
# 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/.
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/* 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/. */
|
||||
|
||||
function run_test() {
|
||||
// we are testing worker startup specifically
|
||||
Services.prefs.setBoolPref("social.allowMultipleWorkers", true);
|
||||
do_register_cleanup(function() {
|
||||
Services.prefs.clearUserPref("social.allowMultipleWorkers");
|
||||
});
|
||||
do_test_pending();
|
||||
add_test(testStartupEnabled);
|
||||
add_test(testDisableAfterStartup);
|
||||
do_initialize_social(true, run_next_test);
|
||||
}
|
||||
|
||||
function testStartupEnabled() {
|
||||
// wait on startup before continuing
|
||||
do_check_eq(Social.providers.length, 2, "two social providers enabled");
|
||||
do_check_true(Social.providers[0].enabled, "provider is enabled");
|
||||
do_check_true(Social.providers[1].enabled, "provider is enabled");
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
function testDisableAfterStartup() {
|
||||
do_wait_observer("social:provider-set", function() {
|
||||
do_check_eq(Social.enabled, false, "Social is disabled");
|
||||
do_check_false(Social.providers[0].enabled, "provider is enabled");
|
||||
do_check_false(Social.providers[1].enabled, "provider is enabled");
|
||||
do_test_finished();
|
||||
run_next_test();
|
||||
});
|
||||
Social.enabled = false;
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/* 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/. */
|
||||
|
||||
function run_test() {
|
||||
// we are testing worker startup specifically
|
||||
Services.prefs.setBoolPref("social.allowMultipleWorkers", true);
|
||||
do_register_cleanup(function() {
|
||||
Services.prefs.clearUserPref("social.allowMultipleWorkers");
|
||||
});
|
||||
do_test_pending();
|
||||
add_test(testStartupDisabled);
|
||||
add_test(testEnableAfterStartup);
|
||||
do_initialize_social(false, run_next_test);
|
||||
}
|
||||
|
||||
function testStartupDisabled() {
|
||||
// wait on startup before continuing
|
||||
do_check_eq(Social.providers.length, 2, "two social providers available");
|
||||
do_check_false(Social.providers[0].enabled, "provider is enabled");
|
||||
do_check_false(Social.providers[1].enabled, "provider is enabled");
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
function testEnableAfterStartup() {
|
||||
do_wait_observer("social:provider-set", function() {
|
||||
do_check_true(Social.enabled, "Social is enabled");
|
||||
do_check_eq(Social.providers.length, 2, "two social providers available");
|
||||
do_check_true(Social.providers[0].enabled, "provider is enabled");
|
||||
do_check_true(Social.providers[1].enabled, "provider is enabled");
|
||||
do_test_finished();
|
||||
run_next_test();
|
||||
});
|
||||
Social.enabled = true;
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
[DEFAULT]
|
||||
head = head.js
|
||||
tail =
|
||||
firefox-appdir = browser
|
||||
|
||||
[test_social.js]
|
||||
|
||||
[test_socialDisabledStartup.js]
|
|
@ -1540,10 +1540,9 @@ abstract public class GeckoApp
|
|||
final Context context = GeckoApp.this;
|
||||
AnnouncementsBroadcastService.recordLastLaunch(context);
|
||||
|
||||
// Kick off our background services that fetch product
|
||||
// announcements and upload health reports. We do this by
|
||||
// invoking the broadcast receiver, which uses the system alarm
|
||||
// infrastructure to perform tasks at intervals.
|
||||
// Kick off our background services. We do this by invoking the broadcast
|
||||
// receiver, which uses the system alarm infrastructure to perform tasks at
|
||||
// intervals.
|
||||
GeckoPreferences.broadcastAnnouncementsPref(context);
|
||||
GeckoPreferences.broadcastHealthReportUploadPref(context);
|
||||
|
||||
|
@ -1938,6 +1937,7 @@ abstract public class GeckoApp
|
|||
public void onPause()
|
||||
{
|
||||
final BrowserHealthRecorder rec = mHealthRecorder;
|
||||
final Context context = this;
|
||||
|
||||
// In some way it's sad that Android will trigger StrictMode warnings
|
||||
// here as the whole point is to save to disk while the activity is not
|
||||
|
@ -1952,6 +1952,11 @@ abstract public class GeckoApp
|
|||
rec.recordSessionEnd("P", editor);
|
||||
}
|
||||
editor.commit();
|
||||
|
||||
// In theory, the first browser session will not run long enough that we need to
|
||||
// prune during it and we'd rather run it when the browser is inactive so we wait
|
||||
// until here to register the prune service.
|
||||
GeckoPreferences.broadcastHealthReportPrune(context);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -77,6 +77,7 @@ public class GeckoPreferences
|
|||
private static String PREFS_HEALTHREPORT_LINK = NON_PREF_PREFIX + "healthreport.link";
|
||||
private static String PREFS_DEVTOOLS_REMOTE_ENABLED = "devtools.debugger.remote-enabled";
|
||||
private static String PREFS_DISPLAY_REFLOW_ON_ZOOM = "browser.zoom.reflowOnZoom";
|
||||
private static String PREFS_SYNC = NON_PREF_PREFIX + "sync";
|
||||
|
||||
public static String PREFS_RESTORE_SESSION = NON_PREF_PREFIX + "restoreSession3";
|
||||
|
||||
|
@ -342,6 +343,11 @@ public class GeckoPreferences
|
|||
CharSequence selectedEntry = listPref.getEntry();
|
||||
listPref.setSummary(selectedEntry);
|
||||
continue;
|
||||
} else if (PREFS_SYNC.equals(key) && GeckoProfile.get(this).inGuestMode()) {
|
||||
// Don't show sync prefs while in guest mode.
|
||||
preferences.removePreference(pref);
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Some Preference UI elements are not actually preferences,
|
||||
|
@ -379,6 +385,11 @@ public class GeckoPreferences
|
|||
return sIsCharEncodingEnabled;
|
||||
}
|
||||
|
||||
public static void broadcastAction(final Context context, final Intent intent) {
|
||||
fillIntentWithProfileInfo(context, intent);
|
||||
context.sendBroadcast(intent, GlobalConstants.PER_ANDROID_PACKAGE_PERMISSION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcast an intent with <code>pref</code>, <code>branch</code>, and
|
||||
* <code>enabled</code> extras. This is intended to represent the
|
||||
|
@ -391,24 +402,23 @@ public class GeckoPreferences
|
|||
final String action,
|
||||
final String pref,
|
||||
final boolean value) {
|
||||
final Intent intent = new Intent(action);
|
||||
intent.setAction(action);
|
||||
intent.putExtra("pref", pref);
|
||||
intent.putExtra("branch", GeckoApp.PREFS_NAME);
|
||||
intent.putExtra("enabled", value);
|
||||
final Intent intent = new Intent(action)
|
||||
.putExtra("pref", pref)
|
||||
.putExtra("branch", GeckoApp.PREFS_NAME)
|
||||
.putExtra("enabled", value);
|
||||
broadcastAction(context, intent);
|
||||
}
|
||||
|
||||
private static void fillIntentWithProfileInfo(final Context context, final Intent intent) {
|
||||
// There is a race here, but GeckoProfile returns the default profile
|
||||
// when Gecko is not explicitly running for a different profile. In a
|
||||
// multi-profile world, this will need to be updated (possibly to
|
||||
// broadcast settings for all profiles). See Bug 882182.
|
||||
GeckoProfile profile = GeckoProfile.get(context);
|
||||
if (profile != null) {
|
||||
intent.putExtra("profileName", profile.getName());
|
||||
intent.putExtra("profilePath", profile.getDir().getAbsolutePath());
|
||||
intent.putExtra("profileName", profile.getName())
|
||||
.putExtra("profilePath", profile.getDir().getAbsolutePath());
|
||||
}
|
||||
|
||||
Log.d(LOGTAG, "Broadcast: " + action + ", " + pref + ", " + GeckoApp.PREFS_NAME + ", " + value);
|
||||
context.sendBroadcast(intent, GlobalConstants.PER_ANDROID_PACKAGE_PERMISSION);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -451,6 +461,11 @@ public class GeckoPreferences
|
|||
broadcastHealthReportUploadPref(context, value);
|
||||
}
|
||||
|
||||
public static void broadcastHealthReportPrune(final Context context) {
|
||||
final Intent intent = new Intent(HealthReportConstants.ACTION_HEALTHREPORT_PRUNE);
|
||||
broadcastAction(context, intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the value of the named preference in the default preferences file.
|
||||
*
|
||||
|
|
|
@ -66,13 +66,15 @@ public final class GeckoProfile {
|
|||
if (((GeckoApp)context).mProfile != null) {
|
||||
return ((GeckoApp)context).mProfile;
|
||||
}
|
||||
}
|
||||
|
||||
GeckoProfile guest = GeckoProfile.getGuestProfile(context);
|
||||
// if the guest profile is locked, return it
|
||||
if (guest != null && guest.locked()) {
|
||||
return guest;
|
||||
}
|
||||
// If the guest profile exists and is locked, return it
|
||||
GeckoProfile guest = GeckoProfile.getGuestProfile(context);
|
||||
if (guest != null && guest.locked()) {
|
||||
return guest;
|
||||
}
|
||||
|
||||
if (context instanceof GeckoApp) {
|
||||
// Otherwise, get the default profile for the Activity
|
||||
return get(context, ((GeckoApp)context).getDefaultProfileName());
|
||||
}
|
||||
|
|
|
@ -109,7 +109,10 @@ public class TabsPanel extends LinearLayout
|
|||
|
||||
mTabWidget.addTab(R.drawable.tabs_normal, R.string.tabs_normal);
|
||||
mTabWidget.addTab(R.drawable.tabs_private, R.string.tabs_private);
|
||||
mTabWidget.addTab(R.drawable.tabs_synced, R.string.tabs_synced);
|
||||
|
||||
if (!GeckoProfile.get(mContext).inGuestMode()) {
|
||||
mTabWidget.addTab(R.drawable.tabs_synced, R.string.tabs_synced);
|
||||
}
|
||||
|
||||
mTabWidget.setTabSelectionListener(this);
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ SYNC_JAVA_FILES := \
|
|||
background/announcements/AnnouncementsFetcher.java \
|
||||
background/announcements/AnnouncementsFetchResourceDelegate.java \
|
||||
background/announcements/AnnouncementsService.java \
|
||||
background/announcements/AnnouncementsStartReceiver.java \
|
||||
background/BackgroundService.java \
|
||||
background/bagheera/BagheeraClient.java \
|
||||
background/bagheera/BagheeraRequestDelegate.java \
|
||||
|
@ -41,6 +40,8 @@ SYNC_JAVA_FILES := \
|
|||
background/db/Tab.java \
|
||||
background/healthreport/Environment.java \
|
||||
background/healthreport/EnvironmentBuilder.java \
|
||||
background/healthreport/HealthReportBroadcastReceiver.java \
|
||||
background/healthreport/HealthReportBroadcastService.java \
|
||||
background/healthreport/HealthReportDatabases.java \
|
||||
background/healthreport/HealthReportDatabaseStorage.java \
|
||||
background/healthreport/HealthReportGenerator.java \
|
||||
|
@ -48,11 +49,12 @@ SYNC_JAVA_FILES := \
|
|||
background/healthreport/HealthReportStorage.java \
|
||||
background/healthreport/HealthReportUtils.java \
|
||||
background/healthreport/ProfileInformationCache.java \
|
||||
background/healthreport/prune/HealthReportPruneService.java \
|
||||
background/healthreport/prune/PrunePolicy.java \
|
||||
background/healthreport/prune/PrunePolicyDatabaseStorage.java \
|
||||
background/healthreport/prune/PrunePolicyStorage.java \
|
||||
background/healthreport/upload/AndroidSubmissionClient.java \
|
||||
background/healthreport/upload/HealthReportBroadcastReceiver.java \
|
||||
background/healthreport/upload/HealthReportBroadcastService.java \
|
||||
background/healthreport/upload/HealthReportUploadService.java \
|
||||
background/healthreport/upload/HealthReportUploadStartReceiver.java \
|
||||
background/healthreport/upload/ObsoleteDocumentTracker.java \
|
||||
background/healthreport/upload/SubmissionClient.java \
|
||||
background/healthreport/upload/SubmissionPolicy.java \
|
||||
|
|
|
@ -12,7 +12,6 @@ import org.mozilla.gecko.background.common.log.Logger;
|
|||
import android.app.AlarmManager;
|
||||
import android.app.IntentService;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.ConnectivityManager;
|
||||
|
@ -53,11 +52,6 @@ public abstract class BackgroundService extends IntentService {
|
|||
return networkInfo.isAvailable();
|
||||
}
|
||||
|
||||
protected static PendingIntent createPendingIntent(Context context, Class<? extends BroadcastReceiver> broadcastReceiverClass) {
|
||||
final Intent service = new Intent(context, broadcastReceiverClass);
|
||||
return PendingIntent.getBroadcast(context, 0, service, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
}
|
||||
|
||||
protected AlarmManager getAlarmManager() {
|
||||
return getAlarmManager(this.getApplicationContext());
|
||||
}
|
||||
|
@ -79,6 +73,8 @@ public abstract class BackgroundService extends IntentService {
|
|||
protected void cancelAlarm(PendingIntent pendingIntent) {
|
||||
final AlarmManager alarm = getAlarmManager();
|
||||
alarm.cancel(pendingIntent);
|
||||
// For testing - allows us to see if the intent, and thus the alarm, has been cancelled.
|
||||
pendingIntent.cancel();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -18,7 +18,7 @@ import android.content.SharedPreferences.Editor;
|
|||
/**
|
||||
* A service which listens to broadcast intents from the system and from the
|
||||
* browser, registering or unregistering the main
|
||||
* {@link AnnouncementsStartReceiver} with the {@link AlarmManager}.
|
||||
* {@link AnnouncementsService} with the {@link AlarmManager}.
|
||||
*/
|
||||
public class AnnouncementsBroadcastService extends BackgroundService {
|
||||
private static final String WORKER_THREAD_NAME = "AnnouncementsBroadcastServiceWorker";
|
||||
|
@ -28,10 +28,24 @@ public class AnnouncementsBroadcastService extends BackgroundService {
|
|||
super(WORKER_THREAD_NAME);
|
||||
}
|
||||
|
||||
private void toggleAlarm(final Context context, boolean enabled) {
|
||||
Logger.info(LOG_TAG, (enabled ? "R" : "Unr") + "egistering announcements broadcast receiver...");
|
||||
protected static SharedPreferences getSharedPreferences(Context context) {
|
||||
return context.getSharedPreferences(AnnouncementsConstants.PREFS_BRANCH,
|
||||
GlobalConstants.SHARED_PREFERENCES_MODE);
|
||||
}
|
||||
|
||||
final PendingIntent pending = createPendingIntent(context, AnnouncementsStartReceiver.class);
|
||||
protected SharedPreferences getSharedPreferences() {
|
||||
return this.getSharedPreferences(AnnouncementsConstants.PREFS_BRANCH,
|
||||
GlobalConstants.SHARED_PREFERENCES_MODE);
|
||||
}
|
||||
|
||||
private void toggleAlarm(final Context context, boolean enabled) {
|
||||
final Class<?> serviceClass = AnnouncementsService.class;
|
||||
Logger.info(LOG_TAG, (enabled ? "R" : "Unr") + "egistering " + serviceClass.getSimpleName() +
|
||||
".");
|
||||
|
||||
final Intent service = new Intent(context, serviceClass);
|
||||
final PendingIntent pending = PendingIntent.getService(context, 0, service,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
|
||||
if (!enabled) {
|
||||
cancelAlarm(pending);
|
||||
|
@ -51,7 +65,7 @@ public class AnnouncementsBroadcastService extends BackgroundService {
|
|||
*/
|
||||
public static void recordLastLaunch(final Context context) {
|
||||
final long now = System.currentTimeMillis();
|
||||
final SharedPreferences preferences = context.getSharedPreferences(AnnouncementsConstants.PREFS_BRANCH, GlobalConstants.SHARED_PREFERENCES_MODE);
|
||||
final SharedPreferences preferences = getSharedPreferences(context);
|
||||
|
||||
// One of several things might be true, according to our logs:
|
||||
//
|
||||
|
@ -93,12 +107,12 @@ public class AnnouncementsBroadcastService extends BackgroundService {
|
|||
}
|
||||
|
||||
public static long getPollInterval(final Context context) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(AnnouncementsConstants.PREFS_BRANCH, GlobalConstants.SHARED_PREFERENCES_MODE);
|
||||
final SharedPreferences preferences = getSharedPreferences(context);
|
||||
return preferences.getLong(AnnouncementsConstants.PREF_ANNOUNCE_FETCH_INTERVAL_MSEC, AnnouncementsConstants.DEFAULT_ANNOUNCE_FETCH_INTERVAL_MSEC);
|
||||
}
|
||||
|
||||
public static void setPollInterval(final Context context, long interval) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(AnnouncementsConstants.PREFS_BRANCH, GlobalConstants.SHARED_PREFERENCES_MODE);
|
||||
final SharedPreferences preferences = getSharedPreferences(context);
|
||||
preferences.edit().putLong(AnnouncementsConstants.PREF_ANNOUNCE_FETCH_INTERVAL_MSEC, interval).commit();
|
||||
}
|
||||
|
||||
|
@ -146,8 +160,7 @@ public class AnnouncementsBroadcastService extends BackgroundService {
|
|||
// Primarily intended for debugging and testing, but this doesn't do any harm.
|
||||
if (!enabled) {
|
||||
Logger.info(LOG_TAG, "!enabled: clearing last fetch.");
|
||||
final SharedPreferences sharedPreferences = this.getSharedPreferences(AnnouncementsConstants.PREFS_BRANCH,
|
||||
GlobalConstants.SHARED_PREFERENCES_MODE);
|
||||
final SharedPreferences sharedPreferences = getSharedPreferences();
|
||||
final Editor editor = sharedPreferences.edit();
|
||||
editor.remove(AnnouncementsConstants.PREF_LAST_FETCH_LOCAL_TIME);
|
||||
editor.remove(AnnouncementsConstants.PREF_EARLIEST_NEXT_ANNOUNCE_FETCH);
|
||||
|
|
|
@ -118,6 +118,11 @@ public class AnnouncementsService extends BackgroundService implements Announcem
|
|||
Logger.setThreadLogTag(AnnouncementsConstants.GLOBAL_LOG_TAG);
|
||||
Logger.debug(LOG_TAG, "Running AnnouncementsService.");
|
||||
|
||||
if (AnnouncementsConstants.DISABLED) {
|
||||
Logger.debug(LOG_TAG, "Announcements disabled. Returning from AnnouncementsService.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldFetchAnnouncements()) {
|
||||
Logger.debug(LOG_TAG, "Not fetching.");
|
||||
return;
|
||||
|
@ -136,7 +141,7 @@ public class AnnouncementsService extends BackgroundService implements Announcem
|
|||
return getSharedPreferences().getLong(AnnouncementsConstants.PREF_LAST_LAUNCH, 0);
|
||||
}
|
||||
|
||||
private SharedPreferences getSharedPreferences() {
|
||||
protected SharedPreferences getSharedPreferences() {
|
||||
return this.getSharedPreferences(AnnouncementsConstants.PREFS_BRANCH, GlobalConstants.SHARED_PREFERENCES_MODE);
|
||||
}
|
||||
|
||||
|
|
|
@ -47,6 +47,7 @@ public class GlobalConstants {
|
|||
public static String GECKO_PREFERENCES_CLASS = "org.mozilla.gecko.GeckoPreferences";
|
||||
public static String GECKO_BROADCAST_ANNOUNCEMENTS_PREF_METHOD = "broadcastAnnouncementsPref";
|
||||
public static String GECKO_BROADCAST_HEALTHREPORT_UPLOAD_PREF_METHOD = "broadcastHealthReportUploadPref";
|
||||
public static String GECKO_BROADCAST_HEALTHREPORT_PRUNE_METHOD = "broadcastHealthReportPrune";
|
||||
|
||||
// Common time values.
|
||||
public static final long MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
|
||||
|
|
|
@ -32,8 +32,11 @@ public class EnvironmentBuilder {
|
|||
* Fetch the storage object associated with the provided
|
||||
* {@link ContentProviderClient}. If no storage instance can be found --
|
||||
* perhaps because the {@link ContentProvider} is running in a different
|
||||
* process -- returns <code>null</code>.
|
||||
*
|
||||
* process -- returns <code>null</code>. On success, the returned
|
||||
* {@link HealthReportDatabaseStorage} instance is owned by the underlying
|
||||
* {@link HealthReportProvider} and thus does not need to be closed by the
|
||||
* caller.
|
||||
*
|
||||
* If the provider is not a {@link HealthReportProvider}, throws a
|
||||
* {@link ClassCastException}, because that would be disastrous.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/* 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/. */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport;
|
||||
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
/**
|
||||
* Watch for notifications to start Health Report background services.
|
||||
*
|
||||
* Some observations:
|
||||
*
|
||||
* From the Android documentation: "Also note that as of Android 3.0 the user
|
||||
* needs to have started the application at least once before your application
|
||||
* can receive android.intent.action.BOOT_COMPLETED events."
|
||||
*
|
||||
* We really do want to launch on BOOT_COMPLETED, since it's possible for a user
|
||||
* to run Firefox, shut down the phone, then power it on again on the same day.
|
||||
* We want to submit a health report in this case, even though they haven't
|
||||
* launched Firefox since boot.
|
||||
*/
|
||||
public class HealthReportBroadcastReceiver extends BroadcastReceiver {
|
||||
public static final String LOG_TAG = HealthReportBroadcastReceiver.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* Forward the intent to an IntentService to do background processing.
|
||||
*/
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Logger.debug(LOG_TAG, "Received intent - forwarding to BroadcastService.");
|
||||
Intent service = new Intent(context, HealthReportBroadcastService.class);
|
||||
service.putExtras(intent);
|
||||
service.setAction(intent.getAction());
|
||||
context.startService(service);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,244 @@
|
|||
/* 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/. */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport;
|
||||
|
||||
import org.mozilla.gecko.background.BackgroundService;
|
||||
import org.mozilla.gecko.background.common.GlobalConstants;
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
import org.mozilla.gecko.background.healthreport.prune.HealthReportPruneService;
|
||||
import org.mozilla.gecko.background.healthreport.upload.HealthReportUploadService;
|
||||
import org.mozilla.gecko.background.healthreport.upload.ObsoleteDocumentTracker;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.Editor;
|
||||
|
||||
/**
|
||||
* A service which listens to broadcast intents from the system and from the
|
||||
* browser, registering or unregistering the background health report services with the
|
||||
* {@link AlarmManager}.
|
||||
*/
|
||||
public class HealthReportBroadcastService extends BackgroundService {
|
||||
public static final String LOG_TAG = HealthReportBroadcastService.class.getSimpleName();
|
||||
public static final String WORKER_THREAD_NAME = LOG_TAG + "Worker";
|
||||
|
||||
public HealthReportBroadcastService() {
|
||||
super(WORKER_THREAD_NAME);
|
||||
}
|
||||
|
||||
protected SharedPreferences getSharedPreferences() {
|
||||
return this.getSharedPreferences(HealthReportConstants.PREFS_BRANCH, GlobalConstants.SHARED_PREFERENCES_MODE);
|
||||
}
|
||||
|
||||
public long getSubmissionPollInterval() {
|
||||
return getSharedPreferences().getLong(HealthReportConstants.PREF_SUBMISSION_INTENT_INTERVAL_MSEC, HealthReportConstants.DEFAULT_SUBMISSION_INTENT_INTERVAL_MSEC);
|
||||
}
|
||||
|
||||
public void setSubmissionPollInterval(final long interval) {
|
||||
getSharedPreferences().edit().putLong(HealthReportConstants.PREF_SUBMISSION_INTENT_INTERVAL_MSEC, interval).commit();
|
||||
}
|
||||
|
||||
public long getPrunePollInterval() {
|
||||
return getSharedPreferences().getLong(HealthReportConstants.PREF_PRUNE_INTENT_INTERVAL_MSEC,
|
||||
HealthReportConstants.DEFAULT_PRUNE_INTENT_INTERVAL_MSEC);
|
||||
}
|
||||
|
||||
public void setPrunePollInterval(final long interval) {
|
||||
getSharedPreferences().edit().putLong(HealthReportConstants.PREF_PRUNE_INTENT_INTERVAL_MSEC,
|
||||
interval).commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set or cancel an alarm to submit data for a profile.
|
||||
*
|
||||
* @param context
|
||||
* Android context.
|
||||
* @param profileName
|
||||
* to submit data for.
|
||||
* @param profilePath
|
||||
* to submit data for.
|
||||
* @param enabled
|
||||
* whether the user has enabled submitting health report data for
|
||||
* this profile.
|
||||
* @param serviceEnabled
|
||||
* whether submitting should be scheduled. If the user turns off
|
||||
* submitting, <code>enabled</code> could be false but we could need
|
||||
* to delete so <code>serviceEnabled</code> could be true.
|
||||
*/
|
||||
protected void toggleSubmissionAlarm(final Context context, String profileName, String profilePath,
|
||||
boolean enabled, boolean serviceEnabled) {
|
||||
final Class<?> serviceClass = HealthReportUploadService.class;
|
||||
Logger.info(LOG_TAG, (serviceEnabled ? "R" : "Unr") + "egistering " +
|
||||
serviceClass.getSimpleName() + ".");
|
||||
|
||||
// PendingIntents are compared without reference to their extras. Therefore
|
||||
// even though we pass the profile details to the action, different
|
||||
// profiles will share the *same* pending intent. In a multi-profile future,
|
||||
// this will need to be addressed. See Bug 882182.
|
||||
final Intent service = new Intent(context, serviceClass);
|
||||
service.setAction("upload"); // PendingIntents "lose" their extras if no action is set.
|
||||
service.putExtra("uploadEnabled", enabled);
|
||||
service.putExtra("profileName", profileName);
|
||||
service.putExtra("profilePath", profilePath);
|
||||
final PendingIntent pending = PendingIntent.getService(context, 0, service, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
|
||||
if (!serviceEnabled) {
|
||||
cancelAlarm(pending);
|
||||
return;
|
||||
}
|
||||
|
||||
final long pollInterval = getSubmissionPollInterval();
|
||||
scheduleAlarm(pollInterval, pending);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(Intent intent) {
|
||||
Logger.setThreadLogTag(HealthReportConstants.GLOBAL_LOG_TAG);
|
||||
|
||||
// The same intent can be handled by multiple methods so do not short-circuit evaluate.
|
||||
boolean handled = attemptHandleIntentForUpload(intent);
|
||||
handled = attemptHandleIntentForPrune(intent) ? true : handled;
|
||||
|
||||
if (!handled) {
|
||||
Logger.warn(LOG_TAG, "Unhandled intent with action " + intent.getAction() + ".");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to handle the given intent for FHR document upload. If it cannot, false is returned.
|
||||
*/
|
||||
protected boolean attemptHandleIntentForUpload(final Intent intent) {
|
||||
if (HealthReportConstants.UPLOAD_FEATURE_DISABLED) {
|
||||
Logger.debug(LOG_TAG, "Health report upload feature is compile-time disabled; not handling intent.");
|
||||
return false;
|
||||
}
|
||||
|
||||
final String action = intent.getAction();
|
||||
Logger.debug(LOG_TAG, "Health report upload feature is compile-time enabled; handling intent with action " + action + ".");
|
||||
|
||||
if (HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF.equals(action)) {
|
||||
handleUploadPrefIntent(intent);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Intent.ACTION_BOOT_COMPLETED.equals(action) ||
|
||||
Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) {
|
||||
BackgroundService.reflectContextToFennec(this,
|
||||
GlobalConstants.GECKO_PREFERENCES_CLASS,
|
||||
GlobalConstants.GECKO_BROADCAST_HEALTHREPORT_UPLOAD_PREF_METHOD);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the intent sent by the browser when it wishes to notify us
|
||||
* of the value of the user preference. Look at the value and toggle the
|
||||
* alarm service accordingly.
|
||||
*/
|
||||
protected void handleUploadPrefIntent(Intent intent) {
|
||||
if (!intent.hasExtra("enabled")) {
|
||||
Logger.warn(LOG_TAG, "Got " + HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF + " intent without enabled. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
final boolean enabled = intent.getBooleanExtra("enabled", true);
|
||||
Logger.debug(LOG_TAG, intent.getStringExtra("branch") + "/" +
|
||||
intent.getStringExtra("pref") + " = " +
|
||||
(intent.hasExtra("enabled") ? enabled : ""));
|
||||
|
||||
String profileName = intent.getStringExtra("profileName");
|
||||
String profilePath = intent.getStringExtra("profilePath");
|
||||
|
||||
if (profileName == null || profilePath == null) {
|
||||
Logger.warn(LOG_TAG, "Got " + HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF + " intent without profilePath or profileName. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.pii(LOG_TAG, "Updating health report upload alarm for profile " + profileName + " at " +
|
||||
profilePath + ".");
|
||||
|
||||
final SharedPreferences sharedPrefs = getSharedPreferences();
|
||||
final ObsoleteDocumentTracker tracker = new ObsoleteDocumentTracker(sharedPrefs);
|
||||
final boolean hasObsoleteIds = tracker.hasObsoleteIds();
|
||||
|
||||
if (!enabled) {
|
||||
final Editor editor = sharedPrefs.edit();
|
||||
editor.remove(HealthReportConstants.PREF_LAST_UPLOAD_DOCUMENT_ID);
|
||||
|
||||
if (hasObsoleteIds) {
|
||||
Logger.debug(LOG_TAG, "Health report upload disabled; scheduling deletion of " + tracker.numberOfObsoleteIds() + " documents.");
|
||||
tracker.limitObsoleteIds();
|
||||
} else {
|
||||
// Primarily intended for debugging and testing.
|
||||
Logger.debug(LOG_TAG, "Health report upload disabled and no deletes to schedule: clearing prefs.");
|
||||
editor.remove(HealthReportConstants.PREF_FIRST_RUN);
|
||||
editor.remove(HealthReportConstants.PREF_NEXT_SUBMISSION);
|
||||
}
|
||||
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
// The user can toggle us off or on, or we can have obsolete documents to
|
||||
// remove.
|
||||
final boolean serviceEnabled = hasObsoleteIds || enabled;
|
||||
toggleSubmissionAlarm(this, profileName, profilePath, enabled, serviceEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to handle the given intent for FHR data pruning. If it cannot, false is returned.
|
||||
*/
|
||||
protected boolean attemptHandleIntentForPrune(final Intent intent) {
|
||||
final String action = intent.getAction();
|
||||
Logger.debug(LOG_TAG, "Prune: Handling intent with action, " + action + ".");
|
||||
|
||||
if (HealthReportConstants.ACTION_HEALTHREPORT_PRUNE.equals(action)) {
|
||||
handlePruneIntent(intent);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Intent.ACTION_BOOT_COMPLETED.equals(action) ||
|
||||
Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(action)) {
|
||||
BackgroundService.reflectContextToFennec(this,
|
||||
GlobalConstants.GECKO_PREFERENCES_CLASS,
|
||||
GlobalConstants.GECKO_BROADCAST_HEALTHREPORT_PRUNE_METHOD);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected void handlePruneIntent(final Intent intent) {
|
||||
final String profileName = intent.getStringExtra("profileName");
|
||||
final String profilePath = intent.getStringExtra("profilePath");
|
||||
|
||||
if (profileName == null || profilePath == null) {
|
||||
Logger.warn(LOG_TAG, "Got " + HealthReportConstants.ACTION_HEALTHREPORT_PRUNE + " intent " +
|
||||
"without profilePath or profileName. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
final Class<?> serviceClass = HealthReportPruneService.class;
|
||||
final Intent service = new Intent(this, serviceClass);
|
||||
service.setAction("prune"); // Intents without actions have their extras removed.
|
||||
service.putExtra("profileName", profileName);
|
||||
service.putExtra("profilePath", profilePath);
|
||||
final PendingIntent pending = PendingIntent.getService(this, 0, service,
|
||||
PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
|
||||
// Set a regular alarm to start PruneService. Since the various actions that PruneService can
|
||||
// take occur on irregular intervals, we can be more efficient by only starting the Service
|
||||
// when one of these time limits runs out. However, subsequent Service invocations must then
|
||||
// be registered by the PruneService itself, which would fail if the PruneService crashes.
|
||||
// Thus, we set this regular (and slightly inefficient) alarm.
|
||||
Logger.info(LOG_TAG, "Registering " + serviceClass.getSimpleName() + ".");
|
||||
final long pollInterval = getPrunePollInterval();
|
||||
scheduleAlarm(pollInterval, pending);
|
||||
}
|
||||
}
|
|
@ -24,13 +24,17 @@ public class HealthReportConstants {
|
|||
// settings are stored.
|
||||
public static final String PREFS_BRANCH = "background";
|
||||
|
||||
// How frequently the submission policy is ticked over. This is how frequently our
|
||||
// intent is scheduled to be called by the Android Alarm Manager, not how
|
||||
// frequently we actually submit.
|
||||
// How frequently the submission and prune policies are ticked over. This is how frequently our
|
||||
// intent is scheduled to be called by the Android Alarm Manager, not how frequently we
|
||||
// actually submit. These values are set as preferences rather than constants so that testing
|
||||
// addons can change their values.
|
||||
public static final String PREF_SUBMISSION_INTENT_INTERVAL_MSEC = "healthreport_submission_intent_interval_msec";
|
||||
public static final long DEFAULT_SUBMISSION_INTENT_INTERVAL_MSEC = GlobalConstants.MILLISECONDS_PER_DAY / 24;
|
||||
public static final String PREF_PRUNE_INTENT_INTERVAL_MSEC = "healthreport_prune_intent_interval_msec";
|
||||
public static final long DEFAULT_PRUNE_INTENT_INTERVAL_MSEC = GlobalConstants.MILLISECONDS_PER_DAY;
|
||||
|
||||
public static final String ACTION_HEALTHREPORT_UPLOAD_PREF = "@ANDROID_PACKAGE_NAME@.HEALTHREPORT_UPLOAD_PREF";
|
||||
public static final String ACTION_HEALTHREPORT_PRUNE = "@ANDROID_PACKAGE_NAME@.HEALTHREPORT_PRUNE";
|
||||
|
||||
public static final String PREF_MINIMUM_TIME_BETWEEN_UPLOADS = "healthreport_time_between_uploads";
|
||||
public static final long DEFAULT_MINIMUM_TIME_BETWEEN_UPLOADS = GlobalConstants.MILLISECONDS_PER_DAY;
|
||||
|
@ -104,4 +108,19 @@ public class HealthReportConstants {
|
|||
// will handle such API usage. This obsoletes 2 days worth of old documents
|
||||
// at a time.
|
||||
public static final int MAXIMUM_DELETIONS_PER_POST = ((int) DEFAULT_MAXIMUM_FAILURES_PER_DAY + 1) * 2;
|
||||
|
||||
public static final String PREF_PRUNE_BY_SIZE_TIME = "healthreport_prune_by_size_time";
|
||||
public static final long MINIMUM_TIME_BETWEEN_PRUNE_BY_SIZE_CHECKS_MILLIS =
|
||||
GlobalConstants.MILLISECONDS_PER_DAY;
|
||||
public static final int MAX_ENVIRONMENT_COUNT = 50;
|
||||
public static final int ENVIRONMENT_COUNT_AFTER_PRUNE = 35;
|
||||
public static final int MAX_EVENT_COUNT = 10000;
|
||||
public static final int EVENT_COUNT_AFTER_PRUNE = 8000;
|
||||
|
||||
public static final String PREF_EXPIRATION_TIME = "healthreport_expiration_time";
|
||||
public static final long MINIMUM_TIME_BETWEEN_EXPIRATION_CHECKS_MILLIS = GlobalConstants.MILLISECONDS_PER_DAY * 7;
|
||||
public static final long EVENT_EXISTENCE_DURATION = GlobalConstants.MILLISECONDS_PER_SIX_MONTHS;
|
||||
|
||||
public static final String PREF_CLEANUP_TIME = "healthreport_cleanup_time";
|
||||
public static final long MINIMUM_TIME_BETWEEN_CLEANUP_CHECKS_MILLIS = GlobalConstants.MILLISECONDS_PER_DAY * 30;
|
||||
}
|
||||
|
|
|
@ -247,111 +247,104 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
|
|||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.execSQL("CREATE TABLE addons (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
" body TEXT, " +
|
||||
" UNIQUE (body) " +
|
||||
")");
|
||||
db.execSQL("CREATE TABLE addons (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
" body TEXT, " +
|
||||
" UNIQUE (body) " +
|
||||
")");
|
||||
|
||||
db.execSQL("CREATE TABLE environments (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
" hash TEXT, " +
|
||||
" profileCreation INTEGER, " +
|
||||
" cpuCount INTEGER, " +
|
||||
" memoryMB INTEGER, " +
|
||||
" isBlocklistEnabled INTEGER, " +
|
||||
" isTelemetryEnabled INTEGER, " +
|
||||
" extensionCount INTEGER, " +
|
||||
" pluginCount INTEGER, " +
|
||||
" themeCount INTEGER, " +
|
||||
" architecture TEXT, " +
|
||||
" sysName TEXT, " +
|
||||
" sysVersion TEXT, " +
|
||||
" vendor TEXT, " +
|
||||
" appName TEXT, " +
|
||||
" appID TEXT, " +
|
||||
" appVersion TEXT, " +
|
||||
" appBuildID TEXT, " +
|
||||
" platformVersion TEXT, " +
|
||||
" platformBuildID TEXT, " +
|
||||
" os TEXT, " +
|
||||
" xpcomabi TEXT, " +
|
||||
" updateChannel TEXT, " +
|
||||
" addonsID INTEGER, " +
|
||||
" FOREIGN KEY (addonsID) REFERENCES addons(id) ON DELETE RESTRICT, " +
|
||||
" UNIQUE (hash) " +
|
||||
")");
|
||||
db.execSQL("CREATE TABLE environments (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
" hash TEXT, " +
|
||||
" profileCreation INTEGER, " +
|
||||
" cpuCount INTEGER, " +
|
||||
" memoryMB INTEGER, " +
|
||||
" isBlocklistEnabled INTEGER, " +
|
||||
" isTelemetryEnabled INTEGER, " +
|
||||
" extensionCount INTEGER, " +
|
||||
" pluginCount INTEGER, " +
|
||||
" themeCount INTEGER, " +
|
||||
" architecture TEXT, " +
|
||||
" sysName TEXT, " +
|
||||
" sysVersion TEXT, " +
|
||||
" vendor TEXT, " +
|
||||
" appName TEXT, " +
|
||||
" appID TEXT, " +
|
||||
" appVersion TEXT, " +
|
||||
" appBuildID TEXT, " +
|
||||
" platformVersion TEXT, " +
|
||||
" platformBuildID TEXT, " +
|
||||
" os TEXT, " +
|
||||
" xpcomabi TEXT, " +
|
||||
" updateChannel TEXT, " +
|
||||
" addonsID INTEGER, " +
|
||||
" FOREIGN KEY (addonsID) REFERENCES addons(id) ON DELETE RESTRICT, " +
|
||||
" UNIQUE (hash) " +
|
||||
")");
|
||||
|
||||
db.execSQL("CREATE TABLE measurements (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
" name TEXT, " +
|
||||
" version INTEGER, " +
|
||||
" UNIQUE (name, version) " +
|
||||
")");
|
||||
db.execSQL("CREATE TABLE measurements (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
" name TEXT, " +
|
||||
" version INTEGER, " +
|
||||
" UNIQUE (name, version) " +
|
||||
")");
|
||||
|
||||
db.execSQL("CREATE TABLE fields (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
" measurement INTEGER, " +
|
||||
" name TEXT, " +
|
||||
" flags INTEGER, " +
|
||||
" FOREIGN KEY (measurement) REFERENCES measurements(id) ON DELETE CASCADE, " +
|
||||
" UNIQUE (measurement, name)" +
|
||||
")");
|
||||
db.execSQL("CREATE TABLE fields (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
" measurement INTEGER, " +
|
||||
" name TEXT, " +
|
||||
" flags INTEGER, " +
|
||||
" FOREIGN KEY (measurement) REFERENCES measurements(id) ON DELETE CASCADE, " +
|
||||
" UNIQUE (measurement, name)" +
|
||||
")");
|
||||
|
||||
db.execSQL("CREATE TABLE " + EVENTS_INTEGER + "(" +
|
||||
" date INTEGER, " +
|
||||
" env INTEGER, " +
|
||||
" field INTEGER, " +
|
||||
" value INTEGER, " +
|
||||
" FOREIGN KEY (field) REFERENCES fields(id) ON DELETE CASCADE, " +
|
||||
" FOREIGN KEY (env) REFERENCES environments(id) ON DELETE CASCADE" +
|
||||
")");
|
||||
db.execSQL("CREATE TABLE " + EVENTS_INTEGER + "(" +
|
||||
" date INTEGER, " +
|
||||
" env INTEGER, " +
|
||||
" field INTEGER, " +
|
||||
" value INTEGER, " +
|
||||
" FOREIGN KEY (field) REFERENCES fields(id) ON DELETE CASCADE, " +
|
||||
" FOREIGN KEY (env) REFERENCES environments(id) ON DELETE CASCADE" +
|
||||
")");
|
||||
|
||||
db.execSQL("CREATE TABLE " + EVENTS_TEXTUAL + "(" +
|
||||
" date INTEGER, " +
|
||||
" env INTEGER, " +
|
||||
" field INTEGER, " +
|
||||
" value TEXT, " +
|
||||
" FOREIGN KEY (field) REFERENCES fields(id) ON DELETE CASCADE, " +
|
||||
" FOREIGN KEY (env) REFERENCES environments(id) ON DELETE CASCADE" +
|
||||
")");
|
||||
db.execSQL("CREATE TABLE " + EVENTS_TEXTUAL + "(" +
|
||||
" date INTEGER, " +
|
||||
" env INTEGER, " +
|
||||
" field INTEGER, " +
|
||||
" value TEXT, " +
|
||||
" FOREIGN KEY (field) REFERENCES fields(id) ON DELETE CASCADE, " +
|
||||
" FOREIGN KEY (env) REFERENCES environments(id) ON DELETE CASCADE" +
|
||||
")");
|
||||
|
||||
db.execSQL("CREATE INDEX idx_events_integer_date_env_field ON events_integer (date, env, field)");
|
||||
db.execSQL("CREATE INDEX idx_events_textual_date_env_field ON events_textual (date, env, field)");
|
||||
db.execSQL("CREATE INDEX idx_events_integer_date_env_field ON events_integer (date, env, field)");
|
||||
db.execSQL("CREATE INDEX idx_events_textual_date_env_field ON events_textual (date, env, field)");
|
||||
|
||||
db.execSQL("CREATE VIEW events AS " +
|
||||
"SELECT date, env, field, value FROM " + EVENTS_INTEGER + " " +
|
||||
"UNION ALL " +
|
||||
"SELECT date, env, field, value FROM " + EVENTS_TEXTUAL);
|
||||
db.execSQL("CREATE VIEW events AS " +
|
||||
"SELECT date, env, field, value FROM " + EVENTS_INTEGER + " " +
|
||||
"UNION ALL " +
|
||||
"SELECT date, env, field, value FROM " + EVENTS_TEXTUAL);
|
||||
|
||||
db.execSQL("CREATE VIEW named_events AS " +
|
||||
"SELECT date, " +
|
||||
" environments.hash AS environment, " +
|
||||
" measurements.name AS measurement_name, " +
|
||||
" measurements.version AS measurement_version, " +
|
||||
" fields.name AS field_name, " +
|
||||
" fields.flags AS field_flags, " +
|
||||
" value FROM " +
|
||||
"events JOIN environments ON events.env = environments.id " +
|
||||
" JOIN fields ON events.field = fields.id " +
|
||||
" JOIN measurements ON fields.measurement = measurements.id");
|
||||
db.execSQL("CREATE VIEW named_events AS " +
|
||||
"SELECT date, " +
|
||||
" environments.hash AS environment, " +
|
||||
" measurements.name AS measurement_name, " +
|
||||
" measurements.version AS measurement_version, " +
|
||||
" fields.name AS field_name, " +
|
||||
" fields.flags AS field_flags, " +
|
||||
" value FROM " +
|
||||
"events JOIN environments ON events.env = environments.id " +
|
||||
" JOIN fields ON events.field = fields.id " +
|
||||
" JOIN measurements ON fields.measurement = measurements.id");
|
||||
|
||||
db.execSQL("CREATE VIEW named_fields AS " +
|
||||
"SELECT measurements.name AS measurement_name, " +
|
||||
" measurements.id AS measurement_id, " +
|
||||
" measurements.version AS measurement_version, " +
|
||||
" fields.name AS field_name, " +
|
||||
" fields.id AS field_id, " +
|
||||
" fields.flags AS field_flags " +
|
||||
"FROM fields JOIN measurements ON fields.measurement = measurements.id");
|
||||
db.execSQL("CREATE VIEW named_fields AS " +
|
||||
"SELECT measurements.name AS measurement_name, " +
|
||||
" measurements.id AS measurement_id, " +
|
||||
" measurements.version AS measurement_version, " +
|
||||
" fields.name AS field_name, " +
|
||||
" fields.id AS field_id, " +
|
||||
" fields.flags AS field_flags " +
|
||||
"FROM fields JOIN measurements ON fields.measurement = measurements.id");
|
||||
|
||||
db.execSQL("CREATE VIEW current_measurements AS " +
|
||||
"SELECT name, MAX(version) AS version FROM measurements GROUP BY name");
|
||||
db.execSQL("CREATE VIEW current_measurements AS " +
|
||||
"SELECT name, MAX(version) AS version FROM measurements GROUP BY name");
|
||||
|
||||
createAddonsEnvironmentsView(db);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
createAddonsEnvironmentsView(db);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -432,7 +425,6 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
|
|||
|
||||
Logger.info(LOG_TAG, "onUpgrade: from " + oldVersion + " to " + newVersion + ".");
|
||||
try {
|
||||
db.beginTransaction();
|
||||
switch (oldVersion) {
|
||||
case 2:
|
||||
upgradeDatabaseFrom2To3(db);
|
||||
|
@ -441,12 +433,9 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
|
|||
case 4:
|
||||
upgradeDatabaseFrom4to5(db);
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
} catch (Exception e) {
|
||||
Logger.error(LOG_TAG, "Failure in onUpgrade.", e);
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1028,6 +1017,19 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
|
|||
this.helper.getWritableDatabase().endTransaction();
|
||||
}
|
||||
|
||||
protected int getIntFromQuery(final String sql, final String[] selectionArgs) {
|
||||
final SQLiteDatabase db = this.helper.getReadableDatabase();
|
||||
final Cursor c = db.rawQuery(sql, selectionArgs);
|
||||
try {
|
||||
if (!c.moveToFirst()) {
|
||||
throw new IllegalStateException("Cursor is empty.");
|
||||
}
|
||||
return c.getInt(0);
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDay(long time) {
|
||||
return DateUtils.getDay(time);
|
||||
|
@ -1251,6 +1253,135 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
|
|||
"date, environment, measurement_name, measurement_version, field_name");
|
||||
}
|
||||
|
||||
public int getEventCount() {
|
||||
return getRowCount("events");
|
||||
}
|
||||
|
||||
public int getEnvironmentCount() {
|
||||
return getRowCount("environments");
|
||||
}
|
||||
|
||||
private int getRowCount(String table) {
|
||||
// table should be parameterized, but SQL throws a compilation error if the table in unknown
|
||||
// in advance.
|
||||
return getIntFromQuery("SELECT COUNT(*) from " + table, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all environments, addons, and events from the database before the given time. If this
|
||||
* data does not have recorded dates but are no longer referenced by other fields, they are also
|
||||
* removed (with exception to the current environment).
|
||||
*
|
||||
* @param time milliseconds since epoch. Will be converted by {@link #getDay(long)}.
|
||||
* @param curEnv The ID of the current environment.
|
||||
* @return The number of environments and addon entries deleted.
|
||||
*/
|
||||
public int deleteDataBefore(final long time, final int curEnv) {
|
||||
final SQLiteDatabase db = this.helper.getWritableDatabase();
|
||||
db.beginTransaction();
|
||||
int numRowsDeleted = 0;
|
||||
try {
|
||||
numRowsDeleted += deleteEnvAndEventsBefore(db, time, curEnv);
|
||||
numRowsDeleted += deleteOrphanedAddons(db);
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
return numRowsDeleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes environments and their referring events recorded before the given time. Environments
|
||||
* referenced by no events are deleted, except for the current environment.
|
||||
*
|
||||
* @param time milliseconds since epoch. Will be converted by {@link #getDay(long)}.
|
||||
* @param curEnv The ID of the current environment.
|
||||
* @return The number of environments (not events) deleted.
|
||||
*/
|
||||
protected int deleteEnvAndEventsBefore(final long time, final int curEnv) {
|
||||
final SQLiteDatabase db = this.helper.getWritableDatabase();
|
||||
return deleteEnvAndEventsBefore(db, time, curEnv);
|
||||
}
|
||||
|
||||
// Called internally only to ensure the same db instance is used.
|
||||
protected int deleteEnvAndEventsBefore(final SQLiteDatabase db, final long time, final int curEnv) {
|
||||
// Delete environments only referenced by events occuring before the given time. Cascade
|
||||
// delete these events.
|
||||
String whereClause =
|
||||
"(SELECT COUNT(*) FROM events WHERE date >= ? " +
|
||||
" AND events.env = environments.id) = 0 " +
|
||||
"AND id IN (SELECT DISTINCT env FROM events WHERE date < ?)";
|
||||
final int day = this.getDay(time);
|
||||
final String dayString = Integer.toString(day, 10);
|
||||
String[] whereArgs = new String[] {dayString, dayString};
|
||||
|
||||
int numEnvDeleted = 0;
|
||||
db.beginTransaction();
|
||||
try {
|
||||
numEnvDeleted += db.delete("environments", whereClause, whereArgs);
|
||||
numEnvDeleted += deleteOrphanedEnv(db, curEnv);
|
||||
// We can't get the number of events deleted through cascading deletions so we do not record
|
||||
// the number of events deleted here.
|
||||
deleteEventsBefore(db, dayString);
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
return numEnvDeleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes environments not referenced by any events except for the given current environment.
|
||||
*/
|
||||
protected int deleteOrphanedEnv(final int curEnv) {
|
||||
final SQLiteDatabase db = this.helper.getWritableDatabase();
|
||||
return deleteOrphanedEnv(db, curEnv);
|
||||
}
|
||||
|
||||
// Called internally only to ensure the same db instance is used.
|
||||
protected int deleteOrphanedEnv(final SQLiteDatabase db, final int curEnv) {
|
||||
final String whereClause =
|
||||
"id != ? AND " +
|
||||
"id NOT IN (SELECT env FROM events)";
|
||||
final String[] whereArgs = new String[] {Integer.toString(curEnv)};
|
||||
return db.delete("environments", whereClause, whereArgs);
|
||||
}
|
||||
|
||||
protected int deleteEventsBefore(final String dayString) {
|
||||
final SQLiteDatabase db = this.helper.getWritableDatabase();
|
||||
return deleteEventsBefore(db, dayString);
|
||||
}
|
||||
|
||||
// Called internally only to ensure the same db instance is used.
|
||||
protected int deleteEventsBefore(final SQLiteDatabase db, final String dayString) {
|
||||
final String whereClause = "date < ?";
|
||||
final String[] whereArgs = new String[] {dayString};
|
||||
int numEventsDeleted = 0;
|
||||
db.beginTransaction();
|
||||
try {
|
||||
numEventsDeleted += db.delete("events_integer", whereClause, whereArgs);
|
||||
numEventsDeleted += db.delete("events_textual", whereClause, whereArgs);
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
return numEventsDeleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes addons not referenced by any environments.
|
||||
*/
|
||||
protected int deleteOrphanedAddons() {
|
||||
final SQLiteDatabase db = this.helper.getWritableDatabase();
|
||||
return deleteOrphanedAddons(db);
|
||||
}
|
||||
|
||||
// Called internally only to ensure the same db instance is used.
|
||||
protected int deleteOrphanedAddons(final SQLiteDatabase db) {
|
||||
final String whereClause = "id NOT IN (SELECT addonsID FROM environments)";
|
||||
return db.delete("addons", whereClause, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a mapping from a table. Keys should be unique; only one key-value
|
||||
* pair will be returned for each key.
|
||||
|
@ -1336,4 +1467,78 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
|
|||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prunes the given number of least-recently used environments. Note that orphaned environments
|
||||
* are not removed.
|
||||
*/
|
||||
public void pruneEnvironments(final int numToPrune) {
|
||||
final SQLiteDatabase db = this.helper.getWritableDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.delete("environments",
|
||||
"id in (SELECT env " +
|
||||
" FROM events " +
|
||||
" GROUP BY env " +
|
||||
" ORDER BY MAX(date), env " +
|
||||
" LIMIT " + numToPrune + ")",
|
||||
null);
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prunes up to a maximum of the given number of the oldest events. While it is more correct to
|
||||
* prune the exact given amount, there is no unique identifier among events so we cannot be so
|
||||
* precise. Instead, we prune on date, deleting all events up to the day before our count of
|
||||
* events reaches the given maximum. Note that this technicality means this method cannot be
|
||||
* used to delete all events.
|
||||
*/
|
||||
public void pruneEvents(final int maxNumToPrune) {
|
||||
final SQLiteDatabase db = this.helper.getWritableDatabase();
|
||||
|
||||
final Cursor c = db.rawQuery(
|
||||
"SELECT MAX(date) " +
|
||||
"FROM (SELECT date " +
|
||||
" FROM events " +
|
||||
" ORDER BY date " +
|
||||
" LIMIT " + maxNumToPrune + ")",
|
||||
null);
|
||||
long pruneDate = -1;
|
||||
try {
|
||||
if (!c.moveToFirst()) {
|
||||
Logger.debug(LOG_TAG, "No max date found in events: table is likely empty. Not pruning " +
|
||||
"events.");
|
||||
return;
|
||||
}
|
||||
pruneDate = c.getLong(0);
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
|
||||
final String selection = "date < " + pruneDate;
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.delete(EVENTS_INTEGER, selection, null);
|
||||
db.delete(EVENTS_TEXTUAL, selection, null);
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public void vacuum() {
|
||||
final SQLiteDatabase db = this.helper.getWritableDatabase();
|
||||
db.execSQL("vacuum");
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables auto_vacuuming. Changes may only take effect after a "vacuum" command.
|
||||
*/
|
||||
public void disableAutoVacuuming() {
|
||||
final SQLiteDatabase db = this.helper.getWritableDatabase();
|
||||
db.execSQL("PRAGMA auto_vacuum=0");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -99,6 +99,9 @@ public class HealthReportProvider extends ContentProvider {
|
|||
|
||||
@Override
|
||||
public void onLowMemory() {
|
||||
// While we could prune the database here, it wouldn't help - it would restore disk space
|
||||
// rather then lower our RAM usage. Additionally, pruning the database may use even more
|
||||
// memory and take too long to run in this method.
|
||||
super.onLowMemory();
|
||||
databases.closeDatabaseHelpers();
|
||||
}
|
||||
|
|
|
@ -219,6 +219,20 @@ public interface HealthReportStorage {
|
|||
public void deleteEverything();
|
||||
public void deleteEnvironments();
|
||||
public void deleteMeasurements();
|
||||
/**
|
||||
* Deletes all environments, addons, and events from the database before the given time.
|
||||
*
|
||||
* @param time milliseconds since epoch.
|
||||
* @param curEnv The ID of the current environment.
|
||||
* @return The number of environments and addon entries deleted.
|
||||
*/
|
||||
public int deleteDataBefore(final long time, final int curEnv);
|
||||
|
||||
public int getEventCount();
|
||||
public int getEnvironmentCount();
|
||||
|
||||
public void pruneEvents(final int num);
|
||||
public void pruneEnvironments(final int num);
|
||||
|
||||
public void enqueueOperation(Runnable runnable);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
/* 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/. */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport.prune;
|
||||
|
||||
import org.mozilla.gecko.background.BackgroundService;
|
||||
import org.mozilla.gecko.background.common.GlobalConstants;
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportConstants;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.IBinder;
|
||||
|
||||
/**
|
||||
* A <code>Service</code> to prune unnecessary or excessive health report data.
|
||||
*
|
||||
* We extend <code>IntentService</code>, rather than just <code>Service</code>,
|
||||
* because this gives us a worker thread to avoid excessive main-thread disk access.
|
||||
*/
|
||||
public class HealthReportPruneService extends BackgroundService {
|
||||
public static final String LOG_TAG = HealthReportPruneService.class.getSimpleName();
|
||||
public static final String WORKER_THREAD_NAME = LOG_TAG + "Worker";
|
||||
|
||||
public HealthReportPruneService() {
|
||||
super(WORKER_THREAD_NAME);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected SharedPreferences getSharedPreferences() {
|
||||
return this.getSharedPreferences(HealthReportConstants.PREFS_BRANCH, GlobalConstants.SHARED_PREFERENCES_MODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHandleIntent(Intent intent) {
|
||||
Logger.setThreadLogTag(HealthReportConstants.GLOBAL_LOG_TAG);
|
||||
Logger.debug(LOG_TAG, "Handling prune intent.");
|
||||
|
||||
if (!isIntentValid(intent)) {
|
||||
Logger.warn(LOG_TAG, "Intent not valid - returning.");
|
||||
return;
|
||||
}
|
||||
|
||||
final String profileName = intent.getStringExtra("profileName");
|
||||
final String profilePath = intent.getStringExtra("profilePath");
|
||||
Logger.debug(LOG_TAG, "Ticking for profile " + profileName + " at " + profilePath + ".");
|
||||
final PrunePolicy policy = getPrunePolicy(profilePath);
|
||||
policy.tick(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
// Generator function wraps constructor for testing purposes.
|
||||
protected PrunePolicy getPrunePolicy(final String profilePath) {
|
||||
final PrunePolicyStorage storage = new PrunePolicyDatabaseStorage(this, profilePath);
|
||||
return new PrunePolicy(storage, getSharedPreferences());
|
||||
}
|
||||
|
||||
protected boolean isIntentValid(final Intent intent) {
|
||||
boolean isValid = true;
|
||||
|
||||
final String profileName = intent.getStringExtra("profileName");
|
||||
if (profileName == null) {
|
||||
Logger.warn(LOG_TAG, "Got intent without profileName.");
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
final String profilePath = intent.getStringExtra("profilePath");
|
||||
if (profilePath == null) {
|
||||
Logger.warn(LOG_TAG, "Got intent without profilePath.");
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,230 @@
|
|||
/* 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/. */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport.prune;
|
||||
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportConstants;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
/**
|
||||
* Manages scheduling of the pruning of old Firefox Health Report data.
|
||||
*
|
||||
* There are three main actions that take place:
|
||||
* 1) Excessive storage pruning: The recorded data is taking up an unreasonable amount of space.
|
||||
* 2) Expired data pruning: Data that is kept around longer than is useful.
|
||||
* 3) Cleanup: To deal with storage maintenance (e.g. bloat and fragmentation)
|
||||
*
|
||||
* (1) and (2) are performed periodically on their own schedules. (3) will activate after a
|
||||
* certain duration but only after (1) or (2) is performed.
|
||||
*/
|
||||
public class PrunePolicy {
|
||||
public static final String LOG_TAG = PrunePolicy.class.getSimpleName();
|
||||
|
||||
protected final PrunePolicyStorage storage;
|
||||
protected final SharedPreferences sharedPreferences;
|
||||
protected final Editor editor;
|
||||
|
||||
public PrunePolicy(final PrunePolicyStorage storage, final SharedPreferences sharedPrefs) {
|
||||
this.storage = storage;
|
||||
this.sharedPreferences = sharedPrefs;
|
||||
this.editor = new Editor(this.sharedPreferences.edit());
|
||||
}
|
||||
|
||||
protected SharedPreferences getSharedPreferences() {
|
||||
return this.sharedPreferences;
|
||||
}
|
||||
|
||||
public void tick(final long time) {
|
||||
try {
|
||||
try {
|
||||
boolean pruned = attemptPruneBySize(time);
|
||||
pruned = attemptExpiration(time) ? true : pruned;
|
||||
// We only need to cleanup after a large pruning.
|
||||
if (pruned) {
|
||||
attemptStorageCleanup(time);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// While catching Exception is ordinarily bad form, this Service runs in the same process
|
||||
// as Fennec so if we crash, it crashes. Additionally, this Service runs regularly so
|
||||
// these crashes could be regular. Thus, we choose to quietly fail instead.
|
||||
Logger.error(LOG_TAG, "Got exception pruning document.", e);
|
||||
} finally {
|
||||
editor.commit();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Logger.error(LOG_TAG, "Got exception committing to SharedPreferences.", e);
|
||||
} finally {
|
||||
storage.close();
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean attemptPruneBySize(final long time) {
|
||||
final long nextPrune = getNextPruneBySizeTime();
|
||||
if (nextPrune < 0) {
|
||||
Logger.debug(LOG_TAG, "Initializing prune-by-size time.");
|
||||
editor.setNextPruneBySizeTime(time + getMinimumTimeBetweenPruneBySizeChecks());
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the system clock is skewed into the past, making the time between prunes too long, reset
|
||||
// the clock.
|
||||
if (nextPrune > getMinimumTimeBetweenPruneBySizeChecks() + time) {
|
||||
Logger.debug(LOG_TAG, "Clock skew detected - resetting prune-by-size time.");
|
||||
editor.setNextPruneBySizeTime(time + getMinimumTimeBetweenPruneBySizeChecks());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nextPrune > time) {
|
||||
Logger.debug(LOG_TAG, "Skipping prune-by-size - wait period has not yet elapsed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Prune environments first because their cascading deletions may delete some events. These
|
||||
// environments are pruned in order of least-recently used first. Note that orphaned
|
||||
// environments are ignored here and should be removed elsewhere.
|
||||
final int environmentCount = storage.getEnvironmentCount();
|
||||
if (environmentCount > getMaxEnvironmentCount()) {
|
||||
final int environmentPruneCount = environmentCount - getEnvironmentCountAfterPrune();
|
||||
Logger.debug(LOG_TAG, "Pruning " + environmentPruneCount + " environments.");
|
||||
storage.pruneEnvironments(environmentPruneCount);
|
||||
}
|
||||
|
||||
final int eventCount = storage.getEventCount();
|
||||
if (eventCount > getMaxEventCount()) {
|
||||
final int eventPruneCount = eventCount - getEventCountAfterPrune();
|
||||
Logger.debug(LOG_TAG, "Pruning up to " + eventPruneCount + " events.");
|
||||
storage.pruneEvents(eventPruneCount);
|
||||
}
|
||||
editor.setNextPruneBySizeTime(time + getMinimumTimeBetweenPruneBySizeChecks());
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean attemptExpiration(final long time) {
|
||||
final long nextPrune = getNextExpirationTime();
|
||||
if (nextPrune < 0) {
|
||||
Logger.debug(LOG_TAG, "Initializing expiration time.");
|
||||
editor.setNextExpirationTime(time + getMinimumTimeBetweenExpirationChecks());
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the system clock is skewed into the past, making the time between prunes too long, reset
|
||||
// the clock.
|
||||
if (nextPrune > getMinimumTimeBetweenExpirationChecks() + time) {
|
||||
Logger.debug(LOG_TAG, "Clock skew detected - resetting expiration time.");
|
||||
editor.setNextExpirationTime(time + getMinimumTimeBetweenExpirationChecks());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nextPrune > time) {
|
||||
Logger.debug(LOG_TAG, "Skipping expiration - wait period has not yet elapsed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
final long oldEventTime = time - getEventExistenceDuration();
|
||||
Logger.debug(LOG_TAG, "Pruning data older than " + oldEventTime + ".");
|
||||
storage.deleteDataBefore(oldEventTime);
|
||||
editor.setNextExpirationTime(time + getMinimumTimeBetweenExpirationChecks());
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean attemptStorageCleanup(final long time) {
|
||||
// Cleanup if max duration since last cleanup is exceeded.
|
||||
final long nextCleanup = getNextCleanupTime();
|
||||
if (nextCleanup < 0) {
|
||||
Logger.debug(LOG_TAG, "Initializing cleanup time.");
|
||||
editor.setNextCleanupTime(time + getMinimumTimeBetweenCleanupChecks());
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the system clock is skewed into the past, making the time between cleanups too long,
|
||||
// reset the clock.
|
||||
if (nextCleanup > getMinimumTimeBetweenCleanupChecks() + time) {
|
||||
Logger.debug(LOG_TAG, "Clock skew detected - resetting cleanup time.");
|
||||
editor.setNextCleanupTime(time + getMinimumTimeBetweenCleanupChecks());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nextCleanup > time) {
|
||||
Logger.debug(LOG_TAG, "Skipping cleanup - wait period has not yet elapsed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
editor.setNextCleanupTime(time + getMinimumTimeBetweenCleanupChecks());
|
||||
storage.cleanup();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected static class Editor {
|
||||
protected final SharedPreferences.Editor editor;
|
||||
|
||||
public Editor(final SharedPreferences.Editor editor) {
|
||||
this.editor = editor;
|
||||
}
|
||||
|
||||
public void commit() {
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
public Editor setNextExpirationTime(final long time) {
|
||||
editor.putLong(HealthReportConstants.PREF_EXPIRATION_TIME, time);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Editor setNextPruneBySizeTime(final long time) {
|
||||
editor.putLong(HealthReportConstants.PREF_PRUNE_BY_SIZE_TIME, time);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Editor setNextCleanupTime(final long time) {
|
||||
editor.putLong(HealthReportConstants.PREF_CLEANUP_TIME, time);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private long getNextExpirationTime() {
|
||||
return getSharedPreferences().getLong(HealthReportConstants.PREF_EXPIRATION_TIME, -1L);
|
||||
}
|
||||
|
||||
private long getEventExistenceDuration() {
|
||||
return HealthReportConstants.EVENT_EXISTENCE_DURATION;
|
||||
}
|
||||
|
||||
private long getMinimumTimeBetweenExpirationChecks() {
|
||||
return HealthReportConstants.MINIMUM_TIME_BETWEEN_EXPIRATION_CHECKS_MILLIS;
|
||||
}
|
||||
|
||||
private long getNextPruneBySizeTime() {
|
||||
return getSharedPreferences().getLong(HealthReportConstants.PREF_PRUNE_BY_SIZE_TIME, -1L);
|
||||
}
|
||||
|
||||
private long getMinimumTimeBetweenPruneBySizeChecks() {
|
||||
return HealthReportConstants.MINIMUM_TIME_BETWEEN_PRUNE_BY_SIZE_CHECKS_MILLIS;
|
||||
}
|
||||
|
||||
private int getMaxEnvironmentCount() {
|
||||
return HealthReportConstants.MAX_ENVIRONMENT_COUNT;
|
||||
}
|
||||
|
||||
private int getEnvironmentCountAfterPrune() {
|
||||
return HealthReportConstants.ENVIRONMENT_COUNT_AFTER_PRUNE;
|
||||
}
|
||||
|
||||
private int getMaxEventCount() {
|
||||
return HealthReportConstants.MAX_EVENT_COUNT;
|
||||
}
|
||||
|
||||
private int getEventCountAfterPrune() {
|
||||
return HealthReportConstants.EVENT_COUNT_AFTER_PRUNE;
|
||||
}
|
||||
|
||||
private long getNextCleanupTime() {
|
||||
return getSharedPreferences().getLong(HealthReportConstants.PREF_CLEANUP_TIME, -1L);
|
||||
}
|
||||
|
||||
private long getMinimumTimeBetweenCleanupChecks() {
|
||||
return HealthReportConstants.MINIMUM_TIME_BETWEEN_CLEANUP_CHECKS_MILLIS;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
/* 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/. */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport.prune;
|
||||
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
import org.mozilla.gecko.background.healthreport.Environment;
|
||||
import org.mozilla.gecko.background.healthreport.EnvironmentBuilder;
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage;
|
||||
import org.mozilla.gecko.background.healthreport.ProfileInformationCache;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* Abstracts over the Storage instance behind the PrunePolicy. The underlying storage instance is
|
||||
* a {@link HealthReportDatabaseStorage} instance. Since our cleanup routine vacuums, auto_vacuum
|
||||
* can be disabled. It is enabled by default, however, turning it off requires an expensive vacuum
|
||||
* so we wait until our first {@link cleanup} call since we are vacuuming anyway.
|
||||
*/
|
||||
public class PrunePolicyDatabaseStorage implements PrunePolicyStorage {
|
||||
public static final String LOG_TAG = PrunePolicyDatabaseStorage.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
private final String profilePath;
|
||||
|
||||
private ContentProviderClient client;
|
||||
private HealthReportDatabaseStorage storage;
|
||||
|
||||
private int currentEnvironmentID; // So we don't prune the current environment.
|
||||
|
||||
public PrunePolicyDatabaseStorage(final Context context, final String profilePath) {
|
||||
this.context = context;
|
||||
this.profilePath = profilePath;
|
||||
|
||||
this.currentEnvironmentID = -1;
|
||||
}
|
||||
|
||||
public void pruneEvents(final int count) {
|
||||
getStorage().pruneEvents(count);
|
||||
}
|
||||
|
||||
public void pruneEnvironments(final int count) {
|
||||
getStorage().pruneEnvironments(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes data recorded before the given time. Note that if this method fails to retrieve the
|
||||
* current environment from the profile cache, it will not delete data so be sure to prune by
|
||||
* other methods (e.g. {@link pruneEvents}) as well.
|
||||
*/
|
||||
public int deleteDataBefore(final long time) {
|
||||
return getStorage().deleteDataBefore(time, getCurrentEnvironmentID());
|
||||
}
|
||||
|
||||
public void cleanup() {
|
||||
final HealthReportDatabaseStorage storage = getStorage();
|
||||
// The change to auto_vacuum will only take affect after a vacuum.
|
||||
storage.disableAutoVacuuming();
|
||||
storage.vacuum();
|
||||
}
|
||||
|
||||
public int getEventCount() {
|
||||
return getStorage().getEventCount();
|
||||
}
|
||||
|
||||
public int getEnvironmentCount() {
|
||||
return getStorage().getEnvironmentCount();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if (client != null) {
|
||||
client.release();
|
||||
client = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the {@link HealthReportDatabaseStorage} associated with the profile of the policy.
|
||||
* For efficiency, the underlying {@link ContentProviderClient} and
|
||||
* {@link HealthReportDatabaseStorage} are cached for later invocations. However, this means a
|
||||
* call to this method MUST be accompanied by a call to {@link close}. Throws
|
||||
* {@link IllegalStateException} if the storage instance could not be retrieved - note that the
|
||||
* {@link ContentProviderClient} instance will not be closed in this case and
|
||||
* {@link releaseClient} should still be called.
|
||||
*/
|
||||
protected HealthReportDatabaseStorage getStorage() {
|
||||
if (storage != null) {
|
||||
return storage;
|
||||
}
|
||||
|
||||
client = EnvironmentBuilder.getContentProviderClient(context);
|
||||
if (client == null) {
|
||||
// TODO: Record prune failures and submit as part of FHR upload.
|
||||
Logger.warn(LOG_TAG, "Unable to get ContentProviderClient - throwing.");
|
||||
throw new IllegalStateException("Unable to get ContentProviderClient.");
|
||||
}
|
||||
|
||||
try {
|
||||
storage = EnvironmentBuilder.getStorage(client, profilePath);
|
||||
if (storage == null) {
|
||||
// TODO: Record prune failures and submit as part of FHR upload.
|
||||
Logger.warn(LOG_TAG,"Unable to get HealthReportDatabaseStorage for " + profilePath +
|
||||
" - throwing.");
|
||||
throw new IllegalStateException("Unable to get HealthReportDatabaseStorage for " +
|
||||
profilePath + " (== null).");
|
||||
}
|
||||
} catch (ClassCastException ex) {
|
||||
// TODO: Record prune failures and submit as part of FHR upload.
|
||||
Logger.warn(LOG_TAG,"Unable to get HealthReportDatabaseStorage for " + profilePath +
|
||||
profilePath + " (ClassCastException).");
|
||||
throw new IllegalStateException("Unable to get HealthReportDatabaseStorage for " +
|
||||
profilePath + ".", ex);
|
||||
}
|
||||
|
||||
return storage;
|
||||
}
|
||||
|
||||
protected int getCurrentEnvironmentID() {
|
||||
if (currentEnvironmentID < 0) {
|
||||
final ProfileInformationCache cache = new ProfileInformationCache(profilePath);
|
||||
if (!cache.restoreUnlessInitialized()) {
|
||||
throw new IllegalStateException("Current environment unknown.");
|
||||
}
|
||||
final Environment env = EnvironmentBuilder.getCurrentEnvironment(cache);
|
||||
currentEnvironmentID = env.register();
|
||||
}
|
||||
return currentEnvironmentID;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/* 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/. */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport.prune;
|
||||
|
||||
/**
|
||||
* Abstracts over the Storage instance behind the PrunePolicy.
|
||||
*/
|
||||
public interface PrunePolicyStorage {
|
||||
public void pruneEvents(final int count);
|
||||
public void pruneEnvironments(final int count);
|
||||
|
||||
public int deleteDataBefore(final long time);
|
||||
|
||||
public void cleanup();
|
||||
|
||||
public int getEventCount();
|
||||
public int getEnvironmentCount();
|
||||
|
||||
/**
|
||||
* Release the resources owned by this helper. MUST be called before this helper is garbage
|
||||
* collected.
|
||||
*/
|
||||
public void close();
|
||||
}
|
|
@ -11,7 +11,8 @@
|
|||
xmlns:gecko="http://schemas.android.com/apk/res-auto"
|
||||
android:enabled="false">
|
||||
|
||||
<org.mozilla.gecko.SyncPreference android:title="@string/pref_sync"
|
||||
<org.mozilla.gecko.SyncPreference android:key="android.not_a_preference.sync"
|
||||
android:title="@string/pref_sync"
|
||||
android:persistent="false" />
|
||||
|
||||
<PreferenceScreen android:title="@string/pref_category_customize"
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
android:title="@string/pref_category_customize"
|
||||
android:enabled="false">
|
||||
|
||||
<org.mozilla.gecko.SyncPreference android:title="@string/pref_sync"
|
||||
<org.mozilla.gecko.SyncPreference android:key="android.not_a_preference.sync"
|
||||
android:title="@string/pref_sync"
|
||||
android:persistent="false" />
|
||||
|
||||
<PreferenceScreen android:title="@string/pref_category_search"
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
xmlns:gecko="http://schemas.android.com/apk/res-auto"
|
||||
android:enabled="false">
|
||||
|
||||
<org.mozilla.gecko.SyncPreference android:title="@string/pref_sync"
|
||||
<org.mozilla.gecko.SyncPreference android:key="android.not_a_preference.sync"
|
||||
android:title="@string/pref_sync"
|
||||
android:persistent="false" />
|
||||
|
||||
<PreferenceScreen android:title="@string/pref_category_customize" >
|
||||
|
|
|
@ -49,7 +49,7 @@ abstract class BaseTest extends ActivityInstrumentationTestCase2<Activity> {
|
|||
private static final int VERIFY_URL_TIMEOUT = 2000;
|
||||
private static final int MAX_LIST_ATTEMPTS = 3;
|
||||
private static final int MAX_WAIT_ENABLED_TEXT_MS = 10000;
|
||||
private static final int MAX_WAIT_HOME_PAGER_HIDDEN_MS = 10000;
|
||||
private static final int MAX_WAIT_HOME_PAGER_HIDDEN_MS = 15000;
|
||||
public static final int MAX_WAIT_MS = 4500;
|
||||
|
||||
// IDs for UI views
|
||||
|
|
|
@ -4,11 +4,12 @@ package @ANDROID_PACKAGE_NAME@.tests;
|
|||
import @ANDROID_PACKAGE_NAME@.*;
|
||||
|
||||
abstract class PixelTest extends BaseTest {
|
||||
private static final long PAINT_CLEAR_DELAY = 3000; // milliseconds
|
||||
private static final long PAINT_CLEAR_DELAY = 10000; // milliseconds
|
||||
|
||||
protected final PaintedSurface loadAndGetPainted(String url) {
|
||||
Actions.RepeatedEventExpecter paintExpecter = mActions.expectPaint();
|
||||
loadUrl(url);
|
||||
verifyHomePagerHidden();
|
||||
paintExpecter.blockUntilClear(PAINT_CLEAR_DELAY);
|
||||
paintExpecter.unregisterListener();
|
||||
PaintedSurface p = mDriver.getPaintedSurface();
|
||||
|
|
|
@ -12,7 +12,6 @@ public class testCheck2 extends PixelTest {
|
|||
|
||||
blockForGeckoReady();
|
||||
loadAndPaint(url);
|
||||
verifyHomePagerHidden();
|
||||
|
||||
mDriver.setupScrollHandling();
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ public class testSystemPages extends PixelTest {
|
|||
public void testSystemPages() {
|
||||
blockForGeckoReady();
|
||||
|
||||
String urls [] = { "about:firefox", "about:rights", "about:home", "about:addons", "about:downloads", "about:buildconfig", "about:feedback", "about:healthreport", "about:" };
|
||||
String urls [] = { "about:firefox", "about:rights", "about:addons", "about:downloads", "about:buildconfig", "about:feedback", "about:healthreport", "about:" };
|
||||
// Pages to be tested from the menu and their expected urls. This if of the form { {{ <path to item> }, { <expected url> }}* }
|
||||
String menuItems [][][] = {{{ "Apps" }, { "about:apps" }},
|
||||
{{ "Downloads" }, { "about:downloads" }},
|
||||
|
|
|
@ -6,7 +6,6 @@ background/announcements/AnnouncementsFetchDelegate.java
|
|||
background/announcements/AnnouncementsFetcher.java
|
||||
background/announcements/AnnouncementsFetchResourceDelegate.java
|
||||
background/announcements/AnnouncementsService.java
|
||||
background/announcements/AnnouncementsStartReceiver.java
|
||||
background/BackgroundService.java
|
||||
background/bagheera/BagheeraClient.java
|
||||
background/bagheera/BagheeraRequestDelegate.java
|
||||
|
@ -28,6 +27,8 @@ background/db/CursorDumper.java
|
|||
background/db/Tab.java
|
||||
background/healthreport/Environment.java
|
||||
background/healthreport/EnvironmentBuilder.java
|
||||
background/healthreport/HealthReportBroadcastReceiver.java
|
||||
background/healthreport/HealthReportBroadcastService.java
|
||||
background/healthreport/HealthReportDatabases.java
|
||||
background/healthreport/HealthReportDatabaseStorage.java
|
||||
background/healthreport/HealthReportGenerator.java
|
||||
|
@ -35,11 +36,12 @@ background/healthreport/HealthReportProvider.java
|
|||
background/healthreport/HealthReportStorage.java
|
||||
background/healthreport/HealthReportUtils.java
|
||||
background/healthreport/ProfileInformationCache.java
|
||||
background/healthreport/prune/HealthReportPruneService.java
|
||||
background/healthreport/prune/PrunePolicy.java
|
||||
background/healthreport/prune/PrunePolicyDatabaseStorage.java
|
||||
background/healthreport/prune/PrunePolicyStorage.java
|
||||
background/healthreport/upload/AndroidSubmissionClient.java
|
||||
background/healthreport/upload/HealthReportBroadcastReceiver.java
|
||||
background/healthreport/upload/HealthReportBroadcastService.java
|
||||
background/healthreport/upload/HealthReportUploadService.java
|
||||
background/healthreport/upload/HealthReportUploadStartReceiver.java
|
||||
background/healthreport/upload/ObsoleteDocumentTracker.java
|
||||
background/healthreport/upload/SubmissionClient.java
|
||||
background/healthreport/upload/SubmissionPolicy.java
|
||||
|
|
|
@ -15,6 +15,3 @@
|
|||
<action android:name="@ANDROID_PACKAGE_NAME@.ANNOUNCEMENTS_PREF" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name="org.mozilla.gecko.background.announcements.AnnouncementsStartReceiver" >
|
||||
</receiver>
|
||||
|
|
|
@ -7,12 +7,10 @@
|
|||
Report upload preference changed" notifications sent by
|
||||
Fennec: @ANDROID_PACKAGE_NAME@.HEALTHREPORT_UPLOAD_PREF.
|
||||
|
||||
We have two receivers. BroadcastReceiver is a thin
|
||||
receiver whose single purpose is to start the background
|
||||
service in response to external events. UploadStartReceiver
|
||||
handles scheduling background work.
|
||||
BroadcastReceiver is a thin receiver whose purpose is to
|
||||
start the background service in response to external events.
|
||||
-->
|
||||
<receiver android:name="org.mozilla.gecko.background.healthreport.upload.HealthReportBroadcastReceiver" >
|
||||
<receiver android:name="org.mozilla.gecko.background.healthreport.HealthReportBroadcastReceiver" >
|
||||
<intent-filter>
|
||||
<!-- Startup. -->
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
|
@ -24,7 +22,7 @@
|
|||
<intent-filter >
|
||||
<action android:name="@ANDROID_PACKAGE_NAME@.HEALTHREPORT_UPLOAD_PREF" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name="org.mozilla.gecko.background.healthreport.upload.HealthReportUploadStartReceiver" >
|
||||
<intent-filter >
|
||||
<action android:name="@ANDROID_PACKAGE_NAME@.HEALTHREPORT_PRUNE" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
|
|
@ -6,9 +6,13 @@
|
|||
-->
|
||||
<service
|
||||
android:exported="false"
|
||||
android:name="org.mozilla.gecko.background.healthreport.upload.HealthReportBroadcastService" >
|
||||
android:name="org.mozilla.gecko.background.healthreport.HealthReportBroadcastService" >
|
||||
</service>
|
||||
<service
|
||||
android:exported="false"
|
||||
android:name="org.mozilla.gecko.background.healthreport.upload.HealthReportUploadService" >
|
||||
</service>
|
||||
<service
|
||||
android:exported="false"
|
||||
android:name="org.mozilla.gecko.background.healthreport.prune.HealthReportPruneService" >
|
||||
</service>
|
||||
|
|
|
@ -487,6 +487,9 @@ pref("devtools.debugger.enable-content-actors", true);
|
|||
// Block tools from seeing / interacting with certified apps
|
||||
pref("devtools.debugger.forbid-certified-apps", true);
|
||||
|
||||
// DevTools default color unit
|
||||
pref("devtools.defaultColorUnit", "hex");
|
||||
|
||||
// view source
|
||||
pref("view_source.syntax_highlight", true);
|
||||
pref("view_source.wrap_long_lines", false);
|
||||
|
|
|
@ -47,6 +47,11 @@
|
|||
#include "zlib.h"
|
||||
#include <algorithm>
|
||||
|
||||
#ifdef MOZ_WIDGET_GONK
|
||||
#include "nsINetworkManager.h"
|
||||
#include "nsINetworkStatsServiceProxy.h"
|
||||
#endif
|
||||
|
||||
// rather than slurp up all of nsIWebSocket.idl, which lives outside necko, just
|
||||
// dupe one constant we need from it
|
||||
#define CLOSE_GOING_AWAY 1001
|
||||
|
@ -953,7 +958,12 @@ WebSocketChannel::WebSocketChannel() :
|
|||
mDynamicOutputSize(0),
|
||||
mDynamicOutput(nullptr),
|
||||
mPrivateBrowsing(false),
|
||||
mConnectionLogService(nullptr)
|
||||
mConnectionLogService(nullptr),
|
||||
mCountRecv(0),
|
||||
mCountSent(0),
|
||||
mAppId(0),
|
||||
mConnectionType(NETWORK_NO_TYPE),
|
||||
mIsInBrowser(false)
|
||||
{
|
||||
NS_ABORT_IF_FALSE(NS_IsMainThread(), "not main thread");
|
||||
|
||||
|
@ -1062,6 +1072,16 @@ WebSocketChannel::BeginOpen()
|
|||
return;
|
||||
}
|
||||
|
||||
// obtain app info
|
||||
if (localChannel) {
|
||||
NS_GetAppInfo(localChannel, &mAppId, &mIsInBrowser);
|
||||
}
|
||||
|
||||
// obtain active connection type
|
||||
if (mAppId != NECKO_NO_APP_ID) {
|
||||
GetConnectionType(&mConnectionType);
|
||||
}
|
||||
|
||||
rv = localChannel->AsyncOpen(this, mHttpChannel);
|
||||
if (NS_FAILED(rv)) {
|
||||
LOG(("WebSocketChannel::BeginOpen: cannot async open\n"));
|
||||
|
@ -2709,6 +2729,9 @@ WebSocketChannel::Close(uint16_t code, const nsACString & reason)
|
|||
LOG(("WebSocketChannel::Close() %p\n", this));
|
||||
NS_ABORT_IF_FALSE(NS_IsMainThread(), "not main thread");
|
||||
|
||||
// save the networkstats (bug 855949)
|
||||
SaveNetworkStats(true);
|
||||
|
||||
if (mRequestedClose) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
@ -3035,6 +3058,9 @@ WebSocketChannel::OnInputStreamReady(nsIAsyncInputStream *aStream)
|
|||
rv = mSocketIn->Read((char *)buffer, 2048, &count);
|
||||
LOG(("WebSocketChannel::OnInputStreamReady: read %u rv %x\n", count, rv));
|
||||
|
||||
// accumulate received bytes
|
||||
CountRecvBytes(count);
|
||||
|
||||
if (rv == NS_BASE_STREAM_WOULD_BLOCK) {
|
||||
mSocketIn->AsyncWait(this, 0, 0, mSocketThread);
|
||||
return NS_OK;
|
||||
|
@ -3114,6 +3140,9 @@ WebSocketChannel::OnOutputStreamReady(nsIAsyncOutputStream *aStream)
|
|||
LOG(("WebSocketChannel::OnOutputStreamReady: write %u rv %x\n",
|
||||
amtSent, rv));
|
||||
|
||||
// accumulate sent bytes
|
||||
CountSentBytes(amtSent);
|
||||
|
||||
if (rv == NS_BASE_STREAM_WOULD_BLOCK) {
|
||||
mSocketOut->AsyncWait(this, 0, 0, nullptr);
|
||||
return NS_OK;
|
||||
|
@ -3239,5 +3268,74 @@ WebSocketChannel::OnDataAvailable(nsIRequest *aRequest,
|
|||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult
|
||||
WebSocketChannel::GetConnectionType(int32_t *type)
|
||||
{
|
||||
#ifdef MOZ_WIDGET_GONK
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
|
||||
nsresult result;
|
||||
nsCOMPtr<nsINetworkManager> networkManager = do_GetService("@mozilla.org/network/manager;1", &result);
|
||||
|
||||
if (NS_FAILED(result) || !networkManager) {
|
||||
*type = NETWORK_NO_TYPE;
|
||||
}
|
||||
|
||||
nsCOMPtr<nsINetworkInterface> networkInterface;
|
||||
result = networkManager->GetActive(getter_AddRefs(networkInterface));
|
||||
|
||||
if (networkInterface) {
|
||||
result = networkInterface->GetType(type);
|
||||
}
|
||||
|
||||
return NS_OK;
|
||||
#else
|
||||
return NS_ERROR_NOT_IMPLEMENTED;
|
||||
#endif
|
||||
}
|
||||
|
||||
nsresult
|
||||
WebSocketChannel::SaveNetworkStats(bool enforce)
|
||||
{
|
||||
#ifdef MOZ_WIDGET_GONK
|
||||
// Check if the connection type and app id are valid.
|
||||
if(mConnectionType == NETWORK_NO_TYPE ||
|
||||
mAppId == NECKO_NO_APP_ID) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
if (mCountRecv <= 0 && mCountSent <= 0) {
|
||||
// There is no traffic, no need to save.
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// If |enforce| is false, the traffic amount is saved
|
||||
// only when the total amount exceeds the predefined
|
||||
// threshold.
|
||||
uint64_t totalBytes = mCountRecv + mCountSent;
|
||||
if (!enforce && totalBytes < NETWORK_STATS_THRESHOLD) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
nsresult rv;
|
||||
nsCOMPtr<nsINetworkStatsServiceProxy> mNetworkStatsServiceProxy =
|
||||
do_GetService("@mozilla.org/networkstatsServiceProxy;1", &rv);
|
||||
if (NS_FAILED(rv)) {
|
||||
return rv;
|
||||
}
|
||||
|
||||
mNetworkStatsServiceProxy->SaveAppStats(mAppId, mConnectionType, PR_Now() / 1000,
|
||||
mCountRecv, mCountSent, nullptr);
|
||||
|
||||
// Reset the counters after saving.
|
||||
mCountSent = 0;
|
||||
mCountRecv = 0;
|
||||
|
||||
return NS_OK;
|
||||
#else
|
||||
return NS_ERROR_NOT_IMPLEMENTED;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace mozilla::net
|
||||
} // namespace mozilla
|
||||
|
|
|
@ -252,6 +252,31 @@ private:
|
|||
nsCOMPtr<nsIDashboardEventNotifier> mConnectionLogService;
|
||||
uint32_t mSerial;
|
||||
static uint32_t sSerialSeed;
|
||||
|
||||
// These members are used for network per-app metering (bug 855949)
|
||||
// Currently, they are only available on gonk.
|
||||
public:
|
||||
const static int32_t NETWORK_NO_TYPE = -1; // default conntection type
|
||||
const static uint64_t NETWORK_STATS_THRESHOLD = 65536;
|
||||
|
||||
private:
|
||||
uint64_t mCountRecv;
|
||||
uint64_t mCountSent;
|
||||
uint32_t mAppId;
|
||||
int32_t mConnectionType;
|
||||
bool mIsInBrowser;
|
||||
nsresult GetConnectionType(int32_t *);
|
||||
nsresult SaveNetworkStats(bool);
|
||||
void CountRecvBytes(uint64_t recvBytes)
|
||||
{
|
||||
mCountRecv += recvBytes;
|
||||
SaveNetworkStats(false);
|
||||
}
|
||||
void CountSentBytes(uint64_t sentBytes)
|
||||
{
|
||||
mCountSent += sentBytes;
|
||||
SaveNetworkStats(false);
|
||||
}
|
||||
};
|
||||
|
||||
class WebSocketSSLChannel : public WebSocketChannel
|
||||
|
|
|
@ -5,15 +5,20 @@
|
|||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
|
||||
let gDashboard = Cc['@mozilla.org/network/dashboard;1']
|
||||
Cu.import("resource://testing-common/httpd.js");
|
||||
|
||||
const gDashboard = Cc['@mozilla.org/network/dashboard;1']
|
||||
.getService(Ci.nsIDashboard);
|
||||
|
||||
const RESOLVE_DISABLE_IPV6 = (1 << 5);
|
||||
const gServerSocket = Components.classes["@mozilla.org/network/server-socket;1"]
|
||||
.createInstance(Components.interfaces.nsIServerSocket);
|
||||
const gHttpServer = new HttpServer();
|
||||
|
||||
add_test(function test_http() {
|
||||
gDashboard.requestHttpConnections(function(data) {
|
||||
do_check_neq(data.host.indexOf("example.com"), -1);
|
||||
do_check_neq(data.host.indexOf("localhost"), -1);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
@ -21,32 +26,54 @@ add_test(function test_http() {
|
|||
|
||||
add_test(function test_dns() {
|
||||
gDashboard.requestDNSInfo(function(data) {
|
||||
do_check_neq(data.hostname.indexOf("example.com"), -1);
|
||||
do_check_neq(data.hostname.indexOf("localhost"), -1);
|
||||
|
||||
do_test_pending();
|
||||
gHttpServer.stop(do_test_finished);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_sockets() {
|
||||
let dns = Cc["@mozilla.org/network/dns-service;1"].getService(Ci.nsIDNSService);
|
||||
let record = dns.resolve("example.com", RESOLVE_DISABLE_IPV6);
|
||||
let answer = record.getNextAddrAsString();
|
||||
let sts = Cc["@mozilla.org/network/socket-transport-service;1"]
|
||||
.getService(Ci.nsISocketTransportService);
|
||||
let threadManager = Cc["@mozilla.org/thread-manager;1"].getService();
|
||||
|
||||
gDashboard.requestSockets(function(data) {
|
||||
do_check_neq(data.host.indexOf(answer), -1);
|
||||
let transport = sts.createTransport(null, 0, "127.0.0.1",
|
||||
gServerSocket.port, null);
|
||||
let listener = {
|
||||
onTransportStatus: function(aTransport, aStatus, aProgress, aProgressMax) {
|
||||
if (aStatus == Ci.nsISocketTransport.STATUS_CONNECTED_TO) {
|
||||
gDashboard.requestSockets(function(data) {
|
||||
gServerSocket.close();
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
do_check_neq(data.host.indexOf("127.0.0.1"), -1);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
transport.setEventSink(listener, threadManager.currentThread);
|
||||
|
||||
transport.openOutputStream(Ci.nsITransport.OPEN_BLOCKING, 0, 0);
|
||||
});
|
||||
|
||||
function run_test() {
|
||||
let ioService = Cc["@mozilla.org/network/io-service;1"]
|
||||
.getService(Ci.nsIIOService);
|
||||
let uri = ioService.newURI("http://example.com", null, null);
|
||||
|
||||
gHttpServer.start(-1);
|
||||
|
||||
let uri = ioService.newURI("http://localhost:" + gHttpServer.identity.primaryPort,
|
||||
null, null);
|
||||
let channel = ioService.newChannelFromURI(uri);
|
||||
|
||||
channel.open();
|
||||
|
||||
gServerSocket.init(-1, true, -1);
|
||||
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/* -*- Mode: Javasript; indent-tab-mode: nil; js-indent-level: 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/. */
|
||||
|
||||
const Cc = Components.classes;
|
||||
const Ci = Components.interfaces;
|
||||
|
||||
const gDashboard = Cc['@mozilla.org/network/dashboard;1']
|
||||
.getService(Ci.nsIDashboard);
|
||||
|
||||
function connectionFailed(status) {
|
||||
let status_ok = [
|
||||
"NS_NET_STATUS_RESOLVING_HOST"
|
||||
,"NS_NET_STATUS_RESOLVED_HOST"
|
||||
,"NS_NET_STATUS_CONNECTING_TO"
|
||||
,"NS_NET_STATUS_CONNECTED_TO"
|
||||
];
|
||||
for (let i = 0; i < status_ok.length; i++) {
|
||||
if (status == status_ok[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
let serverSocket = Components.classes["@mozilla.org/network/server-socket;1"]
|
||||
.createInstance(Ci.nsIServerSocket);
|
||||
|
||||
serverSocket.init(-1, true, -1);
|
||||
|
||||
do_test_pending();
|
||||
gDashboard.requestConnection("localhost", serverSocket.port,
|
||||
"tcp", 15, function(connInfo) {
|
||||
if (connInfo.status == "NS_NET_STATUS_CONNECTED_TO") {
|
||||
do_test_pending();
|
||||
gDashboard.requestDNSInfo(function(data) {
|
||||
do_check_neq(data.hostname.indexOf("localhost"), -1);
|
||||
|
||||
do_test_finished();
|
||||
});
|
||||
|
||||
do_test_pending();
|
||||
gDashboard.requestSockets(function(data) {
|
||||
let index = data.host.indexOf("127.0.0.1");
|
||||
do_check_neq(index, -1);
|
||||
do_check_eq(data.port[index], serverSocket.port);
|
||||
do_check_eq(data.tcp[index], 1);
|
||||
|
||||
serverSocket.close();
|
||||
|
||||
do_test_finished();
|
||||
});
|
||||
|
||||
do_test_finished();
|
||||
}
|
||||
if (connectionFailed(connInfo.status)) {
|
||||
do_throw(connInfo.status);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -247,3 +247,4 @@ skip-if = os == "android"
|
|||
[test_about_protocol.js]
|
||||
[test_bug856978.js]
|
||||
[test_about_networking.js]
|
||||
[test_ping_aboutnetworking.js]
|
||||
|
|
|
@ -401,19 +401,29 @@ Statement::internalFinalize(bool aDestructing)
|
|||
//
|
||||
|
||||
char *msg = ::PR_smprintf("SQL statement (%x) should have been finalized"
|
||||
"before garbage-collection. For more details on this statement, set"
|
||||
"NSPR_LOG_MESSAGES=mozStorage:5 .",
|
||||
" before garbage-collection. For more details on this statement, set"
|
||||
" NSPR_LOG_MESSAGES=mozStorage:5 .",
|
||||
mDBStatement);
|
||||
|
||||
//
|
||||
// Note that we can't display the statement itself, as the data structure
|
||||
// is not valid anymore. However, the address shown here should help
|
||||
// developers correlate with the more complete debug message triggered
|
||||
// by AsyncClose().
|
||||
//
|
||||
|
||||
#if 0
|
||||
// Deactivate the warning until we have fixed the exising culprit
|
||||
// (see bug 914070).
|
||||
NS_WARNING(msg);
|
||||
#endif // 0
|
||||
|
||||
PR_LOG(gStorageLog, PR_LOG_WARNING, (msg));
|
||||
|
||||
::PR_smprintf_free(msg);
|
||||
}
|
||||
#endif // DEBUG
|
||||
|
||||
#endif
|
||||
|
||||
mDBStatement = nullptr;
|
||||
|
||||
|
|
|
@ -445,7 +445,12 @@ class XPCShellTestThread(Thread):
|
|||
def report_message(self, line):
|
||||
""" Reports a message to a consumer, both as a strucutured and
|
||||
human-readable log message. """
|
||||
message = self.message_from_line(line).strip()
|
||||
message = self.message_from_line(line)
|
||||
|
||||
if message.endswith('\n'):
|
||||
# A new line is always added by head.js to delimit messages,
|
||||
# however consumers will want to supply their own.
|
||||
message = message[:-1]
|
||||
|
||||
if self.on_message:
|
||||
self.on_message(line, message)
|
||||
|
|
|
@ -18,6 +18,7 @@ EXTRA_JS_MODULES += [
|
|||
'osfile_win_allthreads.jsm',
|
||||
'osfile_win_back.jsm',
|
||||
'osfile_win_front.jsm',
|
||||
'ospath_unix_back.jsm',
|
||||
'ospath_win_back.jsm',
|
||||
'ospath.jsm',
|
||||
'ospath_unix.jsm',
|
||||
'ospath_win.jsm',
|
||||
]
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
"use strict";
|
||||
|
||||
/* 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/. */
|
||||
|
@ -19,11 +17,16 @@
|
|||
* @rejects {B} reason
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["OS"];
|
||||
|
||||
const Cu = Components.utils;
|
||||
const Ci = Components.interfaces;
|
||||
|
||||
let SharedAll = {};
|
||||
Components.utils.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll);
|
||||
Components.utils.import("resource://gre/modules/Deprecated.jsm", this);
|
||||
Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", SharedAll);
|
||||
Cu.import("resource://gre/modules/Deprecated.jsm", this);
|
||||
|
||||
// Boilerplate, to simplify the transition to require()
|
||||
let OS = SharedAll.OS;
|
||||
|
@ -35,27 +38,27 @@ let isTypedArray = OS.Shared.isTypedArray;
|
|||
// The constructor for file errors.
|
||||
let OSError;
|
||||
if (OS.Constants.Win) {
|
||||
Components.utils.import("resource://gre/modules/osfile/osfile_win_allthreads.jsm", this);
|
||||
Components.utils.import("resource://gre/modules/osfile/ospath_win_back.jsm", this);
|
||||
Cu.import("resource://gre/modules/osfile/osfile_win_allthreads.jsm", this);
|
||||
OSError = OS.Shared.Win.Error;
|
||||
} else if (OS.Constants.libc) {
|
||||
Components.utils.import("resource://gre/modules/osfile/osfile_unix_allthreads.jsm", this);
|
||||
Components.utils.import("resource://gre/modules/osfile/ospath_unix_back.jsm", this);
|
||||
Cu.import("resource://gre/modules/osfile/osfile_unix_allthreads.jsm", this);
|
||||
OSError = OS.Shared.Unix.Error;
|
||||
} else {
|
||||
throw new Error("I am neither under Windows nor under a Posix system");
|
||||
}
|
||||
let Type = OS.Shared.Type;
|
||||
let Path = {};
|
||||
Cu.import("resource://gre/modules/osfile/ospath.jsm", Path);
|
||||
|
||||
// The library of promises.
|
||||
Components.utils.import("resource://gre/modules/Promise.jsm", this);
|
||||
Cu.import("resource://gre/modules/Promise.jsm", this);
|
||||
|
||||
// The implementation of communications
|
||||
Components.utils.import("resource://gre/modules/osfile/_PromiseWorker.jsm", this);
|
||||
Cu.import("resource://gre/modules/osfile/_PromiseWorker.jsm", this);
|
||||
|
||||
Components.utils.import("resource://gre/modules/Services.jsm", this);
|
||||
Cu.import("resource://gre/modules/Services.jsm", this);
|
||||
|
||||
Components.utils.import("resource://gre/modules/AsyncShutdown.jsm", this);
|
||||
Cu.import("resource://gre/modules/AsyncShutdown.jsm", this);
|
||||
|
||||
LOG("Checking profileDir", OS.Constants.Path);
|
||||
|
||||
|
@ -66,7 +69,7 @@ if (!("profileDir" in OS.Constants.Path)) {
|
|||
get: function() {
|
||||
let path = undefined;
|
||||
try {
|
||||
path = Services.dirsvc.get("ProfD", Components.interfaces.nsIFile).path;
|
||||
path = Services.dirsvc.get("ProfD", Ci.nsIFile).path;
|
||||
delete OS.Constants.Path.profileDir;
|
||||
OS.Constants.Path.profileDir = path;
|
||||
} catch (ex) {
|
||||
|
@ -84,7 +87,7 @@ if (!("localProfileDir" in OS.Constants.Path)) {
|
|||
get: function() {
|
||||
let path = undefined;
|
||||
try {
|
||||
path = Services.dirsvc.get("ProfLD", Components.interfaces.nsIFile).path;
|
||||
path = Services.dirsvc.get("ProfLD", Ci.nsIFile).path;
|
||||
delete OS.Constants.Path.localProfileDir;
|
||||
OS.Constants.Path.localProfileDir = path;
|
||||
} catch (ex) {
|
||||
|
@ -1002,6 +1005,7 @@ Object.defineProperty(File, "POS_END", {value: OS.Shared.POS_END});
|
|||
OS.File = File;
|
||||
OS.File.Error = OSError;
|
||||
OS.File.DirectoryIterator = DirectoryIterator;
|
||||
OS.Path = Path;
|
||||
|
||||
|
||||
// Auto-flush OS.File during profile-before-change. This ensures that any I/O
|
||||
|
|
|
@ -390,6 +390,6 @@ if (this.Components) {
|
|||
};
|
||||
} catch(ex) {
|
||||
dump("WORKER ERROR DURING SETUP " + ex + "\n");
|
||||
dump("WORKER ERROR DETAIL " + ex.stack + "\n");
|
||||
dump("WORKER ERROR DETAIL " + ("moduleStack" in ex?ex.moduleStack:ex.stack) + "\n");
|
||||
}
|
||||
})(this);
|
||||
|
|
|
@ -20,6 +20,8 @@
|
|||
"use strict";
|
||||
|
||||
exports.OS = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm").OS;
|
||||
let Path = require("resource://gre/modules/osfile/ospath.jsm");
|
||||
|
||||
// exports.OS.Unix is created by osfile_unix_back.jsm
|
||||
if (exports.OS && exports.OS.File) {
|
||||
return; // Avoid double-initialization
|
||||
|
@ -652,7 +654,7 @@
|
|||
let isDir, isSymLink;
|
||||
if (!("d_type" in contents)) {
|
||||
// |dirent| doesn't have d_type on some platforms (e.g. Solaris).
|
||||
let path = OS.Unix.Path.join(this._path, name);
|
||||
let path = Path.join(this._path, name);
|
||||
throw_on_negative("lstat", UnixFile.lstat(path, gStatDataPtr));
|
||||
isDir = (gStatData.st_mode & OS.Constants.libc.S_IFMT) == OS.Constants.libc.S_IFDIR;
|
||||
isSymLink = (gStatData.st_mode & OS.Constants.libc.S_IFMT) == OS.Constants.libc.S_IFLNK;
|
||||
|
@ -702,7 +704,7 @@
|
|||
// Copy the relevant part of |unix_entry| to ensure that
|
||||
// our data is not overwritten prematurely.
|
||||
this._parent = parent;
|
||||
let path = OS.Unix.Path.join(this._parent, name);
|
||||
let path = Path.join(this._parent, name);
|
||||
|
||||
exports.OS.Shared.Unix.AbstractEntry.call(this, isDir, isSymLink, name, path);
|
||||
};
|
||||
|
|
|
@ -20,6 +20,8 @@
|
|||
"use strict";
|
||||
|
||||
exports.OS = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm").OS;
|
||||
let Path = require("resource://gre/modules/osfile/ospath.jsm");
|
||||
|
||||
// exports.OS.Win is created by osfile_win_back.jsm
|
||||
if (exports.OS && exports.OS.File) {
|
||||
return; // Avoid double-initialization
|
||||
|
@ -646,7 +648,7 @@
|
|||
}
|
||||
this._parent = parent;
|
||||
|
||||
let path = OS.Win.Path.join(this._parent, name);
|
||||
let path = Path.join(this._parent, name);
|
||||
|
||||
exports.OS.Shared.Win.AbstractEntry.call(this, isDir, isSymLink, name,
|
||||
winCreationDate, winLastWriteDate,
|
||||
|
@ -841,7 +843,7 @@
|
|||
File.Error = exports.OS.Shared.Win.Error;
|
||||
exports.OS.File = File;
|
||||
|
||||
exports.OS.Path = exports.OS.Win.Path;
|
||||
exports.OS.Path = exports.Path;
|
||||
|
||||
Object.defineProperty(File, "POS_START", { value: OS.Shared.POS_START });
|
||||
Object.defineProperty(File, "POS_CURRENT", { value: OS.Shared.POS_CURRENT });
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
/* 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/. */
|
||||
|
||||
/**
|
||||
* Handling native paths.
|
||||
*
|
||||
* This module contains a number of functions destined to simplify
|
||||
* working with native paths through a cross-platform API. Functions
|
||||
* of this module will only work with the following assumptions:
|
||||
*
|
||||
* - paths are valid;
|
||||
* - paths are defined with one of the grammars that this module can
|
||||
* parse (see later);
|
||||
* - all path concatenations go through function |join|.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
if (typeof Components == "undefined") {
|
||||
let Path;
|
||||
if (OS.Constants.Win) {
|
||||
Path = require("resource://gre/modules/osfile/ospath_win.jsm");
|
||||
} else {
|
||||
Path = require("resource://gre/modules/osfile/ospath_unix.jsm");
|
||||
}
|
||||
module.exports = Path;
|
||||
} else {
|
||||
let Cu = Components.utils;
|
||||
let Scope = {};
|
||||
Cu.import("resource://gre/modules/osfile/osfile_shared_allthreads.jsm", Scope);
|
||||
|
||||
let Path = {};
|
||||
if (Scope.OS.Constants.Win) {
|
||||
Cu.import("resource://gre/modules/osfile/ospath_win.jsm", Path);
|
||||
} else {
|
||||
Cu.import("resource://gre/modules/osfile/ospath_unix.jsm", Path);
|
||||
}
|
||||
|
||||
this.EXPORTED_SYMBOLS = [];
|
||||
for (let k in Path) {
|
||||
this.EXPORTED_SYMBOLS.push(k);
|
||||
this[k] = Path[k];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,157 @@
|
|||
/* 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/. */
|
||||
|
||||
/**
|
||||
* Handling native paths.
|
||||
*
|
||||
* This module contains a number of functions destined to simplify
|
||||
* working with native paths through a cross-platform API. Functions
|
||||
* of this module will only work with the following assumptions:
|
||||
*
|
||||
* - paths are valid;
|
||||
* - paths are defined with one of the grammars that this module can
|
||||
* parse (see later);
|
||||
* - all path concatenations go through function |join|.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
// Boilerplate used to be able to import this module both from the main
|
||||
// thread and from worker threads.
|
||||
if (typeof Components != "undefined") {
|
||||
// Global definition of |exports|, to keep everybody happy.
|
||||
// In non-main thread, |exports| is provided by the module
|
||||
// loader.
|
||||
this.exports = {};
|
||||
} else if (typeof "module" == "undefined" || typeof "exports" == "undefined") {
|
||||
throw new Error("Please load this module using require()");
|
||||
}
|
||||
|
||||
let EXPORTED_SYMBOLS = [
|
||||
"basename",
|
||||
"dirname",
|
||||
"join",
|
||||
"normalize",
|
||||
"split"
|
||||
];
|
||||
|
||||
/**
|
||||
* Return the final part of the path.
|
||||
* The final part of the path is everything after the last "/".
|
||||
*/
|
||||
let basename = function(path) {
|
||||
return path.slice(path.lastIndexOf("/") + 1);
|
||||
};
|
||||
exports.basename = basename;
|
||||
|
||||
/**
|
||||
* Return the directory part of the path.
|
||||
* The directory part of the path is everything before the last
|
||||
* "/". If the last few characters of this part are also "/",
|
||||
* they are ignored.
|
||||
*
|
||||
* If the path contains no directory, return ".".
|
||||
*/
|
||||
let dirname = function(path) {
|
||||
let index = path.lastIndexOf("/");
|
||||
if (index == -1) {
|
||||
return ".";
|
||||
}
|
||||
while (index >= 0 && path[index] == "/") {
|
||||
--index;
|
||||
}
|
||||
return path.slice(0, index + 1);
|
||||
};
|
||||
exports.dirname = dirname;
|
||||
|
||||
/**
|
||||
* Join path components.
|
||||
* This is the recommended manner of getting the path of a file/subdirectory
|
||||
* in a directory.
|
||||
*
|
||||
* Example: Obtaining $TMP/foo/bar in an OS-independent manner
|
||||
* var tmpDir = OS.Constants.Path.tmpDir;
|
||||
* var path = OS.Path.join(tmpDir, "foo", "bar");
|
||||
*
|
||||
* Under Unix, this will return "/tmp/foo/bar".
|
||||
*/
|
||||
let join = function(path /*...*/) {
|
||||
// If there is a path that starts with a "/", eliminate everything before
|
||||
let paths = [];
|
||||
for each(let i in arguments) {
|
||||
if (i.length != 0 && i[0] == "/") {
|
||||
paths = [i];
|
||||
} else {
|
||||
paths.push(i);
|
||||
}
|
||||
}
|
||||
return paths.join("/");
|
||||
};
|
||||
exports.join = join;
|
||||
|
||||
/**
|
||||
* Normalize a path by removing any unneeded ".", "..", "//".
|
||||
*/
|
||||
let normalize = function(path) {
|
||||
let stack = [];
|
||||
let absolute;
|
||||
if (path.length >= 0 && path[0] == "/") {
|
||||
absolute = true;
|
||||
} else {
|
||||
absolute = false;
|
||||
}
|
||||
path.split("/").forEach(function(v) {
|
||||
switch (v) {
|
||||
case "": case ".":// fallthrough
|
||||
break;
|
||||
case "..":
|
||||
if (stack.length == 0) {
|
||||
if (absolute) {
|
||||
throw new Error("Path is ill-formed: attempting to go past root");
|
||||
} else {
|
||||
stack.push("..");
|
||||
}
|
||||
} else {
|
||||
if (stack[stack.length - 1] == "..") {
|
||||
stack.push("..");
|
||||
} else {
|
||||
stack.pop();
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
stack.push(v);
|
||||
}
|
||||
});
|
||||
let string = stack.join("/");
|
||||
return absolute ? "/" + string : string;
|
||||
};
|
||||
exports.normalize = normalize;
|
||||
|
||||
/**
|
||||
* Return the components of a path.
|
||||
* You should generally apply this function to a normalized path.
|
||||
*
|
||||
* @return {{
|
||||
* {bool} absolute |true| if the path is absolute, |false| otherwise
|
||||
* {array} components the string components of the path
|
||||
* }}
|
||||
*
|
||||
* Other implementations may add additional OS-specific informations.
|
||||
*/
|
||||
let split = function(path) {
|
||||
return {
|
||||
absolute: path.length && path[0] == "/",
|
||||
components: path.split("/")
|
||||
};
|
||||
};
|
||||
exports.split = split;
|
||||
|
||||
//////////// Boilerplate
|
||||
if (typeof Components != "undefined") {
|
||||
this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS;
|
||||
for (let symbol of EXPORTED_SYMBOLS) {
|
||||
this[symbol] = exports[symbol];
|
||||
}
|
||||
}
|
|
@ -1,155 +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/. */
|
||||
|
||||
/**
|
||||
* Handling native paths.
|
||||
*
|
||||
* This module contains a number of functions destined to simplify
|
||||
* working with native paths through a cross-platform API. Functions
|
||||
* of this module will only work with the following assumptions:
|
||||
*
|
||||
* - paths are valid;
|
||||
* - paths are defined with one of the grammars that this module can
|
||||
* parse (see later);
|
||||
* - all path concatenations go through function |join|.
|
||||
*/
|
||||
if (typeof Components != "undefined") {
|
||||
this.EXPORTED_SYMBOLS = ["OS"];
|
||||
let Scope = {};
|
||||
Components.utils.import("resource://gre/modules/Services.jsm", Scope);
|
||||
|
||||
// Some tests need to import this module from any platform.
|
||||
// We detect this by setting a bogus preference "toolkit.osfile.test.syslib_necessary"
|
||||
// from the test suite
|
||||
let syslib_necessary = true;
|
||||
try {
|
||||
syslib_necessary = Scope.Services.prefs.getBoolPref("toolkit.osfile.test.syslib_necessary");
|
||||
} catch (x) {
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
try {
|
||||
Components.utils.import("resource://gre/modules/osfile/osfile_unix_allthreads.jsm", this);
|
||||
} catch (ex if !syslib_necessary && ex.message.startsWith("Could not open system library:")) {
|
||||
// Executing this module without a libc is acceptable for this test
|
||||
}
|
||||
}
|
||||
(function(exports) {
|
||||
"use strict";
|
||||
if (!exports.OS) {
|
||||
exports.OS = {};
|
||||
}
|
||||
if (!exports.OS.Unix) {
|
||||
exports.OS.Unix = {};
|
||||
}
|
||||
if (exports.OS.Unix.Path) {
|
||||
return; // Avoid double-initialization
|
||||
}
|
||||
exports.OS.Unix.Path = {
|
||||
/**
|
||||
* Return the final part of the path.
|
||||
* The final part of the path is everything after the last "/".
|
||||
*/
|
||||
basename: function basename(path) {
|
||||
return path.slice(path.lastIndexOf("/") + 1);
|
||||
},
|
||||
/**
|
||||
* Return the directory part of the path.
|
||||
* The directory part of the path is everything before the last
|
||||
* "/". If the last few characters of this part are also "/",
|
||||
* they are ignored.
|
||||
*
|
||||
* If the path contains no directory, return ".".
|
||||
*/
|
||||
dirname: function dirname(path) {
|
||||
let index = path.lastIndexOf("/");
|
||||
if (index == -1) {
|
||||
return ".";
|
||||
}
|
||||
while (index >= 0 && path[index] == "/") {
|
||||
--index;
|
||||
}
|
||||
return path.slice(0, index + 1);
|
||||
},
|
||||
/**
|
||||
* Join path components.
|
||||
* This is the recommended manner of getting the path of a file/subdirectory
|
||||
* in a directory.
|
||||
*
|
||||
* Example: Obtaining $TMP/foo/bar in an OS-independent manner
|
||||
* var tmpDir = OS.Constants.Path.tmpDir;
|
||||
* var path = OS.Path.join(tmpDir, "foo", "bar");
|
||||
*
|
||||
* Under Unix, this will return "/tmp/foo/bar".
|
||||
*/
|
||||
join: function join(path /*...*/) {
|
||||
// If there is a path that starts with a "/", eliminate everything before
|
||||
let paths = [];
|
||||
for each(let i in arguments) {
|
||||
if (i.length != 0 && i[0] == "/") {
|
||||
paths = [i];
|
||||
} else {
|
||||
paths.push(i);
|
||||
}
|
||||
}
|
||||
return paths.join("/");
|
||||
},
|
||||
/**
|
||||
* Normalize a path by removing any unneeded ".", "..", "//".
|
||||
*/
|
||||
normalize: function normalize(path) {
|
||||
let stack = [];
|
||||
let absolute;
|
||||
if (path.length >= 0 && path[0] == "/") {
|
||||
absolute = true;
|
||||
} else {
|
||||
absolute = false;
|
||||
}
|
||||
path.split("/").forEach(function loop(v) {
|
||||
switch (v) {
|
||||
case "": case ".":// fallthrough
|
||||
break;
|
||||
case "..":
|
||||
if (stack.length == 0) {
|
||||
if (absolute) {
|
||||
throw new Error("Path is ill-formed: attempting to go past root");
|
||||
} else {
|
||||
stack.push("..");
|
||||
}
|
||||
} else {
|
||||
if (stack[stack.length - 1] == "..") {
|
||||
stack.push("..");
|
||||
} else {
|
||||
stack.pop();
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
stack.push(v);
|
||||
}
|
||||
});
|
||||
let string = stack.join("/");
|
||||
return absolute ? "/" + string : string;
|
||||
},
|
||||
/**
|
||||
* Return the components of a path.
|
||||
* You should generally apply this function to a normalized path.
|
||||
*
|
||||
* @return {{
|
||||
* {bool} absolute |true| if the path is absolute, |false| otherwise
|
||||
* {array} components the string components of the path
|
||||
* }}
|
||||
*
|
||||
* Other implementations may add additional OS-specific informations.
|
||||
*/
|
||||
split: function split(path) {
|
||||
return {
|
||||
absolute: path.length && path[0] == "/",
|
||||
components: path.split("/")
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
exports.OS.Path = exports.OS.Unix.Path;
|
||||
}(this));
|
|
@ -0,0 +1,302 @@
|
|||
/**
|
||||
* Handling native paths.
|
||||
*
|
||||
* This module contains a number of functions destined to simplify
|
||||
* working with native paths through a cross-platform API. Functions
|
||||
* of this module will only work with the following assumptions:
|
||||
*
|
||||
* - paths are valid;
|
||||
* - paths are defined with one of the grammars that this module can
|
||||
* parse (see later);
|
||||
* - all path concatenations go through function |join|.
|
||||
*
|
||||
* Limitations of this implementation.
|
||||
*
|
||||
* Windows supports 6 distinct grammars for paths. For the moment, this
|
||||
* implementation supports the following subset:
|
||||
*
|
||||
* - drivename:backslash-separated components
|
||||
* - backslash-separated components
|
||||
* - \\drivename\ followed by backslash-separated components
|
||||
*
|
||||
* Additionally, |normalize| can convert a path containing slash-
|
||||
* separated components to a path containing backslash-separated
|
||||
* components.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
// Boilerplate used to be able to import this module both from the main
|
||||
// thread and from worker threads.
|
||||
if (typeof Components != "undefined") {
|
||||
// Global definition of |exports|, to keep everybody happy.
|
||||
// In non-main thread, |exports| is provided by the module
|
||||
// loader.
|
||||
this.exports = {};
|
||||
} else if (typeof "module" == "undefined" || typeof "exports" == "undefined") {
|
||||
throw new Error("Please load this module using require()");
|
||||
}
|
||||
|
||||
let EXPORTED_SYMBOLS = [
|
||||
"basename",
|
||||
"dirname",
|
||||
"join",
|
||||
"normalize",
|
||||
"split",
|
||||
"winGetDrive",
|
||||
"winIsAbsolute"
|
||||
];
|
||||
|
||||
/**
|
||||
* Return the final part of the path.
|
||||
* The final part of the path is everything after the last "\\".
|
||||
*/
|
||||
let basename = function(path) {
|
||||
if (path.startsWith("\\\\")) {
|
||||
// UNC-style path
|
||||
let index = path.lastIndexOf("\\");
|
||||
if (index != 1) {
|
||||
return path.slice(index + 1);
|
||||
}
|
||||
return ""; // Degenerate case
|
||||
}
|
||||
return path.slice(Math.max(path.lastIndexOf("\\"),
|
||||
path.lastIndexOf(":")) + 1);
|
||||
};
|
||||
exports.basename = basename;
|
||||
|
||||
/**
|
||||
* Return the directory part of the path.
|
||||
*
|
||||
* If the path contains no directory, return the drive letter,
|
||||
* or "." if the path contains no drive letter or if option
|
||||
* |winNoDrive| is set.
|
||||
*
|
||||
* Otherwise, return everything before the last backslash,
|
||||
* including the drive/server name.
|
||||
*
|
||||
*
|
||||
* @param {string} path The path.
|
||||
* @param {*=} options Platform-specific options controlling the behavior
|
||||
* of this function. This implementation supports the following options:
|
||||
* - |winNoDrive| If |true|, also remove the letter from the path name.
|
||||
*/
|
||||
let dirname = function(path, options) {
|
||||
let noDrive = (options && options.winNoDrive);
|
||||
|
||||
// Find the last occurrence of "\\"
|
||||
let index = path.lastIndexOf("\\");
|
||||
if (index == -1) {
|
||||
// If there is no directory component...
|
||||
if (!noDrive) {
|
||||
// Return the drive path if possible, falling back to "."
|
||||
return this.winGetDrive(path) || ".";
|
||||
} else {
|
||||
// Or just "."
|
||||
return ".";
|
||||
}
|
||||
}
|
||||
|
||||
if (index == 1 && path.charAt(0) == "\\") {
|
||||
// The path is reduced to a UNC drive
|
||||
if (noDrive) {
|
||||
return ".";
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore any occurrence of "\\: immediately before that one
|
||||
while (index >= 0 && path[index] == "\\") {
|
||||
--index;
|
||||
}
|
||||
|
||||
// Compute what is left, removing the drive name if necessary
|
||||
let start;
|
||||
if (noDrive) {
|
||||
start = (this.winGetDrive(path) || "").length;
|
||||
} else {
|
||||
start = 0;
|
||||
}
|
||||
return path.slice(start, index + 1);
|
||||
};
|
||||
exports.dirname = dirname;
|
||||
|
||||
/**
|
||||
* Join path components.
|
||||
* This is the recommended manner of getting the path of a file/subdirectory
|
||||
* in a directory.
|
||||
*
|
||||
* Example: Obtaining $TMP/foo/bar in an OS-independent manner
|
||||
* var tmpDir = OS.Constants.Path.tmpDir;
|
||||
* var path = OS.Path.join(tmpDir, "foo", "bar");
|
||||
*
|
||||
* Under Windows, this will return "$TMP\foo\bar".
|
||||
*/
|
||||
let join = function(...path) {
|
||||
let paths = [];
|
||||
let root;
|
||||
let absolute = false;
|
||||
for each(let subpath in path) {
|
||||
let drive = this.winGetDrive(subpath);
|
||||
let abs = this.winIsAbsolute(subpath);
|
||||
if (drive) {
|
||||
root = drive;
|
||||
let component = trimBackslashes(subpath.slice(drive.length));
|
||||
if (component) {
|
||||
paths = [component];
|
||||
} else {
|
||||
paths = [];
|
||||
}
|
||||
absolute = abs;
|
||||
} else if (abs) {
|
||||
paths = [trimBackslashes(subpath)];
|
||||
absolute = true;
|
||||
} else {
|
||||
paths.push(trimBackslashes(subpath));
|
||||
}
|
||||
}
|
||||
let result = "";
|
||||
if (root) {
|
||||
result += root;
|
||||
}
|
||||
if (absolute) {
|
||||
result += "\\";
|
||||
}
|
||||
result += paths.join("\\");
|
||||
return result;
|
||||
};
|
||||
exports.join = join;
|
||||
|
||||
/**
|
||||
* Return the drive name of a path, or |null| if the path does
|
||||
* not contain a drive name.
|
||||
*
|
||||
* Drive name appear either as "DriveName:..." (the return drive
|
||||
* name includes the ":") or "\\\\DriveName..." (the returned drive name
|
||||
* includes "\\\\").
|
||||
*/
|
||||
let winGetDrive = function(path) {
|
||||
if (path.startsWith("\\\\")) {
|
||||
// UNC path
|
||||
if (path.length == 2) {
|
||||
return null;
|
||||
}
|
||||
let index = path.indexOf("\\", 2);
|
||||
if (index == -1) {
|
||||
return path;
|
||||
}
|
||||
return path.slice(0, index);
|
||||
}
|
||||
// Non-UNC path
|
||||
let index = path.indexOf(":");
|
||||
if (index <= 0) return null;
|
||||
return path.slice(0, index + 1);
|
||||
};
|
||||
exports.winGetDrive = winGetDrive;
|
||||
|
||||
/**
|
||||
* Return |true| if the path is absolute, |false| otherwise.
|
||||
*
|
||||
* We consider that a path is absolute if it starts with "\\"
|
||||
* or "driveletter:\\".
|
||||
*/
|
||||
let winIsAbsolute = function(path) {
|
||||
let index = path.indexOf(":");
|
||||
return path.length > index + 1 && path[index + 1] == "\\";
|
||||
};
|
||||
exports.winIsAbsolute = winIsAbsolute;
|
||||
|
||||
/**
|
||||
* Normalize a path by removing any unneeded ".", "..", "\\".
|
||||
* Also convert any "/" to a "\\".
|
||||
*/
|
||||
let normalize = function(path) {
|
||||
let stack = [];
|
||||
|
||||
// Remove the drive (we will put it back at the end)
|
||||
let root = this.winGetDrive(path);
|
||||
if (root) {
|
||||
path = path.slice(root.length);
|
||||
}
|
||||
|
||||
// Remember whether we need to restore a leading "\\" or drive name.
|
||||
let absolute = this.winIsAbsolute(path);
|
||||
|
||||
// Normalize "/" to "\\"
|
||||
path = path.replace("/", "\\");
|
||||
|
||||
// And now, fill |stack| from the components,
|
||||
// popping whenever there is a ".."
|
||||
path.split("\\").forEach(function loop(v) {
|
||||
switch (v) {
|
||||
case "": case ".": // Ignore
|
||||
break;
|
||||
case "..":
|
||||
if (stack.length == 0) {
|
||||
if (absolute) {
|
||||
throw new Error("Path is ill-formed: attempting to go past root");
|
||||
} else {
|
||||
stack.push("..");
|
||||
}
|
||||
} else {
|
||||
if (stack[stack.length - 1] == "..") {
|
||||
stack.push("..");
|
||||
} else {
|
||||
stack.pop();
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
stack.push(v);
|
||||
}
|
||||
});
|
||||
|
||||
// Put everything back together
|
||||
let result = stack.join("\\");
|
||||
if (absolute) {
|
||||
result = "\\" + result;
|
||||
}
|
||||
if (root) {
|
||||
result = root + result;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
exports.normalize = normalize;
|
||||
|
||||
/**
|
||||
* Return the components of a path.
|
||||
* You should generally apply this function to a normalized path.
|
||||
*
|
||||
* @return {{
|
||||
* {bool} absolute |true| if the path is absolute, |false| otherwise
|
||||
* {array} components the string components of the path
|
||||
* {string?} winDrive the drive or server for this path
|
||||
* }}
|
||||
*
|
||||
* Other implementations may add additional OS-specific informations.
|
||||
*/
|
||||
let split = function(path) {
|
||||
return {
|
||||
absolute: this.winIsAbsolute(path),
|
||||
winDrive: this.winGetDrive(path),
|
||||
components: path.split("\\")
|
||||
};
|
||||
};
|
||||
exports.split = split;
|
||||
|
||||
/**
|
||||
* Utility function: Remove any leading/trailing backslashes
|
||||
* from a string.
|
||||
*/
|
||||
let trimBackslashes = function trimBackslashes(string) {
|
||||
return string.replace(/^\\+|\\+$/g,'');
|
||||
};
|
||||
|
||||
//////////// Boilerplate
|
||||
if (typeof Components != "undefined") {
|
||||
this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS;
|
||||
for (let symbol of EXPORTED_SYMBOLS) {
|
||||
this[symbol] = exports[symbol];
|
||||
}
|
||||
}
|
|
@ -1,300 +0,0 @@
|
|||
/**
|
||||
* Handling native paths.
|
||||
*
|
||||
* This module contains a number of functions destined to simplify
|
||||
* working with native paths through a cross-platform API. Functions
|
||||
* of this module will only work with the following assumptions:
|
||||
*
|
||||
* - paths are valid;
|
||||
* - paths are defined with one of the grammars that this module can
|
||||
* parse (see later);
|
||||
* - all path concatenations go through function |join|.
|
||||
*
|
||||
* Limitations of this implementation.
|
||||
*
|
||||
* Windows supports 6 distinct grammars for paths. For the moment, this
|
||||
* implementation supports the following subset:
|
||||
*
|
||||
* - drivename:backslash-separated components
|
||||
* - backslash-separated components
|
||||
* - \\drivename\ followed by backslash-separated components
|
||||
*
|
||||
* Additionally, |normalize| can convert a path containing slash-
|
||||
* separated components to a path containing backslash-separated
|
||||
* components.
|
||||
*/
|
||||
if (typeof Components != "undefined") {
|
||||
this.EXPORTED_SYMBOLS = ["OS"];
|
||||
let Scope = {};
|
||||
Components.utils.import("resource://gre/modules/Services.jsm", Scope);
|
||||
|
||||
// Some tests need to import this module from any platform.
|
||||
// We detect this by setting a bogus preference "toolkit.osfile.test.syslib_necessary"
|
||||
// from the test suite
|
||||
let syslib_necessary = true;
|
||||
try {
|
||||
syslib_necessary = Scope.Services.prefs.getBoolPref("toolkit.osfile.test.syslib_necessary");
|
||||
} catch (x) {
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
try {
|
||||
Components.utils.import("resource://gre/modules/osfile/osfile_win_allthreads.jsm", this);
|
||||
} catch (ex if !syslib_necessary && ex.message.startsWith("Could not open system library:")) {
|
||||
// Executing this module without a libc is acceptable for this test
|
||||
}
|
||||
}
|
||||
(function(exports) {
|
||||
"use strict";
|
||||
if (!exports.OS) {
|
||||
exports.OS = {};
|
||||
}
|
||||
if (!exports.OS.Win) {
|
||||
exports.OS.Win = {};
|
||||
}
|
||||
if (exports.OS.Win.Path) {
|
||||
return; // Avoid double-initialization
|
||||
}
|
||||
exports.OS.Win.Path = {
|
||||
/**
|
||||
* Return the final part of the path.
|
||||
* The final part of the path is everything after the last "\\".
|
||||
*/
|
||||
basename: function basename(path) {
|
||||
if (path.startsWith("\\\\")) {
|
||||
// UNC-style path
|
||||
let index = path.lastIndexOf("\\");
|
||||
if (index != 1) {
|
||||
return path.slice(index + 1);
|
||||
}
|
||||
return ""; // Degenerate case
|
||||
}
|
||||
return path.slice(Math.max(path.lastIndexOf("\\"),
|
||||
path.lastIndexOf(":")) + 1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the directory part of the path.
|
||||
*
|
||||
* If the path contains no directory, return the drive letter,
|
||||
* or "." if the path contains no drive letter or if option
|
||||
* |winNoDrive| is set.
|
||||
*
|
||||
* Otherwise, return everything before the last backslash,
|
||||
* including the drive/server name.
|
||||
*
|
||||
*
|
||||
* @param {string} path The path.
|
||||
* @param {*=} options Platform-specific options controlling the behavior
|
||||
* of this function. This implementation supports the following options:
|
||||
* - |winNoDrive| If |true|, also remove the letter from the path name.
|
||||
*/
|
||||
dirname: function dirname(path, options) {
|
||||
let noDrive = (options && options.winNoDrive);
|
||||
|
||||
// Find the last occurrence of "\\"
|
||||
let index = path.lastIndexOf("\\");
|
||||
if (index == -1) {
|
||||
// If there is no directory component...
|
||||
if (!noDrive) {
|
||||
// Return the drive path if possible, falling back to "."
|
||||
return this.winGetDrive(path) || ".";
|
||||
} else {
|
||||
// Or just "."
|
||||
return ".";
|
||||
}
|
||||
}
|
||||
|
||||
if (index == 1 && path.charAt(0) == "\\") {
|
||||
// The path is reduced to a UNC drive
|
||||
if (noDrive) {
|
||||
return ".";
|
||||
} else {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
// Ignore any occurrence of "\\: immediately before that one
|
||||
while (index >= 0 && path[index] == "\\") {
|
||||
--index;
|
||||
}
|
||||
|
||||
// Compute what is left, removing the drive name if necessary
|
||||
let start;
|
||||
if (noDrive) {
|
||||
start = (this.winGetDrive(path) || "").length;
|
||||
} else {
|
||||
start = 0;
|
||||
}
|
||||
return path.slice(start, index + 1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Join path components.
|
||||
* This is the recommended manner of getting the path of a file/subdirectory
|
||||
* in a directory.
|
||||
*
|
||||
* Example: Obtaining $TMP/foo/bar in an OS-independent manner
|
||||
* var tmpDir = OS.Constants.Path.tmpDir;
|
||||
* var path = OS.Path.join(tmpDir, "foo", "bar");
|
||||
*
|
||||
* Under Windows, this will return "$TMP\foo\bar".
|
||||
*/
|
||||
join: function join(path /*...*/) {
|
||||
let paths = [];
|
||||
let root;
|
||||
let absolute = false;
|
||||
for each(let subpath in arguments) {
|
||||
let drive = this.winGetDrive(subpath);
|
||||
let abs = this.winIsAbsolute(subpath);
|
||||
if (drive) {
|
||||
root = drive;
|
||||
let component = trimBackslashes(subpath.slice(drive.length));
|
||||
if (component) {
|
||||
paths = [component];
|
||||
} else {
|
||||
paths = [];
|
||||
}
|
||||
absolute = abs;
|
||||
} else if (abs) {
|
||||
paths = [trimBackslashes(subpath)];
|
||||
absolute = true;
|
||||
} else {
|
||||
paths.push(trimBackslashes(subpath));
|
||||
}
|
||||
}
|
||||
let result = "";
|
||||
if (root) {
|
||||
result += root;
|
||||
}
|
||||
if (absolute) {
|
||||
result += "\\";
|
||||
}
|
||||
result += paths.join("\\");
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the drive name of a path, or |null| if the path does
|
||||
* not contain a drive name.
|
||||
*
|
||||
* Drive name appear either as "DriveName:..." (the return drive
|
||||
* name includes the ":") or "\\\\DriveName..." (the returned drive name
|
||||
* includes "\\\\").
|
||||
*/
|
||||
winGetDrive: function winGetDrive(path) {
|
||||
if (path.startsWith("\\\\")) {
|
||||
// UNC path
|
||||
if (path.length == 2) {
|
||||
return null;
|
||||
}
|
||||
let index = path.indexOf("\\", 2);
|
||||
if (index == -1) {
|
||||
return path;
|
||||
}
|
||||
return path.slice(0, index);
|
||||
}
|
||||
// Non-UNC path
|
||||
let index = path.indexOf(":");
|
||||
if (index <= 0) return null;
|
||||
return path.slice(0, index + 1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Return |true| if the path is absolute, |false| otherwise.
|
||||
*
|
||||
* We consider that a path is absolute if it starts with "\\"
|
||||
* or "driveletter:\\".
|
||||
*/
|
||||
winIsAbsolute: function winIsAbsolute(path) {
|
||||
let index = path.indexOf(":");
|
||||
return path.length > index + 1 && path[index + 1] == "\\";
|
||||
},
|
||||
|
||||
/**
|
||||
* Normalize a path by removing any unneeded ".", "..", "\\".
|
||||
* Also convert any "/" to a "\\".
|
||||
*/
|
||||
normalize: function normalize(path) {
|
||||
let stack = [];
|
||||
|
||||
// Remove the drive (we will put it back at the end)
|
||||
let root = this.winGetDrive(path);
|
||||
if (root) {
|
||||
path = path.slice(root.length);
|
||||
}
|
||||
|
||||
// Remember whether we need to restore a leading "\\" or drive name.
|
||||
let absolute = this.winIsAbsolute(path);
|
||||
|
||||
// Normalize "/" to "\\"
|
||||
path = path.replace("/", "\\");
|
||||
|
||||
// And now, fill |stack| from the components,
|
||||
// popping whenever there is a ".."
|
||||
path.split("\\").forEach(function loop(v) {
|
||||
switch (v) {
|
||||
case "": case ".": // Ignore
|
||||
break;
|
||||
case "..":
|
||||
if (stack.length == 0) {
|
||||
if (absolute) {
|
||||
throw new Error("Path is ill-formed: attempting to go past root");
|
||||
} else {
|
||||
stack.push("..");
|
||||
}
|
||||
} else {
|
||||
if (stack[stack.length - 1] == "..") {
|
||||
stack.push("..");
|
||||
} else {
|
||||
stack.pop();
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
stack.push(v);
|
||||
}
|
||||
});
|
||||
|
||||
// Put everything back together
|
||||
let result = stack.join("\\");
|
||||
if (absolute) {
|
||||
result = "\\" + result;
|
||||
}
|
||||
if (root) {
|
||||
result = root + result;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the components of a path.
|
||||
* You should generally apply this function to a normalized path.
|
||||
*
|
||||
* @return {{
|
||||
* {bool} absolute |true| if the path is absolute, |false| otherwise
|
||||
* {array} components the string components of the path
|
||||
* {string?} winDrive the drive or server for this path
|
||||
* }}
|
||||
*
|
||||
* Other implementations may add additional OS-specific informations.
|
||||
*/
|
||||
split: function split(path) {
|
||||
return {
|
||||
absolute: this.winIsAbsolute(path),
|
||||
winDrive: this.winGetDrive(path),
|
||||
components: path.split("\\")
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility function: Remove any leading/trailing backslashes
|
||||
* from a string.
|
||||
*/
|
||||
let trimBackslashes = function trimBackslashes(string) {
|
||||
return string.replace(/^\\+|\\+$/g,'');
|
||||
};
|
||||
|
||||
exports.OS.Path = exports.OS.Win.Path;
|
||||
}(this));
|
|
@ -17,7 +17,6 @@ if (typeof Components != "undefined") {
|
|||
importScripts(
|
||||
"resource://gre/modules/workers/require.js",
|
||||
"resource://gre/modules/osfile/osfile_win_allthreads.jsm",
|
||||
"resource://gre/modules/osfile/ospath_win_back.jsm",
|
||||
"resource://gre/modules/osfile/osfile_win_back.jsm",
|
||||
"resource://gre/modules/osfile/osfile_shared_front.jsm",
|
||||
"resource://gre/modules/osfile/osfile_win_front.jsm"
|
||||
|
@ -26,10 +25,10 @@ if (typeof Components != "undefined") {
|
|||
importScripts(
|
||||
"resource://gre/modules/workers/require.js",
|
||||
"resource://gre/modules/osfile/osfile_unix_allthreads.jsm",
|
||||
"resource://gre/modules/osfile/ospath_unix_back.jsm",
|
||||
"resource://gre/modules/osfile/osfile_unix_back.jsm",
|
||||
"resource://gre/modules/osfile/osfile_shared_front.jsm",
|
||||
"resource://gre/modules/osfile/osfile_unix_front.jsm"
|
||||
);
|
||||
#endif
|
||||
OS.Path = require("resource://gre/modules/osfile/ospath.jsm");
|
||||
}
|
||||
|
|
|
@ -10,11 +10,11 @@ Services.prefs.setBoolPref("toolkit.osfile.test.syslib_necessary", false);
|
|||
|
||||
let ImportWin = {};
|
||||
let ImportUnix = {};
|
||||
Components.utils.import("resource://gre/modules/osfile/ospath_win_back.jsm", ImportWin);
|
||||
Components.utils.import("resource://gre/modules/osfile/ospath_unix_back.jsm", ImportUnix);
|
||||
Components.utils.import("resource://gre/modules/osfile/ospath_win.jsm", ImportWin);
|
||||
Components.utils.import("resource://gre/modules/osfile/ospath_unix.jsm", ImportUnix);
|
||||
|
||||
let Win = ImportWin.OS;
|
||||
let Unix = ImportUnix.OS;
|
||||
let Win = ImportWin;
|
||||
let Unix = ImportUnix;
|
||||
|
||||
function do_check_fail(f)
|
||||
{
|
||||
|
@ -32,99 +32,104 @@ function run_test()
|
|||
do_print("Testing Windows paths");
|
||||
|
||||
do_print("Backslash-separated, no drive");
|
||||
do_check_eq(Win.Path.basename("a\\b"), "b");
|
||||
do_check_eq(Win.Path.basename("a\\b\\"), "");
|
||||
do_check_eq(Win.Path.basename("abc"), "abc");
|
||||
do_check_eq(Win.Path.dirname("a\\b"), "a");
|
||||
do_check_eq(Win.Path.dirname("a\\b\\"), "a\\b");
|
||||
do_check_eq(Win.Path.dirname("a\\\\\\\\b"), "a");
|
||||
do_check_eq(Win.Path.dirname("abc"), ".");
|
||||
do_check_eq(Win.Path.normalize("\\a\\b\\c"), "\\a\\b\\c");
|
||||
do_check_eq(Win.Path.normalize("\\a\\b\\\\\\\\c"), "\\a\\b\\c");
|
||||
do_check_eq(Win.Path.normalize("\\a\\b\\c\\\\\\"), "\\a\\b\\c");
|
||||
do_check_eq(Win.Path.normalize("\\a\\b\\c\\..\\..\\..\\d\\e\\f"), "\\d\\e\\f");
|
||||
do_check_eq(Win.Path.normalize("a\\b\\c\\..\\..\\..\\d\\e\\f"), "d\\e\\f");
|
||||
do_check_fail(function() Win.Path.normalize("\\a\\b\\c\\..\\..\\..\\..\\d\\e\\f"));
|
||||
do_check_eq(Win.basename("a\\b"), "b");
|
||||
do_check_eq(Win.basename("a\\b\\"), "");
|
||||
do_check_eq(Win.basename("abc"), "abc");
|
||||
do_check_eq(Win.dirname("a\\b"), "a");
|
||||
do_check_eq(Win.dirname("a\\b\\"), "a\\b");
|
||||
do_check_eq(Win.dirname("a\\\\\\\\b"), "a");
|
||||
do_check_eq(Win.dirname("abc"), ".");
|
||||
do_check_eq(Win.normalize("\\a\\b\\c"), "\\a\\b\\c");
|
||||
do_check_eq(Win.normalize("\\a\\b\\\\\\\\c"), "\\a\\b\\c");
|
||||
do_check_eq(Win.normalize("\\a\\b\\c\\\\\\"), "\\a\\b\\c");
|
||||
do_check_eq(Win.normalize("\\a\\b\\c\\..\\..\\..\\d\\e\\f"), "\\d\\e\\f");
|
||||
do_check_eq(Win.normalize("a\\b\\c\\..\\..\\..\\d\\e\\f"), "d\\e\\f");
|
||||
do_check_fail(function() Win.normalize("\\a\\b\\c\\..\\..\\..\\..\\d\\e\\f"));
|
||||
|
||||
do_check_eq(Win.Path.join("\\tmp", "foo", "bar"), "\\tmp\\foo\\bar", "join \\tmp,foo,bar");
|
||||
do_check_eq(Win.Path.join("\\tmp", "\\foo", "bar"), "\\foo\\bar", "join \\tmp,\\foo,bar");
|
||||
do_check_eq(Win.Path.winGetDrive("\\tmp"), null);
|
||||
do_check_eq(Win.Path.winGetDrive("\\tmp\\a\\b\\c\\d\\e"), null);
|
||||
do_check_eq(Win.Path.winGetDrive("\\"), null);
|
||||
do_check_eq(Win.join("\\tmp", "foo", "bar"), "\\tmp\\foo\\bar", "join \\tmp,foo,bar");
|
||||
do_check_eq(Win.join("\\tmp", "\\foo", "bar"), "\\foo\\bar", "join \\tmp,\\foo,bar");
|
||||
do_check_eq(Win.winGetDrive("\\tmp"), null);
|
||||
do_check_eq(Win.winGetDrive("\\tmp\\a\\b\\c\\d\\e"), null);
|
||||
do_check_eq(Win.winGetDrive("\\"), null);
|
||||
|
||||
|
||||
do_print("Backslash-separated, with a drive");
|
||||
do_check_eq(Win.Path.basename("c:a\\b"), "b");
|
||||
do_check_eq(Win.Path.basename("c:a\\b\\"), "");
|
||||
do_check_eq(Win.Path.basename("c:abc"), "abc");
|
||||
do_check_eq(Win.Path.dirname("c:a\\b"), "c:a");
|
||||
do_check_eq(Win.Path.dirname("c:a\\b\\"), "c:a\\b");
|
||||
do_check_eq(Win.Path.dirname("c:a\\\\\\\\b"), "c:a");
|
||||
do_check_eq(Win.Path.dirname("c:abc"), "c:");
|
||||
do_check_eq(Win.basename("c:a\\b"), "b");
|
||||
do_check_eq(Win.basename("c:a\\b\\"), "");
|
||||
do_check_eq(Win.basename("c:abc"), "abc");
|
||||
do_check_eq(Win.dirname("c:a\\b"), "c:a");
|
||||
do_check_eq(Win.dirname("c:a\\b\\"), "c:a\\b");
|
||||
do_check_eq(Win.dirname("c:a\\\\\\\\b"), "c:a");
|
||||
do_check_eq(Win.dirname("c:abc"), "c:");
|
||||
let options = {
|
||||
winNoDrive: true
|
||||
};
|
||||
do_check_eq(Win.Path.dirname("c:a\\b", options), "a");
|
||||
do_check_eq(Win.Path.dirname("c:a\\b\\", options), "a\\b");
|
||||
do_check_eq(Win.Path.dirname("c:a\\\\\\\\b", options), "a");
|
||||
do_check_eq(Win.Path.dirname("c:abc", options), ".");
|
||||
do_check_eq(Win.dirname("c:a\\b", options), "a");
|
||||
do_check_eq(Win.dirname("c:a\\b\\", options), "a\\b");
|
||||
do_check_eq(Win.dirname("c:a\\\\\\\\b", options), "a");
|
||||
do_check_eq(Win.dirname("c:abc", options), ".");
|
||||
|
||||
do_check_eq(Win.Path.normalize("c:\\a\\b\\c"), "c:\\a\\b\\c");
|
||||
do_check_eq(Win.Path.normalize("c:\\a\\b\\\\\\\\c"), "c:\\a\\b\\c");
|
||||
do_check_eq(Win.Path.normalize("c:\\\\\\\\a\\b\\c"), "c:\\a\\b\\c");
|
||||
do_check_eq(Win.Path.normalize("c:\\a\\b\\c\\\\\\"), "c:\\a\\b\\c");
|
||||
do_check_eq(Win.Path.normalize("c:\\a\\b\\c\\..\\..\\..\\d\\e\\f"), "c:\\d\\e\\f");
|
||||
do_check_eq(Win.Path.normalize("c:a\\b\\c\\..\\..\\..\\d\\e\\f"), "c:d\\e\\f");
|
||||
do_check_fail(function() Win.Path.normalize("c:\\a\\b\\c\\..\\..\\..\\..\\d\\e\\f"));
|
||||
do_check_eq(Win.normalize("c:\\a\\b\\c"), "c:\\a\\b\\c");
|
||||
do_check_eq(Win.normalize("c:\\a\\b\\\\\\\\c"), "c:\\a\\b\\c");
|
||||
do_check_eq(Win.normalize("c:\\\\\\\\a\\b\\c"), "c:\\a\\b\\c");
|
||||
do_check_eq(Win.normalize("c:\\a\\b\\c\\\\\\"), "c:\\a\\b\\c");
|
||||
do_check_eq(Win.normalize("c:\\a\\b\\c\\..\\..\\..\\d\\e\\f"), "c:\\d\\e\\f");
|
||||
do_check_eq(Win.normalize("c:a\\b\\c\\..\\..\\..\\d\\e\\f"), "c:d\\e\\f");
|
||||
do_check_fail(function() Win.normalize("c:\\a\\b\\c\\..\\..\\..\\..\\d\\e\\f"));
|
||||
|
||||
do_check_eq(Win.Path.join("c:\\tmp", "foo", "bar"), "c:\\tmp\\foo\\bar", "join c:\\tmp,foo,bar");
|
||||
do_check_eq(Win.Path.join("c:\\tmp", "\\foo", "bar"), "c:\\foo\\bar", "join c:\\tmp,\\foo,bar");
|
||||
do_check_eq(Win.Path.join("c:\\tmp", "c:\\foo", "bar"), "c:\\foo\\bar", "join c:\\tmp,c:\\foo,bar");
|
||||
do_check_eq(Win.Path.join("c:\\tmp", "c:foo", "bar"), "c:foo\\bar", "join c:\\tmp,c:foo,bar");
|
||||
do_check_eq(Win.Path.winGetDrive("c:"), "c:");
|
||||
do_check_eq(Win.Path.winGetDrive("c:\\"), "c:");
|
||||
do_check_eq(Win.Path.winGetDrive("c:abc"), "c:");
|
||||
do_check_eq(Win.Path.winGetDrive("c:abc\\d\\e\\f\\g"), "c:");
|
||||
do_check_eq(Win.Path.winGetDrive("c:\\abc"), "c:");
|
||||
do_check_eq(Win.Path.winGetDrive("c:\\abc\\d\\e\\f\\g"), "c:");
|
||||
do_check_eq(Win.join("c:\\tmp", "foo", "bar"), "c:\\tmp\\foo\\bar", "join c:\\tmp,foo,bar");
|
||||
do_check_eq(Win.join("c:\\tmp", "\\foo", "bar"), "c:\\foo\\bar", "join c:\\tmp,\\foo,bar");
|
||||
do_check_eq(Win.join("c:\\tmp", "c:\\foo", "bar"), "c:\\foo\\bar", "join c:\\tmp,c:\\foo,bar");
|
||||
do_check_eq(Win.join("c:\\tmp", "c:foo", "bar"), "c:foo\\bar", "join c:\\tmp,c:foo,bar");
|
||||
do_check_eq(Win.winGetDrive("c:"), "c:");
|
||||
do_check_eq(Win.winGetDrive("c:\\"), "c:");
|
||||
do_check_eq(Win.winGetDrive("c:abc"), "c:");
|
||||
do_check_eq(Win.winGetDrive("c:abc\\d\\e\\f\\g"), "c:");
|
||||
do_check_eq(Win.winGetDrive("c:\\abc"), "c:");
|
||||
do_check_eq(Win.winGetDrive("c:\\abc\\d\\e\\f\\g"), "c:");
|
||||
|
||||
do_print("Backslash-separated, UNC-style");
|
||||
do_check_eq(Win.Path.basename("\\\\a\\b"), "b");
|
||||
do_check_eq(Win.Path.basename("\\\\a\\b\\"), "");
|
||||
do_check_eq(Win.Path.basename("\\\\abc"), "");
|
||||
do_check_eq(Win.Path.dirname("\\\\a\\b"), "\\\\a");
|
||||
do_check_eq(Win.Path.dirname("\\\\a\\b\\"), "\\\\a\\b");
|
||||
do_check_eq(Win.Path.dirname("\\\\a\\\\\\\\b"), "\\\\a");
|
||||
do_check_eq(Win.Path.dirname("\\\\abc"), "\\\\abc");
|
||||
do_check_eq(Win.Path.normalize("\\\\a\\b\\c"), "\\\\a\\b\\c");
|
||||
do_check_eq(Win.Path.normalize("\\\\a\\b\\\\\\\\c"), "\\\\a\\b\\c");
|
||||
do_check_eq(Win.Path.normalize("\\\\a\\b\\c\\\\\\"), "\\\\a\\b\\c");
|
||||
do_check_eq(Win.Path.normalize("\\\\a\\b\\c\\..\\..\\d\\e\\f"), "\\\\a\\d\\e\\f");
|
||||
do_check_fail(function() Win.Path.normalize("\\\\a\\b\\c\\..\\..\\..\\d\\e\\f"));
|
||||
do_check_eq(Win.basename("\\\\a\\b"), "b");
|
||||
do_check_eq(Win.basename("\\\\a\\b\\"), "");
|
||||
do_check_eq(Win.basename("\\\\abc"), "");
|
||||
do_check_eq(Win.dirname("\\\\a\\b"), "\\\\a");
|
||||
do_check_eq(Win.dirname("\\\\a\\b\\"), "\\\\a\\b");
|
||||
do_check_eq(Win.dirname("\\\\a\\\\\\\\b"), "\\\\a");
|
||||
do_check_eq(Win.dirname("\\\\abc"), "\\\\abc");
|
||||
do_check_eq(Win.normalize("\\\\a\\b\\c"), "\\\\a\\b\\c");
|
||||
do_check_eq(Win.normalize("\\\\a\\b\\\\\\\\c"), "\\\\a\\b\\c");
|
||||
do_check_eq(Win.normalize("\\\\a\\b\\c\\\\\\"), "\\\\a\\b\\c");
|
||||
do_check_eq(Win.normalize("\\\\a\\b\\c\\..\\..\\d\\e\\f"), "\\\\a\\d\\e\\f");
|
||||
do_check_fail(function() Win.normalize("\\\\a\\b\\c\\..\\..\\..\\d\\e\\f"));
|
||||
|
||||
do_check_eq(Win.Path.join("\\\\a\\tmp", "foo", "bar"), "\\\\a\\tmp\\foo\\bar");
|
||||
do_check_eq(Win.Path.join("\\\\a\\tmp", "\\foo", "bar"), "\\\\a\\foo\\bar");
|
||||
do_check_eq(Win.Path.join("\\\\a\\tmp", "\\\\foo\\", "bar"), "\\\\foo\\bar");
|
||||
do_check_eq(Win.Path.winGetDrive("\\\\"), null);
|
||||
do_check_eq(Win.Path.winGetDrive("\\\\c"), "\\\\c");
|
||||
do_check_eq(Win.Path.winGetDrive("\\\\c\\abc"), "\\\\c");
|
||||
do_check_eq(Win.join("\\\\a\\tmp", "foo", "bar"), "\\\\a\\tmp\\foo\\bar");
|
||||
do_check_eq(Win.join("\\\\a\\tmp", "\\foo", "bar"), "\\\\a\\foo\\bar");
|
||||
do_check_eq(Win.join("\\\\a\\tmp", "\\\\foo\\", "bar"), "\\\\foo\\bar");
|
||||
do_check_eq(Win.winGetDrive("\\\\"), null);
|
||||
do_check_eq(Win.winGetDrive("\\\\c"), "\\\\c");
|
||||
do_check_eq(Win.winGetDrive("\\\\c\\abc"), "\\\\c");
|
||||
|
||||
do_print("Testing unix paths");
|
||||
do_check_eq(Unix.Path.basename("a/b"), "b");
|
||||
do_check_eq(Unix.Path.basename("a/b/"), "");
|
||||
do_check_eq(Unix.Path.basename("abc"), "abc");
|
||||
do_check_eq(Unix.Path.dirname("a/b"), "a");
|
||||
do_check_eq(Unix.Path.dirname("a/b/"), "a/b");
|
||||
do_check_eq(Unix.Path.dirname("a////b"), "a");
|
||||
do_check_eq(Unix.Path.dirname("abc"), ".");
|
||||
do_check_eq(Unix.Path.normalize("/a/b/c"), "/a/b/c");
|
||||
do_check_eq(Unix.Path.normalize("/a/b////c"), "/a/b/c");
|
||||
do_check_eq(Unix.Path.normalize("////a/b/c"), "/a/b/c");
|
||||
do_check_eq(Unix.Path.normalize("/a/b/c///"), "/a/b/c");
|
||||
do_check_eq(Unix.Path.normalize("/a/b/c/../../../d/e/f"), "/d/e/f");
|
||||
do_check_eq(Unix.Path.normalize("a/b/c/../../../d/e/f"), "d/e/f");
|
||||
do_check_fail(function() Unix.Path.normalize("/a/b/c/../../../../d/e/f"));
|
||||
do_check_eq(Unix.basename("a/b"), "b");
|
||||
do_check_eq(Unix.basename("a/b/"), "");
|
||||
do_check_eq(Unix.basename("abc"), "abc");
|
||||
do_check_eq(Unix.dirname("a/b"), "a");
|
||||
do_check_eq(Unix.dirname("a/b/"), "a/b");
|
||||
do_check_eq(Unix.dirname("a////b"), "a");
|
||||
do_check_eq(Unix.dirname("abc"), ".");
|
||||
do_check_eq(Unix.normalize("/a/b/c"), "/a/b/c");
|
||||
do_check_eq(Unix.normalize("/a/b////c"), "/a/b/c");
|
||||
do_check_eq(Unix.normalize("////a/b/c"), "/a/b/c");
|
||||
do_check_eq(Unix.normalize("/a/b/c///"), "/a/b/c");
|
||||
do_check_eq(Unix.normalize("/a/b/c/../../../d/e/f"), "/d/e/f");
|
||||
do_check_eq(Unix.normalize("a/b/c/../../../d/e/f"), "d/e/f");
|
||||
do_check_fail(function() Unix.normalize("/a/b/c/../../../../d/e/f"));
|
||||
|
||||
do_check_eq(Unix.Path.join("/tmp", "foo", "bar"), "/tmp/foo/bar", "join /tmp,foo,bar");
|
||||
do_check_eq(Unix.Path.join("/tmp", "/foo", "bar"), "/foo/bar", "join /tmp,/foo,bar");
|
||||
do_check_eq(Unix.join("/tmp", "foo", "bar"), "/tmp/foo/bar", "join /tmp,foo,bar");
|
||||
do_check_eq(Unix.join("/tmp", "/foo", "bar"), "/foo/bar", "join /tmp,/foo,bar");
|
||||
|
||||
do_print("Testing the presence of ospath.jsm");
|
||||
let Scope = {};
|
||||
Components.utils.import("resource://gre/modules/osfile/ospath.jsm", Scope);
|
||||
do_check_true(!!Scope.basename);
|
||||
}
|
||||
|
|
|
@ -186,6 +186,14 @@ SuggestAutoComplete.prototype = {
|
|||
*/
|
||||
_formHistoryTimer: null,
|
||||
|
||||
/**
|
||||
* Maximum number of history items displayed. This is capped at 7
|
||||
* because the primary consumer (Firefox search bar) displays 10 rows
|
||||
* by default, and so we want to leave some space for suggestions
|
||||
* to be visible.
|
||||
*/
|
||||
_historyLimit: 7,
|
||||
|
||||
/**
|
||||
* This clears all the per-request state.
|
||||
*/
|
||||
|
@ -319,7 +327,8 @@ SuggestAutoComplete.prototype = {
|
|||
if (this._includeFormHistory && this._formHistoryResult &&
|
||||
(this._formHistoryResult.searchResult ==
|
||||
Ci.nsIAutoCompleteResult.RESULT_SUCCESS)) {
|
||||
for (var i = 0; i < this._formHistoryResult.matchCount; ++i) {
|
||||
var maxHistoryItems = Math.min(this._formHistoryResult.matchCount, this._historyLimit);
|
||||
for (var i = 0; i < maxHistoryItems; ++i) {
|
||||
var term = this._formHistoryResult.getValueAt(i);
|
||||
|
||||
// we don't want things to appear in both history and suggestions
|
||||
|
|
|
@ -66,6 +66,11 @@ function injectController(doc, topic, data) {
|
|||
return;
|
||||
}
|
||||
|
||||
// we always handle window.close on social content, even if they are not
|
||||
// "enabled". "enabled" is about the worker state and a provider may
|
||||
// still be in e.g. the share panel without having their worker enabled.
|
||||
handleWindowClose(window);
|
||||
|
||||
SocialService.getProvider(doc.nodePrincipal.origin, function(provider) {
|
||||
if (provider && provider.enabled) {
|
||||
attachToWindow(provider, window);
|
||||
|
@ -214,7 +219,9 @@ function attachToWindow(provider, targetWindow) {
|
|||
schedule(function () { port.close(); });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleWindowClose(targetWindow) {
|
||||
// We allow window.close() to close the panel, so add an event handler for
|
||||
// this, then cancel the event (so the window itself doesn't die) and
|
||||
// close the panel instead.
|
||||
|
|
|
@ -172,18 +172,17 @@
|
|||
}
|
||||
modules.set(path, module);
|
||||
|
||||
|
||||
// Load source of module, synchronously
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", uri, false);
|
||||
xhr.responseType = "text";
|
||||
xhr.send();
|
||||
|
||||
|
||||
let source = xhr.responseText;
|
||||
let name = ":" + path;
|
||||
let objectURL;
|
||||
try {
|
||||
// Load source of module, synchronously
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", uri, false);
|
||||
xhr.responseType = "text";
|
||||
xhr.send();
|
||||
|
||||
|
||||
let source = xhr.responseText;
|
||||
if (source == "") {
|
||||
// There doesn't seem to be a better way to detect that the file couldn't be found
|
||||
throw new Error("Could not find module " + path);
|
||||
|
|
|
@ -57,6 +57,7 @@ var BuiltinProvider = {
|
|||
"devtools/toolkit/webconsole": "resource://gre/modules/devtools/toolkit/webconsole",
|
||||
"devtools/app-actor-front": "resource://gre/modules/devtools/app-actor-front.js",
|
||||
"devtools/styleinspector/css-logic": "resource://gre/modules/devtools/styleinspector/css-logic",
|
||||
"devtools/css-color": "resource://gre/modules/devtools/css-color",
|
||||
"devtools/client": "resource://gre/modules/devtools/client",
|
||||
|
||||
"escodegen/escodegen": "resource://gre/modules/devtools/escodegen/escodegen",
|
||||
|
|
|
@ -6,6 +6,28 @@
|
|||
|
||||
const COLOR_UNIT_PREF = "devtools.defaultColorUnit";
|
||||
const {Cc, Ci, Cu} = require("chrome");
|
||||
|
||||
const REGEX_JUST_QUOTES = /^""$/;
|
||||
const REGEX_RGB_3_TUPLE = /^rgb\(([\d.]+),\s*([\d.]+),\s*([\d.]+)\)$/i;
|
||||
const REGEX_RGBA_4_TUPLE = /^rgba\(([\d.]+),\s*([\d.]+),\s*([\d.]+),\s*([\d.]+|1|0)\)$/i;
|
||||
const REGEX_HSL_3_TUPLE = /^\bhsl\(([\d.]+),\s*([\d.]+%),\s*([\d.]+%)\)$/i;
|
||||
|
||||
/**
|
||||
* This regex matches:
|
||||
* - #F00
|
||||
* - #FF0000
|
||||
* - hsl()
|
||||
* - hsla()
|
||||
* - rgb()
|
||||
* - rgba()
|
||||
* - red
|
||||
*
|
||||
* It also matches css keywords e.g. "background-color" otherwise
|
||||
* "background" would be replaced with #6363CE ("background" is a platform
|
||||
* color).
|
||||
*/
|
||||
const REGEX_ALL_COLORS = /#[0-9a-fA-F]{3}\b|#[0-9a-fA-F]{6}\b|hsl\(.*?\)|hsla\(.*?\)|rgba?\(.*?\)|\b[a-zA-Z-]+\b/g;
|
||||
|
||||
let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
|
||||
|
||||
/**
|
||||
|
@ -13,7 +35,7 @@ let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
|
|||
*
|
||||
* Usage:
|
||||
* let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
|
||||
* let {colorUtils} = devtools.require("devtools/shared/css-color");
|
||||
* let {colorUtils} = devtools.require("devtools/css-color");
|
||||
*
|
||||
* color.authored === "red"
|
||||
* color.hasAlpha === false
|
||||
|
@ -152,6 +174,10 @@ CssColor.prototype = {
|
|||
return "transparent";
|
||||
}
|
||||
if (!this.hasAlpha) {
|
||||
if (this.authored.startsWith("rgb(")) {
|
||||
// The color is valid and begins with rgb(. Return the authored value.
|
||||
return this.authored;
|
||||
}
|
||||
let tuple = this._getRGBATuple();
|
||||
return "rgb(" + tuple.r + ", " + tuple.g + ", " + tuple.b + ")";
|
||||
}
|
||||
|
@ -165,6 +191,10 @@ CssColor.prototype = {
|
|||
if (this.transparent) {
|
||||
return "transparent";
|
||||
}
|
||||
if (this.authored.startsWith("rgba(")) {
|
||||
// The color is valid and begins with rgba(. Return the authored value.
|
||||
return this.authored;
|
||||
}
|
||||
let components = this._getRGBATuple();
|
||||
return "rgba(" + components.r + ", " +
|
||||
components.g + ", " +
|
||||
|
@ -179,6 +209,10 @@ CssColor.prototype = {
|
|||
if (this.transparent) {
|
||||
return "transparent";
|
||||
}
|
||||
if (this.authored.startsWith("hsl(")) {
|
||||
// The color is valid and begins with hsl(. Return the authored value.
|
||||
return this.authored;
|
||||
}
|
||||
if (this.hasAlpha) {
|
||||
return this.hsla;
|
||||
}
|
||||
|
@ -192,11 +226,9 @@ CssColor.prototype = {
|
|||
if (this.transparent) {
|
||||
return "transparent";
|
||||
}
|
||||
// Because an hsla rbg roundtrip would lose accuracy we use the authored
|
||||
// values if this is an hsla color.
|
||||
if (this.authored.startsWith("hsla(")) {
|
||||
let [, h, s, l, a] = /^\bhsla\(([\d.]+),\s*([\d.]+%),\s*([\d.]+%),\s*([\d.]+|0|1)\)$/gi.exec(this.authored);
|
||||
return "hsla(" + h + ", " + s + ", " + l + ", " + a + ")";
|
||||
// The color is valid and begins with hsla(. Return the authored value.
|
||||
return this.authored;
|
||||
}
|
||||
if (this.hasAlpha) {
|
||||
let a = this._getRGBATuple().a;
|
||||
|
@ -261,13 +293,13 @@ CssColor.prototype = {
|
|||
return "transparent";
|
||||
}
|
||||
|
||||
let rgba = /^rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+\.\d+|1|0)\)$/gi.exec(computed);
|
||||
let rgba = computed.match(REGEX_RGBA_4_TUPLE);
|
||||
|
||||
if (rgba) {
|
||||
let [, r, g, b, a] = rgba;
|
||||
return {r: r, g: g, b: b, a: a};
|
||||
} else {
|
||||
let rgb = /^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/gi.exec(computed);
|
||||
let rgb = computed.match(REGEX_RGB_3_TUPLE);
|
||||
let [, r, g, b] = rgb;
|
||||
|
||||
return {r: r, g: g, b: b, a: 1};
|
||||
|
@ -277,10 +309,10 @@ CssColor.prototype = {
|
|||
_hslNoAlpha: function() {
|
||||
let {r, g, b} = this._getRGBATuple();
|
||||
|
||||
// Because an hsl rbg roundtrip would lose accuracy we use the authored
|
||||
// values if this is an hsla color.
|
||||
if (this.authored.startsWith("hsl(")) {
|
||||
let [, h, s, l] = /^\bhsl\(([\d.]+),\s*([\d.]+%),\s*([\d.]+%)\)$/gi.exec(this.authored);
|
||||
// We perform string manipulations on our output so let's ensure that it
|
||||
// is formatted as we expect.
|
||||
let [, h, s, l] = this.authored.match(REGEX_HSL_3_TUPLE);
|
||||
return "hsl(" + h + ", " + s + ", " + l + ")";
|
||||
}
|
||||
|
||||
|
@ -365,23 +397,11 @@ CssColor.prototype = {
|
|||
* Converted CSS String e.g. "color:#F00; background-color:#0F0;"
|
||||
*/
|
||||
function processCSSString(value) {
|
||||
if (value && /^""$/.test(value)) {
|
||||
if (value && REGEX_JUST_QUOTES.test(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// This regex matches:
|
||||
// - #F00
|
||||
// - #FF0000
|
||||
// - hsl()
|
||||
// - hsla()
|
||||
// - rgb()
|
||||
// - rgba()
|
||||
// - red
|
||||
//
|
||||
// It also matches css keywords e.g. "background-color" otherwise
|
||||
// "background" would be replaced with #6363CE ("background" is a platform
|
||||
// color).
|
||||
let colorPattern = /#[0-9a-fA-F]{3}\b|#[0-9a-fA-F]{6}\b|hsl\(.*?\)|hsla\(.*?\)|rgba?\(.*?\)|\b[a-zA-Z-]+\b/g;
|
||||
let colorPattern = REGEX_ALL_COLORS;
|
||||
|
||||
value = value.replace(colorPattern, function(match) {
|
||||
let color = new CssColor(match);
|
|
@ -80,9 +80,7 @@ loadSubScript.call(this, "resource://gre/modules/devtools/DevToolsUtils.js");
|
|||
|
||||
function dumpn(str) {
|
||||
if (wantLogging) {
|
||||
for (let line of str.split(/\n/g)) {
|
||||
dump("DBG-SERVER: " + line + "\n");
|
||||
}
|
||||
dump("DBG-SERVER: " + str + "\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ const RX_PSEUDO = /\s*:?:([\w-]+)(\(?\)?)\s*/g;
|
|||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
let {colorUtils} = require("devtools/shared/css-color");
|
||||
let {colorUtils} = require("devtools/css-color");
|
||||
|
||||
function CssLogic()
|
||||
{
|
||||
|
|
Загрузка…
Ссылка в новой задаче