Bug 977161 - Part 2: Add a clickable footer showing hidden device count. r=rnewman

This is quite delicate, due to the interactions between
ExpandableListView and the footer views.  At the end of the day, this
uses an Adapter that handles header and footer views as separate
view-group types.  That means working around Android's limitations; see
the comments in the code for the details.

The alternative would be to implement our own wrapping Adapter; or to
boil the specifics of our use case into RemoteTabsExpandableListAdapter.
This is certainly quicker than the former; and, I hope, less error prone
than the latter.
This commit is contained in:
Nick Alexander 2014-09-16 15:41:13 -07:00
parent f5ab0b7ea2
commit b226800b48
5 changed files with 126 additions and 7 deletions

View File

@ -5,7 +5,9 @@
package org.mozilla.gecko.home;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.List;
import org.mozilla.gecko.GeckoSharedPrefs;
@ -34,6 +36,7 @@ import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.ExpandableListAdapter;
@ -64,12 +67,19 @@ public class RemoteTabsExpandableListFragment extends HomeFragment {
// Adapter for the list of remote tabs.
private RemoteTabsExpandableListAdapter mAdapter;
// List of hidden remote clients.
// Only accessed from the UI thread.
private final ArrayList<RemoteClient> mHiddenClients = new ArrayList<RemoteClient>();
// The view shown by the fragment.
private HomeExpandableListView mList;
// Reference to the View to display when there are no results.
private View mEmptyView;
// The footer view to display when there are hidden devices not shown.
private View mFooterView;
// Callbacks used for the loader.
private CursorLoaderCallbacks mCursorLoaderCallbacks;
@ -156,9 +166,8 @@ public class RemoteTabsExpandableListFragment extends HomeFragment {
final RemoteClient client = (RemoteClient) adapter.getGroup(groupPosition);
final RemoteTabsClientContextMenuInfo info = new RemoteTabsClientContextMenuInfo(view, position, id, client);
return info;
} else {
return null;
}
return null;
}
});
@ -190,10 +199,36 @@ public class RemoteTabsExpandableListFragment extends HomeFragment {
sState = new RemoteTabsExpandableListState(GeckoSharedPrefs.forProfile(getActivity()));
}
// There is an unfortunate interaction between ExpandableListViews and
// footer onClick handling. The footer view itself appears to not
// receive click events. Its children, however, do receive click events.
// Therefore, we attach an onClick handler to a child of the footer view
// itself.
mFooterView = LayoutInflater.from(getActivity()).inflate(R.layout.home_remote_tabs_hidden_devices_footer, mList, false);
final View view = mFooterView.findViewById(R.id.hidden_devices);
view.setClickable(true);
view.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// Nothing for now. This will be fleshed out in the next commits.
}
});
// There is a delicate interaction, pre-KitKat, between
// {add,remove}FooterView and setAdapter. setAdapter wraps the adapter
// in a footer/header-managing adapter, which only happens (pre-KitKat)
// if a footer/header is present. Therefore, we add our footer before
// setting the adapter; and then we remove it afterward. From there on,
// we can add/remove it at will.
mList.addFooterView(mFooterView, null, true);
// Intialize adapter
mAdapter = new RemoteTabsExpandableListAdapter(R.layout.home_remote_tabs_group, R.layout.home_remote_tabs_child, null);
mList.setAdapter(mAdapter);
// Now the adapter is wrapped; we can remove our footer view.
mList.removeFooterView(mFooterView);
// Create callbacks before the initial loader is started
mCursorLoaderCallbacks = new CursorLoaderCallbacks();
loadIfVisible();
@ -240,17 +275,45 @@ public class RemoteTabsExpandableListFragment extends HomeFragment {
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;
} else 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;
} else {
return false;
}
return false;
}
private void updateUiFromClients(List<RemoteClient> clients) {
private void updateUiFromClients(List<RemoteClient> clients, List<RemoteClient> hiddenClients) {
// We have three states: no clients (including hidden clients) at all;
// all clients hidden; some clients hidden. We want to show the empty
// list view only when we have no clients at all. This flag
// differentiates the first from the latter two states.
boolean displayedSomeClients = false;
if (hiddenClients == null || hiddenClients.isEmpty()) {
mList.removeFooterView(mFooterView);
} else {
displayedSomeClients = true;
final TextView textView = (TextView) mFooterView.findViewById(R.id.hidden_devices);
if (hiddenClients.size() == 1) {
textView.setText(getResources().getString(R.string.home_remote_tabs_one_hidden_device));
} else {
textView.setText(getResources().getString(R.string.home_remote_tabs_many_hidden_devices, hiddenClients.size()));
}
// This is a simple, if not very future-proof, way to determine if
// the footer view has already been added to the list view.
if (mList.getFooterViewsCount() < 1) {
mList.addFooterView(mFooterView);
}
}
if (clients != null && !clients.isEmpty()) {
displayedSomeClients = true;
// No sense crashing if we've made an error.
int groupCount = Math.min(mList.getExpandableListAdapter().getGroupCount(), clients.size());
for (int i = 0; i < groupCount; i++) {
@ -261,10 +324,14 @@ public class RemoteTabsExpandableListFragment extends HomeFragment {
mList.expandGroup(i);
}
}
}
if (displayedSomeClients) {
return;
}
// Cursor is empty, so set the empty view if it hasn't been set already.
// No clients shown, not even hidden clients. Set the empty view if it
// hasn't been set already.
if (mEmptyView == null) {
// Set empty panel view. We delay this so that the empty view won't flash.
final ViewStub emptyViewStub = (ViewStub) getView().findViewById(R.id.home_empty_view_stub);
@ -305,8 +372,22 @@ public class RemoteTabsExpandableListFragment extends HomeFragment {
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
final List<RemoteClient> clients = TabsAccessor.getClientsFromCursor(c);
// Filter the hidden clients out of the clients list. The clients
// list is updated in place; the hidden clients list is built
// incrementally.
mHiddenClients.clear();
final Iterator<RemoteClient> it = clients.iterator();
while (it.hasNext()) {
final RemoteClient client = it.next();
if (sState.isClientHidden(client.guid)) {
it.remove();
mHiddenClients.add(client);
}
}
mAdapter.replaceClients(clients);
updateUiFromClients(clients);
updateUiFromClients(clients, mHiddenClients);
}
@Override

View File

@ -413,6 +413,13 @@ size. -->
<!ENTITY home_remote_tabs_trouble_verifying "Trouble verifying your account?">
<!ENTITY home_remote_tabs_need_to_verify "Please verify your Firefox Account to start syncing.">
<!ENTITY home_remote_tabs_one_hidden_device "1 device hidden">
<!-- Localization note (home_remote_tabs_many_hidden_devices) : The
formatD is replaced with the number of hidden devices. The
number of hidden devices is always more than one. We can't use
Android plural forms, sadly. See Bug #753859. -->
<!ENTITY home_remote_tabs_many_hidden_devices "&formatD; devices hidden">
<!ENTITY private_browsing_title "Private Browsing">
<!ENTITY private_tabs_panel_empty_desc "Your private tabs will show up here. While we don\'t keep any of your browsing history or cookies, bookmarks and files that you download will still be saved on your device.">
<!ENTITY private_tabs_panel_learn_more "Want to learn more?">

View File

@ -0,0 +1,26 @@
<?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"
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:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:maxLength="1024" />
</LinearLayout>

View File

@ -38,6 +38,9 @@
<!-- Remote Tabs static view top padding. Less in landscape on phones. -->
<dimen name="home_remote_tabs_top_padding">48dp</dimen>
<!-- Remote Tabs Hidden devices row height -->
<dimen name="home_remote_tabs_hidden_footer_height">48dp</dimen>
<!-- Search Engine Row height -->
<dimen name="search_row_height">48dp</dimen>

View File

@ -359,6 +359,8 @@
<string name="home_remote_tabs_need_to_sign_in">&home_remote_tabs_need_to_sign_in;</string>
<string name="home_remote_tabs_trouble_verifying">&home_remote_tabs_trouble_verifying;</string>
<string name="home_remote_tabs_need_to_verify">&home_remote_tabs_need_to_verify;</string>
<string name="home_remote_tabs_one_hidden_device">&home_remote_tabs_one_hidden_device;</string>
<string name="home_remote_tabs_many_hidden_devices">&home_remote_tabs_many_hidden_devices;</string>
<string name="private_browsing_title">&private_browsing_title;</string>
<string name="private_tabs_panel_empty_desc">&private_tabs_panel_empty_desc;</string>
<string name="private_tabs_panel_learn_more">&private_tabs_panel_learn_more;</string>