Create ContactInfoCache from CallLogAdapter.

This pulls code from the CallLogAdapter, with only tweaks to variable
names and comments, to create a ContactInfoCache responsible for logic
pertaining to looking up and caching contact info.

The logic is intended to be unchanged for now, although in the future
it can/should probably be cleaned up sometime.

Bug: 20038300
Change-Id: I60a57b0a665496522a6b51c9e6e41a4fd6dbad1f
diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java
index 8ea861f..f5a3f62 100644
--- a/src/com/android/dialer/calllog/CallLogAdapter.java
+++ b/src/com/android/dialer/calllog/CallLogAdapter.java
@@ -21,8 +21,6 @@
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.net.Uri;
-import android.os.Handler;
-import android.os.Message;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.PhoneLookup;
 import android.telecom.PhoneAccountHandle;
@@ -43,15 +41,13 @@
 import com.android.dialer.PhoneCallDetails;
 import com.android.dialer.PhoneCallDetailsHelper;
 import com.android.dialer.R;
-import com.android.dialer.contactinfo.ContactInfoRequest;
-import com.android.dialer.contactinfo.NumberWithCountryIso;
+import com.android.dialer.contactinfo.ContactInfoCache;
+import com.android.dialer.contactinfo.ContactInfoCache.OnContactInfoChangedListener;
 import com.android.dialer.util.DialerUtils;
-import com.android.dialer.util.ExpirableCache;
 
 import com.google.common.annotations.VisibleForTesting;
 
 import java.util.HashMap;
