Bug 873496 - Part 1: searches provider for FHR on Android. r=cpeterson, a=lsblakk
authorRichard Newman <rnewman@mozilla.com>
Thu, 30 May 2013 17:42:56 -0700
changeset 142924 23a61acd3139c3273f5a4c95a3da161a1f61f235
parent 142923 e790d9247363914cb160f5261a5c49338317cccb
child 142925 78a25df6a5b27add773924ef20913fcf9b6769ee
push id2579
push userakeybl@mozilla.com
push dateMon, 24 Jun 2013 18:52:47 +0000
treeherdermozilla-beta@b69b7de8a05a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerscpeterson, lsblakk
bugs873496
milestone23.0a2
Bug 873496 - Part 1: searches provider for FHR on Android. r=cpeterson, a=lsblakk
mobile/android/base/AwesomeBar.java
mobile/android/base/GeckoApp.java
mobile/android/base/health/BrowserHealthRecorder.java
mobile/android/base/util/EventDispatcher.java
--- a/mobile/android/base/AwesomeBar.java
+++ b/mobile/android/base/AwesomeBar.java
@@ -3,16 +3,17 @@
  * 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;
 
 import org.mozilla.gecko.db.BrowserContract.Combined;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.gfx.BitmapUtils;
+import org.mozilla.gecko.health.BrowserHealthRecorder;
 import org.mozilla.gecko.util.GamepadUtils;
 import org.mozilla.gecko.util.StringUtils;
 import org.mozilla.gecko.util.ThreadUtils;
 import org.mozilla.gecko.util.UiAsyncTask;
 
 import android.app.Activity;
 import android.app.AlertDialog;
 import android.content.Context;
@@ -40,16 +41,18 @@ import android.view.View;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputMethodManager;
 import android.widget.Button;
 import android.widget.EditText;
 import android.widget.ImageButton;
 import android.widget.TabWidget;
 import android.widget.Toast;
 
+import org.json.JSONObject;
+
 import java.net.URLEncoder;
 
 interface AutocompleteHandler {
     void onAutocomplete(String res);
 }
 
 public class AwesomeBar extends GeckoActivity
                         implements AutocompleteHandler,
