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         boolean updated = !info.equals(existingInfo);
    392 
    393         // Store the data in the cache so that the UI thread can use to display it. Store it
    394         // even if it has not changed so that it is marked as not expired.
    395         mContactInfoCache.put(numberCountryIso, info);
    396         // Update the call log even if the cache it is up-to-date: it is possible that the cache
    397         // contains the value from a different call log entry.
    398         updateCallLogContactInfoCache(number, countryIso, info, callLogInfo);
    399         return updated;
    400     }
    401 
    402     /*
    403      * Handles requests for contact name and number type.
    404      */
    405     private class QueryThread extends Thread {
    406         private volatile boolean mDone = false;
    407 
    408         public QueryThread() {
    409             super("CallLogAdapter.QueryThread");
    410         }
    411 
    412         public void stopProcessing() {
    413             mDone = true;
    414         }
    415 
    416         @Override
    417         public void run() {
    418             boolean needRedraw = false;
    419             while (true) {
    420                 // Check if thread is finished, and if so return immediately.
    421                 if (mDone) return;
    422 
    423                 // Obtain next request, if any is available.
    424                 // Keep synchronized section small.
    425                 ContactInfoRequest req = null;
    426                 synchronized (mRequests) {
    427                     if (!mRequests.isEmpty()) {
    428                         req = mRequests.removeFirst();
    429                     }
    430                 }
    431 
    432                 if (req != null) {
    433                     // Process the request. If the lookup succeeds, schedule a
    434                     // redraw.
    435                     needRedraw |= queryContactInfo(req.number, req.countryIso, req.callLogInfo);
    436                 } else {
    437                     // Throttle redraw rate by only sending them when there are
    438                     // more requests.
    439                     if (needRedraw) {
    440                         needRedraw = false;
    441                         mHandler.sendEmptyMessage(REDRAW);
    442                     }
    443 
    444                     // Wait until another request is available, or until this
    445                     // thread is no longer needed (as indicated by being
    446                     // interrupted).
    447                     try {
    448                         synchronized (mRequests) {
    449                             mRequests.wait(1000);
    450                         }
    451                     } catch (InterruptedException ie) {
    452                         // Ignore, and attempt to continue processing requests.
    453                     }
    454                 }
    455             }
    456         }
    457     }
    458 
    459     @Override
    460     protected void addGroups(Cursor cursor) {
    461         mCallLogGroupBuilder.addGroups(cursor);
    462     }
    463 
    464     @Override
    465     protected View newStandAloneView(Context context, ViewGroup parent) {
    466         return newChildView(context, parent);
    467     }
    468 
    469     @Override
    470     protected View newGroupView(Context context, ViewGroup parent) {
    471         return newChildView(context, parent);
    472     }
    473 
    474     @Override
    475     protected View newChildView(Context context, ViewGroup parent) {
    476         LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    477         View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
    478         findAndCacheViews(view);
    479         return view;
    480     }
    481 
    482     @Override
    483     protected void bindStandAloneView(View view, Context context, Cursor cursor) {
    484         bindView(view, cursor, 1);
    485     }
    486 
    487     @Override
    488     protected void bindChildView(View view, Context context, Cursor cursor) {
    489         bindView(view, cursor, 1);
    490     }
    491 
    492     @Override
    493     protected void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
    494             boolean expanded) {
    495         bindView(view, cursor, groupSize);
    496     }
    497 
    498     private void findAndCacheViews(View view) {
    499         // Get the views to bind to.
    500         CallLogListItemViews views = CallLogListItemViews.fromView(view);
    501         views.primaryActionView.setOnClickListener(mActionListener);
    502         views.secondaryActionView.setOnClickListener(mActionListener);
    503         view.setTag(views);
    504     }
    505 
    506     /**
    507      * Binds the views in the entry to the data in the call log.
    508      *
    509      * @param view the view corresponding to this entry
    510      * @param c the cursor pointing to the entry in the call log
    511      * @param count the number of entries in the current item, greater than 1 if it is a group
    512      */
    513     private void bindView(View view, Cursor c, int count) {
    514         final CallLogListItemViews views = (CallLogListItemViews) view.getTag();
    515 
    516         // Default case: an item in the call log.
    517         views.primaryActionView.setVisibility(View.VISIBLE);
    518         views.listHeaderTextView.setVisibility(View.GONE);
    519 
    520         final String number = c.getString(CallLogQuery.NUMBER);
    521         final int numberPresentation = c.getInt(CallLogQuery.NUMBER_PRESENTATION);
    522         final long date = c.getLong(CallLogQuery.DATE);
    523         final long duration = c.getLong(CallLogQuery.DURATION);
    524         final int callType = c.getInt(CallLogQuery.CALL_TYPE);
    525         final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO);
    526 
    527         final ContactInfo cachedContactInfo = getContactInfoFromCallLog(c);
    528 
    529         if (!mUseCallAsPrimaryAction) {
    530             // Sets the primary action to open call detail page.
    531             views.primaryActionView.setTag(
    532                     IntentProvider.getCallDetailIntentProvider(
    533                             getCursor(), c.getPosition(), c.getLong(CallLogQuery.ID), count));
    534         } else if (PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation)) {
    535             // Sets the primary action to call the number.
    536             views.primaryActionView.setTag(IntentProvider.getReturnCallIntentProvider(number));
    537         } else {
    538             views.primaryActionView.setTag(null);
    539         }
    540 
    541         // Store away the voicemail information so we can play it directly.
    542         if (callType == Calls.VOICEMAIL_TYPE) {
    543             String voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI);
    544             final long rowId = c.getLong(CallLogQuery.ID);
    545             views.secondaryActionView.setTag(
    546                     IntentProvider.getPlayVoicemailIntentProvider(rowId, voicemailUri));
    547         } else if (!TextUtils.isEmpty(number)) {
    548             // Store away the number so we can call it directly if you click on the call icon.
    549             views.secondaryActionView.setTag(
    550                     IntentProvider.getReturnCallIntentProvider(number));
    551         } else {
    552             // No action enabled.
    553             views.secondaryActionView.setTag(null);
    554         }
    555 
    556         // Lookup contacts with this number
    557         NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
    558         ExpirableCache.CachedValue<ContactInfo> cachedInfo =
    559                 mContactInfoCache.getCachedValue(numberCountryIso);
    560         ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue();
    561         if (!PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation)
    562                 || new PhoneNumberUtilsWrapper().isVoicemailNumber(number)) {
    563             // If this is a number that cannot be dialed, there is no point in looking up a contact
    564             // for it.
    565             info = ContactInfo.EMPTY;
    566         } else if (cachedInfo == null) {
    567             mContactInfoCache.put(numberCountryIso, ContactInfo.EMPTY);
    568             // Use the cached contact info from the call log.
    569             info = cachedContactInfo;
    570             // The db request should happen on a non-UI thread.
    571             // Request the contact details immediately since they are currently missing.
    572             enqueueRequest(number, countryIso, cachedContactInfo, true);
    573             // We will format the phone number when we make the background request.
    574         } else {
    575             if (cachedInfo.isExpired()) {
    576                 // The contact info is no longer up to date, we should request it. However, we
    577                 // do not need to request them immediately.
    578                 enqueueRequest(number, countryIso, cachedContactInfo, false);
    579             } else  if (!callLogInfoMatches(cachedContactInfo, info)) {
    580                 // The call log information does not match the one we have, look it up again.
    581                 // We could simply update the call log directly, but that needs to be done in a
    582                 // background thread, so it is easier to simply request a new lookup, which will, as
    583                 // a side-effect, update the call log.
    584                 enqueueRequest(number, countryIso, cachedContactInfo, false);
    585             }
    586 
    587             if (info == ContactInfo.EMPTY) {
    588                 // Use the cached contact info from the call log.
    589                 info = cachedContactInfo;
    590             }
    591         }
    592 
    593         final Uri lookupUri = info.lookupUri;
    594         final String name = info.name;
    595         final int ntype = info.type;
    596         final String label = info.label;
    597         final long photoId = info.photoId;
    598         final Uri photoUri = info.photoUri;
    599         CharSequence formattedNumber = info.formattedNumber;
    600         final int[] callTypes = getCallTypes(c, count);
    601         final String geocode = c.getString(CallLogQuery.GEOCODED_LOCATION);
    602         final PhoneCallDetails details;
    603 
    604         if (TextUtils.isEmpty(name)) {
    605             details = new PhoneCallDetails(number, numberPresentation,
    606                     formattedNumber, countryIso, geocode, callTypes, date,
    607                     duration);
    608         } else {
    609             details = new PhoneCallDetails(number, numberPresentation,
    610                     formattedNumber, countryIso, geocode, callTypes, date,
    611                     duration, name, ntype, label, lookupUri, photoUri);
    612         }
    613 
    614         final boolean isNew = c.getInt(CallLogQuery.IS_READ) == 0;
    615         // New items also use the highlighted version of the text.
    616         final boolean isHighlighted = isNew;
    617         mCallLogViewsHelper.setPhoneCallDetails(views, details, isHighlighted,
    618                 mUseCallAsPrimaryAction);
    619 
    620         if (photoId == 0 && photoUri != null) {
    621             setPhoto(views, photoUri, lookupUri);
    622         } else {
    623             setPhoto(views, photoId, lookupUri);
    624         }
    625 
    626         views.quickContactView.setContentDescription(views.phoneCallDetailsViews.nameView.
    627                 getText());
    628 
    629         // Listen for the first draw
    630         if (mViewTreeObserver == null) {
    631             mViewTreeObserver = view.getViewTreeObserver();
    632             mViewTreeObserver.addOnPreDrawListener(this);
    633         }
    634 
    635         bindBadge(view, info, details, callType);
    636     }
    637 
    638     protected void bindBadge(View view, ContactInfo info, PhoneCallDetails details, int callType) {
    639 
    640         // Do not show badge in call log.
    641         if (!mIsCallLog) {
    642             final int numMissed = getNumMissedCalls(callType);
    643             final ViewStub stub = (ViewStub) view.findViewById(R.id.link_stub);
    644 
    645             if (shouldShowBadge(numMissed, info, details)) {
    646                 // Do not process if the data has not changed (optimization since bind view is
    647                 // called multiple times due to contact lookup).
    648                 if (numMissed == mNumMissedCallsShown) {
    649                     return;
    650                 }
    651 
    652                 // stub will be null if it was already inflated.
    653                 if (stub != null) {
    654                     final View inflated = stub.inflate();
    655                     inflated.setVisibility(View.VISIBLE);
    656                     mBadgeContainer = inflated.findViewById(R.id.badge_link_container);
    657                     mBadgeImageView = (ImageView) inflated.findViewById(R.id.badge_image);
    658                     mBadgeText = (TextView) inflated.findViewById(R.id.badge_text);
    659                 }
    660 
    661                 mBadgeContainer.setOnClickListener(getBadgeClickListener());
    662                 mBadgeImageView.setImageResource(getBadgeImageResId());
    663                 mBadgeText.setText(getBadgeText(numMissed));
    664 
    665                 mNumMissedCallsShown = numMissed;
    666             } else {
    667                 // Hide badge if it was previously shown.
    668                 if (stub == null) {
    669                     final View container = view.findViewById(R.id.badge_container);
    670                     if (container != null) {
    671                         container.setVisibility(View.GONE);
    672                     }
    673                 }
    674             }
    675         }
    676     }
    677 
    678     public void setMissedCalls(Cursor data) {
    679         final int missed;
    680         if (data == null) {
    681             missed = 0;
    682         } else {
    683             missed = data.getCount();
    684         }
    685         // Only need to update if the number of calls changed.
    686         if (missed != mNumMissedCalls) {
    687             mNumMissedCalls = missed;
    688             notifyDataSetChanged();
    689         }
    690     }
    691 
    692     protected View.OnClickListener getBadgeClickListener() {
    693         return new View.OnClickListener() {
    694             @Override
    695             public void onClick(View v) {
    696                 final Intent intent = new Intent(mContext, CallLogActivity.class);
    697                 mContext.startActivity(intent);
    698             }
    699         };
    700     }
    701 
    702     /**
    703      * Get the resource id for the image to be shown for the badge.
    704      */
    705     protected int getBadgeImageResId() {
    706         return R.drawable.ic_call_log_blue;
    707     }
    708 
    709     /**
    710      * Get the text to be shown for the badge.
    711      *
    712      * @param numMissed The number of missed calls.
    713      */
    714     protected String getBadgeText(int numMissed) {
    715         return mContext.getResources().getString(R.string.num_missed_calls, numMissed);
    716     }
    717 
    718     /**
    719      * Whether to show the badge.
    720      *
    721      * @param numMissedCalls The number of missed calls.
    722      * @param info The contact info.
    723      * @param details The call detail.
    724      * @return {@literal true} if badge should be shown.  {@literal false} otherwise.
    725      */
    726     protected boolean shouldShowBadge(int numMissedCalls, ContactInfo info,
    727             PhoneCallDetails details) {
    728         return numMissedCalls > 0;
    729     }
    730 
    731     private int getNumMissedCalls(int callType) {
    732         if (callType == Calls.MISSED_TYPE) {
    733             // Exclude the current missed call shown in the shortcut.
    734             return mNumMissedCalls - 1;
    735         }
    736         return mNumMissedCalls;
    737     }
    738 
    739     /** Checks whether the contact info from the call log matches the one from the contacts db. */
    740     private boolean callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info) {
    741         // The call log only contains a subset of the fields in the contacts db.
    742         // Only check those.
    743         return TextUtils.equals(callLogInfo.name, info.name)
    744                 && callLogInfo.type == info.type
    745                 && TextUtils.equals(callLogInfo.label, info.label);
    746     }
    747 
    748     /** Stores the updated contact info in the call log if it is different from the current one. */
    749     private void updateCallLogContactInfoCache(String number, String countryIso,
    750             ContactInfo updatedInfo, ContactInfo callLogInfo) {
    751         final ContentValues values = new ContentValues();
    752         boolean needsUpdate = false;
    753 
    754         if (callLogInfo != null) {
    755             if (!TextUtils.equals(updatedInfo.name, callLogInfo.name)) {
    756                 values.put(Calls.CACHED_NAME, updatedInfo.name);
    757                 needsUpdate = true;
    758             }
    759 
    760             if (updatedInfo.type != callLogInfo.type) {
    761                 values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
    762                 needsUpdate = true;
    763             }
    764 
    765             if (!TextUtils.equals(updatedInfo.label, callLogInfo.label)) {
    766                 values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
    767                 needsUpdate = true;
    768             }
    769             if (!UriUtils.areEqual(updatedInfo.lookupUri, callLogInfo.lookupUri)) {
    770                 values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
    771                 needsUpdate = true;
    772             }
    773             if (!TextUtils.equals(updatedInfo.normalizedNumber, callLogInfo.normalizedNumber)) {
    774                 values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
    775                 needsUpdate = true;
    776             }
    777             if (!TextUtils.equals(updatedInfo.number, callLogInfo.number)) {
    778                 values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
    779                 needsUpdate = true;
    780             }
    781             if (updatedInfo.photoId != callLogInfo.photoId) {
    782                 values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
    783                 needsUpdate = true;
    784             }
    785             if (!TextUtils.equals(updatedInfo.formattedNumber, callLogInfo.formattedNumber)) {
    786                 values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
    787                 needsUpdate = true;
    788             }
    789         } else {
    790             // No previous values, store all of them.
    791             values.put(Calls.CACHED_NAME, updatedInfo.name);
    792             values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
    793             values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
    794             values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
    795             values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
    796             values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
    797             values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
    798             values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
    799             needsUpdate = true;
    800         }
    801 
    802         if (!needsUpdate) return;
    803 
    804         if (countryIso == null) {
    805             mContext.getContentResolver().update(Calls.CONTENT_URI_WITH_VOICEMAIL, values,
    806                     Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " IS NULL",
    807                     new String[]{ number });
    808         } else {
    809             mContext.getContentResolver().update(Calls.CONTENT_URI_WITH_VOICEMAIL, values,
    810                     Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " = ?",
    811                     new String[]{ number, countryIso });
    812         }
    813     }
    814 
    815     /** Returns the contact information as stored in the call log. */
    816     private ContactInfo getContactInfoFromCallLog(Cursor c) {
    817         ContactInfo info = new ContactInfo();
    818         info.lookupUri = UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_LOOKUP_URI));
    819         info.name = c.getString(CallLogQuery.CACHED_NAME);
    820         info.type = c.getInt(CallLogQuery.CACHED_NUMBER_TYPE);
    821         info.label = c.getString(CallLogQuery.CACHED_NUMBER_LABEL);
    822         String matchedNumber = c.getString(CallLogQuery.CACHED_MATCHED_NUMBER);
    823         info.number = matchedNumber == null ? c.getString(CallLogQuery.NUMBER) : matchedNumber;
    824         info.normalizedNumber = c.getString(CallLogQuery.CACHED_NORMALIZED_NUMBER);
    825         info.photoId = c.getLong(CallLogQuery.CACHED_PHOTO_ID);
    826         info.photoUri = null;  // We do not cache the photo URI.
    827         info.formattedNumber = c.getString(CallLogQuery.CACHED_FORMATTED_NUMBER);
    828         return info;
    829     }
    830 
    831     /**
    832      * Returns the call types for the given number of items in the cursor.
    833      * <p>
    834      * It uses the next {@code count} rows in the cursor to extract the types.
    835      * <p>
    836      * It position in the cursor is unchanged by this function.
    837      */
    838     private int[] getCallTypes(Cursor cursor, int count) {
    839         int position = cursor.getPosition();
    840         int[] callTypes = new int[count];
    841         for (int index = 0; index < count; ++index) {
    842             callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE);
    843             cursor.moveToNext();
    844         }
    845         cursor.moveToPosition(position);
    846         return callTypes;
    847     }
    848 
    849     private void setPhoto(CallLogListItemViews views, long photoId, Uri contactUri) {
    850         views.quickContactView.assignContactUri(contactUri);
    851         mContactPhotoManager.loadThumbnail(views.quickContactView, photoId, false /* darkTheme */);
    852     }
    853 
    854     private void setPhoto(CallLogListItemViews views, Uri photoUri, Uri contactUri) {
    855         views.quickContactView.assignContactUri(contactUri);
    856         mContactPhotoManager.loadDirectoryPhoto(views.quickContactView, photoUri,
    857                 false /* darkTheme */);
    858     }
    859 
    860 
    861     /**
    862      * Sets whether processing of requests for contact details should be enabled.
    863      * <p>
    864      * This method should be called in tests to disable such processing of requests when not
    865      * needed.
    866      */
    867     @VisibleForTesting
    868     void disableRequestProcessingForTest() {
    869         mRequestProcessingDisabled = true;
    870     }
    871 
    872     @VisibleForTesting
    873     void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) {
    874         NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
    875         mContactInfoCache.put(numberCountryIso, contactInfo);
    876     }
    877 
    878     @Override
    879     public void addGroup(int cursorPosition, int size, boolean expanded) {
    880         super.addGroup(cursorPosition, size, expanded);
    881     }
    882 
    883     /*
    884      * Get the number from the Contacts, if available, since sometimes
    885      * the number provided by caller id may not be formatted properly
    886      * depending on the carrier (roaming) in use at the time of the
    887      * incoming call.
    888      * Logic : If the caller-id number starts with a "+", use it
    889      *         Else if the number in the contacts starts with a "+", use that one
    890      *         Else if the number in the contacts is longer, use that one
    891      */
    892     public String getBetterNumberFromContacts(String number, String countryIso) {
    893         String matchingNumber = null;
    894         // Look in the cache first. If it's not found then query the Phones db
    895         NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
    896         ContactInfo ci = mContactInfoCache.getPossiblyExpired(numberCountryIso);
    897         if (ci != null && ci != ContactInfo.EMPTY) {
    898             matchingNumber = ci.number;
    899         } else {
    900             try {
    901                 Cursor phonesCursor = mContext.getContentResolver().query(
    902                         Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number),
    903                         PhoneQuery._PROJECTION, null, null, null);
    904                 if (phonesCursor != null) {
    905                     if (phonesCursor.moveToFirst()) {
    906                         matchingNumber = phonesCursor.getString(PhoneQuery.MATCHED_NUMBER);
    907                     }
    908                     phonesCursor.close();
    909                 }
    910             } catch (Exception e) {
    911                 // Use the number from the call log
    912             }
    913         }
    914         if (!TextUtils.isEmpty(matchingNumber) &&
    915                 (matchingNumber.startsWith("+")
    916                         || matchingNumber.length() > number.length())) {
    917             number = matchingNumber;
    918         }
    919         return number;
    920     }
    921 }
    922