CallLogFragment.java revision c147e8826c0bb87b43d270e490117292d41b08ef
1/*
2 * Copyright (C) 2011 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.contacts.calllog;
18
19import com.android.common.widget.GroupingListAdapter;
20import com.android.contacts.CallDetailActivity;
21import com.android.contacts.ContactPhotoManager;
22import com.android.contacts.ContactsUtils;
23import com.android.contacts.PhoneCallDetails;
24import com.android.contacts.PhoneCallDetailsHelper;
25import com.android.contacts.R;
26import com.android.contacts.activities.DialtactsActivity;
27import com.android.contacts.activities.DialtactsActivity.ViewPagerVisibilityListener;
28import com.android.contacts.util.ExpirableCache;
29import com.android.contacts.voicemail.VoicemailStatusHelper;
30import com.android.contacts.voicemail.VoicemailStatusHelperImpl;
31import com.android.contacts.voicemail.VoicemailStatusHelper.StatusMessage;
32import com.android.internal.telephony.CallerInfo;
33import com.google.common.annotations.VisibleForTesting;
34
35import android.app.ListFragment;
36import android.content.ContentUris;
37import android.content.Context;
38import android.content.Intent;
39import android.content.res.Resources;
40import android.database.Cursor;
41import android.graphics.drawable.Drawable;
42import android.net.Uri;
43import android.os.Bundle;
44import android.os.Handler;
45import android.os.Message;
46import android.provider.CallLog.Calls;
47import android.provider.ContactsContract.CommonDataKinds.SipAddress;
48import android.provider.ContactsContract.Contacts;
49import android.provider.ContactsContract.Data;
50import android.provider.ContactsContract.PhoneLookup;
51import android.telephony.PhoneNumberUtils;
52import android.telephony.TelephonyManager;
53import android.text.TextUtils;
54import android.util.Log;
55import android.view.LayoutInflater;
56import android.view.Menu;
57import android.view.MenuInflater;
58import android.view.MenuItem;
59import android.view.View;
60import android.view.ViewGroup;
61import android.view.ViewTreeObserver;
62import android.widget.ListView;
63import android.widget.QuickContactBadge;
64import android.widget.TextView;
65
66import java.util.LinkedList;
67import java.util.List;
68
69/**
70 * Displays a list of call log entries.
71 */
72public class CallLogFragment extends ListFragment implements ViewPagerVisibilityListener {
73    private static final String TAG = "CallLogFragment";
74
75    /** The size of the cache of contact info. */
76    private static final int CONTACT_INFO_CACHE_SIZE = 100;
77
78    /** The query for the call log table. */
79    public static final class CallLogQuery {
80        // If you alter this, you must also alter the method that inserts a fake row to the headers
81        // in the CallLogQueryHandler class called createHeaderCursorFor().
82        public static final String[] _PROJECTION = new String[] {
83                Calls._ID,
84                Calls.NUMBER,
85                Calls.DATE,
86                Calls.DURATION,
87                Calls.TYPE,
88                Calls.COUNTRY_ISO,
89                Calls.VOICEMAIL_URI,
90        };
91
92        public static final int ID = 0;
93        public static final int NUMBER = 1;
94        public static final int DATE = 2;
95        public static final int DURATION = 3;
96        public static final int CALL_TYPE = 4;
97        public static final int COUNTRY_ISO = 5;
98        public static final int VOICEMAIL_URI = 6;
99
100        /**
101         * The name of the synthetic "section" column.
102         * <p>
103         * This column identifies whether a row is a header or an actual item, and whether it is
104         * part of the new or old calls.
105         */
106        public static final String SECTION_NAME = "section";
107        /** The index of the "section" column in the projection. */
108        public static final int SECTION = 7;
109        /** The value of the "section" column for the header of the new section. */
110        public static final int SECTION_NEW_HEADER = 0;
111        /** The value of the "section" column for the items of the new section. */
112        public static final int SECTION_NEW_ITEM = 1;
113        /** The value of the "section" column for the header of the old section. */
114        public static final int SECTION_OLD_HEADER = 2;
115        /** The value of the "section" column for the items of the old section. */
116        public static final int SECTION_OLD_ITEM = 3;
117
118        /** The call log projection including the section name. */
119        public static final String[] EXTENDED_PROJECTION;
120        static {
121            EXTENDED_PROJECTION = new String[_PROJECTION.length + 1];
122            System.arraycopy(_PROJECTION, 0, EXTENDED_PROJECTION, 0, _PROJECTION.length);
123            EXTENDED_PROJECTION[_PROJECTION.length] = SECTION_NAME;
124        }
125
126        public static boolean isSectionHeader(Cursor cursor) {
127            int section = cursor.getInt(CallLogQuery.SECTION);
128            return section == CallLogQuery.SECTION_NEW_HEADER
129                    || section == CallLogQuery.SECTION_OLD_HEADER;
130        }
131
132        public static boolean isNewSection(Cursor cursor) {
133            int section = cursor.getInt(CallLogQuery.SECTION);
134            return section == CallLogQuery.SECTION_NEW_ITEM
135                    || section == CallLogQuery.SECTION_NEW_HEADER;
136        }
137    }
138
139    /** The query to use for the phones table */
140    private static final class PhoneQuery {
141        public static final String[] _PROJECTION = new String[] {
142                PhoneLookup._ID,
143                PhoneLookup.DISPLAY_NAME,
144                PhoneLookup.TYPE,
145                PhoneLookup.LABEL,
146                PhoneLookup.NUMBER,
147                PhoneLookup.NORMALIZED_NUMBER,
148                PhoneLookup.PHOTO_THUMBNAIL_URI,
149                PhoneLookup.LOOKUP_KEY};
150
151        public static final int PERSON_ID = 0;
152        public static final int NAME = 1;
153        public static final int PHONE_TYPE = 2;
154        public static final int LABEL = 3;
155        public static final int MATCHED_NUMBER = 4;
156        public static final int NORMALIZED_NUMBER = 5;
157        public static final int THUMBNAIL_URI = 6;
158        public static final int LOOKUP_KEY = 7;
159    }
160
161    private CallLogAdapter mAdapter;
162    private CallLogQueryHandler mCallLogQueryHandler;
163    private String mVoiceMailNumber;
164    private String mCurrentCountryIso;
165    private boolean mScrollToTop;
166
167    private boolean mShowOptionsMenu;
168
169    private VoicemailStatusHelper mVoicemailStatusHelper;
170    private View mStatusMessageView;
171    private TextView mStatusMessageText;
172    private TextView mStatusMessageAction;
173
174    public static final class ContactInfo {
175        public long personId;
176        public String name;
177        public int type;
178        public String label;
179        public String number;
180        public String formattedNumber;
181        public String normalizedNumber;
182        public Uri thumbnailUri;
183        public String lookupKey;
184
185        public static ContactInfo EMPTY = new ContactInfo();
186    }
187
188    public interface GroupCreator {
189        public void addGroup(int cursorPosition, int size, boolean expanded);
190    }
191
192    /** Adapter class to fill in data for the Call Log */
193    public final class CallLogAdapter extends GroupingListAdapter
194            implements Runnable, ViewTreeObserver.OnPreDrawListener, View.OnClickListener,
195            GroupCreator {
196        /** The time in millis to delay starting the thread processing requests. */
197        private static final int START_PROCESSING_REQUESTS_DELAY_MILLIS = 1000;
198
199        /**
200         * A cache of the contact details for the phone numbers in the call log.
201         * <p>
202         * The content of the cache is expired (but not purged) whenever the application comes to
203         * the foreground.
204         */
205        private ExpirableCache<String, ContactInfo> mContactInfoCache;
206
207        /**
208         * List of requests to update contact details.
209         * <p>
210         * The requests are added when displaying the contacts and are processed by a background
211         * thread.
212         */
213        private final LinkedList<String> mRequests;
214
215        private volatile boolean mDone;
216        private boolean mLoading = true;
217        private ViewTreeObserver.OnPreDrawListener mPreDrawListener;
218        private static final int REDRAW = 1;
219        private static final int START_THREAD = 2;
220        private boolean mFirst;
221        private Thread mCallerIdThread;
222
223        /** Instance of helper class for managing views. */
224        private final CallLogListItemHelper mCallLogViewsHelper;
225
226        /** Helper to set up contact photos. */
227        private final ContactPhotoManager mContactPhotoManager;
228        /** Helper to parse and process phone numbers. */
229        private PhoneNumberHelper mPhoneNumberHelper;
230        /** Helper to group call log entries. */
231        private final CallLogGroupBuilder mCallLogGroupBuilder;
232
233        /** Can be set to true by tests to disable processing of requests. */
234        private volatile boolean mRequestProcessingDisabled = false;
235
236        @Override
237        public void onClick(View view) {
238            IntentProvider intentProvider = (IntentProvider) view.getTag();
239            if (intentProvider != null) {
240                startActivity(intentProvider.getIntent(CallLogFragment.this.getActivity()));
241            }
242        }
243
244        @Override
245        public boolean onPreDraw() {
246            if (mFirst) {
247                mHandler.sendEmptyMessageDelayed(START_THREAD,
248                        START_PROCESSING_REQUESTS_DELAY_MILLIS);
249                mFirst = false;
250            }
251            return true;
252        }
253
254        private Handler mHandler = new Handler() {
255            @Override
256            public void handleMessage(Message msg) {
257                switch (msg.what) {
258                    case REDRAW:
259                        notifyDataSetChanged();
260                        break;
261                    case START_THREAD:
262                        startRequestProcessing();
263                        break;
264                }
265            }
266        };
267
268        public CallLogAdapter() {
269            super(getActivity());
270
271            mContactInfoCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE);
272            mRequests = new LinkedList<String>();
273            mPreDrawListener = null;
274
275            Resources resources = getResources();
276            CallTypeHelper callTypeHelper = new CallTypeHelper(resources,
277                    resources.getDrawable(R.drawable.ic_call_incoming_holo_dark),
278                    resources.getDrawable(R.drawable.ic_call_outgoing_holo_dark),
279                    resources.getDrawable(R.drawable.ic_call_missed_holo_dark),
280                    resources.getDrawable(R.drawable.ic_call_voicemail_holo_dark));
281            Drawable callDrawable = resources.getDrawable(R.drawable.ic_dial_action_call);
282            Drawable playDrawable = resources.getDrawable(
283                    R.drawable.ic_call_log_list_action_play);
284
285            mContactPhotoManager = ContactPhotoManager.getInstance(getActivity());
286            mPhoneNumberHelper = new PhoneNumberHelper(getResources(), mVoiceMailNumber);
287            PhoneCallDetailsHelper phoneCallDetailsHelper = new PhoneCallDetailsHelper(
288                    getActivity(), resources, callTypeHelper, mPhoneNumberHelper);
289            mCallLogViewsHelper =
290                    new CallLogListItemHelper(phoneCallDetailsHelper, mPhoneNumberHelper);
291            mCallLogGroupBuilder = new CallLogGroupBuilder(this);
292        }
293
294        /**
295         * Requery on background thread when {@link Cursor} changes.
296         */
297        @Override
298        protected void onContentChanged() {
299            // Start async requery
300            startCallsQuery();
301        }
302
303        void setLoading(boolean loading) {
304            mLoading = loading;
305        }
306
307        @Override
308        public boolean isEmpty() {
309            if (mLoading) {
310                // We don't want the empty state to show when loading.
311                return false;
312            } else {
313                return super.isEmpty();
314            }
315        }
316
317        public ContactInfo getContactInfo(String number) {
318            return mContactInfoCache.getPossiblyExpired(number);
319        }
320
321        public void startRequestProcessing() {
322            if (mRequestProcessingDisabled) {
323                return;
324            }
325
326            mDone = false;
327            mCallerIdThread = new Thread(this);
328            mCallerIdThread.setPriority(Thread.MIN_PRIORITY);
329            mCallerIdThread.start();
330        }
331
332        /**
333         * Stops the background thread that processes updates and cancels any pending requests to
334         * start it.
335         * <p>
336         * Should be called from the main thread to prevent a race condition between the request to
337         * start the thread being processed and stopping the thread.
338         */
339        public void stopRequestProcessing() {
340            // Remove any pending requests to start the processing thread.
341            mHandler.removeMessages(START_THREAD);
342            mDone = true;
343            if (mCallerIdThread != null) mCallerIdThread.interrupt();
344        }
345
346        public void invalidateCache() {
347            mContactInfoCache.expireAll();
348        }
349
350        private void enqueueRequest(String number, boolean immediate) {
351            synchronized (mRequests) {
352                if (!mRequests.contains(number)) {
353                    mRequests.add(number);
354                    mRequests.notifyAll();
355                }
356            }
357            if (mFirst && immediate) {
358                startRequestProcessing();
359                mFirst = false;
360            }
361        }
362
363        private boolean queryContactInfo(String number) {
364            // First check if there was a prior request for the same number
365            // that was already satisfied
366            ContactInfo info = mContactInfoCache.get(number);
367            boolean needNotify = false;
368            if (info != null && info != ContactInfo.EMPTY) {
369                return true;
370            } else {
371                // Ok, do a fresh Contacts lookup for ciq.number.
372                boolean infoUpdated = false;
373
374                if (PhoneNumberUtils.isUriNumber(number)) {
375                    // This "number" is really a SIP address.
376
377                    // TODO: This code is duplicated from the
378                    // CallerInfoAsyncQuery class.  To avoid that, could the
379                    // code here just use CallerInfoAsyncQuery, rather than
380                    // manually running ContentResolver.query() itself?
381
382                    // We look up SIP addresses directly in the Data table:
383                    Uri contactRef = Data.CONTENT_URI;
384
385                    // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent.
386                    //
387                    // Also note we use "upper(data1)" in the WHERE clause, and
388                    // uppercase the incoming SIP address, in order to do a
389                    // case-insensitive match.
390                    //
391                    // TODO: May also need to normalize by adding "sip:" as a
392                    // prefix, if we start storing SIP addresses that way in the
393                    // database.
394                    String selection = "upper(" + Data.DATA1 + ")=?"
395                            + " AND "
396                            + Data.MIMETYPE + "='" + SipAddress.CONTENT_ITEM_TYPE + "'";
397                    String[] selectionArgs = new String[] { number.toUpperCase() };
398
399                    Cursor dataTableCursor =
400                            getActivity().getContentResolver().query(
401                                    contactRef,
402                                    null,  // projection
403                                    selection,  // selection
404                                    selectionArgs,  // selectionArgs
405                                    null);  // sortOrder
406
407                    if (dataTableCursor != null) {
408                        if (dataTableCursor.moveToFirst()) {
409                            info = new ContactInfo();
410
411                            // TODO: we could slightly speed this up using an
412                            // explicit projection (and thus not have to do
413                            // those getColumnIndex() calls) but the benefit is
414                            // very minimal.
415
416                            // Note the Data.CONTACT_ID column here is
417                            // equivalent to the PERSON_ID_COLUMN_INDEX column
418                            // we use with "phonesCursor" below.
419                            info.personId = dataTableCursor.getLong(
420                                    dataTableCursor.getColumnIndex(Data.CONTACT_ID));
421                            info.name = dataTableCursor.getString(
422                                    dataTableCursor.getColumnIndex(Data.DISPLAY_NAME));
423                            // "type" and "label" are currently unused for SIP addresses
424                            info.type = SipAddress.TYPE_OTHER;
425                            info.label = null;
426
427                            // And "number" is the SIP address.
428                            // Note Data.DATA1 and SipAddress.SIP_ADDRESS are equivalent.
429                            info.number = dataTableCursor.getString(
430                                    dataTableCursor.getColumnIndex(Data.DATA1));
431                            info.normalizedNumber = null;  // meaningless for SIP addresses
432                            final String thumbnailUriString = dataTableCursor.getString(
433                                    dataTableCursor.getColumnIndex(Data.PHOTO_THUMBNAIL_URI));
434                            info.thumbnailUri = thumbnailUriString == null
435                                    ? null
436                                    : Uri.parse(thumbnailUriString);
437                            info.lookupKey = dataTableCursor.getString(
438                                    dataTableCursor.getColumnIndex(Data.LOOKUP_KEY));
439
440                            infoUpdated = true;
441                        }
442                        dataTableCursor.close();
443                    }
444                } else {
445                    // "number" is a regular phone number, so use the
446                    // PhoneLookup table:
447                    Cursor phonesCursor =
448                            getActivity().getContentResolver().query(
449                                Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI,
450                                        Uri.encode(number)),
451                                        PhoneQuery._PROJECTION, null, null, null);
452                    if (phonesCursor != null) {
453                        if (phonesCursor.moveToFirst()) {
454                            info = new ContactInfo();
455                            info.personId = phonesCursor.getLong(PhoneQuery.PERSON_ID);
456                            info.name = phonesCursor.getString(PhoneQuery.NAME);
457                            info.type = phonesCursor.getInt(PhoneQuery.PHONE_TYPE);
458                            info.label = phonesCursor.getString(PhoneQuery.LABEL);
459                            info.number = phonesCursor
460                                    .getString(PhoneQuery.MATCHED_NUMBER);
461                            info.normalizedNumber = phonesCursor
462                                    .getString(PhoneQuery.NORMALIZED_NUMBER);
463                            final String thumbnailUriString = phonesCursor.getString(
464                                    PhoneQuery.THUMBNAIL_URI);
465                            info.thumbnailUri = thumbnailUriString == null
466                                    ? null
467                                    : Uri.parse(thumbnailUriString);
468                            info.lookupKey = phonesCursor.getString(PhoneQuery.LOOKUP_KEY);
469
470                            infoUpdated = true;
471                        }
472                        phonesCursor.close();
473                    }
474                }
475
476                if (infoUpdated) {
477                    // New incoming phone number invalidates our formatted
478                    // cache. Any cache fills happen only on the GUI thread.
479                    info.formattedNumber = null;
480                    mContactInfoCache.put(number, info);
481                    // Inform list to update this item, if in view
482                    needNotify = true;
483                }
484            }
485            return needNotify;
486        }
487
488        /*
489         * Handles requests for contact name and number type
490         * @see java.lang.Runnable#run()
491         */
492        @Override
493        public void run() {
494            boolean needNotify = false;
495            while (!mDone) {
496                String number = null;
497                synchronized (mRequests) {
498                    if (!mRequests.isEmpty()) {
499                        number = mRequests.removeFirst();
500                    } else {
501                        if (needNotify) {
502                            needNotify = false;
503                            mHandler.sendEmptyMessage(REDRAW);
504                        }
505                        try {
506                            mRequests.wait(1000);
507                        } catch (InterruptedException ie) {
508                            // Ignore and continue processing requests
509                            Thread.currentThread().interrupt();
510                        }
511                    }
512                }
513                if (!mDone && number != null && queryContactInfo(number)) {
514                    needNotify = true;
515                }
516            }
517        }
518
519        @Override
520        protected void addGroups(Cursor cursor) {
521            mCallLogGroupBuilder.addGroups(cursor);
522        }
523
524        @VisibleForTesting
525        @Override
526        public View newStandAloneView(Context context, ViewGroup parent) {
527            LayoutInflater inflater =
528                    (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
529            View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
530            findAndCacheViews(view);
531            return view;
532        }
533
534        @VisibleForTesting
535        @Override
536        public void bindStandAloneView(View view, Context context, Cursor cursor) {
537            bindView(view, cursor, 1);
538        }
539
540        @VisibleForTesting
541        @Override
542        public View newChildView(Context context, ViewGroup parent) {
543            LayoutInflater inflater =
544                    (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
545            View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
546            findAndCacheViews(view);
547            return view;
548        }
549
550        @VisibleForTesting
551        @Override
552        public void bindChildView(View view, Context context, Cursor cursor) {
553            bindView(view, cursor, 1);
554        }
555
556        @VisibleForTesting
557        @Override
558        public View newGroupView(Context context, ViewGroup parent) {
559            LayoutInflater inflater =
560                    (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
561            View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
562            findAndCacheViews(view);
563            return view;
564        }
565
566        @VisibleForTesting
567        @Override
568        public void bindGroupView(View view, Context context, Cursor cursor, int groupSize,
569                boolean expanded) {
570            bindView(view, cursor, groupSize);
571        }
572
573        private void findAndCacheViews(View view) {
574            // Get the views to bind to.
575            CallLogListItemViews views = CallLogListItemViews.fromView(view);
576            views.callView.setOnClickListener(this);
577            views.playView.setOnClickListener(this);
578            view.setTag(views);
579        }
580
581        /**
582         * Binds the views in the entry to the data in the call log.
583         *
584         * @param view the view corresponding to this entry
585         * @param c the cursor pointing to the entry in the call log
586         * @param count the number of entries in the current item, greater than 1 if it is a group
587         */
588        private void bindView(View view, Cursor c, int count) {
589            final CallLogListItemViews views = (CallLogListItemViews) view.getTag();
590            final int section = c.getInt(CallLogQuery.SECTION);
591
592            // This might be a header: check the value of the section column in the cursor.
593            if (section == CallLogQuery.SECTION_NEW_HEADER
594                    || section == CallLogQuery.SECTION_OLD_HEADER) {
595                views.listItemView.setVisibility(View.GONE);
596                views.listHeaderView.setVisibility(View.VISIBLE);
597                views.listHeaderTextView.setText(
598                        section == CallLogQuery.SECTION_NEW_HEADER
599                                ? R.string.call_log_new_header
600                                : R.string.call_log_old_header);
601                // Nothing else to set up for a header.
602                return;
603            }
604            // Default case: an item in the call log.
605            views.listItemView.setVisibility(View.VISIBLE);
606            views.listHeaderView.setVisibility(View.GONE);
607
608            final String number = c.getString(CallLogQuery.NUMBER);
609            final long date = c.getLong(CallLogQuery.DATE);
610            final long duration = c.getLong(CallLogQuery.DURATION);
611            final int callType = c.getInt(CallLogQuery.CALL_TYPE);
612            final String formattedNumber;
613            final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO);
614
615            // Store away the number so we can call it directly if you click on the call icon.
616            if (!TextUtils.isEmpty(number)) {
617                views.callView.setTag(IntentProvider.getReturnCallIntentProvider(number));
618            } else {
619                views.callView.setTag(null);
620            }
621
622            // Store away the voicemail information so we can play it directly.
623            if (callType == Calls.VOICEMAIL_TYPE) {
624                String voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI);
625                final long rowId = c.getLong(CallLogQuery.ID);
626                views.playView.setTag(
627                        IntentProvider.getPlayVoicemailIntentProvider(rowId, voicemailUri));
628            } else {
629                views.playView.setTag(null);
630            }
631
632            // Lookup contacts with this number
633            ExpirableCache.CachedValue<ContactInfo> cachedInfo =
634                    mContactInfoCache.getCachedValue(number);
635            ContactInfo info = cachedInfo == null ? null : cachedInfo.getValue();
636            if (cachedInfo == null) {
637                // Mark it as empty and queue up a request to find the name.
638                // The db request should happen on a non-UI thread.
639                info = ContactInfo.EMPTY;
640                mContactInfoCache.put(number, info);
641                // Request the contact details immediately since they are currently missing.
642                enqueueRequest(number, true);
643                // Format the phone number in the call log as best as we can.
644                formattedNumber = formatPhoneNumber(number, null, countryIso);
645            } else {
646                if (cachedInfo.isExpired()) {
647                    // The contact info is no longer up to date, we should request it. However, we
648                    // do not need to request them immediately.
649                    enqueueRequest(number, false);
650                }
651
652                if (info != ContactInfo.EMPTY) {
653                    // Format and cache phone number for found contact.
654                    if (info.formattedNumber == null) {
655                        info.formattedNumber =
656                                formatPhoneNumber(info.number, info.normalizedNumber, countryIso);
657                    }
658                    formattedNumber = info.formattedNumber;
659                } else {
660                    // Format the phone number in the call log as best as we can.
661                    formattedNumber = formatPhoneNumber(number, null, countryIso);
662                }
663            }
664
665            final long personId = info.personId;
666            final String name = info.name;
667            final int ntype = info.type;
668            final String label = info.label;
669            final Uri thumbnailUri = info.thumbnailUri;
670            final String lookupKey = info.lookupKey;
671            final int[] callTypes = getCallTypes(c, count);
672            final PhoneCallDetails details;
673            if (TextUtils.isEmpty(name)) {
674                details = new PhoneCallDetails(number, formattedNumber, countryIso,
675                        callTypes, date, duration);
676            } else {
677                details = new PhoneCallDetails(number, formattedNumber, countryIso,
678                        callTypes, date, duration, name, ntype, label, personId, thumbnailUri);
679            }
680
681            final boolean isNew = CallLogQuery.isNewSection(c);
682            // Use icons for old items, but text for new ones.
683            final boolean useIcons = !isNew;
684            // New items also use the highlighted version of the text.
685            final boolean isHighlighted = isNew;
686            mCallLogViewsHelper.setPhoneCallDetails(views, details, useIcons, isHighlighted);
687            if (views.photoView != null) {
688                bindQuickContact(views.photoView, thumbnailUri, personId, lookupKey);
689            }
690
691
692            // Listen for the first draw
693            if (mPreDrawListener == null) {
694                mFirst = true;
695                mPreDrawListener = this;
696                view.getViewTreeObserver().addOnPreDrawListener(this);
697            }
698        }
699
700        /**
701         * Returns the call types for the given number of items in the cursor.
702         * <p>
703         * It uses the next {@code count} rows in the cursor to extract the types.
704         * <p>
705         * It position in the cursor is unchanged by this function.
706         */
707        private int[] getCallTypes(Cursor cursor, int count) {
708            int position = cursor.getPosition();
709            int[] callTypes = new int[count];
710            for (int index = 0; index < count; ++index) {
711                callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE);
712                cursor.moveToNext();
713            }
714            cursor.moveToPosition(position);
715            return callTypes;
716        }
717
718        private void bindQuickContact(QuickContactBadge view, Uri thumbnailUri, long contactId,
719                String lookupKey) {
720            view.assignContactUri(getContactUri(contactId, lookupKey));
721            mContactPhotoManager.loadPhoto(view, thumbnailUri);
722        }
723
724        private Uri getContactUri(long contactId, String lookupKey) {
725            return Contacts.getLookupUri(contactId, lookupKey);
726        }
727
728        /**
729         * Sets whether processing of requests for contact details should be enabled.
730         * <p>
731         * This method should be called in tests to disable such processing of requests when not
732         * needed.
733         */
734        public void disableRequestProcessingForTest() {
735            mRequestProcessingDisabled = true;
736        }
737
738        public void injectContactInfoForTest(String number, ContactInfo contactInfo) {
739            mContactInfoCache.put(number, contactInfo);
740        }
741
742        @Override
743        public void addGroup(int cursorPosition, int size, boolean expanded) {
744            super.addGroup(cursorPosition, size, expanded);
745        }
746    }
747
748    @Override
749    public void onCreate(Bundle state) {
750        super.onCreate(state);
751
752        mVoiceMailNumber = ((TelephonyManager) getActivity().getSystemService(
753                Context.TELEPHONY_SERVICE)).getVoiceMailNumber();
754        mCallLogQueryHandler = new CallLogQueryHandler(this);
755
756        mCurrentCountryIso = ContactsUtils.getCurrentCountryIso(getActivity());
757
758        setHasOptionsMenu(true);
759    }
760
761    /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */
762    public void onCallsFetched(Cursor cursor) {
763        if (getActivity() == null || getActivity().isFinishing()) {
764            return;
765        }
766        Log.d(TAG, "updating adapter");
767        mAdapter.setLoading(false);
768        mAdapter.changeCursor(cursor);
769        if (mScrollToTop) {
770            final ListView listView = getListView();
771            if (listView.getFirstVisiblePosition() > 5) {
772                listView.setSelection(5);
773            }
774            listView.smoothScrollToPosition(0);
775            mScrollToTop = false;
776        }
777    }
778
779    /**
780     * Called by {@link CallLogQueryHandler} after a successful query to voicemail status provider.
781     */
782    public void onVoicemailStatusFetched(Cursor statusCursor) {
783        if (getActivity() == null || getActivity().isFinishing()) {
784            return;
785        }
786        updateVoicemailStatusMessage(statusCursor);
787    }
788
789    @Override
790    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
791        View view = inflater.inflate(R.layout.call_log_fragment, container, false);
792        mVoicemailStatusHelper = new VoicemailStatusHelperImpl();
793        mStatusMessageView = view.findViewById(R.id.voicemail_status);
794        mStatusMessageText = (TextView) view.findViewById(R.id.voicemail_status_message);
795        mStatusMessageAction = (TextView) view.findViewById(R.id.voicemail_status_action);
796        return view;
797    }
798
799    @Override
800    public void onViewCreated(View view, Bundle savedInstanceState) {
801        super.onViewCreated(view, savedInstanceState);
802        mAdapter = new CallLogAdapter();
803        setListAdapter(mAdapter);
804    }
805
806    @Override
807    public void onStart() {
808        mScrollToTop = true;
809        super.onStart();
810    }
811
812    @Override
813    public void onResume() {
814        super.onResume();
815        refreshData();
816    }
817
818    private void updateVoicemailStatusMessage(Cursor statusCursor) {
819        List<StatusMessage> messages = mVoicemailStatusHelper.getStatusMessages(statusCursor);
820        if (messages.size() == 0) {
821            mStatusMessageView.setVisibility(View.GONE);
822        } else {
823            mStatusMessageView.setVisibility(View.VISIBLE);
824            // TODO: Change the code to show all messages. For now just pick the first message.
825            final StatusMessage message = messages.get(0);
826            if (message.showInCallLog()) {
827                mStatusMessageText.setText(message.callLogMessageId);
828            }
829            if (message.actionMessageId != -1) {
830                mStatusMessageAction.setText(message.actionMessageId);
831            }
832            if (message.actionUri != null) {
833                mStatusMessageAction.setClickable(true);
834                mStatusMessageAction.setOnClickListener(new View.OnClickListener() {
835                    @Override
836                    public void onClick(View v) {
837                        getActivity().startActivity(
838                                new Intent(Intent.ACTION_VIEW, message.actionUri));
839                    }
840                });
841            } else {
842                mStatusMessageAction.setClickable(false);
843            }
844        }
845    }
846
847    @Override
848    public void onPause() {
849        super.onPause();
850
851        // Kill the requests thread
852        mAdapter.stopRequestProcessing();
853    }
854
855    @Override
856    public void onDestroy() {
857        super.onDestroy();
858        mAdapter.stopRequestProcessing();
859        mAdapter.changeCursor(null);
860    }
861
862    /**
863     * Format the given phone number
864     *
865     * @param number the number to be formatted.
866     * @param normalizedNumber the normalized number of the given number.
867     * @param countryIso the ISO 3166-1 two letters country code, the country's
868     *        convention will be used to format the number if the normalized
869     *        phone is null.
870     *
871     * @return the formatted number, or the given number if it was formatted.
872     */
873    private String formatPhoneNumber(String number, String normalizedNumber, String countryIso) {
874        if (TextUtils.isEmpty(number)) {
875            return "";
876        }
877        // If "number" is really a SIP address, don't try to do any formatting at all.
878        if (PhoneNumberUtils.isUriNumber(number)) {
879            return number;
880        }
881        if (TextUtils.isEmpty(countryIso)) {
882            countryIso = mCurrentCountryIso;
883        }
884        return PhoneNumberUtils.formatNumber(number, normalizedNumber, countryIso);
885    }
886
887    private void resetNewCallsFlag() {
888        mCallLogQueryHandler.updateMissedCalls();
889    }
890
891    private void startCallsQuery() {
892        mAdapter.setLoading(true);
893        mCallLogQueryHandler.fetchCalls();
894    }
895
896    private void startVoicemailStatusQuery() {
897        mCallLogQueryHandler.fetchVoicemailStatus();
898    }
899
900    @Override
901    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
902        super.onCreateOptionsMenu(menu, inflater);
903        inflater.inflate(R.menu.call_log_options, menu);
904    }
905
906    @Override
907    public void onPrepareOptionsMenu(Menu menu) {
908        menu.findItem(R.id.delete_all).setVisible(mShowOptionsMenu);
909        final MenuItem callSettingsMenuItem = menu.findItem(R.id.menu_call_settings_call_log);
910        if (mShowOptionsMenu) {
911            callSettingsMenuItem.setVisible(true);
912            callSettingsMenuItem.setIntent(DialtactsActivity.getCallSettingsIntent());
913        } else {
914            callSettingsMenuItem.setVisible(false);
915        }
916    }
917
918    @Override
919    public boolean onOptionsItemSelected(MenuItem item) {
920        switch (item.getItemId()) {
921            case R.id.delete_all: {
922                ClearCallLogDialog.show(getFragmentManager());
923                return true;
924            }
925        }
926        return super.onOptionsItemSelected(item);
927    }
928
929    /*
930     * Get the number from the Contacts, if available, since sometimes
931     * the number provided by caller id may not be formatted properly
932     * depending on the carrier (roaming) in use at the time of the
933     * incoming call.
934     * Logic : If the caller-id number starts with a "+", use it
935     *         Else if the number in the contacts starts with a "+", use that one
936     *         Else if the number in the contacts is longer, use that one
937     */
938    private String getBetterNumberFromContacts(String number) {
939        String matchingNumber = null;
940        // Look in the cache first. If it's not found then query the Phones db
941        ContactInfo ci = mAdapter.mContactInfoCache.getPossiblyExpired(number);
942        if (ci != null && ci != ContactInfo.EMPTY) {
943            matchingNumber = ci.number;
944        } else {
945            try {
946                Cursor phonesCursor = getActivity().getContentResolver().query(
947                        Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, number),
948                        PhoneQuery._PROJECTION, null, null, null);
949                if (phonesCursor != null) {
950                    if (phonesCursor.moveToFirst()) {
951                        matchingNumber = phonesCursor.getString(PhoneQuery.MATCHED_NUMBER);
952                    }
953                    phonesCursor.close();
954                }
955            } catch (Exception e) {
956                // Use the number from the call log
957            }
958        }
959        if (!TextUtils.isEmpty(matchingNumber) &&
960                (matchingNumber.startsWith("+")
961                        || matchingNumber.length() > number.length())) {
962            number = matchingNumber;
963        }
964        return number;
965    }
966
967    public void callSelectedEntry() {
968        int position = getListView().getSelectedItemPosition();
969        if (position < 0) {
970            // In touch mode you may often not have something selected, so
971            // just call the first entry to make sure that [send] [send] calls the
972            // most recent entry.
973            position = 0;
974        }
975        final Cursor cursor = (Cursor)mAdapter.getItem(position);
976        if (cursor != null) {
977            String number = cursor.getString(CallLogQuery.NUMBER);
978            if (TextUtils.isEmpty(number)
979                    || number.equals(CallerInfo.UNKNOWN_NUMBER)
980                    || number.equals(CallerInfo.PRIVATE_NUMBER)
981                    || number.equals(CallerInfo.PAYPHONE_NUMBER)) {
982                // This number can't be called, do nothing
983                return;
984            }
985            Intent intent;
986            // If "number" is really a SIP address, construct a sip: URI.
987            if (PhoneNumberUtils.isUriNumber(number)) {
988                intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
989                                    Uri.fromParts("sip", number, null));
990            } else {
991                // We're calling a regular PSTN phone number.
992                // Construct a tel: URI, but do some other possible cleanup first.
993                int callType = cursor.getInt(CallLogQuery.CALL_TYPE);
994                if (!number.startsWith("+") &&
995                       (callType == Calls.INCOMING_TYPE
996                                || callType == Calls.MISSED_TYPE)) {
997                    // If the caller-id matches a contact with a better qualified number, use it
998                    number = getBetterNumberFromContacts(number);
999                }
1000                intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
1001                                    Uri.fromParts("tel", number, null));
1002            }
1003            intent.setFlags(
1004                    Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
1005            startActivity(intent);
1006        }
1007    }
1008
1009    @Override
1010    public void onListItemClick(ListView l, View v, int position, long id) {
1011        Intent intent = new Intent(getActivity(), CallDetailActivity.class);
1012        Cursor cursor = (Cursor) mAdapter.getItem(position);
1013        if (CallLogQuery.isSectionHeader(cursor)) {
1014            // Do nothing when a header is clicked.
1015            return;
1016        }
1017        if (mAdapter.isGroupHeader(position)) {
1018            // We want to restore the position in the cursor at the end.
1019            int currentPosition = cursor.getPosition();
1020            int groupSize = mAdapter.getGroupSize(position);
1021            long[] ids = new long[groupSize];
1022            // Copy the ids of the rows in the group.
1023            for (int index = 0; index < groupSize; ++index) {
1024                ids[index] = cursor.getLong(CallLogQuery.ID);
1025                cursor.moveToNext();
1026            }
1027            intent.putExtra(CallDetailActivity.EXTRA_CALL_LOG_IDS, ids);
1028            cursor.moveToPosition(currentPosition);
1029        } else {
1030            // If there is a single item, use the direct URI for it.
1031            intent.setData(ContentUris.withAppendedId(Calls.CONTENT_URI_WITH_VOICEMAIL, id));
1032            String voicemailUri = cursor.getString(CallLogQuery.VOICEMAIL_URI);
1033            if (voicemailUri != null) {
1034                intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI, Uri.parse(voicemailUri));
1035            }
1036            intent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_START_PLAYBACK, false);
1037        }
1038        startActivity(intent);
1039    }
1040
1041    @VisibleForTesting
1042    public CallLogAdapter getAdapter() {
1043        return mAdapter;
1044    }
1045
1046    @VisibleForTesting
1047    public String getVoiceMailNumber() {
1048        return mVoiceMailNumber;
1049    }
1050
1051    @Override
1052    public void onVisibilityChanged(boolean visible) {
1053        mShowOptionsMenu = visible;
1054        if (visible && isResumed()) {
1055            refreshData();
1056        }
1057    }
1058
1059    /** Requests updates to the data to be shown. */
1060    private void refreshData() {
1061        // Mark all entries in the contact info cache as out of date, so they will be looked up
1062        // again once being shown.
1063        mAdapter.invalidateCache();
1064        startCallsQuery();
1065        resetNewCallsFlag();
1066        startVoicemailStatusQuery();
1067        mAdapter.mPreDrawListener = null; // Let it restart the thread after next draw
1068    }
1069}
1070