From ba2c3ee1de535914510b716328491fdc9edc9dd1 Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Fri, 10 May 2013 21:30:57 -0700 Subject: [PATCH] Bug 857419 - Implement about:healthreport on Android. r=rnewman --- mobile/android/chrome/Makefile.in | 1 + .../chrome/content/aboutHealthReport.js | 204 ++++++++++++++++++ .../chrome/content/aboutHealthReport.xhtml | 32 +++ .../chrome/content/healthreport-prefs.js | 6 + mobile/android/chrome/jar.mn | 6 +- mobile/android/components/AboutRedirector.js | 8 +- .../components/MobileComponents.manifest | 3 + .../en-US/chrome/aboutHealthReport.dtd | 6 + mobile/android/locales/jar.mn | 3 + .../android/themes/core/aboutHealthReport.css | 15 ++ mobile/android/themes/core/jar.mn | 3 + modules/libpref/src/Makefile.in | 4 + 12 files changed, 289 insertions(+), 2 deletions(-) create mode 100644 mobile/android/chrome/content/aboutHealthReport.js create mode 100644 mobile/android/chrome/content/aboutHealthReport.xhtml create mode 100644 mobile/android/chrome/content/healthreport-prefs.js create mode 100644 mobile/android/locales/en-US/chrome/aboutHealthReport.dtd create mode 100644 mobile/android/themes/core/aboutHealthReport.css diff --git a/mobile/android/chrome/Makefile.in b/mobile/android/chrome/Makefile.in index d816fe52e22..9c5d9451bce 100644 --- a/mobile/android/chrome/Makefile.in +++ b/mobile/android/chrome/Makefile.in @@ -12,6 +12,7 @@ include $(DEPTH)/config/autoconf.mk DEFINES += -DAB_CD=$(MOZ_UI_LOCALE) \ -DPACKAGE=browser \ -DMOZ_APP_VERSION=$(MOZ_APP_VERSION) \ + -DANDROID_PACKAGE_NAME=$(ANDROID_PACKAGE_NAME) \ $(NULL) include $(topsrcdir)/config/rules.mk diff --git a/mobile/android/chrome/content/aboutHealthReport.js b/mobile/android/chrome/content/aboutHealthReport.js new file mode 100644 index 00000000000..bd48102e1c8 --- /dev/null +++ b/mobile/android/chrome/content/aboutHealthReport.js @@ -0,0 +1,204 @@ +#filter substitution +// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +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"); + +// Name of Android SharedPreference controlling whether to upload +// 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"; + +function sendMessageToJava(message) { + return Cc["@mozilla.org/android/bridge;1"] + .getService(Ci.nsIAndroidBridge) + .handleGeckoMessage(JSON.stringify(message)); +} + +// Default preferences for the application. +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); + }, + + observe: function (subject, topic, data) { + if (topic != PREF_UPLOAD_ENABLED) { + return; + } + + subject.updatePrefState(); + }, + + uninit: function () { + sharedPrefs.removeObserver(PREF_UPLOAD_ENABLED, this); + }, + + _getReportURI: function () { + let url = Services.urlFormatter.formatURLPref(PREF_REPORTURL); + return Services.io.newURI(url, null, null); + }, + + onOptIn: function () { + policy.recordHealthReportUploadEnabled(true, + "Health report page sent opt-in command."); + this.updatePrefState(); + }, + + onOptOut: function () { + policy.recordHealthReportUploadEnabled(false, + "Health report page sent opt-out command."); + this.updatePrefState(); + }, + + updatePrefState: function () { + try { + let prefs = { + enabled: policy.healthReportUploadEnabled, + }; + this.injectData("prefs", prefs); + } catch (e) { + this.reportFailure(this.ERROR_PREFS_FAILED); + } + }, + + refreshPayload: function () { + reporter.collectAndObtainJSONPayload().then(healthReportWrapper.updatePayload, + healthReportWrapper.handlePayloadFailure); + }, + + updatePayload: function (data) { + healthReportWrapper.injectData("payload", data); + }, + + 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; + + let data = { + type: type, + content: content, + }; + + let iframe = document.getElementById("remote-report"); + iframe.contentWindow.postMessage(data, reportUrl); + }, + + handleRemoteCommand: function (evt) { + switch (evt.detail.command) { + case "DisableDataSubmission": + this.onOptOut(); + break; + case "EnableDataSubmission": + this.onOptIn(); + break; + case "RequestCurrentPrefs": + this.updatePrefState(); + break; + case "RequestCurrentPayload": + this.refreshPayload(); + break; + default: + Cu.reportError("Unexpected remote command received: " + evt.detail.command + + ". Ignoring command."); + break; + } + }, + + initRemotePage: function () { + let iframe = document.getElementById("remote-report").contentDocument; + iframe.addEventListener("RemoteHealthReportCommand", + function onCommand(e) {healthReportWrapper.handleRemoteCommand(e);}, + false); + healthReportWrapper.updatePrefState(); + }, + + // error handling + ERROR_INIT_FAILED: 1, + ERROR_PAYLOAD_FAILED: 2, + ERROR_PREFS_FAILED: 3, + + reportFailure: function (error) { + let details = { + errorType: error, + }; + healthReportWrapper.injectData("error", details); + }, + + handleInitFailure: function () { + healthReportWrapper.reportFailure(healthReportWrapper.ERROR_INIT_FAILED); + }, + + handlePayloadFailure: function () { + healthReportWrapper.reportFailure(healthReportWrapper.ERROR_PAYLOAD_FAILED); + }, +}; diff --git a/mobile/android/chrome/content/aboutHealthReport.xhtml b/mobile/android/chrome/content/aboutHealthReport.xhtml new file mode 100644 index 00000000000..6d2823f5305 --- /dev/null +++ b/mobile/android/chrome/content/aboutHealthReport.xhtml @@ -0,0 +1,32 @@ + + + +%brandDTD; + +%globalDTD; + +%aboutHealthReportDTD; +]> + + + + + + &abouthealth.pagetitle; + + +