Bug 1220928 - Add client hiding/showing. r=sebastian
☠☠ backed out by 3cdaff81f4d5 ☠ ☠
authorChenxia Liu <liuche@mozilla.com>
Tue, 29 Mar 2016 17:10:33 -0700
changeset 290834 81b90ed9ff464c38667488a5d4892728a802bdb0
parent 290833 b2828872f3e78911798083fde9b196e7206aaf19
child 290835 916fb74ed491c4f492d97bb4bbc5b8f786cd223f
push id19656
push usergwagner@mozilla.com
push dateMon, 04 Apr 2016 13:43:23 +0000
treeherderb2g-inbound@e99061fde28a [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssebastian
bugs1220928
milestone48.0a1
Bug 1220928 - Add client hiding/showing. r=sebastian MozReview-Commit-ID: IvyrslMqTOT
mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java
mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryItem.java
mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryRecyclerView.java
mobile/android/base/java/org/mozilla/gecko/home/RemoteTabsBaseFragment.java
mobile/android/base/resources/drawable/action_bar_button.xml
mobile/android/base/resources/layout/home_remote_tabs_hidden_devices.xml
mobile/android/base/resources/menu/home_remote_tabs_client_contextmenu.xml
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryAdapter.java
@@ -10,31 +10,33 @@ import android.content.Context;
 import android.database.Cursor;
 import android.util.Log;
 import android.util.SparseArray;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.TextView;
 import org.json.JSONArray;
+import org.mozilla.gecko.GeckoSharedPrefs;
 import org.mozilla.gecko.R;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.RemoteClient;
 import org.mozilla.gecko.db.RemoteTab;
 import org.mozilla.gecko.home.CombinedHistoryPanel.SectionHeader;
 
 import java.util.Collections;
 import java.util.ArrayList;
+import java.util.Iterator;
 import java.util.List;
 
 public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistoryItem> {
     private static final String LOGTAG = "GeckoCombinedHistAdapt";
 
     public enum ItemType {
-        CLIENT, SECTION_HEADER, HISTORY, NAVIGATION_BACK, CHILD;
+        CLIENT, HIDDEN_DEVICES, SECTION_HEADER, HISTORY, NAVIGATION_BACK, CHILD;
 
         public static ItemType viewTypeToItemType(int viewType) {
             if (viewType >= ItemType.values().length) {
                 Log.e(LOGTAG, "No corresponding ItemType!");
             }
             return ItemType.values()[viewType];
         }
 
@@ -42,38 +44,138 @@ public class CombinedHistoryAdapter exte
             return itemType.ordinal();
         }
     }
 
     private List<RemoteClient> remoteClients = Collections.emptyList();
     private List<RemoteTab> clientChildren;
     private Cursor historyCursor;
 
+    // Maintain group collapsed and hidden state. Only accessed from the UI thread.
+    protected static RemoteTabsExpandableListState sState;
+
+    // List of hidden remote clients.
+    // Only accessed from the UI thread.
+    protected final List<RemoteClient> hiddenClients = new ArrayList<>();
+
     // We use a sparse array to store each section header's position in the panel [more cheaply than a HashMap].
     private final SparseArray<CombinedHistoryPanel.SectionHeader> sectionHeaders;
 
     private final Context context;
 
     private boolean inChildView = false;
 
     public CombinedHistoryAdapter(Context context) {
         super();
         this.context = context;
         sectionHeaders = new SparseArray<>();
+
+        // This races when multiple Fragments are created. That's okay: one
+        // will win, and thereafter, all will be okay. If we create and then
+        // drop an instance the shared SharedPreferences backing all the
+        // instances will maintain the state for us. Since everything happens on
+        // the UI thread, this doesn't even need to be volatile.
+        if (sState == null) {
+            sState = new RemoteTabsExpandableListState(GeckoSharedPrefs.forProfile(context));
+        }
     }
 
     public void setClients(List<RemoteClient> clients) {
+        hiddenClients.clear();
+        remoteClients.clear();
+
+        final Iterator<RemoteClient> it = clients.iterator();
+        while (it.hasNext()) {
+            final RemoteClient client = it.next();
+            if (sState.isClientHidden(client.guid)) {
+                hiddenClients.add(client);
+                it.remove();
+            }
+        }
+
         remoteClients = clients;
-        notifyDataSetChanged();
+
+        // Add item for unhiding clients.
+        if (!hiddenClients.isEmpty()) {
+            remoteClients.add(null);
+        }
+
+        notifyItemRangeChanged(0, remoteClients.size());
     }
 
     public void setHistory(Cursor history) {
         historyCursor = history;
         populateSectionHeaders(historyCursor, sectionHeaders);
-        notifyDataSetChanged();
+        final int historySize = historyCursor == null ? 0 : historyCursor.getCount();
+        notifyItemRangeChanged(remoteClients.size(), historySize + sectionHeaders.size());
+    }
+
+    public void removeItem(int position) {
+        final ItemType  itemType = getItemTypeForPosition(position);
+        switch (itemType) {
+            case CLIENT:
+                final boolean hadHiddenClients = !hiddenClients.isEmpty();
+                final RemoteClient client = remoteClients.remove(transformAdapterPositionForDataStructure(ItemType.CLIENT, position));
+                notifyItemRemoved(position);
+
+                sState.setClientHidden(client.guid, true);
+                hiddenClients.add(client);
+                if (!hadHiddenClients) {
+                    // Add item for unhiding clients;
+                    remoteClients.add(null);
+                } else {
+                    // Update "hidden clients" item because number of hidden clients changed.
+                    notifyItemChanged(getRemoteClientsHiddenItemsIndex());
+                }
+                break;
+        }
+    }
+
+    public void unhideClients(List<RemoteClient> selectedClients) {
+        if (selectedClients.size() == 0) {
+            return;
+        }
+
+        for (RemoteClient client : selectedClients) {
+            sState.setClientHidden(client.guid, false);
+            hiddenClients.remove(client);
+        }
+
+        final int insertIndex = getRemoteClientsHiddenItemsIndex();
+
+        remoteClients.addAll(insertIndex, selectedClients);
+        notifyItemRangeInserted(insertIndex, selectedClients.size());
+
+        if (hiddenClients.isEmpty()) {
+            // No more hidden clients, remove "unhide" item.
+            remoteClients.remove(getRemoteClientsHiddenItemsIndex());
+        } else {
+            // Update "hidden clients" item because number of hidden clients changed.
+            notifyItemChanged(getRemoteClientsHiddenItemsIndex());
+        }
+    }
+
+    /**
+     * Get the position of the "N devices hidden" item in the remoteClients List.
+     *
+     * This is the last item in the remoteClients list, if any items are hidden.
+     * <code>hiddenClients</code> must be in a consistent state with <code>remoteClients</code>
+     * (e.g. each client should be in exactly one of the two lists).
+     *
+     * @return index of the "N devices hidden" item, or -1 if it doesn't exist.
+     */
+    private int getRemoteClientsHiddenItemsIndex() {
+        if (hiddenClients.isEmpty()) {
+            return -1;
+        }
+        return remoteClients.size() - 1;
+    }
+
+    public List<RemoteClient> getHiddenClients() {
+        return hiddenClients;
     }
 
     public JSONArray getCurrentChildTabs() {
         if (clientChildren != null) {
             final JSONArray urls = new JSONArray();
             for (int i = 1; i < clientChildren.size(); i++) {
                 urls.put(clientChildren.get(i).url);
             }
@@ -83,73 +185,95 @@ public class CombinedHistoryAdapter exte
     }
 
     public void showChildView(int parentPosition) {
         if (clientChildren == null) {
             clientChildren = new ArrayList<>();
         }
         // Handle "back" view.
         clientChildren.add(null);
-        clientChildren.addAll(remoteClients.get(transformPosition(ItemType.CLIENT, parentPosition)).tabs);
+        clientChildren.addAll(remoteClients.get(transformAdapterPositionForDataStructure(ItemType.CLIENT, parentPosition)).tabs);
         inChildView = true;
         notifyDataSetChanged();
     }
 
     public void exitChildView() {
         inChildView = false;
         clientChildren.clear();
         notifyDataSetChanged();
     }
 
-    private int transformPosition(ItemType type, int position) {
+    private ItemType getItemTypeForPosition(int position) {
+        return ItemType.viewTypeToItemType(getItemViewType(position));
+    }
+
+    /**
+     * Transform an adapter position to the position for the data structure backing the item type.
+     *
+     * The type is not strictly necessary and could be fetched from <code>getItemTypeForPosition</code>,
+     * but is used for explicitness.
+     *
+     * @param type ItemType of the item
+     * @param position position in the adapter
+     * @return position of the item in the data structure
+     */
+    private int transformAdapterPositionForDataStructure(ItemType type, int position) {
         if (type == ItemType.CLIENT) {
             return position;
         } else if (type == ItemType.SECTION_HEADER) {
             return position - remoteClients.size();
         } else if (type == ItemType.HISTORY){
             return position - remoteClients.size() - getHeadersBefore(position);
         } else {
             return position;
         }
     }
 
     public HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position) {
-        final ItemType itemType = ItemType.viewTypeToItemType(getItemViewType(position));
+        final ItemType itemType = getItemTypeForPosition(position);
         HomeContextMenuInfo info;
         switch (itemType) {
             case CHILD:
                 info = new HomeContextMenuInfo(view, position, -1);
                 return CombinedHistoryPanel.populateChildInfoFromTab(info, clientChildren.get(position));
             case HISTORY:
                 info = new HomeContextMenuInfo(view, position, -1);
-                historyCursor.moveToPosition(transformPosition(ItemType.HISTORY, position));
+                historyCursor.moveToPosition(transformAdapterPositionForDataStructure(ItemType.HISTORY, position));
                 return CombinedHistoryPanel.populateHistoryInfoFromCursor(info, historyCursor);
+            case CLIENT:
+                final int clientPosition = transformAdapterPositionForDataStructure(ItemType.CLIENT, position);
+                info = new CombinedHistoryPanel.RemoteTabsClientContextMenuInfo(view, position,-1, remoteClients.get(clientPosition));
+                return info;
         }
         return null;
     }
 
     @Override
     public CombinedHistoryItem onCreateViewHolder(ViewGroup viewGroup, int viewType) {
         final LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
         final View view;
 
         final ItemType itemType = ItemType.viewTypeToItemType(viewType);
 
         switch (itemType) {
             case CLIENT:
                 view = inflater.inflate(R.layout.home_remote_tabs_group, viewGroup, false);
                 return new CombinedHistoryItem.ClientItem(view);
 
+            case HIDDEN_DEVICES:
+                view = inflater.inflate(R.layout.home_remote_tabs_hidden_devices, viewGroup, false);
+                return new CombinedHistoryItem.BasicItem(view);
+
             case NAVIGATION_BACK:
                 view = inflater.inflate(R.layout.home_combined_back_item, viewGroup, false);
                 return new CombinedHistoryItem.HistoryItem(view);
 
             case SECTION_HEADER:
                 view = inflater.inflate(R.layout.home_header_row, viewGroup, false);
-                return new CombinedHistoryItem.SectionItem(view);
+                return new CombinedHistoryItem.BasicItem(view);
 
             case CHILD:
             case HISTORY:
                 view = inflater.inflate(R.layout.home_item_row, viewGroup, false);
                 return new CombinedHistoryItem.HistoryItem(view);
             default:
                 throw new IllegalArgumentException("Unexpected Home Panel item type");
         }
@@ -160,36 +284,38 @@ public class CombinedHistoryAdapter exte
         if (inChildView) {
             if (position == 0) {
                 return ItemType.itemTypeToViewType(ItemType.NAVIGATION_BACK);
             }
             return ItemType.itemTypeToViewType(ItemType.CHILD);
         } else {
             final int numClients = remoteClients.size();
             if (position < numClients) {
+                if (!hiddenClients.isEmpty() && position == numClients - 1) {
+                    return ItemType.itemTypeToViewType(ItemType.HIDDEN_DEVICES);
+                }
                 return ItemType.itemTypeToViewType(ItemType.CLIENT);
             }
 
-            final int sectionPosition = transformPosition(ItemType.SECTION_HEADER, position);
+            final int sectionPosition = transformAdapterPositionForDataStructure(ItemType.SECTION_HEADER, position);
             if (sectionHeaders.get(sectionPosition) != null) {
                 return ItemType.itemTypeToViewType(ItemType.SECTION_HEADER);
             }
 
             return ItemType.itemTypeToViewType(ItemType.HISTORY);
         }
     }
 
     @Override
     public int getItemCount() {
         if (inChildView) {
             return (clientChildren == null) ? 0 : clientChildren.size();
         } else {
-            final int remoteSize = remoteClients.size();
             final int historySize = historyCursor == null ? 0 : historyCursor.getCount();
-            return remoteSize + historySize + sectionHeaders.size();
+            return remoteClients.size() + historySize + sectionHeaders.size();
         }
     }
 
     /**
      * Add only the SectionHeaders that have history items within their range to a SparseArray, where the
      * array index is the position of the header in the history-only (no clients) ordering.
      * @param c data Cursor
      * @param sparseArray SparseArray to populate
@@ -224,35 +350,41 @@ public class CombinedHistoryAdapter exte
         if (historyCursor == null) {
             return false;
         }
         return (historyCursor.getCount() > 0);
     }
 
     @Override
     public void onBindViewHolder(CombinedHistoryItem viewHolder, int position) {
-        final ItemType itemType = ItemType.viewTypeToItemType(getItemViewType(position));
-        final int localPosition = transformPosition(itemType, position);
+        final ItemType itemType = getItemTypeForPosition(position);
+        final int localPosition = transformAdapterPositionForDataStructure(itemType, position);
 
         switch (itemType) {
             case CLIENT:
                 final CombinedHistoryItem.ClientItem clientItem = (CombinedHistoryItem.ClientItem) viewHolder;
                 final RemoteClient client = remoteClients.get(localPosition);
                 clientItem.bind(client, context);
                 break;
 
+            case HIDDEN_DEVICES:
+                final String hiddenDevicesLabel = context.getResources().getString(R.string.home_remote_tabs_many_hidden_devices, hiddenClients.size());
+                ((TextView) viewHolder.itemView).setText(hiddenDevicesLabel);
+                break;
+
             case CHILD:
                 RemoteTab remoteTab = clientChildren.get(position);
                 ((CombinedHistoryItem.HistoryItem) viewHolder).bind(remoteTab);
                 break;
 
             case SECTION_HEADER:
                 ((TextView) viewHolder.itemView).setText(CombinedHistoryPanel.getSectionHeaderTitle(sectionHeaders.get(localPosition)));
                 break;
 
+
             case HISTORY:
                 if (historyCursor == null || !historyCursor.moveToPosition(localPosition)) {
                     throw new IllegalStateException("Couldn't move cursor to position " + localPosition);
                 }
                 ((CombinedHistoryItem.HistoryItem) viewHolder).bind(historyCursor);
                 break;
         }
     }
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryItem.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryItem.java
@@ -16,18 +16,18 @@ import org.mozilla.gecko.RemoteTabsExpan
 import org.mozilla.gecko.db.RemoteClient;
 import org.mozilla.gecko.db.RemoteTab;
 
 public abstract class CombinedHistoryItem extends RecyclerView.ViewHolder {
     public CombinedHistoryItem(View view) {
         super(view);
     }
 
-    public static class SectionItem extends CombinedHistoryItem {
-        public SectionItem(View view) {
+    public static class BasicItem extends CombinedHistoryItem {
+        public BasicItem(View view) {
             super(view);
         }
     }
 
     public static class HistoryItem extends CombinedHistoryItem {
         public HistoryItem(View view) {
             super(view);
         }
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryPanel.java
@@ -6,53 +6,59 @@
 package org.mozilla.gecko.home;
 
 import android.app.AlertDialog;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.DialogInterface;
 import android.database.Cursor;
 import android.os.Bundle;
+import android.support.v4.app.LoaderManager;
 import android.support.v4.content.Loader;
 import android.support.v7.widget.DefaultItemAnimator;
 import android.text.SpannableStringBuilder;
 import android.text.TextPaint;
 import android.text.method.LinkMovementMethod;
 import android.text.style.ClickableSpan;
 import android.text.style.UnderlineSpan;
 import android.util.Log;
+import android.view.ContextMenu;
 import android.view.LayoutInflater;
+import android.view.MenuInflater;
+import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
 import android.view.ViewStub;
 import android.widget.Button;
 import android.widget.ImageView;
 import android.widget.TextView;
 import org.json.JSONException;
 import org.json.JSONObject;
 import org.json.JSONArray;
 import org.mozilla.gecko.EventDispatcher;
 import org.mozilla.gecko.GeckoAppShell;
-import org.mozilla.gecko.GeckoEvent;
 import org.mozilla.gecko.GeckoProfile;
 import org.mozilla.gecko.R;
+import org.mozilla.gecko.RemoteClientsDialogFragment;
 import org.mozilla.gecko.Restrictions;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.db.BrowserContract;
 import org.mozilla.gecko.db.BrowserDB;
 import org.mozilla.gecko.db.RemoteClient;
 import org.mozilla.gecko.db.RemoteTab;
 import org.mozilla.gecko.home.HistorySectionsHelper.SectionDateRange;
 import org.mozilla.gecko.restrictions.Restrictable;
 import org.mozilla.gecko.widget.DividerItemDecoration;
 
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
-public class CombinedHistoryPanel extends HomeFragment {
+public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsDialogFragment.RemoteClientsListener {
     private static final String LOGTAG = "GeckoCombinedHistoryPnl";
     private final int LOADER_ID_HISTORY = 0;
     private final int LOADER_ID_REMOTE = 1;
 
     // Semantic names for the time covered by each section
     public enum SectionHeader {
         TODAY,
         YESTERDAY,
@@ -101,16 +107,17 @@ public class CombinedHistoryPanel extend
 
         mRecyclerView = (CombinedHistoryRecyclerView) view.findViewById(R.id.combined_recycler_view);
         mAdapter = new CombinedHistoryAdapter(getContext());
         mRecyclerView.setAdapter(mAdapter);
         mRecyclerView.setItemAnimator(new DefaultItemAnimator());
         mRecyclerView.addItemDecoration(new DividerItemDecoration(getContext()));
         mRecyclerView.setOnHistoryClickedListener(mUrlOpenListener);
         mRecyclerView.setOnPanelLevelChangeListener(new OnLevelChangeListener());
+        mRecyclerView.setHiddenClientsDialogBuilder(new HiddenClientsHelper());
         registerForContextMenu(mRecyclerView);
 
         mPanelFooterButton = (Button) view.findViewById(R.id.clear_history_button);
         mPanelFooterButton.setOnClickListener(new OnFooterButtonClickListener());
     }
 
     @Override
     public void onActivityCreated(Bundle savedInstanceState) {
@@ -165,17 +172,17 @@ public class CombinedHistoryPanel extend
             if (time > sectionDateRangeArray[i].start) {
                 return SectionHeader.values()[i];
             }
         }
 
         return SectionHeader.OLDER_THAN_SIX_MONTHS;
     }
 
-    private class CursorLoaderCallbacks extends TransitionAwareCursorLoaderCallbacks {
+    private class CursorLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
         private BrowserDB mDB;    // Pseudo-final: set in onCreateLoader.
 
         @Override
         public Loader<Cursor> onCreateLoader(int id, Bundle args) {
             if (mDB == null) {
                 mDB = GeckoProfile.get(getActivity()).getDB();
             }
 
@@ -185,34 +192,41 @@ public class CombinedHistoryPanel extend
                 case LOADER_ID_REMOTE:
                     return new RemoteTabsCursorLoader(getContext());
                 default:
                     Log.e(LOGTAG, "Unknown loader id!");
                     return null;
             }
         }
 
-        protected void onLoadFinishedAfterTransitions(Loader<Cursor> loader, Cursor c) {
+        @Override
+        public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
             final int loaderId = loader.getId();
             switch (loaderId) {
                 case LOADER_ID_HISTORY:
                     mAdapter.setHistory(c);
                     break;
 
                 case LOADER_ID_REMOTE:
                     final List<RemoteClient> clients = mDB.getTabsAccessor().getClientsFromCursor(c);
-                    // TODO: Handle hidden clients
+
                     mAdapter.setClients(clients);
                     break;
             }
 
             // Check and set empty state.
             updateButtonFromLevel(OnPanelLevelChangeListener.PanelLevel.PARENT);
             updateEmptyView(mAdapter.getItemCount() == 0);
         }
+
+        @Override
+        public void onLoaderReset(Loader<Cursor> loader) {
+            mAdapter.setClients(Collections.<RemoteClient>emptyList());
+            mAdapter.setHistory(null);
+        }
     }
 
     protected class OnLevelChangeListener implements OnPanelLevelChangeListener {
         @Override
         public void onPanelLevelChange(PanelLevel level) {
             updateButtonFromLevel(level);
         }
     }
@@ -257,32 +271,32 @@ public class CombinedHistoryPanel extend
                             // Send message to Java to clear history.
                             final JSONObject json = new JSONObject();
                             try {
                                 json.put("history", true);
                             } catch (JSONException e) {
                                 Log.e(LOGTAG, "JSON error", e);
                             }
 
-                            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Sanitize:ClearData", json.toString()));
+                            GeckoAppShell.notifyObservers("Sanitize:ClearData", json.toString());
                             Telemetry.sendUIEvent(TelemetryContract.Event.SANITIZE, TelemetryContract.Method.BUTTON, "history");
                         }
                     });
 
                     dialogBuilder.show();
                     break;
 
                 case CHILD:
                     final JSONArray tabUrls = ((CombinedHistoryAdapter) mRecyclerView.getAdapter()).getCurrentChildTabs();
                     if (tabUrls != null) {
                         final JSONObject message = new JSONObject();
                         try {
                             message.put("urls", tabUrls);
                             message.put("shouldNotifyTabsOpenedToJava", false);
-                            GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Tabs:OpenMultiple", message.toString()));
+                            GeckoAppShell.notifyObservers("Tabs:OpenMultiple", message.toString());
                         } catch (JSONException e) {
                             Log.e(LOGTAG, "Error making JSON message to open tabs");
                         }
                     }
                     break;
             }
         }
     }
@@ -373,16 +387,91 @@ public class CombinedHistoryPanel extend
         ssb.setSpan(new UnderlineSpan(), underlineStart, underlineEnd, 0);
 
         ssb.delete(underlineEnd, underlineEnd + FORMAT_S2.length());
         ssb.delete(underlineStart, underlineStart + FORMAT_S1.length());
 
         return ssb;
     }
 
+    @Override
+    public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) {
+        if (!(menuInfo instanceof RemoteTabsClientContextMenuInfo)) {
+            // Long pressed item was not a RemoteTabsGroup item. Superclass
+            // can handle this.
+            super.onCreateContextMenu(menu, view, menuInfo);
+            return;
+        }
+
+        // Long pressed item was a remote client; provide the appropriate menu.
+        final MenuInflater inflater = new MenuInflater(view.getContext());
+        inflater.inflate(R.menu.home_remote_tabs_client_contextmenu, menu);
+
+        final RemoteTabsClientContextMenuInfo info = (RemoteTabsClientContextMenuInfo) menuInfo;
+        menu.setHeaderTitle(info.client.name);
+    }
+
+    @Override
+    public boolean onContextItemSelected(MenuItem item) {
+        if (super.onContextItemSelected(item)) {
+            // HomeFragment was able to handle to selected item.
+            return true;
+        }
+
+        final ContextMenu.ContextMenuInfo menuInfo = item.getMenuInfo();
+        if (!(menuInfo instanceof RemoteTabsClientContextMenuInfo)) {
+            return false;
+        }
+
+        final RemoteTabsClientContextMenuInfo info = (RemoteTabsClientContextMenuInfo) menuInfo;
+
+        final int itemId = item.getItemId();
+        if (itemId == R.id.home_remote_tabs_hide_client) {
+            ((CombinedHistoryAdapter) mRecyclerView.getAdapter()).removeItem(info.position);
+            return true;
+        }
+
+        return false;
+    }
+
+    interface DialogBuilder<E> {
+        void createAndShowDialog(List<E> items);
+    }
+
+    protected class HiddenClientsHelper implements DialogBuilder<RemoteClient> {
+        @Override
+        public void createAndShowDialog(List<RemoteClient> clientsList) {
+                        final RemoteClientsDialogFragment dialog = RemoteClientsDialogFragment.newInstance(
+                    getResources().getString(R.string.home_remote_tabs_hidden_devices_title),
+                    getResources().getString(R.string.home_remote_tabs_unhide_selected_devices),
+                    RemoteClientsDialogFragment.ChoiceMode.MULTIPLE, new ArrayList<>(clientsList));
+            dialog.setTargetFragment(CombinedHistoryPanel.this, 0);
+            dialog.show(getActivity().getSupportFragmentManager(), "show-clients");
+        }
+
+
+    }
+
+    @Override
+    public void onClients(List<RemoteClient> clients) {
+        ((CombinedHistoryAdapter) mRecyclerView.getAdapter()).unhideClients(clients);
+    }
+
+    /**
+     * Stores information regarding the creation of the context menu for a remote client.
+     */
+    protected static class RemoteTabsClientContextMenuInfo extends HomeContextMenuInfo {
+        protected final RemoteClient client;
+
+        public RemoteTabsClientContextMenuInfo(View targetView, int position, long id, RemoteClient client) {
+            super(targetView, position, id);
+            this.client = client;
+        }
+    }
+
     protected static HomeContextMenuInfo populateHistoryInfoFromCursor(HomeContextMenuInfo info, Cursor cursor) {
         info.url = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.URL));
         info.title = cursor.getString(cursor.getColumnIndexOrThrow(BrowserContract.Combined.TITLE));
         info.historyId = cursor.getInt(cursor.getColumnIndexOrThrow(BrowserContract.Combined.HISTORY_ID));
         info.itemType = HomeContextMenuInfo.RemoveItemType.HISTORY;
         final int bookmarkIdCol = cursor.getColumnIndexOrThrow(BrowserContract.Combined.BOOKMARK_ID);
         if (cursor.isNull(bookmarkIdCol)) {
             // If this is a combined cursor, we may get a history item without a
--- a/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryRecyclerView.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/CombinedHistoryRecyclerView.java
@@ -5,29 +5,31 @@
 
 package org.mozilla.gecko.home;
 
 import android.content.Context;
 import android.support.v7.widget.LinearLayoutManager;
 import android.support.v7.widget.RecyclerView;
 import android.util.AttributeSet;
 import android.view.View;
+import org.mozilla.gecko.db.RemoteClient;
 import org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener;
 import org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel;
 import org.mozilla.gecko.Telemetry;
 import org.mozilla.gecko.TelemetryContract;
 import org.mozilla.gecko.widget.RecyclerViewClickSupport;
 
 import java.util.EnumSet;
 
 public class CombinedHistoryRecyclerView extends RecyclerView
         implements RecyclerViewClickSupport.OnItemClickListener, RecyclerViewClickSupport.OnItemLongClickListener {
 
     protected HomePager.OnUrlOpenListener mOnUrlOpenListener;
     protected OnPanelLevelChangeListener mOnPanelLevelChangeListener;
+    protected CombinedHistoryPanel.DialogBuilder<RemoteClient> mDialogBuilder;
     protected HomeContextMenuInfo mContextMenuInfo;
 
     public CombinedHistoryRecyclerView(Context context) {
         super(context);
         init(context);
     }
 
     public CombinedHistoryRecyclerView(Context context, AttributeSet attributeSet) {
@@ -53,26 +55,36 @@ public class CombinedHistoryRecyclerView
     public void setOnHistoryClickedListener(HomePager.OnUrlOpenListener listener) {
         this.mOnUrlOpenListener = listener;
     }
 
     public void setOnPanelLevelChangeListener(OnPanelLevelChangeListener listener) {
         this.mOnPanelLevelChangeListener = listener;
     }
 
+    public void setHiddenClientsDialogBuilder(CombinedHistoryPanel.DialogBuilder<RemoteClient> builder) {
+        mDialogBuilder = builder;
+    }
+
     @Override
     public void onItemClicked(RecyclerView recyclerView, int position, View v) {
         final int viewType = getAdapter().getItemViewType(position);
         final CombinedHistoryAdapter.ItemType itemType = CombinedHistoryAdapter.ItemType.viewTypeToItemType(viewType);
 
         switch(itemType) {
             case CLIENT:
                 mOnPanelLevelChangeListener.onPanelLevelChange(PanelLevel.CHILD);
                 ((CombinedHistoryAdapter) getAdapter()).showChildView(position);
                 break;
+            case HIDDEN_DEVICES:
+                if (mDialogBuilder != null) {
+                    mDialogBuilder.createAndShowDialog(((CombinedHistoryAdapter) getAdapter()).getHiddenClients());
+                }
+                break;
+
             case NAVIGATION_BACK:
                 mOnPanelLevelChangeListener.onPanelLevelChange(PanelLevel.PARENT);
                 ((CombinedHistoryAdapter) getAdapter()).exitChildView();
                 break;
             case CHILD:
             case HISTORY:
                 if (mOnUrlOpenListener != null) {
                     final TwoLinePageRow historyItem = (TwoLinePageRow) v;
--- a/mobile/android/base/java/org/mozilla/gecko/home/RemoteTabsBaseFragment.java
+++ b/mobile/android/base/java/org/mozilla/gecko/home/RemoteTabsBaseFragment.java
@@ -137,23 +137,16 @@ public abstract class RemoteTabsBaseFrag
         }
 
         // Long pressed item was a remote client; provide the appropriate menu.
         final MenuInflater inflater = new MenuInflater(view.getContext());
         inflater.inflate(R.menu.home_remote_tabs_client_contextmenu, menu);
 
         final RemoteTabsClientContextMenuInfo info = (RemoteTabsClientContextMenuInfo) menuInfo;
         menu.setHeaderTitle(info.client.name);
-
-        // Hide unused menu items.
-        final boolean isHidden = sState.isClientHidden(info.client.guid);
-        final MenuItem item = menu.findItem(isHidden
-                ? R.id.home_remote_tabs_hide_client
-                : R.id.home_remote_tabs_show_client);
-        item.setVisible(false);
     }
 
     @Override
     public boolean onContextItemSelected(MenuItem item) {
         if (super.onContextItemSelected(item)) {
             // HomeFragment was able to handle to selected item.
             return true;
         }
@@ -167,22 +160,16 @@ public abstract class RemoteTabsBaseFrag
 
         final int itemId = item.getItemId();
         if (itemId == R.id.home_remote_tabs_hide_client) {
             sState.setClientHidden(info.client.guid, true);
             getLoaderManager().restartLoader(LOADER_ID_REMOTE_TABS, null, mCursorLoaderCallbacks);
             return true;
         }
 
-        if (itemId == R.id.home_remote_tabs_show_client) {
-            sState.setClientHidden(info.client.guid, false);
-            getLoaderManager().restartLoader(LOADER_ID_REMOTE_TABS, null, mCursorLoaderCallbacks);
-            return true;
-        }
-
         return false;
     }
 
     @Override
     public void onClients(List<RemoteClient> clients) {
         // The clients listed were hidden and have been checked by the user. We
         // interpret that as "show these clients now".
         for (RemoteClient client : clients) {
--- a/mobile/android/base/resources/drawable/action_bar_button.xml
+++ b/mobile/android/base/resources/drawable/action_bar_button.xml
@@ -14,11 +14,11 @@
 
     <item android:state_focused="true"
           android:state_pressed="false">
         <shape>
             <solid android:color="@color/highlight_focused" />
         </shape>
     </item>
 
-    <item android:drawable="@android:color/transparent"/>
+    <item android:drawable="@color/about_page_header_grey"/>
 
 </selector>
copy from mobile/android/base/resources/layout/home_remote_tabs_hidden_devices_footer.xml
copy to mobile/android/base/resources/layout/home_remote_tabs_hidden_devices.xml
--- a/mobile/android/base/resources/layout/home_remote_tabs_hidden_devices_footer.xml
+++ b/mobile/android/base/resources/layout/home_remote_tabs_hidden_devices.xml
@@ -1,28 +1,16 @@
 <?xml version="1.0" encoding="utf-8"?>
 <!--
    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/.
 -->
 
-<!-- This layout is actually necessary because of an interaction
-     between ExpandableListView and onClick handling.  We need a child
-     to attach a click listener to. -->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:gecko="http://schemas.android.com/apk/res-auto"
-    style="@style/Widget.RemoteTabsClientView"
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/hidden_devices"
+    style="@style/Widget.Home.ActionItem"
+    android:background="@drawable/action_bar_button"
     android:layout_width="match_parent"
     android:layout_height="@dimen/home_remote_tabs_hidden_footer_height"
-    android:gravity="center_vertical" >
-
-    <TextView
-        android:id="@+id/hidden_devices"
-        style="@style/Widget.Home.ActionItem"
-        android:background="@drawable/action_bar_button"
-        android:layout_width="match_parent"
-        android:layout_height="match_parent"
-        android:gravity="center"
-        android:maxLength="1024"
-        android:textColor="@color/tabs_tray_icon_grey" />
-
-</LinearLayout>
+    android:gravity="center"
+    android:maxLength="1024"
+    android:textColor="@color/tabs_tray_icon_grey" />
--- a/mobile/android/base/resources/menu/home_remote_tabs_client_contextmenu.xml
+++ b/mobile/android/base/resources/menu/home_remote_tabs_client_contextmenu.xml
@@ -3,12 +3,9 @@
    - 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/. -->
 
 <menu xmlns:android="http://schemas.android.com/apk/res/android">
 
     <item android:id="@+id/home_remote_tabs_hide_client"
           android:title="@string/pref_panels_hide"/>
 
-    <item android:id="@+id/home_remote_tabs_show_client"
-          android:title="@string/pref_panels_show"/>
-
 </menu>