Bug 876473 - Provide Java-generated Firefox Health Report to about:healthreport. r=rnewman

This commit is contained in:
Nick Alexander 2013-05-30 17:42:57 -07:00
parent 5eb58c3d2a
commit 23648dc3f2
4 changed files with 175 additions and 66 deletions

View File

@ -13,6 +13,7 @@ import org.mozilla.gecko.gfx.GeckoLayerClient;
import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
import org.mozilla.gecko.gfx.LayerView;
import org.mozilla.gecko.gfx.PanZoomController;
import org.mozilla.gecko.health.BrowserHealthReporter;
import org.mozilla.gecko.menu.GeckoMenu;
import org.mozilla.gecko.util.FloatUtils;
import org.mozilla.gecko.util.GamepadUtils;
@ -146,6 +147,8 @@ abstract public class BrowserApp extends GeckoApp
private OrderedBroadcastHelper mOrderedBroadcastHelper;
private BrowserHealthReporter mBrowserHealthReporter;
@Override
public void onTabChanged(Tab tab, Tabs.TabEvents msg, Object data) {
switch(msg) {
@ -426,6 +429,7 @@ abstract public class BrowserApp extends GeckoApp
JavaAddonManager.getInstance().init(getApplicationContext());
mSharedPreferencesHelper = new SharedPreferencesHelper(getApplicationContext());
mOrderedBroadcastHelper = new OrderedBroadcastHelper(getApplicationContext());
mBrowserHealthReporter = new BrowserHealthReporter();
if (AppConstants.MOZ_ANDROID_BEAM && Build.VERSION.SDK_INT >= 14) {
NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
@ -680,6 +684,11 @@ abstract public class BrowserApp extends GeckoApp
mOrderedBroadcastHelper = null;
}
if (mBrowserHealthReporter != null) {
mBrowserHealthReporter.uninit();
mBrowserHealthReporter = null;
}
unregisterEventListener("CharEncoding:Data");
unregisterEventListener("CharEncoding:State");
unregisterEventListener("Feedback:LastUrl");

View File

@ -113,6 +113,7 @@ FENNEC_JAVA_FILES = \
GeckoViewsFactory.java \
GeckoView.java \
health/BrowserHealthRecorder.java \
health/BrowserHealthReporter.java \
InputMethods.java \
JavaAddonManager.java \
LightweightTheme.java \

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

View File

@ -8,8 +8,6 @@
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/SharedPreferences.jsm");
@ -17,86 +15,44 @@ Cu.import("resource://gre/modules/SharedPreferences.jsm");
// health reports.
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.
const PREF_REPORTURL = "datareporting.healthreport.about.reportUrl";
const EVENT_HEALTH_REQUEST = "HealthReport:Request";
const EVENT_HEALTH_RESPONSE = "HealthReport:Response";
function sendMessageToJava(message) {
return Cc["@mozilla.org/android/bridge;1"]
.getService(Ci.nsIAndroidBridge)
.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 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 = {
init: function () {
reporter.onInit().then(healthReportWrapper.refreshPayload,
healthReportWrapper.handleInitFailure);
let iframe = document.getElementById("remote-report");
iframe.addEventListener("load", healthReportWrapper.initRemotePage, false);
let report = this._getReportURI();
iframe.src = report.spec;
sharedPrefs.addObserver(PREF_UPLOAD_ENABLED, this, false);
Services.obs.addObserver(this, EVENT_HEALTH_RESPONSE, false);
},
observe: function (subject, topic, data) {
if (topic != PREF_UPLOAD_ENABLED) {
return;
if (topic == PREF_UPLOAD_ENABLED) {
this.updatePrefState();
} else if (topic == EVENT_HEALTH_RESPONSE) {
this.updatePayload(data);
}
subject.updatePrefState();
},
uninit: function () {
sharedPrefs.removeObserver(PREF_UPLOAD_ENABLED, this);
Services.obs.removeObserver(this, EVENT_HEALTH_RESPONSE);
},
_getReportURI: function () {
@ -105,21 +61,22 @@ let healthReportWrapper = {
},
onOptIn: function () {
policy.recordHealthReportUploadEnabled(true,
"Health report page sent opt-in command.");
console.log("AboutHealthReport: page sent opt-in command.");
sharedPrefs.setBoolPref(PREF_UPLOAD_ENABLED, true);
this.updatePrefState();
},
onOptOut: function () {
policy.recordHealthReportUploadEnabled(false,
"Health report page sent opt-out command.");
console.log("AboutHealthReport: page sent opt-out command.");
sharedPrefs.setBoolPref(PREF_UPLOAD_ENABLED, false);
this.updatePrefState();
},
updatePrefState: function () {
console.log("AboutHealthReport: page requested pref state.");
try {
let prefs = {
enabled: policy.healthReportUploadEnabled,
enabled: sharedPrefs.getBoolPref(PREF_UPLOAD_ENABLED),
};
this.injectData("prefs", prefs);
} catch (e) {
@ -128,21 +85,27 @@ let healthReportWrapper = {
},
refreshPayload: function () {
reporter.collectAndObtainJSONPayload().then(healthReportWrapper.updatePayload,
healthReportWrapper.handlePayloadFailure);
console.log("AboutHealthReport: page requested fresh payload.");
sendMessageToJava({
type: EVENT_HEALTH_REQUEST,
});
},
updatePayload: function (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) {
let report = this._getReportURI();
// file URIs can't be used for targetOrigin, so we use "*" for 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;
// file: URIs can't be used for targetOrigin, so we use "*" for
// 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 data = {
type: type,