Home | History | Annotate | Download | only in calllog
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.contacts.calllog;
     18 
     19 import com.android.common.widget.GroupingListAdapter;
     20 import com.android.contacts.ContactPhotoManager;
     21 import com.android.contacts.PhoneCallDetails;
     22 import com.android.contacts.PhoneCallDetailsHelper;
     23 import com.android.contacts.R;
     24 import com.android.contacts.util.ExpirableCache;
     25 import com.android.contacts.util.UriUtils;
     26 import com.google.common.annotations.VisibleForTesting;
     27 
     28 import android.content.ContentValues;
     29 import android.content.Context;
     30 import android.content.res.Resources;
     31 import android.database.Cursor;
     32 import android.net.Uri;
     33 import android.os.Handler;
     34 import android.os.Message;
     35 import android.provider.CallLog.Calls;
     36 import android.provider.ContactsContract.PhoneLookup;
     37 import android.text.TextUtils;
     38 import android.view.LayoutInflater;
     39 import android.view.View;
     40 import android.view.ViewGroup;
     41 import android.view.ViewTreeObserver;
     42 
     43 import java.util.LinkedList;
     44 
     45 import libcore.util.Objects;
     46 
     47 /**
     48  * Adapter class to fill in data for the Call Log.
     49  */
     50 /*package*/ class CallLogAdapter extends GroupingListAdapter
     51         implements Runnable, ViewTreeObserver.OnPreDrawListener, CallLogGroupBuilder.GroupCreator {
     52     /** Interface used to initiate a refresh of the content. */
     53     public interface CallFetcher {
     54         public void fetchCalls();
     55     }
     56 
     57     /**
     58      * Stores a phone number of a call with the country code where it originally occurred.
     59      * <p>
     60      * Note the country does not necessarily specifies the country of the phone number itself, but
     61      * it is the country in which the user was in when the call was placed or received.
     62      */
     63     private static final class NumberWithCountryIso {
     64         public final String number;
     65         public final String countryIso;
     66 
     67         public NumberWithCountryIso(String number, String countryIso) {
     68             this.number = number;
     69             this.countryIso = countryIso;
     70         }
     71 
     72         @Override
     73         public boolean equals(Object o) {
     74             if (o == null) return false;
     75             if (!(o instanceof NumberWithCountryIso)) return false;
     76             NumberWithCountryIso other = (NumberWithCountryIso) o;
     77             return TextUtils.equals(number, other.number)
     78                     && TextUtils.equals(countryIso, other.countryIso);
     79         }
     80 
     81         @Override
     82         public int hashCode() {
     83             return (number == null ? 0 : number.hashCode())
     84                     ^ (countryIso == null ? 0 : countryIso.hashCode());
     85         }
     86     }
     87 
     88     /** The time in millis to delay starting the thread processing requests. */
     89     private static final int START_PROCESSING_REQUESTS_DELAY_MILLIS = 1000;
     90 
     91     /** The size of the cache of contact info. */
     92     private static final int CONTACT_INFO_CACHE_SIZE = 100;
     93 
     94     private final Context mContext;
     95     private final ContactInfoHelper mContactInfoHelper;
     96     private final CallFetcher mCallFetcher;
     97 
     98     /**
     99      * A cache of the contact details for the phone numbers in the call log.
    100      * <p>
    101      * The content of the cache is expired (but not purged) whenever the application comes to
    102      * the foreground.
    103      * <p>
    104      * The key is number with the country in which the call was placed or received.
    105      */
    106     private ExpirableCache<NumberWithCountryIso, ContactInfo> mContactInfoCache;
    107 
    108     /**
    109      * A request for contact details for the given number.
    110      */
    111     private static final class ContactInfoRequest {
    112         /** The number to look-up. */
    113         public final String number;
    114         /** The country in which a call to or from this number was placed or received. */
    115         public final String countryIso;
    116         /** The cached contact information stored in the call log. */
    117         public final ContactInfo callLogInfo;
    118 
    119         public ContactInfoRequest(String number, String countryIso, ContactInfo callLogInfo) {
    120             this.number = number;
    121             this.countryIso = countryIso;
    122             this.callLogInfo = callLogInfo;
    123         }
    124 
    125         @Override
    126         public boolean equals(Object obj) {
    127             if (this == obj) return true;
    128             if (obj == null) return false;
    129             if (!(obj instanceof ContactInfoRequest)) return false;
    130 
    131             ContactInfoRequest other = (ContactInfoRequest) obj;
    132 
    133             if (!TextUtils.equals(number, other.number)) return false;
    134             if (!TextUtils.equals(countryIso, other.countryIso)) return false;
    135             if (!Objects.equal(callLogInfo, other.callLogInfo)) return false;
    136 
    137             return true;
    138         }
    139 
    140         @Override
    141         public int hashCode() {
    142             final int prime = 31;
    143             int result = 1;
    144             result = prime * result + ((callLogInfo == null) ? 0 : callLogInfo.hashCode());
    145             result = prime * result + ((countryIso == null) ? 0 : countryIso.hashCode());
    146             result = prime * result + ((number == null) ? 0 : number.hashCode());
    147             return result;
    148         }
    149     }
    150 
    151     /**
    152      * List of requests to update contact details.
    153      * <p>
    154      * Each request is made of a phone number to look up, and the contact info currently stored in
    155      * the call log for this number.
    156      * <p>
    157      * The requests are added when displaying the contacts and are processed by a background
    158      * thread.
    159      */
    160     private final LinkedList<ContactInfoRequest> mRequests;
    161 
    162     private volatile boolean mDone;
    163     private boolean mLoading = true;
    164     private ViewTreeObserver.OnPreDrawListener mPreDrawListener;
    165     private static final int REDRAW = 1;
    166     private static final int START_THREAD = 2;
    167 
    168     private boolean mFirst;
    169     private Thread mCallerIdThread;
    170 
    171     /** Instance of helper class for managing views. */
    172     private final CallLogListItemHelper mCallLogViewsHelper;
    173 
    174     /** Helper to set up contact photos. */
    175     private final ContactPhotoManager mContactPhotoManager;
    176     /** Helper to parse and process phone numbers. */
    177     private PhoneNumberHelper mPhoneNumberHelper;
    178     /** Helper to group call log entries. */
    179     private final CallLogGroupBuilder mCallLogGroupBuilder;
    180 
    181     /** Can be set to true by tests to disable processing of requests. */
    182     private volatile boolean mRequestProcessingDisabled = false;
    183 
    184     /** Listener for the primary action in the list, opens the call details. */
    185     private final View.OnClickListener mPrimaryActionListener = new View.OnClickListener() {
    186         @Override
    187         public void onClick(View view) {
    188             IntentProvider intentProvider = (IntentProvider) view.getTag();
    189             if (intentProvider != null) {
    190                 mContext.startActivity(intentProvider.getIntent(mContext));
    191             }
    192         }
    193     };
    194     /** Listener for the secondary action in the list, either call or play. */
    195     private final View.OnClickListener mSecondaryActionListener = new View.OnClickListener() {
    196         @Override
    197         public void onClick(View view) {
    198             IntentProvider intentProvider = (IntentProvider) view.getTag();
    199             if (intentProvider != null) {
    200                 mContext.startActivity(intentProvider.getIntent(mContext));
    201             }
    202         }
    203     };
    204 
    205     @Override
    206     public boolean onPreDraw() {
    207         if (mFirst) {
    208             mHandler.sendEmptyMessageDelayed(START_THREAD,
    209                     START_PROCESSING_REQUESTS_DELAY_MILLIS);
    210             mFirst = false;
    211         }
    212         return true;
    213     }
    214 
    215     private Handler mHandler = new Handler() {
    216         @Override
    217         public void handleMessage(Message msg) {
    218             switch (msg.what) {
    219                 case REDRAW:
    220                     notifyDataSetChanged();
    221                     break;
    222                 case START_THREAD:
    223                     startRequestProcessing();
    224                     break;
    225             }
    226         }
    227     };
    228 
    229     CallLogAdapter(Context context, CallFetcher callFetcher,
    230             ContactInfoHelper contactInfoHelper) {
    231         super(context);
    232 
    233         mContext = context;
    234         mCallFetcher = callFetcher;
    235         mContactInfoHelper = contactInfoHelper;
    236 
    237         mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE);
    238         mRequests = new LinkedList<ContactInfoRequest>();
    239         mPreDrawListener = null;
    240 
    241         Resources resources = mContext.getResources();
    242         CallTypeHelper callTypeHelper = new CallTypeHelper(resources);
    243 
    244         mContactPhotoManager = ContactPhotoManager.getInstance(mContext);
    245         mPhoneNumberHelper = new PhoneNumberHelper(resources);
    246         PhoneCallDetailsHelper phoneCallDetailsHelper = new PhoneCallDetailsHelper(
    247                 resources, callTypeHelper, mPhoneNumberHelper);
    248         mCallLogViewsHelper =
    249                 new CallLogListItemHelper(
    250                         phoneCallDetailsHelper, mPhoneNumberHelper, resources);
    251         mCallLogGroupBuilder = new CallLogGroupBuilder(this);
    252     }
    253 
    254     /**
    255      * Requery on background thread when {@link Cursor} changes.
    256      */
    257     @Override
    258     protected void onContentChanged() {
    259         mCallFetcher.fetchCalls();
    260     }
    261 
    262     void setLoading(boolean loading) {
    263         mLoading = loading;
    264     }
    265 
    266     @Override
    267     public boolean isEmpty() {
    268         if (mLoading) {
    269             // We don't want the empty state to show when loading.
    270             return false;
    271         } else {
    272             return super.isEmpty();
    273         }
    274     }
    275 
    276     private void startRequestProcessing() {
    277         if (mRequestProcessingDisabled) {
    278             return;
    279         }
    280 
    281         mDone = false;
    282         mCallerIdThread = new Thread(this, "CallLogContactLookup");
    283         mCallerIdThread.setPriority(Thread.MIN_PRIORITY);
    284         mCallerIdThread.start();
    285     }
    286 
    287     /**
    288      * Stops the background thread that processes updates and cancels any pending requests to
    289      * start it.
    290      * <p>
    291      * Should be called from the main thread to prevent a race condition between the request to
    292      * start the thread being processed and stopping the thread.
    293      */
    294     public void stopRequestProcessing() {
    295         // Remove any pending requests to start the processing thread.
    296         mHandler.removeMessages(START_THREAD);
    297         mDone = true;
    298         if (mCallerIdThread != null) mCallerIdThread.interrupt();
    299     }
    300 
    301     public void invalidateCache() {
    302         mContactInfoCache.expireAll();
    303         // Let it restart the thread after next draw
    304         mPreDrawListener = null;
    305     }
    306 
    307     /**
    308      * Enqueues a request to look up the contact details for the given phone number.
    309      * <p>
    310      * It also provides the current contact info stored in the call log for this number.
    311      * <p>
    312      * If the {@code immediate} parameter is true, it will start immediately the thread that looks
    313      * up the contact information (if it has not been already started). Otherwise, it will be
    314      * started with a delay. See {@link #START_PROCESSING_REQUESTS_DELAY_MILLIS}.
    315      */
    316     @VisibleForTesting
    317     void enqueueRequest(String number, String countryIso, ContactInfo callLogInfo,
    318             boolean immediate) {
    319         ContactInfoRequest request = new ContactInfoRequest(number, countryIso, callLogInfo);
    320         synchronized (mRequests) {
    321             if (!mRequests.contains(request)) {
    322                 mRequests.add(request);
    323                 mRequests.notifyAll();
    324             }
    325         }
    326         if (mFirst && immediate) {
    327             startRequestProcessing();
    328             mFirst = false;
    329         }
    330     }
    331 
    332     /**
    333      * Queries the appropriate content provider for the contact associated with the number.
    334      * <p>
    335      * Upon completion it also updates the cache in the call log, if it is different from
    336      * {@code callLogInfo}.
    337      * <p>
    338      * The number might be either a SIP address or a phone number.
    339      * <p>
    340      * It returns true if it updated the content of the cache and we should therefore tell the
    341      * view to update its content.
    342      */
    343     private boolean queryContactInfo(String number, String countryIso, ContactInfo callLogInfo) {
    344         final ContactInfo info = mContactInfoHelper.lookupNumber(number, countryIso);
    345 
    346         if (info == null) {
    347             // The lookup failed, just return without requesting to update the view.
    348             return false;
    349         }
    350 
    351         // Check the existing entry in the cache: only if it has changed we should update the
    352         // view.
    353         NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
    354         ContactInfo existingInfo = mContactInfoCache.getPossiblyExpired(numberCountryIso);
    355         boolean updated = !info.equals(existingInfo);
    356         // Store the data in the cache so that the UI thread can use to display it. Store it
    357         // even if it has not changed so that it is marked as not expired.
    358         mContactInfoCache.put(numberCountryIso, info);
    359         // Update the call log even if the cache it is up-to-date: it is possible that the cache
    360         // contains the value from a different call log entry.
    361         updateCallLogContactInfoCache(number, countryIso, info, callLogInfo);
    362         return updated;
    363     }
    364     /*
    365      * Handles requests for contact name and number type
    366      * @see java.lang.Runnable#run()
    367      */
    368     @Override
    369     public void run() {
    370         boolean needNotify = false;
    371         while (!mDone) {
    372             ContactInfoRequest request = null;
    373             synchronized (mRequests) {
    374                 if (!mRequests.isEmpty()) {
    375                     request = mRequests.removeFirst();
    376                 } else {
    377                     if (needNotify) {
    378                         needNotify = false;
    379                         mHandler.sendEmptyMessage(REDRAW);
    380                     }
    381                     try {
    382                         mRequests.wait(1000);
    383                     } catch (InterruptedException ie) {
    384                         // Ignore and continue processing requests
    385                         Thread.currentThread().interrupt();
    386                     }
    387                 }
    388             }
    389             if (!mDone && request != null
    390                     && queryContactInfo(request.number, request.countryIso, request.callLogInfo)) {
    391                 needNotify = true;
    392             }
    393         }
    394     }
    395 
    396     @Override
    397     protected void addGroups(Cursor cursor) {
    398         mCallLogGroupBuilder.addGroups(cursor);
    399     }
    400 
    401     @Override
    402     protected View newStandAloneView(Context context, ViewGroup parent) {
    403         LayoutInflater inflater =
    404                 (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    405         View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
    406         findAndCacheViews(view);
    407         return view;
    408     }
    409 
    410     @Override
    411     protected void bindStandAloneView(View view, Context context, Cursor cursor) {
    412         bindView(view, cursor, 1);
    413     }
    414 
    415     @Override
    416     protected View newChildView(Context context, ViewGroup parent) {
    417         LayoutInflater inflater =
    418                 (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    419         View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
    420         findAndCacheViews(view);
    421         return view;
    422     }
    423 
    424     @Override
    425     protected void bindChildView(View view, Context context, Cursor cursor) {
    426         bindView(view, cursor, 1);
    427     }
    428 
    429     @Override
    430     protected View newGroupView(Context context, ViewGroup parent) {
    431         LayoutInflater inflater =
    432                 (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    433         View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
    434         findAndCacheViews(view);
    435         return view;
    436     }
    437 
    438     @Override
    439     protected void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
    440             boolean expanded) {
    441         bindView(view, cursor, groupSize);
    442     }
    443 
    444     private void findAndCacheViews(View view) {
    445         // Get the views to bind to.
    446         CallLogListItemViews views = CallLogListItemViews.fromView(view);
    447         views.primaryActionView.setOnClickListener(mPrimaryActionListener);
    448         views.secondaryActionView.setOnClickListener(mSecondaryActionListener);
    449         view.setTag(views);
    450     }
    451 
    452     /**
    453      * Binds the views in the entry to the data in the call log.
    454      *
    455      * @param view the view corresponding to this entry
    456      * @param c the cursor pointing to the entry in the call log
    457      * @param count the number of entries in the current item, greater than 1 if it is a group
    458      */
    459     private void bindView(View view, Cursor c, int count) {
    460         final CallLogListItemViews views = (CallLogListItemViews) view.getTag();
    461         final int section = c.getInt(CallLogQuery.SECTION);
    462 
    463         // This might be a header: check the value of the section column in the cursor.
    464         if (section == CallLogQuery.SECTION_NEW_HEADER
    465                 || section == CallLogQuery.SECTION_OLD_HEADER) {
    466             views.primaryActionView.setVisibility(View.GONE);
    467             views.bottomDivider.setVisibility(View.GONE);
    468             views.listHeaderTextView.setVisibility(View.VISIBLE);
    469             views.listHeaderTextView.setText(
    470                     section == CallLogQuery.SECTION_NEW_HEADER
    471                             ? R.string.call_log_new_header
    472                             : R.string.call_log_old_header);
    473             // Nothing else to set up for a header.
    474             return;
    475         }
    476         // Default case: an item in the call log.
    477         views.primaryActionView.setVisibility(View.VISIBLE);
    478         views.bottomDivider.setVisibility(isLastOfSection(c) ? View.GONE : View.VISIBLE);
    479         views.listHeaderTextView.setVisibility(View.GONE);
    480 
    481         final String number = c.getString(CallLogQuery.NUMBER);
    482         final long date = c.getLong(CallLogQuery.DATE);
    483         final long duration = c.getLong(CallLogQuery.DURATION);
    484         final int callType = c.getInt(CallLogQuery.CALL_TYPE);
    485         final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO);
    486 
    487         final ContactInfo cachedContactInfo = getContactInfoFromCallLog(c);
    488 
    489         views.primaryActionView.setTag(
    490                 IntentProvider.getCallDetailIntentProvider(
    491                         this, c.getPosition(), c.getLong(CallLogQuery.ID), count));
    492         // Store away the voicemail information so we can play it directly.
    493         if (callType == Calls.VOICEMAIL_TYPE) {
    494             String voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI);
    495             final long rowId = c.getLong(CallLogQuery.ID);
    496             views.secondaryActionView.setTag(
    497                     IntentProvider.getPlayVoicemailIntentProvider(rowId, voicemailUri));
    498         } else if (!TextUtils.isEmpty(number)) {
    499             // Store away the number so we can call it directly if you click on the call icon.
    500             views.secondaryActionView.setTag(
    501                     IntentProvider.getReturnCallIntentProvider(number));
    502         } else {
    503             // No action enabled.
    504             views.secondaryActionView.setTag(null);
    505         }
    506 
    507         // Lookup contacts with this number
    508         NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
    509         ExpirableCache.CachedValue<ContactInfo> cachedInfo =
    510                 mContactInfoCache.getCachedValue(numberCountryIso);
    511         ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue();
    512         if (!mPhoneNumberHelper.canPlaceCallsTo(number)
    513                 || mPhoneNumberHelper.isVoicemailNumber(number)) {
    514             // If this is a number that cannot be dialed, there is no point in looking up a contact
    515             // for it.
    516             info = ContactInfo.EMPTY;
    517         } else if (cachedInfo == null) {
    518             mContactInfoCache.put(numberCountryIso, ContactInfo.EMPTY);
    519             // Use the cached contact info from the call log.
    520             info = cachedContactInfo;
    521             // The db request should happen on a non-UI thread.
    522             // Request the contact details immediately since they are currently missing.
    523             enqueueRequest(number, countryIso, cachedContactInfo, true);
    524             // We will format the phone number when we make the background request.
    525         } else {
    526             if (cachedInfo.isExpired()) {
    527                 // The contact info is no longer up to date, we should request it. However, we
    528                 // do not need to request them immediately.
    529                 enqueueRequest(number, countryIso, cachedContactInfo, false);
    530             } else  if (!callLogInfoMatches(cachedContactInfo, info)) {
    531                 // The call log information does not match the one we have, look it up again.
    532                 // We could simply update the call log directly, but that needs to be done in a
    533                 // background thread, so it is easier to simply request a new lookup, which will, as
    534                 // a side-effect, update the call log.
    535                 enqueueRequest(number, countryIso, cachedContactInfo, false);
    536             }
    537 
    538             if (info == ContactInfo.EMPTY) {
    539                 // Use the cached contact info from the call log.
    540                 info = cachedContactInfo;
    541             }
    542         }
    543 
    544         final Uri lookupUri = info.lookupUri;
    545         final String name = info.name;
    546         final int ntype = info.type;
    547         final String label = info.label;
    548         final long photoId = info.photoId;
    549         CharSequence formattedNumber = info.formattedNumber;
    550         final int[] callTypes = getCallTypes(c, count);
    551         final String geocode = c.getString(CallLogQuery.GEOCODED_LOCATION);
    552         final PhoneCallDetails details;
    553         if (TextUtils.isEmpty(name)) {
    554             details = new PhoneCallDetails(number, formattedNumber, countryIso, geocode,
    555                     callTypes, date, duration);
    556         } else {
    557             // We do not pass a photo id since we do not need the high-res picture.
    558             details = new PhoneCallDetails(number, formattedNumber, countryIso, geocode,
    559                     callTypes, date, duration, name, ntype, label, lookupUri, null);
    560         }
    561 
    562         final boolean isNew = c.getInt(CallLogQuery.IS_READ) == 0;
    563         // New items also use the highlighted version of the text.
    564         final boolean isHighlighted = isNew;
    565         mCallLogViewsHelper.setPhoneCallDetails(views, details, isHighlighted);
    566         setPhoto(views, photoId, lookupUri);
    567 
    568         // Listen for the first draw
    569         if (mPreDrawListener == null) {
    570             mFirst = true;
    571             mPreDrawListener = this;
    572             view.getViewTreeObserver().addOnPreDrawListener(this);
    573         }
    574     }
    575 
    576     /** Returns true if this is the last item of a section. */
    577     private boolean isLastOfSection(Cursor c) {
    578         if (c.isLast()) return true;
    579         final int section = c.getInt(CallLogQuery.SECTION);
    580         if (!c.moveToNext()) return true;
    581         final int nextSection = c.getInt(CallLogQuery.SECTION);
    582         c.moveToPrevious();
    583         return section != nextSection;
    584     }
    585 
    586     /** Checks whether the contact info from the call log matches the one from the contacts db. */
    587     private boolean callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info) {
    588         // The call log only contains a subset of the fields in the contacts db.
    589         // Only check those.
    590         return TextUtils.equals(callLogInfo.name, info.name)
    591                 && callLogInfo.type == info.type
    592                 && TextUtils.equals(callLogInfo.label, info.label);
    593     }
    594 
    595     /** Stores the updated contact info in the call log if it is different from the current one. */
    596     private void updateCallLogContactInfoCache(String number, String countryIso,
    597             ContactInfo updatedInfo, ContactInfo callLogInfo) {
    598         final ContentValues values = new ContentValues();
    599         boolean needsUpdate = false;
    600 
    601         if (callLogInfo != null) {
    602             if (!TextUtils.equals(updatedInfo.name, callLogInfo.name)) {
    603                 values.put(Calls.CACHED_NAME, updatedInfo.name);
    604                 needsUpdate = true;
    605             }
    606 
    607             if (updatedInfo.type != callLogInfo.type) {
    608                 values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
    609                 needsUpdate = true;
    610             }
    611 
    612             if (!TextUtils.equals(updatedInfo.label, callLogInfo.label)) {
    613                 values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
    614                 needsUpdate = true;
    615             }
    616             if (!UriUtils.areEqual(updatedInfo.lookupUri, callLogInfo.lookupUri)) {
    617                 values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
    618                 needsUpdate = true;
    619             }
    620             if (!TextUtils.equals(updatedInfo.normalizedNumber, callLogInfo.normalizedNumber)) {
    621                 values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
    622                 needsUpdate = true;
    623             }
    624             if (!TextUtils.equals(updatedInfo.number, callLogInfo.number)) {
    625                 values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
    626                 needsUpdate = true;
    627             }
    628             if (updatedInfo.photoId != callLogInfo.photoId) {
    629                 values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
    630                 needsUpdate = true;
    631             }
    632             if (!TextUtils.equals(updatedInfo.formattedNumber, callLogInfo.formattedNumber)) {
    633                 values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
    634                 needsUpdate = true;
    635             }
    636         } else {
    637             // No previous values, store all of them.
    638             values.put(Calls.CACHED_NAME, updatedInfo.name);
    639             values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
    640             values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
    641             values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
    642             values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
    643             values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
    644             values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
    645             values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
    646             needsUpdate = true;
    647         }
    648 
    649         if (!needsUpdate) {
    650             return;
    651         }
    652 
    653         if (countryIso == null) {
    654             mContext.getContentResolver().update(Calls.CONTENT_URI_WITH_VOICEMAIL, values,
    655                     Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " IS NULL",
    656                     new String[]{ number });
    657         } else {
    658             mContext.getContentResolver().update(Calls.CONTENT_URI_WITH_VOICEMAIL, values,
    659                     Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " = ?",
    660                     new String[]{ number, countryIso });
    661         }
    662     }
    663 
    664     /** Returns the contact information as stored in the call log. */
    665     private ContactInfo getContactInfoFromCallLog(Cursor c) {
    666         ContactInfo info = new ContactInfo();
    667         info.lookupUri = UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_LOOKUP_URI));
    668         info.name = c.getString(CallLogQuery.CACHED_NAME);
    669         info.type = c.getInt(CallLogQuery.CACHED_NUMBER_TYPE);
    670         info.label = c.getString(CallLogQuery.CACHED_NUMBER_LABEL);
    671         String matchedNumber = c.getString(CallLogQuery.CACHED_MATCHED_NUMBER);
    672         info.number = matchedNumber == null ? c.getString(CallLogQuery.NUMBER) : matchedNumber;
    673         info.normalizedNumber = c.getString(CallLogQuery.CACHED_NORMALIZED_NUMBER);
    674         info.photoId = c.getLong(CallLogQuery.CACHED_PHOTO_ID);
    675         info.photoUri = null;  // We do not cache the photo URI.
    676         info.formattedNumber = c.getString(CallLogQuery.CACHED_FORMATTED_NUMBER);
    677         return info;
    678     }
    679 
    680     /**
    681      * Returns the call types for the given number of items in the cursor.
    682      * <p>
    683      * It uses the next {@code count} rows in the cursor to extract the types.
    684      * <p>
    685      * It position in the cursor is unchanged by this function.
    686      */
    687     private int[] getCallTypes(Cursor cursor, int count) {
    688         int position = cursor.getPosition();
    689         int[] callTypes = new int[count];
    690         for (int index = 0; index < count; ++index) {
    691             callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE);
    692             cursor.moveToNext();
    693         }
    694         cursor.moveToPosition(position);
    695         return callTypes;
    696     }
    697 
    698     private void setPhoto(CallLogListItemViews views, long photoId, Uri contactUri) {
    699         views.quickContactView.assignContactUri(contactUri);
    700         mContactPhotoManager.loadPhoto(views.quickContactView, photoId, false, true);
    701     }
    702 
    703     /**
    704      * Sets whether processing of requests for contact details should be enabled.
    705      * <p>
    706      * This method should be called in tests to disable such processing of requests when not
    707      * needed.
    708      */
    709     @VisibleForTesting
    710     void disableRequestProcessingForTest() {
    711         mRequestProcessingDisabled = true;
    712     }
    713 
    714     @VisibleForTesting
    715     void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) {
    716         NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
    717         mContactInfoCache.put(numberCountryIso, contactInfo);
    718     }
    719 
    720     @Override
    721     public void addGroup(int cursorPosition, int size, boolean expanded) {
    722         super.addGroup(cursorPosition, size, expanded);
    723     }
    724 
    725     /*
    726      * Get the number from the Contacts, if available, since sometimes
    727      * the number provided by caller id may not be formatted properly
    728      * depending on the carrier (roaming) in use at the time of the
    729      * incoming call.
    730      * Logic : If the caller-id number starts with a "+", use it
    731      *         Else if the number in the contacts starts with a "+", use that one
    732      *         Else if the number in the contacts is longer, use that one
    733      */
    734     public String getBetterNumberFromContacts(String number, String countryIso) {
    735         String matchingNumber = null;
    736         // Look in the cache first. If it's not found then query the Phones db
    737         NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
    738         ContactInfo ci = mContactInfoCache.getPossiblyExpired(numberCountryIso);
    739         if (ci != null && ci != ContactInfo.EMPTY) {
    740             matchingNumber = ci.number;
    741         } else {
    742             try {
    743                 Cursor phonesCursor = mContext.getContentResolver().query(
    744                         Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number),
    745                         PhoneQuery._PROJECTION, null, null, null);
    746                 if (phonesCursor != null) {
    747                     if (phonesCursor.moveToFirst()) {
    748                         matchingNumber = phonesCursor.getString(PhoneQuery.MATCHED_NUMBER);
    749                     }
    750                     phonesCursor.close();
    751                 }
    752             } catch (Exception e) {
    753                 // Use the number from the call log
    754             }
    755         }
    756         if (!TextUtils.isEmpty(matchingNumber) &&
    757                 (matchingNumber.startsWith("+")
    758                         || matchingNumber.length() > number.length())) {
    759             number = matchingNumber;
    760         }
    761         return number;
    762     }
    763 }
    764