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.Intent;
     22 import android.content.res.Resources;
     23 import android.database.Cursor;
     24 import android.net.Uri;
     25 import android.os.Handler;
     26 import android.os.Message;
     27 import android.provider.CallLog.Calls;
     28 import android.provider.ContactsContract.PhoneLookup;
     29 import android.text.TextUtils;
     30 import android.view.LayoutInflater;
     31 import android.view.View;
     32 import android.view.ViewGroup;
     33 import android.view.ViewStub;
     34 import android.view.ViewTreeObserver;
     35 import android.widget.ImageView;
     36 import android.widget.TextView;
     37 
     38 import com.android.common.widget.GroupingListAdapter;
     39 import com.android.contacts.common.ContactPhotoManager;
     40 import com.android.contacts.common.util.UriUtils;
     41 import com.android.dialer.PhoneCallDetails;
     42 import com.android.dialer.PhoneCallDetailsHelper;
     43 import com.android.dialer.R;
     44 import com.android.dialer.util.ExpirableCache;
     45 import com.google.common.annotations.VisibleForTesting;
     46 import com.google.common.base.Objects;
     47 
     48 import java.util.LinkedList;
     49 
     50 /**
     51  * Adapter class to fill in data for the Call Log.
     52  */
     53 public class CallLogAdapter extends GroupingListAdapter
     54         implements ViewTreeObserver.OnPreDrawListener, CallLogGroupBuilder.GroupCreator {
     55 
     56     /** Interface used to initiate a refresh of the content. */
     57     public interface CallFetcher {
     58         public void fetchCalls();
     59     }
     60 
     61     /**
     62      * Stores a phone number of a call with the country code where it originally occurred.
     63      * <p>
     64      * Note the country does not necessarily specifies the country of the phone number itself, but
     65      * it is the country in which the user was in when the call was placed or received.
     66      */
     67     private static final class NumberWithCountryIso {
     68         public final String number;
     69         public final String countryIso;
     70 
     71         public NumberWithCountryIso(String number, String countryIso) {
     72             this.number = number;
     73             this.countryIso = countryIso;
     74         }
     75 
     76         @Override
     77         public boolean equals(Object o) {
     78             if (o == null) return false;
     79             if (!(o instanceof NumberWithCountryIso)) return false;
     80             NumberWithCountryIso other = (NumberWithCountryIso) o;
     81             return TextUtils.equals(number, other.number)
     82                     && TextUtils.equals(countryIso, other.countryIso);
     83         }
     84 
     85         @Override
     86         public int hashCode() {
     87             return (number == null ? 0 : number.hashCode())
     88                     ^ (countryIso == null ? 0 : countryIso.hashCode());
     89         }
     90     }
     91 
     92     /** The time in millis to delay starting the thread processing requests. */
     93     private static final int START_PROCESSING_REQUESTS_DELAY_MILLIS = 1000;
     94 
     95     /** The size of the cache of contact info. */
     96     private static final int CONTACT_INFO_CACHE_SIZE = 100;
     97 
     98     protected final Context mContext;
     99     private final ContactInfoHelper mContactInfoHelper;
    100     private final CallFetcher mCallFetcher;
    101     private ViewTreeObserver mViewTreeObserver = null;
    102 
    103     /**
    104      * A cache of the contact details for the phone numbers in the call log.
    105      * <p>
    106      * The content of the cache is expired (but not purged) whenever the application comes to
    107      * the foreground.
    108      * <p>
    109      * The key is number with the country in which the call was placed or received.
    110      */
    111     private ExpirableCache<NumberWithCountryIso, ContactInfo> mContactInfoCache;
    112 
    113     /**
    114      * A request for contact details for the given number.
    115      */
    116     private static final class ContactInfoRequest {
    117         /** The number to look-up. */
    118         public final String number;
    119         /** The country in which a call to or from this number was placed or received. */
    120         public final String countryIso;
    121         /** The cached contact information stored in the call log. */
    122         public final ContactInfo callLogInfo;
    123 
    124         public ContactInfoRequest(String number, String countryIso, ContactInfo callLogInfo) {
    125             this.number = number;
    126             this.countryIso = countryIso;
    127             this.callLogInfo = callLogInfo;
    128         }
    129 
    130         @Override
    131         public boolean equals(Object obj) {
    132             if (this == obj) return true;
    133             if (obj == null) return false;
    134             if (!(obj instanceof ContactInfoRequest)) return false;
    135 
    136             ContactInfoRequest other = (ContactInfoRequest) obj;
    137 
    138             if (!TextUtils.equals(number, other.number)) return false;
    139             if (!TextUtils.equals(countryIso, other.countryIso)) return false;
    140             if (!Objects.equal(callLogInfo, other.callLogInfo)) return false;
    141 
    142             return true;
    143         }
    144 
    145         @Override
    146         public int hashCode() {
    147             final int prime = 31;
    148             int result = 1;
    149             result = prime * result + ((callLogInfo == null) ? 0 : callLogInfo.hashCode());
    150             result = prime * result + ((countryIso == null) ? 0 : countryIso.hashCode());
    151             result = prime * result + ((number == null) ? 0 : number.hashCode());
    152             return result;
    153         }
    154     }
    155 
    156     /**
    157      * List of requests to update contact details.
    158      * <p>
    159      * Each request is made of a phone number to look up, and the contact info currently stored in
    160      * the call log for this number.
    161      * <p>
    162      * The requests are added when displaying the contacts and are processed by a background
    163      * thread.
    164      */
    165     private final LinkedList<ContactInfoRequest> mRequests;
    166 
    167     private boolean mLoading = true;
    168     private static final int REDRAW = 1;
    169     private static final int START_THREAD = 2;
    170 
    171     private QueryThread mCallerIdThread;
    172 
    173     /** Instance of helper class for managing views. */
    174     private final CallLogListItemHelper mCallLogViewsHelper;
    175 
    176     /** Helper to set up contact photos. */
    177     private final ContactPhotoManager mContactPhotoManager;
    178     /** Helper to parse and process phone numbers. */
    179     private PhoneNumberHelper mPhoneNumberHelper;
    180     /** Helper to group call log entries. */
    181     private final CallLogGroupBuilder mCallLogGroupBuilder;
    182 
    183     /** Can be set to true by tests to disable processing of requests. */
    184     private volatile boolean mRequestProcessingDisabled = false;
    185 
    186     /** True if CallLogAdapter is created from the PhoneFavoriteFragment, where the primary
    187      * action should be set to call a number instead of opening the detail page. */
    188     private boolean mUseCallAsPrimaryAction = false;
    189 
    190     private boolean mIsCallLog = true;
    191     private int mNumMissedCalls = 0;
    192     private int mNumMissedCallsShown = 0;
    193 
    194     private View mBadgeContainer;
    195     private ImageView mBadgeImageView;
    196     private TextView mBadgeText;
    197 
    198     /** Listener for the primary or secondary actions in the list.
    199      *  Primary opens the call details.
    200      *  Secondary calls or plays.
    201      **/
    202     private final View.OnClickListener mActionListener = new View.OnClickListener() {
    203         @Override
    204         public void onClick(View view) {
    205             startActivityForAction(view);
    206         }
    207     };
    208 
    209     private void startActivityForAction(View view) {
    210         final IntentProvider intentProvider = (IntentProvider) view.getTag();
    211         if (intentProvider != null) {
    212             final Intent intent = intentProvider.getIntent(mContext);
    213             // See IntentProvider.getCallDetailIntentProvider() for why this may be null.
    214             if (intent != null) {
    215                 mContext.startActivity(intent);
    216             }
    217         }
    218     }
    219 
    220     @Override
    221     public boolean onPreDraw() {
    222         // We only wanted to listen for the first draw (and this is it).
    223         unregisterPreDrawListener();
    224 
    225         // Only schedule a thread-creation message if the thread hasn't been
    226         // created yet. This is purely an optimization, to queue fewer messages.
    227         if (mCallerIdThread == null) {
    228             mHandler.sendEmptyMessageDelayed(START_THREAD, START_PROCESSING_REQUESTS_DELAY_MILLIS);
    229         }
    230 
    231         return true;
    232     }
    233 
    234     private Handler mHandler = new Handler() {
    235         @Override
    236         public void handleMessage(Message msg) {
    237             switch (msg.what) {
    238                 case REDRAW:
    239                     notifyDataSetChanged();
    240                     break;
    241                 case START_THREAD:
    242                     startRequestProcessing();
    243                     break;
    244             }
    245         }
    246     };
    247 
    248     public CallLogAdapter(Context context, CallFetcher callFetcher,
    249             ContactInfoHelper contactInfoHelper, boolean useCallAsPrimaryAction,
    250             boolean isCallLog) {
    251         super(context);
    252 
    253         mContext = context;
    254         mCallFetcher = callFetcher;
    255         mContactInfoHelper = contactInfoHelper;
    256         mUseCallAsPrimaryAction = useCallAsPrimaryAction;
    257         mIsCallLog = isCallLog;
    258 
    259         mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE);
    260         mRequests = new LinkedList<ContactInfoRequest>();
    261 
    262         Resources resources = mContext.getResources();
    263         CallTypeHelper callTypeHelper = new CallTypeHelper(resources);
    264 
    265         mContactPhotoManager = ContactPhotoManager.getInstance(mContext);
    266         mPhoneNumberHelper = new PhoneNumberHelper(resources);
    267         PhoneCallDetailsHelper phoneCallDetailsHelper = new PhoneCallDetailsHelper(
    268                 resources, callTypeHelper, new PhoneNumberUtilsWrapper());
    269         mCallLogViewsHelper =
    270                 new CallLogListItemHelper(
    271                         phoneCallDetailsHelper, mPhoneNumberHelper, resources);
    272         mCallLogGroupBuilder = new CallLogGroupBuilder(this);
    273     }
    274 
    275     /**
    276      * Requery on background thread when {@link Cursor} changes.
    277      */
    278     @Override
    279     protected void onContentChanged() {
    280         mCallFetcher.fetchCalls();
    281     }
    282 
    283     public void setLoading(boolean loading) {
    284         mLoading = loading;
    285     }
    286 
    287     @Override
    288     public boolean isEmpty() {
    289         if (mLoading) {
    290             // We don't want the empty state to show when loading.
    291             return false;
    292         } else {
    293             return super.isEmpty();
    294         }
    295     }
    296 
    297     /**
    298      * Starts a background thread to process contact-lookup requests, unless one
    299      * has already been started.
    300      */
    301     private synchronized void startRequestProcessing() {
    302         // For unit-testing.
    303         if (mRequestProcessingDisabled) return;
    304 
    305         // Idempotence... if a thread is already started, don't start another.
    306         if (mCallerIdThread != null) return;
    307 
    308         mCallerIdThread = new QueryThread();
    309         mCallerIdThread.setPriority(Thread.MIN_PRIORITY);
    310         mCallerIdThread.start();
    311     }
    312 
    313     /**
    314      * Stops the background thread that processes updates and cancels any
    315      * pending requests to start it.
    316      */
    317     public synchronized void stopRequestProcessing() {
    318         // Remove any pending requests to start the processing thread.
    319         mHandler.removeMessages(START_THREAD);
    320         if (mCallerIdThread != null) {
    321             // Stop the thread; we are finished with it.
    322             mCallerIdThread.stopProcessing();
    323             mCallerIdThread.interrupt();
    324             mCallerIdThread = null;
    325         }
    326     }
    327 
    328     /**
    329      * Stop receiving onPreDraw() notifications.
    330      */
    331     private void unregisterPreDrawListener() {
    332         if (mViewTreeObserver != null && mViewTreeObserver.isAlive()) {
    333             mViewTreeObserver.removeOnPreDrawListener(this);
    334         }
    335         mViewTreeObserver = null;
    336     }
    337 
    338     public void invalidateCache() {
    339         mContactInfoCache.expireAll();
    340 
    341         // Restart the request-processing thread after the next draw.
    342         stopRequestProcessing();
    343         unregisterPreDrawListener();
    344     }
    345 
    346     /**
    347      * Enqueues a request to look up the contact details for the given phone number.
    348      * <p>
    349      * It also provides the current contact info stored in the call log for this number.
    350      * <p>
    351      * If the {@code immediate} parameter is true, it will start immediately the thread that looks
    352      * up the contact information (if it has not been already started). Otherwise, it will be
    353      * started with a delay. See {@link #START_PROCESSING_REQUESTS_DELAY_MILLIS}.
    354      */
    355     protected void enqueueRequest(String number, String countryIso, ContactInfo callLogInfo,
    356             boolean immediate) {
    357         ContactInfoRequest request = new ContactInfoRequest(number, countryIso, callLogInfo);
    358         synchronized (mRequests) {
    359             if (!mRequests.contains(request)) {
    360                 mRequests.add(request);
    361                 mRequests.notifyAll();
    362             }
    363         }
    364         if (immediate) startRequestProcessing();
    365     }
    366 
    367     /**
    368      * Queries the appropriate content provider for the contact associated with the number.
    369      * <p>
    370      * Upon completion it also updates the cache in the call log, if it is different from
    371      * {@code callLogInfo}.
    372      * <p>
    373      * The number might be either a SIP address or a phone number.
    374      * <p>
    375      * It returns true if it updated the content of the cache and we should therefore tell the
    376      * view to update its content.
    377      */
    378     private boolean queryContactInfo(String number, String countryIso, ContactInfo callLogInfo) {
    379         final ContactInfo info = mContactInfoHelper.lookupNumber(number, countryIso);
    380 
    381         if (info == null) {
    382             // The lookup failed, just return without requesting to update the view.
    383             return false;
    384         }
    385 
    386         // Check the existing entry in the cache: only if it has changed we should update the
    387         // view.
    388         NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
    389         ContactInfo existingInfo = mContactInfoCache.getPossiblyExpired(numberCountryIso);
    390 
    391         final boolean isRemoteSource = info.sourceType != 0;
    392 
    393         // Don't force redraw if existing info in the cache is equal to {@link ContactInfo#EMPTY}
    394         // to avoid updating the data set for every new row that is scrolled into view.
    395         // see (https://googleplex-android-review.git.corp.google.com/#/c/166680/)
    396 
    397         // Exception: Photo uris for contacts from remote sources are not cached in the call log
    398         // cache, so we have to force a redraw for these contacts regardless.
    399         boolean updated = (existingInfo != ContactInfo.EMPTY || isRemoteSource) &&
    400                 !info.equals(existingInfo);
    401 
    402         // Store the data in the cache so that the UI thread can use to display it. Store it
    403         // even if it has not changed so that it is marked as not expired.
    404         mContactInfoCache.put(numberCountryIso, info);
    405         // Update the call log even if the cache it is up-to-date: it is possible that the cache
    406         // contains the value from a different call log entry.
    407         updateCallLogContactInfoCache(number, countryIso, info, callLogInfo);
    408         return updated;
    409     }
    410 
    411     /*
    412      * Handles requests for contact name and number type.
    413      */
    414     private class QueryThread extends Thread {
    415         private volatile boolean mDone = false;
    416 
    417         public QueryThread() {
    418             super("CallLogAdapter.QueryThread");
    419         }
    420 
    421         public void stopProcessing() {
    422             mDone = true;
    423         }
    424 
    425         @Override
    426         public void run() {
    427             boolean needRedraw = false;
    428             while (true) {
    429                 // Check if thread is finished, and if so return immediately.
    430                 if (mDone) return;
    431 
    432                 // Obtain next request, if any is available.
    433                 // Keep synchronized section small.
    434                 ContactInfoRequest req = null;
    435                 synchronized (mRequests) {
    436                     if (!mRequests.isEmpty()) {
    437                         req = mRequests.removeFirst();
    438                     }
    439                 }
    440 
    441                 if (req != null) {
    442                     // Process the request. If the lookup succeeds, schedule a
    443                     // redraw.
    444                     needRedraw |= queryContactInfo(req.number, req.countryIso, req.callLogInfo);
    445                 } else {
    446                     // Throttle redraw rate by only sending them when there are
    447                     // more requests.
    448                     if (needRedraw) {
    449                         needRedraw = false;
    450                         mHandler.sendEmptyMessage(REDRAW);
    451                     }
    452 
    453                     // Wait until another request is available, or until this
    454                     // thread is no longer needed (as indicated by being
    455                     // interrupted).
    456                     try {
    457                         synchronized (mRequests) {
    458                             mRequests.wait(1000);
    459                         }
    460                     } catch (InterruptedException ie) {
    461                         // Ignore, and attempt to continue processing requests.
    462                     }
    463                 }
    464             }
    465         }
    466     }
    467 
    468     @Override
    469     protected void addGroups(Cursor cursor) {
    470         mCallLogGroupBuilder.addGroups(cursor);
    471     }
    472 
    473     @Override
    474     protected View newStandAloneView(Context context, ViewGroup parent) {
    475         return newChildView(context, parent);
    476     }
    477 
    478     @Override
    479     protected View newGroupView(Context context, ViewGroup parent) {
    480         return newChildView(context, parent);
    481     }
    482 
    483     @Override
    484     protected View newChildView(Context context, ViewGroup parent) {
    485         LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    486         View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
    487         findAndCacheViews(view);
    488         return view;
    489     }
    490 
    491     @Override
    492     protected void bindStandAloneView(View view, Context context, Cursor cursor) {
    493         bindView(view, cursor, 1);
    494     }
    495 
    496     @Override
    497     protected void bindChildView(View view, Context context, Cursor cursor) {
    498         bindView(view, cursor, 1);
    499     }
    500 
    501     @Override
    502     protected void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
    503             boolean expanded) {
    504         bindView(view, cursor, groupSize);
    505     }
    506 
    507     private void findAndCacheViews(View view) {
    508         // Get the views to bind to.
    509         CallLogListItemViews views = CallLogListItemViews.fromView(view);
    510         views.primaryActionView.setOnClickListener(mActionListener);
    511         views.secondaryActionView.setOnClickListener(mActionListener);
    512         view.setTag(views);
    513     }
    514 
    515     /**
    516      * Binds the views in the entry to the data in the call log.
    517      *
    518      * @param view the view corresponding to this entry
    519      * @param c the cursor pointing to the entry in the call log
    520      * @param count the number of entries in the current item, greater than 1 if it is a group
    521      */
    522     private void bindView(View view, Cursor c, int count) {
    523         final CallLogListItemViews views = (CallLogListItemViews) view.getTag();
    524 
    525         // Default case: an item in the call log.
    526         views.primaryActionView.setVisibility(View.VISIBLE);
    527         views.listHeaderTextView.setVisibility(View.GONE);
    528 
    529         final String number = c.getString(CallLogQuery.NUMBER);
    530         final int numberPresentation = c.getInt(CallLogQuery.NUMBER_PRESENTATION);
    531         final long date = c.getLong(CallLogQuery.DATE);
    532         final long duration = c.getLong(CallLogQuery.DURATION);
    533         final int callType = c.getInt(CallLogQuery.CALL_TYPE);
    534         final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO);
    535 
    536         final ContactInfo cachedContactInfo = getContactInfoFromCallLog(c);
    537 
    538         if (!mUseCallAsPrimaryAction) {
    539             // Sets the primary action to open call detail page.
    540             views.primaryActionView.setTag(
    541                     IntentProvider.getCallDetailIntentProvider(
    542                             getCursor(), c.getPosition(), c.getLong(CallLogQuery.ID), count));
    543         } else if (PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation)) {
    544             // Sets the primary action to call the number.
    545             views.primaryActionView.setTag(IntentProvider.getReturnCallIntentProvider(number));
    546         } else {
    547             views.primaryActionView.setTag(null);
    548         }
    549 
    550         // Store away the voicemail information so we can play it directly.
    551         if (callType == Calls.VOICEMAIL_TYPE) {
    552             String voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI);
    553             final long rowId = c.getLong(CallLogQuery.ID);
    554             views.secondaryActionView.setTag(
    555                     IntentProvider.getPlayVoicemailIntentProvider(rowId, voicemailUri));
    556         } else if (!TextUtils.isEmpty(number)) {
    557             // Store away the number so we can call it directly if you click on the call icon.
    558             views.secondaryActionView.setTag(
    559                     IntentProvider.getReturnCallIntentProvider(number));
    560         } else {
    561             // No action enabled.
    562             views.secondaryActionView.setTag(null);
    563         }
    564 
    565         // Lookup contacts with this number
    566         NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
    567         ExpirableCache.CachedValue<ContactInfo> cachedInfo =
    568                 mContactInfoCache.getCachedValue(numberCountryIso);
    569         ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue();
    570         if (!PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation)
    571                 || new PhoneNumberUtilsWrapper().isVoicemailNumber(number)) {
    572             // If this is a number that cannot be dialed, there is no point in looking up a contact
    573             // for it.
    574             info = ContactInfo.EMPTY;
    575         } else if (cachedInfo == null) {
    576             mContactInfoCache.put(numberCountryIso, ContactInfo.EMPTY);
    577             // Use the cached contact info from the call log.
    578             info = cachedContactInfo;
    579             // The db request should happen on a non-UI thread.
    580             // Request the contact details immediately since they are currently missing.
    581             enqueueRequest(number, countryIso, cachedContactInfo, true);
    582             // We will format the phone number when we make the background request.
    583         } else {
    584             if (cachedInfo.isExpired()) {
    585                 // The contact info is no longer up to date, we should request it. However, we
    586                 // do not need to request them immediately.
    587                 enqueueRequest(number, countryIso, cachedContactInfo, false);
    588             } else  if (!callLogInfoMatches(cachedContactInfo, info)) {
    589                 // The call log information does not match the one we have, look it up again.
    590                 // We could simply update the call log directly, but that needs to be done in a
    591                 // background thread, so it is easier to simply request a new lookup, which will, as
    592                 // a side-effect, update the call log.
    593                 enqueueRequest(number, countryIso, cachedContactInfo, false);
    594             }
    595 
    596             if (info == ContactInfo.EMPTY) {
    597                 // Use the cached contact info from the call log.
    598                 info = cachedContactInfo;
    599             }
    600         }
    601 
    602         final Uri lookupUri = info.lookupUri;
    603         final String name = info.name;
    604         final int ntype = info.type;
    605         final String label = info.label;
    606         final long photoId = info.photoId;
    607         final Uri photoUri = info.photoUri;
    608         CharSequence formattedNumber = info.formattedNumber;
    609         final int[] callTypes = getCallTypes(c, count);
    610         final String geocode = c.getString(CallLogQuery.GEOCODED_LOCATION);
    611         final PhoneCallDetails details;
    612 
    613         if (TextUtils.isEmpty(name)) {
    614             details = new PhoneCallDetails(number, numberPresentation,
    615                     formattedNumber, countryIso, geocode, callTypes, date,
    616                     duration);
    617         } else {
    618             details = new PhoneCallDetails(number, numberPresentation,
    619                     formattedNumber, countryIso, geocode, callTypes, date,
    620                     duration, name, ntype, label, lookupUri, photoUri);
    621         }
    622 
    623         final boolean isNew = c.getInt(CallLogQuery.IS_READ) == 0;
    624         // New items also use the highlighted version of the text.
    625         final boolean isHighlighted = isNew;
    626         mCallLogViewsHelper.setPhoneCallDetails(views, details, isHighlighted,
    627                 mUseCallAsPrimaryAction);
    628 
    629         if (photoId == 0 && photoUri != null) {
    630             setPhoto(views, photoUri, lookupUri);
    631         } else {
    632             setPhoto(views, photoId, lookupUri);
    633         }
    634 
    635         views.quickContactView.setContentDescription(views.phoneCallDetailsViews.nameView.
    636                 getText());
    637 
    638         // Listen for the first draw
    639         if (mViewTreeObserver == null) {
    640             mViewTreeObserver = view.getViewTreeObserver();
    641             mViewTreeObserver.addOnPreDrawListener(this);
    642         }
    643 
    644         bindBadge(view, info, details, callType);
    645     }
    646 
    647     protected void bindBadge(View view, ContactInfo info, PhoneCallDetails details, int callType) {
    648 
    649         // Do not show badge in call log.
    650         if (!mIsCallLog) {
    651             final int numMissed = getNumMissedCalls(callType);
    652             final ViewStub stub = (ViewStub) view.findViewById(R.id.link_stub);
    653 
    654             if (shouldShowBadge(numMissed, info, details)) {
    655                 // Do not process if the data has not changed (optimization since bind view is
    656                 // called multiple times due to contact lookup).
    657                 if (numMissed == mNumMissedCallsShown) {
    658                     return;
    659                 }
    660 
    661                 // stub will be null if it was already inflated.
    662                 if (stub != null) {
    663                     final View inflated = stub.inflate();
    664                     inflated.setVisibility(View.VISIBLE);
    665                     mBadgeContainer = inflated.findViewById(R.id.badge_link_container);
    666                     mBadgeImageView = (ImageView) inflated.findViewById(R.id.badge_image);
    667                     mBadgeText = (TextView) inflated.findViewById(R.id.badge_text);
    668                 }
    669 
    670                 mBadgeContainer.setOnClickListener(getBadgeClickListener());
    671                 mBadgeImageView.setImageResource(getBadgeImageResId());
    672                 mBadgeText.setText(getBadgeText(numMissed));
    673 
    674                 mNumMissedCallsShown = numMissed;
    675             } else {
    676                 // Hide badge if it was previously shown.
    677                 if (stub == null) {
    678                     final View container = view.findViewById(R.id.badge_container);
    679                     if (container != null) {
    680                         container.setVisibility(View.GONE);
    681                     }
    682                 }
    683             }
    684         }
    685     }
    686 
    687     public void setMissedCalls(Cursor data) {
    688         final int missed;
    689         if (data == null) {
    690             missed = 0;
    691         } else {
    692             missed = data.getCount();
    693         }
    694         // Only need to update if the number of calls changed.
    695         if (missed != mNumMissedCalls) {
    696             mNumMissedCalls = missed;
    697             notifyDataSetChanged();
    698         }
    699     }
    700 
    701     protected View.OnClickListener getBadgeClickListener() {
    702         return new View.OnClickListener() {
    703             @Override
    704             public void onClick(View v) {
    705                 final Intent intent = new Intent(mContext, CallLogActivity.class);
    706                 mContext.startActivity(intent);
    707             }
    708         };
    709     }
    710 
    711     /**
    712      * Get the resource id for the image to be shown for the badge.
    713      */
    714     protected int getBadgeImageResId() {
    715         return R.drawable.ic_call_log_blue;
    716     }
    717 
    718     /**
    719      * Get the text to be shown for the badge.
    720      *
    721      * @param numMissed The number of missed calls.
    722      */
    723     protected String getBadgeText(int numMissed) {
    724         return mContext.getResources().getString(R.string.num_missed_calls, numMissed);
    725     }
    726 
    727     /**
    728      * Whether to show the badge.
    729      *
    730      * @param numMissedCalls The number of missed calls.
    731      * @param info The contact info.
    732      * @param details The call detail.
    733      * @return {@literal true} if badge should be shown.  {@literal false} otherwise.
    734      */
    735     protected boolean shouldShowBadge(int numMissedCalls, ContactInfo info,
    736             PhoneCallDetails details) {
    737         return numMissedCalls > 0;
    738     }
    739 
    740     private int getNumMissedCalls(int callType) {
    741         if (callType == Calls.MISSED_TYPE) {
    742             // Exclude the current missed call shown in the shortcut.
    743             return mNumMissedCalls - 1;
    744         }
    745         return mNumMissedCalls;
    746     }
    747 
    748     /** Checks whether the contact info from the call log matches the one from the contacts db. */
    749     private boolean callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info) {
    750         // The call log only contains a subset of the fields in the contacts db.
    751         // Only check those.
    752         return TextUtils.equals(callLogInfo.name, info.name)
    753                 && callLogInfo.type == info.type
    754                 && TextUtils.equals(callLogInfo.label, info.label);
    755     }
    756 
    757     /** Stores the updated contact info in the call log if it is different from the current one. */
    758     private void updateCallLogContactInfoCache(String number, String countryIso,
    759             ContactInfo updatedInfo, ContactInfo callLogInfo) {
    760         final ContentValues values = new ContentValues();
    761         boolean needsUpdate = false;
    762 
    763         if (callLogInfo != null) {
    764             if (!TextUtils.equals(updatedInfo.name, callLogInfo.name)) {
    765                 values.put(Calls.CACHED_NAME, updatedInfo.name);
    766                 needsUpdate = true;
    767             }
    768 
    769             if (updatedInfo.type != callLogInfo.type) {
    770                 values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
    771                 needsUpdate = true;
    772             }
    773 
    774             if (!TextUtils.equals(updatedInfo.label, callLogInfo.label)) {
    775                 values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
    776                 needsUpdate = true;
    777             }
    778             if (!UriUtils.areEqual(updatedInfo.lookupUri, callLogInfo.lookupUri)) {
    779                 values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
    780                 needsUpdate = true;
    781             }
    782             if (!TextUtils.equals(updatedInfo.normalizedNumber, callLogInfo.normalizedNumber)) {
    783                 values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
    784                 needsUpdate = true;
    785             }
    786             if (!TextUtils.equals(updatedInfo.number, callLogInfo.number)) {
    787                 values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
    788                 needsUpdate = true;
    789             }
    790             if (updatedInfo.photoId != callLogInfo.photoId) {
    791                 values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
    792                 needsUpdate = true;
    793             }
    794             if (!TextUtils.equals(updatedInfo.formattedNumber, callLogInfo.formattedNumber)) {
    795                 values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
    796                 needsUpdate = true;
    797             }
    798         } else {
    799             // No previous values, store all of them.
    800             values.put(Calls.CACHED_NAME, updatedInfo.name);
    801             values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
    802             values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
    803             values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
    804             values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
    805             values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
    806             values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
    807             values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
    808             needsUpdate = true;
    809         }
    810 
    811         if (!needsUpdate) return;
    812 
    813         if (countryIso == null) {
    814             mContext.getContentResolver().update(Calls.CONTENT_URI_WITH_VOICEMAIL, values,
    815                     Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " IS NULL",
    816                     new String[]{ number });
    817         } else {
    818             mContext.getContentResolver().update(Calls.CONTENT_URI_WITH_VOICEMAIL, values,
    819                     Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " = ?",
    820                     new String[]{ number, countryIso });
    821         }
    822     }
    823 
    824     /** Returns the contact information as stored in the call log. */
    825     private ContactInfo getContactInfoFromCallLog(Cursor c) {
    826         ContactInfo info = new ContactInfo();
    827         info.lookupUri = UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_LOOKUP_URI));
    828         info.name = c.getString(CallLogQuery.CACHED_NAME);
    829         info.type = c.getInt(CallLogQuery.CACHED_NUMBER_TYPE);
    830         info.label = c.getString(CallLogQuery.CACHED_NUMBER_LABEL);
    831         String matchedNumber = c.getString(CallLogQuery.CACHED_MATCHED_NUMBER);
    832         info.number = matchedNumber == null ? c.getString(CallLogQuery.NUMBER) : matchedNumber;
    833         info.normalizedNumber = c.getString(CallLogQuery.CACHED_NORMALIZED_NUMBER);
    834         info.photoId = c.getLong(CallLogQuery.CACHED_PHOTO_ID);
    835         info.photoUri = null;  // We do not cache the photo URI.
    836         info.formattedNumber = c.getString(CallLogQuery.CACHED_FORMATTED_NUMBER);
    837         return info;
    838     }
    839 
    840     /**
    841      * Returns the call types for the given number of items in the cursor.
    842      * <p>
    843      * It uses the next {@code count} rows in the cursor to extract the types.
    844      * <p>
    845      * It position in the cursor is unchanged by this function.
    846      */
    847     private int[] getCallTypes(Cursor cursor, int count) {
    848         int position = cursor.getPosition();
    849         int[] callTypes = new int[count];
    850         for (int index = 0; index < count; ++index) {
    851             callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE);
    852             cursor.moveToNext();
    853         }
    854         cursor.moveToPosition(position);
    855         return callTypes;
    856     }
    857 
    858     private void setPhoto(CallLogListItemViews views, long photoId, Uri contactUri) {
    859         views.quickContactView.assignContactUri(contactUri);
    860         mContactPhotoManager.loadThumbnail(views.quickContactView, photoId, false /* darkTheme */);
    861     }
    862 
    863     private void setPhoto(CallLogListItemViews views, Uri photoUri, Uri contactUri) {
    864         views.quickContactView.assignContactUri(contactUri);
    865         mContactPhotoManager.loadDirectoryPhoto(views.quickContactView, photoUri,
    866                 false /* darkTheme */);
    867     }
    868 
    869 
    870     /**
    871      * Sets whether processing of requests for contact details should be enabled.
    872      * <p>
    873      * This method should be called in tests to disable such processing of requests when not
    874      * needed.
    875      */
    876     @VisibleForTesting
    877     void disableRequestProcessingForTest() {
    878         mRequestProcessingDisabled = true;
    879     }
    880 
    881     @VisibleForTesting
    882     void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) {
    883         NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
    884         mContactInfoCache.put(numberCountryIso, contactInfo);
    885     }
    886 
    887     @Override
    888     public void addGroup(int cursorPosition, int size, boolean expanded) {
    889         super.addGroup(cursorPosition, size, expanded);
    890     }
    891 
    892     /*
    893      * Get the number from the Contacts, if available, since sometimes
    894      * the number provided by caller id may not be formatted properly
    895      * depending on the carrier (roaming) in use at the time of the
    896      * incoming call.
    897      * Logic : If the caller-id number starts with a "+", use it
    898      *         Else if the number in the contacts starts with a "+", use that one
    899      *         Else if the number in the contacts is longer, use that one
    900      */
    901     public String getBetterNumberFromContacts(String number, String countryIso) {
    902         String matchingNumber = null;
    903         // Look in the cache first. If it's not found then query the Phones db
    904         NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
    905         ContactInfo ci = mContactInfoCache.getPossiblyExpired(numberCountryIso);
    906         if (ci != null && ci != ContactInfo.EMPTY) {
    907             matchingNumber = ci.number;
    908         } else {
    909             try {
    910                 Cursor phonesCursor = mContext.getContentResolver().query(
    911                         Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number),
    912                         PhoneQuery._PROJECTION, null, null, null);
    913                 if (phonesCursor != null) {
    914                     if (phonesCursor.moveToFirst()) {
    915                         matchingNumber = phonesCursor.getString(PhoneQuery.MATCHED_NUMBER);
    916                     }
    917                     phonesCursor.close();
    918                 }
    919             } catch (Exception e) {
    920                 // Use the number from the call log
    921             }
    922         }
    923         if (!TextUtils.isEmpty(matchingNumber) &&
    924                 (matchingNumber.startsWith("+")
    925                         || matchingNumber.length() > number.length())) {
    926             number = matchingNumber;
    927         }
    928         return number;
    929     }
    930 }
    931