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 +}