• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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          */
onItemExpanded(CallLogListItemView view)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          */
getViewForCallId(long callId)89         public CallLogListItemView getViewForCallId(long callId);
90     }
91 
92     /** Interface used to initiate a refresh of the content. */
93     public interface CallFetcher {
fetchCalls()94         public void fetchCalls();
95     }
96 
97     /** Implements onClickListener for the report button. */
98     public interface OnReportButtonClickListener {
onReportButtonClick(String number)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 
NumberWithCountryIso(String number, String countryIso)112         public NumberWithCountryIso(String number, String countryIso) {
113             this.number = number;
114             this.countryIso = countryIso;
115         }
116 
117         @Override
equals(Object o)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
hashCode()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 
ContactInfoRequest(String number, String countryIso, ContactInfo callLogInfo)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
equals(Object obj)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
hashCode()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 
startActivityForAction(View view)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
onPreDraw()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 
CallLogAdapter(Context context, CallFetcher callFetcher, ContactInfoHelper contactInfoHelper, CallItemExpandedListener callItemExpandedListener, OnReportButtonClickListener onReportButtonClickListener, boolean isCallLog)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
onContentChanged()381     protected void onContentChanged() {
382         mCallFetcher.fetchCalls();
383     }
384 
setLoading(boolean loading)385     public void setLoading(boolean loading) {
386         mLoading = loading;
387     }
388 
389     @Override
isEmpty()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      */
startRequestProcessing()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      */
stopRequestProcessing()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      */
unregisterPreDrawListener()433     private void unregisterPreDrawListener() {
434         if (mViewTreeObserver != null && mViewTreeObserver.isAlive()) {
435             mViewTreeObserver.removeOnPreDrawListener(this);
436         }
437         mViewTreeObserver = null;
438     }
439 
invalidateCache()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      */
enqueueRequest(String number, String countryIso, ContactInfo callLogInfo, boolean immediate)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      */
queryContactInfo(String number, String countryIso, ContactInfo callLogInfo)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 
QueryThread()519         public QueryThread() {
520             super("CallLogAdapter.QueryThread");
521         }
522 
stopProcessing()523         public void stopProcessing() {
524             mDone = true;
525         }
526 
527         @Override
run()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
addGroups(Cursor cursor)571     protected void addGroups(Cursor cursor) {
572         mCallLogGroupBuilder.addGroups(cursor);
573     }
574 
575     @Override
newStandAloneView(Context context, ViewGroup parent)576     protected View newStandAloneView(Context context, ViewGroup parent) {
577         return newChildView(context, parent);
578     }
579 
580     @Override
newGroupView(Context context, ViewGroup parent)581     protected View newGroupView(Context context, ViewGroup parent) {
582         return newChildView(context, parent);
583     }
584 
585     @Override
newChildView(Context context, ViewGroup parent)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
bindStandAloneView(View view, Context context, Cursor cursor)603     protected void bindStandAloneView(View view, Context context, Cursor cursor) {
604         bindView(view, cursor, 1);
605     }
606 
607     @Override
bindChildView(View view, Context context, Cursor cursor)608     protected void bindChildView(View view, Context context, Cursor cursor) {
609         bindView(view, cursor, 1);
610     }
611 
612     @Override
bindGroupView(View view, Context context, Cursor cursor, int groupSize, boolean expanded)613     protected void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
614             boolean expanded) {
615         bindView(view, cursor, groupSize);
616     }
617 
findAndCacheViews(View view)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      */
bindView(View view, Cursor c, int count)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      */
getPreviousDayGroup(Cursor cursor)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      */
getDayGroupForCall(long callId)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      */
isExpanded(long rowId)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      */
toggleExpansion(long rowId)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      */
expandOrCollapseActions(CallLogListItemView callLogItem, boolean isExpanded)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 
expandVoicemailTranscriptionView(CallLogListItemViews views, boolean isExpanded)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      */
inflateActionViewStub(final View callLogItem)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      */
bindActionButtons(CallLogListItemViews views)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 
bindBadge( View view, ContactInfo info, final PhoneCallDetails details, int callType)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. */
callLogInfoMatches(ContactInfo callLogInfo, ContactInfo info)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. */
updateCallLogContactInfoCache(String number, String countryIso, ContactInfo updatedInfo, ContactInfo callLogInfo)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. */
getContactInfoFromCallLog(Cursor c)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      */
getCallTypes(Cursor cursor, int count)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      */
getCallFeatures(Cursor cursor, int count)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 
setPhoto(CallLogListItemViews views, long photoId, Uri contactUri, String displayName, String identifier, int contactType)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 
setPhoto(CallLogListItemViews views, Uri photoUri, Uri contactUri, String displayName, String identifier, int contactType)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
bindViewForTest(View view, Context context, Cursor cursor)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
disableRequestProcessingForTest()1248     void disableRequestProcessingForTest() {
1249         mRequestProcessingDisabled = true;
1250     }
1251 
1252     @VisibleForTesting
injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo)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
addGroup(int cursorPosition, int size, boolean expanded)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
setDayGroup(long rowId, int dayGroup)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
clearDayGroups()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      */
getBetterNumberFromContacts(String number, String countryIso)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      */
getCallIds(final Cursor cursor, final int groupSize)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      */
getGroupDescription(int group)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 
onBadDataReported(String number)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      */
handleRowExpanded(CallLogListItemView view, boolean animate, boolean forceExpand)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