Group list headers and selection

- Sort groups according to account
- Add headers to groups in same account with # of groups in that account
- Add "selection" background on list item in tablet, but disable it on
the phone
- Misc: enable fast scroll, move icon to right

Change-Id: I1c83aa686de2431b3483a1591ecad7e9e6893cdc
diff --git a/res/layout/group_browse_list_fragment.xml b/res/layout/group_browse_list_fragment.xml
index 50c02c8..d41772d 100644
--- a/res/layout/group_browse_list_fragment.xml
+++ b/res/layout/group_browse_list_fragment.xml
@@ -26,7 +26,9 @@
       android:layout_height="0dip"
       android:fastScrollEnabled="true"
       android:scrollbarStyle="outsideOverlay"
-      android:layout_weight="1" />
+      android:layout_weight="1"
+      android:cacheColorHint="@android:color/transparent"
+      android:divider="@null" />
 
    <TextView
      android:id="@+id/empty"
diff --git a/res/layout/group_browse_list_item.xml b/res/layout/group_browse_list_item.xml
index 28f4e17..d94d444 100644
--- a/res/layout/group_browse_list_item.xml
+++ b/res/layout/group_browse_list_item.xml
@@ -19,24 +19,15 @@
     class="com.android.contacts.group.GroupBrowseListAdapter$GroupListItem"
     android:orientation="horizontal"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content">
-
-    <ImageView
-        android:id="@+id/icon"
-        android:scaleType="center"
-        android:layout_width="wrap_content"
-        android:layout_height="match_parent"
-        android:layout_marginLeft="10dip"
-        android:layout_marginRight="10dip"
-        android:layout_gravity="center_vertical"
-        android:src="@drawable/ic_menu_display_all_holo_light" />
+    android:layout_height="wrap_content"
+    style="@style/GroupBrowseListItem">
 
     <LinearLayout
         android:orientation="vertical"
-        android:layout_width="wrap_content"
+        android:layout_width="0dip"
         android:layout_height="match_parent"
-        android:paddingTop="5dip"
-        android:paddingBottom="5dip">
+        android:layout_weight="1"
+        android:padding="5dip">
 
         <TextView
             android:id="@+id/label"
@@ -60,4 +51,13 @@
 
     </LinearLayout>
 
+    <ImageView
+        android:id="@+id/icon"
+        android:scaleType="center"
+        android:layout_width="wrap_content"
+        android:layout_height="match_parent"
+        android:layout_marginRight="20dip"
+        android:layout_gravity="center_vertical"
+        android:src="@drawable/ic_menu_display_all_holo_light" />
+
 </view>
diff --git a/res/values-xlarge/styles.xml b/res/values-xlarge/styles.xml
index 0179f6a..f73e51d 100644
--- a/res/values-xlarge/styles.xml
+++ b/res/values-xlarge/styles.xml
@@ -116,4 +116,8 @@
         <item name="android:gravity">center_vertical</item>
         <item name="android:paddingTop">5dip</item>
     </style>
+
+    <style name="GroupBrowseListItem">
+        <item name="android:background">@drawable/list_item_activated_background</item>
+    </style>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 77863d5..52facac 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1497,6 +1497,12 @@
     <!-- Title of the dialog that allows deletion of a contact group [CHAR LIMIT=128] -->
     <string name="delete_group_dialog_title">Delete group</string>
 
+    <!-- Shows how many groups are from the specified account [CHAR LIMIT=15] -->
+    <plurals name="num_groups_in_account">
+        <item quantity="one">1 group</item>
+        <item quantity="other"><xliff:g id="count">%0$d</xliff:g> groups</item>
+    </plurals>
+
     <!-- Confirmation message of the dialog that allows deletion of a contact group  [CHAR LIMIT=256] -->
     <string name="delete_group_dialog_message">Are you sure you want to delete the group 
       \'<xliff:g id="group_label" example="Friends">%1$s</xliff:g>\'?
diff --git a/res/values/styles.xml b/res/values/styles.xml
index f18a340..509180e 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -322,4 +322,8 @@
         <item name="android:padding">5dip</item>
         <item name="android:background">@drawable/list_selector</item>
     </style>
+
+    <style name="GroupBrowseListItem">
+        <item name="android:paddingRight">20dip</item>
+    </style>
 </resources>
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index 502c31d..4b0f0e4 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -408,13 +408,14 @@
             }
 
             mListFragment.setContactsRequest(mRequest);
-            configureListFragmentForRequest();
+            configureContactListFragmentForRequest();
 
         } else {
             mSearchMode = mActionBarAdapter.isSearchMode();
         }
 
-        configureListFragment();
+        configureContactListFragment();
+        configureGroupListFragment();
 
         invalidateOptionsMenu();
     }
@@ -478,7 +479,7 @@
         }
     }
 
-    private void configureListFragmentForRequest() {
+    private void configureContactListFragmentForRequest() {
         Uri contactUri = mRequest.getContactUri();
         if (contactUri != null) {
             mListFragment.setSelectedContactUri(contactUri);
@@ -498,7 +499,7 @@
         }
     }
 
-    private void configureListFragment() {
+    private void configureContactListFragment() {
         mListFragment.setSearchMode(mSearchMode);
 
         mListFragment.setVisibleScrollbarEnabled(!mSearchMode);
@@ -510,6 +511,14 @@
         mListFragment.setQuickContactEnabled(!mContentPaneDisplayed);
     }
 
+    private void configureGroupListFragment() {
+        mGroupsFragment.setVerticalScrollbarPosition(
+                mContentPaneDisplayed
+                        ? View.SCROLLBAR_POSITION_LEFT
+                        : View.SCROLLBAR_POSITION_RIGHT);
+        mGroupsFragment.setSelectionVisible(mContentPaneDisplayed);
+    }
+
     @Override
     public void onProviderStatusChange() {
         updateFragmentVisibility();
diff --git a/src/com/android/contacts/group/GroupBrowseListAdapter.java b/src/com/android/contacts/group/GroupBrowseListAdapter.java
index c17fc08..b836ab9 100644
--- a/src/com/android/contacts/group/GroupBrowseListAdapter.java
+++ b/src/com/android/contacts/group/GroupBrowseListAdapter.java
@@ -18,6 +18,7 @@
 
 import com.android.contacts.GroupMetaData;
 import com.android.contacts.R;
+import com.android.contacts.list.ContactListPinnedHeaderView;
 
 import android.content.ContentUris;
 import android.content.Context;
@@ -31,19 +32,55 @@
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Adapter to populate the list of groups.
  */
 public class GroupBrowseListAdapter extends BaseAdapter {
 
+    private Context mContext;
     private LayoutInflater mLayoutInflater;
-    private List<GroupMetaData> mGroupList;
 
-    public GroupBrowseListAdapter(Context context, List<GroupMetaData> groupList) {
+    private List<GroupListEntry> mGroupList = new ArrayList<GroupListEntry>();
+    private boolean mSelectionVisible;
+    private Uri mSelectedGroupUri;
+
+    enum ViewType {
+        HEADER, ITEM;
+    }
+
+    private static final int VIEW_TYPE_COUNT = ViewType.values().length;
+
+    public GroupBrowseListAdapter(Context context, Map<String, List<GroupMetaData>> groupMap) {
+        mContext = context;
         mLayoutInflater = LayoutInflater.from(context);
-        mGroupList = groupList;
+        for (String accountName : groupMap.keySet()) {
+            List<GroupMetaData> groupsListForAccount = groupMap.get(accountName);
+
+            // Add account name as header for section
+            mGroupList.add(GroupListEntry.createEntryForHeader(accountName,
+                    groupsListForAccount.size()));
+
+            // Add groups within that account as subsequent list items.
+            for (GroupMetaData singleGroup : groupsListForAccount) {
+                mGroupList.add(GroupListEntry.createEntryForGroup(singleGroup));
+            }
+        }
+    }
+
+    public void setSelectionVisible(boolean flag) {
+        mSelectionVisible = flag;
+    }
+
+    public void setSelectedGroup(Uri groupUri) {
+        mSelectedGroupUri = groupUri;
+    }
+
+    private boolean isSelectedGroup(Uri groupUri) {
+        return mSelectedGroupUri != null && mSelectedGroupUri.equals(groupUri);
     }
 
     @Override
@@ -53,24 +90,109 @@
 
     @Override
     public long getItemId(int position) {
-        return getItem(position).getGroupId();
+        return mGroupList.get(position).id;
     }
 
     @Override
-    public GroupMetaData getItem(int position) {
+    public GroupListEntry getItem(int position) {
         return mGroupList.get(position);
     }
 
     @Override
+    public int getItemViewType(int position) {
+        return mGroupList.get(position).type.ordinal();
+    }
+
+    @Override
+    public int getViewTypeCount() {
+        return VIEW_TYPE_COUNT;
+    }
+
+    @Override
+    public boolean areAllItemsEnabled() {
+        return false;
+    }
+
+    @Override
+    public boolean isEnabled(int position) {
+        return mGroupList.get(position).type == ViewType.ITEM;
+    }
+
+    @Override
     public View getView(int position, View convertView, ViewGroup parent) {
+        GroupListEntry item = getItem(position);
+        switch (item.type) {
+            case HEADER:
+                return getHeaderView(item, convertView, parent);
+            case ITEM:
+                return getGroupListItemView(item, convertView, parent);
+            default:
+                throw new IllegalStateException("Invalid GroupListEntry item type " + item.type);
+        }
+
+    }
+
+    private View getHeaderView(GroupListEntry entry, View convertView, ViewGroup parent) {
+        ContactListPinnedHeaderView result = (ContactListPinnedHeaderView) (convertView == null ?
+                new ContactListPinnedHeaderView(mContext, null) :
+                convertView);
+        String groupCountString = mContext.getResources().getQuantityString(
+                R.plurals.num_groups_in_account, entry.count, entry.count);
+        // TODO: Format this correctly by using 2 TextViews when the
+        // ContactListPinnedHeaderView is refactored.
+        result.setSectionHeader(entry.title + " " + groupCountString);
+                        return result;
+    }
+
+    private View getGroupListItemView(GroupListEntry entry, View convertView, ViewGroup parent) {
         GroupListItem result = (GroupListItem) (convertView == null ?
                 mLayoutInflater.inflate(R.layout.group_browse_list_item, parent, false) :
                 convertView);
-        result.loadFromGroup(getItem(position));
+        result.loadFromGroup(entry.groupData);
+        if (mSelectionVisible) {
+            result.setActivated(isSelectedGroup(result.getUri()));
+        }
         return result;
     }
 
     /**
+     * This is a data model object to represent one row in the list of groups were the entry
+     * could be a header or group item.
+     */
+    public static class GroupListEntry {
+        public final ViewType type;
+        public final String title;
+        public final int count;
+        public final GroupMetaData groupData;
+        /**
+         * The id is equal to the group ID (if groupData is available), otherwise it is -1 for
+         * header entries.
+         */
+        public final long id;
+
+        private GroupListEntry(ViewType entryType, String headerTitle, int headerGroupCount,
+                GroupMetaData groupMetaData, long entryId) {
+            type = entryType;
+            title = headerTitle;
+            count = headerGroupCount;
+            groupData = groupMetaData;
+            id = entryId;
+        }
+
+        public static GroupListEntry createEntryForHeader(String headerTitle, int groupCount) {
+            return new GroupListEntry(ViewType.HEADER, headerTitle, groupCount, null, -1);
+        }
+
+        public static GroupListEntry createEntryForGroup(GroupMetaData groupMetaData) {
+            if (groupMetaData == null) {
+                throw new IllegalStateException("Cannot create list entry for a hull group");
+            }
+            return new GroupListEntry(ViewType.ITEM, null, 0, groupMetaData,
+                    groupMetaData.getGroupId());
+        }
+    }
+
+    /**
      * A row in a list of groups, where this row displays a single group's title
      * and associated account.
      */
diff --git a/src/com/android/contacts/group/GroupBrowseListFragment.java b/src/com/android/contacts/group/GroupBrowseListFragment.java
index 150a00f..958d9f8 100644
--- a/src/com/android/contacts/group/GroupBrowseListFragment.java
+++ b/src/com/android/contacts/group/GroupBrowseListFragment.java
@@ -44,7 +44,9 @@
 import android.widget.ListView;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /**
  * Fragment to display the list of groups.
@@ -71,12 +73,24 @@
 
     private Context mContext;
     private Cursor mGroupListCursor;
-    private List<GroupMetaData> mGroupList = new ArrayList<GroupMetaData>();
+
+    /**
+     * Map of account name to a list of {@link GroupMetaData} objects
+     * representing groups within that account.
+     * TODO: Change account name string into a wrapper object that has
+     * account name, type, and authority.
+     */
+    private Map<String, List<GroupMetaData>> mGroupMap = new HashMap<String, List<GroupMetaData>>();
 
     private View mRootView;
     private ListView mListView;
     private View mEmptyView;
 
+    private GroupBrowseListAdapter mAdapter;
+    private boolean mSelectionVisible;
+
+    private int mVerticalScrollbarPosition = View.SCROLLBAR_POSITION_RIGHT;
+
     private OnGroupBrowserActionListener mListener;
 
     public GroupBrowseListFragment() {
@@ -93,6 +107,31 @@
         return mRootView;
     }
 
+    public void setVerticalScrollbarPosition(int position) {
+        if (mVerticalScrollbarPosition != position) {
+            mVerticalScrollbarPosition = position;
+            configureVerticalScrollbar();
+        }
+    }
+
+    private void configureVerticalScrollbar() {
+        mListView.setFastScrollEnabled(true);
+        mListView.setFastScrollAlwaysVisible(true);
+        mListView.setVerticalScrollbarPosition(mVerticalScrollbarPosition);
+        mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
+        int leftPadding = 0;
+        int rightPadding = 0;
+        if (mVerticalScrollbarPosition == View.SCROLLBAR_POSITION_LEFT) {
+            leftPadding = mContext.getResources().getDimensionPixelOffset(
+                    R.dimen.list_visible_scrollbar_padding);
+        } else {
+            rightPadding = mContext.getResources().getDimensionPixelOffset(
+                    R.dimen.list_visible_scrollbar_padding);
+        }
+        mListView.setPadding(leftPadding, mListView.getPaddingTop(),
+                rightPadding, mListView.getPaddingBottom());
+    }
+
     @Override
     public void onAttach(Activity activity) {
         super.onAttach(activity);
@@ -136,7 +175,7 @@
         if (mGroupListCursor == null) {
             return;
         }
-        mGroupList.clear();
+        mGroupMap.clear();
         mGroupListCursor.moveToPosition(-1);
         while (mGroupListCursor.moveToNext()) {
             String accountName = mGroupListCursor.getString(GroupMetaDataLoader.ACCOUNT_NAME);
@@ -150,12 +189,24 @@
                     ? false
                     : mGroupListCursor.getInt(GroupMetaDataLoader.FAVORITES) != 0;
 
-            // TODO: Separate groups according to account name and type.
-            mGroupList.add(new GroupMetaData(
-                    accountName, accountType, groupId, title, defaultGroup, favorites));
+            GroupMetaData newGroup = new GroupMetaData(accountName, accountType, groupId, title,
+                    defaultGroup, favorites);
+
+            if (mGroupMap.containsKey(accountName)) {
+                List<GroupMetaData> groups = mGroupMap.get(accountName);
+                groups.add(newGroup);
+            } else {
+                List<GroupMetaData> groups = new ArrayList<GroupMetaData>();
+                groups.add(newGroup);
+                mGroupMap.put(accountName, groups);
+            }
+
         }
 
-        mListView.setAdapter(new GroupBrowseListAdapter(mContext, mGroupList));
+        mAdapter = new GroupBrowseListAdapter(mContext, mGroupMap);
+        mAdapter.setSelectionVisible(mSelectionVisible);
+
+        mListView.setAdapter(mAdapter);
         mListView.setEmptyView(mEmptyView);
         mListView.setOnItemClickListener(new OnItemClickListener() {
             @Override
@@ -170,7 +221,17 @@
         mListener = listener;
     }
 
+    public void setSelectionVisible(boolean flag) {
+        mSelectionVisible = flag;
+    }
+
+    private void setSelectedGroup(Uri groupUri) {
+        mAdapter.setSelectedGroup(groupUri);
+        mListView.invalidateViews();
+    }
+
     private void viewGroup(Uri groupUri) {
+        setSelectedGroup(groupUri);
         if (mListener != null) mListener.onViewGroupAction(groupUri);
     }