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