Merge "Split new and older items in the call log."
diff --git a/res/layout/call_log_list_item.xml b/res/layout/call_log_list_item.xml
index 7e82b40..61b7340 100644
--- a/res/layout/call_log_list_item.xml
+++ b/res/layout/call_log_list_item.xml
@@ -16,11 +16,41 @@
 
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
-    android:layout_height="?attr/call_log_list_item_height"
+    android:layout_height="wrap_content"
 >
+    <!--
+        This layout may represent either a call log item (but not a group thereof) or one of the
+        headers in the call log.
 
-    <include layout="@layout/call_log_contact_photo"/>
-    <include layout="@layout/call_log_action_call"/>
-    <include layout="@layout/call_log_list_item_layout"/>
+        The former will make the @id/call_log_item visible and the @id/call_log_header gone.
 
+        The latter will make the @id/call_log_header visible and the @id/call_log_item gone
+    -->
+
+    <RelativeLayout
+        android:id="@+id/call_log_item"
+        android:layout_width="fill_parent"
+        android:layout_height="?attr/call_log_list_item_height"
+    >
+        <include layout="@layout/call_log_contact_photo"/>
+        <include layout="@layout/call_log_action_call"/>
+        <include layout="@layout/call_log_list_item_layout"/>
+    </RelativeLayout>
+
+    <LinearLayout
+        android:id="@+id/call_log_header"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:background="?attr/call_log_list_header_background"
+        android:layout_marginLeft="5dip"
+        android:layout_marginRight="20dip"
+    >
+        <TextView
+            android:id="@+id/call_log_header_text"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:textColor="?attr/call_log_list_header_text_color"
+            android:textAppearance="?android:attr/textAppearanceLarge"
+        />
+    </LinearLayout>
 </RelativeLayout>
diff --git a/res/values/colors.xml b/res/values/colors.xml
index 5641d35..f6591f3 100644
--- a/res/values/colors.xml
+++ b/res/values/colors.xml
@@ -57,4 +57,10 @@
 
     <!-- Color of the text in the updates tab in the tab carousel on the contact detail page -->
     <color name="detail_update_tab_text_color">#777777</color>
+
+    <!-- Color of the text describing an unconsumed missed call. -->
+    <color name="call_log_missed_call_highlight_color">#FF0000</color>
+
+    <!-- Color of the text describing an unconsumed voicemail. -->
+    <color name="call_log_voicemail_highlight_color">#0000FF</color>
 </resources>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 9e2e085..0e56ef4 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1607,4 +1607,10 @@
 
     <!-- The separator between the call type text and the date in the call log [CHAR LIMIT=3] -->
     <string name="call_log_type_date_separator">/</string>
+
+    <!-- The header in the call log used to identify missed calls and voicemail that have not yet been consumed [CHAR LIMIT=10] -->
+    <string name="call_log_new_header">New</string>
+
+    <!-- The header in the call log used to identify items that have been already consumed [CHAR LIMIT=10] -->
+    <string name="call_log_old_header">Older</string>
 </resources>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index ff8f841..4638b2e 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -41,6 +41,8 @@
         <item name="call_log_list_contact_photo_size">60dip</item>
         <item name="call_log_list_contact_photo_margin">5dip</item>
         <item name="call_log_list_item_height">70dip</item>
+        <item name="call_log_list_header_text_color">#AAAAFF</item>
+        <item name="call_log_list_header_background">@drawable/call_log_action_bar_bg</item>
         <!-- CallLog -->
         <item name="call_log_date_margin">5dip</item>
         <item name="call_log_primary_text_color">#FFFFFF</item>
@@ -155,6 +157,8 @@
         <attr name="call_log_list_contact_photo_size" format="dimension" />
         <attr name="call_log_list_contact_photo_margin" format="dimension" />
         <attr name="call_log_list_item_height" format="dimension" />
+        <attr name="call_log_list_header_text_color" format="color" />
+        <attr name="call_log_list_header_background" format="reference" />
     </declare-styleable>
 
     <style name="PeopleTheme" parent="android:Theme.Holo.Light.SplitActionBarWhenNarrow">
diff --git a/src/com/android/contacts/CallDetailActivity.java b/src/com/android/contacts/CallDetailActivity.java
index 5f55f80..cce48dd 100644
--- a/src/com/android/contacts/CallDetailActivity.java
+++ b/src/com/android/contacts/CallDetailActivity.java
@@ -224,7 +224,7 @@
 
         // Set the details header, based on the first phone call.
         mPhoneCallDetailsHelper.setPhoneCallDetails(mPhoneCallDetailsViews,
-                details[0], false);
+                details[0], false, false);
 
         // Cache the details about the phone number.
         final Uri numberCallUri = mPhoneNumberHelper.getCallUri(mNumber);
diff --git a/src/com/android/contacts/PhoneCallDetailsHelper.java b/src/com/android/contacts/PhoneCallDetailsHelper.java
index 6bdfbaa..e115d21 100644
--- a/src/com/android/contacts/PhoneCallDetailsHelper.java
+++ b/src/com/android/contacts/PhoneCallDetailsHelper.java
@@ -60,7 +60,7 @@
 
     /** Fills the call details views with content. */
     public void setPhoneCallDetails(PhoneCallDetailsViews views, PhoneCallDetails details,
-            boolean useIcons) {
+            boolean useIcons, boolean isHighlighted) {
         if (useIcons) {
             views.callTypeIcons.removeAllViews();
             int count = details.callTypes.length;
@@ -77,7 +77,9 @@
             // Use the name of the first call type.
             // TODO: We should update this to handle the text for multiple calls as well.
             int callType = details.callTypes[0];
-            views.callTypeText.setText(mCallTypeHelper.getCallTypeText(callType));
+            views.callTypeText.setText(
+                    isHighlighted ? mCallTypeHelper.getHighlightedCallTypeText(callType)
+                            : mCallTypeHelper.getCallTypeText(callType));
             views.callTypeIcons.removeAllViews();
 
             views.callTypeText.setVisibility(View.VISIBLE);
diff --git a/src/com/android/contacts/calllog/CallLogFragment.java b/src/com/android/contacts/calllog/CallLogFragment.java
index 90e405a..8996821 100644
--- a/src/com/android/contacts/calllog/CallLogFragment.java
+++ b/src/com/android/contacts/calllog/CallLogFragment.java
@@ -16,6 +16,7 @@
 
 package com.android.contacts.calllog;
 
+import com.android.common.io.MoreCloseables;
 import com.android.common.widget.GroupingListAdapter;
 import com.android.contacts.CallDetailActivity;
 import com.android.contacts.ContactPhotoManager;
@@ -38,6 +39,8 @@
 import android.content.res.Resources;
 import android.database.CharArrayBuffer;
 import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.database.MergeCursor;
 import android.database.sqlite.SQLiteDatabaseCorruptException;
 import android.database.sqlite.SQLiteDiskIOException;
 import android.database.sqlite.SQLiteException;
@@ -70,6 +73,8 @@
 import java.lang.ref.WeakReference;
 import java.util.LinkedList;
 
+import javax.annotation.concurrent.GuardedBy;
+
 /**
  * Displays a list of call log entries.
  */
@@ -80,21 +85,39 @@
     private static final int CONTACT_INFO_CACHE_SIZE = 100;
 
     /** The query for the call log table */
-    private static final class CallLogQuery {
+    public static final class CallLogQuery {
         public static final String[] _PROJECTION = new String[] {
                 Calls._ID,
                 Calls.NUMBER,
                 Calls.DATE,
                 Calls.DURATION,
                 Calls.TYPE,
-                Calls.COUNTRY_ISO};
-
+                Calls.COUNTRY_ISO,
+        };
         public static final int ID = 0;
         public static final int NUMBER = 1;
         public static final int DATE = 2;
         public static final int DURATION = 3;
         public static final int CALL_TYPE = 4;
         public static final int COUNTRY_ISO = 5;
+
+        /**
+         * The name of the synthetic "section" column.
+         * <p>
+         * This column identifies whether a row is a header or an actual item, and whether it is
+         * part of the new or old calls.
+         */
+        public static final String SECTION_NAME = "section";
+        /** The index of the "section" column in the projection. */
+        public static final int SECTION = 6;
+        /** The value of the "section" column for the header of the new section. */
+        public static final int SECTION_NEW_HEADER = 0;
+        /** The value of the "section" column for the items of the new section. */
+        public static final int SECTION_NEW_ITEM = 1;
+        /** The value of the "section" column for the header of the old section. */
+        public static final int SECTION_OLD_HEADER = 2;
+        /** The value of the "section" column for the items of the old section. */
+        public static final int SECTION_OLD_ITEM = 3;
     }
 
     /** The query to use for the phones table */
@@ -123,9 +146,6 @@
         public static final int DELETE_ALL = 1;
     }
 
-    private static final int QUERY_TOKEN = 53;
-    private static final int UPDATE_TOKEN = 54;
-
     private CallLogAdapter mAdapter;
     private QueryHandler mQueryHandler;
     private String mVoiceMailNumber;
@@ -499,7 +519,6 @@
 
         @Override
         protected void addGroups(Cursor cursor) {
-
             int count = cursor.getCount();
             if (count == 0) {
                 return;
@@ -520,7 +539,8 @@
                 // Group adjacent calls with the same number. Make an exception
                 // for the latest item if it was a missed call.  We don't want
                 // a missed call to be hidden inside a group.
-                if (sameNumber && currentCallType != Calls.MISSED_TYPE) {
+                if (sameNumber && currentCallType != Calls.MISSED_TYPE
+                        && !isSectionHeader(cursor)) {
                     groupItemCount++;
                 } else {
                     if (groupItemCount > 1) {
@@ -549,6 +569,18 @@
             }
         }
 
+        private boolean isSectionHeader(Cursor cursor) {
+            int section = cursor.getInt(CallLogQuery.SECTION);
+            return section == CallLogQuery.SECTION_NEW_HEADER
+                    || section == CallLogQuery.SECTION_OLD_HEADER;
+        }
+
+        private boolean isNewSection(Cursor cursor) {
+            int section = cursor.getInt(CallLogQuery.SECTION);
+            return section == CallLogQuery.SECTION_NEW_ITEM
+                    || section == CallLogQuery.SECTION_NEW_HEADER;
+        }
+
         protected boolean equalPhoneNumbers(CharArrayBuffer buffer1, CharArrayBuffer buffer2) {
 
             // TODO add PhoneNumberUtils.compare(CharSequence, CharSequence) to avoid
@@ -625,12 +657,32 @@
          */
         private void bindView(View view, Cursor c, int count) {
             final CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+            final int section = c.getInt(CallLogQuery.SECTION);
 
-            String number = c.getString(CallLogQuery.NUMBER);
-            long date = c.getLong(CallLogQuery.DATE);
-            long duration = c.getLong(CallLogQuery.DURATION);
+            if (views.standAloneItemView != null) {
+                // This is stand-alone item: it might, however, be a header: check the value of the
+                // section column in the cursor.
+                if (section == CallLogQuery.SECTION_NEW_HEADER
+                        || section == CallLogQuery.SECTION_OLD_HEADER) {
+                    views.standAloneItemView.setVisibility(View.GONE);
+                    views.standAloneHeaderView.setVisibility(View.VISIBLE);
+                    views.standAloneHeaderTextView.setText(
+                            section == CallLogQuery.SECTION_NEW_HEADER
+                                    ? R.string.call_log_new_header
+                                    : R.string.call_log_old_header);
+                    // Nothing else to set up for a header.
+                    return;
+                }
+                // Default case: an item in the call log.
+                views.standAloneItemView.setVisibility(View.VISIBLE);
+                views.standAloneHeaderView.setVisibility(View.GONE);
+            }
+
+            final String number = c.getString(CallLogQuery.NUMBER);
+            final long date = c.getLong(CallLogQuery.DATE);
+            final long duration = c.getLong(CallLogQuery.DURATION);
             final String formattedNumber;
-            String countryIso = c.getString(CallLogQuery.COUNTRY_ISO);
+            final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO);
             // Store away the number so we can call it directly if you click on the call icon
             if (views.callView != null) {
                 views.callView.setTag(number);
@@ -673,19 +725,19 @@
                 formattedNumber = formatPhoneNumber(number, null, countryIso);
             }
 
-            long personId = info.personId;
-            String name = info.name;
-            int ntype = info.type;
-            String label = info.label;
-            long photoId = info.photoId;
-            String lookupKey = info.lookupKey;
+            final long personId = info.personId;
+            final String name = info.name;
+            final int ntype = info.type;
+            final String label = info.label;
+            final long photoId = info.photoId;
+            final String lookupKey = info.lookupKey;
             // Assumes the call back feature is on most of the
             // time. For private and unknown numbers: hide it.
             if (views.callView != null) {
                 views.callView.setVisibility(View.VISIBLE);
             }
 
-            int[] callTypes = getCallTypes(c, count);
+            final int[] callTypes = getCallTypes(c, count);
             final PhoneCallDetails details;
             if (TextUtils.isEmpty(name)) {
                 details = new PhoneCallDetails(number, formattedNumber, callTypes, date, duration);
@@ -693,7 +745,13 @@
                 details = new PhoneCallDetails(number, formattedNumber, callTypes, date, duration,
                         name, ntype, label, personId, photoId);
             }
-            mCallLogViewsHelper.setPhoneCallDetails(views, details , true);
+
+            final boolean isNew = isNewSection(c);
+            // Use icons for old items, but text for new ones.
+            final boolean useIcons = !isNew;
+            // New items also use the highlighted version of the text.
+            final boolean isHighlighted = isNew;
+            mCallLogViewsHelper.setPhoneCallDetails(views, details, useIcons, isHighlighted);
             if (views.photoView != null) {
                 bindQuickContact(views.photoView, photoId, personId, lookupKey);
             }
@@ -750,9 +808,22 @@
         }
     }
 
+    /** Handles asynchronous queries to the call log. */
     private static final class QueryHandler extends AsyncQueryHandler {
+        /** The token for the query to fetch the new entries from the call log. */
+        private static final int QUERY_NEW_CALLS_TOKEN = 53;
+        /** The token for the query to fetch the old entries from the call log. */
+        private static final int QUERY_OLD_CALLS_TOKEN = 54;
+        /** The token for the query to mark all missed calls as old after seeing the call log. */
+        private static final int UPDATE_MISSED_CALLS_TOKEN = 55;
+
         private final WeakReference<CallLogFragment> mFragment;
 
+        /** The cursor containing the new calls, or null if they have not yet been fetched. */
+        @GuardedBy("this") private Cursor mNewCallsCursor;
+        /** The cursor containing the old calls, or null if they have not yet been fetched. */
+        @GuardedBy("this") private Cursor mOldCallsCursor;
+
         /**
          * Simple handler that wraps background calls to catch
          * {@link SQLiteException}, such as when the disk is full.
@@ -788,14 +859,173 @@
             mFragment = new WeakReference<CallLogFragment>(fragment);
         }
 
+        /** Returns the list of columns for the headers. */
+        private String[] getHeaderColumns() {
+            int length = CallLogQuery._PROJECTION.length;
+            String[] columns = new String[length + 1];
+            System.arraycopy(CallLogQuery._PROJECTION, 0, columns, 0, length);
+            columns[length] = CallLogQuery.SECTION_NAME;
+            return columns;
+        }
+
+        /** Creates a cursor that contains a single row and maps the section to the given value. */
+        private Cursor createHeaderCursorFor(int section) {
+            MatrixCursor matrixCursor = new MatrixCursor(getHeaderColumns());
+            matrixCursor.addRow(new Object[]{ -1L, "", 0L, 0L, 0, "", section });
+            return matrixCursor;
+        }
+
+        /** Returns a cursor for the old calls header. */
+        private Cursor createOldCallsHeaderCursor() {
+            return createHeaderCursorFor(CallLogQuery.SECTION_OLD_HEADER);
+        }
+
+        /** Returns a cursor for the new calls header. */
+        private Cursor createNewCallsHeaderCursor() {
+            return createHeaderCursorFor(CallLogQuery.SECTION_NEW_HEADER);
+        }
+
+        /**
+         * Fetches the list of calls from the call log.
+         * <p>
+         * It will asynchronously update the content of the list view when the fetch completes.
+         */
+        public void fetchCalls() {
+            cancelFetch();
+            invalidate();
+            fetchNewCalls();
+            fetchOldCalls();
+        }
+
+        /** Fetches the list of new calls in the call log. */
+        private void fetchNewCalls() {
+            fetchCalls(QUERY_NEW_CALLS_TOKEN, true);
+        }
+
+        /** Fetch the list of old calls in the call log. */
+        private void fetchOldCalls() {
+            fetchCalls(QUERY_OLD_CALLS_TOKEN, false);
+        }
+
+        /** Fetches the list of calls in the call log, either the new one or the old ones. */
+        private void fetchCalls(int token, boolean isNew) {
+            String selection =
+                    String.format("%s = 1 AND (%s = ? OR %s = ?)",
+                            Calls.NEW, Calls.TYPE, Calls.TYPE);
+            String[] selectionArgs = new String[]{
+                    Integer.toString(Calls.MISSED_TYPE),
+                    Integer.toString(Calls.VOICEMAIL_TYPE),
+            };
+            if (!isNew) {
+                selection = String.format("NOT (%s)", selection);
+            }
+            startQuery(token, null, Calls.CONTENT_URI_WITH_VOICEMAIL,
+                    CallLogQuery._PROJECTION, selection, selectionArgs, Calls.DEFAULT_SORT_ORDER);
+        }
+
+        /** Cancel any pending fetch request. */
+        private void cancelFetch() {
+            cancelOperation(QUERY_NEW_CALLS_TOKEN);
+            cancelOperation(QUERY_OLD_CALLS_TOKEN);
+        }
+
+        /** Updates the missed calls to mark them as old. */
+        public void updateMissedCalls() {
+            // Mark all "new" missed calls as not new anymore
+            StringBuilder where = new StringBuilder();
+            where.append("type = ");
+            where.append(Calls.MISSED_TYPE);
+            where.append(" AND ");
+            where.append(Calls.NEW);
+            where.append(" = 1");
+
+            ContentValues values = new ContentValues(1);
+            values.put(Calls.NEW, "0");
+
+            startUpdate(UPDATE_MISSED_CALLS_TOKEN, null, Calls.CONTENT_URI_WITH_VOICEMAIL,
+                    values, where.toString(), null);
+        }
+
+        /**
+         * Invalidate the current list of calls.
+         * <p>
+         * This method is synchronized because it must close the cursors and reset them atomically.
+         */
+        private synchronized void invalidate() {
+            MoreCloseables.closeQuietly(mNewCallsCursor);
+            MoreCloseables.closeQuietly(mOldCallsCursor);
+            mNewCallsCursor = null;
+            mOldCallsCursor = null;
+        }
+
         @Override
-        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+        protected synchronized void onQueryComplete(int token, Object cookie, Cursor cursor) {
+            if (token == QUERY_NEW_CALLS_TOKEN) {
+                // Store the returned cursor.
+                mNewCallsCursor = new ExtendedCursor(
+                        cursor, CallLogQuery.SECTION_NAME, CallLogQuery.SECTION_NEW_ITEM);
+            } else if (token == QUERY_OLD_CALLS_TOKEN) {
+                // Store the returned cursor.
+                mOldCallsCursor = new ExtendedCursor(
+                        cursor, CallLogQuery.SECTION_NAME, CallLogQuery.SECTION_OLD_ITEM);
+            } else {
+                Log.w(TAG, "Unknown query completed: ignoring: " + token);
+                return;
+            }
+
+            if (mNewCallsCursor != null && mOldCallsCursor != null) {
+                updateAdapterData(createMergedCursor());
+            }
+        }
+
+        /** Creates the merged cursor representing the data to show in the call log. */
+        @GuardedBy("this")
+        private Cursor createMergedCursor() {
+            try {
+                final boolean noNewCalls = mNewCallsCursor.getCount() == 0;
+                final boolean noOldCalls = mOldCallsCursor.getCount() == 0;
+
+                if (noNewCalls && noOldCalls) {
+                    // Nothing in either cursors.
+                    MoreCloseables.closeQuietly(mNewCallsCursor);
+                    return mOldCallsCursor;
+                }
+
+                if (noNewCalls) {
+                    // Return only the old calls.
+                    MoreCloseables.closeQuietly(mNewCallsCursor);
+                    return new MergeCursor(
+                            new Cursor[]{ createOldCallsHeaderCursor(), mOldCallsCursor });
+                }
+
+                if (noOldCalls) {
+                    // Return only the new calls.
+                    MoreCloseables.closeQuietly(mOldCallsCursor);
+                    return new MergeCursor(
+                            new Cursor[]{ createNewCallsHeaderCursor(), mNewCallsCursor });
+                }
+
+                return new MergeCursor(new Cursor[]{
+                        createNewCallsHeaderCursor(), mNewCallsCursor,
+                        createOldCallsHeaderCursor(), mOldCallsCursor});
+            } finally {
+                // Any cursor still open is now owned, directly or indirectly, by the caller.
+                mNewCallsCursor = null;
+                mOldCallsCursor = null;
+            }
+        }
+
+        /**
+         * Updates the adapter in the call log fragment to show the new cursor data.
+         */
+        private void updateAdapterData(Cursor combinedCursor) {
             final CallLogFragment fragment = mFragment.get();
             if (fragment != null && fragment.getActivity() != null &&
                     !fragment.getActivity().isFinishing()) {
-                final CallLogFragment.CallLogAdapter callsAdapter = fragment.mAdapter;
+                Log.d(TAG, "updating adapter");
+                final CallLogAdapter callsAdapter = fragment.mAdapter;
                 callsAdapter.setLoading(false);
-                callsAdapter.changeCursor(cursor);
+                callsAdapter.changeCursor(combinedCursor);
                 if (fragment.mScrollToTop) {
                     final ListView listView = fragment.getListView();
                     if (listView.getFirstVisiblePosition() > 5) {
@@ -804,8 +1034,6 @@
                     listView.smoothScrollToPosition(0);
                     fragment.mScrollToTop = false;
                 }
-            } else {
-                cursor.close();
             }
         }
     }
@@ -898,24 +1126,12 @@
     }
 
     private void resetNewCallsFlag() {
-        // Mark all "new" missed calls as not new anymore
-        StringBuilder where = new StringBuilder("type=");
-        where.append(Calls.MISSED_TYPE);
-        where.append(" AND new=1");
-
-        ContentValues values = new ContentValues(1);
-        values.put(Calls.NEW, "0");
-        mQueryHandler.startUpdate(UPDATE_TOKEN, null, Calls.CONTENT_URI_WITH_VOICEMAIL,
-                values, where.toString(), null);
+        mQueryHandler.updateMissedCalls();
     }
 
     private void startQuery() {
         mAdapter.setLoading(true);
-
-        // Cancel any pending queries
-        mQueryHandler.cancelOperation(QUERY_TOKEN);
-        mQueryHandler.startQuery(QUERY_TOKEN, null, Calls.CONTENT_URI_WITH_VOICEMAIL,
-                CallLogQuery._PROJECTION, null, null, Calls.DEFAULT_SORT_ORDER);
+        mQueryHandler.fetchCalls();
     }
 
     @Override
diff --git a/src/com/android/contacts/calllog/CallLogListItemHelper.java b/src/com/android/contacts/calllog/CallLogListItemHelper.java
index a8894da..4011929 100644
--- a/src/com/android/contacts/calllog/CallLogListItemHelper.java
+++ b/src/com/android/contacts/calllog/CallLogListItemHelper.java
@@ -57,10 +57,12 @@
      * @param views the views to populate
      * @param details the details of a phone call needed to fill in the data
      * @param useIcons whether to use icons to show the type of the call
+     * @param isHighlighted whether to use the highlight text for the call
      */
     public void setPhoneCallDetails(CallLogListItemViews views, PhoneCallDetails details,
-            boolean useIcons) {
-        mPhoneCallDetailsHelper.setPhoneCallDetails(views.phoneCallDetailsViews, details, useIcons);
+            boolean useIcons, boolean isHighlighted) {
+        mPhoneCallDetailsHelper.setPhoneCallDetails(views.phoneCallDetailsViews, details, useIcons,
+                isHighlighted);
         if (views.callView != null) {
             // The type of icon, call or play, is determined by the first call in the group.
             views.callView.setImageDrawable(
diff --git a/src/com/android/contacts/calllog/CallLogListItemViews.java b/src/com/android/contacts/calllog/CallLogListItemViews.java
index 0cf15cc..75c54a4 100644
--- a/src/com/android/contacts/calllog/CallLogListItemViews.java
+++ b/src/com/android/contacts/calllog/CallLogListItemViews.java
@@ -22,6 +22,7 @@
 import android.view.View;
 import android.widget.ImageView;
 import android.widget.QuickContactBadge;
+import android.widget.TextView;
 
 /**
  * Simple value object containing the various views within a call log entry.
@@ -33,22 +34,37 @@
     public final ImageView callView;
     /** The details of the phone call. */
     public final PhoneCallDetailsViews phoneCallDetailsViews;
+    /** The item view for a stand-alone row, or null for other types of rows. */
+    public final View standAloneItemView;
+    /** The header view for a stand-alone row, or null for other types of rows. */
+    public final View standAloneHeaderView;
+    /** The text of the header in a stand-alone row, or null for other types of rows. */
+    public final TextView standAloneHeaderTextView;
 
     private CallLogListItemViews(QuickContactBadge photoView, ImageView callView,
-            PhoneCallDetailsViews phoneCallDetailsViews) {
+            PhoneCallDetailsViews phoneCallDetailsViews, View standAloneItemView,
+            View standAloneHeaderView, TextView standAloneHeaderTextView) {
         this.photoView = photoView;
         this.callView = callView;
         this.phoneCallDetailsViews = phoneCallDetailsViews;
+        this.standAloneItemView = standAloneItemView;
+        this.standAloneHeaderView = standAloneHeaderView;
+        this.standAloneHeaderTextView = standAloneHeaderTextView;
     }
 
     public static CallLogListItemViews fromView(View view) {
         return new CallLogListItemViews((QuickContactBadge) view.findViewById(R.id.contact_photo),
                 (ImageView) view.findViewById(R.id.call_icon),
-                PhoneCallDetailsViews.fromView(view));
+                PhoneCallDetailsViews.fromView(view),
+                view.findViewById(R.id.call_log_item),
+                view.findViewById(R.id.call_log_header),
+                (TextView) view.findViewById(R.id.call_log_header_text));
     }
 
     public static CallLogListItemViews createForTest(QuickContactBadge photoView,
-            ImageView callView, PhoneCallDetailsViews phoneCallDetailsViews) {
-        return new CallLogListItemViews(photoView, callView, phoneCallDetailsViews);
+            ImageView callView, PhoneCallDetailsViews phoneCallDetailsViews,
+            View standAloneItemView, View standAloneHeaderView, TextView standAloneHeaderTextView) {
+        return new CallLogListItemViews(photoView, callView, phoneCallDetailsViews,
+                standAloneItemView, standAloneHeaderView, standAloneHeaderTextView);
     }
 }
diff --git a/src/com/android/contacts/calllog/CallTypeHelper.java b/src/com/android/contacts/calllog/CallTypeHelper.java
index b06a1c1..0c2068e 100644
--- a/src/com/android/contacts/calllog/CallTypeHelper.java
+++ b/src/com/android/contacts/calllog/CallTypeHelper.java
@@ -19,8 +19,13 @@
 import com.android.contacts.R;
 
 import android.content.res.Resources;
+import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
 import android.provider.CallLog.Calls;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
 
 /**
  * Helper class to perform operations related to call types.
@@ -35,13 +40,17 @@
     /** Icon for voicemails. */
     private final Drawable mVoicemailDrawable;
     /** Name used to identify incoming calls. */
-    private final String mIncomingName;
+    private final CharSequence mIncomingName;
     /** Name used to identify outgoing calls. */
-    private final String mOutgoingName;
+    private final CharSequence mOutgoingName;
     /** Name used to identify missed calls. */
-    private final String mMissedName;
+    private final CharSequence mMissedName;
     /** Name used to identify voicemail calls. */
-    private final String mVoicemailName;
+    private final CharSequence mVoicemailName;
+    /** Name used to identify new missed calls. */
+    private final CharSequence mNewMissedName;
+    /** Name used to identify new voicemail calls. */
+    private final CharSequence mNewVoicemailName;
 
     public CallTypeHelper(Resources resources, Drawable incomingDrawable, Drawable outgoingDrawable,
             Drawable missedDrawable, Drawable voicemailDrawable) {
@@ -54,10 +63,14 @@
         mOutgoingName = resources.getString(R.string.type_outgoing);
         mMissedName = resources.getString(R.string.type_missed);
         mVoicemailName = resources.getString(R.string.type_voicemail);
+        mNewMissedName = addBoldAndColor(mMissedName,
+                resources.getColor(R.color.call_log_missed_call_highlight_color));
+        mNewVoicemailName = addBoldAndColor(mVoicemailName,
+                resources.getColor(R.color.call_log_voicemail_highlight_color));
     }
 
     /** Returns the text used to represent the given call type. */
-    public String getCallTypeText(int callType) {
+    public CharSequence getCallTypeText(int callType) {
         switch (callType) {
             case Calls.INCOMING_TYPE:
                 return mIncomingName;
@@ -76,6 +89,28 @@
         }
     }
 
+    /** Returns the text used to represent the given call type. */
+    public CharSequence getHighlightedCallTypeText(int callType) {
+        switch (callType) {
+            case Calls.INCOMING_TYPE:
+                // New incoming calls are not highlighted.
+                return mIncomingName;
+
+            case Calls.OUTGOING_TYPE:
+                // New outgoing calls are not highlighted.
+                return mOutgoingName;
+
+            case Calls.MISSED_TYPE:
+                return mNewMissedName;
+
+            case Calls.VOICEMAIL_TYPE:
+                return mNewVoicemailName;
+
+            default:
+                throw new IllegalArgumentException("invalid call type: " + callType);
+        }
+    }
+
     /** Returns the drawable of the icon associated with the given call type. */
     public Drawable getCallTypeDrawable(int callType) {
         switch (callType) {
@@ -95,4 +130,13 @@
                 throw new IllegalArgumentException("invalid call type: " + callType);
         }
     }
+
+    /** Creates a SpannableString for the given text which is bold and in the given color. */
+    private CharSequence addBoldAndColor(CharSequence text, int color) {
+        int flags = Spanned.SPAN_INCLUSIVE_INCLUSIVE;
+        SpannableString result = new SpannableString(text);
+        result.setSpan(new StyleSpan(Typeface.BOLD), 0, text.length(), flags);
+        result.setSpan(new ForegroundColorSpan(color), 0, text.length(), flags);
+        return result;
+    }
 }
diff --git a/src/com/android/contacts/calllog/ExtendedCursor.java b/src/com/android/contacts/calllog/ExtendedCursor.java
new file mode 100644
index 0000000..b17c018
--- /dev/null
+++ b/src/com/android/contacts/calllog/ExtendedCursor.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.contacts.calllog;
+
+import com.android.common.io.MoreCloseables;
+
+import android.database.AbstractCursor;
+import android.database.Cursor;
+
+/**
+ * Wraps a cursor to add an additional column with the same value for all rows.
+ * <p>
+ * The number of rows in the cursor and the set of columns is determined by the cursor being
+ * wrapped.
+ */
+public class ExtendedCursor extends AbstractCursor {
+    /** The cursor to wrap. */
+    private final Cursor mCursor;
+    /** The name of the additional column. */
+    private final String mColumnName;
+    /** The value to be assigned to the additional column. */
+    private final Object mValue;
+
+    /**
+     * Creates a new cursor which extends the given cursor by adding a column with a constant value.
+     *
+     * @param cursor the cursor to extend
+     * @param columnName the name of the additional column
+     * @param value the value to be assigned to the additional column
+     */
+    public ExtendedCursor(Cursor cursor, String columnName, Object value) {
+        mCursor = cursor;
+        mColumnName = columnName;
+        mValue = value;
+    }
+
+    @Override
+    public int getCount() {
+        return mCursor.getCount();
+    }
+
+    @Override
+    public String[] getColumnNames() {
+        String[] columnNames = mCursor.getColumnNames();
+        int length = columnNames.length;
+        String[] extendedColumnNames = new String[length + 1];
+        System.arraycopy(columnNames, 0, extendedColumnNames, 0, length);
+        extendedColumnNames[length] = mColumnName;
+        return extendedColumnNames;
+    }
+
+    @Override
+    public String getString(int column) {
+        if (column == mCursor.getColumnCount()) {
+            return (String) mValue;
+        }
+        return mCursor.getString(column);
+    }
+
+    @Override
+    public short getShort(int column) {
+        if (column == mCursor.getColumnCount()) {
+            return (Short) mValue;
+        }
+        return mCursor.getShort(column);
+    }
+
+    @Override
+    public int getInt(int column) {
+        if (column == mCursor.getColumnCount()) {
+            return (Integer) mValue;
+        }
+        return mCursor.getInt(column);
+    }
+
+    @Override
+    public long getLong(int column) {
+        if (column == mCursor.getColumnCount()) {
+            return (Long) mValue;
+        }
+        return mCursor.getLong(column);
+    }
+
+    @Override
+    public float getFloat(int column) {
+        if (column == mCursor.getColumnCount()) {
+            return (Float) mValue;
+        }
+        return mCursor.getFloat(column);
+    }
+
+    @Override
+    public double getDouble(int column) {
+        if (column == mCursor.getColumnCount()) {
+            return (Double) mValue;
+        }
+        return mCursor.getDouble(column);
+    }
+
+    @Override
+    public boolean isNull(int column) {
+        if (column == mCursor.getColumnCount()) {
+            return mValue == null;
+        }
+        return mCursor.isNull(column);
+    }
+
+    @Override
+    public boolean onMove(int oldPosition, int newPosition) {
+        return mCursor.moveToPosition(newPosition);
+    }
+
+    @Override
+    public void close() {
+        MoreCloseables.closeQuietly(mCursor);
+        super.close();
+    }
+}
\ No newline at end of file
diff --git a/tests/src/com/android/contacts/PhoneCallDetailsHelperTest.java b/tests/src/com/android/contacts/PhoneCallDetailsHelperTest.java
index 4f95563..b628a5e 100644
--- a/tests/src/com/android/contacts/PhoneCallDetailsHelperTest.java
+++ b/tests/src/com/android/contacts/PhoneCallDetailsHelperTest.java
@@ -213,7 +213,7 @@
         mHelper.setPhoneCallDetails(mViews,
                 new PhoneCallDetails(number, formattedNumber, new int[]{ Calls.INCOMING_TYPE },
                         TEST_DATE, TEST_DURATION),
-                false);
+                false, false);
     }
 
     /** Sets the phone call details with default values and the given date. */
@@ -221,7 +221,7 @@
         mHelper.setPhoneCallDetails(mViews,
                 new PhoneCallDetails(TEST_NUMBER, TEST_FORMATTED_NUMBER,
                         new int[]{ Calls.INCOMING_TYPE }, date, TEST_DURATION),
-                false);
+                false, false);
     }
 
     /** Sets the phone call details with default values and the given call types using icons. */
@@ -238,6 +238,6 @@
         mHelper.setPhoneCallDetails(mViews,
                 new PhoneCallDetails(TEST_NUMBER, TEST_FORMATTED_NUMBER, callTypes, TEST_DATE,
                         TEST_DURATION),
-                useIcons);
+                useIcons, false);
     }
 }
diff --git a/tests/src/com/android/contacts/activities/CallLogActivityTests.java b/tests/src/com/android/contacts/activities/CallLogActivityTests.java
index 4004227..dfb6d0c 100644
--- a/tests/src/com/android/contacts/activities/CallLogActivityTests.java
+++ b/tests/src/com/android/contacts/activities/CallLogActivityTests.java
@@ -55,13 +55,14 @@
         extends ActivityInstrumentationTestCase2<CallLogActivity> {
     private static final String TAG = "CallLogActivityTests";
 
-    private static final String[] CALL_LOG_PROJECTION = new String[] {
+    private static final String[] EXTENDED_CALL_LOG_PROJECTION = new String[] {
             Calls._ID,
             Calls.NUMBER,
             Calls.DATE,
             Calls.DURATION,
             Calls.TYPE,
             Calls.COUNTRY_ISO,
+            CallLogFragment.CallLogQuery.SECTION_NAME,
     };
     private static final int RAND_DURATION = -1;
     private static final long NOW = -1L;
@@ -123,7 +124,7 @@
         mAdapter.disableRequestProcessingForTest();
         mAdapter.stopRequestProcessing();
         mParentView = new FrameLayout(mActivity);
-        mCursor = new MatrixCursor(CALL_LOG_PROJECTION);
+        mCursor = new MatrixCursor(EXTENDED_CALL_LOG_PROJECTION);
         buildIconMap();
     }
 
@@ -431,6 +432,7 @@
         }
         row.add(type);  // type
         row.add(TEST_COUNTRY_ISO);  // country ISO
