From 74479d448bc39c3534585a627fba603aa89e93ca Mon Sep 17 00:00:00 2001 From: Andrew Lee Date: Thu, 2 Apr 2015 17:23:33 -0700 Subject: 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 --- src/com/android/dialer/calllog/CallLogAdapter.java | 295 ++---------------- .../android/dialer/calllog/CallLogFragment.java | 5 +- .../dialer/contactinfo/ContactInfoCache.java | 342 +++++++++++++++++++++ .../android/dialer/calllog/CallLogAdapterTest.java | 41 ++- .../dialer/calllog/CallLogFragmentTest.java | 2 +- 5 files changed, 399 insertions(+), 286 deletions(-) create mode 100644 src/com/android/dialer/contactinfo/ContactInfoCache.java diff --git a/src/com/android/dialer/calllog/CallLogAdapter.java b/src/com/android/dialer/calllog/CallLogAdapter.java index 8ea861fb3..f5a3f62ed 100644 --- a/src/com/android/dialer/calllog/CallLogAdapter.java +++ b/src/com/android/dialer/calllog/CallLogAdapter.java @@ -21,8 +21,6 @@ import android.content.Intent; 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.contacts.common.util.UriUtils; 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 @@ public class CallLogAdapter extends GroupingListAdapter 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 class CallLogAdapter extends GroupingListAdapter 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 @@ public class CallLogAdapter extends GroupingListAdapter private final OnReportButtonClickListener mOnReportButtonClickListener; private ViewTreeObserver mViewTreeObserver = null; - /** - * A cache of the contact details for the phone numbers in the call log. - *

- * The content of the cache is expired (but not purged) whenever the application comes to - * the foreground. - *

- * The key is number with the country in which the call was placed or received. - */ - private ExpirableCache mContactInfoCache; + protected ContactInfoCache mContactInfoCache; /** * Tracks the call log row which was previously expanded. Used so that the closure of a @@ -143,22 +120,7 @@ public class CallLogAdapter extends GroupingListAdapter */ private HashMap mDayGroups = new HashMap(); - /** - * List of requests to update contact details. - *

- * Each request is made of 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 the contacts and are processed by a background - * thread. - */ - private final LinkedList 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 @@ public class CallLogAdapter extends GroupingListAdapter 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 @@ public class CallLogAdapter extends GroupingListAdapter } }; + 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 @@ public class CallLogAdapter extends GroupingListAdapter // 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 @@ public class CallLogAdapter extends GroupingListAdapter mOnReportButtonClickListener = onReportButtonClickListener; - mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE); - mRequests = new LinkedList(); + mContactInfoCache = new ContactInfoCache( + mContactInfoHelper, mOnContactInfoChangedListener); Resources resources = mContext.getResources(); CallTypeHelper callTypeHelper = new CallTypeHelper(resources); @@ -294,37 +242,6 @@ public class CallLogAdapter extends GroupingListAdapter } } - /** - * 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. */ @@ -336,134 +253,14 @@ public class CallLogAdapter extends GroupingListAdapter } 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. - *

- * It also provides the current contact info stored in the call log for this number. - *

- * 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. - *

- * 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 = 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 @@ public class CallLogAdapter extends GroupingListAdapter // 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 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 @@ public class CallLogAdapter extends GroupingListAdapter } } - /** 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. *

@@ -810,19 +568,20 @@ public class CallLogAdapter extends GroupingListAdapter /** * 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 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 7b5907c1f..77565766e 100644 --- a/src/com/android/dialer/calllog/CallLogFragment.java +++ b/src/com/android/dialer/calllog/CallLogFragment.java @@ -378,8 +378,7 @@ public class CallLogFragment extends ListFragment @Override public void onPause() { super.onPause(); - // Kill the requests thread - mAdapter.stopRequestProcessing(); + mAdapter.pauseCache(); } @Override @@ -392,7 +391,7 @@ public class CallLogFragment extends ListFragment @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 000000000..2bb0f1e95 --- /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 mRequests; + + private ExpirableCache mCache; + + private ContactInfoHelper mContactInfoHelper; + private QueryThread mContactInfoQueryThread; + private OnContactInfoChangedListener mOnContactInfoChangedListener; + + public ContactInfoCache(ContactInfoHelper contactInfoHelper, + OnContactInfoChangedListener onContactInfoChangedListener) { + mContactInfoHelper = contactInfoHelper; + mOnContactInfoChangedListener = onContactInfoChangedListener; + + mRequests = new LinkedList(); + mCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE); + } + + public ContactInfo getValue(String number, String countryIso, ContactInfo cachedContactInfo) { + NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso); + ExpirableCache.CachedValue 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. + *

+ * It also provides the current contact info stored in the call log for this number. + *

+ * 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 0f175117d..dbdde6875 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.test.suitebuilder.annotation.SmallTest; 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 @@ public class CallLogAdapterTest extends AndroidTestCase { 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 @@ public class CallLogAdapterTest extends AndroidTestCase { 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 @@ public class CallLogAdapterTest extends AndroidTestCase { 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 @@ public class CallLogAdapterTest extends AndroidTestCase { 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 @@ public class CallLogAdapterTest extends AndroidTestCase { 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 @@ public class CallLogAdapterTest extends AndroidTestCase { /** * 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 class CallLogAdapterTest extends AndroidTestCase { public final List 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 5d9a05f83..055342250 100644 --- a/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java +++ b/tests/src/com/android/dialer/calllog/CallLogFragmentTest.java @@ -126,7 +126,7 @@ public class CallLogFragmentTest extends ActivityInstrumentationTestCase2