mobile/android/base/background/healthreport/HealthReportDatabaseStorage.java

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/mobile/android/base/background/healthreport/HealthReportDatabaseStorage.java	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,1618 @@
     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.File;
    1.11 +import java.util.Collection;
    1.12 +import java.util.HashMap;
    1.13 +import java.util.concurrent.ConcurrentHashMap;
    1.14 +import java.util.concurrent.Executor;
    1.15 +import java.util.concurrent.Executors;
    1.16 +
    1.17 +import org.json.JSONObject;
    1.18 +import org.mozilla.gecko.background.common.DateUtils;
    1.19 +import org.mozilla.gecko.background.common.log.Logger;
    1.20 +import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields.FieldSpec;
    1.21 +
    1.22 +import android.content.ContentValues;
    1.23 +import android.content.Context;
    1.24 +import android.content.ContextWrapper;
    1.25 +import android.database.Cursor;
    1.26 +import android.database.SQLException;
    1.27 +import android.database.sqlite.SQLiteConstraintException;
    1.28 +import android.database.sqlite.SQLiteDatabase;
    1.29 +import android.database.sqlite.SQLiteOpenHelper;
    1.30 +import android.os.Build;
    1.31 +import android.util.SparseArray;
    1.32 +
    1.33 +/**
    1.34 + * <code>HealthReportDatabaseStorage</code> provides an interface on top of
    1.35 + * SQLite storage for Health Report data. It exposes methods for management of
    1.36 + * environments, measurements, fields, and values, and a cursor-based API for
    1.37 + * querying.
    1.38 + *
    1.39 + * Health Report data is structured as follows.
    1.40 + *
    1.41 + * Records are primarily broken down by date, at day granularity. Each day's data
    1.42 + * is then split according to environment. An environment is a collection of
    1.43 + * constant attributes, such as version and processor; if one of these attributes
    1.44 + * changes, a new environment becomes active.
    1.45 + *
    1.46 + * Environments are identified by a stable hash of their attributes.
    1.47 + *
    1.48 + * The database includes a persistent numeric identifier for each environment. Create
    1.49 + * or fetch this identifier via:
    1.50 + *
    1.51 + * <pre>
    1.52 + *  final Environment environment = storage.getEnvironment();
    1.53 + *  // Init the environment now.
    1.54 + *  String envHash = environment.getHash();
    1.55 + *  int env = environment.register();
    1.56 + * </pre>
    1.57 + *
    1.58 + * You can safely cache this environment identifier for the life of the database.
    1.59 + *
    1.60 + * Orthogonal to environments are measurements. Each measurement is a named and
    1.61 + * versioned scope for a collection of fields. It is assumed that if a measurement
    1.62 + * with the given name and version is known to the database, then all of its fields
    1.63 + * are also known; if you change the collection of fields in the measurement, the
    1.64 + * measurement's version must be incremented, too.
    1.65 + *
    1.66 + * As with environments, measurements have an internal numeric identifier.
    1.67 + *
    1.68 + * Calling code should initialize its measurements as follows:
    1.69 + *
    1.70 + * <pre>
    1.71 + *   public static class FooFieldsV1 implements MeasurementFields {
    1.72 + *     {@literal @}Override
    1.73 + *     public Iterable<String> getFields() {
    1.74 + *       ArrayList<String> fields = new ArrayList<String>();
    1.75 + *       fields.add("bar");
    1.76 + *       fields.add("baz");
    1.77 + *       return fields;
    1.78 + *     }
    1.79 + *   }
    1.80 + *
    1.81 + *   storage.beginInitialization();
    1.82 + *
    1.83 + *   try {
    1.84 + *     storage.ensureMeasurementInitialized("org.mozilla.fooProvider.fooMeasurement",
    1.85 + *                                          1, new FooFieldsV1());
    1.86 + *     storage.finishInitialization();
    1.87 + *   } catch (Exception e) {
    1.88 + *     storage.abortInitialization();
    1.89 + *   }
    1.90 + * </pre>
    1.91 + *
    1.92 + * Measurements have fields. Fields can conceptually be divided into "daily last"
    1.93 + * (we only care about the last value), "daily counter" (increments per day),
    1.94 + * "daily discrete" (multiple records per day). Simply call the correct method for each.
    1.95 + *
    1.96 + * To do so you need a field ID, to avoid constant costly string lookups. You can get
    1.97 + * this value from storage:
    1.98 + *
    1.99 + * <pre>
   1.100 + *   Field field = storage.getField("org.mozilla.fooProvider.fooMeasurement", 1, "bar");
   1.101 + *   int fieldID = field.getID();
   1.102 + * </pre>
   1.103 + *
   1.104 + * This lookup is cached, and so is relatively inexpensive.
   1.105 + *
   1.106 + * You can then do something like the following:
   1.107 + *
   1.108 + * <pre>
   1.109 + *   storage.recordDailyLast(storage.getDay(), env, fieldID, "last value");
   1.110 + * </pre>
   1.111 + *
   1.112 + * or equivalently for numeric values, discrete or counters, etc.
   1.113 + *
   1.114 + * To retrieve values, use {@link #getRawEventsSince(long)}.
   1.115 + *
   1.116 + * For safety, perform operations on the storage executor thread:
   1.117 + *
   1.118 + * <pre>
   1.119 + *   storage.enqueueOperation(runnable);
   1.120 + * </pre>
   1.121 + */
   1.122 +public class HealthReportDatabaseStorage implements HealthReportStorage {
   1.123 +
   1.124 +  private static final String WHERE_DATE_AND_ENV_AND_FIELD = "date = ? AND env = ? AND field = ?";
   1.125 +
   1.126 +  public static final String[] COLUMNS_HASH = new String[] {"hash"};
   1.127 +  public static final String[] COLUMNS_DATE_ENV_FIELD_VALUE = new String[] {"date", "env", "field", "value"};
   1.128 +  public static final String[] COLUMNS_DATE_ENVSTR_M_MV_F_VALUE = new String[] {
   1.129 +    "date", "environment", "measurement_name", "measurement_version",
   1.130 +    "field_name", "field_flags", "value"
   1.131 +  };
   1.132 +
   1.133 +  private static final String[] COLUMNS_ENVIRONMENT_DETAILS = new String[] {
   1.134 +      "id", "version", "hash",
   1.135 +      "profileCreation", "cpuCount", "memoryMB",
   1.136 +
   1.137 +      "isBlocklistEnabled", "isTelemetryEnabled", "extensionCount",
   1.138 +      "pluginCount", "themeCount",
   1.139 +
   1.140 +      "architecture", "sysName", "sysVersion", "vendor", "appName", "appID",
   1.141 +      "appVersion", "appBuildID", "platformVersion", "platformBuildID", "os",
   1.142 +      "xpcomabi", "updateChannel",
   1.143 +
   1.144 +      "distribution", "osLocale", "appLocale", "acceptLangSet",
   1.145 +
   1.146 +      // Joined to the add-ons table.
   1.147 +      "addonsBody"
   1.148 +  };
   1.149 +
   1.150 +  public static final String[] COLUMNS_MEASUREMENT_DETAILS = new String[] {"id", "name", "version"};
   1.151 +  public static final String[] COLUMNS_MEASUREMENT_AND_FIELD_DETAILS =
   1.152 +      new String[] {"measurement_name", "measurement_id", "measurement_version",
   1.153 +                    "field_name", "field_id", "field_flags"};
   1.154 +
   1.155 +  private static final String[] COLUMNS_VALUE = new String[] {"value"};
   1.156 +  private static final String[] COLUMNS_ID = new String[] {"id"};
   1.157 +
   1.158 +  private static final String EVENTS_TEXTUAL = "events_textual";
   1.159 +  private static final String EVENTS_INTEGER = "events_integer";
   1.160 +
   1.161 +  protected static final String DB_NAME = "health.db";
   1.162 +
   1.163 +  private static final String LOG_TAG = "HealthReportStorage";
   1.164 +
   1.165 +  private final Executor executor = Executors.newSingleThreadExecutor();
   1.166 +
   1.167 +  @Override
   1.168 +  public void enqueueOperation(Runnable runnable) {
   1.169 +    executor.execute(runnable);
   1.170 +  }
   1.171 +
   1.172 +  public HealthReportDatabaseStorage(final Context context,
   1.173 +                                     final File profileDirectory) {
   1.174 +    this.helper = new HealthReportSQLiteOpenHelper(context, profileDirectory,
   1.175 +                                                   DB_NAME);
   1.176 +    executor.execute(new Runnable() {
   1.177 +      @Override
   1.178 +      public void run() {
   1.179 +        Logger.setThreadLogTag(HealthReportConstants.GLOBAL_LOG_TAG);
   1.180 +        Logger.debug(LOG_TAG, "Creating HealthReportDatabaseStorage.");
   1.181 +      }
   1.182 +    });
   1.183 +  }
   1.184 +
   1.185 +  @Override
   1.186 +  public void close() {
   1.187 +    this.helper.close();
   1.188 +    this.fields.clear();
   1.189 +    this.envs.clear();
   1.190 +    this.measurementVersions.clear();
   1.191 +  }
   1.192 +
   1.193 +  protected final HealthReportSQLiteOpenHelper helper;
   1.194 +
   1.195 +  public static class HealthReportSQLiteOpenHelper extends SQLiteOpenHelper {
   1.196 +    public static final int CURRENT_VERSION = 6;
   1.197 +    public static final String LOG_TAG = "HealthReportSQL";
   1.198 +
   1.199 +    /**
   1.200 +     * A little helper to avoid SQLiteOpenHelper misbehaving on Android 2.1.
   1.201 +     * Partly cribbed from
   1.202 +     * <http://stackoverflow.com/questions/5332328/sqliteopenhelper-problem-with-fully-qualified-db-path-name>.
   1.203 +     */
   1.204 +    public static class AbsolutePathContext extends ContextWrapper {
   1.205 +      private final File parent;
   1.206 +
   1.207 +      public AbsolutePathContext(Context base, File parent) {
   1.208 +        super(base);
   1.209 +        this.parent = parent;
   1.210 +      }
   1.211 +
   1.212 +      @Override
   1.213 +      public File getDatabasePath(String name) {
   1.214 +        return new File(getAbsolutePath(parent, name));
   1.215 +      }
   1.216 +
   1.217 +      // Won't be called after API v11, but we can't override the version that
   1.218 +      // *is* called and still support v8.
   1.219 +      // Instead we check the version code in the HealthReportSQLiteOpenHelper
   1.220 +      // constructor, and only use this workaround if we need to.
   1.221 +      @Override
   1.222 +      public SQLiteDatabase openOrCreateDatabase(String name,
   1.223 +                                                 int mode,
   1.224 +                                                 SQLiteDatabase.CursorFactory factory) {
   1.225 +        final File path = getDatabasePath(name);
   1.226 +        Logger.pii(LOG_TAG, "Opening database through absolute path " + path.getAbsolutePath());
   1.227 +        return SQLiteDatabase.openOrCreateDatabase(path, null);
   1.228 +      }
   1.229 +    }
   1.230 +
   1.231 +    public static String getAbsolutePath(File parent, String name) {
   1.232 +      return parent.getAbsolutePath() + File.separator + name;
   1.233 +    }
   1.234 +
   1.235 +    public static boolean CAN_USE_ABSOLUTE_DB_PATH = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO);
   1.236 +    public HealthReportSQLiteOpenHelper(Context context, File profileDirectory, String name) {
   1.237 +      this(context, profileDirectory, name, CURRENT_VERSION);
   1.238 +    }
   1.239 +
   1.240 +    // For testing DBs of different versions.
   1.241 +    public HealthReportSQLiteOpenHelper(Context context, File profileDirectory, String name, int version) {
   1.242 +      super(
   1.243 +          (CAN_USE_ABSOLUTE_DB_PATH ? context : new AbsolutePathContext(context, profileDirectory)),
   1.244 +          (CAN_USE_ABSOLUTE_DB_PATH ? getAbsolutePath(profileDirectory, name) : name),
   1.245 +          null,
   1.246 +          version);
   1.247 +
   1.248 +      if (CAN_USE_ABSOLUTE_DB_PATH) {
   1.249 +        Logger.pii(LOG_TAG, "Opening: " + getAbsolutePath(profileDirectory, name));
   1.250 +      }
   1.251 +    }
   1.252 +
   1.253 +    @Override
   1.254 +    public void onCreate(SQLiteDatabase db) {
   1.255 +      db.execSQL("CREATE TABLE addons (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
   1.256 +                 "                     body TEXT, " +
   1.257 +                 "                     UNIQUE (body) " +
   1.258 +                 ")");
   1.259 +
   1.260 +      // N.B., hash collisions can occur across versions. In that case, the system
   1.261 +      // is likely to persist the original environment version.
   1.262 +      db.execSQL("CREATE TABLE environments (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
   1.263 +                 "                           version INTEGER, " +
   1.264 +                 "                           hash TEXT, " +
   1.265 +                 "                           profileCreation INTEGER, " +
   1.266 +                 "                           cpuCount        INTEGER, " +
   1.267 +                 "                           memoryMB        INTEGER, " +
   1.268 +                 "                           isBlocklistEnabled INTEGER, " +
   1.269 +                 "                           isTelemetryEnabled INTEGER, " +
   1.270 +                 "                           extensionCount     INTEGER, " +
   1.271 +                 "                           pluginCount        INTEGER, " +
   1.272 +                 "                           themeCount         INTEGER, " +
   1.273 +                 "                           architecture    TEXT, " +
   1.274 +                 "                           sysName         TEXT, " +
   1.275 +                 "                           sysVersion      TEXT, " +
   1.276 +                 "                           vendor          TEXT, " +
   1.277 +                 "                           appName         TEXT, " +
   1.278 +                 "                           appID           TEXT, " +
   1.279 +                 "                           appVersion      TEXT, " +
   1.280 +                 "                           appBuildID      TEXT, " +
   1.281 +                 "                           platformVersion TEXT, " +
   1.282 +                 "                           platformBuildID TEXT, " +
   1.283 +                 "                           os              TEXT, " +
   1.284 +                 "                           xpcomabi        TEXT, " +
   1.285 +                 "                           updateChannel   TEXT, " +
   1.286 +
   1.287 +                 "                           distribution    TEXT, " +
   1.288 +                 "                           osLocale        TEXT, " +
   1.289 +                 "                           appLocale       TEXT, " +
   1.290 +                 "                           acceptLangSet   INTEGER, " +
   1.291 +
   1.292 +                 "                           addonsID        INTEGER, " +
   1.293 +                 "                           FOREIGN KEY (addonsID) REFERENCES addons(id) ON DELETE RESTRICT, " +
   1.294 +                 "                           UNIQUE (hash) " +
   1.295 +                 ")");
   1.296 +
   1.297 +      db.execSQL("CREATE TABLE measurements (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
   1.298 +                 "                           name TEXT, " +
   1.299 +                 "                           version INTEGER, " +
   1.300 +                 "                           UNIQUE (name, version) " +
   1.301 +                 ")");
   1.302 +
   1.303 +      db.execSQL("CREATE TABLE fields (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
   1.304 +                 "                     measurement INTEGER, " +
   1.305 +                 "                     name TEXT, " +
   1.306 +                 "                     flags INTEGER, " +
   1.307 +                 "                     FOREIGN KEY (measurement) REFERENCES measurements(id) ON DELETE CASCADE, " +
   1.308 +                 "                     UNIQUE (measurement, name)" +
   1.309 +                 ")");
   1.310 +
   1.311 +      db.execSQL("CREATE TABLE " + EVENTS_INTEGER + "(" +
   1.312 +                 "                 date  INTEGER, " +
   1.313 +                 "                 env   INTEGER, " +
   1.314 +                 "                 field INTEGER, " +
   1.315 +                 "                 value INTEGER, " +
   1.316 +                 "                 FOREIGN KEY (field) REFERENCES fields(id) ON DELETE CASCADE, " +
   1.317 +                 "                 FOREIGN KEY (env) REFERENCES environments(id) ON DELETE CASCADE" +
   1.318 +                 ")");
   1.319 +
   1.320 +      db.execSQL("CREATE TABLE " + EVENTS_TEXTUAL + "(" +
   1.321 +                 "                 date  INTEGER, " +
   1.322 +                 "                 env   INTEGER, " +
   1.323 +                 "                 field INTEGER, " +
   1.324 +                 "                 value TEXT, " +
   1.325 +                 "                 FOREIGN KEY (field) REFERENCES fields(id) ON DELETE CASCADE, " +
   1.326 +                 "                 FOREIGN KEY (env) REFERENCES environments(id) ON DELETE CASCADE" +
   1.327 +                 ")");
   1.328 +
   1.329 +      db.execSQL("CREATE INDEX idx_events_integer_date_env_field ON events_integer (date, env, field)");
   1.330 +      db.execSQL("CREATE INDEX idx_events_textual_date_env_field ON events_textual (date, env, field)");
   1.331 +
   1.332 +      db.execSQL("CREATE VIEW events AS " +
   1.333 +                 "SELECT date, env, field, value FROM " + EVENTS_INTEGER + " " +
   1.334 +                 "UNION ALL " +
   1.335 +                 "SELECT date, env, field, value FROM " + EVENTS_TEXTUAL);
   1.336 +
   1.337 +      db.execSQL("CREATE VIEW named_events AS " +
   1.338 +                 "SELECT date, " +
   1.339 +                 "       environments.hash AS environment, " +
   1.340 +                 "       measurements.name AS measurement_name, " +
   1.341 +                 "       measurements.version AS measurement_version, " +
   1.342 +                 "       fields.name AS field_name, " +
   1.343 +                 "       fields.flags AS field_flags, " +
   1.344 +                 "       value FROM " +
   1.345 +                 "events JOIN environments ON events.env = environments.id " +
   1.346 +                 "       JOIN fields ON events.field = fields.id " +
   1.347 +                 "       JOIN measurements ON fields.measurement = measurements.id");
   1.348 +
   1.349 +      db.execSQL("CREATE VIEW named_fields AS " +
   1.350 +                 "SELECT measurements.name AS measurement_name, " +
   1.351 +                 "       measurements.id AS measurement_id, " +
   1.352 +                 "       measurements.version AS measurement_version, " +
   1.353 +                 "       fields.name AS field_name, " +
   1.354 +                 "       fields.id AS field_id, " +
   1.355 +                 "       fields.flags AS field_flags " +
   1.356 +                 "FROM fields JOIN measurements ON fields.measurement = measurements.id");
   1.357 +
   1.358 +      db.execSQL("CREATE VIEW current_measurements AS " +
   1.359 +                 "SELECT name, MAX(version) AS version FROM measurements GROUP BY name");
   1.360 +
   1.361 +      createAddonsEnvironmentsView(db);
   1.362 +    }
   1.363 +
   1.364 +    @Override
   1.365 +    public void onOpen(SQLiteDatabase db) {
   1.366 +      if (!db.isReadOnly()) {
   1.367 +        db.execSQL("PRAGMA foreign_keys=ON;");
   1.368 +      }
   1.369 +    }
   1.370 +
   1.371 +    private void createAddonsEnvironmentsView(SQLiteDatabase db) {
   1.372 +      db.execSQL("CREATE VIEW environments_with_addons AS " +
   1.373 +          "SELECT e.id AS id, " +
   1.374 +          "       e.version AS version, " +
   1.375 +          "       e.hash AS hash, " +
   1.376 +          "       e.profileCreation AS profileCreation, " +
   1.377 +          "       e.cpuCount AS cpuCount, " +
   1.378 +          "       e.memoryMB AS memoryMB, " +
   1.379 +          "       e.isBlocklistEnabled AS isBlocklistEnabled, " +
   1.380 +          "       e.isTelemetryEnabled AS isTelemetryEnabled, " +
   1.381 +          "       e.extensionCount AS extensionCount, " +
   1.382 +          "       e.pluginCount AS pluginCount, " +
   1.383 +          "       e.themeCount AS themeCount, " +
   1.384 +          "       e.architecture AS architecture, " +
   1.385 +          "       e.sysName AS sysName, " +
   1.386 +          "       e.sysVersion AS sysVersion, " +
   1.387 +          "       e.vendor AS vendor, " +
   1.388 +          "       e.appName AS appName, " +
   1.389 +          "       e.appID AS appID, " +
   1.390 +          "       e.appVersion AS appVersion, " +
   1.391 +          "       e.appBuildID AS appBuildID, " +
   1.392 +          "       e.platformVersion AS platformVersion, " +
   1.393 +          "       e.platformBuildID AS platformBuildID, " +
   1.394 +          "       e.os AS os, " +
   1.395 +          "       e.xpcomabi AS xpcomabi, " +
   1.396 +          "       e.updateChannel AS updateChannel, " +
   1.397 +          "       e.distribution AS distribution, " +
   1.398 +          "       e.osLocale AS osLocale, " +
   1.399 +          "       e.appLocale AS appLocale, " +
   1.400 +          "       e.acceptLangSet AS acceptLangSet, " +
   1.401 +          "       addons.body AS addonsBody " +
   1.402 +          "FROM environments AS e, addons " +
   1.403 +          "WHERE e.addonsID = addons.id");
   1.404 +    }
   1.405 +
   1.406 +    private void upgradeDatabaseFrom2To3(SQLiteDatabase db) {
   1.407 +      db.execSQL("CREATE TABLE addons (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
   1.408 +                 "                     body TEXT, " +
   1.409 +                 "                     UNIQUE (body) " +
   1.410 +                 ")");
   1.411 +
   1.412 +      db.execSQL("ALTER TABLE environments ADD COLUMN addonsID INTEGER REFERENCES addons(id) ON DELETE RESTRICT");
   1.413 +
   1.414 +      createAddonsEnvironmentsView(db);
   1.415 +    }
   1.416 +
   1.417 +    private void upgradeDatabaseFrom3To4(SQLiteDatabase db) {
   1.418 +      // Update search measurements to use a different type.
   1.419 +      db.execSQL("UPDATE OR IGNORE fields SET flags = " + Field.TYPE_COUNTED_STRING_DISCRETE +
   1.420 +                 " WHERE measurement IN (SELECT id FROM measurements WHERE name = 'org.mozilla.searches.counts')");
   1.421 +    }
   1.422 +
   1.423 +    private void upgradeDatabaseFrom4to5(SQLiteDatabase db) {
   1.424 +      // Delete NULL in addons.body, which appeared as a result of Bug 886156. Note that the
   1.425 +      // foreign key constraint, "ON DELETE RESTRICT", may be violated, but since onOpen() is
   1.426 +      // called after this method, foreign keys are not yet enabled and constraints can be broken.
   1.427 +      db.delete("addons", "body IS NULL", null);
   1.428 +
   1.429 +      // Purge any data inconsistent with foreign key references (which may have appeared before
   1.430 +      // foreign keys were enabled in Bug 900289).
   1.431 +      db.delete("fields", "measurement NOT IN (SELECT id FROM measurements)", null);
   1.432 +      db.delete("environments", "addonsID NOT IN (SELECT id from addons)", null);
   1.433 +      db.delete(EVENTS_INTEGER, "env NOT IN (SELECT id FROM environments)", null);
   1.434 +      db.delete(EVENTS_TEXTUAL, "env NOT IN (SELECT id FROM environments)", null);
   1.435 +      db.delete(EVENTS_INTEGER, "field NOT IN (SELECT id FROM fields)", null);
   1.436 +      db.delete(EVENTS_TEXTUAL, "field NOT IN (SELECT id FROM fields)", null);
   1.437 +    }
   1.438 +
   1.439 +    private void upgradeDatabaseFrom5to6(SQLiteDatabase db) {
   1.440 +      db.execSQL("DROP VIEW environments_with_addons");
   1.441 +
   1.442 +      // Add version to environment (default to 1).
   1.443 +      db.execSQL("ALTER TABLE environments ADD COLUMN version INTEGER DEFAULT 1");
   1.444 +
   1.445 +      // Add fields to environment (default to empty string).
   1.446 +      db.execSQL("ALTER TABLE environments ADD COLUMN distribution TEXT DEFAULT ''");
   1.447 +      db.execSQL("ALTER TABLE environments ADD COLUMN osLocale TEXT DEFAULT ''");
   1.448 +      db.execSQL("ALTER TABLE environments ADD COLUMN appLocale TEXT DEFAULT ''");
   1.449 +      db.execSQL("ALTER TABLE environments ADD COLUMN acceptLangSet INTEGER DEFAULT 0");
   1.450 +
   1.451 +      // Recreate view.
   1.452 +      createAddonsEnvironmentsView(db);
   1.453 +    }
   1.454 +
   1.455 +    @Override
   1.456 +    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
   1.457 +      if (oldVersion >= newVersion) {
   1.458 +        return;
   1.459 +      }
   1.460 +
   1.461 +      Logger.info(LOG_TAG, "onUpgrade: from " + oldVersion + " to " + newVersion + ".");
   1.462 +      try {
   1.463 +        switch (oldVersion) {
   1.464 +        case 2:
   1.465 +          upgradeDatabaseFrom2To3(db);
   1.466 +        case 3:
   1.467 +          upgradeDatabaseFrom3To4(db);
   1.468 +        case 4:
   1.469 +          upgradeDatabaseFrom4to5(db);
   1.470 +        case 5:
   1.471 +          upgradeDatabaseFrom5to6(db);
   1.472 +        }
   1.473 +      } catch (Exception e) {
   1.474 +        Logger.error(LOG_TAG, "Failure in onUpgrade.", e);
   1.475 +        throw new RuntimeException(e);
   1.476 +      }
   1.477 +   }
   1.478 +
   1.479 +    public void deleteEverything() {
   1.480 +      final SQLiteDatabase db = this.getWritableDatabase();
   1.481 +
   1.482 +      Logger.info(LOG_TAG, "Deleting everything.");
   1.483 +      db.beginTransaction();
   1.484 +      try {
   1.485 +        // Cascade will clear the rest.
   1.486 +        db.delete("measurements", null, null);
   1.487 +        db.delete("environments", null, null);
   1.488 +        db.delete("addons", null, null);
   1.489 +        db.setTransactionSuccessful();
   1.490 +        Logger.info(LOG_TAG, "Deletion successful.");
   1.491 +      } finally {
   1.492 +        db.endTransaction();
   1.493 +      }
   1.494 +    }
   1.495 +  }
   1.496 +
   1.497 +  public class DatabaseField extends Field {
   1.498 +    public DatabaseField(String mName, int mVersion, String fieldName) {
   1.499 +      this(mName, mVersion, fieldName, UNKNOWN_TYPE_OR_FIELD_ID, UNKNOWN_TYPE_OR_FIELD_ID);
   1.500 +    }
   1.501 +
   1.502 +    public DatabaseField(String mName, int mVersion, String fieldName, int flags) {
   1.503 +      this(mName, mVersion, fieldName, UNKNOWN_TYPE_OR_FIELD_ID, flags);
   1.504 +    }
   1.505 +
   1.506 +    public DatabaseField(String mName, int mVersion, String fieldName, int fieldID, int flags) {
   1.507 +      super(mName, mVersion, fieldName, flags);
   1.508 +      this.fieldID = fieldID;
   1.509 +    }
   1.510 +
   1.511 +    private void loadFlags() {
   1.512 +      if (this.flags == UNKNOWN_TYPE_OR_FIELD_ID) {
   1.513 +        if (this.fieldID == UNKNOWN_TYPE_OR_FIELD_ID) {
   1.514 +          this.getID();
   1.515 +        }
   1.516 +        this.flags = integerQuery("fields", "flags", "id = ?", new String[] { Integer.toString(this.fieldID, 10) }, -1);
   1.517 +      }
   1.518 +    }
   1.519 +
   1.520 +    @Override
   1.521 +    public synchronized boolean isIntegerField() {
   1.522 +      loadFlags();
   1.523 +      return super.isIntegerField();
   1.524 +    }
   1.525 +
   1.526 +    @Override
   1.527 +    public synchronized boolean isStringField() {
   1.528 +      loadFlags();
   1.529 +      return super.isStringField();
   1.530 +    }
   1.531 +
   1.532 +    @Override
   1.533 +    public synchronized boolean isDiscreteField() {
   1.534 +      loadFlags();
   1.535 +      return super.isDiscreteField();
   1.536 +    }
   1.537 +
   1.538 +    @Override
   1.539 +    public synchronized int getID() throws IllegalStateException {
   1.540 +      if (this.fieldID == UNKNOWN_TYPE_OR_FIELD_ID) {
   1.541 +        this.fieldID = integerQuery("named_fields", "field_id",
   1.542 +                                    "measurement_name = ? AND measurement_version = ? AND field_name = ?",
   1.543 +                                    new String[] {measurementName, measurementVersion, fieldName},
   1.544 +                                    UNKNOWN_TYPE_OR_FIELD_ID);
   1.545 +        if (this.fieldID == UNKNOWN_TYPE_OR_FIELD_ID) {
   1.546 +          throw new IllegalStateException("No field with name " + fieldName +
   1.547 +                                          " (" + measurementName + ", " + measurementVersion + ")");
   1.548 +        }
   1.549 +      }
   1.550 +      return this.fieldID;
   1.551 +    }
   1.552 +  }
   1.553 +
   1.554 +  // `envs` and `fields` look similar, but they are touched differently and
   1.555 +  // store differently stable kinds of data, hence type difference.
   1.556 +  // Note that we don't pre-populate the environment cache. We'll typically only
   1.557 +  // handle one per session.
   1.558 +  //
   1.559 +  // protected for testing purposes only.
   1.560 +  protected final ConcurrentHashMap<String, Integer> envs = new ConcurrentHashMap<String, Integer>();
   1.561 +
   1.562 +  /**
   1.563 +   * An {@link Environment} that knows how to persist to and from our database.
   1.564 +   */
   1.565 +  public static class DatabaseEnvironment extends Environment {
   1.566 +    protected final HealthReportDatabaseStorage storage;
   1.567 +
   1.568 +    @Override
   1.569 +    public int register() {
   1.570 +      final String h = getHash();
   1.571 +      if (storage.envs.containsKey(h)) {
   1.572 +        this.id = storage.envs.get(h);
   1.573 +        return this.id;
   1.574 +      }
   1.575 +
   1.576 +      // Otherwise, add data and hash to the DB.
   1.577 +      ContentValues v = new ContentValues();
   1.578 +      v.put("version", version);
   1.579 +      v.put("hash", h);
   1.580 +      v.put("profileCreation", profileCreation);
   1.581 +      v.put("cpuCount", cpuCount);
   1.582 +      v.put("memoryMB", memoryMB);
   1.583 +      v.put("isBlocklistEnabled", isBlocklistEnabled);
   1.584 +      v.put("isTelemetryEnabled", isTelemetryEnabled);
   1.585 +      v.put("extensionCount", extensionCount);
   1.586 +      v.put("pluginCount", pluginCount);
   1.587 +      v.put("themeCount", themeCount);
   1.588 +      v.put("architecture", architecture);
   1.589 +      v.put("sysName", sysName);
   1.590 +      v.put("sysVersion", sysVersion);
   1.591 +      v.put("vendor", vendor);
   1.592 +      v.put("appName", appName);
   1.593 +      v.put("appID", appID);
   1.594 +      v.put("appVersion", appVersion);
   1.595 +      v.put("appBuildID", appBuildID);
   1.596 +      v.put("platformVersion", platformVersion);
   1.597 +      v.put("platformBuildID", platformBuildID);
   1.598 +      v.put("os", os);
   1.599 +      v.put("xpcomabi", xpcomabi);
   1.600 +      v.put("updateChannel", updateChannel);
   1.601 +      v.put("distribution", distribution);
   1.602 +      v.put("osLocale", osLocale);
   1.603 +      v.put("appLocale", appLocale);
   1.604 +      v.put("acceptLangSet", acceptLangSet);
   1.605 +
   1.606 +      final SQLiteDatabase db = storage.helper.getWritableDatabase();
   1.607 +
   1.608 +      // If we're not already, we want all of our inserts to be in a transaction.
   1.609 +      boolean newTransaction = !db.inTransaction();
   1.610 +
   1.611 +      // Insert, with a little error handling to populate the cache in case of
   1.612 +      // omission and consequent collision.
   1.613 +      //
   1.614 +      // We would like to hang a trigger off a view here, and just use that for
   1.615 +      // inserts. But triggers don't seem to correctly update the last inserted
   1.616 +      // ID, so Android's insertOrThrow method returns -1.
   1.617 +      //
   1.618 +      // Instead, we go without the trigger, simply running the inserts ourselves.
   1.619 +      //
   1.620 +      // insertWithOnConflict doesn't work as documented: <http://stackoverflow.com/questions/11328877/android-sqllite-on-conflict-ignore-is-ignored-in-ics/11424150>.
   1.621 +      // So we do this the hard way.
   1.622 +      // We presume that almost every get will hit the cache (except for the first, obviously), so we
   1.623 +      // bias here towards inserts for the environments.
   1.624 +      // For add-ons we assume that different environments will share add-ons, so we query first.
   1.625 +
   1.626 +      final String addonsJSON = getNormalizedAddonsJSON();
   1.627 +      if (newTransaction) {
   1.628 +        db.beginTransaction();
   1.629 +      }
   1.630 +
   1.631 +      try {
   1.632 +        int addonsID = ensureAddons(db, addonsJSON);
   1.633 +        v.put("addonsID", addonsID);
   1.634 +
   1.635 +        try {
   1.636 +          int inserted = (int) db.insertOrThrow("environments", null, v);
   1.637 +          Logger.debug(LOG_TAG, "Inserted ID: " + inserted + " for hash " + h);
   1.638 +          if (inserted == -1) {
   1.639 +            throw new SQLException("Insert returned -1!");
   1.640 +          }
   1.641 +          this.id = inserted;
   1.642 +          storage.envs.put(h, this.id);
   1.643 +          if (newTransaction) {
   1.644 +            db.setTransactionSuccessful();
   1.645 +          }
   1.646 +          return inserted;
   1.647 +        } catch (SQLException e) {
   1.648 +          // The inserter should take care of updating `envs`. But if it
   1.649 +          // doesn't...
   1.650 +          Cursor c = db.query("environments", COLUMNS_ID, "hash = ?",
   1.651 +                              new String[] { h }, null, null, null);
   1.652 +          try {
   1.653 +            if (!c.moveToFirst()) {
   1.654 +              throw e;
   1.655 +            }
   1.656 +            this.id = (int) c.getLong(0);
   1.657 +            Logger.debug(LOG_TAG, "Found " + this.id + " for hash " + h);
   1.658 +            storage.envs.put(h, this.id);
   1.659 +            if (newTransaction) {
   1.660 +              db.setTransactionSuccessful();
   1.661 +            }
   1.662 +            return this.id;
   1.663 +          } finally {
   1.664 +            c.close();
   1.665 +          }
   1.666 +        }
   1.667 +      } finally {
   1.668 +        if (newTransaction) {
   1.669 +          db.endTransaction();
   1.670 +        }
   1.671 +      }
   1.672 +    }
   1.673 +
   1.674 +    protected static int ensureAddons(SQLiteDatabase db, String json) {
   1.675 +      Cursor c = db.query("addons", COLUMNS_ID, "body = ?",
   1.676 +                          new String[] { (json == null) ? "null" : json }, null, null, null);
   1.677 +      try {
   1.678 +        if (c.moveToFirst()) {
   1.679 +          return c.getInt(0);
   1.680 +        }
   1.681 +        ContentValues values = new ContentValues();
   1.682 +        values.put("body", json);
   1.683 +        return (int) db.insert("addons", null, values);
   1.684 +      } finally {
   1.685 +        c.close();
   1.686 +      }
   1.687 +    }
   1.688 +
   1.689 +    public void init(ContentValues v) {
   1.690 +      version         = v.containsKey("version") ? v.getAsInteger("version") : Environment.CURRENT_VERSION;
   1.691 +      profileCreation = v.getAsInteger("profileCreation");
   1.692 +      cpuCount        = v.getAsInteger("cpuCount");
   1.693 +      memoryMB        = v.getAsInteger("memoryMB");
   1.694 +
   1.695 +      isBlocklistEnabled = v.getAsInteger("isBlocklistEnabled");
   1.696 +      isTelemetryEnabled = v.getAsInteger("isTelemetryEnabled");
   1.697 +      extensionCount     = v.getAsInteger("extensionCount");
   1.698 +      pluginCount        = v.getAsInteger("pluginCount");
   1.699 +      themeCount         = v.getAsInteger("themeCount");
   1.700 +
   1.701 +      architecture    = v.getAsString("architecture");
   1.702 +      sysName         = v.getAsString("sysName");
   1.703 +      sysVersion      = v.getAsString("sysVersion");
   1.704 +      vendor          = v.getAsString("vendor");
   1.705 +      appName         = v.getAsString("appName");
   1.706 +      appID           = v.getAsString("appID");
   1.707 +      appVersion      = v.getAsString("appVersion");
   1.708 +      appBuildID      = v.getAsString("appBuildID");
   1.709 +      platformVersion = v.getAsString("platformVersion");
   1.710 +      platformBuildID = v.getAsString("platformBuildID");
   1.711 +      os              = v.getAsString("os");
   1.712 +      xpcomabi        = v.getAsString("xpcomabi");
   1.713 +      updateChannel   = v.getAsString("updateChannel");
   1.714 +
   1.715 +      distribution    = v.getAsString("distribution");
   1.716 +      osLocale        = v.getAsString("osLocale");
   1.717 +      appLocale       = v.getAsString("appLocale");
   1.718 +      acceptLangSet   = v.getAsInteger("acceptLangSet");
   1.719 +
   1.720 +      try {
   1.721 +        setJSONForAddons(v.getAsString("addonsBody"));
   1.722 +      } catch (Exception e) {
   1.723 +        // Nothing we can do.
   1.724 +      }
   1.725 +
   1.726 +      this.hash = null;
   1.727 +      this.id = -1;
   1.728 +    }
   1.729 +
   1.730 +    /**
   1.731 +     * Fill ourselves with data from the DB, then advance the cursor.
   1.732 +     *
   1.733 +     * @param cursor a {@link Cursor} pointing at a record to load.
   1.734 +     * @return true if the cursor was successfully advanced.
   1.735 +     */
   1.736 +    public boolean init(Cursor cursor) {
   1.737 +      int i = 0;
   1.738 +      this.id         = cursor.getInt(i++);
   1.739 +      this.version    = cursor.getInt(i++);
   1.740 +      this.hash       = cursor.getString(i++);
   1.741 +
   1.742 +      profileCreation = cursor.getInt(i++);
   1.743 +      cpuCount        = cursor.getInt(i++);
   1.744 +      memoryMB        = cursor.getInt(i++);
   1.745 +
   1.746 +      isBlocklistEnabled = cursor.getInt(i++);
   1.747 +      isTelemetryEnabled = cursor.getInt(i++);
   1.748 +      extensionCount     = cursor.getInt(i++);
   1.749 +      pluginCount        = cursor.getInt(i++);
   1.750 +      themeCount         = cursor.getInt(i++);
   1.751 +
   1.752 +      architecture    = cursor.getString(i++);
   1.753 +      sysName         = cursor.getString(i++);
   1.754 +      sysVersion      = cursor.getString(i++);
   1.755 +      vendor          = cursor.getString(i++);
   1.756 +      appName         = cursor.getString(i++);
   1.757 +      appID           = cursor.getString(i++);
   1.758 +      appVersion      = cursor.getString(i++);
   1.759 +      appBuildID      = cursor.getString(i++);
   1.760 +      platformVersion = cursor.getString(i++);
   1.761 +      platformBuildID = cursor.getString(i++);
   1.762 +      os              = cursor.getString(i++);
   1.763 +      xpcomabi        = cursor.getString(i++);
   1.764 +      updateChannel   = cursor.getString(i++);
   1.765 +
   1.766 +      distribution    = cursor.getString(i++);
   1.767 +      osLocale        = cursor.getString(i++);
   1.768 +      appLocale       = cursor.getString(i++);
   1.769 +      acceptLangSet   = cursor.getInt(i++);
   1.770 +
   1.771 +      try {
   1.772 +        setJSONForAddons(cursor.getBlob(i++));
   1.773 +      } catch (Exception e) {
   1.774 +        // Nothing we can do.
   1.775 +      }
   1.776 +
   1.777 +      return cursor.moveToNext();
   1.778 +    }
   1.779 +
   1.780 +    public DatabaseEnvironment(HealthReportDatabaseStorage storage, Class<? extends EnvironmentAppender> appender) {
   1.781 +      super(appender);
   1.782 +      this.storage = storage;
   1.783 +    }
   1.784 +
   1.785 +    public DatabaseEnvironment(HealthReportDatabaseStorage storage) {
   1.786 +      this.storage = storage;
   1.787 +    }
   1.788 +  }
   1.789 +
   1.790 +  /**
   1.791 +   * Factory method. Returns a new {@link Environment} that callers can
   1.792 +   * populate and then register.
   1.793 +   */
   1.794 +  @Override
   1.795 +  public DatabaseEnvironment getEnvironment() {
   1.796 +    return new DatabaseEnvironment(this);
   1.797 +  }
   1.798 +
   1.799 +  @Override
   1.800 +  public SparseArray<Environment> getEnvironmentRecordsByID() {
   1.801 +    final SQLiteDatabase db = this.helper.getReadableDatabase();
   1.802 +    Cursor c = db.query("environments_with_addons", COLUMNS_ENVIRONMENT_DETAILS, null, null, null, null, null);
   1.803 +    try {
   1.804 +      SparseArray<Environment> results = new SparseArray<Environment>();
   1.805 +      if (!c.moveToFirst()) {
   1.806 +        return results;
   1.807 +      }
   1.808 +
   1.809 +      DatabaseEnvironment e = getEnvironment();
   1.810 +      while (e.init(c)) {
   1.811 +        results.put(e.id, e);
   1.812 +        e = getEnvironment();
   1.813 +      }
   1.814 +      results.put(e.id, e);
   1.815 +      return results;
   1.816 +    } finally {
   1.817 +      c.close();
   1.818 +    }
   1.819 +  }
   1.820 +
   1.821 +  /**
   1.822 +   * Reverse lookup for an env. Only really used for tests: document generation
   1.823 +   * fetches all environments at once, and insertion only uses the integer key
   1.824 +   * that's returned during insertion.
   1.825 +   *
   1.826 +   * @param id
   1.827 +   *          the identifier for the environment.
   1.828 +   * @return a cursor over its details.
   1.829 +   */
   1.830 +  @Override
   1.831 +  public Cursor getEnvironmentRecordForID(int id) {
   1.832 +    final SQLiteDatabase db = this.helper.getReadableDatabase();
   1.833 +    return db.query("environments_with_addons", COLUMNS_ENVIRONMENT_DETAILS, "id = " + id, null, null, null, null);
   1.834 +  }
   1.835 +
   1.836 +  @Override
   1.837 +  public SparseArray<String> getEnvironmentHashesByID() {
   1.838 +    final SQLiteDatabase db = this.helper.getReadableDatabase();
   1.839 +    Cursor c = db.query("environments", new String[] {"id", "hash"}, null, null, null, null, null);
   1.840 +    try {
   1.841 +      SparseArray<String> results = new SparseArray<String>();
   1.842 +      if (!c.moveToFirst()) {
   1.843 +        return results;
   1.844 +      }
   1.845 +
   1.846 +      while (!c.isAfterLast()) {
   1.847 +        results.put(c.getInt(0), c.getString(1));
   1.848 +        c.moveToNext();
   1.849 +      }
   1.850 +      return results;
   1.851 +    } finally {
   1.852 +      c.close();
   1.853 +    }
   1.854 +  }
   1.855 +
   1.856 +  /**
   1.857 +   * Cache the lookup from measurement and field specifier to field instance.
   1.858 +   * This allows us to memoize the field ID lookup, too.
   1.859 +   */
   1.860 +  private HashMap<String, Field> fields = new HashMap<String, Field>();
   1.861 +  private boolean fieldsCacheUpdated = false;
   1.862 +
   1.863 +  private void invalidateFieldsCache() {
   1.864 +    synchronized (this.fields) {
   1.865 +      fieldsCacheUpdated = false;
   1.866 +    }
   1.867 +  }
   1.868 +
   1.869 +  private String getFieldKey(String mName, int mVersion, String fieldName) {
   1.870 +    return mVersion + "." + mName + "/" + fieldName;
   1.871 +  }
   1.872 +
   1.873 +  @Override
   1.874 +  public Field getField(String mName, int mVersion, String fieldName) {
   1.875 +    final String key = getFieldKey(mName, mVersion, fieldName);
   1.876 +    synchronized (fields) {
   1.877 +      if (fields.containsKey(key)) {
   1.878 +        return fields.get(key);
   1.879 +      }
   1.880 +      Field f = new DatabaseField(mName, mVersion, fieldName);
   1.881 +      fields.put(key, f);
   1.882 +      return f;
   1.883 +    }
   1.884 +  }
   1.885 +
   1.886 +  private void populateFieldCache() {
   1.887 +    synchronized (fields) {
   1.888 +      if (fieldsCacheUpdated) {
   1.889 +        return;
   1.890 +      }
   1.891 +
   1.892 +      fields.clear();
   1.893 +      Cursor c = getFieldVersions();
   1.894 +      try {
   1.895 +        if (!c.moveToFirst()) {
   1.896 +          return;
   1.897 +        }
   1.898 +        do {
   1.899 +          // We don't use the measurement ID here, so column 1 is unused.
   1.900 +          final String mName = c.getString(0);
   1.901 +          final int mVersion = c.getInt(2);
   1.902 +          final String fieldName = c.getString(3);
   1.903 +          final int fieldID = c.getInt(4);
   1.904 +          final int flags = c.getInt(5);
   1.905 +          final String key = getFieldKey(mName, mVersion, fieldName);
   1.906 +
   1.907 +          Field f = new DatabaseField(mName, mVersion, fieldName, fieldID, flags);
   1.908 +          fields.put(key, f);
   1.909 +        } while (c.moveToNext());
   1.910 +        fieldsCacheUpdated = true;
   1.911 +      } finally {
   1.912 +        c.close();
   1.913 +      }
   1.914 +    }
   1.915 +  }
   1.916 +
   1.917 +  /**
   1.918 +   * Return mappings from field ID to Field instance. Do so by looking in the DB.
   1.919 +   */
   1.920 +  @Override
   1.921 +  public SparseArray<Field> getFieldsByID() {
   1.922 +    final SparseArray<Field> out = new SparseArray<Field>();
   1.923 +    synchronized (fields) {
   1.924 +      populateFieldCache();
   1.925 +      Collection<Field> values = fields.values();
   1.926 +      for (Field field : values) {
   1.927 +        // Cache is up-to-date at this point, so we don't need to hit the DB.
   1.928 +        out.put(field.getID(), field);
   1.929 +      }
   1.930 +    }
   1.931 +    return out;
   1.932 +  }
   1.933 +
   1.934 +  private final HashMap<String, Integer> measurementVersions = new HashMap<String, Integer>();
   1.935 +
   1.936 +  private void populateMeasurementVersionsCache(SQLiteDatabase db) {
   1.937 +    HashMap<String, Integer> results = getIntegers(db, "current_measurements", "name", "version");
   1.938 +    if (results == null) {
   1.939 +      measurementVersions.clear();
   1.940 +      return;
   1.941 +    }
   1.942 +    synchronized (measurementVersions) {
   1.943 +      measurementVersions.clear();
   1.944 +      measurementVersions.putAll(results);
   1.945 +    }
   1.946 +  }
   1.947 +
   1.948 +  /**
   1.949 +   * Return the version of the measurement for which the DB is currently configured, or
   1.950 +   * 0 if unknown.
   1.951 +   * @param measurement String measurement identifier.
   1.952 +   * @return Current version.
   1.953 +   */
   1.954 +  private int getMeasurementVersion(String measurement) {
   1.955 +    synchronized (measurementVersions) {
   1.956 +      if (measurementVersions.containsKey(measurement)) {
   1.957 +        return measurementVersions.get(measurement);
   1.958 +      }
   1.959 +
   1.960 +      // This should never be necessary, unless the measurement does not exist.
   1.961 +      int value = integerQuery("measurements", "version", "name = ?", new String[] {measurement}, 0);
   1.962 +      measurementVersions.put(measurement, value);
   1.963 +      return value;
   1.964 +    }
   1.965 +  }
   1.966 +
   1.967 +  /**
   1.968 +   * Inform the storage layer that fields for the given measurement have been updated
   1.969 +   * to this version.
   1.970 +   *
   1.971 +   * This should be one of the final calls in a configuration transaction.
   1.972 +   * Always call this inside a transaction.
   1.973 +   */
   1.974 +  private void notifyMeasurementVersionUpdated(String measurement, int version) {
   1.975 +    Logger.info(LOG_TAG, "Measurement " + measurement + " now at " + version);
   1.976 +
   1.977 +    final SQLiteDatabase db = this.helper.getWritableDatabase();
   1.978 +    final ContentValues values = new ContentValues();
   1.979 +    values.put("name", measurement);
   1.980 +    values.put("version", version);
   1.981 +
   1.982 +    synchronized (measurementVersions) {
   1.983 +      measurementVersions.put(measurement, version);
   1.984 +    }
   1.985 +
   1.986 +    db.insertWithOnConflict("measurements", null, values, SQLiteDatabase.CONFLICT_IGNORE);
   1.987 +  }
   1.988 +
   1.989 +  /**
   1.990 +   * Call in a transaction.
   1.991 +   * This method could race with other accesses, but (a) it's within a transaction,
   1.992 +   * (b) configuration should be single-threaded, (c) we initialize the cache up-front.
   1.993 +   */
   1.994 +  @Override
   1.995 +  public void ensureMeasurementInitialized(String measurement, int version, MeasurementFields fields) {
   1.996 +    final int currentVersion = getMeasurementVersion(measurement);
   1.997 +    Logger.info(LOG_TAG, "Initializing measurement " + measurement + " to " +
   1.998 +                         version + " (current " + currentVersion + ")");
   1.999 +
  1.1000 +    if (currentVersion == version) {
  1.1001 +      Logger.info(LOG_TAG, "Measurement " + measurement + " already at v" + version);
  1.1002 +      return;
  1.1003 +    }
  1.1004 +
  1.1005 +    final SQLiteDatabase db = this.helper.getWritableDatabase();
  1.1006 +    if (!db.inTransaction()) {
  1.1007 +      Logger.warn(LOG_TAG, "ensureMeasurementInitialized should be called within a transaction.");
  1.1008 +    }
  1.1009 +
  1.1010 +    final ContentValues mv = new ContentValues();
  1.1011 +    mv.put("name", measurement);
  1.1012 +    mv.put("version", version);
  1.1013 +
  1.1014 +    final int measurementID = (int) db.insert("measurements", null, mv);
  1.1015 +
  1.1016 +    final ContentValues v = new ContentValues();
  1.1017 +    v.put("measurement", measurementID);
  1.1018 +    for (FieldSpec field : fields.getFields()) {
  1.1019 +      v.put("name", field.name);
  1.1020 +      v.put("flags", field.type);
  1.1021 +      Logger.debug(LOG_TAG, "M: " + measurementID + " F: " + field.name + " (" + field.type + ")");
  1.1022 +      db.insert("fields", null, v);
  1.1023 +    }
  1.1024 +
  1.1025 +    notifyMeasurementVersionUpdated(measurement, version);
  1.1026 +
  1.1027 +    // Let's be easy for now.
  1.1028 +    invalidateFieldsCache();
  1.1029 +  }
  1.1030 +
  1.1031 +  /**
  1.1032 +   * Return a cursor over the measurements and fields in the DB.
  1.1033 +   * Columns are {@link HealthReportDatabaseStorage#COLUMNS_MEASUREMENT_AND_FIELD_DETAILS}.
  1.1034 +   */
  1.1035 +  @Override
  1.1036 +  public Cursor getFieldVersions() {
  1.1037 +    final SQLiteDatabase db = this.helper.getReadableDatabase();
  1.1038 +    return db.query("named_fields", COLUMNS_MEASUREMENT_AND_FIELD_DETAILS,
  1.1039 +                    null, null, null, null, "measurement_name, measurement_version, field_name");
  1.1040 +  }
  1.1041 +
  1.1042 +  @Override
  1.1043 +  public Cursor getFieldVersions(String measurement, int measurementVersion) {
  1.1044 +    final SQLiteDatabase db = this.helper.getReadableDatabase();
  1.1045 +    return db.query("named_fields", COLUMNS_MEASUREMENT_AND_FIELD_DETAILS,
  1.1046 +                    "measurement_name = ? AND measurement_version = ?",
  1.1047 +                    new String[] {measurement, Integer.toString(measurementVersion)},
  1.1048 +                    null, null, "field_name");
  1.1049 +  }
  1.1050 +
  1.1051 +  @Override
  1.1052 +  public Cursor getMeasurementVersions() {
  1.1053 +    final SQLiteDatabase db = this.helper.getReadableDatabase();
  1.1054 +    return db.query("measurements", COLUMNS_MEASUREMENT_DETAILS,
  1.1055 +                    null, null, null, null, "name, version");
  1.1056 +  }
  1.1057 +
  1.1058 +  /**
  1.1059 +   * A thin wrapper around the database transactional semantics. Clients can
  1.1060 +   * use this to more efficiently ensure that measurements are initialized.
  1.1061 +   *
  1.1062 +   * Note that caches are also initialized here.
  1.1063 +   */
  1.1064 +  public void beginInitialization() {
  1.1065 +    SQLiteDatabase db = this.helper.getWritableDatabase();
  1.1066 +    db.beginTransaction();
  1.1067 +    populateMeasurementVersionsCache(db);
  1.1068 +  }
  1.1069 +
  1.1070 +  public void finishInitialization() {
  1.1071 +    SQLiteDatabase db = this.helper.getWritableDatabase();
  1.1072 +    db.setTransactionSuccessful();
  1.1073 +    db.endTransaction();
  1.1074 +  }
  1.1075 +
  1.1076 +  public void abortInitialization() {
  1.1077 +    this.helper.getWritableDatabase().endTransaction();
  1.1078 +  }
  1.1079 +
  1.1080 +  protected int getIntFromQuery(final String sql, final String[] selectionArgs) {
  1.1081 +    final SQLiteDatabase db = this.helper.getReadableDatabase();
  1.1082 +    final Cursor c = db.rawQuery(sql, selectionArgs);
  1.1083 +    try {
  1.1084 +      if (!c.moveToFirst()) {
  1.1085 +        throw new IllegalStateException("Cursor is empty.");
  1.1086 +      }
  1.1087 +      return c.getInt(0);
  1.1088 +    } finally {
  1.1089 +      c.close();
  1.1090 +    }
  1.1091 +  }
  1.1092 +
  1.1093 +  @Override
  1.1094 +  public int getDay(long time) {
  1.1095 +    return DateUtils.getDay(time);
  1.1096 +  }
  1.1097 +
  1.1098 +  @Override
  1.1099 +  public int getDay() {
  1.1100 +    return this.getDay(System.currentTimeMillis());
  1.1101 +  }
  1.1102 +
  1.1103 +  private void recordDailyLast(int env, int day, int field, Object value, String table) {
  1.1104 +    if (env == -1) {
  1.1105 +      Logger.warn(LOG_TAG, "Refusing to record with environment = -1.");
  1.1106 +      return;
  1.1107 +    }
  1.1108 +
  1.1109 +    final SQLiteDatabase db = this.helper.getWritableDatabase();
  1.1110 +
  1.1111 +    final String envString = Integer.toString(env);
  1.1112 +    final String fieldIDString = Integer.toString(field, 10);
  1.1113 +    final String dayString = Integer.toString(day, 10);
  1.1114 +
  1.1115 +    // Java, your capacity for abstraction leaves me wanting.
  1.1116 +    final ContentValues v = new ContentValues();
  1.1117 +    putValue(v, value);
  1.1118 +
  1.1119 +    // If we used a separate table, such that we could have a
  1.1120 +    // UNIQUE(env, field, day) constraint for daily-last values, then we could
  1.1121 +    // use INSERT OR REPLACE.
  1.1122 +    final int updated = db.update(table, v, WHERE_DATE_AND_ENV_AND_FIELD,
  1.1123 +                                  new String[] {dayString, envString, fieldIDString});
  1.1124 +    if (0 == updated) {
  1.1125 +      v.put("env", env);
  1.1126 +      v.put("field", field);
  1.1127 +      v.put("date", day);
  1.1128 +      try {
  1.1129 +        db.insertOrThrow(table, null, v);
  1.1130 +      } catch (SQLiteConstraintException e) {
  1.1131 +        throw new IllegalStateException("Event did not reference existing an environment or field.", e);
  1.1132 +      }
  1.1133 +    }
  1.1134 +  }
  1.1135 +
  1.1136 +  @Override
  1.1137 +  public void recordDailyLast(int env, int day, int field, JSONObject value) {
  1.1138 +    this.recordDailyLast(env, day, field, value == null ? "null" : value.toString(), EVENTS_TEXTUAL);
  1.1139 +  }
  1.1140 +
  1.1141 +  @Override
  1.1142 +  public void recordDailyLast(int env, int day, int field, String value) {
  1.1143 +    this.recordDailyLast(env, day, field, value, EVENTS_TEXTUAL);
  1.1144 +  }
  1.1145 +
  1.1146 +  @Override
  1.1147 +  public void recordDailyLast(int env, int day, int field, int value) {
  1.1148 +    this.recordDailyLast(env, day, field, Integer.valueOf(value), EVENTS_INTEGER);
  1.1149 +  }
  1.1150 +
  1.1151 +  private void recordDailyDiscrete(int env, int day, int field, Object value, String table) {
  1.1152 +    if (env == -1) {
  1.1153 +      Logger.warn(LOG_TAG, "Refusing to record with environment = -1.");
  1.1154 +      return;
  1.1155 +    }
  1.1156 +
  1.1157 +    final ContentValues v = new ContentValues();
  1.1158 +    v.put("env", env);
  1.1159 +    v.put("field", field);
  1.1160 +    v.put("date", day);
  1.1161 +
  1.1162 +    final SQLiteDatabase db = this.helper.getWritableDatabase();
  1.1163 +    putValue(v, value);
  1.1164 +
  1.1165 +    // Using SQLiteDatabase.insertOrThrow throws SQLiteConstraintException we cannot catch for
  1.1166 +    // unknown reasons (bug 961526 comment 13). We believe these are thrown because we attempt to
  1.1167 +    // record events using environment IDs removed from the database by the prune service. We
  1.1168 +    // invalidate the currentEnvironment ID after pruning, preventing further propagation,
  1.1169 +    // however, any event recording waiting for the prune service to complete on the background
  1.1170 +    // thread may carry an invalid ID: we expect an insertion failure and drop these events here.
  1.1171 +    final long res = db.insert(table, null, v);
  1.1172 +    if (res == -1) {
  1.1173 +      Logger.error(LOG_TAG, "Unable to record daily discrete event. Ignoring.");
  1.1174 +    }
  1.1175 +  }
  1.1176 +
  1.1177 +  @Override
  1.1178 +  public void recordDailyDiscrete(int env, int day, int field, JSONObject value) {
  1.1179 +    this.recordDailyDiscrete(env, day, field, value == null ? "null" : value.toString(), EVENTS_TEXTUAL);
  1.1180 +  }
  1.1181 +
  1.1182 +  @Override
  1.1183 +  public void recordDailyDiscrete(int env, int day, int field, String value) {
  1.1184 +    this.recordDailyDiscrete(env, day, field, value, EVENTS_TEXTUAL);
  1.1185 +  }
  1.1186 +
  1.1187 +  @Override
  1.1188 +  public void recordDailyDiscrete(int env, int day, int field, int value) {
  1.1189 +    this.recordDailyDiscrete(env, day, field, value, EVENTS_INTEGER);
  1.1190 +  }
  1.1191 +
  1.1192 +  /**
  1.1193 +   * Increment the specified field value by the specified amount. Counts start
  1.1194 +   * at zero.
  1.1195 +   *
  1.1196 +   * Note that this method can misbehave or throw if not executed within a
  1.1197 +   * transaction, because correct behavior involves querying then
  1.1198 +   * insert-or-update, and a race condition can otherwise occur.
  1.1199 +   *
  1.1200 +   * @param env the environment ID
  1.1201 +   * @param day the current day, in days since epoch
  1.1202 +   * @param field the field ID
  1.1203 +   * @param by how much to increment the counter.
  1.1204 +   */
  1.1205 +  @Override
  1.1206 +  public void incrementDailyCount(int env, int day, int field, int by) {
  1.1207 +    if (env == -1) {
  1.1208 +      Logger.warn(LOG_TAG, "Refusing to record with environment = -1.");
  1.1209 +      return;
  1.1210 +    }
  1.1211 +
  1.1212 +    final SQLiteDatabase db = this.helper.getWritableDatabase();
  1.1213 +    final String envString = Integer.toString(env);
  1.1214 +    final String fieldIDString = Integer.toString(field, 10);
  1.1215 +    final String dayString = Integer.toString(day, 10);
  1.1216 +
  1.1217 +    // Can't run a complex UPDATE and get the number of changed rows, so we'll
  1.1218 +    // do this the hard way.
  1.1219 +    // This relies on being called within a transaction.
  1.1220 +    final String[] args = new String[] {dayString, envString, fieldIDString};
  1.1221 +    final Cursor c = db.query(EVENTS_INTEGER,
  1.1222 +                              COLUMNS_VALUE,
  1.1223 +                              WHERE_DATE_AND_ENV_AND_FIELD,
  1.1224 +                              args, null, null, null, "1");
  1.1225 +
  1.1226 +    boolean present = false;
  1.1227 +    try {
  1.1228 +      present = c.moveToFirst();
  1.1229 +    } finally {
  1.1230 +      c.close();
  1.1231 +    }
  1.1232 +
  1.1233 +    if (present) {
  1.1234 +      // It's an int, so safe to concatenate. Avoids us having to mess with args.
  1.1235 +      db.execSQL("UPDATE " + EVENTS_INTEGER + " SET value = value + " + by + " WHERE " +
  1.1236 +                 WHERE_DATE_AND_ENV_AND_FIELD,
  1.1237 +                 args);
  1.1238 +    } else {
  1.1239 +      final ContentValues v = new ContentValues();
  1.1240 +      v.put("env", env);
  1.1241 +      v.put("value", by);
  1.1242 +      v.put("field", field);
  1.1243 +      v.put("date", day);
  1.1244 +      try {
  1.1245 +        db.insertOrThrow(EVENTS_INTEGER, null, v);
  1.1246 +      } catch (SQLiteConstraintException e) {
  1.1247 +        throw new IllegalStateException("Event did not reference existing an environment or field.", e);
  1.1248 +      }
  1.1249 +    }
  1.1250 +  }
  1.1251 +
  1.1252 +  @Override
  1.1253 +  public void incrementDailyCount(int env, int day, int field) {
  1.1254 +    this.incrementDailyCount(env, day, field, 1);
  1.1255 +  }
  1.1256 +
  1.1257 +  /**
  1.1258 +   * Are there events recorded on or after <code>time</code>?
  1.1259 +   *
  1.1260 +   * @param time milliseconds since epoch. Will be converted by {@link #getDay(long)}.
  1.1261 +   * @return true if such events exist, false otherwise.
  1.1262 +   */
  1.1263 +  @Override
  1.1264 +  public boolean hasEventSince(long time) {
  1.1265 +    final int start = this.getDay(time);
  1.1266 +    final SQLiteDatabase db = this.helper.getReadableDatabase();
  1.1267 +    final String dayString = Integer.toString(start, 10);
  1.1268 +    Cursor cur = db.query("events", COLUMNS_DATE_ENV_FIELD_VALUE,
  1.1269 +        "date >= ?", new String[] {dayString}, null, null, null, "1");
  1.1270 +    if (cur == null) {
  1.1271 +      // Something is horribly wrong; let the caller who actually reads the
  1.1272 +      // events deal with it.
  1.1273 +      return true;
  1.1274 +    }
  1.1275 +    try {
  1.1276 +      return cur.getCount() > 0;
  1.1277 +    } finally {
  1.1278 +      cur.close();
  1.1279 +    }
  1.1280 +  }
  1.1281 +
  1.1282 +  /**
  1.1283 +   * Returns a cursor over field events in the database. The results will be
  1.1284 +   * strictly ordered first by date, then by environment, and finally by field.
  1.1285 +   *
  1.1286 +   * Each row includes columns in {@link #COLUMNS_DATE_ENV_FIELD_VALUE}:
  1.1287 +   * "date", "env", "field", "value".
  1.1288 +   *
  1.1289 +   * @param time milliseconds since epoch. Will be converted by {@link #getDay(long)}.
  1.1290 +   * @return a cursor. The caller is responsible for closing this.
  1.1291 +   */
  1.1292 +  @Override
  1.1293 +  public Cursor getRawEventsSince(long time) {
  1.1294 +    final int start = this.getDay(time);
  1.1295 +    final SQLiteDatabase db = this.helper.getReadableDatabase();
  1.1296 +    final String dayString = Integer.toString(start, 10);
  1.1297 +    return db.query("events", COLUMNS_DATE_ENV_FIELD_VALUE,
  1.1298 +                    "date >= ?", new String[] {dayString}, null, null, "date, env, field");
  1.1299 +  }
  1.1300 +
  1.1301 +  /**
  1.1302 +   * Returns a cursor over field events in the database. The results will be
  1.1303 +   * strictly ordered first by date, then by environment, and finally by field.
  1.1304 +   *
  1.1305 +   * Each row includes columns in {@link #COLUMNS_DATE_ENVSTR_M_MV_F_VALUE}:
  1.1306 +   * "date", "environment" (as a String), "measurement_name", "measurement_version",
  1.1307 +   * "field_name", "field_flags", "value".
  1.1308 +   *
  1.1309 +   * @param time milliseconds since epoch. Will be converted by {@link #getDay(long)}.
  1.1310 +   * @return a cursor. The caller is responsible for closing this.
  1.1311 +   */
  1.1312 +  @Override
  1.1313 +  public Cursor getEventsSince(long time) {
  1.1314 +    final int start = this.getDay(time);
  1.1315 +    final SQLiteDatabase db = this.helper.getReadableDatabase();
  1.1316 +    final String dayString = Integer.toString(start, 10);
  1.1317 +    return db.query("named_events", COLUMNS_DATE_ENVSTR_M_MV_F_VALUE,
  1.1318 +                    "date >= ?", new String[] {dayString}, null, null,
  1.1319 +                    "date, environment, measurement_name, measurement_version, field_name");
  1.1320 +  }
  1.1321 +
  1.1322 +  public int getEventCount() {
  1.1323 +    return getRowCount("events");
  1.1324 +  }
  1.1325 +
  1.1326 +  public int getEnvironmentCount() {
  1.1327 +    return getRowCount("environments");
  1.1328 +  }
  1.1329 +
  1.1330 +  private int getRowCount(String table) {
  1.1331 +    // table should be parameterized, but SQL throws a compilation error if the table in unknown
  1.1332 +    // in advance.
  1.1333 +    return getIntFromQuery("SELECT COUNT(*) from " + table, null);
  1.1334 +  }
  1.1335 +
  1.1336 +  /**
  1.1337 +   * Deletes all environments, addons, and events from the database before the given time. If this
  1.1338 +   * data does not have recorded dates but are no longer referenced by other fields, they are also
  1.1339 +   * removed (with exception to the current environment).
  1.1340 +   *
  1.1341 +   * @param time milliseconds since epoch. Will be converted by {@link #getDay(long)}.
  1.1342 +   * @param curEnv The ID of the current environment.
  1.1343 +   * @return The number of environments and addon entries deleted.
  1.1344 +   */
  1.1345 +  public int deleteDataBefore(final long time, final int curEnv) {
  1.1346 +    final SQLiteDatabase db = this.helper.getWritableDatabase();
  1.1347 +    db.beginTransaction();
  1.1348 +    int numRowsDeleted = 0;
  1.1349 +    try {
  1.1350 +      numRowsDeleted += deleteEnvAndEventsBefore(db, time, curEnv);
  1.1351 +      numRowsDeleted += deleteOrphanedAddons(db);
  1.1352 +      db.setTransactionSuccessful();
  1.1353 +    } finally {
  1.1354 +      db.endTransaction();
  1.1355 +    }
  1.1356 +    return numRowsDeleted;
  1.1357 +  }
  1.1358 +
  1.1359 +  /**
  1.1360 +   * Deletes environments and their referring events recorded before the given time. Environments
  1.1361 +   * referenced by no events are deleted, except for the current environment.
  1.1362 +   *
  1.1363 +   * @param time milliseconds since epoch. Will be converted by {@link #getDay(long)}.
  1.1364 +   * @param curEnv The ID of the current environment.
  1.1365 +   * @return The number of environments (not events) deleted.
  1.1366 +   */
  1.1367 +  protected int deleteEnvAndEventsBefore(final long time, final int curEnv) {
  1.1368 +    final SQLiteDatabase db = this.helper.getWritableDatabase();
  1.1369 +    return deleteEnvAndEventsBefore(db, time, curEnv);
  1.1370 +  }
  1.1371 +
  1.1372 +  // Called internally only to ensure the same db instance is used.
  1.1373 +  protected int deleteEnvAndEventsBefore(final SQLiteDatabase db, final long time, final int curEnv) {
  1.1374 +    // Delete environments only referenced by events occuring before the given time. Cascade
  1.1375 +    // delete these events.
  1.1376 +    String whereClause =
  1.1377 +        "(SELECT COUNT(*) FROM events WHERE date >= ? " +
  1.1378 +        "    AND events.env = environments.id) = 0 " +
  1.1379 +        "AND id IN (SELECT DISTINCT env FROM events WHERE date < ?)";
  1.1380 +    final int day = this.getDay(time);
  1.1381 +    final String dayString = Integer.toString(day, 10);
  1.1382 +    String[] whereArgs = new String[] {dayString, dayString};
  1.1383 +
  1.1384 +    int numEnvDeleted = 0;
  1.1385 +    db.beginTransaction();
  1.1386 +    try {
  1.1387 +      numEnvDeleted += db.delete("environments", whereClause, whereArgs);
  1.1388 +      numEnvDeleted += deleteOrphanedEnv(db, curEnv);
  1.1389 +      // We can't get the number of events deleted through cascading deletions so we do not record
  1.1390 +      // the number of events deleted here.
  1.1391 +      deleteEventsBefore(db, dayString);
  1.1392 +      db.setTransactionSuccessful();
  1.1393 +    } finally {
  1.1394 +      db.endTransaction();
  1.1395 +    }
  1.1396 +    return numEnvDeleted;
  1.1397 +  }
  1.1398 +
  1.1399 +  /**
  1.1400 +   * Deletes environments not referenced by any events except for the given current environment.
  1.1401 +   */
  1.1402 +  protected int deleteOrphanedEnv(final int curEnv) {
  1.1403 +    final SQLiteDatabase db = this.helper.getWritableDatabase();
  1.1404 +    return deleteOrphanedEnv(db, curEnv);
  1.1405 +  }
  1.1406 +
  1.1407 +  // Called internally only to ensure the same db instance is used.
  1.1408 +  @SuppressWarnings("static-method")
  1.1409 +  protected int deleteOrphanedEnv(final SQLiteDatabase db, final int curEnv) {
  1.1410 +    final String whereClause =
  1.1411 +        "id != ? AND " +
  1.1412 +        "id NOT IN (SELECT env FROM events)";
  1.1413 +    final String[] whereArgs = new String[] {Integer.toString(curEnv)};
  1.1414 +    return db.delete("environments", whereClause, whereArgs);
  1.1415 +  }
  1.1416 +
  1.1417 +  protected int deleteEventsBefore(final String dayString) {
  1.1418 +    final SQLiteDatabase db = this.helper.getWritableDatabase();
  1.1419 +    return deleteEventsBefore(db, dayString);
  1.1420 +  }
  1.1421 +
  1.1422 +  // Called internally only to ensure the same db instance is used.
  1.1423 +  @SuppressWarnings("static-method")
  1.1424 +  protected int deleteEventsBefore(final SQLiteDatabase db, final String dayString) {
  1.1425 +    final String whereClause = "date < ?";
  1.1426 +    final String[] whereArgs = new String[] {dayString};
  1.1427 +    int numEventsDeleted = 0;
  1.1428 +    db.beginTransaction();
  1.1429 +    try {
  1.1430 +      numEventsDeleted += db.delete("events_integer", whereClause, whereArgs);
  1.1431 +      numEventsDeleted += db.delete("events_textual", whereClause, whereArgs);
  1.1432 +      db.setTransactionSuccessful();
  1.1433 +    } finally {
  1.1434 +      db.endTransaction();
  1.1435 +    }
  1.1436 +    return numEventsDeleted;
  1.1437 +  }
  1.1438 +
  1.1439 +  /**
  1.1440 +   * Deletes addons not referenced by any environments.
  1.1441 +   */
  1.1442 +  protected int deleteOrphanedAddons() {
  1.1443 +    final SQLiteDatabase db = this.helper.getWritableDatabase();
  1.1444 +    return deleteOrphanedAddons(db);
  1.1445 +  }
  1.1446 +
  1.1447 +  // Called internally only to ensure the same db instance is used.
  1.1448 +  @SuppressWarnings("static-method")
  1.1449 +  protected int deleteOrphanedAddons(final SQLiteDatabase db) {
  1.1450 +    final String whereClause = "id NOT IN (SELECT addonsID FROM environments)";
  1.1451 +    return db.delete("addons", whereClause, null);
  1.1452 +  }
  1.1453 +
  1.1454 +  /**
  1.1455 +   * Retrieve a mapping from a table. Keys should be unique; only one key-value
  1.1456 +   * pair will be returned for each key.
  1.1457 +   */
  1.1458 +  private static HashMap<String, Integer> getIntegers(SQLiteDatabase db, String table, String columnA, String columnB) {
  1.1459 +    Cursor c = db.query(table, new String[] {columnA, columnB}, null, null, null, null, null);
  1.1460 +    try {
  1.1461 +      if (!c.moveToFirst()) {
  1.1462 +        return null;
  1.1463 +      }
  1.1464 +
  1.1465 +      HashMap<String, Integer> results = new HashMap<String, Integer>();
  1.1466 +      while (!c.isAfterLast()) {
  1.1467 +        results.put(c.getString(0), c.getInt(1));
  1.1468 +        c.moveToNext();
  1.1469 +      }
  1.1470 +      return results;
  1.1471 +    } finally {
  1.1472 +      c.close();
  1.1473 +    }
  1.1474 +  }
  1.1475 +
  1.1476 +  /**
  1.1477 +   * Retrieve a single value from a mapping table.
  1.1478 +   */
  1.1479 +  private int integerQuery(String table, String column, String where, String[] args, int defaultValue) {
  1.1480 +    final SQLiteDatabase db = this.helper.getReadableDatabase();
  1.1481 +    Cursor c = db.query(table, new String[] {column}, where, args, null, null, column + " DESC", "1");
  1.1482 +    try {
  1.1483 +      if (!c.moveToFirst()) {
  1.1484 +        return defaultValue;
  1.1485 +      }
  1.1486 +      return c.getInt(0);
  1.1487 +    } finally {
  1.1488 +      c.close();
  1.1489 +    }
  1.1490 +  }
  1.1491 +
  1.1492 +  /**
  1.1493 +   * Helper to allow us to avoid excessive code duplication.
  1.1494 +   *
  1.1495 +   * @param v
  1.1496 +   *          the destination <code>ContentValues</code>.
  1.1497 +   * @param value
  1.1498 +   *          either a <code>String</code> or an <code>Integer</code>. No type
  1.1499 +   *          checking is performed.
  1.1500 +   */
  1.1501 +  private static final void putValue(final ContentValues v, Object value) {
  1.1502 +    if (value instanceof String) {
  1.1503 +      v.put("value", (String) value);
  1.1504 +    } else {
  1.1505 +      v.put("value", (Integer) value);
  1.1506 +    }
  1.1507 +  }
  1.1508 +
  1.1509 +  @Override
  1.1510 +  public void deleteEverything() {
  1.1511 +    this.helper.deleteEverything();
  1.1512 +  }
  1.1513 +
  1.1514 +  @Override
  1.1515 +  public void deleteEnvironments() {
  1.1516 +    final SQLiteDatabase db = this.helper.getWritableDatabase();
  1.1517 +    db.beginTransaction();
  1.1518 +    try {
  1.1519 +      // Cascade will clear the rest.
  1.1520 +      db.delete("environments", null, null);
  1.1521 +      db.setTransactionSuccessful();
  1.1522 +    } finally {
  1.1523 +      db.endTransaction();
  1.1524 +    }
  1.1525 +  }
  1.1526 +
  1.1527 +  @Override
  1.1528 +  public void deleteMeasurements() {
  1.1529 +    final SQLiteDatabase db = this.helper.getWritableDatabase();
  1.1530 +    db.beginTransaction();
  1.1531 +    try {
  1.1532 +      // Cascade will clear the rest.
  1.1533 +      db.delete("measurements", null, null);
  1.1534 +
  1.1535 +      // Clear measurements and fields cache, because some of their IDs are now invalid.
  1.1536 +      invalidateFieldsCache(); // Let it repopulate on its own.
  1.1537 +      populateMeasurementVersionsCache(db); // Performed at Storage init so repopulate now.
  1.1538 +
  1.1539 +      db.setTransactionSuccessful();
  1.1540 +    } finally {
  1.1541 +      db.endTransaction();
  1.1542 +    }
  1.1543 +  }
  1.1544 +
  1.1545 +  /**
  1.1546 +   * Prunes the given number of least-recently used environments. Note that orphaned environments
  1.1547 +   * are not removed and the environment cache is cleared.
  1.1548 +   */
  1.1549 +  public void pruneEnvironments(final int numToPrune) {
  1.1550 +    final SQLiteDatabase db = this.helper.getWritableDatabase();
  1.1551 +    db.beginTransaction();
  1.1552 +    try {
  1.1553 +      db.delete("environments",
  1.1554 +          "id in (SELECT env " +
  1.1555 +          "       FROM events " +
  1.1556 +          "       GROUP BY env " +
  1.1557 +          "       ORDER BY MAX(date), env " +
  1.1558 +          "       LIMIT " + numToPrune + ")",
  1.1559 +          null);
  1.1560 +      db.setTransactionSuccessful();
  1.1561 +
  1.1562 +      // Clear environment cache, because some of their IDs are now invalid.
  1.1563 +      this.envs.clear();
  1.1564 +    } finally {
  1.1565 +      db.endTransaction();
  1.1566 +    }
  1.1567 +  }
  1.1568 +
  1.1569 +  /**
  1.1570 +   * Prunes up to a maximum of the given number of the oldest events. While it is more correct to
  1.1571 +   * prune the exact given amount, there is no unique identifier among events so we cannot be so
  1.1572 +   * precise. Instead, we prune on date, deleting all events up to the day before our count of
  1.1573 +   * events reaches the given maximum. Note that this technicality means this method cannot be
  1.1574 +   * used to delete all events.
  1.1575 +   */
  1.1576 +  public void pruneEvents(final int maxNumToPrune) {
  1.1577 +    final SQLiteDatabase db = this.helper.getWritableDatabase();
  1.1578 +
  1.1579 +    final Cursor c = db.rawQuery(
  1.1580 +        "SELECT MAX(date) " +
  1.1581 +        "FROM (SELECT date " +
  1.1582 +        "      FROM events " +
  1.1583 +        "      ORDER BY date " +
  1.1584 +        "      LIMIT " + maxNumToPrune + ")",
  1.1585 +        null);
  1.1586 +    long pruneDate = -1;
  1.1587 +    try {
  1.1588 +      if (!c.moveToFirst()) {
  1.1589 +        Logger.debug(LOG_TAG, "No max date found in events: table is likely empty. Not pruning " +
  1.1590 +            "events.");
  1.1591 +        return;
  1.1592 +      }
  1.1593 +      pruneDate = c.getLong(0);
  1.1594 +    } finally {
  1.1595 +      c.close();
  1.1596 +    }
  1.1597 +
  1.1598 +    final String selection = "date < " + pruneDate;
  1.1599 +    db.beginTransaction();
  1.1600 +    try {
  1.1601 +      db.delete(EVENTS_INTEGER, selection, null);
  1.1602 +      db.delete(EVENTS_TEXTUAL, selection, null);
  1.1603 +      db.setTransactionSuccessful();
  1.1604 +    } finally {
  1.1605 +      db.endTransaction();
  1.1606 +    }
  1.1607 +  }
  1.1608 +
  1.1609 +  public void vacuum() {
  1.1610 +    final SQLiteDatabase db = this.helper.getWritableDatabase();
  1.1611 +    db.execSQL("vacuum");
  1.1612 +  }
  1.1613 +
  1.1614 +  /**
  1.1615 +   * Disables auto_vacuuming. Changes may only take effect after a "vacuum" command.
  1.1616 +   */
  1.1617 +  public void disableAutoVacuuming() {
  1.1618 +    final SQLiteDatabase db = this.helper.getWritableDatabase();
  1.1619 +    db.execSQL("PRAGMA auto_vacuum=0");
  1.1620 +  }
  1.1621 +}

mercurial