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.

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

mercurial