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.dialer.calllog;
18
19import android.content.ContentValues;
20import android.content.Context;
21import android.content.Intent;
22import android.content.res.Resources;
23import android.database.Cursor;
24import android.graphics.drawable.Drawable;
25import android.net.Uri;
26import android.os.Handler;
27import android.os.Message;
28import android.provider.CallLog.Calls;
29import android.provider.ContactsContract.PhoneLookup;
30import android.telecom.PhoneAccountHandle;
31import android.text.TextUtils;
32import android.view.LayoutInflater;
33import android.view.View;
34import android.view.View.AccessibilityDelegate;
35import android.view.ViewGroup;
36import android.view.ViewStub;
37import android.view.ViewTreeObserver;
38import android.view.accessibility.AccessibilityEvent;
39import android.widget.ImageView;
40import android.widget.TextView;
41import android.widget.Toast;
42
43import com.android.common.widget.GroupingListAdapter;
44import com.android.contacts.common.CallUtil;
45import com.android.contacts.common.ContactPhotoManager;
46import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
47import com.android.contacts.common.util.UriUtils;
48import com.android.dialer.DialtactsActivity;
49import com.android.dialer.PhoneCallDetails;
50import com.android.dialer.PhoneCallDetailsHelper;
51import com.android.dialer.R;
52import com.android.dialer.util.DialerUtils;
53import com.android.dialer.util.ExpirableCache;
54
55import com.google.common.annotations.VisibleForTesting;
56import com.google.common.base.Objects;
57
58import java.util.HashMap;
59import java.util.LinkedList;
60
61/**
62 * Adapter class to fill in data for the Call Log.
63 */
64public class CallLogAdapter extends GroupingListAdapter
65        implements ViewTreeObserver.OnPreDrawListener, CallLogGroupBuilder.GroupCreator {
66
67    private static final int VOICEMAIL_TRANSCRIPTION_MAX_LINES = 10;
68
69    /** The enumeration of {@link android.os.AsyncTask} objects used in this class. */
70    public enum Tasks {
71        REMOVE_CALL_LOG_ENTRIES,
72    }
73
74    /** Interface used to inform a parent UI element that a list item has been expanded. */
75    public interface CallItemExpandedListener {
76        /**
77         * @param view The {@link CallLogListItemView} that represents the item that was clicked
78         *         on.
79         */
80        public void onItemExpanded(CallLogListItemView view);
81
82        /**
83         * Retrieves the call log view for the specified call Id.  If the view is not currently
84         * visible, returns null.
85         *
86         * @param callId The call Id.
87         * @return The call log view.
88         */
89        public CallLogListItemView getViewForCallId(long callId);
90    }
91
92    /** Interface used to initiate a refresh of the content. */
93    public interface CallFetcher {
94        public void fetchCalls();
95    }
96
97    /** Implements onClickListener for the report button. */
98    public interface OnReportButtonClickListener {
99        public void onReportButtonClick(String number);
100    }
101
102    /**
103     * Stores a phone number of a call with the country code where it originally occurred.
104     * <p>
105     * Note the country does not necessarily specifies the country of the phone number itself, but
106     * it is the country in which the user was in when the call was placed or received.
107     */
108    private static final class NumberWithCountryIso {
109        public final String number;
110        public final String countryIso;
111
112        public NumberWithCountryIso(String number, String countryIso) {
113            this.number = number;
114            this.countryIso = countryIso;
115        }
116
117        @Override
118        public boolean equals(Object o) {
119            if (o == null) return false;
120            if (!(o instanceof NumberWithCountryIso)) return false;
121            NumberWithCountryIso other = (NumberWithCountryIso) o;
122            return TextUtils.equals(number, other.number)
123                    && TextUtils.equals(countryIso, other.countryIso);
124        }
125
126        @Override
127        public int hashCode() {
128            return (number == null ? 0 : number.hashCode())
129                    ^ (countryIso == null ? 0 : countryIso.hashCode());
130        }
131    }
132
133    /** The time in millis to delay starting the thread processing requests. */
134    private static final int START_PROCESSING_REQUESTS_DELAY_MILLIS = 1000;
135
136    /** The size of the cache of contact info. */
137    private static final int CONTACT_INFO_CACHE_SIZE = 100;
138
139    /** Constant used to indicate no row is expanded. */
140    private static final long NONE_EXPANDED = -1;
141
142    protected final Context mContext;
143    private final ContactInfoHelper mContactInfoHelper;
144    private final CallFetcher mCallFetcher;
145    private final Toast mReportedToast;
146    private final OnReportButtonClickListener mOnReportButtonClickListener;
147    private ViewTreeObserver mViewTreeObserver = null;
148
149    /**
150     * A cache of the contact details for the phone numbers in the call log.
151     * <p>
152     * The content of the cache is expired (but not purged) whenever the application comes to
153     * the foreground.
154     * <p>
155     * The key is number with the country in which the call was placed or received.
156     */
157    private ExpirableCache<NumberWithCountryIso, ContactInfo> mContactInfoCache;
158
159    /**
160     * Tracks the call log row which was previously expanded.  Used so that the closure of a
161     * previously expanded call log entry can be animated on rebind.
162     */
163    private long mPreviouslyExpanded = NONE_EXPANDED;
164
165    /**
166     * Tracks the currently expanded call log row.
167     */
168    private long mCurrentlyExpanded = NONE_EXPANDED;
169
170    /**
171     *  Hashmap, keyed by call Id, used to track the day group for a call.  As call log entries are
172     *  put into the primary call groups in {@link com.android.dialer.calllog.CallLogGroupBuilder},
173     *  they are also assigned a secondary "day group".  This hashmap tracks the day group assigned
174     *  to all calls in the call log.  This information is used to trigger the display of a day
175     *  group header above the call log entry at the start of a day group.
176     *  Note: Multiple calls are grouped into a single primary "call group" in the call log, and
177     *  the cursor used to bind rows includes all of these calls.  When determining if a day group
178     *  change has occurred it is necessary to look at the last entry in the call log to determine
179     *  its day group.  This hashmap provides a means of determining the previous day group without
180     *  having to reverse the cursor to the start of the previous day call log entry.
181     */
182    private HashMap<Long,Integer> mDayGroups = new HashMap<Long, Integer>();
183
184    /**
185     * A request for contact details for the given number.
186     */
187    private static final class ContactInfoRequest {
188        /** The number to look-up. */
189        public final String number;
190        /** The country in which a call to or from this number was placed or received. */
191        public final String countryIso;
192        /** The cached contact information stored in the call log. */
193        public final ContactInfo callLogInfo;
194
195        public ContactInfoRequest(String number, String countryIso, ContactInfo callLogInfo) {
196            this.number = number;
197            this.countryIso = countryIso;
198            this.callLogInfo = callLogInfo;
199        }
200
201        @Override
202        public boolean equals(Object obj) {
203            if (this == obj) return true;
204            if (obj == null) return false;
205            if (!(obj instanceof ContactInfoRequest)) return false;
206
207            ContactInfoRequest other = (ContactInfoRequest) obj;
208
209            if (!TextUtils.equals(number, other.number)) return false;
210            if (!TextUtils.equals(countryIso, other.countryIso)) return false;
211            if (!Objects.equal(callLogInfo, other.callLogInfo)) return false;
212
213            return true;
214        }
215
216        @Override
217        public int hashCode() {
218            final int prime = 31;
219            int result = 1;
220            result = prime * result + ((callLogInfo == null) ? 0 : callLogInfo.hashCode());
221            result = prime * result + ((countryIso == null) ? 0 : countryIso.hashCode());
222            result = prime * result + ((number == null) ? 0 : number.hashCode());
223            return result;
224        }
225    }
226
227    /**
228     * List of requests to update contact details.
229     * <p>
230     * Each request is made of a phone number to look up, and the contact info currently stored in
231     * the call log for this number.
232     * <p>
233     * The requests are added when displaying the contacts and are processed by a background
234     * thread.
235     */
236    private final LinkedList<ContactInfoRequest> mRequests;
237
238    private boolean mLoading = true;
239    private static final int REDRAW = 1;
240    private static final int START_THREAD = 2;
241
242    private QueryThread mCallerIdThread;
243
244    /** Instance of helper class for managing views. */
245    private final CallLogListItemHelper mCallLogViewsHelper;
246
247    /** Helper to set up contact photos. */
248    private final ContactPhotoManager mContactPhotoManager;
249    /** Helper to parse and process phone numbers. */
250    private PhoneNumberDisplayHelper mPhoneNumberHelper;
251    /** Helper to group call log entries. */
252    private final CallLogGroupBuilder mCallLogGroupBuilder;
253
254    private CallItemExpandedListener mCallItemExpandedListener;
255
256    /** Can be set to true by tests to disable processing of requests. */
257    private volatile boolean mRequestProcessingDisabled = false;
258
259    private boolean mIsCallLog = true;
260
261    private View mBadgeContainer;
262    private ImageView mBadgeImageView;
263    private TextView mBadgeText;
264
265    private int mCallLogBackgroundColor;
266    private int mExpandedBackgroundColor;
267    private float mExpandedTranslationZ;
268
269    /** Listener for the primary or secondary actions in the list.
270     *  Primary opens the call details.
271     *  Secondary calls or plays.
272     **/
273    private final View.OnClickListener mActionListener = new View.OnClickListener() {
274        @Override
275        public void onClick(View view) {
276            startActivityForAction(view);
277        }
278    };
279
280    /**
281     * The onClickListener used to expand or collapse the action buttons section for a call log
282     * entry.
283     */
284    private final View.OnClickListener mExpandCollapseListener = new View.OnClickListener() {
285        @Override
286        public void onClick(View v) {
287            final CallLogListItemView callLogItem = (CallLogListItemView) v.getParent().getParent();
288            handleRowExpanded(callLogItem, true /* animate */, false /* forceExpand */);
289        }
290    };
291
292    private AccessibilityDelegate mAccessibilityDelegate = new AccessibilityDelegate() {
293        @Override
294        public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child,
295                AccessibilityEvent event) {
296            if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
297                handleRowExpanded((CallLogListItemView) host, false /* animate */,
298                        true /* forceExpand */);
299            }
300            return super.onRequestSendAccessibilityEvent(host, child, event);
301        }
302    };
303
304    private void startActivityForAction(View view) {
305        final IntentProvider intentProvider = (IntentProvider) view.getTag();
306        if (intentProvider != null) {
307            final Intent intent = intentProvider.getIntent(mContext);
308            // See IntentProvider.getCallDetailIntentProvider() for why this may be null.
309            if (intent != null) {
310                DialerUtils.startActivityWithErrorToast(mContext, intent);
311            }
312        }
313    }
314
315    @Override
316    public boolean onPreDraw() {
317        // We only wanted to listen for the first draw (and this is it).
318        unregisterPreDrawListener();
319
320        // Only schedule a thread-creation message if the thread hasn't been
321        // created yet. This is purely an optimization, to queue fewer messages.
322        if (mCallerIdThread == null) {
323            mHandler.sendEmptyMessageDelayed(START_THREAD, START_PROCESSING_REQUESTS_DELAY_MILLIS);
324        }
325
326        return true;
327    }
328
329    private Handler mHandler = new Handler() {
330        @Override
331        public void handleMessage(Message msg) {
332            switch (msg.what) {
333                case REDRAW:
334                    notifyDataSetChanged();
335                    break;
336                case START_THREAD:
337                    startRequestProcessing();
338                    break;
339            }
340        }
341    };
342
343    public CallLogAdapter(Context context, CallFetcher callFetcher,
344            ContactInfoHelper contactInfoHelper, CallItemExpandedListener callItemExpandedListener,
345            OnReportButtonClickListener onReportButtonClickListener, boolean isCallLog) {
346        super(context);
347
348        mContext = context;
349        mCallFetcher = callFetcher;
350        mContactInfoHelper = contactInfoHelper;
351        mIsCallLog = isCallLog;
352        mCallItemExpandedListener = callItemExpandedListener;
353
354        mOnReportButtonClickListener = onReportButtonClickListener;
355        mReportedToast = Toast.makeText(mContext, R.string.toast_caller_id_reported,
356                Toast.LENGTH_SHORT);
357
358        mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE);
359        mRequests = new LinkedList<ContactInfoRequest>();
360
361        Resources resources = mContext.getResources();
362        CallTypeHelper callTypeHelper = new CallTypeHelper(resources);
363        mCallLogBackgroundColor = resources.getColor(R.color.background_dialer_list_items);
364        mExpandedBackgroundColor = resources.getColor(R.color.call_log_expanded_background_color);
365        mExpandedTranslationZ = resources.getDimension(R.dimen.call_log_expanded_translation_z);
366
367        mContactPhotoManager = ContactPhotoManager.getInstance(mContext);
368        mPhoneNumberHelper = new PhoneNumberDisplayHelper(resources);
369        PhoneCallDetailsHelper phoneCallDetailsHelper = new PhoneCallDetailsHelper(
370                resources, callTypeHelper, new PhoneNumberUtilsWrapper());
371        mCallLogViewsHelper =
372                new CallLogListItemHelper(
373                        phoneCallDetailsHelper, mPhoneNumberHelper, resources);
374        mCallLogGroupBuilder = new CallLogGroupBuilder(this);
375    }
376
377    /**
378     * Requery on background thread when {@link Cursor} changes.
379     */
380    @Override
381    protected void onContentChanged() {
382        mCallFetcher.fetchCalls();
383    }
384
385    public void setLoading(boolean loading) {
386        mLoading = loading;
387    }
388
389    @Override
390    public boolean isEmpty() {
391        if (mLoading) {
392            // We don't want the empty state to show when loading.
393            return false;
394        } else {
395            return super.isEmpty();
396        }
397    }
398
399    /**
400     * Starts a background thread to process contact-lookup requests, unless one
401     * has already been started.
402     */
403    private synchronized void startRequestProcessing() {
404        // For unit-testing.
405        if (mRequestProcessingDisabled) return;
406
407        // Idempotence... if a thread is already started, don't start another.
408        if (mCallerIdThread != null) return;
409
410        mCallerIdThread = new QueryThread();
411        mCallerIdThread.setPriority(Thread.MIN_PRIORITY);
412        mCallerIdThread.start();
413    }
414
415    /**
416     * Stops the background thread that processes updates and cancels any
417     * pending requests to start it.
418     */
419    public synchronized void stopRequestProcessing() {
420        // Remove any pending requests to start the processing thread.
421        mHandler.removeMessages(START_THREAD);
422        if (mCallerIdThread != null) {
423            // Stop the thread; we are finished with it.
424            mCallerIdThread.stopProcessing();
425            mCallerIdThread.interrupt();
426            mCallerIdThread = null;
427        }
428    }
429
430    /**
431     * Stop receiving onPreDraw() notifications.
432     */
433    private void unregisterPreDrawListener() {
434        if (mViewTreeObserver != null && mViewTreeObserver.isAlive()) {
435            mViewTreeObserver.removeOnPreDrawListener(this);
436        }
437        mViewTreeObserver = null;
438    }
439
440    public void invalidateCache() {
441        mContactInfoCache.expireAll();
442
443        // Restart the request-processing thread after the next draw.
444        stopRequestProcessing();
445        unregisterPreDrawListener();
446    }
447
448    /**
449     * Enqueues a request to look up the contact details for the given phone number.
450     * <p>
451     * It also provides the current contact info stored in the call log for this number.
452     * <p>
453     * If the {@code immediate} parameter is true, it will start immediately the thread that looks
454     * up the contact information (if it has not been already started). Otherwise, it will be
455     * started with a delay. See {@link #START_PROCESSING_REQUESTS_DELAY_MILLIS}.
456     */
457    protected void enqueueRequest(String number, String countryIso, ContactInfo callLogInfo,
458            boolean immediate) {
459        ContactInfoRequest request = new ContactInfoRequest(number, countryIso, callLogInfo);
460        synchronized (mRequests) {
461            if (!mRequests.contains(request)) {
462                mRequests.add(request);
463                mRequests.notifyAll();
464            }
465        }
466        if (immediate) startRequestProcessing();
467    }
468
469    /**
470     * Queries the appropriate content provider for the contact associated with the number.
471     * <p>
472     * Upon completion it also updates the cache in the call log, if it is different from
473     * {@code callLogInfo}.
474     * <p>
475     * The number might be either a SIP address or a phone number.
476     * <p>
477     * It returns true if it updated the content of the cache and we should therefore tell the
478     * view to update its content.
479     */
480    private boolean queryContactInfo(String number, String countryIso, ContactInfo callLogInfo) {
481        final ContactInfo info = mContactInfoHelper.lookupNumber(number, countryIso);
482
483        if (info == null) {
484            // The lookup failed, just return without requesting to update the view.
485            return false;
486        }
487
488        // Check the existing entry in the cache: only if it has changed we should update the
489        // view.
490        NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
491        ContactInfo existingInfo = mContactInfoCache.getPossiblyExpired(numberCountryIso);
492
493        final boolean isRemoteSource = info.sourceType != 0;
494
495        // Don't force redraw if existing info in the cache is equal to {@link ContactInfo#EMPTY}
496        // to avoid updating the data set for every new row that is scrolled into view.
497        // see (https://googleplex-android-review.git.corp.google.com/#/c/166680/)
498
499        // Exception: Photo uris for contacts from remote sources are not cached in the call log
500        // cache, so we have to force a redraw for these contacts regardless.
501        boolean updated = (existingInfo != ContactInfo.EMPTY || isRemoteSource) &&
502                !info.equals(existingInfo);
503
504        // Store the data in the cache so that the UI thread can use to display it. Store it
505        // even if it has not changed so that it is marked as not expired.
506        mContactInfoCache.put(numberCountryIso, info);
507        // Update the call log even if the cache it is up-to-date: it is possible that the cache
508        // contains the value from a different call log entry.
509        updateCallLogContactInfoCache(number, countryIso, info, callLogInfo);
510        return updated;
511    }
512
513    /*
514     * Handles requests for contact name and number type.
515     */
516    private class QueryThread extends Thread {
517        private volatile boolean mDone = false;
518
519        public QueryThread() {
520            super("CallLogAdapter.QueryThread");
521        }
522
523        public void stopProcessing() {
524            mDone = true;
525        }
526
527        @Override
528        public void run() {
529            boolean needRedraw = false;
530            while (true) {
531                // Check if thread is finished, and if so return immediately.
532                if (mDone) return;
533
534                // Obtain next request, if any is available.
535                // Keep synchronized section small.
536                ContactInfoRequest req = null;
537                synchronized (mRequests) {
538                    if (!mRequests.isEmpty()) {
539                        req = mRequests.removeFirst();
540                    }
541                }
542
543                if (req != null) {
544                    // Process the request. If the lookup succeeds, schedule a
545                    // redraw.
546                    needRedraw |= queryContactInfo(req.number, req.countryIso, req.callLogInfo);
547                } else {
548                    // Throttle redraw rate by only sending them when there are
549                    // more requests.
550                    if (needRedraw) {
551                        needRedraw = false;
552                        mHandler.sendEmptyMessage(REDRAW);
553                    }
554
555                    // Wait until another request is available, or until this
556                    // thread is no longer needed (as indicated by being
557                    // interrupted).
558                    try {
559                        synchronized (mRequests) {
560                            mRequests.wait(1000);
561                        }
562                    } catch (InterruptedException ie) {
563                        // Ignore, and attempt to continue processing requests.
564                    }
565                }
566            }
567        }
568    }
569
570    @Override
571    protected void addGroups(Cursor cursor) {
572        mCallLogGroupBuilder.addGroups(cursor);
573    }
574
575    @Override
576    protected View newStandAloneView(Context context, ViewGroup parent) {
577        return newChildView(context, parent);
578    }
579
580    @Override
581    protected View newGroupView(Context context, ViewGroup parent) {
582        return newChildView(context, parent);
583    }
584
585    @Override
586    protected View newChildView(Context context, ViewGroup parent) {
587        LayoutInflater inflater = LayoutInflater.from(context);
588        CallLogListItemView view =
589                (CallLogListItemView) inflater.inflate(R.layout.call_log_list_item, parent, false);
590
591        // Get the views to bind to and cache them.
592        CallLogListItemViews views = CallLogListItemViews.fromView(view);
593        view.setTag(views);
594
595        // Set text height to false on the TextViews so they don't have extra padding.
596        views.phoneCallDetailsViews.nameView.setElegantTextHeight(false);
597        views.phoneCallDetailsViews.callLocationAndDate.setElegantTextHeight(false);
598
599        return view;
600    }
601
602    @Override
603    protected void bindStandAloneView(View view, Context context, Cursor cursor) {
604        bindView(view, cursor, 1);
605    }
606
607    @Override
608    protected void bindChildView(View view, Context context, Cursor cursor) {
609        bindView(view, cursor, 1);
610    }
611
612    @Override
613    protected void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
614            boolean expanded) {
615        bindView(view, cursor, groupSize);
616    }
617
618    private void findAndCacheViews(View view) {
619    }
620
621    /**
622     * Binds the views in the entry to the data in the call log.
623     *
624     * @param view the view corresponding to this entry
625     * @param c the cursor pointing to the entry in the call log
626     * @param count the number of entries in the current item, greater than 1 if it is a group
627     */
628    private void bindView(View view, Cursor c, int count) {
629        view.setAccessibilityDelegate(mAccessibilityDelegate);
630        final CallLogListItemView callLogItemView = (CallLogListItemView) view;
631        final CallLogListItemViews views = (CallLogListItemViews) view.getTag();
632
633        // Default case: an item in the call log.
634        views.primaryActionView.setVisibility(View.VISIBLE);
635
636        final String number = c.getString(CallLogQuery.NUMBER);
637        final int numberPresentation = c.getInt(CallLogQuery.NUMBER_PRESENTATION);
638        final long date = c.getLong(CallLogQuery.DATE);
639        final long duration = c.getLong(CallLogQuery.DURATION);
640        final int callType = c.getInt(CallLogQuery.CALL_TYPE);
641        final PhoneAccountHandle accountHandle = PhoneAccountUtils.getAccount(
642                c.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME),
643                c.getString(CallLogQuery.ACCOUNT_ID));
644        final Drawable accountIcon = PhoneAccountUtils.getAccountIcon(mContext,
645                accountHandle);
646        final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO);
647
648        final long rowId = c.getLong(CallLogQuery.ID);
649        views.rowId = rowId;
650
651        // For entries in the call log, check if the day group has changed and display a header
652        // if necessary.
653        if (mIsCallLog) {
654            int currentGroup = getDayGroupForCall(rowId);
655            int previousGroup = getPreviousDayGroup(c);
656            if (currentGroup != previousGroup) {
657                views.dayGroupHeader.setVisibility(View.VISIBLE);
658                views.dayGroupHeader.setText(getGroupDescription(currentGroup));
659            } else {
660                views.dayGroupHeader.setVisibility(View.GONE);
661            }
662        } else {
663            views.dayGroupHeader.setVisibility(View.GONE);
664        }
665
666        // Store some values used when the actions ViewStub is inflated on expansion of the actions
667        // section.
668        views.number = number;
669        views.numberPresentation = numberPresentation;
670        views.callType = callType;
671        // NOTE: This is currently not being used, but can be used in future versions.
672        views.accountHandle = accountHandle;
673        views.voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI);
674        // Stash away the Ids of the calls so that we can support deleting a row in the call log.
675        views.callIds = getCallIds(c, count);
676
677        final ContactInfo cachedContactInfo = getContactInfoFromCallLog(c);
678
679        final boolean isVoicemailNumber =
680                PhoneNumberUtilsWrapper.INSTANCE.isVoicemailNumber(number);
681
682        // Where binding and not in the call log, use default behaviour of invoking a call when
683        // tapping the primary view.
684        if (!mIsCallLog) {
685            views.primaryActionView.setOnClickListener(this.mActionListener);
686
687            // Set return call intent, otherwise null.
688            if (PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation)) {
689                // Sets the primary action to call the number.
690                views.primaryActionView.setTag(IntentProvider.getReturnCallIntentProvider(number));
691            } else {
692                // Number is not callable, so hide button.
693                views.primaryActionView.setTag(null);
694            }
695        } else {
696            // In the call log, expand/collapse an actions section for the call log entry when
697            // the primary view is tapped.
698            views.primaryActionView.setOnClickListener(this.mExpandCollapseListener);
699
700            // Note: Binding of the action buttons is done as required in configureActionViews
701            // when the user expands the actions ViewStub.
702        }
703
704        // Lookup contacts with this number
705        NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
706        ExpirableCache.CachedValue<ContactInfo> cachedInfo =
707                mContactInfoCache.getCachedValue(numberCountryIso);
708        ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue();
709        if (!PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation)
710                || isVoicemailNumber) {
711            // If this is a number that cannot be dialed, there is no point in looking up a contact
712            // for it.
713            info = ContactInfo.EMPTY;
714        } else if (cachedInfo == null) {
715            mContactInfoCache.put(numberCountryIso, ContactInfo.EMPTY);
716            // Use the cached contact info from the call log.
717            info = cachedContactInfo;
718            // The db request should happen on a non-UI thread.
719            // Request the contact details immediately since they are currently missing.
720            enqueueRequest(number, countryIso, cachedContactInfo, true);
721            // We will format the phone number when we make the background request.
722        } else {
723            if (cachedInfo.isExpired()) {
724                // The contact info is no longer up to date, we should request it. However, we
725                // do not need to request them immediately.
726                enqueueRequest(number, countryIso, cachedContactInfo, false);
727            } else  if (!callLogInfoMatches(cachedContactInfo, info)) {
728                // The call log information does not match the one we have, look it up again.
729                // We could simply update the call log directly, but that needs to be done in a
730                // background thread, so it is easier to simply request a new lookup, which will, as
731                // a side-effect, update the call log.
732                enqueueRequest(number, countryIso, cachedContactInfo, false);
733            }
734
735            if (info == ContactInfo.EMPTY) {
736                // Use the cached contact info from the call log.
737                info = cachedContactInfo;
738            }
739        }
740
741        final Uri lookupUri = info.lookupUri;
742        final String name = info.name;
743        final int ntype = info.type;
744        final String label = info.label;
745        final long photoId = info.photoId;
746        final Uri photoUri = info.photoUri;
747        CharSequence formattedNumber = info.formattedNumber;
748        final int[] callTypes = getCallTypes(c, count);
749        final String geocode = c.getString(CallLogQuery.GEOCODED_LOCATION);
750        final int sourceType = info.sourceType;
751        final int features = getCallFeatures(c, count);
752        final String transcription = c.getString(CallLogQuery.TRANSCRIPTION);
753        Long dataUsage = null;
754        if (!c.isNull(CallLogQuery.DATA_USAGE)) {
755            dataUsage = c.getLong(CallLogQuery.DATA_USAGE);
756        }
757
758        final PhoneCallDetails details;
759
760        views.reported = info.isBadData;
761
762        // The entry can only be reported as invalid if it has a valid ID and the source of the
763        // entry supports marking entries as invalid.
764        views.canBeReportedAsInvalid = mContactInfoHelper.canReportAsInvalid(info.sourceType,
765                info.objectId);
766
767        // Restore expansion state of the row on rebind.  Inflate the actions ViewStub if required,
768        // and set its visibility state accordingly.
769        expandOrCollapseActions(callLogItemView, isExpanded(rowId));
770
771        if (TextUtils.isEmpty(name)) {
772            details = new PhoneCallDetails(number, numberPresentation,
773                    formattedNumber, countryIso, geocode, callTypes, date,
774                    duration, null, accountIcon, features, dataUsage, transcription);
775        } else {
776            details = new PhoneCallDetails(number, numberPresentation,
777                    formattedNumber, countryIso, geocode, callTypes, date,
778                    duration, name, ntype, label, lookupUri, photoUri, sourceType,
779                    null, accountIcon, features, dataUsage, transcription);
780        }
781
782        mCallLogViewsHelper.setPhoneCallDetails(mContext, views, details);
783
784        int contactType = ContactPhotoManager.TYPE_DEFAULT;
785
786        if (isVoicemailNumber) {
787            contactType = ContactPhotoManager.TYPE_VOICEMAIL;
788        } else if (mContactInfoHelper.isBusiness(info.sourceType)) {
789            contactType = ContactPhotoManager.TYPE_BUSINESS;
790        }
791
792        String lookupKey = lookupUri == null ? null
793                : ContactInfoHelper.getLookupKeyFromUri(lookupUri);
794
795        String nameForDefaultImage = null;
796        if (TextUtils.isEmpty(name)) {
797            nameForDefaultImage = mPhoneNumberHelper.getDisplayNumber(details.number,
798                    details.numberPresentation, details.formattedNumber).toString();
799        } else {
800            nameForDefaultImage = name;
801        }
802
803        if (photoId == 0 && photoUri != null) {
804            setPhoto(views, photoUri, lookupUri, nameForDefaultImage, lookupKey, contactType);
805        } else {
806            setPhoto(views, photoId, lookupUri, nameForDefaultImage, lookupKey, contactType);
807        }
808
809        // Listen for the first draw
810        if (mViewTreeObserver == null) {
811            mViewTreeObserver = view.getViewTreeObserver();
812            mViewTreeObserver.addOnPreDrawListener(this);
813        }
814
815        bindBadge(view, info, details, callType);
816    }
817
818    /**
819     * Retrieves the day group of the previous call in the call log.  Used to determine if the day
820     * group has changed and to trigger display of the day group text.
821     *
822     * @param cursor The call log cursor.
823     * @return The previous day group, or DAY_GROUP_NONE if this is the first call.
824     */
825    private int getPreviousDayGroup(Cursor cursor) {
826        // We want to restore the position in the cursor at the end.
827        int startingPosition = cursor.getPosition();
828        int dayGroup = CallLogGroupBuilder.DAY_GROUP_NONE;
829        if (cursor.moveToPrevious()) {
830            long previousRowId = cursor.getLong(CallLogQuery.ID);
831            dayGroup = getDayGroupForCall(previousRowId);
832        }
833        cursor.moveToPosition(startingPosition);
834        return dayGroup;
835    }
836
837    /**
838     * Given a call Id, look up the day group that the call belongs to.  The day group data is
839     * populated in {@link com.android.dialer.calllog.CallLogGroupBuilder}.
840     *
841     * @param callId The call to retrieve the day group for.
842     * @return The day group for the call.
843     */
844    private int getDayGroupForCall(long callId) {
845        if (mDayGroups.containsKey(callId)) {
846            return mDayGroups.get(callId);
847        }
848        return CallLogGroupBuilder.DAY_GROUP_NONE;
849    }
850    /**
851     * Determines if a call log row with the given Id is expanded.
852     * @param rowId The row Id of the call.
853     * @return True if the row should be expanded.
854     */
855    private boolean isExpanded(long rowId) {
856        return mCurrentlyExpanded == rowId;
857    }
858
859    /**
860     * Toggles the expansion state tracked for the call log row identified by rowId and returns
861     * the new expansion state.  Assumes that only a single call log row will be expanded at any
862     * one point and tracks the current and previous expanded item.
863     *
864     * @param rowId The row Id associated with the call log row to expand/collapse.
865     * @return True where the row is now expanded, false otherwise.
866     */
867    private boolean toggleExpansion(long rowId) {
868        if (rowId == mCurrentlyExpanded) {
869            // Collapsing currently expanded row.
870            mPreviouslyExpanded = NONE_EXPANDED;
871            mCurrentlyExpanded = NONE_EXPANDED;
872
873            return false;
874        } else {
875            // Expanding a row (collapsing current expanded one).
876
877            mPreviouslyExpanded = mCurrentlyExpanded;
878            mCurrentlyExpanded = rowId;
879            return true;
880        }
881    }
882
883    /**
884     * Expands or collapses the view containing the CALLBACK, VOICEMAIL and DETAILS action buttons.
885     *
886     * @param callLogItem The call log entry parent view.
887     * @param isExpanded The new expansion state of the view.
888     */
889    private void expandOrCollapseActions(CallLogListItemView callLogItem, boolean isExpanded) {
890        final CallLogListItemViews views = (CallLogListItemViews)callLogItem.getTag();
891
892        expandVoicemailTranscriptionView(views, isExpanded);
893        if (isExpanded) {
894            // Inflate the view stub if necessary, and wire up the event handlers.
895            inflateActionViewStub(callLogItem);
896
897            views.actionsView.setVisibility(View.VISIBLE);
898            views.actionsView.setAlpha(1.0f);
899            views.callLogEntryView.setBackgroundColor(mExpandedBackgroundColor);
900            views.callLogEntryView.setTranslationZ(mExpandedTranslationZ);
901            callLogItem.setTranslationZ(mExpandedTranslationZ); // WAR
902        } else {
903            // When recycling a view, it is possible the actionsView ViewStub was previously
904            // inflated so we should hide it in this case.
905            if (views.actionsView != null) {
906                views.actionsView.setVisibility(View.GONE);
907            }
908
909            views.callLogEntryView.setBackgroundColor(mCallLogBackgroundColor);
910            views.callLogEntryView.setTranslationZ(0);
911            callLogItem.setTranslationZ(0); // WAR
912        }
913    }
914
915    public static void expandVoicemailTranscriptionView(CallLogListItemViews views,
916            boolean isExpanded) {
917        if (views.callType != Calls.VOICEMAIL_TYPE) {
918            return;
919        }
920
921        final TextView view = views.phoneCallDetailsViews.voicemailTranscriptionView;
922        if (TextUtils.isEmpty(view.getText())) {
923            return;
924        }
925        view.setMaxLines(isExpanded ? VOICEMAIL_TRANSCRIPTION_MAX_LINES : 1);
926        view.setSingleLine(!isExpanded);
927    }
928
929    /**
930     * Configures the action buttons in the expandable actions ViewStub.  The ViewStub is not
931     * inflated during initial binding, so click handlers, tags and accessibility text must be set
932     * here, if necessary.
933     *
934     * @param callLogItem The call log list item view.
935     */
936    private void inflateActionViewStub(final View callLogItem) {
937        final CallLogListItemViews views = (CallLogListItemViews)callLogItem.getTag();
938
939        ViewStub stub = (ViewStub)callLogItem.findViewById(R.id.call_log_entry_actions_stub);
940        if (stub != null) {
941            views.actionsView = (ViewGroup) stub.inflate();
942        }
943
944        if (views.callBackButtonView == null) {
945            views.callBackButtonView = (TextView)views.actionsView.findViewById(
946                    R.id.call_back_action);
947        }
948
949        if (views.videoCallButtonView == null) {
950            views.videoCallButtonView = (TextView)views.actionsView.findViewById(
951                    R.id.video_call_action);
952        }
953
954        if (views.voicemailButtonView == null) {
955            views.voicemailButtonView = (TextView)views.actionsView.findViewById(
956                    R.id.voicemail_action);
957        }
958
959        if (views.detailsButtonView == null) {
960            views.detailsButtonView = (TextView)views.actionsView.findViewById(R.id.details_action);
961        }
962
963        if (views.reportButtonView == null) {
964            views.reportButtonView = (TextView)views.actionsView.findViewById(R.id.report_action);
965            views.reportButtonView.setOnClickListener(new View.OnClickListener() {
966                @Override
967                public void onClick(View v) {
968                    if (mOnReportButtonClickListener != null) {
969                        mOnReportButtonClickListener.onReportButtonClick(views.number);
970                    }
971                }
972            });
973        }
974
975        bindActionButtons(views);
976    }
977
978    /***
979     * Binds click handlers and intents to the voicemail, details and callback action buttons.
980     *
981     * @param views  The call log item views.
982     */
983    private void bindActionButtons(CallLogListItemViews views) {
984        boolean canPlaceCallToNumber =
985                PhoneNumberUtilsWrapper.canPlaceCallsTo(views.number, views.numberPresentation);
986        // Set return call intent, otherwise null.
987        if (canPlaceCallToNumber) {
988            // Sets the primary action to call the number.
989            views.callBackButtonView.setTag(
990                    IntentProvider.getReturnCallIntentProvider(views.number));
991            views.callBackButtonView.setVisibility(View.VISIBLE);
992            views.callBackButtonView.setOnClickListener(mActionListener);
993        } else {
994            // Number is not callable, so hide button.
995            views.callBackButtonView.setTag(null);
996            views.callBackButtonView.setVisibility(View.GONE);
997        }
998
999        // If one of the calls had video capabilities, show the video call button.
1000        if (CallUtil.isVideoEnabled(mContext) && canPlaceCallToNumber &&
1001                views.phoneCallDetailsViews.callTypeIcons.isVideoShown()) {
1002            views.videoCallButtonView.setTag(
1003                    IntentProvider.getReturnVideoCallIntentProvider(views.number));
1004            views.videoCallButtonView.setVisibility(View.VISIBLE);
1005            views.videoCallButtonView.setOnClickListener(mActionListener);
1006        } else {
1007            views.videoCallButtonView.setTag(null);
1008            views.videoCallButtonView.setVisibility(View.GONE);
1009        }
1010
1011        // For voicemail calls, show the "VOICEMAIL" action button; hide otherwise.
1012        if (views.callType == Calls.VOICEMAIL_TYPE) {
1013            views.voicemailButtonView.setOnClickListener(mActionListener);
1014            views.voicemailButtonView.setTag(
1015                    IntentProvider.getPlayVoicemailIntentProvider(
1016                            views.rowId, views.voicemailUri));
1017            views.voicemailButtonView.setVisibility(View.VISIBLE);
1018
1019            views.detailsButtonView.setVisibility(View.GONE);
1020        } else {
1021            views.voicemailButtonView.setTag(null);
1022            views.voicemailButtonView.setVisibility(View.GONE);
1023
1024            views.detailsButtonView.setOnClickListener(mActionListener);
1025            views.detailsButtonView.setTag(
1026                    IntentProvider.getCallDetailIntentProvider(
1027                            views.rowId, views.callIds, null)
1028            );
1029
1030            if (views.canBeReportedAsInvalid && !views.reported) {
1031                views.reportButtonView.setVisibility(View.VISIBLE);
1032            } else {
1033                views.reportButtonView.setVisibility(View.GONE);
1034            }
1035        }
1036
1037        mCallLogViewsHelper.setActionContentDescriptions(views);
1038    }
1039
1040    protected void bindBadge(
1041            View view, ContactInfo info, final PhoneCallDetails details, int callType) {
1042        // Do not show badge in call log.
1043        if (!mIsCallLog) {
1044            final ViewStub stub = (ViewStub) view.findViewById(R.id.link_stub);
1045            if (UriUtils.isEncodedContactUri(info.lookupUri)) {
1046                if (stub != null) {
1047                    final View inflated = stub.inflate();
1048                    inflated.setVisibility(View.VISIBLE);
1049                    mBadgeContainer = inflated.findViewById(R.id.badge_link_container);
1050                    mBadgeImageView = (ImageView) inflated.findViewById(R.id.badge_image);
1051                    mBadgeText = (TextView) inflated.findViewById(R.id.badge_text);
1052                }
1053
1054                mBadgeContainer.setOnClickListener(new View.OnClickListener() {
1055                    @Override
1056                    public void onClick(View v) {
1057                        final Intent intent =
1058                                DialtactsActivity.getAddNumberToContactIntent(details.number);
1059                        mContext.startActivity(intent);
1060                    }
1061                });
1062                mBadgeImageView.setImageResource(R.drawable.ic_person_add_24dp);
1063                mBadgeText.setText(R.string.recentCalls_addToContact);
1064            } else {
1065                // Hide badge if it was previously shown.
1066                if (stub == null) {
1067                    final View container = view.findViewById(R.id.badge_container);
1068                    if (container != null) {
1069                        container.setVisibility(View.GONE);
1070                    }
1071                }
1072            }
1073        }
1074    }
1075
1076    /** Checks whether the contact info from the call log matches the one from the contacts db. */
1077    private boolean callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info) {
1078        // The call log only contains a subset of the fields in the contacts db.
1079        // Only check those.
1080        return TextUtils.equals(callLogInfo.name, info.name)
1081                && callLogInfo.type == info.type
1082                && TextUtils.equals(callLogInfo.label, info.label);
1083    }
1084
1085    /** Stores the updated contact info in the call log if it is different from the current one. */
1086    private void updateCallLogContactInfoCache(String number, String countryIso,
1087            ContactInfo updatedInfo, ContactInfo callLogInfo) {
1088        final ContentValues values = new ContentValues();
1089        boolean needsUpdate = false;
1090
1091        if (callLogInfo != null) {
1092            if (!TextUtils.equals(updatedInfo.name, callLogInfo.name)) {
1093                values.put(Calls.CACHED_NAME, updatedInfo.name);
1094                needsUpdate = true;
1095            }
1096
1097            if (updatedInfo.type != callLogInfo.type) {
1098                values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
1099                needsUpdate = true;
1100            }
1101
1102            if (!TextUtils.equals(updatedInfo.label, callLogInfo.label)) {
1103                values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
1104                needsUpdate = true;
1105            }
1106            if (!UriUtils.areEqual(updatedInfo.lookupUri, callLogInfo.lookupUri)) {
1107                values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
1108                needsUpdate = true;
1109            }
1110            // Only replace the normalized number if the new updated normalized number isn't empty.
1111            if (!TextUtils.isEmpty(updatedInfo.normalizedNumber) &&
1112                    !TextUtils.equals(updatedInfo.normalizedNumber, callLogInfo.normalizedNumber)) {
1113                values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
1114                needsUpdate = true;
1115            }
1116            if (!TextUtils.equals(updatedInfo.number, callLogInfo.number)) {
1117                values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
1118                needsUpdate = true;
1119            }
1120            if (updatedInfo.photoId != callLogInfo.photoId) {
1121                values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
1122                needsUpdate = true;
1123            }
1124            if (!TextUtils.equals(updatedInfo.formattedNumber, callLogInfo.formattedNumber)) {
1125                values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
1126                needsUpdate = true;
1127            }
1128        } else {
1129            // No previous values, store all of them.
1130            values.put(Calls.CACHED_NAME, updatedInfo.name);
1131            values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.type);
1132            values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.label);
1133            values.put(Calls.CACHED_LOOKUP_URI, UriUtils.uriToString(updatedInfo.lookupUri));
1134            values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.number);
1135            values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.normalizedNumber);
1136            values.put(Calls.CACHED_PHOTO_ID, updatedInfo.photoId);
1137            values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.formattedNumber);
1138            needsUpdate = true;
1139        }
1140
1141        if (!needsUpdate) return;
1142
1143        if (countryIso == null) {
1144            mContext.getContentResolver().update(Calls.CONTENT_URI_WITH_VOICEMAIL, values,
1145                    Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " IS NULL",
1146                    new String[]{ number });
1147        } else {
1148            mContext.getContentResolver().update(Calls.CONTENT_URI_WITH_VOICEMAIL, values,
1149                    Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " = ?",
1150                    new String[]{ number, countryIso });
1151        }
1152    }
1153
1154    /** Returns the contact information as stored in the call log. */
1155    private ContactInfo getContactInfoFromCallLog(Cursor c) {
1156        ContactInfo info = new ContactInfo();
1157        info.lookupUri = UriUtils.parseUriOrNull(c.getString(CallLogQuery.CACHED_LOOKUP_URI));
1158        info.name = c.getString(CallLogQuery.CACHED_NAME);
1159        info.type = c.getInt(CallLogQuery.CACHED_NUMBER_TYPE);
1160        info.label = c.getString(CallLogQuery.CACHED_NUMBER_LABEL);
1161        String matchedNumber = c.getString(CallLogQuery.CACHED_MATCHED_NUMBER);
1162        info.number = matchedNumber == null ? c.getString(CallLogQuery.NUMBER) : matchedNumber;
1163        info.normalizedNumber = c.getString(CallLogQuery.CACHED_NORMALIZED_NUMBER);
1164        info.photoId = c.getLong(CallLogQuery.CACHED_PHOTO_ID);
1165        info.photoUri = null;  // We do not cache the photo URI.
1166        info.formattedNumber = c.getString(CallLogQuery.CACHED_FORMATTED_NUMBER);
1167        return info;
1168    }
1169
1170    /**
1171     * Returns the call types for the given number of items in the cursor.
1172     * <p>
1173     * It uses the next {@code count} rows in the cursor to extract the types.
1174     * <p>
1175     * It position in the cursor is unchanged by this function.
1176     */
1177    private int[] getCallTypes(Cursor cursor, int count) {
1178        int position = cursor.getPosition();
1179        int[] callTypes = new int[count];
1180        for (int index = 0; index < count; ++index) {
1181            callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE);
1182            cursor.moveToNext();
1183        }
1184        cursor.moveToPosition(position);
1185        return callTypes;
1186    }
1187
1188    /**
1189     * Determine the features which were enabled for any of the calls that make up a call log
1190     * entry.
1191     *
1192     * @param cursor The cursor.
1193     * @param count The number of calls for the current call log entry.
1194     * @return The features.
1195     */
1196    private int getCallFeatures(Cursor cursor, int count) {
1197        int features = 0;
1198        int position = cursor.getPosition();
1199        for (int index = 0; index < count; ++index) {
1200            features |= cursor.getInt(CallLogQuery.FEATURES);
1201            cursor.moveToNext();
1202        }
1203        cursor.moveToPosition(position);
1204        return features;
1205    }
1206
1207    private void setPhoto(CallLogListItemViews views, long photoId, Uri contactUri,
1208            String displayName, String identifier, int contactType) {
1209        views.quickContactView.assignContactUri(contactUri);
1210        views.quickContactView.setOverlay(null);
1211        DefaultImageRequest request = new DefaultImageRequest(displayName, identifier,
1212                contactType, true /* isCircular */);
1213        mContactPhotoManager.loadThumbnail(views.quickContactView, photoId, false /* darkTheme */,
1214                true /* isCircular */, request);
1215    }
1216
1217    private void setPhoto(CallLogListItemViews views, Uri photoUri, Uri contactUri,
1218            String displayName, String identifier, int contactType) {
1219        views.quickContactView.assignContactUri(contactUri);
1220        views.quickContactView.setOverlay(null);
1221        DefaultImageRequest request = new DefaultImageRequest(displayName, identifier,
1222                contactType, true /* isCircular */);
1223        mContactPhotoManager.loadDirectoryPhoto(views.quickContactView, photoUri,
1224                false /* darkTheme */, true /* isCircular */, request);
1225    }
1226
1227    /**
1228     * Bind a call log entry view for testing purposes.  Also inflates the action view stub so
1229     * unit tests can access the buttons contained within.
1230     *
1231     * @param view The current call log row.
1232     * @param context The current context.
1233     * @param cursor The cursor to bind from.
1234     */
1235    @VisibleForTesting
1236    void bindViewForTest(View view, Context context, Cursor cursor) {
1237        bindStandAloneView(view, context, cursor);
1238        inflateActionViewStub(view);
1239    }
1240
1241    /**
1242     * Sets whether processing of requests for contact details should be enabled.
1243     * <p>
1244     * This method should be called in tests to disable such processing of requests when not
1245     * needed.
1246     */
1247    @VisibleForTesting
1248    void disableRequestProcessingForTest() {
1249        mRequestProcessingDisabled = true;
1250    }
1251
1252    @VisibleForTesting
1253    void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) {
1254        NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
1255        mContactInfoCache.put(numberCountryIso, contactInfo);
1256    }
1257
1258    @Override
1259    public void addGroup(int cursorPosition, int size, boolean expanded) {
1260        super.addGroup(cursorPosition, size, expanded);
1261    }
1262
1263    /**
1264     * Stores the day group associated with a call in the call log.
1265     *
1266     * @param rowId The row Id of the current call.
1267     * @param dayGroup The day group the call belongs in.
1268     */
1269    @Override
1270    public void setDayGroup(long rowId, int dayGroup) {
1271        if (!mDayGroups.containsKey(rowId)) {
1272            mDayGroups.put(rowId, dayGroup);
1273        }
1274    }
1275
1276    /**
1277     * Clears the day group associations on re-bind of the call log.
1278     */
1279    @Override
1280    public void clearDayGroups() {
1281        mDayGroups.clear();
1282    }
1283
1284    /*
1285     * Get the number from the Contacts, if available, since sometimes
1286     * the number provided by caller id may not be formatted properly
1287     * depending on the carrier (roaming) in use at the time of the
1288     * incoming call.
1289     * Logic : If the caller-id number starts with a "+", use it
1290     *         Else if the number in the contacts starts with a "+", use that one
1291     *         Else if the number in the contacts is longer, use that one
1292     */
1293    public String getBetterNumberFromContacts(String number, String countryIso) {
1294        String matchingNumber = null;
1295        // Look in the cache first. If it's not found then query the Phones db
1296        NumberWithCountryIso numberCountryIso = new NumberWithCountryIso(number, countryIso);
1297        ContactInfo ci = mContactInfoCache.getPossiblyExpired(numberCountryIso);
1298        if (ci != null && ci != ContactInfo.EMPTY) {
1299            matchingNumber = ci.number;
1300        } else {
1301            try {
1302                Cursor phonesCursor = mContext.getContentResolver().query(
1303                        Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number),
1304                        PhoneQuery._PROJECTION, null, null, null);
1305                if (phonesCursor != null) {
1306                    try {
1307                        if (phonesCursor.moveToFirst()) {
1308                            matchingNumber = phonesCursor.getString(PhoneQuery.MATCHED_NUMBER);
1309                        }
1310                    } finally {
1311                        phonesCursor.close();
1312                    }
1313                }
1314            } catch (Exception e) {
1315                // Use the number from the call log
1316            }
1317        }
1318        if (!TextUtils.isEmpty(matchingNumber) &&
1319                (matchingNumber.startsWith("+")
1320                        || matchingNumber.length() > number.length())) {
1321            number = matchingNumber;
1322        }
1323        return number;
1324    }
1325
1326    /**
1327     * Retrieves the call Ids represented by the current call log row.
1328     *
1329     * @param cursor Call log cursor to retrieve call Ids from.
1330     * @param groupSize Number of calls associated with the current call log row.
1331     * @return Array of call Ids.
1332     */
1333    private long[] getCallIds(final Cursor cursor, final int groupSize) {
1334        // We want to restore the position in the cursor at the end.
1335        int startingPosition = cursor.getPosition();
1336        long[] ids = new long[groupSize];
1337        // Copy the ids of the rows in the group.
1338        for (int index = 0; index < groupSize; ++index) {
1339            ids[index] = cursor.getLong(CallLogQuery.ID);
1340            cursor.moveToNext();
1341        }
1342        cursor.moveToPosition(startingPosition);
1343        return ids;
1344    }
1345
1346    /**
1347     * Determines the description for a day group.
1348     *
1349     * @param group The day group to retrieve the description for.
1350     * @return The day group description.
1351     */
1352    private CharSequence getGroupDescription(int group) {
1353       if (group == CallLogGroupBuilder.DAY_GROUP_TODAY) {
1354           return mContext.getResources().getString(R.string.call_log_header_today);
1355       } else if (group == CallLogGroupBuilder.DAY_GROUP_YESTERDAY) {
1356           return mContext.getResources().getString(R.string.call_log_header_yesterday);
1357       } else {
1358           return mContext.getResources().getString(R.string.call_log_header_other);
1359       }
1360    }
1361
1362    public void onBadDataReported(String number) {
1363        mContactInfoCache.expireAll();
1364        mReportedToast.show();
1365    }
1366
1367    /**
1368     * Manages the state changes for the UI interaction where a call log row is expanded.
1369     *
1370     * @param view The view that was tapped
1371     * @param animate Whether or not to animate the expansion/collapse
1372     * @param forceExpand Whether or not to force the call log row into an expanded state regardless
1373     *        of its previous state
1374     */
1375    private void handleRowExpanded(CallLogListItemView view, boolean animate, boolean forceExpand) {
1376        final CallLogListItemViews views = (CallLogListItemViews) view.getTag();
1377
1378        if (forceExpand && isExpanded(views.rowId)) {
1379            return;
1380        }
1381
1382        // Hide or show the actions view.
1383        boolean expanded = toggleExpansion(views.rowId);
1384
1385        // Trigger loading of the viewstub and visual expand or collapse.
1386        expandOrCollapseActions(view, expanded);
1387
1388        // Animate the expansion or collapse.
1389        if (mCallItemExpandedListener != null) {
1390            if (animate) {
1391                mCallItemExpandedListener.onItemExpanded(view);
1392            }
1393
1394            // Animate the collapse of the previous item if it is still visible on screen.
1395            if (mPreviouslyExpanded != NONE_EXPANDED) {
1396                CallLogListItemView previousItem = mCallItemExpandedListener.getViewForCallId(
1397                        mPreviouslyExpanded);
1398
1399                if (previousItem != null) {
1400                    expandOrCollapseActions(previousItem, false);
1401                    if (animate) {
1402                        mCallItemExpandedListener.onItemExpanded(previousItem);
1403                    }
1404                }
1405                mPreviouslyExpanded = NONE_EXPANDED;
1406            }
1407        }
1408    }
1409}
1410