gecko/mobile/android/base/background/healthreport/ProfileInformationCache.java

375 lines
11 KiB
Java

/* 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.
*/
public static final int FORMAT_VERSION = 3;
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;
public ProfileInformationCache(String profilePath) {
file = new File(profilePath + File.separator + CACHE_FILE);
Logger.pii(LOG_TAG, "Using " + file.getAbsolutePath() + " for profile information cache.");
}
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 {
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");
final String contents = scanner.useDelimiter("\\A").next();
return new JSONObject(contents);
} 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;
}
}