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

Wed, 31 Dec 2014 07:22:50 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 07:22:50 +0100
branch
TOR_BUG_3246
changeset 4
fc2d59ddac77
permissions
-rw-r--r--

Correct previous dual key logic pending first delivery installment.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 package org.mozilla.gecko.background.healthreport;
michael@0 6
michael@0 7 import java.io.File;
michael@0 8 import java.util.Collection;
michael@0 9 import java.util.HashMap;
michael@0 10 import java.util.concurrent.ConcurrentHashMap;
michael@0 11 import java.util.concurrent.Executor;
michael@0 12 import java.util.concurrent.Executors;
michael@0 13
michael@0 14 import org.json.JSONObject;
michael@0 15 import org.mozilla.gecko.background.common.DateUtils;
michael@0 16 import org.mozilla.gecko.background.common.log.Logger;
michael@0 17 import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields.FieldSpec;
michael@0 18
michael@0 19 import android.content.ContentValues;
michael@0 20 import android.content.Context;
michael@0 21 import android.content.ContextWrapper;
michael@0 22 import android.database.Cursor;
michael@0 23 import android.database.SQLException;
michael@0 24 import android.database.sqlite.SQLiteConstraintException;
michael@0 25 import android.database.sqlite.SQLiteDatabase;
michael@0 26 import android.database.sqlite.SQLiteOpenHelper;
michael@0 27 import android.os.Build;
michael@0 28 import android.util.SparseArray;
michael@0 29
michael@0 30 /**
michael@0 31 * <code>HealthReportDatabaseStorage</code> provides an interface on top of
michael@0 32 * SQLite storage for Health Report data. It exposes methods for management of
michael@0 33 * environments, measurements, fields, and values, and a cursor-based API for
michael@0 34 * querying.
michael@0 35 *
michael@0 36 * Health Report data is structured as follows.
michael@0 37 *
michael@0 38 * Records are primarily broken down by date, at day granularity. Each day's data
michael@0 39 * is then split according to environment. An environment is a collection of
michael@0 40 * constant attributes, such as version and processor; if one of these attributes
michael@0 41 * changes, a new environment becomes active.
michael@0 42 *
michael@0 43 * Environments are identified by a stable hash of their attributes.
michael@0 44 *
michael@0 45 * The database includes a persistent numeric identifier for each environment. Create
michael@0 46 * or fetch this identifier via:
michael@0 47 *
michael@0 48 * <pre>
michael@0 49 * final Environment environment = storage.getEnvironment();
michael@0 50 * // Init the environment now.
michael@0 51 * String envHash = environment.getHash();
michael@0 52 * int env = environment.register();
michael@0 53 * </pre>
michael@0 54 *
michael@0 55 * You can safely cache this environment identifier for the life of the database.
michael@0 56 *
michael@0 57 * Orthogonal to environments are measurements. Each measurement is a named and
michael@0 58 * versioned scope for a collection of fields. It is assumed that if a measurement
michael@0 59 * with the given name and version is known to the database, then all of its fields
michael@0 60 * are also known; if you change the collection of fields in the measurement, the
michael@0 61 * measurement's version must be incremented, too.
michael@0 62 *
michael@0 63 * As with environments, measurements have an internal numeric identifier.
michael@0 64 *
michael@0 65 * Calling code should initialize its measurements as follows:
michael@0 66 *
michael@0 67 * <pre>
michael@0 68 * public static class FooFieldsV1 implements MeasurementFields {
michael@0 69 * {@literal @}Override
michael@0 70 * public Iterable<String> getFields() {
michael@0 71 * ArrayList<String> fields = new ArrayList<String>();
michael@0 72 * fields.add("bar");
michael@0 73 * fields.add("baz");
michael@0 74 * return fields;
michael@0 75 * }
michael@0 76 * }
michael@0 77 *
michael@0 78 * storage.beginInitialization();
michael@0 79 *
michael@0 80 * try {
michael@0 81 * storage.ensureMeasurementInitialized("org.mozilla.fooProvider.fooMeasurement",
michael@0 82 * 1, new FooFieldsV1());
michael@0 83 * storage.finishInitialization();
michael@0 84 * } catch (Exception e) {
michael@0 85 * storage.abortInitialization();
michael@0 86 * }
michael@0 87 * </pre>
michael@0 88 *
michael@0 89 * Measurements have fields. Fields can conceptually be divided into "daily last"
michael@0 90 * (we only care about the last value), "daily counter" (increments per day),
michael@0 91 * "daily discrete" (multiple records per day). Simply call the correct method for each.
michael@0 92 *
michael@0 93 * To do so you need a field ID, to avoid constant costly string lookups. You can get
michael@0 94 * this value from storage:
michael@0 95 *
michael@0 96 * <pre>
michael@0 97 * Field field = storage.getField("org.mozilla.fooProvider.fooMeasurement", 1, "bar");
michael@0 98 * int fieldID = field.getID();
michael@0 99 * </pre>
michael@0 100 *
michael@0 101 * This lookup is cached, and so is relatively inexpensive.
michael@0 102 *
michael@0 103 * You can then do something like the following:
michael@0 104 *
michael@0 105 * <pre>
michael@0 106 * storage.recordDailyLast(storage.getDay(), env, fieldID, "last value");
michael@0 107 * </pre>
michael@0 108 *
michael@0 109 * or equivalently for numeric values, discrete or counters, etc.
michael@0 110 *
michael@0 111 * To retrieve values, use {@link #getRawEventsSince(long)}.
michael@0 112 *
michael@0 113 * For safety, perform operations on the storage executor thread:
michael@0 114 *
michael@0 115 * <pre>
michael@0 116 * storage.enqueueOperation(runnable);
michael@0 117 * </pre>
michael@0 118 */
michael@0 119 public class HealthReportDatabaseStorage implements HealthReportStorage {
michael@0 120
michael@0 121 private static final String WHERE_DATE_AND_ENV_AND_FIELD = "date = ? AND env = ? AND field = ?";
michael@0 122
michael@0 123 public static final String[] COLUMNS_HASH = new String[] {"hash"};
michael@0 124 public static final String[] COLUMNS_DATE_ENV_FIELD_VALUE = new String[] {"date", "env", "field", "value"};
michael@0 125 public static final String[] COLUMNS_DATE_ENVSTR_M_MV_F_VALUE = new String[] {
michael@0 126 "date", "environment", "measurement_name", "measurement_version",
michael@0 127 "field_name", "field_flags", "value"
michael@0 128 };
michael@0 129
michael@0 130 private static final String[] COLUMNS_ENVIRONMENT_DETAILS = new String[] {
michael@0 131 "id", "version", "hash",
michael@0 132 "profileCreation", "cpuCount", "memoryMB",
michael@0 133
michael@0 134 "isBlocklistEnabled", "isTelemetryEnabled", "extensionCount",
michael@0 135 "pluginCount", "themeCount",
michael@0 136
michael@0 137 "architecture", "sysName", "sysVersion", "vendor", "appName", "appID",
michael@0 138 "appVersion", "appBuildID", "platformVersion", "platformBuildID", "os",
michael@0 139 "xpcomabi", "updateChannel",
michael@0 140
michael@0 141 "distribution", "osLocale", "appLocale", "acceptLangSet",
michael@0 142
michael@0 143 // Joined to the add-ons table.
michael@0 144 "addonsBody"
michael@0 145 };
michael@0 146
michael@0 147 public static final String[] COLUMNS_MEASUREMENT_DETAILS = new String[] {"id", "name", "version"};
michael@0 148 public static final String[] COLUMNS_MEASUREMENT_AND_FIELD_DETAILS =
michael@0 149 new String[] {"measurement_name", "measurement_id", "measurement_version",
michael@0 150 "field_name", "field_id", "field_flags"};
michael@0 151
michael@0 152 private static final String[] COLUMNS_VALUE = new String[] {"value"};
michael@0 153 private static final String[] COLUMNS_ID = new String[] {"id"};
michael@0 154
michael@0 155 private static final String EVENTS_TEXTUAL = "events_textual";
michael@0 156 private static final String EVENTS_INTEGER = "events_integer";
michael@0 157
michael@0 158 protected static final String DB_NAME = "health.db";
michael@0 159
michael@0 160 private static final String LOG_TAG = "HealthReportStorage";
michael@0 161
michael@0 162 private final Executor executor = Executors.newSingleThreadExecutor();
michael@0 163
michael@0 164 @Override
michael@0 165 public void enqueueOperation(Runnable runnable) {
michael@0 166 executor.execute(runnable);
michael@0 167 }
michael@0 168
michael@0 169 public HealthReportDatabaseStorage(final Context context,
michael@0 170 final File profileDirectory) {
michael@0 171 this.helper = new HealthReportSQLiteOpenHelper(context, profileDirectory,
michael@0 172 DB_NAME);
michael@0 173 executor.execute(new Runnable() {
michael@0 174 @Override
michael@0 175 public void run() {
michael@0 176 Logger.setThreadLogTag(HealthReportConstants.GLOBAL_LOG_TAG);
michael@0 177 Logger.debug(LOG_TAG, "Creating HealthReportDatabaseStorage.");
michael@0 178 }
michael@0 179 });
michael@0 180 }
michael@0 181
michael@0 182 @Override
michael@0 183 public void close() {
michael@0 184 this.helper.close();
michael@0 185 this.fields.clear();
michael@0 186 this.envs.clear();
michael@0 187 this.measurementVersions.clear();
michael@0 188 }
michael@0 189
michael@0 190 protected final HealthReportSQLiteOpenHelper helper;
michael@0 191
michael@0 192 public static class HealthReportSQLiteOpenHelper extends SQLiteOpenHelper {
michael@0 193 public static final int CURRENT_VERSION = 6;
michael@0 194 public static final String LOG_TAG = "HealthReportSQL";
michael@0 195
michael@0 196 /**
michael@0 197 * A little helper to avoid SQLiteOpenHelper misbehaving on Android 2.1.
michael@0 198 * Partly cribbed from
michael@0 199 * <http://stackoverflow.com/questions/5332328/sqliteopenhelper-problem-with-fully-qualified-db-path-name>.
michael@0 200 */
michael@0 201 public static class AbsolutePathContext extends ContextWrapper {
michael@0 202 private final File parent;
michael@0 203
michael@0 204 public AbsolutePathContext(Context base, File parent) {
michael@0 205 super(base);
michael@0 206 this.parent = parent;
michael@0 207 }
michael@0 208
michael@0 209 @Override
michael@0 210 public File getDatabasePath(String name) {
michael@0 211 return new File(getAbsolutePath(parent, name));
michael@0 212 }
michael@0 213
michael@0 214 // Won't be called after API v11, but we can't override the version that
michael@0 215 // *is* called and still support v8.
michael@0 216 // Instead we check the version code in the HealthReportSQLiteOpenHelper
michael@0 217 // constructor, and only use this workaround if we need to.
michael@0 218 @Override
michael@0 219 public SQLiteDatabase openOrCreateDatabase(String name,
michael@0 220 int mode,
michael@0 221 SQLiteDatabase.CursorFactory factory) {
michael@0 222 final File path = getDatabasePath(name);
michael@0 223 Logger.pii(LOG_TAG, "Opening database through absolute path " + path.getAbsolutePath());
michael@0 224 return SQLiteDatabase.openOrCreateDatabase(path, null);
michael@0 225 }
michael@0 226 }
michael@0 227
michael@0 228 public static String getAbsolutePath(File parent, String name) {
michael@0 229 return parent.getAbsolutePath() + File.separator + name;
michael@0 230 }
michael@0 231
michael@0 232 public static boolean CAN_USE_ABSOLUTE_DB_PATH = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO);
michael@0 233 public HealthReportSQLiteOpenHelper(Context context, File profileDirectory, String name) {
michael@0 234 this(context, profileDirectory, name, CURRENT_VERSION);
michael@0 235 }
michael@0 236
michael@0 237 // For testing DBs of different versions.
michael@0 238 public HealthReportSQLiteOpenHelper(Context context, File profileDirectory, String name, int version) {
michael@0 239 super(
michael@0 240 (CAN_USE_ABSOLUTE_DB_PATH ? context : new AbsolutePathContext(context, profileDirectory)),
michael@0 241 (CAN_USE_ABSOLUTE_DB_PATH ? getAbsolutePath(profileDirectory, name) : name),
michael@0 242 null,
michael@0 243 version);
michael@0 244
michael@0 245 if (CAN_USE_ABSOLUTE_DB_PATH) {
michael@0 246 Logger.pii(LOG_TAG, "Opening: " + getAbsolutePath(profileDirectory, name));
michael@0 247 }
michael@0 248 }
michael@0 249
michael@0 250 @Override
michael@0 251 public void onCreate(SQLiteDatabase db) {
michael@0 252 db.execSQL("CREATE TABLE addons (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
michael@0 253 " body TEXT, " +
michael@0 254 " UNIQUE (body) " +
michael@0 255 ")");
michael@0 256
michael@0 257 // N.B., hash collisions can occur across versions. In that case, the system
michael@0 258 // is likely to persist the original environment version.
michael@0 259 db.execSQL("CREATE TABLE environments (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
michael@0 260 " version INTEGER, " +
michael@0 261 " hash TEXT, " +
michael@0 262 " profileCreation INTEGER, " +
michael@0 263 " cpuCount INTEGER, " +
michael@0 264 " memoryMB INTEGER, " +
michael@0 265 " isBlocklistEnabled INTEGER, " +
michael@0 266 " isTelemetryEnabled INTEGER, " +
michael@0 267 " extensionCount INTEGER, " +
michael@0 268 " pluginCount INTEGER, " +
michael@0 269 " themeCount INTEGER, " +
michael@0 270 " architecture TEXT, " +
michael@0 271 " sysName TEXT, " +
michael@0 272 " sysVersion TEXT, " +
michael@0 273 " vendor TEXT, " +
michael@0 274 " appName TEXT, " +
michael@0 275 " appID TEXT, " +
michael@0 276 " appVersion TEXT, " +
michael@0 277 " appBuildID TEXT, " +
michael@0 278 " platformVersion TEXT, " +
michael@0 279 " platformBuildID TEXT, " +
michael@0 280 " os TEXT, " +
michael@0 281 " xpcomabi TEXT, " +
michael@0 282 " updateChannel TEXT, " +
michael@0 283
michael@0 284 " distribution TEXT, " +
michael@0 285 " osLocale TEXT, " +
michael@0 286 " appLocale TEXT, " +
michael@0 287 " acceptLangSet INTEGER, " +
michael@0 288
michael@0 289 " addonsID INTEGER, " +
michael@0 290 " FOREIGN KEY (addonsID) REFERENCES addons(id) ON DELETE RESTRICT, " +
michael@0 291 " UNIQUE (hash) " +
michael@0 292 ")");
michael@0 293
michael@0 294 db.execSQL("CREATE TABLE measurements (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
michael@0 295 " name TEXT, " +
michael@0 296 " version INTEGER, " +
michael@0 297 " UNIQUE (name, version) " +
michael@0 298 ")");
michael@0 299
michael@0 300 db.execSQL("CREATE TABLE fields (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
michael@0 301 " measurement INTEGER, " +
michael@0 302 " name TEXT, " +
michael@0 303 " flags INTEGER, " +
michael@0 304 " FOREIGN KEY (measurement) REFERENCES measurements(id) ON DELETE CASCADE, " +
michael@0 305 " UNIQUE (measurement, name)" +
michael@0 306 ")");
michael@0 307
michael@0 308 db.execSQL("CREATE TABLE " + EVENTS_INTEGER + "(" +
michael@0 309 " date INTEGER, " +
michael@0 310 " env INTEGER, " +
michael@0 311 " field INTEGER, " +
michael@0 312 " value INTEGER, " +
michael@0 313 " FOREIGN KEY (field) REFERENCES fields(id) ON DELETE CASCADE, " +
michael@0 314 " FOREIGN KEY (env) REFERENCES environments(id) ON DELETE CASCADE" +
michael@0 315 ")");
michael@0 316
michael@0 317 db.execSQL("CREATE TABLE " + EVENTS_TEXTUAL + "(" +
michael@0 318 " date INTEGER, " +
michael@0 319 " env INTEGER, " +
michael@0 320 " field INTEGER, " +
michael@0 321 " value TEXT, " +
michael@0 322 " FOREIGN KEY (field) REFERENCES fields(id) ON DELETE CASCADE, " +
michael@0 323 " FOREIGN KEY (env) REFERENCES environments(id) ON DELETE CASCADE" +
michael@0 324 ")");
michael@0 325
michael@0 326 db.execSQL("CREATE INDEX idx_events_integer_date_env_field ON events_integer (date, env, field)");
michael@0 327 db.execSQL("CREATE INDEX idx_events_textual_date_env_field ON events_textual (date, env, field)");
michael@0 328
michael@0 329 db.execSQL("CREATE VIEW events AS " +
michael@0 330 "SELECT date, env, field, value FROM " + EVENTS_INTEGER + " " +
michael@0 331 "UNION ALL " +
michael@0 332 "SELECT date, env, field, value FROM " + EVENTS_TEXTUAL);
michael@0 333
michael@0 334 db.execSQL("CREATE VIEW named_events AS " +
michael@0 335 "SELECT date, " +
michael@0 336 " environments.hash AS environment, " +
michael@0 337 " measurements.name AS measurement_name, " +
michael@0 338 " measurements.version AS measurement_version, " +
michael@0 339 " fields.name AS field_name, " +
michael@0 340 " fields.flags AS field_flags, " +
michael@0 341 " value FROM " +
michael@0 342 "events JOIN environments ON events.env = environments.id " +
michael@0 343 " JOIN fields ON events.field = fields.id " +
michael@0 344 " JOIN measurements ON fields.measurement = measurements.id");
michael@0 345
michael@0 346 db.execSQL("CREATE VIEW named_fields AS " +
michael@0 347 "SELECT measurements.name AS measurement_name, " +
michael@0 348 " measurements.id AS measurement_id, " +
michael@0 349 " measurements.version AS measurement_version, " +
michael@0 350 " fields.name AS field_name, " +
michael@0 351 " fields.id AS field_id, " +
michael@0 352 " fields.flags AS field_flags " +
michael@0 353 "FROM fields JOIN measurements ON fields.measurement = measurements.id");
michael@0 354
michael@0 355 db.execSQL("CREATE VIEW current_measurements AS " +
michael@0 356 "SELECT name, MAX(version) AS version FROM measurements GROUP BY name");
michael@0 357
michael@0 358 createAddonsEnvironmentsView(db);
michael@0 359 }
michael@0 360
michael@0 361 @Override
michael@0 362 public void onOpen(SQLiteDatabase db) {
michael@0 363 if (!db.isReadOnly()) {
michael@0 364 db.execSQL("PRAGMA foreign_keys=ON;");
michael@0 365 }
michael@0 366 }
michael@0 367
michael@0 368 private void createAddonsEnvironmentsView(SQLiteDatabase db) {
michael@0 369 db.execSQL("CREATE VIEW environments_with_addons AS " +
michael@0 370 "SELECT e.id AS id, " +
michael@0 371 " e.version AS version, " +
michael@0 372 " e.hash AS hash, " +
michael@0 373 " e.profileCreation AS profileCreation, " +
michael@0 374 " e.cpuCount AS cpuCount, " +
michael@0 375 " e.memoryMB AS memoryMB, " +
michael@0 376 " e.isBlocklistEnabled AS isBlocklistEnabled, " +
michael@0 377 " e.isTelemetryEnabled AS isTelemetryEnabled, " +
michael@0 378 " e.extensionCount AS extensionCount, " +
michael@0 379 " e.pluginCount AS pluginCount, " +
michael@0 380 " e.themeCount AS themeCount, " +
michael@0 381 " e.architecture AS architecture, " +
michael@0 382 " e.sysName AS sysName, " +
michael@0 383 " e.sysVersion AS sysVersion, " +
michael@0 384 " e.vendor AS vendor, " +
michael@0 385 " e.appName AS appName, " +
michael@0 386 " e.appID AS appID, " +
michael@0 387 " e.appVersion AS appVersion, " +
michael@0 388 " e.appBuildID AS appBuildID, " +
michael@0 389 " e.platformVersion AS platformVersion, " +
michael@0 390 " e.platformBuildID AS platformBuildID, " +
michael@0 391 " e.os AS os, " +
michael@0 392 " e.xpcomabi AS xpcomabi, " +
michael@0 393 " e.updateChannel AS updateChannel, " +
michael@0 394 " e.distribution AS distribution, " +
michael@0 395 " e.osLocale AS osLocale, " +
michael@0 396 " e.appLocale AS appLocale, " +
michael@0 397 " e.acceptLangSet AS acceptLangSet, " +
michael@0 398 " addons.body AS addonsBody " +
michael@0 399 "FROM environments AS e, addons " +
michael@0 400 "WHERE e.addonsID = addons.id");
michael@0 401 }
michael@0 402
michael@0 403 private void upgradeDatabaseFrom2To3(SQLiteDatabase db) {
michael@0 404 db.execSQL("CREATE TABLE addons (id INTEGER PRIMARY KEY AUTOINCREMENT, " +
michael@0 405 " body TEXT, " +
michael@0 406 " UNIQUE (body) " +
michael@0 407 ")");
michael@0 408
michael@0 409 db.execSQL("ALTER TABLE environments ADD COLUMN addonsID INTEGER REFERENCES addons(id) ON DELETE RESTRICT");
michael@0 410
michael@0 411 createAddonsEnvironmentsView(db);
michael@0 412 }
michael@0 413
michael@0 414 private void upgradeDatabaseFrom3To4(SQLiteDatabase db) {
michael@0 415 // Update search measurements to use a different type.
michael@0 416 db.execSQL("UPDATE OR IGNORE fields SET flags = " + Field.TYPE_COUNTED_STRING_DISCRETE +
michael@0 417 " WHERE measurement IN (SELECT id FROM measurements WHERE name = 'org.mozilla.searches.counts')");
michael@0 418 }
michael@0 419
michael@0 420 private void upgradeDatabaseFrom4to5(SQLiteDatabase db) {
michael@0 421 // Delete NULL in addons.body, which appeared as a result of Bug 886156. Note that the
michael@0 422 // foreign key constraint, "ON DELETE RESTRICT", may be violated, but since onOpen() is
michael@0 423 // called after this method, foreign keys are not yet enabled and constraints can be broken.
michael@0 424 db.delete("addons", "body IS NULL", null);
michael@0 425
michael@0 426 // Purge any data inconsistent with foreign key references (which may have appeared before
michael@0 427 // foreign keys were enabled in Bug 900289).
michael@0 428 db.delete("fields", "measurement NOT IN (SELECT id FROM measurements)", null);
michael@0 429 db.delete("environments", "addonsID NOT IN (SELECT id from addons)", null);
michael@0 430 db.delete(EVENTS_INTEGER, "env NOT IN (SELECT id FROM environments)", null);
michael@0 431 db.delete(EVENTS_TEXTUAL, "env NOT IN (SELECT id FROM environments)", null);
michael@0 432 db.delete(EVENTS_INTEGER, "field NOT IN (SELECT id FROM fields)", null);
michael@0 433 db.delete(EVENTS_TEXTUAL, "field NOT IN (SELECT id FROM fields)", null);
michael@0 434 }
michael@0 435
michael@0 436 private void upgradeDatabaseFrom5to6(SQLiteDatabase db) {
michael@0 437 db.execSQL("DROP VIEW environments_with_addons");
michael@0 438
michael@0 439 // Add version to environment (default to 1).
michael@0 440 db.execSQL("ALTER TABLE environments ADD COLUMN version INTEGER DEFAULT 1");
michael@0 441
michael@0 442 // Add fields to environment (default to empty string).
michael@0 443 db.execSQL("ALTER TABLE environments ADD COLUMN distribution TEXT DEFAULT ''");
michael@0 444 db.execSQL("ALTER TABLE environments ADD COLUMN osLocale TEXT DEFAULT ''");
michael@0 445 db.execSQL("ALTER TABLE environments ADD COLUMN appLocale TEXT DEFAULT ''");
michael@0 446 db.execSQL("ALTER TABLE environments ADD COLUMN acceptLangSet INTEGER DEFAULT 0");
michael@0 447
michael@0 448 // Recreate view.
michael@0 449 createAddonsEnvironmentsView(db);
michael@0 450 }
michael@0 451
michael@0 452 @Override
michael@0 453 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
michael@0 454 if (oldVersion >= newVersion) {
michael@0 455 return;
michael@0 456 }
michael@0 457
michael@0 458 Logger.info(LOG_TAG, "onUpgrade: from " + oldVersion + " to " + newVersion + ".");
michael@0 459 try {
michael@0 460 switch (oldVersion) {
michael@0 461 case 2:
michael@0 462 upgradeDatabaseFrom2To3(db);
michael@0 463 case 3:
michael@0 464 upgradeDatabaseFrom3To4(db);
michael@0 465 case 4:
michael@0 466 upgradeDatabaseFrom4to5(db);
michael@0 467 case 5:
michael@0 468 upgradeDatabaseFrom5to6(db);
michael@0 469 }
michael@0 470 } catch (Exception e) {
michael@0 471 Logger.error(LOG_TAG, "Failure in onUpgrade.", e);
michael@0 472 throw new RuntimeException(e);
michael@0 473 }
michael@0 474 }
michael@0 475
michael@0 476 public void deleteEverything() {
michael@0 477 final SQLiteDatabase db = this.getWritableDatabase();
michael@0 478
michael@0 479 Logger.info(LOG_TAG, "Deleting everything.");
michael@0 480 db.beginTransaction();
michael@0 481 try {
michael@0 482 // Cascade will clear the rest.
michael@0 483 db.delete("measurements", null, null);
michael@0 484 db.delete("environments", null, null);
michael@0 485 db.delete("addons", null, null);
michael@0 486 db.setTransactionSuccessful();
michael@0 487 Logger.info(LOG_TAG, "Deletion successful.");
michael@0 488 } finally {
michael@0 489 db.endTransaction();
michael@0 490 }
michael@0 491 }
michael@0 492 }
michael@0 493
michael@0 494 public class DatabaseField extends Field {
michael@0 495 public DatabaseField(String mName, int mVersion, String fieldName) {
michael@0 496 this(mName, mVersion, fieldName, UNKNOWN_TYPE_OR_FIELD_ID, UNKNOWN_TYPE_OR_FIELD_ID);
michael@0 497 }
michael@0 498
michael@0 499 public DatabaseField(String mName, int mVersion, String fieldName, int flags) {
michael@0 500 this(mName, mVersion, fieldName, UNKNOWN_TYPE_OR_FIELD_ID, flags);
michael@0 501 }
michael@0 502
michael@0 503 public DatabaseField(String mName, int mVersion, String fieldName, int fieldID, int flags) {
michael@0 504 super(mName, mVersion, fieldName, flags);
michael@0 505 this.fieldID = fieldID;
michael@0 506 }
michael@0 507
michael@0 508 private void loadFlags() {
michael@0 509 if (this.flags == UNKNOWN_TYPE_OR_FIELD_ID) {
michael@0 510 if (this.fieldID == UNKNOWN_TYPE_OR_FIELD_ID) {
michael@0 511 this.getID();
michael@0 512 }
michael@0 513 this.flags = integerQuery("fields", "flags", "id = ?", new String[] { Integer.toString(this.fieldID, 10) }, -1);
michael@0 514 }
michael@0 515 }
michael@0 516
michael@0 517 @Override
michael@0 518 public synchronized boolean isIntegerField() {
michael@0 519 loadFlags();
michael@0 520 return super.isIntegerField();
michael@0 521 }
michael@0 522
michael@0 523 @Override
michael@0 524 public synchronized boolean isStringField() {
michael@0 525 loadFlags();
michael@0 526 return super.isStringField();
michael@0 527 }
michael@0 528
michael@0 529 @Override
michael@0 530 public synchronized boolean isDiscreteField() {
michael@0 531 loadFlags();
michael@0 532 return super.isDiscreteField();
michael@0 533 }
michael@0 534
michael@0 535 @Override
michael@0 536 public synchronized int getID() throws IllegalStateException {
michael@0 537 if (this.fieldID == UNKNOWN_TYPE_OR_FIELD_ID) {
michael@0 538 this.fieldID = integerQuery("named_fields", "field_id",
michael@0 539 "measurement_name = ? AND measurement_version = ? AND field_name = ?",
michael@0 540 new String[] {measurementName, measurementVersion, fieldName},
michael@0 541 UNKNOWN_TYPE_OR_FIELD_ID);
michael@0 542 if (this.fieldID == UNKNOWN_TYPE_OR_FIELD_ID) {
michael@0 543 throw new IllegalStateException("No field with name " + fieldName +
michael@0 544 " (" + measurementName + ", " + measurementVersion + ")");
michael@0 545 }
michael@0 546 }
michael@0 547 return this.fieldID;
michael@0 548 }
michael@0 549 }
michael@0 550
michael@0 551 // `envs` and `fields` look similar, but they are touched differently and
michael@0 552 // store differently stable kinds of data, hence type difference.
michael@0 553 // Note that we don't pre-populate the environment cache. We'll typically only
michael@0 554 // handle one per session.
michael@0 555 //
michael@0 556 // protected for testing purposes only.
michael@0 557 protected final ConcurrentHashMap<String, Integer> envs = new ConcurrentHashMap<String, Integer>();
michael@0 558
michael@0 559 /**
michael@0 560 * An {@link Environment} that knows how to persist to and from our database.
michael@0 561 */
michael@0 562 public static class DatabaseEnvironment extends Environment {
michael@0 563 protected final HealthReportDatabaseStorage storage;
michael@0 564
michael@0 565 @Override
michael@0 566 public int register() {
michael@0 567 final String h = getHash();
michael@0 568 if (storage.envs.containsKey(h)) {
michael@0 569 this.id = storage.envs.get(h);
michael@0 570 return this.id;
michael@0 571 }
michael@0 572
michael@0 573 // Otherwise, add data and hash to the DB.
michael@0 574 ContentValues v = new ContentValues();
michael@0 575 v.put("version", version);
michael@0 576 v.put("hash", h);
michael@0 577 v.put("profileCreation", profileCreation);
michael@0 578 v.put("cpuCount", cpuCount);
michael@0 579 v.put("memoryMB", memoryMB);
michael@0 580 v.put("isBlocklistEnabled", isBlocklistEnabled);
michael@0 581 v.put("isTelemetryEnabled", isTelemetryEnabled);
michael@0 582 v.put("extensionCount", extensionCount);
michael@0 583 v.put("pluginCount", pluginCount);
michael@0 584 v.put("themeCount", themeCount);
michael@0 585 v.put("architecture", architecture);
michael@0 586 v.put("sysName", sysName);
michael@0 587 v.put("sysVersion", sysVersion);
michael@0 588 v.put("vendor", vendor);
michael@0 589 v.put("appName", appName);
michael@0 590 v.put("appID", appID);
michael@0 591 v.put("appVersion", appVersion);
michael@0 592 v.put("appBuildID", appBuildID);
michael@0 593 v.put("platformVersion", platformVersion);
michael@0 594 v.put("platformBuildID", platformBuildID);
michael@0 595 v.put("os", os);
michael@0 596 v.put("xpcomabi", xpcomabi);
michael@0 597 v.put("updateChannel", updateChannel);
michael@0 598 v.put("distribution", distribution);
michael@0 599 v.put("osLocale", osLocale);
michael@0 600 v.put("appLocale", appLocale);
michael@0 601 v.put("acceptLangSet", acceptLangSet);
michael@0 602
michael@0 603 final SQLiteDatabase db = storage.helper.getWritableDatabase();
michael@0 604
michael@0 605 // If we're not already, we want all of our inserts to be in a transaction.
michael@0 606 boolean newTransaction = !db.inTransaction();
michael@0 607
michael@0 608 // Insert, with a little error handling to populate the cache in case of
michael@0 609 // omission and consequent collision.
michael@0 610 //
michael@0 611 // We would like to hang a trigger off a view here, and just use that for
michael@0 612 // inserts. But triggers don't seem to correctly update the last inserted
michael@0 613 // ID, so Android's insertOrThrow method returns -1.
michael@0 614 //
michael@0 615 // Instead, we go without the trigger, simply running the inserts ourselves.
michael@0 616 //
michael@0 617 // insertWithOnConflict doesn't work as documented: <http://stackoverflow.com/questions/11328877/android-sqllite-on-conflict-ignore-is-ignored-in-ics/11424150>.
michael@0 618 // So we do this the hard way.
michael@0 619 // We presume that almost every get will hit the cache (except for the first, obviously), so we
michael@0 620 // bias here towards inserts for the environments.
michael@0 621 // For add-ons we assume that different environments will share add-ons, so we query first.
michael@0 622
michael@0 623 final String addonsJSON = getNormalizedAddonsJSON();
michael@0 624 if (newTransaction) {
michael@0 625 db.beginTransaction();
michael@0 626 }
michael@0 627
michael@0 628 try {
michael@0 629 int addonsID = ensureAddons(db, addonsJSON);
michael@0 630 v.put("addonsID", addonsID);
michael@0 631
michael@0 632 try {
michael@0 633 int inserted = (int) db.insertOrThrow("environments", null, v);
michael@0 634 Logger.debug(LOG_TAG, "Inserted ID: " + inserted + " for hash " + h);
michael@0 635 if (inserted == -1) {
michael@0 636 throw new SQLException("Insert returned -1!");
michael@0 637 }
michael@0 638 this.id = inserted;
michael@0 639 storage.envs.put(h, this.id);
michael@0 640 if (newTransaction) {
michael@0 641 db.setTransactionSuccessful();
michael@0 642 }
michael@0 643 return inserted;
michael@0 644 } catch (SQLException e) {
michael@0 645 // The inserter should take care of updating `envs`. But if it
michael@0 646 // doesn't...
michael@0 647 Cursor c = db.query("environments", COLUMNS_ID, "hash = ?",
michael@0 648 new String[] { h }, null, null, null);
michael@0 649 try {
michael@0 650 if (!c.moveToFirst()) {
michael@0 651 throw e;
michael@0 652 }
michael@0 653 this.id = (int) c.getLong(0);
michael@0 654 Logger.debug(LOG_TAG, "Found " + this.id + " for hash " + h);
michael@0 655 storage.envs.put(h, this.id);
michael@0 656 if (newTransaction) {
michael@0 657 db.setTransactionSuccessful();
michael@0 658 }
michael@0 659 return this.id;
michael@0 660 } finally {
michael@0 661 c.close();
michael@0 662 }
michael@0 663 }
michael@0 664 } finally {
michael@0 665 if (newTransaction) {
michael@0 666 db.endTransaction();
michael@0 667 }
michael@0 668 }
michael@0 669 }
michael@0 670
michael@0 671 protected static int ensureAddons(SQLiteDatabase db, String json) {
michael@0 672 Cursor c = db.query("addons", COLUMNS_ID, "body = ?",
michael@0 673 new String[] { (json == null) ? "null" : json }, null, null, null);
michael@0 674 try {
michael@0 675 if (c.moveToFirst()) {
michael@0 676 return c.getInt(0);
michael@0 677 }
michael@0 678 ContentValues values = new ContentValues();
michael@0 679 values.put("body", json);
michael@0 680 return (int) db.insert("addons", null, values);
michael@0 681 } finally {
michael@0 682 c.close();
michael@0 683 }
michael@0 684 }
michael@0 685
michael@0 686 public void init(ContentValues v) {
michael@0 687 version = v.containsKey("version") ? v.getAsInteger("version") : Environment.CURRENT_VERSION;
michael@0 688 profileCreation = v.getAsInteger("profileCreation");
michael@0 689 cpuCount = v.getAsInteger("cpuCount");
michael@0 690 memoryMB = v.getAsInteger("memoryMB");
michael@0 691
michael@0 692 isBlocklistEnabled = v.getAsInteger("isBlocklistEnabled");
michael@0 693 isTelemetryEnabled = v.getAsInteger("isTelemetryEnabled");
michael@0 694 extensionCount = v.getAsInteger("extensionCount");
michael@0 695 pluginCount = v.getAsInteger("pluginCount");
michael@0 696 themeCount = v.getAsInteger("themeCount");
michael@0 697
michael@0 698 architecture = v.getAsString("architecture");
michael@0 699 sysName = v.getAsString("sysName");
michael@0 700 sysVersion = v.getAsString("sysVersion");
michael@0 701 vendor = v.getAsString("vendor");
michael@0 702 appName = v.getAsString("appName");
michael@0 703 appID = v.getAsString("appID");
michael@0 704 appVersion = v.getAsString("appVersion");
michael@0 705 appBuildID = v.getAsString("appBuildID");
michael@0 706 platformVersion = v.getAsString("platformVersion");
michael@0 707 platformBuildID = v.getAsString("platformBuildID");
michael@0 708 os = v.getAsString("os");
michael@0 709 xpcomabi = v.getAsString("xpcomabi");
michael@0 710 updateChannel = v.getAsString("updateChannel");
michael@0 711
michael@0 712 distribution = v.getAsString("distribution");
michael@0 713 osLocale = v.getAsString("osLocale");
michael@0 714 appLocale = v.getAsString("appLocale");
michael@0 715 acceptLangSet = v.getAsInteger("acceptLangSet");
michael@0 716
michael@0 717 try {
michael@0 718 setJSONForAddons(v.getAsString("addonsBody"));
michael@0 719 } catch (Exception e) {
michael@0 720 // Nothing we can do.
michael@0 721 }
michael@0 722
michael@0 723 this.hash = null;
michael@0 724 this.id = -1;
michael@0 725 }
michael@0 726
michael@0 727 /**
michael@0 728 * Fill ourselves with data from the DB, then advance the cursor.
michael@0 729 *
michael@0 730 * @param cursor a {@link Cursor} pointing at a record to load.
michael@0 731 * @return true if the cursor was successfully advanced.
michael@0 732 */
michael@0 733 public boolean init(Cursor cursor) {
michael@0 734 int i = 0;
michael@0 735 this.id = cursor.getInt(i++);
michael@0 736 this.version = cursor.getInt(i++);
michael@0 737 this.hash = cursor.getString(i++);
michael@0 738
michael@0 739 profileCreation = cursor.getInt(i++);
michael@0 740 cpuCount = cursor.getInt(i++);
michael@0 741 memoryMB = cursor.getInt(i++);
michael@0 742
michael@0 743 isBlocklistEnabled = cursor.getInt(i++);
michael@0 744 isTelemetryEnabled = cursor.getInt(i++);
michael@0 745 extensionCount = cursor.getInt(i++);
michael@0 746 pluginCount = cursor.getInt(i++);
michael@0 747 themeCount = cursor.getInt(i++);
michael@0 748
michael@0 749 architecture = cursor.getString(i++);
michael@0 750 sysName = cursor.getString(i++);
michael@0 751 sysVersion = cursor.getString(i++);
michael@0 752 vendor = cursor.getString(i++);
michael@0 753 appName = cursor.getString(i++);
michael@0 754 appID = cursor.getString(i++);
michael@0 755 appVersion = cursor.getString(i++);
michael@0 756 appBuildID = cursor.getString(i++);
michael@0 757 platformVersion = cursor.getString(i++);
michael@0 758 platformBuildID = cursor.getString(i++);
michael@0 759 os = cursor.getString(i++);
michael@0 760 xpcomabi = cursor.getString(i++);
michael@0 761 updateChannel = cursor.getString(i++);
michael@0 762
michael@0 763 distribution = cursor.getString(i++);
michael@0 764 osLocale = cursor.getString(i++);
michael@0 765 appLocale = cursor.getString(i++);
michael@0 766 acceptLangSet = cursor.getInt(i++);
michael@0 767
michael@0 768 try {
michael@0 769 setJSONForAddons(cursor.getBlob(i++));
michael@0 770 } catch (Exception e) {
michael@0 771 // Nothing we can do.
michael@0 772 }
michael@0 773
michael@0 774 return cursor.moveToNext();
michael@0 775 }
michael@0 776
michael@0 777 public DatabaseEnvironment(HealthReportDatabaseStorage storage, Class<? extends EnvironmentAppender> appender) {
michael@0 778 super(appender);
michael@0 779 this.storage = storage;
michael@0 780 }
michael@0 781
michael@0 782 public DatabaseEnvironment(HealthReportDatabaseStorage storage) {
michael@0 783 this.storage = storage;
michael@0 784 }
michael@0 785 }
michael@0 786
michael@0 787 /**
michael@0 788 * Factory method. Returns a new {@link Environment} that callers can
michael@0 789 * populate and then register.
michael@0 790 */
michael@0 791 @Override
michael@0 792 public DatabaseEnvironment getEnvironment() {
michael@0 793 return new DatabaseEnvironment(this);
michael@0 794 }
michael@0 795
michael@0 796 @Override
michael@0 797 public SparseArray<Environment> getEnvironmentRecordsByID() {
michael@0 798 final SQLiteDatabase db = this.helper.getReadableDatabase();
michael@0 799 Cursor c = db.query("environments_with_addons", COLUMNS_ENVIRONMENT_DETAILS, null, null, null, null, null);
michael@0 800 try {
michael@0 801 SparseArray<Environment> results = new SparseArray<Environment>();
michael@0 802 if (!c.moveToFirst()) {
michael@0 803 return results;
michael@0 804 }
michael@0 805
michael@0 806 DatabaseEnvironment e = getEnvironment();
michael@0 807 while (e.init(c)) {
michael@0 808 results.put(e.id, e);
michael@0 809 e = getEnvironment();
michael@0 810 }
michael@0 811 results.put(e.id, e);
michael@0 812 return results;
michael@0 813 } finally {
michael@0 814 c.close();
michael@0 815 }
michael@0 816 }
michael@0 817
michael@0 818 /**
michael@0 819 * Reverse lookup for an env. Only really used for tests: document generation
michael@0 820 * fetches all environments at once, and insertion only uses the integer key
michael@0 821 * that's returned during insertion.
michael@0 822 *
michael@0 823 * @param id
michael@0 824 * the identifier for the environment.
michael@0 825 * @return a cursor over its details.
michael@0 826 */
michael@0 827 @Override
michael@0 828 public Cursor getEnvironmentRecordForID(int id) {
michael@0 829 final SQLiteDatabase db = this.helper.getReadableDatabase();
michael@0 830 return db.query("environments_with_addons", COLUMNS_ENVIRONMENT_DETAILS, "id = " + id, null, null, null, null);
michael@0 831 }
michael@0 832
michael@0 833 @Override
michael@0 834 public SparseArray<String> getEnvironmentHashesByID() {
michael@0 835 final SQLiteDatabase db = this.helper.getReadableDatabase();
michael@0 836 Cursor c = db.query("environments", new String[] {"id", "hash"}, null, null, null, null, null);
michael@0 837 try {
michael@0 838 SparseArray<String> results = new SparseArray<String>();
michael@0 839 if (!c.moveToFirst()) {
michael@0 840 return results;
michael@0 841 }
michael@0 842
michael@0 843 while (!c.isAfterLast()) {
michael@0 844 results.put(c.getInt(0), c.getString(1));
michael@0 845 c.moveToNext();
michael@0 846 }
michael@0 847 return results;
michael@0 848 } finally {
michael@0 849 c.close();
michael@0 850 }
michael@0 851 }
michael@0 852
michael@0 853 /**
michael@0 854 * Cache the lookup from measurement and field specifier to field instance.
michael@0 855 * This allows us to memoize the field ID lookup, too.
michael@0 856 */
michael@0 857 private HashMap<String, Field> fields = new HashMap<String, Field>();
michael@0 858 private boolean fieldsCacheUpdated = false;
michael@0 859
michael@0 860 private void invalidateFieldsCache() {
michael@0 861 synchronized (this.fields) {
michael@0 862 fieldsCacheUpdated = false;
michael@0 863 }
michael@0 864 }
michael@0 865
michael@0 866 private String getFieldKey(String mName, int mVersion, String fieldName) {
michael@0 867 return mVersion + "." + mName + "/" + fieldName;
michael@0 868 }
michael@0 869
michael@0 870 @Override
michael@0 871 public Field getField(String mName, int mVersion, String fieldName) {
michael@0 872 final String key = getFieldKey(mName, mVersion, fieldName);
michael@0 873 synchronized (fields) {
michael@0 874 if (fields.containsKey(key)) {
michael@0 875 return fields.get(key);
michael@0 876 }
michael@0 877 Field f = new DatabaseField(mName, mVersion, fieldName);
michael@0 878 fields.put(key, f);
michael@0 879 return f;
michael@0 880 }
michael@0 881 }
michael@0 882
michael@0 883 private void populateFieldCache() {
michael@0 884 synchronized (fields) {
michael@0 885 if (fieldsCacheUpdated) {
michael@0 886 return;
michael@0 887 }
michael@0 888
michael@0 889 fields.clear();
michael@0 890 Cursor c = getFieldVersions();
michael@0 891 try {
michael@0 892 if (!c.moveToFirst()) {
michael@0 893 return;
michael@0 894 }
michael@0 895 do {
michael@0 896 // We don't use the measurement ID here, so column 1 is unused.
michael@0 897 final String mName = c.getString(0);
michael@0 898 final int mVersion = c.getInt(2);
michael@0 899 final String fieldName = c.getString(3);
michael@0 900 final int fieldID = c.getInt(4);
michael@0 901 final int flags = c.getInt(5);
michael@0 902 final String key = getFieldKey(mName, mVersion, fieldName);
michael@0 903
michael@0 904 Field f = new DatabaseField(mName, mVersion, fieldName, fieldID, flags);
michael@0 905 fields.put(key, f);
michael@0 906 } while (c.moveToNext());
michael@0 907 fieldsCacheUpdated = true;
michael@0 908 } finally {
michael@0 909 c.close();
michael@0 910 }
michael@0 911 }
michael@0 912 }
michael@0 913
michael@0 914 /**
michael@0 915 * Return mappings from field ID to Field instance. Do so by looking in the DB.
michael@0 916 */
michael@0 917 @Override
michael@0 918 public SparseArray<Field> getFieldsByID() {
michael@0 919 final SparseArray<Field> out = new SparseArray<Field>();
michael@0 920 synchronized (fields) {
michael@0 921 populateFieldCache();
michael@0 922 Collection<Field> values = fields.values();
michael@0 923 for (Field field : values) {
michael@0 924 // Cache is up-to-date at this point, so we don't need to hit the DB.
michael@0 925 out.put(field.getID(), field);
michael@0 926 }
michael@0 927 }
michael@0 928 return out;
michael@0 929 }
michael@0 930
michael@0 931 private final HashMap<String, Integer> measurementVersions = new HashMap<String, Integer>();
michael@0 932
michael@0 933 private void populateMeasurementVersionsCache(SQLiteDatabase db) {
michael@0 934 HashMap<String, Integer> results = getIntegers(db, "current_measurements", "name", "version");
michael@0 935 if (results == null) {
michael@0 936 measurementVersions.clear();
michael@0 937 return;
michael@0 938 }
michael@0 939 synchronized (measurementVersions) {
michael@0 940 measurementVersions.clear();
michael@0 941 measurementVersions.putAll(results);
michael@0 942 }
michael@0 943 }
michael@0 944
michael@0 945 /**
michael@0 946 * Return the version of the measurement for which the DB is currently configured, or
michael@0 947 * 0 if unknown.
michael@0 948 * @param measurement String measurement identifier.
michael@0 949 * @return Current version.
michael@0 950 */
michael@0 951 private int getMeasurementVersion(String measurement) {
michael@0 952 synchronized (measurementVersions) {
michael@0 953 if (measurementVersions.containsKey(measurement)) {
michael@0 954 return measurementVersions.get(measurement);
michael@0 955 }
michael@0 956
michael@0 957 // This should never be necessary, unless the measurement does not exist.
michael@0 958 int value = integerQuery("measurements", "version", "name = ?", new String[] {measurement}, 0);
michael@0 959 measurementVersions.put(measurement, value);
michael@0 960 return value;
michael@0 961 }
michael@0 962 }
michael@0 963
michael@0 964 /**
michael@0 965 * Inform the storage layer that fields for the given measurement have been updated
michael@0 966 * to this version.
michael@0 967 *
michael@0 968 * This should be one of the final calls in a configuration transaction.
michael@0 969 * Always call this inside a transaction.
michael@0 970 */
michael@0 971 private void notifyMeasurementVersionUpdated(String measurement, int version) {
michael@0 972 Logger.info(LOG_TAG, "Measurement " + measurement + " now at " + version);
michael@0 973
michael@0 974 final SQLiteDatabase db = this.helper.getWritableDatabase();
michael@0 975 final ContentValues values = new ContentValues();
michael@0 976 values.put("name", measurement);
michael@0 977 values.put("version", version);
michael@0 978
michael@0 979 synchronized (measurementVersions) {
michael@0 980 measurementVersions.put(measurement, version);
michael@0 981 }
michael@0 982
michael@0 983 db.insertWithOnConflict("measurements", null, values, SQLiteDatabase.CONFLICT_IGNORE);
michael@0 984 }
michael@0 985
michael@0 986 /**
michael@0 987 * Call in a transaction.
michael@0 988 * This method could race with other accesses, but (a) it's within a transaction,
michael@0 989 * (b) configuration should be single-threaded, (c) we initialize the cache up-front.
michael@0 990 */
michael@0 991 @Override
michael@0 992 public void ensureMeasurementInitialized(String measurement, int version, MeasurementFields fields) {
michael@0 993 final int currentVersion = getMeasurementVersion(measurement);
michael@0 994 Logger.info(LOG_TAG, "Initializing measurement " + measurement + " to " +
michael@0 995 version + " (current " + currentVersion + ")");
michael@0 996
michael@0 997 if (currentVersion == version) {
michael@0 998 Logger.info(LOG_TAG, "Measurement " + measurement + " already at v" + version);
michael@0 999 return;
michael@0 1000 }
michael@0 1001
michael@0 1002 final SQLiteDatabase db = this.helper.getWritableDatabase();
michael@0 1003 if (!db.inTransaction()) {
michael@0 1004 Logger.warn(LOG_TAG, "ensureMeasurementInitialized should be called within a transaction.");
michael@0 1005 }
michael@0 1006
michael@0 1007 final ContentValues mv = new ContentValues();
michael@0 1008 mv.put("name", measurement);
michael@0 1009 mv.put("version", version);
michael@0 1010
michael@0 1011 final int measurementID = (int) db.insert("measurements", null, mv);
michael@0 1012
michael@0 1013 final ContentValues v = new ContentValues();
michael@0 1014 v.put("measurement", measurementID);
michael@0 1015 for (FieldSpec field : fields.getFields()) {
michael@0 1016 v.put("name", field.name);
michael@0 1017 v.put("flags", field.type);
michael@0 1018 Logger.debug(LOG_TAG, "M: " + measurementID + " F: " + field.name + " (" + field.type + ")");
michael@0 1019 db.insert("fields", null, v);
michael@0 1020 }
michael@0 1021
michael@0 1022 notifyMeasurementVersionUpdated(measurement, version);
michael@0 1023
michael@0 1024 // Let's be easy for now.
michael@0 1025 invalidateFieldsCache();
michael@0 1026 }
michael@0 1027
michael@0 1028 /**
michael@0 1029 * Return a cursor over the measurements and fields in the DB.
michael@0 1030 * Columns are {@link HealthReportDatabaseStorage#COLUMNS_MEASUREMENT_AND_FIELD_DETAILS}.
michael@0 1031 */
michael@0 1032 @Override
michael@0 1033 public Cursor getFieldVersions() {
michael@0 1034 final SQLiteDatabase db = this.helper.getReadableDatabase();
michael@0 1035 return db.query("named_fields", COLUMNS_MEASUREMENT_AND_FIELD_DETAILS,
michael@0 1036 null, null, null, null, "measurement_name, measurement_version, field_name");
michael@0 1037 }
michael@0 1038
michael@0 1039 @Override
michael@0 1040 public Cursor getFieldVersions(String measurement, int measurementVersion) {
michael@0 1041 final SQLiteDatabase db = this.helper.getReadableDatabase();
michael@0 1042 return db.query("named_fields", COLUMNS_MEASUREMENT_AND_FIELD_DETAILS,
michael@0 1043 "measurement_name = ? AND measurement_version = ?",
michael@0 1044 new String[] {measurement, Integer.toString(measurementVersion)},
michael@0 1045 null, null, "field_name");
michael@0 1046 }
michael@0 1047
michael@0 1048 @Override
michael@0 1049 public Cursor getMeasurementVersions() {
michael@0 1050 final SQLiteDatabase db = this.helper.getReadableDatabase();
michael@0 1051 return db.query("measurements", COLUMNS_MEASUREMENT_DETAILS,
michael@0 1052 null, null, null, null, "name, version");
michael@0 1053 }
michael@0 1054
michael@0 1055 /**
michael@0 1056 * A thin wrapper around the database transactional semantics. Clients can
michael@0 1057 * use this to more efficiently ensure that measurements are initialized.
michael@0 1058 *
michael@0 1059 * Note that caches are also initialized here.
michael@0 1060 */
michael@0 1061 public void beginInitialization() {
michael@0 1062 SQLiteDatabase db = this.helper.getWritableDatabase();
michael@0 1063 db.beginTransaction();
michael@0 1064 populateMeasurementVersionsCache(db);
michael@0 1065 }
michael@0 1066
michael@0 1067 public void finishInitialization() {
michael@0 1068 SQLiteDatabase db = this.helper.getWritableDatabase();
michael@0 1069 db.setTransactionSuccessful();
michael@0 1070 db.endTransaction();
michael@0 1071 }
michael@0 1072
michael@0 1073 public void abortInitialization() {
michael@0 1074 this.helper.getWritableDatabase().endTransaction();
michael@0 1075 }
michael@0 1076
michael@0 1077 protected int getIntFromQuery(final String sql, final String[] selectionArgs) {
michael@0 1078 final SQLiteDatabase db = this.helper.getReadableDatabase();
michael@0 1079 final Cursor c = db.rawQuery(sql, selectionArgs);
michael@0 1080 try {
michael@0 1081 if (!c.moveToFirst()) {
michael@0 1082 throw new IllegalStateException("Cursor is empty.");
michael@0 1083 }
michael@0 1084 return c.getInt(0);
michael@0 1085 } finally {
michael@0 1086 c.close();
michael@0 1087 }
michael@0 1088 }
michael@0 1089
michael@0 1090 @Override
michael@0 1091 public int getDay(long time) {
michael@0 1092 return DateUtils.getDay(time);
michael@0 1093 }
michael@0 1094
michael@0 1095 @Override
michael@0 1096 public int getDay() {
michael@0 1097 return this.getDay(System.currentTimeMillis());
michael@0 1098 }
michael@0 1099
michael@0 1100 private void recordDailyLast(int env, int day, int field, Object value, String table) {
michael@0 1101 if (env == -1) {
michael@0 1102 Logger.warn(LOG_TAG, "Refusing to record with environment = -1.");
michael@0 1103 return;
michael@0 1104 }
michael@0 1105
michael@0 1106 final SQLiteDatabase db = this.helper.getWritableDatabase();
michael@0 1107
michael@0 1108 final String envString = Integer.toString(env);
michael@0 1109 final String fieldIDString = Integer.toString(field, 10);
michael@0 1110 final String dayString = Integer.toString(day, 10);
michael@0 1111
michael@0 1112 // Java, your capacity for abstraction leaves me wanting.
michael@0 1113 final ContentValues v = new ContentValues();
michael@0 1114 putValue(v, value);
michael@0 1115
michael@0 1116 // If we used a separate table, such that we could have a
michael@0 1117 // UNIQUE(env, field, day) constraint for daily-last values, then we could
michael@0 1118 // use INSERT OR REPLACE.
michael@0 1119 final int updated = db.update(table, v, WHERE_DATE_AND_ENV_AND_FIELD,
michael@0 1120 new String[] {dayString, envString, fieldIDString});
michael@0 1121 if (0 == updated) {
michael@0 1122 v.put("env", env);
michael@0 1123 v.put("field", field);
michael@0 1124 v.put("date", day);
michael@0 1125 try {
michael@0 1126 db.insertOrThrow(table, null, v);
michael@0 1127 } catch (SQLiteConstraintException e) {
michael@0 1128 throw new IllegalStateException("Event did not reference existing an environment or field.", e);
michael@0 1129 }
michael@0 1130 }
michael@0 1131 }
michael@0 1132
michael@0 1133 @Override
michael@0 1134 public void recordDailyLast(int env, int day, int field, JSONObject value) {
michael@0 1135 this.recordDailyLast(env, day, field, value == null ? "null" : value.toString(), EVENTS_TEXTUAL);
michael@0 1136 }
michael@0 1137
michael@0 1138 @Override
michael@0 1139 public void recordDailyLast(int env, int day, int field, String value) {
michael@0 1140 this.recordDailyLast(env, day, field, value, EVENTS_TEXTUAL);
michael@0 1141 }
michael@0 1142
michael@0 1143 @Override
michael@0 1144 public void recordDailyLast(int env, int day, int field, int value) {
michael@0 1145 this.recordDailyLast(env, day, field, Integer.valueOf(value), EVENTS_INTEGER);
michael@0 1146 }
michael@0 1147
michael@0 1148 private void recordDailyDiscrete(int env, int day, int field, Object value, String table) {
michael@0 1149 if (env == -1) {
michael@0 1150 Logger.warn(LOG_TAG, "Refusing to record with environment = -1.");
michael@0 1151 return;
michael@0 1152 }
michael@0 1153
michael@0 1154 final ContentValues v = new ContentValues();
michael@0 1155 v.put("env", env);
michael@0 1156 v.put("field", field);
michael@0 1157 v.put("date", day);
michael@0 1158
michael@0 1159 final SQLiteDatabase db = this.helper.getWritableDatabase();
michael@0 1160 putValue(v, value);
michael@0 1161
michael@0 1162 // Using SQLiteDatabase.insertOrThrow throws SQLiteConstraintException we cannot catch for
michael@0 1163 // unknown reasons (bug 961526 comment 13). We believe these are thrown because we attempt to
michael@0 1164 // record events using environment IDs removed from the database by the prune service. We
michael@0 1165 // invalidate the currentEnvironment ID after pruning, preventing further propagation,
michael@0 1166 // however, any event recording waiting for the prune service to complete on the background
michael@0 1167 // thread may carry an invalid ID: we expect an insertion failure and drop these events here.
michael@0 1168 final long res = db.insert(table, null, v);
michael@0 1169 if (res == -1) {
michael@0 1170 Logger.error(LOG_TAG, "Unable to record daily discrete event. Ignoring.");
michael@0 1171 }
michael@0 1172 }
michael@0 1173
michael@0 1174 @Override
michael@0 1175 public void recordDailyDiscrete(int env, int day, int field, JSONObject value) {
michael@0 1176 this.recordDailyDiscrete(env, day, field, value == null ? "null" : value.toString(), EVENTS_TEXTUAL);
michael@0 1177 }
michael@0 1178
michael@0 1179 @Override
michael@0 1180 public void recordDailyDiscrete(int env, int day, int field, String value) {
michael@0 1181 this.recordDailyDiscrete(env, day, field, value, EVENTS_TEXTUAL);
michael@0 1182 }
michael@0 1183
michael@0 1184 @Override
michael@0 1185 public void recordDailyDiscrete(int env, int day, int field, int value) {
michael@0 1186 this.recordDailyDiscrete(env, day, field, value, EVENTS_INTEGER);
michael@0 1187 }
michael@0 1188
michael@0 1189 /**
michael@0 1190 * Increment the specified field value by the specified amount. Counts start
michael@0 1191 * at zero.
michael@0 1192 *
michael@0 1193 * Note that this method can misbehave or throw if not executed within a
michael@0 1194 * transaction, because correct behavior involves querying then
michael@0 1195 * insert-or-update, and a race condition can otherwise occur.
michael@0 1196 *
michael@0 1197 * @param env the environment ID
michael@0 1198 * @param day the current day, in days since epoch
michael@0 1199 * @param field the field ID
michael@0 1200 * @param by how much to increment the counter.
michael@0 1201 */
michael@0 1202 @Override
michael@0 1203 public void incrementDailyCount(int env, int day, int field, int by) {
michael@0 1204 if (env == -1) {
michael@0 1205 Logger.warn(LOG_TAG, "Refusing to record with environment = -1.");
michael@0 1206 return;
michael@0 1207 }
michael@0 1208
michael@0 1209 final SQLiteDatabase db = this.helper.getWritableDatabase();
michael@0 1210 final String envString = Integer.toString(env);
michael@0 1211 final String fieldIDString = Integer.toString(field, 10);
michael@0 1212 final String dayString = Integer.toString(day, 10);
michael@0 1213
michael@0 1214 // Can't run a complex UPDATE and get the number of changed rows, so we'll
michael@0 1215 // do this the hard way.
michael@0 1216 // This relies on being called within a transaction.
michael@0 1217 final String[] args = new String[] {dayString, envString, fieldIDString};
michael@0 1218 final Cursor c = db.query(EVENTS_INTEGER,
michael@0 1219 COLUMNS_VALUE,
michael@0 1220 WHERE_DATE_AND_ENV_AND_FIELD,
michael@0 1221 args, null, null, null, "1");
michael@0 1222
michael@0 1223 boolean present = false;
michael@0 1224 try {
michael@0 1225 present = c.moveToFirst();
michael@0 1226 } finally {
michael@0 1227 c.close();
michael@0 1228 }
michael@0 1229
michael@0 1230 if (present) {
michael@0 1231 // It's an int, so safe to concatenate. Avoids us having to mess with args.
michael@0 1232 db.execSQL("UPDATE " + EVENTS_INTEGER + " SET value = value + " + by + " WHERE " +
michael@0 1233 WHERE_DATE_AND_ENV_AND_FIELD,
michael@0 1234 args);
michael@0 1235 } else {
michael@0 1236 final ContentValues v = new ContentValues();
michael@0 1237 v.put("env", env);
michael@0 1238 v.put("value", by);
michael@0 1239 v.put("field", field);
michael@0 1240 v.put("date", day);
michael@0 1241 try {
michael@0 1242 db.insertOrThrow(EVENTS_INTEGER, null, v);
michael@0 1243 } catch (SQLiteConstraintException e) {
michael@0 1244 throw new IllegalStateException("Event did not reference existing an environment or field.", e);
michael@0 1245 }
michael@0 1246 }
michael@0 1247 }
michael@0 1248
michael@0 1249 @Override
michael@0 1250 public void incrementDailyCount(int env, int day, int field) {
michael@0 1251 this.incrementDailyCount(env, day, field, 1);
michael@0 1252 }
michael@0 1253
michael@0 1254 /**
michael@0 1255 * Are there events recorded on or after <code>time</code>?
michael@0 1256 *
michael@0 1257 * @param time milliseconds since epoch. Will be converted by {@link #getDay(long)}.
michael@0 1258 * @return true if such events exist, false otherwise.
michael@0 1259 */
michael@0 1260 @Override
michael@0 1261 public boolean hasEventSince(long time) {
michael@0 1262 final int start = this.getDay(time);
michael@0 1263 final SQLiteDatabase db = this.helper.getReadableDatabase();
michael@0 1264 final String dayString = Integer.toString(start, 10);
michael@0 1265 Cursor cur = db.query("events", COLUMNS_DATE_ENV_FIELD_VALUE,
michael@0 1266 "date >= ?", new String[] {dayString}, null, null, null, "1");
michael@0 1267 if (cur == null) {
michael@0 1268 // Something is horribly wrong; let the caller who actually reads the
michael@0 1269 // events deal with it.
michael@0 1270 return true;
michael@0 1271 }
michael@0 1272 try {
michael@0 1273 return cur.getCount() > 0;
michael@0 1274 } finally {
michael@0 1275 cur.close();
michael@0 1276 }
michael@0 1277 }
michael@0 1278
michael@0 1279 /**
michael@0 1280 * Returns a cursor over field events in the database. The results will be
michael@0 1281 * strictly ordered first by date, then by environment, and finally by field.
michael@0 1282 *
michael@0 1283 * Each row includes columns in {@link #COLUMNS_DATE_ENV_FIELD_VALUE}:
michael@0 1284 * "date", "env", "field", "value".
michael@0 1285 *
michael@0 1286 * @param time milliseconds since epoch. Will be converted by {@link #getDay(long)}.
michael@0 1287 * @return a cursor. The caller is responsible for closing this.
michael@0 1288 */
michael@0 1289 @Override
michael@0 1290 public Cursor getRawEventsSince(long time) {
michael@0 1291 final int start = this.getDay(time);
michael@0 1292 final SQLiteDatabase db = this.helper.getReadableDatabase();
michael@0 1293 final String dayString = Integer.toString(start, 10);
michael@0 1294 return db.query("events", COLUMNS_DATE_ENV_FIELD_VALUE,
michael@0 1295 "date >= ?", new String[] {dayString}, null, null, "date, env, field");
michael@0 1296 }
michael@0 1297
michael@0 1298 /**
michael@0 1299 * Returns a cursor over field events in the database. The results will be
michael@0 1300 * strictly ordered first by date, then by environment, and finally by field.
michael@0 1301 *
michael@0 1302 * Each row includes columns in {@link #COLUMNS_DATE_ENVSTR_M_MV_F_VALUE}:
michael@0 1303 * "date", "environment" (as a String), "measurement_name", "measurement_version",
michael@0 1304 * "field_name", "field_flags", "value".
michael@0 1305 *
michael@0 1306 * @param time milliseconds since epoch. Will be converted by {@link #getDay(long)}.
michael@0 1307 * @return a cursor. The caller is responsible for closing this.
michael@0 1308 */
michael@0 1309 @Override
michael@0 1310 public Cursor getEventsSince(long time) {
michael@0 1311 final int start = this.getDay(time);
michael@0 1312 final SQLiteDatabase db = this.helper.getReadableDatabase();
michael@0 1313 final String dayString = Integer.toString(start, 10);
michael@0 1314 return db.query("named_events", COLUMNS_DATE_ENVSTR_M_MV_F_VALUE,
michael@0 1315 "date >= ?", new String[] {dayString}, null, null,
michael@0 1316 "date, environment, measurement_name, measurement_version, field_name");
michael@0 1317 }
michael@0 1318
michael@0 1319 public int getEventCount() {
michael@0 1320 return getRowCount("events");
michael@0 1321 }
michael@0 1322
michael@0 1323 public int getEnvironmentCount() {
michael@0 1324 return getRowCount("environments");
michael@0 1325 }
michael@0 1326
michael@0 1327 private int getRowCount(String table) {
michael@0 1328 // table should be parameterized, but SQL throws a compilation error if the table in unknown
michael@0 1329 // in advance.
michael@0 1330 return getIntFromQuery("SELECT COUNT(*) from " + table, null);
michael@0 1331 }
michael@0 1332
michael@0 1333 /**
michael@0 1334 * Deletes all environments, addons, and events from the database before the given time. If this
michael@0 1335 * data does not have recorded dates but are no longer referenced by other fields, they are also
michael@0 1336 * removed (with exception to the current environment).
michael@0 1337 *
michael@0 1338 * @param time milliseconds since epoch. Will be converted by {@link #getDay(long)}.
michael@0 1339 * @param curEnv The ID of the current environment.
michael@0 1340 * @return The number of environments and addon entries deleted.
michael@0 1341 */
michael@0 1342 public int deleteDataBefore(final long time, final int curEnv) {
michael@0 1343 final SQLiteDatabase db = this.helper.getWritableDatabase();
michael@0 1344 db.beginTransaction();
michael@0 1345 int numRowsDeleted = 0;
michael@0 1346 try {
michael@0 1347 numRowsDeleted += deleteEnvAndEventsBefore(db, time, curEnv);
michael@0 1348 numRowsDeleted += deleteOrphanedAddons(db);
michael@0 1349 db.setTransactionSuccessful();
michael@0 1350 } finally {
michael@0 1351 db.endTransaction();
michael@0 1352 }
michael@0 1353 return numRowsDeleted;
michael@0 1354 }
michael@0 1355
michael@0 1356 /**
michael@0 1357 * Deletes environments and their referring events recorded before the given time. Environments
michael@0 1358 * referenced by no events are deleted, except for the current environment.
michael@0 1359 *
michael@0 1360 * @param time milliseconds since epoch. Will be converted by {@link #getDay(long)}.
michael@0 1361 * @param curEnv The ID of the current environment.
michael@0 1362 * @return The number of environments (not events) deleted.
michael@0 1363 */
michael@0 1364 protected int deleteEnvAndEventsBefore(final long time, final int curEnv) {
michael@0 1365 final SQLiteDatabase db = this.helper.getWritableDatabase();
michael@0 1366 return deleteEnvAndEventsBefore(db, time, curEnv);
michael@0 1367 }
michael@0 1368
michael@0 1369 // Called internally only to ensure the same db instance is used.
michael@0 1370 protected int deleteEnvAndEventsBefore(final SQLiteDatabase db, final long time, final int curEnv) {
michael@0 1371 // Delete environments only referenced by events occuring before the given time. Cascade
michael@0 1372 // delete these events.
michael@0 1373 String whereClause =
michael@0 1374 "(SELECT COUNT(*) FROM events WHERE date >= ? " +
michael@0 1375 " AND events.env = environments.id) = 0 " +
michael@0 1376 "AND id IN (SELECT DISTINCT env FROM events WHERE date < ?)";
michael@0 1377 final int day = this.getDay(time);
michael@0 1378 final String dayString = Integer.toString(day, 10);
michael@0 1379 String[] whereArgs = new String[] {dayString, dayString};
michael@0 1380
michael@0 1381 int numEnvDeleted = 0;
michael@0 1382 db.beginTransaction();
michael@0 1383 try {
michael@0 1384 numEnvDeleted += db.delete("environments", whereClause, whereArgs);
michael@0 1385 numEnvDeleted += deleteOrphanedEnv(db, curEnv);
michael@0 1386 // We can't get the number of events deleted through cascading deletions so we do not record
michael@0 1387 // the number of events deleted here.
michael@0 1388 deleteEventsBefore(db, dayString);
michael@0 1389 db.setTransactionSuccessful();
michael@0 1390 } finally {
michael@0 1391 db.endTransaction();
michael@0 1392 }
michael@0 1393 return numEnvDeleted;
michael@0 1394 }
michael@0 1395
michael@0 1396 /**
michael@0 1397 * Deletes environments not referenced by any events except for the given current environment.
michael@0 1398 */
michael@0 1399 protected int deleteOrphanedEnv(final int curEnv) {
michael@0 1400 final SQLiteDatabase db = this.helper.getWritableDatabase();
michael@0 1401 return deleteOrphanedEnv(db, curEnv);
michael@0 1402 }
michael@0 1403
michael@0 1404 // Called internally only to ensure the same db instance is used.
michael@0 1405 @SuppressWarnings("static-method")
michael@0 1406 protected int deleteOrphanedEnv(final SQLiteDatabase db, final int curEnv) {
michael@0 1407 final String whereClause =
michael@0 1408 "id != ? AND " +
michael@0 1409 "id NOT IN (SELECT env FROM events)";
michael@0 1410 final String[] whereArgs = new String[] {Integer.toString(curEnv)};
michael@0 1411 return db.delete("environments", whereClause, whereArgs);
michael@0 1412 }
michael@0 1413
michael@0 1414 protected int deleteEventsBefore(final String dayString) {
michael@0 1415 final SQLiteDatabase db = this.helper.getWritableDatabase();
michael@0 1416 return deleteEventsBefore(db, dayString);
michael@0 1417 }
michael@0 1418
michael@0 1419 // Called internally only to ensure the same db instance is used.
michael@0 1420 @SuppressWarnings("static-method")
michael@0 1421 protected int deleteEventsBefore(final SQLiteDatabase db, final String dayString) {
michael@0 1422 final String whereClause = "date < ?";
michael@0 1423 final String[] whereArgs = new String[] {dayString};
michael@0 1424 int numEventsDeleted = 0;
michael@0 1425 db.beginTransaction();
michael@0 1426 try {
michael@0 1427 numEventsDeleted += db.delete("events_integer", whereClause, whereArgs);
michael@0 1428 numEventsDeleted += db.delete("events_textual", whereClause, whereArgs);
michael@0 1429 db.setTransactionSuccessful();
michael@0 1430 } finally {
michael@0 1431 db.endTransaction();
michael@0 1432 }
michael@0 1433 return numEventsDeleted;
michael@0 1434 }
michael@0 1435
michael@0 1436 /**
michael@0 1437 * Deletes addons not referenced by any environments.
michael@0 1438 */
michael@0 1439 protected int deleteOrphanedAddons() {
michael@0 1440 final SQLiteDatabase db = this.helper.getWritableDatabase();
michael@0 1441 return deleteOrphanedAddons(db);
michael@0 1442 }
michael@0 1443
michael@0 1444 // Called internally only to ensure the same db instance is used.
michael@0 1445 @SuppressWarnings("static-method")
michael@0 1446 protected int deleteOrphanedAddons(final SQLiteDatabase db) {
michael@0 1447 final String whereClause = "id NOT IN (SELECT addonsID FROM environments)";
michael@0 1448 return db.delete("addons", whereClause, null);
michael@0 1449 }
michael@0 1450
michael@0 1451 /**
michael@0 1452 * Retrieve a mapping from a table. Keys should be unique; only one key-value
michael@0 1453 * pair will be returned for each key.
michael@0 1454 */
michael@0 1455 private static HashMap<String, Integer> getIntegers(SQLiteDatabase db, String table, String columnA, String columnB) {
michael@0 1456 Cursor c = db.query(table, new String[] {columnA, columnB}, null, null, null, null, null);
michael@0 1457 try {
michael@0 1458 if (!c.moveToFirst()) {
michael@0 1459 return null;
michael@0 1460 }
michael@0 1461
michael@0 1462 HashMap<String, Integer> results = new HashMap<String, Integer>();
michael@0 1463 while (!c.isAfterLast()) {
michael@0 1464 results.put(c.getString(0), c.getInt(1));
michael@0 1465 c.moveToNext();
michael@0 1466 }
michael@0 1467 return results;
michael@0 1468 } finally {
michael@0 1469 c.close();
michael@0 1470 }
michael@0 1471 }
michael@0 1472
michael@0 1473 /**
michael@0 1474 * Retrieve a single value from a mapping table.
michael@0 1475 */
michael@0 1476 private int integerQuery(String table, String column, String where, String[] args, int defaultValue) {
michael@0 1477 final SQLiteDatabase db = this.helper.getReadableDatabase();
michael@0 1478 Cursor c = db.query(table, new String[] {column}, where, args, null, null, column + " DESC", "1");
michael@0 1479 try {
michael@0 1480 if (!c.moveToFirst()) {
michael@0 1481 return defaultValue;
michael@0 1482 }
michael@0 1483 return c.getInt(0);
michael@0 1484 } finally {
michael@0 1485 c.close();
michael@0 1486 }
michael@0 1487 }
michael@0 1488
michael@0 1489 /**
michael@0 1490 * Helper to allow us to avoid excessive code duplication.
michael@0 1491 *
michael@0 1492 * @param v
michael@0 1493 * the destination <code>ContentValues</code>.
michael@0 1494 * @param value
michael@0 1495 * either a <code>String</code> or an <code>Integer</code>. No type
michael@0 1496 * checking is performed.
michael@0 1497 */
michael@0 1498 private static final void putValue(final ContentValues v, Object value) {
michael@0 1499 if (value instanceof String) {
michael@0 1500 v.put("value", (String) value);
michael@0 1501 } else {
michael@0 1502 v.put("value", (Integer) value);
michael@0 1503 }
michael@0 1504 }
michael@0 1505
michael@0 1506 @Override
michael@0 1507 public void deleteEverything() {
michael@0 1508 this.helper.deleteEverything();
michael@0 1509 }
michael@0 1510
michael@0 1511 @Override
michael@0 1512 public void deleteEnvironments() {
michael@0 1513 final SQLiteDatabase db = this.helper.getWritableDatabase();
michael@0 1514 db.beginTransaction();
michael@0 1515 try {
michael@0 1516 // Cascade will clear the rest.
michael@0 1517 db.delete("environments", null, null);
michael@0 1518 db.setTransactionSuccessful();
michael@0 1519 } finally {
michael@0 1520 db.endTransaction();
michael@0 1521 }
michael@0 1522 }
michael@0 1523
michael@0 1524 @Override
michael@0 1525 public void deleteMeasurements() {
michael@0 1526 final SQLiteDatabase db = this.helper.getWritableDatabase();
michael@0 1527 db.beginTransaction();
michael@0 1528 try {
michael@0 1529 // Cascade will clear the rest.
michael@0 1530 db.delete("measurements", null, null);
michael@0 1531
michael@0 1532 // Clear measurements and fields cache, because some of their IDs are now invalid.
michael@0 1533 invalidateFieldsCache(); // Let it repopulate on its own.
michael@0 1534 populateMeasurementVersionsCache(db); // Performed at Storage init so repopulate now.
michael@0 1535
michael@0 1536 db.setTransactionSuccessful();
michael@0 1537 } finally {
michael@0 1538 db.endTransaction();
michael@0 1539 }
michael@0 1540 }
michael@0 1541
michael@0 1542 /**
michael@0 1543 * Prunes the given number of least-recently used environments. Note that orphaned environments
michael@0 1544 * are not removed and the environment cache is cleared.
michael@0 1545 */
michael@0 1546 public void pruneEnvironments(final int numToPrune) {
michael@0 1547 final SQLiteDatabase db = this.helper.getWritableDatabase();
michael@0 1548 db.beginTransaction();
michael@0 1549 try {
michael@0 1550 db.delete("environments",
michael@0 1551 "id in (SELECT env " +
michael@0 1552 " FROM events " +
michael@0 1553 " GROUP BY env " +
michael@0 1554 " ORDER BY MAX(date), env " +
michael@0 1555 " LIMIT " + numToPrune + ")",
michael@0 1556 null);
michael@0 1557 db.setTransactionSuccessful();
michael@0 1558
michael@0 1559 // Clear environment cache, because some of their IDs are now invalid.
michael@0 1560 this.envs.clear();
michael@0 1561 } finally {
michael@0 1562 db.endTransaction();
michael@0 1563 }
michael@0 1564 }
michael@0 1565
michael@0 1566 /**
michael@0 1567 * Prunes up to a maximum of the given number of the oldest events. While it is more correct to
michael@0 1568 * prune the exact given amount, there is no unique identifier among events so we cannot be so
michael@0 1569 * precise. Instead, we prune on date, deleting all events up to the day before our count of
michael@0 1570 * events reaches the given maximum. Note that this technicality means this method cannot be
michael@0 1571 * used to delete all events.
michael@0 1572 */
michael@0 1573 public void pruneEvents(final int maxNumToPrune) {
michael@0 1574 final SQLiteDatabase db = this.helper.getWritableDatabase();
michael@0 1575
michael@0 1576 final Cursor c = db.rawQuery(
michael@0 1577 "SELECT MAX(date) " +
michael@0 1578 "FROM (SELECT date " +
michael@0 1579 " FROM events " +
michael@0 1580 " ORDER BY date " +
michael@0 1581 " LIMIT " + maxNumToPrune + ")",
michael@0 1582 null);
michael@0 1583 long pruneDate = -1;
michael@0 1584 try {
michael@0 1585 if (!c.moveToFirst()) {
michael@0 1586 Logger.debug(LOG_TAG, "No max date found in events: table is likely empty. Not pruning " +
michael@0 1587 "events.");
michael@0 1588 return;
michael@0 1589 }
michael@0 1590 pruneDate = c.getLong(0);
michael@0 1591 } finally {
michael@0 1592 c.close();
michael@0 1593 }
michael@0 1594
michael@0 1595 final String selection = "date < " + pruneDate;
michael@0 1596 db.beginTransaction();
michael@0 1597 try {
michael@0 1598 db.delete(EVENTS_INTEGER, selection, null);
michael@0 1599 db.delete(EVENTS_TEXTUAL, selection, null);
michael@0 1600 db.setTransactionSuccessful();
michael@0 1601 } finally {
michael@0 1602 db.endTransaction();
michael@0 1603 }
michael@0 1604 }
michael@0 1605
michael@0 1606 public void vacuum() {
michael@0 1607 final SQLiteDatabase db = this.helper.getWritableDatabase();
michael@0 1608 db.execSQL("vacuum");
michael@0 1609 }
michael@0 1610
michael@0 1611 /**
michael@0 1612 * Disables auto_vacuuming. Changes may only take effect after a "vacuum" command.
michael@0 1613 */
michael@0 1614 public void disableAutoVacuuming() {
michael@0 1615 final SQLiteDatabase db = this.helper.getWritableDatabase();
michael@0 1616 db.execSQL("PRAGMA auto_vacuum=0");
michael@0 1617 }
michael@0 1618 }

mercurial