Merge fx-team to m-c.

--HG--
rename : mobile/android/base/UpdateServiceHelper.java => mobile/android/base/updater/UpdateServiceHelper.java
This commit is contained in:
Ryan VanderMeulen 2013-09-30 16:18:53 -04:00
commit cc6799fe38
24 changed files with 819 additions and 249 deletions

View File

@ -123,6 +123,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "SessionHistory",
"resource:///modules/sessionstore/SessionHistory.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "_SessionFile",
"resource:///modules/sessionstore/_SessionFile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TabStateCache",
"resource:///modules/sessionstore/TabStateCache.jsm");
#ifdef MOZ_CRASHREPORTER
XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter",
@ -4179,126 +4181,6 @@ function TabData(obj = null) {
return this;
}
/**
* A cache for tabs data.
*
* This cache implements a weak map from tabs (as XUL elements)
* to tab data (as instances of TabData).
*
* Note that we should never cache private data, as:
* - that data is used very seldom by SessionStore;
* - caching private data in addition to public data is memory consuming.
*/
let TabStateCache = {
_data: new WeakMap(),
/**
* Tells whether an entry is in the cache.
*
* @param {XULElement} aKey The tab or the associated browser.
* @return {bool} Whether there's a cached entry for the given tab.
*/
has: function (aTab) {
let key = this._normalizeToBrowser(aTab);
return this._data.has(key);
},
/**
* Add or replace an entry in the cache.
*
* @param {XULElement} aTab The key, which may be either a tab
* or the corresponding browser. The binding will disappear
* if the tab/browser is destroyed.
* @param {TabData} aValue The data associated to |aTab|.
*/
set: function(aTab, aValue) {
let key = this._normalizeToBrowser(aTab);
if (!(aValue instanceof TabData)) {
throw new TypeError("Attempting to cache a non TabData");
}
this._data.set(key, aValue);
},
/**
* Return the tab data associated with a tab.
*
* @param {XULElement} aKey The tab or the associated browser.
*
* @return {TabData|undefined} The data if available, |undefined|
* otherwise.
*/
get: function(aKey) {
let key = this._normalizeToBrowser(aKey);
let result = this._data.get(key);
TabStateCacheTelemetry.recordAccess(!!result);
return result;
},
/**
* Delete the tab data associated with a tab.
*
* @param {XULElement} aKey The tab or the associated browser.
*
* Noop of there is no tab data associated with the tab.
*/
delete: function(aKey) {
let key = this._normalizeToBrowser(aKey);
this._data.delete(key);
},
/**
* Delete all tab data.
*/
clear: function() {
TabStateCacheTelemetry.recordClear();
this._data.clear();
},
/**
* Update in place a piece of data.
*
* @param {XULElement} aKey The tab or the associated browser.
* If the tab/browser is not present, do nothing.
* @param {string} aField The field to update.
* @param {*} aValue The new value to place in the field.
*/
updateField: function(aKey, aField, aValue) {
let key = this._normalizeToBrowser(aKey);
let data = this._data.get(key);
if (data) {
data[aField] = aValue;
}
TabStateCacheTelemetry.recordAccess(!!data);
},
/**
* Remove a given field from a cached tab state.
*
* @param {XULElement} aKey The tab or the associated browser.
* If the tab/browser is not present, do nothing.
* @param {string} aField The field to remove.
*/
removeField: function(aKey, aField) {
let key = this._normalizeToBrowser(aKey);
let data = this._data.get(key);
if (data && aField in data) {
delete data[aField];
}
TabStateCacheTelemetry.recordAccess(!!data);
},
_normalizeToBrowser: function(aKey) {
let nodeName = aKey.localName;
if (nodeName == "tab") {
return aKey.linkedBrowser;
}
if (nodeName == "browser") {
return aKey;
}
throw new TypeError("Key is neither a tab nor a browser: " + nodeName);
}
};
/**
* Module that contains tab state collection methods.
*/
@ -4347,7 +4229,7 @@ let TabState = {
let options = {omitSessionHistory: true,
omitSessionStorage: true,
omitDocShellCapabilities: true};
let tabData = new TabData(this._collectBaseTabData(tab, options));
let tabData = this._collectBaseTabData(tab, options);
// Apply collected data.
tabData.entries = history.entries;
@ -4401,7 +4283,7 @@ let TabState = {
return TabStateCache.get(tab);
}
let tabData = new TabData(this._collectBaseTabData(tab));
let tabData = this._collectBaseTabData(tab);
if (this._updateTextAndScrollDataForTab(tab, tabData)) {
TabStateCache.set(tab, tabData);
}
@ -4715,68 +4597,4 @@ let TabState = {
}
};
let TabStateCacheTelemetry = {
// Total number of hits during the session
_hits: 0,
// Total number of misses during the session
_misses: 0,
// Total number of clears during the session
_clears: 0,
// |true| once we have been initialized
_initialized: false,
/**
* Record a cache access.
*
* @param {boolean} isHit If |true|, the access was a hit, otherwise
* a miss.
*/
recordAccess: function(isHit) {
this._init();
if (isHit) {
++this._hits;
} else {
++this._misses;
}
},
/**
* Record a cache clear
*/
recordClear: function() {
this._init();
++this._clears;
},
/**
* Initialize the telemetry.
*/
_init: function() {
if (this._initialized) {
// Avoid double initialization
return;
}
this._initialized = true;
Services.obs.addObserver(this, "profile-before-change", false);
},
observe: function() {
Services.obs.removeObserver(this, "profile-before-change");
// Record hit/miss rate
let accesses = this._hits + this._misses;
if (accesses == 0) {
return;
}
this._fillHistogram("HIT_RATE", this._hits, accesses);
this._fillHistogram("CLEAR_RATIO", this._clears, accesses);
},
_fillHistogram: function(suffix, positive, total) {
let PREFIX = "FX_SESSION_RESTORE_TABSTATECACHE_";
let histo = Services.telemetry.getHistogramById(PREFIX + suffix);
let rate = Math.floor( ( positive * 100 ) / total );
histo.add(rate);
}
};

