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

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 package org.mozilla.gecko.background.healthreport;
     7 import java.io.File;
     8 import java.io.FileNotFoundException;
     9 import java.io.FileOutputStream;
    10 import java.io.IOException;
    11 import java.io.OutputStreamWriter;
    12 import java.nio.charset.Charset;
    13 import java.util.Locale;
    14 import java.util.Scanner;
    16 import org.json.JSONException;
    17 import org.json.JSONObject;
    18 import org.mozilla.gecko.background.common.log.Logger;
    19 import org.mozilla.gecko.background.healthreport.EnvironmentBuilder.ProfileInformationProvider;
    21 /**
    22  * There are some parts of the FHR environment that can't be readily computed
    23  * without a running Gecko -- add-ons, for example. In order to make this
    24  * information available without launching Gecko, we persist it on Fennec
    25  * startup. This class is the notepad in which we write.
    26  */
    27 public class ProfileInformationCache implements ProfileInformationProvider {
    28   private static final String LOG_TAG = "GeckoProfileInfo";
    29   private static final String CACHE_FILE = "profile_info_cache.json";
    31   /*
    32    * FORMAT_VERSION history:
    33    *   -: No version number; implicit v1.
    34    *   1: Add versioning (Bug 878670).
    35    *   2: Bump to regenerate add-on set after landing Bug 900694 (Bug 901622).
    36    *   3: Add distribution, osLocale, appLocale.
    37    */
    38   public static final int FORMAT_VERSION = 3;
    40   protected boolean initialized = false;
    41   protected boolean needsWrite = false;
    43   protected final File file;
    45   private volatile boolean blocklistEnabled = true;
    46   private volatile boolean telemetryEnabled = false;
    47   private volatile boolean isAcceptLangUserSet = false;
    49   private volatile long profileCreationTime = 0;
    50   private volatile String distribution = "";
    52   // There are really four kinds of locale in play:
    53   //
    54   // * The OS
    55   // * The Android environment of the app (setDefault)
    56   // * The Gecko locale
    57   // * The requested content locale (Accept-Language).
    58   //
    59   // We track only the first two, assuming that the Gecko locale will typically
    60   // be the same as the app locale.
    61   //
    62   // The app locale is fetched from the PIC because it can be modified at
    63   // runtime -- it won't necessarily be what Locale.getDefaultLocale() returns
    64   // in a fresh non-browser profile.
    65   //
    66   // We also track the OS locale here for the same reason -- we need to store
    67   // the default (OS) value before the locale-switching code takes effect!
    68   private volatile String osLocale = "";
    69   private volatile String appLocale = "";
    71   private volatile JSONObject addons = null;
    73   public ProfileInformationCache(String profilePath) {
    74     file = new File(profilePath + File.separator + CACHE_FILE);
    75     Logger.pii(LOG_TAG, "Using " + file.getAbsolutePath() + " for profile information cache.");
    76   }
    78   public synchronized void beginInitialization() {
    79     initialized = false;
    80     needsWrite = true;
    81   }
    83   public JSONObject toJSON() {
    84     JSONObject object = new JSONObject();
    85     try {
    86       object.put("version", FORMAT_VERSION);
    87       object.put("blocklist", blocklistEnabled);
    88       object.put("telemetry", telemetryEnabled);
    89       object.put("isAcceptLangUserSet", isAcceptLangUserSet);
    90       object.put("profileCreated", profileCreationTime);
    91       object.put("osLocale", osLocale);
    92       object.put("appLocale", appLocale);
    93       object.put("distribution", distribution);
    94       object.put("addons", addons);
    95     } catch (JSONException e) {
    96       // There isn't much we can do about this.
    97       // Let's just quietly muffle.
    98       return null;
    99     }
   100     return object;
   101   }
   103   /**
   104    * Attempt to restore this object from a JSON blob. If there is a version mismatch, there has
   105    * likely been an upgrade to the cache format. The cache can be reconstructed without data loss
   106    * so rather than migrating, we invalidate the cache by refusing to store the given JSONObject
   107    * and returning false.
   108    *
   109    * @return false if there's a version mismatch or an error, true on success.
   110    */
   111   private boolean fromJSON(JSONObject object) throws JSONException {
   112     int version = object.optInt("version", 1);
   113     switch (version) {
   114     case FORMAT_VERSION:
   115       blocklistEnabled = object.getBoolean("blocklist");
   116       telemetryEnabled = object.getBoolean("telemetry");
   117       isAcceptLangUserSet = object.getBoolean("isAcceptLangUserSet");
   118       profileCreationTime = object.getLong("profileCreated");
   119       addons = object.getJSONObject("addons");
   120       distribution = object.getString("distribution");
   121       osLocale = object.getString("osLocale");
   122       appLocale = object.getString("appLocale");
   123       return true;
   124     default:
   125       Logger.warn(LOG_TAG, "Unable to restore from version " + version + " PIC file: expecting " + FORMAT_VERSION);
   126       return false;
   127     }
   128   }
   130   protected JSONObject readFromFile() throws FileNotFoundException, JSONException {
   131     Scanner scanner = null;
   132     try {
   133       scanner = new Scanner(file, "UTF-8");
   134       final String contents = scanner.useDelimiter("\\A").next();
   135       return new JSONObject(contents);
   136     } finally {
   137       if (scanner != null) {
   138         scanner.close();
   139       }
   140     }
   141   }
   143   protected void writeToFile(JSONObject object) throws IOException {
   144     Logger.debug(LOG_TAG, "Writing profile information.");
   145     Logger.pii(LOG_TAG, "Writing to file: " + file.getAbsolutePath());
   146     FileOutputStream stream = new FileOutputStream(file);
   147     OutputStreamWriter writer = new OutputStreamWriter(stream, Charset.forName("UTF-8"));
   148     try {
   149       writer.append(object.toString());
   150       needsWrite = false;
   151     } finally {
   152       writer.close();
   153     }
   154   }
   156   /**
   157    * Call this <b>on a background thread</b> when you're done adding things.
   158    * @throws IOException if there was a problem serializing or writing the cache to disk.
   159    */
   160   public synchronized void completeInitialization() throws IOException {
   161     initialized = true;
   162     if (!needsWrite) {
   163       Logger.debug(LOG_TAG, "No write needed.");
   164       return;
   165     }
   167     JSONObject object = toJSON();
   168     if (object == null) {
   169       throw new IOException("Couldn't serialize JSON.");
   170     }
   172     writeToFile(object);
   173   }
   175   /**
   176    * Call this if you're interested in reading.
   177    *
   178    * You should be doing so on a background thread.
   179    *
   180    * @return true if this object was initialized correctly.
   181    */
   182   public synchronized boolean restoreUnlessInitialized() {
   183     if (initialized) {
   184       return true;
   185     }
   187     if (!file.exists()) {
   188       return false;
   189     }
   191     // One-liner for file reading in Java. So sorry.
   192     Logger.info(LOG_TAG, "Restoring ProfileInformationCache from file.");
   193     Logger.pii(LOG_TAG, "Restoring from file: " + file.getAbsolutePath());
   195     try {
   196       if (!fromJSON(readFromFile())) {
   197         // No need to blow away the file; the caller can eventually overwrite it.
   198         return false;
   199       }
   200       initialized = true;
   201       needsWrite = false;
   202       return true;
   203     } catch (FileNotFoundException e) {
   204       return false;
   205     } catch (JSONException e) {
   206       Logger.warn(LOG_TAG, "Malformed ProfileInformationCache. Not restoring.");
   207       return false;
   208     }
   209   }
   211   private void ensureInitialized() {
   212     if (!initialized) {
   213       throw new IllegalStateException("Not initialized.");
   214     }
   215   }
   217   @Override
   218   public boolean isBlocklistEnabled() {
   219     ensureInitialized();
   220     return blocklistEnabled;
   221   }
   223   public void setBlocklistEnabled(boolean value) {
   224     Logger.debug(LOG_TAG, "Setting blocklist enabled: " + value);
   225     blocklistEnabled = value;
   226     needsWrite = true;
   227   }
   229   @Override
   230   public boolean isTelemetryEnabled() {
   231     ensureInitialized();
   232     return telemetryEnabled;
   233   }
   235   public void setTelemetryEnabled(boolean value) {
   236     Logger.debug(LOG_TAG, "Setting telemetry enabled: " + value);
   237     telemetryEnabled = value;
   238     needsWrite = true;
   239   }
   241   @Override
   242   public boolean isAcceptLangUserSet() {
   243     ensureInitialized();
   244     return isAcceptLangUserSet;
   245   }
   247   public void setAcceptLangUserSet(boolean value) {
   248     Logger.debug(LOG_TAG, "Setting accept-lang as user-set: " + value);
   249     isAcceptLangUserSet = value;
   250     needsWrite = true;
   251   }
   253   @Override
   254   public long getProfileCreationTime() {
   255     ensureInitialized();
   256     return profileCreationTime;
   257   }
   259   public void setProfileCreationTime(long value) {
   260     Logger.debug(LOG_TAG, "Setting profile creation time: " + value);
   261     profileCreationTime = value;
   262     needsWrite = true;
   263   }
   265   @Override
   266   public String getDistributionString() {
   267     ensureInitialized();
   268     return distribution;
   269   }
   271   /**
   272    * Ensure that your arguments are non-null.
   273    */
   274   public void setDistributionString(String distributionID, String distributionVersion) {
   275     Logger.debug(LOG_TAG, "Setting distribution: " + distributionID + ", " + distributionVersion);
   276     distribution = distributionID + ":" + distributionVersion;
   277     needsWrite = true;
   278   }
   280   @Override
   281   public String getAppLocale() {
   282     ensureInitialized();
   283     return appLocale;
   284   }
   286   public void setAppLocale(String value) {
   287     if (value.equalsIgnoreCase(appLocale)) {
   288       return;
   289     }
   290     Logger.debug(LOG_TAG, "Setting app locale: " + value);
   291     appLocale = value.toLowerCase(Locale.US);
   292     needsWrite = true;
   293   }
   295   @Override
   296   public String getOSLocale() {
   297     ensureInitialized();
   298     return osLocale;
   299   }
   301   public void setOSLocale(String value) {
   302     if (value.equalsIgnoreCase(osLocale)) {
   303       return;
   304     }
   305     Logger.debug(LOG_TAG, "Setting OS locale: " + value);
   306     osLocale = value.toLowerCase(Locale.US);
   307     needsWrite = true;
   308   }
   310   /**
   311    * Update the PIC, if necessary, to match the current locale environment.
   312    *
   313    * @return true if the PIC needed to be updated.
   314    */
   315   public boolean updateLocales(String osLocale, String appLocale) {
   316     if (this.osLocale.equalsIgnoreCase(osLocale) &&
   317         (appLocale == null || this.appLocale.equalsIgnoreCase(appLocale))) {
   318       return false;
   319     }
   320     this.setOSLocale(osLocale);
   321     if (appLocale != null) {
   322       this.setAppLocale(appLocale);
   323     }
   324     return true;
   325   }
   327   @Override
   328   public JSONObject getAddonsJSON() {
   329     ensureInitialized();
   330     return addons;
   331   }
   333   public void updateJSONForAddon(String id, String json) throws Exception {
   334     addons.put(id, new JSONObject(json));
   335     needsWrite = true;
   336   }
   338   public void removeAddon(String id) {
   339     if (null != addons.remove(id)) {
   340       needsWrite = true;
   341     }
   342   }
   344   /**
   345    * Will throw if you haven't done a full update at least once.
   346    */
   347   public void updateJSONForAddon(String id, JSONObject json) {
   348     if (addons == null) {
   349       throw new IllegalStateException("Cannot incrementally update add-ons without first initializing.");
   350     }
   351     try {
   352       addons.put(id, json);
   353       needsWrite = true;
   354     } catch (Exception e) {
   355       // Why would this happen?
   356       Logger.warn(LOG_TAG, "Unexpected failure updating JSON for add-on.", e);
   357     }
   358   }
   360   /**
   361    * Update the cached set of add-ons. Throws on invalid input.
   362    *
   363    * @param json a valid add-ons JSON string.
   364    */
   365   public void setJSONForAddons(String json) throws Exception {
   366     addons = new JSONObject(json);
   367     needsWrite = true;
   368   }
   370   public void setJSONForAddons(JSONObject json) {
   371     addons = json;
   372     needsWrite = true;
   373   }
   374 }

mercurial