Bug 1205835 - Add TelemetryPingGenerator for core pings. r=rnewman draft
authorMichael Comella <michael.l.comella@gmail.com>
Thu, 21 Jan 2016 11:33:33 -0800
changeset 324110 f6f0ac733e767c12104e69c60668345fa8e6bce2
parent 323642 63eb0ddd1cdbe82b4fdfd1c7bf9be38a95bc5234
child 324111 56652bb047815ed25fe806e51835e4898612b890
push id9850
push usermichael.l.comella@gmail.com
push dateThu, 21 Jan 2016 23:53:52 +0000
reviewersrnewman
bugs1205835
milestone46.0a1
Bug 1205835 - Add TelemetryPingGenerator for core pings. r=rnewman
mobile/android/base/java/org/mozilla/gecko/GeckoProfile.java
mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryConstants.java
mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPing.java
mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryPingGenerator.java
mobile/android/base/java/org/mozilla/gecko/util/StringUtils.java
mobile/android/base/moz.build
--- 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,84 @@
+/* -*- 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 java.util.UUID;
+
+import org.mozilla.gecko.AppConstants;
+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 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 serverUrlSchemeHostPort, final String docType) {
+        final UUID docId = UUID.randomUUID();
+        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 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 clientId, final String serverUrlSchemeHostPort,
+            final int pingCount) {
+        final String serverUrl = getTelemetryServerUrl(serverUrlSchemeHostPort, CorePing.NAME);
+        final ExtendedJSONObject payload = createCorePingPayload(clientId, pingCount);
+        return new TelemetryPing(serverUrl, payload);
+    }
+
+    private static ExtendedJSONObject createCorePingPayload(final String clientId, final int pingCount) {
+        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, Locale.getDefault().toString()); // TODO: Which locale here?
+        ping.put(CorePing.OS_VERSION, Integer.toString(Build.VERSION.SDK_INT)); // A String for cross-platform reasons.
+        ping.put(CorePing.SEQ, pingCount);
+        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
@@ -534,16 +534,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',