-import java.util.LinkedList;
 
 /**
  * Adapter class to fill in data for the Call Log.
@@ -60,11 +56,6 @@
         implements ViewTreeObserver.OnPreDrawListener, CallLogGroupBuilder.GroupCreator {
     private static final String TAG = CallLogAdapter.class.getSimpleName();
 
-    /** The enumeration of {@link android.os.AsyncTask} objects used in this class. */
-    public enum Tasks {
-        REMOVE_CALL_LOG_ENTRIES,
-    }
-
     /** Interface used to inform a parent UI element that a list item has been expanded. */
     public interface CallItemExpandedListener {
         /**
@@ -93,12 +84,6 @@
         public void onReportButtonClick(String number);
     }
 
-    /** 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;
-
     /** Constant used to indicate no row is expanded. */
     private static final long NONE_EXPANDED = -1;
 
@@ -108,15 +93,7 @@
     private final OnReportButtonClickListener mOnReportButtonClickListener;
     private ViewTreeObserver mViewTreeObserver = null;
 
-    /**
-     * 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.
-     * <p>
-     * The key is number with the country in which the call was placed or received.
-     */
-    private ExpirableCache<NumberWithCountryIso, ContactInfo> mContactInfoCache;
+    protected ContactInfoCache mContactInfoCache;
 
     /**
      * Tracks the call log row which was previously expanded.  Used so that the closure of a
@@ -143,22 +120,7 @@
      */
     private HashMap<Long,Integer> mDayGroups = new HashMap<Long, Integer>();
 
-    /**
-     * List of requests to update contact details.
-     * <p>
-     * Each request is made of a phone number to look up, and the contact info currently stored in
-     * the call log for this number.
-     * <p>
-     * The requests are added when displaying the contacts and are processed by a background
-     * thread.
-     */
-    private final LinkedList<ContactInfoRequest> mRequests;
-
     private boolean mLoading = true;
-    private static final int REDRAW = 1;
-    private static final int START_THREAD = 2;
-
-    private QueryThread mCallerIdThread;
 
     /** Instance of helper class for managing views. */
     private final CallLogListItemHelper mCallLogViewsHelper;
@@ -172,9 +134,6 @@
 
     private CallItemExpandedListener mCallItemExpandedListener;
 
-    /** Can be set to true by tests to disable processing of requests. */
-    private volatile boolean mRequestProcessingDisabled = false;
-
     /** Listener for the primary or secondary actions in the list.
      *  Primary opens the call details.
      *  Secondary calls or plays.
@@ -205,6 +164,14 @@
         }
     };
 
+    protected final OnContactInfoChangedListener mOnContactInfoChangedListener =
+            new OnContactInfoChangedListener() {
+                @Override
+                public void onContactInfoChanged() {
+                    notifyDataSetChanged();
+                }
+            };
+
     private AccessibilityDelegate mAccessibilityDelegate = new AccessibilityDelegate() {
         @Override
         public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child,
@@ -222,29 +189,10 @@
         // We only wanted to listen for the first draw (and this is it).
         unregisterPreDrawListener();
 
-        // Only schedule a thread-creation message if the thread hasn't been
-        // created yet. This is purely an optimization, to queue fewer messages.
-        if (mCallerIdThread == null) {
-            mHandler.sendEmptyMessageDelayed(START_THREAD, START_PROCESSING_REQUESTS_DELAY_MILLIS);
-        }
-
+        mContactInfoCache.start();
         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,
             ContactInfoHelper contactInfoHelper, CallItemExpandedListener callItemExpandedListener,
             OnReportButtonClickListener onReportButtonClickListener) {
@@ -257,8 +205,8 @@
 
         mOnReportButtonClickListener = onReportButtonClickListener;
 
-        mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE);
-        mRequests = new LinkedList<ContactInfoRequest>();
+        mContactInfoCache = new ContactInfoCache(
+                mContactInfoHelper, mOnContactInfoChangedListener);
 
         Resources resources = mContext.getResources();
         CallTypeHelper callTypeHelper = new CallTypeHelper(resources);
@@ -295,37 +243,6 @@
     }
 
     /**
-     * Starts a background thread to process contact-lookup requests, unless one
-     * has already been started.
-     */
-    private synchronized void startRequestProcessing() {
-        // For unit-testing.
-        if (mRequestProcessingDisabled) return;
-
-        // Idempotence... if a thread is already started, don't start another.
-        if (mCallerIdThread != null) return;
-
-        mCallerIdThread = new QueryThread();
-        mCallerIdThread.setPriority(Thread.MIN_PRIORITY);
-        mCallerIdThread.start();
-    }
-
-    /**
-     * Stops the background thread that processes updates and cancels any
-     * pending requests to start it.
-     */
-    public synchronized void stopRequestProcessing() {
-        // Remove any pending requests to start the processing thread.
-        mHandler.removeMessages(START_THREAD);
-        if (mCallerIdThread != null) {
-            // Stop the thread; we are finished with it.
-            mCallerIdThread.stopProcessing();
-            mCallerIdThread.interrupt();
-            mCallerIdThread = null;
-        }
-    }
-
-    /**
      * Stop receiving onPreDraw() notifications.
      */
     private void unregisterPreDrawListener() {
@@ -336,134 +253,14 @@
     }
 
     public void invalidateCache() {
-        mContactInfoCache.expireAll();
+        mContactInfoCache.invalidate();
 
         // Restart the request-processing thread after the next draw.
-        stopRequestProcessing();
         unregisterPreDrawListener();
     }
 
-    /**
-     * Enqueues a request to look up the contact details for the given phone number.
-     * <p>
-     * It also provides the current contact info stored in the call log for this number.
-     * <p>
-     * If the {@code immediate} parameter is true, it will start immediately the thread that looks
-     * up the contact information (if it has not been already started). Otherwise, it will be
-     * started with a delay. See {@link #START_PROCESSING_REQUESTS_DELAY_MILLIS}.
-     */
-    protected void enqueueRequest(String number, String countryIso, ContactInfo callLogInfo,
-            boolean immediate) {
-        ContactInfoRequest request = new ContactInfoRequest(number, countryIso, callLogInfo);
-        synchronized (mRequests) {
-            if (!mRequests.contains(request)) {
-                mRequests.add(request);
-                mRequests.notifyAll();
-            }
-        }
-        if (immediate) startRequestProcessing();
-    }
-
-    /**
-     * Queries the appropriate content provider for the contact associated with the number.
-     * <p>
-     * Upon completion it also updates the cache in the call log, if it is different from
-     * {@code callLogInfo}.
-     * <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, String countryIso, ContactInfo callLogInfo) {
-        final ContactInfo info = mContactInfoHelper.lookupNumber(number, countryIso);
-
-        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.
-        NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
-        ContactInfo existingInfo = mContactInfoCache.getPossiblyExpired(numberCountryIso);
-
-        final boolean isRemoteSource = info.sourceType != 0;
-
-        // Don't force redraw if existing info in the cache is equal to {@link ContactInfo#EMPTY}
-        // to avoid updating the data set for every new row that is scrolled into view.
-        // see (https://googleplex-android-review.git.corp.google.com/#/c/166680/)
-
-        // Exception: Photo uris for contacts from remote sources are not cached in the call log
-        // cache, so we have to force a redraw for these contacts regardless.
-        boolean updated = (existingInfo != ContactInfo.EMPTY || isRemoteSource) &&
-                !info.equals(existingInfo);
-
-        // 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(numberCountryIso, info);
-
-        // Update the call log even if the cache it is up-to-date: it is possible that the cache
-        // contains the value from a different call log entry.
-        mContactInfoHelper.updateCallLogContactInfo(number, countryIso, info, callLogInfo);
-        return updated;
-    }
-
-    /*
-     * Handles requests for contact name and number type.
-     */
-    private class QueryThread extends Thread {
-        private volatile boolean mDone = false;
-
-        public QueryThread() {
-            super("CallLogAdapter.QueryThread");
-        }
-
-        public void stopProcessing() {
-            mDone = true;
-        }
-
-        @Override
-        public void run() {
-            boolean needRedraw = false;
-            while (true) {
-                // Check if thread is finished, and if so return immediately.
-                if (mDone) return;
-
-                // Obtain next request, if any is available.
-                // Keep synchronized section small.
-                ContactInfoRequest req = null;
-                synchronized (mRequests) {
-                    if (!mRequests.isEmpty()) {
-                        req = mRequests.removeFirst();
-                    }
-                }
-
-                if (req != null) {
-                    // Process the request. If the lookup succeeds, schedule a
-                    // redraw.
-                    needRedraw |= queryContactInfo(req.number, req.countryIso, req.callLogInfo);
-                } else {
-                    // Throttle redraw rate by only sending them when there are
-                    // more requests.
-                    if (needRedraw) {
-                        needRedraw = false;
-                        mHandler.sendEmptyMessage(REDRAW);
-                    }
-
-                    // Wait until another request is available, or until this
-                    // thread is no longer needed (as indicated by being
-                    // interrupted).
-                    try {
-                        synchronized (mRequests) {
-                            mRequests.wait(1000);
-                        }
-                    } catch (InterruptedException ie) {
-                        // Ignore, and attempt to continue processing requests.
-                    }
-                }
-            }
-        }
+    public void pauseCache() {
+        mContactInfoCache.stop();
     }
 
     @Override
@@ -576,41 +373,11 @@
         // Note: Binding of the action buttons is done as required in configureActionViews when the
         // user expands the actions ViewStub.
 
-        // Lookup contacts with this number
-        NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
-        ExpirableCache.CachedValue<ContactInfo> cachedInfo =
-                mContactInfoCache.getCachedValue(numberCountryIso);
-        ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue();
-        if (!PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation)
-                || isVoicemailNumber) {
-            // If this is a number that cannot be dialed, there is no point in looking up a contact
-            // for it.
-            info = ContactInfo.EMPTY;
-        } else if (cachedInfo == null) {
-            mContactInfoCache.put(numberCountryIso, ContactInfo.EMPTY);
-            // Use the cached contact info from the call log.
-            info = cachedContactInfo;
-            // The db request should happen on a non-UI thread.
-            // Request the contact details immediately since they are currently missing.
-            enqueueRequest(number, countryIso, cachedContactInfo, true);
-            // We will format the phone number when we make the background request.
-        } 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, countryIso, cachedContactInfo, false);
-            } else  if (!callLogInfoMatches(cachedContactInfo, info)) {
-                // The call log information does not match the one we have, look it up again.
-                // We could simply update the call log directly, but that needs to be done in a
-                // background thread, so it is easier to simply request a new lookup, which will, as
-                // a side-effect, update the call log.
-                enqueueRequest(number, countryIso, cachedContactInfo, false);
-            }
-
-            if (info == ContactInfo.EMPTY) {
-                // Use the cached contact info from the call log.
-                info = cachedContactInfo;
-            }
+        ContactInfo info = ContactInfo.EMPTY;
+        if (PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation)
+                && !isVoicemailNumber) {
+            // Lookup contacts with this number
+            info = mContactInfoCache.getValue(number, countryIso, cachedContactInfo);
         }
 
         final Uri lookupUri = info.lookupUri;
@@ -746,15 +513,6 @@
         }
     }
 
-    /** Checks whether the contact info from the call log matches the one from the contacts db. */
-    private boolean callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info) {
-        // The call log only contains a subset of the fields in the contacts db.
-        // Only check those.
-        return TextUtils.equals(callLogInfo.name, info.name)
-                && callLogInfo.type == info.type
-                && TextUtils.equals(callLogInfo.label, info.label);
-    }
-
     /**
      * Returns the call types for the given number of items in the cursor.
      * <p>
@@ -810,19 +568,20 @@
 
     /**
      * 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.
      */
     @VisibleForTesting
     void disableRequestProcessingForTest() {
-        mRequestProcessingDisabled = true;
+        // TODO: Remove this and test the cache directly.
+        mContactInfoCache.disableRequestProcessingForTest();
     }
 
     @VisibleForTesting
     void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) {
-        NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
-        mContactInfoCache.put(numberCountryIso, contactInfo);
+        // TODO: Remove this and test the cache directly.
+        mContactInfoCache.injectContactInfoForTest(number, countryIso, contactInfo);
     }
 
     @Override
diff --git a/src/com/android/dialer/calllog/CallLogFragment.java b/src/com/android/dialer/calllog/CallLogFragment.java
index 7b5907c..7756576 100644
--- a/src/com/android/dialer/calllog/CallLogFragment.java
+++ b/src/com/android/dialer/calllog/CallLogFragment.java
@@ -378,8 +378,7 @@
     @Override
     public void onPause() {
         super.onPause();
-        // Kill the requests thread
-        mAdapter.stopRequestProcessing();
+        mAdapter.pauseCache();
     }
 
     @Override
@@ -392,7 +391,7 @@
     @Override
     public void onDestroy() {
         super.onDestroy();
-        mAdapter.stopRequestProcessing();
+        mAdapter.pauseCache();
         mAdapter.changeCursor(null);
         getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver);
         getActivity().getContentResolver().unregisterContentObserver(mContactsObserver);
diff --git a/src/com/android/dialer/contactinfo/ContactInfoCache.java b/src/com/android/dialer/contactinfo/ContactInfoCache.java
new file mode 100644
index 0000000..2bb0f1e
--- /dev/null
+++ b/src/com/android/dialer/contactinfo/ContactInfoCache.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2015 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.dialer.contactinfo;
+
+import android.os.Handler;
+import android.os.Message;
+import android.text.TextUtils;
+
+import com.android.dialer.calllog.ContactInfo;
+import com.android.dialer.calllog.ContactInfoHelper;
+import com.android.dialer.util.ExpirableCache;
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.LinkedList;
+
+/**
+ * This is a cache of contact details for the phone numbers in the c all log. The key is the
+ * phone number with the country in which teh call was placed or received. The content of the
+ * cache is expired (but not purged) whenever the application comes to the foreground.
+ *
+ * This cache queues request for information and queries for information on a background thread,
+ * so {@code start()} and {@code stop()} must be called to initiate or halt that thread's exeuction
+ * as needed.
+ *
+ * TODO: Explore whether there is a pattern to remove external dependencies for starting and
+ * stopping the query thread.
+ */
+public class ContactInfoCache {
+    public interface OnContactInfoChangedListener {
+        public void onContactInfoChanged();
+    }
+
+    /*
+     * Handles requests for contact name and number type.
+     */
+    private class QueryThread extends Thread {
+        private volatile boolean mDone = false;
+
+        public QueryThread() {
+            super("CallLogAdapter.QueryThread");
+        }
+
+        public void stopProcessing() {
+            mDone = true;
+        }
+
+        @Override
+        public void run() {
+            boolean needRedraw = false;
+            while (true) {
+                // Check if thread is finished, and if so return immediately.
+                if (mDone) return;
+
+                // Obtain next request, if any is available.
+                // Keep synchronized section small.
+                ContactInfoRequest req = null;
+                synchronized (mRequests) {
+                    if (!mRequests.isEmpty()) {
+                        req = mRequests.removeFirst();
+                    }
+                }
+
+                if (req != null) {
+                    // Process the request. If the lookup succeeds, schedule a redraw.
+                    needRedraw |= queryContactInfo(req.number, req.countryIso, req.callLogInfo);
+                } else {
+                    // Throttle redraw rate by only sending them when there are
+                    // more requests.
+                    if (needRedraw) {
+                        needRedraw = false;
+                        mHandler.sendEmptyMessage(REDRAW);
+                    }
+
+                    // Wait until another request is available, or until this
+                    // thread is no longer needed (as indicated by being
+                    // interrupted).
+                    try {
+                        synchronized (mRequests) {
+                            mRequests.wait(1000);
+                        }
+                    } catch (InterruptedException ie) {
+                        // Ignore, and attempt to continue processing requests.
+                    }
+                }
+            }
+        }
+    }
+
+    private Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case REDRAW:
+                    mOnContactInfoChangedListener.onContactInfoChanged();
+                    break;
+                case START_THREAD:
+                    startRequestProcessing();
+                    break;
+            }
+        }
+    };
+
+    private static final int REDRAW = 1;
+    private static final int START_THREAD = 2;
+
+    private static final int CONTACT_INFO_CACHE_SIZE = 100;
+    private static final int START_PROCESSING_REQUESTS_DELAY_MS = 1000;
+
+
+    /**
+     * List of requests to update contact details. Each request contains a phone number to look up,
+     * and the contact info currently stored in the call log for this number.
+     *
+     * The requests are added when displaying contacts and are processed by a background thread.
+     */
+    private final LinkedList<ContactInfoRequest> mRequests;
+
+    private ExpirableCache<NumberWithCountryIso, ContactInfo> mCache;
+
+    private ContactInfoHelper mContactInfoHelper;
+    private QueryThread mContactInfoQueryThread;
+    private OnContactInfoChangedListener mOnContactInfoChangedListener;
+
+    public ContactInfoCache(ContactInfoHelper contactInfoHelper,
+            OnContactInfoChangedListener onContactInfoChangedListener) {
+        mContactInfoHelper = contactInfoHelper;
+        mOnContactInfoChangedListener = onContactInfoChangedListener;
+
+        mRequests = new LinkedList<ContactInfoRequest>();
+        mCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE);
+    }
+
+    public ContactInfo getValue(String number, String countryIso, ContactInfo cachedContactInfo) {
+        NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
+        ExpirableCache.CachedValue<ContactInfo> cachedInfo =
+                mCache.getCachedValue(numberCountryIso);
+        ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue();
+        if (cachedInfo == null) {
+            mCache.put(numberCountryIso, ContactInfo.EMPTY);
+            // Use the cached contact info from the call log.
+            info = cachedContactInfo;
+            // The db request should happen on a non-UI thread.
+            // Request the contact details immediately since they are currently missing.
+            enqueueRequest(number, countryIso, cachedContactInfo, true);
+            // We will format the phone number when we make the background request.
+        } 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, countryIso, cachedContactInfo, false);
+            } else  if (!callLogInfoMatches(cachedContactInfo, info)) {
+                // The call log information does not match the one we have, look it up again.
+                // We could simply update the call log directly, but that needs to be done in a
+                // background thread, so it is easier to simply request a new lookup, which will, as
+                // a side-effect, update the call log.
+                enqueueRequest(number, countryIso, cachedContactInfo, false);
+            }
+
+            if (info == ContactInfo.EMPTY) {
+                // Use the cached contact info from the call log.
+                info = cachedContactInfo;
+            }
+        }
+        return info;
+    }
+
+    /**
+     * Queries the appropriate content provider for the contact associated with the number.
+     *
+     * Upon completion it also updates the cache in the call log, if it is different from
+     * {@code callLogInfo}.
+     *
+     * The number might be either a SIP address or a phone number.
+     *
+     * 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, String countryIso, ContactInfo callLogInfo) {
+        final ContactInfo info = mContactInfoHelper.lookupNumber(number, countryIso);
+
+        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.
+        NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
+        ContactInfo existingInfo = mCache.getPossiblyExpired(numberCountryIso);
+
+        final boolean isRemoteSource = info.sourceType != 0;
+
+        // Don't force redraw if existing info in the cache is equal to {@link ContactInfo#EMPTY}
+        // to avoid updating the data set for every new row that is scrolled into view.
+        // see (https://googleplex-android-review.git.corp.google.com/#/c/166680/)
+
+        // Exception: Photo uris for contacts from remote sources are not cached in the call log
+        // cache, so we have to force a redraw for these contacts regardless.
+        boolean updated = (existingInfo != ContactInfo.EMPTY || isRemoteSource) &&
+                !info.equals(existingInfo);
+
+        // 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.
+        mCache.put(numberCountryIso, info);
+
+        // Update the call log even if the cache it is up-to-date: it is possible that the cache
+        // contains the value from a different call log entry.
+        mContactInfoHelper.updateCallLogContactInfo(number, countryIso, info, callLogInfo);
+        return updated;
+    }
+
+    /**
+     * After a delay, start the thread to begin processing requests. We perform lookups on a
+     * background thread, but this must be called to indicate the thread should be running.
+     */
+    public void start() {
+        // Schedule a thread-creation message if the thread hasn't been created yet, as an
+        // optimization to queue fewer messages.
+        if (mContactInfoQueryThread == null) {
+            // TODO: Check whether this delay before starting to process is necessary.
+            mHandler.sendEmptyMessageDelayed(START_THREAD, START_PROCESSING_REQUESTS_DELAY_MS);
+        }
+    }
+
+    /**
+     * Stops the thread and clears the queue of messages to process. This cleans up the thread
+     * for lookups so that it is not perpetually running.
+     */
+    public void stop() {
+        stopRequestProcessing();
+    }
+
+    /**
+     * Starts a background thread to process contact-lookup requests, unless one
+     * has already been started.
+     */
+    private synchronized void startRequestProcessing() {
+        // For unit-testing.
+        if (mRequestProcessingDisabled) return;
+
+        // If a thread is already started, don't start another.
+        if (mContactInfoQueryThread != null) {
+            return;
+        }
+
+        mContactInfoQueryThread = new QueryThread();
+        mContactInfoQueryThread.setPriority(Thread.MIN_PRIORITY);
+        mContactInfoQueryThread.start();
+    }
+
+    public void invalidate() {
+        mCache.expireAll();
+        stopRequestProcessing();
+    }
+
+    /**
+     * Stops the background thread that processes updates and cancels any
+     * pending requests to start it.
+     */
+    private synchronized void stopRequestProcessing() {
+        // Remove any pending requests to start the processing thread.
+        mHandler.removeMessages(START_THREAD);
+        if (mContactInfoQueryThread != null) {
+            // Stop the thread; we are finished with it.
+            mContactInfoQueryThread.stopProcessing();
+            mContactInfoQueryThread.interrupt();
+            mContactInfoQueryThread = null;
+        }
+    }
+
+    /**
+     * Enqueues a request to look up the contact details for the given phone number.
+     * <p>
+     * It also provides the current contact info stored in the call log for this number.
+     * <p>
+     * If the {@code immediate} parameter is true, it will start immediately the thread that looks
+     * up the contact information (if it has not been already started). Otherwise, it will be
+     * started with a delay. See {@link #START_PROCESSING_REQUESTS_DELAY_MILLIS}.
+     */
+    protected void enqueueRequest(String number, String countryIso, ContactInfo callLogInfo,
+            boolean immediate) {
+        ContactInfoRequest request = new ContactInfoRequest(number, countryIso, callLogInfo);
+        synchronized (mRequests) {
+            if (!mRequests.contains(request)) {
+                mRequests.add(request);
+                mRequests.notifyAll();
+            }
+        }
+        if (immediate) {
+            startRequestProcessing();
+        }
+    }
+
+    /**
+     * Checks whether the contact info from the call log matches the one from the contacts db.
+     */
+    private boolean callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info) {
+        // The call log only contains a subset of the fields in the contacts db.
+        // Only check those.
+        return TextUtils.equals(callLogInfo.name, info.name)
+                && callLogInfo.type == info.type
+                && TextUtils.equals(callLogInfo.label, info.label);
+    }
+
+    /**
+     * Can be set to true by tests to disable processing of requests.
+     */
+    @VisibleForTesting
+    private volatile boolean mRequestProcessingDisabled = false;
+
+    /**
+     * Sets whether processing of requests for contact details should be enabled.
+     *
+     * This method should be called in tests to disable such processing of requests when not
+     * needed.
+     */
+    @VisibleForTesting
+    public void disableRequestProcessingForTest() {
+        mRequestProcessingDisabled = true;
+    }
+
+    @VisibleForTesting
+    public void injectContactInfoForTest(
+            String number, String countryIso, ContactInfo contactInfo) {
+        NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
+        mCache.put(numberCountryIso, contactInfo);
+    }
+}
diff --git a/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java b/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
index 0f17511..dbdde68 100644
--- a/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
+++ b/tests/src/com/android/dialer/calllog/CallLogAdapterTest.java
@@ -23,6 +23,8 @@
 import android.view.View;
 import android.widget.LinearLayout;
 
+import com.android.dialer.contactinfo.ContactInfoCache;
+import com.android.dialer.contactinfo.ContactInfoCache.OnContactInfoChangedListener;
 import com.google.common.collect.Lists;
 
 import java.util.List;
@@ -88,9 +90,9 @@
         mAdapter.bindStandAloneView(mView, getContext(), mCursor);
 
         // There is one request for contact details.
-        assertEquals(1, mAdapter.requests.size());
+        assertEquals(1, mAdapter.getContactInfoCache().requests.size());
 
-        TestCallLogAdapter.Request request = mAdapter.requests.get(0);
+        TestContactInfoCache.Request request = mAdapter.getContactInfoCache().requests.get(0);
         // It is for the number we need to show.
         assertEquals(TEST_NUMBER, request.number);
         // It has the right country.
@@ -106,9 +108,9 @@
         mAdapter.bindStandAloneView(mView, getContext(), mCursor);
 
         // There is one request for contact details.
-        assertEquals(1, mAdapter.requests.size());
+        assertEquals(1, mAdapter.getContactInfoCache().requests.size());
 
-        TestCallLogAdapter.Request request = mAdapter.requests.get(0);
+        TestContactInfoCache.Request request = mAdapter.getContactInfoCache().requests.get(0);
         // The values passed to the request, match the ones in the call log cache.
         assertEquals(TEST_NAME, request.callLogInfo.name);
         assertEquals(1, request.callLogInfo.type);
@@ -124,9 +126,9 @@
         mAdapter.bindStandAloneView(mView, getContext(), mCursor);
 
         // There is one request for contact details.
-        assertEquals(1, mAdapter.requests.size());
+        assertEquals(1, mAdapter.getContactInfoCache().requests.size());
 
-        TestCallLogAdapter.Request request = mAdapter.requests.get(0);
+        TestContactInfoCache.Request request = mAdapter.getContactInfoCache().requests.get(0);
         // Since there is something in the cache, it is not an immediate request.
         assertFalse("should not be immediate", request.immediate);
     }
@@ -139,7 +141,7 @@
         mAdapter.bindStandAloneView(mView, getContext(), mCursor);
 
         // Cache and call log are up-to-date: no need to request update.
-        assertEquals(0, mAdapter.requests.size());
+        assertEquals(0, mAdapter.getContactInfoCache().requests.size());
     }
 
     public void testBindView_MismatchBetwenCallLogAndMemoryCache_EnqueueRequest() {
@@ -154,9 +156,9 @@
         mAdapter.bindStandAloneView(mView, getContext(), mCursor);
 
         // There is one request for contact details.
-        assertEquals(1, mAdapter.requests.size());
+        assertEquals(1, mAdapter.getContactInfoCache().requests.size());
 
-        TestCallLogAdapter.Request request = mAdapter.requests.get(0);
+        TestContactInfoCache.Request request = mAdapter.getContactInfoCache().requests.get(0);
         // Since there is something in the cache, it is not an immediate request.
         assertFalse("should not be immediate", request.immediate);
     }
