• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
fetchCalls()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 
NumberWithCountryIso(String number, String countryIso)74         public NumberWithCountryIso(String number, String countryIso) {
75             this.number = number;
76             this.countryIso = countryIso;
77         }
78 
79         @Override
equals(Object o)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
hashCode()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 
ContactInfoRequest(String number, String countryIso, ContactInfo callLogInfo)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
equals(Object obj)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
hashCode()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 
startActivityForAction(View view)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
onPreDraw()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 
CallLogAdapter(Context context, CallFetcher callFetcher, ContactInfoHelper contactInfoHelper, boolean showSecondaryActionButton, boolean isCallLog)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
onContentChanged()284     protected void onContentChanged() {
285         mCallFetcher.fetchCalls();
286     }
287 
setLoading(boolean loading)288     public void setLoading(boolean loading) {
289         mLoading = loading;
290     }
291 
292     @Override
isEmpty()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      */
startRequestProcessing()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      */
stopRequestProcessing()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      */
unregisterPreDrawListener()336     private void unregisterPreDrawListener() {
337         if (mViewTreeObserver != null && mViewTreeObserver.isAlive()) {
338             mViewTreeObserver.removeOnPreDrawListener(this);
339         }
340         mViewTreeObserver = null;
341     }
342 
invalidateCache()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      */
enqueueRequest(String number, String countryIso, ContactInfo callLogInfo, boolean immediate)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      */
queryContactInfo(String number, String countryIso, ContactInfo callLogInfo)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 
QueryThread()422         public QueryThread() {
423             super("CallLogAdapter.QueryThread");
424         }
425 
stopProcessing()426         public void stopProcessing() {
427             mDone = true;
428         }
429 
430         @Override
run()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
addGroups(Cursor cursor)474     protected void addGroups(Cursor cursor) {
475         mCallLogGroupBuilder.addGroups(cursor);
476     }
477 
478     @Override
newStandAloneView(Context context, ViewGroup parent)479     protected View newStandAloneView(Context context, ViewGroup parent) {
480         return newChildView(context, parent);
481     }
482 
483     @Override
newGroupView(Context context, ViewGroup parent)484     protected View newGroupView(Context context, ViewGroup parent) {
485         return newChildView(context, parent);
486     }
487 
488     @Override
newChildView(Context context, ViewGroup parent)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
bindStandAloneView(View view, Context context, Cursor cursor)497     protected void bindStandAloneView(View view, Context context, Cursor cursor) {
498         bindView(view, cursor, 1);
499     }
500 
501     @Override
bindChildView(View view, Context context, Cursor cursor)502     protected void bindChildView(View view, Context context, Cursor cursor) {
503         bindView(view, cursor, 1);
504     }
505 
506     @Override
bindGroupView(View view, Context context, Cursor cursor, int groupSize, boolean expanded)507     protected void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
508             boolean expanded) {
509         bindView(view, cursor, groupSize);
510     }
511 
findAndCacheViews(View view)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      */
bindView(View view, Cursor c, int count)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 
bindBadge(View view, ContactInfo info, PhoneCallDetails details, int callType)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 
setMissedCalls(Cursor data)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 
getBadgeClickListener()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