Bug 1205835 - Create telemetry upload service and upload in onStart. r=rnewman draft
authorMichael Comella <michael.l.comella@gmail.com>
Thu, 21 Jan 2016 15:52:00 -0800
changeset 324111 56652bb047815ed25fe806e51835e4898612b890
parent 324110 f6f0ac733e767c12104e69c60668345fa8e6bce2
child 324113 972d1506d50d48b8ee7004c612c0aff07a929bce
push id9850
push usermichael.l.comella@gmail.com
push dateThu, 21 Jan 2016 23:53:52 +0000
reviewersrnewman
bugs1205835
milestone46.0a1
Bug 1205835 - Create telemetry upload service and upload in onStart. r=rnewman
mobile/android/base/AndroidManifest.xml.in
mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryConstants.java
mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java
mobile/android/base/moz.build
--- a/mobile/android/base/AndroidManifest.xml.in
+++ b/mobile/android/base/AndroidManifest.xml.in
@@ -456,16 +456,19 @@
             android:name="org.mozilla.gecko.NotificationService">
         </service>
 
         <service
             android:exported="false"
             android:name="org.mozilla.gecko.dlc.DownloadContentService">
         </service>
 
+        <service
+          android:name="org.mozilla.gecko.telemetry.TelemetryUploadService"
+          android:exported="false"/>
 
 #include ../services/manifests/FxAccountAndroidManifest_services.xml.in
 #include ../services/manifests/SyncAndroidManifest_services.xml.in
 
         <service
             android:name="org.mozilla.gecko.tabqueue.TabReceivedService"
             android:exported="false" />
 
--- a/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
+++ b/mobile/android/base/java/org/mozilla/gecko/BrowserApp.java
@@ -55,16 +55,18 @@ import org.mozilla.gecko.restrictions.Re
 import org.mozilla.gecko.sync.repositories.android.FennecTabsRepository;
 import org.mozilla.gecko.tabqueue.TabQueueHelper;
 import org.mozilla.gecko.tabqueue.TabQueuePrompt;
 import org.mozilla.gecko.tabs.TabHistoryController;
 import org.mozilla.gecko.tabs.TabHistoryController.OnShowTabHistory;
 import org.mozilla.gecko.tabs.TabHistoryFragment;
 import org.mozilla.gecko.tabs.TabHistoryPage;
 import org.mozilla.gecko.tabs.TabsPanel;
+import org.mozilla.gecko.telemetry.TelemetryConstants;
+import org.mozilla.gecko.telemetry.TelemetryUploadService;
 import org.mozilla.gecko.toolbar.AutocompleteHandler;
 import org.mozilla.gecko.toolbar.BrowserToolbar;
 import org.mozilla.gecko.toolbar.BrowserToolbar.TabEditingState;
 import org.mozilla.gecko.toolbar.ToolbarProgressView;
 import org.mozilla.gecko.trackingprotection.TrackingProtectionPrompt;
 import org.mozilla.gecko.util.ActivityUtils;
 import org.mozilla.gecko.util.Clipboard;
 import org.mozilla.gecko.util.EventCallback;
@@ -963,16 +965,18 @@ public class BrowserApp extends GeckoApp
                 } else {
                     // If we're restarting, we won't destroy the activity.
                     // Make sure we remove any guest notifications that might
                     // have been shown.
                     GuestSession.hideNotification(BrowserApp.this);
                 }
             }
         });
+
+        uploadTelemetry();
     }
 
     @Override
     public void onStop() {
         super.onStop();
 
         // We only show the guest mode notification when our activity is in the foreground.
         GuestSession.hideNotification(this);
@@ -3857,16 +3861,27 @@ public class BrowserApp extends GeckoApp
 
         mActionBarFlipper.showPrevious();
 
         // Only slide the urlbar out if it was hidden when the action mode started
         // Don't animate hiding it so that there's no flash as we switch back to url mode
         mDynamicToolbar.setTemporarilyVisible(false, VisibilityTransition.IMMEDIATE);
     }
 
