Merge "Use solid black background for phone search UI"
diff --git a/res/menu/call_log_options.xml b/res/menu/call_log_options.xml
index c62be77..c75c856 100644
--- a/res/menu/call_log_options.xml
+++ b/res/menu/call_log_options.xml
@@ -29,10 +29,4 @@
         android:id="@+id/show_all_calls"
         android:title="@string/menu_show_all_calls"
         android:showAsAction="withText" />
-
-    <item
-        android:id="@+id/menu_call_settings_call_log"
-        android:title="@string/call_settings"
-        android:icon="@drawable/ic_menu_settings_holo_light"
-        android:showAsAction="withText" />
 </menu>
diff --git a/res/menu/dialpad_options.xml b/res/menu/dialpad_options.xml
index 4dc62a8..77da9cb 100644
--- a/res/menu/dialpad_options.xml
+++ b/res/menu/dialpad_options.xml
@@ -30,10 +30,4 @@
         android:icon="@drawable/ic_menu_wait"
         android:title="@string/add_wait"
         android:showAsAction="withText" />
-
-    <item
-        android:id="@+id/menu_call_settings_dialpad"
-        android:title="@string/call_settings"
-        android:icon="@drawable/ic_menu_settings_holo_light"
-        android:showAsAction="withText" />
 </menu>
diff --git a/res/menu/dialtacts_options.xml b/res/menu/dialtacts_options.xml
index 99f87ff..cc9543a 100644
--- a/res/menu/dialtacts_options.xml
+++ b/res/menu/dialtacts_options.xml
@@ -20,6 +20,12 @@
         android:showAsAction="always" />
 
     <item
+        android:id="@+id/menu_call_settings"
+        android:title="@string/call_settings"
+        android:icon="@drawable/ic_menu_settings_holo_light"
+        android:showAsAction="withText" />
+
+    <item
         android:id="@+id/filter_option"
         android:title="@string/menu_contacts_filter"
         android:showAsAction="withText" />
diff --git a/src/com/android/contacts/ContactLoader.java b/src/com/android/contacts/ContactLoader.java
index ceaa246..dbfe411 100644
--- a/src/com/android/contacts/ContactLoader.java
+++ b/src/com/android/contacts/ContactLoader.java
@@ -23,6 +23,7 @@
 import com.android.contacts.util.StreamItemPhotoEntry;
 import com.google.android.collect.Lists;
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Sets;
 
 import android.content.ContentResolver;
 import android.content.ContentUris;
@@ -62,6 +63,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Loads a single Contact and all it constituent RawContacts.
@@ -76,7 +78,7 @@
     private Result mContact;
     private ForceLoadContentObserver mObserver;
     private boolean mDestroyed;
-
+    private final Set<Long> mNotifiedRawContactIds = Sets.newHashSet();
 
     public interface Listener {
         public void onContactLoaded(Result contact);
@@ -1115,14 +1117,18 @@
         Context context = getContext();
         for (Entity entity : mContact.getEntities()) {
             final ContentValues entityValues = entity.getEntityValues();
+            final long rawContactId = entityValues.getAsLong(RawContacts.Entity._ID);
+            if (mNotifiedRawContactIds.contains(rawContactId)) {
+                continue; // Already notified for this raw contact.
+            }
+            mNotifiedRawContactIds.add(rawContactId);
             final String type = entityValues.getAsString(RawContacts.ACCOUNT_TYPE);
             final String dataSet = entityValues.getAsString(RawContacts.DATA_SET);
-            final AccountType accountType = AccountTypeManager.getInstance(context ).getAccountType(
+            final AccountType accountType = AccountTypeManager.getInstance(context).getAccountType(
                     type, dataSet);
             final String serviceName = accountType.getViewContactNotifyServiceClassName();
             final String resPackageName = accountType.resPackageName;
             if (!TextUtils.isEmpty(serviceName) && !TextUtils.isEmpty(resPackageName)) {
-                final long rawContactId = entityValues.getAsLong(RawContacts.Entity._ID);
                 final Uri uri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
                 final Intent intent = new Intent();
                 intent.setClassName(resPackageName, serviceName);
diff --git a/src/com/android/contacts/ContactSaveService.java b/src/com/android/contacts/ContactSaveService.java
index 3bb330f..78c4b18 100644
--- a/src/com/android/contacts/ContactSaveService.java
+++ b/src/com/android/contacts/ContactSaveService.java
@@ -53,8 +53,8 @@
 
 import java.util.ArrayList;
 import java.util.HashSet;
-import java.util.LinkedList;
 import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
 
 /**
  * A service responsible for saving changes to the content provider.
@@ -133,7 +133,8 @@
         public void onServiceCompleted(Intent callbackIntent);
     }
 
-    private static final LinkedList<Listener> sListeners = new LinkedList<Listener>();
+    private static final CopyOnWriteArrayList<Listener> sListeners =
+            new CopyOnWriteArrayList<Listener>();
 
     private Handler mMainHandler;
 
@@ -148,15 +149,11 @@
             throw new ClassCastException("Only activities can be registered to"
                     + " receive callback from " + ContactSaveService.class.getName());
         }
-        synchronized (sListeners) {
-            sListeners.addFirst(listener);
-        }
+        sListeners.add(0, listener);
     }
 
     public static void unregisterListener(Listener listener) {
-        synchronized (sListeners) {
-            sListeners.remove(listener);
-        }
+        sListeners.remove(listener);
     }
 
     @Override
@@ -975,13 +972,11 @@
         // TODO: this assumes that if there are multiple instances of the same
         // activity registered, the last one registered is the one waiting for
         // the callback. Validity of this assumption needs to be verified.
-        synchronized (sListeners) {
-            for (Listener listener : sListeners) {
-                if (callbackIntent.getComponent().equals(
-                        ((Activity) listener).getIntent().getComponent())) {
-                    listener.onServiceCompleted(callbackIntent);
-                    return;
-                }
+        for (Listener listener : sListeners) {
+            if (callbackIntent.getComponent().equals(
+                    ((Activity) listener).getIntent().getComponent())) {
+                listener.onServiceCompleted(callbackIntent);
+                return;
             }
         }
     }
diff --git a/src/com/android/contacts/ContactsUtils.java b/src/com/android/contacts/ContactsUtils.java
index 76cbc7d..45ce4fe 100644
--- a/src/com/android/contacts/ContactsUtils.java
+++ b/src/com/android/contacts/ContactsUtils.java
@@ -16,10 +16,10 @@
 
 package com.android.contacts;
 
-import com.google.i18n.phonenumbers.NumberParseException;
-import com.google.i18n.phonenumbers.PhoneNumberUtil;
-import com.google.i18n.phonenumbers.PhoneNumberUtil.MatchType;
-import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
+import com.android.i18n.phonenumbers.NumberParseException;
+import com.android.i18n.phonenumbers.PhoneNumberUtil;
+import com.android.i18n.phonenumbers.PhoneNumberUtil.MatchType;
+import com.android.i18n.phonenumbers.Phonenumber.PhoneNumber;
 
 import android.content.Context;
 import android.content.Intent;
diff --git a/src/com/android/contacts/PhoneCallDetailsHelper.java b/src/com/android/contacts/PhoneCallDetailsHelper.java
index e970fcc..e79bdce 100644
--- a/src/com/android/contacts/PhoneCallDetailsHelper.java
+++ b/src/com/android/contacts/PhoneCallDetailsHelper.java
@@ -107,7 +107,8 @@
             mPhoneNumberHelper.getDisplayNumber(details.number, details.formattedNumber);
         if (TextUtils.isEmpty(details.name)) {
             nameText = displayNumber;
-            if (TextUtils.isEmpty(details.geocode)) {
+            if (TextUtils.isEmpty(details.geocode)
+                    || mPhoneNumberHelper.isVoicemailNumber(details.number)) {
                 numberText = mResources.getString(R.string.call_log_empty_gecode);
             } else {
                 numberText = details.geocode;
diff --git a/src/com/android/contacts/activities/DialtactsActivity.java b/src/com/android/contacts/activities/DialtactsActivity.java
index cfca831..d0acc6b 100644
--- a/src/com/android/contacts/activities/DialtactsActivity.java
+++ b/src/com/android/contacts/activities/DialtactsActivity.java
@@ -644,19 +644,24 @@
     public boolean onPrepareOptionsMenu(Menu menu) {
         final MenuItem searchMenuItem = menu.findItem(R.id.search_on_action_bar);
         final MenuItem filterOptionMenuItem = menu.findItem(R.id.filter_option);
+        final MenuItem callSettingsMenuItem = menu.findItem(R.id.menu_call_settings);
         Tab tab = getActionBar().getSelectedTab();
         if (mInSearchUi) {
             searchMenuItem.setVisible(false);
             filterOptionMenuItem.setVisible(true);
             filterOptionMenuItem.setOnMenuItemClickListener(
                     mFilterOptionsMenuItemClickListener);
-        } else if (tab == null || tab.getPosition() == TAB_INDEX_DIALER) {
-            searchMenuItem.setVisible(false);
-            filterOptionMenuItem.setVisible(false);
+            callSettingsMenuItem.setVisible(false);
         } else {
+            if (tab != null && tab.getPosition() == TAB_INDEX_DIALER) {
+                searchMenuItem.setVisible(false);
+            } else {
+                searchMenuItem.setVisible(true);
+                searchMenuItem.setOnMenuItemClickListener(mSearchMenuItemClickListener);
+            }
             filterOptionMenuItem.setVisible(false);
-            searchMenuItem.setVisible(true);
-            searchMenuItem.setOnMenuItemClickListener(mSearchMenuItemClickListener);
+            callSettingsMenuItem.setVisible(true);
+            callSettingsMenuItem.setIntent(DialtactsActivity.getCallSettingsIntent());
         }
 
         return true;
diff --git a/src/com/android/contacts/calllog/CallLogAdapter.java b/src/com/android/contacts/calllog/CallLogAdapter.java
new file mode 100644
index 0000000..7e934b6
--- /dev/null
+++ b/src/com/android/contacts/calllog/CallLogAdapter.java
@@ -0,0 +1,755 @@
+/*
+ * 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.widget.GroupingListAdapter;
+import com.android.contacts.ContactPhotoManager;
+import com.android.contacts.PhoneCallDetails;
+import com.android.contacts.PhoneCallDetailsHelper;
+import com.android.contacts.R;
+import com.android.contacts.util.ExpirableCache;
+import com.google.common.annotations.VisibleForTesting;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.CommonDataKinds.SipAddress;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.PhoneLookup;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+
+import java.util.LinkedList;
+
+/**
+ * Adapter class to fill in data for the Call Log.
+ */
+public final class CallLogAdapter extends GroupingListAdapter
+        implements Runnable, ViewTreeObserver.OnPreDrawListener, CallLogGroupBuilder.GroupCreator {
+    /** Interface used to initiate a refresh of the content. */
+    public interface CallFetcher {
+        public void startCallsQuery();
+    }
+
+    /** The time in millis to delay starting the thread processing requests. */
+    private static final int START_PROCESSING_REQUESTS_DELAY_MILLIS = 1000;
+
+    /** The size of the cache of contact info. */
+    private static final int CONTACT_INFO_CACHE_SIZE = 100;
+
+    private final Context mContext;
+    private final String mCurrentCountryIso;
+    private final CallFetcher mCallFetcher;
+
+    /**
+     * A cache of the contact details for the phone numbers in the call log.
+     * <p>
+     * The content of the cache is expired (but not purged) whenever the application comes to
+     * the foreground.
+     */
+    private ExpirableCache<String, ContactInfo> mContactInfoCache;
+
+    /**
+     * List of requests to update contact details.
+     * <p>
+     * The requests are added when displaying the contacts and are processed by a background
+     * thread.
+     */
+    private final LinkedList<String> mRequests;
+
+    private volatile boolean mDone;
+    private boolean mLoading = true;
+    private ViewTreeObserver.OnPreDrawListener mPreDrawListener;
+    private static final int REDRAW = 1;
+    private static final int START_THREAD = 2;
+    private boolean mFirst;
+    private Thread mCallerIdThread;
+
+    /** Instance of helper class for managing views. */
+    private final CallLogListItemHelper mCallLogViewsHelper;
+
+    /** Helper to set up contact photos. */
+    private final ContactPhotoManager mContactPhotoManager;
+    /** Helper to parse and process phone numbers. */
+    private PhoneNumberHelper mPhoneNumberHelper;
+    /** Helper to group call log entries. */
+    private final CallLogGroupBuilder mCallLogGroupBuilder;
+
+    /** Can be set to true by tests to disable processing of requests. */
+    private volatile boolean mRequestProcessingDisabled = false;
+
+    /** Listener for the primary action in the list, opens the call details. */
+    private final View.OnClickListener mPrimaryActionListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View view) {
+            IntentProvider intentProvider = (IntentProvider) view.getTag();
+            if (intentProvider != null) {
+                mContext.startActivity(intentProvider.getIntent(mContext));
+            }
+        }
+    };
+    /** Listener for the secondary action in the list, either call or play. */
+    private final View.OnClickListener mSecondaryActionListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View view) {
+            IntentProvider intentProvider = (IntentProvider) view.getTag();
+            if (intentProvider != null) {
+                mContext.startActivity(intentProvider.getIntent(mContext));
+            }
+        }
+    };
+
+    @Override
+    public boolean onPreDraw() {
+        if (mFirst) {
+            mHandler.sendEmptyMessageDelayed(START_THREAD,
+                    START_PROCESSING_REQUESTS_DELAY_MILLIS);
+            mFirst = false;
+        }
+        return true;
+    }
+
+    private Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case REDRAW:
+                    notifyDataSetChanged();
+                    break;
+                case START_THREAD:
+                    startRequestProcessing();
+                    break;
+            }
+        }
+    };
+
+    public CallLogAdapter(Context context, CallFetcher callFetcher,
+            String currentCountryIso, String voicemailNumber) {
+        super(context);
+
+        mContext = context;
+        mCurrentCountryIso = currentCountryIso;
+        mCallFetcher = callFetcher;
+
+        mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE);
+        mRequests = new LinkedList<String>();
+        mPreDrawListener = null;
+
+        Resources resources = mContext.getResources();
+        CallTypeHelper callTypeHelper = new CallTypeHelper(resources);
+
+        mContactPhotoManager = ContactPhotoManager.getInstance(mContext);
+        mPhoneNumberHelper = new PhoneNumberHelper(resources, voicemailNumber);
+        PhoneCallDetailsHelper phoneCallDetailsHelper = new PhoneCallDetailsHelper(
+                resources, callTypeHelper, mPhoneNumberHelper);
+        mCallLogViewsHelper =
+                new CallLogListItemHelper(
+                        phoneCallDetailsHelper, mPhoneNumberHelper, resources);
+        mCallLogGroupBuilder = new CallLogGroupBuilder(this);
+    }
+
+    /**
+     * Requery on background thread when {@link Cursor} changes.
+     */
+    @Override
+    protected void onContentChanged() {
+        // When the content changes, always fetch all the calls, in case a new missed call came
+        // in and we were filtering over voicemail only, so that we see the missed call.
+        mCallFetcher.startCallsQuery();
+    }
+
+    void setLoading(boolean loading) {
+        mLoading = loading;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        if (mLoading) {
+            // We don't want the empty state to show when loading.
+            return false;
+        } else {
+            return super.isEmpty();
+        }
+    }
+
+    public ContactInfo getContactInfo(String number) {
+        return mContactInfoCache.getPossiblyExpired(number);
+    }
+
+    public void startRequestProcessing() {
+        if (mRequestProcessingDisabled) {
+            return;
+        }
+
+        mDone = false;
+        mCallerIdThread = new Thread(this, "CallLogContactLookup");
+        mCallerIdThread.setPriority(Thread.MIN_PRIORITY);
+        mCallerIdThread.start();
+    }
+
+    /**
+     * Stops the background thread that processes updates and cancels any pending requests to
+     * start it.
+     * <p>
+     * Should be called from the main thread to prevent a race condition between the request to
+     * start the thread being processed and stopping the thread.
+     */
+    public void stopRequestProcessing() {
+        // Remove any pending requests to start the processing thread.
+        mHandler.removeMessages(START_THREAD);
+        mDone = true;
+        if (mCallerIdThread != null) mCallerIdThread.interrupt();
+    }
+
+    public void invalidateCache() {
+        mContactInfoCache.expireAll();
+        // Let it restart the thread after next draw
+        mPreDrawListener = null;
+    }
+
+    private void enqueueRequest(String number, boolean immediate) {
+        synchronized (mRequests) {
+            if (!mRequests.contains(number)) {
+                mRequests.add(number);
+                mRequests.notifyAll();
+            }
+        }
+        if (mFirst && immediate) {
+            startRequestProcessing();
+            mFirst = false;
+        }
+    }
+
+    /**
+     * Determines the contact information for the given SIP address.
+     * <p>
+     * It returns the contact info if found.
+     * <p>
+     * If no contact corresponds to the given SIP address, returns {@link ContactInfo#EMPTY}.
+     * <p>
+     * If the lookup fails for some other reason, it returns null.
+     */
+    private ContactInfo queryContactInfoForSipAddress(String sipAddress) {
+        final ContactInfo info;
+
+        // TODO: This code is duplicated from the
+        // CallerInfoAsyncQuery class.  To avoid that, could the
+        // code here just use CallerInfoAsyncQuery, rather than
+        // manually running ContentResolver.query() itself?
+
+        // We look up SIP addresses directly in the Data table:
+        Uri contactRef = Data.CONTENT_URI;
+
+        // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent.
+        //
+        // Also note we use "upper(data1)" in the WHERE clause, and
+        // uppercase the incoming SIP address, in order to do a
+        // case-insensitive match.
+        //
+        // TODO: May also need to normalize by adding "sip:" as a
+        // prefix, if we start storing SIP addresses that way in the
+        // database.
+        String selection = "upper(" + Data.DATA1 + ")=?"
+                + " AND "
+                + Data.MIMETYPE + "='" + SipAddress.CONTENT_ITEM_TYPE + "'";
+        String[] selectionArgs = new String[] { sipAddress.toUpperCase() };
+
+        Cursor dataTableCursor =
+                mContext.getContentResolver().query(
+                        contactRef,
+                        null,  // projection
+                        selection,  // selection
+                        selectionArgs,  // selectionArgs
+                        null);  // sortOrder
+
+        if (dataTableCursor != null) {
+            if (dataTableCursor.moveToFirst()) {
+                info = new ContactInfo();
+
+                // TODO: we could slightly speed this up using an
+                // explicit projection (and thus not have to do
+                // those getColumnIndex() calls) but the benefit is
+                // very minimal.
+
+                // Note the Data.CONTACT_ID column here is
+                // equivalent to the PERSON_ID_COLUMN_INDEX column
+                // we use with "phonesCursor" below.
+                info.personId = dataTableCursor.getLong(
+                        dataTableCursor.getColumnIndex(Data.CONTACT_ID));
+                info.name = dataTableCursor.getString(
+                        dataTableCursor.getColumnIndex(Data.DISPLAY_NAME));
+                // "type" and "label" are currently unused for SIP addresses
+                info.type = SipAddress.TYPE_OTHER;
+                info.label = null;
+
+                // And "number" is the SIP address.
+                // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent.
+                info.number = dataTableCursor.getString(
+                        dataTableCursor.getColumnIndex(Data.DATA1));
+                info.normalizedNumber = null;  // meaningless for SIP addresses
+                final String thumbnailUriString = dataTableCursor.getString(
+                        dataTableCursor.getColumnIndex(Data.PHOTO_THUMBNAIL_URI));
+                info.thumbnailUri = thumbnailUriString == null
+                        ? null
+                        : Uri.parse(thumbnailUriString);
+                info.lookupKey = dataTableCursor.getString(
+                        dataTableCursor.getColumnIndex(Data.LOOKUP_KEY));
+            } else {
+                info = ContactInfo.EMPTY;
+            }
+            dataTableCursor.close();
+        } else {
+            // Failed to fetch the data, ignore this request.
+            info = null;
+        }
+        return info;
+    }
+
+    /**
+     * Determines the contact information for the given phone number.
+     * <p>
+     * It returns the contact info if found.
+     * <p>
+     * If no contact corresponds to the given phone number, returns {@link ContactInfo#EMPTY}.
+     * <p>
+     * If the lookup fails for some other reason, it returns null.
+     */
+    private ContactInfo queryContactInfoForPhoneNumber(String number) {
+        final ContactInfo info;
+
+        // "number" is a regular phone number, so use the
+        // PhoneLookup table:
+        Cursor phonesCursor =
+                mContext.getContentResolver().query(
+                    Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI,
+                            Uri.encode(number)),
+                            PhoneQuery._PROJECTION, null, null, null);
+        if (phonesCursor != null) {
+            if (phonesCursor.moveToFirst()) {
+                info = new ContactInfo();
+                info.personId = phonesCursor.getLong(PhoneQuery.PERSON_ID);
+                info.name = phonesCursor.getString(PhoneQuery.NAME);
+                info.type = phonesCursor.getInt(PhoneQuery.PHONE_TYPE);
+                info.label = phonesCursor.getString(PhoneQuery.LABEL);
+                info.number = phonesCursor
+                        .getString(PhoneQuery.MATCHED_NUMBER);
+                info.normalizedNumber = phonesCursor
+                        .getString(PhoneQuery.NORMALIZED_NUMBER);
+                final String thumbnailUriString = phonesCursor.getString(
+                        PhoneQuery.THUMBNAIL_URI);
+                info.thumbnailUri = thumbnailUriString == null
+                        ? null
+                        : Uri.parse(thumbnailUriString);
+                info.lookupKey = phonesCursor.getString(PhoneQuery.LOOKUP_KEY);
+            } else {
+                info = ContactInfo.EMPTY;
+            }
+            phonesCursor.close();
+        } else {
+            // Failed to fetch the data, ignore this request.
+            info = null;
+        }
+        return info;
+    }
+
+    /**
+     * Queries the appropriate content provider for the contact associated with the number.
+     * <p>
+     * The number might be either a SIP address or a phone number.
+     * <p>
+     * It returns true if it updated the content of the cache and we should therefore tell the
+     * view to update its content.
+     */
+    private boolean queryContactInfo(String number) {
+        final ContactInfo info;
+
+        // Determine the contact info.
+        if (PhoneNumberUtils.isUriNumber(number)) {
+            // This "number" is really a SIP address.
+            info = queryContactInfoForSipAddress(number);
+        } else {
+            info = queryContactInfoForPhoneNumber(number);
+        }
+
+        if (info == null) {
+            // The lookup failed, just return without requesting to update the view.
+            return false;
+        }
+
+        // Check the existing entry in the cache: only if it has changed we should update the
+        // view.
+        ContactInfo existingInfo = mContactInfoCache.getPossiblyExpired(number);
+        boolean updated = !info.equals(existingInfo);
+        if (updated) {
+            // The formattedNumber is computed by the UI thread when needed. Since we updated
+            // the details of the contact, set this value to null for now.
+            info.formattedNumber = null;
+        }
+        // Store the data in the cache so that the UI thread can use to display it. Store it
+        // even if it has not changed so that it is marked as not expired.
+        mContactInfoCache.put(number, info);
+        return updated;
+    }
+
+    /*
+     * Handles requests for contact name and number type
+     * @see java.lang.Runnable#run()
+     */
+    @Override
+    public void run() {
+        boolean needNotify = false;
+        while (!mDone) {
+            String number = null;
+            synchronized (mRequests) {
+                if (!mRequests.isEmpty()) {
+                    number = mRequests.removeFirst();
+                } else {
+                    if (needNotify) {
+                        needNotify = false;
+                        mHandler.sendEmptyMessage(REDRAW);
+                    }
+                    try {
+                        mRequests.wait(1000);
+                    } catch (InterruptedException ie) {
+                        // Ignore and continue processing requests
+                        Thread.currentThread().interrupt();
+                    }
+                }
+            }
+            if (!mDone && number != null && queryContactInfo(number)) {
+                needNotify = true;
+            }
+        }
+    }
+
+    @Override
+    protected void addGroups(Cursor cursor) {
+        mCallLogGroupBuilder.addGroups(cursor);
+    }
+
+    @VisibleForTesting
+    @Override
+    public View newStandAloneView(Context context, ViewGroup parent) {
+        LayoutInflater inflater =
+                (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
+        findAndCacheViews(view);
+        return view;
+    }
+
+    @VisibleForTesting
+    @Override
+    public void bindStandAloneView(View view, Context context, Cursor cursor) {
+        bindView(view, cursor, 1);
+    }
+
+    @VisibleForTesting
+    @Override
+    public View newChildView(Context context, ViewGroup parent) {
+        LayoutInflater inflater =
+                (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
+        findAndCacheViews(view);
+        return view;
+    }
+
+    @VisibleForTesting
+    @Override
+    public void bindChildView(View view, Context context, Cursor cursor) {
+        bindView(view, cursor, 1);
+    }
+
+    @VisibleForTesting
+    @Override
+    public View newGroupView(Context context, ViewGroup parent) {
+        LayoutInflater inflater =
+                (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
+        findAndCacheViews(view);
+        return view;
+    }
+
+    @VisibleForTesting
+    @Override
+    public void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
+            boolean expanded) {
+        bindView(view, cursor, groupSize);
+    }
+
+    private void findAndCacheViews(View view) {
+        // Get the views to bind to.
+        CallLogListItemViews views = CallLogListItemViews.fromView(view);
+        views.primaryActionView.setOnClickListener(mPrimaryActionListener);
+        views.secondaryActionView.setOnClickListener(mSecondaryActionListener);
+        view.setTag(views);
+    }
+
+    /**
+     * Binds the views in the entry to the data in the call log.
+     *
+     * @param view the view corresponding to this entry
+     * @param c the cursor pointing to the entry in the call log
+     * @param count the number of entries in the current item, greater than 1 if it is a group
+     */
+    private void bindView(View view, Cursor c, int count) {
+        final CallLogListItemViews views = (CallLogListItemViews) view.getTag();
+        final int section = c.getInt(CallLogQuery.SECTION);
+
+        // This might 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.listItemView.setVisibility(View.GONE);
+            views.listHeaderView.setVisibility(View.VISIBLE);
+            views.listHeaderTextView.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.listItemView.setVisibility(View.VISIBLE);
+        views.listHeaderView.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 int callType = c.getInt(CallLogQuery.CALL_TYPE);
+        final String formattedNumber;
+        final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO);
+
+        final ContactInfo cachedContactInfo = getContactInfoFromCallLog(c);
+
+        views.primaryActionView.setTag(
+                IntentProvider.getCallDetailIntentProvider(
+                        this, c.getPosition(), c.getLong(CallLogQuery.ID), count));
+        // Store away the voicemail information so we can play it directly.
+        if (callType == Calls.VOICEMAIL_TYPE) {
+            String voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI);
+            final long rowId = c.getLong(CallLogQuery.ID);
+            views.secondaryActionView.setTag(
+                    IntentProvider.getPlayVoicemailIntentProvider(rowId, voicemailUri));
+        } else if (!TextUtils.isEmpty(number)) {
+            // Store away the number so we can call it directly if you click on the call icon.
+            views.secondaryActionView.setTag(
+                    IntentProvider.getReturnCallIntentProvider(number));
+        } else {
+            // No action enabled.
+            views.secondaryActionView.setTag(null);
+        }
+
+        // Lookup contacts with this number
+        ExpirableCache.CachedValue<ContactInfo> cachedInfo =
+                mContactInfoCache.getCachedValue(number);
+        ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue();
+        if (cachedInfo == null) {
+            // Mark it as empty and queue up a request to find the name.
+            // The db request should happen on a non-UI thread.
+            info = ContactInfo.EMPTY;
+            mContactInfoCache.put(number, info);
+            // Request the contact details immediately since they are currently missing.
+            enqueueRequest(number, true);
+            // Format the phone number in the call log as best as we can.
+            formattedNumber = formatPhoneNumber(number, null, countryIso);
+        } else {
+            if (cachedInfo.isExpired()) {
+                // The contact info is no longer up to date, we should request it. However, we
+                // do not need to request them immediately.
+                enqueueRequest(number, false);
+            }
+
+            if (info != ContactInfo.EMPTY) {
+                // Format and cache phone number for found contact.
+                if (info.formattedNumber == null) {
+                    info.formattedNumber =
+                            formatPhoneNumber(info.number, info.normalizedNumber, countryIso);
+                }
+                formattedNumber = info.formattedNumber;
+            } else {
+                // Format the phone number in the call log as best as we can.
+                formattedNumber = formatPhoneNumber(number, null, countryIso);
+            }
+        }
+
+        if (info == null || info == ContactInfo.EMPTY) {
+            info = cachedContactInfo;
+        }
+
+        final long personId = info.personId;
+        final String name = info.name;
+        final int ntype = info.type;
+        final String label = info.label;
+        final Uri thumbnailUri = info.thumbnailUri;
+        final String lookupKey = info.lookupKey;
+        final int[] callTypes = getCallTypes(c, count);
+        final String geocode = c.getString(CallLogQuery.GEOCODED_LOCATION);
+        final PhoneCallDetails details;
+        if (TextUtils.isEmpty(name)) {
+            details = new PhoneCallDetails(number, formattedNumber, countryIso, geocode,
+                    callTypes, date, duration);
+        } else {
+            details = new PhoneCallDetails(number, formattedNumber, countryIso, geocode,
+                    callTypes, date, duration, name, ntype, label, personId, thumbnailUri);
+        }
+
+        final boolean isNew = CallLogQuery.isNewSection(c);
+        // New items also use the highlighted version of the text.
+        final boolean isHighlighted = isNew;
+        mCallLogViewsHelper.setPhoneCallDetails(views, details, isHighlighted);
+        setPhoto(views, thumbnailUri, personId, lookupKey);
+
+        // Listen for the first draw
+        if (mPreDrawListener == null) {
+            mFirst = true;
+            mPreDrawListener = this;
+            view.getViewTreeObserver().addOnPreDrawListener(this);
+        }
+    }
+
+    /** Returns the contact information as stored in the call log. */
+    private ContactInfo getContactInfoFromCallLog(Cursor c) {
+        ContactInfo info = new ContactInfo();
+        info.personId = -1;
+        info.name = c.getString(CallLogQuery.CACHED_NAME);
+        info.type = c.getInt(CallLogQuery.CACHED_NUMBER_TYPE);
+        info.label = c.getString(CallLogQuery.CACHED_NUMBER_LABEL);
+        // TODO: This should be added to the call log cached values.
+        info.number = c.getString(CallLogQuery.NUMBER);
+        info.formattedNumber = info.number;
+        info.normalizedNumber = info.number;
+        info.thumbnailUri = null;
+        info.lookupKey = null;
+        return info;
+    }
+
+    /**
+     * Returns the call types for the given number of items in the cursor.
+     * <p>
+     * It uses the next {@code count} rows in the cursor to extract the types.
+     * <p>
+     * It position in the cursor is unchanged by this function.
+     */
+    private int[] getCallTypes(Cursor cursor, int count) {
+        int position = cursor.getPosition();
+        int[] callTypes = new int[count];
+        for (int index = 0; index < count; ++index) {
+            callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE);
+            cursor.moveToNext();
+        }
+        cursor.moveToPosition(position);
+        return callTypes;
+    }
+
+    private void setPhoto(CallLogListItemViews views, Uri thumbnailUri, long contactId,
+            String lookupKey) {
+        views.quickContactView.assignContactUri(contactId == -1 ? null :
+                Contacts.getLookupUri(contactId, lookupKey));
+        mContactPhotoManager.loadPhoto(views.quickContactView, thumbnailUri);
+    }
+
+    /**
+     * Sets whether processing of requests for contact details should be enabled.
+     * <p>
+     * This method should be called in tests to disable such processing of requests when not
+     * needed.
+     */
+    public void disableRequestProcessingForTest() {
+        mRequestProcessingDisabled = true;
+    }
+
+    public void injectContactInfoForTest(String number, ContactInfo contactInfo) {
+        mContactInfoCache.put(number, contactInfo);
+    }
+
+    @Override
+    public void addGroup(int cursorPosition, int size, boolean expanded) {
+        super.addGroup(cursorPosition, size, expanded);
+    }
+
+    /**
+     * Format the given phone number
+     *
+     * @param number the number to be formatted.
+     * @param normalizedNumber the normalized number of the given number.
+     * @param countryIso the ISO 3166-1 two letters country code, the country's
+     *        convention will be used to format the number if the normalized
+     *        phone is null.
+     *
+     * @return the formatted number, or the given number if it was formatted.
+     */
+    private String formatPhoneNumber(String number, String normalizedNumber,
+            String countryIso) {
+        if (TextUtils.isEmpty(number)) {
+            return "";
+        }
+        // If "number" is really a SIP address, don't try to do any formatting at all.
+        if (PhoneNumberUtils.isUriNumber(number)) {
+            return number;
+        }
+        if (TextUtils.isEmpty(countryIso)) {
+            countryIso = mCurrentCountryIso;
+        }
+        return PhoneNumberUtils.formatNumber(number, normalizedNumber, countryIso);
+    }
+
+    /*
+     * Get the number from the Contacts, if available, since sometimes
+     * the number provided by caller id may not be formatted properly
+     * depending on the carrier (roaming) in use at the time of the
+     * incoming call.
+     * Logic : If the caller-id number starts with a "+", use it
+     *         Else if the number in the contacts starts with a "+", use that one
+     *         Else if the number in the contacts is longer, use that one
+     */
+    public String getBetterNumberFromContacts(String number) {
+        String matchingNumber = null;
+        // Look in the cache first. If it's not found then query the Phones db
+        ContactInfo ci = mContactInfoCache.getPossiblyExpired(number);
+        if (ci != null && ci != ContactInfo.EMPTY) {
+            matchingNumber = ci.number;
+        } else {
+            try {
+                Cursor phonesCursor = mContext.getContentResolver().query(
+                        Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number),
+                        PhoneQuery._PROJECTION, null, null, null);
+                if (phonesCursor != null) {
+                    if (phonesCursor.moveToFirst()) {
+                        matchingNumber = phonesCursor.getString(PhoneQuery.MATCHED_NUMBER);
+                    }
+                    phonesCursor.close();
+                }
+            } catch (Exception e) {
+                // Use the number from the call log
+            }
+        }
+        if (!TextUtils.isEmpty(matchingNumber) &&
+                (matchingNumber.startsWith("+")
+                        || matchingNumber.length() > number.length())) {
+            number = matchingNumber;
+        }
+        return number;
+    }
+}
diff --git a/src/com/android/contacts/calllog/CallLogFragment.java b/src/com/android/contacts/calllog/CallLogFragment.java
index 2d0ddbd..215fd7b 100644
--- a/src/com/android/contacts/calllog/CallLogFragment.java
+++ b/src/com/android/contacts/calllog/CallLogFragment.java
@@ -16,40 +16,27 @@
 
 package com.android.contacts.calllog;
 
-import com.android.common.widget.GroupingListAdapter;
-import com.android.contacts.ContactPhotoManager;
 import com.android.contacts.ContactsUtils;
-import com.android.contacts.PhoneCallDetails;
-import com.android.contacts.PhoneCallDetailsHelper;
 import com.android.contacts.R;
 import com.android.contacts.activities.DialtactsActivity;
 import com.android.contacts.activities.DialtactsActivity.ViewPagerVisibilityListener;
 import com.android.contacts.test.NeededForTesting;
-import com.android.contacts.util.ExpirableCache;
 import com.android.contacts.voicemail.VoicemailStatusHelper;
 import com.android.contacts.voicemail.VoicemailStatusHelper.StatusMessage;
 import com.android.contacts.voicemail.VoicemailStatusHelperImpl;
 import com.android.internal.telephony.CallerInfo;
 import com.android.internal.telephony.ITelephony;
-import com.google.common.annotations.VisibleForTesting;
 
 import android.app.KeyguardManager;
 import android.app.ListFragment;
 import android.content.Context;
 import android.content.Intent;
-import android.content.res.Resources;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Bundle;
-import android.os.Handler;
-import android.os.Message;
 import android.os.RemoteException;
 import android.os.ServiceManager;
 import android.provider.CallLog.Calls;
-import android.provider.ContactsContract.CommonDataKinds.SipAddress;
-import android.provider.ContactsContract.Contacts;
-import android.provider.ContactsContract.Data;
-import android.provider.ContactsContract.PhoneLookup;
 import android.telephony.PhoneNumberUtils;
 import android.telephony.TelephonyManager;
 import android.text.TextUtils;
@@ -60,108 +47,18 @@
 import android.view.MenuItem;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.ViewTreeObserver;
 import android.widget.ListView;
 import android.widget.TextView;
 
-import java.util.LinkedList;
 import java.util.List;
 
 /**
  * Displays a list of call log entries.
  */
 public class CallLogFragment extends ListFragment implements ViewPagerVisibilityListener,
-        CallLogQueryHandler.Listener {
+        CallLogQueryHandler.Listener, CallLogAdapter.CallFetcher {
     private static final String TAG = "CallLogFragment";
 
-    /** The size of the cache of contact info. */
-    private static final int CONTACT_INFO_CACHE_SIZE = 100;
-
-    /** The query for the call log table. */
-    public static final class CallLogQuery {
-        // If you alter this, you must also alter the method that inserts a fake row to the headers
-        // in the CallLogQueryHandler class called createHeaderCursorFor().
-        public static final String[] _PROJECTION = new String[] {
-                Calls._ID,
-                Calls.NUMBER,
-                Calls.DATE,
-                Calls.DURATION,
-                Calls.TYPE,
-                Calls.COUNTRY_ISO,
-                Calls.VOICEMAIL_URI,
-                Calls.GEOCODED_LOCATION,
-        };
-
-        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;
-        public static final int VOICEMAIL_URI = 6;
-        public static final int GEOCODED_LOCATION = 7;
-
-        /**
-         * 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 = 8;
-        /** 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 call log projection including the section name. */
-        public static final String[] EXTENDED_PROJECTION;
-        static {
-            EXTENDED_PROJECTION = new String[_PROJECTION.length + 1];
-            System.arraycopy(_PROJECTION, 0, EXTENDED_PROJECTION, 0, _PROJECTION.length);
-            EXTENDED_PROJECTION[_PROJECTION.length] = SECTION_NAME;
-        }
-
-        public static boolean isSectionHeader(Cursor cursor) {
-            int section = cursor.getInt(CallLogQuery.SECTION);
-            return section == CallLogQuery.SECTION_NEW_HEADER
-                    || section == CallLogQuery.SECTION_OLD_HEADER;
-        }
-
-        public static boolean isNewSection(Cursor cursor) {
-            int section = cursor.getInt(CallLogQuery.SECTION);
-            return section == CallLogQuery.SECTION_NEW_ITEM
-                    || section == CallLogQuery.SECTION_NEW_HEADER;
-        }
-    }
-
-    /** The query to use for the phones table */
-    private static final class PhoneQuery {
-        public static final String[] _PROJECTION = new String[] {
-                PhoneLookup._ID,
-                PhoneLookup.DISPLAY_NAME,
-                PhoneLookup.TYPE,
-                PhoneLookup.LABEL,
-                PhoneLookup.NUMBER,
-                PhoneLookup.NORMALIZED_NUMBER,
-                PhoneLookup.PHOTO_THUMBNAIL_URI,
-                PhoneLookup.LOOKUP_KEY};
-
-        public static final int PERSON_ID = 0;
-        public static final int NAME = 1;
-        public static final int PHONE_TYPE = 2;
-        public static final int LABEL = 3;
-        public static final int MATCHED_NUMBER = 4;
-        public static final int NORMALIZED_NUMBER = 5;
-        public static final int THUMBNAIL_URI = 6;
-        public static final int LOOKUP_KEY = 7;
-    }
-
     private CallLogAdapter mAdapter;
     private CallLogQueryHandler mCallLogQueryHandler;
     private String mVoiceMailNumber;
@@ -177,702 +74,6 @@
     private TextView mStatusMessageAction;
     private KeyguardManager mKeyguardManager;
 
-    public static final class ContactInfo {
-        public long personId = -1;
-        public String name;
-        public int type;
-        public String label;
-        public String number;
-        public String formattedNumber;
-        public String normalizedNumber;
-        public Uri thumbnailUri;
-        public String lookupKey;
-
-        public static ContactInfo EMPTY = new ContactInfo();
-
-        @Override
-        public int hashCode() {
-            // Uses only name and personId to determine hashcode.
-            // This should be sufficient to have a reasonable distribution of hash codes.
-            // Moreover, there should be no two people with the same personId.
-            final int prime = 31;
-            int result = 1;
-            result = prime * result + (int) (personId ^ (personId >>> 32));
-            result = prime * result + ((name == null) ? 0 : name.hashCode());
-            return result;
-        }
-
-        @Override
-        public boolean equals(Object obj) {
-            if (this == obj) return true;
-            if (obj == null) return false;
-            if (getClass() != obj.getClass()) return false;
-            ContactInfo other = (ContactInfo) obj;
-            if (personId != other.personId) return false;
-            if (!TextUtils.equals(name, other.name)) return false;
-            if (type != other.type) return false;
-            if (!TextUtils.equals(label, other.label)) return false;
-            if (!TextUtils.equals(number, other.number)) return false;
-            // Ignore formatted number.
-            if (!TextUtils.equals(normalizedNumber, other.normalizedNumber)) return false;
-            if (!uriEquals(thumbnailUri, other.thumbnailUri)) return false;
-            if (!TextUtils.equals(lookupKey, other.lookupKey)) return false;
-            return true;
-        }
-
-        private static boolean uriEquals(Uri thumbnailUri1, Uri thumbnailUri2) {
-            if (thumbnailUri1 == thumbnailUri2) return true;
-            if (thumbnailUri1 == null) return false;
-            return thumbnailUri1.equals(thumbnailUri2);
-        }
-    }
-
-    public interface GroupCreator {
-        public void addGroup(int cursorPosition, int size, boolean expanded);
-    }
-
-    public interface CallFetcher {
-        public void fetchAllCalls();
-    }
-
-    /** Adapter class to fill in data for the Call Log */
-    public static final class CallLogAdapter extends GroupingListAdapter
-            implements Runnable, ViewTreeObserver.OnPreDrawListener, GroupCreator {
-        /** The time in millis to delay starting the thread processing requests. */
-        private static final int START_PROCESSING_REQUESTS_DELAY_MILLIS = 1000;
-
-        private final Context mContext;
-        private final String mCurrentCountryIso;
-        private final CallFetcher mCallFetcher;
-
-        /**
-         * A cache of the contact details for the phone numbers in the call log.
-         * <p>
-         * The content of the cache is expired (but not purged) whenever the application comes to
-         * the foreground.
-         */
-        private ExpirableCache<String, ContactInfo> mContactInfoCache;
-
-        /**
-         * List of requests to update contact details.
-         * <p>
-         * The requests are added when displaying the contacts and are processed by a background
-         * thread.
-         */
-        private final LinkedList<String> mRequests;
-
-        private volatile boolean mDone;
-        private boolean mLoading = true;
-        private ViewTreeObserver.OnPreDrawListener mPreDrawListener;
-        private static final int REDRAW = 1;
-        private static final int START_THREAD = 2;
-        private boolean mFirst;
-        private Thread mCallerIdThread;
-
-        /** Instance of helper class for managing views. */
-        private final CallLogListItemHelper mCallLogViewsHelper;
-
-        /** Helper to set up contact photos. */
-        private final ContactPhotoManager mContactPhotoManager;
-        /** Helper to parse and process phone numbers. */
-        private PhoneNumberHelper mPhoneNumberHelper;
-        /** Helper to group call log entries. */
-        private final CallLogGroupBuilder mCallLogGroupBuilder;
-
-        /** Can be set to true by tests to disable processing of requests. */
-        private volatile boolean mRequestProcessingDisabled = false;
-
-        /** Listener for the primary action in the list, opens the call details. */
-        private final View.OnClickListener mPrimaryActionListener = new View.OnClickListener() {
-            @Override
-            public void onClick(View view) {
-                IntentProvider intentProvider = (IntentProvider) view.getTag();
-                if (intentProvider != null) {
-                    mContext.startActivity(intentProvider.getIntent(mContext));
-                }
-            }
-        };
-        /** Listener for the secondary action in the list, either call or play. */
-        private final View.OnClickListener mSecondaryActionListener = new View.OnClickListener() {
-            @Override
-            public void onClick(View view) {
-                IntentProvider intentProvider = (IntentProvider) view.getTag();
-                if (intentProvider != null) {
-                    mContext.startActivity(intentProvider.getIntent(mContext));
-                }
-            }
-        };
-
-        @Override
-        public boolean onPreDraw() {
-            if (mFirst) {
-                mHandler.sendEmptyMessageDelayed(START_THREAD,
-                        START_PROCESSING_REQUESTS_DELAY_MILLIS);
-                mFirst = false;
-            }
-            return true;
-        }
-
-        private Handler mHandler = new Handler() {
-            @Override
-            public void handleMessage(Message msg) {
-                switch (msg.what) {
-                    case REDRAW:
-                        notifyDataSetChanged();
-                        break;
-                    case START_THREAD:
-                        startRequestProcessing();
-                        break;
-                }
-            }
-        };
-
-        public CallLogAdapter(Context context, CallFetcher callFetcher,
-                String currentCountryIso, String voicemailNumber) {
-            super(context);
-
-            mContext = context;
-            mCurrentCountryIso = currentCountryIso;
-            mCallFetcher = callFetcher;
-
-            mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE);
-            mRequests = new LinkedList<String>();
-            mPreDrawListener = null;
-
-            Resources resources = mContext.getResources();
-            CallTypeHelper callTypeHelper = new CallTypeHelper(resources);
-
-            mContactPhotoManager = ContactPhotoManager.getInstance(mContext);
-            mPhoneNumberHelper = new PhoneNumberHelper(resources, voicemailNumber);
-            PhoneCallDetailsHelper phoneCallDetailsHelper = new PhoneCallDetailsHelper(
-                    resources, callTypeHelper, mPhoneNumberHelper);
-            mCallLogViewsHelper =
-                    new CallLogListItemHelper(
-                            phoneCallDetailsHelper, mPhoneNumberHelper, resources);
-            mCallLogGroupBuilder = new CallLogGroupBuilder(this);
-        }
-
-        /**
-         * Requery on background thread when {@link Cursor} changes.
-         */
-        @Override
-        protected void onContentChanged() {
-            // When the content changes, always fetch all the calls, in case a new missed call came
-            // in and we were filtering over voicemail only, so that we see the missed call.
-            mCallFetcher.fetchAllCalls();
-        }
-
-        void setLoading(boolean loading) {
-            mLoading = loading;
-        }
-
-        @Override
-        public boolean isEmpty() {
-            if (mLoading) {
-                // We don't want the empty state to show when loading.
-                return false;
-            } else {
-                return super.isEmpty();
-            }
-        }
-
-        public ContactInfo getContactInfo(String number) {
-            return mContactInfoCache.getPossiblyExpired(number);
-        }
-
-        public void startRequestProcessing() {
-            if (mRequestProcessingDisabled) {
-                return;
-            }
-
-            mDone = false;
-            mCallerIdThread = new Thread(this, "CallLogContactLookup");
-            mCallerIdThread.setPriority(Thread.MIN_PRIORITY);
-            mCallerIdThread.start();
-        }
-
-        /**
-         * Stops the background thread that processes updates and cancels any pending requests to
-         * start it.
-         * <p>
-         * Should be called from the main thread to prevent a race condition between the request to
-         * start the thread being processed and stopping the thread.
-         */
-        public void stopRequestProcessing() {
-            // Remove any pending requests to start the processing thread.
-            mHandler.removeMessages(START_THREAD);
-            mDone = true;
-            if (mCallerIdThread != null) mCallerIdThread.interrupt();
-        }
-
-        public void invalidateCache() {
-            mContactInfoCache.expireAll();
-        }
-
-        private void enqueueRequest(String number, boolean immediate) {
-            synchronized (mRequests) {
-                if (!mRequests.contains(number)) {
-                    mRequests.add(number);
-                    mRequests.notifyAll();
-                }
-            }
-            if (mFirst && immediate) {
-                startRequestProcessing();
-                mFirst = false;
-            }
-        }
-
-        /**
-         * Determines the contact information for the given SIP address.
-         * <p>
-         * It returns the contact info if found.
-         * <p>
-         * If no contact corresponds to the given SIP address, returns {@link ContactInfo#EMPTY}.
-         * <p>
-         * If the lookup fails for some other reason, it returns null.
-         */
-        private ContactInfo queryContactInfoForSipAddress(String sipAddress) {
-            final ContactInfo info;
-
-            // TODO: This code is duplicated from the
-            // CallerInfoAsyncQuery class.  To avoid that, could the
-            // code here just use CallerInfoAsyncQuery, rather than
-            // manually running ContentResolver.query() itself?
-
-            // We look up SIP addresses directly in the Data table:
-            Uri contactRef = Data.CONTENT_URI;
-
-            // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent.
-            //
-            // Also note we use "upper(data1)" in the WHERE clause, and
-            // uppercase the incoming SIP address, in order to do a
-            // case-insensitive match.
-            //
-            // TODO: May also need to normalize by adding "sip:" as a
-            // prefix, if we start storing SIP addresses that way in the
-            // database.
-            String selection = "upper(" + Data.DATA1 + ")=?"
-                    + " AND "
-                    + Data.MIMETYPE + "='" + SipAddress.CONTENT_ITEM_TYPE + "'";
-            String[] selectionArgs = new String[] { sipAddress.toUpperCase() };
-
-            Cursor dataTableCursor =
-                    mContext.getContentResolver().query(
-                            contactRef,
-                            null,  // projection
-                            selection,  // selection
-                            selectionArgs,  // selectionArgs
-                            null);  // sortOrder
-
-            if (dataTableCursor != null) {
-                if (dataTableCursor.moveToFirst()) {
-                    info = new ContactInfo();
-
-                    // TODO: we could slightly speed this up using an
-                    // explicit projection (and thus not have to do
-                    // those getColumnIndex() calls) but the benefit is
-                    // very minimal.
-
-                    // Note the Data.CONTACT_ID column here is
-                    // equivalent to the PERSON_ID_COLUMN_INDEX column
-                    // we use with "phonesCursor" below.
-                    info.personId = dataTableCursor.getLong(
-                            dataTableCursor.getColumnIndex(Data.CONTACT_ID));
-                    info.name = dataTableCursor.getString(
-                            dataTableCursor.getColumnIndex(Data.DISPLAY_NAME));
-                    // "type" and "label" are currently unused for SIP addresses
-                    info.type = SipAddress.TYPE_OTHER;
-                    info.label = null;
-
-                    // And "number" is the SIP address.
-                    // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent.
-                    info.number = dataTableCursor.getString(
-                            dataTableCursor.getColumnIndex(Data.DATA1));
-                    info.normalizedNumber = null;  // meaningless for SIP addresses
-                    final String thumbnailUriString = dataTableCursor.getString(
-                            dataTableCursor.getColumnIndex(Data.PHOTO_THUMBNAIL_URI));
-                    info.thumbnailUri = thumbnailUriString == null
-                            ? null
-                            : Uri.parse(thumbnailUriString);
-                    info.lookupKey = dataTableCursor.getString(
-                            dataTableCursor.getColumnIndex(Data.LOOKUP_KEY));
-                } else {
-                    info = ContactInfo.EMPTY;
-                }
-                dataTableCursor.close();
-            } else {
-                // Failed to fetch the data, ignore this request.
-                info = null;
-            }
-            return info;
-        }
-
-        /**
-         * Determines the contact information for the given phone number.
-         * <p>
-         * It returns the contact info if found.
-         * <p>
-         * If no contact corresponds to the given phone number, returns {@link ContactInfo#EMPTY}.
-         * <p>
-         * If the lookup fails for some other reason, it returns null.
-         */
-        private ContactInfo queryContactInfoForPhoneNumber(String number) {
-            final ContactInfo info;
-
-            // "number" is a regular phone number, so use the
-            // PhoneLookup table:
-            Cursor phonesCursor =
-                    mContext.getContentResolver().query(
-                        Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI,
-                                Uri.encode(number)),
-                                PhoneQuery._PROJECTION, null, null, null);
-            if (phonesCursor != null) {
-                if (phonesCursor.moveToFirst()) {
-                    info = new ContactInfo();
-                    info.personId = phonesCursor.getLong(PhoneQuery.PERSON_ID);
-                    info.name = phonesCursor.getString(PhoneQuery.NAME);
-                    info.type = phonesCursor.getInt(PhoneQuery.PHONE_TYPE);
-                    info.label = phonesCursor.getString(PhoneQuery.LABEL);
-                    info.number = phonesCursor
-                            .getString(PhoneQuery.MATCHED_NUMBER);
-                    info.normalizedNumber = phonesCursor
-                            .getString(PhoneQuery.NORMALIZED_NUMBER);
-                    final String thumbnailUriString = phonesCursor.getString(
-                            PhoneQuery.THUMBNAIL_URI);
-                    info.thumbnailUri = thumbnailUriString == null
-                            ? null
-                            : Uri.parse(thumbnailUriString);
-                    info.lookupKey = phonesCursor.getString(PhoneQuery.LOOKUP_KEY);
-                } else {
-                    info = ContactInfo.EMPTY;
-                }
-                phonesCursor.close();
-            } else {
-                // Failed to fetch the data, ignore this request.
-                info = null;
-            }
-            return info;
-        }
-
-        /**
-         * Queries the appropriate content provider for the contact associated with the number.
-         * <p>
-         * The number might be either a SIP address or a phone number.
-         * <p>
-         * It returns true if it updated the content of the cache and we should therefore tell the
-         * view to update its content.
-         */
-        private boolean queryContactInfo(String number) {
-            final ContactInfo info;
-
-            // Determine the contact info.
-            if (PhoneNumberUtils.isUriNumber(number)) {
-                // This "number" is really a SIP address.
-                info = queryContactInfoForSipAddress(number);
-            } else {
-                info = queryContactInfoForPhoneNumber(number);
-            }
-
-            if (info == null) {
-                // The lookup failed, just return without requesting to update the view.
-                return false;
-            }
-
-            // Check the existing entry in the cache: only if it has changed we should update the
-            // view.
-            ContactInfo existingInfo = mContactInfoCache.getPossiblyExpired(number);
-            boolean updated = !info.equals(existingInfo);
-            if (updated) {
-                // The formattedNumber is computed by the UI thread when needed. Since we updated
-                // the details of the contact, set this value to null for now.
-                info.formattedNumber = null;
-            }
-            // Store the data in the cache so that the UI thread can use to display it. Store it
-            // even if it has not changed so that it is marked as not expired.
-            mContactInfoCache.put(number, info);
-            return updated;
-        }
-
-        /*
-         * Handles requests for contact name and number type
-         * @see java.lang.Runnable#run()
-         */
-        @Override
-        public void run() {
-            boolean needNotify = false;
-            while (!mDone) {
-                String number = null;
-                synchronized (mRequests) {
-                    if (!mRequests.isEmpty()) {
-                        number = mRequests.removeFirst();
-                    } else {
-                        if (needNotify) {
-                            needNotify = false;
-                            mHandler.sendEmptyMessage(REDRAW);
-                        }
-                        try {
-                            mRequests.wait(1000);
-                        } catch (InterruptedException ie) {
-                            // Ignore and continue processing requests
-                            Thread.currentThread().interrupt();
-                        }
-                    }
-                }
-                if (!mDone && number != null && queryContactInfo(number)) {
-                    needNotify = true;
-                }
-            }
-        }
-
-        @Override
-        protected void addGroups(Cursor cursor) {
-            mCallLogGroupBuilder.addGroups(cursor);
-        }
-
-        @VisibleForTesting
-        @Override
-        public View newStandAloneView(Context context, ViewGroup parent) {
-            LayoutInflater inflater =
-                    (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-            View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
-            findAndCacheViews(view);
-            return view;
-        }
-
-        @VisibleForTesting
-        @Override
-        public void bindStandAloneView(View view, Context context, Cursor cursor) {
-            bindView(view, cursor, 1);
-        }
-
-        @VisibleForTesting
-        @Override
-        public View newChildView(Context context, ViewGroup parent) {
-            LayoutInflater inflater =
-                    (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-            View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
-            findAndCacheViews(view);
-            return view;
-        }
-
-        @VisibleForTesting
-        @Override
-        public void bindChildView(View view, Context context, Cursor cursor) {
-            bindView(view, cursor, 1);
-        }
-
-        @VisibleForTesting
-        @Override
-        public View newGroupView(Context context, ViewGroup parent) {
-            LayoutInflater inflater =
-                    (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-            View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
-            findAndCacheViews(view);
-            return view;
-        }
-
-        @VisibleForTesting
-        @Override
-        public void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
-                boolean expanded) {
-            bindView(view, cursor, groupSize);
-        }
-
-        private void findAndCacheViews(View view) {
-            // Get the views to bind to.
-            CallLogListItemViews views = CallLogListItemViews.fromView(view);
-            views.primaryActionView.setOnClickListener(mPrimaryActionListener);
-            views.secondaryActionView.setOnClickListener(mSecondaryActionListener);
-            view.setTag(views);
-        }
-
-        /**
-         * Binds the views in the entry to the data in the call log.
-         *
-         * @param view the view corresponding to this entry
-         * @param c the cursor pointing to the entry in the call log
-         * @param count the number of entries in the current item, greater than 1 if it is a group
-         */
-        private void bindView(View view, Cursor c, int count) {
-            final CallLogListItemViews views = (CallLogListItemViews) view.getTag();
-            final int section = c.getInt(CallLogQuery.SECTION);
-
-            // This might 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.listItemView.setVisibility(View.GONE);
-                views.listHeaderView.setVisibility(View.VISIBLE);
-                views.listHeaderTextView.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.listItemView.setVisibility(View.VISIBLE);
-            views.listHeaderView.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 int callType = c.getInt(CallLogQuery.CALL_TYPE);
-            final String formattedNumber;
-            final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO);
-
-            views.primaryActionView.setTag(
-                    IntentProvider.getCallDetailIntentProvider(
-                            this, c.getPosition(), c.getLong(CallLogQuery.ID), count));
-            // Store away the voicemail information so we can play it directly.
-            if (callType == Calls.VOICEMAIL_TYPE) {
-                String voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI);
-                final long rowId = c.getLong(CallLogQuery.ID);
-                views.secondaryActionView.setTag(
-                        IntentProvider.getPlayVoicemailIntentProvider(rowId, voicemailUri));
-            } else if (!TextUtils.isEmpty(number)) {
-                // Store away the number so we can call it directly if you click on the call icon.
-                views.secondaryActionView.setTag(
-                        IntentProvider.getReturnCallIntentProvider(number));
-            } else {
-                // No action enabled.
-                views.secondaryActionView.setTag(null);
-            }
-
-            // Lookup contacts with this number
-            ExpirableCache.CachedValue<ContactInfo> cachedInfo =
-                    mContactInfoCache.getCachedValue(number);
-            ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue();
-            if (cachedInfo == null) {
-                // Mark it as empty and queue up a request to find the name.
-                // The db request should happen on a non-UI thread.
-                info = ContactInfo.EMPTY;
-                mContactInfoCache.put(number, info);
-                // Request the contact details immediately since they are currently missing.
-                enqueueRequest(number, true);
-                // Format the phone number in the call log as best as we can.
-                formattedNumber = formatPhoneNumber(number, null, countryIso);
-            } else {
-                if (cachedInfo.isExpired()) {
-                    // The contact info is no longer up to date, we should request it. However, we
-                    // do not need to request them immediately.
-                    enqueueRequest(number, false);
-                }
-
-                if (info != ContactInfo.EMPTY) {
-                    // Format and cache phone number for found contact.
-                    if (info.formattedNumber == null) {
-                        info.formattedNumber =
-                                formatPhoneNumber(info.number, info.normalizedNumber, countryIso);
-                    }
-                    formattedNumber = info.formattedNumber;
-                } else {
-                    // Format the phone number in the call log as best as we can.
-                    formattedNumber = formatPhoneNumber(number, null, countryIso);
-                }
-            }
-
-            final long personId = info.personId;
-            final String name = info.name;
-            final int ntype = info.type;
-            final String label = info.label;
-            final Uri thumbnailUri = info.thumbnailUri;
-            final String lookupKey = info.lookupKey;
-            final int[] callTypes = getCallTypes(c, count);
-            final String geocode = c.getString(CallLogQuery.GEOCODED_LOCATION);
-            final PhoneCallDetails details;
-            if (TextUtils.isEmpty(name)) {
-                details = new PhoneCallDetails(number, formattedNumber, countryIso, geocode,
-                        callTypes, date, duration);
-            } else {
-                details = new PhoneCallDetails(number, formattedNumber, countryIso, geocode,
-                        callTypes, date, duration, name, ntype, label, personId, thumbnailUri);
-            }
-
-            final boolean isNew = CallLogQuery.isNewSection(c);
-            // New items also use the highlighted version of the text.
-            final boolean isHighlighted = isNew;
-            mCallLogViewsHelper.setPhoneCallDetails(views, details, isHighlighted);
-            setPhoto(views, thumbnailUri, personId, lookupKey);
-
-            // Listen for the first draw
-            if (mPreDrawListener == null) {
-                mFirst = true;
-                mPreDrawListener = this;
-                view.getViewTreeObserver().addOnPreDrawListener(this);
-            }
-        }
-
-        /**
-         * Returns the call types for the given number of items in the cursor.
-         * <p>
-         * It uses the next {@code count} rows in the cursor to extract the types.
-         * <p>
-         * It position in the cursor is unchanged by this function.
-         */
-        private int[] getCallTypes(Cursor cursor, int count) {
-            int position = cursor.getPosition();
-            int[] callTypes = new int[count];
-            for (int index = 0; index < count; ++index) {
-                callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE);
-                cursor.moveToNext();
-            }
-            cursor.moveToPosition(position);
-            return callTypes;
-        }
-
-        private void setPhoto(CallLogListItemViews views, Uri thumbnailUri, long contactId,
-                String lookupKey) {
-            views.quickContactView.assignContactUri(contactId == -1 ? null :
-                    Contacts.getLookupUri(contactId, lookupKey));
-            mContactPhotoManager.loadPhoto(views.quickContactView, thumbnailUri);
-        }
-
-        /**
-         * Sets whether processing of requests for contact details should be enabled.
-         * <p>
-         * This method should be called in tests to disable such processing of requests when not
-         * needed.
-         */
-        public void disableRequestProcessingForTest() {
-            mRequestProcessingDisabled = true;
-        }
-
-        public void injectContactInfoForTest(String number, ContactInfo contactInfo) {
-            mContactInfoCache.put(number, contactInfo);
-        }
-
-        @Override
-        public void addGroup(int cursorPosition, int size, boolean expanded) {
-            super.addGroup(cursorPosition, size, expanded);
-        }
-
-        /**
-         * Format the given phone number
-         *
-         * @param number the number to be formatted.
-         * @param normalizedNumber the normalized number of the given number.
-         * @param countryIso the ISO 3166-1 two letters country code, the country's
-         *        convention will be used to format the number if the normalized
-         *        phone is null.
-         *
-         * @return the formatted number, or the given number if it was formatted.
-         */
-        private String formatPhoneNumber(String number, String normalizedNumber,
-                String countryIso) {
-            if (TextUtils.isEmpty(number)) {
-                return "";
-            }
-            // If "number" is really a SIP address, don't try to do any formatting at all.
-            if (PhoneNumberUtils.isUriNumber(number)) {
-                return number;
-            }
-            if (TextUtils.isEmpty(countryIso)) {
-                countryIso = mCurrentCountryIso;
-            }
-            return PhoneNumberUtils.formatNumber(number, normalizedNumber, countryIso);
-        }
-    }
-
     @Override
     public void onCreate(Bundle state) {
         super.onCreate(state);
@@ -928,13 +129,7 @@
     public void onViewCreated(View view, Bundle savedInstanceState) {
         super.onViewCreated(view, savedInstanceState);
         String currentCountryIso = ContactsUtils.getCurrentCountryIso(getActivity());
-        mAdapter = new CallLogAdapter(getActivity(),
-                new CallFetcher() {
-                    @Override
-                    public void fetchAllCalls() {
-                        startCallsQuery();
-                    }
-                }, currentCountryIso, mVoiceMailNumber);
+        mAdapter = new CallLogAdapter(getActivity(), this, currentCountryIso, mVoiceMailNumber);
         setListAdapter(mAdapter);
         getListView().setItemsCanFocus(true);
     }
@@ -1000,7 +195,8 @@
         mAdapter.changeCursor(null);
     }
 
-    private void startCallsQuery() {
+    @Override
+    public void startCallsQuery() {
         mAdapter.setLoading(true);
         mCallLogQueryHandler.fetchAllCalls();
         if (mShowingVoicemailOnly) {
@@ -1024,8 +220,6 @@
     @Override
     public void onPrepareOptionsMenu(Menu menu) {
         if (mShowOptionsMenu) {
-            menu.findItem(R.id.menu_call_settings_call_log)
-                .setIntent(DialtactsActivity.getCallSettingsIntent());
             menu.findItem(R.id.show_voicemails_only).setVisible(!mShowingVoicemailOnly);
             menu.findItem(R.id.show_all_calls).setVisible(mShowingVoicemailOnly);
         }
@@ -1052,45 +246,6 @@
                 return false;
         }
     }
-
-    /*
-     * Get the number from the Contacts, if available, since sometimes
-     * the number provided by caller id may not be formatted properly
-     * depending on the carrier (roaming) in use at the time of the
-     * incoming call.
-     * Logic : If the caller-id number starts with a "+", use it
-     *         Else if the number in the contacts starts with a "+", use that one
-     *         Else if the number in the contacts is longer, use that one
-     */
-    private String getBetterNumberFromContacts(String number) {
-        String matchingNumber = null;
-        // Look in the cache first. If it's not found then query the Phones db
-        ContactInfo ci = mAdapter.mContactInfoCache.getPossiblyExpired(number);
-        if (ci != null && ci != ContactInfo.EMPTY) {
-            matchingNumber = ci.number;
-        } else {
-            try {
-                Cursor phonesCursor = getActivity().getContentResolver().query(
-                        Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number),
-                        PhoneQuery._PROJECTION, null, null, null);
-                if (phonesCursor != null) {
-                    if (phonesCursor.moveToFirst()) {
-                        matchingNumber = phonesCursor.getString(PhoneQuery.MATCHED_NUMBER);
-                    }
-                    phonesCursor.close();
-                }
-            } catch (Exception e) {
-                // Use the number from the call log
-            }
-        }
-        if (!TextUtils.isEmpty(matchingNumber) &&
-                (matchingNumber.startsWith("+")
-                        || matchingNumber.length() > number.length())) {
-            number = matchingNumber;
-        }
-        return number;
-    }
-
     public void callSelectedEntry() {
         int position = getListView().getSelectedItemPosition();
         if (position < 0) {
@@ -1122,7 +277,7 @@
                        (callType == Calls.INCOMING_TYPE
                                 || callType == Calls.MISSED_TYPE)) {
                     // If the caller-id matches a contact with a better qualified number, use it
-                    number = getBetterNumberFromContacts(number);
+                    number = mAdapter.getBetterNumberFromContacts(number);
                 }
                 intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
                                     Uri.fromParts("tel", number, null));
@@ -1162,7 +317,6 @@
         mAdapter.invalidateCache();
         startCallsQuery();
         startVoicemailStatusQuery();
-        mAdapter.mPreDrawListener = null; // Let it restart the thread after next draw
         updateOnEntry();
     }
 
