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