Bug 1205835 - Add TelemetryPingGenerator for core pings. r=rnewman
--- a/mobile/android/base/java/org/mozilla/gecko/GeckoProfile.java
+++ b/mobile/android/base/java/org/mozilla/gecko/GeckoProfile.java
@@ -33,22 +33,28 @@ import org.mozilla.gecko.firstrun.Firstr
import org.mozilla.gecko.preferences.DistroSharedPrefsImport;
import org.mozilla.gecko.util.INIParser;
import org.mozilla.gecko.util.INISection;
import android.app.Activity;
import android.content.ContentResolver;
import android.content.Context;
import android.content.SharedPreferences;
+import android.support.annotation.WorkerThread;
import android.text.TextUtils;
import android.util.Log;
public final class GeckoProfile {
private static final String LOGTAG = "GeckoProfile";
+ // The path in the profile to the file containing the client ID.
+ private static final String CLIENT_ID_FILE_PATH = "datareporting/state.json";
+ // In the client ID file, the attribute title in the JSON object containing the client ID value.
+ private static final String CLIENT_ID_JSON_ATTR = "clientID";
+
// Only tests should need to do this.
// We can default this to AppConstants.RELEASE_BUILD once we fix Bug 1069687.
private static volatile boolean sAcceptDirectoryChanges = true;
@RobocopTarget
public static void enableDirectoryChanges() {
Log.w(LOGTAG, "Directory changes should only be enabled for tests. And even then it's a bad idea.");
sAcceptDirectoryChanges = true;
@@ -584,16 +590,49 @@ public final class GeckoProfile {
File f = getDir();
if (f == null)
return null;
return new File(f, aFile);
}
/**
+ * Retrieves the Gecko client ID from the filesystem.
+ *
+ * This method assumes the client ID is located in a file at a hard-coded path within the profile. The format of
+ * this file is a JSONObject which at the bottom level contains a String -> String mapping containing the client ID.
+ *
+ * WARNING: the platform provides a JSM to retrieve the client ID [1] and this would be a
+ * robust way to access it. However, we don't want to rely on Gecko running in order to get
+ * the client ID so instead we access the file this module accesses directly. However, it's
+ * possible the format of this file (and the access calls in the jsm) will change, leaving
+ * this code to fail.
+ *
+ * TODO: Write tests to prevent regressions. Mention them here. Test both file location and file format.
+ *
+ * [1]: https://mxr.mozilla.org/mozilla-central/source/toolkit/modules/ClientID.jsm
+ */
+ @WorkerThread
+ public String getClientId() throws IOException {
+ final String clientIdFileContents;
+ try {
+ clientIdFileContents = readFile(CLIENT_ID_FILE_PATH);
+ } catch (final IOException e) {
+ throw new IOException("Could not read client ID file to retrieve client ID", e);
+ }
+
+ try {
+ final org.json.JSONObject json = new org.json.JSONObject(clientIdFileContents);
+ return json.getString(CLIENT_ID_JSON_ATTR);
+ } catch (final JSONException e) {
+ throw new IOException("Could not parse JSON to retrieve client ID", e);
+ }
+ }
+
+ /**
* Moves the session file to the backup session file.
*
* sessionstore.js should hold the current session, and sessionstore.bak
* should hold the previous session (where it is used to read the "tabs
* from last time"). Normally, sessionstore.js is moved to sessionstore.bak
* on a clean quit, but this doesn't happen if Fennec crashed. Thus, this
* method should be called after a crash so sessionstore.bak correctly
* holds the previous session.
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryConstants.java
@@ -0,0 +1,25 @@
+/* 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;
+
+public class TelemetryConstants {
+
+ public static class CorePing {
+ private CorePing() { /* To prevent instantiation */ }
+
+ public static final String NAME = "core";
+ public static final int VERSION_VALUE = 1;
+ public static final String OS_VALUE = "Android";
+
+ public static final String ARCHITECTURE = "arch";
+ public static final String CLIENT_ID = "clientId";
+ public static final String DEVICE = "device";
+ public static final String LOCALE = "locale";
+ public static final String OS_ATTR = "os";
+ public static final String OS_VERSION = "osversion";
+ public static final String SEQ = "seq";
+ public static final String VERSION_ATTR = "v";
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPing.java
@@ -0,0 +1,24 @@
+/* -*- 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 org.mozilla.gecko.sync.ExtendedJSONObject;
+
+/**
+ * Container for telemetry data and the data necessary to upload it.
+ */
+public class TelemetryPing {
+ private final String url;
+ private final ExtendedJSONObject payload;
+
+ public TelemetryPing(final String url, final ExtendedJSONObject payload) {
+ this.url = url;
+ this.payload = payload;
+ }
+
+ public String getURL() { return url; }
+ public ExtendedJSONObject getPayload() { return payload; }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPingGenerator.java
@@ -0,0 +1,86 @@
+/* -*- 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.os.Build;
+import java.io.IOException;
+import java.util.Locale;
+
+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.StringUtils;
+
+/**
+ * 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")
+ * @throws IOException when client ID could not be created
+ */
+ public static TelemetryPing createCorePing(final String docId, final String clientId,
+ final String serverURLSchemeHostPort, final int seq) {
+ final String serverURL = getTelemetryServerURL(docId, serverURLSchemeHostPort, CorePing.NAME);
+ final ExtendedJSONObject payload = createCorePingPayload(clientId, seq);
+ return new TelemetryPing(serverURL, payload);
+ }
+
+ private static ExtendedJSONObject createCorePingPayload(final String clientId, final int seq) {
+ 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.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);
+ return ping;
+ }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/util/StringUtils.java
+++ b/mobile/android/base/java/org/mozilla/gecko/util/StringUtils.java
@@ -1,16 +1,17 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; 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.util;
import android.net.Uri;
+import android.support.annotation.NonNull;
import android.text.TextUtils;
import org.mozilla.gecko.AppConstants.Versions;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
@@ -235,9 +236,15 @@ public class StringUtils {
names.add(Uri.decode(name));
// Move start to end of name.
start = end + 1;
} while (start < query.length());
return Collections.unmodifiableSet(names);
}
+
+ public static String safeSubstring(@NonNull final String str, final int start, final int end) {
+ return str.substring(
+ Math.max(0, start),
+ Math.min(end, str.length()));
+ }
}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -535,16 +535,19 @@ gbjar.sources += ['java/org/mozilla/geck
'tabs/TabPanelBackButton.java',
'tabs/TabsGridLayout.java',
'tabs/TabsLayoutAdapter.java',
'tabs/TabsLayoutItemView.java',
'tabs/TabsListLayout.java',
'tabs/TabsPanel.java',
'tabs/TabsPanelThumbnailView.java',
'Telemetry.java',
+ 'telemetry/TelemetryConstants.java',
+ 'telemetry/TelemetryPing.java',
+ 'telemetry/TelemetryPingGenerator.java',
'TelemetryContract.java',
'TextSelection.java',
'TextSelectionHandle.java',
'ThumbnailHelper.java',
'toolbar/AutocompleteHandler.java',
'toolbar/BackButton.java',
'toolbar/BrowserToolbar.java',
'toolbar/BrowserToolbarPhone.java',