1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/background/healthreport/HealthReportGenerator.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,652 @@ 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.util.HashSet; 1.11 +import java.util.Iterator; 1.12 +import java.util.Set; 1.13 + 1.14 +import org.json.JSONException; 1.15 +import org.json.JSONObject; 1.16 +import org.mozilla.gecko.background.common.DateUtils.DateFormatter; 1.17 +import org.mozilla.gecko.background.common.log.Logger; 1.18 +import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field; 1.19 + 1.20 +import android.database.Cursor; 1.21 +import android.util.SparseArray; 1.22 + 1.23 +public class HealthReportGenerator { 1.24 + private static final int PAYLOAD_VERSION = 3; 1.25 + 1.26 + private static final String LOG_TAG = "GeckoHealthGen"; 1.27 + 1.28 + private final HealthReportStorage storage; 1.29 + private final DateFormatter dateFormatter; 1.30 + 1.31 + public HealthReportGenerator(HealthReportStorage storage) { 1.32 + this.storage = storage; 1.33 + this.dateFormatter = new DateFormatter(); 1.34 + } 1.35 + 1.36 + @SuppressWarnings("static-method") 1.37 + protected long now() { 1.38 + return System.currentTimeMillis(); 1.39 + } 1.40 + 1.41 + /** 1.42 + * Ensure that you have initialized the Locale to your satisfaction 1.43 + * prior to calling this method. 1.44 + * 1.45 + * @return null if no environment could be computed, or else the resulting document. 1.46 + * @throws JSONException if there was an error adding environment data to the resulting document. 1.47 + */ 1.48 + public JSONObject generateDocument(long since, long lastPingTime, String profilePath) throws JSONException { 1.49 + Logger.info(LOG_TAG, "Generating FHR document from " + since + "; last ping " + lastPingTime); 1.50 + Logger.pii(LOG_TAG, "Generating for profile " + profilePath); 1.51 + 1.52 + ProfileInformationCache cache = new ProfileInformationCache(profilePath); 1.53 + if (!cache.restoreUnlessInitialized()) { 1.54 + Logger.warn(LOG_TAG, "Not enough profile information to compute current environment."); 1.55 + return null; 1.56 + } 1.57 + Environment current = EnvironmentBuilder.getCurrentEnvironment(cache); 1.58 + return generateDocument(since, lastPingTime, current); 1.59 + } 1.60 + 1.61 + /** 1.62 + * The document consists of: 1.63 + * 1.64 + *<ul> 1.65 + *<li>Basic metadata: last ping time, current ping time, version.</li> 1.66 + *<li>A map of environments: <code>current</code> and others named by hash. <code>current</code> is fully specified, 1.67 + * and others are deltas from current.</li> 1.68 + *<li>A <code>data</code> object. This includes <code>last</code> and <code>days</code>.</li> 1.69 + *</ul> 1.70 + * 1.71 + * <code>days</code> is a map from date strings to <tt>{hash: {measurement: {_v: version, fields...}}}</tt>. 1.72 + * @throws JSONException if there was an error adding environment data to the resulting document. 1.73 + */ 1.74 + public JSONObject generateDocument(long since, long lastPingTime, Environment currentEnvironment) throws JSONException { 1.75 + final String currentHash = currentEnvironment.getHash(); 1.76 + 1.77 + Logger.debug(LOG_TAG, "Current environment hash: " + currentHash); 1.78 + if (currentHash == null) { 1.79 + Logger.warn(LOG_TAG, "Current hash is null; aborting."); 1.80 + return null; 1.81 + } 1.82 + 1.83 + // We want to map field IDs to some strings as we go. 1.84 + SparseArray<Environment> envs = storage.getEnvironmentRecordsByID(); 1.85 + 1.86 + JSONObject document = new JSONObject(); 1.87 + 1.88 + if (lastPingTime >= HealthReportConstants.EARLIEST_LAST_PING) { 1.89 + document.put("lastPingDate", dateFormatter.getDateString(lastPingTime)); 1.90 + } 1.91 + 1.92 + document.put("thisPingDate", dateFormatter.getDateString(now())); 1.93 + document.put("version", PAYLOAD_VERSION); 1.94 + 1.95 + document.put("environments", getEnvironmentsJSON(currentEnvironment, envs)); 1.96 + document.put("data", getDataJSON(currentEnvironment, envs, since)); 1.97 + 1.98 + return document; 1.99 + } 1.100 + 1.101 + protected JSONObject getDataJSON(Environment currentEnvironment, 1.102 + SparseArray<Environment> envs, long since) throws JSONException { 1.103 + SparseArray<Field> fields = storage.getFieldsByID(); 1.104 + 1.105 + JSONObject days = getDaysJSON(currentEnvironment, envs, fields, since); 1.106 + 1.107 + JSONObject last = new JSONObject(); 1.108 + 1.109 + JSONObject data = new JSONObject(); 1.110 + data.put("days", days); 1.111 + data.put("last", last); 1.112 + return data; 1.113 + } 1.114 + 1.115 + protected JSONObject getDaysJSON(Environment currentEnvironment, SparseArray<Environment> envs, SparseArray<Field> fields, long since) throws JSONException { 1.116 + if (Logger.shouldLogVerbose(LOG_TAG)) { 1.117 + for (int i = 0; i < envs.size(); ++i) { 1.118 + Logger.trace(LOG_TAG, "Days environment " + envs.keyAt(i) + ": " + envs.get(envs.keyAt(i)).getHash()); 1.119 + } 1.120 + } 1.121 + 1.122 + JSONObject days = new JSONObject(); 1.123 + Cursor cursor = storage.getRawEventsSince(since); 1.124 + try { 1.125 + if (!cursor.moveToFirst()) { 1.126 + return days; 1.127 + } 1.128 + 1.129 + // A classic walking partition. 1.130 + // Columns are "date", "env", "field", "value". 1.131 + // Note that we care about the type (integer, string) and kind 1.132 + // (last/counter, discrete) of each field. 1.133 + // Each field will be accessed once for each date/env pair, so 1.134 + // Field memoizes these facts. 1.135 + // We also care about which measurement contains each field. 1.136 + int lastDate = -1; 1.137 + int lastEnv = -1; 1.138 + JSONObject dateObject = null; 1.139 + JSONObject envObject = null; 1.140 + 1.141 + while (!cursor.isAfterLast()) { 1.142 + int cEnv = cursor.getInt(1); 1.143 + if (cEnv == -1 || 1.144 + (cEnv != lastEnv && 1.145 + envs.indexOfKey(cEnv) < 0)) { 1.146 + Logger.warn(LOG_TAG, "Invalid environment " + cEnv + " in cursor. Skipping."); 1.147 + cursor.moveToNext(); 1.148 + continue; 1.149 + } 1.150 + 1.151 + int cDate = cursor.getInt(0); 1.152 + int cField = cursor.getInt(2); 1.153 + 1.154 + Logger.trace(LOG_TAG, "Event row: " + cDate + ", " + cEnv + ", " + cField); 1.155 + boolean dateChanged = cDate != lastDate; 1.156 + boolean envChanged = cEnv != lastEnv; 1.157 + 1.158 + if (dateChanged) { 1.159 + if (dateObject != null) { 1.160 + days.put(dateFormatter.getDateStringForDay(lastDate), dateObject); 1.161 + } 1.162 + dateObject = new JSONObject(); 1.163 + lastDate = cDate; 1.164 + } 1.165 + 1.166 + if (dateChanged || envChanged) { 1.167 + envObject = new JSONObject(); 1.168 + // This is safe because we checked above that cEnv is valid. 1.169 + dateObject.put(envs.get(cEnv).getHash(), envObject); 1.170 + lastEnv = cEnv; 1.171 + } 1.172 + 1.173 + final Field field = fields.get(cField); 1.174 + JSONObject measurement = envObject.optJSONObject(field.measurementName); 1.175 + if (measurement == null) { 1.176 + // We will never have more than one measurement version within a 1.177 + // single environment -- to do so involves changing the build ID. And 1.178 + // even if we did, we have no way to represent it. So just build the 1.179 + // output object once. 1.180 + measurement = new JSONObject(); 1.181 + measurement.put("_v", field.measurementVersion); 1.182 + envObject.put(field.measurementName, measurement); 1.183 + } 1.184 + 1.185 + // How we record depends on the type of the field, so we 1.186 + // break this out into a separate method for clarity. 1.187 + recordMeasurementFromCursor(field, measurement, cursor); 1.188 + 1.189 + cursor.moveToNext(); 1.190 + continue; 1.191 + } 1.192 + days.put(dateFormatter.getDateStringForDay(lastDate), dateObject); 1.193 + } finally { 1.194 + cursor.close(); 1.195 + } 1.196 + return days; 1.197 + } 1.198 + 1.199 + /** 1.200 + * Return the {@link JSONObject} parsed from the provided index of the given 1.201 + * cursor, or {@link JSONObject#NULL} if either SQL <code>NULL</code> or 1.202 + * string <code>"null"</code> is present at that index. 1.203 + */ 1.204 + private static Object getJSONAtIndex(Cursor cursor, int index) throws JSONException { 1.205 + if (cursor.isNull(index)) { 1.206 + return JSONObject.NULL; 1.207 + } 1.208 + final String value = cursor.getString(index); 1.209 + if ("null".equals(value)) { 1.210 + return JSONObject.NULL; 1.211 + } 1.212 + return new JSONObject(value); 1.213 + } 1.214 + 1.215 + protected static void recordMeasurementFromCursor(final Field field, 1.216 + JSONObject measurement, 1.217 + Cursor cursor) 1.218 + throws JSONException { 1.219 + if (field.isDiscreteField()) { 1.220 + // Discrete counted. Increment the named counter. 1.221 + if (field.isCountedField()) { 1.222 + if (!field.isStringField()) { 1.223 + throw new IllegalStateException("Unable to handle non-string counted types."); 1.224 + } 1.225 + HealthReportUtils.count(measurement, field.fieldName, cursor.getString(3)); 1.226 + return; 1.227 + } 1.228 + 1.229 + // Discrete string or integer. Append it. 1.230 + if (field.isStringField()) { 1.231 + HealthReportUtils.append(measurement, field.fieldName, cursor.getString(3)); 1.232 + return; 1.233 + } 1.234 + if (field.isJSONField()) { 1.235 + HealthReportUtils.append(measurement, field.fieldName, getJSONAtIndex(cursor, 3)); 1.236 + return; 1.237 + } 1.238 + if (field.isIntegerField()) { 1.239 + HealthReportUtils.append(measurement, field.fieldName, cursor.getLong(3)); 1.240 + return; 1.241 + } 1.242 + throw new IllegalStateException("Unknown field type: " + field.flags); 1.243 + } 1.244 + 1.245 + // Non-discrete -- must be LAST or COUNTER, so just accumulate the value. 1.246 + if (field.isStringField()) { 1.247 + measurement.put(field.fieldName, cursor.getString(3)); 1.248 + return; 1.249 + } 1.250 + if (field.isJSONField()) { 1.251 + measurement.put(field.fieldName, getJSONAtIndex(cursor, 3)); 1.252 + return; 1.253 + } 1.254 + measurement.put(field.fieldName, cursor.getLong(3)); 1.255 + } 1.256 + 1.257 + public static JSONObject getEnvironmentsJSON(Environment currentEnvironment, 1.258 + SparseArray<Environment> envs) throws JSONException { 1.259 + JSONObject environments = new JSONObject(); 1.260 + 1.261 + // Always do this, even if it hasn't recorded anything in the DB. 1.262 + environments.put("current", jsonify(currentEnvironment, null)); 1.263 + 1.264 + String currentHash = currentEnvironment.getHash(); 1.265 + for (int i = 0; i < envs.size(); i++) { 1.266 + Environment e = envs.valueAt(i); 1.267 + if (currentHash.equals(e.getHash())) { 1.268 + continue; 1.269 + } 1.270 + environments.put(e.getHash(), jsonify(e, currentEnvironment)); 1.271 + } 1.272 + return environments; 1.273 + } 1.274 + 1.275 + public static JSONObject jsonify(Environment e, Environment current) throws JSONException { 1.276 + JSONObject age = getProfileAge(e, current); 1.277 + JSONObject sysinfo = getSysInfo(e, current); 1.278 + JSONObject gecko = getGeckoInfo(e, current); 1.279 + JSONObject appinfo = getAppInfo(e, current); 1.280 + JSONObject counts = getAddonCounts(e, current); 1.281 + 1.282 + JSONObject out = new JSONObject(); 1.283 + if (age != null) 1.284 + out.put("org.mozilla.profile.age", age); 1.285 + if (sysinfo != null) 1.286 + out.put("org.mozilla.sysinfo.sysinfo", sysinfo); 1.287 + if (gecko != null) 1.288 + out.put("geckoAppInfo", gecko); 1.289 + if (appinfo != null) 1.290 + out.put("org.mozilla.appInfo.appinfo", appinfo); 1.291 + if (counts != null) 1.292 + out.put("org.mozilla.addons.counts", counts); 1.293 + 1.294 + JSONObject active = getActiveAddons(e, current); 1.295 + if (active != null) 1.296 + out.put("org.mozilla.addons.active", active); 1.297 + 1.298 + if (current == null) { 1.299 + out.put("hash", e.getHash()); 1.300 + } 1.301 + return out; 1.302 + } 1.303 + 1.304 + private static JSONObject getProfileAge(Environment e, Environment current) throws JSONException { 1.305 + JSONObject age = new JSONObject(); 1.306 + int changes = 0; 1.307 + if (current == null || current.profileCreation != e.profileCreation) { 1.308 + age.put("profileCreation", e.profileCreation); 1.309 + changes++; 1.310 + } 1.311 + if (current != null && changes == 0) { 1.312 + return null; 1.313 + } 1.314 + age.put("_v", 1); 1.315 + return age; 1.316 + } 1.317 + 1.318 + private static JSONObject getSysInfo(Environment e, Environment current) throws JSONException { 1.319 + JSONObject sysinfo = new JSONObject(); 1.320 + int changes = 0; 1.321 + if (current == null || current.cpuCount != e.cpuCount) { 1.322 + sysinfo.put("cpuCount", e.cpuCount); 1.323 + changes++; 1.324 + } 1.325 + if (current == null || current.memoryMB != e.memoryMB) { 1.326 + sysinfo.put("memoryMB", e.memoryMB); 1.327 + changes++; 1.328 + } 1.329 + if (current == null || !current.architecture.equals(e.architecture)) { 1.330 + sysinfo.put("architecture", e.architecture); 1.331 + changes++; 1.332 + } 1.333 + if (current == null || !current.sysName.equals(e.sysName)) { 1.334 + sysinfo.put("name", e.sysName); 1.335 + changes++; 1.336 + } 1.337 + if (current == null || !current.sysVersion.equals(e.sysVersion)) { 1.338 + sysinfo.put("version", e.sysVersion); 1.339 + changes++; 1.340 + } 1.341 + if (current != null && changes == 0) { 1.342 + return null; 1.343 + } 1.344 + sysinfo.put("_v", 1); 1.345 + return sysinfo; 1.346 + } 1.347 + 1.348 + private static JSONObject getGeckoInfo(Environment e, Environment current) throws JSONException { 1.349 + JSONObject gecko = new JSONObject(); 1.350 + int changes = 0; 1.351 + if (current == null || !current.vendor.equals(e.vendor)) { 1.352 + gecko.put("vendor", e.vendor); 1.353 + changes++; 1.354 + } 1.355 + if (current == null || !current.appName.equals(e.appName)) { 1.356 + gecko.put("name", e.appName); 1.357 + changes++; 1.358 + } 1.359 + if (current == null || !current.appID.equals(e.appID)) { 1.360 + gecko.put("id", e.appID); 1.361 + changes++; 1.362 + } 1.363 + if (current == null || !current.appVersion.equals(e.appVersion)) { 1.364 + gecko.put("version", e.appVersion); 1.365 + changes++; 1.366 + } 1.367 + if (current == null || !current.appBuildID.equals(e.appBuildID)) { 1.368 + gecko.put("appBuildID", e.appBuildID); 1.369 + changes++; 1.370 + } 1.371 + if (current == null || !current.platformVersion.equals(e.platformVersion)) { 1.372 + gecko.put("platformVersion", e.platformVersion); 1.373 + changes++; 1.374 + } 1.375 + if (current == null || !current.platformBuildID.equals(e.platformBuildID)) { 1.376 + gecko.put("platformBuildID", e.platformBuildID); 1.377 + changes++; 1.378 + } 1.379 + if (current == null || !current.os.equals(e.os)) { 1.380 + gecko.put("os", e.os); 1.381 + changes++; 1.382 + } 1.383 + if (current == null || !current.xpcomabi.equals(e.xpcomabi)) { 1.384 + gecko.put("xpcomabi", e.xpcomabi); 1.385 + changes++; 1.386 + } 1.387 + if (current == null || !current.updateChannel.equals(e.updateChannel)) { 1.388 + gecko.put("updateChannel", e.updateChannel); 1.389 + changes++; 1.390 + } 1.391 + if (current != null && changes == 0) { 1.392 + return null; 1.393 + } 1.394 + gecko.put("_v", 1); 1.395 + return gecko; 1.396 + } 1.397 + 1.398 + // Null-safe string comparison. 1.399 + private static boolean stringsDiffer(final String a, final String b) { 1.400 + if (a == null) { 1.401 + return b != null; 1.402 + } 1.403 + return !a.equals(b); 1.404 + } 1.405 + 1.406 + private static JSONObject getAppInfo(Environment e, Environment current) throws JSONException { 1.407 + JSONObject appinfo = new JSONObject(); 1.408 + 1.409 + Logger.debug(LOG_TAG, "Generating appinfo for v" + e.version + " env " + e.hash); 1.410 + 1.411 + // Is the environment in question newer than the diff target, or is 1.412 + // there no diff target? 1.413 + final boolean outdated = current == null || 1.414 + e.version > current.version; 1.415 + 1.416 + // Is the environment in question a different version (lower or higher), 1.417 + // or is there no diff target? 1.418 + final boolean differ = outdated || current.version > e.version; 1.419 + 1.420 + // Always produce an output object if there's a version mismatch or this 1.421 + // isn't a diff. Otherwise, track as we go if there's any difference. 1.422 + boolean changed = differ; 1.423 + 1.424 + switch (e.version) { 1.425 + // There's a straightforward correspondence between environment versions 1.426 + // and appinfo versions. 1.427 + case 2: 1.428 + appinfo.put("_v", 3); 1.429 + break; 1.430 + case 1: 1.431 + appinfo.put("_v", 2); 1.432 + break; 1.433 + default: 1.434 + Logger.warn(LOG_TAG, "Unknown environment version: " + e.version); 1.435 + return appinfo; 1.436 + } 1.437 + 1.438 + switch (e.version) { 1.439 + case 2: 1.440 + if (populateAppInfoV2(appinfo, e, current, outdated)) { 1.441 + changed = true; 1.442 + } 1.443 + // Fall through. 1.444 + 1.445 + case 1: 1.446 + // There is no older version than v1, so don't check outdated. 1.447 + if (populateAppInfoV1(e, current, appinfo)) { 1.448 + changed = true; 1.449 + } 1.450 + } 1.451 + 1.452 + if (!changed) { 1.453 + return null; 1.454 + } 1.455 + 1.456 + return appinfo; 1.457 + } 1.458 + 1.459 + private static boolean populateAppInfoV1(Environment e, 1.460 + Environment current, 1.461 + JSONObject appinfo) 1.462 + throws JSONException { 1.463 + boolean changes = false; 1.464 + if (current == null || current.isBlocklistEnabled != e.isBlocklistEnabled) { 1.465 + appinfo.put("isBlocklistEnabled", e.isBlocklistEnabled); 1.466 + changes = true; 1.467 + } 1.468 + 1.469 + if (current == null || current.isTelemetryEnabled != e.isTelemetryEnabled) { 1.470 + appinfo.put("isTelemetryEnabled", e.isTelemetryEnabled); 1.471 + changes = true; 1.472 + } 1.473 + 1.474 + return changes; 1.475 + } 1.476 + 1.477 + private static boolean populateAppInfoV2(JSONObject appinfo, 1.478 + Environment e, 1.479 + Environment current, 1.480 + final boolean outdated) 1.481 + throws JSONException { 1.482 + boolean changes = false; 1.483 + if (outdated || 1.484 + stringsDiffer(current.osLocale, e.osLocale)) { 1.485 + appinfo.put("osLocale", e.osLocale); 1.486 + changes = true; 1.487 + } 1.488 + 1.489 + if (outdated || 1.490 + stringsDiffer(current.appLocale, e.appLocale)) { 1.491 + appinfo.put("appLocale", e.appLocale); 1.492 + changes = true; 1.493 + } 1.494 + 1.495 + if (outdated || 1.496 + stringsDiffer(current.distribution, e.distribution)) { 1.497 + appinfo.put("distribution", e.distribution); 1.498 + changes = true; 1.499 + } 1.500 + 1.501 + if (outdated || 1.502 + current.acceptLangSet != e.acceptLangSet) { 1.503 + appinfo.put("acceptLangIsUserSet", e.acceptLangSet); 1.504 + changes = true; 1.505 + } 1.506 + return changes; 1.507 + } 1.508 + 1.509 + private static JSONObject getAddonCounts(Environment e, Environment current) throws JSONException { 1.510 + JSONObject counts = new JSONObject(); 1.511 + int changes = 0; 1.512 + if (current == null || current.extensionCount != e.extensionCount) { 1.513 + counts.put("extension", e.extensionCount); 1.514 + changes++; 1.515 + } 1.516 + if (current == null || current.pluginCount != e.pluginCount) { 1.517 + counts.put("plugin", e.pluginCount); 1.518 + changes++; 1.519 + } 1.520 + if (current == null || current.themeCount != e.themeCount) { 1.521 + counts.put("theme", e.themeCount); 1.522 + changes++; 1.523 + } 1.524 + if (current != null && changes == 0) { 1.525 + return null; 1.526 + } 1.527 + counts.put("_v", 1); 1.528 + return counts; 1.529 + } 1.530 + 1.531 + /** 1.532 + * Compute the *tree* difference set between the two objects. If the two 1.533 + * objects are identical, returns <code>null</code>. If <code>from</code> is 1.534 + * <code>null</code>, returns <code>to</code>. If <code>to</code> is 1.535 + * <code>null</code>, behaves as if <code>to</code> were an empty object. 1.536 + * 1.537 + * (Note that this method does not check for {@link JSONObject#NULL}, because 1.538 + * by definition it can't be provided as input to this method.) 1.539 + * 1.540 + * This behavior is intended to simplify life for callers: a missing object 1.541 + * can be viewed as (and behaves as) an empty map, to a useful extent, rather 1.542 + * than throwing an exception. 1.543 + * 1.544 + * @param from 1.545 + * a JSONObject. 1.546 + * @param to 1.547 + * a JSONObject. 1.548 + * @param includeNull 1.549 + * if true, keys present in <code>from</code> but not in 1.550 + * <code>to</code> are included as {@link JSONObject#NULL} in the 1.551 + * output. 1.552 + * 1.553 + * @return a JSONObject, or null if the two objects are identical. 1.554 + * @throws JSONException 1.555 + * should not occur, but... 1.556 + */ 1.557 + public static JSONObject diff(JSONObject from, 1.558 + JSONObject to, 1.559 + boolean includeNull) throws JSONException { 1.560 + if (from == null) { 1.561 + return to; 1.562 + } 1.563 + 1.564 + if (to == null) { 1.565 + return diff(from, new JSONObject(), includeNull); 1.566 + } 1.567 + 1.568 + JSONObject out = new JSONObject(); 1.569 + 1.570 + HashSet<String> toKeys = includeNull ? new HashSet<String>(to.length()) 1.571 + : null; 1.572 + 1.573 + @SuppressWarnings("unchecked") 1.574 + Iterator<String> it = to.keys(); 1.575 + while (it.hasNext()) { 1.576 + String key = it.next(); 1.577 + 1.578 + // Track these as we go if we'll need them later. 1.579 + if (includeNull) { 1.580 + toKeys.add(key); 1.581 + } 1.582 + 1.583 + Object value = to.get(key); 1.584 + if (!from.has(key)) { 1.585 + // It must be new. 1.586 + out.put(key, value); 1.587 + continue; 1.588 + } 1.589 + 1.590 + // Not new? Then see if it changed. 1.591 + Object old = from.get(key); 1.592 + 1.593 + // Two JSONObjects should be diffed. 1.594 + if (old instanceof JSONObject && value instanceof JSONObject) { 1.595 + JSONObject innerDiff = diff(((JSONObject) old), ((JSONObject) value), 1.596 + includeNull); 1.597 + // No change? No output. 1.598 + if (innerDiff == null) { 1.599 + continue; 1.600 + } 1.601 + 1.602 + // Otherwise include the diff. 1.603 + out.put(key, innerDiff); 1.604 + continue; 1.605 + } 1.606 + 1.607 + // A regular value, or a type change. Only skip if they're the same. 1.608 + if (value.equals(old)) { 1.609 + continue; 1.610 + } 1.611 + out.put(key, value); 1.612 + } 1.613 + 1.614 + // Now -- if requested -- include any removed keys. 1.615 + if (includeNull) { 1.616 + Set<String> fromKeys = HealthReportUtils.keySet(from); 1.617 + fromKeys.removeAll(toKeys); 1.618 + for (String notPresent : fromKeys) { 1.619 + out.put(notPresent, JSONObject.NULL); 1.620 + } 1.621 + } 1.622 + 1.623 + if (out.length() == 0) { 1.624 + return null; 1.625 + } 1.626 + return out; 1.627 + } 1.628 + 1.629 + private static JSONObject getActiveAddons(Environment e, Environment current) throws JSONException { 1.630 + // Just return the current add-on set, with a version annotation. 1.631 + // To do so requires copying. 1.632 + if (current == null) { 1.633 + JSONObject out = e.getNonIgnoredAddons(); 1.634 + if (out == null) { 1.635 + Logger.warn(LOG_TAG, "Null add-ons to return in FHR document. Returning {}."); 1.636 + out = new JSONObject(); // So that we always return something. 1.637 + } 1.638 + out.put("_v", 1); 1.639 + return out; 1.640 + } 1.641 + 1.642 + // Otherwise, return the diff. 1.643 + JSONObject diff = diff(current.getNonIgnoredAddons(), e.getNonIgnoredAddons(), true); 1.644 + if (diff == null) { 1.645 + return null; 1.646 + } 1.647 + if (diff == e.addons) { 1.648 + // Again, needs to copy. 1.649 + return getActiveAddons(e, null); 1.650 + } 1.651 + 1.652 + diff.put("_v", 1); 1.653 + return diff; 1.654 + } 1.655 +}