View File

@ -0,0 +1,292 @@
/* 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 = ["TabStateCache"];
const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm", this);
/**
* A cache for tabs data.
*
* This cache implements a weak map from tabs (as XUL elements)
* to tab data (as objects).
*
* Note that we should never cache private data, as:
* - that data is used very seldom by SessionStore;
* - caching private data in addition to public data is memory consuming.
*/
this.TabStateCache = Object.freeze({
/**
* Tells whether an entry is in the cache.
*
* @param {XULElement} aKey The tab or the associated browser.
* @return {bool} Whether there's a cached entry for the given tab.
*/
has: function (aTab) {
return TabStateCacheInternal.has(aTab);
},
/**
* Add or replace an entry in the cache.
*
* @param {XULElement} aTab The key, which may be either a tab
* or the corresponding browser. The binding will disappear
* if the tab/browser is destroyed.
* @param {*} aValue The data associated to |aTab|.
*/
set: function(aTab, aValue) {
return TabStateCacheInternal.set(aTab, aValue);
},
/**
* Return the tab data associated with a tab.
*
* @param {XULElement} aKey The tab or the associated browser.
*
* @return {*|undefined} The data if available, |undefined|
* otherwise.
*/
get: function(aKey) {
return TabStateCacheInternal.get(aKey);
},
/**
* Delete the tab data associated with a tab.
*
* @param {XULElement} aKey The tab or the associated browser.
*
* Noop of there is no tab data associated with the tab.
*/
delete: function(aKey) {
return TabStateCacheInternal.delete(aKey);
},
/**
* Delete all tab data.
*/
clear: function() {
return TabStateCacheInternal.clear();
},
/**
* Update in place a piece of data.
*
* @param {XULElement} aKey The tab or the associated browser.
* If the tab/browser is not present, do nothing.
* @param {string} aField The field to update.
* @param {*} aValue The new value to place in the field.
*/
updateField: function(aKey, aField, aValue) {
return TabStateCacheInternal.updateField(aKey, aField, aValue);
},
/**
* Remove a given field from a cached tab state.
*
* @param {XULElement} aKey The tab or the associated browser.
* If the tab/browser is not present, do nothing.
* @param {string} aField The field to remove.
*/
removeField: function(aKey, aField) {
return TabStateCacheInternal.removeField(aKey, aField);
},
/**
* Total number of cache hits during the session.
*/
get hits() {
return TabStateCacheTelemetry.hits;
},
/**
* Total number of cache misses during the session.
*/
get misses() {
return TabStateCacheTelemetry.misses;
},
/**
* Total number of cache clears during the session.
*/
get clears() {
return TabStateCacheTelemetry.clears;
},
});
let TabStateCacheInternal = {
_data: new WeakMap(),
/**
* Tells whether an entry is in the cache.
*
* @param {XULElement} aKey The tab or the associated browser.
* @return {bool} Whether there's a cached entry for the given tab.
*/
has: function (aTab) {
let key = this._normalizeToBrowser(aTab);
return this._data.has(key);
},
/**
* Add or replace an entry in the cache.
*
* @param {XULElement} aTab The key, which may be either a tab
* or the corresponding browser. The binding will disappear
* if the tab/browser is destroyed.
* @param {*} aValue The data associated to |aTab|.
*/
set: function(aTab, aValue) {
let key = this._normalizeToBrowser(aTab);
this._data.set(key, aValue);
},
/**
* Return the tab data associated with a tab.
*
* @param {XULElement} aKey The tab or the associated browser.
*
* @return {*|undefined} The data if available, |undefined|
* otherwise.
*/
get: function(aKey) {
let key = this._normalizeToBrowser(aKey);
let result = this._data.get(key);
TabStateCacheTelemetry.recordAccess(!!result);
return result;
},
/**
* Delete the tab data associated with a tab.
*
* @param {XULElement} aKey The tab or the associated browser.
*
* Noop of there is no tab data associated with the tab.
*/
delete: function(aKey) {
let key = this._normalizeToBrowser(aKey);
this._data.delete(key);
},
/**
* Delete all tab data.
*/
clear: function() {
TabStateCacheTelemetry.recordClear();
this._data.clear();
},
/**
* Update in place a piece of data.
*
* @param {XULElement} aKey The tab or the associated browser.
* If the tab/browser is not present, do nothing.
* @param {string} aField The field to update.
* @param {*} aValue The new value to place in the field.
*/
updateField: function(aKey, aField, aValue) {
let key = this._normalizeToBrowser(aKey);
let data = this._data.get(key);
if (data) {
data[aField] = aValue;
}
TabStateCacheTelemetry.recordAccess(!!data);
},
/**
* Remove a given field from a cached tab state.
*
* @param {XULElement} aKey The tab or the associated browser.
* If the tab/browser is not present, do nothing.
* @param {string} aField The field to remove.
*/
removeField: function(aKey, aField) {
let key = this._normalizeToBrowser(aKey);
let data = this._data.get(key);
if (data && aField in data) {
delete data[aField];
}
TabStateCacheTelemetry.recordAccess(!!data);
},
_normalizeToBrowser: function(aKey) {
let nodeName = aKey.localName;
if (nodeName == "tab") {
return aKey.linkedBrowser;
}
if (nodeName == "browser") {
return aKey;
}
throw new TypeError("Key is neither a tab nor a browser: " + nodeName);
}
};
let TabStateCacheTelemetry = {
// Total number of hits during the session
hits: 0,
// Total number of misses during the session
misses: 0,
// Total number of clears during the session
clears: 0,
// |true| once we have been initialized
_initialized: false,
/**
* Record a cache access.
*
* @param {boolean} isHit If |true|, the access was a hit, otherwise
* a miss.
*/
recordAccess: function(isHit) {
this._init();
if (isHit) {
++this.hits;
} else {
++this.misses;
}
},
/**
* Record a cache clear
*/
recordClear: function() {
this._init();
++this.clears;
},
/**
* Initialize the telemetry.
*/
_init: function() {
if (this._initialized) {
// Avoid double initialization
return;
}
this._initialized = true;
Services.obs.addObserver(this, "profile-before-change", false);
},
observe: function() {
Services.obs.removeObserver(this, "profile-before-change");
// Record hit/miss rate
let accesses = this.hits + this.misses;
if (accesses == 0) {
return;
}
this._fillHistogram("HIT_RATE", this.hits, accesses);
this._fillHistogram("CLEAR_RATIO", this.clears, accesses);
},
_fillHistogram: function(suffix, positive, total) {
let PREFIX = "FX_SESSION_RESTORE_TABSTATECACHE_";
let histo = Services.telemetry.getHistogramById(PREFIX + suffix);
let rate = Math.floor( ( positive * 100 ) / total );
histo.add(rate);
}
};

