Bug 858742 - Part 1: Firefox Health Report storage for Android. r=nalexander

This commit is contained in:
Richard Newman 2013-05-22 10:23:29 -07:00
Родитель d2c2c223e8
Коммит 8c7a39a683
12 изменённых файлов: 2185 добавлений и 0 удалений

Просмотреть файл

@ -7,6 +7,7 @@ SYNC_PP_JAVA_FILES := \
background/common/GlobalConstants.java \
sync/SyncConstants.java \
background/announcements/AnnouncementsConstants.java \
background/healthreport/HealthReportConstants.java \
$(NULL)
SYNC_JAVA_FILES := \
@ -36,6 +37,13 @@ SYNC_JAVA_FILES := \
background/common/log/writers/ThreadLocalTagLogWriter.java \
background/db/CursorDumper.java \
background/db/Tab.java \
background/healthreport/Environment.java \
background/healthreport/HealthReportDatabases.java \
background/healthreport/HealthReportDatabaseStorage.java \
background/healthreport/HealthReportGenerator.java \
background/healthreport/HealthReportProvider.java \
background/healthreport/HealthReportStorage.java \
background/healthreport/HealthReportUtils.java \
sync/AlreadySyncingException.java \
sync/CollectionKeys.java \
sync/CommandProcessor.java \

Просмотреть файл

@ -0,0 +1,110 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.background.healthreport;
import java.util.ArrayList;
/**
* This captures all of the details that define an 'environment' for FHR's purposes.
* Whenever this format changes, it'll be changing with a build ID, so no migration
* of values is needed.
*
* Unless you remove the build descriptors from the set, of course.
*
* Or store these in a database.
*
* Instances of this class should be considered "effectively immutable": control their
* scope such that clear creation/sharing boundaries exist. Once you've populated and
* registered an <code>Environment</code>, don't do so again; start from scratch.
*
*/
public abstract class Environment {
public static int VERSION = 1;
protected volatile String hash = null;
protected volatile int id = -1;
// org.mozilla.profile.age.
public int profileCreation;
// org.mozilla.sysinfo.sysinfo.
public int cpuCount;
public int memoryMB;
public String architecture;
public String sysName;
public String sysVersion; // Kernel.
// geckoAppInfo. Not sure if we can/should provide this on Android.
public String vendor;
public String appName;
public String appID;
public String appVersion;
public String appBuildID;
public String platformVersion;
public String platformBuildID;
public String os;
public String xpcomabi;
public String updateChannel;
// appInfo.
public int isBlocklistEnabled;
public int isTelemetryEnabled;
// public int isDefaultBrowser; // This is meaningless on Android.
// org.mozilla.addons.active.
public final ArrayList<String> addons = new ArrayList<String>();
// org.mozilla.addons.counts.
public int extensionCount;
public int pluginCount;
public int themeCount;
public String getHash() {
// It's never unset, so we only care about partial reads. volatile is enough.
if (hash != null) {
return hash;
}
StringBuilder b = new StringBuilder();
b.append(profileCreation);
b.append(cpuCount);
b.append(memoryMB);
b.append(architecture);
b.append(sysName);
b.append(sysVersion);
b.append(vendor);
b.append(appName);
b.append(appID);
b.append(appVersion);
b.append(appBuildID);
b.append(platformVersion);
b.append(platformBuildID);
b.append(os);
b.append(xpcomabi);
b.append(updateChannel);
b.append(isBlocklistEnabled);
b.append(isTelemetryEnabled);
b.append(extensionCount);
b.append(pluginCount);
b.append(themeCount);
for (String addon : addons) {
b.append(addon);
}
return hash = HealthReportUtils.getEnvironmentHash(b.toString());
}
/**
* Ensure that the {@link Environment} has been registered with its
* storage layer, and can be used to annotate events.
*
* It's safe to call this method more than once, and each time you'll
* get the same ID.
*
* @return the integer ID to use in subsequent DB insertions.
*/
public abstract int register();
}

Просмотреть файл

