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