Bug 1325303 - A-S Telemetry: track positions of Top Site items for tile and menu interactions r=sebastian draft
authorGrigory Kruglov <gkruglov@mozilla.com>
Fri, 27 Jan 2017 12:13:13 -0800
changeset 467476 93d70efb5410f8af8f977fa2310f8d0c55d16bd0
parent 467095 1e0e193b0812f68a12fbd69198552af62347af1e
child 467477 9aa7ee95ed9fe563e31a276b6c0438faa72e9918
push id43172
push usergkruglov@mozilla.com
push dateFri, 27 Jan 2017 20:17:25 +0000
reviewerssebastian
bugs1325303
milestone54.0a1
Bug 1325303 - A-S Telemetry: track positions of Top Site items for tile and menu interactions r=sebastian MozReview-Commit-ID: B4XpqpwfjMi
mobile/android/base/java/org/mozilla/gecko/activitystream/ActivityStreamTelemetry.java
mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesCard.java
mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPageAdapter.java
mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPagerAdapter.java
--- a/mobile/android/base/java/org/mozilla/gecko/activitystream/ActivityStreamTelemetry.java
+++ b/mobile/android/base/java/org/mozilla/gecko/activitystream/ActivityStreamTelemetry.java
@@ -7,31 +7,33 @@ package org.mozilla.gecko.activitystream
 
 import android.support.annotation.NonNull;
 
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.home.activitystream.model.TopSite;
 
 import java.util.HashMap;
 
 /**
  * Telemetry constants and an 'extras' builder specific to Activity Stream.
  */
 public class ActivityStreamTelemetry {
     public static class Contract {
         // Keys
         public final static String FX_ACCOUNT_PRESENT = "fx_account_present";
         public final static String ITEM = "item";
         public final static String SOURCE_TYPE = "source_type";
         public final static String SOURCE_SUBTYPE = "source_subtype";
         public final static String ACTION_POSITION = "action_position";
         public final static String COUNT = "count";
+        public final static String PAGE_NUMBER = "page_number";
 
         // Values
         public final static String TYPE_TOPSITES = "topsites";
         public final static String TYPE_HIGHLIGHTS = "highlights";
         public final static String SUBTYPE_PINNED = "pinned";
         public final static String SUBTYPE_SUGGESTED = "suggested";
         public final static String SUBTYPE_TOP = "top";
         public final static String SUBTYPE_VISITED = "visited";
@@ -139,32 +141,38 @@ public class ActivityStreamTelemetry {
                         this.set(Contract.SOURCE_SUBTYPE, Contract.SUBTYPE_BOOKMARKED);
                         break;
                     default:
                         throw new IllegalStateException("Unknown highlight source: " + source);
                 }
                 return this;
             }
 
-            public Builder forTopSiteType(@BrowserContract.TopSites.TopSiteType int type) {
-                switch (type) {
+            public Builder forTopSite(final TopSite topSite) {
+                this.set(
+                        ActivityStreamTelemetry.Contract.SOURCE_TYPE,
+                        ActivityStreamTelemetry.Contract.TYPE_TOPSITES
+                );
+
+                switch (topSite.getType()) {
                     case BrowserContract.TopSites.TYPE_PINNED:
                         this.set(Contract.SOURCE_SUBTYPE, Contract.SUBTYPE_PINNED);
                         break;
                     case BrowserContract.TopSites.TYPE_SUGGESTED:
                         this.set(Contract.SOURCE_SUBTYPE, Contract.SUBTYPE_SUGGESTED);
                         break;
                     case BrowserContract.TopSites.TYPE_TOP:
                         this.set(Contract.SOURCE_SUBTYPE, Contract.SUBTYPE_TOP);
                         break;
                     // While we also have a "blank" type, it is not used by Activity Stream.
                     case BrowserContract.TopSites.TYPE_BLANK:
                     default:
-                        throw new IllegalStateException("Unknown top site type: " + type);
+                        throw new IllegalStateException("Unknown top site type: " + topSite.getType());
                 }
+
                 return this;
             }
 
             public String build() {
                 return data.toString();
             }
         }
     }
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesCard.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesCard.java
@@ -1,77 +1,95 @@
 /* -*- 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.home.activitystream.topsites;
 
 import android.graphics.Color;
-import android.support.annotation.Nullable;
 import android.support.v4.widget.TextViewCompat;
 import android.support.v7.widget.RecyclerView;
 import android.view.View;
 import android.widget.FrameLayout;
 import android.widget.ImageView;
 import android.widget.TextView;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.activitystream.ActivityStream;
 import org.mozilla.gecko.activitystream.ActivityStreamTelemetry;
-import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.home.HomePager;
 import org.mozilla.gecko.home.activitystream.menu.ActivityStreamContextMenu;
 import org.mozilla.gecko.home.activitystream.model.TopSite;
 import org.mozilla.gecko.icons.IconCallback;
 import org.mozilla.gecko.icons.IconResponse;
 import org.mozilla.gecko.icons.Icons;
 import org.mozilla.gecko.util.DrawableUtil;
 import org.mozilla.gecko.util.ViewUtil;
 import org.mozilla.gecko.util.TouchTargetUtil;
 import org.mozilla.gecko.widget.FaviconView;
 
-import java.util.EnumSet;
 import java.util.concurrent.Future;
 
-class TopSitesCard extends RecyclerView.ViewHolder
-        implements IconCallback, View.OnClickListener {
+/* package-local */ class TopSitesCard extends RecyclerView.ViewHolder
+        implements IconCallback {
     private final FaviconView faviconView;
 
     private final TextView title;
     private final ImageView menuButton;
     private Future<IconResponse> ongoingIconLoad;
 
     private TopSite topSite;
+    private int absolutePosition;
 
     private final HomePager.OnUrlOpenListener onUrlOpenListener;
     private final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener;
 
-    public TopSitesCard(FrameLayout card, final HomePager.OnUrlOpenListener onUrlOpenListener, final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
+    /* package-local */ TopSitesCard(FrameLayout card, final HomePager.OnUrlOpenListener onUrlOpenListener, final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
         super(card);
 
         faviconView = (FaviconView) card.findViewById(R.id.favicon);
 
         title = (TextView) card.findViewById(R.id.title);
         menuButton = (ImageView) card.findViewById(R.id.menu);
 
         this.onUrlOpenListener = onUrlOpenListener;
         this.onUrlOpenInBackgroundListener = onUrlOpenInBackgroundListener;
 
-        card.setOnClickListener(this);
+        TouchTargetUtil.ensureTargetHitArea(menuButton, card);
+        menuButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                ActivityStreamTelemetry.Extras.Builder extras = ActivityStreamTelemetry.Extras.builder()
+                        .forTopSite(topSite)
+                        .set(ActivityStreamTelemetry.Contract.ACTION_POSITION, absolutePosition);
 
-        TouchTargetUtil.ensureTargetHitArea(menuButton, card);
-        menuButton.setOnClickListener(this);
+                ActivityStreamContextMenu.show(v.getContext(),
+                        menuButton,
+                        extras,
+                        ActivityStreamContextMenu.MenuMode.TOPSITE,
+                        topSite,
+                        onUrlOpenListener, onUrlOpenInBackgroundListener,
+                        faviconView.getWidth(), faviconView.getHeight());
+
+                Telemetry.sendUIEvent(
+                        TelemetryContract.Event.SHOW,
+                        TelemetryContract.Method.CONTEXT_MENU,
+                        extras.build()
+                );
+            }
+        });
 
         ViewUtil.enableTouchRipple(menuButton);
     }
 
