Bug 1220928 - Add client hiding/showing. r=sebastian

MozReview-Commit-ID: IvyrslMqTOT

--HG--
rename : mobile/android/base/resources/layout/home_remote_tabs_hidden_devices_footer.xml => mobile/android/base/resources/layout/home_remote_tabs_hidden_devices.xml
extra : rebase_source : 0090df7352b4378040cef9c2288d28452e7aadea
extra : source : 81b90ed9ff464c38667488a5d4892728a802bdb0
This commit is contained in:
Chenxia Liu 2016-03-29 17:10:33 -07:00
Родитель 959ca283cb
Коммит 4ed44674ba
8 изменённых файлов: 265 добавлений и 37 удалений

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

@ -15,6 +15,7 @@ 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;
@ -23,13 +24,14 @@ 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) {
@ -47,6 +49,13 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
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;
@ -58,11 +67,38 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
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) {
@ -71,6 +107,74 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
notifyDataSetChanged();
}
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;
case HISTORY:
notifyItemRemoved(position);
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();
@ -88,7 +192,7 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
}
// 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();
}
@ -99,7 +203,21 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
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) {
@ -112,7 +230,7 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
}
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:
@ -120,8 +238,12 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
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;
}
@ -138,13 +260,17 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
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:
@ -165,10 +291,13 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
} 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);
}
@ -182,9 +311,8 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
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();
}
}
@ -229,8 +357,8 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
@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:
@ -239,6 +367,11 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
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);
@ -248,6 +381,7 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
((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);

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

@ -21,8 +21,8 @@ public abstract class CombinedHistoryItem extends RecyclerView.ViewHolder {
super(view);
}
public static class SectionItem extends CombinedHistoryItem {
public SectionItem(View view) {
public static class BasicItem extends CombinedHistoryItem {
public BasicItem(View view) {
super(view);
}
}

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

@ -20,7 +20,10 @@ 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;
@ -34,6 +37,7 @@ import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.GeckoAppShell;
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;
@ -45,10 +49,11 @@ 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;
@ -107,6 +112,7 @@ public class CombinedHistoryPanel extends HomeFragment {
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);
@ -171,7 +177,6 @@ public class CombinedHistoryPanel extends HomeFragment {
return SectionHeader.OLDER_THAN_SIX_MONTHS;
}
private class CursorLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
private BrowserDB mDB; // Pseudo-final: set in onCreateLoader.
@ -192,6 +197,7 @@ public class CombinedHistoryPanel extends HomeFragment {
}
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
final int loaderId = loader.getId();
switch (loaderId) {
@ -201,7 +207,7 @@ public class CombinedHistoryPanel extends HomeFragment {
case LOADER_ID_REMOTE:
final List<RemoteClient> clients = mDB.getTabsAccessor().getClientsFromCursor(c);
// TODO: Handle hidden clients
mAdapter.setClients(clients);
break;
}
@ -211,7 +217,8 @@ public class CombinedHistoryPanel extends HomeFragment {
updateEmptyView(mAdapter.getItemCount() == 0);
}
public void onLoaderReset(Loader<Cursor> c) {
@Override
public void onLoaderReset(Loader<Cursor> loader) {
mAdapter.setClients(Collections.<RemoteClient>emptyList());
mAdapter.setHistory(null);
}
@ -270,7 +277,7 @@ public class CombinedHistoryPanel extends HomeFragment {
Log.e(LOGTAG, "JSON error", e);
}
GeckoAppShell.notifyObservers("Sanitize:ClearData", json.toString());;
GeckoAppShell.notifyObservers("Sanitize:ClearData", json.toString());
Telemetry.sendUIEvent(TelemetryContract.Event.SANITIZE, TelemetryContract.Method.BUTTON, "history");
}
});
@ -285,7 +292,7 @@ public class CombinedHistoryPanel extends HomeFragment {
try {
message.put("urls", tabUrls);
message.put("shouldNotifyTabsOpenedToJava", false);
GeckoAppShell.notifyObservers("Tabs:OpenMultiple", message.toString());;
GeckoAppShell.notifyObservers("Tabs:OpenMultiple", message.toString());
} catch (JSONException e) {
Log.e(LOGTAG, "Error making JSON message to open tabs");
}
@ -386,6 +393,81 @@ public class CombinedHistoryPanel extends HomeFragment {
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));

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

@ -10,6 +10,7 @@ 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;
@ -23,6 +24,7 @@ public class CombinedHistoryRecyclerView extends RecyclerView
protected HomePager.OnUrlOpenListener mOnUrlOpenListener;
protected OnPanelLevelChangeListener mOnPanelLevelChangeListener;
protected CombinedHistoryPanel.DialogBuilder<RemoteClient> mDialogBuilder;
protected HomeContextMenuInfo mContextMenuInfo;
public CombinedHistoryRecyclerView(Context context) {
@ -58,6 +60,10 @@ public class CombinedHistoryRecyclerView extends RecyclerView
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);
@ -68,6 +74,12 @@ public class CombinedHistoryRecyclerView extends RecyclerView
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();

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

@ -142,13 +142,6 @@ public abstract class RemoteTabsBaseFragment extends HomeFragment implements Rem
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
@ -172,12 +165,6 @@ public abstract class RemoteTabsBaseFragment extends HomeFragment implements Rem
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;
}

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

@ -19,6 +19,6 @@
</shape>
</item>
<item android:drawable="@android:color/transparent"/>
<item android:drawable="@color/about_page_header_grey"/>
</selector>

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

@ -0,0 +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/.
-->
<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"
android:maxLength="1024"
android:textColor="@color/tabs_tray_icon_grey" />

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

@ -8,7 +8,4 @@
<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>