зеркало из https://github.com/mozilla/gecko-dev.git
merge fx-team to mozilla-central a=merge
This commit is contained in:
Коммит
54b57749ad
|
@ -201,6 +201,7 @@ toolkit/content/contentAreaUtils.js
|
|||
toolkit/content/widgets/videocontrols.xml
|
||||
toolkit/components/jsdownloads/src/DownloadIntegration.jsm
|
||||
toolkit/components/search/nsSearchService.js
|
||||
toolkit/components/telemetry/healthreport-prefs.js
|
||||
toolkit/components/url-classifier/**
|
||||
toolkit/components/urlformatter/nsURLFormatter.js
|
||||
toolkit/identity/FirefoxAccounts.jsm
|
||||
|
|
|
@ -19,7 +19,6 @@ MOZ_OFFICIAL_BRANDING_DIRECTORY=b2g/branding/official
|
|||
|
||||
MOZ_SAFE_BROWSING=1
|
||||
MOZ_SERVICES_COMMON=1
|
||||
MOZ_SERVICES_METRICS=1
|
||||
|
||||
MOZ_WEBSMS_BACKEND=1
|
||||
MOZ_NO_SMART_CARDS=1
|
||||
|
|
|
@ -26,7 +26,6 @@ MOZ_OFFICIAL_BRANDING_DIRECTORY=b2g/branding/official
|
|||
|
||||
MOZ_SAFE_BROWSING=1
|
||||
MOZ_SERVICES_COMMON=1
|
||||
MOZ_SERVICES_METRICS=1
|
||||
MOZ_CAPTIVEDETECT=1
|
||||
|
||||
MOZ_WEBSMS_BACKEND=1
|
||||
|
|
|
@ -627,10 +627,6 @@
|
|||
#endif
|
||||
@RESPATH@/components/servicesComponents.manifest
|
||||
@RESPATH@/components/cryptoComponents.manifest
|
||||
#ifdef MOZ_SERVICES_HEALTHREPORT
|
||||
@RESPATH@/components/HealthReportComponents.manifest
|
||||
@RESPATH@/components/HealthReportService.js
|
||||
#endif
|
||||
@RESPATH@/components/CaptivePortalDetectComponents.manifest
|
||||
@RESPATH@/components/captivedetect.js
|
||||
@RESPATH@/components/TelemetryStartup.js
|
||||
|
|
|
@ -1548,7 +1548,8 @@ pref("experiments.supported", true);
|
|||
pref("media.gmp-provider.enabled", true);
|
||||
|
||||
#ifdef NIGHTLY_BUILD
|
||||
pref("browser.polaris.enabled", false);
|
||||
pref("privacy.trackingprotection.ui.enabled", true);
|
||||
#else
|
||||
pref("privacy.trackingprotection.ui.enabled", false);
|
||||
#endif
|
||||
pref("privacy.trackingprotection.introCount", 0);
|
||||
|
|
|
@ -23,10 +23,6 @@ var healthReportWrapper = {
|
|||
let iframe = document.getElementById("remote-report");
|
||||
iframe.addEventListener("load", healthReportWrapper.initRemotePage, false);
|
||||
iframe.src = this._getReportURI().spec;
|
||||
iframe.onload = () => {
|
||||
MozSelfSupport.getHealthReportPayload().then(this.updatePayload,
|
||||
this.handleInitFailure);
|
||||
};
|
||||
prefs.observe("uploadEnabled", this.updatePrefState, healthReportWrapper);
|
||||
},
|
||||
|
||||
|
@ -103,15 +99,6 @@ var healthReportWrapper = {
|
|||
});
|
||||
},
|
||||
|
||||
refreshPayload: function () {
|
||||
MozSelfSupport.getHealthReportPayload().then(this.updatePayload,
|
||||
this.handlePayloadFailure);
|
||||
},
|
||||
|
||||
updatePayload: function (payload) {
|
||||
healthReportWrapper.injectData("payload", JSON.stringify(payload));
|
||||
},
|
||||
|
||||
injectData: function (type, content) {
|
||||
let report = this._getReportURI();
|
||||
|
||||
|
@ -139,9 +126,6 @@ var healthReportWrapper = {
|
|||
case "RequestCurrentPrefs":
|
||||
this.updatePrefState();
|
||||
break;
|
||||
case "RequestCurrentPayload":
|
||||
this.refreshPayload();
|
||||
break;
|
||||
case "RequestTelemetryPingList":
|
||||
this.sendTelemetryPingList();
|
||||
break;
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
* 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 LOGGER_NAME = "Toolkit.Telemetry";
|
||||
const LOGGER_PREFIX = "DataNotificationInfoBar::";
|
||||
|
||||
/**
|
||||
* Represents an info bar that shows a data submission notification.
|
||||
*/
|
||||
|
@ -21,7 +24,7 @@ var gDataNotificationInfoBar = {
|
|||
get _log() {
|
||||
let Log = Cu.import("resource://gre/modules/Log.jsm", {}).Log;
|
||||
delete this._log;
|
||||
return this._log = Log.repository.getLogger("Services.DataReporting.InfoBar");
|
||||
return this._log = Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX);
|
||||
},
|
||||
|
||||
init: function() {
|
||||
|
|
|
@ -3633,7 +3633,7 @@ const BrowserSearch = {
|
|||
loadSearchFromContext: function (terms) {
|
||||
let engine = BrowserSearch._loadSearch(terms, true, "contextmenu");
|
||||
if (engine) {
|
||||
BrowserSearch.recordSearchInHealthReport(engine, "contextmenu");
|
||||
BrowserSearch.recordSearchInTelemetry(engine, "contextmenu");
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -3657,10 +3657,26 @@ const BrowserSearch = {
|
|||
openUILinkIn(searchEnginesURL, where);
|
||||
},
|
||||
|
||||
_getSearchEngineId: function (engine) {
|
||||
if (!engine) {
|
||||
return "other";
|
||||
}
|
||||
|
||||
if (engine.identifier) {
|
||||
return engine.identifier;
|
||||
}
|
||||
|
||||
if (!("name" in engine) || engine.name === undefined) {
|
||||
return "other";
|
||||
}
|
||||
|
||||
return "other-" + engine.name;
|
||||
},
|
||||
|
||||
/**
|
||||
* Helper to record a search with Firefox Health Report.
|
||||
* Helper to record a search with Telemetry.
|
||||
*
|
||||
* FHR records only search counts and nothing pertaining to the search itself.
|
||||
* Telemetry records only search counts and nothing pertaining to the search itself.
|
||||
*
|
||||
* @param engine
|
||||
* (nsISearchEngine) The engine handling the search.
|
||||
|
@ -3672,45 +3688,7 @@ const BrowserSearch = {
|
|||
* the search was a suggested search, this indicates where the
|
||||
* item was in the suggestion list and how the user selected it.
|
||||
*/
|
||||
recordSearchInHealthReport: function (engine, source, selection) {
|
||||
BrowserUITelemetry.countSearchEvent(source, null, selection);
|
||||
this.recordSearchInTelemetry(engine, source);
|
||||
|
||||
let reporter = AppConstants.MOZ_SERVICES_HEALTHREPORT
|
||||
? Cc["@mozilla.org/datareporting/service;1"]
|
||||
.getService()
|
||||
.wrappedJSObject
|
||||
.healthReporter
|
||||
: null;
|
||||
|
||||
// This can happen if the FHR component of the data reporting service is
|
||||
// disabled. This is controlled by a pref that most will never use.
|
||||
if (!reporter) {
|
||||
return;
|
||||
}
|
||||
|
||||
reporter.onInit().then(function record() {
|
||||
try {
|
||||
reporter.getProvider("org.mozilla.searches").recordSearch(engine, source);
|
||||
} catch (ex) {
|
||||
Cu.reportError(ex);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_getSearchEngineId: function (engine) {
|
||||
if (!engine) {
|
||||
return "other";
|
||||
}
|
||||
|
||||
if (engine.identifier) {
|
||||
return engine.identifier;
|
||||
}
|
||||
|
||||
return "other-" + engine.name;
|
||||
},
|
||||
|
||||
recordSearchInTelemetry: function (engine, source) {
|
||||
recordSearchInTelemetry: function (engine, source, selection) {
|
||||
const SOURCES = [
|
||||
"abouthome",
|
||||
"contextmenu",
|
||||
|
@ -3719,6 +3697,8 @@ const BrowserSearch = {
|
|||
"urlbar",
|
||||
];
|
||||
|
||||
BrowserUITelemetry.countSearchEvent(source, null, selection);
|
||||
|
||||
if (SOURCES.indexOf(source) == -1) {
|
||||
Cu.reportError("Unknown source for search: " + source);
|
||||
return;
|
||||
|
|
|
@ -286,8 +286,6 @@ skip-if = e10s # Bug 1094510 - test hits the network in e10s mode only
|
|||
[browser_contextSearchTabPosition.js]
|
||||
skip-if = os == "mac" || e10s # bug 967013; e10s: bug 1094761 - test hits the network in e10s, causing next test to crash
|
||||
[browser_ctrlTab.js]
|
||||
[browser_datareporting_notification.js]
|
||||
skip-if = !datareporting
|
||||
[browser_datachoices_notification.js]
|
||||
skip-if = !datareporting
|
||||
[browser_devedition.js]
|
||||
|
@ -480,7 +478,6 @@ skip-if = os == "linux" # Bug 1073339 - Investigate autocomplete test unreliabil
|
|||
[browser_urlbarStop.js]
|
||||
[browser_urlbarTrimURLs.js]
|
||||
[browser_urlbar_autoFill_backspaced.js]
|
||||
[browser_urlbar_search_healthreport.js]
|
||||
[browser_urlbar_searchsettings.js]
|
||||
[browser_utilityOverlay.js]
|
||||
[browser_viewSourceInTabOnViewSource.js]
|
||||
|
|
|
@ -1,154 +1,143 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/
|
||||
*/
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
||||
"resource://gre/modules/Promise.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
"resource://gre/modules/Task.jsm");
|
||||
|
||||
const CHROME_BASE = "chrome://mochitests/content/browser/browser/base/content/test/general/";
|
||||
const HTTPS_BASE = "https://example.com/browser/browser/base/content/test/general/";
|
||||
|
||||
const TELEMETRY_LOG_PREF = "toolkit.telemetry.log.level";
|
||||
const telemetryOriginalLogPref = Preferences.get(TELEMETRY_LOG_PREF, null);
|
||||
|
||||
const originalReportUrl = Services.prefs.getCharPref("datareporting.healthreport.about.reportUrl");
|
||||
const originalReportUrlUnified = Services.prefs.getCharPref("datareporting.healthreport.about.reportUrlUnified");
|
||||
|
||||
registerCleanupFunction(function() {
|
||||
// Ensure we don't pollute prefs for next tests.
|
||||
if (telemetryOriginalLogPref) {
|
||||
Preferences.set(TELEMETRY_LOG_PREF, telemetryOriginalLogPref);
|
||||
} else {
|
||||
Preferences.reset(TELEMETRY_LOG_PREF);
|
||||
}
|
||||
|
||||
try {
|
||||
Services.prefs.setCharPref("datareporting.healthreport.about.reportUrl", originalReportUrl);
|
||||
Services.prefs.setCharPref("datareporting.healthreport.about.reportUrlUnified", originalReportUrlUnified);
|
||||
let policy = Cc["@mozilla.org/datareporting/service;1"]
|
||||
.getService(Ci.nsISupports)
|
||||
.wrappedJSObject
|
||||
.policy;
|
||||
policy.recordHealthReportUploadEnabled(true,
|
||||
"Resetting after tests.");
|
||||
} catch (ex) {}
|
||||
});
|
||||
|
||||
function fakeTelemetryNow(...args) {
|
||||
let date = new Date(...args);
|
||||
let scope = {};
|
||||
const modules = [
|
||||
Cu.import("resource://gre/modules/TelemetrySession.jsm", scope),
|
||||
Cu.import("resource://gre/modules/TelemetryEnvironment.jsm", scope),
|
||||
Cu.import("resource://gre/modules/TelemetryController.jsm", scope),
|
||||
];
|
||||
|
||||
for (let m of modules) {
|
||||
m.Policy.now = () => new Date(date);
|
||||
}
|
||||
|
||||
return date;
|
||||
}
|
||||
|
||||
function setupPingArchive() {
|
||||
let scope = {};
|
||||
Cu.import("resource://gre/modules/TelemetryController.jsm", scope);
|
||||
Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader)
|
||||
.loadSubScript(CHROME_BASE + "healthreport_pingData.js", scope);
|
||||
|
||||
for (let p of scope.TEST_PINGS) {
|
||||
fakeTelemetryNow(p.date);
|
||||
p.id = yield scope.TelemetryController.submitExternalPing(p.type, p.payload);
|
||||
}
|
||||
}
|
||||
|
||||
var gTests = [
|
||||
|
||||
{
|
||||
desc: "Test the remote commands",
|
||||
setup: Task.async(function*()
|
||||
{
|
||||
Preferences.set(TELEMETRY_LOG_PREF, "Trace");
|
||||
yield setupPingArchive();
|
||||
Preferences.set("datareporting.healthreport.about.reportUrl",
|
||||
HTTPS_BASE + "healthreport_testRemoteCommands.html");
|
||||
Preferences.set("datareporting.healthreport.about.reportUrlUnified",
|
||||
HTTPS_BASE + "healthreport_testRemoteCommands.html");
|
||||
}),
|
||||
run: function (iframe)
|
||||
{
|
||||
let deferred = Promise.defer();
|
||||
|
||||
let policy = Cc["@mozilla.org/datareporting/service;1"]
|
||||
.getService(Ci.nsISupports)
|
||||
.wrappedJSObject
|
||||
.policy;
|
||||
|
||||
let results = 0;
|
||||
try {
|
||||
iframe.contentWindow.addEventListener("FirefoxHealthReportTestResponse", function evtHandler(event) {
|
||||
let data = event.detail.data;
|
||||
if (data.type == "testResult") {
|
||||
ok(data.pass, data.info);
|
||||
results++;
|
||||
}
|
||||
else if (data.type == "testsComplete") {
|
||||
is(results, data.count, "Checking number of results received matches the number of tests that should have run");
|
||||
iframe.contentWindow.removeEventListener("FirefoxHealthReportTestResponse", evtHandler, true);
|
||||
deferred.resolve();
|
||||
}
|
||||
}, true);
|
||||
|
||||
} catch(e) {
|
||||
ok(false, "Failed to get all commands");
|
||||
deferred.reject();
|
||||
}
|
||||
return deferred.promise;
|
||||
}
|
||||
},
|
||||
|
||||
]; // gTests
|
||||
|
||||
function test()
|
||||
{
|
||||
waitForExplicitFinish();
|
||||
|
||||
// xxxmpc leaving this here until we resolve bug 854038 and bug 854060
|
||||
requestLongerTimeout(10);
|
||||
|
||||
Task.spawn(function () {
|
||||
for (let test of gTests) {
|
||||
info(test.desc);
|
||||
yield test.setup();
|
||||
|
||||
let iframe = yield promiseNewTabLoadEvent("about:healthreport");
|
||||
|
||||
yield test.run(iframe);
|
||||
|
||||
gBrowser.removeCurrentTab();
|
||||
}
|
||||
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
function promiseNewTabLoadEvent(aUrl, aEventType="load")
|
||||
{
|
||||
let deferred = Promise.defer();
|
||||
let tab = gBrowser.selectedTab = gBrowser.addTab(aUrl);
|
||||
tab.linkedBrowser.addEventListener(aEventType, function load(event) {
|
||||
tab.linkedBrowser.removeEventListener(aEventType, load, true);
|
||||
let iframe = tab.linkedBrowser.contentDocument.getElementById("remote-report");
|
||||
iframe.addEventListener("load", function frameLoad(e) {
|
||||
if (iframe.contentWindow.location.href == "about:blank" ||
|
||||
e.target != iframe) {
|
||||
return;
|
||||
}
|
||||
iframe.removeEventListener("load", frameLoad, false);
|
||||
deferred.resolve(iframe);
|
||||
}, false);
|
||||
}, true);
|
||||
return deferred.promise;
|
||||
}
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/
|
||||
*/
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
|
||||
"resource://gre/modules/Promise.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||
"resource://gre/modules/Task.jsm");
|
||||
|
||||
const CHROME_BASE = "chrome://mochitests/content/browser/browser/base/content/test/general/";
|
||||
const HTTPS_BASE = "https://example.com/browser/browser/base/content/test/general/";
|
||||
|
||||
const TELEMETRY_LOG_PREF = "toolkit.telemetry.log.level";
|
||||
const telemetryOriginalLogPref = Preferences.get(TELEMETRY_LOG_PREF, null);
|
||||
|
||||
const originalReportUrl = Services.prefs.getCharPref("datareporting.healthreport.about.reportUrl");
|
||||
const originalReportUrlUnified = Services.prefs.getCharPref("datareporting.healthreport.about.reportUrlUnified");
|
||||
|
||||
registerCleanupFunction(function() {
|
||||
// Ensure we don't pollute prefs for next tests.
|
||||
if (telemetryOriginalLogPref) {
|
||||
Preferences.set(TELEMETRY_LOG_PREF, telemetryOriginalLogPref);
|
||||
} else {
|
||||
Preferences.reset(TELEMETRY_LOG_PREF);
|
||||
}
|
||||
|
||||
try {
|
||||
Services.prefs.setCharPref("datareporting.healthreport.about.reportUrl", originalReportUrl);
|
||||
Services.prefs.setCharPref("datareporting.healthreport.about.reportUrlUnified", originalReportUrlUnified);
|
||||
Services.prefs.setBoolPref("datareporting.healthreport.uploadEnabled", true);
|
||||
} catch (ex) {}
|
||||
});
|
||||
|
||||
function fakeTelemetryNow(...args) {
|
||||
let date = new Date(...args);
|
||||
let scope = {};
|
||||
const modules = [
|
||||
Cu.import("resource://gre/modules/TelemetrySession.jsm", scope),
|
||||
Cu.import("resource://gre/modules/TelemetryEnvironment.jsm", scope),
|
||||
Cu.import("resource://gre/modules/TelemetryController.jsm", scope),
|
||||
];
|
||||
|
||||
for (let m of modules) {
|
||||
m.Policy.now = () => new Date(date);
|
||||
}
|
||||
|
||||
return date;
|
||||
}
|
||||
|
||||
function setupPingArchive() {
|
||||
let scope = {};
|
||||
Cu.import("resource://gre/modules/TelemetryController.jsm", scope);
|
||||
Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader)
|
||||
.loadSubScript(CHROME_BASE + "healthreport_pingData.js", scope);
|
||||
|
||||
for (let p of scope.TEST_PINGS) {
|
||||
fakeTelemetryNow(p.date);
|
||||
p.id = yield scope.TelemetryController.submitExternalPing(p.type, p.payload);
|
||||
}
|
||||
}
|
||||
|
||||
var gTests = [
|
||||
|
||||
{
|
||||
desc: "Test the remote commands",
|
||||
setup: Task.async(function*()
|
||||
{
|
||||
Preferences.set(TELEMETRY_LOG_PREF, "Trace");
|
||||
yield setupPingArchive();
|
||||
Preferences.set("datareporting.healthreport.about.reportUrl",
|
||||
HTTPS_BASE + "healthreport_testRemoteCommands.html");
|
||||
Preferences.set("datareporting.healthreport.about.reportUrlUnified",
|
||||
HTTPS_BASE + "healthreport_testRemoteCommands.html");
|
||||
}),
|
||||
run: function (iframe)
|
||||
{
|
||||
let deferred = Promise.defer();
|
||||
let results = 0;
|
||||
try {
|
||||
iframe.contentWindow.addEventListener("FirefoxHealthReportTestResponse", function evtHandler(event) {
|
||||
let data = event.detail.data;
|
||||
if (data.type == "testResult") {
|
||||
ok(data.pass, data.info);
|
||||
results++;
|
||||
}
|
||||
else if (data.type == "testsComplete") {
|
||||
is(results, data.count, "Checking number of results received matches the number of tests that should have run");
|
||||
iframe.contentWindow.removeEventListener("FirefoxHealthReportTestResponse", evtHandler, true);
|
||||
deferred.resolve();
|
||||
}
|
||||
}, true);
|
||||
|
||||
} catch(e) {
|
||||
ok(false, "Failed to get all commands");
|
||||
deferred.reject();
|
||||
}
|
||||
return deferred.promise;
|
||||
}
|
||||
},
|
||||
|
||||
]; // gTests
|
||||
|
||||
function test()
|
||||
{
|
||||
waitForExplicitFinish();
|
||||
|
||||
// xxxmpc leaving this here until we resolve bug 854038 and bug 854060
|
||||
requestLongerTimeout(10);
|
||||
|
||||
Task.spawn(function () {
|
||||
for (let test of gTests) {
|
||||
info(test.desc);
|
||||
yield test.setup();
|
||||
|
||||
let iframe = yield promiseNewTabLoadEvent("about:healthreport");
|
||||
|
||||
yield test.run(iframe);
|
||||
|
||||
gBrowser.removeCurrentTab();
|
||||
}
|
||||
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
function promiseNewTabLoadEvent(aUrl, aEventType="load")
|
||||
{
|
||||
let deferred = Promise.defer();
|
||||
let tab = gBrowser.selectedTab = gBrowser.addTab(aUrl);
|
||||
tab.linkedBrowser.addEventListener(aEventType, function load(event) {
|
||||
tab.linkedBrowser.removeEventListener(aEventType, load, true);
|
||||
let iframe = tab.linkedBrowser.contentDocument.getElementById("remote-report");
|
||||
iframe.addEventListener("load", function frameLoad(e) {
|
||||
if (iframe.contentWindow.location.href == "about:blank" ||
|
||||
e.target != iframe) {
|
||||
return;
|
||||
}
|
||||
iframe.removeEventListener("load", frameLoad, false);
|
||||
deferred.resolve(iframe);
|
||||
}, false);
|
||||
}, true);
|
||||
return deferred.promise;
|
||||
}
|
||||
|
|
|
@ -78,24 +78,11 @@ var gTests = [
|
|||
}
|
||||
},
|
||||
|
||||
// Disabled on Linux for intermittent issues with FHR, see Bug 945667.
|
||||
{
|
||||
desc: "Check that performing a search fires a search event and records to " +
|
||||
"Firefox Health Report.",
|
||||
"Telemetry.",
|
||||
setup: function () { },
|
||||
run: function* () {
|
||||
// Skip this test on Linux.
|
||||
if (navigator.platform.indexOf("Linux") == 0) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
try {
|
||||
let cm = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
|
||||
cm.getCategoryEntry("healthreport-js-provider-default", "SearchesProvider");
|
||||
} catch (ex) {
|
||||
// Health Report disabled, or no SearchesProvider.
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
let engine = yield promiseNewEngine("searchSuggestionEngine.xml");
|
||||
// Make this actually work in healthreport by giving it an ID:
|
||||
|
@ -113,23 +100,32 @@ var gTests = [
|
|||
is(engine.name, engineName, "Engine name in DOM should match engine we just added");
|
||||
|
||||
// Get the current number of recorded searches.
|
||||
let searchStr = "a search";
|
||||
getNumberOfSearchesInFHR(engineName, "abouthome").then(num => {
|
||||
numSearchesBefore = num;
|
||||
let histogramKey = engine.identifier + ".abouthome";
|
||||
try {
|
||||
let hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
|
||||
if (histogramKey in hs) {
|
||||
numSearchesBefore = hs[histogramKey].sum;
|
||||
}
|
||||
} catch (ex) {
|
||||
// No searches performed yet, not a problem, |numSearchesBefore| is 0.
|
||||
}
|
||||
|
||||
info("Perform a search.");
|
||||
doc.getElementById("searchText").value = searchStr;
|
||||
doc.getElementById("searchSubmit").click();
|
||||
});
|
||||
// Perform a search to increase the SEARCH_COUNT histogram.
|
||||
let searchStr = "a search";
|
||||
info("Perform a search.");
|
||||
doc.getElementById("searchText").value = searchStr;
|
||||
doc.getElementById("searchSubmit").click();
|
||||
|
||||
let expectedURL = Services.search.currentEngine.
|
||||
getSubmission(searchStr, null, "homepage").
|
||||
uri.spec;
|
||||
let loadPromise = waitForDocLoadAndStopIt(expectedURL).then(() => {
|
||||
getNumberOfSearchesInFHR(engineName, "abouthome").then(num => {
|
||||
is(num, numSearchesBefore + 1, "One more search recorded.");
|
||||
searchEventDeferred.resolve();
|
||||
});
|
||||
// Make sure the SEARCH_COUNTS histogram has the right key and count.
|
||||
let hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
|
||||
Assert.ok(histogramKey in hs, "histogram with key should be recorded");
|
||||
Assert.equal(hs[histogramKey].sum, numSearchesBefore + 1,
|
||||
"histogram sum should be incremented");
|
||||
searchEventDeferred.resolve();
|
||||
});
|
||||
|
||||
try {
|
||||
|
|
|
@ -2,15 +2,34 @@
|
|||
* 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 test() {
|
||||
waitForExplicitFinish();
|
||||
add_task(function* test() {
|
||||
|
||||
// Will need to be changed if Google isn't the default search engine.
|
||||
// Note: geoSpecificDefaults are disabled for mochitests, so this is the
|
||||
// non-US en-US default.
|
||||
let histogramKey = "google.contextmenu";
|
||||
let numSearchesBefore = 0;
|
||||
try {
|
||||
let hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
|
||||
if (histogramKey in hs) {
|
||||
numSearchesBefore = hs[histogramKey].sum;
|
||||
}
|
||||
} catch (ex) {
|
||||
// No searches performed yet, not a problem, |numSearchesBefore| is 0.
|
||||
}
|
||||
|
||||
let tabs = [];
|
||||
let tabsLoadedDeferred = Promise.defer();
|
||||
|
||||
function tabAdded(event) {
|
||||
let tab = event.target;
|
||||
tabs.push(tab);
|
||||
}
|
||||
|
||||
let tabs = [];
|
||||
// We wait for the blank tab and the two context searches tabs to open.
|
||||
if (tabs.length == 3) {
|
||||
tabsLoadedDeferred.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
let container = gBrowser.tabContainer;
|
||||
container.addEventListener("TabOpen", tabAdded, false);
|
||||
|
@ -19,6 +38,9 @@ function test() {
|
|||
BrowserSearch.loadSearchFromContext("mozilla");
|
||||
BrowserSearch.loadSearchFromContext("firefox");
|
||||
|
||||
// Wait for all the tabs to open.
|
||||
yield tabsLoadedDeferred.promise;
|
||||
|
||||
is(tabs[0], gBrowser.tabs[3], "blank tab has been pushed to the end");
|
||||
is(tabs[1], gBrowser.tabs[1], "first search tab opens next to the current tab");
|
||||
is(tabs[2], gBrowser.tabs[2], "second search tab opens next to the first search tab");
|
||||
|
@ -26,45 +48,9 @@ function test() {
|
|||
container.removeEventListener("TabOpen", tabAdded, false);
|
||||
tabs.forEach(gBrowser.removeTab, gBrowser);
|
||||
|
||||
try {
|
||||
let cm = Components.classes["@mozilla.org/categorymanager;1"]
|
||||
.getService(Components.interfaces.nsICategoryManager);
|
||||
cm.getCategoryEntry("healthreport-js-provider-default", "SearchesProvider");
|
||||
} catch (ex) {
|
||||
// Health Report disabled, or no SearchesProvider.
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
let reporter = Components.classes["@mozilla.org/datareporting/service;1"]
|
||||
.getService()
|
||||
.wrappedJSObject
|
||||
.healthReporter;
|
||||
|
||||
// reporter should always be available in automation.
|
||||
ok(reporter, "Health Reporter available.");
|
||||
reporter.onInit().then(function onInit() {
|
||||
let provider = reporter.getProvider("org.mozilla.searches");
|
||||
ok(provider, "Searches provider is available.");
|
||||
|
||||
let m = provider.getMeasurement("counts", 3);
|
||||
m.getValues().then(function onValues(data) {
|
||||
let now = new Date();
|
||||
ok(data.days.hasDay(now), "Have data for today.");
|
||||
let day = data.days.getDay(now);
|
||||
|
||||
// Will need to be changed if Google isn't the default search engine.
|
||||
// Note: geoSpecificDefaults are disabled for mochitests, so this is the
|
||||
// non-US en-US default.
|
||||
let defaultProviderID = "google";
|
||||
let field = defaultProviderID + ".contextmenu";
|
||||
ok(day.has(field), "Have search recorded for context menu.");
|
||||
|
||||
// If any other mochitests perform a context menu search, this will fail.
|
||||
// The solution will be to look up count at test start and ensure it is
|
||||
// incremented by two.
|
||||
is(day.get(field), 2, "2 searches recorded in FHR.");
|
||||
finish();
|
||||
});
|
||||
});
|
||||
}
|
||||
// Make sure that the context searches are correctly recorded.
|
||||
let hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
|
||||
Assert.ok(histogramKey in hs, "The histogram must contain the correct key");
|
||||
Assert.equal(hs[histogramKey].sum, numSearchesBefore + 2,
|
||||
"The histogram must contain the correct search count");
|
||||
});
|
||||
|
|
|
@ -10,13 +10,7 @@ var Preferences = Cu.import("resource://gre/modules/Preferences.jsm", {}).Prefer
|
|||
var TelemetryReportingPolicy =
|
||||
Cu.import("resource://gre/modules/TelemetryReportingPolicy.jsm", {}).TelemetryReportingPolicy;
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "gDatareportingService",
|
||||
() => Cc["@mozilla.org/datareporting/service;1"]
|
||||
.getService(Ci.nsISupports)
|
||||
.wrappedJSObject);
|
||||
|
||||
const PREF_BRANCH = "datareporting.policy.";
|
||||
const PREF_DRS_ENABLED = "datareporting.healthreport.service.enabled";
|
||||
const PREF_BYPASS_NOTIFICATION = PREF_BRANCH + "dataSubmissionPolicyBypassNotification";
|
||||
const PREF_CURRENT_POLICY_VERSION = PREF_BRANCH + "currentPolicyVersion";
|
||||
const PREF_ACCEPTED_POLICY_VERSION = PREF_BRANCH + "dataSubmissionPolicyAcceptedVersion";
|
||||
|
@ -103,31 +97,21 @@ var checkInfobarButton = Task.async(function* (aNotification) {
|
|||
});
|
||||
|
||||
add_task(function* setup(){
|
||||
const drsEnabled = Preferences.get(PREF_DRS_ENABLED, true);
|
||||
const bypassNotification = Preferences.get(PREF_BYPASS_NOTIFICATION, true);
|
||||
const currentPolicyVersion = Preferences.get(PREF_CURRENT_POLICY_VERSION, 1);
|
||||
|
||||
// Register a cleanup function to reset our preferences.
|
||||
registerCleanupFunction(() => {
|
||||
Preferences.set(PREF_DRS_ENABLED, drsEnabled);
|
||||
Preferences.set(PREF_BYPASS_NOTIFICATION, bypassNotification);
|
||||
Preferences.set(PREF_CURRENT_POLICY_VERSION, currentPolicyVersion);
|
||||
|
||||
// Start polling again.
|
||||
gDatareportingService.policy.startPolling();
|
||||
|
||||
return closeAllNotifications();
|
||||
});
|
||||
|
||||
// Disable Healthreport/Data reporting service.
|
||||
Preferences.set(PREF_DRS_ENABLED, false);
|
||||
// Don't skip the infobar visualisation.
|
||||
Preferences.set(PREF_BYPASS_NOTIFICATION, false);
|
||||
// Set the current policy version.
|
||||
Preferences.set(PREF_CURRENT_POLICY_VERSION, TEST_POLICY_VERSION);
|
||||
|
||||
// Stop the polling to make sure no policy gets displayed by FHR.
|
||||
gDatareportingService.policy.stopPolling();
|
||||
});
|
||||
|
||||
function clearAcceptedPolicy() {
|
||||
|
|
|
@ -1,213 +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/. */
|
||||
|
||||
var originalPolicy = null;
|
||||
|
||||
/**
|
||||
* Display a datareporting notification to the user.
|
||||
*
|
||||
* @param {String} name
|
||||
*/
|
||||
function sendNotifyRequest(name) {
|
||||
let ns = {};
|
||||
Cu.import("resource://gre/modules/services/datareporting/policy.jsm", ns);
|
||||
Cu.import("resource://gre/modules/Preferences.jsm", ns);
|
||||
|
||||
let service = Cc["@mozilla.org/datareporting/service;1"]
|
||||
.getService(Ci.nsISupports)
|
||||
.wrappedJSObject;
|
||||
ok(service.healthReporter, "Health Reporter instance is available.");
|
||||
|
||||
Cu.import("resource://gre/modules/Promise.jsm", ns);
|
||||
let deferred = ns.Promise.defer();
|
||||
|
||||
if (!originalPolicy) {
|
||||
originalPolicy = service.policy;
|
||||
}
|
||||
|
||||
let policyPrefs = new ns.Preferences("testing." + name + ".");
|
||||
ok(service._prefs, "Health Reporter prefs are available.");
|
||||
let hrPrefs = service._prefs;
|
||||
|
||||
let policy = new ns.DataReportingPolicy(policyPrefs, hrPrefs, service);
|
||||
policy.dataSubmissionPolicyBypassNotification = false;
|
||||
service.policy = policy;
|
||||
policy.firstRunDate = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||
|
||||
service.healthReporter.onInit().then(function onSuccess () {
|
||||
is(policy.ensureUserNotified(), false, "User not notified about data policy on init.");
|
||||
ok(policy._userNotifyPromise, "_userNotifyPromise defined.");
|
||||
policy._userNotifyPromise.then(
|
||||
deferred.resolve.bind(deferred),
|
||||
deferred.reject.bind(deferred)
|
||||
);
|
||||
}.bind(this), deferred.reject.bind(deferred));
|
||||
|
||||
return [policy, deferred.promise];
|
||||
}
|
||||
|
||||
var dumpAppender, rootLogger;
|
||||
|
||||
function test() {
|
||||
registerCleanupFunction(cleanup);
|
||||
waitForExplicitFinish();
|
||||
|
||||
let ns = {};
|
||||
Components.utils.import("resource://gre/modules/Log.jsm", ns);
|
||||
rootLogger = ns.Log.repository.rootLogger;
|
||||
dumpAppender = new ns.Log.DumpAppender();
|
||||
dumpAppender.level = ns.Log.Level.All;
|
||||
rootLogger.addAppender(dumpAppender);
|
||||
|
||||
closeAllNotifications().then(function onSuccess () {
|
||||
let notification = document.getElementById("global-notificationbox");
|
||||
|
||||
notification.addEventListener("AlertActive", function active() {
|
||||
notification.removeEventListener("AlertActive", active, true);
|
||||
is(notification.allNotifications.length, 1, "Notification Displayed.");
|
||||
|
||||
executeSoon(function afterNotification() {
|
||||
waitForNotificationClose(notification.currentNotification, function onClose() {
|
||||
is(notification.allNotifications.length, 0, "No notifications remain.");
|
||||
is(policy.dataSubmissionPolicyAcceptedVersion, 1, "Version pref set.");
|
||||
ok(policy.dataSubmissionPolicyNotifiedDate.getTime() > -1, "Date pref set.");
|
||||
test_multiple_windows();
|
||||
});
|
||||
notification.currentNotification.close();
|
||||
});
|
||||
}, true);
|
||||
|
||||
let [policy, promise] = sendNotifyRequest("single_window_notified");
|
||||
|
||||
is(policy.dataSubmissionPolicyAcceptedVersion, 0, "No version should be set on init.");
|
||||
is(policy.dataSubmissionPolicyNotifiedDate.getTime(), 0, "No date should be set on init.");
|
||||
is(policy.userNotifiedOfCurrentPolicy, false, "User not notified about datareporting policy.");
|
||||
|
||||
promise.then(function () {
|
||||
is(policy.dataSubmissionPolicyAcceptedVersion, 1, "Policy version set.");
|
||||
is(policy.dataSubmissionPolicyNotifiedDate.getTime() > 0, true, "Policy date set.");
|
||||
is(policy.userNotifiedOfCurrentPolicy, true, "User notified about datareporting policy.");
|
||||
}.bind(this), function (err) {
|
||||
throw err;
|
||||
});
|
||||
|
||||
}.bind(this), function onError (err) {
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
function test_multiple_windows() {
|
||||
// Ensure we see the notification on all windows and that action on one window
|
||||
// results in dismiss on every window.
|
||||
let window2 = OpenBrowserWindow();
|
||||
whenDelayedStartupFinished(window2, function onWindow() {
|
||||
let notification1 = document.getElementById("global-notificationbox");
|
||||
let notification2 = window2.document.getElementById("global-notificationbox");
|
||||
ok(notification2, "2nd window has a global notification box.");
|
||||
|
||||
let [policy, promise] = sendNotifyRequest("multiple_window_behavior");
|
||||
let displayCount = 0;
|
||||
let prefWindowOpened = false;
|
||||
let mutationObserversRemoved = false;
|
||||
|
||||
function onAlertDisplayed() {
|
||||
displayCount++;
|
||||
|
||||
if (displayCount != 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
ok(true, "Data reporting info bar displayed on all open windows.");
|
||||
|
||||
// We register two independent observers and we need both to clean up
|
||||
// properly. This handles gating for test completion.
|
||||
function maybeFinish() {
|
||||
if (!prefWindowOpened) {
|
||||
dump("Not finishing test yet because pref pane hasn't yet appeared.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mutationObserversRemoved) {
|
||||
dump("Not finishing test yet because mutation observers haven't been removed yet.\n");
|
||||
return;
|
||||
}
|
||||
|
||||
window2.close();
|
||||
|
||||
dump("Finishing multiple window test.\n");
|
||||
rootLogger.removeAppender(dumpAppender);
|
||||
dumpAppender = null;
|
||||
rootLogger = null;
|
||||
finish();
|
||||
}
|
||||
let closeCount = 0;
|
||||
|
||||
function onAlertClose() {
|
||||
closeCount++;
|
||||
|
||||
if (closeCount != 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
ok(true, "Closing info bar on one window closed them on all.");
|
||||
is(policy.userNotifiedOfCurrentPolicy, true, "Data submission policy accepted.");
|
||||
|
||||
is(notification1.allNotifications.length, 0, "No notifications remain on main window.");
|
||||
is(notification2.allNotifications.length, 0, "No notifications remain on 2nd window.");
|
||||
|
||||
mutationObserversRemoved = true;
|
||||
maybeFinish();
|
||||
}
|
||||
|
||||
waitForNotificationClose(notification1.currentNotification, onAlertClose);
|
||||
waitForNotificationClose(notification2.currentNotification, onAlertClose);
|
||||
|
||||
// While we're here, we dual purpose this test to check that pressing the
|
||||
// button does the right thing.
|
||||
let buttons = notification2.currentNotification.getElementsByTagName("button");
|
||||
is(buttons.length, 1, "There is 1 button in the data reporting notification.");
|
||||
let button = buttons[0];
|
||||
|
||||
// Add an observer to ensure the "advanced" pane opened (but don't bother
|
||||
// closing it - we close the entire window when done.)
|
||||
Services.obs.addObserver(function observer(prefWin, topic, data) {
|
||||
Services.obs.removeObserver(observer, "advanced-pane-loaded");
|
||||
|
||||
ok(true, "Advanced preferences opened on info bar button press.");
|
||||
executeSoon(function soon() {
|
||||
prefWindowOpened = true;
|
||||
maybeFinish();
|
||||
});
|
||||
}, "advanced-pane-loaded", false);
|
||||
|
||||
button.click();
|
||||
}
|
||||
|
||||
notification1.addEventListener("AlertActive", function active1() {
|
||||
notification1.removeEventListener("AlertActive", active1, true);
|
||||
executeSoon(onAlertDisplayed);
|
||||
}, true);
|
||||
|
||||
notification2.addEventListener("AlertActive", function active2() {
|
||||
notification2.removeEventListener("AlertActive", active2, true);
|
||||
executeSoon(onAlertDisplayed);
|
||||
}, true);
|
||||
|
||||
promise.then(null, function onError(err) {
|
||||
throw err;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function cleanup () {
|
||||
// In case some test fails.
|
||||
if (originalPolicy) {
|
||||
let service = Cc["@mozilla.org/datareporting/service;1"]
|
||||
.getService(Ci.nsISupports)
|
||||
.wrappedJSObject;
|
||||
service.policy = originalPolicy;
|
||||
}
|
||||
|
||||
return closeAllNotifications();
|
||||
}
|
|
@ -103,7 +103,6 @@ function* compareCounts(clickCallback) {
|
|||
// FHR -- first make sure the engine has an identifier so that FHR is happy.
|
||||
Object.defineProperty(engine.wrappedJSObject, "identifier",
|
||||
{ value: engineID });
|
||||
let fhrCount = yield getNumberOfSearchesInFHR(engine.name, "urlbar");
|
||||
|
||||
gURLBar.focus();
|
||||
yield clickCallback();
|
||||
|
@ -126,10 +125,6 @@ function* compareCounts(clickCallback) {
|
|||
Assert.ok(histogramKey in snapshot, "histogram with key should be recorded");
|
||||
Assert.equal(snapshot[histogramKey].sum, histogramCount + 1,
|
||||
"histogram sum should be incremented");
|
||||
|
||||
// FHR
|
||||
let newFHRCount = yield getNumberOfSearchesInFHR(engine.name, "urlbar");
|
||||
Assert.equal(newFHRCount, fhrCount + 1, "should be recorded in FHR");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
add_task(function* test_healthreport_search_recording() {
|
||||
try {
|
||||
let cm = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
|
||||
cm.getCategoryEntry("healthreport-js-provider-default", "SearchesProvider");
|
||||
} catch (ex) {
|
||||
// Health Report disabled, or no SearchesProvider.
|
||||
ok(true, "Firefox Health Report is not enabled.");
|
||||
return;
|
||||
}
|
||||
|
||||
let reporter = Cc["@mozilla.org/datareporting/service;1"]
|
||||
.getService()
|
||||
.wrappedJSObject
|
||||
.healthReporter;
|
||||
ok(reporter, "Health Reporter available.");
|
||||
yield reporter.onInit();
|
||||
let provider = reporter.getProvider("org.mozilla.searches");
|
||||
ok(provider, "Searches provider is available.");
|
||||
let m = provider.getMeasurement("counts", 3);
|
||||
|
||||
let data = yield m.getValues();
|
||||
let now = new Date();
|
||||
let oldCount = 0;
|
||||
|
||||
// This will to be need changed if default search engine is not Google.
|
||||
// Note: geoSpecificDefaults are disabled for mochitests, so this is the
|
||||
// non-US en-US default.
|
||||
let defaultEngineID = "google";
|
||||
|
||||
let field = defaultEngineID + ".urlbar";
|
||||
|
||||
if (data.days.hasDay(now)) {
|
||||
let day = data.days.getDay(now);
|
||||
if (day.has(field)) {
|
||||
oldCount = day.get(field);
|
||||
}
|
||||
}
|
||||
|
||||
let tab = gBrowser.addTab("about:blank");
|
||||
yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
|
||||
gBrowser.selectedTab = tab;
|
||||
|
||||
let searchStr = "firefox health report";
|
||||
let expectedURL = Services.search.currentEngine.
|
||||
getSubmission(searchStr, "", "keyword").uri.spec;
|
||||
|
||||
// Expect the search URL to load but stop it as soon as it starts.
|
||||
let docLoadPromise = waitForDocLoadAndStopIt(expectedURL);
|
||||
|
||||
// Trigger the search.
|
||||
gURLBar.value = searchStr;
|
||||
gURLBar.handleCommand();
|
||||
|
||||
yield docLoadPromise;
|
||||
|
||||
data = yield m.getValues();
|
||||
ok(data.days.hasDay(now), "We have a search measurement for today.");
|
||||
let day = data.days.getDay(now);
|
||||
ok(day.has(field), "Have a search count for the urlbar.");
|
||||
let newCount = day.get(field);
|
||||
is(newCount, oldCount + 1, "We recorded one new search.");
|
||||
|
||||
// We should record the default search engine if Telemetry is enabled.
|
||||
let oldTelemetry = Services.prefs.getBoolPref("toolkit.telemetry.enabled");
|
||||
Services.prefs.setBoolPref("toolkit.telemetry.enabled", true);
|
||||
|
||||
m = provider.getMeasurement("engines", 2);
|
||||
yield provider.collectDailyData();
|
||||
data = yield m.getValues();
|
||||
|
||||
ok(data.days.hasDay(now), "Have engines data when Telemetry is enabled.");
|
||||
day = data.days.getDay(now);
|
||||
ok(day.has("default"), "We have default engine data.");
|
||||
is(day.get("default"), defaultEngineID, "The default engine is reported properly.");
|
||||
|
||||
// Restore.
|
||||
Services.prefs.setBoolPref("toolkit.telemetry.enabled", oldTelemetry);
|
||||
|
||||
gBrowser.removeTab(tab);
|
||||
});
|
|
@ -1202,61 +1202,3 @@ function promiseCrashReport(expectedExtra) {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the number of searches recorded in FHR for the current day.
|
||||
*
|
||||
* @param aEngineName
|
||||
* name of the setup search engine.
|
||||
* @param aSource
|
||||
* The FHR "source" name for the search, like "abouthome" or "urlbar".
|
||||
*
|
||||
* @return {Promise} Returns a promise resolving to the number of searches.
|
||||
*/
|
||||
function getNumberOfSearchesInFHR(aEngineName, aSource) {
|
||||
let reporter = Components.classes["@mozilla.org/datareporting/service;1"]
|
||||
.getService()
|
||||
.wrappedJSObject
|
||||
.healthReporter;
|
||||
ok(reporter, "Health Reporter instance available.");
|
||||
|
||||
return reporter.onInit().then(function onInit() {
|
||||
let provider = reporter.getProvider("org.mozilla.searches");
|
||||
ok(provider, "Searches provider is available.");
|
||||
|
||||
let m = provider.getMeasurement("counts", 3);
|
||||
return m.getValues().then(data => {
|
||||
let now = new Date();
|
||||
let yday = new Date(now);
|
||||
yday.setDate(yday.getDate() - 1);
|
||||
|
||||
// Add the number of searches recorded yesterday to the number of searches
|
||||
// recorded today. This makes the test not fail intermittently when it is
|
||||
// run at midnight and we accidentally compare the number of searches from
|
||||
// different days. Tests are always run with an empty profile so there
|
||||
// are no searches from yesterday, normally. Should the test happen to run
|
||||
// past midnight we make sure to count them in as well.
|
||||
return getNumberOfSearchesInFHRByDate(aEngineName, aSource, data, now) +
|
||||
getNumberOfSearchesInFHRByDate(aEngineName, aSource, data, yday);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for getNumberOfSearchesInFHR. You probably don't want to call this
|
||||
* directly.
|
||||
*/
|
||||
function getNumberOfSearchesInFHRByDate(aEngineName, aSource, aData, aDate) {
|
||||
if (aData.days.hasDay(aDate)) {
|
||||
let id = Services.search.getEngineByName(aEngineName).identifier;
|
||||
|
||||
let day = aData.days.getDay(aDate);
|
||||
let field = id + "." + aSource;
|
||||
|
||||
if (day.has(field)) {
|
||||
return day.get(field) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
return 0; // No records found.
|
||||
}
|
||||
|
|
|
@ -7,35 +7,14 @@
|
|||
<script type="application/javascript;version=1.7">
|
||||
|
||||
function init() {
|
||||
window.addEventListener("message", function process(e) {
|
||||
// The init function of abouthealth.js schedules an initial payload event,
|
||||
// which will be sent after the payload data has been collected. This extra
|
||||
// event can cause unexpected successes/failures in this test, so we wait
|
||||
// for the extra event to arrive here before progressing with the actual
|
||||
// test.
|
||||
if (e.data.type == "payload") {
|
||||
window.removeEventListener("message", process, false);
|
||||
|
||||
window.addEventListener("message", doTest, false);
|
||||
doTest();
|
||||
}
|
||||
}, false);
|
||||
window.addEventListener("message", doTest, false);
|
||||
doTest();
|
||||
}
|
||||
|
||||
function checkSubmissionValue(payload, expectedValue) {
|
||||
return payload.enabled == expectedValue;
|
||||
}
|
||||
|
||||
function validatePayload(payload) {
|
||||
payload = JSON.parse(payload);
|
||||
|
||||
// xxxmpc - this is some pretty low-bar validation, but we have plenty of tests of that API elsewhere
|
||||
if (!payload.thisPingDate)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function isArray(arg) {
|
||||
return Object.prototype.toString.call(arg) === '[object Array]';
|
||||
}
|
||||
|
@ -141,11 +120,11 @@ var tests = [
|
|||
},
|
||||
},
|
||||
{
|
||||
info: "Verifying we can get a payload while submission is disabled",
|
||||
event: "RequestCurrentPayload",
|
||||
payloadType: "payload",
|
||||
info: "Verifying that we can get the current ping data while submission is disabled",
|
||||
event: "RequestCurrentPingData",
|
||||
payloadType: "telemetry-current-ping-data",
|
||||
validateResponse: function(payload) {
|
||||
return validatePayload(payload);
|
||||
return validateCurrentTelemetryPingData(payload);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -164,14 +143,6 @@ var tests = [
|
|||
return checkSubmissionValue(payload, true);
|
||||
},
|
||||
},
|
||||
{
|
||||
info: "Verifying we can get a payload after re-enabling",
|
||||
event: "RequestCurrentPayload",
|
||||
payloadType: "payload",
|
||||
validateResponse: function(payload) {
|
||||
return validatePayload(payload);
|
||||
},
|
||||
},
|
||||
{
|
||||
info: "Verifying that we can get the current Telemetry environment data",
|
||||
event: "RequestCurrentEnvironment",
|
||||
|
|
|
@ -454,7 +454,7 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|||
<body><![CDATA[
|
||||
let engine =
|
||||
Services.search.getEngineByName(action.params.engineName);
|
||||
BrowserSearch.recordSearchInHealthReport(engine, "urlbar");
|
||||
BrowserSearch.recordSearchInTelemetry(engine, "urlbar");
|
||||
let query = action.params.searchSuggestion ||
|
||||
action.params.searchQuery;
|
||||
let submission = engine.getSubmission(query, null, "keyword");
|
||||
|
@ -948,15 +948,12 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|||
|
||||
<method name="handleDelete">
|
||||
<body><![CDATA[
|
||||
// When UnifiedComplete is enabled, we arrange for the popup to
|
||||
// always have a "special" first item that's always selected. The
|
||||
// autocomplete controller's handleDelete() implementation will
|
||||
// remove the selected entry from the popup in that case.
|
||||
// So when our first special item is selected, we call handleText
|
||||
// instead so it acts as a delete on the text value instead of
|
||||
// removing that item.
|
||||
if (Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete") &&
|
||||
this.popup.selectedIndex == 0) {
|
||||
// If the heuristic result is selected, then the autocomplete
|
||||
// controller's handleDelete implementation will remove it, which is
|
||||
// not what we want. So in that case, call handleText so it acts as
|
||||
// a backspace on the text value instead of removing the result.
|
||||
if (this.popup.selectedIndex == 0 &&
|
||||
this.popup._isFirstResultHeuristic) {
|
||||
return this.mController.handleText();
|
||||
}
|
||||
return this.mController.handleDelete();
|
||||
|
@ -1264,13 +1261,9 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|||
// ie, hitting page-down will only cause is to wrap if we're already
|
||||
// at one end of the list.
|
||||
|
||||
// Do not allow the selection to be removed if UnifiedComplete is
|
||||
// enabled and the popup's first result is a heuristic result.
|
||||
if (!Services.prefs.getBoolPref("browser.urlbar.unifiedcomplete") ||
|
||||
(this.input.mController.matchCount > 0 &&
|
||||
this.input.mController
|
||||
.getStyleAt(0)
|
||||
.split(/\s+/).indexOf("heuristic") == -1)) {
|
||||
// Allow the selection to be removed if the first result is not a
|
||||
// heuristic result.
|
||||
if (!this._isFirstResultHeuristic) {
|
||||
if (reverse && index == -1 || newIndex > maxRow && index != maxRow)
|
||||
newIndex = maxRow;
|
||||
else if (!reverse && index == -1 || newIndex < 0 && index != 0)
|
||||
|
@ -1282,16 +1275,30 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|||
return newIndex;
|
||||
}
|
||||
|
||||
// Otherwise do not allow the selection to be removed.
|
||||
if (newIndex < 0) {
|
||||
newIndex = index > 0 ? 0 : maxRow;
|
||||
} else if (newIndex > maxRow) {
|
||||
newIndex = index < maxRow ? maxRow : 0;
|
||||
}
|
||||
|
||||
return newIndex;
|
||||
]]></body>
|
||||
</method>
|
||||
|
||||
<property name="_isFirstResultHeuristic" readonly="true">
|
||||
<getter>
|
||||
<![CDATA[
|
||||
// The popup usually has a special "heuristic" first result (added
|
||||
// by UnifiedComplete.js) that is automatically selected when the
|
||||
// popup opens.
|
||||
return this.input.mController.matchCount > 0 &&
|
||||
this.input.mController
|
||||
.getStyleAt(0)
|
||||
.split(/\s+/).indexOf("heuristic") > 0;
|
||||
]]>
|
||||
</getter>
|
||||
</property>
|
||||
|
||||
<property name="maxResults" readonly="true">
|
||||
<getter>
|
||||
<![CDATA[
|
||||
|
@ -1526,20 +1533,17 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|||
|
||||
// If nothing is selected yet, select the first result if it is a
|
||||
// pre-selected "heuristic" result. (See UnifiedComplete.js.)
|
||||
if (this._matchCount > 0 && this.selectedIndex == -1) {
|
||||
let styles = this.input.mController.getStyleAt(0).split(/\s+/);
|
||||
if (styles.indexOf("heuristic") >= 0) {
|
||||
// Don't handle this as a user-initiated action.
|
||||
this._ignoreNextSelect = true;
|
||||
if (this.selectedIndex == -1 && this._isFirstResultHeuristic) {
|
||||
// Don't handle this as a user-initiated action.
|
||||
this._ignoreNextSelect = true;
|
||||
|
||||
// Don't fire DOMMenuItemActive so that screen readers still see
|
||||
// the input as being focused.
|
||||
this.richlistbox.suppressMenuItemEvent = true;
|
||||
// Don't fire DOMMenuItemActive so that screen readers still see
|
||||
// the input as being focused.
|
||||
this.richlistbox.suppressMenuItemEvent = true;
|
||||
|
||||
this.selectedIndex = 0;
|
||||
this.richlistbox.suppressMenuItemEvent = false;
|
||||
this._ignoreNextSelect = false;
|
||||
}
|
||||
this.selectedIndex = 0;
|
||||
this.richlistbox.suppressMenuItemEvent = false;
|
||||
this._ignoreNextSelect = false;
|
||||
}
|
||||
|
||||
this.input.gotResultForCurrentQuery = true;
|
||||
|
|
|
@ -9,7 +9,6 @@ const Cr = Components.results;
|
|||
const Cu = Components.utils;
|
||||
|
||||
const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
const POLARIS_ENABLED = "browser.polaris.enabled";
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
@ -431,7 +430,7 @@ BrowserGlue.prototype = {
|
|||
Cu.reportError(ex);
|
||||
}
|
||||
let win = RecentWindow.getMostRecentBrowserWindow();
|
||||
win.BrowserSearch.recordSearchInHealthReport(engine, "urlbar");
|
||||
win.BrowserSearch.recordSearchInTelemetry(engine, "urlbar");
|
||||
break;
|
||||
case "browser-search-engine-modified":
|
||||
// Ensure we cleanup the hiddenOneOffs pref when removing
|
||||
|
@ -447,23 +446,6 @@ BrowserGlue.prototype = {
|
|||
hiddenList.join(","));
|
||||
}
|
||||
break;
|
||||
#ifdef NIGHTLY_BUILD
|
||||
case "nsPref:changed":
|
||||
if (data == POLARIS_ENABLED) {
|
||||
let enabled = Services.prefs.getBoolPref(POLARIS_ENABLED);
|
||||
if (enabled) {
|
||||
Services.prefs.setBoolPref("privacy.donottrackheader.enabled", enabled);
|
||||
Services.prefs.setBoolPref("privacy.trackingprotection.enabled", enabled);
|
||||
Services.prefs.setBoolPref("privacy.trackingprotection.ui.enabled", enabled);
|
||||
} else {
|
||||
// Don't reset DNT because its visible pref is independent of
|
||||
// Polaris and may have been previously set.
|
||||
Services.prefs.clearUserPref("privacy.trackingprotection.enabled");
|
||||
Services.prefs.clearUserPref("privacy.trackingprotection.ui.enabled");
|
||||
}
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
case "flash-plugin-hang":
|
||||
this._handleFlashHang();
|
||||
break;
|
||||
|
@ -634,9 +616,6 @@ BrowserGlue.prototype = {
|
|||
os.removeObserver(this, "keyword-search");
|
||||
#endif
|
||||
os.removeObserver(this, "browser-search-engine-modified");
|
||||
#ifdef NIGHTLY_BUILD
|
||||
Services.prefs.removeObserver(POLARIS_ENABLED, this);
|
||||
#endif
|
||||
os.removeObserver(this, "flash-plugin-hang");
|
||||
os.removeObserver(this, "xpi-signature-changed");
|
||||
os.removeObserver(this, "autocomplete-did-enter-text");
|
||||
|
@ -821,10 +800,6 @@ BrowserGlue.prototype = {
|
|||
|
||||
SelfSupportBackend.init();
|
||||
|
||||
#ifdef NIGHTLY_BUILD
|
||||
Services.prefs.addObserver(POLARIS_ENABLED, this, false);
|
||||
#endif
|
||||
|
||||
#ifndef RELEASE_BUILD
|
||||
let themeName = gBrowserBundle.GetStringFromName("deveditionTheme.name");
|
||||
let vendorShortName = gBrandBundle.GetStringFromName("vendorShortName");
|
||||
|
|
|
@ -7,6 +7,8 @@ Components.utils.import("resource://gre/modules/DownloadUtils.jsm");
|
|||
Components.utils.import("resource://gre/modules/LoadContextInfo.jsm");
|
||||
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
const PREF_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
|
||||
|
||||
var gAdvancedPane = {
|
||||
_inited: false,
|
||||
|
||||
|
@ -289,38 +291,23 @@ var gAdvancedPane = {
|
|||
initSubmitHealthReport: function () {
|
||||
this._setupLearnMoreLink("datareporting.healthreport.infoURL", "FHRLearnMore");
|
||||
|
||||
let policy = Components.classes["@mozilla.org/datareporting/service;1"]
|
||||
.getService(Components.interfaces.nsISupports)
|
||||
.wrappedJSObject
|
||||
.policy;
|
||||
|
||||
let checkbox = document.getElementById("submitHealthReportBox");
|
||||
|
||||
if (!policy || policy.healthReportUploadLocked) {
|
||||
if (Services.prefs.prefIsLocked(PREF_UPLOAD_ENABLED)) {
|
||||
checkbox.setAttribute("disabled", "true");
|
||||
return;
|
||||
}
|
||||
|
||||
checkbox.checked = policy.healthReportUploadEnabled;
|
||||
checkbox.checked = Services.prefs.getBoolPref(PREF_UPLOAD_ENABLED);
|
||||
this.setTelemetrySectionEnabled(checkbox.checked);
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the health report policy acceptance with state from checkbox.
|
||||
* Update the health report preference with state from checkbox.
|
||||
*/
|
||||
updateSubmitHealthReport: function () {
|
||||
let policy = Components.classes["@mozilla.org/datareporting/service;1"]
|
||||
.getService(Components.interfaces.nsISupports)
|
||||
.wrappedJSObject
|
||||
.policy;
|
||||
|
||||
if (!policy) {
|
||||
return;
|
||||
}
|
||||
|
||||
let checkbox = document.getElementById("submitHealthReportBox");
|
||||
policy.recordHealthReportUploadEnabled(checkbox.checked,
|
||||
"Checkbox from preferences pane");
|
||||
Services.prefs.setBoolPref(PREF_UPLOAD_ENABLED, checkbox.checked);
|
||||
this.setTelemetrySectionEnabled(checkbox.checked);
|
||||
},
|
||||
#endif
|
||||
|
|
|
@ -8,7 +8,7 @@ browser.jar:
|
|||
content/browser/preferences/in-content/subdialogs.js
|
||||
|
||||
* content/browser/preferences/in-content/main.js
|
||||
* content/browser/preferences/in-content/privacy.js
|
||||
content/browser/preferences/in-content/privacy.js
|
||||
* content/browser/preferences/in-content/advanced.js
|
||||
* content/browser/preferences/in-content/applications.js
|
||||
content/browser/preferences/in-content/content.js
|
||||
|
|
|
@ -16,7 +16,6 @@ var gPrivacyPane = {
|
|||
*/
|
||||
_shouldPromptForRestart: true,
|
||||
|
||||
#ifdef NIGHTLY_BUILD
|
||||
/**
|
||||
* Show the Tracking Protection UI depending on the
|
||||
* privacy.trackingprotection.ui.enabled pref, and linkify its Learn More link
|
||||
|
@ -35,7 +34,6 @@ var gPrivacyPane = {
|
|||
document.getElementById("trackingprotectionbox").hidden = false;
|
||||
document.getElementById("trackingprotectionpbmbox").hidden = true;
|
||||
},
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Linkify the Learn More link of the Private Browsing Mode Tracking
|
||||
|
@ -83,9 +81,7 @@ var gPrivacyPane = {
|
|||
this.updateHistoryModePane();
|
||||
this.updatePrivacyMicroControls();
|
||||
this.initAutoStartPrivateBrowsingReverter();
|
||||
#ifdef NIGHTLY_BUILD
|
||||
this._initTrackingProtection();
|
||||
#endif
|
||||
this._initTrackingProtectionPBM();
|
||||
this._initAutocomplete();
|
||||
|
||||
|
@ -510,7 +506,7 @@ var gPrivacyPane = {
|
|||
|
||||
acceptThirdPartyLabel.disabled = acceptThirdPartyMenu.disabled = !acceptCookies;
|
||||
keepUntil.disabled = menu.disabled = this._autoStartPrivateBrowsing || !acceptCookies;
|
||||
|
||||
|
||||
return acceptCookies;
|
||||
},
|
||||
|
||||
|
@ -529,7 +525,7 @@ var gPrivacyPane = {
|
|||
|
||||
return accept.checked ? 0 : 2;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Converts between network.cookie.cookieBehavior and the third-party cookie UI
|
||||
*/
|
||||
|
@ -550,7 +546,7 @@ var gPrivacyPane = {
|
|||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
writeAcceptThirdPartyCookies: function ()
|
||||
{
|
||||
var accept = document.getElementById("acceptThirdPartyMenu").selectedItem;
|
||||
|
|
|
@ -18,7 +18,7 @@ skip-if = os != "win" # This test tests the windows-specific app selection dialo
|
|||
[browser_connection_bug388287.js]
|
||||
[browser_cookies_exceptions.js]
|
||||
[browser_healthreport.js]
|
||||
skip-if = true || !healthreport || (os == 'linux' && debug) # Bug 1185403 for the "true"
|
||||
skip-if = true || !healthreport # Bug 1185403 for the "true"
|
||||
[browser_homepages_filter_aboutpreferences.js]
|
||||
[browser_notifications_do_not_disturb.js]
|
||||
[browser_permissions_urlFieldHidden.js]
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
|
||||
|
||||
function runPaneTest(fn) {
|
||||
open_preferences((win) => {
|
||||
let doc = win.document;
|
||||
|
@ -10,14 +12,7 @@ function runPaneTest(fn) {
|
|||
let advancedPrefs = doc.getElementById("advancedPrefs");
|
||||
let tab = doc.getElementById("dataChoicesTab");
|
||||
advancedPrefs.selectedTab = tab;
|
||||
|
||||
let policy = Components.classes["@mozilla.org/datareporting/service;1"]
|
||||
.getService(Components.interfaces.nsISupports)
|
||||
.wrappedJSObject
|
||||
.policy;
|
||||
|
||||
ok(policy, "Policy object is defined.");
|
||||
fn(win, doc, policy);
|
||||
fn(win, doc);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -28,8 +23,9 @@ function test() {
|
|||
runPaneTest(testBasic);
|
||||
}
|
||||
|
||||
function testBasic(win, doc, policy) {
|
||||
is(policy.healthReportUploadEnabled, true, "Health Report upload enabled on app first run.");
|
||||
function testBasic(win, doc) {
|
||||
is(Services.prefs.getBoolPref(FHR_UPLOAD_ENABLED), true,
|
||||
"Health Report upload enabled on app first run.");
|
||||
|
||||
let checkbox = doc.getElementById("submitHealthReportBox");
|
||||
ok(checkbox);
|
||||
|
@ -37,28 +33,30 @@ function testBasic(win, doc, policy) {
|
|||
|
||||
checkbox.checked = false;
|
||||
checkbox.doCommand();
|
||||
is(policy.healthReportUploadEnabled, false, "Unchecking checkbox opts out of FHR upload.");
|
||||
is(Services.prefs.getBoolPref(FHR_UPLOAD_ENABLED), false,
|
||||
"Unchecking checkbox opts out of FHR upload.");
|
||||
|
||||
checkbox.checked = true;
|
||||
checkbox.doCommand();
|
||||
is(policy.healthReportUploadEnabled, true, "Checking checkbox allows FHR upload.");
|
||||
is(Services.prefs.getBoolPref(FHR_UPLOAD_ENABLED), true,
|
||||
"Checking checkbox allows FHR upload.");
|
||||
|
||||
win.close();
|
||||
Services.prefs.lockPref("datareporting.healthreport.uploadEnabled");
|
||||
Services.prefs.lockPref(FHR_UPLOAD_ENABLED);
|
||||
runPaneTest(testUploadDisabled);
|
||||
}
|
||||
|
||||
function testUploadDisabled(win, doc, policy) {
|
||||
ok(policy.healthReportUploadLocked, "Upload enabled flag is locked.");
|
||||
function testUploadDisabled(win, doc) {
|
||||
ok(Services.prefs.prefIsLocked(FHR_UPLOAD_ENABLED), "Upload enabled flag is locked.");
|
||||
let checkbox = doc.getElementById("submitHealthReportBox");
|
||||
is(checkbox.getAttribute("disabled"), "true", "Checkbox is disabled if upload flag is locked.");
|
||||
policy._healthReportPrefs.unlock("uploadEnabled");
|
||||
Services.prefs.unlockPref(FHR_UPLOAD_ENABLED);
|
||||
|
||||
win.close();
|
||||
finish();
|
||||
}
|
||||
|
||||
function resetPreferences() {
|
||||
Services.prefs.clearUserPref("datareporting.healthreport.uploadEnabled");
|
||||
Services.prefs.clearUserPref(FHR_UPLOAD_ENABLED);
|
||||
}
|
||||
|
||||
|
|
|
@ -420,7 +420,7 @@
|
|||
if (telemetrySearchDetails && telemetrySearchDetails.index == -1) {
|
||||
telemetrySearchDetails = null;
|
||||
}
|
||||
BrowserSearch.recordSearchInHealthReport(engine, "searchbar", telemetrySearchDetails);
|
||||
BrowserSearch.recordSearchInTelemetry(engine, "searchbar", telemetrySearchDetails);
|
||||
// null parameter below specifies HTML response for search
|
||||
let params = {
|
||||
postData: submission.postData,
|
||||
|
|
|
@ -3,75 +3,50 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
var Preferences = Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences;
|
||||
|
||||
function test() {
|
||||
requestLongerTimeout(2);
|
||||
waitForExplicitFinish();
|
||||
resetPreferences();
|
||||
|
||||
try {
|
||||
let cm = Components.classes["@mozilla.org/categorymanager;1"]
|
||||
.getService(Components.interfaces.nsICategoryManager);
|
||||
cm.getCategoryEntry("healthreport-js-provider-default", "SearchesProvider");
|
||||
} catch (ex) {
|
||||
// Health Report disabled, or no SearchesProvider.
|
||||
// We need a test or else we'll be marked as failure.
|
||||
ok(true, "Firefox Health Report is not enabled.");
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
function testTelemetry() {
|
||||
// Find the right bucket for the "Foo" engine.
|
||||
let engine = Services.search.getEngineByName("Foo");
|
||||
let histogramKey = (engine.identifier || "other-Foo") + ".searchbar";
|
||||
let numSearchesBefore = 0;
|
||||
try {
|
||||
let hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
|
||||
if (histogramKey in hs) {
|
||||
numSearchesBefore = hs[histogramKey].sum;
|
||||
}
|
||||
} catch (ex) {
|
||||
// No searches performed yet, not a problem, |numSearchesBefore| is 0.
|
||||
}
|
||||
|
||||
function testFHR() {
|
||||
let reporter = Components.classes["@mozilla.org/datareporting/service;1"]
|
||||
.getService()
|
||||
.wrappedJSObject
|
||||
.healthReporter;
|
||||
ok(reporter, "Health Reporter available.");
|
||||
reporter.onInit().then(function onInit() {
|
||||
let provider = reporter.getProvider("org.mozilla.searches");
|
||||
let m = provider.getMeasurement("counts", 3);
|
||||
// Now perform a search and ensure the count is incremented.
|
||||
let tab = gBrowser.addTab();
|
||||
gBrowser.selectedTab = tab;
|
||||
let searchBar = BrowserSearch.searchBar;
|
||||
|
||||
m.getValues().then(function onData(data) {
|
||||
let now = new Date();
|
||||
let oldCount = 0;
|
||||
searchBar.value = "firefox health report";
|
||||
searchBar.focus();
|
||||
|
||||
function afterSearch() {
|
||||
searchBar.value = "";
|
||||
gBrowser.removeTab(tab);
|
||||
|
||||
// Make sure that the context searches are correctly recorded.
|
||||
let hs = Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS").snapshot();
|
||||
Assert.ok(histogramKey in hs, "The histogram must contain the correct key");
|
||||
Assert.equal(hs[histogramKey].sum, numSearchesBefore + 1,
|
||||
"Performing a search increments the related SEARCH_COUNTS key by 1.");
|
||||
|
||||
// Find the right bucket for the "Foo" engine.
|
||||
let engine = Services.search.getEngineByName("Foo");
|
||||
let field = (engine.identifier || "other-Foo") + ".searchbar";
|
||||
Services.search.removeEngine(engine);
|
||||
}
|
||||
|
||||
if (data.days.hasDay(now)) {
|
||||
let day = data.days.getDay(now);
|
||||
if (day.has(field)) {
|
||||
oldCount = day.get(field);
|
||||
}
|
||||
}
|
||||
|
||||
// Now perform a search and ensure the count is incremented.
|
||||
let tab = gBrowser.addTab();
|
||||
gBrowser.selectedTab = tab;
|
||||
let searchBar = BrowserSearch.searchBar;
|
||||
|
||||
searchBar.value = "firefox health report";
|
||||
searchBar.focus();
|
||||
|
||||
function afterSearch() {
|
||||
searchBar.value = "";
|
||||
gBrowser.removeTab(tab);
|
||||
|
||||
m.getValues().then(function onData(data) {
|
||||
ok(data.days.hasDay(now), "Have data for today.");
|
||||
let day = data.days.getDay(now);
|
||||
|
||||
is(day.get(field), oldCount + 1, "Performing a search increments FHR count by 1.");
|
||||
|
||||
let engine = Services.search.getEngineByName("Foo");
|
||||
Services.search.removeEngine(engine);
|
||||
});
|
||||
}
|
||||
|
||||
EventUtils.synthesizeKey("VK_RETURN", {});
|
||||
executeSoon(() => executeSoon(afterSearch));
|
||||
});
|
||||
});
|
||||
EventUtils.synthesizeKey("VK_RETURN", {});
|
||||
executeSoon(() => executeSoon(afterSearch));
|
||||
}
|
||||
|
||||
function observer(subject, topic, data) {
|
||||
|
@ -84,7 +59,7 @@ function test() {
|
|||
|
||||
case "engine-current":
|
||||
is(Services.search.currentEngine.name, "Foo", "Current engine is Foo");
|
||||
testFHR();
|
||||
testTelemetry();
|
||||
break;
|
||||
|
||||
case "engine-removed":
|
||||
|
@ -101,9 +76,6 @@ function test() {
|
|||
}
|
||||
|
||||
function resetPreferences() {
|
||||
let service = Components.classes["@mozilla.org/datareporting/service;1"]
|
||||
.getService(Components.interfaces.nsISupports)
|
||||
.wrappedJSObject;
|
||||
service.policy._prefs.resetBranch("datareporting.policy.");
|
||||
service.policy.dataSubmissionPolicyBypassNotification = true;
|
||||
Preferences.resetBranch("datareporting.policy.");
|
||||
Preferences.set("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
|
||||
}
|
||||
|
|
|
@ -12,24 +12,6 @@ Cu.import("resource://gre/modules/Preferences.jsm");
|
|||
|
||||
const PREF_FHR_UPLOAD_ENABLED = "datareporting.healthreport.uploadEnabled";
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "gPolicy", () => {
|
||||
try {
|
||||
return Cc["@mozilla.org/datareporting/service;1"]
|
||||
.getService(Ci.nsISupports)
|
||||
.wrappedJSObject
|
||||
.policy;
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "reporter", () => {
|
||||
return Cc["@mozilla.org/datareporting/service;1"]
|
||||
.getService(Ci.nsISupports)
|
||||
.wrappedJSObject
|
||||
.healthReporter;
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "TelemetryArchive",
|
||||
"resource://gre/modules/TelemetryArchive.jsm");
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "TelemetryEnvironment",
|
||||
|
@ -53,45 +35,13 @@ MozSelfSupportInterface.prototype = {
|
|||
},
|
||||
|
||||
get healthReportDataSubmissionEnabled() {
|
||||
if (gPolicy) {
|
||||
return gPolicy.healthReportUploadEnabled;
|
||||
}
|
||||
|
||||
// The datareporting service is unavailable or disabled.
|
||||
return Preferences.get(PREF_FHR_UPLOAD_ENABLED, false);
|
||||
},
|
||||
|
||||
set healthReportDataSubmissionEnabled(enabled) {
|
||||
if (gPolicy) {
|
||||
let reason = "Self-support interface sent " +
|
||||
(enabled ? "opt-in" : "opt-out") +
|
||||
" command.";
|
||||
gPolicy.recordHealthReportUploadEnabled(enabled, reason);
|
||||
return;
|
||||
}
|
||||
|
||||
// The datareporting service is unavailable or disabled.
|
||||
Preferences.set(PREF_FHR_UPLOAD_ENABLED, enabled);
|
||||
},
|
||||
|
||||
getHealthReportPayload: function () {
|
||||
return new this._window.Promise(function (aResolve, aReject) {
|
||||
if (reporter) {
|
||||
let resolvePayload = function () {
|
||||
reporter.collectAndObtainJSONPayload(true).then(aResolve, aReject);
|
||||
};
|
||||
|
||||
if (reporter.initialized) {
|
||||
resolvePayload();
|
||||
} else {
|
||||
reporter.onInit().then(resolvePayload, aReject);
|
||||
}
|
||||
} else {
|
||||
aReject(new Error("No reporter"));
|
||||
}
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
resetPref: function(name) {
|
||||
Services.prefs.clearUserPref(name);
|
||||
},
|
||||
|
|
|
@ -2,5 +2,3 @@
|
|||
|
||||
[browser_bug538331.js]
|
||||
skip-if = e10s # Bug ?????? - child process crash, but only when run as part of the suite (ie, probably not actually this tests fault!?)
|
||||
|
||||
[browser_polaris_prefs.js]
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
const POLARIS_ENABLED = "browser.polaris.enabled";
|
||||
const PREF_DNT = "privacy.donottrackheader.enabled";
|
||||
const PREF_TP = "privacy.trackingprotection.enabled";
|
||||
const PREF_TPUI = "privacy.trackingprotection.ui.enabled";
|
||||
|
||||
var prefs = [PREF_DNT, PREF_TP, PREF_TPUI];
|
||||
|
||||
function spinEventLoop() {
|
||||
return new Promise((resolve) => executeSoon(resolve));
|
||||
};
|
||||
|
||||
// Spin event loop before checking so that polaris pref observer can set
|
||||
// dependent prefs.
|
||||
function* assertPref(pref, enabled) {
|
||||
yield spinEventLoop();
|
||||
let prefEnabled = Services.prefs.getBoolPref(pref);
|
||||
Assert.equal(prefEnabled, enabled, "Checking state of pref " + pref + ".");
|
||||
};
|
||||
|
||||
function* testPrefs(test) {
|
||||
for (let pref of prefs) {
|
||||
yield test(pref);
|
||||
}
|
||||
}
|
||||
|
||||
function isNightly() {
|
||||
return Services.appinfo.version.includes("a1");
|
||||
}
|
||||
|
||||
add_task(function* test_default_values() {
|
||||
if (!isNightly()) {
|
||||
ok(true, "Skipping test, not Nightly")
|
||||
return;
|
||||
}
|
||||
Assert.ok(!Services.prefs.getBoolPref(POLARIS_ENABLED), POLARIS_ENABLED + " is disabled by default.");
|
||||
Assert.ok(!Services.prefs.getBoolPref(PREF_TPUI), PREF_TPUI + "is disabled by default.");
|
||||
});
|
||||
|
||||
add_task(function* test_changing_pref_changes_tracking() {
|
||||
if (!isNightly()) {
|
||||
ok(true, "Skipping test, not Nightly")
|
||||
return;
|
||||
}
|
||||
|
||||
// Register a cleanup function for all the prefs affected by this entire test file.
|
||||
registerCleanupFunction(function () {
|
||||
Services.prefs.clearUserPref(POLARIS_ENABLED);
|
||||
for (let pref of prefs) {
|
||||
Services.prefs.clearUserPref(pref);
|
||||
}
|
||||
});
|
||||
|
||||
function* testPref(pref) {
|
||||
Services.prefs.setBoolPref(POLARIS_ENABLED, true);
|
||||
yield assertPref(pref, true);
|
||||
Services.prefs.setBoolPref(POLARIS_ENABLED, false);
|
||||
// We don't clear the DNT pref if Polaris is disabled.
|
||||
if (pref != PREF_DNT) {
|
||||
yield assertPref(pref, false);
|
||||
} else {
|
||||
yield assertPref(pref, true);
|
||||
}
|
||||
Services.prefs.setBoolPref(POLARIS_ENABLED, true);
|
||||
yield assertPref(pref, true);
|
||||
}
|
||||
yield testPrefs(testPref);
|
||||
});
|
||||
|
||||
add_task(function* test_prefs_can_be_changed_individually() {
|
||||
if (!isNightly()) {
|
||||
ok(true, "Skipping test, not Nightly")
|
||||
return;
|
||||
}
|
||||
function* testPref(pref) {
|
||||
Services.prefs.setBoolPref(POLARIS_ENABLED, true);
|
||||
yield assertPref(pref, true);
|
||||
Services.prefs.setBoolPref(pref, false);
|
||||
yield assertPref(pref, false);
|
||||
yield assertPref(POLARIS_ENABLED, true);
|
||||
|
||||
Services.prefs.setBoolPref(POLARIS_ENABLED, false);
|
||||
yield assertPref(pref, false);
|
||||
|
||||
Services.prefs.setBoolPref(pref, true);
|
||||
yield assertPref(pref, true);
|
||||
yield assertPref(POLARIS_ENABLED, false);
|
||||
}
|
||||
yield testPrefs(testPref);
|
||||
});
|
|
@ -31,7 +31,6 @@ MOZ_SAFE_BROWSING=1
|
|||
MOZ_SERVICES_COMMON=1
|
||||
MOZ_SERVICES_CRYPTO=1
|
||||
MOZ_SERVICES_HEALTHREPORT=1
|
||||
MOZ_SERVICES_METRICS=1
|
||||
MOZ_SERVICES_SYNC=1
|
||||
MOZ_SERVICES_CLOUDSYNC=1
|
||||
MOZ_APP_VERSION=$FIREFOX_VERSION
|
||||
|
|
|
@ -19,7 +19,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
|
|||
|
||||
const PREF_EXPERIMENTS_ENABLED = "experiments.enabled";
|
||||
const PREF_ACTIVE_EXPERIMENT = "experiments.activeExperiment"; // whether we have an active experiment
|
||||
const PREF_HEALTHREPORT_ENABLED = "datareporting.healthreport.service.enabled";
|
||||
const PREF_TELEMETRY_ENABLED = "toolkit.telemetry.enabled";
|
||||
const PREF_TELEMETRY_UNIFIED = "toolkit.telemetry.unified";
|
||||
const DELAY_INIT_MS = 30 * 1000;
|
||||
|
@ -38,8 +37,7 @@ XPCOMUtils.defineLazyGetter(
|
|||
// We can enable experiments if either unified Telemetry or FHR is on, and the user
|
||||
// has opted into Telemetry.
|
||||
return gPrefs.get(PREF_EXPERIMENTS_ENABLED, false) &&
|
||||
(gPrefs.get(PREF_HEALTHREPORT_ENABLED, false) || IS_UNIFIED_TELEMETRY) &&
|
||||
gPrefs.get(PREF_TELEMETRY_ENABLED, false);
|
||||
IS_UNIFIED_TELEMETRY && gPrefs.get(PREF_TELEMETRY_ENABLED, false);
|
||||
});
|
||||
|
||||
XPCOMUtils.defineLazyGetter(
|
||||
|
|
|
@ -14,7 +14,7 @@ FIREFOX_PREFERENCES = {
|
|||
"devtools.debugger.prompt-connection": False,
|
||||
"devtools.debugger.remote-enabled": True,
|
||||
"media.volume_scale": "0",
|
||||
"loop.gettingStarted.latestFTUVersion": 0,
|
||||
"loop.gettingStarted.latestFTUVersion": 1,
|
||||
|
||||
# this dialog is fragile, and likely to introduce intermittent failures
|
||||
"media.navigator.permission.disabled": True,
|
||||
|
|
|
@ -263,41 +263,41 @@ class Test1BrowserCall(MarionetteTestCase):
|
|||
def test_1_browser_call(self):
|
||||
self.switch_to_panel()
|
||||
|
||||
# self.local_start_a_conversation()
|
||||
self.local_start_a_conversation()
|
||||
|
||||
# # Check the self video in the conversation window
|
||||
# self.local_check_room_self_video()
|
||||
# Check the self video in the conversation window
|
||||
self.local_check_room_self_video()
|
||||
|
||||
# # make sure that the media start time is not initialized
|
||||
# self.local_check_media_start_time_uninitialized()
|
||||
# make sure that the media start time is not initialized
|
||||
self.local_check_media_start_time_uninitialized()
|
||||
|
||||
# room_url = self.local_get_and_verify_room_url()
|
||||
room_url = self.local_get_and_verify_room_url()
|
||||
|
||||
# # load the link clicker interface into the current content browser
|
||||
# self.standalone_load_and_join_room(room_url)
|
||||
# load the link clicker interface into the current content browser
|
||||
self.standalone_load_and_join_room(room_url)
|
||||
|
||||
# # Check we get the video streams
|
||||
# self.standalone_check_remote_video()
|
||||
# self.local_check_remote_video()
|
||||
# Check we get the video streams
|
||||
self.standalone_check_remote_video()
|
||||
self.local_check_remote_video()
|
||||
|
||||
# # Check text messaging
|
||||
# self.check_text_messaging()
|
||||
# Check text messaging
|
||||
self.check_text_messaging()
|
||||
|
||||
# # since bi-directional media is connected, make sure we've set
|
||||
# # the start time
|
||||
# self.local_check_media_start_time_initialized()
|
||||
# since bi-directional media is connected, make sure we've set
|
||||
# the start time
|
||||
self.local_check_media_start_time_initialized()
|
||||
|
||||
# # Check that screenshare was automatically started
|
||||
# self.standalone_check_remote_screenshare()
|
||||
# Check that screenshare was automatically started
|
||||
self.standalone_check_remote_screenshare()
|
||||
|
||||
# # We hangup on the remote (standalone) side, because this also leaves
|
||||
# # the local chatbox with the local publishing media still connected,
|
||||
# # which means that the local_check_connection_length below
|
||||
# # verifies that the connection is noted at the time the remote media
|
||||
# # drops, rather than waiting until the window closes.
|
||||
# self.remote_leave_room()
|
||||
# We hangup on the remote (standalone) side, because this also leaves
|
||||
# the local chatbox with the local publishing media still connected,
|
||||
# which means that the local_check_connection_length below
|
||||
# verifies that the connection is noted at the time the remote media
|
||||
# drops, rather than waiting until the window closes.
|
||||
self.remote_leave_room()
|
||||
|
||||
# self.local_check_connection_length_noted()
|
||||
self.local_check_connection_length_noted()
|
||||
|
||||
def tearDown(self):
|
||||
self.loop_test_servers.shutdown()
|
||||
|
|
|
@ -498,12 +498,7 @@
|
|||
@RESPATH@/components/nsINIProcessor.js
|
||||
@RESPATH@/components/nsPrompter.manifest
|
||||
@RESPATH@/components/nsPrompter.js
|
||||
#ifdef MOZ_DATA_REPORTING
|
||||
@RESPATH@/components/DataReporting.manifest
|
||||
@RESPATH@/components/DataReportingService.js
|
||||
#endif
|
||||
#ifdef MOZ_SERVICES_HEALTHREPORT
|
||||
@RESPATH@/components/HealthReportComponents.manifest
|
||||
@RESPATH@/browser/components/SelfSupportService.manifest
|
||||
@RESPATH@/browser/components/SelfSupportService.js
|
||||
#endif
|
||||
|
|
|
@ -303,8 +303,8 @@ this.ContentSearch = {
|
|||
};
|
||||
win.openUILinkIn(submission.uri.spec, where, params);
|
||||
}
|
||||
win.BrowserSearch.recordSearchInHealthReport(engine, data.healthReportKey,
|
||||
data.selection || null);
|
||||
win.BrowserSearch.recordSearchInTelemetry(engine, data.healthReportKey,
|
||||
data.selection || null);
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
|
|
|
@ -23,8 +23,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "HiddenFrame",
|
|||
const PREF_ENABLED = "browser.selfsupport.enabled";
|
||||
// Url to open in the Self Support browser, in the urlFormatter service format.
|
||||
const PREF_URL = "browser.selfsupport.url";
|
||||
// FHR status.
|
||||
const PREF_FHR_ENABLED = "datareporting.healthreport.service.enabled";
|
||||
// Unified Telemetry status.
|
||||
const PREF_TELEMETRY_UNIFIED = "toolkit.telemetry.unified";
|
||||
// UITour status.
|
||||
|
@ -84,8 +82,8 @@ var SelfSupportBackendInternal = {
|
|||
|
||||
Preferences.observe(PREF_BRANCH_LOG, this._configureLogging, this);
|
||||
|
||||
// Only allow to use SelfSupport if either FHR or Unified Telemetry is enabled.
|
||||
let reportingEnabled = Preferences.get(PREF_FHR_ENABLED, false) || IS_UNIFIED_TELEMETRY;
|
||||
// Only allow to use SelfSupport if Unified Telemetry is enabled.
|
||||
let reportingEnabled = IS_UNIFIED_TELEMETRY;
|
||||
if (!reportingEnabled) {
|
||||
this._log.config("init - Disabling SelfSupport because FHR and Unified Telemetry are disabled.");
|
||||
return;
|
||||
|
|
|
@ -10,7 +10,7 @@ var toolbox;
|
|||
add_task(function* themeRegistration() {
|
||||
let tab = yield addTab("data:text/html,test");
|
||||
let target = TargetFactory.forTab(tab);
|
||||
toolbox = yield gDevTools.showToolbox(target);
|
||||
toolbox = yield gDevTools.showToolbox(target, "options");
|
||||
|
||||
let themeId = yield new Promise(resolve => {
|
||||
gDevTools.once("theme-registered", (e, themeId) => {
|
||||
|
@ -31,9 +31,6 @@ add_task(function* themeRegistration() {
|
|||
});
|
||||
|
||||
add_task(function* themeInOptionsPanel() {
|
||||
|
||||
yield toolbox.selectTool("options");
|
||||
|
||||
let panel = toolbox.getCurrentPanel();
|
||||
let panelWin = toolbox.getCurrentPanel().panelWin;
|
||||
let doc = panelWin.frameElement.contentDocument;
|
||||
|
|
|
@ -65,15 +65,6 @@
|
|||
}
|
||||
|
||||
let oldThemeDef = gDevTools.getThemeDefinition(oldTheme);
|
||||
|
||||
// Unload all theme stylesheets related to the old theme.
|
||||
if (oldThemeDef) {
|
||||
for (let sheet of devtoolsStyleSheets.get(oldThemeDef) || []) {
|
||||
sheet.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Load all stylesheets associated with the new theme.
|
||||
let newThemeDef = gDevTools.getThemeDefinition(newTheme);
|
||||
|
||||
// The theme might not be available anymore (e.g. uninstalled)
|
||||
|
@ -110,28 +101,35 @@
|
|||
forceStyle();
|
||||
}
|
||||
|
||||
if (oldThemeDef) {
|
||||
for (let name of oldThemeDef.classList) {
|
||||
documentElement.classList.remove(name);
|
||||
Promise.all(loadEvents).then(() => {
|
||||
// Unload all stylesheets and classes from the old theme.
|
||||
if (oldThemeDef) {
|
||||
for (let name of oldThemeDef.classList) {
|
||||
documentElement.classList.remove(name);
|
||||
}
|
||||
|
||||
for (let sheet of devtoolsStyleSheets.get(oldThemeDef) || []) {
|
||||
sheet.remove();
|
||||
}
|
||||
|
||||
if (oldThemeDef.onUnapply) {
|
||||
oldThemeDef.onUnapply(window, newTheme);
|
||||
}
|
||||
}
|
||||
|
||||
if (oldThemeDef.onUnapply) {
|
||||
oldThemeDef.onUnapply(window, newTheme);
|
||||
// Load all stylesheets and classes from the new theme.
|
||||
for (let name of newThemeDef.classList) {
|
||||
documentElement.classList.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
for (let name of newThemeDef.classList) {
|
||||
documentElement.classList.add(name);
|
||||
}
|
||||
if (newThemeDef.onApply) {
|
||||
newThemeDef.onApply(window, oldTheme);
|
||||
}
|
||||
|
||||
if (newThemeDef.onApply) {
|
||||
newThemeDef.onApply(window, oldTheme);
|
||||
}
|
||||
|
||||
// Final notification for further theme-switching related logic.
|
||||
gDevTools.emit("theme-switched", window, newTheme, oldTheme);
|
||||
|
||||
Promise.all(loadEvents).then(notifyWindow, console.error.bind(console));
|
||||
// Final notification for further theme-switching related logic.
|
||||
gDevTools.emit("theme-switched", window, newTheme, oldTheme);
|
||||
notifyWindow();
|
||||
}, console.error.bind(console));
|
||||
}
|
||||
|
||||
function handlePrefChange(event, data) {
|
||||
|
|
|
@ -73,6 +73,15 @@ var inputTests = [
|
|||
inspectorIcon: true
|
||||
},
|
||||
|
||||
{
|
||||
input: "testLotsOfAttributes()",
|
||||
output: '<p n="" m="" l="" k="" j="" i="" h="" g="" f="" e="" d="" c="" b="" a="" id="lots-of-attributes">',
|
||||
printOutput: "[object HTMLParagraphElement]",
|
||||
inspectable: true,
|
||||
noClick: true,
|
||||
inspectorIcon: true
|
||||
},
|
||||
|
||||
{
|
||||
input: "testDocumentFragment()",
|
||||
output: "DocumentFragment [ <span.foo>, <div#fragdiv> ]",
|
||||
|
@ -99,15 +108,6 @@ var inputTests = [
|
|||
noClick: true,
|
||||
inspectorIcon: false
|
||||
},
|
||||
|
||||
{
|
||||
input: "testLotsOfAttributes()",
|
||||
output: '<p n="" m="" l="" k="" j="" i="" h="" g="" f="" e="" d="" c="" b="" a="" id="lots-of-attributes">',
|
||||
printOutput: "[object HTMLParagraphElement]",
|
||||
inspectable: true,
|
||||
noClick: true,
|
||||
inspectorIcon: true
|
||||
}
|
||||
];
|
||||
|
||||
function test() {
|
||||
|
|
|
@ -222,6 +222,7 @@ MeasuringToolHighlighter.prototype = {
|
|||
pageListenerTarget.removeEventListener("mouseup", this);
|
||||
pageListenerTarget.removeEventListener("scroll", this);
|
||||
pageListenerTarget.removeEventListener("pagehide", this);
|
||||
pageListenerTarget.removeEventListener("mouseleave", this);
|
||||
|
||||
this.markup.destroy();
|
||||
|
||||
|
|
|
@ -20,26 +20,6 @@ interface MozSelfSupport
|
|||
*/
|
||||
attribute boolean healthReportDataSubmissionEnabled;
|
||||
|
||||
/**
|
||||
* Retrieves the FHR payload object, which is of the form:
|
||||
*
|
||||
* {
|
||||
* version: Number,
|
||||
* clientID: String,
|
||||
* clientIDVersion: Number,
|
||||
* thisPingDate: String,
|
||||
* geckoAppInfo: Object,
|
||||
* data: Object
|
||||
* }
|
||||
*
|
||||
* Refer to the getJSONPayload function in healthreporter.jsm for more
|
||||
* information.
|
||||
*
|
||||
* @return Promise<Object>
|
||||
* Resolved when the FHR payload data has been collected.
|
||||
*/
|
||||
Promise<object> getHealthReportPayload();
|
||||
|
||||
/**
|
||||
* Retrieve a list of the archived Telemetry pings.
|
||||
* This contains objects with ping info, which are of the form:
|
||||
|
|
|
@ -12,7 +12,6 @@ MOZ_PLACES=1
|
|||
MOZ_EXTENSIONS_DEFAULT=" gio"
|
||||
MOZ_SERVICES_COMMON=1
|
||||
MOZ_SERVICES_CRYPTO=1
|
||||
MOZ_SERVICES_METRICS=1
|
||||
MOZ_SERVICES_SYNC=1
|
||||
MOZ_MEDIA_NAVIGATOR=1
|
||||
MOZ_SERVICES_HEALTHREPORT=1
|
||||
|
|
|
@ -428,11 +428,6 @@
|
|||
@BINPATH@/components/PeerConnection.manifest
|
||||
#endif
|
||||
|
||||
#ifdef MOZ_SERVICES_HEALTHREPORT
|
||||
@BINPATH@/components/HealthReportComponents.manifest
|
||||
@BINPATH@/components/HealthReportService.js
|
||||
#endif
|
||||
|
||||
@BINPATH@/components/CaptivePortalDetectComponents.manifest
|
||||
@BINPATH@/components/captivedetect.js
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
android:targetSdkVersion="22"/>
|
||||
|
||||
#include ../services/manifests/FxAccountAndroidManifest_permissions.xml.in
|
||||
#include ../services/manifests/HealthReportAndroidManifest_permissions.xml.in
|
||||
#include ../services/manifests/SyncAndroidManifest_permissions.xml.in
|
||||
|
||||
#ifdef MOZ_ANDROID_SEARCH_ACTIVITY
|
||||
|
@ -357,7 +356,6 @@
|
|||
</receiver>
|
||||
|
||||
#include ../services/manifests/FxAccountAndroidManifest_activities.xml.in
|
||||
#include ../services/manifests/HealthReportAndroidManifest_activities.xml.in
|
||||
#include ../services/manifests/SyncAndroidManifest_activities.xml.in
|
||||
#ifdef MOZ_ANDROID_SEARCH_ACTIVITY
|
||||
#include ../search/manifests/SearchAndroidManifest_activities.xml.in
|
||||
|
@ -465,7 +463,6 @@
|
|||
|
||||
|
||||
#include ../services/manifests/FxAccountAndroidManifest_services.xml.in
|
||||
#include ../services/manifests/HealthReportAndroidManifest_services.xml.in
|
||||
#include ../services/manifests/SyncAndroidManifest_services.xml.in
|
||||
|
||||
<service
|
||||
|
|
|
@ -765,10 +765,6 @@ sync_thirdparty_java_files = [
|
|||
|
||||
sync_java_files = [TOPSRCDIR + '/mobile/android/services/src/main/java/org/mozilla/gecko/' + x for x in [
|
||||
'background/BackgroundService.java',
|
||||
'background/bagheera/BagheeraClient.java',
|
||||
'background/bagheera/BagheeraRequestDelegate.java',
|
||||
'background/bagheera/BoundedByteArrayEntity.java',
|
||||
'background/bagheera/DeflateHelper.java',
|
||||
'background/common/DateUtils.java',
|
||||
'background/common/EditorBranch.java',
|
||||
'background/common/GlobalConstants.java',
|
||||
|
@ -805,31 +801,6 @@ sync_java_files = [TOPSRCDIR + '/mobile/android/services/src/main/java/org/mozil
|
|||
'background/fxa/profile/FxAccountProfileClient10.java',
|
||||
'background/fxa/QuickPasswordStretcher.java',
|
||||
'background/fxa/SkewHandler.java',
|
||||
'background/healthreport/AndroidConfigurationProvider.java',
|
||||
'background/healthreport/Environment.java',
|
||||
'background/healthreport/EnvironmentBuilder.java',
|
||||
'background/healthreport/EnvironmentV1.java',
|
||||
'background/healthreport/EnvironmentV2.java',
|
||||
'background/healthreport/HealthReportBroadcastReceiver.java',
|
||||
'background/healthreport/HealthReportBroadcastService.java',
|
||||
'background/healthreport/HealthReportConstants.java',
|
||||
'background/healthreport/HealthReportDatabases.java',
|
||||
'background/healthreport/HealthReportDatabaseStorage.java',
|
||||
'background/healthreport/HealthReportExportedBroadcastReceiver.java',
|
||||
'background/healthreport/HealthReportGenerator.java',
|
||||
'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/HealthReportUploadService.java',
|
||||
'background/healthreport/upload/ObsoleteDocumentTracker.java',
|
||||
'background/healthreport/upload/SubmissionClient.java',
|
||||
'background/healthreport/upload/SubmissionPolicy.java',
|
||||
'background/nativecode/NativeCrypto.java',
|
||||
'background/preferences/PreferenceFragment.java',
|
||||
'background/preferences/PreferenceManagerCompat.java',
|
||||
|
|
|
@ -30,8 +30,6 @@ import org.mozilla.gecko.firstrun.FirstrunAnimationContainer;
|
|||
import org.mozilla.gecko.gfx.DynamicToolbarAnimator;
|
||||
import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
|
||||
import org.mozilla.gecko.gfx.LayerView;
|
||||
import org.mozilla.gecko.health.BrowserHealthRecorder;
|
||||
import org.mozilla.gecko.health.BrowserHealthReporter;
|
||||
import org.mozilla.gecko.health.HealthRecorder;
|
||||
import org.mozilla.gecko.health.SessionInformation;
|
||||
import org.mozilla.gecko.home.BrowserSearch;
|
||||
|
@ -256,8 +254,6 @@ public class BrowserApp extends GeckoApp
|
|||
|
||||
private OrderedBroadcastHelper mOrderedBroadcastHelper;
|
||||
|
||||
private BrowserHealthReporter mBrowserHealthReporter;
|
||||
|
||||
private ReadingListHelper mReadingListHelper;
|
||||
|
||||
private AccountsHelper mAccountsHelper;
|
||||
|
@ -700,7 +696,6 @@ public class BrowserApp extends GeckoApp
|
|||
JavaAddonManager.getInstance().init(appContext);
|
||||
mSharedPreferencesHelper = new SharedPreferencesHelper(appContext);
|
||||
mOrderedBroadcastHelper = new OrderedBroadcastHelper(appContext);
|
||||
mBrowserHealthReporter = new BrowserHealthReporter();
|
||||
mReadingListHelper = new ReadingListHelper(appContext, getProfile(), this);
|
||||
mAccountsHelper = new AccountsHelper(appContext, getProfile());
|
||||
|
||||
|
@ -1306,11 +1301,6 @@ public class BrowserApp extends GeckoApp
|
|||
mOrderedBroadcastHelper = null;
|
||||
}
|
||||
|
||||
if (mBrowserHealthReporter != null) {
|
||||
mBrowserHealthReporter.uninit();
|
||||
mBrowserHealthReporter = null;
|
||||
}
|
||||
|
||||
if (mReadingListHelper != null) {
|
||||
mReadingListHelper.uninit();
|
||||
mReadingListHelper = null;
|
||||
|
@ -2340,16 +2330,16 @@ public class BrowserApp extends GeckoApp
|
|||
* {@link BrowserHealthRecorder#SEARCH_LOCATIONS}.
|
||||
*/
|
||||
private static void recordSearch(SearchEngine engine, String where) {
|
||||
try {
|
||||
String identifier = (engine == null) ? "other" : engine.getEngineIdentifier();
|
||||
JSONObject message = new JSONObject();
|
||||
message.put("type", BrowserHealthRecorder.EVENT_SEARCH);
|
||||
message.put("location", where);
|
||||
message.put("identifier", identifier);
|
||||
EventDispatcher.getInstance().dispatchEvent(message, null);
|
||||
} catch (Exception e) {
|
||||
Log.e(LOGTAG, "Error recording search.", e);
|
||||
}
|
||||
//try {
|
||||
// String identifier = (engine == null) ? "other" : engine.getEngineIdentifier();
|
||||
// JSONObject message = new JSONObject();
|
||||
// message.put("type", BrowserHealthRecorder.EVENT_SEARCH);
|
||||
// message.put("location", where);
|
||||
// message.put("identifier", identifier);
|
||||
// EventDispatcher.getInstance().dispatchEvent(message, null);
|
||||
//} catch (Exception e) {
|
||||
// Log.e(LOGTAG, "Error recording search.", e);
|
||||
//}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3865,22 +3855,6 @@ public class BrowserApp extends GeckoApp
|
|||
mDynamicToolbar.setTemporarilyVisible(false, VisibilityTransition.IMMEDIATE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected HealthRecorder createHealthRecorder(final Context context,
|
||||
final String profilePath,
|
||||
final EventDispatcher dispatcher,
|
||||
final String osLocale,
|
||||
final String appLocale,
|
||||
final SessionInformation previousSession) {
|
||||
return new BrowserHealthRecorder(context,
|
||||
GeckoSharedPrefs.forApp(context),
|
||||
profilePath,
|
||||
dispatcher,
|
||||
osLocale,
|
||||
appLocale,
|
||||
previousSession);
|
||||
}
|
||||
|
||||
public static interface Refreshable {
|
||||
public void refresh();
|
||||
}
|
||||
|
|
|
@ -2201,7 +2201,7 @@ public abstract class GeckoApp
|
|||
final HealthRecorder rec = mHealthRecorder;
|
||||
mHealthRecorder = null;
|
||||
if (rec != null && rec.isEnabled()) {
|
||||
// Closing a BrowserHealthRecorder could incur a write.
|
||||
// Closing a HealthRecorder could incur a write.
|
||||
ThreadUtils.postToBackgroundThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
|
@ -2793,7 +2793,7 @@ public abstract class GeckoApp
|
|||
|
||||
/**
|
||||
* Use BrowserLocaleManager to change our persisted and current locales,
|
||||
* and poke HealthRecorder to tell it of our changed state.
|
||||
* and poke the system to tell it of our changed state.
|
||||
*/
|
||||
protected void setLocale(final String locale) {
|
||||
if (locale == null) {
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -1,157 +0,0 @@
|
|||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* 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.health;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.gecko.EventDispatcher;
|
||||
import org.mozilla.gecko.GeckoAppShell;
|
||||
import org.mozilla.gecko.GeckoEvent;
|
||||
import org.mozilla.gecko.GeckoProfile;
|
||||
import org.mozilla.gecko.background.common.GlobalConstants;
|
||||
import org.mozilla.gecko.background.healthreport.AndroidConfigurationProvider;
|
||||
import org.mozilla.gecko.background.healthreport.EnvironmentBuilder;
|
||||
import org.mozilla.gecko.background.healthreport.EnvironmentBuilder.ConfigurationProvider;
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportConstants;
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage;
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportGenerator;
|
||||
import org.mozilla.gecko.util.GeckoEventListener;
|
||||
import org.mozilla.gecko.util.ThreadUtils;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.Context;
|
||||
import android.util.Log;
|
||||
|
||||
/**
|
||||
* BrowserHealthReporter is the browser's interface to the Firefox Health
|
||||
* Report report generator.
|
||||
*
|
||||
* Each instance registers Gecko event listeners, so keep a single instance
|
||||
* around for the life of the browser. Java callers should use this globally
|
||||
* available singleton.
|
||||
*/
|
||||
public class BrowserHealthReporter implements GeckoEventListener {
|
||||
private static final String LOGTAG = "GeckoHealthRep";
|
||||
|
||||
public static final String EVENT_REQUEST = "HealthReport:Request";
|
||||
public static final String EVENT_RESPONSE = "HealthReport:Response";
|
||||
|
||||
protected final Context context;
|
||||
|
||||
public BrowserHealthReporter() {
|
||||
EventDispatcher.getInstance().registerGeckoThreadListener(this, EVENT_REQUEST);
|
||||
|
||||
context = GeckoAppShell.getContext();
|
||||
if (context == null) {
|
||||
throw new IllegalStateException("Null Gecko context");
|
||||
}
|
||||
}
|
||||
|
||||
public void uninit() {
|
||||
EventDispatcher.getInstance().unregisterGeckoThreadListener(this, EVENT_REQUEST);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new Health Report.
|
||||
*
|
||||
* This method performs IO, so call it from a background thread.
|
||||
*
|
||||
* @param since timestamp of first day to report (milliseconds since epoch).
|
||||
* @param lastPingTime timestamp when last health report was uploaded
|
||||
* (milliseconds since epoch).
|
||||
* @param profilePath path of the profile to generate report for.
|
||||
* @throws JSONException if JSON generation fails.
|
||||
* @throws IllegalStateException if the environment does not allow to generate a report.
|
||||
* @return non-null report.
|
||||
*/
|
||||
public JSONObject generateReport(long since, long lastPingTime, String profilePath) throws JSONException {
|
||||
// We abuse the life-cycle of an Android ContentProvider slightly by holding
|
||||
// onto a ContentProviderClient while we generate a payload. This keeps
|
||||
// our database storage alive, while also allowing us to share a database
|
||||
// connection with BrowserHealthRecorder and the uploader.
|
||||
// The ContentProvider owns all underlying Storage instances, so we don't
|
||||
// need to explicitly close them.
|
||||
ContentProviderClient client = EnvironmentBuilder.getContentProviderClient(context);
|
||||
if (client == null) {
|
||||
throw new IllegalStateException("Could not fetch Health Report content provider.");
|
||||
}
|
||||
|
||||
try {
|
||||
// Storage instance is owned by HealthReportProvider, so we don't need
|
||||
// to close it.
|
||||
HealthReportDatabaseStorage storage = EnvironmentBuilder.getStorage(client, profilePath);
|
||||
if (storage == null) {
|
||||
throw new IllegalStateException("No storage in Health Reporter.");
|
||||
}
|
||||
|
||||
HealthReportGenerator generator = new HealthReportGenerator(storage);
|
||||
ConfigurationProvider configProvider = new AndroidConfigurationProvider(context);
|
||||
JSONObject report = generator.generateDocument(since, lastPingTime, profilePath, configProvider);
|
||||
if (report == null) {
|
||||
throw new IllegalStateException("Not enough profile information to generate report.");
|
||||
}
|
||||
return report;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last time a health report was successfully uploaded.
|
||||
*
|
||||
* This is read from shared preferences, so call it from a background
|
||||
* thread. Bug 882182 tracks making this work with multiple profiles.
|
||||
*
|
||||
* @return milliseconds since the epoch, or 0 if never uploaded.
|
||||
*/
|
||||
protected long getLastUploadLocalTime() {
|
||||
return context
|
||||
.getSharedPreferences(HealthReportConstants.PREFS_BRANCH, 0)
|
||||
.getLong(HealthReportConstants.PREF_LAST_UPLOAD_LOCAL_TIME, 0L);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a new Health Report for the current Gecko profile.
|
||||
*
|
||||
* This method performs IO, so call it from a background thread.
|
||||
*
|
||||
* @throws JSONException if JSON generation fails.
|
||||
* @throws IllegalStateException if the environment does not allow to generate a report.
|
||||
* @return non-null Health Report.
|
||||
*/
|
||||
public JSONObject generateReport() throws JSONException {
|
||||
GeckoProfile profile = GeckoAppShell.getGeckoInterface().getProfile();
|
||||
String profilePath = profile.getDir().getAbsolutePath();
|
||||
|
||||
long since = System.currentTimeMillis() - GlobalConstants.MILLISECONDS_PER_SIX_MONTHS;
|
||||
long lastPingTime = Math.max(getLastUploadLocalTime(), HealthReportConstants.EARLIEST_LAST_PING);
|
||||
|
||||
return generateReport(since, lastPingTime, profilePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleMessage(String event, JSONObject message) {
|
||||
try {
|
||||
ThreadUtils.postToBackgroundThread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
JSONObject report = null;
|
||||
try {
|
||||
report = generateReport(); // non-null if it returns.
|
||||
} catch (Exception e) {
|
||||
Log.e(LOGTAG, "Generating report failed; responding with empty report.", e);
|
||||
report = new JSONObject();
|
||||
}
|
||||
|
||||
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(EVENT_RESPONSE, report.toString()));
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -29,7 +29,6 @@ import org.mozilla.gecko.Telemetry;
|
|||
import org.mozilla.gecko.TelemetryContract;
|
||||
import org.mozilla.gecko.TelemetryContract.Method;
|
||||
import org.mozilla.gecko.background.common.GlobalConstants;
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportConstants;
|
||||
import org.mozilla.gecko.db.BrowserContract.SuggestedSites;
|
||||
import org.mozilla.gecko.restrictions.Restrictable;
|
||||
import org.mozilla.gecko.tabqueue.TabQueueHelper;
|
||||
|
@ -981,10 +980,10 @@ OnSharedPreferenceChangeListener
|
|||
* <code>PREFS_HEALTHREPORT_UPLOAD_ENABLED</code> pref.
|
||||
*/
|
||||
public static void broadcastHealthReportUploadPref(final Context context, final boolean value) {
|
||||
broadcastPrefAction(context,
|
||||
HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF,
|
||||
PREFS_HEALTHREPORT_UPLOAD_ENABLED,
|
||||
value);
|
||||
//broadcastPrefAction(context,
|
||||
// HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF,
|
||||
// PREFS_HEALTHREPORT_UPLOAD_ENABLED,
|
||||
// value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -992,13 +991,13 @@ OnSharedPreferenceChangeListener
|
|||
* <code>PREFS_HEALTHREPORT_UPLOAD_ENABLED</code> pref.
|
||||
*/
|
||||
public static void broadcastHealthReportUploadPref(final Context context) {
|
||||
final boolean value = getBooleanPref(context, PREFS_HEALTHREPORT_UPLOAD_ENABLED, true);
|
||||
broadcastHealthReportUploadPref(context, value);
|
||||
//final boolean value = getBooleanPref(context, PREFS_HEALTHREPORT_UPLOAD_ENABLED, true);
|
||||
//broadcastHealthReportUploadPref(context, value);
|
||||
}
|
||||
|
||||
public static void broadcastHealthReportPrune(final Context context) {
|
||||
final Intent intent = new Intent(HealthReportConstants.ACTION_HEALTHREPORT_PRUNE);
|
||||
broadcastAction(context, intent);
|
||||
//final Intent intent = new Intent(HealthReportConstants.ACTION_HEALTHREPORT_PRUNE);
|
||||
//broadcastAction(context, intent);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -52,9 +52,11 @@ final class UnusedResourcesUtil {
|
|||
};
|
||||
|
||||
public static final int[] USED_IN_SUGGESTEDSITES = {
|
||||
R.drawable.suggestedsites_fxaddons,
|
||||
R.drawable.suggestedsites_fxsupport,
|
||||
R.drawable.suggestedsites_mozilla,
|
||||
R.drawable.suggestedsites_amazon,
|
||||
R.drawable.suggestedsites_facebook,
|
||||
R.drawable.suggestedsites_twitter,
|
||||
R.drawable.suggestedsites_wikipedia,
|
||||
R.drawable.suggestedsites_youtube,
|
||||
};
|
||||
|
||||
public static final int[] USED_IN_BOOKMARKDEFAULTS = {
|
||||
|
|
|
@ -352,8 +352,6 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
|
|||
'gfx/VirtualLayer.java',
|
||||
'GlobalHistory.java',
|
||||
'GuestSession.java',
|
||||
'health/BrowserHealthRecorder.java',
|
||||
'health/BrowserHealthReporter.java',
|
||||
'health/HealthRecorder.java',
|
||||
'health/SessionInformation.java',
|
||||
'health/StubbedHealthRecorder.java',
|
||||
|
|
|
@ -492,7 +492,6 @@ var BrowserApp = {
|
|||
NativeWindow.init();
|
||||
FormAssistant.init();
|
||||
IndexedDB.init();
|
||||
HealthReportStatusListener.init();
|
||||
XPInstallObserver.init();
|
||||
CharacterEncoding.init();
|
||||
ActivityObserver.init();
|
||||
|
@ -4691,17 +4690,14 @@ Tab.prototype = {
|
|||
}
|
||||
this.contentDocumentIsDisplayed = true;
|
||||
|
||||
if (contentDocument instanceof Ci.nsIImageDocument) {
|
||||
contentDocument.shrinkToFit();
|
||||
}
|
||||
|
||||
let zoom = this.restoredSessionZoom();
|
||||
if (zoom) {
|
||||
this.setResolution(zoom, true);
|
||||
}
|
||||
|
||||
if (!this.restoredSessionZoom() && contentDocument.mozSyntheticDocument) {
|
||||
let fitZoom = Math.min(gScreenWidth / contentDocument.body.scrollWidth,
|
||||
gScreenHeight / contentDocument.body.scrollHeight);
|
||||
this.setResolution(fitZoom, false);
|
||||
this.sendViewportUpdate(); // recompute displayport
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -5688,192 +5684,6 @@ var FormAssistant = {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* An object to watch for Gecko status changes -- add-on installs, pref changes
|
||||
* -- and reflect them back to Java.
|
||||
*/
|
||||
var HealthReportStatusListener = {
|
||||
PREF_ACCEPT_LANG: "intl.accept_languages",
|
||||
PREF_BLOCKLIST_ENABLED: "extensions.blocklist.enabled",
|
||||
|
||||
PREF_TELEMETRY_ENABLED: AppConstants.MOZ_TELEMETRY_REPORTING ?
|
||||
"toolkit.telemetry.enabled" :
|
||||
null,
|
||||
|
||||
init: function () {
|
||||
try {
|
||||
AddonManager.addAddonListener(this);
|
||||
} catch (ex) {
|
||||
dump("Failed to initialize add-on status listener. FHR cannot report add-on state. " + ex);
|
||||
}
|
||||
|
||||
dump("Adding HealthReport:RequestSnapshot observer.");
|
||||
Services.obs.addObserver(this, "HealthReport:RequestSnapshot", false);
|
||||
Services.prefs.addObserver(this.PREF_ACCEPT_LANG, this, false);
|
||||
Services.prefs.addObserver(this.PREF_BLOCKLIST_ENABLED, this, false);
|
||||
if (this.PREF_TELEMETRY_ENABLED) {
|
||||
Services.prefs.addObserver(this.PREF_TELEMETRY_ENABLED, this, false);
|
||||
}
|
||||
},
|
||||
|
||||
observe: function (aSubject, aTopic, aData) {
|
||||
switch (aTopic) {
|
||||
case "HealthReport:RequestSnapshot":
|
||||
HealthReportStatusListener.sendSnapshotToJava();
|
||||
break;
|
||||
case "nsPref:changed":
|
||||
let response = {
|
||||
type: "Pref:Change",
|
||||
pref: aData,
|
||||
isUserSet: Services.prefs.prefHasUserValue(aData),
|
||||
};
|
||||
|
||||
switch (aData) {
|
||||
case this.PREF_ACCEPT_LANG:
|
||||
response.value = Services.prefs.getCharPref(aData);
|
||||
break;
|
||||
case this.PREF_TELEMETRY_ENABLED:
|
||||
case this.PREF_BLOCKLIST_ENABLED:
|
||||
response.value = Services.prefs.getBoolPref(aData);
|
||||
break;
|
||||
default:
|
||||
console.log("Unexpected pref in HealthReportStatusListener: " + aData);
|
||||
return;
|
||||
}
|
||||
|
||||
Messaging.sendRequest(response);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
MILLISECONDS_PER_DAY: 24 * 60 * 60 * 1000,
|
||||
|
||||
COPY_FIELDS: [
|
||||
"blocklistState",
|
||||
"userDisabled",
|
||||
"appDisabled",
|
||||
"version",
|
||||
"type",
|
||||
"scope",
|
||||
"foreignInstall",
|
||||
"hasBinaryComponents",
|
||||
],
|
||||
|
||||
// Add-on types for which full details are recorded in FHR.
|
||||
// All other types are ignored.
|
||||
FULL_DETAIL_TYPES: [
|
||||
"plugin",
|
||||
"extension",
|
||||
"service",
|
||||
],
|
||||
|
||||
/**
|
||||
* Return true if the add-on is not of a type for which we report full details.
|
||||
* These add-ons will still make it over to Java, but will be filtered out.
|
||||
*/
|
||||
_shouldIgnore: function (aAddon) {
|
||||
return this.FULL_DETAIL_TYPES.indexOf(aAddon.type) == -1;
|
||||
},
|
||||
|
||||
_dateToDays: function (aDate) {
|
||||
return Math.floor(aDate.getTime() / this.MILLISECONDS_PER_DAY);
|
||||
},
|
||||
|
||||
jsonForAddon: function (aAddon) {
|
||||
let o = {};
|
||||
if (aAddon.installDate) {
|
||||
o.installDay = this._dateToDays(aAddon.installDate);
|
||||
}
|
||||
if (aAddon.updateDate) {
|
||||
o.updateDay = this._dateToDays(aAddon.updateDate);
|
||||
}
|
||||
|
||||
for (let field of this.COPY_FIELDS) {
|
||||
o[field] = aAddon[field];
|
||||
}
|
||||
|
||||
return o;
|
||||
},
|
||||
|
||||
notifyJava: function (aAddon, aNeedsRestart, aAction="Addons:Change") {
|
||||
let json = this.jsonForAddon(aAddon);
|
||||
if (this._shouldIgnore(aAddon)) {
|
||||
json.ignore = true;
|
||||
}
|
||||
Messaging.sendRequest({ type: aAction, id: aAddon.id, json: json });
|
||||
},
|
||||
|
||||
// Add-on listeners.
|
||||
onEnabling: function (aAddon, aNeedsRestart) {
|
||||
this.notifyJava(aAddon, aNeedsRestart);
|
||||
},
|
||||
onDisabling: function (aAddon, aNeedsRestart) {
|
||||
this.notifyJava(aAddon, aNeedsRestart);
|
||||
},
|
||||
onInstalling: function (aAddon, aNeedsRestart) {
|
||||
this.notifyJava(aAddon, aNeedsRestart);
|
||||
},
|
||||
onUninstalling: function (aAddon, aNeedsRestart) {
|
||||
this.notifyJava(aAddon, aNeedsRestart, "Addons:Uninstalling");
|
||||
},
|
||||
onPropertyChanged: function (aAddon, aProperties) {
|
||||
this.notifyJava(aAddon);
|
||||
},
|
||||
onOperationCancelled: function (aAddon) {
|
||||
this.notifyJava(aAddon);
|
||||
},
|
||||
|
||||
sendSnapshotToJava: function () {
|
||||
AddonManager.getAllAddons(function (aAddons) {
|
||||
let jsonA = {};
|
||||
if (aAddons) {
|
||||
for (let i = 0; i < aAddons.length; ++i) {
|
||||
let addon = aAddons[i];
|
||||
try {
|
||||
let addonJSON = HealthReportStatusListener.jsonForAddon(addon);
|
||||
if (HealthReportStatusListener._shouldIgnore(addon)) {
|
||||
addonJSON.ignore = true;
|
||||
}
|
||||
jsonA[addon.id] = addonJSON;
|
||||
} catch (e) {
|
||||
// Just skip this add-on.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now add prefs.
|
||||
let jsonP = {};
|
||||
for (let pref of [this.PREF_BLOCKLIST_ENABLED, this.PREF_TELEMETRY_ENABLED]) {
|
||||
if (!pref) {
|
||||
// This will be the case for PREF_TELEMETRY_ENABLED in developer builds.
|
||||
continue;
|
||||
}
|
||||
jsonP[pref] = {
|
||||
pref: pref,
|
||||
value: Services.prefs.getBoolPref(pref),
|
||||
isUserSet: Services.prefs.prefHasUserValue(pref),
|
||||
};
|
||||
}
|
||||
for (let pref of [this.PREF_ACCEPT_LANG]) {
|
||||
jsonP[pref] = {
|
||||
pref: pref,
|
||||
value: Services.prefs.getCharPref(pref),
|
||||
isUserSet: Services.prefs.prefHasUserValue(pref),
|
||||
};
|
||||
}
|
||||
|
||||
console.log("Sending snapshot message.");
|
||||
Messaging.sendRequest({
|
||||
type: "HealthReport:Snapshot",
|
||||
json: {
|
||||
addons: jsonA,
|
||||
prefs: jsonP,
|
||||
},
|
||||
});
|
||||
}.bind(this));
|
||||
},
|
||||
};
|
||||
|
||||
var XPInstallObserver = {
|
||||
init: function() {
|
||||
Services.obs.addObserver(this, "addon-install-origin-blocked", false);
|
||||
|
|
|
@ -203,8 +203,7 @@ function updateBanner(messages) {
|
|||
icon: message.icon,
|
||||
weight: message.weight,
|
||||
onclick: function() {
|
||||
let parentId = gChromeWin.BrowserApp.selectedTab.id;
|
||||
gChromeWin.BrowserApp.addTab(message.url, { parentId: parentId });
|
||||
gChromeWin.BrowserApp.loadURI(message.url);
|
||||
UITelemetry.addEvent("action.1", "banner", null, message.id);
|
||||
},
|
||||
ondismiss: function() {
|
||||
|
|
|
@ -11,7 +11,6 @@ import org.mozilla.gecko.Telemetry;
|
|||
import org.mozilla.gecko.TelemetryContract;
|
||||
import org.mozilla.gecko.db.BrowserContract.SearchHistory;
|
||||
import org.mozilla.gecko.distribution.Distribution;
|
||||
import org.mozilla.gecko.health.BrowserHealthRecorder;
|
||||
import org.mozilla.search.autocomplete.SearchBar;
|
||||
import org.mozilla.search.autocomplete.SuggestionsFragment;
|
||||
import org.mozilla.search.providers.SearchEngine;
|
||||
|
@ -255,7 +254,7 @@ public class SearchActivity extends Locales.LocaleAwareFragmentActivity
|
|||
storeQuery(query);
|
||||
|
||||
try {
|
||||
BrowserHealthRecorder.recordSearchDelayed("activity", engine.getIdentifier());
|
||||
//BrowserHealthRecorder.recordSearchDelayed("activity", engine.getIdentifier());
|
||||
} catch (Exception e) {
|
||||
// This should never happen: it'll only throw if the
|
||||
// search location is wrong. But let's not tempt fate.
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
<provider android:name="org.mozilla.gecko.background.healthreport.HealthReportProvider"
|
||||
android:authorities="@ANDROID_PACKAGE_NAME@.health"
|
||||
android:exported="false">
|
||||
</provider>
|
||||
|
||||
<!-- HealthReportBroadcastReceiver$ExportedReceiver is a thin receiver
|
||||
whose purpose is to start the background service in response to
|
||||
system events. It's exported so that it can receive system events.
|
||||
Such events cannot specify Health Report settings.
|
||||
-->
|
||||
<receiver
|
||||
android:name="org.mozilla.gecko.background.healthreport.HealthReportExportedBroadcastReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<!-- Startup. -->
|
||||
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<!-- SD card remounted. -->
|
||||
<action android:name="android.intent.action.EXTERNAL_APPLICATIONS_AVAILABLE" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- HealthReportBroadcastReceiver is a thin receiver whose purpose is
|
||||
to start the background service in response to events internal to
|
||||
Health Report. Such events can specify Health Report settings, so
|
||||
these intents must come from a trusted source; hence, this receiver
|
||||
is not exported.
|
||||
-->
|
||||
<receiver
|
||||
android:name="org.mozilla.gecko.background.healthreport.HealthReportBroadcastReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter >
|
||||
<!-- Toggle Health Report upload service alarm (based on preferences value) -->
|
||||
<action android:name="@ANDROID_PACKAGE_NAME@.HEALTHREPORT_UPLOAD_PREF" />
|
||||
</intent-filter>
|
||||
<intent-filter >
|
||||
<!-- Enable Health Report prune service alarm -->
|
||||
<action android:name="@ANDROID_PACKAGE_NAME@.HEALTHREPORT_PRUNE" />
|
||||
</intent-filter>
|
||||
</receiver>
|
|
@ -1,5 +0,0 @@
|
|||
<!-- So we can start our service. -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
|
||||
<!-- So we can receive messages from Fennec. -->
|
||||
<uses-permission android:name="@ANDROID_PACKAGE_NAME@.permission.PER_ANDROID_PACKAGE" />
|
|
@ -1,17 +0,0 @@
|
|||
<!-- BroadcastService responds to external events and starts
|
||||
the other background services. We don't export any of
|
||||
these services, since they are only started by components
|
||||
internal to the Fennec package.
|
||||
-->
|
||||
<service
|
||||
android:exported="false"
|
||||
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>
|
|
@ -29,3 +29,5 @@
|
|||
<permission
|
||||
android:name="@ANDROID_PACKAGE_NAME@.permission.PER_ANDROID_PACKAGE"
|
||||
android:protectionLevel="signature"/>
|
||||
|
||||
<uses-permission android:name="@ANDROID_PACKAGE_NAME@.permission.PER_ANDROID_PACKAGE" />
|
||||
|
|
|
@ -1,258 +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/. */
|
||||
|
||||
package org.mozilla.gecko.background.bagheera;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.mozilla.gecko.sync.Utils;
|
||||
import org.mozilla.gecko.sync.net.BaseResource;
|
||||
import org.mozilla.gecko.sync.net.BaseResourceDelegate;
|
||||
import org.mozilla.gecko.sync.net.Resource;
|
||||
|
||||
import ch.boye.httpclientandroidlib.HttpEntity;
|
||||
import ch.boye.httpclientandroidlib.HttpResponse;
|
||||
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
|
||||
import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
|
||||
import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
|
||||
import ch.boye.httpclientandroidlib.protocol.HTTP;
|
||||
|
||||
/**
|
||||
* Provides encapsulated access to a Bagheera document server.
|
||||
* The two permitted operations are:
|
||||
* * Delete a document.
|
||||
* * Upload a document, optionally deleting an expired document.
|
||||
*/
|
||||
public class BagheeraClient {
|
||||
|
||||
protected final String serverURI;
|
||||
protected final Executor executor;
|
||||
protected static final Pattern URI_PATTERN = Pattern.compile("^[a-zA-Z0-9_-]+$");
|
||||
|
||||
protected static String PROTOCOL_VERSION = "1.0";
|
||||
protected static String SUBMIT_PATH = "/submit/";
|
||||
|
||||
/**
|
||||
* Instantiate a new client pointing at the provided server.
|
||||
* {@link #deleteDocument(String, String, BagheeraRequestDelegate)} and
|
||||
* {@link #uploadJSONDocument(String, String, String, String, BagheeraRequestDelegate)}
|
||||
* both accept delegate arguments; the {@link Executor} provided to this
|
||||
* constructor will be used to invoke callbacks on those delegates.
|
||||
*
|
||||
* @param serverURI
|
||||
* the destination server URI.
|
||||
* @param executor
|
||||
* the executor which will be used to invoke delegate callbacks.
|
||||
*/
|
||||
public BagheeraClient(final String serverURI, final Executor executor) {
|
||||
if (serverURI == null) {
|
||||
throw new IllegalArgumentException("Must provide a server URI.");
|
||||
}
|
||||
if (executor == null) {
|
||||
throw new IllegalArgumentException("Must provide a non-null executor.");
|
||||
}
|
||||
this.serverURI = serverURI.endsWith("/") ? serverURI : serverURI + "/";
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a new client pointing at the provided server.
|
||||
* Delegate callbacks will be invoked on a new background thread.
|
||||
*
|
||||
* See {@link #BagheeraClient(String, Executor)} for more details.
|
||||
*
|
||||
* @param serverURI
|
||||
* the destination server URI.
|
||||
*/
|
||||
public BagheeraClient(final String serverURI) {
|
||||
this(serverURI, Executors.newSingleThreadExecutor());
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the specified document from the server.
|
||||
* The delegate's callbacks will be invoked by the BagheeraClient's executor.
|
||||
*/
|
||||
public void deleteDocument(final String namespace,
|
||||
final String id,
|
||||
final BagheeraRequestDelegate delegate) throws URISyntaxException {
|
||||
if (namespace == null) {
|
||||
throw new IllegalArgumentException("Must provide namespace.");
|
||||
}
|
||||
if (id == null) {
|
||||
throw new IllegalArgumentException("Must provide id.");
|
||||
}
|
||||
|
||||
final BaseResource resource = makeResource(namespace, id);
|
||||
resource.delegate = new BagheeraResourceDelegate(resource, namespace, id, delegate);
|
||||
resource.delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a JSON document to a Bagheera server. The delegate's callbacks will
|
||||
* be invoked in tasks run by the client's executor.
|
||||
*
|
||||
* @param namespace
|
||||
* the namespace, such as "test"
|
||||
* @param id
|
||||
* the document ID, which is typically a UUID.
|
||||
* @param payload
|
||||
* a document, typically JSON-encoded.
|
||||
* @param oldIDs
|
||||
* an optional collection of IDs which denote documents to supersede. Can be null or empty.
|
||||
* @param delegate
|
||||
* the delegate whose methods should be invoked on success or
|
||||
* failure.
|
||||
*/
|
||||
public void uploadJSONDocument(final String namespace,
|
||||
final String id,
|
||||
final String payload,
|
||||
Collection<String> oldIDs,
|
||||
final BagheeraRequestDelegate delegate) throws URISyntaxException {
|
||||
if (namespace == null) {
|
||||
throw new IllegalArgumentException("Must provide namespace.");
|
||||
}
|
||||
if (id == null) {
|
||||
throw new IllegalArgumentException("Must provide id.");
|
||||
}
|
||||
if (payload == null) {
|
||||
throw new IllegalArgumentException("Must provide payload.");
|
||||
}
|
||||
|
||||
final BaseResource resource = makeResource(namespace, id);
|
||||
final HttpEntity deflatedBody = DeflateHelper.deflateBody(payload);
|
||||
|
||||
resource.delegate = new BagheeraUploadResourceDelegate(resource, namespace, id, oldIDs, delegate);
|
||||
resource.post(deflatedBody);
|
||||
}
|
||||
|
||||
public static boolean isValidURIComponent(final String in) {
|
||||
return URI_PATTERN.matcher(in).matches();
|
||||
}
|
||||
|
||||
protected BaseResource makeResource(final String namespace, final String id) throws URISyntaxException {
|
||||
if (!isValidURIComponent(namespace)) {
|
||||
throw new URISyntaxException(namespace, "Illegal namespace name. Must be alphanumeric + [_-].");
|
||||
}
|
||||
|
||||
if (!isValidURIComponent(id)) {
|
||||
throw new URISyntaxException(id, "Illegal id value. Must be alphanumeric + [_-].");
|
||||
}
|
||||
|
||||
final String uri = this.serverURI + PROTOCOL_VERSION + SUBMIT_PATH +
|
||||
namespace + "/" + id;
|
||||
return new BaseResource(uri);
|
||||
}
|
||||
|
||||
public class BagheeraResourceDelegate extends BaseResourceDelegate {
|
||||
private static final int DEFAULT_SOCKET_TIMEOUT_MSEC = 5 * 60 * 1000; // Five minutes.
|
||||
protected final BagheeraRequestDelegate delegate;
|
||||
protected final String namespace;
|
||||
protected final String id;
|
||||
|
||||
public BagheeraResourceDelegate(final Resource resource,
|
||||
final String namespace,
|
||||
final String id,
|
||||
final BagheeraRequestDelegate delegate) {
|
||||
super(resource);
|
||||
this.namespace = namespace;
|
||||
this.id = id;
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUserAgent() {
|
||||
return delegate.getUserAgent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int socketTimeout() {
|
||||
return DEFAULT_SOCKET_TIMEOUT_MSEC;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleHttpResponse(HttpResponse response) {
|
||||
final int status = response.getStatusLine().getStatusCode();
|
||||
switch (status) {
|
||||
case 200:
|
||||
case 201:
|
||||
invokeHandleSuccess(status, response);
|
||||
return;
|
||||
default:
|
||||
invokeHandleFailure(status, response);
|
||||
}
|
||||
}
|
||||
|
||||
protected void invokeHandleError(final Exception e) {
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
delegate.handleError(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void invokeHandleFailure(final int status, final HttpResponse response) {
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
delegate.handleFailure(status, namespace, response);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected void invokeHandleSuccess(final int status, final HttpResponse response) {
|
||||
executor.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
delegate.handleSuccess(status, namespace, id, response);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleHttpProtocolException(final ClientProtocolException e) {
|
||||
invokeHandleError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleHttpIOException(IOException e) {
|
||||
invokeHandleError(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTransportException(GeneralSecurityException e) {
|
||||
invokeHandleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
public final class BagheeraUploadResourceDelegate extends BagheeraResourceDelegate {
|
||||
private static final String HEADER_OBSOLETE_DOCUMENT = "X-Obsolete-Document";
|
||||
private static final String COMPRESSED_CONTENT_TYPE = "application/json+zlib; charset=utf-8";
|
||||
protected final Collection<String> obsoleteDocumentIDs;
|
||||
|
||||
public BagheeraUploadResourceDelegate(Resource resource,
|
||||
String namespace,
|
||||
String id,
|
||||
Collection<String> obsoleteDocumentIDs,
|
||||
BagheeraRequestDelegate delegate) {
|
||||
super(resource, namespace, id, delegate);
|
||||
this.obsoleteDocumentIDs = obsoleteDocumentIDs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
|
||||
super.addHeaders(request, client);
|
||||
request.setHeader(HTTP.CONTENT_TYPE, COMPRESSED_CONTENT_TYPE);
|
||||
if (this.obsoleteDocumentIDs != null && this.obsoleteDocumentIDs.size() > 0) {
|
||||
request.addHeader(HEADER_OBSOLETE_DOCUMENT, Utils.toCommaSeparatedString(this.obsoleteDocumentIDs));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +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/. */
|
||||
|
||||
package org.mozilla.gecko.background.bagheera;
|
||||
|
||||
import ch.boye.httpclientandroidlib.HttpResponse;
|
||||
|
||||
public interface BagheeraRequestDelegate {
|
||||
void handleSuccess(int status, String namespace, String id, HttpResponse response);
|
||||
void handleError(Exception e);
|
||||
void handleFailure(int status, String namespace, HttpResponse response);
|
||||
|
||||
public String getUserAgent();
|
||||
}
|
|
@ -1,88 +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/. */
|
||||
|
||||
package org.mozilla.gecko.background.bagheera;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import ch.boye.httpclientandroidlib.entity.AbstractHttpEntity;
|
||||
import ch.boye.httpclientandroidlib.entity.ByteArrayEntity;
|
||||
|
||||
/**
|
||||
* An entity that acts like {@link ByteArrayEntity}, but exposes a window onto
|
||||
* the byte array that is a subsection of the array. The purpose of this is to
|
||||
* allow a smaller entity to be created without having to resize the source
|
||||
* array.
|
||||
*/
|
||||
public class BoundedByteArrayEntity extends AbstractHttpEntity implements
|
||||
Cloneable {
|
||||
protected final byte[] content;
|
||||
protected final int start;
|
||||
protected final int end;
|
||||
protected final int length;
|
||||
|
||||
/**
|
||||
* Create a new entity that behaves exactly like a {@link ByteArrayEntity}
|
||||
* created with a copy of <code>b</code> truncated to (
|
||||
* <code>end - start</code>) bytes, starting at <code>start</code>.
|
||||
*
|
||||
* @param b the byte array to use.
|
||||
* @param start the start index.
|
||||
* @param end the end index.
|
||||
*/
|
||||
public BoundedByteArrayEntity(final byte[] b, final int start, final int end) {
|
||||
if (b == null) {
|
||||
throw new IllegalArgumentException("Source byte array may not be null.");
|
||||
}
|
||||
|
||||
if (end < start ||
|
||||
start < 0 ||
|
||||
end < 0 ||
|
||||
start > b.length ||
|
||||
end > b.length) {
|
||||
throw new IllegalArgumentException("Bounds out of range.");
|
||||
}
|
||||
this.content = b;
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
this.length = end - start;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRepeatable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getContentLength() {
|
||||
return this.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getContent() {
|
||||
return new ByteArrayInputStream(this.content, this.start, this.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(final OutputStream outstream) throws IOException {
|
||||
if (outstream == null) {
|
||||
throw new IllegalArgumentException("Output stream may not be null.");
|
||||
}
|
||||
outstream.write(this.content);
|
||||
outstream.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStreaming() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object clone() throws CloneNotSupportedException {
|
||||
return super.clone();
|
||||
}
|
||||
}
|
|
@ -1,77 +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/. */
|
||||
|
||||
package org.mozilla.gecko.background.bagheera;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.zip.Deflater;
|
||||
|
||||
import ch.boye.httpclientandroidlib.HttpEntity;
|
||||
|
||||
public class DeflateHelper {
|
||||
/**
|
||||
* Conservative upper bound for zlib size, equivalent to the first few lines
|
||||
* in zlib's deflateBound function.
|
||||
*
|
||||
* Includes zlib header.
|
||||
*
|
||||
* @param sourceLen
|
||||
* the number of bytes to compress.
|
||||
* @return the number of bytes to allocate for the compressed output.
|
||||
*/
|
||||
public static int deflateBound(final int sourceLen) {
|
||||
return sourceLen + ((sourceLen + 7) >> 3) + ((sourceLen + 63) >> 6) + 5 + 6;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deflate the input into the output array, returning the number of bytes
|
||||
* written to output.
|
||||
*/
|
||||
public static int deflate(byte[] input, byte[] output) {
|
||||
final Deflater deflater = new Deflater();
|
||||
deflater.setInput(input);
|
||||
deflater.finish();
|
||||
|
||||
final int length = deflater.deflate(output);
|
||||
deflater.end();
|
||||
return length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deflate the input, returning an HttpEntity that offers an accurate window
|
||||
* on the output.
|
||||
*
|
||||
* Note that this method does not trim the output array. (Test code can use
|
||||
* TestDeflation#deflateTrimmed(byte[]).)
|
||||
*
|
||||
* Trimming would be more efficient for long-term space use, but we expect this
|
||||
* entity to be transient.
|
||||
*
|
||||
* Note also that deflate can require <b>more</b> space than the input.
|
||||
* {@link #deflateBound(int)} tells us the most it will use.
|
||||
*
|
||||
* @param bytes the input to deflate.
|
||||
* @return the deflated input as an entity.
|
||||
*/
|
||||
public static HttpEntity deflateBytes(final byte[] bytes) {
|
||||
// We would like to use DeflaterInputStream here, but it's minSDK=9, and we
|
||||
// still target 8. It would also force us to use chunked Transfer-Encoding,
|
||||
// so perhaps it's for the best!
|
||||
|
||||
final byte[] out = new byte[deflateBound(bytes.length)];
|
||||
final int outLength = deflate(bytes, out);
|
||||
return new BoundedByteArrayEntity(out, 0, outLength);
|
||||
}
|
||||
|
||||
public static HttpEntity deflateBody(final String payload) {
|
||||
final byte[] bytes;
|
||||
try {
|
||||
bytes = payload.getBytes("UTF-8");
|
||||
} catch (UnsupportedEncodingException ex) {
|
||||
// This will never happen. Thanks, Java!
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
return deflateBytes(bytes);
|
||||
}
|
||||
}
|
|
@ -23,15 +23,6 @@ public class GlobalConstants {
|
|||
|
||||
public static final int SHARED_PREFERENCES_MODE = 0;
|
||||
|
||||
// These are used to ask Fennec (via reflection) to send
|
||||
// us a pref notification. This avoids us having to guess
|
||||
// Fennec's prefs branch and pref name.
|
||||
// Eventually Fennec might listen to startup notifications and
|
||||
// do this automatically, but this will do for now. See Bug 800244.
|
||||
public static String GECKO_PREFERENCES_CLASS = "org.mozilla.gecko.preferences.GeckoPreferences";
|
||||
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;
|
||||
public static final long MILLISECONDS_PER_SIX_MONTHS = 180 * MILLISECONDS_PER_DAY;
|
||||
|
|
|
@ -1,76 +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/. */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport;
|
||||
|
||||
import org.mozilla.gecko.background.healthreport.Environment.UIType;
|
||||
import org.mozilla.gecko.background.healthreport.EnvironmentBuilder.ConfigurationProvider;
|
||||
import org.mozilla.gecko.util.HardwareUtils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.util.DisplayMetrics;
|
||||
|
||||
public class AndroidConfigurationProvider implements ConfigurationProvider {
|
||||
private static final float MILLIMETERS_PER_INCH = 25.4f;
|
||||
|
||||
private final Configuration configuration;
|
||||
private final DisplayMetrics displayMetrics;
|
||||
|
||||
public AndroidConfigurationProvider(final Context context) {
|
||||
final Resources resources = context.getResources();
|
||||
this.configuration = resources.getConfiguration();
|
||||
this.displayMetrics = resources.getDisplayMetrics();
|
||||
|
||||
HardwareUtils.init(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasHardwareKeyboard() {
|
||||
return configuration.keyboard != Configuration.KEYBOARD_NOKEYS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UIType getUIType() {
|
||||
if (HardwareUtils.isLargeTablet()) {
|
||||
return UIType.LARGE_TABLET;
|
||||
}
|
||||
|
||||
if (HardwareUtils.isSmallTablet()) {
|
||||
return UIType.SMALL_TABLET;
|
||||
}
|
||||
|
||||
return UIType.DEFAULT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getUIModeType() {
|
||||
return configuration.uiMode & Configuration.UI_MODE_TYPE_MASK;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getScreenLayoutSize() {
|
||||
return configuration.screenLayout & Configuration.SCREENLAYOUT_SIZE_MASK;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate screen horizontal width, in millimeters.
|
||||
* This is approximate, will be wrong on some devices, and
|
||||
* most likely doesn't include screen area that the app doesn't own.
|
||||
* http://stackoverflow.com/questions/2193457/is-there-a-way-to-determine-android-physical-screen-height-in-cm-or-inches
|
||||
*/
|
||||
@Override
|
||||
public int getScreenXInMM() {
|
||||
return Math.round((displayMetrics.widthPixels / displayMetrics.xdpi) * MILLIMETERS_PER_INCH);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see #getScreenXInMM() for caveats.
|
||||
*/
|
||||
@Override
|
||||
public int getScreenYInMM() {
|
||||
return Math.round((displayMetrics.heightPixels / displayMetrics.ydpi) * MILLIMETERS_PER_INCH);
|
||||
}
|
||||
}
|
|
@ -1,98 +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/. */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport;
|
||||
|
||||
/**
|
||||
* This captures all of the details that define an 'environment' for FHR's purposes.
|
||||
* Whenever this format changes, it'll be changing with a build ID, so no migration
|
||||
* of values is needed.
|
||||
*
|
||||
* Unless you remove the build descriptors from the set, of course.
|
||||
*
|
||||
* Or store these in a database.
|
||||
*
|
||||
* Instances of this class should be considered "effectively immutable": control their
|
||||
* scope such that clear creation/sharing boundaries exist. Once you've populated and
|
||||
* registered an <code>Environment</code>, don't do so again; start from scratch.
|
||||
*
|
||||
*/
|
||||
public abstract class Environment extends EnvironmentV2 {
|
||||
// Version 2 adds osLocale, appLocale, acceptLangSet, and distribution.
|
||||
// Version 3 adds device characteristics.
|
||||
public static final int CURRENT_VERSION = 3;
|
||||
|
||||
public static enum UIType {
|
||||
// Corresponds to the typical phone interface.
|
||||
DEFAULT("default"),
|
||||
|
||||
// Corresponds to a device for which Fennec is displaying the large tablet UI.
|
||||
LARGE_TABLET("largetablet"),
|
||||
|
||||
// Corresponds to a device for which Fennec is displaying the small tablet UI.
|
||||
SMALL_TABLET("smalltablet");
|
||||
|
||||
private final String label;
|
||||
|
||||
private UIType(final String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return this.label;
|
||||
}
|
||||
|
||||
public static UIType fromLabel(final String label) {
|
||||
for (UIType type : UIType.values()) {
|
||||
if (type.label.equals(label)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Bad enum value: " + label);
|
||||
}
|
||||
}
|
||||
|
||||
public UIType uiType = UIType.DEFAULT;
|
||||
|
||||
/**
|
||||
* Mask of Configuration#uiMode. E.g., UI_MODE_TYPE_CAR.
|
||||
*/
|
||||
public int uiMode = 0; // UI_MODE_TYPE_UNDEFINED = 0
|
||||
|
||||
/**
|
||||
* Computed physical dimensions in millimeters.
|
||||
*/
|
||||
public int screenXInMM;
|
||||
public int screenYInMM;
|
||||
|
||||
/**
|
||||
* One of the Configuration#SCREENLAYOUT_SIZE_* constants.
|
||||
*/
|
||||
public int screenLayout = 0; // SCREENLAYOUT_SIZE_UNDEFINED = 0
|
||||
|
||||
public boolean hasHardwareKeyboard;
|
||||
|
||||
public Environment() {
|
||||
this(Environment.HashAppender.class);
|
||||
}
|
||||
|
||||
public Environment(Class<? extends EnvironmentAppender> appenderClass) {
|
||||
super(appenderClass);
|
||||
version = CURRENT_VERSION;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void appendHash(EnvironmentAppender appender) {
|
||||
super.appendHash(appender);
|
||||
|
||||
// v3.
|
||||
appender.append(hasHardwareKeyboard ? 1 : 0);
|
||||
appender.append(uiType.toString());
|
||||
appender.append(uiMode);
|
||||
appender.append(screenLayout);
|
||||
appender.append(screenXInMM);
|
||||
appender.append(screenYInMM);
|
||||
}
|
||||
}
|
|
@ -1,189 +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/. */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.gecko.AppConstants;
|
||||
import org.mozilla.gecko.SysInfo;
|
||||
import org.mozilla.gecko.background.common.GlobalConstants;
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
import org.mozilla.gecko.background.healthreport.Environment.UIType;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* Construct a HealthReport environment from the current running system.
|
||||
*/
|
||||
public class EnvironmentBuilder {
|
||||
private static final String LOG_TAG = "GeckoEnvBuilder";
|
||||
|
||||
public static ContentProviderClient getContentProviderClient(Context context) {
|
||||
ContentResolver cr = context.getContentResolver();
|
||||
return cr.acquireContentProviderClient(HealthReportConstants.HEALTH_AUTHORITY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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>. 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.
|
||||
*/
|
||||
public static HealthReportDatabaseStorage getStorage(ContentProviderClient cpc,
|
||||
String profilePath) {
|
||||
ContentProvider pr = cpc.getLocalContentProvider();
|
||||
if (pr == null) {
|
||||
Logger.error(LOG_TAG, "Unable to retrieve local content provider. Running in a different process?");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return ((HealthReportProvider) pr).getProfileStorage(profilePath);
|
||||
} catch (ClassCastException ex) {
|
||||
Logger.error(LOG_TAG, "ContentProvider not a HealthReportProvider!", ex);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
public static interface ProfileInformationProvider {
|
||||
public boolean isBlocklistEnabled();
|
||||
public boolean isTelemetryEnabled();
|
||||
public boolean isAcceptLangUserSet();
|
||||
public long getProfileCreationTime();
|
||||
|
||||
public String getDistributionString();
|
||||
public String getOSLocale();
|
||||
public String getAppLocale();
|
||||
|
||||
public JSONObject getAddonsJSON();
|
||||
}
|
||||
|
||||
public static interface ConfigurationProvider {
|
||||
public boolean hasHardwareKeyboard();
|
||||
|
||||
public UIType getUIType();
|
||||
public int getUIModeType();
|
||||
|
||||
public int getScreenLayoutSize();
|
||||
public int getScreenXInMM();
|
||||
public int getScreenYInMM();
|
||||
}
|
||||
|
||||
protected static void populateEnvironment(Environment e,
|
||||
ProfileInformationProvider info,
|
||||
ConfigurationProvider config) {
|
||||
e.cpuCount = SysInfo.getCPUCount();
|
||||
e.memoryMB = SysInfo.getMemSize();
|
||||
|
||||
e.appName = AppConstants.MOZ_APP_NAME;
|
||||
e.appID = AppConstants.MOZ_APP_ID;
|
||||
e.appVersion = AppConstants.MOZ_APP_VERSION;
|
||||
e.appBuildID = AppConstants.MOZ_APP_BUILDID;
|
||||
e.updateChannel = AppConstants.MOZ_UPDATE_CHANNEL;
|
||||
e.vendor = AppConstants.MOZ_APP_VENDOR;
|
||||
e.platformVersion = AppConstants.MOZILLA_VERSION;
|
||||
e.platformBuildID = AppConstants.MOZ_APP_BUILDID;
|
||||
e.xpcomabi = AppConstants.TARGET_XPCOM_ABI;
|
||||
e.os = "Android";
|
||||
e.architecture = SysInfo.getArchABI(); // Not just "arm".
|
||||
e.sysName = SysInfo.getName();
|
||||
e.sysVersion = SysInfo.getReleaseVersion();
|
||||
|
||||
e.profileCreation = (int) (info.getProfileCreationTime() / GlobalConstants.MILLISECONDS_PER_DAY);
|
||||
|
||||
// Corresponds to Gecko pref "extensions.blocklist.enabled".
|
||||
e.isBlocklistEnabled = (info.isBlocklistEnabled() ? 1 : 0);
|
||||
|
||||
// Corresponds to Gecko pref "toolkit.telemetry.enabled".
|
||||
e.isTelemetryEnabled = (info.isTelemetryEnabled() ? 1 : 0);
|
||||
|
||||
e.extensionCount = 0;
|
||||
e.pluginCount = 0;
|
||||
e.themeCount = 0;
|
||||
|
||||
JSONObject addons = info.getAddonsJSON();
|
||||
if (addons != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Iterator<String> it = addons.keys();
|
||||
while (it.hasNext()) {
|
||||
String key = it.next();
|
||||
try {
|
||||
JSONObject addon = addons.getJSONObject(key);
|
||||
String type = addon.optString("type");
|
||||
Logger.pii(LOG_TAG, "Add-on " + key + " is a " + type);
|
||||
if ("extension".equals(type)) {
|
||||
++e.extensionCount;
|
||||
} else if ("plugin".equals(type)) {
|
||||
++e.pluginCount;
|
||||
} else if ("theme".equals(type)) {
|
||||
++e.themeCount;
|
||||
} else if ("service".equals(type)) {
|
||||
// Later.
|
||||
} else {
|
||||
Logger.debug(LOG_TAG, "Unknown add-on type: " + type);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Logger.warn(LOG_TAG, "Failed to process add-on " + key, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
e.addons = addons;
|
||||
|
||||
// v2 environment fields.
|
||||
e.distribution = info.getDistributionString();
|
||||
e.osLocale = info.getOSLocale();
|
||||
e.appLocale = info.getAppLocale();
|
||||
e.acceptLangSet = info.isAcceptLangUserSet() ? 1 : 0;
|
||||
|
||||
// v3 environment fields.
|
||||
e.hasHardwareKeyboard = config.hasHardwareKeyboard();
|
||||
e.uiType = config.getUIType();
|
||||
e.uiMode = config.getUIModeType();
|
||||
e.screenLayout = config.getScreenLayoutSize();
|
||||
e.screenXInMM = config.getScreenXInMM();
|
||||
e.screenYInMM = config.getScreenYInMM();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@link Environment} not linked to a storage instance, but
|
||||
* populated with current field values.
|
||||
*
|
||||
* @param info a source of profile data
|
||||
* @return the new {@link Environment}
|
||||
*/
|
||||
public static Environment getCurrentEnvironment(ProfileInformationProvider info, ConfigurationProvider config) {
|
||||
Environment e = new Environment() {
|
||||
@Override
|
||||
public int register() {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
populateEnvironment(e, info, config);
|
||||
return e;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the current environment's ID in the provided storage layer
|
||||
*/
|
||||
public static int registerCurrentEnvironment(final HealthReportStorage storage,
|
||||
final ProfileInformationProvider info,
|
||||
final ConfigurationProvider config) {
|
||||
Environment e = storage.getEnvironment();
|
||||
populateEnvironment(e, info, config);
|
||||
e.register();
|
||||
Logger.debug(LOG_TAG, "Registering current environment: " + e.getHash() + " = " + e.id);
|
||||
return e.id;
|
||||
}
|
||||
}
|
|
@ -1,270 +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/. */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Iterator;
|
||||
import java.util.SortedSet;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.apache.commons.codec.binary.Base64;
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
import org.mozilla.gecko.background.nativecode.NativeCrypto;
|
||||
|
||||
public abstract class EnvironmentV1 {
|
||||
private static final String LOG_TAG = "GeckoEnvironment";
|
||||
private static final int VERSION = 1;
|
||||
|
||||
protected final Class<? extends EnvironmentAppender> appenderClass;
|
||||
|
||||
protected volatile String hash = null;
|
||||
protected volatile int id = -1;
|
||||
|
||||
public int version = VERSION;
|
||||
|
||||
// org.mozilla.profile.age.
|
||||
public int profileCreation;
|
||||
|
||||
// org.mozilla.sysinfo.sysinfo.
|
||||
public int cpuCount;
|
||||
public int memoryMB;
|
||||
public String architecture;
|
||||
public String sysName;
|
||||
public String sysVersion; // Kernel.
|
||||
|
||||
// geckoAppInfo.
|
||||
public String vendor;
|
||||
public String appName;
|
||||
public String appID;
|
||||
public String appVersion;
|
||||
public String appBuildID;
|
||||
public String platformVersion;
|
||||
public String platformBuildID;
|
||||
public String os;
|
||||
public String xpcomabi;
|
||||
public String updateChannel;
|
||||
|
||||
// appinfo.
|
||||
public int isBlocklistEnabled;
|
||||
public int isTelemetryEnabled;
|
||||
|
||||
// org.mozilla.addons.active.
|
||||
public JSONObject addons = null;
|
||||
|
||||
// org.mozilla.addons.counts.
|
||||
public int extensionCount;
|
||||
public int pluginCount;
|
||||
public int themeCount;
|
||||
|
||||
/**
|
||||
* We break out this interface in order to allow for testing -- pass in your
|
||||
* own appender that just records strings, for example.
|
||||
*/
|
||||
public static abstract class EnvironmentAppender {
|
||||
public abstract void append(String s);
|
||||
public abstract void append(int v);
|
||||
}
|
||||
|
||||
public static class HashAppender extends EnvironmentAppender {
|
||||
private final StringBuilder builder;
|
||||
|
||||
public HashAppender() throws NoSuchAlgorithmException {
|
||||
builder = new StringBuilder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void append(String s) {
|
||||
builder.append((s == null) ? "null" : s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void append(int profileCreation) {
|
||||
append(Integer.toString(profileCreation, 10));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
// We *could* use ASCII85… but the savings would be negated by the
|
||||
// inclusion of JSON-unsafe characters like double-quote.
|
||||
final byte[] inputBytes;
|
||||
try {
|
||||
inputBytes = builder.toString().getBytes("UTF-8");
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
Logger.warn(LOG_TAG, "Invalid charset String passed to getBytes", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Note to the security-minded reader: we deliberately use SHA-1 here, not
|
||||
// a stronger hash. These identifiers don't strictly need a cryptographic
|
||||
// hash function, because there is negligible value in attacking the hash.
|
||||
// We use SHA-1 because it's *shorter* -- the exact same reason that Git
|
||||
// chose SHA-1.
|
||||
final byte[] hash = NativeCrypto.sha1(inputBytes);
|
||||
return new Base64(-1, null, false).encodeAsString(hash);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the {@link Environment} has been registered with its
|
||||
* storage layer, and can be used to annotate events.
|
||||
*
|
||||
* It's safe to call this method more than once, and each time you'll
|
||||
* get the same ID.
|
||||
*
|
||||
* @return the integer ID to use in subsequent DB insertions.
|
||||
*/
|
||||
public abstract int register();
|
||||
|
||||
protected EnvironmentAppender getAppender() {
|
||||
EnvironmentAppender appender = null;
|
||||
try {
|
||||
appender = appenderClass.newInstance();
|
||||
} catch (InstantiationException | IllegalAccessException ex) {
|
||||
// Should never happen, but...
|
||||
Logger.warn(LOG_TAG, "Could not compute hash.", ex);
|
||||
}
|
||||
|
||||
return appender;
|
||||
}
|
||||
|
||||
protected void appendHash(EnvironmentAppender appender) {
|
||||
appender.append(profileCreation);
|
||||
appender.append(cpuCount);
|
||||
appender.append(memoryMB);
|
||||
appender.append(architecture);
|
||||
appender.append(sysName);
|
||||
appender.append(sysVersion);
|
||||
appender.append(vendor);
|
||||
appender.append(appName);
|
||||
appender.append(appID);
|
||||
appender.append(appVersion);
|
||||
appender.append(appBuildID);
|
||||
appender.append(platformVersion);
|
||||
appender.append(platformBuildID);
|
||||
appender.append(os);
|
||||
appender.append(xpcomabi);
|
||||
appender.append(updateChannel);
|
||||
appender.append(isBlocklistEnabled);
|
||||
appender.append(isTelemetryEnabled);
|
||||
appender.append(extensionCount);
|
||||
appender.append(pluginCount);
|
||||
appender.append(themeCount);
|
||||
|
||||
// We need sorted values.
|
||||
if (addons != null) {
|
||||
appendSortedAddons(getNonIgnoredAddons(), appender);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the stable hash of the configured environment.
|
||||
*
|
||||
* @return the hash in base34, or null if there was a problem.
|
||||
*/
|
||||
public String getHash() {
|
||||
// It's never unset, so we only care about partial reads. volatile is enough.
|
||||
if (hash != null) {
|
||||
return hash;
|
||||
}
|
||||
|
||||
EnvironmentAppender appender = getAppender();
|
||||
if (appender == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
appendHash(appender);
|
||||
return hash = appender.toString();
|
||||
}
|
||||
|
||||
public EnvironmentV1(Class<? extends EnvironmentAppender> appenderClass) {
|
||||
super();
|
||||
this.appenderClass = appenderClass;
|
||||
}
|
||||
|
||||
public JSONObject getNonIgnoredAddons() {
|
||||
if (addons == null) {
|
||||
return null;
|
||||
}
|
||||
JSONObject out = new JSONObject();
|
||||
@SuppressWarnings("unchecked")
|
||||
Iterator<String> keys = addons.keys();
|
||||
while (keys.hasNext()) {
|
||||
try {
|
||||
final String key = keys.next();
|
||||
final Object obj = addons.get(key);
|
||||
if (obj != null &&
|
||||
obj instanceof JSONObject &&
|
||||
((JSONObject) obj).optBoolean("ignore", false)) {
|
||||
continue;
|
||||
}
|
||||
out.put(key, obj);
|
||||
} catch (JSONException ex) {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Take a collection of add-on descriptors, appending a consistent string
|
||||
* to the provided builder.
|
||||
*/
|
||||
public static void appendSortedAddons(JSONObject addons, final EnvironmentAppender builder) {
|
||||
final SortedSet<String> keys = HealthReportUtils.sortedKeySet(addons);
|
||||
|
||||
// For each add-on, produce a consistent, sorted mapping of its descriptor.
|
||||
for (String key : keys) {
|
||||
try {
|
||||
JSONObject addon = addons.getJSONObject(key);
|
||||
|
||||
// Now produce the output for this add-on.
|
||||
builder.append(key);
|
||||
builder.append("={");
|
||||
|
||||
for (String addonKey : HealthReportUtils.sortedKeySet(addon)) {
|
||||
builder.append(addonKey);
|
||||
builder.append("==");
|
||||
try {
|
||||
builder.append(addon.get(addonKey).toString());
|
||||
} catch (JSONException e) {
|
||||
builder.append("_e_");
|
||||
}
|
||||
}
|
||||
|
||||
builder.append("}");
|
||||
} catch (Exception e) {
|
||||
// Muffle.
|
||||
Logger.warn(LOG_TAG, "Invalid add-on for ID " + key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setJSONForAddons(byte[] json) throws Exception {
|
||||
setJSONForAddons(new String(json, "UTF-8"));
|
||||
}
|
||||
|
||||
public void setJSONForAddons(String json) throws Exception {
|
||||
if (json == null || "null".equals(json)) {
|
||||
addons = null;
|
||||
return;
|
||||
}
|
||||
addons = new JSONObject(json);
|
||||
}
|
||||
|
||||
public void setJSONForAddons(JSONObject json) {
|
||||
addons = json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Includes ignored add-ons.
|
||||
*/
|
||||
public String getNormalizedAddonsJSON() {
|
||||
// We trust that our input will already be normalized. If that assumption
|
||||
// is invalidated, then we'll be sorry.
|
||||
return (addons == null) ? "null" : addons.toString();
|
||||
}
|
||||
}
|
|
@ -1,30 +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/. */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport;
|
||||
|
||||
public abstract class EnvironmentV2 extends EnvironmentV1 {
|
||||
private static final int VERSION = 2;
|
||||
|
||||
public String osLocale;
|
||||
public String appLocale;
|
||||
public int acceptLangSet;
|
||||
public String distribution;
|
||||
|
||||
public EnvironmentV2(Class<? extends EnvironmentAppender> appenderClass) {
|
||||
super(appenderClass);
|
||||
version = VERSION;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void appendHash(EnvironmentAppender appender) {
|
||||
super.appendHash(appender);
|
||||
|
||||
// v2.
|
||||
appender.append(osLocale);
|
||||
appender.append(appLocale);
|
||||
appender.append(acceptLangSet);
|
||||
appender.append(distribution);
|
||||
}
|
||||
}
|
|
@ -1,31 +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/. */
|
||||
|
||||
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 internal notifications to start Health Report background services.
|
||||
*/
|
||||
public class HealthReportBroadcastReceiver extends BroadcastReceiver {
|
||||
public static final String LOG_TAG = HealthReportBroadcastReceiver.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* Forward the intent (action and extras) 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);
|
||||
// It's safe to forward extras since these are internal intents.
|
||||
service.putExtras(intent);
|
||||
service.setAction(intent.getAction());
|
||||
context.startService(service);
|
||||
}
|
||||
}
|
|
@ -1,260 +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/. */
|
||||
|
||||
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);
|
||||
|
||||
// Intent can be null. Bug 1025937.
|
||||
if (intent == null) {
|
||||
Logger.debug(LOG_TAG, "Short-circuiting on null intent.");
|
||||
return;
|
||||
}
|
||||
|
||||
// The same intent can be handled by multiple methods so do not short-circuit evaluate.
|
||||
boolean handled = attemptHandleIntentForUpload(intent);
|
||||
handled = attemptHandleIntentForPrune(intent) || 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.
|
||||
*
|
||||
* @param intent must be non-null.
|
||||
*/
|
||||
private 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; attempting to " +
|
||||
"handle 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.
|
||||
*
|
||||
* @param intent must be non-null.
|
||||
*/
|
||||
private 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.
|
||||
*
|
||||
* @param intent must be non-null.
|
||||
*/
|
||||
private boolean attemptHandleIntentForPrune(final Intent intent) {
|
||||
final String action = intent.getAction();
|
||||
Logger.debug(LOG_TAG, "Prune: Attempting to handle 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param intent must be non-null.
|
||||
*/
|
||||
private 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);
|
||||
}
|
||||
}
|
|
@ -1,128 +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/. */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport;
|
||||
|
||||
import org.mozilla.gecko.background.common.GlobalConstants;
|
||||
import org.mozilla.gecko.AppConstants;
|
||||
|
||||
public class HealthReportConstants {
|
||||
public static final String HEALTH_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".health";
|
||||
public static final String GLOBAL_LOG_TAG = "GeckoHealth";
|
||||
|
||||
public static final String USER_AGENT = "Firefox-Android-HealthReport/" + AppConstants.MOZ_APP_VERSION + " (" + AppConstants.MOZ_APP_UA_NAME + ")";
|
||||
|
||||
/**
|
||||
* The earliest allowable value for the last ping time, corresponding to May 2nd 2013.
|
||||
* Used for sanity checks.
|
||||
*/
|
||||
public static final long EARLIEST_LAST_PING = 1367500000000L;
|
||||
|
||||
// Not `final` so we have the option to turn this on at runtime with a magic addon.
|
||||
public static boolean UPLOAD_FEATURE_DISABLED = false;
|
||||
|
||||
// Android SharedPreferences branch where global (not per-profile) uploader
|
||||
// settings are stored.
|
||||
public static final String PREFS_BRANCH = "background";
|
||||
|
||||
// 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 = AppConstants.ANDROID_PACKAGE_NAME + ".HEALTHREPORT_UPLOAD_PREF";
|
||||
public static final String ACTION_HEALTHREPORT_PRUNE = AppConstants.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;
|
||||
|
||||
public static final String PREF_MINIMUM_TIME_BEFORE_FIRST_SUBMISSION = "healthreport_time_before_first_submission";
|
||||
public static final long DEFAULT_MINIMUM_TIME_BEFORE_FIRST_SUBMISSION = GlobalConstants.MILLISECONDS_PER_DAY;
|
||||
|
||||
public static final String PREF_MINIMUM_TIME_AFTER_FAILURE = "healthreport_time_after_failure";
|
||||
public static final long DEFAULT_MINIMUM_TIME_AFTER_FAILURE = DEFAULT_SUBMISSION_INTENT_INTERVAL_MSEC;
|
||||
|
||||
public static final String PREF_MAXIMUM_FAILURES_PER_DAY = "healthreport_maximum_failures_per_day";
|
||||
public static final long DEFAULT_MAXIMUM_FAILURES_PER_DAY = 2;
|
||||
|
||||
// Authoritative.
|
||||
public static final String PREF_FIRST_RUN = "healthreport_first_run";
|
||||
public static final String PREF_NEXT_SUBMISSION = "healthreport_next_submission";
|
||||
public static final String PREF_CURRENT_DAY_FAILURE_COUNT = "healthreport_current_day_failure_count";
|
||||
public static final String PREF_CURRENT_DAY_RESET_TIME = "healthreport_current_day_reset_time";
|
||||
|
||||
// Forensic.
|
||||
public static final String PREF_LAST_UPLOAD_REQUESTED = "healthreport_last_upload_requested";
|
||||
public static final String PREF_LAST_UPLOAD_SUCCEEDED = "healthreport_last_upload_succeeded";
|
||||
public static final String PREF_LAST_UPLOAD_FAILED = "healthreport_last_upload_failed";
|
||||
|
||||
// Preferences for deleting obsolete documents.
|
||||
public static final String PREF_MINIMUM_TIME_BETWEEN_DELETES = "healthreport_time_between_deletes";
|
||||
public static final long DEFAULT_MINIMUM_TIME_BETWEEN_DELETES = DEFAULT_SUBMISSION_INTENT_INTERVAL_MSEC;
|
||||
|
||||
public static final String PREF_OBSOLETE_DOCUMENT_IDS_TO_DELETION_ATTEMPTS_REMAINING = "healthreport_obsolete_document_ids_to_deletions_remaining";
|
||||
|
||||
// We don't want to try to delete forever, but we also don't want to orphan
|
||||
// obsolete document IDs from devices that fail to reach the server for a few
|
||||
// days. This tries to delete document IDs for at least one week (of upload
|
||||
// failures). Note that if the device is really offline, no upload is
|
||||
// performed and our count of attempts is not altered.
|
||||
public static final long DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID = (DEFAULT_MAXIMUM_FAILURES_PER_DAY + 1) * 7;
|
||||
|
||||
// If we absolutely know that a document ID reached the server, we really
|
||||
// don't want to orphan it. This tries to delete document IDs that will
|
||||
// definitely be orphaned for at least six weeks (of upload failures). Note
|
||||
// that if the device is really offline, no upload is performed and our count
|
||||
// of attempts is not altered.
|
||||
public static final long DELETION_ATTEMPTS_PER_KNOWN_TO_BE_ON_SERVER_DOCUMENT_ID = (DEFAULT_MAXIMUM_FAILURES_PER_DAY + 1) * 7 * 6;
|
||||
|
||||
// We don't want to allocate unbounded storage for obsolete IDs, but we also
|
||||
// don't want to orphan obsolete document IDs from devices that fail to delete
|
||||
// for a few days. This stores as many IDs as are expected to be generated in
|
||||
// a month. Note that if the device is really offline, no upload is performed
|
||||
// and our count of attempts is not altered.
|
||||
public static final long MAXIMUM_STORED_OBSOLETE_DOCUMENT_IDS = (DEFAULT_MAXIMUM_FAILURES_PER_DAY + 1) * 30;
|
||||
|
||||
// Forensic.
|
||||
public static final String PREF_LAST_DELETE_REQUESTED = "healthreport_last_delete_requested";
|
||||
public static final String PREF_LAST_DELETE_SUCCEEDED = "healthreport_last_delete_succeeded";
|
||||
public static final String PREF_LAST_DELETE_FAILED = "healthreport_last_delete_failed";
|
||||
|
||||
// Preferences for upload client.
|
||||
public static final String PREF_LAST_UPLOAD_LOCAL_TIME = "healthreport_last_upload_local_time";
|
||||
public static final String PREF_LAST_UPLOAD_DOCUMENT_ID = "healthreport_last_upload_document_id";
|
||||
|
||||
public static final String PREF_DOCUMENT_SERVER_URI = "healthreport_document_server_uri";
|
||||
public static final String DEFAULT_DOCUMENT_SERVER_URI = "https://fhr.data.mozilla.com/";
|
||||
|
||||
public static final String PREF_DOCUMENT_SERVER_NAMESPACE = "healthreport_document_server_namespace";
|
||||
public static final String DEFAULT_DOCUMENT_SERVER_NAMESPACE = "metrics";
|
||||
|
||||
// One UUID is 36 characters (like e56542e0-e4d2-11e2-a28f-0800200c9a66), so
|
||||
// we limit the number of obsolete IDs passed so that each request is not a
|
||||
// large upload (and therefore more likely to fail). We also don't want to
|
||||
// push Bagheera to make too many deletes, since we don't know how the cluster
|
||||
// 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;
|
||||
}
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -1,53 +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/. */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
/**
|
||||
* Manages a set of per-profile Health Report storage helpers.
|
||||
*/
|
||||
public class HealthReportDatabases {
|
||||
private static final String LOG_TAG = "HealthReportDatabases";
|
||||
|
||||
private final Context context;
|
||||
private final HashMap<File, HealthReportDatabaseStorage> storages = new HashMap<File, HealthReportDatabaseStorage>();
|
||||
|
||||
|
||||
public HealthReportDatabases(final Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public synchronized HealthReportDatabaseStorage getDatabaseHelperForProfile(final File profileDir) {
|
||||
if (profileDir == null) {
|
||||
throw new IllegalArgumentException("No profile provided.");
|
||||
}
|
||||
|
||||
if (this.storages.containsKey(profileDir)) {
|
||||
return this.storages.get(profileDir);
|
||||
}
|
||||
|
||||
final HealthReportDatabaseStorage helper;
|
||||
helper = new HealthReportDatabaseStorage(this.context, profileDir);
|
||||
this.storages.put(profileDir, helper);
|
||||
return helper;
|
||||
}
|
||||
|
||||
public synchronized void closeDatabaseHelpers() {
|
||||
for (HealthReportDatabaseStorage helper : storages.values()) {
|
||||
try {
|
||||
helper.close();
|
||||
} catch (Exception e) {
|
||||
Logger.warn(LOG_TAG, "Failed to close database helper.", e);
|
||||
}
|
||||
}
|
||||
storages.clear();
|
||||
}
|
||||
}
|
|
@ -1,42 +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/. */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
|
||||
/**
|
||||
* Watch for external system 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 HealthReportExportedBroadcastReceiver extends BroadcastReceiver {
|
||||
public static final String LOG_TAG = HealthReportExportedBroadcastReceiver.class.getSimpleName();
|
||||
|
||||
/**
|
||||
* Forward the intent action to an IntentService to do background processing.
|
||||
* We intentionally do not forward extras, since there are none needed from
|
||||
* external events.
|
||||
*/
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
Logger.debug(LOG_TAG, "Received intent - forwarding to BroadcastService.");
|
||||
final Intent service = new Intent(context, HealthReportBroadcastService.class);
|
||||
// We intentionally copy only the intent action.
|
||||
service.setAction(intent.getAction());
|
||||
context.startService(service);
|
||||
}
|
||||
}
|
|
@ -1,711 +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/. */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.gecko.background.common.DateUtils.DateFormatter;
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
import org.mozilla.gecko.background.healthreport.EnvironmentBuilder.ConfigurationProvider;
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.util.SparseArray;
|
||||
|
||||
public class HealthReportGenerator {
|
||||
private static final int PAYLOAD_VERSION = 3;
|
||||
|
||||
private static final String LOG_TAG = "GeckoHealthGen";
|
||||
|
||||
private final HealthReportStorage storage;
|
||||
private final DateFormatter dateFormatter;
|
||||
|
||||
public HealthReportGenerator(HealthReportStorage storage) {
|
||||
this.storage = storage;
|
||||
this.dateFormatter = new DateFormatter();
|
||||
}
|
||||
|
||||
@SuppressWarnings("static-method")
|
||||
protected long now() {
|
||||
return System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that you have initialized the Locale to your satisfaction
|
||||
* prior to calling this method.
|
||||
*
|
||||
* @return null if no environment could be computed, or else the resulting document.
|
||||
* @throws JSONException if there was an error adding environment data to the resulting document.
|
||||
*/
|
||||
public JSONObject generateDocument(long since, long lastPingTime, String profilePath, ConfigurationProvider config) throws JSONException {
|
||||
Logger.info(LOG_TAG, "Generating FHR document from " + since + "; last ping " + lastPingTime);
|
||||
Logger.pii(LOG_TAG, "Generating for profile " + profilePath);
|
||||
|
||||
ProfileInformationCache cache = new ProfileInformationCache(profilePath);
|
||||
if (!cache.restoreUnlessInitialized()) {
|
||||
Logger.warn(LOG_TAG, "Not enough profile information to compute current environment.");
|
||||
return null;
|
||||
}
|
||||
|
||||
Environment current = EnvironmentBuilder.getCurrentEnvironment(cache, config);
|
||||
return generateDocument(since, lastPingTime, current);
|
||||
}
|
||||
|
||||
/**
|
||||
* The document consists of:
|
||||
*
|
||||
*<ul>
|
||||
*<li>Basic metadata: last ping time, current ping time, version.</li>
|
||||
*<li>A map of environments: <code>current</code> and others named by hash. <code>current</code> is fully specified,
|
||||
* and others are deltas from current.</li>
|
||||
*<li>A <code>data</code> object. This includes <code>last</code> and <code>days</code>.</li>
|
||||
*</ul>
|
||||
*
|
||||
* <code>days</code> is a map from date strings to <tt>{hash: {measurement: {_v: version, fields...}}}</tt>.
|
||||
* @throws JSONException if there was an error adding environment data to the resulting document.
|
||||
*/
|
||||
public JSONObject generateDocument(long since, long lastPingTime, Environment currentEnvironment) throws JSONException {
|
||||
final String currentHash = currentEnvironment.getHash();
|
||||
|
||||
Logger.debug(LOG_TAG, "Current environment hash: " + currentHash);
|
||||
if (currentHash == null) {
|
||||
Logger.warn(LOG_TAG, "Current hash is null; aborting.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// We want to map field IDs to some strings as we go.
|
||||
SparseArray<Environment> envs = storage.getEnvironmentRecordsByID();
|
||||
|
||||
JSONObject document = new JSONObject();
|
||||
|
||||
if (lastPingTime >= HealthReportConstants.EARLIEST_LAST_PING) {
|
||||
document.put("lastPingDate", dateFormatter.getDateString(lastPingTime));
|
||||
}
|
||||
|
||||
document.put("thisPingDate", dateFormatter.getDateString(now()));
|
||||
document.put("version", PAYLOAD_VERSION);
|
||||
|
||||
document.put("environments", getEnvironmentsJSON(currentEnvironment, envs));
|
||||
document.put("data", getDataJSON(currentEnvironment, envs, since));
|
||||
|
||||
return document;
|
||||
}
|
||||
|
||||
protected JSONObject getDataJSON(Environment currentEnvironment,
|
||||
SparseArray<Environment> envs, long since) throws JSONException {
|
||||
SparseArray<Field> fields = storage.getFieldsByID();
|
||||
|
||||
JSONObject days = getDaysJSON(currentEnvironment, envs, fields, since);
|
||||
|
||||
JSONObject last = new JSONObject();
|
||||
|
||||
JSONObject data = new JSONObject();
|
||||
data.put("days", days);
|
||||
data.put("last", last);
|
||||
return data;
|
||||
}
|
||||
|
||||
protected JSONObject getDaysJSON(Environment currentEnvironment, SparseArray<Environment> envs, SparseArray<Field> fields, long since) throws JSONException {
|
||||
if (Logger.shouldLogVerbose(LOG_TAG)) {
|
||||
for (int i = 0; i < envs.size(); ++i) {
|
||||
Logger.trace(LOG_TAG, "Days environment " + envs.keyAt(i) + ": " + envs.get(envs.keyAt(i)).getHash());
|
||||
}
|
||||
}
|
||||
|
||||
JSONObject days = new JSONObject();
|
||||
Cursor cursor = storage.getRawEventsSince(since);
|
||||
try {
|
||||
if (!cursor.moveToFirst()) {
|
||||
return days;
|
||||
}
|
||||
|
||||
// A classic walking partition.
|
||||
// Columns are "date", "env", "field", "value".
|
||||
// Note that we care about the type (integer, string) and kind
|
||||
// (last/counter, discrete) of each field.
|
||||
// Each field will be accessed once for each date/env pair, so
|
||||
// Field memoizes these facts.
|
||||
// We also care about which measurement contains each field.
|
||||
int lastDate = -1;
|
||||
int lastEnv = -1;
|
||||
JSONObject dateObject = null;
|
||||
JSONObject envObject = null;
|
||||
|
||||
while (!cursor.isAfterLast()) {
|
||||
int cEnv = cursor.getInt(1);
|
||||
if (cEnv == -1 ||
|
||||
(cEnv != lastEnv &&
|
||||
envs.indexOfKey(cEnv) < 0)) {
|
||||
Logger.warn(LOG_TAG, "Invalid environment " + cEnv + " in cursor. Skipping.");
|
||||
cursor.moveToNext();
|
||||
continue;
|
||||
}
|
||||
|
||||
int cDate = cursor.getInt(0);
|
||||
int cField = cursor.getInt(2);
|
||||
|
||||
Logger.trace(LOG_TAG, "Event row: " + cDate + ", " + cEnv + ", " + cField);
|
||||
boolean dateChanged = cDate != lastDate;
|
||||
boolean envChanged = cEnv != lastEnv;
|
||||
|
||||
if (dateChanged) {
|
||||
if (dateObject != null) {
|
||||
days.put(dateFormatter.getDateStringForDay(lastDate), dateObject);
|
||||
}
|
||||
dateObject = new JSONObject();
|
||||
lastDate = cDate;
|
||||
}
|
||||
|
||||
if (dateChanged || envChanged) {
|
||||
envObject = new JSONObject();
|
||||
// This is safe because we checked above that cEnv is valid.
|
||||
dateObject.put(envs.get(cEnv).getHash(), envObject);
|
||||
lastEnv = cEnv;
|
||||
}
|
||||
|
||||
final Field field = fields.get(cField);
|
||||
JSONObject measurement = envObject.optJSONObject(field.measurementName);
|
||||
if (measurement == null) {
|
||||
// We will never have more than one measurement version within a
|
||||
// single environment -- to do so involves changing the build ID. And
|
||||
// even if we did, we have no way to represent it. So just build the
|
||||
// output object once.
|
||||
measurement = new JSONObject();
|
||||
measurement.put("_v", field.measurementVersion);
|
||||
envObject.put(field.measurementName, measurement);
|
||||
}
|
||||
|
||||
// How we record depends on the type of the field, so we
|
||||
// break this out into a separate method for clarity.
|
||||
recordMeasurementFromCursor(field, measurement, cursor);
|
||||
|
||||
cursor.moveToNext();
|
||||
continue;
|
||||
}
|
||||
days.put(dateFormatter.getDateStringForDay(lastDate), dateObject);
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link JSONObject} parsed from the provided index of the given
|
||||
* cursor, or {@link JSONObject#NULL} if either SQL <code>NULL</code> or
|
||||
* string <code>"null"</code> is present at that index.
|
||||
*/
|
||||
private static Object getJSONAtIndex(Cursor cursor, int index) throws JSONException {
|
||||
if (cursor.isNull(index)) {
|
||||
return JSONObject.NULL;
|
||||
}
|
||||
final String value = cursor.getString(index);
|
||||
if ("null".equals(value)) {
|
||||
return JSONObject.NULL;
|
||||
}
|
||||
return new JSONObject(value);
|
||||
}
|
||||
|
||||
protected static void recordMeasurementFromCursor(final Field field,
|
||||
JSONObject measurement,
|
||||
Cursor cursor)
|
||||
throws JSONException {
|
||||
if (field.isDiscreteField()) {
|
||||
// Discrete counted. Increment the named counter.
|
||||
if (field.isCountedField()) {
|
||||
if (!field.isStringField()) {
|
||||
throw new IllegalStateException("Unable to handle non-string counted types.");
|
||||
}
|
||||
HealthReportUtils.count(measurement, field.fieldName, cursor.getString(3));
|
||||
return;
|
||||
}
|
||||
|
||||
// Discrete string or integer. Append it.
|
||||
if (field.isStringField()) {
|
||||
HealthReportUtils.append(measurement, field.fieldName, cursor.getString(3));
|
||||
return;
|
||||
}
|
||||
if (field.isJSONField()) {
|
||||
HealthReportUtils.append(measurement, field.fieldName, getJSONAtIndex(cursor, 3));
|
||||
return;
|
||||
}
|
||||
if (field.isIntegerField()) {
|
||||
HealthReportUtils.append(measurement, field.fieldName, cursor.getLong(3));
|
||||
return;
|
||||
}
|
||||
throw new IllegalStateException("Unknown field type: " + field.flags);
|
||||
}
|
||||
|
||||
// Non-discrete -- must be LAST or COUNTER, so just accumulate the value.
|
||||
if (field.isStringField()) {
|
||||
measurement.put(field.fieldName, cursor.getString(3));
|
||||
return;
|
||||
}
|
||||
if (field.isJSONField()) {
|
||||
measurement.put(field.fieldName, getJSONAtIndex(cursor, 3));
|
||||
return;
|
||||
}
|
||||
measurement.put(field.fieldName, cursor.getLong(3));
|
||||
}
|
||||
|
||||
public static JSONObject getEnvironmentsJSON(Environment currentEnvironment,
|
||||
SparseArray<Environment> envs) throws JSONException {
|
||||
JSONObject environments = new JSONObject();
|
||||
|
||||
// Always do this, even if it hasn't recorded anything in the DB.
|
||||
environments.put("current", jsonify(currentEnvironment, null));
|
||||
|
||||
String currentHash = currentEnvironment.getHash();
|
||||
for (int i = 0; i < envs.size(); i++) {
|
||||
Environment e = envs.valueAt(i);
|
||||
if (currentHash.equals(e.getHash())) {
|
||||
continue;
|
||||
}
|
||||
environments.put(e.getHash(), jsonify(e, currentEnvironment));
|
||||
}
|
||||
return environments;
|
||||
}
|
||||
|
||||
public static JSONObject jsonify(Environment e, Environment current) throws JSONException {
|
||||
JSONObject age = getProfileAge(e, current);
|
||||
JSONObject sysinfo = getSysInfo(e, current);
|
||||
JSONObject gecko = getGeckoInfo(e, current);
|
||||
JSONObject appinfo = getAppInfo(e, current);
|
||||
JSONObject counts = getAddonCounts(e, current);
|
||||
JSONObject config = getDeviceConfig(e, current);
|
||||
|
||||
JSONObject out = new JSONObject();
|
||||
if (age != null)
|
||||
out.put("org.mozilla.profile.age", age);
|
||||
if (sysinfo != null)
|
||||
out.put("org.mozilla.sysinfo.sysinfo", sysinfo);
|
||||
if (gecko != null)
|
||||
out.put("geckoAppInfo", gecko);
|
||||
if (appinfo != null)
|
||||
out.put("org.mozilla.appInfo.appinfo", appinfo);
|
||||
if (counts != null)
|
||||
out.put("org.mozilla.addons.counts", counts);
|
||||
|
||||
JSONObject active = getActiveAddons(e, current);
|
||||
if (active != null)
|
||||
out.put("org.mozilla.addons.active", active);
|
||||
|
||||
if (config != null)
|
||||
out.put("org.mozilla.device.config", config);
|
||||
|
||||
if (current == null) {
|
||||
out.put("hash", e.getHash());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// v3 environment fields.
|
||||
private static JSONObject getDeviceConfig(Environment e, Environment current) throws JSONException {
|
||||
JSONObject config = new JSONObject();
|
||||
int changes = 0;
|
||||
if (e.version < 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (current != null && current.version < 3) {
|
||||
return getDeviceConfig(e, null);
|
||||
}
|
||||
|
||||
if (current == null || current.hasHardwareKeyboard != e.hasHardwareKeyboard) {
|
||||
config.put("hasHardwareKeyboard", e.hasHardwareKeyboard);
|
||||
changes++;
|
||||
}
|
||||
|
||||
if (current == null || current.screenLayout != e.screenLayout) {
|
||||
config.put("screenLayout", e.screenLayout);
|
||||
changes++;
|
||||
}
|
||||
|
||||
if (current == null || current.screenXInMM != e.screenXInMM) {
|
||||
config.put("screenXInMM", e.screenXInMM);
|
||||
changes++;
|
||||
}
|
||||
|
||||
if (current == null || current.screenYInMM != e.screenYInMM) {
|
||||
config.put("screenYInMM", e.screenYInMM);
|
||||
changes++;
|
||||
}
|
||||
|
||||
if (current == null || current.uiType != e.uiType) {
|
||||
config.put("uiType", e.uiType.toString());
|
||||
changes++;
|
||||
}
|
||||
|
||||
if (current == null || current.uiMode != e.uiMode) {
|
||||
config.put("uiMode", e.uiMode);
|
||||
changes++;
|
||||
}
|
||||
|
||||
if (current != null && changes == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
config.put("_v", 1);
|
||||
return config;
|
||||
}
|
||||
|
||||
private static JSONObject getProfileAge(Environment e, Environment current) throws JSONException {
|
||||
JSONObject age = new JSONObject();
|
||||
int changes = 0;
|
||||
if (current == null || current.profileCreation != e.profileCreation) {
|
||||
age.put("profileCreation", e.profileCreation);
|
||||
changes++;
|
||||
}
|
||||
if (current != null && changes == 0) {
|
||||
return null;
|
||||
}
|
||||
age.put("_v", 1);
|
||||
return age;
|
||||
}
|
||||
|
||||
private static JSONObject getSysInfo(Environment e, Environment current) throws JSONException {
|
||||
JSONObject sysinfo = new JSONObject();
|
||||
int changes = 0;
|
||||
if (current == null || current.cpuCount != e.cpuCount) {
|
||||
sysinfo.put("cpuCount", e.cpuCount);
|
||||
changes++;
|
||||
}
|
||||
if (current == null || current.memoryMB != e.memoryMB) {
|
||||
sysinfo.put("memoryMB", e.memoryMB);
|
||||
changes++;
|
||||
}
|
||||
if (current == null || !current.architecture.equals(e.architecture)) {
|
||||
sysinfo.put("architecture", e.architecture);
|
||||
changes++;
|
||||
}
|
||||
if (current == null || !current.sysName.equals(e.sysName)) {
|
||||
sysinfo.put("name", e.sysName);
|
||||
changes++;
|
||||
}
|
||||
if (current == null || !current.sysVersion.equals(e.sysVersion)) {
|
||||
sysinfo.put("version", e.sysVersion);
|
||||
changes++;
|
||||
}
|
||||
if (current != null && changes == 0) {
|
||||
return null;
|
||||
}
|
||||
sysinfo.put("_v", 1);
|
||||
return sysinfo;
|
||||
}
|
||||
|
||||
private static JSONObject getGeckoInfo(Environment e, Environment current) throws JSONException {
|
||||
JSONObject gecko = new JSONObject();
|
||||
int changes = 0;
|
||||
if (current == null || !current.vendor.equals(e.vendor)) {
|
||||
gecko.put("vendor", e.vendor);
|
||||
changes++;
|
||||
}
|
||||
if (current == null || !current.appName.equals(e.appName)) {
|
||||
gecko.put("name", e.appName);
|
||||
changes++;
|
||||
}
|
||||
if (current == null || !current.appID.equals(e.appID)) {
|
||||
gecko.put("id", e.appID);
|
||||
changes++;
|
||||
}
|
||||
if (current == null || !current.appVersion.equals(e.appVersion)) {
|
||||
gecko.put("version", e.appVersion);
|
||||
changes++;
|
||||
}
|
||||
if (current == null || !current.appBuildID.equals(e.appBuildID)) {
|
||||
gecko.put("appBuildID", e.appBuildID);
|
||||
changes++;
|
||||
}
|
||||
if (current == null || !current.platformVersion.equals(e.platformVersion)) {
|
||||
gecko.put("platformVersion", e.platformVersion);
|
||||
changes++;
|
||||
}
|
||||
if (current == null || !current.platformBuildID.equals(e.platformBuildID)) {
|
||||
gecko.put("platformBuildID", e.platformBuildID);
|
||||
changes++;
|
||||
}
|
||||
if (current == null || !current.os.equals(e.os)) {
|
||||
gecko.put("os", e.os);
|
||||
changes++;
|
||||
}
|
||||
if (current == null || !current.xpcomabi.equals(e.xpcomabi)) {
|
||||
gecko.put("xpcomabi", e.xpcomabi);
|
||||
changes++;
|
||||
}
|
||||
if (current == null || !current.updateChannel.equals(e.updateChannel)) {
|
||||
gecko.put("updateChannel", e.updateChannel);
|
||||
changes++;
|
||||
}
|
||||
if (current != null && changes == 0) {
|
||||
return null;
|
||||
}
|
||||
gecko.put("_v", 1);
|
||||
return gecko;
|
||||
}
|
||||
|
||||
// Null-safe string comparison.
|
||||
private static boolean stringsDiffer(final String a, final String b) {
|
||||
if (a == null) {
|
||||
return b != null;
|
||||
}
|
||||
return !a.equals(b);
|
||||
}
|
||||
|
||||
@SuppressWarnings("fallthrough")
|
||||
private static JSONObject getAppInfo(Environment e, Environment current) throws JSONException {
|
||||
JSONObject appinfo = new JSONObject();
|
||||
|
||||
Logger.debug(LOG_TAG, "Generating appinfo for v" + e.version + " env " + e.hash);
|
||||
|
||||
// Is the environment in question newer than the diff target, or is
|
||||
// there no diff target?
|
||||
final boolean outdated = current == null ||
|
||||
e.version > current.version;
|
||||
|
||||
// Is the environment in question a different version (lower or higher),
|
||||
// or is there no diff target?
|
||||
final boolean differ = outdated || current.version > e.version;
|
||||
|
||||
// Always produce an output object if there's a version mismatch or this
|
||||
// isn't a diff. Otherwise, track as we go if there's any difference.
|
||||
boolean changed = differ;
|
||||
|
||||
switch (e.version) {
|
||||
// There's a straightforward correspondence between environment versions
|
||||
// and appinfo versions.
|
||||
case 3:
|
||||
case 2:
|
||||
appinfo.put("_v", 3);
|
||||
break;
|
||||
case 1:
|
||||
appinfo.put("_v", 2);
|
||||
break;
|
||||
default:
|
||||
Logger.warn(LOG_TAG, "Unknown environment version: " + e.version);
|
||||
return appinfo;
|
||||
}
|
||||
|
||||
switch (e.version) {
|
||||
case 3:
|
||||
case 2:
|
||||
if (populateAppInfoV2(appinfo, e, current, outdated)) {
|
||||
changed = true;
|
||||
}
|
||||
// Fall through.
|
||||
|
||||
case 1:
|
||||
// There is no older version than v1, so don't check outdated.
|
||||
if (populateAppInfoV1(e, current, appinfo)) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return appinfo;
|
||||
}
|
||||
|
||||
private static boolean populateAppInfoV1(Environment e,
|
||||
Environment current,
|
||||
JSONObject appinfo)
|
||||
throws JSONException {
|
||||
boolean changes = false;
|
||||
if (current == null || current.isBlocklistEnabled != e.isBlocklistEnabled) {
|
||||
appinfo.put("isBlocklistEnabled", e.isBlocklistEnabled);
|
||||
changes = true;
|
||||
}
|
||||
|
||||
if (current == null || current.isTelemetryEnabled != e.isTelemetryEnabled) {
|
||||
appinfo.put("isTelemetryEnabled", e.isTelemetryEnabled);
|
||||
changes = true;
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
private static boolean populateAppInfoV2(JSONObject appinfo,
|
||||
Environment e,
|
||||
Environment current,
|
||||
final boolean outdated)
|
||||
throws JSONException {
|
||||
boolean changes = false;
|
||||
if (outdated ||
|
||||
stringsDiffer(current.osLocale, e.osLocale)) {
|
||||
appinfo.put("osLocale", e.osLocale);
|
||||
changes = true;
|
||||
}
|
||||
|
||||
if (outdated ||
|
||||
stringsDiffer(current.appLocale, e.appLocale)) {
|
||||
appinfo.put("appLocale", e.appLocale);
|
||||
changes = true;
|
||||
}
|
||||
|
||||
if (outdated ||
|
||||
stringsDiffer(current.distribution, e.distribution)) {
|
||||
appinfo.put("distribution", e.distribution);
|
||||
changes = true;
|
||||
}
|
||||
|
||||
if (outdated ||
|
||||
current.acceptLangSet != e.acceptLangSet) {
|
||||
appinfo.put("acceptLangIsUserSet", e.acceptLangSet);
|
||||
changes = true;
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
|
||||
private static JSONObject getAddonCounts(Environment e, Environment current) throws JSONException {
|
||||
JSONObject counts = new JSONObject();
|
||||
int changes = 0;
|
||||
if (current == null || current.extensionCount != e.extensionCount) {
|
||||
counts.put("extension", e.extensionCount);
|
||||
changes++;
|
||||
}
|
||||
if (current == null || current.pluginCount != e.pluginCount) {
|
||||
counts.put("plugin", e.pluginCount);
|
||||
changes++;
|
||||
}
|
||||
if (current == null || current.themeCount != e.themeCount) {
|
||||
counts.put("theme", e.themeCount);
|
||||
changes++;
|
||||
}
|
||||
if (current != null && changes == 0) {
|
||||
return null;
|
||||
}
|
||||
counts.put("_v", 1);
|
||||
return counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the *tree* difference set between the two objects. If the two
|
||||
* objects are identical, returns <code>null</code>. If <code>from</code> is
|
||||
* <code>null</code>, returns <code>to</code>. If <code>to</code> is
|
||||
* <code>null</code>, behaves as if <code>to</code> were an empty object.
|
||||
*
|
||||
* (Note that this method does not check for {@link JSONObject#NULL}, because
|
||||
* by definition it can't be provided as input to this method.)
|
||||
*
|
||||
* This behavior is intended to simplify life for callers: a missing object
|
||||
* can be viewed as (and behaves as) an empty map, to a useful extent, rather
|
||||
* than throwing an exception.
|
||||
*
|
||||
* @param from
|
||||
* a JSONObject.
|
||||
* @param to
|
||||
* a JSONObject.
|
||||
* @param includeNull
|
||||
* if true, keys present in <code>from</code> but not in
|
||||
* <code>to</code> are included as {@link JSONObject#NULL} in the
|
||||
* output.
|
||||
*
|
||||
* @return a JSONObject, or null if the two objects are identical.
|
||||
* @throws JSONException
|
||||
* should not occur, but...
|
||||
*/
|
||||
public static JSONObject diff(JSONObject from,
|
||||
JSONObject to,
|
||||
boolean includeNull) throws JSONException {
|
||||
if (from == null) {
|
||||
return to;
|
||||
}
|
||||
|
||||
if (to == null) {
|
||||
return diff(from, new JSONObject(), includeNull);
|
||||
}
|
||||
|
||||
JSONObject out = new JSONObject();
|
||||
|
||||
HashSet<String> toKeys = includeNull ? new HashSet<String>(to.length())
|
||||
: null;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Iterator<String> it = to.keys();
|
||||
while (it.hasNext()) {
|
||||
String key = it.next();
|
||||
|
||||
// Track these as we go if we'll need them later.
|
||||
if (includeNull) {
|
||||
toKeys.add(key);
|
||||
}
|
||||
|
||||
Object value = to.get(key);
|
||||
if (!from.has(key)) {
|
||||
// It must be new.
|
||||
out.put(key, value);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Not new? Then see if it changed.
|
||||
Object old = from.get(key);
|
||||
|
||||
// Two JSONObjects should be diffed.
|
||||
if (old instanceof JSONObject && value instanceof JSONObject) {
|
||||
JSONObject innerDiff = diff(((JSONObject) old), ((JSONObject) value),
|
||||
includeNull);
|
||||
// No change? No output.
|
||||
if (innerDiff == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise include the diff.
|
||||
out.put(key, innerDiff);
|
||||
continue;
|
||||
}
|
||||
|
||||
// A regular value, or a type change. Only skip if they're the same.
|
||||
if (value.equals(old)) {
|
||||
continue;
|
||||
}
|
||||
out.put(key, value);
|
||||
}
|
||||
|
||||
// Now -- if requested -- include any removed keys.
|
||||
if (includeNull) {
|
||||
Set<String> fromKeys = HealthReportUtils.keySet(from);
|
||||
fromKeys.removeAll(toKeys);
|
||||
for (String notPresent : fromKeys) {
|
||||
out.put(notPresent, JSONObject.NULL);
|
||||
}
|
||||
}
|
||||
|
||||
if (out.length() == 0) {
|
||||
return null;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static JSONObject getActiveAddons(Environment e, Environment current) throws JSONException {
|
||||
// Just return the current add-on set, with a version annotation.
|
||||
// To do so requires copying.
|
||||
if (current == null) {
|
||||
JSONObject out = e.getNonIgnoredAddons();
|
||||
if (out == null) {
|
||||
Logger.warn(LOG_TAG, "Null add-ons to return in FHR document. Returning {}.");
|
||||
out = new JSONObject(); // So that we always return something.
|
||||
}
|
||||
out.put("_v", 1);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Otherwise, return the diff.
|
||||
JSONObject diff = diff(current.getNonIgnoredAddons(), e.getNonIgnoredAddons(), true);
|
||||
if (diff == null) {
|
||||
return null;
|
||||
}
|
||||
if (diff == e.addons) {
|
||||
// Again, needs to copy.
|
||||
return getActiveAddons(e, null);
|
||||
}
|
||||
|
||||
diff.put("_v", 1);
|
||||
return diff;
|
||||
}
|
||||
}
|
|
@ -1,301 +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/. */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage.DatabaseEnvironment;
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field;
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields;
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields.FieldSpec;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
|
||||
/**
|
||||
* This is a {@link ContentProvider} wrapper around a database-backed Health
|
||||
* Report storage layer.
|
||||
*
|
||||
* It stores environments, fields, and measurements, and events which refer to
|
||||
* each of these by integer ID.
|
||||
*
|
||||
* Insert = daily discrete.
|
||||
* content://org.mozilla.gecko.health/events/env/measurement/v/field
|
||||
*
|
||||
* Update = daily last or daily counter
|
||||
* content://org.mozilla.gecko.health/events/env/measurement/v/field/counter
|
||||
* content://org.mozilla.gecko.health/events/env/measurement/v/field/last
|
||||
*
|
||||
* Delete = drop today's row
|
||||
* content://org.mozilla.gecko.health/events/env/measurement/v/field/
|
||||
*
|
||||
* Query, of course: content://org.mozilla.gecko.health/events/?since
|
||||
*
|
||||
* Each operation accepts an optional `time` query parameter, formatted as
|
||||
* milliseconds since epoch. If omitted, it defaults to the current time.
|
||||
*
|
||||
* Each operation also accepts mandatory `profilePath` and `env` arguments.
|
||||
*
|
||||
* TODO: document measurements.
|
||||
*/
|
||||
public class HealthReportProvider extends ContentProvider {
|
||||
private HealthReportDatabases databases;
|
||||
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
|
||||
public static final String HEALTH_AUTHORITY = HealthReportConstants.HEALTH_AUTHORITY;
|
||||
|
||||
// URI matches.
|
||||
private static final int ENVIRONMENTS_ROOT = 10;
|
||||
private static final int EVENTS_ROOT = 11;
|
||||
private static final int EVENTS_RAW_ROOT = 12;
|
||||
private static final int FIELDS_ROOT = 13;
|
||||
private static final int MEASUREMENTS_ROOT = 14;
|
||||
|
||||
private static final int EVENTS_FIELD_GENERIC = 20;
|
||||
private static final int EVENTS_FIELD_COUNTER = 21;
|
||||
private static final int EVENTS_FIELD_LAST = 22;
|
||||
|
||||
private static final int ENVIRONMENT_DETAILS = 30;
|
||||
private static final int FIELDS_MEASUREMENT = 31;
|
||||
|
||||
static {
|
||||
uriMatcher.addURI(HEALTH_AUTHORITY, "environments/", ENVIRONMENTS_ROOT);
|
||||
uriMatcher.addURI(HEALTH_AUTHORITY, "events/", EVENTS_ROOT);
|
||||
uriMatcher.addURI(HEALTH_AUTHORITY, "rawevents/", EVENTS_RAW_ROOT);
|
||||
uriMatcher.addURI(HEALTH_AUTHORITY, "fields/", FIELDS_ROOT);
|
||||
uriMatcher.addURI(HEALTH_AUTHORITY, "measurements/", MEASUREMENTS_ROOT);
|
||||
|
||||
uriMatcher.addURI(HEALTH_AUTHORITY, "events/#/*/#/*", EVENTS_FIELD_GENERIC);
|
||||
uriMatcher.addURI(HEALTH_AUTHORITY, "events/#/*/#/*/counter", EVENTS_FIELD_COUNTER);
|
||||
uriMatcher.addURI(HEALTH_AUTHORITY, "events/#/*/#/*/last", EVENTS_FIELD_LAST);
|
||||
|
||||
uriMatcher.addURI(HEALTH_AUTHORITY, "environments/#", ENVIRONMENT_DETAILS);
|
||||
uriMatcher.addURI(HEALTH_AUTHORITY, "fields/*/#", FIELDS_MEASUREMENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* So we can bypass the ContentProvider layer.
|
||||
*/
|
||||
public HealthReportDatabaseStorage getProfileStorage(final String profilePath) {
|
||||
if (profilePath == null) {
|
||||
throw new IllegalArgumentException("profilePath must be provided.");
|
||||
}
|
||||
return databases.getDatabaseHelperForProfile(new File(profilePath));
|
||||
}
|
||||
|
||||
private HealthReportDatabaseStorage getProfileStorageForUri(Uri uri) {
|
||||
final String profilePath = uri.getQueryParameter("profilePath");
|
||||
return getProfileStorage(profilePath);
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(Uri uri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
databases = new HealthReportDatabases(getContext());
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues values) {
|
||||
int match = uriMatcher.match(uri);
|
||||
HealthReportDatabaseStorage storage = getProfileStorageForUri(uri);
|
||||
switch (match) {
|
||||
case FIELDS_MEASUREMENT:
|
||||
// The keys of this ContentValues are field names.
|
||||
List<String> pathSegments = uri.getPathSegments();
|
||||
String measurement = pathSegments.get(1);
|
||||
int v = Integer.parseInt(pathSegments.get(2));
|
||||
storage.ensureMeasurementInitialized(measurement, v, getFieldSpecs(values));
|
||||
return uri;
|
||||
|
||||
case ENVIRONMENTS_ROOT:
|
||||
DatabaseEnvironment environment = storage.getEnvironment();
|
||||
environment.init(values);
|
||||
return ContentUris.withAppendedId(uri, environment.register());
|
||||
|
||||
case EVENTS_FIELD_GENERIC:
|
||||
long time = getTimeFromUri(uri);
|
||||
int day = storage.getDay(time);
|
||||
int env = getEnvironmentFromUri(uri);
|
||||
Field field = getFieldFromUri(storage, uri);
|
||||
|
||||
if (!values.containsKey("value")) {
|
||||
throw new IllegalArgumentException("Must provide ContentValues including 'value' key.");
|
||||
}
|
||||
|
||||
Object object = values.get("value");
|
||||
if (object instanceof Integer ||
|
||||
object instanceof Long) {
|
||||
storage.recordDailyDiscrete(env, day, field.getID(), ((Integer) object).intValue());
|
||||
} else if (object instanceof String) {
|
||||
storage.recordDailyDiscrete(env, day, field.getID(), (String) object);
|
||||
} else {
|
||||
storage.recordDailyDiscrete(env, day, field.getID(), object.toString());
|
||||
}
|
||||
|
||||
// TODO: eventually we might want to return something more useful than
|
||||
// the input URI.
|
||||
return uri;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown insert URI");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues values, String selection,
|
||||
String[] selectionArgs) {
|
||||
|
||||
int match = uriMatcher.match(uri);
|
||||
if (match != EVENTS_FIELD_COUNTER &&
|
||||
match != EVENTS_FIELD_LAST) {
|
||||
throw new IllegalArgumentException("Must provide operation for update.");
|
||||
}
|
||||
|
||||
HealthReportStorage storage = getProfileStorageForUri(uri);
|
||||
long time = getTimeFromUri(uri);
|
||||
int day = storage.getDay(time);
|
||||
int env = getEnvironmentFromUri(uri);
|
||||
Field field = getFieldFromUri(storage, uri);
|
||||
|
||||
switch (match) {
|
||||
case EVENTS_FIELD_COUNTER:
|
||||
int by = values.containsKey("value") ? values.getAsInteger("value") : 1;
|
||||
storage.incrementDailyCount(env, day, field.getID(), by);
|
||||
return 1;
|
||||
|
||||
case EVENTS_FIELD_LAST:
|
||||
Object object = values.get("value");
|
||||
if (object instanceof Integer ||
|
||||
object instanceof Long) {
|
||||
storage.recordDailyLast(env, day, field.getID(), (Integer) object);
|
||||
} else if (object instanceof String) {
|
||||
storage.recordDailyLast(env, day, field.getID(), (String) object);
|
||||
} else {
|
||||
storage.recordDailyLast(env, day, field.getID(), object.toString());
|
||||
}
|
||||
return 1;
|
||||
|
||||
default:
|
||||
// javac's flow control analysis sucks.
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(Uri uri, String selection, String[] selectionArgs) {
|
||||
int match = uriMatcher.match(uri);
|
||||
HealthReportStorage storage = getProfileStorageForUri(uri);
|
||||
switch (match) {
|
||||
case MEASUREMENTS_ROOT:
|
||||
storage.deleteMeasurements();
|
||||
return 1;
|
||||
case ENVIRONMENTS_ROOT:
|
||||
storage.deleteEnvironments();
|
||||
return 1;
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
|
||||
// TODO: more
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String selection,
|
||||
String[] selectionArgs, String sortOrder) {
|
||||
int match = uriMatcher.match(uri);
|
||||
|
||||
HealthReportStorage storage = getProfileStorageForUri(uri);
|
||||
switch (match) {
|
||||
case EVENTS_ROOT:
|
||||
return storage.getEventsSince(getTimeFromUri(uri));
|
||||
case EVENTS_RAW_ROOT:
|
||||
return storage.getRawEventsSince(getTimeFromUri(uri));
|
||||
case MEASUREMENTS_ROOT:
|
||||
return storage.getMeasurementVersions();
|
||||
case FIELDS_ROOT:
|
||||
return storage.getFieldVersions();
|
||||
}
|
||||
List<String> pathSegments = uri.getPathSegments();
|
||||
switch (match) {
|
||||
case ENVIRONMENT_DETAILS:
|
||||
return storage.getEnvironmentRecordForID(Integer.parseInt(pathSegments.get(1), 10));
|
||||
case FIELDS_MEASUREMENT:
|
||||
String measurement = pathSegments.get(1);
|
||||
int v = Integer.parseInt(pathSegments.get(2));
|
||||
return storage.getFieldVersions(measurement, v);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static long getTimeFromUri(final Uri uri) {
|
||||
String t = uri.getQueryParameter("time");
|
||||
if (t == null) {
|
||||
return System.currentTimeMillis();
|
||||
} else {
|
||||
return Long.parseLong(t, 10);
|
||||
}
|
||||
}
|
||||
|
||||
private static int getEnvironmentFromUri(final Uri uri) {
|
||||
return Integer.parseInt(uri.getPathSegments().get(1), 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Assumes a URI structured like:
|
||||
*
|
||||
* <code>content://org.mozilla.gecko.health/events/env/measurement/v/field</code>
|
||||
*
|
||||
* @param uri a URI formatted as expected.
|
||||
* @return a {@link Field} instance.
|
||||
*/
|
||||
private static Field getFieldFromUri(HealthReportStorage storage, final Uri uri) {
|
||||
String measurement;
|
||||
String field;
|
||||
int measurementVersion;
|
||||
|
||||
List<String> pathSegments = uri.getPathSegments();
|
||||
measurement = pathSegments.get(2);
|
||||
measurementVersion = Integer.parseInt(pathSegments.get(3), 10);
|
||||
field = pathSegments.get(4);
|
||||
|
||||
return storage.getField(measurement, measurementVersion, field);
|
||||
}
|
||||
|
||||
private MeasurementFields getFieldSpecs(ContentValues values) {
|
||||
final ArrayList<FieldSpec> specs = new ArrayList<FieldSpec>(values.size());
|
||||
for (Entry<String, Object> entry : values.valueSet()) {
|
||||
specs.add(new FieldSpec(entry.getKey(), (Integer) entry.getValue()));
|
||||
}
|
||||
|
||||
return new MeasurementFields() {
|
||||
@Override
|
||||
public Iterable<FieldSpec> getFields() {
|
||||
return specs;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
|
@ -1,238 +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/. */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.util.SparseArray;
|
||||
|
||||
/**
|
||||
* Abstraction over storage for Firefox Health Report on Android.
|
||||
*/
|
||||
public interface HealthReportStorage {
|
||||
// Right now we only care about the name of the field.
|
||||
public interface MeasurementFields {
|
||||
public class FieldSpec {
|
||||
public final String name;
|
||||
public final int type;
|
||||
public FieldSpec(String name, int type) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
}
|
||||
}
|
||||
Iterable<FieldSpec> getFields();
|
||||
}
|
||||
|
||||
public abstract class Field {
|
||||
protected static final int UNKNOWN_TYPE_OR_FIELD_ID = -1;
|
||||
|
||||
protected static final int FLAG_INTEGER = 1 << 0;
|
||||
protected static final int FLAG_STRING = 1 << 1;
|
||||
protected static final int FLAG_JSON = 1 << 2;
|
||||
|
||||
protected static final int FLAG_DISCRETE = 1 << 8;
|
||||
protected static final int FLAG_LAST = 1 << 9;
|
||||
protected static final int FLAG_COUNTER = 1 << 10;
|
||||
|
||||
protected static final int FLAG_COUNTED = 1 << 14;
|
||||
|
||||
public static final int TYPE_INTEGER_DISCRETE = FLAG_INTEGER | FLAG_DISCRETE;
|
||||
public static final int TYPE_INTEGER_LAST = FLAG_INTEGER | FLAG_LAST;
|
||||
public static final int TYPE_INTEGER_COUNTER = FLAG_INTEGER | FLAG_COUNTER;
|
||||
|
||||
public static final int TYPE_STRING_DISCRETE = FLAG_STRING | FLAG_DISCRETE;
|
||||
public static final int TYPE_STRING_LAST = FLAG_STRING | FLAG_LAST;
|
||||
|
||||
public static final int TYPE_JSON_DISCRETE = FLAG_JSON | FLAG_DISCRETE;
|
||||
public static final int TYPE_JSON_LAST = FLAG_JSON | FLAG_LAST;
|
||||
|
||||
public static final int TYPE_COUNTED_STRING_DISCRETE = FLAG_COUNTED | TYPE_STRING_DISCRETE;
|
||||
|
||||
protected int fieldID = UNKNOWN_TYPE_OR_FIELD_ID;
|
||||
protected int flags;
|
||||
|
||||
protected final String measurementName;
|
||||
protected final String measurementVersion;
|
||||
protected final String fieldName;
|
||||
|
||||
public Field(String mName, int mVersion, String fieldName, int type) {
|
||||
this.measurementName = mName;
|
||||
this.measurementVersion = Integer.toString(mVersion, 10);
|
||||
this.fieldName = fieldName;
|
||||
this.flags = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the ID for this <code>Field</code>
|
||||
* @throws IllegalStateException if this field is not found in storage
|
||||
*/
|
||||
public abstract int getID() throws IllegalStateException;
|
||||
|
||||
public boolean isIntegerField() {
|
||||
return (this.flags & FLAG_INTEGER) > 0;
|
||||
}
|
||||
|
||||
public boolean isStringField() {
|
||||
return (this.flags & FLAG_STRING) > 0;
|
||||
}
|
||||
|
||||
public boolean isJSONField() {
|
||||
return (this.flags & FLAG_JSON) > 0;
|
||||
}
|
||||
|
||||
public boolean isStoredAsString() {
|
||||
return (this.flags & (FLAG_JSON | FLAG_STRING)) > 0;
|
||||
}
|
||||
|
||||
public boolean isDiscreteField() {
|
||||
return (this.flags & FLAG_DISCRETE) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the accrued values are intended to be bucket-counted. For strings,
|
||||
* each discrete value will name a bucket, with the number of instances per
|
||||
* day being the value in the bucket.
|
||||
*/
|
||||
public boolean isCountedField() {
|
||||
return (this.flags & FLAG_COUNTED) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close open storage handles and otherwise finish up.
|
||||
*/
|
||||
public void close();
|
||||
|
||||
/**
|
||||
* Return the day integer corresponding to the provided time.
|
||||
*
|
||||
* @param time
|
||||
* milliseconds since Unix epoch.
|
||||
* @return an integer day.
|
||||
*/
|
||||
public int getDay(long time);
|
||||
|
||||
/**
|
||||
* Return the day integer corresponding to the current time.
|
||||
*
|
||||
* @return an integer day.
|
||||
*/
|
||||
public int getDay();
|
||||
|
||||
/**
|
||||
* Return a new {@link Environment}, suitable for being populated, hashed, and
|
||||
* registered.
|
||||
*
|
||||
* @return a new {@link Environment} instance.
|
||||
*/
|
||||
public Environment getEnvironment();
|
||||
|
||||
/**
|
||||
* @return a mapping from environment IDs to hashes, suitable for use in
|
||||
* payload generation.
|
||||
*/
|
||||
public SparseArray<String> getEnvironmentHashesByID();
|
||||
|
||||
/**
|
||||
* @return a mapping from environment IDs to registered {@link Environment}
|
||||
* records, suitable for use in payload generation.
|
||||
*/
|
||||
public SparseArray<Environment> getEnvironmentRecordsByID();
|
||||
|
||||
/**
|
||||
* @param id
|
||||
* the environment ID, as returned by {@link Environment#register()}.
|
||||
* @return a cursor for the record.
|
||||
*/
|
||||
public Cursor getEnvironmentRecordForID(int id);
|
||||
|
||||
/**
|
||||
* @param measurement
|
||||
* the name of a measurement, such as "org.mozilla.appInfo.appInfo".
|
||||
* @param measurementVersion
|
||||
* the version of a measurement, such as '3'.
|
||||
* @param fieldName
|
||||
* the name of a field, such as "platformVersion".
|
||||
*
|
||||
* @return a {@link Field} instance corresponding to the provided values.
|
||||
*/
|
||||
public Field getField(String measurement, int measurementVersion,
|
||||
String fieldName);
|
||||
|
||||
/**
|
||||
* @return a mapping from field IDs to {@link Field} instances, suitable for
|
||||
* use in payload generation.
|
||||
*/
|
||||
public SparseArray<Field> getFieldsByID();
|
||||
|
||||
public void recordDailyLast(int env, int day, int field, JSONObject value);
|
||||
public void recordDailyLast(int env, int day, int field, String value);
|
||||
public void recordDailyLast(int env, int day, int field, int value);
|
||||
public void recordDailyDiscrete(int env, int day, int field, JSONObject value);
|
||||
public void recordDailyDiscrete(int env, int day, int field, String value);
|
||||
public void recordDailyDiscrete(int env, int day, int field, int value);
|
||||
public void incrementDailyCount(int env, int day, int field, int by);
|
||||
public void incrementDailyCount(int env, int day, int field);
|
||||
|
||||
/**
|
||||
* Return true if events exist that were recorded on or after <code>time</code>.
|
||||
*/
|
||||
boolean hasEventSince(long time);
|
||||
|
||||
/**
|
||||
* Obtain a cursor over events that were recorded since <code>time</code>.
|
||||
* This cursor exposes 'raw' events, with integer identifiers for values.
|
||||
*/
|
||||
public Cursor getRawEventsSince(long time);
|
||||
|
||||
/**
|
||||
* Obtain a cursor over events that were recorded since <code>time</code>.
|
||||
*
|
||||
* This cursor exposes 'friendly' events, with string names and full
|
||||
* measurement metadata.
|
||||
*/
|
||||
public Cursor getEventsSince(long time);
|
||||
|
||||
/**
|
||||
* Ensure that a measurement and all of its fields are registered with the DB.
|
||||
* No fields will be processed if the measurement exists with the specified
|
||||
* version.
|
||||
*
|
||||
* @param measurement
|
||||
* a measurement name, such as "org.mozilla.appInfo.appInfo".
|
||||
* @param version
|
||||
* a version number, such as '3'.
|
||||
* @param fields
|
||||
* a {@link MeasurementFields} instance, consisting of a collection
|
||||
* of field names.
|
||||
*/
|
||||
public void ensureMeasurementInitialized(String measurement,
|
||||
int version,
|
||||
MeasurementFields fields);
|
||||
public Cursor getMeasurementVersions();
|
||||
public Cursor getFieldVersions();
|
||||
public Cursor getFieldVersions(String measurement, int measurementVersion);
|
||||
|
||||
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);
|
||||
}
|
|
@ -1,136 +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/. */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.apache.commons.codec.digest.DigestUtils;
|
||||
|
||||
import android.content.ContentUris;
|
||||
import android.net.Uri;
|
||||
|
||||
public class HealthReportUtils {
|
||||
public static final String LOG_TAG = HealthReportUtils.class.getSimpleName();
|
||||
|
||||
public static String getEnvironmentHash(final String input) {
|
||||
return DigestUtils.shaHex(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* Take an environment URI (one that identifies an environment) and produce an
|
||||
* event URI.
|
||||
*
|
||||
* That this is needed is tragic.
|
||||
*
|
||||
* @param environmentURI
|
||||
* the {@link Uri} returned by an environment operation.
|
||||
* @return a {@link Uri} to which insertions can be dispatched.
|
||||
*/
|
||||
public static Uri getEventURI(Uri environmentURI) {
|
||||
return environmentURI.buildUpon().path("/events/" + ContentUris.parseId(environmentURI) + "/").build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the keys from the provided {@link JSONObject} into the provided {@link Set}.
|
||||
*/
|
||||
private static <T extends Set<String>> T intoKeySet(T keys, JSONObject o) {
|
||||
if (o == null || o == JSONObject.NULL) {
|
||||
return keys;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Iterator<String> it = o.keys();
|
||||
while (it.hasNext()) {
|
||||
keys.add(it.next());
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce a {@link SortedSet} containing the string keys of the provided
|
||||
* object.
|
||||
*
|
||||
* @param o a {@link JSONObject} with string keys.
|
||||
* @return a sorted set.
|
||||
*/
|
||||
public static SortedSet<String> sortedKeySet(JSONObject o) {
|
||||
return intoKeySet(new TreeSet<String>(), o);
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce a {@link Set} containing the string keys of the provided object.
|
||||
* @param o a {@link JSONObject} with string keys.
|
||||
* @return an unsorted set.
|
||||
*/
|
||||
public static Set<String> keySet(JSONObject o) {
|
||||
return intoKeySet(new HashSet<String>(), o);
|
||||
}
|
||||
|
||||
/**
|
||||
* Just like {@link JSONObject#accumulate(String, Object)}, but doesn't do the wrong thing for single values.
|
||||
* @throws JSONException
|
||||
*/
|
||||
public static void append(JSONObject o, String key, Object value) throws JSONException {
|
||||
if (!o.has(key)) {
|
||||
JSONArray arr = new JSONArray();
|
||||
arr.put(value);
|
||||
o.put(key, arr);
|
||||
return;
|
||||
}
|
||||
Object dest = o.get(key);
|
||||
if (dest instanceof JSONArray) {
|
||||
((JSONArray) dest).put(value);
|
||||
return;
|
||||
}
|
||||
JSONArray arr = new JSONArray();
|
||||
arr.put(dest);
|
||||
arr.put(value);
|
||||
o.put(key, arr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Accumulate counts for how often each provided value occurs.
|
||||
*
|
||||
* <code>
|
||||
* HealthReportUtils.count(o, "foo", "bar");
|
||||
* </code>
|
||||
*
|
||||
* will change
|
||||
*
|
||||
* <pre>
|
||||
* {"foo", {"bar": 1}}
|
||||
* </pre>
|
||||
*
|
||||
* into
|
||||
*
|
||||
* <pre>
|
||||
* {"foo", {"bar": 2}}
|
||||
* </pre>
|
||||
*
|
||||
*/
|
||||
public static void count(JSONObject o, String key,
|
||||
String value) throws JSONException {
|
||||
if (!o.has(key)) {
|
||||
JSONObject counts = new JSONObject();
|
||||
counts.put(value, 1);
|
||||
o.put(key, counts);
|
||||
return;
|
||||
}
|
||||
JSONObject dest = o.getJSONObject(key);
|
||||
dest.put(value, dest.optInt(value, 0) + 1);
|
||||
}
|
||||
|
||||
public static String generateDocumentId() {
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
}
|
|
@ -1,386 +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/. */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Locale;
|
||||
import java.util.Scanner;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
import org.mozilla.gecko.background.healthreport.EnvironmentBuilder.ProfileInformationProvider;
|
||||
|
||||
/**
|
||||
* There are some parts of the FHR environment that can't be readily computed
|
||||
* without a running Gecko -- add-ons, for example. In order to make this
|
||||
* information available without launching Gecko, we persist it on Fennec
|
||||
* startup. This class is the notepad in which we write.
|
||||
*/
|
||||
public class ProfileInformationCache implements ProfileInformationProvider {
|
||||
private static final String LOG_TAG = "GeckoProfileInfo";
|
||||
private static final String CACHE_FILE = "profile_info_cache.json";
|
||||
|
||||
/*
|
||||
* FORMAT_VERSION history:
|
||||
* -: No version number; implicit v1.
|
||||
* 1: Add versioning (Bug 878670).
|
||||
* 2: Bump to regenerate add-on set after landing Bug 900694 (Bug 901622).
|
||||
* 3: Add distribution, osLocale, appLocale.
|
||||
* 4: Add experiments as add-ons.
|
||||
*/
|
||||
public static final int FORMAT_VERSION = 4;
|
||||
|
||||
protected boolean initialized = false;
|
||||
protected boolean needsWrite = false;
|
||||
|
||||
protected final File file;
|
||||
|
||||
private volatile boolean blocklistEnabled = true;
|
||||
private volatile boolean telemetryEnabled = false;
|
||||
private volatile boolean isAcceptLangUserSet = false;
|
||||
|
||||
private volatile long profileCreationTime = 0;
|
||||
private volatile String distribution = "";
|
||||
|
||||
// There are really four kinds of locale in play:
|
||||
//
|
||||
// * The OS
|
||||
// * The Android environment of the app (setDefault)
|
||||
// * The Gecko locale
|
||||
// * The requested content locale (Accept-Language).
|
||||
//
|
||||
// We track only the first two, assuming that the Gecko locale will typically
|
||||
// be the same as the app locale.
|
||||
//
|
||||
// The app locale is fetched from the PIC because it can be modified at
|
||||
// runtime -- it won't necessarily be what Locale.getDefaultLocale() returns
|
||||
// in a fresh non-browser profile.
|
||||
//
|
||||
// We also track the OS locale here for the same reason -- we need to store
|
||||
// the default (OS) value before the locale-switching code takes effect!
|
||||
private volatile String osLocale = "";
|
||||
private volatile String appLocale = "";
|
||||
|
||||
private volatile JSONObject addons = null;
|
||||
|
||||
protected ProfileInformationCache(final File f) {
|
||||
file = f;
|
||||
Logger.pii(LOG_TAG, "Using " + file.getAbsolutePath() + " for profile information cache.");
|
||||
}
|
||||
|
||||
public ProfileInformationCache(final String profilePath) {
|
||||
this(new File(profilePath + File.separator + CACHE_FILE));
|
||||
}
|
||||
|
||||
public synchronized void beginInitialization() {
|
||||
initialized = false;
|
||||
needsWrite = true;
|
||||
}
|
||||
|
||||
public JSONObject toJSON() {
|
||||
JSONObject object = new JSONObject();
|
||||
try {
|
||||
object.put("version", FORMAT_VERSION);
|
||||
object.put("blocklist", blocklistEnabled);
|
||||
object.put("telemetry", telemetryEnabled);
|
||||
object.put("isAcceptLangUserSet", isAcceptLangUserSet);
|
||||
object.put("profileCreated", profileCreationTime);
|
||||
object.put("osLocale", osLocale);
|
||||
object.put("appLocale", appLocale);
|
||||
object.put("distribution", distribution);
|
||||
object.put("addons", addons);
|
||||
} catch (JSONException e) {
|
||||
// There isn't much we can do about this.
|
||||
// Let's just quietly muffle.
|
||||
return null;
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to restore this object from a JSON blob. If there is a version mismatch, there has
|
||||
* likely been an upgrade to the cache format. The cache can be reconstructed without data loss
|
||||
* so rather than migrating, we invalidate the cache by refusing to store the given JSONObject
|
||||
* and returning false.
|
||||
*
|
||||
* @return false if there's a version mismatch or an error, true on success.
|
||||
*/
|
||||
private boolean fromJSON(JSONObject object) throws JSONException {
|
||||
if (object == null) {
|
||||
Logger.debug(LOG_TAG, "Can't load restore PIC from null JSON object.");
|
||||
return false;
|
||||
}
|
||||
|
||||
int version = object.optInt("version", 1);
|
||||
switch (version) {
|
||||
case FORMAT_VERSION:
|
||||
blocklistEnabled = object.getBoolean("blocklist");
|
||||
telemetryEnabled = object.getBoolean("telemetry");
|
||||
isAcceptLangUserSet = object.getBoolean("isAcceptLangUserSet");
|
||||
profileCreationTime = object.getLong("profileCreated");
|
||||
addons = object.getJSONObject("addons");
|
||||
distribution = object.getString("distribution");
|
||||
osLocale = object.getString("osLocale");
|
||||
appLocale = object.getString("appLocale");
|
||||
return true;
|
||||
default:
|
||||
Logger.warn(LOG_TAG, "Unable to restore from version " + version + " PIC file: expecting " + FORMAT_VERSION);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
protected JSONObject readFromFile() throws FileNotFoundException, JSONException {
|
||||
Scanner scanner = null;
|
||||
try {
|
||||
scanner = new Scanner(file, "UTF-8").useDelimiter("\\A");
|
||||
if (!scanner.hasNext()) {
|
||||
return null;
|
||||
}
|
||||
return new JSONObject(scanner.next());
|
||||
} finally {
|
||||
if (scanner != null) {
|
||||
scanner.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void writeToFile(JSONObject object) throws IOException {
|
||||
Logger.debug(LOG_TAG, "Writing profile information.");
|
||||
Logger.pii(LOG_TAG, "Writing to file: " + file.getAbsolutePath());
|
||||
FileOutputStream stream = new FileOutputStream(file);
|
||||
OutputStreamWriter writer = new OutputStreamWriter(stream, Charset.forName("UTF-8"));
|
||||
try {
|
||||
writer.append(object.toString());
|
||||
needsWrite = false;
|
||||
} finally {
|
||||
writer.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this <b>on a background thread</b> when you're done adding things.
|
||||
* @throws IOException if there was a problem serializing or writing the cache to disk.
|
||||
*/
|
||||
public synchronized void completeInitialization() throws IOException {
|
||||
initialized = true;
|
||||
if (!needsWrite) {
|
||||
Logger.debug(LOG_TAG, "No write needed.");
|
||||
return;
|
||||
}
|
||||
|
||||
JSONObject object = toJSON();
|
||||
if (object == null) {
|
||||
throw new IOException("Couldn't serialize JSON.");
|
||||
}
|
||||
|
||||
writeToFile(object);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this if you're interested in reading.
|
||||
*
|
||||
* You should be doing so on a background thread.
|
||||
*
|
||||
* @return true if this object was initialized correctly.
|
||||
*/
|
||||
public synchronized boolean restoreUnlessInitialized() {
|
||||
if (initialized) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!file.exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// One-liner for file reading in Java. So sorry.
|
||||
Logger.info(LOG_TAG, "Restoring ProfileInformationCache from file.");
|
||||
Logger.pii(LOG_TAG, "Restoring from file: " + file.getAbsolutePath());
|
||||
|
||||
try {
|
||||
if (!fromJSON(readFromFile())) {
|
||||
// No need to blow away the file; the caller can eventually overwrite it.
|
||||
return false;
|
||||
}
|
||||
initialized = true;
|
||||
needsWrite = false;
|
||||
return true;
|
||||
} catch (FileNotFoundException e) {
|
||||
return false;
|
||||
} catch (JSONException e) {
|
||||
Logger.warn(LOG_TAG, "Malformed ProfileInformationCache. Not restoring.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureInitialized() {
|
||||
if (!initialized) {
|
||||
throw new IllegalStateException("Not initialized.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBlocklistEnabled() {
|
||||
ensureInitialized();
|
||||
return blocklistEnabled;
|
||||
}
|
||||
|
||||
public void setBlocklistEnabled(boolean value) {
|
||||
Logger.debug(LOG_TAG, "Setting blocklist enabled: " + value);
|
||||
blocklistEnabled = value;
|
||||
needsWrite = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTelemetryEnabled() {
|
||||
ensureInitialized();
|
||||
return telemetryEnabled;
|
||||
}
|
||||
|
||||
public void setTelemetryEnabled(boolean value) {
|
||||
Logger.debug(LOG_TAG, "Setting telemetry enabled: " + value);
|
||||
telemetryEnabled = value;
|
||||
needsWrite = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAcceptLangUserSet() {
|
||||
ensureInitialized();
|
||||
return isAcceptLangUserSet;
|
||||
}
|
||||
|
||||
public void setAcceptLangUserSet(boolean value) {
|
||||
Logger.debug(LOG_TAG, "Setting accept-lang as user-set: " + value);
|
||||
isAcceptLangUserSet = value;
|
||||
needsWrite = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getProfileCreationTime() {
|
||||
ensureInitialized();
|
||||
return profileCreationTime;
|
||||
}
|
||||
|
||||
public void setProfileCreationTime(long value) {
|
||||
Logger.debug(LOG_TAG, "Setting profile creation time: " + value);
|
||||
profileCreationTime = value;
|
||||
needsWrite = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDistributionString() {
|
||||
ensureInitialized();
|
||||
return distribution;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that your arguments are non-null.
|
||||
*/
|
||||
public void setDistributionString(String distributionID, String distributionVersion) {
|
||||
Logger.debug(LOG_TAG, "Setting distribution: " + distributionID + ", " + distributionVersion);
|
||||
distribution = distributionID + ":" + distributionVersion;
|
||||
needsWrite = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getAppLocale() {
|
||||
ensureInitialized();
|
||||
return appLocale;
|
||||
}
|
||||
|
||||
public void setAppLocale(String value) {
|
||||
if (value.equalsIgnoreCase(appLocale)) {
|
||||
return;
|
||||
}
|
||||
Logger.debug(LOG_TAG, "Setting app locale: " + value);
|
||||
appLocale = value.toLowerCase(Locale.US);
|
||||
needsWrite = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getOSLocale() {
|
||||
ensureInitialized();
|
||||
return osLocale;
|
||||
}
|
||||
|
||||
public void setOSLocale(String value) {
|
||||
if (value.equalsIgnoreCase(osLocale)) {
|
||||
return;
|
||||
}
|
||||
Logger.debug(LOG_TAG, "Setting OS locale: " + value);
|
||||
osLocale = value.toLowerCase(Locale.US);
|
||||
needsWrite = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the PIC, if necessary, to match the current locale environment.
|
||||
*
|
||||
* @return true if the PIC needed to be updated.
|
||||
*/
|
||||
public boolean updateLocales(String osLocale, String appLocale) {
|
||||
if (this.osLocale.equalsIgnoreCase(osLocale) &&
|
||||
(appLocale == null || this.appLocale.equalsIgnoreCase(appLocale))) {
|
||||
return false;
|
||||
}
|
||||
this.setOSLocale(osLocale);
|
||||
if (appLocale != null) {
|
||||
this.setAppLocale(appLocale);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject getAddonsJSON() {
|
||||
ensureInitialized();
|
||||
return addons;
|
||||
}
|
||||
|
||||
public void updateJSONForAddon(String id, String json) throws Exception {
|
||||
addons.put(id, new JSONObject(json));
|
||||
needsWrite = true;
|
||||
}
|
||||
|
||||
public void removeAddon(String id) {
|
||||
if (null != addons.remove(id)) {
|
||||
needsWrite = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will throw if you haven't done a full update at least once.
|
||||
*/
|
||||
public void updateJSONForAddon(String id, JSONObject json) {
|
||||
if (addons == null) {
|
||||
throw new IllegalStateException("Cannot incrementally update add-ons without first initializing.");
|
||||
}
|
||||
try {
|
||||
addons.put(id, json);
|
||||
needsWrite = true;
|
||||
} catch (Exception e) {
|
||||
// Why would this happen?
|
||||
Logger.warn(LOG_TAG, "Unexpected failure updating JSON for add-on.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the cached set of add-ons. Throws on invalid input.
|
||||
*
|
||||
* @param json a valid add-ons JSON string.
|
||||
*/
|
||||
public void setJSONForAddons(String json) throws Exception {
|
||||
addons = new JSONObject(json);
|
||||
needsWrite = true;
|
||||
}
|
||||
|
||||
public void setJSONForAddons(JSONObject json) {
|
||||
addons = json;
|
||||
needsWrite = true;
|
||||
}
|
||||
}
|
|
@ -1,90 +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/. */
|
||||
|
||||
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);
|
||||
|
||||
// Intent can be null. Bug 1025937.
|
||||
if (intent == null) {
|
||||
Logger.debug(LOG_TAG, "Short-circuiting on null intent.");
|
||||
return;
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param intent must be non-null.
|
||||
* @return true if the supplied intent contains both profileName and profilePath.
|
||||
*/
|
||||
private static 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;
|
||||
}
|
||||
}
|
|
@ -1,233 +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/. */
|
||||
|
||||
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) || 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;
|
||||
}
|
||||
|
||||
Logger.debug(LOG_TAG, "Attempting prune-by-size.");
|
||||
|
||||
// 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());
|
||||
Logger.debug(LOG_TAG, "Cleaning up storage.");
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -1,147 +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/. */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport.prune;
|
||||
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
import org.mozilla.gecko.background.healthreport.AndroidConfigurationProvider;
|
||||
import org.mozilla.gecko.background.healthreport.Environment;
|
||||
import org.mozilla.gecko.background.healthreport.EnvironmentBuilder;
|
||||
import org.mozilla.gecko.background.healthreport.EnvironmentBuilder.ConfigurationProvider;
|
||||
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 final ConfigurationProvider config;
|
||||
|
||||
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.config = new AndroidConfigurationProvider(context);
|
||||
|
||||
this.currentEnvironmentID = -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pruneEvents(final int count) {
|
||||
getStorage().pruneEvents(count);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pruneEnvironments(final int count) {
|
||||
getStorage().pruneEnvironments(count);
|
||||
|
||||
// Re-populate the DB and environment cache with the current environment in the unlikely event
|
||||
// that it was deleted.
|
||||
this.currentEnvironmentID = -1;
|
||||
getCurrentEnvironmentID();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@Override
|
||||
public int deleteDataBefore(final long time) {
|
||||
return getStorage().deleteDataBefore(time, getCurrentEnvironmentID());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cleanup() {
|
||||
final HealthReportDatabaseStorage storage = getStorage();
|
||||
// The change to auto_vacuum will only take affect after a vacuum.
|
||||
storage.disableAutoVacuuming();
|
||||
storage.vacuum();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEventCount() {
|
||||
return getStorage().getEventCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getEnvironmentCount() {
|
||||
return getStorage().getEnvironmentCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
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, config);
|
||||
currentEnvironmentID = env.register();
|
||||
}
|
||||
return currentEnvironmentID;
|
||||
}
|
||||
}
|
|
@ -1,26 +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/. */
|
||||
|
||||
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();
|
||||
}
|
|
@ -1,470 +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/. */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport.upload;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import ch.boye.httpclientandroidlib.HttpResponse;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.gecko.Locales;
|
||||
import org.mozilla.gecko.background.bagheera.BagheeraClient;
|
||||
import org.mozilla.gecko.background.bagheera.BagheeraRequestDelegate;
|
||||
import org.mozilla.gecko.background.common.GlobalConstants;
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
import org.mozilla.gecko.background.healthreport.AndroidConfigurationProvider;
|
||||
import org.mozilla.gecko.background.healthreport.Environment;
|
||||
import org.mozilla.gecko.background.healthreport.EnvironmentBuilder;
|
||||
import org.mozilla.gecko.background.healthreport.EnvironmentBuilder.ConfigurationProvider;
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportConstants;
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage;
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportGenerator;
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportStorage;
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field;
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields;
|
||||
import org.mozilla.gecko.background.healthreport.ProfileInformationCache;
|
||||
import org.mozilla.gecko.sync.net.BaseResource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
||||
public class AndroidSubmissionClient implements SubmissionClient {
|
||||
protected static final String LOG_TAG = AndroidSubmissionClient.class.getSimpleName();
|
||||
|
||||
private static final String MEASUREMENT_NAME_SUBMISSIONS = "org.mozilla.healthreport.submissions";
|
||||
private static final int MEASUREMENT_VERSION_SUBMISSIONS = 1;
|
||||
|
||||
protected final Context context;
|
||||
protected final SharedPreferences sharedPreferences;
|
||||
protected final String profilePath;
|
||||
protected final ConfigurationProvider config;
|
||||
|
||||
public AndroidSubmissionClient(Context context, SharedPreferences sharedPreferences, String profilePath) {
|
||||
this(context, sharedPreferences, profilePath, new AndroidConfigurationProvider(context));
|
||||
}
|
||||
|
||||
public AndroidSubmissionClient(Context context, SharedPreferences sharedPreferences, String profilePath, ConfigurationProvider config) {
|
||||
this.context = context;
|
||||
this.sharedPreferences = sharedPreferences;
|
||||
this.profilePath = profilePath;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public SharedPreferences getSharedPreferences() {
|
||||
return sharedPreferences;
|
||||
}
|
||||
|
||||
public String getDocumentServerURI() {
|
||||
return getSharedPreferences().getString(HealthReportConstants.PREF_DOCUMENT_SERVER_URI, HealthReportConstants.DEFAULT_DOCUMENT_SERVER_URI);
|
||||
}
|
||||
|
||||
public String getDocumentServerNamespace() {
|
||||
return getSharedPreferences().getString(HealthReportConstants.PREF_DOCUMENT_SERVER_NAMESPACE, HealthReportConstants.DEFAULT_DOCUMENT_SERVER_NAMESPACE);
|
||||
}
|
||||
|
||||
public long getLastUploadLocalTime() {
|
||||
return getSharedPreferences().getLong(HealthReportConstants.PREF_LAST_UPLOAD_LOCAL_TIME, 0L);
|
||||
}
|
||||
|
||||
public String getLastUploadDocumentId() {
|
||||
return getSharedPreferences().getString(HealthReportConstants.PREF_LAST_UPLOAD_DOCUMENT_ID, null);
|
||||
}
|
||||
|
||||
public boolean hasUploadBeenRequested() {
|
||||
return getSharedPreferences().contains(HealthReportConstants.PREF_LAST_UPLOAD_REQUESTED);
|
||||
}
|
||||
|
||||
public void setLastUploadLocalTimeAndDocumentId(long localTime, String id) {
|
||||
getSharedPreferences().edit()
|
||||
.putLong(HealthReportConstants.PREF_LAST_UPLOAD_LOCAL_TIME, localTime)
|
||||
.putString(HealthReportConstants.PREF_LAST_UPLOAD_DOCUMENT_ID, id)
|
||||
.commit();
|
||||
}
|
||||
|
||||
protected HealthReportDatabaseStorage getStorage(final ContentProviderClient client) {
|
||||
return EnvironmentBuilder.getStorage(client, profilePath);
|
||||
}
|
||||
|
||||
protected JSONObject generateDocument(final long localTime, final long last,
|
||||
final SubmissionsTracker tracker) throws JSONException {
|
||||
final long since = localTime - GlobalConstants.MILLISECONDS_PER_SIX_MONTHS;
|
||||
final HealthReportGenerator generator = tracker.getGenerator();
|
||||
return generator.generateDocument(since, last, profilePath, config);
|
||||
}
|
||||
|
||||
protected void uploadPayload(String id, String payload, Collection<String> oldIds, BagheeraRequestDelegate uploadDelegate) {
|
||||
final BagheeraClient client = new BagheeraClient(getDocumentServerURI());
|
||||
|
||||
Logger.pii(LOG_TAG, "New health report has id " + id +
|
||||
"and obsoletes " + (oldIds != null ? Integer.toString(oldIds.size()) : "no") + " old ids.");
|
||||
|
||||
try {
|
||||
client.uploadJSONDocument(getDocumentServerNamespace(),
|
||||
id,
|
||||
payload,
|
||||
oldIds,
|
||||
uploadDelegate);
|
||||
} catch (Exception e) {
|
||||
uploadDelegate.handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upload(long localTime, String id, Collection<String> oldIds, Delegate delegate) {
|
||||
// We abuse the life-cycle of an Android ContentProvider slightly by holding
|
||||
// onto a ContentProviderClient while we generate a payload. This keeps our
|
||||
// database storage alive, and may also allow us to share a database
|
||||
// connection with a BrowserHealthRecorder from Fennec. The ContentProvider
|
||||
// owns all underlying Storage instances, so we don't need to explicitly
|
||||
// close them.
|
||||
ContentProviderClient client = EnvironmentBuilder.getContentProviderClient(context);
|
||||
if (client == null) {
|
||||
// TODO: Bug 910898 - Store client failure in SharedPrefs so we can increment next time with storage.
|
||||
delegate.onHardFailure(localTime, null, "Could not fetch content provider client.", null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Storage instance is owned by HealthReportProvider, so we don't need to
|
||||
// close it. It's worth noting that this call will fail if called
|
||||
// out-of-process.
|
||||
final HealthReportDatabaseStorage storage = getStorage(client);
|
||||
if (storage == null) {
|
||||
// TODO: Bug 910898 - Store error in SharedPrefs so we can increment next time with storage.
|
||||
delegate.onHardFailure(localTime, null, "No storage when generating report.", null);
|
||||
return;
|
||||
}
|
||||
|
||||
long last = Math.max(getLastUploadLocalTime(), HealthReportConstants.EARLIEST_LAST_PING);
|
||||
if (!storage.hasEventSince(last)) {
|
||||
delegate.onHardFailure(localTime, null, "No new events in storage.", null);
|
||||
return;
|
||||
}
|
||||
|
||||
initializeStorageForUploadProviders(storage);
|
||||
|
||||
final SubmissionsTracker tracker =
|
||||
getSubmissionsTracker(storage, localTime, hasUploadBeenRequested());
|
||||
try {
|
||||
// TODO: Bug 910898 - Add errors from sharedPrefs to tracker.
|
||||
final JSONObject document = generateDocument(localTime, last, tracker);
|
||||
if (document == null) {
|
||||
delegate.onHardFailure(localTime, null, "Generator returned null document.", null);
|
||||
return;
|
||||
}
|
||||
|
||||
final BagheeraRequestDelegate uploadDelegate = tracker.getDelegate(delegate, localTime,
|
||||
true, id);
|
||||
this.uploadPayload(id, document.toString(), oldIds, uploadDelegate);
|
||||
} catch (Exception e) {
|
||||
// Incrementing the failure count here could potentially cause the failure count to be
|
||||
// incremented twice, but this helper class checks and prevents this.
|
||||
tracker.incrementUploadClientFailureCount();
|
||||
throw e;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// TODO: Bug 910898 - Store client failure in SharedPrefs so we can increment next time with storage.
|
||||
Logger.warn(LOG_TAG, "Got exception generating document.", e);
|
||||
delegate.onHardFailure(localTime, null, "Got exception uploading.", e);
|
||||
return;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
protected SubmissionsTracker getSubmissionsTracker(final HealthReportStorage storage,
|
||||
final long localTime, final boolean hasUploadBeenRequested) {
|
||||
return new SubmissionsTracker(storage, localTime, hasUploadBeenRequested);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(final long localTime, final String id, Delegate delegate) {
|
||||
final BagheeraClient client = new BagheeraClient(getDocumentServerURI());
|
||||
|
||||
Logger.pii(LOG_TAG, "Deleting health report with id " + id + ".");
|
||||
|
||||
BagheeraRequestDelegate deleteDelegate = new RequestDelegate(delegate, localTime, false, id);
|
||||
try {
|
||||
client.deleteDocument(getDocumentServerNamespace(), id, deleteDelegate);
|
||||
} catch (Exception e) {
|
||||
deleteDelegate.handleError(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected class RequestDelegate implements BagheeraRequestDelegate {
|
||||
protected final Delegate delegate;
|
||||
protected final boolean isUpload;
|
||||
protected final String methodString;
|
||||
protected final long localTime;
|
||||
protected final String id;
|
||||
|
||||
public RequestDelegate(Delegate delegate, long localTime, boolean isUpload, String id) {
|
||||
this.delegate = delegate;
|
||||
this.localTime = localTime;
|
||||
this.isUpload = isUpload;
|
||||
this.methodString = this.isUpload ? "upload" : "delete";
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUserAgent() {
|
||||
return HealthReportConstants.USER_AGENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleSuccess(int status, String namespace, String id, HttpResponse response) {
|
||||
BaseResource.consumeEntity(response);
|
||||
if (isUpload) {
|
||||
setLastUploadLocalTimeAndDocumentId(localTime, id);
|
||||
}
|
||||
Logger.debug(LOG_TAG, "Successful " + methodString + " at " + localTime + ".");
|
||||
delegate.onSuccess(localTime, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bagheera status codes:
|
||||
*
|
||||
* 403 Forbidden - Violated access restrictions. Most likely because of the method used.
|
||||
* 413 Request Too Large - Request payload was larger than the configured maximum.
|
||||
* 400 Bad Request - Returned if the POST/PUT failed validation in some manner.
|
||||
* 404 Not Found - Returned if the URI path doesn't exist or if the URI was not in the proper format.
|
||||
* 500 Server Error - General server error. Someone with access should look at the logs for more details.
|
||||
*/
|
||||
@Override
|
||||
public void handleFailure(int status, String namespace, HttpResponse response) {
|
||||
BaseResource.consumeEntity(response);
|
||||
Logger.debug(LOG_TAG, "Failed " + methodString + " at " + localTime + ".");
|
||||
if (status >= 500) {
|
||||
delegate.onSoftFailure(localTime, id, "Got status " + status + " from server.", null);
|
||||
return;
|
||||
}
|
||||
// Things are either bad locally (bad payload format, too much data) or
|
||||
// bad remotely (badly configured server, temporarily unavailable). Try
|
||||
// again tomorrow.
|
||||
delegate.onHardFailure(localTime, id, "Got status " + status + " from server.", null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleError(Exception e) {
|
||||
Logger.debug(LOG_TAG, "Exception during " + methodString + " at " + localTime + ".", e);
|
||||
if (e instanceof IOException) {
|
||||
// Let's assume IO exceptions are Android dropping the network.
|
||||
delegate.onSoftFailure(localTime, id, "Got exception during " + methodString + ".", e);
|
||||
return;
|
||||
}
|
||||
delegate.onHardFailure(localTime, id, "Got exception during " + methodString + ".", e);
|
||||
}
|
||||
};
|
||||
|
||||
private void initializeStorageForUploadProviders(HealthReportDatabaseStorage storage) {
|
||||
storage.beginInitialization();
|
||||
try {
|
||||
initializeSubmissionsProvider(storage);
|
||||
storage.finishInitialization();
|
||||
} catch (Exception e) {
|
||||
// TODO: Bug 910898 - Store error in SharedPrefs so we can increment next time with storage.
|
||||
storage.abortInitialization();
|
||||
throw new IllegalStateException("Could not initialize storage for upload provider.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeSubmissionsProvider(HealthReportDatabaseStorage storage) {
|
||||
storage.ensureMeasurementInitialized(
|
||||
MEASUREMENT_NAME_SUBMISSIONS,
|
||||
MEASUREMENT_VERSION_SUBMISSIONS,
|
||||
new MeasurementFields() {
|
||||
@Override
|
||||
public Iterable<FieldSpec> getFields() {
|
||||
final ArrayList<FieldSpec> out = new ArrayList<FieldSpec>();
|
||||
for (SubmissionsFieldName fieldName : SubmissionsFieldName.values()) {
|
||||
FieldSpec spec = new FieldSpec(fieldName.getName(), Field.TYPE_INTEGER_COUNTER);
|
||||
out.add(spec);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static enum SubmissionsFieldName {
|
||||
FIRST_ATTEMPT("firstDocumentUploadAttempt"),
|
||||
CONTINUATION_ATTEMPT("continuationDocumentUploadAttempt"),
|
||||
SUCCESS("uploadSuccess"),
|
||||
TRANSPORT_FAILURE("uploadTransportFailure"),
|
||||
SERVER_FAILURE("uploadServerFailure"),
|
||||
CLIENT_FAILURE("uploadClientFailure");
|
||||
|
||||
private final String name;
|
||||
|
||||
SubmissionsFieldName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public int getID(HealthReportStorage storage) {
|
||||
final Field field = storage.getField(MEASUREMENT_NAME_SUBMISSIONS,
|
||||
MEASUREMENT_VERSION_SUBMISSIONS,
|
||||
name);
|
||||
return field.getID();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encapsulates the counting mechanisms for submissions status counts. Ensures multiple failures
|
||||
* and successes are not recorded for a single instance.
|
||||
*/
|
||||
public class SubmissionsTracker {
|
||||
private final HealthReportStorage storage;
|
||||
private final ProfileInformationCache profileCache;
|
||||
private final int day;
|
||||
private final int envID;
|
||||
|
||||
private boolean isUploadStatusCountIncremented;
|
||||
|
||||
public SubmissionsTracker(final HealthReportStorage storage, final long localTime,
|
||||
final boolean hasUploadBeenRequested) throws IllegalStateException {
|
||||
this.storage = storage;
|
||||
this.profileCache = getProfileInformationCache();
|
||||
this.day = storage.getDay(localTime);
|
||||
this.envID = registerCurrentEnvironment();
|
||||
|
||||
this.isUploadStatusCountIncremented = false;
|
||||
|
||||
if (!hasUploadBeenRequested) {
|
||||
incrementFirstUploadAttemptCount();
|
||||
} else {
|
||||
incrementContinuationAttemptCount();
|
||||
}
|
||||
}
|
||||
|
||||
protected ProfileInformationCache getProfileInformationCache() {
|
||||
final ProfileInformationCache profileCache = new ProfileInformationCache(profilePath);
|
||||
if (!profileCache.restoreUnlessInitialized()) {
|
||||
Logger.warn(LOG_TAG, "Not enough profile information to compute current environment.");
|
||||
throw new IllegalStateException("Could not retrieve current environment.");
|
||||
}
|
||||
return profileCache;
|
||||
}
|
||||
|
||||
protected int registerCurrentEnvironment() {
|
||||
return EnvironmentBuilder.registerCurrentEnvironment(storage, profileCache, config);
|
||||
}
|
||||
|
||||
protected void incrementFirstUploadAttemptCount() {
|
||||
Logger.debug(LOG_TAG, "Incrementing first upload attempt field.");
|
||||
storage.incrementDailyCount(envID, day, SubmissionsFieldName.FIRST_ATTEMPT.getID(storage));
|
||||
}
|
||||
|
||||
protected void incrementContinuationAttemptCount() {
|
||||
Logger.debug(LOG_TAG, "Incrementing continuation upload attempt field.");
|
||||
storage.incrementDailyCount(envID, day, SubmissionsFieldName.CONTINUATION_ATTEMPT.getID(storage));
|
||||
}
|
||||
|
||||
public void incrementUploadSuccessCount() {
|
||||
incrementStatusCount(SubmissionsFieldName.SUCCESS.getID(storage), "success");
|
||||
}
|
||||
|
||||
public void incrementUploadClientFailureCount() {
|
||||
incrementStatusCount(SubmissionsFieldName.CLIENT_FAILURE.getID(storage), "client failure");
|
||||
}
|
||||
|
||||
public void incrementUploadTransportFailureCount() {
|
||||
incrementStatusCount(SubmissionsFieldName.TRANSPORT_FAILURE.getID(storage), "transport failure");
|
||||
}
|
||||
|
||||
public void incrementUploadServerFailureCount() {
|
||||
incrementStatusCount(SubmissionsFieldName.SERVER_FAILURE.getID(storage), "server failure");
|
||||
}
|
||||
|
||||
private void incrementStatusCount(final int fieldID, final String countType) {
|
||||
if (!isUploadStatusCountIncremented) {
|
||||
Logger.debug(LOG_TAG, "Incrementing upload attempt " + countType + " count.");
|
||||
storage.incrementDailyCount(envID, day, fieldID);
|
||||
isUploadStatusCountIncremented = true;
|
||||
} else {
|
||||
Logger.warn(LOG_TAG, "Upload status count already incremented - not incrementing " +
|
||||
countType + " count.");
|
||||
}
|
||||
}
|
||||
|
||||
public TrackingGenerator getGenerator() {
|
||||
return new TrackingGenerator();
|
||||
}
|
||||
|
||||
public class TrackingGenerator extends HealthReportGenerator {
|
||||
public TrackingGenerator() {
|
||||
super(storage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSONObject generateDocument(long since, long lastPingTime,
|
||||
String generationProfilePath, ConfigurationProvider providedConfig) throws JSONException {
|
||||
|
||||
// Let's make sure we have an accurate locale.
|
||||
Locales.getLocaleManager().getAndApplyPersistedLocale(context);
|
||||
|
||||
final JSONObject document;
|
||||
// If the given profilePath matches the one we cached for the tracker, use the cached env.
|
||||
if (profilePath != null && profilePath.equals(generationProfilePath)) {
|
||||
final Environment environment = getCurrentEnvironment();
|
||||
document = super.generateDocument(since, lastPingTime, environment);
|
||||
} else {
|
||||
document = super.generateDocument(since, lastPingTime, generationProfilePath, providedConfig);
|
||||
}
|
||||
|
||||
if (document == null) {
|
||||
incrementUploadClientFailureCount();
|
||||
}
|
||||
return document;
|
||||
}
|
||||
|
||||
protected Environment getCurrentEnvironment() {
|
||||
return EnvironmentBuilder.getCurrentEnvironment(profileCache, config);
|
||||
}
|
||||
}
|
||||
|
||||
public TrackingRequestDelegate getDelegate(final Delegate delegate, final long localTime,
|
||||
final boolean isUpload, final String id) {
|
||||
return new TrackingRequestDelegate(delegate, localTime, isUpload, id);
|
||||
}
|
||||
|
||||
public class TrackingRequestDelegate extends RequestDelegate {
|
||||
public TrackingRequestDelegate(final Delegate delegate, final long localTime,
|
||||
final boolean isUpload, final String id) {
|
||||
super(delegate, localTime, isUpload, id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleSuccess(int status, String namespace, String id, HttpResponse response) {
|
||||
super.handleSuccess(status, namespace, id, response);
|
||||
incrementUploadSuccessCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleFailure(int status, String namespace, HttpResponse response) {
|
||||
super.handleFailure(status, namespace, response);
|
||||
incrementUploadServerFailureCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleError(Exception e) {
|
||||
super.handleError(e);
|
||||
if (e instanceof IllegalArgumentException ||
|
||||
e instanceof UnsupportedEncodingException ||
|
||||
e instanceof URISyntaxException) {
|
||||
incrementUploadClientFailureCount();
|
||||
} else {
|
||||
incrementUploadTransportFailureCount();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,94 +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/. */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport.upload;
|
||||
|
||||
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 manage and upload health report data.
|
||||
*
|
||||
* We extend <code>IntentService</code>, rather than just <code>Service</code>,
|
||||
* because this gives us a worker thread to avoid main-thread networking.
|
||||
*
|
||||
* Yes, even though we're in an alarm-triggered service, it still counts as
|
||||
* main-thread.
|
||||
*/
|
||||
public class HealthReportUploadService extends BackgroundService {
|
||||
public static final String LOG_TAG = HealthReportUploadService.class.getSimpleName();
|
||||
public static final String WORKER_THREAD_NAME = LOG_TAG + "Worker";
|
||||
|
||||
public HealthReportUploadService() {
|
||||
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);
|
||||
|
||||
// Intent can be null. Bug 1025937.
|
||||
if (intent == null) {
|
||||
Logger.debug(LOG_TAG, "Short-circuiting on null intent.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (HealthReportConstants.UPLOAD_FEATURE_DISABLED) {
|
||||
Logger.debug(LOG_TAG, "Health report upload feature is compile-time disabled; not handling upload intent.");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.debug(LOG_TAG, "Health report upload feature is compile-time enabled; handling upload intent.");
|
||||
|
||||
String profileName = intent.getStringExtra("profileName");
|
||||
String profilePath = intent.getStringExtra("profilePath");
|
||||
|
||||
if (profileName == null || profilePath == null) {
|
||||
Logger.warn(LOG_TAG, "Got intent without profilePath or profileName. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!intent.hasExtra("uploadEnabled")) {
|
||||
Logger.warn(LOG_TAG, "Got intent without uploadEnabled. Ignoring.");
|
||||
return;
|
||||
}
|
||||
|
||||
// We disabled Health Report uploads in Bug 1230206, because the service is being decommissioned.
|
||||
// We chose this specific place to turn uploads off because we wish to preserve deletions in the
|
||||
// interim, and this is the tested code path for when a user turns off upload, but still expects
|
||||
// deletions to work.
|
||||
boolean uploadEnabled = false;
|
||||
|
||||
// Don't do anything if the device can't talk to the server.
|
||||
if (!backgroundDataIsEnabled()) {
|
||||
Logger.debug(LOG_TAG, "Background data is not enabled; skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
Logger.pii(LOG_TAG, "Ticking policy for profile " + profileName + " at " + profilePath + ".");
|
||||
|
||||
final SharedPreferences sharedPrefs = getSharedPreferences();
|
||||
final ObsoleteDocumentTracker tracker = new ObsoleteDocumentTracker(sharedPrefs);
|
||||
SubmissionClient client = new AndroidSubmissionClient(this, sharedPrefs, profilePath);
|
||||
SubmissionPolicy policy = new SubmissionPolicy(sharedPrefs, client, tracker, uploadEnabled);
|
||||
|
||||
final long now = System.currentTimeMillis();
|
||||
policy.tick(now);
|
||||
}
|
||||
}
|
|
@ -1,245 +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/. */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport.upload;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportConstants;
|
||||
import org.mozilla.gecko.sync.ExtendedJSONObject;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
public class ObsoleteDocumentTracker {
|
||||
public static final String LOG_TAG = ObsoleteDocumentTracker.class.getSimpleName();
|
||||
|
||||
protected final SharedPreferences sharedPrefs;
|
||||
|
||||
public ObsoleteDocumentTracker(SharedPreferences sharedPrefs) {
|
||||
this.sharedPrefs = sharedPrefs;
|
||||
}
|
||||
|
||||
protected ExtendedJSONObject getObsoleteIds() {
|
||||
String s = sharedPrefs.getString(HealthReportConstants.PREF_OBSOLETE_DOCUMENT_IDS_TO_DELETION_ATTEMPTS_REMAINING, null);
|
||||
if (s == null) {
|
||||
// It's possible we're migrating an old profile forward.
|
||||
String lastId = sharedPrefs.getString(HealthReportConstants.PREF_LAST_UPLOAD_DOCUMENT_ID, null);
|
||||
if (lastId == null) {
|
||||
return new ExtendedJSONObject();
|
||||
}
|
||||
ExtendedJSONObject ids = new ExtendedJSONObject();
|
||||
ids.put(lastId, HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID);
|
||||
setObsoleteIds(ids);
|
||||
return ids;
|
||||
}
|
||||
try {
|
||||
return ExtendedJSONObject.parseJSONObject(s);
|
||||
} catch (Exception e) {
|
||||
Logger.warn(LOG_TAG, "Got exception getting obsolete ids.", e);
|
||||
return new ExtendedJSONObject();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write obsolete ids to disk.
|
||||
*
|
||||
* @param ids to write.
|
||||
*/
|
||||
protected void setObsoleteIds(ExtendedJSONObject ids) {
|
||||
sharedPrefs
|
||||
.edit()
|
||||
.putString(HealthReportConstants.PREF_OBSOLETE_DOCUMENT_IDS_TO_DELETION_ATTEMPTS_REMAINING, ids.toString())
|
||||
.commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove id from set of obsolete document ids tracked for deletion.
|
||||
*
|
||||
* Public for testing.
|
||||
*
|
||||
* @param id to stop tracking.
|
||||
*/
|
||||
public void removeObsoleteId(String id) {
|
||||
ExtendedJSONObject ids = getObsoleteIds();
|
||||
ids.remove(id);
|
||||
setObsoleteIds(ids);
|
||||
}
|
||||
|
||||
protected void decrementObsoleteId(ExtendedJSONObject ids, String id) {
|
||||
if (!ids.containsKey(id)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Long attempts = ids.getLong(id);
|
||||
if (attempts == null || --attempts < 1) {
|
||||
ids.remove(id);
|
||||
} else {
|
||||
ids.put(id, attempts);
|
||||
}
|
||||
} catch (ClassCastException e) {
|
||||
ids.remove(id);
|
||||
Logger.info(LOG_TAG, "Got exception decrementing obsolete ids counter.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrement attempts remaining for id in set of obsolete document ids tracked
|
||||
* for deletion.
|
||||
*
|
||||
* Public for testing.
|
||||
*
|
||||
* @param id to decrement attempts.
|
||||
*/
|
||||
public void decrementObsoleteIdAttempts(String id) {
|
||||
ExtendedJSONObject ids = getObsoleteIds();
|
||||
decrementObsoleteId(ids, id);
|
||||
setObsoleteIds(ids);
|
||||
}
|
||||
|
||||
public void purgeObsoleteIds(Collection<String> oldIds) {
|
||||
ExtendedJSONObject ids = getObsoleteIds();
|
||||
for (String oldId : oldIds) {
|
||||
ids.remove(oldId);
|
||||
}
|
||||
setObsoleteIds(ids);
|
||||
}
|
||||
|
||||
public void decrementObsoleteIdAttempts(Collection<String> oldIds) {
|
||||
ExtendedJSONObject ids = getObsoleteIds();
|
||||
for (String oldId : oldIds) {
|
||||
decrementObsoleteId(ids, oldId);
|
||||
}
|
||||
setObsoleteIds(ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort Longs in decreasing order, moving null and non-Longs to the front.
|
||||
*
|
||||
* Public for testing only.
|
||||
*/
|
||||
public static class PairComparator implements Comparator<Entry<String, Object>> {
|
||||
@Override
|
||||
public int compare(Entry<String, Object> lhs, Entry<String, Object> rhs) {
|
||||
Object l = lhs.getValue();
|
||||
Object r = rhs.getValue();
|
||||
if (!(l instanceof Long)) {
|
||||
if (!(r instanceof Long)) {
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if (!(r instanceof Long)) {
|
||||
return 1;
|
||||
}
|
||||
return ((Long) r).compareTo((Long) l);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a batch of obsolete document IDs that should be deleted next.
|
||||
*
|
||||
* Document IDs are long and sending too many in a single request might
|
||||
* increase the likelihood of POST failures, so we delete a (deterministic)
|
||||
* subset here.
|
||||
*
|
||||
* @return a non-null collection.
|
||||
*/
|
||||
public Collection<String> getBatchOfObsoleteIds() {
|
||||
ExtendedJSONObject ids = getObsoleteIds();
|
||||
// Sort by increasing order of key values.
|
||||
List<Entry<String, Object>> pairs = new ArrayList<Entry<String,Object>>(ids.entrySet());
|
||||
Collections.sort(pairs, new PairComparator());
|
||||
List<String> batch = new ArrayList<String>(HealthReportConstants.MAXIMUM_DELETIONS_PER_POST);
|
||||
int i = 0;
|
||||
while (batch.size() < HealthReportConstants.MAXIMUM_DELETIONS_PER_POST && i < pairs.size()) {
|
||||
batch.add(pairs.get(i++).getKey());
|
||||
}
|
||||
return batch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Track the given document ID for eventual obsolescence and deletion.
|
||||
* Obsolete IDs are not known to have been uploaded to the server, so we just
|
||||
* give a best effort attempt at deleting them
|
||||
*
|
||||
* @param id to eventually delete.
|
||||
*/
|
||||
public void addObsoleteId(String id) {
|
||||
ExtendedJSONObject ids = getObsoleteIds();
|
||||
if (ids.size() >= HealthReportConstants.MAXIMUM_STORED_OBSOLETE_DOCUMENT_IDS) {
|
||||
// Remove the one that's been tried the most and is least likely to be
|
||||
// known to be on the server. Since the comparator orders in decreasing
|
||||
// order, we take the max.
|
||||
ids.remove(Collections.max(ids.entrySet(), new PairComparator()).getKey());
|
||||
}
|
||||
ids.put(id, HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID);
|
||||
setObsoleteIds(ids);
|
||||
}
|
||||
|
||||
/**
|
||||
* Track the given document ID for eventual obsolescence and deletion, and
|
||||
* give it priority since we know this ID has made it to the server, and we
|
||||
* definitely don't want to orphan it.
|
||||
*
|
||||
* @param id to eventually delete.
|
||||
*/
|
||||
public void markIdAsUploaded(String id) {
|
||||
ExtendedJSONObject ids = getObsoleteIds();
|
||||
ids.put(id, HealthReportConstants.DELETION_ATTEMPTS_PER_KNOWN_TO_BE_ON_SERVER_DOCUMENT_ID);
|
||||
setObsoleteIds(ids);
|
||||
}
|
||||
|
||||
public boolean hasObsoleteIds() {
|
||||
return getObsoleteIds().size() > 0;
|
||||
}
|
||||
|
||||
public int numberOfObsoleteIds() {
|
||||
return getObsoleteIds().size();
|
||||
}
|
||||
|
||||
public String getNextObsoleteId() {
|
||||
ExtendedJSONObject ids = getObsoleteIds();
|
||||
if (ids.size() < 1) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
// Delete the one that's most likely to be known to be on the server, and
|
||||
// that's not been tried as much. Since the comparator orders in
|
||||
// decreasing order, we take the min.
|
||||
return Collections.min(ids.entrySet(), new PairComparator()).getKey();
|
||||
} catch (Exception e) {
|
||||
Logger.warn(LOG_TAG, "Got exception picking obsolete id to delete.", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* We want cleaning up documents on the server to be best effort. Purge badly
|
||||
* formed IDs and cap the number of times we try to delete so that the queue
|
||||
* doesn't take too long.
|
||||
*/
|
||||
public void limitObsoleteIds() {
|
||||
ExtendedJSONObject ids = getObsoleteIds();
|
||||
|
||||
Set<String> keys = new HashSet<String>(ids.keySet()); // Avoid invalidating an iterator.
|
||||
for (String key : keys) {
|
||||
Object o = ids.get(key);
|
||||
if (!(o instanceof Long)) {
|
||||
continue;
|
||||
}
|
||||
if ((Long) o > HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID) {
|
||||
ids.put(key, HealthReportConstants.DELETION_ATTEMPTS_PER_OBSOLETE_DOCUMENT_ID);
|
||||
}
|
||||
}
|
||||
setObsoleteIds(ids);
|
||||
}
|
||||
}
|
|
@ -1,42 +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/. */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport.upload;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public interface SubmissionClient {
|
||||
public interface Delegate {
|
||||
/**
|
||||
* Called in the event of a temporary failure; we should try again soon.
|
||||
*
|
||||
* @param localTime milliseconds since the epoch.
|
||||
* @param id if known; may be null.
|
||||
* @param reason for failure.
|
||||
* @param e if there was an exception; may be null.
|
||||
*/
|
||||
public void onSoftFailure(long localTime, String id, String reason, Exception e);
|
||||
|
||||
/**
|
||||
* Called in the event of a failure; we should try again, but not today.
|
||||
*
|
||||
* @param localTime milliseconds since the epoch.
|
||||
* @param id if known; may be null.
|
||||
* @param reason for failure.
|
||||
* @param e if there was an exception; may be null.
|
||||
*/
|
||||
public void onHardFailure(long localTime, String id, String reason, Exception e);
|
||||
|
||||
/**
|
||||
* Success!
|
||||
*
|
||||
* @param localTime milliseconds since the epoch.
|
||||
* @param id is always known; not null.
|
||||
*/
|
||||
public void onSuccess(long localTime, String id);
|
||||
}
|
||||
|
||||
public void upload(long localTime, String id, Collection<String> oldIds, Delegate delegate);
|
||||
public void delete(long localTime, String id, Delegate delegate);
|
||||
}
|
|
@ -1,462 +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/. */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport.upload;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.SocketException;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.Collection;
|
||||
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportConstants;
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportUtils;
|
||||
import org.mozilla.gecko.background.healthreport.upload.SubmissionClient.Delegate;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
/**
|
||||
* Manages scheduling of Firefox Health Report data submission.
|
||||
*
|
||||
* The rules of data submission are as follows:
|
||||
*
|
||||
* 1. Do not submit data more than once every 24 hours.
|
||||
*
|
||||
* 2. Try to submit as close to 24 hours apart as possible.
|
||||
*
|
||||
* 3. Do not submit too soon after application startup so as to not negatively
|
||||
* impact performance at startup.
|
||||
*
|
||||
* 4. Before first ever data submission, the user should be notified about data
|
||||
* collection practices.
|
||||
*
|
||||
* 5. User should have opportunity to react to this notification before data
|
||||
* submission.
|
||||
*
|
||||
* 6. Display of notification without any explicit user action constitutes
|
||||
* implicit consent after a certain duration of time.
|
||||
*
|
||||
* 7. If data submission fails, try at most 2 additional times before giving up
|
||||
* on that day's submission.
|
||||
*
|
||||
* On Android, items 4, 5, and 6 are addressed by displaying an Android
|
||||
* notification on first run.
|
||||
*/
|
||||
public class SubmissionPolicy {
|
||||
public static final String LOG_TAG = SubmissionPolicy.class.getSimpleName();
|
||||
|
||||
protected final SharedPreferences sharedPreferences;
|
||||
protected final SubmissionClient client;
|
||||
protected final boolean uploadEnabled;
|
||||
protected final ObsoleteDocumentTracker tracker;
|
||||
|
||||
public SubmissionPolicy(final SharedPreferences sharedPreferences,
|
||||
final SubmissionClient client,
|
||||
final ObsoleteDocumentTracker tracker,
|
||||
boolean uploadEnabled) {
|
||||
if (sharedPreferences == null) {
|
||||
throw new IllegalArgumentException("sharedPreferences must not be null");
|
||||
}
|
||||
this.sharedPreferences = sharedPreferences;
|
||||
this.client = client;
|
||||
this.tracker = tracker;
|
||||
this.uploadEnabled = uploadEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check what action must happen, advance counters and timestamps, and
|
||||
* possibly spawn a request to the server.
|
||||
*
|
||||
* @param localTime now.
|
||||
* @return true if a request was spawned; false otherwise.
|
||||
*/
|
||||
public boolean tick(final long localTime) {
|
||||
final long nextUpload = getNextSubmission();
|
||||
|
||||
// If the system clock were ever set to a time in the distant future,
|
||||
// it's possible our next schedule date is far out as well. We know
|
||||
// we shouldn't schedule for more than a day out, so we reset the next
|
||||
// scheduled date appropriately. 3 days was chosen to match desktop's
|
||||
// arbitrary choice.
|
||||
if (nextUpload >= localTime + 3 * getMinimumTimeBetweenUploads()) {
|
||||
Logger.warn(LOG_TAG, "Next upload scheduled far in the future; system clock reset? " + nextUpload + " > " + localTime);
|
||||
// Things are strange, we want to start again but we don't want to stampede.
|
||||
editor()
|
||||
.setNextSubmission(localTime + getMinimumTimeBetweenUploads())
|
||||
.commit();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't upload unless an interval has elapsed.
|
||||
if (localTime < nextUpload) {
|
||||
Logger.debug(LOG_TAG, "We uploaded less than an interval ago; skipping. " + nextUpload + " > " + localTime);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!uploadEnabled) {
|
||||
// We only delete (rather than mark as obsolete during upload) when
|
||||
// uploading is disabled. We try to delete aggressively, since the volume
|
||||
// of deletes should be very low. But we don't want to send too many
|
||||
// delete requests at the same time, so we process these one at a time. In
|
||||
// the future (Bug 872756), we will be able to delete multiple documents
|
||||
// with one request.
|
||||
final String obsoleteId = tracker.getNextObsoleteId();
|
||||
if (obsoleteId == null) {
|
||||
Logger.debug(LOG_TAG, "Upload disabled and nothing to delete.");
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger.info(LOG_TAG, "Upload disabled. Deleting obsolete document.");
|
||||
Editor editor = editor();
|
||||
editor.setLastDeleteRequested(localTime); // Write committed by delegate.
|
||||
client.delete(localTime, obsoleteId, new DeleteDelegate(editor));
|
||||
return true;
|
||||
}
|
||||
|
||||
long firstRun = getFirstRunLocalTime();
|
||||
if (firstRun < 0) {
|
||||
firstRun = localTime;
|
||||
// Make sure we start clean and as soon as possible.
|
||||
editor()
|
||||
.setFirstRunLocalTime(firstRun)
|
||||
.setNextSubmission(localTime + getMinimumTimeBeforeFirstSubmission())
|
||||
.setCurrentDayFailureCount(0)
|
||||
.commit();
|
||||
}
|
||||
|
||||
// This case will occur if the nextSubmission time is not set (== -1) but firstRun is.
|
||||
if (localTime < firstRun + getMinimumTimeBeforeFirstSubmission()) {
|
||||
Logger.info(LOG_TAG, "Need to wait " + getMinimumTimeBeforeFirstSubmission() + " before first upload.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// The first upload attempt for a given document submission begins a 24-hour period in which
|
||||
// the upload will retry upon a soft failure. At the end of this period, the submission
|
||||
// failure count is reset, ensuring each day's first submission attempt has a zeroed failure
|
||||
// count. A period may also end on upload success or hard failure.
|
||||
if (localTime >= getCurrentDayResetTime()) {
|
||||
editor()
|
||||
.setCurrentDayResetTime(localTime + getMinimumTimeBetweenUploads())
|
||||
.setCurrentDayFailureCount(0)
|
||||
.commit();
|
||||
}
|
||||
|
||||
String id = HealthReportUtils.generateDocumentId();
|
||||
Collection<String> oldIds = tracker.getBatchOfObsoleteIds();
|
||||
tracker.addObsoleteId(id);
|
||||
|
||||
Editor editor = editor();
|
||||
editor.setLastUploadRequested(localTime); // Write committed by delegate.
|
||||
client.upload(localTime, id, oldIds, new UploadDelegate(editor, oldIds));
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the upload that produced <code>e</code> definitely did not
|
||||
* produce a new record on the remote server.
|
||||
*
|
||||
* @param e
|
||||
* <code>Exception</code> that upload produced.
|
||||
* @return true if the server could not have a new record.
|
||||
*/
|
||||
protected boolean isLocalException(Exception e) {
|
||||
return (e instanceof MalformedURLException) ||
|
||||
(e instanceof SocketException) ||
|
||||
(e instanceof UnknownHostException);
|
||||
}
|
||||
|
||||
protected class UploadDelegate implements Delegate {
|
||||
protected final Editor editor;
|
||||
protected final Collection<String> oldIds;
|
||||
|
||||
public UploadDelegate(Editor editor, Collection<String> oldIds) {
|
||||
this.editor = editor;
|
||||
this.oldIds = oldIds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(long localTime, String id) {
|
||||
long next = localTime + getMinimumTimeBetweenUploads();
|
||||
tracker.markIdAsUploaded(id);
|
||||
tracker.purgeObsoleteIds(oldIds);
|
||||
editor
|
||||
.setNextSubmission(next)
|
||||
.setLastUploadSucceeded(localTime)
|
||||
.setCurrentDayFailureCount(0)
|
||||
.clearCurrentDayResetTime() // Set again on the next submission's first upload attempt.
|
||||
.commit();
|
||||
if (Logger.LOG_PERSONAL_INFORMATION) {
|
||||
Logger.pii(LOG_TAG, "Successful upload with id " + id + " obsoleting "
|
||||
+ oldIds.size() + " old records reported at " + localTime + "; next upload at " + next + ".");
|
||||
} else {
|
||||
Logger.info(LOG_TAG, "Successful upload obsoleting " + oldIds.size()
|
||||
+ " old records reported at " + localTime + "; next upload at " + next + ".");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHardFailure(long localTime, String id, String reason, Exception e) {
|
||||
long next = localTime + getMinimumTimeBetweenUploads();
|
||||
if (isLocalException(e)) {
|
||||
Logger.info(LOG_TAG, "Hard failure caused by local exception; not tracking id and not decrementing attempts.");
|
||||
tracker.removeObsoleteId(id);
|
||||
} else {
|
||||
tracker.decrementObsoleteIdAttempts(oldIds);
|
||||
}
|
||||
editor
|
||||
.setNextSubmission(next)
|
||||
.setLastUploadFailed(localTime)
|
||||
.setCurrentDayFailureCount(0)
|
||||
.clearCurrentDayResetTime() // Set again on the next submission's first upload attempt.
|
||||
.commit();
|
||||
Logger.warn(LOG_TAG, "Hard failure reported at " + localTime + ": " + reason + " Next upload at " + next + ".", e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSoftFailure(long localTime, String id, String reason, Exception e) {
|
||||
int failuresToday = getCurrentDayFailureCount();
|
||||
Logger.warn(LOG_TAG, "Soft failure reported at " + localTime + ": " + reason + " Previously failed " + failuresToday + " time(s) today.");
|
||||
|
||||
if (failuresToday >= getMaximumFailuresPerDay()) {
|
||||
onHardFailure(localTime, id, "Reached the limit of daily upload attempts: " + failuresToday, e);
|
||||
return;
|
||||
}
|
||||
|
||||
long next = localTime + getMinimumTimeAfterFailure();
|
||||
if (isLocalException(e)) {
|
||||
Logger.info(LOG_TAG, "Soft failure caused by local exception; not tracking id and not decrementing attempts.");
|
||||
tracker.removeObsoleteId(id);
|
||||
} else {
|
||||
tracker.decrementObsoleteIdAttempts(oldIds);
|
||||
}
|
||||
editor
|
||||
.setNextSubmission(next)
|
||||
.setLastUploadFailed(localTime)
|
||||
.setCurrentDayFailureCount(failuresToday + 1)
|
||||
.commit();
|
||||
Logger.info(LOG_TAG, "Retrying upload at " + next + ".");
|
||||
}
|
||||
}
|
||||
|
||||
protected class DeleteDelegate implements Delegate {
|
||||
protected final Editor editor;
|
||||
|
||||
public DeleteDelegate(Editor editor) {
|
||||
this.editor = editor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSoftFailure(final long localTime, String id, String reason, Exception e) {
|
||||
long next = localTime + getMinimumTimeBetweenDeletes();
|
||||
if (isLocalException(e)) {
|
||||
Logger.info(LOG_TAG, "Soft failure caused by local exception; not decrementing attempts.");
|
||||
} else {
|
||||
tracker.decrementObsoleteIdAttempts(id);
|
||||
}
|
||||
editor
|
||||
.setNextSubmission(next)
|
||||
.setLastDeleteFailed(localTime)
|
||||
.commit();
|
||||
|
||||
if (Logger.LOG_PERSONAL_INFORMATION) {
|
||||
Logger.info(LOG_TAG, "Got soft failure at " + localTime + " deleting obsolete document with id " + id + ": " + reason + " Trying again later.");
|
||||
} else {
|
||||
Logger.info(LOG_TAG, "Got soft failure at " + localTime + " deleting obsolete document: " + reason + " Trying again later.");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHardFailure(final long localTime, String id, String reason, Exception e) {
|
||||
// We're never going to be able to delete this id, so don't keep trying.
|
||||
long next = localTime + getMinimumTimeBetweenDeletes();
|
||||
tracker.removeObsoleteId(id);
|
||||
editor
|
||||
.setNextSubmission(next)
|
||||
.setLastDeleteFailed(localTime)
|
||||
.commit();
|
||||
|
||||
if (Logger.LOG_PERSONAL_INFORMATION) {
|
||||
Logger.warn(LOG_TAG, "Got hard failure at " + localTime + " deleting obsolete document with id " + id + ": " + reason + " Abandoning delete request.", e);
|
||||
} else {
|
||||
Logger.warn(LOG_TAG, "Got hard failure at " + localTime + " deleting obsolete document: " + reason + " Abandoning delete request.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(final long localTime, String id) {
|
||||
long next = localTime + getMinimumTimeBetweenDeletes();
|
||||
tracker.removeObsoleteId(id);
|
||||
editor
|
||||
.setNextSubmission(next)
|
||||
.setLastDeleteSucceeded(localTime)
|
||||
.commit();
|
||||
|
||||
if (Logger.LOG_PERSONAL_INFORMATION) {
|
||||
Logger.pii(LOG_TAG, "Deleted an obsolete document with id " + id + " at " + localTime + ".");
|
||||
} else {
|
||||
Logger.info(LOG_TAG, "Deleted an obsolete document at " + localTime + ".");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public SharedPreferences getSharedPreferences() {
|
||||
return this.sharedPreferences;
|
||||
}
|
||||
|
||||
public long getMinimumTimeBetweenUploads() {
|
||||
return getSharedPreferences().getLong(HealthReportConstants.PREF_MINIMUM_TIME_BETWEEN_UPLOADS, HealthReportConstants.DEFAULT_MINIMUM_TIME_BETWEEN_UPLOADS);
|
||||
}
|
||||
|
||||
public long getMinimumTimeBeforeFirstSubmission() {
|
||||
return getSharedPreferences().getLong(HealthReportConstants.PREF_MINIMUM_TIME_BEFORE_FIRST_SUBMISSION, HealthReportConstants.DEFAULT_MINIMUM_TIME_BEFORE_FIRST_SUBMISSION);
|
||||
}
|
||||
|
||||
public long getMinimumTimeAfterFailure() {
|
||||
return getSharedPreferences().getLong(HealthReportConstants.PREF_MINIMUM_TIME_AFTER_FAILURE, HealthReportConstants.DEFAULT_MINIMUM_TIME_AFTER_FAILURE);
|
||||
}
|
||||
|
||||
public long getMaximumFailuresPerDay() {
|
||||
return getSharedPreferences().getLong(HealthReportConstants.PREF_MAXIMUM_FAILURES_PER_DAY, HealthReportConstants.DEFAULT_MAXIMUM_FAILURES_PER_DAY);
|
||||
}
|
||||
|
||||
// Authoritative.
|
||||
public long getFirstRunLocalTime() {
|
||||
return getSharedPreferences().getLong(HealthReportConstants.PREF_FIRST_RUN, -1);
|
||||
}
|
||||
|
||||
// Authoritative.
|
||||
public long getNextSubmission() {
|
||||
return getSharedPreferences().getLong(HealthReportConstants.PREF_NEXT_SUBMISSION, -1);
|
||||
}
|
||||
|
||||
// Authoritative.
|
||||
public int getCurrentDayFailureCount() {
|
||||
return getSharedPreferences().getInt(HealthReportConstants.PREF_CURRENT_DAY_FAILURE_COUNT, 0);
|
||||
}
|
||||
|
||||
// Authoritative.
|
||||
public long getCurrentDayResetTime() {
|
||||
return getSharedPreferences().getLong(HealthReportConstants.PREF_CURRENT_DAY_RESET_TIME, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* To avoid writing to disk multiple times, we encapsulate writes in a
|
||||
* helper class. Be sure to call <code>commit</code> to flush to disk!
|
||||
*/
|
||||
protected Editor editor() {
|
||||
return new Editor(getSharedPreferences().edit());
|
||||
}
|
||||
|
||||
protected static class Editor {
|
||||
protected final SharedPreferences.Editor editor;
|
||||
|
||||
public Editor(SharedPreferences.Editor editor) {
|
||||
this.editor = editor;
|
||||
}
|
||||
|
||||
public void commit() {
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
// Authoritative.
|
||||
public Editor setFirstRunLocalTime(long localTime) {
|
||||
editor.putLong(HealthReportConstants.PREF_FIRST_RUN, localTime);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Authoritative.
|
||||
public Editor setNextSubmission(long localTime) {
|
||||
editor.putLong(HealthReportConstants.PREF_NEXT_SUBMISSION, localTime);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Authoritative.
|
||||
public Editor setCurrentDayFailureCount(int failureCount) {
|
||||
editor.putInt(HealthReportConstants.PREF_CURRENT_DAY_FAILURE_COUNT, failureCount);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Authoritative.
|
||||
public Editor setCurrentDayResetTime(long resetTime) {
|
||||
editor.putLong(HealthReportConstants.PREF_CURRENT_DAY_RESET_TIME, resetTime);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Authoritative.
|
||||
public Editor clearCurrentDayResetTime() {
|
||||
editor.putLong(HealthReportConstants.PREF_CURRENT_DAY_RESET_TIME, -1);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Authoritative.
|
||||
public Editor setLastUploadRequested(long localTime) {
|
||||
editor.putLong(HealthReportConstants.PREF_LAST_UPLOAD_REQUESTED, localTime);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Forensics only.
|
||||
public Editor setLastUploadSucceeded(long localTime) {
|
||||
editor.putLong(HealthReportConstants.PREF_LAST_UPLOAD_SUCCEEDED, localTime);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Forensics only.
|
||||
public Editor setLastUploadFailed(long localTime) {
|
||||
editor.putLong(HealthReportConstants.PREF_LAST_UPLOAD_FAILED, localTime);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Forensics only.
|
||||
public Editor setLastDeleteRequested(long localTime) {
|
||||
editor.putLong(HealthReportConstants.PREF_LAST_DELETE_REQUESTED, localTime);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Forensics only.
|
||||
public Editor setLastDeleteSucceeded(long localTime) {
|
||||
editor.putLong(HealthReportConstants.PREF_LAST_DELETE_SUCCEEDED, localTime);
|
||||
return this;
|
||||
}
|
||||
|
||||
// Forensics only.
|
||||
public Editor setLastDeleteFailed(long localTime) {
|
||||
editor.putLong(HealthReportConstants.PREF_LAST_DELETE_FAILED, localTime);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
// Authoritative.
|
||||
public long getLastUploadRequested() {
|
||||
return getSharedPreferences().getLong(HealthReportConstants.PREF_LAST_UPLOAD_REQUESTED, -1);
|
||||
}
|
||||
|
||||
// Forensics only.
|
||||
public long getLastUploadSucceeded() {
|
||||
return getSharedPreferences().getLong(HealthReportConstants.PREF_LAST_UPLOAD_SUCCEEDED, -1);
|
||||
}
|
||||
|
||||
// Forensics only.
|
||||
public long getLastUploadFailed() {
|
||||
return getSharedPreferences().getLong(HealthReportConstants.PREF_LAST_UPLOAD_FAILED, -1);
|
||||
}
|
||||
|
||||
// Forensics only.
|
||||
public long getLastDeleteRequested() {
|
||||
return getSharedPreferences().getLong(HealthReportConstants.PREF_LAST_DELETE_REQUESTED, -1);
|
||||
}
|
||||
|
||||
// Forensics only.
|
||||
public long getLastDeleteSucceeded() {
|
||||
return getSharedPreferences().getLong(HealthReportConstants.PREF_LAST_DELETE_SUCCEEDED, -1);
|
||||
}
|
||||
|
||||
// Forensics only.
|
||||
public long getLastDeleteFailed() {
|
||||
return getSharedPreferences().getLong(HealthReportConstants.PREF_LAST_DELETE_FAILED, -1);
|
||||
}
|
||||
|
||||
public long getMinimumTimeBetweenDeletes() {
|
||||
return getSharedPreferences().getLong(HealthReportConstants.PREF_MINIMUM_TIME_BETWEEN_DELETES, HealthReportConstants.DEFAULT_MINIMUM_TIME_BETWEEN_DELETES);
|
||||
}
|
||||
}
|
|
@ -26,22 +26,6 @@ background_junit3_sources = [
|
|||
'src/org/mozilla/gecko/background/fxa/TestAccountLoader.java',
|
||||
'src/org/mozilla/gecko/background/fxa/TestBrowserIDKeyPairGeneration.java',
|
||||
'src/org/mozilla/gecko/background/fxa/TestFirefoxAccounts.java',
|
||||
'src/org/mozilla/gecko/background/healthreport/MockDatabaseEnvironment.java',
|
||||
'src/org/mozilla/gecko/background/healthreport/MockHealthReportDatabaseStorage.java',
|
||||
'src/org/mozilla/gecko/background/healthreport/MockHealthReportSQLiteOpenHelper.java',
|
||||
'src/org/mozilla/gecko/background/healthreport/MockProfileInformationCache.java',
|
||||
'src/org/mozilla/gecko/background/healthreport/prune/TestHealthReportPruneService.java',
|
||||
'src/org/mozilla/gecko/background/healthreport/prune/TestPrunePolicyDatabaseStorage.java',
|
||||
'src/org/mozilla/gecko/background/healthreport/TestEnvironmentBuilder.java',
|
||||
'src/org/mozilla/gecko/background/healthreport/TestEnvironmentV1HashAppender.java',
|
||||
'src/org/mozilla/gecko/background/healthreport/TestHealthReportBroadcastService.java',
|
||||
'src/org/mozilla/gecko/background/healthreport/TestHealthReportDatabaseStorage.java',
|
||||
'src/org/mozilla/gecko/background/healthreport/TestHealthReportGenerator.java',
|
||||
'src/org/mozilla/gecko/background/healthreport/TestHealthReportProvider.java',
|
||||
'src/org/mozilla/gecko/background/healthreport/TestHealthReportSQLiteOpenHelper.java',
|
||||
'src/org/mozilla/gecko/background/healthreport/TestProfileInformationCache.java',
|
||||
'src/org/mozilla/gecko/background/healthreport/upload/TestAndroidSubmissionClient.java',
|
||||
'src/org/mozilla/gecko/background/healthreport/upload/TestHealthReportUploadService.java',
|
||||
'src/org/mozilla/gecko/background/helpers/AndroidSyncTestCase.java',
|
||||
'src/org/mozilla/gecko/background/helpers/BackgroundServiceTestCase.java',
|
||||
'src/org/mozilla/gecko/background/helpers/DBHelpers.java',
|
||||
|
@ -105,7 +89,6 @@ background_junit3_sources = [
|
|||
'src/org/mozilla/gecko/background/testhelpers/MockRecord.java',
|
||||
'src/org/mozilla/gecko/background/testhelpers/MockServerSyncStage.java',
|
||||
'src/org/mozilla/gecko/background/testhelpers/MockSharedPreferences.java',
|
||||
'src/org/mozilla/gecko/background/testhelpers/StubDelegate.java',
|
||||
'src/org/mozilla/gecko/background/testhelpers/WaitHelper.java',
|
||||
'src/org/mozilla/gecko/background/testhelpers/WBORepository.java',
|
||||
]
|
||||
|
|
|
@ -19,18 +19,6 @@ subsuite = background
|
|||
[src/org/mozilla/gecko/background/db/TestPasswordsRepository.java]
|
||||
[src/org/mozilla/gecko/background/fxa/TestBrowserIDKeyPairGeneration.java]
|
||||
[src/org/mozilla/gecko/background/fxa/authenticator/TestAccountPickler.java]
|
||||
[src/org/mozilla/gecko/background/healthreport/TestEnvironmentBuilder.java]
|
||||
[src/org/mozilla/gecko/background/healthreport/TestEnvironmentV1HashAppender.java]
|
||||
[src/org/mozilla/gecko/background/healthreport/TestHealthReportBroadcastService.java]
|
||||
[src/org/mozilla/gecko/background/healthreport/TestHealthReportDatabaseStorage.java]
|
||||
[src/org/mozilla/gecko/background/healthreport/TestHealthReportGenerator.java]
|
||||
[src/org/mozilla/gecko/background/healthreport/TestHealthReportProvider.java]
|
||||
[src/org/mozilla/gecko/background/healthreport/TestHealthReportSQLiteOpenHelper.java]
|
||||
[src/org/mozilla/gecko/background/healthreport/TestProfileInformationCache.java]
|
||||
[src/org/mozilla/gecko/background/healthreport/prune/TestHealthReportPruneService.java]
|
||||
[src/org/mozilla/gecko/background/healthreport/prune/TestPrunePolicyDatabaseStorage.java]
|
||||
[src/org/mozilla/gecko/background/healthreport/upload/TestAndroidSubmissionClient.java]
|
||||
[src/org/mozilla/gecko/background/healthreport/upload/TestHealthReportUploadService.java]
|
||||
[src/org/mozilla/gecko/background/nativecode/test/TestNativeCrypto.java]
|
||||
[src/org/mozilla/gecko/background/sync/TestAccountPickler.java]
|
||||
[src/org/mozilla/gecko/background/sync/TestClientsStage.java]
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport;
|
||||
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage;
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage.DatabaseEnvironment;
|
||||
|
||||
public class MockDatabaseEnvironment extends DatabaseEnvironment {
|
||||
public MockDatabaseEnvironment(HealthReportDatabaseStorage storage, Class<? extends EnvironmentAppender> appender) {
|
||||
super(storage, appender);
|
||||
}
|
||||
|
||||
public MockDatabaseEnvironment(HealthReportDatabaseStorage storage) {
|
||||
super(storage);
|
||||
}
|
||||
|
||||
public static class MockEnvironmentAppender extends EnvironmentAppender {
|
||||
public StringBuilder appended = new StringBuilder();
|
||||
|
||||
public MockEnvironmentAppender() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void append(String s) {
|
||||
appended.append(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void append(int v) {
|
||||
appended.append(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return appended.toString();
|
||||
}
|
||||
}
|
||||
|
||||
public MockDatabaseEnvironment mockInit(String appVersion) {
|
||||
profileCreation = 1234;
|
||||
cpuCount = 2;
|
||||
memoryMB = 512;
|
||||
|
||||
isBlocklistEnabled = 1;
|
||||
isTelemetryEnabled = 1;
|
||||
extensionCount = 0;
|
||||
pluginCount = 0;
|
||||
themeCount = 0;
|
||||
|
||||
architecture = "";
|
||||
sysName = "";
|
||||
sysVersion = "";
|
||||
vendor = "";
|
||||
appName = "";
|
||||
appID = "";
|
||||
this.appVersion = appVersion;
|
||||
appBuildID = "";
|
||||
platformVersion = "";
|
||||
platformBuildID = "";
|
||||
os = "";
|
||||
xpcomabi = "";
|
||||
updateChannel = "";
|
||||
|
||||
// v2 fields.
|
||||
distribution = "";
|
||||
appLocale = "";
|
||||
osLocale = "";
|
||||
acceptLangSet = 0;
|
||||
|
||||
version = Environment.CURRENT_VERSION;
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -1,280 +0,0 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.gecko.background.common.GlobalConstants;
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage;
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields.FieldSpec;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
public class MockHealthReportDatabaseStorage extends HealthReportDatabaseStorage {
|
||||
public long now = System.currentTimeMillis();
|
||||
|
||||
public long getOneDayAgo() {
|
||||
return now - GlobalConstants.MILLISECONDS_PER_DAY;
|
||||
}
|
||||
|
||||
public int getYesterday() {
|
||||
return super.getDay(this.getOneDayAgo());
|
||||
}
|
||||
|
||||
public int getToday() {
|
||||
return super.getDay(now);
|
||||
}
|
||||
|
||||
public int getTomorrow() {
|
||||
return super.getDay(now + GlobalConstants.MILLISECONDS_PER_DAY);
|
||||
}
|
||||
|
||||
public int getGivenDaysAgo(int numDays) {
|
||||
return super.getDay(this.getGivenDaysAgoMillis(numDays));
|
||||
}
|
||||
|
||||
public long getGivenDaysAgoMillis(int numDays) {
|
||||
return now - numDays * GlobalConstants.MILLISECONDS_PER_DAY;
|
||||
}
|
||||
|
||||
public ConcurrentHashMap<String, Integer> getEnvironmentCache() {
|
||||
return this.envs;
|
||||
}
|
||||
|
||||
public MockHealthReportDatabaseStorage(Context context, File fakeProfileDirectory) {
|
||||
super(context, fakeProfileDirectory);
|
||||
}
|
||||
|
||||
public SQLiteDatabase getDB() {
|
||||
return this.helper.getWritableDatabase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MockDatabaseEnvironment getEnvironment() {
|
||||
return new MockDatabaseEnvironment(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteEnvAndEventsBefore(long time, int curEnv) {
|
||||
return super.deleteEnvAndEventsBefore(time, curEnv);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteOrphanedEnv(int curEnv) {
|
||||
return super.deleteOrphanedEnv(curEnv);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteEventsBefore(String dayString) {
|
||||
return super.deleteEventsBefore(dayString);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deleteOrphanedAddons() {
|
||||
return super.deleteOrphanedAddons();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIntFromQuery(final String sql, final String[] selectionArgs) {
|
||||
return super.getIntFromQuery(sql, selectionArgs);
|
||||
}
|
||||
|
||||
/**
|
||||
* A storage instance prepopulated with dummy data to be used for testing.
|
||||
*
|
||||
* Modifying this data directly will cause tests relying on it to fail so use the versioned
|
||||
* constructor to change the data if it's the desired version. Example:
|
||||
* <pre>
|
||||
* if (version >= 3) {
|
||||
* addVersion3Stuff();
|
||||
* }
|
||||
* if (version >= 2) {
|
||||
* addVersion2Stuff();
|
||||
* }
|
||||
* addVersion1Stuff();
|
||||
* </pre>
|
||||
*
|
||||
* Don't forget to increment the {@link MAX_VERSION_USED} constant.
|
||||
*
|
||||
* Note that all instances of this class use the same underlying database and so each newly
|
||||
* created instance will share the same data.
|
||||
*/
|
||||
public static class PrepopulatedMockHealthReportDatabaseStorage extends MockHealthReportDatabaseStorage {
|
||||
// A constant to enforce which version constructor is the maximum used so far.
|
||||
private int MAX_VERSION_USED = 2;
|
||||
|
||||
public String[] measurementNames;
|
||||
public int[] measurementVers;
|
||||
public FieldSpecContainer[] fieldSpecContainers;
|
||||
public int env;
|
||||
private final JSONObject addonJSON = new JSONObject(
|
||||
"{ " +
|
||||
"\"amznUWL2@amazon.com\": { " +
|
||||
" \"userDisabled\": false, " +
|
||||
" \"appDisabled\": false, " +
|
||||
" \"version\": \"1.10\", " +
|
||||
" \"type\": \"extension\", " +
|
||||
" \"scope\": 1, " +
|
||||
" \"foreignInstall\": false, " +
|
||||
" \"hasBinaryComponents\": false, " +
|
||||
" \"installDay\": 15269, " +
|
||||
" \"updateDay\": 15602 " +
|
||||
"}, " +
|
||||
"\"jid0-qBnIpLfDFa4LpdrjhAC6vBqN20Q@jetpack\": { " +
|
||||
" \"userDisabled\": false, " +
|
||||
" \"appDisabled\": false, " +
|
||||
" \"version\": \"1.12.1\", " +
|
||||
" \"type\": \"extension\", " +
|
||||
" \"scope\": 1, " +
|
||||
" \"foreignInstall\": false, " +
|
||||
" \"hasBinaryComponents\": false, " +
|
||||
" \"installDay\": 15062, " +
|
||||
" \"updateDay\": 15580 " +
|
||||
"} " +
|
||||
"} ");
|
||||
|
||||
public static class FieldSpecContainer {
|
||||
public final FieldSpec counter;
|
||||
public final FieldSpec discrete;
|
||||
public final FieldSpec last;
|
||||
|
||||
public FieldSpecContainer(FieldSpec counter, FieldSpec discrete, FieldSpec last) {
|
||||
this.counter = counter;
|
||||
this.discrete = discrete;
|
||||
this.last = last;
|
||||
}
|
||||
|
||||
public ArrayList<FieldSpec> asList() {
|
||||
final ArrayList<FieldSpec> out = new ArrayList<FieldSpec>(3);
|
||||
out.add(counter);
|
||||
out.add(discrete);
|
||||
out.add(last);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
public PrepopulatedMockHealthReportDatabaseStorage(Context context, File fakeProfileDirectory) throws Exception {
|
||||
this(context, fakeProfileDirectory, 1);
|
||||
}
|
||||
|
||||
public PrepopulatedMockHealthReportDatabaseStorage(Context context, File fakeProfileDirectory, int version) throws Exception {
|
||||
super(context, fakeProfileDirectory);
|
||||
|
||||
if (version > MAX_VERSION_USED || version < 1) {
|
||||
throw new IllegalStateException("Invalid version number! Check " +
|
||||
"PrepopulatedMockHealthReportDatabaseStorage.MAX_VERSION_USED!");
|
||||
}
|
||||
|
||||
measurementNames = new String[2];
|
||||
measurementNames[0] = "a_string_measurement";
|
||||
measurementNames[1] = "b_integer_measurement";
|
||||
|
||||
measurementVers = new int[2];
|
||||
measurementVers[0] = 1;
|
||||
measurementVers[1] = 2;
|
||||
|
||||
fieldSpecContainers = new FieldSpecContainer[2];
|
||||
fieldSpecContainers[0] = new FieldSpecContainer(
|
||||
new FieldSpec("a_counter_integer_field", Field.TYPE_INTEGER_COUNTER),
|
||||
new FieldSpec("a_discrete_string_field", Field.TYPE_STRING_DISCRETE),
|
||||
new FieldSpec("a_last_string_field", Field.TYPE_STRING_LAST));
|
||||
fieldSpecContainers[1] = new FieldSpecContainer(
|
||||
new FieldSpec("b_counter_integer_field", Field.TYPE_INTEGER_COUNTER),
|
||||
new FieldSpec("b_discrete_integer_field", Field.TYPE_INTEGER_DISCRETE),
|
||||
new FieldSpec("b_last_integer_field", Field.TYPE_INTEGER_LAST));
|
||||
|
||||
final MeasurementFields[] measurementFields =
|
||||
new MeasurementFields[fieldSpecContainers.length];
|
||||
for (int i = 0; i < fieldSpecContainers.length; i++) {
|
||||
final FieldSpecContainer fieldSpecContainer = fieldSpecContainers[i];
|
||||
measurementFields[i] = new MeasurementFields() {
|
||||
@Override
|
||||
public Iterable<FieldSpec> getFields() {
|
||||
return fieldSpecContainer.asList();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.beginInitialization();
|
||||
for (int i = 0; i < measurementNames.length; i++) {
|
||||
this.ensureMeasurementInitialized(measurementNames[i], measurementVers[i],
|
||||
measurementFields[i]);
|
||||
}
|
||||
this.finishInitialization();
|
||||
|
||||
MockDatabaseEnvironment environment = this.getEnvironment();
|
||||
environment.mockInit("v123");
|
||||
environment.setJSONForAddons(addonJSON);
|
||||
env = environment.register();
|
||||
|
||||
String mName = measurementNames[0];
|
||||
int mVer = measurementVers[0];
|
||||
FieldSpecContainer fieldSpecCont = fieldSpecContainers[0];
|
||||
int fieldID = this.getField(mName, mVer, fieldSpecCont.counter.name).getID();
|
||||
this.incrementDailyCount(env, this.getGivenDaysAgo(7), fieldID, 1);
|
||||
this.incrementDailyCount(env, this.getGivenDaysAgo(4), fieldID, 2);
|
||||
this.incrementDailyCount(env, this.getToday(), fieldID, 3);
|
||||
fieldID = this.getField(mName, mVer, fieldSpecCont.discrete.name).getID();
|
||||
this.recordDailyDiscrete(env, this.getGivenDaysAgo(5), fieldID, "five");
|
||||
this.recordDailyDiscrete(env, this.getGivenDaysAgo(5), fieldID, "five-two");
|
||||
this.recordDailyDiscrete(env, this.getGivenDaysAgo(2), fieldID, "two");
|
||||
this.recordDailyDiscrete(env, this.getToday(), fieldID, "zero");
|
||||
fieldID = this.getField(mName, mVer, fieldSpecCont.last.name).getID();
|
||||
this.recordDailyLast(env, this.getGivenDaysAgo(6), fieldID, "six");
|
||||
this.recordDailyLast(env, this.getGivenDaysAgo(3), fieldID, "three");
|
||||
this.recordDailyLast(env, this.getToday(), fieldID, "zero");
|
||||
|
||||
mName = measurementNames[1];
|
||||
mVer = measurementVers[1];
|
||||
fieldSpecCont = fieldSpecContainers[1];
|
||||
fieldID = this.getField(mName, mVer, fieldSpecCont.counter.name).getID();
|
||||
this.incrementDailyCount(env, this.getGivenDaysAgo(2), fieldID, 2);
|
||||
fieldID = this.getField(mName, mVer, fieldSpecCont.discrete.name).getID();
|
||||
this.recordDailyDiscrete(env, this.getToday(), fieldID, 0);
|
||||
this.recordDailyDiscrete(env, this.getToday(), fieldID, 1);
|
||||
fieldID = this.getField(mName, mVer, fieldSpecCont.last.name).getID();
|
||||
this.recordDailyLast(env, this.getYesterday(), fieldID, 1);
|
||||
|
||||
if (version >= 2) {
|
||||
// Insert more diverse environments.
|
||||
for (int i = 1; i <= 3; i++) {
|
||||
environment = this.getEnvironment();
|
||||
environment.mockInit("v" + i);
|
||||
env = environment.register();
|
||||
this.recordDailyLast(env, this.getGivenDaysAgo(7 * i + 1), fieldID, 13);
|
||||
}
|
||||
environment = this.getEnvironment();
|
||||
environment.mockInit("v4");
|
||||
env = environment.register();
|
||||
this.recordDailyLast(env, this.getGivenDaysAgo(1000), fieldID, 14);
|
||||
this.recordDailyLast(env, this.getToday(), fieldID, 15);
|
||||
}
|
||||
}
|
||||
|
||||
public void insertTextualEvents(final int count) {
|
||||
final ContentValues v = new ContentValues();
|
||||
v.put("env", env);
|
||||
final int fieldID = this.getField(measurementNames[0], measurementVers[0],
|
||||
fieldSpecContainers[0].discrete.name).getID();
|
||||
v.put("field", fieldID);
|
||||
v.put("value", "data");
|
||||
final SQLiteDatabase db = this.helper.getWritableDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
for (int i = 1; i <= count; i++) {
|
||||
v.put("date", i);
|
||||
db.insertOrThrow("events_textual", null, v);
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,172 +0,0 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage.HealthReportSQLiteOpenHelper;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
public class MockHealthReportSQLiteOpenHelper extends HealthReportSQLiteOpenHelper {
|
||||
private int version;
|
||||
|
||||
public MockHealthReportSQLiteOpenHelper(Context context, File fakeProfileDirectory, String name) {
|
||||
super(context, fakeProfileDirectory, name);
|
||||
version = HealthReportSQLiteOpenHelper.CURRENT_VERSION;
|
||||
}
|
||||
|
||||
public MockHealthReportSQLiteOpenHelper(Context context, File fakeProfileDirectory, String name, int version) {
|
||||
super(context, fakeProfileDirectory, name, version);
|
||||
this.version = version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase db) {
|
||||
if (version == HealthReportSQLiteOpenHelper.CURRENT_VERSION) {
|
||||
super.onCreate(db);
|
||||
} else if (version == 4) {
|
||||
onCreateSchemaVersion4(db);
|
||||
} else {
|
||||
throw new IllegalStateException("Unknown version number, " + version + ".");
|
||||
}
|
||||
}
|
||||
|
||||
// Copy-pasta from HealthReportDatabaseStorage.onCreate from v4.
|
||||
public void onCreateSchemaVersion4(SQLiteDatabase db) {
|
||||
db.beginTransaction();
|
||||
try {
|
||||
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 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 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 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 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 current_measurements AS " +
|
||||
"SELECT name, MAX(version) AS version FROM measurements GROUP BY name");
|
||||
|
||||
// createAddonsEnvironmentsView(db):
|
||||
db.execSQL("CREATE VIEW environments_with_addons AS " +
|
||||
"SELECT e.id AS id, " +
|
||||
" e.hash AS hash, " +
|
||||
" e.profileCreation AS profileCreation, " +
|
||||
" e.cpuCount AS cpuCount, " +
|
||||
" e.memoryMB AS memoryMB, " +
|
||||
" e.isBlocklistEnabled AS isBlocklistEnabled, " +
|
||||
" e.isTelemetryEnabled AS isTelemetryEnabled, " +
|
||||
" e.extensionCount AS extensionCount, " +
|
||||
" e.pluginCount AS pluginCount, " +
|
||||
" e.themeCount AS themeCount, " +
|
||||
" e.architecture AS architecture, " +
|
||||
" e.sysName AS sysName, " +
|
||||
" e.sysVersion AS sysVersion, " +
|
||||
" e.vendor AS vendor, " +
|
||||
" e.appName AS appName, " +
|
||||
" e.appID AS appID, " +
|
||||
" e.appVersion AS appVersion, " +
|
||||
" e.appBuildID AS appBuildID, " +
|
||||
" e.platformVersion AS platformVersion, " +
|
||||
" e.platformBuildID AS platformBuildID, " +
|
||||
" e.os AS os, " +
|
||||
" e.xpcomabi AS xpcomabi, " +
|
||||
" e.updateChannel AS updateChannel, " +
|
||||
" addons.body AS addonsBody " +
|
||||
"FROM environments AS e, addons " +
|
||||
"WHERE e.addonsID = addons.id");
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.gecko.background.healthreport.ProfileInformationCache;
|
||||
|
||||
public class MockProfileInformationCache extends ProfileInformationCache {
|
||||
public MockProfileInformationCache(String profilePath) {
|
||||
super(profilePath);
|
||||
}
|
||||
|
||||
public MockProfileInformationCache(File mockFile) {
|
||||
super(mockFile);
|
||||
}
|
||||
|
||||
public boolean isInitialized() {
|
||||
return this.initialized;
|
||||
}
|
||||
public boolean needsWrite() {
|
||||
return this.needsWrite;
|
||||
}
|
||||
public File getFile() {
|
||||
return this.file;
|
||||
}
|
||||
|
||||
public void writeJSON(JSONObject toWrite) throws IOException {
|
||||
writeToFile(toWrite);
|
||||
}
|
||||
|
||||
public JSONObject readJSON() throws FileNotFoundException, JSONException {
|
||||
return readFromFile();
|
||||
}
|
||||
|
||||
public void setInitialized(final boolean initialized) {
|
||||
this.initialized = initialized;
|
||||
}
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.gecko.AppConstants;
|
||||
import org.mozilla.gecko.background.common.GlobalConstants;
|
||||
import org.mozilla.gecko.background.helpers.FakeProfileTestCase;
|
||||
|
||||
public class TestEnvironmentBuilder extends FakeProfileTestCase {
|
||||
public static void testIgnoringAddons() throws JSONException {
|
||||
Environment env = new Environment() {
|
||||
@Override
|
||||
public int register() {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
JSONObject addons = new JSONObject();
|
||||
JSONObject foo = new JSONObject();
|
||||
foo.put("a", 1);
|
||||
foo.put("b", "c");
|
||||
addons.put("foo", foo);
|
||||
JSONObject ignore = new JSONObject();
|
||||
ignore.put("ignore", true);
|
||||
addons.put("ig", ignore);
|
||||
|
||||
env.setJSONForAddons(addons);
|
||||
|
||||
JSONObject kept = env.getNonIgnoredAddons();
|
||||
assertTrue(kept.has("foo"));
|
||||
assertFalse(kept.has("ig"));
|
||||
JSONObject fooCopy = kept.getJSONObject("foo");
|
||||
assertSame(foo, fooCopy);
|
||||
}
|
||||
|
||||
public void testSanity() throws IOException {
|
||||
File subdir = new File(this.fakeProfileDirectory.getAbsolutePath() +
|
||||
File.separator + "testPersisting");
|
||||
subdir.mkdir();
|
||||
long now = System.currentTimeMillis();
|
||||
int expectedDays = (int) (now / GlobalConstants.MILLISECONDS_PER_DAY);
|
||||
|
||||
MockProfileInformationCache cache = new MockProfileInformationCache(subdir.getAbsolutePath());
|
||||
assertFalse(cache.getFile().exists());
|
||||
cache.beginInitialization();
|
||||
cache.setBlocklistEnabled(true);
|
||||
cache.setTelemetryEnabled(false);
|
||||
cache.setProfileCreationTime(now);
|
||||
cache.completeInitialization();
|
||||
assertTrue(cache.getFile().exists());
|
||||
|
||||
final AndroidConfigurationProvider configProvider = new AndroidConfigurationProvider(context);
|
||||
Environment environment = EnvironmentBuilder.getCurrentEnvironment(cache, configProvider);
|
||||
assertEquals(AppConstants.MOZ_APP_BUILDID, environment.appBuildID);
|
||||
assertEquals("Android", environment.os);
|
||||
assertTrue(100 < environment.memoryMB); // Seems like a sane lower bound...
|
||||
assertTrue(environment.cpuCount >= 1);
|
||||
assertEquals(1, environment.isBlocklistEnabled);
|
||||
assertEquals(0, environment.isTelemetryEnabled);
|
||||
assertEquals(expectedDays, environment.profileCreation);
|
||||
assertEquals(EnvironmentBuilder.getCurrentEnvironment(cache, configProvider).getHash(),
|
||||
environment.getHash());
|
||||
|
||||
// v3 sanity.
|
||||
assertEquals(configProvider.hasHardwareKeyboard(), environment.hasHardwareKeyboard);
|
||||
assertEquals(configProvider.getScreenXInMM(), environment.screenXInMM);
|
||||
assertTrue(1 < environment.screenXInMM);
|
||||
assertTrue(2000 > environment.screenXInMM);
|
||||
|
||||
cache.beginInitialization();
|
||||
cache.setBlocklistEnabled(false);
|
||||
cache.completeInitialization();
|
||||
|
||||
assertFalse(EnvironmentBuilder.getCurrentEnvironment(cache, configProvider).getHash()
|
||||
.equals(environment.getHash()));
|
||||
}
|
||||
}
|
|
@ -1,146 +0,0 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedList;
|
||||
|
||||
import org.mozilla.apache.commons.codec.binary.Base64;
|
||||
import org.mozilla.gecko.background.healthreport.EnvironmentV1.EnvironmentAppender;
|
||||
import org.mozilla.gecko.background.healthreport.EnvironmentV1.HashAppender;
|
||||
import org.mozilla.gecko.background.helpers.FakeProfileTestCase;
|
||||
import org.mozilla.gecko.sync.Utils;
|
||||
|
||||
/**
|
||||
* Tests the HashAppender functionality. Note that these tests must be run on an Android
|
||||
* device because the SHA-1 native library needs to be loaded.
|
||||
*/
|
||||
public class TestEnvironmentV1HashAppender extends FakeProfileTestCase {
|
||||
// input and expected values via: http://oauth.googlecode.com/svn/code/c/liboauth/src/sha1.c
|
||||
private final static String[] INPUTS = new String[] {
|
||||
"abc",
|
||||
"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
|
||||
"" // To be filled in below.
|
||||
};
|
||||
static {
|
||||
final String baseStr = "01234567";
|
||||
final int repetitions = 80;
|
||||
final StringBuilder builder = new StringBuilder(baseStr.length() * repetitions);
|
||||
for (int i = 0; i < 80; ++i) {
|
||||
builder.append(baseStr);
|
||||
}
|
||||
INPUTS[2] = builder.toString();
|
||||
}
|
||||
|
||||
private final static String[] EXPECTEDS = new String[] {
|
||||
"a9993e364706816aba3e25717850c26c9cd0d89d",
|
||||
"84983e441c3bd26ebaae4aa1f95129e5e54670f1",
|
||||
"dea356a2cddd90c7a7ecedc5ebb563934f460452"
|
||||
};
|
||||
static {
|
||||
for (int i = 0; i < EXPECTEDS.length; ++i) {
|
||||
EXPECTEDS[i] = new Base64(-1, null, false).encodeAsString(Utils.hex2Byte(EXPECTEDS[i]));
|
||||
}
|
||||
}
|
||||
|
||||
public void testSHA1Hashing() throws Exception {
|
||||
for (int i = 0; i < INPUTS.length; ++i) {
|
||||
final String input = INPUTS[i];
|
||||
final String expected = EXPECTEDS[i];
|
||||
|
||||
final HashAppender appender = new HashAppender();
|
||||
addStringToAppenderInParts(appender, input);
|
||||
final String result = appender.toString();
|
||||
|
||||
assertEquals(expected, result);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests to ensure output is the same as the former MessageDigest implementation (bug 959652).
|
||||
*/
|
||||
public void testAgainstMessageDigestImpl() throws Exception {
|
||||
// List.add doesn't allow add(null) so we make a LinkedList here.
|
||||
final LinkedList<String> inputs = new LinkedList<String>(Arrays.asList(INPUTS));
|
||||
inputs.add(null);
|
||||
|
||||
for (final String input : inputs) {
|
||||
final HashAppender hAppender = new HashAppender();
|
||||
final MessageDigestHashAppender mdAppender = new MessageDigestHashAppender();
|
||||
|
||||
hAppender.append(input);
|
||||
mdAppender.append(input);
|
||||
|
||||
final String hResult = hAppender.toString();
|
||||
final String mdResult = mdAppender.toString();
|
||||
assertEquals(mdResult, hResult);
|
||||
}
|
||||
}
|
||||
|
||||
public void testIntegersAgainstMessageDigestImpl() throws Exception {
|
||||
final int[] INPUTS = {Integer.MIN_VALUE, -1337, -42, 0, 42, 1337, Integer.MAX_VALUE};
|
||||
for (final int input : INPUTS) {
|
||||
final HashAppender hAppender = new HashAppender();
|
||||
final MessageDigestHashAppender mdAppender = new MessageDigestHashAppender();
|
||||
|
||||
hAppender.append(input);
|
||||
mdAppender.append(input);
|
||||
|
||||
final String hResult = hAppender.toString();
|
||||
final String mdResult = mdAppender.toString();
|
||||
assertEquals(mdResult, hResult);
|
||||
}
|
||||
}
|
||||
|
||||
private void addStringToAppenderInParts(final EnvironmentAppender appender, final String input) {
|
||||
int substrInd = 0;
|
||||
int substrLength = 1;
|
||||
while (substrInd < input.length()) {
|
||||
final int endInd = Math.min(substrInd + substrLength, input.length());
|
||||
|
||||
appender.append(input.substring(substrInd, endInd));
|
||||
|
||||
substrInd = endInd;
|
||||
++substrLength;
|
||||
}
|
||||
}
|
||||
|
||||
// --- COPY-PASTA'D CODE, FOR TESTING PURPOSES. ---
|
||||
public static class MessageDigestHashAppender extends EnvironmentAppender {
|
||||
final MessageDigest hasher;
|
||||
|
||||
public MessageDigestHashAppender() throws NoSuchAlgorithmException {
|
||||
// Note to the security-minded reader: we deliberately use SHA-1 here, not
|
||||
// a stronger hash. These identifiers don't strictly need a cryptographic
|
||||
// hash function, because there is negligible value in attacking the hash.
|
||||
// We use SHA-1 because it's *shorter* -- the exact same reason that Git
|
||||
// chose SHA-1.
|
||||
hasher = MessageDigest.getInstance("SHA-1");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void append(String s) {
|
||||
try {
|
||||
hasher.update(((s == null) ? "null" : s).getBytes("UTF-8"));
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
// This can never occur. Thanks, Java.
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void append(int profileCreation) {
|
||||
append(Integer.toString(profileCreation, 10));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
// We *could* use ASCII85… but the savings would be negated by the
|
||||
// inclusion of JSON-unsafe characters like double-quote.
|
||||
return new Base64(-1, null, false).encodeAsString(hasher.digest());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,145 +0,0 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport;
|
||||
|
||||
import java.util.concurrent.BrokenBarrierException;
|
||||
|
||||
import org.mozilla.gecko.background.common.GlobalConstants;
|
||||
import org.mozilla.gecko.background.healthreport.prune.HealthReportPruneService;
|
||||
import org.mozilla.gecko.background.healthreport.upload.HealthReportUploadService;
|
||||
import org.mozilla.gecko.background.helpers.BackgroundServiceTestCase;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
public class TestHealthReportBroadcastService
|
||||
extends BackgroundServiceTestCase<TestHealthReportBroadcastService.MockHealthReportBroadcastService> {
|
||||
public static class MockHealthReportBroadcastService extends HealthReportBroadcastService {
|
||||
@Override
|
||||
protected SharedPreferences getSharedPreferences() {
|
||||
return this.getSharedPreferences(sharedPrefsName, GlobalConstants.SHARED_PREFERENCES_MODE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHandleIntent(Intent intent) {
|
||||
super.onHandleIntent(intent);
|
||||
try {
|
||||
barrier.await();
|
||||
} catch (InterruptedException e) {
|
||||
fail("Awaiting Service thread should not be interrupted.");
|
||||
} catch (BrokenBarrierException e) {
|
||||
// This will happen on timeout - do nothing.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public TestHealthReportBroadcastService() {
|
||||
super(MockHealthReportBroadcastService.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
// We can't mock AlarmManager since it has a package-private constructor, so instead we reset
|
||||
// the alarm by hand.
|
||||
cancelAlarm(getUploadIntent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tearDown() throws Exception {
|
||||
cancelAlarm(getUploadIntent());
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
protected Intent getUploadIntent() {
|
||||
final Intent intent = new Intent(getContext(), HealthReportUploadService.class);
|
||||
intent.setAction("upload");
|
||||
return intent;
|
||||
}
|
||||
|
||||
protected Intent getPruneIntent() {
|
||||
final Intent intent = new Intent(getContext(), HealthReportPruneService.class);
|
||||
intent.setAction("prune");
|
||||
return intent;
|
||||
}
|
||||
|
||||
public void testIgnoredUploadPrefIntents() throws Exception {
|
||||
// Intent without "upload" extra is ignored.
|
||||
intent.setAction(HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF)
|
||||
.putExtra("profileName", "profileName")
|
||||
.putExtra("profilePath", "profilePath");
|
||||
startService(intent);
|
||||
await();
|
||||
|
||||
assertFalse(isServiceAlarmSet(getUploadIntent()));
|
||||
barrier.reset();
|
||||
|
||||
// No "profileName" extra.
|
||||
intent.putExtra("enabled", true)
|
||||
.removeExtra("profileName");
|
||||
startService(intent);
|
||||
await();
|
||||
|
||||
assertFalse(isServiceAlarmSet(getUploadIntent()));
|
||||
barrier.reset();
|
||||
|
||||
// No "profilePath" extra.
|
||||
intent.putExtra("profileName", "profileName")
|
||||
.removeExtra("profilePath");
|
||||
startService(intent);
|
||||
await();
|
||||
|
||||
assertFalse(isServiceAlarmSet(getUploadIntent()));
|
||||
}
|
||||
|
||||
public void testUploadPrefIntentDisabled() throws Exception {
|
||||
intent.setAction(HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF)
|
||||
.putExtra("enabled", false)
|
||||
.putExtra("profileName", "profileName")
|
||||
.putExtra("profilePath", "profilePath");
|
||||
startService(intent);
|
||||
await();
|
||||
|
||||
assertFalse(isServiceAlarmSet(getUploadIntent()));
|
||||
}
|
||||
|
||||
public void testUploadPrefIntentEnabled() throws Exception {
|
||||
intent.setAction(HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF)
|
||||
.putExtra("enabled", true)
|
||||
.putExtra("profileName", "profileName")
|
||||
.putExtra("profilePath", "profilePath");
|
||||
startService(intent);
|
||||
await();
|
||||
|
||||
assertTrue(isServiceAlarmSet(getUploadIntent()));
|
||||
}
|
||||
|
||||
public void testUploadServiceCancelled() throws Exception {
|
||||
intent.setAction(HealthReportConstants.ACTION_HEALTHREPORT_UPLOAD_PREF)
|
||||
.putExtra("enabled", true)
|
||||
.putExtra("profileName", "profileName")
|
||||
.putExtra("profilePath", "profilePath");
|
||||
startService(intent);
|
||||
await();
|
||||
|
||||
assertTrue(isServiceAlarmSet(getUploadIntent()));
|
||||
barrier.reset();
|
||||
|
||||
intent.putExtra("enabled", false);
|
||||
startService(intent);
|
||||
await();
|
||||
|
||||
assertFalse(isServiceAlarmSet(getUploadIntent()));
|
||||
}
|
||||
|
||||
public void testPruneService() throws Exception {
|
||||
intent.setAction(HealthReportConstants.ACTION_HEALTHREPORT_PRUNE)
|
||||
.putExtra("profileName", "profileName")
|
||||
.putExtra("profilePath", "profilePath");
|
||||
startService(intent);
|
||||
await();
|
||||
assertTrue(isServiceAlarmSet(getPruneIntent()));
|
||||
barrier.reset();
|
||||
}
|
||||
}
|
|
@ -1,662 +0,0 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
package org.mozilla.gecko.background.healthreport;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.mozilla.gecko.background.common.GlobalConstants;
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportStorage;
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field;
|
||||
import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields;
|
||||
import org.mozilla.gecko.background.healthreport.MockHealthReportDatabaseStorage.PrepopulatedMockHealthReportDatabaseStorage;
|
||||
import org.mozilla.gecko.background.helpers.DBHelpers;
|
||||
import org.mozilla.gecko.background.helpers.FakeProfileTestCase;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteConstraintException;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
|
||||
public class TestHealthReportDatabaseStorage extends FakeProfileTestCase {
|
||||
private String[] TABLE_NAMES = {
|
||||
"addons",
|
||||
"environments",
|
||||
"measurements",
|
||||
"fields",
|
||||
"events_integer",
|
||||
"events_textual"
|
||||
};
|
||||
|
||||
public static class MockMeasurementFields implements MeasurementFields {
|
||||
@Override
|
||||
public Iterable<FieldSpec> getFields() {
|
||||
ArrayList<FieldSpec> fields = new ArrayList<FieldSpec>();
|
||||
fields.add(new FieldSpec("testfield1", Field.TYPE_INTEGER_COUNTER));
|
||||
fields.add(new FieldSpec("testfield2", Field.TYPE_INTEGER_COUNTER));
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
|
||||
public void testInitializingProvider() {
|
||||
MockHealthReportDatabaseStorage storage = new MockHealthReportDatabaseStorage(context, fakeProfileDirectory);
|
||||
storage.beginInitialization();
|
||||
|
||||
// Two providers with the same measurement and field names. Shouldn't conflict.
|
||||
storage.ensureMeasurementInitialized("testpA.testm", 1, new MockMeasurementFields());
|
||||
storage.ensureMeasurementInitialized("testpB.testm", 2, new MockMeasurementFields());
|
||||
storage.finishInitialization();
|
||||
|
||||
// Now make sure our stuff is in the DB.
|
||||
SQLiteDatabase db = storage.getDB();
|
||||
Cursor c = db.query("measurements", new String[] {"id", "name", "version"}, null, null, null, null, "name");
|
||||
assertTrue(c.moveToFirst());
|
||||
assertEquals(2, c.getCount());
|
||||
|
||||
Object[][] expected = new Object[][] {
|
||||
{null, "testpA.testm", 1},
|
||||
{null, "testpB.testm", 2},
|
||||
};
|
||||
|
||||
DBHelpers.assertCursorContains(expected, c);
|
||||
c.close();
|
||||
}
|
||||
|
||||
private static final JSONObject EXAMPLE_ADDONS = safeJSONObject(
|
||||
"{ " +
|
||||
"\"amznUWL2@amazon.com\": { " +
|
||||
" \"userDisabled\": false, " +
|
||||
" \"appDisabled\": false, " +
|
||||
" \"version\": \"1.10\", " +
|
||||
" \"type\": \"extension\", " +
|
||||
" \"scope\": 1, " +
|
||||
" \"foreignInstall\": false, " +
|
||||
" \"hasBinaryComponents\": false, " +
|
||||
" \"installDay\": 15269, " +
|
||||
" \"updateDay\": 15602 " +
|
||||
"}, " +
|
||||
"\"jid0-qBnIpLfDFa4LpdrjhAC6vBqN20Q@jetpack\": { " +
|
||||
" \"userDisabled\": false, " +
|
||||
" \"appDisabled\": false, " +
|
||||
" \"version\": \"1.12.1\", " +
|
||||
" \"type\": \"extension\", " +
|
||||
" \"scope\": 1, " +
|
||||
" \"foreignInstall\": false, " +
|
||||
" \"hasBinaryComponents\": false, " +
|
||||
" \"installDay\": 15062, " +
|
||||
" \"updateDay\": 15580 " +
|
||||
"} " +
|
||||
"} ");
|
||||
|
||||
private static JSONObject safeJSONObject(String s) {
|
||||
try {
|
||||
return new JSONObject(s);
|
||||
} catch (JSONException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void testEnvironmentsAndFields() throws Exception {
|
||||
MockHealthReportDatabaseStorage storage = new MockHealthReportDatabaseStorage(context, fakeProfileDirectory);
|
||||
storage.beginInitialization();
|
||||
storage.ensureMeasurementInitialized("testpA.testm", 1, new MockMeasurementFields());
|
||||
storage.ensureMeasurementInitialized("testpB.testn", 1, new MockMeasurementFields());
|
||||
storage.finishInitialization();
|
||||
|
||||
MockDatabaseEnvironment environmentA = storage.getEnvironment();
|
||||
environmentA.mockInit("v123");
|
||||
environmentA.setJSONForAddons(EXAMPLE_ADDONS);
|
||||
final int envA = environmentA.register();
|
||||
assertEquals(envA, environmentA.register());
|
||||
|
||||
// getField memoizes.
|
||||
assertSame(storage.getField("foo", 2, "bar"),
|
||||
storage.getField("foo", 2, "bar"));
|
||||
|
||||
// It throws if you refer to a non-existent field.
|
||||
try {
|
||||
storage.getField("foo", 2, "bar").getID();
|
||||
fail("Should throw.");
|
||||
} catch (IllegalStateException ex) {
|
||||
// Expected.
|
||||
}
|
||||
|
||||
// It returns the field ID for a valid field.
|
||||
Field field = storage.getField("testpA.testm", 1, "testfield1");
|
||||
assertTrue(field.getID() >= 0);
|
||||
|
||||
// These IDs are stable.
|
||||
assertEquals(field.getID(), field.getID());
|
||||
int fieldID = field.getID();
|
||||
|
||||
// Before inserting, no events.
|
||||
assertFalse(storage.hasEventSince(0));
|
||||
assertFalse(storage.hasEventSince(storage.now));
|
||||
|
||||
// Store some data for two environments across two days.
|
||||
storage.incrementDailyCount(envA, storage.getYesterday(), fieldID, 4);
|
||||
storage.incrementDailyCount(envA, storage.getYesterday(), fieldID, 1);
|
||||
storage.incrementDailyCount(envA, storage.getToday(), fieldID, 2);
|
||||
|
||||
// After inserting, we have events.
|
||||
assertTrue(storage.hasEventSince(storage.now - GlobalConstants.MILLISECONDS_PER_DAY));
|
||||
assertTrue(storage.hasEventSince(storage.now));
|
||||
// But not in the future.
|
||||
assertFalse(storage.hasEventSince(storage.now + GlobalConstants.MILLISECONDS_PER_DAY));
|
||||
|
||||
MockDatabaseEnvironment environmentB = storage.getEnvironment();
|
||||
environmentB.mockInit("v234");
|
||||
environmentB.setJSONForAddons(EXAMPLE_ADDONS);
|
||||
final int envB = environmentB.register();
|
||||
assertFalse(envA == envB);
|
||||
|
||||
storage.incrementDailyCount(envB, storage.getToday(), fieldID, 6);
|
||||
storage.incrementDailyCount(envB, storage.getToday(), fieldID, 2);
|
||||
|
||||
// Let's make sure everything's there.
|
||||
Cursor c = storage.getRawEventsSince(storage.getOneDayAgo());
|
||||
try {
|
||||
assertTrue(c.moveToFirst());
|
||||
assertTrue(assertRowEquals(c, storage.getYesterday(), envA, fieldID, 5));
|
||||
assertTrue(assertRowEquals(c, storage.getToday(), envA, fieldID, 2));
|
||||
assertFalse(assertRowEquals(c, storage.getToday(), envB, fieldID, 8));
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
|
||||
// The stored environment has the provided JSON add-ons bundle.
|
||||
Cursor e = storage.getEnvironmentRecordForID(envA);
|
||||
e.moveToFirst();
|
||||
assertEquals(EXAMPLE_ADDONS.toString(), e.getString(e.getColumnIndex("addonsBody")));
|
||||
e.close();
|
||||
|
||||
e = storage.getEnvironmentRecordForID(envB);
|
||||
e.moveToFirst();
|
||||
assertEquals(EXAMPLE_ADDONS.toString(), e.getString(e.getColumnIndex("addonsBody")));
|
||||
e.close();
|
||||
|
||||
// There's only one add-ons bundle in the DB, despite having two environments.
|
||||
Cursor addons = storage.getDB().query("addons", null, null, null, null, null, null);
|
||||
assertEquals(1, addons.getCount());
|
||||
addons.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts validity for a storage cursor. Returns whether there is another row to process.
|
||||
*/
|
||||
private static boolean assertRowEquals(Cursor c, int day, int env, int field, int value) {
|
||||
assertEquals(day, c.getInt(0));
|
||||
assertEquals(env, c.getInt(1));
|
||||
assertEquals(field, c.getInt(2));
|
||||
assertEquals(value, c.getLong(3));
|
||||
return c.moveToNext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test robust insertions. This also acts as a test for the getPrepopulatedStorage method,
|
||||
* allowing faster debugging if this fails and other tests relying on getPrepopulatedStorage
|
||||
* also fail.
|
||||
*/
|
||||
public void testInsertions() throws Exception {
|
||||
final PrepopulatedMockHealthReportDatabaseStorage storage =
|
||||
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
|
||||
assertNotNull(storage);
|
||||
}
|
||||
|
||||
public void testForeignKeyConstraints() throws Exception {
|
||||
final PrepopulatedMockHealthReportDatabaseStorage storage =
|
||||
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
|
||||
final SQLiteDatabase db = storage.getDB();
|
||||
|
||||
final int envID = storage.getEnvironment().register();
|
||||
final int counterFieldID = storage.getField(storage.measurementNames[0], storage.measurementVers[0],
|
||||
storage.fieldSpecContainers[0].counter.name).getID();
|
||||
final int discreteFieldID = storage.getField(storage.measurementNames[0], storage.measurementVers[0],
|
||||
storage.fieldSpecContainers[0].discrete.name).getID();
|
||||
|
||||
final int nonExistentEnvID = DBHelpers.getNonExistentID(db, "environments");
|
||||
final int nonExistentFieldID = DBHelpers.getNonExistentID(db, "fields");
|
||||
final int nonExistentAddonID = DBHelpers.getNonExistentID(db, "addons");
|
||||
final int nonExistentMeasurementID = DBHelpers.getNonExistentID(db, "measurements");
|
||||
|
||||
ContentValues v = new ContentValues();
|
||||
v.put("field", counterFieldID);
|
||||
v.put("env", nonExistentEnvID);
|
||||
try {
|
||||
db.insertOrThrow("events_integer", null, v);
|
||||
fail("Should throw - events_integer(env) is referencing non-existent environments(id)");
|
||||
} catch (SQLiteConstraintException e) { }
|
||||
v.put("field", discreteFieldID);
|
||||
try {
|
||||
db.insertOrThrow("events_textual", null, v);
|
||||
fail("Should throw - events_textual(env) is referencing non-existent environments(id)");
|
||||
} catch (SQLiteConstraintException e) { }
|
||||
|
||||
v.put("field", nonExistentFieldID);
|
||||
v.put("env", envID);
|
||||
try {
|
||||
db.insertOrThrow("events_integer", null, v);
|
||||
fail("Should throw - events_integer(field) is referencing non-existent fields(id)");
|
||||
} catch (SQLiteConstraintException e) { }
|
||||
try {
|
||||
db.insertOrThrow("events_textual", null, v);
|
||||
fail("Should throw - events_textual(field) is referencing non-existent fields(id)");
|
||||
} catch (SQLiteConstraintException e) { }
|
||||
|
||||
v = new ContentValues();
|
||||
v.put("addonsID", nonExistentAddonID);
|
||||
try {
|
||||
db.insertOrThrow("environments", null, v);
|
||||
fail("Should throw - environments(addonsID) is referencing non-existent addons(id).");
|
||||
} catch (SQLiteConstraintException e) { }
|
||||
|
||||
v = new ContentValues();
|
||||
v.put("measurement", nonExistentMeasurementID);
|
||||
try {
|
||||
db.insertOrThrow("fields", null, v);
|
||||
fail("Should throw - fields(measurement) is referencing non-existent measurements(id).");
|
||||
} catch (SQLiteConstraintException e) { }
|
||||
}
|
||||
|
||||
private int getTotalEventCount(HealthReportStorage storage) {
|
||||
final Cursor c = storage.getEventsSince(0);
|
||||
try {
|
||||
return c.getCount();
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
|
||||
public void testCascadingDeletions() throws Exception {
|
||||
PrepopulatedMockHealthReportDatabaseStorage storage =
|
||||
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
|
||||
SQLiteDatabase db = storage.getDB();
|
||||
db.delete("environments", null, null);
|
||||
assertEquals(0, DBHelpers.getRowCount(db, "events_integer"));
|
||||
assertEquals(0, DBHelpers.getRowCount(db, "events_textual"));
|
||||
|
||||
storage = new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
|
||||
db = storage.getDB();
|
||||
db.delete("measurements", null, null);
|
||||
assertEquals(0, DBHelpers.getRowCount(db, "fields"));
|
||||
assertEquals(0, DBHelpers.getRowCount(db, "events_integer"));
|
||||
assertEquals(0, DBHelpers.getRowCount(db, "events_textual"));
|
||||
}
|
||||
|
||||
public void testRestrictedDeletions() throws Exception {
|
||||
final PrepopulatedMockHealthReportDatabaseStorage storage =
|
||||
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
|
||||
SQLiteDatabase db = storage.getDB();
|
||||
try {
|
||||
db.delete("addons", null, null);
|
||||
fail("Should throw - environment references addons and thus addons cannot be deleted.");
|
||||
} catch (SQLiteConstraintException e) { }
|
||||
}
|
||||
|
||||
public void testDeleteEverything() throws Exception {
|
||||
final PrepopulatedMockHealthReportDatabaseStorage storage =
|
||||
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
|
||||
storage.deleteEverything();
|
||||
|
||||
final SQLiteDatabase db = storage.getDB();
|
||||
for (String table : TABLE_NAMES) {
|
||||
if (DBHelpers.getRowCount(db, table) != 0) {
|
||||
fail("Not everything has been deleted for table " + table + ".");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void testMeasurementRecordingConstraintViolation() throws Exception {
|
||||
final PrepopulatedMockHealthReportDatabaseStorage storage =
|
||||
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
|
||||
final SQLiteDatabase db = storage.getDB();
|
||||
|
||||
final int envID = storage.getEnvironment().register();
|
||||
final int counterFieldID = storage.getField(storage.measurementNames[0], storage.measurementVers[0],
|
||||
storage.fieldSpecContainers[0].counter.name).getID();
|
||||
final int discreteFieldID = storage.getField(storage.measurementNames[0], storage.measurementVers[0],
|
||||
storage.fieldSpecContainers[0].discrete.name).getID();
|
||||
|
||||
final int nonExistentEnvID = DBHelpers.getNonExistentID(db, "environments");
|
||||
final int nonExistentFieldID = DBHelpers.getNonExistentID(db, "fields");
|
||||
|
||||
try {
|
||||
storage.incrementDailyCount(nonExistentEnvID, storage.getToday(), counterFieldID);
|
||||
fail("Should throw - event_integer(env) references environments(id), which is given as a non-existent value.");
|
||||
} catch (IllegalStateException e) { }
|
||||
try {
|
||||
storage.recordDailyLast(nonExistentEnvID, storage.getToday(), discreteFieldID, "iu");
|
||||
fail("Should throw - event_textual(env) references environments(id), which is given as a non-existent value.");
|
||||
} catch (IllegalStateException e) { }
|
||||
|
||||
try {
|
||||
storage.incrementDailyCount(envID, storage.getToday(), nonExistentFieldID);
|
||||
fail("Should throw - event_integer(field) references fields(id), which is given as a non-existent value.");
|
||||
} catch (IllegalStateException e) { }
|
||||
try {
|
||||
storage.recordDailyLast(envID, storage.getToday(), nonExistentFieldID, "iu");
|
||||
fail("Should throw - event_textual(field) references fields(id), which is given as a non-existent value.");
|
||||
} catch (IllegalStateException e) { }
|
||||
|
||||
// Test dropped events due to constraint violations that do not throw (see bug 961526).
|
||||
final String eventValue = "a value not in the database";
|
||||
assertFalse(isEventInDB(db, eventValue)); // Better safe than sorry.
|
||||
|
||||
storage.recordDailyDiscrete(nonExistentEnvID, storage.getToday(), discreteFieldID, eventValue);
|
||||
assertFalse(isEventInDB(db, eventValue));
|
||||
|
||||
storage.recordDailyDiscrete(envID, storage.getToday(), nonExistentFieldID, "iu");
|
||||
assertFalse(isEventInDB(db, eventValue));
|
||||
}
|
||||
|
||||
private static boolean isEventInDB(final SQLiteDatabase db, final String value) {
|
||||
final Cursor c = db.query("events_textual", new String[] {"value"}, "value = ?",
|
||||
new String[] {value}, null, null, null);
|
||||
try {
|
||||
return c.getCount() > 0;
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Largely taken from testDeleteEnvAndEventsBefore and testDeleteOrphanedAddons.
|
||||
public void testDeleteDataBefore() throws Exception {
|
||||
final PrepopulatedMockHealthReportDatabaseStorage storage =
|
||||
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
|
||||
final SQLiteDatabase db = storage.getDB();
|
||||
|
||||
// Insert (and delete) an environment not referenced by any events.
|
||||
ContentValues v = new ContentValues();
|
||||
v.put("hash", "I really hope this is a unique hash! ^_^");
|
||||
v.put("addonsID", DBHelpers.getExistentID(db, "addons"));
|
||||
db.insertOrThrow("environments", null, v);
|
||||
v.put("hash", "Another unique hash!");
|
||||
final int curEnv = (int) db.insertOrThrow("environments", null, v);
|
||||
final ContentValues addonV = new ContentValues();
|
||||
addonV.put("body", "addon1");
|
||||
db.insertOrThrow("addons", null, addonV);
|
||||
// 2 = 1 addon + 1 env.
|
||||
assertEquals(2, storage.deleteDataBefore(storage.getGivenDaysAgoMillis(8), curEnv));
|
||||
assertEquals(1, storage.deleteDataBefore(storage.getGivenDaysAgoMillis(8),
|
||||
DBHelpers.getNonExistentID(db, "environments")));
|
||||
assertEquals(1, DBHelpers.getRowCount(db, "addons"));
|
||||
|
||||
// Insert (and delete) new environment and referencing events.
|
||||
final long envID = db.insertOrThrow("environments", null, v);
|
||||
v = new ContentValues();
|
||||
v.put("date", storage.getGivenDaysAgo(9));
|
||||
v.put("env", envID);
|
||||
v.put("field", DBHelpers.getExistentID(db, "fields"));
|
||||
db.insertOrThrow("events_integer", null, v);
|
||||
db.insertOrThrow("events_integer", null, v);
|
||||
assertEquals(16, getTotalEventCount(storage));
|
||||
final int nonExistentEnvID = DBHelpers.getNonExistentID(db, "environments");
|
||||
assertEquals(1, storage.deleteDataBefore(storage.getGivenDaysAgoMillis(8), nonExistentEnvID));
|
||||
assertEquals(14, getTotalEventCount(storage));
|
||||
|
||||
// Assert only pre-populated storage is stored.
|
||||
assertEquals(1, DBHelpers.getRowCount(db, "environments"));
|
||||
|
||||
assertEquals(0, storage.deleteDataBefore(storage.getGivenDaysAgoMillis(5), nonExistentEnvID));
|
||||
assertEquals(12, getTotalEventCount(storage));
|
||||
|
||||
assertEquals(0, storage.deleteDataBefore(storage.getGivenDaysAgoMillis(4), nonExistentEnvID));
|
||||
assertEquals(10, getTotalEventCount(storage));
|
||||
|
||||
assertEquals(0, storage.deleteDataBefore(storage.now, nonExistentEnvID));
|
||||
assertEquals(5, getTotalEventCount(storage));
|
||||
assertEquals(1, DBHelpers.getRowCount(db, "addons"));
|
||||
|
||||
// 2 = 1 addon + 1 env.
|
||||
assertEquals(2, storage.deleteDataBefore(storage.now + GlobalConstants.MILLISECONDS_PER_DAY,
|
||||
nonExistentEnvID));
|
||||
assertEquals(0, getTotalEventCount(storage));
|
||||
assertEquals(0, DBHelpers.getRowCount(db, "addons"));
|
||||
}
|
||||
|
||||
// Largely taken from testDeleteOrphanedEnv and testDeleteEventsBefore.
|
||||
public void testDeleteEnvAndEventsBefore() throws Exception {
|
||||
final PrepopulatedMockHealthReportDatabaseStorage storage =
|
||||
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
|
||||
final SQLiteDatabase db = storage.getDB();
|
||||
|
||||
// Insert (and delete) an environment not referenced by any events.
|
||||
ContentValues v = new ContentValues();
|
||||
v.put("hash", "I really hope this is a unique hash! ^_^");
|
||||
v.put("addonsID", DBHelpers.getExistentID(db, "addons"));
|
||||
db.insertOrThrow("environments", null, v);
|
||||
v.put("hash", "Another unique hash!");
|
||||
final int curEnv = (int) db.insertOrThrow("environments", null, v);
|
||||
assertEquals(1, storage.deleteEnvAndEventsBefore(storage.getGivenDaysAgoMillis(8), curEnv));
|
||||
assertEquals(1, storage.deleteEnvAndEventsBefore(storage.getGivenDaysAgoMillis(8),
|
||||
DBHelpers.getNonExistentID(db, "environments")));
|
||||
|
||||
// Insert (and delete) new environment and referencing events.
|
||||
final long envID = db.insertOrThrow("environments", null, v);
|
||||
v = new ContentValues();
|
||||
v.put("date", storage.getGivenDaysAgo(9));
|
||||
v.put("env", envID);
|
||||
v.put("field", DBHelpers.getExistentID(db, "fields"));
|
||||
db.insertOrThrow("events_integer", null, v);
|
||||
db.insertOrThrow("events_integer", null, v);
|
||||
assertEquals(16, getTotalEventCount(storage));
|
||||
final int nonExistentEnvID = DBHelpers.getNonExistentID(db, "environments");
|
||||
assertEquals(1, storage.deleteEnvAndEventsBefore(storage.getGivenDaysAgoMillis(8), nonExistentEnvID));
|
||||
assertEquals(14, getTotalEventCount(storage));
|
||||
|
||||
// Assert only pre-populated storage is stored.
|
||||
assertEquals(1, DBHelpers.getRowCount(db, "environments"));
|
||||
|
||||
assertEquals(0, storage.deleteEnvAndEventsBefore(storage.getGivenDaysAgoMillis(5), nonExistentEnvID));
|
||||
assertEquals(12, getTotalEventCount(storage));
|
||||
|
||||
assertEquals(0, storage.deleteEnvAndEventsBefore(storage.getGivenDaysAgoMillis(4), nonExistentEnvID));
|
||||
assertEquals(10, getTotalEventCount(storage));
|
||||
|
||||
assertEquals(0, storage.deleteEnvAndEventsBefore(storage.now, nonExistentEnvID));
|
||||
assertEquals(5, getTotalEventCount(storage));
|
||||
|
||||
assertEquals(1, storage.deleteEnvAndEventsBefore(storage.now + GlobalConstants.MILLISECONDS_PER_DAY,
|
||||
nonExistentEnvID));
|
||||
assertEquals(0, getTotalEventCount(storage));
|
||||
}
|
||||
|
||||
public void testDeleteOrphanedEnv() throws Exception {
|
||||
final PrepopulatedMockHealthReportDatabaseStorage storage =
|
||||
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
|
||||
final SQLiteDatabase db = storage.getDB();
|
||||
|
||||
final ContentValues v = new ContentValues();
|
||||
v.put("addonsID", DBHelpers.getExistentID(db, "addons"));
|
||||
v.put("hash", "unique");
|
||||
final int envID = (int) db.insert("environments", null, v);
|
||||
|
||||
assertEquals(0, storage.deleteOrphanedEnv(envID));
|
||||
assertEquals(1, storage.deleteOrphanedEnv(storage.env));
|
||||
this.deleteEvents(db);
|
||||
assertEquals(1, storage.deleteOrphanedEnv(envID));
|
||||
}
|
||||
|
||||
private void deleteEvents(final SQLiteDatabase db) throws Exception {
|
||||
db.beginTransaction();
|
||||
try {
|
||||
db.delete("events_integer", null, null);
|
||||
db.delete("events_textual", null, null);
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public void testDeleteEventsBefore() throws Exception {
|
||||
final PrepopulatedMockHealthReportDatabaseStorage storage =
|
||||
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
|
||||
assertEquals(2, storage.deleteEventsBefore(Integer.toString(storage.getGivenDaysAgo(5))));
|
||||
assertEquals(12, getTotalEventCount(storage));
|
||||
|
||||
assertEquals(2, storage.deleteEventsBefore(Integer.toString(storage.getGivenDaysAgo(4))));
|
||||
assertEquals(10, getTotalEventCount(storage));
|
||||
|
||||
assertEquals(5, storage.deleteEventsBefore(Integer.toString(storage.getToday())));
|
||||
assertEquals(5, getTotalEventCount(storage));
|
||||
|
||||
assertEquals(5, storage.deleteEventsBefore(Integer.toString(storage.getTomorrow())));
|
||||
assertEquals(0, getTotalEventCount(storage));
|
||||
}
|
||||
|
||||
public void testDeleteOrphanedAddons() throws Exception {
|
||||
final PrepopulatedMockHealthReportDatabaseStorage storage =
|
||||
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
|
||||
final SQLiteDatabase db = storage.getDB();
|
||||
|
||||
final ArrayList<Integer> nonOrphanIDs = new ArrayList<Integer>();
|
||||
final Cursor c = db.query("addons", new String[] {"id"}, null, null, null, null, null);
|
||||
try {
|
||||
assertTrue(c.moveToFirst());
|
||||
do {
|
||||
nonOrphanIDs.add(c.getInt(0));
|
||||
} while (c.moveToNext());
|
||||
} finally {
|
||||
c.close();
|
||||
}
|
||||
|
||||
// Ensure we don't delete non-orphans.
|
||||
assertEquals(0, storage.deleteOrphanedAddons());
|
||||
|
||||
// Insert orphans.
|
||||
final long[] orphanIDs = new long[2];
|
||||
final ContentValues v = new ContentValues();
|
||||
v.put("body", "addon1");
|
||||
orphanIDs[0] = db.insertOrThrow("addons", null, v);
|
||||
v.put("body", "addon2");
|
||||
orphanIDs[1] = db.insertOrThrow("addons", null, v);
|
||||
assertEquals(2, storage.deleteOrphanedAddons());
|
||||
assertEquals(0, DBHelpers.getRowCount(db, "addons", "ID = ? OR ID = ?",
|
||||
new String[] {Long.toString(orphanIDs[0]), Long.toString(orphanIDs[1])}));
|
||||
|
||||
// Orphan all addons.
|
||||
db.delete("environments", null, null);
|
||||
assertEquals(nonOrphanIDs.size(), storage.deleteOrphanedAddons());
|
||||
assertEquals(0, DBHelpers.getRowCount(db, "addons"));
|
||||
}
|
||||
|
||||
public void testGetEventCount() throws Exception {
|
||||
final PrepopulatedMockHealthReportDatabaseStorage storage =
|
||||
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
|
||||
assertEquals(14, storage.getEventCount());
|
||||
final SQLiteDatabase db = storage.getDB();
|
||||
this.deleteEvents(db);
|
||||
assertEquals(0, storage.getEventCount());
|
||||
}
|
||||
|
||||
public void testGetEnvironmentCount() throws Exception {
|
||||
final PrepopulatedMockHealthReportDatabaseStorage storage =
|
||||
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
|
||||
assertEquals(1, storage.getEnvironmentCount());
|
||||
final SQLiteDatabase db = storage.getDB();
|
||||
db.delete("environments", null, null);
|
||||
assertEquals(0, storage.getEnvironmentCount());
|
||||
}
|
||||
|
||||
public void testPruneEnvironments() throws Exception {
|
||||
final PrepopulatedMockHealthReportDatabaseStorage storage =
|
||||
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory, 2);
|
||||
final SQLiteDatabase db = storage.getDB();
|
||||
assertEquals(5, DBHelpers.getRowCount(db, "environments"));
|
||||
assertEquals(5, storage.getEnvironmentCache().size());
|
||||
|
||||
storage.pruneEnvironments(1);
|
||||
assertEquals(0, storage.getEnvironmentCache().size());
|
||||
assertTrue(!getEnvAppVersions(db).contains("v3"));
|
||||
storage.pruneEnvironments(2);
|
||||
assertTrue(!getEnvAppVersions(db).contains("v2"));
|
||||
assertTrue(!getEnvAppVersions(db).contains("v1"));
|
||||
storage.pruneEnvironments(1);
|
||||
assertTrue(!getEnvAppVersions(db).contains("v123"));
|
||||
storage.pruneEnvironments(1);
|
||||
assertTrue(!getEnvAppVersions(db).contains("v4"));
|
||||
}
|
||||
|
||||
private ArrayList<String> getEnvAppVersions(final SQLiteDatabase db) {
|
||||
ArrayList<String> out = new ArrayList<String>();
|
||||
Cursor c = null;
|
||||
try {
|
||||
c = db.query(true, "environments", new String[] {"appVersion"}, null, null, null, null, null, null);
|
||||
while (c.moveToNext()) {
|
||||
out.add(c.getString(0));
|
||||
}
|
||||
} finally {
|
||||
if (c != null) {
|
||||
c.close();
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
public void testPruneEvents() throws Exception {
|
||||
final PrepopulatedMockHealthReportDatabaseStorage storage =
|
||||
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
|
||||
SQLiteDatabase db = storage.getDB();
|
||||
assertEquals(14, DBHelpers.getRowCount(db, "events"));
|
||||
storage.pruneEvents(1); // Delete < 7 days ago.
|
||||
assertEquals(14, DBHelpers.getRowCount(db, "events"));
|
||||
storage.pruneEvents(2); // Delete < 5 days ago.
|
||||
assertEquals(13, DBHelpers.getRowCount(db, "events"));
|
||||
storage.pruneEvents(5); // Delete < 2 days ago.
|
||||
assertEquals(9, DBHelpers.getRowCount(db, "events"));
|
||||
storage.pruneEvents(14); // Delete < today.
|
||||
assertEquals(5, DBHelpers.getRowCount(db, "events"));
|
||||
}
|
||||
|
||||
public void testVacuum() throws Exception {
|
||||
final PrepopulatedMockHealthReportDatabaseStorage storage =
|
||||
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
|
||||
final SQLiteDatabase db = storage.getDB();
|
||||
// Need to disable auto_vacuum to allow free page fragmentation. Note that the pragma changes
|
||||
// only after a vacuum command.
|
||||
db.execSQL("PRAGMA auto_vacuum=0");
|
||||
db.execSQL("vacuum");
|
||||
assertTrue(isAutoVacuumingDisabled(storage));
|
||||
|
||||
createFreePages(storage);
|
||||
storage.vacuum();
|
||||
assertEquals(0, getFreelistCount(storage));
|
||||
}
|
||||
|
||||
public long getFreelistCount(final MockHealthReportDatabaseStorage storage) {
|
||||
return storage.getIntFromQuery("PRAGMA freelist_count", null);
|
||||
}
|
||||
|
||||
public boolean isAutoVacuumingDisabled(final MockHealthReportDatabaseStorage storage) {
|
||||
return storage.getIntFromQuery("PRAGMA auto_vacuum", null) == 0;
|
||||
}
|
||||
|
||||
private void createFreePages(final PrepopulatedMockHealthReportDatabaseStorage storage) throws Exception {
|
||||
// Insert and delete until DB has free page fragmentation. The loop helps ensure that the
|
||||
// fragmentation will occur with minimal disk usage. The upper loop limits are arbitrary.
|
||||
final SQLiteDatabase db = storage.getDB();
|
||||
for (int i = 10; i <= 1250; i *= 5) {
|
||||
storage.insertTextualEvents(i);
|
||||
db.delete("events_textual", "date < ?", new String[] {Integer.toString(i / 2)});
|
||||
if (getFreelistCount(storage) > 0) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
fail("Database free pages failed to fragment.");
|
||||
}
|
||||
|
||||
public void testDisableAutoVacuuming() throws Exception {
|
||||
final PrepopulatedMockHealthReportDatabaseStorage storage =
|
||||
new PrepopulatedMockHealthReportDatabaseStorage(context, fakeProfileDirectory);
|
||||
final SQLiteDatabase db = storage.getDB();
|
||||
// The pragma changes only after a vacuum command.
|
||||
db.execSQL("PRAGMA auto_vacuum=1");
|
||||
db.execSQL("vacuum");
|
||||
assertEquals(1, storage.getIntFromQuery("PRAGMA auto_vacuum", null));
|
||||
storage.disableAutoVacuuming();
|
||||
db.execSQL("vacuum");
|
||||
assertTrue(isAutoVacuumingDisabled(storage));
|
||||
}
|
||||
}
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче