From 3f1715f74690dc44265191c784e9544b57b563ab Mon Sep 17 00:00:00 2001 From: Nick Alexander Date: Fri, 29 Aug 2014 11:56:41 -0700 Subject: [PATCH] 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 --- .../base/RemoteTabsExpandableListAdapter.java | 149 ++++++++++ mobile/android/base/TabsAccessor.java | 268 +++++++++++++----- mobile/android/base/moz.build | 1 + .../resources/layout/remote_tabs_child.xml | 2 +- mobile/android/base/tabs/RemoteTabsList.java | 98 ++----- 5 files changed, 366 insertions(+), 152 deletions(-) create mode 100644 mobile/android/base/RemoteTabsExpandableListAdapter.java diff --git a/mobile/android/base/RemoteTabsExpandableListAdapter.java b/mobile/android/base/RemoteTabsExpandableListAdapter.java new file mode 100644 index 000000000000..94f25bd7488f --- /dev/null +++ b/mobile/android/base/RemoteTabsExpandableListAdapter.java @@ -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. + *

+ * The group and child view resources are parameters to allow future + * specialization to home fragment styles. + */ +public class RemoteTabsExpandableListAdapter extends BaseExpandableListAdapter { + protected final ArrayList clients; + protected int groupLayoutId; + protected int childLayoutId; + + /** + * Construct a new adapter. + *

+ * 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 clients) { + this.groupLayoutId = groupLayoutId; + this.childLayoutId = childLayoutId; + this.clients = new ArrayList(); + if (clients != null) { + this.clients.addAll(clients); + } + } + + public void replaceClients(List 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; + } +} diff --git a/mobile/android/base/TabsAccessor.java b/mobile/android/base/TabsAccessor.java index 9247b755c093..5cd29c2a164f 100644 --- a/mobile/android/base/TabsAccessor.java +++ b/mobile/android/base/TabsAccessor.java @@ -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. + *

+ * 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 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(); + } + } + + /** + * A thin representation of a remote tab. + *

+ * 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. + *

+ * 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 getClientsFromCursor(final Cursor cursor) { + final ArrayList clients = new ArrayList(); + + 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 tabs); + public void onQueryTabsComplete(List 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>(ThreadUtils.getBackgroundHandler()) { + (new UIAsyncTask.WithoutParams>(ThreadUtils.getBackgroundHandler()) { @Override - protected List 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 doInBackground() { + final Cursor cursor = getRemoteTabsCursor(context, limit); if (cursor == null) return null; - - RemoteTab tab; - final ArrayList tabs = new ArrayList (); - 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 tabs) { - listener.onQueryTabsComplete(tabs); + protected void onPostExecute(List 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); + } } diff --git a/mobile/android/base/moz.build b/mobile/android/base/moz.build index 12f53223796e..4aee57bd0061 100644 --- a/mobile/android/base/moz.build +++ b/mobile/android/base/moz.build @@ -368,6 +368,7 @@ gbjar.sources += [ 'prompts/PromptService.java', 'prompts/TabInput.java', 'ReaderModeUtils.java', + 'RemoteTabsExpandableListAdapter.java', 'Restarter.java', 'ScrollAnimator.java', 'ServiceNotificationClient.java', diff --git a/mobile/android/base/resources/layout/remote_tabs_child.xml b/mobile/android/base/resources/layout/remote_tabs_child.xml index 4da264407ebc..75617fb81661 100644 --- a/mobile/android/base/resources/layout/remote_tabs_child.xml +++ b/mobile/android/base/resources/layout/remote_tabs_child.xml @@ -12,7 +12,7 @@ android:paddingLeft="4dp" android:paddingRight="4dp"> - > clients; - private ArrayList >> tabsList; - // A list of the clients that are currently expanded. private List 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 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 remoteTabsList) { - ArrayList remoteTabs = new ArrayList (remoteTabsList); - if (remoteTabs == null || remoteTabs.size() == 0) - return; - - clients = new ArrayList >(); - tabsList = new ArrayList >>(); - - String oldGuid = null; - ArrayList > tabsForClient = null; - HashMap client; - HashMap 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 (); - client.put("name", remoteTab.name); - client.put("last_synced", getLastSyncedString(now, remoteTab.lastModified)); - client.put("guid", clientGuid); - clients.add(client); - - tabsForClient = new ArrayList >(); - tabsList.add(tabsForClient); - - oldGuid = new String(clientGuid); - } - - tab = new HashMap(); - 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 clients) { + ((RemoteTabsExpandableListAdapter) getExpandableListAdapter()).replaceClients(clients); // Either set the initial UI state, or restore it. List newExpandedClientList = new ArrayList(); 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); - } }