Bug 1319245 - Track rich telemetry data for Activity Stream r=sebastian

General concept is to populate the "extras" field with a stringified JSON object
which contains bunch of additional data which, in aggregate, might give insight
into user's actions.

A builder is used in order to make constructing the extras json string easier.
This builder has a concept of a "global state" which lets us easily include some information
with every A-S telemetry ping.
Currently this is used to track whether or not user has a firefox account.

An instance of a builder is passed around, augmented as more information becomes known,
and is materialized into an "extras" string whenever an action occurs and telemetry
event needs to be sent.

MozReview-Commit-ID: GDmxkWChnnA

--HG--
extra : rebase_source : 025d198e16d3a8af8b6e94bd531e916b80f9841a
This commit is contained in:
Grisha Kruglov 2016-12-21 22:51:27 -08:00
Родитель 8b13894203
Коммит 2f9475c465
12 изменённых файлов: 345 добавлений и 68 удалений

Просмотреть файл

@ -8,10 +8,14 @@ package org.mozilla.gecko.activitystream;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.support.annotation.NonNull;
import android.text.TextUtils; import android.text.TextUtils;
import com.keepsafe.switchboard.SwitchBoard; import com.keepsafe.switchboard.SwitchBoard;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.AppConstants; import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.Experiments; import org.mozilla.gecko.Experiments;
import org.mozilla.gecko.GeckoSharedPrefs; import org.mozilla.gecko.GeckoSharedPrefs;
@ -20,6 +24,7 @@ import org.mozilla.gecko.util.StringUtils;
import org.mozilla.gecko.util.publicsuffix.PublicSuffix; import org.mozilla.gecko.util.publicsuffix.PublicSuffix;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.List; import java.util.List;
public class ActivityStream { public class ActivityStream {

Просмотреть файл

@ -0,0 +1,171 @@
/* -*- 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.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 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";
// 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";
public final static String SUBTYPE_BOOKMARKED = "bookmarked";
public final static String ITEM_SHARE = "share";
public final static String ITEM_ADD_BOOKMARK = "add_bookmark";
public final static String ITEM_REMOVE_BOOKMARK = "remove_bookmark";
public final static String ITEM_PIN = "pin";
public final static String ITEM_UNPIN = "unpin";
public final static String ITEM_COPY = "copy";
public final static String ITEM_ADD_TO_HOMESCREEN = "homescreen";
public final static String ITEM_NEW_TAB = "newtab";
public final static String ITEM_PRIVATE_TAB = "privatetab";
public final static String ITEM_DISMISS = "dismiss";
public final static String ITEM_DELETE_HISTORY = "delete";
}
/**
* A helper class used for composing an 'extras' field. It encapsulates a holder of "global"
* key/value pairs which will be present in every 'extras' constructed by this class, and a
* static builder which is aware of Activity Stream telemetry needs.
*/
public final static class Extras {
private static final HashMap<String, Object> globals = new HashMap<>();
public static void setGlobal(String key, Object value) {
globals.put(key, value);
}
public static Builder builder() {
return new Builder();
}
/**
* Allows composing a JSON extras blob, which is then "built" into a string representation.
*/
public final static class Builder {
private final JSONObject data;
private Builder() {
data = new JSONObject(globals);
}
/**
* @param value a {@link JSONObject}, {@link JSONArray}, String, Boolean,
* Integer, Long, Double, {@link JSONObject#NULL}, or {@code null}. May not be
* {@link Double#isNaN() NaNs} or {@link Double#isInfinite()
* infinities}.
* @return this object.
*/
public Builder set(@NonNull String key, Object value) {
try {
data.put(key, value);
} catch (JSONException e) {
throw new IllegalArgumentException("Key can't be null");
}
return this;
}
/**
* Sets extras values describing a context menu interaction based on the menu item ID.
*
* @param itemId ID of a menu item, which is transformed into a corresponding item
* key/value pair and passed off to {@link this#set(String, Object)}.
* @return this object.
*/
public Builder fromMenuItemId(int itemId) {
switch (itemId) {
case R.id.share:
this.set(Contract.ITEM, Contract.ITEM_SHARE);
break;
case R.id.copy_url:
this.set(Contract.ITEM, Contract.ITEM_COPY);
break;
case R.id.add_homescreen:
this.set(Contract.ITEM, Contract.ITEM_ADD_TO_HOMESCREEN);
break;
case R.id.open_new_tab:
this.set(Contract.ITEM, Contract.ITEM_NEW_TAB);
break;
case R.id.open_new_private_tab:
this.set(Contract.ITEM, Contract.ITEM_PRIVATE_TAB);
break;
case R.id.dismiss:
this.set(Contract.ITEM, Contract.ITEM_DISMISS);
break;
case R.id.delete:
this.set(Contract.ITEM, Contract.ITEM_DELETE_HISTORY);
break;
}
return this;
}
public Builder forHighlightSource(Utils.HighlightSource source) {
switch (source) {
case VISITED:
this.set(Contract.SOURCE_SUBTYPE, Contract.SUBTYPE_VISITED);
break;
case BOOKMARKED:
this.set(Contract.SOURCE_SUBTYPE, Contract.SUBTYPE_BOOKMARKED);
break;
default:
throw new IllegalStateException("Unknown highlight source: " + source);
}
return this;
}
public Builder forTopSiteType(int type) {
switch (type) {
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.
default:
throw new IllegalStateException("Unknown top site type: " + type);
}
return this;
}
public String build() {
return data.toString();
}
}
}
}

Просмотреть файл

@ -0,0 +1,32 @@
/* -*- 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.activitystream;
import android.database.Cursor;
import org.mozilla.gecko.db.BrowserContract;
/**
* Various util methods and constants that are shared by different parts of Activity Stream.
*/
public class Utils {
public enum HighlightSource {
VISITED,
BOOKMARKED
}
public static HighlightSource highlightSource(final Cursor cursor) {
if (-1 != cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Combined.BOOKMARK_ID))) {
return HighlightSource.BOOKMARKED;
}
if (-1 != cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID))) {
return HighlightSource.VISITED;
}
throw new IllegalArgumentException("Unknown highlight source.");
}
}

