OSDN Git Service

Show the downloads list divided into groups based on how recently downloaded.
authorLeon Scroggins <scroggo@google.com>
Wed, 13 Jan 2010 18:43:35 +0000 (13:43 -0500)
committerLeon Scroggins <scroggo@google.com>
Wed, 13 Jan 2010 20:07:53 +0000 (15:07 -0500)
Fixes http://b/issue?id=2367226

res/layout/browser_downloads_page.xml
src/com/android/browser/BrowserDownloadAdapter.java
src/com/android/browser/BrowserDownloadPage.java
src/com/android/browser/BrowserHistoryPage.java
src/com/android/browser/DateSortedExpandableListAdapter.java

index 1d4d4e6..06c27c6 100644 (file)
@@ -19,8 +19,8 @@
 */
 -->
 <merge xmlns:android="http://schemas.android.com/apk/res/android">
-    <ListView 
-        android:id="@+id/list"
+    <ExpandableListView
+        android:id="@android:id/list"
         android:layout_width="fill_parent"
         android:layout_height="fill_parent"/>
     <ViewStub 
index 16cb982..85539c3 100644 (file)
@@ -30,10 +30,12 @@ import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.provider.Downloads;
 import android.text.format.Formatter;
+import android.view.LayoutInflater;
 import android.view.View;
+import android.view.ViewGroup;
 import android.widget.ImageView;
 import android.widget.ProgressBar;
-import android.widget.ResourceCursorAdapter;
+import android.widget.RelativeLayout;
 import android.widget.TextView;
 
 import java.io.File;
@@ -46,7 +48,7 @@ import java.util.List;
  * real work done by this class is to construct a custom view for the line
  * items.
  */
-public class BrowserDownloadAdapter extends ResourceCursorAdapter {
+public class BrowserDownloadAdapter extends DateSortedExpandableListAdapter {
     
     private int mFilenameColumnId;
     private int mTitleColumnId;
@@ -57,8 +59,8 @@ public class BrowserDownloadAdapter extends ResourceCursorAdapter {
     private int mMimetypeColumnId;
     private int mDateColumnId;
 
-    public BrowserDownloadAdapter(Context context, int layout, Cursor c) {
-        super(context, layout, c);
+    public BrowserDownloadAdapter(Context context, Cursor c, int index) {
+        super(context, c, index);
         mFilenameColumnId = c.getColumnIndexOrThrow(Downloads._DATA);
         mTitleColumnId = c.getColumnIndexOrThrow(Downloads.COLUMN_TITLE);
         mDescColumnId = c.getColumnIndexOrThrow(Downloads.COLUMN_DESCRIPTION);
@@ -71,12 +73,26 @@ public class BrowserDownloadAdapter extends ResourceCursorAdapter {
     }
 
     @Override
-    public void bindView(View view, Context context, Cursor cursor) {
+    public View getChildView(int groupPosition, int childPosition,
+                boolean isLastChild, View convertView, ViewGroup parent) {
+        Context context = getContext();
+        // The layout file uses a RelativeLayout, whereas the GroupViews use
+        // TextView.
+        if (null == convertView || !(convertView instanceof RelativeLayout)) {
+            convertView = LayoutInflater.from(context).inflate(
+                    R.layout.browser_download_item, null);
+        }
+
+        // Bail early if the Cursor is closed.
+        if (!moveCursorToChildPosition(groupPosition, childPosition)) {
+            return convertView;
+        }
+
         Resources r = context.getResources();
         
         // Retrieve the icon for this download
-        String mimeType = cursor.getString(mMimetypeColumnId);
-        ImageView iv = (ImageView) view.findViewById(R.id.download_icon);
+        String mimeType = getString(mMimetypeColumnId);
+        ImageView iv = (ImageView) convertView.findViewById(R.id.download_icon);
         if (DrmRawContent.DRM_MIMETYPE_MESSAGE_STRING.equalsIgnoreCase(mimeType)) {
             iv.setImageResource(R.drawable.ic_launcher_drm_file);
         } else if (mimeType == null) {
@@ -96,10 +112,10 @@ public class BrowserDownloadAdapter extends ResourceCursorAdapter {
             }
         }
         
-        TextView tv = (TextView) view.findViewById(R.id.download_title);
-        String title = cursor.getString(mTitleColumnId);
+        TextView tv = (TextView) convertView.findViewById(R.id.download_title);
+        String title = getString(mTitleColumnId);
         if (title == null) {
-            String fullFilename = cursor.getString(mFilenameColumnId);
+            String fullFilename = getString(mFilenameColumnId);
             if (fullFilename == null) {
                 title = r.getString(R.string.download_unknown_filename);
             } else {
@@ -110,51 +126,51 @@ public class BrowserDownloadAdapter extends ResourceCursorAdapter {
                 // assume "_id" is the first column for the cursor 
                 context.getContentResolver().update(
                         ContentUris.withAppendedId(Downloads.CONTENT_URI,
-                        cursor.getLong(0)), values, null, null);
+                        getLong(0)), values, null, null);
             }
         }
         tv.setText(title);
         
-        tv = (TextView) view.findViewById(R.id.domain);
-        tv.setText(cursor.getString(mDescColumnId));
+        tv = (TextView) convertView.findViewById(R.id.domain);
+        tv.setText(getString(mDescColumnId));
         
-        long totalBytes = cursor.getLong(mTotalBytesColumnId);
+        long totalBytes = getLong(mTotalBytesColumnId);
         
-        int status = cursor.getInt(mStatusColumnId);
+        int status = getInt(mStatusColumnId);
         if (Downloads.isStatusCompleted(status)) { // Download stopped
-            View v = view.findViewById(R.id.progress_text);
+            View v = convertView.findViewById(R.id.progress_text);
             v.setVisibility(View.GONE);
 
-            v = view.findViewById(R.id.download_progress);
+            v = convertView.findViewById(R.id.download_progress);
             v.setVisibility(View.GONE);
 
-            tv = (TextView) view.findViewById(R.id.complete_text);
+            tv = (TextView) convertView.findViewById(R.id.complete_text);
             tv.setVisibility(View.VISIBLE);
             if (Downloads.isStatusError(status)) {
                 tv.setText(getErrorText(status));
             } else {
                 tv.setText(r.getString(R.string.download_success, 
-                        Formatter.formatFileSize(mContext, totalBytes)));
+                        Formatter.formatFileSize(context, totalBytes)));
             }
             
-            long time = cursor.getLong(mDateColumnId);
+            long time = getLong(mDateColumnId);
             Date d = new Date(time);
             DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT);
-            tv = (TextView) view.findViewById(R.id.complete_date);
+            tv = (TextView) convertView.findViewById(R.id.complete_date);
             tv.setVisibility(View.VISIBLE);
             tv.setText(df.format(d));
             
         } else { // Download is still running
-            tv = (TextView) view.findViewById(R.id.progress_text);
+            tv = (TextView) convertView.findViewById(R.id.progress_text);
             tv.setVisibility(View.VISIBLE);
 
-            View progress = view.findViewById(R.id.download_progress);
+            View progress = convertView.findViewById(R.id.download_progress);
             progress.setVisibility(View.VISIBLE);
             
-            View v = view.findViewById(R.id.complete_date);
+            View v = convertView.findViewById(R.id.complete_date);
             v.setVisibility(View.GONE);
 
-            v = view.findViewById(R.id.complete_text);
+            v = convertView.findViewById(R.id.complete_text);
             v.setVisibility(View.GONE);
             
             if (status == Downloads.STATUS_PENDING) {
@@ -171,14 +187,14 @@ public class BrowserDownloadAdapter extends ResourceCursorAdapter {
                     sb.append(r.getText(R.string.download_running_paused));
                 }
                 if (totalBytes > 0) {
-                    long currentBytes = cursor.getLong(mCurrentBytesColumnId); 
+                    long currentBytes = getLong(mCurrentBytesColumnId);
                     int progressAmount = (int)(currentBytes * 100 / totalBytes);
                     sb.append(' ');
                     sb.append(progressAmount);
                     sb.append("% (");
-                    sb.append(Formatter.formatFileSize(mContext, currentBytes));
+                    sb.append(Formatter.formatFileSize(context, currentBytes));
                     sb.append("/");
-                    sb.append(Formatter.formatFileSize(mContext, totalBytes));
+                    sb.append(Formatter.formatFileSize(context, totalBytes));
                     sb.append(")");
                     pb.setIndeterminate(false);
                     pb.setProgress(progressAmount);
@@ -188,7 +204,7 @@ public class BrowserDownloadAdapter extends ResourceCursorAdapter {
                 tv.setText(sb.toString()); 
             }
         }
-        
+        return convertView;
     }
     
     /**
index 22e0e65..3a41afd 100644 (file)
@@ -16,8 +16,8 @@
 
 package com.android.browser;
 
-import android.app.Activity;
 import android.app.AlertDialog;
+import android.app.ExpandableListActivity;
 import android.content.ActivityNotFoundException;
 import android.content.ContentValues;
 import android.content.DialogInterface;
@@ -38,8 +38,7 @@ import android.view.MenuInflater;
 import android.view.View;
 import android.view.ViewGroup.LayoutParams;
 import android.widget.AdapterView;
-import android.widget.ListView;
-import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ExpandableListView;
 
 import java.io.File;
 import java.util.List;
@@ -47,16 +46,15 @@ import java.util.List;
 /**
  *  View showing the user's current browser downloads
  */
-public class BrowserDownloadPage extends Activity 
-        implements View.OnCreateContextMenuListener, OnItemClickListener {
+public class BrowserDownloadPage extends ExpandableListActivity {
     
-    private ListView                mListView;
+    private ExpandableListView      mListView;
     private Cursor                  mDownloadCursor;
     private BrowserDownloadAdapter  mDownloadAdapter;
     private int                     mStatusColumnId;
     private int                     mIdColumnId;
     private int                     mTitleColumnId;
-    private int                     mContextMenuPosition;
+    private long                    mContextMenuPosition;
     
     @Override 
     public void onCreate(Bundle icicle) {
@@ -65,16 +63,15 @@ public class BrowserDownloadPage extends Activity
         
         setTitle(getText(R.string.download_title));
 
-        mListView = (ListView) findViewById(R.id.list);
+        mListView = (ExpandableListView) findViewById(android.R.id.list);
         mListView.setEmptyView(findViewById(R.id.empty));
-        
         mDownloadCursor = managedQuery(Downloads.CONTENT_URI, 
                 new String [] {"_id", Downloads.COLUMN_TITLE, Downloads.COLUMN_STATUS,
                 Downloads.COLUMN_TOTAL_BYTES, Downloads.COLUMN_CURRENT_BYTES, 
                 Downloads._DATA, Downloads.COLUMN_DESCRIPTION, 
                 Downloads.COLUMN_MIME_TYPE, Downloads.COLUMN_LAST_MODIFICATION,
                 Downloads.COLUMN_VISIBILITY}, 
-                null, null);
+                null, Downloads.COLUMN_LAST_MODIFICATION + " DESC");
         
         // only attach everything to the listbox if we can access
         // the download database. Otherwise, just show it empty
@@ -88,20 +85,24 @@ public class BrowserDownloadPage extends Activity
             
             // Create a list "controller" for the data
             mDownloadAdapter = new BrowserDownloadAdapter(this, 
-                    R.layout.browser_download_item, mDownloadCursor);
+                    mDownloadCursor, mDownloadCursor.getColumnIndexOrThrow(
+                    Downloads.COLUMN_LAST_MODIFICATION));
 
-            mListView.setAdapter(mDownloadAdapter);
+            setListAdapter(mDownloadAdapter);
             mListView.setScrollBarStyle(View.SCROLLBARS_INSIDE_INSET);
             mListView.setOnCreateContextMenuListener(this);
-            mListView.setOnItemClickListener(this);
-            
+
             Intent intent = getIntent();
-            if (intent != null && intent.getData() != null) {
-                int position = checkStatus(
-                        ContentUris.parseId(intent.getData()));
-                if (position >= 0) {
-                    mListView.setSelection(position);
-                }
+            final int groupToShow = intent == null || intent.getData() == null
+                    ? 0 : checkStatus(ContentUris.parseId(intent.getData()));
+            if (mDownloadAdapter.getGroupCount() > groupToShow) {
+                mListView.post(new Runnable() {
+                    public void run() {
+                        if (mDownloadAdapter.getGroupCount() > groupToShow) {
+                            mListView.expandGroup(groupToShow);
+                        }
+                    }
+                });
             }
         }
     }
@@ -141,7 +142,10 @@ public class BrowserDownloadPage extends Activity
 
     @Override
     public boolean onContextItemSelected(MenuItem item) {
-        mDownloadCursor.moveToPosition(mContextMenuPosition);
+        if (!mDownloadAdapter.moveCursorToPackedChildPosition(
+                mContextMenuPosition)) {
+            return false;
+        }
         switch (item.getItemId()) {
             case R.id.download_menu_open:
                 hideCompletedDownload();
@@ -162,10 +166,15 @@ public class BrowserDownloadPage extends Activity
     public void onCreateContextMenu(ContextMenu menu, View v,
             ContextMenuInfo menuInfo) {
         if (mDownloadCursor != null) {
-            AdapterView.AdapterContextMenuInfo info = 
-                    (AdapterView.AdapterContextMenuInfo) menuInfo;
-            mDownloadCursor.moveToPosition(info.position);
-            mContextMenuPosition = info.position;
+            ExpandableListView.ExpandableListContextMenuInfo info
+                    = (ExpandableListView.ExpandableListContextMenuInfo) menuInfo;
+            long packedPosition = info.packedPosition;
+            // Only show a context menu for the child views
+            if (!mDownloadAdapter.moveCursorToPackedChildPosition(
+                    packedPosition)) {
+                return;
+            }
+            mContextMenuPosition = packedPosition;
             menu.setHeaderTitle(mDownloadCursor.getString(mTitleColumnId));
             
             MenuInflater inflater = getMenuInflater();
@@ -178,60 +187,50 @@ public class BrowserDownloadPage extends Activity
                 inflater.inflate(R.menu.downloadhistorycontextrunning, menu);
             }
         }
+        super.onCreateContextMenu(menu, v, menuInfo);
     }
 
     /**
      * This function is called to check the status of the download and if it
      * has an error show an error dialog.
      * @param id Row id of the download to check
-     * @return position of item
+     * @return Group which contains the download
      */
-    int checkStatus(final long id) {
-        int position = -1;
-        for (mDownloadCursor.moveToFirst(); !mDownloadCursor.isAfterLast(); 
-                mDownloadCursor.moveToNext()) {
-            if (id == mDownloadCursor.getLong(mIdColumnId)) {
-                position = mDownloadCursor.getPosition();
-                break;
-            }
-            
+    private int checkStatus(final long id) {
+        int groupToShow = mDownloadAdapter.groupFromChildId(id);
+        if (-1 == groupToShow) return 0;
+        int status = mDownloadCursor.getInt(mStatusColumnId);
+        if (!Downloads.isStatusError(status)) {
+            return groupToShow;
         }
-        if (!mDownloadCursor.isAfterLast()) {
-            int status = mDownloadCursor.getInt(mStatusColumnId);
-            if (!Downloads.isStatusError(status)) {
-                return position;
-            }
-            
-            if (status == Downloads.STATUS_FILE_ERROR) {
-                String title = mDownloadCursor.getString(mTitleColumnId);
-                if (title == null || title.length() == 0) {
-                    title = getString(R.string.download_unknown_filename);
-                }
-                String msg = getString(R.string.download_file_error_dlg_msg, 
-                        title);
-                new AlertDialog.Builder(this)
-                        .setTitle(R.string.download_file_error_dlg_title)
-                        .setIcon(android.R.drawable.ic_popup_disk_full)
-                        .setMessage(msg)
-                        .setPositiveButton(R.string.ok, null)
-                        .setNegativeButton(R.string.retry, 
-                                new DialogInterface.OnClickListener() {
-                                    public void onClick(DialogInterface dialog, 
-                                            int whichButton) {
-                                        resumeDownload(id);
-                                    }
-                                })
-                        .show();
-            } else {
-                new AlertDialog.Builder(this)
-                        .setTitle(R.string.download_failed_generic_dlg_title)
-                        .setIcon(R.drawable.ssl_icon)
-                        .setMessage(BrowserDownloadAdapter.getErrorText(status))
-                        .setPositiveButton(R.string.ok, null)
-                        .show();
+        if (status == Downloads.STATUS_FILE_ERROR) {
+            String title = mDownloadCursor.getString(mTitleColumnId);
+            if (title == null || title.length() == 0) {
+                title = getString(R.string.download_unknown_filename);
             }
+            String msg = getString(R.string.download_file_error_dlg_msg, title);
+            new AlertDialog.Builder(this)
+                    .setTitle(R.string.download_file_error_dlg_title)
+                    .setIcon(android.R.drawable.ic_popup_disk_full)
+                    .setMessage(msg)
+                    .setPositiveButton(R.string.ok, null)
+                    .setNegativeButton(R.string.retry,
+                            new DialogInterface.OnClickListener() {
+                                public void onClick(DialogInterface dialog,
+                                        int whichButton) {
+                                    resumeDownload(id);
+                                }
+                            })
+                    .show();
+        } else {
+            new AlertDialog.Builder(this)
+                    .setTitle(R.string.download_failed_generic_dlg_title)
+                    .setIcon(R.drawable.ssl_icon)
+                    .setMessage(BrowserDownloadAdapter.getErrorText(status))
+                    .setPositiveButton(R.string.ok, null)
+                    .show();
         }
-        return position;
+        return groupToShow;
     }
     
     /**
@@ -421,14 +420,12 @@ public class BrowserDownloadPage extends Activity
         }
     }
 
-    /*
-     * (non-Javadoc)
-     * @see android.widget.AdapterView.OnItemClickListener#onItemClick(android.widget.AdapterView, android.view.View, int, long)
-     */
-    public void onItemClick(AdapterView parent, View view, int position, 
-            long id) {
+    @Override
+    public boolean onChildClick(ExpandableListView parent, View v,
+            int groupPosition, int childPosition, long id) {
         // Open the selected item
-        mDownloadCursor.moveToPosition(position);
+        mDownloadAdapter.moveCursorToChildPosition(groupPosition,
+                childPosition);
         
         hideCompletedDownload();
 
@@ -440,6 +437,7 @@ public class BrowserDownloadPage extends Activity
             // Check to see if there is an error.
             checkStatus(id);
         }
+        return true;
     }
     
     /**
index 5818e1d..d23137e 100644 (file)
@@ -32,7 +32,6 @@ import android.text.IClipboard;
 import android.util.Log;
 import android.view.ContextMenu;
 import android.view.KeyEvent;
-import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuInflater;
 import android.view.MenuItem;
@@ -46,7 +45,6 @@ import android.widget.AdapterView;
 import android.widget.ExpandableListAdapter;
 import android.widget.ExpandableListView;
 import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
-import android.widget.TextView;
 import android.widget.Toast;
 
 /**
@@ -336,20 +334,5 @@ public class BrowserHistoryPage extends ExpandableListActivity {
                     getInt(Browser.HISTORY_PROJECTION_BOOKMARK_INDEX));
             return item;
         }
-        
-        public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) {
-            TextView item;
-            if (null == convertView || !(convertView instanceof TextView)) {
-                LayoutInflater factory = 
-                        LayoutInflater.from(BrowserHistoryPage.this);
-                item = (TextView) 
-                        factory.inflate(R.layout.history_header, null);
-            } else {
-                item = (TextView) convertView;
-            }
-            item.setText(getGroupLabel(groupPosition));
-            return item;
-        }
-
     }
 }
index 05d16bd..1d04493 100644 (file)
@@ -21,10 +21,14 @@ import android.database.ContentObserver;
 import android.database.Cursor;
 import android.database.DataSetObserver;
 import android.os.Handler;
+import android.provider.BaseColumns;
+import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import android.webkit.DateSorter;
 import android.widget.ExpandableListAdapter;
+import android.widget.ExpandableListView;
+import android.widget.TextView;
 
 import java.util.Vector;
 
@@ -43,6 +47,8 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter {
     private Cursor mCursor;
     private DateSorter mDateSorter;
     private int mDateIndex;
+    private int mIdIndex;
+    private Context mContext;
 
     private class ChangeObserver extends ContentObserver {
         public ChangeObserver() {
@@ -62,9 +68,11 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter {
 
     public DateSortedExpandableListAdapter(Context context, Cursor cursor,
             int dateIndex) {
+        mContext = context;
         mDateSorter = new DateSorter(context);
         mObservers = new Vector<DataSetObserver>();
         mCursor = cursor;
+        mIdIndex = cursor.getColumnIndexOrThrow(BaseColumns._ID);
         cursor.registerContentObserver(new ChangeObserver());
         mDateIndex = dateIndex;
         buildMap();
@@ -85,7 +93,7 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter {
         int dateIndex = -1;
         if (mCursor.moveToFirst() && mCursor.getCount() > 0) {
             while (!mCursor.isAfterLast()) {
-                long date = mCursor.getLong(mDateIndex);
+                long date = getLong(mDateIndex);
                 int index = mDateSorter.getIndex(date);
                 if (index > dateIndex) {
                     mNumberOfBins++;
@@ -117,6 +125,10 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter {
         return mCursor.getBlob(cursorIndex);
     }
 
+    /* package */ Context getContext() {
+        return mContext;
+    }
+
     /**
      * Get the integer at cursorIndex from the Cursor.  Assumes the Cursor has
      * already been moved to the correct position.  Along with
@@ -130,12 +142,11 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter {
     }
 
     /**
-     * Get the label for a group, as specified by the ExpandableList
-     * @param groupPosition Position in the ExpandableList's set of groups
-     * @return String label for the corresponding bin.
+     * Get the long at cursorIndex from the Cursor.  Assumes the Cursor has
+     * already been moved to the correct position.
      */
-    /* package */ String getGroupLabel(int groupPosition) {
-        return mDateSorter.getLabel(groupPositionToBin(groupPosition));
+    /* package */ long getLong(int cursorIndex) {
+        return mCursor.getLong(cursorIndex);
     }
 
     /**
@@ -151,6 +162,30 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter {
     }
 
     /**
+     * Determine which group an item belongs to.
+     * @param childId ID of the child view in question.
+     * @return int Group position of the containing group.
+    /* package */ int groupFromChildId(long childId) {
+        int group = -1;
+        for (mCursor.moveToFirst(); !mCursor.isAfterLast();
+                mCursor.moveToNext()) {
+            if (getLong(mIdIndex) == childId) {
+                int bin = mDateSorter.getIndex(getLong(mDateIndex));
+                // bin is the same as the group if the number of bins is the
+                // same as DateSorter
+                if (mDateSorter.DAY_COUNT == mNumberOfBins) return bin;
+                // There are some empty bins.  Find the corresponding group.
+                group = 0;
+                for (int i = 0; i < bin; i++) {
+                    if (mItemMap[i] != 0) group++;
+                }
+                break;
+            }
+        }
+        return group;
+    }
+
+    /**
      * Translates from a group position in the ExpandableList to a bin.  This is
      * necessary because some groups have no history items, so we do not include
      * those in the ExpandableList.
@@ -181,6 +216,23 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter {
     }
 
     /**
+     * Move the cursor to the position indicated.
+     * @param packedPosition Position in packed position representation.
+     * @return True on success, false otherwise.
+     */
+    boolean moveCursorToPackedChildPosition(long packedPosition) {
+        if (ExpandableListView.getPackedPositionType(packedPosition) !=
+                ExpandableListView.PACKED_POSITION_TYPE_CHILD) {
+            return false;
+        }
+        int groupPosition = ExpandableListView.getPackedPositionGroup(
+                packedPosition);
+        int childPosition = ExpandableListView.getPackedPositionChild(
+                packedPosition);
+        return moveCursorToChildPosition(groupPosition, childPosition);
+    }
+
+    /**
      * Move the cursor the the position indicated.
      * @param groupPosition Index of the group containing the desired item.
      * @param childPosition Index of the item within the specified group.
@@ -195,8 +247,7 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter {
         for (int i = 0; i < groupPosition; i++) {
             index += mItemMap[i];
         }
-        mCursor.moveToPosition(index);
-        return true;
+        return mCursor.moveToPosition(index);
     }
 
     /* package */ void refreshData() {
@@ -212,7 +263,16 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter {
 
     public View getGroupView(int groupPosition, boolean isExpanded,
             View convertView, ViewGroup parent) {
-        return null;
+        TextView item;
+        if (null == convertView || !(convertView instanceof TextView)) {
+            LayoutInflater factory = LayoutInflater.from(mContext);
+            item = (TextView) factory.inflate(R.layout.history_header, null);
+        } else {
+            item = (TextView) convertView;
+        }
+        String label = mDateSorter.getLabel(groupPositionToBin(groupPosition));
+        item.setText(label);
+        return item;
     }
 
     public View getChildView(int groupPosition, int childPosition,
@@ -249,7 +309,10 @@ public class DateSortedExpandableListAdapter implements ExpandableListAdapter {
     }
 
     public long getChildId(int groupPosition, int childPosition) {
-        return (childPosition << 3) + groupPosition;
+        if (moveCursorToChildPosition(groupPosition, childPosition)) {
+            return getLong(mIdIndex);
+        }
+        return 0;
     }
 
     public boolean hasStableIds() {