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

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/mobile/android/base/background/healthreport/ProfileInformationCache.java	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,374 @@
     1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.6 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.7 +
     1.8 +package org.mozilla.gecko.background.healthreport;
     1.9 +
    1.10 +import java.io.File;
    1.11 +import java.io.FileNotFoundException;
    1.12 +import java.io.FileOutputStream;
    1.13 +import java.io.IOException;
    1.14 +import java.io.OutputStreamWriter;
    1.15 +import java.nio.charset.Charset;
    1.16 +import java.util.Locale;
    1.17 +import java.util.Scanner;
    1.18 +
    1.19 +import org.json.JSONException;
    1.20 +import org.json.JSONObject;
    1.21 +import org.mozilla.gecko.background.common.log.Logger;
    1.22 +import org.mozilla.gecko.background.healthreport.EnvironmentBuilder.ProfileInformationProvider;
    1.23 +
    1.24 +/**
    1.25 + * There are some parts of the FHR environment that can't be readily computed
    1.26 + * without a running Gecko -- add-ons, for example. In order to make this
    1.27 + * information available without launching Gecko, we persist it on Fennec
    1.28 + * startup. This class is the notepad in which we write.
    1.29 + */
    1.30 +public class ProfileInformationCache implements ProfileInformationProvider {
    1.31 +  private static final String LOG_TAG = "GeckoProfileInfo";
    1.32 +  private static final String CACHE_FILE = "profile_info_cache.json";
    1.33 +
    1.34 +  /*
    1.35 +   * FORMAT_VERSION history:
    1.36 +   *   -: No version number; implicit v1.
    1.37 +   *   1: Add versioning (Bug 878670).
    1.38 +   *   2: Bump to regenerate add-on set after landing Bug 900694 (Bug 901622).
    1.39 +   *   3: Add distribution, osLocale, appLocale.
    1.40 +   */
    1.41 +  public static final int FORMAT_VERSION = 3;
    1.42 +
    1.43 +  protected boolean initialized = false;
    1.44 +  protected boolean needsWrite = false;
    1.45 +
    1.46 +  protected final File file;
    1.47 +
    1.48 +  private volatile boolean blocklistEnabled = true;
    1.49 +  private volatile boolean telemetryEnabled = false;
    1.50 +  private volatile boolean isAcceptLangUserSet = false;
    1.51 +
    1.52 +  private volatile long profileCreationTime = 0;
    1.53 +  private volatile String distribution = "";
    1.54 +
    1.55 +  // There are really four kinds of locale in play:
    1.56 +  //
    1.57 +  // * The OS
    1.58 +  // * The Android environment of the app (setDefault)
    1.59 +  // * The Gecko locale
    1.60 +  // * The requested content locale (Accept-Language).
    1.61 +  //
    1.62 +  // We track only the first two, assuming that the Gecko locale will typically
    1.63 +  // be the same as the app locale.
    1.64 +  //
    1.65 +  // The app locale is fetched from the PIC because it can be modified at
    1.66 +  // runtime -- it won't necessarily be what Locale.getDefaultLocale() returns
    1.67 +  // in a fresh non-browser profile.
    1.68 +  //
    1.69 +  // We also track the OS locale here for the same reason -- we need to store
    1.70 +  // the default (OS) value before the locale-switching code takes effect!
    1.71 +  private volatile String osLocale = "";
    1.72 +  private volatile String appLocale = "";
    1.73 +
    1.74 +  private volatile JSONObject addons = null;
    1.75 +
    1.76 +  public ProfileInformationCache(String profilePath) {
    1.77 +    file = new File(profilePath + File.separator + CACHE_FILE);
    1.78 +    Logger.pii(LOG_TAG, "Using " + file.getAbsolutePath() + " for profile information cache.");
    1.79 +  }
    1.80 +
    1.81 +  public synchronized void beginInitialization() {
    1.82 +    initialized = false;
    1.83 +    needsWrite = true;
    1.84 +  }
    1.85 +
    1.86 +  public JSONObject toJSON() {
    1.87 +    JSONObject object = new JSONObject();
    1.88 +    try {
    1.89 +      object.put("version", FORMAT_VERSION);
    1.90 +      object.put("blocklist", blocklistEnabled);
    1.91 +      object.put("telemetry", telemetryEnabled);
    1.92 +      object.put("isAcceptLangUserSet", isAcceptLangUserSet);
    1.93 +      object.put("profileCreated", profileCreationTime);
    1.94 +      object.put("osLocale", osLocale);
    1.95 +      object.put("appLocale", appLocale);
    1.96 +      object.put("distribution", distribution);
    1.97 +      object.put("addons", addons);
    1.98 +    } catch (JSONException e) {
    1.99 +      // There isn't much we can do about this.
   1.100 +      // Let's just quietly muffle.
   1.101 +      return null;
   1.102 +    }
   1.103 +    return object;
   1.104 +  }
   1.105 +
   1.106 +  /**
   1.107 +   * Attempt to restore this object from a JSON blob. If there is a version mismatch, there has
   1.108 +   * likely been an upgrade to the cache format. The cache can be reconstructed without data loss
   1.109 +   * so rather than migrating, we invalidate the cache by refusing to store the given JSONObject
   1.110 +   * and returning false.
   1.111 +   *
   1.112 +   * @return false if there's a version mismatch or an error, true on success.
   1.113 +   */
   1.114 +  private boolean fromJSON(JSONObject object) throws JSONException {
   1.115 +    int version = object.optInt("version", 1);
   1.116 +    switch (version) {
   1.117 +    case FORMAT_VERSION:
   1.118 +      blocklistEnabled = object.getBoolean("blocklist");
   1.119 +      telemetryEnabled = object.getBoolean("telemetry");
   1.120 +      isAcceptLangUserSet = object.getBoolean("isAcceptLangUserSet");
   1.121 +      profileCreationTime = object.getLong("profileCreated");
   1.122 +      addons = object.getJSONObject("addons");
   1.123 +      distribution = object.getString("distribution");
   1.124 +      osLocale = object.getString("osLocale");
   1.125 +      appLocale = object.getString("appLocale");
   1.126 +      return true;
   1.127 +    default:
   1.128 +      Logger.warn(LOG_TAG, "Unable to restore from version " + version + " PIC file: expecting " + FORMAT_VERSION);
   1.129 +      return false;
   1.130 +    }
   1.131 +  }
   1.132 +
   1.133 +  protected JSONObject readFromFile() throws FileNotFoundException, JSONException {
   1.134 +    Scanner scanner = null;
   1.135 +    try {
   1.136 +      scanner = new Scanner(file, "UTF-8");
   1.137 +      final String contents = scanner.useDelimiter("\\A").next();
   1.138 +      return new JSONObject(contents);
   1.139 +    } finally {
   1.140 +      if (scanner != null) {
   1.141 +        scanner.close();
   1.142 +      }
   1.143 +    }
   1.144 +  }
   1.145 +
   1.146 +  protected void writeToFile(JSONObject object) throws IOException {
   1.147 +    Logger.debug(LOG_TAG, "Writing profile information.");
   1.148 +    Logger.pii(LOG_TAG, "Writing to file: " + file.getAbsolutePath());
   1.149 +    FileOutputStream stream = new FileOutputStream(file);
   1.150 +    OutputStreamWriter writer = new OutputStreamWriter(stream, Charset.forName("UTF-8"));
   1.151 +    try {
   1.152 +      writer.append(object.toString());
   1.153 +      needsWrite = false;
   1.154 +    } finally {
   1.155 +      writer.close();
   1.156 +    }
   1.157 +  }
   1.158 +
   1.159 +  /**
   1.160 +   * Call this <b>on a background thread</b> when you're done adding things.
   1.161 +   * @throws IOException if there was a problem serializing or writing the cache to disk.
   1.162 +   */
   1.163 +  public synchronized void completeInitialization() throws IOException {
   1.164 +    initialized = true;
   1.165 +    if (!needsWrite) {
   1.166 +      Logger.debug(LOG_TAG, "No write needed.");
   1.167 +      return;
   1.168 +    }
   1.169 +
   1.170 +    JSONObject object = toJSON();
   1.171 +    if (object == null) {
   1.172 +      throw new IOException("Couldn't serialize JSON.");
   1.173 +    }
   1.174 +
   1.175 +    writeToFile(object);
   1.176 +  }
   1.177 +
   1.178 +  /**
   1.179 +   * Call this if you're interested in reading.
   1.180 +   *
   1.181 +   * You should be doing so on a background thread.
   1.182 +   *
   1.183 +   * @return true if this object was initialized correctly.
   1.184 +   */
   1.185 +  public synchronized boolean restoreUnlessInitialized() {
   1.186 +    if (initialized) {
   1.187 +      return true;
   1.188 +    }
   1.189 +
   1.190 +    if (!file.exists()) {
   1.191 +      return false;
   1.192 +    }
   1.193 +
   1.194 +    // One-liner for file reading in Java. So sorry.
   1.195 +    Logger.info(LOG_TAG, "Restoring ProfileInformationCache from file.");
   1.196 +    Logger.pii(LOG_TAG, "Restoring from file: " + file.getAbsolutePath());
   1.197 +
   1.198 +    try {
   1.199 +      if (!fromJSON(readFromFile())) {
   1.200 +        // No need to blow away the file; the caller can eventually overwrite it.
   1.201 +        return false;
   1.202 +      }
   1.203 +      initialized = true;
   1.204 +      needsWrite = false;
   1.205 +      return true;
   1.206 +    } catch (FileNotFoundException e) {
   1.207 +      return false;
   1.208 +    } catch (JSONException e) {
   1.209 +      Logger.warn(LOG_TAG, "Malformed ProfileInformationCache. Not restoring.");
   1.210 +      return false;
   1.211 +    }
   1.212 +  }
   1.213 +
   1.214 +  private void ensureInitialized() {
   1.215 +    if (!initialized) {
   1.216 +      throw new IllegalStateException("Not initialized.");
   1.217 +    }
   1.218 +  }
   1.219 +
   1.220 +  @Override
   1.221 +  public boolean isBlocklistEnabled() {
   1.222 +    ensureInitialized();
   1.223 +    return blocklistEnabled;
   1.224 +  }
   1.225 +
   1.226 +  public void setBlocklistEnabled(boolean value) {
   1.227 +    Logger.debug(LOG_TAG, "Setting blocklist enabled: " + value);
   1.228 +    blocklistEnabled = value;
   1.229 +    needsWrite = true;
   1.230 +  }
   1.231 +
   1.232 +  @Override
   1.233 +  public boolean isTelemetryEnabled() {
   1.234 +    ensureInitialized();
   1.235 +    return telemetryEnabled;
   1.236 +  }
   1.237 +
   1.238 +  public void setTelemetryEnabled(boolean value) {
   1.239 +    Logger.debug(LOG_TAG, "Setting telemetry enabled: " + value);
   1.240 +    telemetryEnabled = value;
   1.241 +    needsWrite = true;
   1.242 +  }
   1.243 +
   1.244 +  @Override
   1.245 +  public boolean isAcceptLangUserSet() {
   1.246 +    ensureInitialized();
   1.247 +    return isAcceptLangUserSet;
   1.248 +  }
   1.249 +
   1.250 +  public void setAcceptLangUserSet(boolean value) {
   1.251 +    Logger.debug(LOG_TAG, "Setting accept-lang as user-set: " + value);
   1.252 +    isAcceptLangUserSet = value;
   1.253 +    needsWrite = true;
   1.254 +  }
   1.255 +
   1.256 +  @Override
   1.257 +  public long getProfileCreationTime() {
   1.258 +    ensureInitialized();
   1.259 +    return profileCreationTime;
   1.260 +  }
   1.261 +
   1.262 +  public void setProfileCreationTime(long value) {
   1.263 +    Logger.debug(LOG_TAG, "Setting profile creation time: " + value);
   1.264 +    profileCreationTime = value;
   1.265 +    needsWrite = true;
   1.266 +  }
   1.267 +
   1.268 +  @Override
   1.269 +  public String getDistributionString() {
   1.270 +    ensureInitialized();
   1.271 +    return distribution;
   1.272 +  }
   1.273 +
   1.274 +  /**
   1.275 +   * Ensure that your arguments are non-null.
   1.276 +   */
   1.277 +  public void setDistributionString(String distributionID, String distributionVersion) {
   1.278 +    Logger.debug(LOG_TAG, "Setting distribution: " + distributionID + ", " + distributionVersion);
   1.279 +    distribution = distributionID + ":" + distributionVersion;
   1.280 +    needsWrite = true;
   1.281 +  }
   1.282 +
   1.283 +  @Override
   1.284 +  public String getAppLocale() {
   1.285 +    ensureInitialized();
   1.286 +    return appLocale;
   1.287 +  }
   1.288 +
   1.289 +  public void setAppLocale(String value) {
   1.290 +    if (value.equalsIgnoreCase(appLocale)) {
   1.291 +      return;
   1.292 +    }
   1.293 +    Logger.debug(LOG_TAG, "Setting app locale: " + value);
   1.294 +    appLocale = value.toLowerCase(Locale.US);
   1.295 +    needsWrite = true;
   1.296 +  }
   1.297 +
   1.298 +  @Override
   1.299 +  public String getOSLocale() {
   1.300 +    ensureInitialized();
   1.301 +    return osLocale;
   1.302 +  }
   1.303 +
   1.304 +  public void setOSLocale(String value) {
   1.305 +    if (value.equalsIgnoreCase(osLocale)) {
   1.306 +      return;
   1.307 +    }
   1.308 +    Logger.debug(LOG_TAG, "Setting OS locale: " + value);
   1.309 +    osLocale = value.toLowerCase(Locale.US);
   1.310 +    needsWrite = true;
   1.311 +  }
   1.312 +
   1.313 +  /**
   1.314 +   * Update the PIC, if necessary, to match the current locale environment.
   1.315 +   *
   1.316 +   * @return true if the PIC needed to be updated.
   1.317 +   */
   1.318 +  public boolean updateLocales(String osLocale, String appLocale) {
   1.319 +    if (this.osLocale.equalsIgnoreCase(osLocale) &&
   1.320 +        (appLocale == null || this.appLocale.equalsIgnoreCase(appLocale))) {
   1.321 +      return false;
   1.322 +    }
   1.323 +    this.setOSLocale(osLocale);
   1.324 +    if (appLocale != null) {
   1.325 +      this.setAppLocale(appLocale);
   1.326 +    }
   1.327 +    return true;
   1.328 +  }
   1.329 +
   1.330 +  @Override
   1.331 +  public JSONObject getAddonsJSON() {
   1.332 +    ensureInitialized();
   1.333 +    return addons;
   1.334 +  }
   1.335 +
   1.336 +  public void updateJSONForAddon(String id, String json) throws Exception {
   1.337 +    addons.put(id, new JSONObject(json));
   1.338 +    needsWrite = true;
   1.339 +  }
   1.340 +
   1.341 +  public void removeAddon(String id) {
   1.342 +    if (null != addons.remove(id)) {
   1.343 +      needsWrite = true;
   1.344 +    }
   1.345 +  }
   1.346 +
   1.347 +  /**
   1.348 +   * Will throw if you haven't done a full update at least once.
   1.349 +   */
   1.350 +  public void updateJSONForAddon(String id, JSONObject json) {
   1.351 +    if (addons == null) {
   1.352 +      throw new IllegalStateException("Cannot incrementally update add-ons without first initializing.");
   1.353 +    }
   1.354 +    try {
   1.355 +      addons.put(id, json);
   1.356 +      needsWrite = true;
   1.357 +    } catch (Exception e) {
   1.358 +      // Why would this happen?
   1.359 +      Logger.warn(LOG_TAG, "Unexpected failure updating JSON for add-on.", e);
   1.360 +    }
   1.361 +  }
   1.362 +
   1.363 +  /**
   1.364 +   * Update the cached set of add-ons. Throws on invalid input.
   1.365 +   *
   1.366 +   * @param json a valid add-ons JSON string.
   1.367 +   */
   1.368 +  public void setJSONForAddons(String json) throws Exception {
   1.369 +    addons = new JSONObject(json);
   1.370 +    needsWrite = true;
   1.371 +  }
   1.372 +
   1.373 +  public void setJSONForAddons(JSONObject json) {
   1.374 +    addons = json;
   1.375 +    needsWrite = true;
   1.376 +  }
   1.377 +}

mercurial