Просмотреть файл

@ -11,20 +11,17 @@ import android.os.Bundle;
import android.support.v4.app.LoaderManager; import android.support.v4.app.LoaderManager;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.support.v4.content.Loader; import android.support.v4.content.Loader;
import android.support.v4.graphics.ColorUtils;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.TypedValue;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import org.mozilla.gecko.R; import org.mozilla.gecko.R;
import org.mozilla.gecko.activitystream.ActivityStreamTelemetry;
import org.mozilla.gecko.db.BrowserDB; import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.home.HomePager; import org.mozilla.gecko.home.HomePager;
import org.mozilla.gecko.home.activitystream.topsites.TopSitesPagerAdapter; import org.mozilla.gecko.home.activitystream.topsites.TopSitesPagerAdapter;
import org.mozilla.gecko.util.ContextUtils;
import org.mozilla.gecko.widget.RecyclerViewClickSupport; import org.mozilla.gecko.widget.RecyclerViewClickSupport;
public class ActivityStream extends FrameLayout { public class ActivityStream extends FrameLayout {
@ -62,6 +59,11 @@ public class ActivityStream extends FrameLayout {
desiredTileWidth = resources.getDimensionPixelSize(R.dimen.activity_stream_desired_tile_width); desiredTileWidth = resources.getDimensionPixelSize(R.dimen.activity_stream_desired_tile_width);
desiredTilesHeight = resources.getDimensionPixelSize(R.dimen.activity_stream_desired_tile_height); desiredTilesHeight = resources.getDimensionPixelSize(R.dimen.activity_stream_desired_tile_height);
tileMargin = resources.getDimensionPixelSize(R.dimen.activity_stream_base_margin); tileMargin = resources.getDimensionPixelSize(R.dimen.activity_stream_base_margin);
ActivityStreamTelemetry.Extras.setGlobal(
ActivityStreamTelemetry.Contract.FX_ACCOUNT_PRESENT,
FirefoxAccounts.firefoxAccountsExist(context)
);
} }
void setOnUrlOpenListeners(HomePager.OnUrlOpenListener onUrlOpenListener, HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) { void setOnUrlOpenListeners(HomePager.OnUrlOpenListener onUrlOpenListener, HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {

Просмотреть файл

@ -23,7 +23,11 @@ import android.widget.TextView;
import org.mozilla.gecko.GeckoSharedPrefs; import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.R; import org.mozilla.gecko.R;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.activitystream.Utils;
import org.mozilla.gecko.activitystream.ActivityStream.LabelCallback; import org.mozilla.gecko.activitystream.ActivityStream.LabelCallback;
import org.mozilla.gecko.activitystream.ActivityStreamTelemetry;
import org.mozilla.gecko.db.BrowserContract; import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.home.HomePager; import org.mozilla.gecko.home.HomePager;
import org.mozilla.gecko.home.activitystream.menu.ActivityStreamContextMenu; import org.mozilla.gecko.home.activitystream.menu.ActivityStreamContextMenu;
@ -135,17 +139,14 @@ public abstract class StreamItem extends RecyclerView.ViewHolder {
public static class HighlightItem extends StreamItem implements IconCallback { public static class HighlightItem extends StreamItem implements IconCallback {
public static final int LAYOUT_ID = R.layout.activity_stream_card_history_item; public static final int LAYOUT_ID = R.layout.activity_stream_card_history_item;
enum HighlightSource {
VISITED,
BOOKMARKED
}
String title; String title;
String url; String url;
@Nullable Boolean isPinned; @Nullable Boolean isPinned;
@Nullable Boolean isBookmarked; @Nullable Boolean isBookmarked;
Utils.HighlightSource source;
final FaviconView vIconView; final FaviconView vIconView;
final TextView vLabel; final TextView vLabel;
final TextView vTimeSince; final TextView vTimeSince;
@ -180,12 +181,23 @@ public abstract class StreamItem extends RecyclerView.ViewHolder {
menuButton.setOnClickListener(new View.OnClickListener() { menuButton.setOnClickListener(new View.OnClickListener() {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
ActivityStreamTelemetry.Extras.Builder extras = ActivityStreamTelemetry.Extras.builder()
.set(ActivityStreamTelemetry.Contract.SOURCE_TYPE, ActivityStreamTelemetry.Contract.TYPE_HIGHLIGHTS)
.forHighlightSource(source);
ActivityStreamContextMenu.show(v.getContext(), ActivityStreamContextMenu.show(v.getContext(),
menuButton, menuButton,
extras,
ActivityStreamContextMenu.MenuMode.HIGHLIGHT, ActivityStreamContextMenu.MenuMode.HIGHLIGHT,
title, url, isBookmarked, isPinned, title, url, isBookmarked, isPinned,
onUrlOpenListener, onUrlOpenInBackgroundListener, onUrlOpenListener, onUrlOpenInBackgroundListener,
vIconView.getWidth(), vIconView.getHeight()); vIconView.getWidth(), vIconView.getHeight());
Telemetry.sendUIEvent(
TelemetryContract.Event.SHOW,
TelemetryContract.Method.CONTEXT_MENU,
extras.build()
);
} }
}); });
@ -193,12 +205,12 @@ public abstract class StreamItem extends RecyclerView.ViewHolder {
} }
public void bind(Cursor cursor, int tilesWidth, int tilesHeight) { public void bind(Cursor cursor, int tilesWidth, int tilesHeight) {
final long time = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Highlights.DATE)); final long time = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Highlights.DATE));
final String ago = DateUtils.getRelativeTimeSpanString(time, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, 0).toString(); final String ago = DateUtils.getRelativeTimeSpanString(time, System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, 0).toString();
title = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.History.TITLE)); title = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.History.TITLE));
url = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL)); url = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL));
source = Utils.highlightSource(cursor);
vLabel.setText(title); vLabel.setText(title);
vTimeSince.setText(ago); vTimeSince.setText(ago);
@ -208,9 +220,7 @@ public abstract class StreamItem extends RecyclerView.ViewHolder {
layoutParams.height = tilesHeight; layoutParams.height = tilesHeight;
vIconView.setLayoutParams(layoutParams); vIconView.setLayoutParams(layoutParams);
final HighlightSource source = highlightSource(cursor); updateStateForSource(source);
updateStateForSource(source, cursor);
updateUiForSource(source); updateUiForSource(source);
updatePage(url); updatePage(url);
@ -225,7 +235,7 @@ public abstract class StreamItem extends RecyclerView.ViewHolder {
.execute(this); .execute(this);
} }
private void updateStateForSource(HighlightSource source, Cursor cursor) { private void updateStateForSource(Utils.HighlightSource source) {
// We can only be certain of bookmark state if an item is a bookmark item. // We can only be certain of bookmark state if an item is a bookmark item.
// Otherwise, due to the underlying highlights query, we have to look up states when // Otherwise, due to the underlying highlights query, we have to look up states when
// menus are displayed. // menus are displayed.
@ -243,7 +253,7 @@ public abstract class StreamItem extends RecyclerView.ViewHolder {
} }
} }
private void updateUiForSource(HighlightSource source) { private void updateUiForSource(Utils.HighlightSource source) {
switch (source) { switch (source) {
case BOOKMARKED: case BOOKMARKED:
vSourceView.setText(R.string.activity_stream_highlight_label_bookmarked); vSourceView.setText(R.string.activity_stream_highlight_label_bookmarked);
@ -279,16 +289,4 @@ public abstract class StreamItem extends RecyclerView.ViewHolder {
vIconView.updateImage(response); vIconView.updateImage(response);
} }
} }
private static HighlightItem.HighlightSource highlightSource(final Cursor cursor) {
if (-1 != cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Combined.BOOKMARK_ID))) {
return HighlightItem.HighlightSource.BOOKMARKED;
}
if (-1 != cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID))) {
return HighlightItem.HighlightSource.VISITED;
}
throw new IllegalArgumentException("Unknown highlight source.");
}
} }

