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