@@ -191,9 +193,20 @@
     /**
      * Subclass of {@link CallLogAdapter} used in tests to intercept certain calls.
      */
-    // TODO: This would be better done by splitting the contact lookup into a collaborator class
-    // instead.
     private static final class TestCallLogAdapter extends CallLogAdapter {
+        public TestCallLogAdapter(Context context, CallFetcher callFetcher,
+                ContactInfoHelper contactInfoHelper) {
+            super(context, callFetcher, contactInfoHelper, null, null);
+            mContactInfoCache = new TestContactInfoCache(
+                    contactInfoHelper, mOnContactInfoChangedListener);
+        }
+
+        public TestContactInfoCache getContactInfoCache() {
+            return (TestContactInfoCache) mContactInfoCache;
+        }
+    }
+
+    private static final class TestContactInfoCache extends ContactInfoCache {
         public static class Request {
             public final String number;
             public final String countryIso;
@@ -211,9 +224,9 @@
 
         public final List<Request> requests = Lists.newArrayList();
 
-        public TestCallLogAdapter(Context context, CallFetcher callFetcher,
-                ContactInfoHelper contactInfoHelper) {
-            super(context, callFetcher, contactInfoHelper, null, null);
+        public TestContactInfoCache(
+                ContactInfoHelper contactInfoHelper, OnContactInfoChangedListener listener) {
+            super(contactInfoHelper, listener);
         }
 
         @Override
diff --git a/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java b/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java
index 5d9a05f..0553422 100644
--- a/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java
+++ b/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java
@@ -126,7 +126,7 @@
         // Do not process requests for details during tests. This would start a background thread,
         // which makes the tests flaky.
         mAdapter.disableRequestProcessingForTest();
-        mAdapter.stopRequestProcessing();
+        mAdapter.pauseCache();
         mParentView = new FrameLayout(mActivity);
         mCursor = new MatrixCursor(CallLogQuery._PROJECTION);
     }