diff --git a/src/com/android/contacts/calllog/CallLogGroupBuilder.java b/src/com/android/contacts/calllog/CallLogGroupBuilder.java
index f5aef3f..a7f8fa3 100644
--- a/src/com/android/contacts/calllog/CallLogGroupBuilder.java
+++ b/src/com/android/contacts/calllog/CallLogGroupBuilder.java
@@ -17,7 +17,6 @@
 package com.android.contacts.calllog;
 
 import com.android.common.widget.GroupingListAdapter;
-import com.android.contacts.calllog.CallLogFragment.CallLogQuery;
 
 import android.database.CharArrayBuffer;
 import android.database.Cursor;
@@ -30,15 +29,19 @@
  * This class is meant to be used in conjunction with {@link GroupingListAdapter}.
  */
 public class CallLogGroupBuilder {
+    public interface GroupCreator {
+        public void addGroup(int cursorPosition, int size, boolean expanded);
+    }
+
     /** Reusable char array buffer. */
     private CharArrayBuffer mBuffer1 = new CharArrayBuffer(128);
     /** Reusable char array buffer. */
     private CharArrayBuffer mBuffer2 = new CharArrayBuffer(128);
 
     /** The object on which the groups are created. */
-    private final CallLogFragment.GroupCreator mGroupCreator;
+    private final GroupCreator mGroupCreator;
 
-    public CallLogGroupBuilder(CallLogFragment.GroupCreator groupCreator) {
+    public CallLogGroupBuilder(GroupCreator groupCreator) {
         mGroupCreator = groupCreator;
     }
 
@@ -74,7 +77,7 @@
             final boolean sameNumber = equalPhoneNumbers(firstNumber, currentNumber);
             final boolean shouldGroup;
 
-            if (CallLogFragment.CallLogQuery.isSectionHeader(cursor)) {
+            if (CallLogQuery.isSectionHeader(cursor)) {
                 // Cannot group headers.
                 shouldGroup = false;
             } else if (!sameNumber) {
@@ -120,7 +123,7 @@
      * <p>
      * The group is always unexpanded.
      *
-     * @see CallLogFragment.CallLogAdapter#addGroup(int, int, boolean)
+     * @see CallLogAdapter#addGroup(int, int, boolean)
      */
     private void addGroup(int cursorPosition, int size) {
         mGroupCreator.addGroup(cursorPosition, size, false);
diff --git a/src/com/android/contacts/calllog/CallLogQuery.java b/src/com/android/contacts/calllog/CallLogQuery.java
new file mode 100644
index 0000000..f596032
--- /dev/null
+++ b/src/com/android/contacts/calllog/CallLogQuery.java
@@ -0,0 +1,91 @@
+/*
+ * 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 android.database.Cursor;
+import android.provider.CallLog.Calls;
+
+/**
+ * The query for the call log table.
+ */
+public final class CallLogQuery {
+    // If you alter this, you must also alter the method that inserts a fake row to the headers
+    // in the CallLogQueryHandler class called createHeaderCursorFor().
+    public static final String[] _PROJECTION = new String[] {
+            Calls._ID,
+            Calls.NUMBER,
+            Calls.DATE,
+            Calls.DURATION,
+            Calls.TYPE,
+            Calls.COUNTRY_ISO,
+            Calls.VOICEMAIL_URI,
+            Calls.GEOCODED_LOCATION,
+            Calls.CACHED_NAME,
+            Calls.CACHED_NUMBER_TYPE,
+            Calls.CACHED_NUMBER_LABEL,
+    };
+
+    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;
+    public static final int VOICEMAIL_URI = 6;
+    public static final int GEOCODED_LOCATION = 7;
+    public static final int CACHED_NAME = 8;
+    public static final int CACHED_NUMBER_TYPE = 9;
+    public static final int CACHED_NUMBER_LABEL = 10;
+
+    /**
+     * 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 = 11;
+    /** 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 call log projection including the section name. */
+    public static final String[] EXTENDED_PROJECTION;
+    static {
+        EXTENDED_PROJECTION = new String[_PROJECTION.length + 1];
+        System.arraycopy(_PROJECTION, 0, EXTENDED_PROJECTION, 0, _PROJECTION.length);
+        EXTENDED_PROJECTION[_PROJECTION.length] = SECTION_NAME;
+    }
+
+    public static boolean isSectionHeader(Cursor cursor) {
+        int section = cursor.getInt(CallLogQuery.SECTION);
+        return section == CallLogQuery.SECTION_NEW_HEADER
+                || section == CallLogQuery.SECTION_OLD_HEADER;
+    }
+
+    public static boolean isNewSection(Cursor cursor) {
+        int section = cursor.getInt(CallLogQuery.SECTION);
+        return section == CallLogQuery.SECTION_NEW_ITEM
+                || section == CallLogQuery.SECTION_NEW_HEADER;
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/calllog/CallLogQueryHandler.java b/src/com/android/contacts/calllog/CallLogQueryHandler.java
index 979db4b..25beba5 100644
--- a/src/com/android/contacts/calllog/CallLogQueryHandler.java
+++ b/src/com/android/contacts/calllog/CallLogQueryHandler.java
@@ -17,7 +17,6 @@
 package com.android.contacts.calllog;
 
 import com.android.common.io.MoreCloseables;
-import com.android.contacts.calllog.CallLogFragment.CallLogQuery;
 import com.android.contacts.voicemail.VoicemailStatusHelperImpl;
 
 import android.content.AsyncQueryHandler;
@@ -104,10 +103,10 @@
     /** 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(CallLogFragment.CallLogQuery.EXTENDED_PROJECTION);
+                new MatrixCursor(CallLogQuery.EXTENDED_PROJECTION);
         // The values in this row correspond to default values for _PROJECTION from CallLogQuery
         // plus the section value.
-        matrixCursor.addRow(new Object[]{ -1L, "", 0L, 0L, 0, "", "", "", section });
+        matrixCursor.addRow(new Object[]{ -1L, "", 0L, 0L, 0, "", "", "", null, 0, null, section });
         return matrixCursor;
     }
 
diff --git a/src/com/android/contacts/calllog/ContactInfo.java b/src/com/android/contacts/calllog/ContactInfo.java
new file mode 100644
index 0000000..1f106e4
--- /dev/null
+++ b/src/com/android/contacts/calllog/ContactInfo.java
@@ -0,0 +1,73 @@
+/*
+ * 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 android.net.Uri;
+import android.text.TextUtils;
+
+/**
+ * Information for a contact as needed by the Call Log.
+ */
+public final class ContactInfo {
+    public long personId = -1;
+    public String name;
+    public int type;
+    public String label;
+    public String number;
+    public String formattedNumber;
+    public String normalizedNumber;
+    public Uri thumbnailUri;
+    public String lookupKey;
+
+    public static ContactInfo EMPTY = new ContactInfo();
+
+    @Override
+    public int hashCode() {
+        // Uses only name and personId to determine hashcode.
+        // This should be sufficient to have a reasonable distribution of hash codes.
+        // Moreover, there should be no two people with the same personId.
+        final int prime = 31;
+        int result = 1;
+        result = prime * result + (int) (personId ^ (personId >>> 32));
+        result = prime * result + ((name == null) ? 0 : name.hashCode());
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+        if (obj == null) return false;
+        if (getClass() != obj.getClass()) return false;
+        ContactInfo other = (ContactInfo) obj;
+        if (personId != other.personId) return false;
+        if (!TextUtils.equals(name, other.name)) return false;
+        if (type != other.type) return false;
+        if (!TextUtils.equals(label, other.label)) return false;
+        if (!TextUtils.equals(number, other.number)) return false;
+        // Ignore formatted number.
+        if (!TextUtils.equals(normalizedNumber, other.normalizedNumber)) return false;
+        if (!uriEquals(thumbnailUri, other.thumbnailUri)) return false;
+        if (!TextUtils.equals(lookupKey, other.lookupKey)) return false;
+        return true;
+    }
+
+    private static boolean uriEquals(Uri thumbnailUri1, Uri thumbnailUri2) {
+        if (thumbnailUri1 == thumbnailUri2) return true;
+        if (thumbnailUri1 == null) return false;
+        return thumbnailUri1.equals(thumbnailUri2);
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/calllog/IntentProvider.java b/src/com/android/contacts/calllog/IntentProvider.java
index 9ce3d3d..bfee5ec 100644
--- a/src/com/android/contacts/calllog/IntentProvider.java
+++ b/src/com/android/contacts/calllog/IntentProvider.java
@@ -17,8 +17,6 @@
 package com.android.contacts.calllog;
 
 import com.android.contacts.CallDetailActivity;
-import com.android.contacts.calllog.CallLogFragment.CallLogAdapter;
-import com.android.contacts.calllog.CallLogFragment.CallLogQuery;
 
 import android.content.ContentUris;
 import android.content.Context;
diff --git a/src/com/android/contacts/calllog/PhoneQuery.java b/src/com/android/contacts/calllog/PhoneQuery.java
new file mode 100644
index 0000000..52faa8b
--- /dev/null
+++ b/src/com/android/contacts/calllog/PhoneQuery.java
@@ -0,0 +1,43 @@
+/*
+ * 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 android.provider.ContactsContract.PhoneLookup;
+
+/**
+ * The query to look up the {@link ContactInfo} for a given number in the Call Log.
+ */
+final class PhoneQuery {
+    public static final String[] _PROJECTION = new String[] {
+            PhoneLookup._ID,
+            PhoneLookup.DISPLAY_NAME,
+            PhoneLookup.TYPE,
+            PhoneLookup.LABEL,
+            PhoneLookup.NUMBER,
+            PhoneLookup.NORMALIZED_NUMBER,
+            PhoneLookup.PHOTO_THUMBNAIL_URI,
+            PhoneLookup.LOOKUP_KEY};
+
+    public static final int PERSON_ID = 0;
+    public static final int NAME = 1;
+    public static final int PHONE_TYPE = 2;
+    public static final int LABEL = 3;
+    public static final int MATCHED_NUMBER = 4;
+    public static final int NORMALIZED_NUMBER = 5;
+    public static final int THUMBNAIL_URI = 6;
+    public static final int LOOKUP_KEY = 7;
+}
\ No newline at end of file
diff --git a/src/com/android/contacts/dialpad/DialpadFragment.java b/src/com/android/contacts/dialpad/DialpadFragment.java
index a5db5ce..377a595 100644
--- a/src/com/android/contacts/dialpad/DialpadFragment.java
+++ b/src/com/android/contacts/dialpad/DialpadFragment.java
@@ -571,14 +571,10 @@
     }
 
     private void setupMenuItems(Menu menu) {
-        final MenuItem callSettingsMenuItem = menu.findItem(R.id.menu_call_settings_dialpad);
         final MenuItem addToContactMenuItem = menu.findItem(R.id.menu_add_contacts);
         final MenuItem twoSecPauseMenuItem = menu.findItem(R.id.menu_2s_pause);
         final MenuItem waitMenuItem = menu.findItem(R.id.menu_add_wait);
 
-        callSettingsMenuItem.setVisible(true);
-        callSettingsMenuItem.setIntent(DialtactsActivity.getCallSettingsIntent());
-
         // We show "add to contacts", "2sec pause", and "add wait" menus only when the user is
         // seeing usual dialpads and has typed at least one digit.
         // We never show a menu if the "choose dialpad" UI is up.
diff --git a/src/com/android/contacts/interactions/PhoneNumberInteraction.java b/src/com/android/contacts/interactions/PhoneNumberInteraction.java
index 0448bd5..918dac0 100644
--- a/src/com/android/contacts/interactions/PhoneNumberInteraction.java
+++ b/src/com/android/contacts/interactions/PhoneNumberInteraction.java
@@ -24,11 +24,11 @@
 import com.android.contacts.model.AccountType.StringInflater;
 import com.android.contacts.model.AccountTypeManager;
 import com.android.contacts.model.DataKind;
+import com.android.i18n.phonenumbers.NumberParseException;
+import com.android.i18n.phonenumbers.PhoneNumberUtil;
+import com.android.i18n.phonenumbers.PhoneNumberUtil.MatchType;
+import com.android.i18n.phonenumbers.Phonenumber.PhoneNumber;
 import com.google.common.annotations.VisibleForTesting;
-import com.google.i18n.phonenumbers.NumberParseException;
-import com.google.i18n.phonenumbers.PhoneNumberUtil;
-import com.google.i18n.phonenumbers.PhoneNumberUtil.MatchType;
-import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
 
 import android.app.Activity;
 import android.app.AlertDialog;
diff --git a/src/com/android/contacts/model/AccountTypeManager.java b/src/com/android/contacts/model/AccountTypeManager.java
index b517c2c..04b3fa8 100644
--- a/src/com/android/contacts/model/AccountTypeManager.java
+++ b/src/com/android/contacts/model/AccountTypeManager.java
@@ -16,12 +16,12 @@
 
 package com.android.contacts.model;
 
+import com.android.i18n.phonenumbers.PhoneNumberUtil;
 import com.android.internal.util.Objects;
 import com.google.android.collect.Lists;
 import com.google.android.collect.Maps;
 import com.google.android.collect.Sets;
 import com.google.common.annotations.VisibleForTesting;
-import com.google.i18n.phonenumbers.PhoneNumberUtil;
 
 import android.accounts.Account;
 import android.accounts.AccountManager;
diff --git a/tests/src/com/android/contacts/ContactLoaderTest.java b/tests/src/com/android/contacts/ContactLoaderTest.java
index a731f24..3560ce1 100644
--- a/tests/src/com/android/contacts/ContactLoaderTest.java
+++ b/tests/src/com/android/contacts/ContactLoaderTest.java
@@ -16,7 +16,12 @@
 
 package com.android.contacts;
 
+import com.android.contacts.model.AccountType;
+import com.android.contacts.model.AccountWithDataSet;
+import com.android.contacts.model.BaseAccountType;
+import com.android.contacts.test.InjectedServices;
 import com.android.contacts.tests.mocks.ContactsMockContext;
+import com.android.contacts.tests.mocks.MockAccountTypeManager;
 import com.android.contacts.tests.mocks.MockContentProvider;
 
 import android.content.ContentUris;
@@ -36,18 +41,31 @@
  */
 @LargeTest
 public class ContactLoaderTest extends LoaderTestCase {
-    ContactsMockContext mMockContext;
-    MockContentProvider mContactsProvider;
+    private ContactsMockContext mMockContext;
+    private MockContentProvider mContactsProvider;
 
     @Override
     protected void setUp() throws Exception {
         super.setUp();
         mMockContext = new ContactsMockContext(getContext());
         mContactsProvider = mMockContext.getContactsProvider();
+
+        InjectedServices services = new InjectedServices();
+        AccountType accountType = new BaseAccountType();
+        accountType.accountType = "mockAccountType";
+
+        AccountWithDataSet account =
+                new AccountWithDataSet("mockAccountName", "mockAccountType", null);
+
+        mMockContext.setMockAccountTypeManager(
+                new MockAccountTypeManager(
+                        new AccountType[] { accountType }, new AccountWithDataSet[] { account }));
     }
 
     @Override
     protected void tearDown() throws Exception {
+        mMockContext = null;
+        mContactsProvider = null;
         super.tearDown();
     }
 
diff --git a/tests/src/com/android/contacts/activities/CallLogActivityTests.java b/tests/src/com/android/contacts/activities/CallLogActivityTests.java
index 070941c..5926fd9 100644
--- a/tests/src/com/android/contacts/activities/CallLogActivityTests.java
+++ b/tests/src/com/android/contacts/activities/CallLogActivityTests.java
@@ -18,10 +18,12 @@
 
 import com.android.contacts.CallDetailActivity;
 import com.android.contacts.R;
+import com.android.contacts.calllog.CallLogAdapter;
 import com.android.contacts.calllog.CallLogFragment;
-import com.android.contacts.calllog.CallLogFragment.CallLogQuery;
-import com.android.contacts.calllog.CallLogFragment.ContactInfo;
 import com.android.contacts.calllog.CallLogListItemViews;
+import com.android.contacts.calllog.CallLogQuery;
+import com.android.contacts.calllog.CallLogQueryTestUtils;
+import com.android.contacts.calllog.ContactInfo;
 import com.android.contacts.calllog.IntentProvider;
 import com.android.internal.telephony.CallerInfo;
 
@@ -61,17 +63,6 @@
 @LargeTest
 public class CallLogActivityTests
         extends ActivityInstrumentationTestCase2<CallLogActivity> {
-    private static final String[] EXTENDED_CALL_LOG_PROJECTION = new String[] {
-            Calls._ID,
-            Calls.NUMBER,
-            Calls.DATE,
-            Calls.DURATION,
-            Calls.TYPE,
-            Calls.COUNTRY_ISO,
-            Calls.VOICEMAIL_URI,
-            Calls.GEOCODED_LOCATION,
-            CallLogFragment.CallLogQuery.SECTION_NAME,
-    };
     private static final int RAND_DURATION = -1;
     private static final long NOW = -1L;
 
@@ -95,7 +86,7 @@
     private CallLogActivity mActivity;
     private CallLogFragment mFragment;
     private FrameLayout mParentView;
-    private CallLogFragment.CallLogAdapter mAdapter;
+    private CallLogAdapter mAdapter;
     private String mVoicemail;
 
     // In memory array to hold the rows corresponding to the 'calls' table.
@@ -132,7 +123,7 @@
         mAdapter.disableRequestProcessingForTest();
         mAdapter.stopRequestProcessing();
         mParentView = new FrameLayout(mActivity);
-        mCursor = new MatrixCursor(EXTENDED_CALL_LOG_PROJECTION);
+        mCursor = new MatrixCursor(CallLogQuery.EXTENDED_PROJECTION);
         buildIconMap();
     }
 
@@ -487,27 +478,19 @@
      * @param type Either Call.OUTGOING_TYPE or Call.INCOMING_TYPE or Call.MISSED_TYPE.
      */
     private void insert(String number, long date, int duration, int type) {
-        MatrixCursor.RowBuilder row = mCursor.newRow();
-        row.add(mIndex);
-        mIndex ++;
-        row.add(number);
-        if (NOW == date) {
-            row.add(new Date().getTime());
-        } else {
-            row.add(date);
-        }
-        if (duration < 0) {
-            duration = mRnd.nextInt(10 * 60);  // 0 - 10 minutes random.
-        }
-        row.add(duration);  // duration
+        Object[] values = CallLogQueryTestUtils.createTestExtendedValues();
+        values[CallLogQuery.ID] = mIndex;
+        values[CallLogQuery.NUMBER] = number;
+        values[CallLogQuery.DATE] = date == NOW ? new Date().getTime() : date;
+        values[CallLogQuery.DURATION] = duration < 0 ? mRnd.nextInt(10 * 60) : duration;
         if (mVoicemail != null && mVoicemail.equals(number)) {
             assertEquals(Calls.OUTGOING_TYPE, type);
         }
-        row.add(type);  // type
-        row.add(TEST_COUNTRY_ISO);  // country ISO
-        row.add(null);  // voicemail_uri
-        row.add(null);  // geocoded_location
-        row.add(CallLogFragment.CallLogQuery.SECTION_OLD_ITEM);  // section
+        values[CallLogQuery.CALL_TYPE] = type;
+        values[CallLogQuery.COUNTRY_ISO] = TEST_COUNTRY_ISO;
+        values[CallLogQuery.SECTION] = CallLogQuery.SECTION_OLD_ITEM;
+        mCursor.addRow(values);
+        ++mIndex;
     }
 
     /**
@@ -518,27 +501,19 @@
      * @param duration In seconds of the call. Use RAND_DURATION to pick a random one.
      */
     private void insertVoicemail(String number, long date, int duration) {
-        MatrixCursor.RowBuilder row = mCursor.newRow();
+        Object[] values = CallLogQueryTestUtils.createTestExtendedValues();
+        values[CallLogQuery.ID] = mIndex;
+        values[CallLogQuery.NUMBER] = number;
+        values[CallLogQuery.DATE] = date == NOW ? new Date().getTime() : date;
+        values[CallLogQuery.DURATION] = duration < 0 ? mRnd.nextInt(10 * 60) : duration;
+        values[CallLogQuery.CALL_TYPE] = Calls.VOICEMAIL_TYPE;
+        values[CallLogQuery.COUNTRY_ISO] = TEST_COUNTRY_ISO;
         // Must have the same index as the row.
-        Uri voicemailUri =
+        values[CallLogQuery.VOICEMAIL_URI] =
                 ContentUris.withAppendedId(VoicemailContract.Voicemails.CONTENT_URI, mIndex);
-        row.add(mIndex);
-        mIndex ++;
-        row.add(number);
-        if (NOW == date) {
-            row.add(new Date().getTime());
-        } else {
-            row.add(date);
-        }
-        if (duration < 0) {
-            duration = mRnd.nextInt(10 * 60);  // 0 - 10 minutes random.
-        }
-        row.add(duration);  // duration
-        row.add(Calls.VOICEMAIL_TYPE);  // type
-        row.add(TEST_COUNTRY_ISO);  // country ISO
-        row.add(voicemailUri);  // voicemail_uri
-        row.add(null);  // geocoded_location
-        row.add(CallLogFragment.CallLogQuery.SECTION_OLD_ITEM);  // section
+        values[CallLogQuery.SECTION] = CallLogQuery.SECTION_OLD_ITEM;
+        mCursor.addRow(values);
+        ++mIndex;
     }
 
     /**
diff --git a/tests/src/com/android/contacts/calllog/CallLogGroupBuilderTest.java b/tests/src/com/android/contacts/calllog/CallLogGroupBuilderTest.java
index 8a7e946..31ad548 100644
--- a/tests/src/com/android/contacts/calllog/CallLogGroupBuilderTest.java
+++ b/tests/src/com/android/contacts/calllog/CallLogGroupBuilderTest.java
@@ -18,8 +18,6 @@
 
 import static com.google.android.collect.Lists.newArrayList;
 
-import com.android.contacts.calllog.CallLogFragment.CallLogQuery;
-
 import android.database.MatrixCursor;
 import android.provider.CallLog.Calls;
 import android.test.AndroidTestCase;
@@ -171,7 +169,7 @@
 
     /** Creates (or recreates) the cursor used to store the call log content for the tests. */
     private void createCursor() {
-        mCursor = new MatrixCursor(CallLogFragment.CallLogQuery.EXTENDED_PROJECTION);
+        mCursor = new MatrixCursor(CallLogQuery.EXTENDED_PROJECTION);
     }
 
     /** Clears the content of the {@link FakeGroupCreator} used in the tests. */
@@ -223,9 +221,12 @@
             throw new IllegalArgumentException("not an item section: " + section);
         }
         mCursor.moveToNext();
-        mCursor.addRow(new Object[]{
-                mCursor.getPosition(), number, 0L, 0L, type, "", "", "", section
-        });
+        Object[] values = CallLogQueryTestUtils.createTestExtendedValues();
+        values[CallLogQuery.ID] = mCursor.getPosition();
+        values[CallLogQuery.NUMBER] = number;
+        values[CallLogQuery.CALL_TYPE] = type;
+        values[CallLogQuery.SECTION] = section;
+        mCursor.addRow(values);
     }
 
     /** Adds the old section header to the call log. */
@@ -245,7 +246,10 @@
             throw new IllegalArgumentException("not a header section: " + section);
         }
         mCursor.moveToNext();
-        mCursor.addRow(new Object[]{ mCursor.getPosition(), "", 0L, 0L, 0, "", "", "", section });
+        Object[] values = CallLogQueryTestUtils.createTestExtendedValues();
+        values[CallLogQuery.ID] = mCursor.getPosition();
+        values[CallLogQuery.SECTION] = section;
+        mCursor.addRow(values);
     }
 
     /** Asserts that the group matches the given values. */
@@ -272,7 +276,7 @@
     }
 
     /** Fake implementation of a GroupCreator which stores the created groups in a member field. */
-    private static class FakeGroupCreator implements CallLogFragment.GroupCreator {
+    private static class FakeGroupCreator implements CallLogGroupBuilder.GroupCreator {
         /** The list of created groups. */
         public final List<GroupSpec> groups = newArrayList();
 
diff --git a/tests/src/com/android/contacts/calllog/CallLogQueryTestUtils.java b/tests/src/com/android/contacts/calllog/CallLogQueryTestUtils.java
new file mode 100644
index 0000000..0e1952a
--- /dev/null
+++ b/tests/src/com/android/contacts/calllog/CallLogQueryTestUtils.java
@@ -0,0 +1,37 @@
+/*
+ * 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 static junit.framework.Assert.assertEquals;
+import junit.framework.Assert;
+
+/**
+ * Helper class to create test values for {@link CallLogQuery}.
+ */
+public class CallLogQueryTestUtils {
+    public static Object[] createTestValues() {
+        Object[] values = new Object[]{ -1L, "", 0L, 0L, 0, "", "", "", null, 0, null };
+        assertEquals(CallLogQuery._PROJECTION.length, values.length);
+        return values;
+    }
+
+    public static Object[] createTestExtendedValues() {
+        Object[] values = new Object[]{ -1L, "", 0L, 0L, 0, "", "", "", null, 0, null, 0 };
+        Assert.assertEquals(CallLogQuery.EXTENDED_PROJECTION.length, values.length);
+        return values;
+    }
+}
diff --git a/tests/src/com/android/contacts/detail/StreamItemAdapterTest.java b/tests/src/com/android/contacts/detail/StreamItemAdapterTest.java
index 4687f77..d862d6e 100644
--- a/tests/src/com/android/contacts/detail/StreamItemAdapterTest.java
+++ b/tests/src/com/android/contacts/detail/StreamItemAdapterTest.java
@@ -53,13 +53,13 @@
     public void testGetCount_Empty() {
         mAdapter.setStreamItems(createStreamItemList(0));
         // There is actually one view: the header.
-        assertEquals(1, mAdapter.getCount());
+        assertEquals(2, mAdapter.getCount());
     }
 
     public void testGetCount_NonEmpty() {
         mAdapter.setStreamItems(createStreamItemList(3));
         // There is one extra view: the header.
-        assertEquals(4, mAdapter.getCount());
+        assertEquals(5, mAdapter.getCount());
     }
 
     public void testGetView_Header() {
diff --git a/tests/src/com/android/contacts/tests/mocks/ContactsMockContext.java b/tests/src/com/android/contacts/tests/mocks/ContactsMockContext.java
index 93ea4f4..2f959f4 100644
--- a/tests/src/com/android/contacts/tests/mocks/ContactsMockContext.java
+++ b/tests/src/com/android/contacts/tests/mocks/ContactsMockContext.java
@@ -16,6 +16,8 @@
 
 package com.android.contacts.tests.mocks;
 
+import com.android.contacts.model.AccountTypeManager;
+
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.ContextWrapper;
@@ -32,13 +34,11 @@
  * to mock content providers.
  */
 public class ContactsMockContext extends ContextWrapper {
-
-    private static final String TAG = "ContactsMockContext";
-
     private ContactsMockPackageManager mPackageManager;
     private MockContentResolver mContentResolver;
     private MockContentProvider mContactsProvider;
     private MockContentProvider mSettingsProvider;
+    private MockAccountTypeManager mMockAccountTypeManager;
     private Intent mIntentForStartActivity;
 
     public ContactsMockContext(Context base) {
@@ -53,6 +53,10 @@
         mContentResolver.addProvider(Settings.AUTHORITY, mSettingsProvider);
     }
 
+    public void setMockAccountTypeManager(MockAccountTypeManager mockAccountTypeManager) {
+        mMockAccountTypeManager = mockAccountTypeManager;
+    }
+
     @Override
     public ContentResolver getContentResolver() {
         return mContentResolver;
@@ -93,4 +97,12 @@
         mContactsProvider.verify();
         mSettingsProvider.verify();
     }
+
+    @Override
+    public Object getSystemService(String name) {
+        if (AccountTypeManager.ACCOUNT_TYPE_SERVICE.equals(name)) {
+           return mMockAccountTypeManager;
+        }
+        return super.getSystemService(name);
+    }
 }