diff --git a/mobile/android/base/GeckoApp.java b/mobile/android/base/GeckoApp.java index 322d0c5836ea..6a2a0552d2b8 100644 --- a/mobile/android/base/GeckoApp.java +++ b/mobile/android/base/GeckoApp.java @@ -117,6 +117,7 @@ import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; @@ -1291,7 +1292,16 @@ abstract public class GeckoApp final String profilePath = getProfile().getDir().getAbsolutePath(); final EventDispatcher dispatcher = GeckoAppShell.getEventDispatcher(); Log.i(LOGTAG, "Creating BrowserHealthRecorder."); - mHealthRecorder = new BrowserHealthRecorder(GeckoApp.this, profilePath, dispatcher, + final String osLocale = Locale.getDefault().toString(); + Log.d(LOGTAG, "Locale is " + osLocale); + + // Replace the duplicate `osLocale` argument when we support switchable + // application locales. + mHealthRecorder = new BrowserHealthRecorder(GeckoApp.this, + profilePath, + dispatcher, + osLocale, + osLocale, // Placeholder. previousSession); } }); @@ -1555,8 +1565,15 @@ abstract public class GeckoApp GeckoPreferences.broadcastHealthReportUploadPref(context); /* - XXXX see bug 635342 - We want to disable this code if possible. It is about 145ms in runtime + XXXX see Bug 635342. + We want to disable this code if possible. It is about 145ms in runtime. + + If this code ever becomes live again, you'll need to chain the + new locale into BrowserHealthRecorder correctly. See + GeckoAppShell.setSelectedLocale. + We pass the OS locale into the BHR constructor: we need to grab + that *before* we modify the current locale! + SharedPreferences settings = getPreferences(Activity.MODE_PRIVATE); String localeCode = settings.getString(getPackageName() + ".locale", ""); if (localeCode != null && localeCode.length() > 0) diff --git a/mobile/android/base/GeckoAppShell.java b/mobile/android/base/GeckoAppShell.java index 28d91f00cff6..4e5cc9fda45f 100644 --- a/mobile/android/base/GeckoAppShell.java +++ b/mobile/android/base/GeckoAppShell.java @@ -1525,6 +1525,12 @@ public class GeckoAppShell Gecko resets the locale to en-US by calling this function with an empty string. This affects GeckoPreferences activity in multi-locale builds. + N.B., if this code ever becomes live again, you need to hook it up to locale + recording in BrowserHealthRecorder: we track the current app and OS locales + as part of the recorded environment. + + See similar note in GeckoApp.java for the startup path. + //We're not using this, not need to save it (see bug 635342) SharedPreferences settings = getContext().getPreferences(Activity.MODE_PRIVATE); diff --git a/mobile/android/base/health/BrowserHealthRecorder.java b/mobile/android/base/health/BrowserHealthRecorder.java index c73cbcdfc76f..055abb56c8c4 100644 --- a/mobile/android/base/health/BrowserHealthRecorder.java +++ b/mobile/android/base/health/BrowserHealthRecorder.java @@ -16,8 +16,6 @@ import org.mozilla.gecko.AppConstants; import org.mozilla.gecko.GeckoApp; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoEvent; -import org.mozilla.gecko.PrefsHelper; -import org.mozilla.gecko.PrefsHelper.PrefHandler; import org.mozilla.gecko.background.healthreport.EnvironmentBuilder; import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage; @@ -38,6 +36,7 @@ import java.io.FileOutputStream; import java.io.OutputStreamWriter; import java.nio.charset.Charset; import java.util.ArrayList; +import java.util.Iterator; import java.util.Scanner; import java.util.concurrent.atomic.AtomicBoolean; @@ -50,8 +49,7 @@ import java.util.concurrent.atomic.AtomicBoolean; * Keep an instance of this class around. * * Tell it when an environment attribute has changed: call {@link - * #onBlocklistPrefChanged(boolean)} or {@link - * #onTelemetryPrefChanged(boolean)}, followed by {@link + * #onAppLocaleChanged(String)} followed by {@link * #onEnvironmentChanged()}. * * Use it to record events: {@link #recordSearch(String, String)}. @@ -60,8 +58,9 @@ import java.util.concurrent.atomic.AtomicBoolean; */ public class BrowserHealthRecorder implements GeckoEventListener { private static final String LOG_TAG = "GeckoHealthRec"; + private static final String PREF_ACCEPT_LANG = "intl.accept_languages"; private static final String PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled"; - private static final String EVENT_ADDONS_ALL = "Addons:All"; + private static final String EVENT_SNAPSHOT = "HealthReport:Snapshot"; private static final String EVENT_ADDONS_CHANGE = "Addons:Change"; private static final String EVENT_ADDONS_UNINSTALLING = "Addons:Uninstalling"; private static final String EVENT_PREF_CHANGE = "Pref:Change"; @@ -242,8 +241,15 @@ public class BrowserHealthRecorder implements GeckoEventListener { /** * This constructor does IO. Run it on a background thread. + * + * appLocale can be null, which indicates that it will be provided later. */ - public BrowserHealthRecorder(final Context context, final String profilePath, final EventDispatcher dispatcher, SessionInformation previousSession) { + public BrowserHealthRecorder(final Context context, + final String profilePath, + final EventDispatcher dispatcher, + final String osLocale, + final String appLocale, + SessionInformation previousSession) { Log.d(LOG_TAG, "Initializing. Dispatcher is " + dispatcher); this.dispatcher = dispatcher; this.previousSession = previousSession; @@ -263,9 +269,12 @@ public class BrowserHealthRecorder implements GeckoEventListener { this.client = null; } + // Note that the PIC is not necessarily fully initialized at this point: + // we haven't set the app locale. This must be done before an environment + // is recorded. this.profileCache = new ProfileInformationCache(profilePath); try { - this.initialize(context, profilePath); + this.initialize(context, profilePath, osLocale, appLocale); } catch (Exception e) { Log.e(LOG_TAG, "Exception initializing.", e); } @@ -299,7 +308,7 @@ public class BrowserHealthRecorder implements GeckoEventListener { } private void unregisterEventListeners() { - this.dispatcher.unregisterEventListener(EVENT_ADDONS_ALL, this); + this.dispatcher.unregisterEventListener(EVENT_SNAPSHOT, this); this.dispatcher.unregisterEventListener(EVENT_ADDONS_CHANGE, this); this.dispatcher.unregisterEventListener(EVENT_ADDONS_UNINSTALLING, this); this.dispatcher.unregisterEventListener(EVENT_PREF_CHANGE, this); @@ -307,14 +316,9 @@ public class BrowserHealthRecorder implements GeckoEventListener { this.dispatcher.unregisterEventListener(EVENT_SEARCH, this); } - public void onBlocklistPrefChanged(boolean to) { + public void onAppLocaleChanged(String to) { this.profileCache.beginInitialization(); - this.profileCache.setBlocklistEnabled(to); - } - - public void onTelemetryPrefChanged(boolean to) { - this.profileCache.beginInitialization(); - this.profileCache.setTelemetryEnabled(to); + this.profileCache.setAppLocale(to); } public void onAddonChanged(String id, JSONObject json) { @@ -340,8 +344,7 @@ public class BrowserHealthRecorder implements GeckoEventListener { * environment, such that a new environment should be computed and prepared * for use in future events. * - * Invoke this method after calls that mutate the environment, such as - * {@link #onBlocklistPrefChanged(boolean)}. + * Invoke this method after calls that mutate the environment. * * If this change resulted in a transition between two environments, {@link * #onEnvironmentTransition(int, int)} will be invoked on the background @@ -491,14 +494,36 @@ public class BrowserHealthRecorder implements GeckoEventListener { return time; } - private void handlePrefValue(final String pref, final boolean value) { - Log.d(LOG_TAG, "Incorporating environment: " + pref + " = " + value); - if (AppConstants.TELEMETRY_PREF_NAME.equals(pref)) { - profileCache.setTelemetryEnabled(value); + private void onPrefMessage(final String pref, final JSONObject message) { + Log.d(LOG_TAG, "Incorporating environment: " + pref); + if (PREF_ACCEPT_LANG.equals(pref)) { + // We only record whether this is user-set. + try { + this.profileCache.beginInitialization(); + this.profileCache.setAcceptLangUserSet(message.getBoolean("isUserSet")); + } catch (JSONException ex) { + Log.w(LOG_TAG, "Unexpected JSONException fetching isUserSet for " + pref); + } return; } - if (PREF_BLOCKLIST_ENABLED.equals(pref)) { - profileCache.setBlocklistEnabled(value); + + // (We only handle boolean prefs right now.) + try { + boolean value = message.getBoolean("value"); + + if (AppConstants.TELEMETRY_PREF_NAME.equals(pref)) { + this.profileCache.beginInitialization(); + this.profileCache.setTelemetryEnabled(value); + return; + } + + if (PREF_BLOCKLIST_ENABLED.equals(pref)) { + this.profileCache.beginInitialization(); + this.profileCache.setBlocklistEnabled(value); + return; + } + } catch (JSONException ex) { + Log.w(LOG_TAG, "Unexpected JSONException fetching boolean value for " + pref); return; } Log.w(LOG_TAG, "Unexpected pref: " + pref); @@ -571,7 +596,9 @@ public class BrowserHealthRecorder implements GeckoEventListener { * Add provider-specific initialization in this method. */ private synchronized void initialize(final Context context, - final String profilePath) + final String profilePath, + final String osLocale, + final String appLocale) throws java.io.IOException { Log.d(LOG_TAG, "Initializing profile cache."); @@ -579,6 +606,9 @@ public class BrowserHealthRecorder implements GeckoEventListener { // If we can restore state from last time, great. if (this.profileCache.restoreUnlessInitialized()) { + this.profileCache.updateLocales(osLocale, appLocale); + this.profileCache.completeInitialization(); + Log.d(LOG_TAG, "Successfully restored state. Initializing storage."); initializeStorage(); return; @@ -587,31 +617,12 @@ public class BrowserHealthRecorder implements GeckoEventListener { // Otherwise, let's initialize it from scratch. this.profileCache.beginInitialization(); this.profileCache.setProfileCreationTime(getAndPersistProfileInitTime(context, profilePath)); + this.profileCache.setOSLocale(osLocale); + this.profileCache.setAppLocale(appLocale); - final BrowserHealthRecorder self = this; - - PrefHandler handler = new PrefsHelper.PrefHandlerBase() { - @Override - public void prefValue(String pref, boolean value) { - handlePrefValue(pref, value); - } - - @Override - public void finish() { - Log.d(LOG_TAG, "Requesting all add-ons from Gecko."); - dispatcher.registerEventListener(EVENT_ADDONS_ALL, self); - GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Addons:FetchAll", null)); - // Wait for the broadcast event which completes our initialization. - } - }; - - // Oh, singletons. - PrefsHelper.getPrefs(new String[] { - AppConstants.TELEMETRY_PREF_NAME, - PREF_BLOCKLIST_ENABLED - }, - handler); - Log.d(LOG_TAG, "Requested prefs."); + Log.d(LOG_TAG, "Requesting all add-ons and FHR prefs from Gecko."); + dispatcher.registerEventListener(EVENT_SNAPSHOT, this); + GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("HealthReport:RequestSnapshot", null)); } /** @@ -638,12 +649,22 @@ public class BrowserHealthRecorder implements GeckoEventListener { @Override public void handleMessage(String event, JSONObject message) { try { - if (EVENT_ADDONS_ALL.equals(event)) { - Log.d(LOG_TAG, "Got all add-ons."); + if (EVENT_SNAPSHOT.equals(event)) { + Log.d(LOG_TAG, "Got all add-ons and prefs."); try { - JSONObject addons = message.getJSONObject("json"); + JSONObject json = message.getJSONObject("json"); + JSONObject addons = json.getJSONObject("addons"); Log.i(LOG_TAG, "Persisting " + addons.length() + " add-ons."); profileCache.setJSONForAddons(addons); + + JSONObject prefs = json.getJSONObject("prefs"); + Log.i(LOG_TAG, "Persisting prefs."); + Iterator keys = prefs.keys(); + while (keys.hasNext()) { + String pref = (String) keys.next(); + this.onPrefMessage(pref, prefs.getJSONObject(pref)); + } + profileCache.completeInitialization(); } catch (java.io.IOException e) { Log.e(LOG_TAG, "Error completing profile cache initialization.", e); @@ -675,7 +696,7 @@ public class BrowserHealthRecorder implements GeckoEventListener { if (EVENT_PREF_CHANGE.equals(event)) { final String pref = message.getString("pref"); Log.d(LOG_TAG, "Pref changed: " + pref); - handlePrefValue(pref, message.getBoolean("value")); + this.onPrefMessage(pref, message); this.onEnvironmentChanged(); return; } diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js index 9cd65d525629..fd5ab59d8b30 100644 --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -5311,7 +5311,10 @@ var FormAssistant = { * -- and reflect them back to Java. */ let HealthReportStatusListener = { - TELEMETRY_PREF: + PREF_ACCEPT_LANG: "intl.accept_languages", + PREF_BLOCKLIST_ENABLED: "extensions.blocklist.enabled", + + PREF_TELEMETRY_ENABLED: #ifdef MOZ_TELEMETRY_REPORTING // Telemetry pref differs based on build. #ifdef MOZ_TELEMETRY_ON_BY_DEFAULT @@ -5330,18 +5333,21 @@ let HealthReportStatusListener = { console.log("Failed to initialize add-on status listener. FHR cannot report add-on state. " + ex); } - Services.obs.addObserver(this, "Addons:FetchAll", false); - Services.prefs.addObserver("extensions.blocklist.enabled", this, false); - if (this.TELEMETRY_PREF) { - Services.prefs.addObserver(this.TELEMETRY_PREF, this, false); + console.log("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); } }, uninit: function () { - Services.obs.removeObserver(this, "Addons:FetchAll"); - Services.prefs.removeObserver("extensions.blocklist.enabled", this); - if (this.TELEMETRY_PREF) { - Services.prefs.removeObserver(this.TELEMETRY_PREF, this); + Services.obs.removeObserver(this, "HealthReport:RequestSnapshot"); + Services.prefs.removeObserver(this.PREF_ACCEPT_LANG, this); + Services.prefs.removeObserver(this.PREF_BLOCKLIST_ENABLED, this); + if (this.PREF_TELEMETRY_ENABLED) { + Services.prefs.removeObserver(this.PREF_TELEMETRY_ENABLED, this); } AddonManager.removeAddonListener(this); @@ -5349,11 +5355,30 @@ let HealthReportStatusListener = { observe: function (aSubject, aTopic, aData) { switch (aTopic) { - case "Addons:FetchAll": - HealthReportStatusListener.sendAllAddonsToJava(); + case "HealthReport:RequestSnapshot": + HealthReportStatusListener.sendSnapshotToJava(); break; case "nsPref:changed": - sendMessageToJava({ type: "Pref:Change", pref: aData, value: Services.prefs.getBoolPref(aData) }); + 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; + } + + sendMessageToJava(response); break; } }, @@ -5435,9 +5460,9 @@ let HealthReportStatusListener = { this.notifyJava(aAddon); }, - sendAllAddonsToJava: function () { + sendSnapshotToJava: function () { AddonManager.getAllAddons(function (aAddons) { - let json = {}; + let jsonA = {}; if (aAddons) { for (let i = 0; i < aAddons.length; ++i) { let addon = aAddons[i]; @@ -5446,14 +5471,43 @@ let HealthReportStatusListener = { if (HealthReportStatusListener._shouldIgnore(addon)) { addonJSON.ignore = true; } - json[addon.id] = addonJSON; + jsonA[addon.id] = addonJSON; } catch (e) { // Just skip this add-on. } } } - sendMessageToJava({ type: "Addons:All", json: json }); - }); + + // 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."); + sendMessageToJava({ + type: "HealthReport:Snapshot", + json: { + addons: jsonA, + prefs: jsonP, + }, + }); + }.bind(this)); }, };