1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/mobile/android/base/background/healthreport/HealthReportProvider.java Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,301 @@ 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.ArrayList; 1.12 +import java.util.List; 1.13 +import java.util.Map.Entry; 1.14 + 1.15 +import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage.DatabaseEnvironment; 1.16 +import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field; 1.17 +import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields; 1.18 +import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields.FieldSpec; 1.19 + 1.20 +import android.content.ContentProvider; 1.21 +import android.content.ContentUris; 1.22 +import android.content.ContentValues; 1.23 +import android.content.UriMatcher; 1.24 +import android.database.Cursor; 1.25 +import android.net.Uri; 1.26 + 1.27 +/** 1.28 + * This is a {@link ContentProvider} wrapper around a database-backed Health 1.29 + * Report storage layer. 1.30 + * 1.31 + * It stores environments, fields, and measurements, and events which refer to 1.32 + * each of these by integer ID. 1.33 + * 1.34 + * Insert = daily discrete. 1.35 + * content://org.mozilla.gecko.health/events/env/measurement/v/field 1.36 + * 1.37 + * Update = daily last or daily counter 1.38 + * content://org.mozilla.gecko.health/events/env/measurement/v/field/counter 1.39 + * content://org.mozilla.gecko.health/events/env/measurement/v/field/last 1.40 + * 1.41 + * Delete = drop today's row 1.42 + * content://org.mozilla.gecko.health/events/env/measurement/v/field/ 1.43 + * 1.44 + * Query, of course: content://org.mozilla.gecko.health/events/?since 1.45 + * 1.46 + * Each operation accepts an optional `time` query parameter, formatted as 1.47 + * milliseconds since epoch. If omitted, it defaults to the current time. 1.48 + * 1.49 + * Each operation also accepts mandatory `profilePath` and `env` arguments. 1.50 + * 1.51 + * TODO: document measurements. 1.52 + */ 1.53 +public class HealthReportProvider extends ContentProvider { 1.54 + private HealthReportDatabases databases; 1.55 + private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); 1.56 + 1.57 + public static final String HEALTH_AUTHORITY = HealthReportConstants.HEALTH_AUTHORITY; 1.58 + 1.59 + // URI matches. 1.60 + private static final int ENVIRONMENTS_ROOT = 10; 1.61 + private static final int EVENTS_ROOT = 11; 1.62 + private static final int EVENTS_RAW_ROOT = 12; 1.63 + private static final int FIELDS_ROOT = 13; 1.64 + private static final int MEASUREMENTS_ROOT = 14; 1.65 + 1.66 + private static final int EVENTS_FIELD_GENERIC = 20; 1.67 + private static final int EVENTS_FIELD_COUNTER = 21; 1.68 + private static final int EVENTS_FIELD_LAST = 22; 1.69 + 1.70 + private static final int ENVIRONMENT_DETAILS = 30; 1.71 + private static final int FIELDS_MEASUREMENT = 31; 1.72 + 1.73 + static { 1.74 + uriMatcher.addURI(HEALTH_AUTHORITY, "environments/", ENVIRONMENTS_ROOT); 1.75 + uriMatcher.addURI(HEALTH_AUTHORITY, "events/", EVENTS_ROOT); 1.76 + uriMatcher.addURI(HEALTH_AUTHORITY, "rawevents/", EVENTS_RAW_ROOT); 1.77 + uriMatcher.addURI(HEALTH_AUTHORITY, "fields/", FIELDS_ROOT); 1.78 + uriMatcher.addURI(HEALTH_AUTHORITY, "measurements/", MEASUREMENTS_ROOT); 1.79 + 1.80 + uriMatcher.addURI(HEALTH_AUTHORITY, "events/#/*/#/*", EVENTS_FIELD_GENERIC); 1.81 + uriMatcher.addURI(HEALTH_AUTHORITY, "events/#/*/#/*/counter", EVENTS_FIELD_COUNTER); 1.82 + uriMatcher.addURI(HEALTH_AUTHORITY, "events/#/*/#/*/last", EVENTS_FIELD_LAST); 1.83 + 1.84 + uriMatcher.addURI(HEALTH_AUTHORITY, "environments/#", ENVIRONMENT_DETAILS); 1.85 + uriMatcher.addURI(HEALTH_AUTHORITY, "fields/*/#", FIELDS_MEASUREMENT); 1.86 + } 1.87 + 1.88 + /** 1.89 + * So we can bypass the ContentProvider layer. 1.90 + */ 1.91 + public HealthReportDatabaseStorage getProfileStorage(final String profilePath) { 1.92 + if (profilePath == null) { 1.93 + throw new IllegalArgumentException("profilePath must be provided."); 1.94 + } 1.95 + return databases.getDatabaseHelperForProfile(new File(profilePath)); 1.96 + } 1.97 + 1.98 + private HealthReportDatabaseStorage getProfileStorageForUri(Uri uri) { 1.99 + final String profilePath = uri.getQueryParameter("profilePath"); 1.100 + return getProfileStorage(profilePath); 1.101 + } 1.102 + 1.103 + @Override 1.104 + public void onLowMemory() { 1.105 + // While we could prune the database here, it wouldn't help - it would restore disk space 1.106 + // rather then lower our RAM usage. Additionally, pruning the database may use even more 1.107 + // memory and take too long to run in this method. 1.108 + super.onLowMemory(); 1.109 + databases.closeDatabaseHelpers(); 1.110 + } 1.111 + 1.112 + @Override 1.113 + public String getType(Uri uri) { 1.114 + return null; 1.115 + } 1.116 + 1.117 + @Override 1.118 + public boolean onCreate() { 1.119 + databases = new HealthReportDatabases(getContext()); 1.120 + return true; 1.121 + } 1.122 + 1.123 + @Override 1.124 + public Uri insert(Uri uri, ContentValues values) { 1.125 + int match = uriMatcher.match(uri); 1.126 + HealthReportDatabaseStorage storage = getProfileStorageForUri(uri); 1.127 + switch (match) { 1.128 + case FIELDS_MEASUREMENT: 1.129 + // The keys of this ContentValues are field names. 1.130 + List<String> pathSegments = uri.getPathSegments(); 1.131 + String measurement = pathSegments.get(1); 1.132 + int v = Integer.parseInt(pathSegments.get(2)); 1.133 + storage.ensureMeasurementInitialized(measurement, v, getFieldSpecs(values)); 1.134 + return uri; 1.135 + 1.136 + case ENVIRONMENTS_ROOT: 1.137 + DatabaseEnvironment environment = storage.getEnvironment(); 1.138 + environment.init(values); 1.139 + return ContentUris.withAppendedId(uri, environment.register()); 1.140 + 1.141 + case EVENTS_FIELD_GENERIC: 1.142 + long time = getTimeFromUri(uri); 1.143 + int day = storage.getDay(time); 1.144 + int env = getEnvironmentFromUri(uri); 1.145 + Field field = getFieldFromUri(storage, uri); 1.146 + 1.147 + if (!values.containsKey("value")) { 1.148 + throw new IllegalArgumentException("Must provide ContentValues including 'value' key."); 1.149 + } 1.150 + 1.151 + Object object = values.get("value"); 1.152 + if (object instanceof Integer || 1.153 + object instanceof Long) { 1.154 + storage.recordDailyDiscrete(env, day, field.getID(), ((Integer) object).intValue()); 1.155 + } else if (object instanceof String) { 1.156 + storage.recordDailyDiscrete(env, day, field.getID(), (String) object); 1.157 + } else { 1.158 + storage.recordDailyDiscrete(env, day, field.getID(), object.toString()); 1.159 + } 1.160 + 1.161 + // TODO: eventually we might want to return something more useful than 1.162 + // the input URI. 1.163 + return uri; 1.164 + default: 1.165 + throw new IllegalArgumentException("Unknown insert URI"); 1.166 + } 1.167 + } 1.168 + 1.169 + @Override 1.170 + public int update(Uri uri, ContentValues values, String selection, 1.171 + String[] selectionArgs) { 1.172 + 1.173 + int match = uriMatcher.match(uri); 1.174 + if (match != EVENTS_FIELD_COUNTER && 1.175 + match != EVENTS_FIELD_LAST) { 1.176 + throw new IllegalArgumentException("Must provide operation for update."); 1.177 + } 1.178 + 1.179 + HealthReportStorage storage = getProfileStorageForUri(uri); 1.180 + long time = getTimeFromUri(uri); 1.181 + int day = storage.getDay(time); 1.182 + int env = getEnvironmentFromUri(uri); 1.183 + Field field = getFieldFromUri(storage, uri); 1.184 + 1.185 + switch (match) { 1.186 + case EVENTS_FIELD_COUNTER: 1.187 + int by = values.containsKey("value") ? values.getAsInteger("value") : 1; 1.188 + storage.incrementDailyCount(env, day, field.getID(), by); 1.189 + return 1; 1.190 + 1.191 + case EVENTS_FIELD_LAST: 1.192 + Object object = values.get("value"); 1.193 + if (object instanceof Integer || 1.194 + object instanceof Long) { 1.195 + storage.recordDailyLast(env, day, field.getID(), ((Integer) object).intValue()); 1.196 + } else if (object instanceof String) { 1.197 + storage.recordDailyLast(env, day, field.getID(), (String) object); 1.198 + } else { 1.199 + storage.recordDailyLast(env, day, field.getID(), object.toString()); 1.200 + } 1.201 + return 1; 1.202 + 1.203 + default: 1.204 + // javac's flow control analysis sucks. 1.205 + return 0; 1.206 + } 1.207 + } 1.208 + 1.209 + @Override 1.210 + public int delete(Uri uri, String selection, String[] selectionArgs) { 1.211 + int match = uriMatcher.match(uri); 1.212 + HealthReportStorage storage = getProfileStorageForUri(uri); 1.213 + switch (match) { 1.214 + case MEASUREMENTS_ROOT: 1.215 + storage.deleteMeasurements(); 1.216 + return 1; 1.217 + case ENVIRONMENTS_ROOT: 1.218 + storage.deleteEnvironments(); 1.219 + return 1; 1.220 + default: 1.221 + throw new IllegalArgumentException(); 1.222 + } 1.223 + 1.224 + // TODO: more 1.225 + } 1.226 + 1.227 + @Override 1.228 + public Cursor query(Uri uri, String[] projection, String selection, 1.229 + String[] selectionArgs, String sortOrder) { 1.230 + int match = uriMatcher.match(uri); 1.231 + 1.232 + HealthReportStorage storage = getProfileStorageForUri(uri); 1.233 + switch (match) { 1.234 + case EVENTS_ROOT: 1.235 + return storage.getEventsSince(getTimeFromUri(uri)); 1.236 + case EVENTS_RAW_ROOT: 1.237 + return storage.getRawEventsSince(getTimeFromUri(uri)); 1.238 + case MEASUREMENTS_ROOT: 1.239 + return storage.getMeasurementVersions(); 1.240 + case FIELDS_ROOT: 1.241 + return storage.getFieldVersions(); 1.242 + } 1.243 + List<String> pathSegments = uri.getPathSegments(); 1.244 + switch (match) { 1.245 + case ENVIRONMENT_DETAILS: 1.246 + return storage.getEnvironmentRecordForID(Integer.parseInt(pathSegments.get(1), 10)); 1.247 + case FIELDS_MEASUREMENT: 1.248 + String measurement = pathSegments.get(1); 1.249 + int v = Integer.parseInt(pathSegments.get(2)); 1.250 + return storage.getFieldVersions(measurement, v); 1.251 + default: 1.252 + return null; 1.253 + } 1.254 + } 1.255 + 1.256 + private static long getTimeFromUri(final Uri uri) { 1.257 + String t = uri.getQueryParameter("time"); 1.258 + if (t == null) { 1.259 + return System.currentTimeMillis(); 1.260 + } else { 1.261 + return Long.parseLong(t, 10); 1.262 + } 1.263 + } 1.264 + 1.265 + private static int getEnvironmentFromUri(final Uri uri) { 1.266 + return Integer.parseInt(uri.getPathSegments().get(1), 10); 1.267 + } 1.268 + 1.269 + /** 1.270 + * Assumes a URI structured like: 1.271 + * 1.272 + * <code>content://org.mozilla.gecko.health/events/env/measurement/v/field</code> 1.273 + * 1.274 + * @param uri a URI formatted as expected. 1.275 + * @return a {@link Field} instance. 1.276 + */ 1.277 + private static Field getFieldFromUri(HealthReportStorage storage, final Uri uri) { 1.278 + String measurement; 1.279 + String field; 1.280 + int measurementVersion; 1.281 + 1.282 + List<String> pathSegments = uri.getPathSegments(); 1.283 + measurement = pathSegments.get(2); 1.284 + measurementVersion = Integer.parseInt(pathSegments.get(3), 10); 1.285 + field = pathSegments.get(4); 1.286 + 1.287 + return storage.getField(measurement, measurementVersion, field); 1.288 + } 1.289 + 1.290 + private MeasurementFields getFieldSpecs(ContentValues values) { 1.291 + final ArrayList<FieldSpec> specs = new ArrayList<FieldSpec>(values.size()); 1.292 + for (Entry<String, Object> entry : values.valueSet()) { 1.293 + specs.add(new FieldSpec(entry.getKey(), ((Integer) entry.getValue()).intValue())); 1.294 + } 1.295 + 1.296 + return new MeasurementFields() { 1.297 + @Override 1.298 + public Iterable<FieldSpec> getFields() { 1.299 + return specs; 1.300 + } 1.301 + }; 1.302 + } 1.303 + 1.304 +}