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