+    private void uploadTelemetry() {
+        if (!TelemetryConstants.UPLOAD_ENABLED || getProfile().inGuestMode()) {
+            return;
+        }
+
+        final Intent i = new Intent(TelemetryConstants.ACTION_UPLOAD_CORE);
+        i.setClass(this, TelemetryUploadService.class);
+        startService(i);
+        Log.d("GeckoTelemetry", "Upload service started");
+    }
+
     public static interface Refreshable {
         public void refresh();
     }
 
     @Override
     protected StartupAction getStartupAction(final String passedURL) {
         final boolean inGuestMode = GeckoProfile.get(this).inGuestMode();
         if (inGuestMode) {
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryConstants.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryConstants.java
@@ -1,16 +1,26 @@
 /* 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;
+
 public class TelemetryConstants {
 
+    public static final boolean UPLOAD_ENABLED = AppConstants.MOZILLA_OFFICIAL; // Disabled for developer builds.
+    public static final String DEFAULT_SERVER_URL = "https://incoming.telemetry.mozilla.org";
+
+    public static final String ACTION_UPLOAD_CORE = "uploadCore";
+
+    public static final String PREF_SERVER_URL = "telemetry-serverUrl";
+    public static final String PREF_PING_COUNT = "telemetry-pingCount";
+
     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";
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryUploadService.java
@@ -0,0 +1,171 @@
+/* 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.Intent;
+import android.content.SharedPreferences;
+import android.util.Log;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.client.ClientProtocolException;
+import org.mozilla.gecko.GeckoProfile;
+import org.mozilla.gecko.GeckoSharedPrefs;
+import org.mozilla.gecko.background.BackgroundService;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.BaseResourceDelegate;
+import org.mozilla.gecko.sync.net.Resource;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.security.GeneralSecurityException;
+
+/**
+ * The service that handles uploading telemetry payloads to the server.
+ */
+public class TelemetryUploadService extends BackgroundService {
+    private static final String LOGTAG = "Gecko" + TelemetryUploadService.class.getSimpleName();
+    private static final String WORKER_THREAD_NAME = LOGTAG + "Worker";
+
+    public TelemetryUploadService() {
+        super(WORKER_THREAD_NAME);
+        // We may upload to the server more than once but we have no metrics
+        // that can double count at the moment so it doesn't matter.
+        setIntentRedelivery(true);
+    }
+
+    @Override
+    public void onHandleIntent(final Intent intent) {
+        Log.d(LOGTAG, "Service started");
+
+        if (!TelemetryConstants.UPLOAD_ENABLED) {
+            Log.d(LOGTAG, "Health report upload feature is compile-time disabled; not handling upload intent.");
+            return;
+        }
+
+        if (!isReadyToUpload(intent)) {
+            return;
+        }
+
+        if (!TelemetryConstants.ACTION_UPLOAD_CORE.equals(intent.getAction())) {
+            Log.w(LOGTAG, "Unknown action: " + intent.getAction() + ". Returning");
+            return;
+        }
+
+        uploadCorePing();
+    }
+
+    private boolean isReadyToUpload(final Intent intent) {
+        // Intent can be null. Bug 1025937.
+        if (intent == null) {
+            Log.d(LOGTAG, "Received null intent. Returning.");
+            return false;
+        }
+
+        // Don't do anything if the device can't talk to the server.
+        if (!backgroundDataIsEnabled()) {
+            Log.d(LOGTAG, "Background data is not enabled; skipping.");
+            return false;
+        }
+
+        return true;
+    }
+
+    private void uploadCorePing() {
+        final String clientId;
+        try {
+            clientId = GeckoProfile.get(this).getClientId();
+        } catch (final IOException e) {
+            Log.w(LOGTAG, "Unable to get client ID to generate core ping: returning.", e);
+            return;
+        }
+
+        // Each profile can have different telemetry data so we intentionally grab the shared prefs for the profile.
+        final SharedPreferences sharedPrefs = GeckoSharedPrefs.forProfile(this);
+        // TODO (bug 1241685): Sync this preference with the gecko preference.
+        final String serverUrlSchemeHostPort =
+                sharedPrefs.getString(TelemetryConstants.PREF_SERVER_URL, TelemetryConstants.DEFAULT_SERVER_URL);
+        final int pingCount = sharedPrefs.getInt(TelemetryConstants.PREF_PING_COUNT, 1);
+
+        final TelemetryPing corePing = TelemetryPingGenerator.createCorePing(clientId, serverUrlSchemeHostPort, pingCount);
+        final CorePingResultDelegate resultDelegate = new CorePingResultDelegate(sharedPrefs, pingCount);
+        uploadPing(corePing, resultDelegate);
+    }
+
+    private void uploadPing(final TelemetryPing ping, final ResultDelegate delegate) {
+        final BaseResource resource;
+        try {
+            resource = new BaseResource(ping.getUrl());
+        } catch (final URISyntaxException e) {
+            Log.w(LOGTAG, "URISyntaxException for server url when creating BaseResource: returning.");
+            return;
+        }
+
+        delegate.setResource(resource);
+        resource.delegate = delegate;
+        resource.post(ping.getPayload());
+        Log.d(LOGTAG, "Ping upload initiated.");
+    }
+
+    private static class CorePingResultDelegate extends ResultDelegate {
+        final SharedPreferences sharedPrefs;
+        final int pingCount;
+
+        public CorePingResultDelegate(final SharedPreferences sharedPrefs, final int pingCount) {
+            super();
+            this.sharedPrefs = sharedPrefs;
+            this.pingCount = pingCount;
+        }
+
+        @Override
+        public String getUserAgent() {
+            return null;
+        }
+
+        @Override
+        public void handleHttpResponse(final HttpResponse response) {
+            final int status = response.getStatusLine().getStatusCode();
+            switch (status) {
+                case 200:
+                case 201:
+                    Log.d(LOGTAG, "Telemetry upload success.");
+                    sharedPrefs.edit().putInt(TelemetryConstants.PREF_PING_COUNT, pingCount + 1).apply();
+                    break;
+                default:
+                    Log.w(LOGTAG, "Telemetry upload failure. HTTP status: " + status);
+            }
+        }
+
+        @Override
+        public void handleHttpProtocolException(final ClientProtocolException e) {
+            // We don't log the exception to prevent leaking user data.
+            Log.w(LOGTAG, "HttpProtocolException when trying to upload telemetry");
+        }
+
+        @Override
+        public void handleHttpIOException(final IOException e) {
+            // We don't log the exception to prevent leaking user data.
+            Log.w(LOGTAG, "HttpIOException when trying to upload telemetry");
+        }
+
+        @Override
+        public void handleTransportException(final GeneralSecurityException e) {
+            // We don't log the exception to prevent leaking user data.
+            Log.w(LOGTAG, "HttpTransportException when trying to upload telemetry");
+        }
+    }
+
+    /**
+     * A hack because I want to set the resource after the Delegate is constructed.
+     * Be sure to call {@link #setResource(Resource)}!
+     */
+    private static abstract class ResultDelegate extends BaseResourceDelegate {
+        public ResultDelegate() {
+            super(null);
+        }
+
+        protected void setResource(final Resource resource) {
+            this.resource = resource;
+        }
+    }
+}
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -537,16 +537,17 @@ gbjar.sources += ['java/org/mozilla/geck
     'tabs/TabsLayoutItemView.java',
     'tabs/TabsListLayout.java',
     'tabs/TabsPanel.java',
     'tabs/TabsPanelThumbnailView.java',
     'Telemetry.java',
     'telemetry/TelemetryConstants.java',
     'telemetry/TelemetryPing.java',
     'telemetry/TelemetryPingGenerator.java',
+    'telemetry/TelemetryUploadService.java',
     'TelemetryContract.java',
     'TextSelection.java',
     'TextSelectionHandle.java',
     'ThumbnailHelper.java',
     'toolbar/AutocompleteHandler.java',
     'toolbar/BackButton.java',
     'toolbar/BrowserToolbar.java',
     'toolbar/BrowserToolbarPhone.java',