Wed, 31 Dec 2014 07:22:50 +0100
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 }