@@ -101,16 +104,17 @@ public class AwesomeBar extends GeckoAct
             }
 
             @Override
             public void onSearch(SearchEngine engine, String text) {
                 Intent resultIntent = new Intent();
                 resultIntent.putExtra(URL_KEY, text);
                 resultIntent.putExtra(TARGET_KEY, mTarget);
                 resultIntent.putExtra(SEARCH_KEY, engine.name);
+                recordSearch(engine.identifier, "barsuggest");
                 finishWithResult(resultIntent);
             }
 
             @Override
             public void onEditSuggestion(final String text) {
                 ThreadUtils.postToUiThread(new Runnable() {
                     @Override
                     public void run() {
@@ -383,16 +387,38 @@ public class AwesomeBar extends GeckoAct
         if (title != null && !TextUtils.isEmpty(title))
             resultIntent.putExtra(TITLE_KEY, title);
         if (userEntered)
             resultIntent.putExtra(USER_ENTERED_KEY, userEntered);
         resultIntent.putExtra(TARGET_KEY, mTarget);
         finishWithResult(resultIntent);
     }
 
+    /**
+     * Record in Health Report that a search has occurred.
+     *
+     * @param identifier
+     *        a search identifier, such as "partnername". Can be null.
+     * @param where
+     *        where the search was initialized; one of the values in
+     *        {@link BrowserHealthRecorder#SEARCH_LOCATIONS}.
+     */
+    private static void recordSearch(String identifier, String where) {
+        Log.i(LOGTAG, "Recording search: " + identifier + ", " + where);
+        try {
+            JSONObject message = new JSONObject();
+            message.put("type", BrowserHealthRecorder.EVENT_SEARCH);
+            message.put("location", where);
+            message.put("identifier", identifier);
+            GeckoAppShell.getEventDispatcher().dispatchEvent(message);
+        } catch (Exception e) {
+            Log.w(LOGTAG, "Error recording search.", e);
+        }
+    }
+
     private void openUserEnteredAndFinish(final String url) {
         final int index = url.indexOf(' ');
 
         // Check for a keyword if the URL looks like a search query
         if (!StringUtils.isSearchQuery(url, true)) {
             openUrlAndFinish(url, "", true);
             return;
         }
@@ -409,16 +435,19 @@ public class AwesomeBar extends GeckoAct
                     keyword = url.substring(0, index);
                     keywordSearch = url.substring(index + 1);
                 }
 
                 final String keywordUrl = BrowserDB.getUrlForKeyword(getContentResolver(), keyword);
                 final String searchUrl = (keywordUrl != null)
                                        ? keywordUrl.replace("%s", URLEncoder.encode(keywordSearch))
                                        : url;
+                if (keywordUrl != null) {
+                    recordSearch(null, "barkeyword");
+                }
                 openUrlAndFinish(searchUrl, "", true);
             }
         });
     }
 
     @Override
     public boolean onKeyDown(int keyCode, KeyEvent event) {
         // Galaxy Note sends key events for the stylus that are outside of the
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -2123,23 +2123,24 @@ abstract public class GeckoApp
         Tabs.getInstance().detachFromActivity(this);
 
         if (SmsManager.getInstance() != null) {
             SmsManager.getInstance().stop();
             if (isFinishing())
                 SmsManager.getInstance().shutdown();
         }
 
-        super.onDestroy();
-
-        Tabs.unregisterOnTabsChangedListener(this);
         if (mHealthRecorder != null) {
             mHealthRecorder.close();
             mHealthRecorder = null;
         }
+
+        super.onDestroy();
+
+        Tabs.unregisterOnTabsChangedListener(this);
     }
 
     protected void registerEventListener(String event) {
         GeckoAppShell.getEventDispatcher().registerEventListener(event, this);
     }
 
     protected void unregisterEventListener(String event) {
         GeckoAppShell.getEventDispatcher().unregisterEventListener(event, this);
--- a/mobile/android/base/health/BrowserHealthRecorder.java
+++ b/mobile/android/base/health/BrowserHealthRecorder.java
@@ -29,16 +29,17 @@ import org.mozilla.gecko.util.GeckoEvent
 import org.mozilla.gecko.util.ThreadUtils;
 
 import org.json.JSONObject;
 
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.OutputStreamWriter;
 import java.nio.charset.Charset;
+import java.util.ArrayList;
 import java.util.Scanner;
 
 /**
  * BrowserHealthRecorder is the browser's interface to the Firefox Health
  * Report storage system. It manages environments (a collection of attributes
  * that are tracked longitudinally) on the browser's behalf, exposing a simpler
  * interface for recording changes.
  *
@@ -54,16 +55,23 @@ import java.util.Scanner;
  * Shut it down when you're done being a browser: {@link #close(EventDispatcher)}.
  */
 public class BrowserHealthRecorder implements GeckoEventListener {
     private static final String LOG_TAG = "GeckoHealthRec";
     private static final String PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled";
     private static final String EVENT_ADDONS_ALL = "Addons:All";
     private static final String EVENT_ADDONS_CHANGE = "Addons:Change";
     private static final String EVENT_PREF_CHANGE = "Pref:Change";
+ 
+    // This is raised from Gecko. It avoids browser.js having to know about the
+    // location that invoked it (the URL bar).
+    public static final String EVENT_KEYWORD_SEARCH = "Search:Keyword";
+
+    // This is raised from Java. We include the location in the message.
+    public static final String EVENT_SEARCH = "Search:Event";
 
     public enum State {
         NOT_INITIALIZED,
         INITIALIZING,
         INITIALIZED,
         INITIALIZATION_FAILED,
         CLOSED
     }
@@ -136,16 +144,18 @@ public class BrowserHealthRecorder imple
             this.client = null;
         }
     }
 
     private void unregisterEventListeners() {
         this.dispatcher.unregisterEventListener(EVENT_ADDONS_ALL, this);
         this.dispatcher.unregisterEventListener(EVENT_ADDONS_CHANGE, this);
         this.dispatcher.unregisterEventListener(EVENT_PREF_CHANGE, this);
+        this.dispatcher.unregisterEventListener(EVENT_KEYWORD_SEARCH, this);
+        this.dispatcher.unregisterEventListener(EVENT_SEARCH, this);
     }
 
     public void onBlocklistPrefChanged(boolean to) {
         this.profileCache.beginInitialization();
         this.profileCache.setBlocklistEnabled(to);
     }
 
     public void onTelemetryPrefChanged(boolean to) {
@@ -338,16 +348,17 @@ public class BrowserHealthRecorder imple
                     }
 
                     try {
                         // Listen for add-ons and prefs changes.
                         dispatcher.registerEventListener(EVENT_ADDONS_CHANGE, self);
                         dispatcher.registerEventListener(EVENT_PREF_CHANGE, self);
 
                         // Initialize each provider here.
+                        initializeSearchProvider();
 
                         Log.d(LOG_TAG, "Ensuring environment.");
                         ensureEnvironment();
 
                         Log.d(LOG_TAG, "Finishing init.");
                         storage.finishInitialization();
                         state = State.INITIALIZED;
                     } catch (Exception e) {
@@ -439,14 +450,100 @@ public class BrowserHealthRecorder imple
             }
             if (EVENT_PREF_CHANGE.equals(event)) {
                 final String pref = message.getString("pref");
                 Log.d(LOG_TAG, "Pref changed: " + pref);
                 handlePrefValue(pref, message.getBoolean("value"));
                 this.onEnvironmentChanged();
                 return;
             }
