Bug 1247489 - Replace TelemetryPingGenerator with TelemetryPingBuilder & friends. r=grisha

The Builder pattern has the following benefits:
  * Encapsulate identifying optional arguments
  * Encapsulate parameter validation
  * More fluent parameter insertion (e.g. instead as unnamed arguments to a
function)
  * My implementation makes it fairly straight-forward to construct new
telemetry pings.

MozReview-Commit-ID: EpcW3N57HJj

--HG--
extra : rebase_source : a33ef584ed47b36910417854208fa02438556467
This commit is contained in:
Michael Comella 2016-04-11 17:45:29 -07:00
parent 8fd989536c
commit 4a899f9e1f
7 changed files with 245 additions and 132 deletions

View File

@ -24,25 +24,4 @@ public class TelemetryConstants {
public static final String PREF_SERVER_URL = "telemetry-serverUrl";
public static final String PREF_SEQ_COUNT = "telemetry-seqCount";
public static class CorePing {
private CorePing() { /* To prevent instantiation */ }
public static final String NAME = "core";
public static final int VERSION_VALUE = 4; // For version history, see toolkit/components/telemetry/docs/core-ping.rst
public static final String OS_VALUE = "Android";
public static final String ARCHITECTURE = "arch";
public static final String CLIENT_ID = "clientId";
public static final String DEFAULT_SEARCH_ENGINE = "defaultSearch";
public static final String DEVICE = "device";
public static final String DISTRIBUTION_ID = "distributionId";
public static final String EXPERIMENTS = "experiments";
public static final String LOCALE = "locale";
public static final String OS_ATTR = "os";
public static final String OS_VERSION = "osversion";
public static final String PROFILE_CREATION_DATE = "profileDate";
public static final String SEQ = "seq";
public static final String VERSION_ATTR = "v";
}
}

View File

@ -0,0 +1,138 @@
/*
* 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.telemetry;
import android.content.Context;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.Locales;
import org.mozilla.gecko.util.Experiments;
import org.mozilla.gecko.util.StringUtils;
import java.util.Locale;
/**
* Builds a {@link TelemetryPing} representing a core ping.
*
* See https://gecko.readthedocs.org/en/latest/toolkit/components/telemetry/telemetry/core-ping.html
* for details on the core ping.
*/
class TelemetryCorePingBuilder extends TelemetryPingBuilder {
private static final String NAME = "core";
private static final int VERSION_VALUE = 4; // For version history, see toolkit/components/telemetry/docs/core-ping.rst
private static final String OS_VALUE = "Android";
private static final String ARCHITECTURE = "arch";
private static final String CLIENT_ID = "clientId";
private static final String DEFAULT_SEARCH_ENGINE = "defaultSearch";
private static final String DEVICE = "device";
private static final String DISTRIBUTION_ID = "distributionId";
private static final String EXPERIMENTS = "experiments";
private static final String LOCALE = "locale";
private static final String OS_ATTR = "os";
private static final String OS_VERSION = "osversion";
private static final String PROFILE_CREATION_DATE = "profileDate";
private static final String SEQ = "seq";
private static final String VERSION_ATTR = "v";
public TelemetryCorePingBuilder(final Context context, final String serverURLSchemeHostPort) {
super(serverURLSchemeHostPort);
initPayloadConstants(context);
}
private void initPayloadConstants(final Context context) {
payload.put(VERSION_ATTR, VERSION_VALUE);
payload.put(OS_ATTR, OS_VALUE);
// We limit the device descriptor to 32 characters because it can get long. We give fewer characters to the
// manufacturer because we're less likely to have manufacturers with similar names than we are for a
// manufacturer to have two devices with the similar names (e.g. Galaxy S6 vs. Galaxy Note 6).
final String deviceDescriptor =
StringUtils.safeSubstring(Build.MANUFACTURER, 0, 12) + '-' + StringUtils.safeSubstring(Build.MODEL, 0, 19);
payload.put(ARCHITECTURE, AppConstants.ANDROID_CPU_ARCH);
payload.put(DEVICE, deviceDescriptor);
payload.put(LOCALE, Locales.getLanguageTag(Locale.getDefault()));
payload.put(OS_VERSION, Integer.toString(Build.VERSION.SDK_INT)); // A String for cross-platform reasons.
payload.putArray(EXPERIMENTS, Experiments.getActiveExperiments(context));
}
@Override
String getDocType() {
return NAME;
}
@Override
String[] getMandatoryFields() {
return new String[] {
ARCHITECTURE,
CLIENT_ID,
DEFAULT_SEARCH_ENGINE,
DEVICE,
LOCALE,
OS_ATTR,
OS_VERSION,
PROFILE_CREATION_DATE,
SEQ,
VERSION_ATTR,
};
}
public TelemetryCorePingBuilder setClientID(@NonNull final String clientID) {
if (clientID == null) {
throw new IllegalArgumentException("Expected non-null clientID");
}
payload.put(CLIENT_ID, clientID);
return this;
}
/**
* @param engine the default search engine identifier, or null if there is an error.
*/
public TelemetryCorePingBuilder setDefaultSearchEngine(@Nullable final String engine) {
if (engine != null && engine.isEmpty()) {
throw new IllegalArgumentException("Received empty string. Expected identifier or null.");
}
payload.put(DEFAULT_SEARCH_ENGINE, engine);
return this;
}
public TelemetryCorePingBuilder setOptDistributionID(@NonNull final String distributionID) {
if (distributionID == null) {
throw new IllegalArgumentException("Expected non-null distribution ID");
}
payload.put(DISTRIBUTION_ID, distributionID);
return this;
}
/**
* @param date a positive date value, or null if there is an error.
*/
public TelemetryCorePingBuilder setProfileCreationDate(@Nullable final Long date) {
if (date != null && date < 0) {
throw new IllegalArgumentException("Expect positive date value. Received: " + date);
}
payload.put(PROFILE_CREATION_DATE, date);
return this;
}
// TODO (mcomella): We can potentially build two pings with the same seq no if we leave seq as an argument.
/**
* @param seq a positive sequence number.
*/
public TelemetryCorePingBuilder setSequenceNumber(final int seq) {
if (seq < 0) {
throw new IllegalArgumentException("Expected positive sequence number. Recived: " + seq);
}
payload.put(SEQ, seq);
return this;
}
}

