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