Просмотреть файл

@ -13,6 +13,8 @@ import android.view.ViewGroup;
import org.mozilla.gecko.Telemetry; import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract; import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.activitystream.ActivityStreamTelemetry;
import org.mozilla.gecko.activitystream.Utils;
import org.mozilla.gecko.db.BrowserContract; import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.home.HomePager; import org.mozilla.gecko.home.HomePager;
import org.mozilla.gecko.home.activitystream.StreamItem.HighlightItem; import org.mozilla.gecko.home.activitystream.StreamItem.HighlightItem;
@ -113,15 +115,25 @@ public class StreamRecyclerAdapter extends RecyclerView.Adapter<StreamItem> impl
return; return;
} }
highlightsCursor.moveToPosition( int actualPosition = translatePositionToCursor(position);
translatePositionToCursor(position)); highlightsCursor.moveToPosition(actualPosition);
final String url = highlightsCursor.getString( final String url = highlightsCursor.getString(
highlightsCursor.getColumnIndexOrThrow(BrowserContract.Combined.URL)); highlightsCursor.getColumnIndexOrThrow(BrowserContract.Combined.URL));
onUrlOpenListener.onUrlOpen(url, EnumSet.of(HomePager.OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB)); onUrlOpenListener.onUrlOpen(url, EnumSet.of(HomePager.OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, "as_highlights"); ActivityStreamTelemetry.Extras.Builder extras = ActivityStreamTelemetry.Extras.builder()
.forHighlightSource(Utils.highlightSource(highlightsCursor))
.set(ActivityStreamTelemetry.Contract.SOURCE_TYPE, ActivityStreamTelemetry.Contract.TYPE_HIGHLIGHTS)
.set(ActivityStreamTelemetry.Contract.ACTION_POSITION, actualPosition)
.set(ActivityStreamTelemetry.Contract.COUNT, highlightsCursor.getCount());
Telemetry.sendUIEvent(
TelemetryContract.Event.LOAD_URL,
TelemetryContract.Method.LIST_ITEM,
extras.build()
);
} }
@Override @Override

