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