+        row.add(CallLogFragment.CallLogQuery.SECTION_OLD_ITEM);  // section
     }
 
     /**
diff --git a/tests/src/com/android/contacts/calllog/CallLogListItemHelperTest.java b/tests/src/com/android/contacts/calllog/CallLogListItemHelperTest.java
index b311454..ae8906f 100644
--- a/tests/src/com/android/contacts/calllog/CallLogListItemHelperTest.java
+++ b/tests/src/com/android/contacts/calllog/CallLogListItemHelperTest.java
@@ -87,7 +87,8 @@
         mViews = CallLogListItemViews.createForTest(new QuickContactBadge(context),
                 new ImageView(context), PhoneCallDetailsViews.createForTest(new TextView(context),
                         new LinearLayout(context), new TextView(context), new TextView(context),
-                        new TextView(context), new TextView(context)));
+                        new TextView(context), new TextView(context)),
+                new View(context), new View(context), new TextView(context));
     }
 
     @Override
@@ -135,7 +136,7 @@
         mHelper.setPhoneCallDetails(mViews,
                 new PhoneCallDetails(number, formattedNumber, new int[]{ Calls.INCOMING_TYPE },
                         TEST_DATE, TEST_DURATION),
-                true);
+                true, false);
     }
 
     /** Sets the details of a phone call using the specified call type. */
@@ -143,6 +144,6 @@
         mHelper.setPhoneCallDetails(mViews,
                 new PhoneCallDetails(
                         TEST_NUMBER, TEST_FORMATTED_NUMBER, types, TEST_DATE, TEST_DURATION),
-                true);
+                true, false);
     }
 }