michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: package org.mozilla.gecko.background.healthreport; michael@0: michael@0: import java.io.File; michael@0: import java.util.ArrayList; michael@0: import java.util.List; michael@0: import java.util.Map.Entry; michael@0: michael@0: import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage.DatabaseEnvironment; michael@0: import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field; michael@0: import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields; michael@0: import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields.FieldSpec; michael@0: michael@0: import android.content.ContentProvider; michael@0: import android.content.ContentUris; michael@0: import android.content.ContentValues; michael@0: import android.content.UriMatcher; michael@0: import android.database.Cursor; michael@0: import android.net.Uri; michael@0: michael@0: /** michael@0: * This is a {@link ContentProvider} wrapper around a database-backed Health michael@0: * Report storage layer. michael@0: * michael@0: * It stores environments, fields, and measurements, and events which refer to michael@0: * each of these by integer ID. michael@0: * michael@0: * Insert = daily discrete. michael@0: * content://org.mozilla.gecko.health/events/env/measurement/v/field michael@0: * michael@0: * Update = daily last or daily counter michael@0: * content://org.mozilla.gecko.health/events/env/measurement/v/field/counter michael@0: * content://org.mozilla.gecko.health/events/env/measurement/v/field/last michael@0: * michael@0: * Delete = drop today's row michael@0: * content://org.mozilla.gecko.health/events/env/measurement/v/field/ michael@0: * michael@0: * Query, of course: content://org.mozilla.gecko.health/events/?since michael@0: * michael@0: * Each operation accepts an optional `time` query parameter, formatted as michael@0: * milliseconds since epoch. If omitted, it defaults to the current time. michael@0: * michael@0: * Each operation also accepts mandatory `profilePath` and `env` arguments. michael@0: * michael@0: * TODO: document measurements. michael@0: */ michael@0: public class HealthReportProvider extends ContentProvider { michael@0: private HealthReportDatabases databases; michael@0: private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); michael@0: michael@0: public static final String HEALTH_AUTHORITY = HealthReportConstants.HEALTH_AUTHORITY; michael@0: michael@0: // URI matches. michael@0: private static final int ENVIRONMENTS_ROOT = 10; michael@0: private static final int EVENTS_ROOT = 11; michael@0: private static final int EVENTS_RAW_ROOT = 12; michael@0: private static final int FIELDS_ROOT = 13; michael@0: private static final int MEASUREMENTS_ROOT = 14; michael@0: michael@0: private static final int EVENTS_FIELD_GENERIC = 20; michael@0: private static final int EVENTS_FIELD_COUNTER = 21; michael@0: private static final int EVENTS_FIELD_LAST = 22; michael@0: michael@0: private static final int ENVIRONMENT_DETAILS = 30; michael@0: private static final int FIELDS_MEASUREMENT = 31; michael@0: michael@0: static { michael@0: uriMatcher.addURI(HEALTH_AUTHORITY, "environments/", ENVIRONMENTS_ROOT); michael@0: uriMatcher.addURI(HEALTH_AUTHORITY, "events/", EVENTS_ROOT); michael@0: uriMatcher.addURI(HEALTH_AUTHORITY, "rawevents/", EVENTS_RAW_ROOT); michael@0: uriMatcher.addURI(HEALTH_AUTHORITY, "fields/", FIELDS_ROOT); michael@0: uriMatcher.addURI(HEALTH_AUTHORITY, "measurements/", MEASUREMENTS_ROOT); michael@0: michael@0: uriMatcher.addURI(HEALTH_AUTHORITY, "events/#/*/#/*", EVENTS_FIELD_GENERIC); michael@0: uriMatcher.addURI(HEALTH_AUTHORITY, "events/#/*/#/*/counter", EVENTS_FIELD_COUNTER); michael@0: uriMatcher.addURI(HEALTH_AUTHORITY, "events/#/*/#/*/last", EVENTS_FIELD_LAST); michael@0: michael@0: uriMatcher.addURI(HEALTH_AUTHORITY, "environments/#", ENVIRONMENT_DETAILS); michael@0: uriMatcher.addURI(HEALTH_AUTHORITY, "fields/*/#", FIELDS_MEASUREMENT); michael@0: } michael@0: michael@0: /** michael@0: * So we can bypass the ContentProvider layer. michael@0: */ michael@0: public HealthReportDatabaseStorage getProfileStorage(final String profilePath) { michael@0: if (profilePath == null) { michael@0: throw new IllegalArgumentException("profilePath must be provided."); michael@0: } michael@0: return databases.getDatabaseHelperForProfile(new File(profilePath)); michael@0: } michael@0: michael@0: private HealthReportDatabaseStorage getProfileStorageForUri(Uri uri) { michael@0: final String profilePath = uri.getQueryParameter("profilePath"); michael@0: return getProfileStorage(profilePath); michael@0: } michael@0: michael@0: @Override michael@0: public void onLowMemory() { michael@0: // While we could prune the database here, it wouldn't help - it would restore disk space michael@0: // rather then lower our RAM usage. Additionally, pruning the database may use even more michael@0: // memory and take too long to run in this method. michael@0: super.onLowMemory(); michael@0: databases.closeDatabaseHelpers(); michael@0: } michael@0: michael@0: @Override michael@0: public String getType(Uri uri) { michael@0: return null; michael@0: } michael@0: michael@0: @Override michael@0: public boolean onCreate() { michael@0: databases = new HealthReportDatabases(getContext()); michael@0: return true; michael@0: } michael@0: michael@0: @Override michael@0: public Uri insert(Uri uri, ContentValues values) { michael@0: int match = uriMatcher.match(uri); michael@0: HealthReportDatabaseStorage storage = getProfileStorageForUri(uri); michael@0: switch (match) { michael@0: case FIELDS_MEASUREMENT: michael@0: // The keys of this ContentValues are field names. michael@0: List pathSegments = uri.getPathSegments(); michael@0: String measurement = pathSegments.get(1); michael@0: int v = Integer.parseInt(pathSegments.get(2)); michael@0: storage.ensureMeasurementInitialized(measurement, v, getFieldSpecs(values)); michael@0: return uri; michael@0: michael@0: case ENVIRONMENTS_ROOT: michael@0: DatabaseEnvironment environment = storage.getEnvironment(); michael@0: environment.init(values); michael@0: return ContentUris.withAppendedId(uri, environment.register()); michael@0: michael@0: case EVENTS_FIELD_GENERIC: michael@0: long time = getTimeFromUri(uri); michael@0: int day = storage.getDay(time); michael@0: int env = getEnvironmentFromUri(uri); michael@0: Field field = getFieldFromUri(storage, uri); michael@0: michael@0: if (!values.containsKey("value")) { michael@0: throw new IllegalArgumentException("Must provide ContentValues including 'value' key."); michael@0: } michael@0: michael@0: Object object = values.get("value"); michael@0: if (object instanceof Integer || michael@0: object instanceof Long) { michael@0: storage.recordDailyDiscrete(env, day, field.getID(), ((Integer) object).intValue()); michael@0: } else if (object instanceof String) { michael@0: storage.recordDailyDiscrete(env, day, field.getID(), (String) object); michael@0: } else { michael@0: storage.recordDailyDiscrete(env, day, field.getID(), object.toString()); michael@0: } michael@0: michael@0: // TODO: eventually we might want to return something more useful than michael@0: // the input URI. michael@0: return uri; michael@0: default: michael@0: throw new IllegalArgumentException("Unknown insert URI"); michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public int update(Uri uri, ContentValues values, String selection, michael@0: String[] selectionArgs) { michael@0: michael@0: int match = uriMatcher.match(uri); michael@0: if (match != EVENTS_FIELD_COUNTER && michael@0: match != EVENTS_FIELD_LAST) { michael@0: throw new IllegalArgumentException("Must provide operation for update."); michael@0: } michael@0: michael@0: HealthReportStorage storage = getProfileStorageForUri(uri); michael@0: long time = getTimeFromUri(uri); michael@0: int day = storage.getDay(time); michael@0: int env = getEnvironmentFromUri(uri); michael@0: Field field = getFieldFromUri(storage, uri); michael@0: michael@0: switch (match) { michael@0: case EVENTS_FIELD_COUNTER: michael@0: int by = values.containsKey("value") ? values.getAsInteger("value") : 1; michael@0: storage.incrementDailyCount(env, day, field.getID(), by); michael@0: return 1; michael@0: michael@0: case EVENTS_FIELD_LAST: michael@0: Object object = values.get("value"); michael@0: if (object instanceof Integer || michael@0: object instanceof Long) { michael@0: storage.recordDailyLast(env, day, field.getID(), ((Integer) object).intValue()); michael@0: } else if (object instanceof String) { michael@0: storage.recordDailyLast(env, day, field.getID(), (String) object); michael@0: } else { michael@0: storage.recordDailyLast(env, day, field.getID(), object.toString()); michael@0: } michael@0: return 1; michael@0: michael@0: default: michael@0: // javac's flow control analysis sucks. michael@0: return 0; michael@0: } michael@0: } michael@0: michael@0: @Override michael@0: public int delete(Uri uri, String selection, String[] selectionArgs) { michael@0: int match = uriMatcher.match(uri); michael@0: HealthReportStorage storage = getProfileStorageForUri(uri); michael@0: switch (match) { michael@0: case MEASUREMENTS_ROOT: michael@0: storage.deleteMeasurements(); michael@0: return 1; michael@0: case ENVIRONMENTS_ROOT: michael@0: storage.deleteEnvironments(); michael@0: return 1; michael@0: default: michael@0: throw new IllegalArgumentException(); michael@0: } michael@0: michael@0: // TODO: more michael@0: } michael@0: michael@0: @Override michael@0: public Cursor query(Uri uri, String[] projection, String selection, michael@0: String[] selectionArgs, String sortOrder) { michael@0: int match = uriMatcher.match(uri); michael@0: michael@0: HealthReportStorage storage = getProfileStorageForUri(uri); michael@0: switch (match) { michael@0: case EVENTS_ROOT: michael@0: return storage.getEventsSince(getTimeFromUri(uri)); michael@0: case EVENTS_RAW_ROOT: michael@0: return storage.getRawEventsSince(getTimeFromUri(uri)); michael@0: case MEASUREMENTS_ROOT: michael@0: return storage.getMeasurementVersions(); michael@0: case FIELDS_ROOT: michael@0: return storage.getFieldVersions(); michael@0: } michael@0: List pathSegments = uri.getPathSegments(); michael@0: switch (match) { michael@0: case ENVIRONMENT_DETAILS: michael@0: return storage.getEnvironmentRecordForID(Integer.parseInt(pathSegments.get(1), 10)); michael@0: case FIELDS_MEASUREMENT: michael@0: String measurement = pathSegments.get(1); michael@0: int v = Integer.parseInt(pathSegments.get(2)); michael@0: return storage.getFieldVersions(measurement, v); michael@0: default: michael@0: return null; michael@0: } michael@0: } michael@0: michael@0: private static long getTimeFromUri(final Uri uri) { michael@0: String t = uri.getQueryParameter("time"); michael@0: if (t == null) { michael@0: return System.currentTimeMillis(); michael@0: } else { michael@0: return Long.parseLong(t, 10); michael@0: } michael@0: } michael@0: michael@0: private static int getEnvironmentFromUri(final Uri uri) { michael@0: return Integer.parseInt(uri.getPathSegments().get(1), 10); michael@0: } michael@0: michael@0: /** michael@0: * Assumes a URI structured like: michael@0: * michael@0: * content://org.mozilla.gecko.health/events/env/measurement/v/field michael@0: * michael@0: * @param uri a URI formatted as expected. michael@0: * @return a {@link Field} instance. michael@0: */ michael@0: private static Field getFieldFromUri(HealthReportStorage storage, final Uri uri) { michael@0: String measurement; michael@0: String field; michael@0: int measurementVersion; michael@0: michael@0: List pathSegments = uri.getPathSegments(); michael@0: measurement = pathSegments.get(2); michael@0: measurementVersion = Integer.parseInt(pathSegments.get(3), 10); michael@0: field = pathSegments.get(4); michael@0: michael@0: return storage.getField(measurement, measurementVersion, field); michael@0: } michael@0: michael@0: private MeasurementFields getFieldSpecs(ContentValues values) { michael@0: final ArrayList specs = new ArrayList(values.size()); michael@0: for (Entry entry : values.valueSet()) { michael@0: specs.add(new FieldSpec(entry.getKey(), ((Integer) entry.getValue()).intValue())); michael@0: } michael@0: michael@0: return new MeasurementFields() { michael@0: @Override michael@0: public Iterable getFields() { michael@0: return specs; michael@0: } michael@0: }; michael@0: } michael@0: michael@0: }