+
+            // Searches.
+            if (EVENT_KEYWORD_SEARCH.equals(event)) {
+                recordSearch(message.getString("identifier"), "bartext");
+                return;
+            }
+            if (EVENT_SEARCH.equals(event)) {
+                if (!message.has("location")) {
+                    Log.d(LOG_TAG, "Ignoring search without location.");
+                    return;
+                }
+                recordSearch(message.getString("identifier"), message.getString("location"));
+                return;
+            }
         } catch (Exception e) {
             Log.e(LOG_TAG, "Exception handling message \"" + event + "\":", e);
         }
     }
+
+    /*
+     * Searches.
+     */
+
+    public static final String MEASUREMENT_NAME_SEARCH_COUNTS = "org.mozilla.searches.counts";
+    public static final int MEASUREMENT_VERSION_SEARCH_COUNTS = 3;
+
+    public static final String[] SEARCH_LOCATIONS = {
+        "barkeyword",
+        "barsuggest",
+        "bartext",
+    };
+
+    private void initializeSearchProvider() {
+        this.storage.ensureMeasurementInitialized(
+            MEASUREMENT_NAME_SEARCH_COUNTS,
+            MEASUREMENT_VERSION_SEARCH_COUNTS,
+            new MeasurementFields() {
+                @Override
+                public Iterable<FieldSpec> getFields() {
+                    ArrayList<FieldSpec> out = new ArrayList<FieldSpec>(SEARCH_LOCATIONS.length);
+                    for (String location : SEARCH_LOCATIONS) {
+                        // We're not using a counter, because the set of engine
+                        // identifiers is potentially unbounded, and thus our
+                        // measurement version would have to keep growing as
+                        // fields changed.
+                        out.add(new FieldSpec(location, Field.TYPE_STRING_DISCRETE));
+                    }
+                    return out;
+                }
+        });
+
+        // Do this here, rather than in a centralized registration spot, in
+        // case the above throws and we wind up handling events that we can't
+        // store.
+        this.dispatcher.registerEventListener(EVENT_KEYWORD_SEARCH, this);
+        this.dispatcher.registerEventListener(EVENT_SEARCH, this);
+    }
+
+    /**
+     * Record a search.
+     *
+     * @param engine the string identifier for the engine, or null if it's not a partner.
+     * @param location one of a fixed set of locations: see {@link #SEARCH_LOCATIONS}.
+     */
+    public void recordSearch(final String engine, final String location) {
+        if (this.state != State.INITIALIZED) {
+            Log.d(LOG_TAG, "Not initialized: not recording search. (" + this.state + ")");
+            return;
+        }
+
+        if (location == null) {
+            throw new IllegalArgumentException("location must be provided for search.");
+        }
+
+        final int day = storage.getDay();
+        final int env = this.env;
+        final String key = ((engine == null) ? "other" : engine);
+        ThreadUtils.postToBackgroundThread(new Runnable() {
+            @Override
+            public void run() {
+                Log.d(LOG_TAG, "Recording search: " + key + ", " + location +
+                               " (" + day + ", " + env + ").");
+                final int searchField = storage.getField(MEASUREMENT_NAME_SEARCH_COUNTS,
+                                                         MEASUREMENT_VERSION_SEARCH_COUNTS,
+                                                         location)
+                                               .getID();
+                storage.recordDailyDiscrete(env, day, searchField, key);
+            }
+        });
+    }
 }
 
--- a/mobile/android/base/util/EventDispatcher.java
+++ b/mobile/android/base/util/EventDispatcher.java
@@ -48,22 +48,32 @@ public final class EventDispatcher {
             }
             if (listeners.size() == 0) {
                 mEventListeners.remove(event);
             }
         }
     }
 
     public String dispatchEvent(String message) {
+        try {
+            JSONObject json = new JSONObject(message);
+            return dispatchEvent(json);
+        } catch (Exception e) {
+            Log.e(LOGTAG, "dispatchEvent: malformed JSON.", e);
+        }
+
+        return "";
+    }
+
+    public String dispatchEvent(JSONObject json) {
         // {
         //   "type": "value",
         //   "event_specific": "value",
         //   ...
         try {
-            JSONObject json = new JSONObject(message);
             JSONObject gecko = json.has("gecko") ? json.getJSONObject("gecko") : null;
             if (gecko != null) {
                 json = gecko;
             }
 
             String type = json.getString("type");
 
             if (gecko != null) {