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