View File

@ -9,6 +9,9 @@ import org.mozilla.gecko.sync.ExtendedJSONObject;
/**
* Container for telemetry data and the data necessary to upload it.
*
* If you want to create one of these, consider extending
* {@link TelemetryPingBuilder} or one of its descendants.
*/
public class TelemetryPing {
private final String url;

View File

@ -0,0 +1,88 @@
/*
* 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.telemetry;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import java.util.Set;
import java.util.UUID;
/**
* A generic Builder for {@link TelemetryPing} instances. Each overriding class is
* expected to create a specific type of ping (e.g. "core").
*
* This base class handles the common ping operations under the hood:
* * Validating mandatory fields
* * Forming the server url
*/
abstract class TelemetryPingBuilder {
// In the server url, the initial path directly after the "scheme://host:port/"
private static final String SERVER_INITIAL_PATH = "submit/telemetry";
private final String serverUrl;
protected final ExtendedJSONObject payload;
public TelemetryPingBuilder(final String serverURLSchemeHostPort) {
serverUrl = getTelemetryServerURL(getDocType(), serverURLSchemeHostPort);
payload = new ExtendedJSONObject();
}
/**
* @return the name of the ping (e.g. "core")
*/
abstract String getDocType();
/**
* @return the fields that are mandatory for the resultant ping to be uploaded to
* the server. These will be validated before the ping is built.
*/
abstract String[] getMandatoryFields();
public TelemetryPing build() {
validatePayload();
return new TelemetryPing(serverUrl, payload);
}
private void validatePayload() {
final Set<String> keySet = payload.keySet();
for (final String mandatoryField : getMandatoryFields()) {
if (!keySet.contains(mandatoryField)) {
throw new IllegalArgumentException("Builder does not contain mandatory field: " +
mandatoryField);
}
}
}
/**
* Returns a url of the format:
* http://hostname/submit/telemetry/docId/docType/appName/appVersion/appUpdateChannel/appBuildID
*
* @param serverURLSchemeHostPort The server url with the scheme, host, and port (e.g. "http://mozilla.org:80")
* @param docType The name of the ping (e.g. "main")
* @return a url at which to POST the telemetry data to
*/
private static String getTelemetryServerURL(final String docType,
final String serverURLSchemeHostPort) {
final String docId = UUID.randomUUID().toString();
final String appName = AppConstants.MOZ_APP_BASENAME;
final String appVersion = AppConstants.MOZ_APP_VERSION;
final String appUpdateChannel = AppConstants.MOZ_UPDATE_CHANNEL;
final String appBuildId = AppConstants.MOZ_APP_BUILDID;
// The compiler will optimize a single String concatenation into a StringBuilder statement.
// If you change this `return`, be sure to keep it as a single statement to keep it optimized!
return serverURLSchemeHostPort + '/' +
SERVER_INITIAL_PATH + '/' +
docId + '/' +
docType + '/' +
appName + '/' +
appVersion + '/' +
appUpdateChannel + '/' +
appBuildId;
}
}

View File

@ -1,107 +0,0 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
* 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.telemetry;
import android.content.Context;
import android.os.Build;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.Locales;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.telemetry.TelemetryConstants.CorePing;
import org.mozilla.gecko.util.Experiments;
import org.mozilla.gecko.util.StringUtils;
import java.io.IOException;
import java.util.Locale;
/**
* A class with static methods to generate the various Java-created Telemetry pings to upload to the telemetry server.
*/
public class TelemetryPingGenerator {
// In the server url, the initial path directly after the "scheme://host:port/"
private static final String SERVER_INITIAL_PATH = "submit/telemetry";
/**
* Returns a url of the format:
* http://hostname/submit/telemetry/docId/docType/appName/appVersion/appUpdateChannel/appBuildID
*
* @param docId A unique document ID for the ping associated with the upload to this server
* @param serverURLSchemeHostPort The server url with the scheme, host, and port (e.g. "http://mozilla.org:80")
* @param docType The name of the ping (e.g. "main")
* @return a url at which to POST the telemetry data to
*/
private static String getTelemetryServerURL(final String docId, final String serverURLSchemeHostPort,
final String docType) {
final String appName = AppConstants.MOZ_APP_BASENAME;
final String appVersion = AppConstants.MOZ_APP_VERSION;
final String appUpdateChannel = AppConstants.MOZ_UPDATE_CHANNEL;
final String appBuildId = AppConstants.MOZ_APP_BUILDID;
// The compiler will optimize a single String concatenation into a StringBuilder statement.
// If you change this `return`, be sure to keep it as a single statement to keep it optimized!
return serverURLSchemeHostPort + '/' +
SERVER_INITIAL_PATH + '/' +
docId + '/' +
docType + '/' +
appName + '/' +
appVersion + '/' +
appUpdateChannel + '/' +
appBuildId;
}
/**
* @param docId A unique document ID for the ping associated with the upload to this server
* @param clientId The client ID of this profile (from Gecko)
* @param serverURLSchemeHostPort The server url with the scheme, host, and port (e.g. "http://mozilla.org:80")
* @param profileCreationDateDays The profile creation date in days to the UNIX epoch, NOT MILLIS.
* @throws IOException when client ID could not be created
*/
public static TelemetryPing createCorePing(final Context context, final String docId, final String clientId,
final String serverURLSchemeHostPort, final int seq, final long profileCreationDateDays,
@Nullable final String distributionId, @Nullable final String defaultSearchEngine) {
final String serverURL = getTelemetryServerURL(docId, serverURLSchemeHostPort, CorePing.NAME);
final ExtendedJSONObject payload =
createCorePingPayload(context, clientId, seq, profileCreationDateDays, distributionId, defaultSearchEngine);
return new TelemetryPing(serverURL, payload);
}
private static ExtendedJSONObject createCorePingPayload(final Context context, final String clientId,
final int seq, final long profileCreationDate, @Nullable final String distributionId,
@Nullable final String defaultSearchEngine) {
final ExtendedJSONObject ping = new ExtendedJSONObject();
ping.put(CorePing.VERSION_ATTR, CorePing.VERSION_VALUE);
ping.put(CorePing.OS_ATTR, CorePing.OS_VALUE);
// We limit the device descriptor to 32 characters because it can get long. We give fewer characters to the
// manufacturer because we're less likely to have manufacturers with similar names than we are for a
// manufacturer to have two devices with the similar names (e.g. Galaxy S6 vs. Galaxy Note 6).
final String deviceDescriptor =
StringUtils.safeSubstring(Build.MANUFACTURER, 0, 12) + '-' + StringUtils.safeSubstring(Build.MODEL, 0, 19);
ping.put(CorePing.ARCHITECTURE, AppConstants.ANDROID_CPU_ARCH);
ping.put(CorePing.CLIENT_ID, clientId);
ping.put(CorePing.DEFAULT_SEARCH_ENGINE, TextUtils.isEmpty(defaultSearchEngine) ? null : defaultSearchEngine);
ping.put(CorePing.DEVICE, deviceDescriptor);
ping.put(CorePing.LOCALE, Locales.getLanguageTag(Locale.getDefault()));
ping.put(CorePing.OS_VERSION, Integer.toString(Build.VERSION.SDK_INT)); // A String for cross-platform reasons.
ping.put(CorePing.SEQ, seq);
ping.putArray(CorePing.EXPERIMENTS, Experiments.getActiveExperiments(context));
// Optional.
if (distributionId != null) {
ping.put(CorePing.DISTRIBUTION_ID, distributionId);
}
// `null` indicates failure more clearly than < 0.
final Long finalProfileCreationDate = (profileCreationDate < 0) ? null : profileCreationDate;
ping.put(CorePing.PROFILE_CREATION_DATE, finalProfileCreationDate);
return ping;
}
}

View File

@ -10,6 +10,7 @@ import android.content.SharedPreferences;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import android.util.Log;
import ch.boye.httpclientandroidlib.HttpResponse;
import ch.boye.httpclientandroidlib.client.ClientProtocolException;
@ -170,7 +171,6 @@ public class TelemetryUploadService extends BackgroundService {
private void uploadCorePing(@NonNull final String docId, final int seq, @NonNull final String profileName,
@NonNull final String profilePath, @Nullable final String defaultSearchEngine) {
final GeckoProfile profile = GeckoProfile.get(this, profileName, profilePath);
final long profileCreationDate = getProfileCreationDate(profile);
final String clientId;
try {
clientId = profile.getClientId();
@ -184,9 +184,20 @@ public class TelemetryUploadService extends BackgroundService {
// TODO (bug 1241685): Sync this preference with the gecko preference.
final String serverURLSchemeHostPort =
sharedPrefs.getString(TelemetryConstants.PREF_SERVER_URL, TelemetryConstants.DEFAULT_SERVER_URL);
final long profileCreationDate = getProfileCreationDate(profile);
final TelemetryCorePingBuilder builder = new TelemetryCorePingBuilder(this, serverURLSchemeHostPort)
.setClientID(clientId)
.setDefaultSearchEngine(TextUtils.isEmpty(defaultSearchEngine) ? null : defaultSearchEngine)
.setProfileCreationDate(profileCreationDate < 0 ? null : profileCreationDate)
.setSequenceNumber(seq);
final String distributionId = sharedPrefs.getString(DistributionStoreCallback.PREF_DISTRIBUTION_ID, null);
final TelemetryPing corePing = TelemetryPingGenerator.createCorePing(this, docId, clientId,
serverURLSchemeHostPort, seq, profileCreationDate, distributionId, defaultSearchEngine);
if (distributionId != null) {
builder.setOptDistributionID(distributionId);
}
final TelemetryPing corePing = builder.build();
final CorePingResultDelegate resultDelegate = new CorePingResultDelegate();
uploadPing(corePing, resultDelegate);
}

View File

@ -573,8 +573,9 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
'tabs/TabsPanelThumbnailView.java',
'Telemetry.java',
'telemetry/TelemetryConstants.java',
'telemetry/TelemetryCorePingBuilder.java',
'telemetry/TelemetryPing.java',
'telemetry/TelemetryPingGenerator.java',
'telemetry/TelemetryPingBuilder.java',
'telemetry/TelemetryUploadService.java',
'TelemetryContract.java',
'text/FloatingActionModeCallback.java',