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