Split new and older items in the call log.
This commit changes the way call log items are displayed.
Instead of using a single cursor showing all items, it creates two
cursors, one for new items (missed calls and voicemail with the NEW
field set to 1) and older items (missed calls and voicemail with the NEW
field set to 0 and incoming and outgoing calls).
To make this as much as possible transparent to the UI code, it actually
creates a merged cursor from the two cursor above, so that the UI does
not need to know about the fact that these correspond to two different
queries.
To allocate space for the headers (which are present only if the
underlying cursor is not empty), they are also added to the merged
cursor, using MatrixCursors.
Since the UI needs to know whether to show a header, a new call log
item, or an older call log item, add a synthetic column called "section"
which represents which section a given row belongs to.
In the process, encapsulate the details of the queries being made to the
call log. Probably a follow-up can be done to further hide the details
of how the query works.
Change-Id: I0be6ac5b4ca5b0ccd74a648a5d8687e05ad77a82
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 af36e21..d6b4f56 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -40,6 +40,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>
@@ -154,6 +156,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);
}
}