@ -0,0 +1,18 @@
#filter substitution
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.background.healthreport;
public class HealthReportConstants {
public static final String HEALTH_AUTHORITY = "@ANDROID_PACKAGE_NAME@.health";
public static final String GLOBAL_LOG_TAG = "GeckoHealth";
public static final int MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
/**
* The earliest allowable value for the last ping time, corresponding to May 2nd 2013.
* Used for sanity checks.
*/
public static final long EARLIEST_LAST_PING = 1367500000000L;
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -0,0 +1,53 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.background.healthreport;
import java.io.File;
import java.util.HashMap;
import org.mozilla.gecko.background.common.log.Logger;
import android.content.Context;
/**
* Manages a set of per-profile Health Report storage helpers.
*/
public class HealthReportDatabases {
private static final String LOG_TAG = "HealthReportDatabases";
private Context context;
private final HashMap<File, HealthReportDatabaseStorage> storages = new HashMap<File, HealthReportDatabaseStorage>();
public HealthReportDatabases(final Context context) {
this.context = context;
}
public synchronized HealthReportDatabaseStorage getDatabaseHelperForProfile(final File profileDir) {
if (profileDir == null) {
throw new IllegalArgumentException("No profile provided.");
}
if (this.storages.containsKey(profileDir)) {
return this.storages.get(profileDir);
}
final HealthReportDatabaseStorage helper;
helper = new HealthReportDatabaseStorage(this.context, profileDir);
this.storages.put(profileDir, helper);
return helper;
}
public synchronized void closeDatabaseHelpers() {
for (HealthReportDatabaseStorage helper : storages.values()) {
try {
helper.close();
} catch (Exception e) {
Logger.warn(LOG_TAG, "Failed to close database helper.", e);
}
}
storages.clear();
}
}

Просмотреть файл

@ -0,0 +1,354 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.background.healthreport;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field;
import android.database.Cursor;
import android.util.SparseArray;
public class HealthReportGenerator {
private static final int PAYLOAD_VERSION = 3;
private final HealthReportStorage storage;
public HealthReportGenerator(HealthReportStorage storage) {
this.storage = storage;
}
@SuppressWarnings("static-method")
protected long now() {
return System.currentTimeMillis();
}
/**
* The document consists of:
*
* * Basic metadata: last ping time, current ping time, version.
* * A map of environments: 'current' and others named by hash. 'current' is fully specified,
* and others are deltas from current.
* * A 'data' object. This includes 'last' and 'days'.
* 'days' is a map from date strings to {hash: {measurement: {_v: version, fields...}}}.
*/
@SuppressWarnings("unchecked")
public JSONObject generateDocument(long since, long lastPingTime, Environment currentEnvironment) {
// We want to map field IDs to some strings as we go.
SparseArray<Environment> envs = storage.getEnvironmentRecordsByID();
JSONObject document = new JSONObject();
if (lastPingTime >= HealthReportConstants.EARLIEST_LAST_PING) {
document.put("lastPingDate", HealthReportUtils.getDateString(lastPingTime));
}
document.put("thisPingDate", HealthReportUtils.getDateString(now()));
document.put("version", PAYLOAD_VERSION);
document.put("environments", getEnvironmentsJSON(currentEnvironment, envs));
document.put("data", getDataJSON(currentEnvironment, envs, since));
return document;
}
@SuppressWarnings("unchecked")
protected JSONObject getDataJSON(Environment currentEnvironment,
SparseArray<Environment> envs, long since) {
SparseArray<Field> fields = storage.getFieldsByID();
JSONObject days = getDaysJSON(currentEnvironment, envs, fields, since);
JSONObject last = new JSONObject();
JSONObject data = new JSONObject();
data.put("days", days);
data.put("last", last);
return data;
}
@SuppressWarnings("unchecked")
protected JSONObject getDaysJSON(Environment currentEnvironment, SparseArray<Environment> envs, SparseArray<Field> fields, long since) {
JSONObject days = new JSONObject();
Cursor cursor = storage.getRawEventsSince(since);
try {
if (!cursor.moveToNext()) {
return days;
}
// A classic walking partition.
// Columns are "date", "env", "field", "value".
// Note that we care about the type (integer, string) and kind
// (last/counter, discrete) of each field.
// Each field will be accessed once for each date/env pair, so
// Field memoizes these facts.
// We also care about which measurement contains each field.
int lastDate = -1;
int lastEnv = -1;
JSONObject dateObject = null;
JSONObject envObject = null;
while (!cursor.isAfterLast()) {
int cDate = cursor.getInt(0);
int cEnv = cursor.getInt(1);
int cField = cursor.getInt(2);
boolean dateChanged = cDate != lastDate;
boolean envChanged = cEnv != lastEnv;
if (dateChanged) {
if (dateObject != null) {
days.put(HealthReportUtils.getDateStringForDay(lastDate), dateObject);
}
dateObject = new JSONObject();
lastDate = cDate;
}
if (dateChanged || envChanged) {
envObject = new JSONObject();
dateObject.put(envs.get(cEnv).hash, envObject);
lastEnv = cEnv;
}
final Field field = fields.get(cField);
JSONObject measurement = (JSONObject) envObject.get(field.measurementName);
if (measurement == null) {
// We will never have more than one measurement version within a
// single environment -- to do so involves changing the build ID. And
// even if we did, we have no way to represent it. So just build the
// output object once.
measurement = new JSONObject();
measurement.put("_v", field.measurementVersion);
envObject.put(field.measurementName, measurement);
}
if (field.isDiscreteField()) {
JSONArray discrete = (JSONArray) measurement.get(field.fieldName);
if (discrete == null) {
discrete = new JSONArray();
measurement.put(field.fieldName, discrete);
}
if (field.isStringField()) {
discrete.add(cursor.getString(3));
} else if (field.isIntegerField()) {
discrete.add(cursor.getLong(3));
} else {
// Uh oh!
}
} else {
if (field.isStringField()) {
measurement.put(field.fieldName, cursor.getString(3));
} else {
measurement.put(field.fieldName, cursor.getLong(3));
}
}
cursor.moveToNext();
continue;
}
days.put(HealthReportUtils.getDateStringForDay(lastDate), dateObject);
} finally {
cursor.close();
}
return days;
}
@SuppressWarnings("unchecked")
protected JSONObject getEnvironmentsJSON(Environment currentEnvironment,
SparseArray<Environment> envs) {
JSONObject environments = new JSONObject();
// Always do this, even if it hasn't recorded anything in the DB.
environments.put("current", jsonify(currentEnvironment, null));
String currentHash = currentEnvironment.getHash();
for (int i = 0; i < envs.size(); i++) {
Environment e = envs.valueAt(i);
if (currentHash.equals(e.getHash())) {
continue;
}
environments.put(e.getHash(), jsonify(e, currentEnvironment));
}
return environments;
}
@SuppressWarnings("unchecked")
private JSONObject jsonify(Environment e, Environment current) {
JSONObject age = getProfileAge(e, current);
JSONObject sysinfo = getSysInfo(e, current);
JSONObject gecko = getGeckoInfo(e, current);
JSONObject appinfo = getAppInfo(e, current);
JSONObject counts = getAddonCounts(e, current);
JSONObject out = new JSONObject();
if (age != null)
out.put("org.mozilla.profile.age", age);
if (sysinfo != null)
out.put("org.mozilla.sysinfo.sysinfo", sysinfo);
if (gecko != null)
out.put("geckoAppInfo", gecko);
if (appinfo != null)
out.put("org.mozilla.appInfo.appinfo", appinfo);
if (counts != null)
out.put("org.mozilla.addons.counts", counts);
JSONObject active = getActiveAddons(e, current);
if (active != null)
out.put("org.mozilla.addons.active", active);
if (current == null) {
out.put("hash", e.getHash());
}
return out;
}
@SuppressWarnings("unchecked")
private JSONObject getProfileAge(Environment e, Environment current) {
JSONObject age = new JSONObject();
int changes = 0;
if (current == null || current.profileCreation != e.profileCreation) {
age.put("profileCreation", e.profileCreation);
changes++;
}
if (current != null && changes == 0) {
return null;
}
age.put("_v", 1);
return age;
}
@SuppressWarnings("unchecked")
private JSONObject getSysInfo(Environment e, Environment current) {
JSONObject sysinfo = new JSONObject();
int changes = 0;
if (current == null || current.cpuCount != e.cpuCount) {
sysinfo.put("cpuCount", e.cpuCount);
changes++;
}
if (current == null || current.memoryMB != e.memoryMB) {
sysinfo.put("memoryMB", e.memoryMB);
changes++;
}
if (current == null || !current.architecture.equals(e.architecture)) {
sysinfo.put("architecture", e.architecture);
changes++;
}
if (current == null || !current.sysName.equals(e.sysName)) {
sysinfo.put("name", e.sysName);
changes++;
}
if (current == null || !current.sysVersion.equals(e.sysVersion)) {
sysinfo.put("version", e.sysVersion);
changes++;
}
if (current != null && changes == 0) {
return null;
}
sysinfo.put("_v", 1);
return sysinfo;
}
@SuppressWarnings("unchecked")
private JSONObject getGeckoInfo(Environment e, Environment current) {
JSONObject gecko = new JSONObject();
int changes = 0;
if (current == null || !current.vendor.equals(e.vendor)) {
gecko.put("vendor", e.vendor);
changes++;
}
if (current == null || !current.appName.equals(e.appName)) {
gecko.put("name", e.appName);
changes++;
}
if (current == null || !current.appID.equals(e.appID)) {
gecko.put("id", e.appID);
changes++;
}
if (current == null || !current.appVersion.equals(e.appVersion)) {
gecko.put("version", e.appVersion);
changes++;
}
if (current == null || !current.appBuildID.equals(e.appBuildID)) {
gecko.put("appBuildID", e.appBuildID);
changes++;
}
if (current == null || !current.platformVersion.equals(e.platformVersion)) {
gecko.put("platformVersion", e.platformVersion);
changes++;
}
if (current == null || !current.platformBuildID.equals(e.platformBuildID)) {
gecko.put("platformBuildID", e.platformBuildID);
changes++;
}
if (current == null || !current.os.equals(e.os)) {
gecko.put("os", e.os);
changes++;
}
if (current == null || !current.xpcomabi.equals(e.xpcomabi)) {
gecko.put("xpcomabi", e.xpcomabi);
changes++;
}
if (current == null || !current.updateChannel.equals(e.updateChannel)) {
gecko.put("updateChannel", e.updateChannel);
changes++;
}
if (current != null && changes == 0) {
return null;
}
gecko.put("_v", 1);
return gecko;
}
@SuppressWarnings("unchecked")
private JSONObject getAppInfo(Environment e, Environment current) {
JSONObject appinfo = new JSONObject();
int changes = 0;
if (current == null || current.isBlocklistEnabled != e.isBlocklistEnabled) {
appinfo.put("isBlocklistEnabled", e.isBlocklistEnabled);
changes++;
}
if (current == null || current.isTelemetryEnabled != e.isTelemetryEnabled) {
appinfo.put("isTelemetryEnabled", e.isTelemetryEnabled);
changes++;
}
if (current != null && changes == 0) {
return null;
}
appinfo.put("_v", 2);
return appinfo;
}
@SuppressWarnings("unchecked")
private JSONObject getAddonCounts(Environment e, Environment current) {
JSONObject counts = new JSONObject();
int changes = 0;
if (current == null || current.extensionCount != e.extensionCount) {
counts.put("extension", e.extensionCount);
changes++;
}
if (current == null || current.pluginCount != e.pluginCount) {
counts.put("plugin", e.pluginCount);
changes++;
}
if (current == null || current.themeCount != e.themeCount) {
counts.put("theme", e.themeCount);
changes++;
}
if (current != null && changes == 0) {
return null;
}
counts.put("_v", 1);
return counts;
}
@SuppressWarnings("unchecked")
private JSONObject getActiveAddons(Environment e, Environment current) {
JSONObject active = new JSONObject();
int changes = 0;
if (current != null && changes == 0) {
return null;
}
active.put("_v", 1);
return active;
}
}

Просмотреть файл

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

Просмотреть файл

@ -0,0 +1,190 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.background.healthreport;
import android.database.Cursor;
import android.util.SparseArray;
/**
* Abstraction over storage for Firefox Health Report on Android.
*/
public interface HealthReportStorage {
// Right now we only care about the name of the field.
public interface MeasurementFields {
public class FieldSpec {
public final String name;
public final int type;
public FieldSpec(String name, int type) {
this.name = name;
this.type = type;
}
}
Iterable<FieldSpec> getFields();
}
public abstract class Field {
protected static final int UNKNOWN_TYPE_OR_FIELD_ID = -1;
protected static final int FLAG_INTEGER = 1 << 0;
protected static final int FLAG_STRING = 1 << 1;
protected static final int FLAG_DISCRETE = 1 << 8;
protected static final int FLAG_LAST = 1 << 9;
protected static final int FLAG_COUNTER = 1 << 10;
public static final int TYPE_INTEGER_DISCRETE = FLAG_INTEGER | FLAG_DISCRETE;
public static final int TYPE_INTEGER_LAST = FLAG_INTEGER | FLAG_LAST;
public static final int TYPE_INTEGER_COUNTER = FLAG_INTEGER | FLAG_COUNTER;
public static final int TYPE_STRING_DISCRETE = FLAG_STRING | FLAG_DISCRETE;
public static final int TYPE_STRING_LAST = FLAG_STRING | FLAG_LAST;
protected int fieldID = UNKNOWN_TYPE_OR_FIELD_ID;
protected int flags;
protected final String measurementName;
protected final String measurementVersion;
protected final String fieldName;
public Field(String mName, int mVersion, String fieldName, int type) {
this.measurementName = mName;
this.measurementVersion = Integer.toString(mVersion, 10);
this.fieldName = fieldName;
this.flags = type;
}
/**
* @return the ID for this <code>Field</code>
* @throws IllegalStateException if this field is not found in storage
*/
public abstract int getID() throws IllegalStateException;
public boolean isIntegerField() {
return (this.flags & FLAG_INTEGER) > 0;
}
public boolean isStringField() {
return (this.flags & FLAG_STRING) > 0;
}
public boolean isDiscreteField() {
return (this.flags & FLAG_DISCRETE) > 0;
}
}
/**
* Close open storage handles and otherwise finish up.
*/
public void close();
/**
* Return the day integer corresponding to the provided time.
*
* @param time
* milliseconds since Unix epoch.
* @return an integer day.
*/
public int getDay(long time);
/**
* Return the day integer corresponding to the current time.
*
* @return an integer day.
*/
public int getDay();
/**
* Return a new {@link Environment}, suitable for being populated, hashed, and
* registered.
*
* @return a new {@link Environment} instance.
*/
public Environment getEnvironment();
/**
* @return a mapping from environment IDs to hashes, suitable for use in
* payload generation.
*/
public SparseArray<String> getEnvironmentHashesByID();
/**
* @return a mapping from environment IDs to registered {@link Environment}
* records, suitable for use in payload generation.
*/
public SparseArray<Environment> getEnvironmentRecordsByID();
/**
* @param id
* the environment ID, as returned by {@link Environment#register()}.
* @return a cursor for the record.
*/
public Cursor getEnvironmentRecordForID(int id);
/**
* @param measurement
* the name of a measurement, such as "org.mozilla.appInfo.appInfo".
* @param measurementVersion
* the version of a measurement, such as '3'.
* @param fieldName
* the name of a field, such as "platformVersion".
*
* @return a {@link Field} instance corresponding to the provided values.
*/
public Field getField(String measurement, int measurementVersion,
String fieldName);
/**
* @return a mapping from field IDs to {@link Field} instances, suitable for
* use in payload generation.
*/
public SparseArray<Field> getFieldsByID();
public void recordDailyLast(int env, int day, int field, String value);
public void recordDailyLast(int env, int day, int field, int value);
public void recordDailyDiscrete(int env, int day, int field, String value);
public void recordDailyDiscrete(int env, int day, int field, int value);
public void incrementDailyCount(int env, int day, int field, int by);
public void incrementDailyCount(int env, int day, int field);
/**
* Obtain a cursor over events that were recorded since <code>time</code>.
* This cursor exposes 'raw' events, with integer identifiers for values.
*/
public Cursor getRawEventsSince(long time);
/**
* Obtain a cursor over events that were recorded since <code>time</code>.
*
* This cursor exposes 'friendly' events, with string names and full
* measurement metadata.
*/
public Cursor getEventsSince(long time);
/**
* Ensure that a measurement and all of its fields are registered with the DB.
* No fields will be processed if the measurement exists with the specified
* version.
*
* @param measurement
* a measurement name, such as "org.mozila.appInfo.appInfo".
* @param version
* a version number, such as '3'.
* @param fields
* a {@link MeasurementFields} instance, consisting of a collection
* of field names.
*/
public void ensureMeasurementInitialized(String measurement,
int version,
MeasurementFields fields);
public Cursor getMeasurementVersions();
public Cursor getFieldVersions();
public Cursor getFieldVersions(String measurement, int measurementVersion);
public void deleteEverything();
public void deleteEnvironments();
public void deleteMeasurements();
public void enqueueOperation(Runnable runnable);
}

Просмотреть файл

@ -0,0 +1,48 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.background.healthreport;
import java.text.SimpleDateFormat;
import java.util.Locale;
import java.util.TimeZone;
import org.mozilla.apache.commons.codec.digest.DigestUtils;
import android.content.ContentUris;
import android.net.Uri;
public class HealthReportUtils {
public static int getDay(final long time) {
return (int) Math.floor(time / HealthReportConstants.MILLISECONDS_PER_DAY);
}
public static String getEnvironmentHash(final String input) {
return DigestUtils.shaHex(input);
}
public static String getDateStringForDay(long day) {
return getDateString(HealthReportConstants.MILLISECONDS_PER_DAY * day);
}
public static String getDateString(long time) {
final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
format.setTimeZone(TimeZone.getTimeZone("UTC"));
return format.format(time);
}
/**
* Take an environment URI (one that identifies an environment) and produce an
* event URI.
*
* That this is needed is tragic.
*
* @param environmentURI
* the {@link Uri} returned by an environment operation.
* @return a {@link Uri} to which insertions can be dispatched.
*/
public static Uri getEventURI(Uri environmentURI) {
return environmentURI.buildUpon().path("/events/" + ContentUris.parseId(environmentURI) + "/").build();
}
}

Просмотреть файл

@ -24,6 +24,13 @@ background/common/log/writers/TagLogWriter.java
background/common/log/writers/ThreadLocalTagLogWriter.java
background/db/CursorDumper.java
background/db/Tab.java
background/healthreport/Environment.java
background/healthreport/HealthReportDatabases.java
background/healthreport/HealthReportDatabaseStorage.java
background/healthreport/HealthReportGenerator.java
background/healthreport/HealthReportProvider.java
background/healthreport/HealthReportStorage.java
background/healthreport/HealthReportUtils.java
sync/AlreadySyncingException.java
sync/CollectionKeys.java
sync/CommandProcessor.java

Просмотреть файл

@ -0,0 +1,4 @@
<provider android:name="org.mozilla.gecko.background.healthreport.HealthReportProvider"
android:authorities="@ANDROID_PACKAGE_NAME@.health"
android:permission="@ANDROID_PACKAGE_NAME@.permissions.HEALTH_PROVIDER">
</provider>

Просмотреть файл

@ -1,3 +1,4 @@
background/common/GlobalConstants.java
sync/SyncConstants.java
background/announcements/AnnouncementsConstants.java
background/healthreport/HealthReportConstants.java