merge fx-team to mozilla-central a=merge

This commit is contained in:
Carsten "Tomcat" Book 2016-01-12 11:52:48 +01:00
Родитель 0be5e80e7b 00f8f44c83
Коммит 54b57749ad
202 изменённых файлов: 566 добавлений и 29038 удалений

Просмотреть файл

@ -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));
}
}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше