Bug 1057637 - Extract a re-usable ExpandableListAdapter out of RemoteTabsList. r=mcomella

This patch does a few other small things as well:

* It exposes the client device type.  This will be used later, as part of
  the visual refresh.
* Aligns the field names in remote_tabs_child with the names used by
  TwoLinePageRow.  This will be used later, when we finally use said
  standard view class.

--HG--
extra : rebase_source : 291f69a5f9b68801dd9154c5d291c6795d218ff6
This commit is contained in:
Nick Alexander 2014-08-29 11:56:41 -07:00
Родитель 45478db795
Коммит 3f1715f746
5 изменённых файлов: 366 добавлений и 152 удалений

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

@ -0,0 +1,149 @@
/* 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;
import java.util.ArrayList;
import java.util.List;
import org.mozilla.gecko.TabsAccessor.RemoteClient;
import org.mozilla.gecko.TabsAccessor.RemoteTab;
import android.content.Context;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseExpandableListAdapter;
import android.widget.TextView;
/**
* An adapter that populates group and child views with remote client and tab
* data maintained in a monolithic static array.
* <p>
* The group and child view resources are parameters to allow future
* specialization to home fragment styles.
*/
public class RemoteTabsExpandableListAdapter extends BaseExpandableListAdapter {
protected final ArrayList<RemoteClient> clients;
protected int groupLayoutId;
protected int childLayoutId;
/**
* Construct a new adapter.
* <p>
* It's fine to create with clients to be null, and then to use
* {@link RemoteTabsExpandableListAdapter#replaceClients(List)} to
* update this list of clients.
*
* @param groupLayoutId
* @param childLayoutId
* @param clients
* initial list of clients; can be null.
*/
public RemoteTabsExpandableListAdapter(int groupLayoutId, int childLayoutId, List<RemoteClient> clients) {
this.groupLayoutId = groupLayoutId;
this.childLayoutId = childLayoutId;
this.clients = new ArrayList<TabsAccessor.RemoteClient>();
if (clients != null) {
this.clients.addAll(clients);
}
}
public void replaceClients(List<RemoteClient> clients) {
this.clients.clear();
if (clients != null) {
this.clients.addAll(clients);
this.notifyDataSetChanged();
} else {
this.notifyDataSetInvalidated();
}
}
@Override
public boolean hasStableIds() {
return false; // Client GUIDs are stable, but tab hashes are not.
}
@Override
public long getGroupId(int groupPosition) {
return clients.get(groupPosition).guid.hashCode();
}
@Override
public int getGroupCount() {
return clients.size();
}
@Override
public Object getGroup(int groupPosition) {
return clients.get(groupPosition);
}
@Override
public int getChildrenCount(int groupPosition) {
return clients.get(groupPosition).tabs.size();
}
@Override
public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
final Context context = parent.getContext();
final View view;
if (convertView != null) {
view = convertView;
} else {
final LayoutInflater inflater = LayoutInflater.from(context);
view = inflater.inflate(groupLayoutId, parent, false);
}
final RemoteClient client = clients.get(groupPosition);
final TextView nameView = (TextView) view.findViewById(R.id.client);
nameView.setText(client.name);
final TextView lastModifiedView = (TextView) view.findViewById(R.id.last_synced);
final long now = System.currentTimeMillis();
lastModifiedView.setText(TabsAccessor.getLastSyncedString(context, now, client.lastModified));
return view;
}
@Override
public boolean isChildSelectable(int groupPosition, int childPosition) {
return true;
}
@Override
public Object getChild(int groupPosition, int childPosition) {
return clients.get(groupPosition).tabs.get(childPosition);
}
@Override
public long getChildId(int groupPosition, int childPosition) {
return clients.get(groupPosition).tabs.get(childPosition).hashCode();
}
@Override
public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) {
final Context context = parent.getContext();
final View view;
if (convertView != null) {
view = convertView;
} else {
final LayoutInflater inflater = LayoutInflater.from(context);
view = inflater.inflate(childLayoutId, parent, false);
}
final RemoteClient client = clients.get(groupPosition);
final RemoteTab tab = client.tabs.get(childPosition);
final TextView titleView = (TextView) view.findViewById(R.id.title);
titleView.setText(TextUtils.isEmpty(tab.title) ? tab.url : tab.title);
final TextView urlView = (TextView) view.findViewById(R.id.url);
urlView.setText(tab.url);
return view;
}
}

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

