зеркало из https://github.com/mozilla/gecko-dev.git
Bug 858742 - Part 1: Firefox Health Report storage for Android. r=nalexander
This commit is contained in:
Родитель
d2c2c223e8
Коммит
8c7a39a683
|
@ -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
|
||||
|
|
Загрузка…
Ссылка в новой задаче