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