mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-01-10 13:54:27 +00:00
Bug 876473 - Provide Java-generated Firefox Health Report to about:healthreport. r=rnewman
This commit is contained in:
parent
5eb58c3d2a
commit
23648dc3f2
@ -13,6 +13,7 @@ import org.mozilla.gecko.gfx.GeckoLayerClient;
|
|||||||
import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
|
import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
|
||||||
import org.mozilla.gecko.gfx.LayerView;
|
import org.mozilla.gecko.gfx.LayerView;
|
||||||
import org.mozilla.gecko.gfx.PanZoomController;
|
import org.mozilla.gecko.gfx.PanZoomController;
|
||||||
|
import org.mozilla.gecko.health.BrowserHealthReporter;
|
||||||
import org.mozilla.gecko.menu.GeckoMenu;
|
import org.mozilla.gecko.menu.GeckoMenu;
|
||||||
import org.mozilla.gecko.util.FloatUtils;
|
import org.mozilla.gecko.util.FloatUtils;
|
||||||
import org.mozilla.gecko.util.GamepadUtils;
|
import org.mozilla.gecko.util.GamepadUtils;
|
||||||
@ -146,6 +147,8 @@ abstract public class BrowserApp extends GeckoApp
|
|||||||
|
|
||||||
private OrderedBroadcastHelper mOrderedBroadcastHelper;
|
private OrderedBroadcastHelper mOrderedBroadcastHelper;
|
||||||
|
|
||||||
|
private BrowserHealthReporter mBrowserHealthReporter;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
|
public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
|
||||||
switch(msg) {
|
switch(msg) {
|
||||||
@ -426,6 +429,7 @@ abstract public class BrowserApp extends GeckoApp
|
|||||||
JavaAddonManager.getInstance().init(getApplicationContext());
|
JavaAddonManager.getInstance().init(getApplicationContext());
|
||||||
mSharedPreferencesHelper = new SharedPreferencesHelper(getApplicationContext());
|
mSharedPreferencesHelper = new SharedPreferencesHelper(getApplicationContext());
|
||||||
mOrderedBroadcastHelper = new OrderedBroadcastHelper(getApplicationContext());
|
mOrderedBroadcastHelper = new OrderedBroadcastHelper(getApplicationContext());
|
||||||
|
mBrowserHealthReporter = new BrowserHealthReporter();
|
||||||
|
|
||||||
if (AppConstants.MOZ_ANDROID_BEAM && Build.VERSION.SDK_INT >= 14) {
|
if (AppConstants.MOZ_ANDROID_BEAM && Build.VERSION.SDK_INT >= 14) {
|
||||||
NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
|
NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
|
||||||
@ -680,6 +684,11 @@ abstract public class BrowserApp extends GeckoApp
|
|||||||
mOrderedBroadcastHelper = null;
|
mOrderedBroadcastHelper = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (mBrowserHealthReporter != null) {
|
||||||
|
mBrowserHealthReporter.uninit();
|
||||||
|
mBrowserHealthReporter = null;
|
||||||
|
}
|
||||||
|
|
||||||
unregisterEventListener("CharEncoding:Data");
|
unregisterEventListener("CharEncoding:Data");
|
||||||
unregisterEventListener("CharEncoding:State");
|
unregisterEventListener("CharEncoding:State");
|
||||||
unregisterEventListener("Feedback:LastUrl");
|
unregisterEventListener("Feedback:LastUrl");
|
||||||
|
@ -113,6 +113,7 @@ FENNEC_JAVA_FILES = \
|
|||||||
GeckoViewsFactory.java \
|
GeckoViewsFactory.java \
|
||||||
GeckoView.java \
|
GeckoView.java \
|
||||||
health/BrowserHealthRecorder.java \
|
health/BrowserHealthRecorder.java \
|
||||||
|
health/BrowserHealthReporter.java \
|
||||||
InputMethods.java \
|
InputMethods.java \
|
||||||
JavaAddonManager.java \
|
JavaAddonManager.java \
|
||||||
LightweightTheme.java \
|
LightweightTheme.java \
|
||||||
|
136
mobile/android/base/health/BrowserHealthReporter.java
Normal file
136
mobile/android/base/health/BrowserHealthReporter.java
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
/* -*- 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 android.content.ContentProviderClient;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import org.mozilla.gecko.GeckoAppShell;
|
||||||
|
import org.mozilla.gecko.GeckoEvent;
|
||||||
|
import org.mozilla.gecko.GeckoProfile;
|
||||||
|
|
||||||
|
import org.mozilla.gecko.background.healthreport.EnvironmentBuilder;
|
||||||
|
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 org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 long MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
|
||||||
|
public static final long MILLISECONDS_PER_SIX_MONTHS = 180 * MILLISECONDS_PER_DAY;
|
||||||
|
|
||||||
|
public static final String EVENT_REQUEST = "HealthReport:Request";
|
||||||
|
public static final String EVENT_RESPONSE = "HealthReport:Response";
|
||||||
|
|
||||||
|
public BrowserHealthReporter() {
|
||||||
|
GeckoAppShell.registerEventListener(EVENT_REQUEST, this);
|
||||||
|
|
||||||
|
final Context context = GeckoAppShell.getContext();
|
||||||
|
if (context == null) {
|
||||||
|
throw new IllegalStateException("Null Gecko context");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void uninit() {
|
||||||
|
GeckoAppShell.unregisterEventListener(EVENT_REQUEST, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
public JSONObject generateReport(long since, long lastPingTime, String profilePath) throws JSONException {
|
||||||
|
final Context context = GeckoAppShell.getContext();
|
||||||
|
if (context == null) {
|
||||||
|
Log.e(LOGTAG, "Null Gecko context; returning null report.", new RuntimeException());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
Log.e(LOGTAG, "No storage in health reporter; returning null report.", new RuntimeException());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
HealthReportGenerator generator = new HealthReportGenerator(storage);
|
||||||
|
return generator.generateDocument(since, lastPingTime, profilePath);
|
||||||
|
} finally {
|
||||||
|
client.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a new Health Report for the current Gecko profile.
|
||||||
|
*
|
||||||
|
* This method performs IO, so call it from a background thread.
|
||||||
|
*/
|
||||||
|
public JSONObject generateReport() throws JSONException {
|
||||||
|
GeckoProfile profile = GeckoAppShell.getGeckoInterface().getProfile();
|
||||||
|
String profilePath = profile.getDir().getAbsolutePath();
|
||||||
|
|
||||||
|
long since = System.currentTimeMillis() - MILLISECONDS_PER_SIX_MONTHS;
|
||||||
|
// TODO: read this from per-profile SharedPreference owned by background uploader.
|
||||||
|
long lastPingTime = since;
|
||||||
|
return generateReport(since, lastPingTime, profilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleMessage(String event, JSONObject message) {
|
||||||
|
try {
|
||||||
|
ThreadUtils.postToBackgroundThread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
JSONObject report = new JSONObject();
|
||||||
|
try {
|
||||||
|
report = generateReport();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(LOGTAG, "Generating report failed; responding with null.", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent(EVENT_RESPONSE, report.toString()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(LOGTAG, "Exception handling message \"" + event + "\":", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,8 +8,6 @@
|
|||||||
|
|
||||||
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
|
||||||
|
|
||||||
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
|
|
||||||
Cu.import("resource://gre/modules/OrderedBroadcast.jsm");
|
|
||||||
Cu.import("resource://gre/modules/Services.jsm");
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
Cu.import("resource://gre/modules/SharedPreferences.jsm");
|
Cu.import("resource://gre/modules/SharedPreferences.jsm");
|
||||||
|
|
||||||
@ -17,86 +15,44 @@ Cu.import("resource://gre/modules/SharedPreferences.jsm");
|
|||||||
// health reports.
|
// health reports.
|
||||||
const PREF_UPLOAD_ENABLED = "android.not_a_preference.healthreport.uploadEnabled";
|
const PREF_UPLOAD_ENABLED = "android.not_a_preference.healthreport.uploadEnabled";
|
||||||
|
|
||||||
// Action sent via Android Ordered Broadcast to background service.
|
|
||||||
const BROADCAST_ACTION_HEALTH_REPORT = "@ANDROID_PACKAGE_NAME@" + ".healthreport.request";
|
|
||||||
|
|
||||||
// Name of Gecko Pref specifying report content location.
|
// Name of Gecko Pref specifying report content location.
|
||||||
const PREF_REPORTURL = "datareporting.healthreport.about.reportUrl";
|
const PREF_REPORTURL = "datareporting.healthreport.about.reportUrl";
|
||||||
|
|
||||||
|
const EVENT_HEALTH_REQUEST = "HealthReport:Request";
|
||||||
|
const EVENT_HEALTH_RESPONSE = "HealthReport:Response";
|
||||||
|
|
||||||
function sendMessageToJava(message) {
|
function sendMessageToJava(message) {
|
||||||
return Cc["@mozilla.org/android/bridge;1"]
|
return Cc["@mozilla.org/android/bridge;1"]
|
||||||
.getService(Ci.nsIAndroidBridge)
|
.getService(Ci.nsIAndroidBridge)
|
||||||
.handleGeckoMessage(JSON.stringify(message));
|
.handleGeckoMessage(JSON.stringify(message));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default preferences for the application.
|
// about:healthreport prefs are stored in Firefox's default Android
|
||||||
|
// SharedPreferences.
|
||||||
let sharedPrefs = new SharedPreferences();
|
let sharedPrefs = new SharedPreferences();
|
||||||
|
|
||||||
let reporter = {
|
|
||||||
onInit: function () {
|
|
||||||
let deferred = Promise.defer();
|
|
||||||
deferred.resolve();
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
},
|
|
||||||
|
|
||||||
collectAndObtainJSONPayload: function () {
|
|
||||||
let deferred = Promise.defer();
|
|
||||||
|
|
||||||
let callback = function (data, token, action) {
|
|
||||||
if (data) {
|
|
||||||
// Bug 870992: the FHR report content expects FHR report data
|
|
||||||
// in string form. This costs us a JSON parsing round trip,
|
|
||||||
// since the ordered broadcast module parses the stringified
|
|
||||||
// JSON returned from Java. Since the FHR report content
|
|
||||||
// expects updates to preferences as a Javascript object, we
|
|
||||||
// cannot handle the situation uniformly, and we pay the price
|
|
||||||
// here, stringifying a huge chunk of JSON.
|
|
||||||
deferred.resolve(JSON.stringify(data));
|
|
||||||
} else {
|
|
||||||
deferred.reject();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
sendOrderedBroadcast(BROADCAST_ACTION_HEALTH_REPORT, null, callback);
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let policy = {
|
|
||||||
get healthReportUploadEnabled() {
|
|
||||||
return sharedPrefs.getBoolPref(PREF_UPLOAD_ENABLED);
|
|
||||||
},
|
|
||||||
|
|
||||||
recordHealthReportUploadEnabled: function (enabled) {
|
|
||||||
sharedPrefs.setBoolPref(PREF_UPLOAD_ENABLED, !!enabled);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let healthReportWrapper = {
|
let healthReportWrapper = {
|
||||||
init: function () {
|
init: function () {
|
||||||
reporter.onInit().then(healthReportWrapper.refreshPayload,
|
|
||||||
healthReportWrapper.handleInitFailure);
|
|
||||||
|
|
||||||
let iframe = document.getElementById("remote-report");
|
let iframe = document.getElementById("remote-report");
|
||||||
iframe.addEventListener("load", healthReportWrapper.initRemotePage, false);
|
iframe.addEventListener("load", healthReportWrapper.initRemotePage, false);
|
||||||
let report = this._getReportURI();
|
let report = this._getReportURI();
|
||||||
iframe.src = report.spec;
|
iframe.src = report.spec;
|
||||||
|
|
||||||
sharedPrefs.addObserver(PREF_UPLOAD_ENABLED, this, false);
|
sharedPrefs.addObserver(PREF_UPLOAD_ENABLED, this, false);
|
||||||
|
Services.obs.addObserver(this, EVENT_HEALTH_RESPONSE, false);
|
||||||
},
|
},
|
||||||
|
|
||||||
observe: function (subject, topic, data) {
|
observe: function (subject, topic, data) {
|
||||||
if (topic != PREF_UPLOAD_ENABLED) {
|
if (topic == PREF_UPLOAD_ENABLED) {
|
||||||
return;
|
this.updatePrefState();
|
||||||
|
} else if (topic == EVENT_HEALTH_RESPONSE) {
|
||||||
|
this.updatePayload(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
subject.updatePrefState();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
uninit: function () {
|
uninit: function () {
|
||||||
sharedPrefs.removeObserver(PREF_UPLOAD_ENABLED, this);
|
sharedPrefs.removeObserver(PREF_UPLOAD_ENABLED, this);
|
||||||
|
Services.obs.removeObserver(this, EVENT_HEALTH_RESPONSE);
|
||||||
},
|
},
|
||||||
|
|
||||||
_getReportURI: function () {
|
_getReportURI: function () {
|
||||||
@ -105,21 +61,22 @@ let healthReportWrapper = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onOptIn: function () {
|
onOptIn: function () {
|
||||||
policy.recordHealthReportUploadEnabled(true,
|
console.log("AboutHealthReport: page sent opt-in command.");
|
||||||
"Health report page sent opt-in command.");
|
sharedPrefs.setBoolPref(PREF_UPLOAD_ENABLED, true);
|
||||||
this.updatePrefState();
|
this.updatePrefState();
|
||||||
},
|
},
|
||||||
|
|
||||||
onOptOut: function () {
|
onOptOut: function () {
|
||||||
policy.recordHealthReportUploadEnabled(false,
|
console.log("AboutHealthReport: page sent opt-out command.");
|
||||||
"Health report page sent opt-out command.");
|
sharedPrefs.setBoolPref(PREF_UPLOAD_ENABLED, false);
|
||||||
this.updatePrefState();
|
this.updatePrefState();
|
||||||
},
|
},
|
||||||
|
|
||||||
updatePrefState: function () {
|
updatePrefState: function () {
|
||||||
|
console.log("AboutHealthReport: page requested pref state.");
|
||||||
try {
|
try {
|
||||||
let prefs = {
|
let prefs = {
|
||||||
enabled: policy.healthReportUploadEnabled,
|
enabled: sharedPrefs.getBoolPref(PREF_UPLOAD_ENABLED),
|
||||||
};
|
};
|
||||||
this.injectData("prefs", prefs);
|
this.injectData("prefs", prefs);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -128,21 +85,27 @@ let healthReportWrapper = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
refreshPayload: function () {
|
refreshPayload: function () {
|
||||||
reporter.collectAndObtainJSONPayload().then(healthReportWrapper.updatePayload,
|
console.log("AboutHealthReport: page requested fresh payload.");
|
||||||
healthReportWrapper.handlePayloadFailure);
|
sendMessageToJava({
|
||||||
|
type: EVENT_HEALTH_REQUEST,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
updatePayload: function (data) {
|
updatePayload: function (data) {
|
||||||
healthReportWrapper.injectData("payload", data);
|
healthReportWrapper.injectData("payload", data);
|
||||||
|
// Data is supposed to be a string, so the length should be
|
||||||
|
// defined. Just in case, we do this after injecting the data.
|
||||||
|
console.log("AboutHealthReport: sending payload to page " +
|
||||||
|
"(" + typeof(data) + " of length " + data.length + ").");
|
||||||
},
|
},
|
||||||
|
|
||||||
injectData: function (type, content) {
|
injectData: function (type, content) {
|
||||||
let report = this._getReportURI();
|
let report = this._getReportURI();
|
||||||
|
|
||||||
// file URIs can't be used for targetOrigin, so we use "*" for this special case
|
// file: URIs can't be used for targetOrigin, so we use "*" for
|
||||||
// in all other cases, pass in the URL to the report so we properly restrict the message dispatch
|
// this special case. In all other cases, pass in the URL to the
|
||||||
|
// report so we properly restrict the message dispatch.
|
||||||
let reportUrl = report.scheme == "file" ? "*" : report.spec;
|
let reportUrl = (report.scheme == "file") ? "*" : report.spec;
|
||||||
|
|
||||||
let data = {
|
let data = {
|
||||||
type: type,
|
type: type,
|
||||||
|
Loading…
Reference in New Issue
Block a user