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