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 android.content.ContentValues;
20import android.content.Context;
21import android.content.res.Resources;
22import android.database.Cursor;
23import android.net.Uri;
24import android.os.Handler;
25import android.os.Message;
26import android.provider.CallLog.Calls;
27import android.provider.ContactsContract.PhoneLookup;
28import android.text.TextUtils;
29import android.view.LayoutInflater;
30import android.view.View;
31import android.view.ViewGroup;
32import android.view.ViewTreeObserver;
33
34import com.android.common.widget.GroupingListAdapter;
35import com.android.contacts.ContactPhotoManager;
36import com.android.contacts.PhoneCallDetails;
37import com.android.contacts.PhoneCallDetailsHelper;
38import com.android.contacts.R;
39import com.android.contacts.util.ExpirableCache;
40import com.android.contacts.util.UriUtils;
41import com.google.common.annotations.VisibleForTesting;
42import com.google.common.base.Objects;
43
44import 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 {
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
66        public NumberWithCountryIso(String number, String countryIso) {
67            this.number = number;
68            this.countryIso = countryIso;
69        }
70
71        @Override
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
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
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 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
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
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
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    /**
277     * Starts a background thread to process contact-lookup requests, unless one
278     * has already been started.
279     */
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     */
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     */
310    private void unregisterPreDrawListener() {
311        if (mViewTreeObserver != null && mViewTreeObserver.isAlive()) {
312            mViewTreeObserver.removeOnPreDrawListener(this);
313        }
314        mViewTreeObserver = null;
315    }
316
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
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     */
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
387        public QueryThread() {
388            super("CallLogAdapter.QueryThread");
389        }
390
391        public void stopProcessing() {
392            mDone = true;
393        }
394
395        @Override
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
439    protected void addGroups(Cursor cursor) {
440        mCallLogGroupBuilder.addGroups(cursor);
441    }
442
443    @Override
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
453    protected void bindStandAloneView(View view, Context context, Cursor cursor) {
454        bindView(view, cursor, 1);
455    }
456
457    @Override
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
467    protected void bindChildView(View view, Context context, Cursor cursor) {
468        bindView(view, cursor, 1);
469    }
470
471    @Override
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
481    protected void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
482            boolean expanded) {
483        bindView(view, cursor, groupSize);
484    }
485
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     */
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. */
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. */
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. */
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. */
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     */
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
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
749    void disableRequestProcessingForTest() {
750        mRequestProcessingDisabled = true;
751    }
752
753    @VisibleForTesting
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
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     */
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