-    void bind(final TopSite topSite) {
+    void bind(final TopSite topSite, final int absolutePosition) {
         this.topSite = topSite;
+        this.absolutePosition = absolutePosition;
 
         ActivityStream.extractLabel(itemView.getContext(), topSite.getUrl(), true, new ActivityStream.LabelCallback() {
             @Override
             public void onLabelExtracted(String label) {
                 title.setText(label);
             }
         });
 
@@ -93,40 +111,9 @@ class TopSitesCard extends RecyclerView.
     public void onIconResponse(IconResponse response) {
         faviconView.updateImage(response);
 
         final int tintColor = !response.hasColor() || response.getColor() == Color.WHITE ? Color.LTGRAY : Color.WHITE;
 
         menuButton.setImageDrawable(
                 DrawableUtil.tintDrawable(menuButton.getContext(), R.drawable.menu, tintColor));
     }
-
-    @Override
-    public void onClick(View clickedView) {
-        ActivityStreamTelemetry.Extras.Builder extras = ActivityStreamTelemetry.Extras.builder()
-                .set(ActivityStreamTelemetry.Contract.SOURCE_TYPE, ActivityStreamTelemetry.Contract.TYPE_TOPSITES)
-                .forTopSiteType(topSite.getType());
-
-        if (clickedView == itemView) {
-            onUrlOpenListener.onUrlOpen(topSite.getUrl(), EnumSet.noneOf(HomePager.OnUrlOpenListener.Flags.class));
-
-            Telemetry.sendUIEvent(
-                    TelemetryContract.Event.LOAD_URL,
-                    TelemetryContract.Method.LIST_ITEM,
-                    extras.build()
-            );
-        } else if (clickedView == menuButton) {
-            ActivityStreamContextMenu.show(clickedView.getContext(),
-                    menuButton,
-                    extras,
-                    ActivityStreamContextMenu.MenuMode.TOPSITE,
-                    topSite,
-                    onUrlOpenListener, onUrlOpenInBackgroundListener,
-                    faviconView.getWidth(), faviconView.getHeight());
-
-            Telemetry.sendUIEvent(
-                    TelemetryContract.Event.SHOW,
-                    TelemetryContract.Method.CONTEXT_MENU,
-                    extras.build()
-            );
-        }
-    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPageAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPageAdapter.java
@@ -1,47 +1,52 @@
 /* -*- 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.home.activitystream.topsites;
 
 import android.content.Context;
 import android.database.Cursor;
-import android.support.annotation.Nullable;
 import android.support.annotation.UiThread;
 import android.support.v7.widget.RecyclerView;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.FrameLayout;
 
 import org.mozilla.gecko.R;
-import org.mozilla.gecko.db.BrowserContract;
+import org.mozilla.gecko.Telemetry;
+import org.mozilla.gecko.TelemetryContract;
+import org.mozilla.gecko.activitystream.ActivityStreamTelemetry;
 import org.mozilla.gecko.home.HomePager;
 import org.mozilla.gecko.home.activitystream.model.TopSite;
+import org.mozilla.gecko.widget.RecyclerViewClickSupport;
 
 import java.util.ArrayList;
+import java.util.EnumSet;
 import java.util.List;
 
-public class TopSitesPageAdapter extends RecyclerView.Adapter<TopSitesCard> {
+/* package-local */ class TopSitesPageAdapter extends RecyclerView.Adapter<TopSitesCard> implements RecyclerViewClickSupport.OnItemClickListener {
     private List<TopSite> topSites;
+    private final int pageNumber;
     private int tiles;
     private int tilesWidth;
     private int tilesHeight;
     private int textHeight;
 
     private final HomePager.OnUrlOpenListener onUrlOpenListener;
     private final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener;
 
-    public TopSitesPageAdapter(Context context, int tiles, int tilesWidth, int tilesHeight,
+    /* package-local */ TopSitesPageAdapter(Context context, int pageNumber, int tiles, int tilesWidth, int tilesHeight,
                                HomePager.OnUrlOpenListener onUrlOpenListener, HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
         setHasStableIds(true);
 
         this.topSites = new ArrayList<>();
+        this.pageNumber = pageNumber;
         this.tiles = tiles;
         this.tilesWidth = tilesWidth;
         this.tilesHeight = tilesHeight;
         this.textHeight = context.getResources().getDimensionPixelSize(R.dimen.activity_stream_top_sites_text_height);
 
         this.onUrlOpenListener = onUrlOpenListener;
         this.onUrlOpenInBackgroundListener = onUrlOpenInBackgroundListener;
     }
@@ -62,18 +67,37 @@ public class TopSitesPageAdapter extends
 
             topSites.add(TopSite.fromCursor(cursor));
         }
 
         notifyDataSetChanged();
     }
 
     @Override
+    public void onItemClicked(RecyclerView recyclerView, int position, View v) {
+        final TopSite topSite = topSites.get(position);
+        final int absolutePosition = getTopSiteAbsolutePosition(position);
+
+        ActivityStreamTelemetry.Extras.Builder extras = ActivityStreamTelemetry.Extras.builder()
+                .forTopSite(topSite)
+                .set(ActivityStreamTelemetry.Contract.PAGE_NUMBER, pageNumber)
+                .set(ActivityStreamTelemetry.Contract.ACTION_POSITION, absolutePosition);
+
+        Telemetry.sendUIEvent(
+                TelemetryContract.Event.LOAD_URL,
+                TelemetryContract.Method.LIST_ITEM,
+                extras.build()
+        );
+
+        onUrlOpenListener.onUrlOpen(topSite.getUrl(), EnumSet.noneOf(HomePager.OnUrlOpenListener.Flags.class));
+    }
+
+    @Override
     public void onBindViewHolder(TopSitesCard holder, int position) {
-        holder.bind(topSites.get(position));
+        holder.bind(topSites.get(position), getTopSiteAbsolutePosition(position));
     }
 
     @Override
     public TopSitesCard onCreateViewHolder(ViewGroup parent, int viewType) {
         final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
 
         final FrameLayout card = (FrameLayout) inflater.inflate(R.layout.activity_stream_topsites_card, parent, false);
         final View content = card.findViewById(R.id.content);
@@ -91,9 +115,20 @@ public class TopSitesPageAdapter extends
         return topSites.size();
     }
 
     @Override
     @UiThread
     public long getItemId(int position) {
         return topSites.get(position).getId();
     }
+
+    /**
+     * This assumes that every TopSite page up to the current one has the same number of tiles.
+     * relativePosition must range from 0 to {number of tiles on the current page}.
+     */
+    private int getTopSiteAbsolutePosition(int relativePosition) {
+        if (relativePosition < 0 || relativePosition > tiles) {
+            throw new IllegalArgumentException("Illegal relative top site position encountered");
+        }
+        return relativePosition + pageNumber * tiles;
+    }
 }
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPagerAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/topsites/TopSitesPagerAdapter.java
@@ -8,16 +8,17 @@ import android.content.Context;
 import android.database.Cursor;
 import android.support.v4.view.PagerAdapter;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.home.HomePager;
+import org.mozilla.gecko.widget.RecyclerViewClickSupport;
 
 import java.util.LinkedList;
 
 /**
  * The primary / top-level TopSites adapter: it handles the ViewPager, and also handles
  * all lower-level Adapters that populate the individual topsite items.
  */
 public class TopSitesPagerAdapter extends PagerAdapter {
@@ -91,19 +92,21 @@ public class TopSitesPagerAdapter extend
         final int pageDelta = count;
 
         if (pageDelta > 0) {
             final LayoutInflater inflater = LayoutInflater.from(context);
             for (int i = 0; i < pageDelta; i++) {
                 final TopSitesPage page = (TopSitesPage) inflater.inflate(R.layout.activity_stream_topsites_page, null, false);
 
                 page.setTiles(tiles);
-                final TopSitesPageAdapter adapter = new TopSitesPageAdapter(context, tiles, tilesWidth, tilesHeight,
+                final TopSitesPageAdapter adapter = new TopSitesPageAdapter(
+                        context, i, tiles, tilesWidth, tilesHeight,
                         onUrlOpenListener, onUrlOpenInBackgroundListener);
                 page.setAdapter(adapter);
+                RecyclerViewClickSupport.addTo(page).setOnItemClickListener(adapter);
                 pages.add(page);
             }
         } else if (pageDelta < 0) {
             for (int i = 0; i > pageDelta; i--) {
                 final TopSitesPage page = pages.getLast();
 
                 // Ensure the page doesn't use the old/invalid cursor anymore
                 page.getAdapter().swapCursor(null, 0);