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