Wed, 31 Dec 2014 07:22:50 +0100
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.util.HashSet; |
michael@0 | 8 | import java.util.Iterator; |
michael@0 | 9 | import java.util.Set; |
michael@0 | 10 | |
michael@0 | 11 | import org.json.JSONException; |
michael@0 | 12 | import org.json.JSONObject; |
michael@0 | 13 | import org.mozilla.gecko.background.common.DateUtils.DateFormatter; |
michael@0 | 14 | import org.mozilla.gecko.background.common.log.Logger; |
michael@0 | 15 | import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field; |
michael@0 | 16 | |
michael@0 | 17 | import android.database.Cursor; |
michael@0 | 18 | import android.util.SparseArray; |
michael@0 | 19 | |
michael@0 | 20 | public class HealthReportGenerator { |
michael@0 | 21 | private static final int PAYLOAD_VERSION = 3; |
michael@0 | 22 | |
michael@0 | 23 | private static final String LOG_TAG = "GeckoHealthGen"; |
michael@0 | 24 | |
michael@0 | 25 | private final HealthReportStorage storage; |
michael@0 | 26 | private final DateFormatter dateFormatter; |
michael@0 | 27 | |
michael@0 | 28 | public HealthReportGenerator(HealthReportStorage storage) { |
michael@0 | 29 | this.storage = storage; |
michael@0 | 30 | this.dateFormatter = new DateFormatter(); |
michael@0 | 31 | } |
michael@0 | 32 | |
michael@0 | 33 | @SuppressWarnings("static-method") |
michael@0 | 34 | protected long now() { |
michael@0 | 35 | return System.currentTimeMillis(); |
michael@0 | 36 | } |
michael@0 | 37 | |
michael@0 | 38 | /** |
michael@0 | 39 | * Ensure that you have initialized the Locale to your satisfaction |
michael@0 | 40 | * prior to calling this method. |
michael@0 | 41 | * |
michael@0 | 42 | * @return null if no environment could be computed, or else the resulting document. |
michael@0 | 43 | * @throws JSONException if there was an error adding environment data to the resulting document. |
michael@0 | 44 | */ |
michael@0 | 45 | public JSONObject generateDocument(long since, long lastPingTime, String profilePath) throws JSONException { |
michael@0 | 46 | Logger.info(LOG_TAG, "Generating FHR document from " + since + "; last ping " + lastPingTime); |
michael@0 | 47 | Logger.pii(LOG_TAG, "Generating for profile " + profilePath); |
michael@0 | 48 | |
michael@0 | 49 | ProfileInformationCache cache = new ProfileInformationCache(profilePath); |
michael@0 | 50 | if (!cache.restoreUnlessInitialized()) { |
michael@0 | 51 | Logger.warn(LOG_TAG, "Not enough profile information to compute current environment."); |
michael@0 | 52 | return null; |
michael@0 | 53 | } |
michael@0 | 54 | Environment current = EnvironmentBuilder.getCurrentEnvironment(cache); |
michael@0 | 55 | return generateDocument(since, lastPingTime, current); |
michael@0 | 56 | } |
michael@0 | 57 | |
michael@0 | 58 | /** |
michael@0 | 59 | * The document consists of: |
michael@0 | 60 | * |
michael@0 | 61 | *<ul> |
michael@0 | 62 | *<li>Basic metadata: last ping time, current ping time, version.</li> |
michael@0 | 63 | *<li>A map of environments: <code>current</code> and others named by hash. <code>current</code> is fully specified, |
michael@0 | 64 | * and others are deltas from current.</li> |
michael@0 | 65 | *<li>A <code>data</code> object. This includes <code>last</code> and <code>days</code>.</li> |
michael@0 | 66 | *</ul> |
michael@0 | 67 | * |
michael@0 | 68 | * <code>days</code> is a map from date strings to <tt>{hash: {measurement: {_v: version, fields...}}}</tt>. |
michael@0 | 69 | * @throws JSONException if there was an error adding environment data to the resulting document. |
michael@0 | 70 | */ |
michael@0 | 71 | public JSONObject generateDocument(long since, long lastPingTime, Environment currentEnvironment) throws JSONException { |
michael@0 | 72 | final String currentHash = currentEnvironment.getHash(); |
michael@0 | 73 | |
michael@0 | 74 | Logger.debug(LOG_TAG, "Current environment hash: " + currentHash); |
michael@0 | 75 | if (currentHash == null) { |
michael@0 | 76 | Logger.warn(LOG_TAG, "Current hash is null; aborting."); |
michael@0 | 77 | return null; |
michael@0 | 78 | } |
michael@0 | 79 | |
michael@0 | 80 | // We want to map field IDs to some strings as we go. |
michael@0 | 81 | SparseArray<Environment> envs = storage.getEnvironmentRecordsByID(); |
michael@0 | 82 | |
michael@0 | 83 | JSONObject document = new JSONObject(); |
michael@0 | 84 | |
michael@0 | 85 | if (lastPingTime >= HealthReportConstants.EARLIEST_LAST_PING) { |
michael@0 | 86 | document.put("lastPingDate", dateFormatter.getDateString(lastPingTime)); |
michael@0 | 87 | } |
michael@0 | 88 | |
michael@0 | 89 | document.put("thisPingDate", dateFormatter.getDateString(now())); |
michael@0 | 90 | document.put("version", PAYLOAD_VERSION); |
michael@0 | 91 | |
michael@0 | 92 | document.put("environments", getEnvironmentsJSON(currentEnvironment, envs)); |
michael@0 | 93 | document.put("data", getDataJSON(currentEnvironment, envs, since)); |
michael@0 | 94 | |
michael@0 | 95 | return document; |
michael@0 | 96 | } |
michael@0 | 97 | |
michael@0 | 98 | protected JSONObject getDataJSON(Environment currentEnvironment, |
michael@0 | 99 | SparseArray<Environment> envs, long since) throws JSONException { |
michael@0 | 100 | SparseArray<Field> fields = storage.getFieldsByID(); |
michael@0 | 101 | |
michael@0 | 102 | JSONObject days = getDaysJSON(currentEnvironment, envs, fields, since); |
michael@0 | 103 | |
michael@0 | 104 | JSONObject last = new JSONObject(); |
michael@0 | 105 | |
michael@0 | 106 | JSONObject data = new JSONObject(); |
michael@0 | 107 | data.put("days", days); |
michael@0 | 108 | data.put("last", last); |
michael@0 | 109 | return data; |
michael@0 | 110 | } |
michael@0 | 111 | |
michael@0 | 112 | protected JSONObject getDaysJSON(Environment currentEnvironment, SparseArray<Environment> envs, SparseArray<Field> fields, long since) throws JSONException { |
michael@0 | 113 | if (Logger.shouldLogVerbose(LOG_TAG)) { |
michael@0 | 114 | for (int i = 0; i < envs.size(); ++i) { |
michael@0 | 115 | Logger.trace(LOG_TAG, "Days environment " + envs.keyAt(i) + ": " + envs.get(envs.keyAt(i)).getHash()); |
michael@0 | 116 | } |
michael@0 | 117 | } |
michael@0 | 118 | |
michael@0 | 119 | JSONObject days = new JSONObject(); |
michael@0 | 120 | Cursor cursor = storage.getRawEventsSince(since); |
michael@0 | 121 | try { |
michael@0 | 122 | if (!cursor.moveToFirst()) { |
michael@0 | 123 | return days; |
michael@0 | 124 | } |
michael@0 | 125 | |
michael@0 | 126 | // A classic walking partition. |
michael@0 | 127 | // Columns are "date", "env", "field", "value". |
michael@0 | 128 | // Note that we care about the type (integer, string) and kind |
michael@0 | 129 | // (last/counter, discrete) of each field. |
michael@0 | 130 | // Each field will be accessed once for each date/env pair, so |
michael@0 | 131 | // Field memoizes these facts. |
michael@0 | 132 | // We also care about which measurement contains each field. |
michael@0 | 133 | int lastDate = -1; |
michael@0 | 134 | int lastEnv = -1; |
michael@0 | 135 | JSONObject dateObject = null; |
michael@0 | 136 | JSONObject envObject = null; |
michael@0 | 137 | |
michael@0 | 138 | while (!cursor.isAfterLast()) { |
michael@0 | 139 | int cEnv = cursor.getInt(1); |
michael@0 | 140 | if (cEnv == -1 || |
michael@0 | 141 | (cEnv != lastEnv && |
michael@0 | 142 | envs.indexOfKey(cEnv) < 0)) { |
michael@0 | 143 | Logger.warn(LOG_TAG, "Invalid environment " + cEnv + " in cursor. Skipping."); |
michael@0 | 144 | cursor.moveToNext(); |
michael@0 | 145 | continue; |
michael@0 | 146 | } |
michael@0 | 147 | |
michael@0 | 148 | int cDate = cursor.getInt(0); |
michael@0 | 149 | int cField = cursor.getInt(2); |
michael@0 | 150 | |
michael@0 | 151 | Logger.trace(LOG_TAG, "Event row: " + cDate + ", " + cEnv + ", " + cField); |
michael@0 | 152 | boolean dateChanged = cDate != lastDate; |
michael@0 | 153 | boolean envChanged = cEnv != lastEnv; |
michael@0 | 154 | |
michael@0 | 155 | if (dateChanged) { |
michael@0 | 156 | if (dateObject != null) { |
michael@0 | 157 | days.put(dateFormatter.getDateStringForDay(lastDate), dateObject); |
michael@0 | 158 | } |
michael@0 | 159 | dateObject = new JSONObject(); |
michael@0 | 160 | lastDate = cDate; |
michael@0 | 161 | } |
michael@0 | 162 | |
michael@0 | 163 | if (dateChanged || envChanged) { |
michael@0 | 164 | envObject = new JSONObject(); |
michael@0 | 165 | // This is safe because we checked above that cEnv is valid. |
michael@0 | 166 | dateObject.put(envs.get(cEnv).getHash(), envObject); |
michael@0 | 167 | lastEnv = cEnv; |
michael@0 | 168 | } |
michael@0 | 169 | |
michael@0 | 170 | final Field field = fields.get(cField); |
michael@0 | 171 | JSONObject measurement = envObject.optJSONObject(field.measurementName); |
michael@0 | 172 | if (measurement == null) { |
michael@0 | 173 | // We will never have more than one measurement version within a |
michael@0 | 174 | // single environment -- to do so involves changing the build ID. And |
michael@0 | 175 | // even if we did, we have no way to represent it. So just build the |
michael@0 | 176 | // output object once. |
michael@0 | 177 | measurement = new JSONObject(); |
michael@0 | 178 | measurement.put("_v", field.measurementVersion); |
michael@0 | 179 | envObject.put(field.measurementName, measurement); |
michael@0 | 180 | } |
michael@0 | 181 | |
michael@0 | 182 | // How we record depends on the type of the field, so we |
michael@0 | 183 | // break this out into a separate method for clarity. |
michael@0 | 184 | recordMeasurementFromCursor(field, measurement, cursor); |
michael@0 | 185 | |
michael@0 | 186 | cursor.moveToNext(); |
michael@0 | 187 | continue; |
michael@0 | 188 | } |
michael@0 | 189 | days.put(dateFormatter.getDateStringForDay(lastDate), dateObject); |
michael@0 | 190 | } finally { |
michael@0 | 191 | cursor.close(); |
michael@0 | 192 | } |
michael@0 | 193 | return days; |
michael@0 | 194 | } |
michael@0 | 195 | |
michael@0 | 196 | /** |
michael@0 | 197 | * Return the {@link JSONObject} parsed from the provided index of the given |
michael@0 | 198 | * cursor, or {@link JSONObject#NULL} if either SQL <code>NULL</code> or |
michael@0 | 199 | * string <code>"null"</code> is present at that index. |
michael@0 | 200 | */ |
michael@0 | 201 | private static Object getJSONAtIndex(Cursor cursor, int index) throws JSONException { |
michael@0 | 202 | if (cursor.isNull(index)) { |
michael@0 | 203 | return JSONObject.NULL; |
michael@0 | 204 | } |
michael@0 | 205 | final String value = cursor.getString(index); |
michael@0 | 206 | if ("null".equals(value)) { |
michael@0 | 207 | return JSONObject.NULL; |
michael@0 | 208 | } |
michael@0 | 209 | return new JSONObject(value); |
michael@0 | 210 | } |
michael@0 | 211 | |
michael@0 | 212 | protected static void recordMeasurementFromCursor(final Field field, |
michael@0 | 213 | JSONObject measurement, |
michael@0 | 214 | Cursor cursor) |
michael@0 | 215 | throws JSONException { |
michael@0 | 216 | if (field.isDiscreteField()) { |
michael@0 | 217 | // Discrete counted. Increment the named counter. |
michael@0 | 218 | if (field.isCountedField()) { |
michael@0 | 219 | if (!field.isStringField()) { |
michael@0 | 220 | throw new IllegalStateException("Unable to handle non-string counted types."); |
michael@0 | 221 | } |
michael@0 | 222 | HealthReportUtils.count(measurement, field.fieldName, cursor.getString(3)); |
michael@0 | 223 | return; |
michael@0 | 224 | } |
michael@0 | 225 | |
michael@0 | 226 | // Discrete string or integer. Append it. |
michael@0 | 227 | if (field.isStringField()) { |
michael@0 | 228 | HealthReportUtils.append(measurement, field.fieldName, cursor.getString(3)); |
michael@0 | 229 | return; |
michael@0 | 230 | } |
michael@0 | 231 | if (field.isJSONField()) { |
michael@0 | 232 | HealthReportUtils.append(measurement, field.fieldName, getJSONAtIndex(cursor, 3)); |
michael@0 | 233 | return; |
michael@0 | 234 | } |
michael@0 | 235 | if (field.isIntegerField()) { |
michael@0 | 236 | HealthReportUtils.append(measurement, field.fieldName, cursor.getLong(3)); |
michael@0 | 237 | return; |
michael@0 | 238 | } |
michael@0 | 239 | throw new IllegalStateException("Unknown field type: " + field.flags); |
michael@0 | 240 | } |
michael@0 | 241 | |
michael@0 | 242 | // Non-discrete -- must be LAST or COUNTER, so just accumulate the value. |
michael@0 | 243 | if (field.isStringField()) { |
michael@0 | 244 | measurement.put(field.fieldName, cursor.getString(3)); |
michael@0 | 245 | return; |
michael@0 | 246 | } |
michael@0 | 247 | if (field.isJSONField()) { |
michael@0 | 248 | measurement.put(field.fieldName, getJSONAtIndex(cursor, 3)); |
michael@0 | 249 | return; |
michael@0 | 250 | } |
michael@0 | 251 | measurement.put(field.fieldName, cursor.getLong(3)); |
michael@0 | 252 | } |
michael@0 | 253 | |
michael@0 | 254 | public static JSONObject getEnvironmentsJSON(Environment currentEnvironment, |
michael@0 | 255 | SparseArray<Environment> envs) throws JSONException { |
michael@0 | 256 | JSONObject environments = new JSONObject(); |
michael@0 | 257 | |
michael@0 | 258 | // Always do this, even if it hasn't recorded anything in the DB. |
michael@0 | 259 | environments.put("current", jsonify(currentEnvironment, null)); |
michael@0 | 260 | |
michael@0 | 261 | String currentHash = currentEnvironment.getHash(); |
michael@0 | 262 | for (int i = 0; i < envs.size(); i++) { |
michael@0 | 263 | Environment e = envs.valueAt(i); |
michael@0 | 264 | if (currentHash.equals(e.getHash())) { |
michael@0 | 265 | continue; |
michael@0 | 266 | } |
michael@0 | 267 | environments.put(e.getHash(), jsonify(e, currentEnvironment)); |
michael@0 | 268 | } |
michael@0 | 269 | return environments; |
michael@0 | 270 | } |
michael@0 | 271 | |
michael@0 | 272 | public static JSONObject jsonify(Environment e, Environment current) throws JSONException { |
michael@0 | 273 | JSONObject age = getProfileAge(e, current); |
michael@0 | 274 | JSONObject sysinfo = getSysInfo(e, current); |
michael@0 | 275 | JSONObject gecko = getGeckoInfo(e, current); |
michael@0 | 276 | JSONObject appinfo = getAppInfo(e, current); |
michael@0 | 277 | JSONObject counts = getAddonCounts(e, current); |
michael@0 | 278 | |
michael@0 | 279 | JSONObject out = new JSONObject(); |
michael@0 | 280 | if (age != null) |
michael@0 | 281 | out.put("org.mozilla.profile.age", age); |
michael@0 | 282 | if (sysinfo != null) |
michael@0 | 283 | out.put("org.mozilla.sysinfo.sysinfo", sysinfo); |
michael@0 | 284 | if (gecko != null) |
michael@0 | 285 | out.put("geckoAppInfo", gecko); |
michael@0 | 286 | if (appinfo != null) |
michael@0 | 287 | out.put("org.mozilla.appInfo.appinfo", appinfo); |
michael@0 | 288 | if (counts != null) |
michael@0 | 289 | out.put("org.mozilla.addons.counts", counts); |
michael@0 | 290 | |
michael@0 | 291 | JSONObject active = getActiveAddons(e, current); |
michael@0 | 292 | if (active != null) |
michael@0 | 293 | out.put("org.mozilla.addons.active", active); |
michael@0 | 294 | |
michael@0 | 295 | if (current == null) { |
michael@0 | 296 | out.put("hash", e.getHash()); |
michael@0 | 297 | } |
michael@0 | 298 | return out; |
michael@0 | 299 | } |
michael@0 | 300 | |
michael@0 | 301 | private static JSONObject getProfileAge(Environment e, Environment current) throws JSONException { |
michael@0 | 302 | JSONObject age = new JSONObject(); |
michael@0 | 303 | int changes = 0; |
michael@0 | 304 | if (current == null || current.profileCreation != e.profileCreation) { |
michael@0 | 305 | age.put("profileCreation", e.profileCreation); |
michael@0 | 306 | changes++; |
michael@0 | 307 | } |
michael@0 | 308 | if (current != null && changes == 0) { |
michael@0 | 309 | return null; |
michael@0 | 310 | } |
michael@0 | 311 | age.put("_v", 1); |
michael@0 | 312 | return age; |
michael@0 | 313 | } |
michael@0 | 314 | |
michael@0 | 315 | private static JSONObject getSysInfo(Environment e, Environment current) throws JSONException { |
michael@0 | 316 | JSONObject sysinfo = new JSONObject(); |
michael@0 | 317 | int changes = 0; |
michael@0 | 318 | if (current == null || current.cpuCount != e.cpuCount) { |
michael@0 | 319 | sysinfo.put("cpuCount", e.cpuCount); |
michael@0 | 320 | changes++; |
michael@0 | 321 | } |
michael@0 | 322 | if (current == null || current.memoryMB != e.memoryMB) { |
michael@0 | 323 | sysinfo.put("memoryMB", e.memoryMB); |
michael@0 | 324 | changes++; |
michael@0 | 325 | } |
michael@0 | 326 | if (current == null || !current.architecture.equals(e.architecture)) { |
michael@0 | 327 | sysinfo.put("architecture", e.architecture); |
michael@0 | 328 | changes++; |
michael@0 | 329 | } |
michael@0 | 330 | if (current == null || !current.sysName.equals(e.sysName)) { |
michael@0 | 331 | sysinfo.put("name", e.sysName); |
michael@0 | 332 | changes++; |
michael@0 | 333 | } |
michael@0 | 334 | if (current == null || !current.sysVersion.equals(e.sysVersion)) { |
michael@0 | 335 | sysinfo.put("version", e.sysVersion); |
michael@0 | 336 | changes++; |
michael@0 | 337 | } |
michael@0 | 338 | if (current != null && changes == 0) { |
michael@0 | 339 | return null; |
michael@0 | 340 | } |
michael@0 | 341 | sysinfo.put("_v", 1); |
michael@0 | 342 | return sysinfo; |
michael@0 | 343 | } |
michael@0 | 344 | |
michael@0 | 345 | private static JSONObject getGeckoInfo(Environment e, Environment current) throws JSONException { |
michael@0 | 346 | JSONObject gecko = new JSONObject(); |
michael@0 | 347 | int changes = 0; |
michael@0 | 348 | if (current == null || !current.vendor.equals(e.vendor)) { |
michael@0 | 349 | gecko.put("vendor", e.vendor); |
michael@0 | 350 | changes++; |
michael@0 | 351 | } |
michael@0 | 352 | if (current == null || !current.appName.equals(e.appName)) { |
michael@0 | 353 | gecko.put("name", e.appName); |
michael@0 | 354 | changes++; |
michael@0 | 355 | } |
michael@0 | 356 | if (current == null || !current.appID.equals(e.appID)) { |
michael@0 | 357 | gecko.put("id", e.appID); |
michael@0 | 358 | changes++; |
michael@0 | 359 | } |
michael@0 | 360 | if (current == null || !current.appVersion.equals(e.appVersion)) { |
michael@0 | 361 | gecko.put("version", e.appVersion); |
michael@0 | 362 | changes++; |
michael@0 | 363 | } |
michael@0 | 364 | if (current == null || !current.appBuildID.equals(e.appBuildID)) { |
michael@0 | 365 | gecko.put("appBuildID", e.appBuildID); |
michael@0 | 366 | changes++; |
michael@0 | 367 | } |
michael@0 | 368 | if (current == null || !current.platformVersion.equals(e.platformVersion)) { |
michael@0 | 369 | gecko.put("platformVersion", e.platformVersion); |
michael@0 | 370 | changes++; |
michael@0 | 371 | } |
michael@0 | 372 | if (current == null || !current.platformBuildID.equals(e.platformBuildID)) { |
michael@0 | 373 | gecko.put("platformBuildID", e.platformBuildID); |
michael@0 | 374 | changes++; |
michael@0 | 375 | } |
michael@0 | 376 | if (current == null || !current.os.equals(e.os)) { |
michael@0 | 377 | gecko.put("os", e.os); |
michael@0 | 378 | changes++; |
michael@0 | 379 | } |
michael@0 | 380 | if (current == null || !current.xpcomabi.equals(e.xpcomabi)) { |
michael@0 | 381 | gecko.put("xpcomabi", e.xpcomabi); |
michael@0 | 382 | changes++; |
michael@0 | 383 | } |
michael@0 | 384 | if (current == null || !current.updateChannel.equals(e.updateChannel)) { |
michael@0 | 385 | gecko.put("updateChannel", e.updateChannel); |
michael@0 | 386 | changes++; |
michael@0 | 387 | } |
michael@0 | 388 | if (current != null && changes == 0) { |
michael@0 | 389 | return null; |
michael@0 | 390 | } |
michael@0 | 391 | gecko.put("_v", 1); |
michael@0 | 392 | return gecko; |
michael@0 | 393 | } |
michael@0 | 394 | |
michael@0 | 395 | // Null-safe string comparison. |
michael@0 | 396 | private static boolean stringsDiffer(final String a, final String b) { |
michael@0 | 397 | if (a == null) { |
michael@0 | 398 | return b != null; |
michael@0 | 399 | } |
michael@0 | 400 | return !a.equals(b); |
michael@0 | 401 | } |
michael@0 | 402 | |
michael@0 | 403 | private static JSONObject getAppInfo(Environment e, Environment current) throws JSONException { |
michael@0 | 404 | JSONObject appinfo = new JSONObject(); |
michael@0 | 405 | |
michael@0 | 406 | Logger.debug(LOG_TAG, "Generating appinfo for v" + e.version + " env " + e.hash); |
michael@0 | 407 | |
michael@0 | 408 | // Is the environment in question newer than the diff target, or is |
michael@0 | 409 | // there no diff target? |
michael@0 | 410 | final boolean outdated = current == null || |
michael@0 | 411 | e.version > current.version; |
michael@0 | 412 | |
michael@0 | 413 | // Is the environment in question a different version (lower or higher), |
michael@0 | 414 | // or is there no diff target? |
michael@0 | 415 | final boolean differ = outdated || current.version > e.version; |
michael@0 | 416 | |
michael@0 | 417 | // Always produce an output object if there's a version mismatch or this |
michael@0 | 418 | // isn't a diff. Otherwise, track as we go if there's any difference. |
michael@0 | 419 | boolean changed = differ; |
michael@0 | 420 | |
michael@0 | 421 | switch (e.version) { |
michael@0 | 422 | // There's a straightforward correspondence between environment versions |
michael@0 | 423 | // and appinfo versions. |
michael@0 | 424 | case 2: |
michael@0 | 425 | appinfo.put("_v", 3); |
michael@0 | 426 | break; |
michael@0 | 427 | case 1: |
michael@0 | 428 | appinfo.put("_v", 2); |
michael@0 | 429 | break; |
michael@0 | 430 | default: |
michael@0 | 431 | Logger.warn(LOG_TAG, "Unknown environment version: " + e.version); |
michael@0 | 432 | return appinfo; |
michael@0 | 433 | } |
michael@0 | 434 | |
michael@0 | 435 | switch (e.version) { |
michael@0 | 436 | case 2: |
michael@0 | 437 | if (populateAppInfoV2(appinfo, e, current, outdated)) { |
michael@0 | 438 | changed = true; |
michael@0 | 439 | } |
michael@0 | 440 | // Fall through. |
michael@0 | 441 | |
michael@0 | 442 | case 1: |
michael@0 | 443 | // There is no older version than v1, so don't check outdated. |
michael@0 | 444 | if (populateAppInfoV1(e, current, appinfo)) { |
michael@0 | 445 | changed = true; |
michael@0 | 446 | } |
michael@0 | 447 | } |
michael@0 | 448 | |
michael@0 | 449 | if (!changed) { |
michael@0 | 450 | return null; |
michael@0 | 451 | } |
michael@0 | 452 | |
michael@0 | 453 | return appinfo; |
michael@0 | 454 | } |
michael@0 | 455 | |
michael@0 | 456 | private static boolean populateAppInfoV1(Environment e, |
michael@0 | 457 | Environment current, |
michael@0 | 458 | JSONObject appinfo) |
michael@0 | 459 | throws JSONException { |
michael@0 | 460 | boolean changes = false; |
michael@0 | 461 | if (current == null || current.isBlocklistEnabled != e.isBlocklistEnabled) { |
michael@0 | 462 | appinfo.put("isBlocklistEnabled", e.isBlocklistEnabled); |
michael@0 | 463 | changes = true; |
michael@0 | 464 | } |
michael@0 | 465 | |
michael@0 | 466 | if (current == null || current.isTelemetryEnabled != e.isTelemetryEnabled) { |
michael@0 | 467 | appinfo.put("isTelemetryEnabled", e.isTelemetryEnabled); |
michael@0 | 468 | changes = true; |
michael@0 | 469 | } |
michael@0 | 470 | |
michael@0 | 471 | return changes; |
michael@0 | 472 | } |
michael@0 | 473 | |
michael@0 | 474 | private static boolean populateAppInfoV2(JSONObject appinfo, |
michael@0 | 475 | Environment e, |
michael@0 | 476 | Environment current, |
michael@0 | 477 | final boolean outdated) |
michael@0 | 478 | throws JSONException { |
michael@0 | 479 | boolean changes = false; |
michael@0 | 480 | if (outdated || |
michael@0 | 481 | stringsDiffer(current.osLocale, e.osLocale)) { |
michael@0 | 482 | appinfo.put("osLocale", e.osLocale); |
michael@0 | 483 | changes = true; |
michael@0 | 484 | } |
michael@0 | 485 | |
michael@0 | 486 | if (outdated || |
michael@0 | 487 | stringsDiffer(current.appLocale, e.appLocale)) { |
michael@0 | 488 | appinfo.put("appLocale", e.appLocale); |
michael@0 | 489 | changes = true; |
michael@0 | 490 | } |
michael@0 | 491 | |
michael@0 | 492 | if (outdated || |
michael@0 | 493 | stringsDiffer(current.distribution, e.distribution)) { |
michael@0 | 494 | appinfo.put("distribution", e.distribution); |
michael@0 | 495 | changes = true; |
michael@0 | 496 | } |
michael@0 | 497 | |
michael@0 | 498 | if (outdated || |
michael@0 | 499 | current.acceptLangSet != e.acceptLangSet) { |
michael@0 | 500 | appinfo.put("acceptLangIsUserSet", e.acceptLangSet); |
michael@0 | 501 | changes = true; |
michael@0 | 502 | } |
michael@0 | 503 | return changes; |
michael@0 | 504 | } |
michael@0 | 505 | |
michael@0 | 506 | private static JSONObject getAddonCounts(Environment e, Environment current) throws JSONException { |
michael@0 | 507 | JSONObject counts = new JSONObject(); |
michael@0 | 508 | int changes = 0; |
michael@0 | 509 | if (current == null || current.extensionCount != e.extensionCount) { |
michael@0 | 510 | counts.put("extension", e.extensionCount); |
michael@0 | 511 | changes++; |
michael@0 | 512 | } |
michael@0 | 513 | if (current == null || current.pluginCount != e.pluginCount) { |
michael@0 | 514 | counts.put("plugin", e.pluginCount); |
michael@0 | 515 | changes++; |
michael@0 | 516 | } |
michael@0 | 517 | if (current == null || current.themeCount != e.themeCount) { |
michael@0 | 518 | counts.put("theme", e.themeCount); |
michael@0 | 519 | changes++; |
michael@0 | 520 | } |
michael@0 | 521 | if (current != null && changes == 0) { |
michael@0 | 522 | return null; |
michael@0 | 523 | } |
michael@0 | 524 | counts.put("_v", 1); |
michael@0 | 525 | return counts; |
michael@0 | 526 | } |
michael@0 | 527 | |
michael@0 | 528 | /** |
michael@0 | 529 | * Compute the *tree* difference set between the two objects. If the two |
michael@0 | 530 | * objects are identical, returns <code>null</code>. If <code>from</code> is |
michael@0 | 531 | * <code>null</code>, returns <code>to</code>. If <code>to</code> is |
michael@0 | 532 | * <code>null</code>, behaves as if <code>to</code> were an empty object. |
michael@0 | 533 | * |
michael@0 | 534 | * (Note that this method does not check for {@link JSONObject#NULL}, because |
michael@0 | 535 | * by definition it can't be provided as input to this method.) |
michael@0 | 536 | * |
michael@0 | 537 | * This behavior is intended to simplify life for callers: a missing object |
michael@0 | 538 | * can be viewed as (and behaves as) an empty map, to a useful extent, rather |
michael@0 | 539 | * than throwing an exception. |
michael@0 | 540 | * |
michael@0 | 541 | * @param from |
michael@0 | 542 | * a JSONObject. |
michael@0 | 543 | * @param to |
michael@0 | 544 | * a JSONObject. |
michael@0 | 545 | * @param includeNull |
michael@0 | 546 | * if true, keys present in <code>from</code> but not in |
michael@0 | 547 | * <code>to</code> are included as {@link JSONObject#NULL} in the |
michael@0 | 548 | * output. |
michael@0 | 549 | * |
michael@0 | 550 | * @return a JSONObject, or null if the two objects are identical. |
michael@0 | 551 | * @throws JSONException |
michael@0 | 552 | * should not occur, but... |
michael@0 | 553 | */ |
michael@0 | 554 | public static JSONObject diff(JSONObject from, |
michael@0 | 555 | JSONObject to, |
michael@0 | 556 | boolean includeNull) throws JSONException { |
michael@0 | 557 | if (from == null) { |
michael@0 | 558 | return to; |
michael@0 | 559 | } |
michael@0 | 560 | |
michael@0 | 561 | if (to == null) { |
michael@0 | 562 | return diff(from, new JSONObject(), includeNull); |
michael@0 | 563 | } |
michael@0 | 564 | |
michael@0 | 565 | JSONObject out = new JSONObject(); |
michael@0 | 566 | |
michael@0 | 567 | HashSet<String> toKeys = includeNull ? new HashSet<String>(to.length()) |
michael@0 | 568 | : null; |
michael@0 | 569 | |
michael@0 | 570 | @SuppressWarnings("unchecked") |
michael@0 | 571 | Iterator<String> it = to.keys(); |
michael@0 | 572 | while (it.hasNext()) { |
michael@0 | 573 | String key = it.next(); |
michael@0 | 574 | |
michael@0 | 575 | // Track these as we go if we'll need them later. |
michael@0 | 576 | if (includeNull) { |
michael@0 | 577 | toKeys.add(key); |
michael@0 | 578 | } |
michael@0 | 579 | |
michael@0 | 580 | Object value = to.get(key); |
michael@0 | 581 | if (!from.has(key)) { |
michael@0 | 582 | // It must be new. |
michael@0 | 583 | out.put(key, value); |
michael@0 | 584 | continue; |
michael@0 | 585 | } |
michael@0 | 586 | |
michael@0 | 587 | // Not new? Then see if it changed. |
michael@0 | 588 | Object old = from.get(key); |
michael@0 | 589 | |
michael@0 | 590 | // Two JSONObjects should be diffed. |
michael@0 | 591 | if (old instanceof JSONObject && value instanceof JSONObject) { |
michael@0 | 592 | JSONObject innerDiff = diff(((JSONObject) old), ((JSONObject) value), |
michael@0 | 593 | includeNull); |
michael@0 | 594 | // No change? No output. |
michael@0 | 595 | if (innerDiff == null) { |
michael@0 | 596 | continue; |
michael@0 | 597 | } |
michael@0 | 598 | |
michael@0 | 599 | // Otherwise include the diff. |
michael@0 | 600 | out.put(key, innerDiff); |
michael@0 | 601 | continue; |
michael@0 | 602 | } |
michael@0 | 603 | |
michael@0 | 604 | // A regular value, or a type change. Only skip if they're the same. |
michael@0 | 605 | if (value.equals(old)) { |
michael@0 | 606 | continue; |
michael@0 | 607 | } |
michael@0 | 608 | out.put(key, value); |
michael@0 | 609 | } |
michael@0 | 610 | |
michael@0 | 611 | // Now -- if requested -- include any removed keys. |
michael@0 | 612 | if (includeNull) { |
michael@0 | 613 | Set<String> fromKeys = HealthReportUtils.keySet(from); |
michael@0 | 614 | fromKeys.removeAll(toKeys); |
michael@0 | 615 | for (String notPresent : fromKeys) { |
michael@0 | 616 | out.put(notPresent, JSONObject.NULL); |
michael@0 | 617 | } |
michael@0 | 618 | } |
michael@0 | 619 | |
michael@0 | 620 | if (out.length() == 0) { |
michael@0 | 621 | return null; |
michael@0 | 622 | } |
michael@0 | 623 | return out; |
michael@0 | 624 | } |
michael@0 | 625 | |
michael@0 | 626 | private static JSONObject getActiveAddons(Environment e, Environment current) throws JSONException { |
michael@0 | 627 | // Just return the current add-on set, with a version annotation. |
michael@0 | 628 | // To do so requires copying. |
michael@0 | 629 | if (current == null) { |
michael@0 | 630 | JSONObject out = e.getNonIgnoredAddons(); |
michael@0 | 631 | if (out == null) { |
michael@0 | 632 | Logger.warn(LOG_TAG, "Null add-ons to return in FHR document. Returning {}."); |
michael@0 | 633 | out = new JSONObject(); // So that we always return something. |
michael@0 | 634 | } |
michael@0 | 635 | out.put("_v", 1); |
michael@0 | 636 | return out; |
michael@0 | 637 | } |
michael@0 | 638 | |
michael@0 | 639 | // Otherwise, return the diff. |
michael@0 | 640 | JSONObject diff = diff(current.getNonIgnoredAddons(), e.getNonIgnoredAddons(), true); |
michael@0 | 641 | if (diff == null) { |
michael@0 | 642 | return null; |
michael@0 | 643 | } |
michael@0 | 644 | if (diff == e.addons) { |
michael@0 | 645 | // Again, needs to copy. |
michael@0 | 646 | return getActiveAddons(e, null); |
michael@0 | 647 | } |
michael@0 | 648 | |
michael@0 | 649 | diff.put("_v", 1); |
michael@0 | 650 | return diff; |
michael@0 | 651 | } |
michael@0 | 652 | } |