@ -4,127 +4,241 @@
package org.mozilla.gecko;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import org.json.JSONArray;
import org.json.JSONException;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.util.UIAsyncTask;
import org.json.JSONArray;
import org.json.JSONException;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.Log;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
public final class TabsAccessor {
private static final String LOGTAG = "GeckoTabsAccessor";
private static final String[] CLIENTS_AVAILABILITY_PROJECTION = new String[] {
BrowserContract.Clients.GUID
};
private static final String[] TABS_PROJECTION_COLUMNS = new String[] {
public static final String[] TABS_PROJECTION_COLUMNS = new String[] {
BrowserContract.Tabs.TITLE,
BrowserContract.Tabs.URL,
BrowserContract.Clients.GUID,
BrowserContract.Clients.NAME,
BrowserContract.Clients.LAST_MODIFIED,
BrowserContract.Clients.DEVICE_TYPE,
};
// Projection column numbers
public static enum TABS_COLUMN {
TITLE,
URL,
GUID,
NAME,
LAST_MODIFIED,
};
private static final String LOCAL_TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NULL";
private static final String REMOTE_TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NOT NULL";
private static final String CLIENTS_SELECTION = BrowserContract.Clients.GUID + " IS NOT NULL";
private static final String TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NOT NULL";
private static final String REMOTE_TABS_SORT_ORDER =
// Most recently synced clients first.
BrowserContract.Clients.LAST_MODIFIED + " DESC, " +
// If two clients somehow had the same last modified time, this will
// group them (arbitrarily).
BrowserContract.Clients.GUID + " DESC, " +
// Within a single client, most recently used tabs first.
BrowserContract.Tabs.LAST_USED + " DESC";
private static final String LOCAL_CLIENT_SELECTION = BrowserContract.Clients.GUID + " IS NULL";
private static final String LOCAL_TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NULL";
private static final Pattern FILTERED_URL_PATTERN = Pattern.compile("^(about|chrome|wyciwyg|file):");
/**
* A thin representation of a remote client.
* <p>
* We use the hash of the client's GUID as the ID in
* {@link RemoteTabsExpandableListAdapter#getGroupId(int)}.
*/
public static class RemoteClient {
public final String guid;
public final String name;
public final long lastModified;
public final String deviceType;
public final ArrayList<RemoteTab> tabs;
public RemoteClient(String guid, String name, long lastModified, String deviceType) {
this.guid = guid;
this.name = name;
this.lastModified = lastModified;
this.deviceType = deviceType;
this.tabs = new ArrayList<RemoteTab>();
}
}
/**
* A thin representation of a remote tab.
* <p>
* We use the hash of the tab as the ID in
* {@link RemoteTabsExpandableListAdapter#getClientId(int)}, and therefore we
* must implement equality as well. These are generated functions.
*/
public static class RemoteTab {
public String title;
public String url;
public String guid;
public String name;
/**
* This is the last time the remote client uploaded a tabs record; that
* is, it is not per tab, but per remote client.
*/
public long lastModified;
public final String title;
public final String url;
public RemoteTab(String title, String url) {
this.title = title;
this.url = url;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((title == null) ? 0 : title.hashCode());
result = prime * result + ((url == null) ? 0 : url.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
RemoteTab other = (RemoteTab) obj;
if (title == null) {
if (other.title != null) {
return false;
}
} else if (!title.equals(other.title)) {
return false;
}
if (url == null) {
if (other.url != null) {
return false;
}
} else if (!url.equals(other.url)) {
return false;
}
return true;
}
}
/**
* Extract client and tab records from a cursor.
* <p>
* The position of the cursor is moved to before the first record before
* reading. The cursor is advanced until there are no more records to be
* read. The position of the cursor is restored before returning.
*
* @param cursor
* to extract records from. The records should already be grouped
* by client GUID.
* @return list of clients, each containing list of tabs.
*/
public static List<RemoteClient> getClientsFromCursor(final Cursor cursor) {
final ArrayList<RemoteClient> clients = new ArrayList<TabsAccessor.RemoteClient>();
final int originalPosition = cursor.getPosition();
try {
if (!cursor.moveToFirst()) {
return clients;
}
final int tabTitleIndex = cursor.getColumnIndex(BrowserContract.Tabs.TITLE);
final int tabUrlIndex = cursor.getColumnIndex(BrowserContract.Tabs.URL);
final int clientGuidIndex = cursor.getColumnIndex(BrowserContract.Clients.GUID);
final int clientNameIndex = cursor.getColumnIndex(BrowserContract.Clients.NAME);
final int clientLastModifiedIndex = cursor.getColumnIndex(BrowserContract.Clients.LAST_MODIFIED);
final int clientDeviceTypeIndex = cursor.getColumnIndex(BrowserContract.Clients.DEVICE_TYPE);
// A walking partition, chunking by client GUID. We assume the
// cursor records are already grouped by client GUID; see the query
// sort order.
RemoteClient lastClient = null;
while (!cursor.isAfterLast()) {
final String clientGuid = cursor.getString(clientGuidIndex);
if (lastClient == null || !TextUtils.equals(lastClient.guid, clientGuid)) {
final String clientName = cursor.getString(clientNameIndex);
final long lastModified = cursor.getLong(clientLastModifiedIndex);
final String deviceType = cursor.getString(clientDeviceTypeIndex);
lastClient = new RemoteClient(clientGuid, clientName, lastModified, deviceType);
clients.add(lastClient);
}
final String tabTitle = cursor.getString(tabTitleIndex);
final String tabUrl = cursor.getString(tabUrlIndex);
lastClient.tabs.add(new RemoteTab(tabTitle, tabUrl));
cursor.moveToNext();
}
} finally {
cursor.moveToPosition(originalPosition);
}
return clients;
}
public static Cursor getRemoteTabsCursor(Context context) {
return getRemoteTabsCursor(context, -1);
}
public static Cursor getRemoteTabsCursor(Context context, int limit) {
Uri uri = BrowserContract.Tabs.CONTENT_URI;
if (limit > 0) {
uri = uri.buildUpon()
.appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(limit))
.build();
}
final Cursor cursor = context.getContentResolver().query(uri,
TABS_PROJECTION_COLUMNS,
REMOTE_TABS_SELECTION,
null,
REMOTE_TABS_SORT_ORDER);
return cursor;
}
public interface OnQueryTabsCompleteListener {
public void onQueryTabsComplete(List<RemoteTab> tabs);
public void onQueryTabsComplete(List<RemoteClient> clients);
}
// This method returns all tabs from all remote clients,
// ordered by most recent client first, most recent tab first
// This method returns all tabs from all remote clients,
// ordered by most recent client first, most recent tab first
public static void getTabs(final Context context, final OnQueryTabsCompleteListener listener) {
getTabs(context, 0, listener);
}
// This method returns limited number of tabs from all remote clients,
// ordered by most recent client first, most recent tab first
// This method returns limited number of tabs from all remote clients,
// ordered by most recent client first, most recent tab first
public static void getTabs(final Context context, final int limit, final OnQueryTabsCompleteListener listener) {
// If there is no listener, no point in doing work.
if (listener == null)
return;
(new UIAsyncTask.WithoutParams<List<RemoteTab>>(ThreadUtils.getBackgroundHandler()) {
(new UIAsyncTask.WithoutParams<List<RemoteClient>>(ThreadUtils.getBackgroundHandler()) {
@Override
protected List<RemoteTab> doInBackground() {
Uri uri = BrowserContract.Tabs.CONTENT_URI;
if (limit > 0) {
uri = uri.buildUpon()
.appendQueryParameter(BrowserContract.PARAM_LIMIT, String.valueOf(limit))
.build();
}
Cursor cursor = context.getContentResolver().query(uri,
TABS_PROJECTION_COLUMNS,
TABS_SELECTION,
null,
null);
protected List<RemoteClient> doInBackground() {
final Cursor cursor = getRemoteTabsCursor(context, limit);
if (cursor == null)
return null;
RemoteTab tab;
final ArrayList<RemoteTab> tabs = new ArrayList<RemoteTab> ();
try {
while (cursor.moveToNext()) {
tab = new RemoteTab();
tab.title = cursor.getString(TABS_COLUMN.TITLE.ordinal());
tab.url = cursor.getString(TABS_COLUMN.URL.ordinal());
tab.guid = cursor.getString(TABS_COLUMN.GUID.ordinal());
tab.name = cursor.getString(TABS_COLUMN.NAME.ordinal());
tab.lastModified = cursor.getLong(TABS_COLUMN.LAST_MODIFIED.ordinal());
tabs.add(tab);
}
try {
return Collections.unmodifiableList(getClientsFromCursor(cursor));
} finally {
cursor.close();
}
return Collections.unmodifiableList(tabs);
}
}
@Override
protected void onPostExecute(List<RemoteTab> tabs) {
listener.onQueryTabsComplete(tabs);
protected void onPostExecute(List<RemoteClient> clients) {
listener.onQueryTabsComplete(clients);
}
}).execute();
}
@ -210,4 +324,16 @@ public final class TabsAccessor {
private static boolean isFilteredURL(String url) {
return FILTERED_URL_PATTERN.matcher(url).lookingAt();
}
/**
* Return a relative "Last synced" time span for the given tab record.
*
* @param now local time.
* @param time to format string for.
* @return string describing time span
*/
public static String getLastSyncedString(Context context, long now, long time) {
final CharSequence relativeTimeSpanString = DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS);
return context.getResources().getString(R.string.remote_tabs_last_synced, relativeTimeSpanString);
}
}

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

@ -368,6 +368,7 @@ gbjar.sources += [
'prompts/PromptService.java',
'prompts/TabInput.java',
'ReaderModeUtils.java',
'RemoteTabsExpandableListAdapter.java',
'Restarter.java',
'ScrollAnimator.java',
'ServiceNotificationClient.java',

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

@ -12,7 +12,7 @@
android:paddingLeft="4dp"
android:paddingRight="4dp">
<TextView android:id="@+id/tab"
<TextView android:id="@+id/title"
style="@style/TabRowTextAppearance"
android:layout_width="match_parent"
android:layout_height="wrap_content"

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

@ -5,42 +5,32 @@
package org.mozilla.gecko.tabs;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import org.mozilla.gecko.R;
import org.mozilla.gecko.RemoteTabsExpandableListAdapter;
import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.TabsAccessor;
import org.mozilla.gecko.TabsAccessor.RemoteClient;
import org.mozilla.gecko.TabsAccessor.RemoteTab;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import android.content.Context;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ExpandableListView;
import android.widget.SimpleExpandableListAdapter;
/**
* The actual list of synced tabs. This serves as the only child view of {@link RemoteTabsContainerPanel}
* so it can be refreshed using a swipe-to-refresh gesture.
*/
class RemoteTabsList extends ExpandableListView
implements ExpandableListView.OnGroupClickListener,
ExpandableListView.OnChildClickListener,
TabsAccessor.OnQueryTabsCompleteListener {
private static final String[] CLIENT_KEY = new String[] { "name", "last_synced" };
private static final String[] TAB_KEY = new String[] { "title", "url" };
private static final int[] CLIENT_RESOURCE = new int[] { R.id.client, R.id.last_synced };
private static final int[] TAB_RESOURCE = new int[] { R.id.tab, R.id.url };
private final Context context;
public class RemoteTabsList extends ExpandableListView
implements ExpandableListView.OnGroupClickListener,
ExpandableListView.OnChildClickListener,
TabsAccessor.OnQueryTabsCompleteListener {
private TabsPanel tabsPanel;
private ArrayList <HashMap <String, String>> clients;
private ArrayList <ArrayList <HashMap <String, String>>> tabsList;
// A list of the clients that are currently expanded.
private List<String> expandedClientList;
@ -49,10 +39,10 @@ class RemoteTabsList extends ExpandableListView
public RemoteTabsList(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
setOnGroupClickListener(this);
setOnChildClickListener(this);
setAdapter(new RemoteTabsExpandableListAdapter(R.layout.remote_tabs_group, R.layout.remote_tabs_child, null));
}
public void setTabsPanel(TabsPanel panel) {
@ -65,7 +55,8 @@ class RemoteTabsList extends ExpandableListView
@Override
public boolean onGroupClick(ExpandableListView parent, View view, int groupPosition, long id) {
final String clientGuid = clients.get(groupPosition).get("guid");
final RemoteClient client = (RemoteClient) parent.getExpandableListAdapter().getGroup(groupPosition);
final String clientGuid = client.guid;
if (isGroupExpanded(groupPosition)) {
collapseGroup(groupPosition);
@ -81,7 +72,7 @@ class RemoteTabsList extends ExpandableListView
@Override
public boolean onChildClick(ExpandableListView parent, View view, int groupPosition, int childPosition, long id) {
HashMap <String, String> tab = tabsList.get(groupPosition).get(childPosition);
final RemoteTab tab = (RemoteTab) parent.getExpandableListAdapter().getChild(groupPosition, childPosition);
if (tab == null) {
autoHidePanel();
return true;
@ -89,64 +80,23 @@ class RemoteTabsList extends ExpandableListView
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, "remote");
Tabs.getInstance().loadUrl(tab.get("url"), Tabs.LOADURL_NEW_TAB);
Tabs.getInstance().loadUrl(tab.url, Tabs.LOADURL_NEW_TAB);
autoHidePanel();
clientScrollPosition = clients.get(groupPosition).get("guid");
final RemoteClient client = (RemoteClient) parent.getExpandableListAdapter().getGroup(groupPosition);
clientScrollPosition = client.guid;
return true;
}
@Override
public void onQueryTabsComplete(List<TabsAccessor.RemoteTab> remoteTabsList) {
ArrayList<TabsAccessor.RemoteTab> remoteTabs = new ArrayList<TabsAccessor.RemoteTab> (remoteTabsList);
if (remoteTabs == null || remoteTabs.size() == 0)
return;
clients = new ArrayList <HashMap <String, String>>();
tabsList = new ArrayList <ArrayList <HashMap <String, String>>>();
String oldGuid = null;
ArrayList <HashMap <String, String>> tabsForClient = null;
HashMap <String, String> client;
HashMap <String, String> tab;
final long now = System.currentTimeMillis();
for (TabsAccessor.RemoteTab remoteTab : remoteTabs) {
final String clientGuid = remoteTab.guid;
if (oldGuid == null || !TextUtils.equals(oldGuid, clientGuid)) {
client = new HashMap <String, String>();
client.put("name", remoteTab.name);
client.put("last_synced", getLastSyncedString(now, remoteTab.lastModified));
client.put("guid", clientGuid);
clients.add(client);
tabsForClient = new ArrayList <HashMap <String, String>>();
tabsList.add(tabsForClient);
oldGuid = new String(clientGuid);
}
tab = new HashMap<String, String>();
tab.put("title", TextUtils.isEmpty(remoteTab.title) ? remoteTab.url : remoteTab.title);
tab.put("url", remoteTab.url);
tabsForClient.add(tab);
}
setAdapter(new SimpleExpandableListAdapter(context,
clients,
R.layout.remote_tabs_group,
CLIENT_KEY,
CLIENT_RESOURCE,
tabsList,
R.layout.remote_tabs_child,
TAB_KEY,
TAB_RESOURCE));
public void onQueryTabsComplete(List<RemoteClient> clients) {
((RemoteTabsExpandableListAdapter) getExpandableListAdapter()).replaceClients(clients);
// Either set the initial UI state, or restore it.
List<String> newExpandedClientList = new ArrayList<String>();
for (int i = 0; i < clients.size(); i++) {
final String clientGuid = clients.get(i).get("guid");
final String clientGuid = clients.get(i).guid;
if (expandedClientList == null) {
// On initial entry we expand all clients by default.
@ -167,16 +117,4 @@ class RemoteTabsList extends ExpandableListView
}
expandedClientList = newExpandedClientList;
}
/**
* Return a relative "Last synced" time span for the given tab record.
*
* @param now local time.
* @param time to format string for.
* @return string describing time span
*/
protected String getLastSyncedString(long now, long time) {
CharSequence relativeTimeSpanString = DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS);
return getResources().getString(R.string.remote_tabs_last_synced, relativeTimeSpanString);
}
}