Home | History | Annotate | Download | only in calllog
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.dialer.calllog;
     18 
     19 import android.content.ContentValues;
     20 import android.content.Context;
     21 import android.content.Intent;
     22 import android.content.res.Resources;
     23 import android.database.Cursor;
     24 import android.graphics.drawable.Drawable;
     25 import android.net.Uri;
     26 import android.os.Handler;
     27 import android.os.Message;
     28 import android.provider.CallLog.Calls;
     29 import android.provider.ContactsContract.PhoneLookup;
     30 import android.telecom.PhoneAccountHandle;
     31 import android.text.TextUtils;
     32 import android.view.LayoutInflater;
     33 import android.view.View;
     34 import android.view.View.AccessibilityDelegate;
     35 import android.view.ViewGroup;
     36 import android.view.ViewStub;
     37 import android.view.ViewTreeObserver;
     38 import android.view.accessibility.AccessibilityEvent;
     39 import android.widget.ImageView;
     40 import android.widget.TextView;
     41 import android.widget.Toast;
     42 
     43 import com.android.common.widget.GroupingListAdapter;
     44 import com.android.contacts.common.CallUtil;
     45 import com.android.contacts.common.ContactPhotoManager;
     46 import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
     47 import com.android.contacts.common.util.UriUtils;
     48 import com.android.dialer.DialtactsActivity;
     49 import com.android.dialer.PhoneCallDetails;
     50 import com.android.dialer.PhoneCallDetailsHelper;
     51 import com.android.dialer.R;
     52 import com.android.dialer.util.DialerUtils;
     53 import com.android.dialer.util.ExpirableCache;
     54 
     55 import com.google.common.annotations.VisibleForTesting;
     56 import com.google.common.base.Objects;
     57 
     58 import java.util.HashMap;
     59 import java.util.LinkedList;
     60 
     61 /**
     62  * Adapter class to fill in data for the Call Log.
     63  */
     64 public 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