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