--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserContract.java
@@ -40,23 +40,23 @@ public class BrowserContract {
public static final String LOGINS_AUTHORITY = AppConstants.ANDROID_PACKAGE_NAME + ".db.logins";
public static final Uri LOGINS_AUTHORITY_URI = Uri.parse("content://" + LOGINS_AUTHORITY);
public static final String PARAM_PROFILE = "profile";
public static final String PARAM_PROFILE_PATH = "profilePath";
public static final String PARAM_LIMIT = "limit";
public static final String PARAM_SUGGESTEDSITES_LIMIT = "suggestedsites_limit";
- public static final String PARAM_TOPSITES_DISABLE_PINNED = "topsites_disable_pinned";
public static final String PARAM_IS_SYNC = "sync";
public static final String PARAM_SHOW_DELETED = "show_deleted";
public static final String PARAM_IS_TEST = "test";
public static final String PARAM_INSERT_IF_NEEDED = "insert_if_needed";
public static final String PARAM_INCREMENT_VISITS = "increment_visits";
public static final String PARAM_INCREMENT_REMOTE_AGGREGATES = "increment_remote_aggregates";
+ public static final String PARAM_NON_POSITIONED_PINS = "non_positioned_pins";
public static final String PARAM_EXPIRE_PRIORITY = "priority";
public static final String PARAM_DATASET_ID = "dataset_id";
public static final String PARAM_GROUP_BY = "group_by";
static public enum ExpirePriority {
NORMAL,
AGGRESSIVE
}
@@ -237,16 +237,20 @@ public class BrowserContract {
public static final int FIXED_ROOT_ID = 0;
public static final int FAKE_DESKTOP_FOLDER_ID = -1;
public static final int FIXED_READING_LIST_ID = -2;
public static final int FIXED_PINNED_LIST_ID = -3;
public static final int FIXED_SCREENSHOT_FOLDER_ID = -4;
public static final int FAKE_READINGLIST_SMARTFOLDER_ID = -5;
+ // Fixed position used by Activity Stream pins. A-S displays pins in front of other top sites,
+ // so position is constant for all pins.
+ public static final int FIXED_AS_PIN_POSITION = -1;
+
/**
* This ID and the following negative IDs are reserved for bookmarks from Android's partner
* bookmark provider.
*/
public static final long FAKE_PARTNER_BOOKMARKS_START = -1000;
public static final String MOBILE_FOLDER_GUID = "mobile";
public static final String PLACES_FOLDER_GUID = "places";
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserDB.java
@@ -156,19 +156,27 @@ public abstract class BrowserDB {
Collection<ContentProviderOperation> operations, String url,
String title, long date, int visits);
public abstract void updateBookmarkInBatch(ContentResolver cr,
Collection<ContentProviderOperation> operations, String url,
String title, String guid, long parent, long added, long modified,
long position, String keyword, int type);
+ // Used by regular top sites, which observe pinning position.
public abstract void pinSite(ContentResolver cr, String url, String title, int position);
public abstract void unpinSite(ContentResolver cr, int position);
+ // Used by activity stream top sites, which ignore position - it's always 0.
+ // Pins show up in front of other top sites.
+ public abstract void pinSiteForAS(ContentResolver cr, String url, String title);
+ public abstract void unpinSiteForAS(ContentResolver cr, String url);
+
+ public abstract boolean isPinnedForAS(ContentResolver cr, String url);
+
public abstract boolean hideSuggestedSite(String url);
public abstract void setSuggestedSites(SuggestedSites suggestedSites);
public abstract SuggestedSites getSuggestedSites();
public abstract boolean hasSuggestedImageUrl(String url);
public abstract String getSuggestedImageUrlForUrl(String url);
public abstract int getSuggestedBackgroundColorForUrl(String url);
/**
--- a/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/BrowserProvider.java
@@ -891,35 +891,49 @@ public class BrowserProvider extends Sha
// 2. The topsites list is larger, hence we use a temporary table, which automatically provides rowids.
final SQLiteDatabase db = getWritableDatabase(uri);
final String TABLE_TOPSITES = "topsites";
final String limitParam = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
final String gridLimitParam = uri.getQueryParameter(BrowserContract.PARAM_SUGGESTEDSITES_LIMIT);
+ final String nonPositionedPins = uri.getQueryParameter(BrowserContract.PARAM_NON_POSITIONED_PINS);
final int totalLimit;
final int suggestedGridLimit;
if (limitParam == null) {
totalLimit = 50;
} else {
totalLimit = Integer.parseInt(limitParam, 10);
}
if (gridLimitParam == null) {
suggestedGridLimit = getContext().getResources().getInteger(R.integer.number_of_top_sites);
} else {
suggestedGridLimit = Integer.parseInt(gridLimitParam, 10);
}
- final String pinnedSitesFromClause = "FROM " + TABLE_BOOKMARKS + " WHERE " +
- Bookmarks.PARENT + " == " + Bookmarks.FIXED_PINNED_LIST_ID +
- " AND " + Bookmarks.IS_DELETED + " IS NOT 1";
+ // We have two types of pinned sites, positioned and non-positioned. Positioned pins are used
+ // by regular Top Sites, where position in the grid is of importance. Non-positioned pins are
+ // used by Activity Stream Top Sites, where pins are displayed in front of other top site items.
+ // Non-positioned pins all have the same special position value which is used to identify them.
+ // An alternative to this is creating a separate special folder for non-positioned pins, introducing
+ // a database migration, adjusting sync code, etc. While on some level this might
+ // be a cleaner solution, a "position hack" is simpler to implement and manage over time in light
+ // of A-S being either a likely replacement for regular Top Sites, or being scrapped.
+ String pinnedSitesFromClause = "FROM " + TABLE_BOOKMARKS + " WHERE " +
+ Bookmarks.PARENT + " = " + Bookmarks.FIXED_PINNED_LIST_ID +
+ " AND " + Bookmarks.IS_DELETED + " IS NOT 1";
+ if (nonPositionedPins != null) {
+ pinnedSitesFromClause += " AND " + Bookmarks.POSITION + " = " + Bookmarks.FIXED_AS_PIN_POSITION;
+ } else {
+ pinnedSitesFromClause += " AND " + Bookmarks.POSITION + " != " + Bookmarks.FIXED_AS_PIN_POSITION;
+ }
// Ideally we'd use a recursive CTE to generate our sequence, e.g. something like this worked at one point:
// " WITH RECURSIVE" +
// " cnt(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM cnt WHERE x < 6)" +
// However that requires SQLite >= 3.8.3 (available on Android >= 5.0), so in the meantime
// we use a temporary numbers table.
// Note: SQLite rowids are 1-indexed, whereas we're expecting 0-indexed values for the position. Our numbers
// table starts at position = 0, which ensures the correct results here.
@@ -1124,17 +1138,19 @@ public class BrowserProvider extends Sha
" -1 AS " + TopSites.HISTORY_ID + ", " +
Bookmarks.URL + ", " +
Bookmarks.TITLE + ", " +
Bookmarks.POSITION + ", " +
"NULL AS " + Combined.HISTORY_ID + ", " +
TopSites.TYPE_PINNED + " as " + TopSites.TYPE +
" " + pinnedSitesFromClause +
- " ORDER BY " + Bookmarks.POSITION,
+ // In case position is non-unique (as in Activity Stream pins, whose position
+ // is always zero), we need to ensure we get stable ordering.
+ " ORDER BY " + Bookmarks.POSITION + ", " + Bookmarks.URL,
null);
c.setNotificationUri(getContext().getContentResolver(),
BrowserContract.AUTHORITY_URI);
// Force the cursor to be compiled and the cursor-window filled now:
// (A) without compiling the cursor now we won't have access to the TEMP table which
@@ -1161,51 +1177,59 @@ public class BrowserProvider extends Sha
final long threeDaysAgo = System.currentTimeMillis() - (1000 * 60 * 60 * 24 * 3);
final long bookmarkLimit = 1;
// Select recent bookmarks that have not been visited much
final String bookmarksQuery = "SELECT * FROM (SELECT " +
"-1 AS " + Combined.HISTORY_ID + ", " +
DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks._ID) + " AS " + Combined.BOOKMARK_ID + ", " +
+ DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.PARENT) + " AS " + Bookmarks.PARENT + ", " +
+ DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.POSITION) + " AS " + Bookmarks.POSITION + ", " +
DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.URL) + ", " +
DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.TITLE) + ", " +
DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.DATE_CREATED) + " AS " + Highlights.DATE + " " +
"FROM " + Bookmarks.TABLE_NAME + " " +
"LEFT JOIN " + History.TABLE_NAME + " ON " +
DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.URL) + " = " +
DBUtils.qualifyColumn(History.TABLE_NAME, History.URL) + " " +
"WHERE " + DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.DATE_CREATED) + " > " + threeDaysAgo + " " +
"AND (" + DBUtils.qualifyColumn(History.TABLE_NAME, History.VISITS) + " <= 3 " +
"OR " + DBUtils.qualifyColumn(History.TABLE_NAME, History.VISITS) + " IS NULL) " +
"AND " + DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.IS_DELETED) + " = 0 " +
"AND " + DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.TYPE) + " = " + Bookmarks.TYPE_BOOKMARK + " " +
+ "AND " + DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.PARENT) + " >= " + Bookmarks.FIXED_ROOT_ID + " " +
"AND " + DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.URL) + " NOT IN (SELECT " + ActivityStreamBlocklist.URL + " FROM " + ActivityStreamBlocklist.TABLE_NAME + " )" +
"ORDER BY " + DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.DATE_CREATED) + " DESC " +
"LIMIT " + bookmarkLimit + ")";
final long last30Minutes = System.currentTimeMillis() - (1000 * 60 * 30);
final long historyLimit = totalLimit - bookmarkLimit;
// Select recent history that has not been visited much.
final String historyQuery = "SELECT * FROM (SELECT " +
- History._ID + " AS " + Combined.HISTORY_ID + ", " +
+ DBUtils.qualifyColumn(History.TABLE_NAME, History._ID) + " AS " + Combined.HISTORY_ID + ", " +
"-1 AS " + Combined.BOOKMARK_ID + ", " +
- History.URL + ", " +
- History.TITLE + ", " +
- History.DATE_LAST_VISITED + " AS " + Highlights.DATE + " " +
+ DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.PARENT) + " AS " + Bookmarks.PARENT + ", " +
+ DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.POSITION) + " AS " + Bookmarks.POSITION + ", " +
+ DBUtils.qualifyColumn(History.TABLE_NAME, History.URL) + ", " +
+ DBUtils.qualifyColumn(History.TABLE_NAME, History.TITLE) + ", " +
+ DBUtils.qualifyColumn(History.TABLE_NAME, History.DATE_LAST_VISITED) + " AS " + Highlights.DATE + " " +
"FROM " + History.TABLE_NAME + " " +
- "WHERE " + History.DATE_LAST_VISITED + " < " + last30Minutes + " " +
- "AND " + History.VISITS + " <= 3 " +
- "AND " + History.TITLE + " NOT NULL AND " + History.TITLE + " != '' " +
- "AND " + History.IS_DELETED + " = 0 " +
- "AND " + History.URL + " NOT IN (SELECT " + ActivityStreamBlocklist.URL + " FROM " + ActivityStreamBlocklist.TABLE_NAME + " )" +
+ "LEFT JOIN " + Bookmarks.TABLE_NAME + " ON " +
+ DBUtils.qualifyColumn(History.TABLE_NAME, History.URL) + " = " +
+ DBUtils.qualifyColumn(Bookmarks.TABLE_NAME, Bookmarks.URL) + " " +
+ "WHERE " + DBUtils.qualifyColumn(History.TABLE_NAME, History.DATE_LAST_VISITED) + " < " + last30Minutes + " " +
+ "AND " + DBUtils.qualifyColumn(History.TABLE_NAME, History.VISITS) + " <= 3 " +
+ "AND " + DBUtils.qualifyColumn(History.TABLE_NAME, History.TITLE) + " NOT NULL AND " + DBUtils.qualifyColumn(History.TABLE_NAME, History.TITLE) + " != '' " +
+ "AND " + DBUtils.qualifyColumn(History.TABLE_NAME, History.IS_DELETED) + " = 0 " +
+ "AND " + DBUtils.qualifyColumn(History.TABLE_NAME, History.URL) + " NOT IN (SELECT " + ActivityStreamBlocklist.URL + " FROM " + ActivityStreamBlocklist.TABLE_NAME + " )" +
// TODO: Implement domain black list (bug 1298786)
// TODO: Group by host (bug 1298785)
- "ORDER BY " + History.DATE_LAST_VISITED + " DESC " +
+ "ORDER BY " + DBUtils.qualifyColumn(History.TABLE_NAME, History.DATE_LAST_VISITED) + " DESC " +
"LIMIT " + historyLimit + ")";
final String query = "SELECT DISTINCT * " +
"FROM (" + bookmarksQuery + " " +
"UNION ALL " + historyQuery + ") " +
"GROUP BY " + Combined.URL + ";";
final Cursor cursor = db.rawQuery(query, null);
--- a/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
+++ b/mobile/android/base/java/org/mozilla/gecko/db/LocalBrowserDB.java
@@ -1725,16 +1725,65 @@ public class LocalBrowserDB extends Brow
Bookmarks.PARENT + " == ? AND " + Bookmarks.POSITION + " = ?",
new String[] {
String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID),
Integer.toString(position)
});
}
@Override
+ public void pinSiteForAS(ContentResolver cr, String url, String title) {
+ ContentValues values = new ContentValues();
+ final long now = System.currentTimeMillis();
+ values.put(Bookmarks.TITLE, title);
+ values.put(Bookmarks.URL, url);
+ values.put(Bookmarks.PARENT, Bookmarks.FIXED_PINNED_LIST_ID);
+ values.put(Bookmarks.DATE_MODIFIED, now);
+ values.put(Bookmarks.POSITION, Bookmarks.FIXED_AS_PIN_POSITION);
+ values.put(Bookmarks.IS_DELETED, 0);
+
+ cr.insert(mBookmarksUriWithProfile, values);
+ }
+
+ @Override
+ public void unpinSiteForAS(ContentResolver cr, String url) {
+ cr.delete(mBookmarksUriWithProfile,
+ Bookmarks.PARENT + " == ? AND " +
+ Bookmarks.POSITION + " == ? AND " +
+ Bookmarks.URL + " = ?",
+ new String[] {
+ String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID),
+ String.valueOf(Bookmarks.FIXED_AS_PIN_POSITION),
+ url
+ });
+ }
+
+ @Override
+ public boolean isPinnedForAS(ContentResolver cr, String url) {
+ final Cursor c = cr.query(bookmarksUriWithLimit(1),
+ new String[] { Bookmarks._ID },
+ Bookmarks.URL + " = ? AND " + Bookmarks.PARENT + " = ? AND " + Bookmarks.POSITION + " = ?",
+ new String[] {
+ url,
+ String.valueOf(Bookmarks.FIXED_PINNED_LIST_ID),
+ String.valueOf(Bookmarks.FIXED_AS_PIN_POSITION)
+ }, null);
+
+ if (c == null) {
+ throw new IllegalStateException("Null cursor in isPinnedByUrl");
+ }
+
+ try {
+ return c.getCount() > 0;
+ } finally {
+ c.close();
+ }
+ }
+
+ @Override
@RobocopTarget
public Cursor getBookmarkForUrl(ContentResolver cr, String url) {
Cursor c = cr.query(bookmarksUriWithLimit(1),
new String[] { Bookmarks._ID,
Bookmarks.URL,
Bookmarks.TITLE,
Bookmarks.KEYWORD },
Bookmarks.URL + " = ?",
@@ -1852,16 +1901,18 @@ public class LocalBrowserDB extends Brow
}
public CursorLoader getActivityStreamTopSites(Context context, int suggestedRangeLimit, int limit) {
final Uri uri = mTopSitesUriWithProfile.buildUpon()
.appendQueryParameter(BrowserContract.PARAM_LIMIT,
String.valueOf(limit))
.appendQueryParameter(BrowserContract.PARAM_SUGGESTEDSITES_LIMIT,
String.valueOf(suggestedRangeLimit))
+ .appendQueryParameter(BrowserContract.PARAM_NON_POSITIONED_PINS,
+ String.valueOf(true))
.build();
return new TelemetrisedCursorLoader(context,
uri,
new String[]{ Combined._ID,
Combined.URL,
Combined.TITLE,
Combined.BOOKMARK_ID,
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamItem.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/StreamItem.java
@@ -4,16 +4,17 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.home.activitystream;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Color;
+import android.support.annotation.Nullable;
import android.support.v4.view.ViewPager;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.Button;
@@ -129,19 +130,27 @@ public abstract class StreamItem extends
layoutParams.height = tilesHeight + tilesMargin + textHeight;
topSitesPager.setLayoutParams(layoutParams);
}
}
public static class HighlightItem extends StreamItem implements IconCallback {
public static final int LAYOUT_ID = R.layout.activity_stream_card_history_item;
+ enum HighlightSource {
+ VISITED,
+ BOOKMARKED
+ }
+
String title;
String url;
+ @Nullable Boolean isPinned;
+ @Nullable Boolean isBookmarked;
+
final FaviconView vIconView;
final TextView vLabel;
final TextView vTimeSince;
final TextView vSourceView;
final TextView vPageView;
final ImageView vSourceIconView;
private Future<IconResponse> ongoingIconLoad;
@@ -169,17 +178,18 @@ public abstract class StreamItem extends
TouchTargetUtil.ensureTargetHitArea(menuButton, itemView);
menuButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ActivityStreamContextMenu.show(v.getContext(),
menuButton,
ActivityStreamContextMenu.MenuMode.HIGHLIGHT,
- title, url, onUrlOpenListener, onUrlOpenInBackgroundListener,
+ title, url, isBookmarked, isPinned,
+ onUrlOpenListener, onUrlOpenInBackgroundListener,
vIconView.getWidth(), vIconView.getHeight());
}
});
ViewUtil.enableTouchRipple(menuButton);
}
public void bind(Cursor cursor, int tilesWidth, int tilesHeight) {
@@ -193,57 +203,92 @@ public abstract class StreamItem extends
vLabel.setText(title);
vTimeSince.setText(ago);
ViewGroup.LayoutParams layoutParams = vIconView.getLayoutParams();
layoutParams.width = tilesWidth - tilesMargin;
layoutParams.height = tilesHeight;
vIconView.setLayoutParams(layoutParams);
- updateSource(cursor);
+ final HighlightSource source = highlightSource(cursor);
+
+ updateStateForSource(source, cursor);
+ updateUiForSource(source);
updatePage(url);
if (ongoingIconLoad != null) {
ongoingIconLoad.cancel(true);
}
ongoingIconLoad = Icons.with(itemView.getContext())
.pageUrl(url)
.skipNetwork()
.build()
.execute(this);
}
- private void updateSource(final Cursor cursor) {
- final boolean isBookmark = -1 != cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Combined.BOOKMARK_ID));
- final boolean isHistory = -1 != cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID));
+ private void updateStateForSource(HighlightSource source, Cursor cursor) {
+ // 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
+ // menus are displayed.
+ switch (source) {
+ case BOOKMARKED:
+ isBookmarked = true;
+ isPinned = null;
+ break;
+ case VISITED:
+ isBookmarked = null;
+ isPinned = null;
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown source: " + source);
+ }
+ }
- if (isBookmark) {
- vSourceView.setText(R.string.activity_stream_highlight_label_bookmarked);
- vSourceView.setVisibility(View.VISIBLE);
- vSourceIconView.setImageResource(R.drawable.ic_as_bookmarked);
- } else if (isHistory) {
- vSourceView.setText(R.string.activity_stream_highlight_label_visited);
- vSourceView.setVisibility(View.VISIBLE);
- vSourceIconView.setImageResource(R.drawable.ic_as_visited);
- } else {
- vSourceView.setVisibility(View.INVISIBLE);
- vSourceIconView.setImageResource(0);
+ private void updateUiForSource(HighlightSource source) {
+ switch (source) {
+ case BOOKMARKED:
+ vSourceView.setText(R.string.activity_stream_highlight_label_bookmarked);
+ vSourceView.setVisibility(View.VISIBLE);
+ vSourceIconView.setImageResource(R.drawable.ic_as_bookmarked);
+ break;
+ case VISITED:
+ vSourceView.setText(R.string.activity_stream_highlight_label_visited);
+ vSourceView.setVisibility(View.VISIBLE);
+ vSourceIconView.setImageResource(R.drawable.ic_as_visited);
+ break;
+ default:
+ vSourceView.setVisibility(View.INVISIBLE);
+ vSourceIconView.setImageResource(0);
+ break;
}
- vSourceView.setText(vSourceView.getText());
+ // TODO Why?
+ // vSourceView.setText(vSourceView.getText());
}
private void updatePage(final String url) {
extractLabel(itemView.getContext(), url, false, new LabelCallback() {
@Override
public void onLabelExtracted(String label) {
vPageView.setText(TextUtils.isEmpty(label) ? url : label);
}
});
}
@Override
public void onIconResponse(IconResponse 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.");
+ }
}
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/ActivityStreamContextMenu.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/ActivityStreamContextMenu.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.home.activitystream.menu;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.design.widget.NavigationView;
import android.view.MenuItem;
import android.view.View;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.IntentHelper;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Telemetry;
@@ -37,79 +38,118 @@ public abstract class ActivityStreamCont
TOPSITE
}
final Context context;
final String title;
final String url;
+ // We might not know bookmarked/pinned states, so we allow for null values.
+ private @Nullable Boolean isBookmarked;
+ private @Nullable Boolean isPinned;
+
final HomePager.OnUrlOpenListener onUrlOpenListener;
final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener;
- boolean isAlreadyBookmarked; // default false;
public abstract MenuItem getItemByID(int id);
public abstract void show();
public abstract void dismiss();
final MenuMode mode;
/* package-private */ ActivityStreamContextMenu(final Context context,
final MenuMode mode,
final String title, @NonNull final String url,
+ @Nullable final Boolean isBookmarked, @Nullable final Boolean isPinned,
HomePager.OnUrlOpenListener onUrlOpenListener,
HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
this.context = context;
this.mode = mode;
this.title = title;
this.url = url;
+ this.isBookmarked = isBookmarked;
+ this.isPinned = isPinned;
this.onUrlOpenListener = onUrlOpenListener;
this.onUrlOpenInBackgroundListener = onUrlOpenInBackgroundListener;
}
/**
* Must be called before the menu is shown.
* <p/>
* Your implementation must be ready to return items from getItemByID() before postInit() is
* called, i.e. you should probably inflate your menu items before this call.
*/
protected void postInit() {
+ final MenuItem bookmarkItem = getItemByID(R.id.bookmark);
+ if (Boolean.TRUE.equals(this.isBookmarked)) {
+ bookmarkItem.setTitle(R.string.bookmark_remove);
+ }
+
+ final MenuItem pinItem = getItemByID(R.id.pin);
+ if (Boolean.TRUE.equals(this.isPinned)) {
+ pinItem.setTitle(R.string.contextmenu_top_sites_unpin);
+ }
+
// Disable "dismiss" for topsites until we have decided on its behaviour for topsites
// (currently "dismiss" adds the URL to a highlights-specific blocklist, which the topsites
// query has no knowledge of).
if (mode == MenuMode.TOPSITE) {
final MenuItem dismissItem = getItemByID(R.id.dismiss);
dismissItem.setVisible(false);
}
- // Disable the bookmark item until we know its bookmark state
- final MenuItem bookmarkItem = getItemByID(R.id.bookmark);
- bookmarkItem.setEnabled(false);
+ if (isBookmarked == null) {
+ // Disable the bookmark item until we know its bookmark state
+ bookmarkItem.setEnabled(false);
- (new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
- @Override
- protected Void doInBackground() {
- isAlreadyBookmarked = BrowserDB.from(context).isBookmark(context.getContentResolver(), url);
- return null;
- }
-
- @Override
- protected void onPostExecute(Void aVoid) {
- if (isAlreadyBookmarked) {
- bookmarkItem.setTitle(R.string.bookmark_remove);
+ (new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
+ @Override
+ protected Void doInBackground() {
+ isBookmarked = BrowserDB.from(context).isBookmark(context.getContentResolver(), url);
+ return null;
}
- bookmarkItem.setEnabled(true);
- }
- }).execute();
+ @Override
+ protected void onPostExecute(Void aVoid) {
+ if (isBookmarked) {
+ bookmarkItem.setTitle(R.string.bookmark_remove);
+ }
+
+ bookmarkItem.setEnabled(true);
+ }
+ }).execute();
+ }
+
+ if (isPinned == null) {
+ // Disable the pin item until we know its pinned state
+ pinItem.setEnabled(false);
+
+ (new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
+ @Override
+ protected Void doInBackground() {
+ isPinned = BrowserDB.from(context).isPinnedForAS(context.getContentResolver(), url);
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void aVoid) {
+ if (isPinned) {
+ pinItem.setTitle(R.string.contextmenu_top_sites_unpin);
+ }
+
+ pinItem.setEnabled(true);
+ }
+ }).execute();
+ }
// Only show the "remove from history" item if a page actually has history
final MenuItem deleteHistoryItem = getItemByID(R.id.delete);
deleteHistoryItem.setVisible(false);
(new UIAsyncTask.WithoutParams<Void>(ThreadUtils.getBackgroundHandler()) {
boolean hasHistory;
@@ -151,17 +191,17 @@ public abstract class ActivityStreamCont
case R.id.bookmark:
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
final BrowserDB db = BrowserDB.from(context);
final TelemetryContract.Event telemetryEvent;
final String telemetryExtra;
- if (isAlreadyBookmarked) {
+ if (isBookmarked) {
db.removeBookmarksWithURL(context.getContentResolver(), url);
SavedReaderViewHelper rch = SavedReaderViewHelper.getSavedReaderViewHelper(context);
final boolean isReaderViewPage = rch.isURLCached(url);
telemetryEvent = TelemetryContract.Event.UNSAVE;
if (isReaderViewPage) {
@@ -178,16 +218,30 @@ public abstract class ActivityStreamCont
telemetryExtra = "as_bookmark";
}
Telemetry.sendUIEvent(telemetryEvent, TelemetryContract.Method.CONTEXT_MENU, telemetryExtra);
}
});
break;
+ case R.id.pin:
+ ThreadUtils.postToBackgroundThread(new Runnable() {
+ @Override
+ public void run() {
+ final BrowserDB db = BrowserDB.from(context);
+
+ if (isPinned) {
+ db.unpinSiteForAS(context.getContentResolver(), url);
+ } else {
+ db.pinSiteForAS(context.getContentResolver(), url, title);
+ }
+ }
+ });
+
case R.id.copy_url:
Clipboard.setText(url);
break;
case R.id.add_homescreen:
GeckoAppShell.createShortcut(title, url);
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.CONTEXT_MENU, "as_add_to_launcher");
@@ -236,31 +290,32 @@ public abstract class ActivityStreamCont
}
@RobocopTarget
public static ActivityStreamContextMenu show(Context context,
View anchor,
final MenuMode menuMode,
final String title, @NonNull final String url,
+ @Nullable final Boolean isBookmarked, @Nullable final Boolean isPinned,
HomePager.OnUrlOpenListener onUrlOpenListener,
HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener,
final int tilesWidth, final int tilesHeight) {
final ActivityStreamContextMenu menu;
if (!HardwareUtils.isTablet()) {
menu = new BottomSheetContextMenu(context,
menuMode,
- title, url,
+ title, url, isBookmarked, isPinned,
onUrlOpenListener, onUrlOpenInBackgroundListener,
tilesWidth, tilesHeight);
} else {
menu = new PopupContextMenu(context,
anchor,
menuMode,
- title, url,
+ title, url, isBookmarked, isPinned,
onUrlOpenListener, onUrlOpenInBackgroundListener);
}
menu.show();
return menu;
}
}
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/BottomSheetContextMenu.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/BottomSheetContextMenu.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.home.activitystream.menu;
import android.content.Context;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.design.widget.BottomSheetBehavior;
import android.support.design.widget.BottomSheetDialog;
import android.support.design.widget.NavigationView;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
@@ -29,26 +30,29 @@ import static org.mozilla.gecko.activity
extends ActivityStreamContextMenu {
private final BottomSheetDialog bottomSheetDialog;
private final NavigationView navigationView;
public BottomSheetContextMenu(final Context context,
- final MenuMode mode,
- final String title, @NonNull final String url,
- HomePager.OnUrlOpenListener onUrlOpenListener,
- HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener,
- final int tilesWidth, final int tilesHeight) {
+ final MenuMode mode,
+ final String title, @NonNull final String url,
+ @Nullable final Boolean isBookmarked, @Nullable final Boolean isPinned,
+ HomePager.OnUrlOpenListener onUrlOpenListener,
+ HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener,
+ final int tilesWidth, final int tilesHeight) {
super(context,
mode,
title,
url,
+ isBookmarked,
+ isPinned,
onUrlOpenListener,
onUrlOpenInBackgroundListener);
final LayoutInflater inflater = LayoutInflater.from(context);
final View content = inflater.inflate(R.layout.activity_stream_contextmenu_bottomsheet, null);
bottomSheetDialog = new BottomSheetDialog(context);
bottomSheetDialog.setContentView(content);
--- a/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/PopupContextMenu.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/activitystream/menu/PopupContextMenu.java
@@ -1,19 +1,19 @@
/* -*- 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.menu;
import android.content.Context;
-import android.content.Intent;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.design.widget.NavigationView;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.PopupWindow;
import org.mozilla.gecko.R;
import org.mozilla.gecko.home.HomePager;
@@ -26,22 +26,26 @@ import org.mozilla.gecko.home.HomePager;
private final View anchor;
public PopupContextMenu(final Context context,
View anchor,
final MenuMode mode,
final String title,
@NonNull final String url,
+ @Nullable final Boolean isBookmarked,
+ @Nullable final Boolean isPinned,
HomePager.OnUrlOpenListener onUrlOpenListener,
HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
super(context,
mode,
title,
url,
+ isBookmarked,
+ isPinned,
onUrlOpenListener,
onUrlOpenInBackgroundListener);
this.anchor = anchor;
final LayoutInflater inflater = LayoutInflater.from(context);
View card = inflater.inflate(R.layout.activity_stream_contextmenu_popupmenu, null);
--- 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,15 +1,16 @@
/* -*- 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;
@@ -34,16 +35,18 @@ class TopSitesCard extends RecyclerView.
implements IconCallback, View.OnClickListener {
private final FaviconView faviconView;
private final TextView title;
private final ImageView menuButton;
private Future<IconResponse> ongoingIconLoad;
private String url;
+ private int type;
+ @Nullable private Boolean isBookmarked;
private final HomePager.OnUrlOpenListener onUrlOpenListener;
private final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener;
public TopSitesCard(FrameLayout card, final HomePager.OnUrlOpenListener onUrlOpenListener, final HomePager.OnUrlOpenInBackgroundListener onUrlOpenInBackgroundListener) {
super(card);
faviconView = (FaviconView) card.findViewById(R.id.favicon);
@@ -66,29 +69,30 @@ class TopSitesCard extends RecyclerView.
ActivityStream.extractLabel(itemView.getContext(), topSite.url, true, new ActivityStream.LabelCallback() {
@Override
public void onLabelExtracted(String label) {
title.setText(label);
}
});
this.url = topSite.url;
+ this.type = topSite.type;
+ this.isBookmarked = topSite.isBookmarked;
if (ongoingIconLoad != null) {
ongoingIconLoad.cancel(true);
}
ongoingIconLoad = Icons.with(itemView.getContext())
.pageUrl(topSite.url)
.skipNetwork()
.build()
.execute(this);
- final int pinResourceId = (topSite.type == BrowserContract.TopSites.TYPE_PINNED ?
- R.drawable.pin : 0);
+ final int pinResourceId = (isPinned(this.type) ? R.drawable.pin : 0);
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(title, pinResourceId, 0, 0, 0);
}
@Override
public void onIconResponse(IconResponse response) {
faviconView.updateImage(response);
final int tintColor = !response.hasColor() || response.getColor() == Color.WHITE ? Color.LTGRAY : Color.WHITE;
@@ -103,15 +107,22 @@ class TopSitesCard extends RecyclerView.
onUrlOpenListener.onUrlOpen(url, EnumSet.noneOf(HomePager.OnUrlOpenListener.Flags.class));
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, "as_top_sites");
} else if (clickedView == menuButton) {
ActivityStreamContextMenu.show(clickedView.getContext(),
menuButton,
ActivityStreamContextMenu.MenuMode.TOPSITE,
title.getText().toString(), url,
+
+ isBookmarked, isPinned(type),
+
onUrlOpenListener, onUrlOpenInBackgroundListener,
faviconView.getWidth(), faviconView.getHeight());
Telemetry.sendUIEvent(TelemetryContract.Event.SHOW, TelemetryContract.Method.CONTEXT_MENU, "as_top_sites");
}
}
+
+ private static boolean isPinned(int type) {
+ return type == BrowserContract.TopSites.TYPE_PINNED;
+ }
}
--- 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,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.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;
@@ -20,22 +21,24 @@ import org.mozilla.gecko.home.HomePager;
import java.util.ArrayList;
import java.util.List;
public class TopSitesPageAdapter extends RecyclerView.Adapter<TopSitesCard> {
static final class TopSite {
public final long id;
public final String url;
public final String title;
+ @Nullable public final Boolean isBookmarked;
public final int type;
- TopSite(long id, String url, String title, int type) {
+ TopSite(long id, String url, String title, @Nullable Boolean isBookmarked, int type) {
this.id = id;
this.url = url;
this.title = title;
+ this.isBookmarked = isBookmarked;
this.type = type;
}
}
private List<TopSite> topSites;
private int tiles;
private int tilesWidth;
private int tilesHeight;
@@ -76,17 +79,22 @@ public class TopSitesPageAdapter extends
// The Combined View only contains pages that have been visited at least once, i.e. any
// page in the TopSites query will contain a HISTORY_ID. _ID however will be 0 for all rows.
final long id = cursor.getLong(cursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID));
final String url = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL));
final String title = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.TITLE));
final int type = cursor.getInt(cursor.getColumnIndexOrThrow(BrowserContract.TopSites.TYPE));
- topSites.add(new TopSite(id, url, title, type));
+ // We can't figure out bookmark state of a pin, so we leave it as unknown to be queried later.
+ Boolean isBookmarked = null;
+ if (type != BrowserContract.TopSites.TYPE_PINNED) {
+ isBookmarked = !cursor.isNull(cursor.getColumnIndexOrThrow(BrowserContract.Combined.BOOKMARK_ID));
+ }
+ topSites.add(new TopSite(id, url, title, isBookmarked, type));
}
notifyDataSetChanged();
}
@Override
public void onBindViewHolder(TopSitesCard holder, int position) {
holder.bind(topSites.get(position));
--- a/mobile/android/base/resources/menu/activitystream_contextmenu.xml
+++ b/mobile/android/base/resources/menu/activitystream_contextmenu.xml
@@ -2,16 +2,20 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Group ID's are required, otherwise NavigationView won't show any dividers. The ID's are unused, but still required. -->
<group android:id="@+id/group0">
<item
android:id="@+id/bookmark"
android:icon="@drawable/as_bookmark"
android:title="@string/bookmark"/>
<item
+ android:id="@+id/pin"
+ android:icon="@drawable/pin"
+ android:title="@string/contextmenu_top_sites_pin"/>
+ <item
android:id="@+id/share"
android:icon="@drawable/as_share"
android:title="@string/share"/>
<item
android:id="@+id/copy_url"
android:icon="@drawable/as_copy"
android:title="@string/contextmenu_copyurl"/>
<item
--- a/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testActivityStreamContextMenu.java
+++ b/mobile/android/tests/browser/robocop/src/org/mozilla/gecko/tests/testActivityStreamContextMenu.java
@@ -1,69 +1,90 @@
/* 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.tests;
-import android.support.design.widget.NavigationView;
-import android.support.v4.app.Fragment;
-import android.view.KeyEvent;
import android.view.MenuItem;
import android.view.View;
import com.robotium.solo.Condition;
import org.mozilla.gecko.R;
import org.mozilla.gecko.db.BrowserDB;
-import org.mozilla.gecko.home.activitystream.ActivityStream;
import org.mozilla.gecko.home.activitystream.menu.ActivityStreamContextMenu;
/**
* This test is unfortunately closely coupled to the current implementation, however it is still
- * useful in that it tests the bookmark/history state specific menu items for correctness.
+ * useful in that it tests the bookmark/history/pinned state specific menu items for correctness.
*/
public class testActivityStreamContextMenu extends BaseTest {
public void testActivityStreamContextMenu() {
blockForGeckoReady();
final String testURL = "http://mozilla.org";
BrowserDB db = BrowserDB.from(getActivity());
db.removeHistoryEntry(getActivity().getContentResolver(), testURL);
db.removeBookmarksWithURL(getActivity().getContentResolver(), testURL);
- testMenuForUrl(testURL, false, false);
+ testMenuForUrl(testURL, null, false, null, false, false);
db.addBookmark(getActivity().getContentResolver(), "foobar", testURL);
- testMenuForUrl(testURL, true, false);
+ testMenuForUrl(testURL, null, true, null, false, false);
+ testMenuForUrl(testURL, null, true, Boolean.FALSE, false, false);
+ testMenuForUrl(testURL, Boolean.TRUE, true, null, false, false);
+ testMenuForUrl(testURL, Boolean.TRUE, true, Boolean.FALSE, false, false);
db.updateVisitedHistory(getActivity().getContentResolver(), testURL);
- testMenuForUrl(testURL, true, true);
+ testMenuForUrl(testURL, null, true, null, false, true);
+ testMenuForUrl(testURL, null, true, Boolean.FALSE, false, true);
+ testMenuForUrl(testURL, Boolean.TRUE, true, null, false, true);
+ testMenuForUrl(testURL, Boolean.TRUE, true, Boolean.FALSE, false, true);
db.removeBookmarksWithURL(getActivity().getContentResolver(), testURL);
- testMenuForUrl(testURL, false, true);
+ testMenuForUrl(testURL, null, false, null, false, true);
+ testMenuForUrl(testURL, Boolean.FALSE, false, null, false, true);
+
+ db.pinSiteForAS(getActivity().getContentResolver(), testURL, "test title");
+ testMenuForUrl(testURL, null, false, null, true, true);
+ testMenuForUrl(testURL, null, false, Boolean.TRUE, true, true);
+
+ db.addBookmark(getActivity().getContentResolver(), "foobar", testURL);
+ testMenuForUrl(testURL, null, true, null, true, true);
+ testMenuForUrl(testURL, Boolean.TRUE, true, Boolean.TRUE, true, true);
}
/**
* Test that the menu shows the expected menu items for a given URL, and that these items have
* the correct state.
*/
- private void testMenuForUrl(final String url, final boolean isBookmarked, 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 ActivityStreamContextMenu menu = ActivityStreamContextMenu.show(getActivity(), anchor, ActivityStreamContextMenu.MenuMode.HIGHLIGHT, "foobar", url, null, null, 100, 100);
+ final ActivityStreamContextMenu menu = ActivityStreamContextMenu.show(getActivity(), anchor, ActivityStreamContextMenu.MenuMode.HIGHLIGHT, "foobar", url, isBookmarkedKnownState, isPinnedKnownState, null, null, 100, 100);
final int expectedBookmarkString;
if (isBookmarked) {
expectedBookmarkString = R.string.bookmark_remove;
} else {
expectedBookmarkString = R.string.bookmark;
}
+ final int expectedPinnedString;
+ if (isPinned) {
+ expectedPinnedString = R.string.contextmenu_top_sites_unpin;
+ } else {
+ expectedPinnedString = R.string.contextmenu_top_sites_pin;
+ }
+
+ final MenuItem pinItem = menu.getItemByID(R.id.pin);
+ assertMenuItemHasString(pinItem, expectedPinnedString);
+
final MenuItem bookmarkItem = menu.getItemByID(R.id.bookmark);
assertMenuItemHasString(bookmarkItem, expectedBookmarkString);
final MenuItem deleteItem = menu.getItemByID(R.id.delete);
assertMenuItemIsVisible(deleteItem, isVisited);
menu.dismiss();
}