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