Просмотреть файл

@ -18,6 +18,7 @@ import org.mozilla.gecko.IntentHelper;
import org.mozilla.gecko.R; import org.mozilla.gecko.R;
import org.mozilla.gecko.Telemetry; import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract; import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.activitystream.ActivityStreamTelemetry;
import org.mozilla.gecko.annotation.RobocopTarget; import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.db.BrowserDB; import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.home.HomePager; import org.mozilla.gecko.home.HomePager;
@ -43,6 +44,8 @@ public abstract class ActivityStreamContextMenu
final String title; final String title;
final String url; final String url;
private final ActivityStreamTelemetry.Extras.Builder telemetryExtraBuilder;
// We might not know bookmarked/pinned states, so we allow for null values. // We might not know bookmarked/pinned states, so we allow for null values.
private @Nullable Boolean isBookmarked; private @Nullable Boolean isBookmarked;
private @Nullable Boolean isPinned; private @Nullable Boolean isPinned;
@ -60,12 +63,14 @@ public abstract class ActivityStreamContextMenu
final MenuMode mode; final MenuMode mode;
/* package-private */ ActivityStreamContextMenu(final Context context, /* package-private */ ActivityStreamContextMenu(final Context context,
final ActivityStreamTelemetry.Extras.Builder telemetryExtraBuilder,
final MenuMode mode, final MenuMode mode,
final String title, @NonNull final String url, final String title, @NonNull final String url,
@Nullable final Boolean isBookmarked, @Nullable final Boolean isPinned, @Nullable final Boolean isBookmarked, @Nullable final Boolean isPinned,
HomePager.OnUrlOpenListener onUrlOpenListener, HomePager.OnUrlOpenListener onUrlOpenListener,
HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) { HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
this.context = context; this.context = context;
this.telemetryExtraBuilder = telemetryExtraBuilder;
this.mode = mode; this.mode = mode;
@ -156,13 +161,12 @@ public abstract class ActivityStreamContextMenu
@Override @Override
protected Void doInBackground() { protected Void doInBackground() {
final Cursor cursor = BrowserDB.from(context).getHistoryForURL(context.getContentResolver(), url); final Cursor cursor = BrowserDB.from(context).getHistoryForURL(context.getContentResolver(), url);
try { if (cursor == null) {
if (cursor != null &&
cursor.getCount() == 1) {
hasHistory = true;
} else {
hasHistory = false; hasHistory = false;
return null;
} }
try {
hasHistory = cursor.getCount() == 1;
} finally { } finally {
cursor.close(); cursor.close();
} }
@ -181,27 +185,32 @@ public abstract class ActivityStreamContextMenu
@Override @Override
public boolean onNavigationItemSelected(MenuItem item) { public boolean onNavigationItemSelected(MenuItem item) {
final int menuItemId = item.getItemId();
// Sets extra telemetry which doesn't require additional state information.
// Pin and bookmark items are handled separately below, since they do require state
// information to handle correctly.
telemetryExtraBuilder.fromMenuItemId(menuItemId);
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.share: case R.id.share:
// NB: Generic menu item action event will be sent at the end of this function.
// We have a seemingly duplicate telemetry event here because we want to emit
// a concrete event in case it is used by other queries to estimate feature usage.
Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST, "as_contextmenu"); Telemetry.sendUIEvent(TelemetryContract.Event.SHARE, TelemetryContract.Method.LIST, "as_contextmenu");
IntentHelper.openUriExternal(url, "text/plain", "", "", Intent.ACTION_SEND, title, false); IntentHelper.openUriExternal(url, "text/plain", "", "", Intent.ACTION_SEND, title, false);
break; break;
case R.id.bookmark: case R.id.bookmark:
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
final BrowserDB db = BrowserDB.from(context);
final TelemetryContract.Event telemetryEvent; final TelemetryContract.Event telemetryEvent;
final String telemetryExtra; final String telemetryExtra;
if (isBookmarked) {
db.removeBookmarksWithURL(context.getContentResolver(), url);
SavedReaderViewHelper rch = SavedReaderViewHelper.getSavedReaderViewHelper(context); SavedReaderViewHelper rch = SavedReaderViewHelper.getSavedReaderViewHelper(context);
final boolean isReaderViewPage = rch.isURLCached(url); final boolean isReaderViewPage = rch.isURLCached(url);
// While isBookmarked is nullable, behaviour of postInit - disabling 'bookmark' item
// until we know value of isBookmarked - guarantees that it will be set when we get here.
if (isBookmarked) {
telemetryEvent = TelemetryContract.Event.UNSAVE; telemetryEvent = TelemetryContract.Event.UNSAVE;
if (isReaderViewPage) { if (isReaderViewPage) {
@ -209,21 +218,43 @@ public abstract class ActivityStreamContextMenu
} else { } else {
telemetryExtra = "as_bookmark"; telemetryExtra = "as_bookmark";
} }
telemetryExtraBuilder.set(ActivityStreamTelemetry.Contract.ITEM, ActivityStreamTelemetry.Contract.ITEM_REMOVE_BOOKMARK);
} else {
telemetryEvent = TelemetryContract.Event.SAVE;
telemetryExtra = "as_bookmark";
telemetryExtraBuilder.set(ActivityStreamTelemetry.Contract.ITEM, ActivityStreamTelemetry.Contract.ITEM_ADD_BOOKMARK);
}
// NB: Generic menu item action event will be sent at the end of this function.
// We have a seemingly duplicate telemetry event here because we want to emit
// a concrete event in case it is used by other queries to estimate feature usage.
Telemetry.sendUIEvent(telemetryEvent, TelemetryContract.Method.CONTEXT_MENU, telemetryExtra);
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
final BrowserDB db = BrowserDB.from(context);
if (isBookmarked) {
db.removeBookmarksWithURL(context.getContentResolver(), url);
} else { } else {
// We only store raw URLs in history (and bookmarks), hence we won't ever show about:reader // We only store raw URLs in history (and bookmarks), hence we won't ever show about:reader
// URLs in AS topsites or highlights. Therefore we don't need to do any special about:reader handling here. // URLs in AS topsites or highlights. Therefore we don't need to do any special about:reader handling here.
db.addBookmark(context.getContentResolver(), title, url); db.addBookmark(context.getContentResolver(), title, url);
telemetryEvent = TelemetryContract.Event.SAVE;
telemetryExtra = "as_bookmark";
} }
Telemetry.sendUIEvent(telemetryEvent, TelemetryContract.Method.CONTEXT_MENU, telemetryExtra);
} }
}); });
break; break;
case R.id.pin: case R.id.pin:
// While isPinned is nullable, behaviour of postInit - disabling 'pin' item
// until we know value of isPinned - guarantees that it will be set when we get here.
if (isPinned) {
telemetryExtraBuilder.set(ActivityStreamTelemetry.Contract.ITEM, ActivityStreamTelemetry.Contract.ITEM_UNPIN);
} else {
telemetryExtraBuilder.set(ActivityStreamTelemetry.Contract.ITEM, ActivityStreamTelemetry.Contract.ITEM_PIN);
}
ThreadUtils.postToBackgroundThread(new Runnable() { ThreadUtils.postToBackgroundThread(new Runnable() {
@Override @Override
public void run() { public void run() {
@ -236,6 +267,7 @@ public abstract class ActivityStreamContextMenu
} }
} }
}); });
break;
case R.id.copy_url: case R.id.copy_url:
Clipboard.setText(url); Clipboard.setText(url);
@ -243,20 +275,14 @@ public abstract class ActivityStreamContextMenu
case R.id.add_homescreen: case R.id.add_homescreen:
GeckoAppShell.createShortcut(title, url); GeckoAppShell.createShortcut(title, url);
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "as_add_to_launcher");
break; break;
case R.id.open_new_tab: case R.id.open_new_tab:
onUrlOpenInBackgroundListener.onUrlOpenInBackground(url, EnumSet.noneOf(HomePager.OnUrlOpenInBackgroundListener.Flags.class)); onUrlOpenInBackgroundListener.onUrlOpenInBackground(url, EnumSet.noneOf(HomePager.OnUrlOpenInBackgroundListener.Flags.class));
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.CONTEXT_MENU, "as_new_tab");
break; break;
case R.id.open_new_private_tab: case R.id.open_new_private_tab:
onUrlOpenInBackgroundListener.onUrlOpenInBackground(url, EnumSet.of(HomePager.OnUrlOpenInBackgroundListener.Flags.PRIVATE)); onUrlOpenInBackgroundListener.onUrlOpenInBackground(url, EnumSet.of(HomePager.OnUrlOpenInBackgroundListener.Flags.PRIVATE));
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.CONTEXT_MENU, "as_private_tab");
break; break;
case R.id.dismiss: case R.id.dismiss:
@ -285,6 +311,12 @@ public abstract class ActivityStreamContextMenu
throw new IllegalArgumentException("Menu item with ID=" + item.getItemId() + " not handled"); throw new IllegalArgumentException("Menu item with ID=" + item.getItemId() + " not handled");
} }
Telemetry.sendUIEvent(
TelemetryContract.Event.ACTION,
TelemetryContract.Method.CONTEXT_MENU,
telemetryExtraBuilder.build()
);
dismiss(); dismiss();
return true; return true;
} }
@ -292,7 +324,7 @@ public abstract class ActivityStreamContextMenu
@RobocopTarget @RobocopTarget
public static ActivityStreamContextMenu show(Context context, public static ActivityStreamContextMenu show(Context context,
View anchor, View anchor, ActivityStreamTelemetry.Extras.Builder telemetryExtraBuilder,
final MenuMode menuMode, final MenuMode menuMode,
final String title, @NonNull final String url, final String title, @NonNull final String url,
@Nullable final Boolean isBookmarked, @Nullable final Boolean isPinned, @Nullable final Boolean isBookmarked, @Nullable final Boolean isPinned,
@ -303,14 +335,14 @@ public abstract class ActivityStreamContextMenu
if (!HardwareUtils.isTablet()) { if (!HardwareUtils.isTablet()) {
menu = new BottomSheetContextMenu(context, menu = new BottomSheetContextMenu(context,
menuMode, telemetryExtraBuilder, menuMode,
title, url, isBookmarked, isPinned, title, url, isBookmarked, isPinned,
onUrlOpenListener, onUrlOpenInBackgroundListener, onUrlOpenListener, onUrlOpenInBackgroundListener,
tilesWidth, tilesHeight); tilesWidth, tilesHeight);
} else { } else {
menu = new PopupContextMenu(context, menu = new PopupContextMenu(context,
anchor, anchor,
menuMode, telemetryExtraBuilder, menuMode,
title, url, isBookmarked, isPinned, title, url, isBookmarked, isPinned,
onUrlOpenListener, onUrlOpenInBackgroundListener); onUrlOpenListener, onUrlOpenInBackgroundListener);
} }

Просмотреть файл

@ -7,7 +7,6 @@ package org.mozilla.gecko.home.activitystream.menu;
import android.content.Context; import android.content.Context;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.design.widget.BottomSheetBehavior;
import android.support.design.widget.BottomSheetDialog; import android.support.design.widget.BottomSheetDialog;
import android.support.design.widget.NavigationView; import android.support.design.widget.NavigationView;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -18,6 +17,7 @@ import android.widget.TextView;
import org.mozilla.gecko.R; import org.mozilla.gecko.R;
import org.mozilla.gecko.activitystream.ActivityStream; import org.mozilla.gecko.activitystream.ActivityStream;
import org.mozilla.gecko.activitystream.ActivityStreamTelemetry;
import org.mozilla.gecko.home.HomePager; import org.mozilla.gecko.home.HomePager;
import org.mozilla.gecko.icons.IconCallback; import org.mozilla.gecko.icons.IconCallback;
import org.mozilla.gecko.icons.IconResponse; import org.mozilla.gecko.icons.IconResponse;
@ -35,6 +35,7 @@ import static org.mozilla.gecko.activitystream.ActivityStream.extractLabel;
private final NavigationView navigationView; private final NavigationView navigationView;
public BottomSheetContextMenu(final Context context, public BottomSheetContextMenu(final Context context,
final ActivityStreamTelemetry.Extras.Builder telemetryExtraBuilder,
final MenuMode mode, final MenuMode mode,
final String title, @NonNull final String url, final String title, @NonNull final String url,
@Nullable final Boolean isBookmarked, @Nullable final Boolean isPinned, @Nullable final Boolean isBookmarked, @Nullable final Boolean isPinned,
@ -43,6 +44,7 @@ import static org.mozilla.gecko.activitystream.ActivityStream.extractLabel;
final int tilesWidth, final int tilesHeight) { final int tilesWidth, final int tilesHeight) {
super(context, super(context,
telemetryExtraBuilder,
mode, mode,
title, title,
url, url,

Просмотреть файл

@ -16,6 +16,8 @@ import android.view.View;
import android.widget.PopupWindow; import android.widget.PopupWindow;
import org.mozilla.gecko.R; import org.mozilla.gecko.R;
import org.mozilla.gecko.activitystream.ActivityStream;
import org.mozilla.gecko.activitystream.ActivityStreamTelemetry;
import org.mozilla.gecko.home.HomePager; import org.mozilla.gecko.home.HomePager;
/* package-private */ class PopupContextMenu /* package-private */ class PopupContextMenu
@ -28,6 +30,7 @@ import org.mozilla.gecko.home.HomePager;
public PopupContextMenu(final Context context, public PopupContextMenu(final Context context,
View anchor, View anchor,
final ActivityStreamTelemetry.Extras.Builder telemetryExtraBuilder,
final MenuMode mode, final MenuMode mode,
final String title, final String title,
@NonNull final String url, @NonNull final String url,
@ -36,6 +39,7 @@ import org.mozilla.gecko.home.HomePager;
HomePager.OnUrlOpenListener onUrlOpenListener, HomePager.OnUrlOpenListener onUrlOpenListener,
HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) { HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
super(context, super(context,
telemetryExtraBuilder,
mode, mode,
title, title,
url, url,

Просмотреть файл

@ -17,6 +17,7 @@ import org.mozilla.gecko.R;
import org.mozilla.gecko.Telemetry; import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract; import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.activitystream.ActivityStream; import org.mozilla.gecko.activitystream.ActivityStream;
import org.mozilla.gecko.activitystream.ActivityStreamTelemetry;
import org.mozilla.gecko.db.BrowserContract; import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.home.HomePager; import org.mozilla.gecko.home.HomePager;
import org.mozilla.gecko.home.activitystream.menu.ActivityStreamContextMenu; import org.mozilla.gecko.home.activitystream.menu.ActivityStreamContextMenu;
@ -103,13 +104,22 @@ class TopSitesCard extends RecyclerView.ViewHolder
@Override @Override
public void onClick(View clickedView) { public void onClick(View clickedView) {
ActivityStreamTelemetry.Extras.Builder extras = ActivityStreamTelemetry.Extras.builder()
.set(ActivityStreamTelemetry.Contract.SOURCE_TYPE, ActivityStreamTelemetry.Contract.TYPE_TOPSITES)
.forTopSiteType(type);
if (clickedView == itemView) { if (clickedView == itemView) {
onUrlOpenListener.onUrlOpen(url, EnumSet.noneOf(HomePager.OnUrlOpenListener.Flags.class)); onUrlOpenListener.onUrlOpen(url, EnumSet.noneOf(HomePager.OnUrlOpenListener.Flags.class));
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, "as_top_sites"); Telemetry.sendUIEvent(
TelemetryContract.Event.LOAD_URL,
TelemetryContract.Method.LIST_ITEM,
extras.build()
);
} else if (clickedView == menuButton) { } else if (clickedView == menuButton) {
ActivityStreamContextMenu.show(clickedView.getContext(), ActivityStreamContextMenu.show(clickedView.getContext(),
menuButton, menuButton,
extras,
ActivityStreamContextMenu.MenuMode.TOPSITE, ActivityStreamContextMenu.MenuMode.TOPSITE,
title.getText().toString(), url, title.getText().toString(), url,
@ -118,11 +128,15 @@ class TopSitesCard extends RecyclerView.ViewHolder
onUrlOpenListener, onUrlOpenInBackgroundListener, onUrlOpenListener, onUrlOpenInBackgroundListener,
faviconView.getWidth(), faviconView.getHeight()); faviconView.getWidth(), faviconView.getHeight());
Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.CONTEXT_MENU, "as_top_sites"); Telemetry.sendUIEvent(
TelemetryContract.Event.SHOW,
TelemetryContract.Method.CONTEXT_MENU,
extras.build()
);
} }
} }
private static boolean isPinned(int type) { private boolean isPinned(int type) {
return type == BrowserContract.TopSites.TYPE_PINNED; return type == BrowserContract.TopSites.TYPE_PINNED;
} }
} }

Просмотреть файл

@ -334,6 +334,8 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
'ActionModeCompatView.java', 'ActionModeCompatView.java',
'ActivityHandlerHelper.java', 'ActivityHandlerHelper.java',
'activitystream/ActivityStream.java', 'activitystream/ActivityStream.java',
'activitystream/ActivityStreamTelemetry.java',
'activitystream/Utils.java',
'adjust/AdjustBrowserAppDelegate.java', 'adjust/AdjustBrowserAppDelegate.java',
'animation/AnimationUtils.java', 'animation/AnimationUtils.java',
'animation/HeightChangeAnimation.java', 'animation/HeightChangeAnimation.java',

Просмотреть файл

@ -10,6 +10,8 @@ import android.view.View;
import com.robotium.solo.Condition; import com.robotium.solo.Condition;
import org.mozilla.gecko.R; import org.mozilla.gecko.R;
import org.mozilla.gecko.activitystream.ActivityStream;
import org.mozilla.gecko.activitystream.ActivityStreamTelemetry;
import org.mozilla.gecko.db.BrowserDB; import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.home.activitystream.menu.ActivityStreamContextMenu; import org.mozilla.gecko.home.activitystream.menu.ActivityStreamContextMenu;
@ -61,7 +63,8 @@ public class testActivityStreamContextMenu extends BaseTest {
private void testMenuForUrl(final String url, final Boolean isBookmarkedKnownState, final boolean isBookmarked, final Boolean isPinnedKnownState, final boolean isPinned, final boolean isVisited) { private void testMenuForUrl(final String url, final Boolean isBookmarkedKnownState, final boolean isBookmarked, final Boolean isPinnedKnownState, final boolean isPinned, final boolean isVisited) {
final View anchor = new View(getActivity()); final View anchor = new View(getActivity());
final ActivityStreamContextMenu menu = ActivityStreamContextMenu.show(getActivity(), anchor, ActivityStreamContextMenu.MenuMode.HIGHLIGHT, "foobar", url, isBookmarkedKnownState, isPinnedKnownState, null, null, 100, 100); final ActivityStreamContextMenu menu = ActivityStreamContextMenu.show(
getActivity(), anchor, ActivityStreamTelemetry.Extras.builder(), ActivityStreamContextMenu.MenuMode.HIGHLIGHT, "foobar", url, isBookmarkedKnownState, isPinnedKnownState, null, null, 100, 100);
final int expectedBookmarkString; final int expectedBookmarkString;
if (isBookmarked) { if (isBookmarked) {