|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 package org.mozilla.gecko.background.healthreport; |
|
6 |
|
7 import java.io.File; |
|
8 import java.util.ArrayList; |
|
9 import java.util.List; |
|
10 import java.util.Map.Entry; |
|
11 |
|
12 import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage.DatabaseEnvironment; |
|
13 import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field; |
|
14 import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields; |
|
15 import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields.FieldSpec; |
|
16 |
|
17 import android.content.ContentProvider; |
|
18 import android.content.ContentUris; |
|
19 import android.content.ContentValues; |
|
20 import android.content.UriMatcher; |
|
21 import android.database.Cursor; |
|
22 import android.net.Uri; |
|
23 |
|
24 /** |
|
25 * This is a {@link ContentProvider} wrapper around a database-backed Health |
|
26 * Report storage layer. |
|
27 * |
|
28 * It stores environments, fields, and measurements, and events which refer to |
|
29 * each of these by integer ID. |
|
30 * |
|
31 * Insert = daily discrete. |
|
32 * content://org.mozilla.gecko.health/events/env/measurement/v/field |
|
33 * |
|
34 * Update = daily last or daily counter |
|
35 * content://org.mozilla.gecko.health/events/env/measurement/v/field/counter |
|
36 * content://org.mozilla.gecko.health/events/env/measurement/v/field/last |
|
37 * |
|
38 * Delete = drop today's row |
|
39 * content://org.mozilla.gecko.health/events/env/measurement/v/field/ |
|
40 * |
|
41 * Query, of course: content://org.mozilla.gecko.health/events/?since |
|
42 * |
|
43 * Each operation accepts an optional `time` query parameter, formatted as |
|
44 * milliseconds since epoch. If omitted, it defaults to the current time. |
|
45 * |
|
46 * Each operation also accepts mandatory `profilePath` and `env` arguments. |
|
47 * |
|
48 * TODO: document measurements. |
|
49 */ |
|
50 public class HealthReportProvider extends ContentProvider { |
|
51 private HealthReportDatabases databases; |
|
52 private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); |
|
53 |
|
54 public static final String HEALTH_AUTHORITY = HealthReportConstants.HEALTH_AUTHORITY; |
|
55 |
|
56 // URI matches. |
|
57 private static final int ENVIRONMENTS_ROOT = 10; |
|
58 private static final int EVENTS_ROOT = 11; |
|
59 private static final int EVENTS_RAW_ROOT = 12; |
|
60 private static final int FIELDS_ROOT = 13; |
|
61 private static final int MEASUREMENTS_ROOT = 14; |
|
62 |
|
63 private static final int EVENTS_FIELD_GENERIC = 20; |
|
64 private static final int EVENTS_FIELD_COUNTER = 21; |
|
65 private static final int EVENTS_FIELD_LAST = 22; |
|
66 |
|
67 private static final int ENVIRONMENT_DETAILS = 30; |
|
68 private static final int FIELDS_MEASUREMENT = 31; |
|
69 |
|
70 static { |
|
71 uriMatcher.addURI(HEALTH_AUTHORITY, "environments/", ENVIRONMENTS_ROOT); |
|
72 uriMatcher.addURI(HEALTH_AUTHORITY, "events/", EVENTS_ROOT); |
|
73 uriMatcher.addURI(HEALTH_AUTHORITY, "rawevents/", EVENTS_RAW_ROOT); |
|
74 uriMatcher.addURI(HEALTH_AUTHORITY, "fields/", FIELDS_ROOT); |
|
75 uriMatcher.addURI(HEALTH_AUTHORITY, "measurements/", MEASUREMENTS_ROOT); |
|
76 |
|
77 uriMatcher.addURI(HEALTH_AUTHORITY, "events/#/*/#/*", EVENTS_FIELD_GENERIC); |
|
78 uriMatcher.addURI(HEALTH_AUTHORITY, "events/#/*/#/*/counter", EVENTS_FIELD_COUNTER); |
|
79 uriMatcher.addURI(HEALTH_AUTHORITY, "events/#/*/#/*/last", EVENTS_FIELD_LAST); |
|
80 |
|
81 uriMatcher.addURI(HEALTH_AUTHORITY, "environments/#", ENVIRONMENT_DETAILS); |
|
82 uriMatcher.addURI(HEALTH_AUTHORITY, "fields/*/#", FIELDS_MEASUREMENT); |
|
83 } |
|
84 |
|
85 /** |
|
86 * So we can bypass the ContentProvider layer. |
|
87 */ |
|
88 public HealthReportDatabaseStorage getProfileStorage(final String profilePath) { |
|
89 if (profilePath == null) { |
|
90 throw new IllegalArgumentException("profilePath must be provided."); |
|
91 } |
|
92 return databases.getDatabaseHelperForProfile(new File(profilePath)); |
|
93 } |
|
94 |
|
95 private HealthReportDatabaseStorage getProfileStorageForUri(Uri uri) { |
|
96 final String profilePath = uri.getQueryParameter("profilePath"); |
|
97 return getProfileStorage(profilePath); |
|
98 } |
|
99 |
|
100 @Override |
|
101 public void onLowMemory() { |
|
102 // While we could prune the database here, it wouldn't help - it would restore disk space |
|
103 // rather then lower our RAM usage. Additionally, pruning the database may use even more |
|
104 // memory and take too long to run in this method. |
|
105 super.onLowMemory(); |
|
106 databases.closeDatabaseHelpers(); |
|
107 } |
|
108 |
|
109 @Override |
|
110 public String getType(Uri uri) { |
|
111 return null; |
|
112 } |
|
113 |
|
114 @Override |
|
115 public boolean onCreate() { |
|
116 databases = new HealthReportDatabases(getContext()); |
|
117 return true; |
|
118 } |
|
119 |
|
120 @Override |
|
121 public Uri insert(Uri uri, ContentValues values) { |
|
122 int match = uriMatcher.match(uri); |
|
123 HealthReportDatabaseStorage storage = getProfileStorageForUri(uri); |
|
124 switch (match) { |
|
125 case FIELDS_MEASUREMENT: |
|
126 // The keys of this ContentValues are field names. |
|
127 List<String> pathSegments = uri.getPathSegments(); |
|
128 String measurement = pathSegments.get(1); |
|
129 int v = Integer.parseInt(pathSegments.get(2)); |
|
130 storage.ensureMeasurementInitialized(measurement, v, getFieldSpecs(values)); |
|
131 return uri; |
|
132 |
|
133 case ENVIRONMENTS_ROOT: |
|
134 DatabaseEnvironment environment = storage.getEnvironment(); |
|
135 environment.init(values); |
|
136 return ContentUris.withAppendedId(uri, environment.register()); |
|
137 |
|
138 case EVENTS_FIELD_GENERIC: |
|
139 long time = getTimeFromUri(uri); |
|
140 int day = storage.getDay(time); |
|
141 int env = getEnvironmentFromUri(uri); |
|
142 Field field = getFieldFromUri(storage, uri); |
|
143 |
|
144 if (!values.containsKey("value")) { |
|
145 throw new IllegalArgumentException("Must provide ContentValues including 'value' key."); |
|
146 } |
|
147 |
|
148 Object object = values.get("value"); |
|
149 if (object instanceof Integer || |
|
150 object instanceof Long) { |
|
151 storage.recordDailyDiscrete(env, day, field.getID(), ((Integer) object).intValue()); |
|
152 } else if (object instanceof String) { |
|
153 storage.recordDailyDiscrete(env, day, field.getID(), (String) object); |
|
154 } else { |
|
155 storage.recordDailyDiscrete(env, day, field.getID(), object.toString()); |
|
156 } |
|
157 |
|
158 // TODO: eventually we might want to return something more useful than |
|
159 // the input URI. |
|
160 return uri; |
|
161 default: |
|
162 throw new IllegalArgumentException("Unknown insert URI"); |
|
163 } |
|
164 } |
|
165 |
|
166 @Override |
|
167 public int update(Uri uri, ContentValues values, String selection, |
|
168 String[] selectionArgs) { |
|
169 |
|
170 int match = uriMatcher.match(uri); |
|
171 if (match != EVENTS_FIELD_COUNTER && |
|
172 match != EVENTS_FIELD_LAST) { |
|
173 throw new IllegalArgumentException("Must provide operation for update."); |
|
174 } |
|
175 |
|
176 HealthReportStorage storage = getProfileStorageForUri(uri); |
|
177 long time = getTimeFromUri(uri); |
|
178 int day = storage.getDay(time); |
|
179 int env = getEnvironmentFromUri(uri); |
|
180 Field field = getFieldFromUri(storage, uri); |
|
181 |
|
182 switch (match) { |
|
183 case EVENTS_FIELD_COUNTER: |
|
184 int by = values.containsKey("value") ? values.getAsInteger("value") : 1; |
|
185 storage.incrementDailyCount(env, day, field.getID(), by); |
|
186 return 1; |
|
187 |
|
188 case EVENTS_FIELD_LAST: |
|
189 Object object = values.get("value"); |
|
190 if (object instanceof Integer || |
|
191 object instanceof Long) { |
|
192 storage.recordDailyLast(env, day, field.getID(), ((Integer) object).intValue()); |
|
193 } else if (object instanceof String) { |
|
194 storage.recordDailyLast(env, day, field.getID(), (String) object); |
|
195 } else { |
|
196 storage.recordDailyLast(env, day, field.getID(), object.toString()); |
|
197 } |
|
198 return 1; |
|
199 |
|
200 default: |
|
201 // javac's flow control analysis sucks. |
|
202 return 0; |
|
203 } |
|
204 } |
|
205 |
|
206 @Override |
|
207 public int delete(Uri uri, String selection, String[] selectionArgs) { |
|
208 int match = uriMatcher.match(uri); |
|
209 HealthReportStorage storage = getProfileStorageForUri(uri); |
|
210 switch (match) { |
|
211 case MEASUREMENTS_ROOT: |
|
212 storage.deleteMeasurements(); |
|
213 return 1; |
|
214 case ENVIRONMENTS_ROOT: |
|
215 storage.deleteEnvironments(); |
|
216 return 1; |
|
217 default: |
|
218 throw new IllegalArgumentException(); |
|
219 } |
|
220 |
|
221 // TODO: more |
|
222 } |
|
223 |
|
224 @Override |
|
225 public Cursor query(Uri uri, String[] projection, String selection, |
|
226 String[] selectionArgs, String sortOrder) { |
|
227 int match = uriMatcher.match(uri); |
|
228 |
|
229 HealthReportStorage storage = getProfileStorageForUri(uri); |
|
230 switch (match) { |
|
231 case EVENTS_ROOT: |
|
232 return storage.getEventsSince(getTimeFromUri(uri)); |
|
233 case EVENTS_RAW_ROOT: |
|
234 return storage.getRawEventsSince(getTimeFromUri(uri)); |
|
235 case MEASUREMENTS_ROOT: |
|
236 return storage.getMeasurementVersions(); |
|
237 case FIELDS_ROOT: |
|
238 return storage.getFieldVersions(); |
|
239 } |
|
240 List<String> pathSegments = uri.getPathSegments(); |
|
241 switch (match) { |
|
242 case ENVIRONMENT_DETAILS: |
|
243 return storage.getEnvironmentRecordForID(Integer.parseInt(pathSegments.get(1), 10)); |
|
244 case FIELDS_MEASUREMENT: |
|
245 String measurement = pathSegments.get(1); |
|
246 int v = Integer.parseInt(pathSegments.get(2)); |
|
247 return storage.getFieldVersions(measurement, v); |
|
248 default: |
|
249 return null; |
|
250 } |
|
251 } |
|
252 |
|
253 private static long getTimeFromUri(final Uri uri) { |
|
254 String t = uri.getQueryParameter("time"); |
|
255 if (t == null) { |
|
256 return System.currentTimeMillis(); |
|
257 } else { |
|
258 return Long.parseLong(t, 10); |
|
259 } |
|
260 } |
|
261 |
|
262 private static int getEnvironmentFromUri(final Uri uri) { |
|
263 return Integer.parseInt(uri.getPathSegments().get(1), 10); |
|
264 } |
|
265 |
|
266 /** |
|
267 * Assumes a URI structured like: |
|
268 * |
|
269 * <code>content://org.mozilla.gecko.health/events/env/measurement/v/field</code> |
|
270 * |
|
271 * @param uri a URI formatted as expected. |
|
272 * @return a {@link Field} instance. |
|
273 */ |
|
274 private static Field getFieldFromUri(HealthReportStorage storage, final Uri uri) { |
|
275 String measurement; |
|
276 String field; |
|
277 int measurementVersion; |
|
278 |
|
279 List<String> pathSegments = uri.getPathSegments(); |
|
280 measurement = pathSegments.get(2); |
|
281 measurementVersion = Integer.parseInt(pathSegments.get(3), 10); |
|
282 field = pathSegments.get(4); |
|
283 |
|
284 return storage.getField(measurement, measurementVersion, field); |
|
285 } |
|
286 |
|
287 private MeasurementFields getFieldSpecs(ContentValues values) { |
|
288 final ArrayList<FieldSpec> specs = new ArrayList<FieldSpec>(values.size()); |
|
289 for (Entry<String, Object> entry : values.valueSet()) { |
|
290 specs.add(new FieldSpec(entry.getKey(), ((Integer) entry.getValue()).intValue())); |
|
291 } |
|
292 |
|
293 return new MeasurementFields() { |
|
294 @Override |
|
295 public Iterable<FieldSpec> getFields() { |
|
296 return specs; |
|
297 } |
|
298 }; |
|
299 } |
|
300 |
|
301 } |