1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/background/healthreport/EnvironmentV1.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,272 @@ 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.io.UnsupportedEncodingException; 1.11 +import java.security.NoSuchAlgorithmException; 1.12 +import java.util.Iterator; 1.13 +import java.util.SortedSet; 1.14 + 1.15 +import org.json.JSONException; 1.16 +import org.json.JSONObject; 1.17 +import org.mozilla.apache.commons.codec.binary.Base64; 1.18 +import org.mozilla.gecko.background.common.log.Logger; 1.19 +import org.mozilla.gecko.background.nativecode.NativeCrypto; 1.20 + 1.21 +public abstract class EnvironmentV1 { 1.22 + private static final String LOG_TAG = "GeckoEnvironment"; 1.23 + private static final int VERSION = 1; 1.24 + 1.25 + protected final Class<? extends EnvironmentAppender> appenderClass; 1.26 + 1.27 + protected volatile String hash = null; 1.28 + protected volatile int id = -1; 1.29 + 1.30 + public int version = VERSION; 1.31 + 1.32 + // org.mozilla.profile.age. 1.33 + public int profileCreation; 1.34 + 1.35 + // org.mozilla.sysinfo.sysinfo. 1.36 + public int cpuCount; 1.37 + public int memoryMB; 1.38 + public String architecture; 1.39 + public String sysName; 1.40 + public String sysVersion; // Kernel. 1.41 + 1.42 + // geckoAppInfo. 1.43 + public String vendor; 1.44 + public String appName; 1.45 + public String appID; 1.46 + public String appVersion; 1.47 + public String appBuildID; 1.48 + public String platformVersion; 1.49 + public String platformBuildID; 1.50 + public String os; 1.51 + public String xpcomabi; 1.52 + public String updateChannel; 1.53 + 1.54 + // appinfo. 1.55 + public int isBlocklistEnabled; 1.56 + public int isTelemetryEnabled; 1.57 + 1.58 + // org.mozilla.addons.active. 1.59 + public JSONObject addons = null; 1.60 + 1.61 + // org.mozilla.addons.counts. 1.62 + public int extensionCount; 1.63 + public int pluginCount; 1.64 + public int themeCount; 1.65 + 1.66 + /** 1.67 + * We break out this interface in order to allow for testing -- pass in your 1.68 + * own appender that just records strings, for example. 1.69 + */ 1.70 + public static abstract class EnvironmentAppender { 1.71 + public abstract void append(String s); 1.72 + public abstract void append(int v); 1.73 + } 1.74 + 1.75 + public static class HashAppender extends EnvironmentAppender { 1.76 + private final StringBuilder builder; 1.77 + 1.78 + public HashAppender() throws NoSuchAlgorithmException { 1.79 + builder = new StringBuilder(); 1.80 + } 1.81 + 1.82 + @Override 1.83 + public void append(String s) { 1.84 + builder.append((s == null) ? "null" : s); 1.85 + } 1.86 + 1.87 + @Override 1.88 + public void append(int profileCreation) { 1.89 + append(Integer.toString(profileCreation, 10)); 1.90 + } 1.91 + 1.92 + @Override 1.93 + public String toString() { 1.94 + // We *could* use ASCII85… but the savings would be negated by the 1.95 + // inclusion of JSON-unsafe characters like double-quote. 1.96 + final byte[] inputBytes; 1.97 + try { 1.98 + inputBytes = builder.toString().getBytes("UTF-8"); 1.99 + } catch (UnsupportedEncodingException e) { 1.100 + Logger.warn(LOG_TAG, "Invalid charset String passed to getBytes", e); 1.101 + return null; 1.102 + } 1.103 + 1.104 + // Note to the security-minded reader: we deliberately use SHA-1 here, not 1.105 + // a stronger hash. These identifiers don't strictly need a cryptographic 1.106 + // hash function, because there is negligible value in attacking the hash. 1.107 + // We use SHA-1 because it's *shorter* -- the exact same reason that Git 1.108 + // chose SHA-1. 1.109 + final byte[] hash = NativeCrypto.sha1(inputBytes); 1.110 + return new Base64(-1, null, false).encodeAsString(hash); 1.111 + } 1.112 + } 1.113 + 1.114 + /** 1.115 + * Ensure that the {@link Environment} has been registered with its 1.116 + * storage layer, and can be used to annotate events. 1.117 + * 1.118 + * It's safe to call this method more than once, and each time you'll 1.119 + * get the same ID. 1.120 + * 1.121 + * @return the integer ID to use in subsequent DB insertions. 1.122 + */ 1.123 + public abstract int register(); 1.124 + 1.125 + protected EnvironmentAppender getAppender() { 1.126 + EnvironmentAppender appender = null; 1.127 + try { 1.128 + appender = appenderClass.newInstance(); 1.129 + } catch (InstantiationException ex) { 1.130 + // Should never happen, but... 1.131 + Logger.warn(LOG_TAG, "Could not compute hash.", ex); 1.132 + } catch (IllegalAccessException ex) { 1.133 + // Should never happen, but... 1.134 + Logger.warn(LOG_TAG, "Could not compute hash.", ex); 1.135 + } 1.136 + return appender; 1.137 + } 1.138 + 1.139 + protected void appendHash(EnvironmentAppender appender) { 1.140 + appender.append(profileCreation); 1.141 + appender.append(cpuCount); 1.142 + appender.append(memoryMB); 1.143 + appender.append(architecture); 1.144 + appender.append(sysName); 1.145 + appender.append(sysVersion); 1.146 + appender.append(vendor); 1.147 + appender.append(appName); 1.148 + appender.append(appID); 1.149 + appender.append(appVersion); 1.150 + appender.append(appBuildID); 1.151 + appender.append(platformVersion); 1.152 + appender.append(platformBuildID); 1.153 + appender.append(os); 1.154 + appender.append(xpcomabi); 1.155 + appender.append(updateChannel); 1.156 + appender.append(isBlocklistEnabled); 1.157 + appender.append(isTelemetryEnabled); 1.158 + appender.append(extensionCount); 1.159 + appender.append(pluginCount); 1.160 + appender.append(themeCount); 1.161 + 1.162 + // We need sorted values. 1.163 + if (addons != null) { 1.164 + appendSortedAddons(getNonIgnoredAddons(), appender); 1.165 + } 1.166 + } 1.167 + 1.168 + /** 1.169 + * Compute the stable hash of the configured environment. 1.170 + * 1.171 + * @return the hash in base34, or null if there was a problem. 1.172 + */ 1.173 + public String getHash() { 1.174 + // It's never unset, so we only care about partial reads. volatile is enough. 1.175 + if (hash != null) { 1.176 + return hash; 1.177 + } 1.178 + 1.179 + EnvironmentAppender appender = getAppender(); 1.180 + if (appender == null) { 1.181 + return null; 1.182 + } 1.183 + 1.184 + appendHash(appender); 1.185 + return hash = appender.toString(); 1.186 + } 1.187 + 1.188 + public EnvironmentV1(Class<? extends EnvironmentAppender> appenderClass) { 1.189 + super(); 1.190 + this.appenderClass = appenderClass; 1.191 + } 1.192 + 1.193 + public JSONObject getNonIgnoredAddons() { 1.194 + if (addons == null) { 1.195 + return null; 1.196 + } 1.197 + JSONObject out = new JSONObject(); 1.198 + @SuppressWarnings("unchecked") 1.199 + Iterator<String> keys = addons.keys(); 1.200 + while (keys.hasNext()) { 1.201 + try { 1.202 + final String key = keys.next(); 1.203 + final Object obj = addons.get(key); 1.204 + if (obj != null && 1.205 + obj instanceof JSONObject && 1.206 + ((JSONObject) obj).optBoolean("ignore", false)) { 1.207 + continue; 1.208 + } 1.209 + out.put(key, obj); 1.210 + } catch (JSONException ex) { 1.211 + // Do nothing. 1.212 + } 1.213 + } 1.214 + return out; 1.215 + } 1.216 + 1.217 + /** 1.218 + * Take a collection of add-on descriptors, appending a consistent string 1.219 + * to the provided builder. 1.220 + */ 1.221 + public static void appendSortedAddons(JSONObject addons, final EnvironmentAppender builder) { 1.222 + final SortedSet<String> keys = HealthReportUtils.sortedKeySet(addons); 1.223 + 1.224 + // For each add-on, produce a consistent, sorted mapping of its descriptor. 1.225 + for (String key : keys) { 1.226 + try { 1.227 + JSONObject addon = addons.getJSONObject(key); 1.228 + 1.229 + // Now produce the output for this add-on. 1.230 + builder.append(key); 1.231 + builder.append("={"); 1.232 + 1.233 + for (String addonKey : HealthReportUtils.sortedKeySet(addon)) { 1.234 + builder.append(addonKey); 1.235 + builder.append("=="); 1.236 + try { 1.237 + builder.append(addon.get(addonKey).toString()); 1.238 + } catch (JSONException e) { 1.239 + builder.append("_e_"); 1.240 + } 1.241 + } 1.242 + 1.243 + builder.append("}"); 1.244 + } catch (Exception e) { 1.245 + // Muffle. 1.246 + Logger.warn(LOG_TAG, "Invalid add-on for ID " + key); 1.247 + } 1.248 + } 1.249 + } 1.250 + 1.251 + public void setJSONForAddons(byte[] json) throws Exception { 1.252 + setJSONForAddons(new String(json, "UTF-8")); 1.253 + } 1.254 + 1.255 + public void setJSONForAddons(String json) throws Exception { 1.256 + if (json == null || "null".equals(json)) { 1.257 + addons = null; 1.258 + return; 1.259 + } 1.260 + addons = new JSONObject(json); 1.261 + } 1.262 + 1.263 + public void setJSONForAddons(JSONObject json) { 1.264 + addons = json; 1.265 + } 1.266 + 1.267 + /** 1.268 + * Includes ignored add-ons. 1.269 + */ 1.270 + public String getNormalizedAddonsJSON() { 1.271 + // We trust that our input will already be normalized. If that assumption 1.272 + // is invalidated, then we'll be sorry. 1.273 + return (addons == null) ? "null" : addons.toString(); 1.274 + } 1.275 +}