View File

@ -22,6 +22,7 @@ EXTRA_JS_MODULES = [
'SessionMigration.jsm',
'SessionStorage.jsm',
'SessionWorker.js',
'TabStateCache.jsm',
'XPathGenerator.jsm',
'_SessionFile.jsm',
]

View File

@ -20,6 +20,7 @@ MOCHITEST_BROWSER_FILES = \
browser_input_sample.html \
browser_pageshow.js \
browser_sessionStorage.js \
browser_tabStateCache.js \
browser_upgrade_backup.js \
browser_windowRestore_perwindowpb.js \
browser_248970_b_perwindowpb.js \

View File

@ -7,26 +7,6 @@ Cu.import("resource://gre/modules/Task.jsm", Scope);
Cu.import("resource://gre/modules/Promise.jsm", Scope);
let {Task, Promise} = Scope;
function promiseBrowserLoaded(aBrowser) {
let deferred = Promise.defer();
whenBrowserLoaded(aBrowser, () => deferred.resolve());
return deferred.promise;
}
function forceWriteState() {
let deferred = Promise.defer();
const PREF = "browser.sessionstore.interval";
const TOPIC = "sessionstore-state-write";
Services.obs.addObserver(function observe() {
Services.obs.removeObserver(observe, TOPIC);
Services.prefs.clearUserPref(PREF);
deferred.resolve();
}, TOPIC, false);
Services.prefs.setIntPref(PREF, 0);
return deferred.promise;
}
function waitForStorageChange(aTab) {
let deferred = Promise.defer();
@ -57,7 +37,7 @@ function test() {
// Flush loading and next save, call getBrowserState()
// a few times to ensure that everything is cached.
yield promiseBrowserLoaded(tab.linkedBrowser);
yield forceWriteState();
yield forceSaveState();
info("Calling getBrowserState() to populate cache");
ss.getBrowserState();
@ -66,7 +46,7 @@ function test() {
win.sessionStorage[SESSION_STORAGE_KEY] = SESSION_STORAGE_VALUE;
let storageChanged = yield storageChangedPromise;
ok(storageChanged, "Changing sessionStorage triggered the right message");
yield forceWriteState();
yield forceSaveState();
let state = ss.getBrowserState();
ok(state.indexOf(SESSION_STORAGE_KEY) != -1, "Key appears in state");
@ -78,7 +58,7 @@ function test() {
win.localStorage[LOCAL_STORAGE_KEY] = LOCAL_STORAGE_VALUE;
storageChanged = yield storageChangedPromise;
ok(!storageChanged, "Changing localStorage did not trigger a message");
yield forceWriteState();
yield forceSaveState();
state = ss.getBrowserState();
ok(state.indexOf(LOCAL_STORAGE_KEY) == -1, "Key does not appear in state");

View File

@ -0,0 +1,141 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
let wrapper = {};
Cu.import("resource:///modules/sessionstore/TabStateCache.jsm", wrapper);
let {TabStateCache} = wrapper;
// The number of tabs that are present in the browser but that we're not dealing
// with. This should be one (for an empty about:blank), but let's not make this
// a magic number.
let numberOfUntrackedTabs;
// Arbitrary URL prefix, used to generate the URL of pages we visit
const URL_PREFIX = "http://example.org:80/";
/**
* Check tab state cache telemetry statistics before and after an operation.
*
* @param {function} f The operation being measured. If it returns a promise,
* we wait until the promise is resolved before proceeding.
* @return {promise}
*/
function getTelemetryDelta(f) {
return Task.spawn(function() {
let KEYS = ["hits", "misses", "clears"];
let old = {};
for (let key of KEYS) {
old[key] = TabStateCache[key];
}
yield f();
let result = {};
for (let key of KEYS) {
result[key] = TabStateCache[key] - old[key];
}
ok(result.hits >= 0, "Sanity check: hits have not decreased");
ok(result.misses >= 0, "Sanity check: misses have not decreased");
ok(result.clears >= 0, "Sanity check: clears have not decreased");
throw new Task.Result(result);
});
}
add_task(function init() {
// Start with an empty cache
closeAllButPrimaryWindow();
TabStateCache.clear();
numberOfUntrackedTabs = gBrowser.tabs.length;
info("Starting with " + numberOfUntrackedTabs + " tabs");
});
add_task(function add_remove() {
info("Adding the first tab");
// Initialize one tab, save to initialize cache
let tab1 = gBrowser.addTab(URL_PREFIX + "?tab1");
yield promiseBrowserLoaded(tab1.linkedBrowser);
yield getTelemetryDelta(forceSaveState);
// Save/collect again a few times, ensure that we always hit
info("Save/collect a few times with one tab");
for (let collector of [forceSaveState, ss.getBrowserState]) {
for (let i = 0; i < 5; ++i) {
let PREFIX = "Trivial test " + i + " using " + collector.name + ": ";
let delta = yield getTelemetryDelta(collector);
is(delta.hits, numberOfUntrackedTabs + 1, PREFIX + " has at least one hit " + delta.hits);
is(delta.misses, 0, PREFIX + " has no miss");
is(delta.clears, 0, PREFIX + " has no clear");
}
}
// Add a second tab, ensure that we have both hits and misses
info("Adding the second tab");
let tab2 = gBrowser.addTab(URL_PREFIX + "?tab2");
yield promiseBrowserLoaded(tab2.linkedBrowser);
let PREFIX = "Adding second tab: ";
ok(!TabStateCache.has(tab2), PREFIX + " starts out of the cache");
let delta = yield getTelemetryDelta(forceSaveState);
is(delta.hits, numberOfUntrackedTabs + 2, PREFIX + " we hit all tabs, thanks to prefetching");
is(delta.misses, 0, PREFIX + " we missed no tabs, thanks to prefetching");
is(delta.clears, 0, PREFIX + " has no clear");
// Save/collect again a few times, ensure that we always hit
info("Save/collect a few times with two tabs");
for (let collector of [forceSaveState, ss.getBrowserState]) {
for (let i = 0; i < 5; ++i) {
let PREFIX = "With two tabs " + i + " using " + collector.name + ": ";
let delta = yield getTelemetryDelta(collector);
is(delta.hits, numberOfUntrackedTabs + 2, PREFIX + " both tabs hit");
is(delta.misses, 0, PREFIX + " has no miss");
is(delta.clears, 0, PREFIX + " has no clear");
}
}
info("Removing second tab");
gBrowser.removeTab(tab2);
PREFIX = "Removing second tab: ";
delta = yield getTelemetryDelta(forceSaveState);
is(delta.hits, numberOfUntrackedTabs + 1, PREFIX + " we hit for one tab");
is(delta.misses, 0, PREFIX + " has no miss");
is(delta.clears, 0, PREFIX + " has no clear");
info("Removing first tab");
gBrowser.removeTab(tab1);
});
add_task(function browsing() {
info("Opening first browsing tab");
let tab1 = gBrowser.addTab(URL_PREFIX + "?do_not_move_from_here");
let browser1 = tab1.linkedBrowser;
yield promiseBrowserLoaded(browser1);
yield forceSaveState();
info("Opening second browsing tab");
let tab2 = gBrowser.addTab(URL_PREFIX + "?start_browsing_from_here");
let browser2 = tab2.linkedBrowser;
yield promiseBrowserLoaded(browser2);
for (let i = 0; i < 4; ++i) {
let url = URL_PREFIX + "?browsing" + i; // Arbitrary url, easy to recognize
let PREFIX = "Browsing to " + url;
info(PREFIX);
let delta = yield getTelemetryDelta(function() {
return Task.spawn(function() {
// Move to new URI then save session
let promise = promiseBrowserLoaded(browser2);
browser2.loadURI(url);
yield promise;
ok(!TabStateCache.has(browser2), PREFIX + " starts out of the cache");
yield forceSaveState();
});
});
is(delta.hits, numberOfUntrackedTabs + 2, PREFIX + " has all hits, thanks to prefetching");
is(delta.misses, 0, PREFIX + " has no miss, thanks to prefetching");
is(delta.clears, 0, PREFIX + " has no clear");
}
gBrowser.removeTab(tab2);
gBrowser.removeTab(tab1);
});

View File

@ -240,6 +240,34 @@ function waitForSaveState(aCallback) {
Services.prefs.getIntPref("browser.sessionstore.interval");
return waitForTopic("sessionstore-state-write", timeout, aCallback);
}
function promiseSaveState() {
let deferred = Promise.defer();
waitForSaveState(isSuccessful => {
if (isSuccessful) {
deferred.resolve();
} else {
deferred.reject(new Error("timeout"));
}});
return deferred.promise;
}
function forceSaveState() {
let promise = promiseSaveState();
const PREF = "browser.sessionstore.interval";
// Set interval to an arbitrary non-0 duration
// to ensure that setting it to 0 will notify observers
Services.prefs.setIntPref(PREF, 1000);
Services.prefs.setIntPref(PREF, 0);
return promise.then(
function onSuccess(x) {
Services.prefs.clearUserPref(PREF);
return x;
},
function onError(x) {
Services.prefs.clearUserPref(PREF);
throw x;
}
);
}
function whenBrowserLoaded(aBrowser, aCallback = next) {
aBrowser.addEventListener("load", function onLoad() {
@ -247,6 +275,22 @@ function whenBrowserLoaded(aBrowser, aCallback = next) {
executeSoon(aCallback);
}, true);
}
function promiseBrowserLoaded(aBrowser) {
let deferred = Promise.defer();
whenBrowserLoaded(aBrowser, deferred.resolve);
return deferred.promise;
}
function whenBrowserUnloaded(aBrowser, aContainer, aCallback = next) {
aBrowser.addEventListener("unload", function onUnload() {
aBrowser.removeEventListener("unload", onUnload, true);
executeSoon(aCallback);
}, true);
}
function promiseBrowserUnloaded(aBrowser, aContainer) {
let deferred = Promise.defer();
whenBrowserUnloaded(aBrowser, aContainer, deferred.resolve);
return deferred.promise;
}
function whenWindowLoaded(aWindow, aCallback = next) {
aWindow.addEventListener("load", function windowLoadListener() {

View File

@ -75,6 +75,38 @@ struct Paths {
nsString tmpDir;
nsString profileDir;
nsString localProfileDir;
/**
* The user's home directory
*/
nsString homeDir;
/**
* The user's desktop directory, if there is one. Otherwise this is
* the same as homeDir.
*/
nsString desktopDir;
#if defined(XP_WIN)
/**
* The user's application data directory.
*/
nsString winAppDataDir;
/**
* The programs subdirectory in the user's start menu directory.
*/
nsString winStartMenuProgsDir;
#endif // defined(XP_WIN)
#if defined(XP_MACOSX)
/**
* The user's Library directory.
*/
nsString macUserLibDir;
/**
* The Application directory, that stores applications installed in the
* system.
*/
nsString macLocalApplicationsDir;
#endif // defined(XP_MACOSX)
Paths()
{
@ -82,6 +114,18 @@ struct Paths {
tmpDir.SetIsVoid(true);
profileDir.SetIsVoid(true);
localProfileDir.SetIsVoid(true);
homeDir.SetIsVoid(true);
desktopDir.SetIsVoid(true);
#if defined(XP_WIN)
winAppDataDir.SetIsVoid(true);
winStartMenuProgsDir.SetIsVoid(true);
#endif // defined(XP_WIN)
#if defined(XP_MACOSX)
macUserLibDir.SetIsVoid(true);
macLocalApplicationsDir.SetIsVoid(true);
#endif // defined(XP_MACOSX)
}
};
@ -209,6 +253,18 @@ nsresult InitOSFileConstants()
// some platforms or in non-Firefox embeddings of Gecko).
GetPathToSpecialDir(NS_OS_TEMP_DIR, paths->tmpDir);
GetPathToSpecialDir(NS_OS_HOME_DIR, paths->homeDir);
GetPathToSpecialDir(NS_OS_DESKTOP_DIR, paths->desktopDir);
#if defined(XP_WIN)
GetPathToSpecialDir(NS_WIN_APPDATA_DIR, paths->winAppDataDir);
GetPathToSpecialDir(NS_WIN_PROGRAMS_DIR, paths->winStartMenuProgsDir);
#endif // defined(XP_WIN)
#if defined(XP_MACOSX)
GetPathToSpecialDir(NS_MAC_USER_LIB_DIR, paths->macUserLibDir);
GetPathToSpecialDir(NS_OSX_LOCAL_APPLICATIONS_DIR, paths->macLocalApplicationsDir);
#endif // defined(XP_MACOSX)
gPaths = paths.forget();
return NS_OK;
@ -769,6 +825,34 @@ bool DefineOSFileConstants(JSContext *cx, JS::Handle<JSObject*> global)
return false;
}
if (!SetStringProperty(cx, objPath, "homeDir", gPaths->homeDir)) {
return false;
}
if (!SetStringProperty(cx, objPath, "desktopDir", gPaths->desktopDir)) {
return false;
}
#if defined(XP_WIN)
if (!SetStringProperty(cx, objPath, "winAppDataDir", gPaths->winAppDataDir)) {
return false;
}
if (!SetStringProperty(cx, objPath, "winStartMenuProgsDir", gPaths->winStartMenuProgsDir)) {
return false;
}
#endif // defined(XP_WIN)
#if defined(XP_MACOSX)
if (!SetStringProperty(cx, objPath, "macUserLibDir", gPaths->macUserLibDir)) {
return false;
}
if (!SetStringProperty(cx, objPath, "macLocalApplicationsDir", gPaths->macLocalApplicationsDir)) {
return false;
}
#endif // defined(XP_MACOSX)
return true;
}

View File

@ -835,18 +835,24 @@ abstract public class GeckoApp
void showButtonToast(final String message, final String buttonText,
final String buttonIcon, final String buttonId) {
BitmapUtils.getDrawable(GeckoApp.this, buttonIcon, new BitmapUtils.BitmapLoader() {
public void onBitmapFound(Drawable d) {
mToast.show(false, message, buttonText, d, new ButtonToast.ToastListener() {
@Override
public void onButtonClicked() {
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Toast:Click", buttonId));
}
public void onBitmapFound(final Drawable d) {
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void onToastHidden(ButtonToast.ReasonHidden reason) {
if (reason == ButtonToast.ReasonHidden.TIMEOUT) {
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Toast:Hidden", buttonId));
}
public void run() {
mToast.show(false, message, buttonText, d, new ButtonToast.ToastListener() {
@Override
public void onButtonClicked() {
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Toast:Click", buttonId));
}
@Override
public void onToastHidden(ButtonToast.ReasonHidden reason) {
if (reason == ButtonToast.ReasonHidden.TIMEOUT) {
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Toast:Hidden", buttonId));
}
}
});
}
});
}

View File

@ -157,7 +157,6 @@ FENNEC_JAVA_FILES = \
TextSelectionHandle.java \
ThumbnailHelper.java \
TouchEventInterceptor.java \
UpdateServiceHelper.java \
VideoPlayer.java \
WebAppAllocator.java \
WebAppImpl.java \
@ -253,6 +252,8 @@ FENNEC_JAVA_FILES = \
menu/MenuPopup.java \
preferences/SearchPreferenceCategory.java \
preferences/SearchEnginePreference.java \
updater/UpdateServiceHelper.java \
updater/UpdateService.java \
widget/ActivityChooserModel.java \
widget/ButtonToast.java \
widget/ArrowPopup.java \
@ -267,7 +268,6 @@ FENNEC_JAVA_FILES = \
widget/TwoWayView.java \
GeckoNetworkManager.java \
GeckoScreenOrientationListener.java \
UpdateService.java \
GeckoUpdateReceiver.java \
ReferrerReceiver.java \
$(NULL)

View File

@ -30,7 +30,8 @@ let WebAppRT = {
// Set a future policy version to avoid the telemetry prompt.
pref("toolkit.telemetry.prompted", 999),
pref("toolkit.telemetry.notifiedOptOut", 999),
pref("media.useAudioChannelService", true)
pref("media.useAudioChannelService", true),
pref("dom.mozTCPSocket.enabled", true),
],
init: function(aStatus, aUrl, aCallback) {

View File

@ -4154,7 +4154,7 @@ var BrowserEventHandler = {
if (this._scrollableElement != null) {
// Discard if it's the top-level scrollable, we let Java handle this
let doc = BrowserApp.selectedBrowser.contentDocument;
if (this._scrollableElement != doc.body && this._scrollableElement != doc.documentElement)
if (this._scrollableElement != doc.documentElement)
sendMessageToJava({ type: "Panning:Override" });
}
}
@ -4241,7 +4241,6 @@ var BrowserEventHandler = {
let doc = BrowserApp.selectedBrowser.contentDocument;
if (this._scrollableElement == null ||
this._scrollableElement == doc.body ||
this._scrollableElement == doc.documentElement) {
sendMessageToJava({ type: "Panning:CancelOverride" });
return;
@ -4612,15 +4611,14 @@ var BrowserEventHandler = {
let scrollable = false;
while (elem) {
/* Element is scrollable if its scroll-size exceeds its client size, and:
* - It has overflow 'auto' or 'scroll'
* - It's a textarea
* - It's an HTML/BODY node
* - It has overflow 'auto' or 'scroll', or
* - It's a textarea or HTML node, or
* - It's a select element showing multiple rows
*/
if (checkElem) {
if ((elem.scrollTopMax > 0 || elem.scrollLeftMax > 0) &&
(this._hasScrollableOverflow(elem) ||
elem.mozMatchesSelector("html, body, textarea")) ||
elem.mozMatchesSelector("html, textarea")) ||
(elem instanceof HTMLSelectElement && (elem.size > 1 || elem.multiple))) {
scrollable = true;
break;
@ -6539,6 +6537,13 @@ var SearchEngines = {
prompted: Services.prefs.getBoolPref(this.PREF_SUGGEST_PROMPTED)
}
});
// Send a speculative connection to the default engine.
let connector = Services.io.QueryInterface(Ci.nsISpeculativeConnect);
let searchURI = Services.search.defaultEngine.getSubmission("dummy").uri;
let callbacks = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsILoadContext);
connector.speculativeConnect(searchURI, callbacks);
},
_handleSearchEnginesGetAll: function _handleSearchEnginesGetAll(rv) {

View File

@ -101,6 +101,10 @@ let HomeBanner = {
if (Object.keys(this._messages).length == 1) {
Services.obs.addObserver(this, "HomeBanner:Get", false);
Services.obs.addObserver(this, "HomeBanner:Click", false);
// Send a message to Java, in case there's an active HomeBanner
// waiting for a response.
this._handleGet();
}
return message.id;

View File

@ -483,6 +483,9 @@ pref("toolkit.telemetry.debugSlowSql", false);
pref("toolkit.identity.enabled", false);
pref("toolkit.identity.debug", false);
// AsyncShutdown delay before crashing in case of shutdown freeze
pref("toolkit.asyncshutdown.timeout.crash", 60000);
// Enable deprecation warnings.
pref("devtools.errorconsole.deprecation_warnings", true);

View File

@ -22,16 +22,6 @@ let main = this;
function test() {
info("test_osfile_front.xul: Starting test");
// Test OS.Constants.Path
Components.classes["@mozilla.org/net/osfileconstantsservice;1"].
getService(Components.interfaces.nsIOSFileConstantsService).
init();
Components.utils.import("resource://gre/modules/Services.jsm");
is(OS.Constants.Path.tmpDir, Services.dirsvc.get("TmpD", Components.interfaces.nsIFile).path, "OS.Constants.Path.tmpDir is correct");
is(OS.Constants.Path.profileDir, Services.dirsvc.get("ProfD", Components.interfaces.nsIFile).path, "OS.Constants.Path.profileDir is correct");
// Test the OS.File worker
worker = new ChromeWorker("worker_test_osfile_front.js");

View File

@ -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/. */
"use strict";
Components.utils.import("resource://gre/modules/osfile.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");
function run_test() {
run_next_test();
}
function compare_paths(ospath, key) {
let file;
try {
file = Services.dirsvc.get(key, Components.interfaces.nsIFile);
} catch(ex) {}
if (file) {
do_check_true(!!ospath);
do_check_eq(ospath, file.path);
} else {
do_check_false(!!ospath);
}
}
add_test(function() {
do_check_true(!!OS.Constants.Path.tmpDir);
do_check_eq(OS.Constants.Path.tmpDir, Services.dirsvc.get("TmpD", Components.interfaces.nsIFile).path);
do_check_true(!!OS.Constants.Path.homeDir);
do_check_eq(OS.Constants.Path.homeDir, Services.dirsvc.get("Home", Components.interfaces.nsIFile).path);
do_check_true(!!OS.Constants.Path.desktopDir);
do_check_eq(OS.Constants.Path.desktopDir, Services.dirsvc.get("Desk", Components.interfaces.nsIFile).path);
compare_paths(OS.Constants.Path.winAppDataDir, "AppData");
compare_paths(OS.Constants.Path.winStartMenuProgsDir, "Progs");
compare_paths(OS.Constants.Path.macUserLibDir, "ULibDir");
compare_paths(OS.Constants.Path.macLocalApplicationsDir, "LocApp");
run_next_test();
});

View File

@ -8,3 +8,4 @@ tail =
[test_profiledir.js]
[test_logging.js]
[test_creationDate.js]
[test_path_constants.js]

View File

@ -0,0 +1,32 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
function setup_crash() {
Components.utils.import("resource://gre/modules/AsyncShutdown.jsm", this);
Components.utils.import("resource://gre/modules/Services.jsm", this);
Components.utils.import("resource://gre/modules/Promise.jsm", this);
Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true);
Services.prefs.setIntPref("toolkit.asyncshutdown.crash_timeout", 10);
let TOPIC = "testing-async-shutdown-crash";
let phase = AsyncShutdown._getPhase(TOPIC);
phase.addBlocker("A blocker that is never satisfied", function() {
dump("Installing blocker\n");
let deferred = Promise.defer();
return deferred.promise;
});
Services.obs.notifyObservers(null, TOPIC, null);
dump("Waiting for crash\n");
}
function after_crash(mdump, extra) {
let info = JSON.parse(extra.AsyncShutdownTimeout);
do_check_true(info.phase == "testing-async-shutdown-crash");
do_check_true(info.conditions.indexOf("A blocker that is never satisfied") != -1);
}
function run_test() {
do_crash(setup_crash, after_crash);
}

View File

@ -24,3 +24,5 @@ run-if = os == 'win'
run-if = os == 'win' || os == 'linux'
# we need to skip this due to bug 838613
skip-if = os=='linux' && bits==32
[test_crash_AsyncShutdown.js]

View File

@ -46,12 +46,45 @@
const Cu = Components.utils;
const Cc = Components.classes;
const Ci = Components.interfaces;
Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
Cu.import("resource://gre/modules/Services.jsm", this);
Cu.import("resource://gre/modules/Promise.jsm", this);
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "gDebug",
"@mozilla.org/xpcom/debug;1", "nsIDebug");
Object.defineProperty(this, "gCrashReporter", {
get: function() {
delete this.gCrashReporter;
try {
let reporter = Cc["@mozilla.org/xre/app-info;1"].
getService(Ci.nsICrashReporter);
return this.gCrashReporter = reporter;
} catch (ex) {
return this.gCrashReporter = null;
}
},
configurable: true
});
// Display timeout warnings after 10 seconds
const DELAY_WARNING_MS = 10 * 1000;
// Crash the process if shutdown is really too long
// (allowing for sleep).
const PREF_DELAY_CRASH_MS = "toolkit.asyncshutdown.crash_timeout";
let DELAY_CRASH_MS = 60 * 1000; // One minute
try {
DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS);
} catch (ex) {
// Ignore errors
}
Services.prefs.addObserver(PREF_DELAY_CRASH_MS, function() {
DELAY_CRASH_MS = Services.prefs.getIntPref(PREF_DELAY_CRASH_MS);
}, false);
/**
* Display a warning.
*
@ -59,15 +92,47 @@ const DELAY_WARNING_MS = 10 * 1000;
* that the UX will not be available to display warnings on the
* console. We therefore use dump() rather than Cu.reportError().
*/
function warn(msg, error = null) {
dump("WARNING: " + msg + "\n");
function log(msg, prefix = "", error = null) {
dump(prefix + msg + "\n");
if (error) {
dump("WARNING: " + error + "\n");
dump(prefix + error + "\n");
if (typeof error == "object" && "stack" in error) {
dump("WARNING: " + error.stack + "\n");
dump(prefix + error.stack + "\n");
}
}
}
function warn(msg, error = null) {
return log(msg, "WARNING: ", error);
}
function err(msg, error = null) {
return log(msg, "ERROR: ", error);
}
/**
* Countdown for a given duration, skipping beats if the computer is too busy,
* sleeping or otherwise unavailable.
*
* @param {number} delay An approximate delay to wait in milliseconds (rounded
* up to the closest second).
*
* @return Deferred
*/
function looseTimer(delay) {
let DELAY_BEAT = 1000;
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
let beats = Math.ceil(delay / DELAY_BEAT);
let deferred = Promise.defer();
timer.initWithCallback(function() {
if (beats <= 0) {
deferred.resolve();
}
--beats;
}, DELAY_BEAT, Ci.nsITimer.TYPE_REPEATING_PRECISE_CAN_SKIP);
// Ensure that the timer is both canceled once we are done with it
// and not garbage-collected until then.
deferred.promise.then(() => timer.cancel(), () => timer.cancel());
return deferred;
}
this.EXPORTED_SYMBOLS = ["AsyncShutdown"];
@ -202,8 +267,13 @@ Spinner.prototype = {
return;
}
// The promises for which we are waiting.
let allPromises = [];
// Information to determine and report to the user which conditions
// are not satisfied yet.
let allMonitors = [];
for (let {condition, name} of conditions) {
// Gather all completion conditions
@ -226,20 +296,25 @@ Spinner.prototype = {
// If the promise takes too long to be resolved/rejected,
// we need to notify the user.
//
// Future versions may decide to crash Firefox rather than
// let it loop and suck battery power forever.
// If it takes way too long, we need to crash.
let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
timer.initWithCallback(function() {
let msg = "A phase completion condition is" +
" taking too long to complete." +
" Condition: " + name +
" Condition: " + monitor.name +
" Phase: " + topic;
warn(msg);
}, DELAY_WARNING_MS, Ci.nsITimer.TYPE_ONE_SHOT);
let monitor = {
isFrozen: true,
name: name
};
condition = condition.then(function onSuccess() {
timer.cancel();
timer.cancel(); // As a side-effect, this prevents |timer| from
// being garbage-collected too early.
monitor.isFrozen = false;
}, function onError(error) {
timer.cancel();
let msg = "A completion condition encountered an error" +
@ -247,8 +322,9 @@ Spinner.prototype = {
" Condition: " + name +
" Phase: " + topic;
warn(msg, error);
monitor.isFrozen = false;
});
allMonitors.push(monitor);
allPromises.push(condition);
} catch (error) {
@ -275,8 +351,52 @@ Spinner.prototype = {
let satisfied = false; // |true| once we have satisfied all conditions
// If after DELAY_CRASH_MS (approximately one minute, adjusted to take
// into account sleep and otherwise busy computer) we have not finished
// this shutdown phase, we assume that the shutdown is somehow frozen,
// presumably deadlocked. At this stage, the only thing we can do to
// avoid leaving the user's computer in an unstable (and battery-sucking)
// situation is report the issue and crash.
let timeToCrash = looseTimer(DELAY_CRASH_MS);
timeToCrash.promise.then(
function onTimeout() {
// Report the problem as best as we can, then crash.
let frozen = [];
for (let {name, isFrozen} of allMonitors) {
if (isFrozen) {
frozen.push(name);
}
}
let msg = "At least one completion condition failed to complete" +
" within a reasonable amount of time. Causing a crash to" +
" ensure that we do not leave the user with an unresponsive" +
" process draining resources." +
" Conditions: " + frozen.join(", ") +
" Phase: " + topic;
err(msg);
if (gCrashReporter) {
let data = {
phase: topic,
conditions: frozen
};
gCrashReporter.annotateCrashReport("AsyncShutdownTimeout",
JSON.stringify(data));
} else {
warn("No crash reporter available");
}
let error = new Error();
gDebug.abort(error.fileName, error.lineNumber + 1);
},
function onSatisfied() {
// The promise has been rejected, which means that we have satisfied
// all completion conditions.
});
promise = promise.then(function() {
satisfied = true;
timeToCrash.reject();
}/* No error is possible here*/);
// Now, spin the event loop

View File

@ -6,7 +6,7 @@ support-files =
propertyLists/bug710259_propertyListXML.plist
chromeappsstore.sqlite
[test_phase.js]
[test_AsyncShutdown.js]
[test_dict.js]
[test_FileUtils.js]
[test_Http.js]