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 android.app.Activity;
20import android.app.KeyguardManager;
21import android.app.ListFragment;
22import android.content.Context;
23import android.content.Intent;
24import android.database.ContentObserver;
25import android.database.Cursor;
26import android.net.Uri;
27import android.os.Bundle;
28import android.os.Handler;
29import android.os.RemoteException;
30import android.os.ServiceManager;
31import android.provider.CallLog;
32import android.provider.CallLog.Calls;
33import android.provider.ContactsContract;
34import android.telephony.PhoneNumberUtils;
35import android.telephony.PhoneStateListener;
36import android.telephony.TelephonyManager;
37import android.text.TextUtils;
38import android.util.Log;
39import android.view.LayoutInflater;
40import android.view.Menu;
41import android.view.MenuInflater;
42import android.view.MenuItem;
43import android.view.View;
44import android.view.ViewGroup;
45import android.widget.ListView;
46import android.widget.TextView;
47
48import com.android.common.io.MoreCloseables;
49import com.android.contacts.ContactsUtils;
50import com.android.contacts.R;
51import com.android.contacts.util.Constants;
52import com.android.contacts.util.EmptyLoader;
53import com.android.contacts.voicemail.VoicemailStatusHelper;
54import com.android.contacts.voicemail.VoicemailStatusHelper.StatusMessage;
55import com.android.contacts.voicemail.VoicemailStatusHelperImpl;
56import com.android.internal.telephony.CallerInfo;
57import com.android.internal.telephony.ITelephony;
58import com.google.common.annotations.VisibleForTesting;
59
60import java.util.List;
61
62/**
63 * Displays a list of call log entries.
64 */
65public class CallLogFragment extends ListFragment
66        implements CallLogQueryHandler.Listener, CallLogAdapter.CallFetcher {
67    private static final String TAG = "CallLogFragment";
68
69    /**
70     * ID of the empty loader to defer other fragments.
71     */
72    private static final int EMPTY_LOADER_ID = 0;
73
74    private CallLogAdapter mAdapter;
75    private CallLogQueryHandler mCallLogQueryHandler;
76    private boolean mScrollToTop;
77
78    /** Whether there is at least one voicemail source installed. */
79    private boolean mVoicemailSourcesAvailable = false;
80
81    private VoicemailStatusHelper mVoicemailStatusHelper;
82    private View mStatusMessageView;
83    private TextView mStatusMessageText;
84    private TextView mStatusMessageAction;
85    private TextView mFilterStatusView;
86    private KeyguardManager mKeyguardManager;
87
88    private boolean mEmptyLoaderRunning;
89    private boolean mCallLogFetched;
90    private boolean mVoicemailStatusFetched;
91
92    private final Handler mHandler = new Handler();
93
94    private TelephonyManager mTelephonyManager;
95    private PhoneStateListener mPhoneStateListener;
96
97    private class CustomContentObserver extends ContentObserver {
98        public CustomContentObserver() {
99            super(mHandler);
100        }
101        @Override
102        public void onChange(boolean selfChange) {
103            mRefreshDataRequired = true;
104        }
105    }
106
107    // See issue 6363009
108    private final ContentObserver mCallLogObserver = new CustomContentObserver();
109    private final ContentObserver mContactsObserver = new CustomContentObserver();
110    private boolean mRefreshDataRequired = true;
111
112    // Exactly same variable is in Fragment as a package private.
113    private boolean mMenuVisible = true;
114
115    // Default to all calls.
116    private int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL;
117
118    @Override
119    public void onCreate(Bundle state) {
120        super.onCreate(state);
121
122        mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(), this);
123        mKeyguardManager =
124                (KeyguardManager) getActivity().getSystemService(Context.KEYGUARD_SERVICE);
125        getActivity().getContentResolver().registerContentObserver(
126                CallLog.CONTENT_URI, true, mCallLogObserver);
127        getActivity().getContentResolver().registerContentObserver(
128                ContactsContract.Contacts.CONTENT_URI, true, mContactsObserver);
129        setHasOptionsMenu(true);
130    }
131
132    /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */
133    @Override
134    public void onCallsFetched(Cursor cursor) {
135        if (getActivity() == null || getActivity().isFinishing()) {
136            return;
137        }
138        mAdapter.setLoading(false);
139        mAdapter.changeCursor(cursor);
140        // This will update the state of the "Clear call log" menu item.
141        getActivity().invalidateOptionsMenu();
142        if (mScrollToTop) {
143            final ListView listView = getListView();
144            // The smooth-scroll animation happens over a fixed time period.
145            // As a result, if it scrolls through a large portion of the list,
146            // each frame will jump so far from the previous one that the user
147            // will not experience the illusion of downward motion.  Instead,
148            // if we're not already near the top of the list, we instantly jump
149            // near the top, and animate from there.
150            if (listView.getFirstVisiblePosition() > 5) {
151                listView.setSelection(5);
152            }
153            // Workaround for framework issue: the smooth-scroll doesn't
154            // occur if setSelection() is called immediately before.
155            mHandler.post(new Runnable() {
156               @Override
157               public void run() {
158                   if (getActivity() == null || getActivity().isFinishing()) {
159                       return;
160                   }
161                   listView.smoothScrollToPosition(0);
162               }
163            });
164
165            mScrollToTop = false;
166        }
167        mCallLogFetched = true;
168        destroyEmptyLoaderIfAllDataFetched();
169    }
170
171    /**
172     * Called by {@link CallLogQueryHandler} after a successful query to voicemail status provider.
173     */
174    @Override
175    public void onVoicemailStatusFetched(Cursor statusCursor) {
176        if (getActivity() == null || getActivity().isFinishing()) {
177            return;
178        }
179        updateVoicemailStatusMessage(statusCursor);
180
181        int activeSources = mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor);
182        setVoicemailSourcesAvailable(activeSources != 0);
183        MoreCloseables.closeQuietly(statusCursor);
184        mVoicemailStatusFetched = true;
185        destroyEmptyLoaderIfAllDataFetched();
186    }
187
188    private void destroyEmptyLoaderIfAllDataFetched() {
189        if (mCallLogFetched && mVoicemailStatusFetched && mEmptyLoaderRunning) {
190            mEmptyLoaderRunning = false;
191            getLoaderManager().destroyLoader(EMPTY_LOADER_ID);
192        }
193    }
194
195    /** Sets whether there are any voicemail sources available in the platform. */
196    private void setVoicemailSourcesAvailable(boolean voicemailSourcesAvailable) {
197        if (mVoicemailSourcesAvailable == voicemailSourcesAvailable) return;
198        mVoicemailSourcesAvailable = voicemailSourcesAvailable;
199
200        Activity activity = getActivity();
201        if (activity != null) {
202            // This is so that the options menu content is updated.
203            activity.invalidateOptionsMenu();
204        }
205    }
206
207    @Override
208    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
209        View view = inflater.inflate(R.layout.call_log_fragment, container, false);
210        mVoicemailStatusHelper = new VoicemailStatusHelperImpl();
211        mStatusMessageView = view.findViewById(R.id.voicemail_status);
212        mStatusMessageText = (TextView) view.findViewById(R.id.voicemail_status_message);
213        mStatusMessageAction = (TextView) view.findViewById(R.id.voicemail_status_action);
214        mFilterStatusView = (TextView) view.findViewById(R.id.filter_status);
215        return view;
216    }
217
218    @Override
219    public void onViewCreated(View view, Bundle savedInstanceState) {
220        super.onViewCreated(view, savedInstanceState);
221        String currentCountryIso = ContactsUtils.getCurrentCountryIso(getActivity());
222        mAdapter = new CallLogAdapter(getActivity(), this,
223                new ContactInfoHelper(getActivity(), currentCountryIso));
224        setListAdapter(mAdapter);
225        getListView().setItemsCanFocus(true);
226    }
227
228    /**
229     * Based on the new intent, decide whether the list should be configured
230     * to scroll up to display the first item.
231     */
232    public void configureScreenFromIntent(Intent newIntent) {
233        // Typically, when switching to the call-log we want to show the user
234        // the same section of the list that they were most recently looking
235        // at.  However, under some circumstances, we want to automatically
236        // scroll to the top of the list to present the newest call items.
237        // For example, immediately after a call is finished, we want to
238        // display information about that call.
239        mScrollToTop = Calls.CONTENT_TYPE.equals(newIntent.getType());
240    }
241
242    @Override
243    public void onStart() {
244        // Start the empty loader now to defer other fragments.  We destroy it when both calllog
245        // and the voicemail status are fetched.
246        getLoaderManager().initLoader(EMPTY_LOADER_ID, null,
247                new EmptyLoader.Callback(getActivity()));
248        mEmptyLoaderRunning = true;
249        super.onStart();
250    }
251
252    @Override
253    public void onResume() {
254        super.onResume();
255        refreshData();
256    }
257
258    private void updateVoicemailStatusMessage(Cursor statusCursor) {
259        List<StatusMessage> messages = mVoicemailStatusHelper.getStatusMessages(statusCursor);
260        if (messages.size() == 0) {
261            mStatusMessageView.setVisibility(View.GONE);
262        } else {
263            mStatusMessageView.setVisibility(View.VISIBLE);
264            // TODO: Change the code to show all messages. For now just pick the first message.
265            final StatusMessage message = messages.get(0);
266            if (message.showInCallLog()) {
267                mStatusMessageText.setText(message.callLogMessageId);
268            }
269            if (message.actionMessageId != -1) {
270                mStatusMessageAction.setText(message.actionMessageId);
271            }
272            if (message.actionUri != null) {
273                mStatusMessageAction.setVisibility(View.VISIBLE);
274                mStatusMessageAction.setOnClickListener(new View.OnClickListener() {
275                    @Override
276                    public void onClick(View v) {
277                        getActivity().startActivity(
278                                new Intent(Intent.ACTION_VIEW, message.actionUri));
279                    }
280                });
281            } else {
282                mStatusMessageAction.setVisibility(View.GONE);
283            }
284        }
285    }
286
287    @Override
288    public void onPause() {
289        super.onPause();
290        // Kill the requests thread
291        mAdapter.stopRequestProcessing();
292    }
293
294    @Override
295    public void onStop() {
296        super.onStop();
297        updateOnExit();
298    }
299
300    @Override
301    public void onDestroy() {
302        super.onDestroy();
303        mAdapter.stopRequestProcessing();
304        mAdapter.changeCursor(null);
305        getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver);
306        getActivity().getContentResolver().unregisterContentObserver(mContactsObserver);
307        unregisterPhoneCallReceiver();
308    }
309
310    @Override
311    public void fetchCalls() {
312        mCallLogQueryHandler.fetchCalls(mCallTypeFilter);
313    }
314
315    public void startCallsQuery() {
316        mAdapter.setLoading(true);
317        mCallLogQueryHandler.fetchCalls(mCallTypeFilter);
318    }
319
320    private void startVoicemailStatusQuery() {
321        mCallLogQueryHandler.fetchVoicemailStatus();
322    }
323
324    @Override
325    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
326        super.onCreateOptionsMenu(menu, inflater);
327        inflater.inflate(R.menu.call_log_options, menu);
328    }
329
330    @Override
331    public void onPrepareOptionsMenu(Menu menu) {
332        final MenuItem itemDeleteAll = menu.findItem(R.id.delete_all);
333        // Check if all the menu items are inflated correctly. As a shortcut, we assume all
334        // menu items are ready if the first item is non-null.
335        if (itemDeleteAll != null) {
336            itemDeleteAll.setEnabled(mAdapter != null && !mAdapter.isEmpty());
337
338            showAllFilterMenuOptions(menu);
339            hideCurrentFilterMenuOption(menu);
340
341            // Only hide if not available.  Let the above calls handle showing.
342            if (!mVoicemailSourcesAvailable) {
343                menu.findItem(R.id.show_voicemails_only).setVisible(false);
344            }
345        }
346    }
347
348    private void hideCurrentFilterMenuOption(Menu menu) {
349        MenuItem item = null;
350        switch (mCallTypeFilter) {
351            case CallLogQueryHandler.CALL_TYPE_ALL:
352                item = menu.findItem(R.id.show_all_calls);
353                break;
354            case Calls.INCOMING_TYPE:
355                item = menu.findItem(R.id.show_incoming_only);
356                break;
357            case Calls.OUTGOING_TYPE:
358                item = menu.findItem(R.id.show_outgoing_only);
359                break;
360            case Calls.MISSED_TYPE:
361                item = menu.findItem(R.id.show_missed_only);
362                break;
363            case Calls.VOICEMAIL_TYPE:
364                menu.findItem(R.id.show_voicemails_only);
365                break;
366        }
367        if (item != null) {
368            item.setVisible(false);
369        }
370    }
371
372    private void showAllFilterMenuOptions(Menu menu) {
373        menu.findItem(R.id.show_all_calls).setVisible(true);
374        menu.findItem(R.id.show_incoming_only).setVisible(true);
375        menu.findItem(R.id.show_outgoing_only).setVisible(true);
376        menu.findItem(R.id.show_missed_only).setVisible(true);
377        menu.findItem(R.id.show_voicemails_only).setVisible(true);
378    }
379
380    @Override
381    public boolean onOptionsItemSelected(MenuItem item) {
382        switch (item.getItemId()) {
383            case R.id.delete_all:
384                ClearCallLogDialog.show(getFragmentManager());
385                return true;
386
387            case R.id.show_outgoing_only:
388                // We only need the phone call receiver when there is an active call type filter.
389                // Not many people may use the filters so don't register the receiver until now .
390                registerPhoneCallReceiver();
391                mCallLogQueryHandler.fetchCalls(Calls.OUTGOING_TYPE);
392                updateFilterTypeAndHeader(Calls.OUTGOING_TYPE);
393                return true;
394
395            case R.id.show_incoming_only:
396                registerPhoneCallReceiver();
397                mCallLogQueryHandler.fetchCalls(Calls.INCOMING_TYPE);
398                updateFilterTypeAndHeader(Calls.INCOMING_TYPE);
399                return true;
400
401            case R.id.show_missed_only:
402                registerPhoneCallReceiver();
403                mCallLogQueryHandler.fetchCalls(Calls.MISSED_TYPE);
404                updateFilterTypeAndHeader(Calls.MISSED_TYPE);
405                return true;
406
407            case R.id.show_voicemails_only:
408                registerPhoneCallReceiver();
409                mCallLogQueryHandler.fetchCalls(Calls.VOICEMAIL_TYPE);
410                updateFilterTypeAndHeader(Calls.VOICEMAIL_TYPE);
411                return true;
412
413            case R.id.show_all_calls:
414                // Filter is being turned off, receiver no longer needed.
415                unregisterPhoneCallReceiver();
416                mCallLogQueryHandler.fetchCalls(CallLogQueryHandler.CALL_TYPE_ALL);
417                updateFilterTypeAndHeader(CallLogQueryHandler.CALL_TYPE_ALL);
418                return true;
419
420            default:
421                return false;
422        }
423    }
424
425    private void updateFilterTypeAndHeader(int filterType) {
426        mCallTypeFilter = filterType;
427
428        switch (filterType) {
429            case CallLogQueryHandler.CALL_TYPE_ALL:
430                mFilterStatusView.setVisibility(View.GONE);
431                break;
432            case Calls.INCOMING_TYPE:
433                showFilterStatus(R.string.call_log_incoming_header);
434                break;
435            case Calls.OUTGOING_TYPE:
436                showFilterStatus(R.string.call_log_outgoing_header);
437                break;
438            case Calls.MISSED_TYPE:
439                showFilterStatus(R.string.call_log_missed_header);
440                break;
441            case Calls.VOICEMAIL_TYPE:
442                showFilterStatus(R.string.call_log_voicemail_header);
443                break;
444        }
445    }
446
447    private void showFilterStatus(int resId) {
448        mFilterStatusView.setText(resId);
449        mFilterStatusView.setVisibility(View.VISIBLE);
450    }
451
452    public void callSelectedEntry() {
453        int position = getListView().getSelectedItemPosition();
454        if (position < 0) {
455            // In touch mode you may often not have something selected, so
456            // just call the first entry to make sure that [send] [send] calls the
457            // most recent entry.
458            position = 0;
459        }
460        final Cursor cursor = (Cursor)mAdapter.getItem(position);
461        if (cursor != null) {
462            String number = cursor.getString(CallLogQuery.NUMBER);
463            if (TextUtils.isEmpty(number)
464                    || number.equals(CallerInfo.UNKNOWN_NUMBER)
465                    || number.equals(CallerInfo.PRIVATE_NUMBER)
466                    || number.equals(CallerInfo.PAYPHONE_NUMBER)) {
467                // This number can't be called, do nothing
468                return;
469            }
470            Intent intent;
471            // If "number" is really a SIP address, construct a sip: URI.
472            if (PhoneNumberUtils.isUriNumber(number)) {
473                intent = ContactsUtils.getCallIntent(
474                        Uri.fromParts(Constants.SCHEME_SIP, number, null));
475            } else {
476                // We're calling a regular PSTN phone number.
477                // Construct a tel: URI, but do some other possible cleanup first.
478                int callType = cursor.getInt(CallLogQuery.CALL_TYPE);
479                if (!number.startsWith("+") &&
480                       (callType == Calls.INCOMING_TYPE
481                                || callType == Calls.MISSED_TYPE)) {
482                    // If the caller-id matches a contact with a better qualified number, use it
483                    String countryIso = cursor.getString(CallLogQuery.COUNTRY_ISO);
484                    number = mAdapter.getBetterNumberFromContacts(number, countryIso);
485                }
486                intent = ContactsUtils.getCallIntent(
487                        Uri.fromParts(Constants.SCHEME_TEL, number, null));
488            }
489            intent.setFlags(
490                    Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
491            startActivity(intent);
492        }
493    }
494
495    @VisibleForTesting
496    CallLogAdapter getAdapter() {
497        return mAdapter;
498    }
499
500    @Override
501    public void setMenuVisibility(boolean menuVisible) {
502        super.setMenuVisibility(menuVisible);
503        if (mMenuVisible != menuVisible) {
504            mMenuVisible = menuVisible;
505            if (!menuVisible) {
506                updateOnExit();
507            } else if (isResumed()) {
508                refreshData();
509            }
510        }
511    }
512
513    /** Requests updates to the data to be shown. */
514    private void refreshData() {
515        // Prevent unnecessary refresh.
516        if (mRefreshDataRequired) {
517            // Mark all entries in the contact info cache as out of date, so they will be looked up
518            // again once being shown.
519            mAdapter.invalidateCache();
520            startCallsQuery();
521            startVoicemailStatusQuery();
522            updateOnEntry();
523            mRefreshDataRequired = false;
524        }
525    }
526
527    /** Removes the missed call notifications. */
528    private void removeMissedCallNotifications() {
529        try {
530            ITelephony telephony =
531                    ITelephony.Stub.asInterface(ServiceManager.getService("phone"));
532            if (telephony != null) {
533                telephony.cancelMissedCallsNotification();
534            } else {
535                Log.w(TAG, "Telephony service is null, can't call " +
536                        "cancelMissedCallsNotification");
537            }
538        } catch (RemoteException e) {
539            Log.e(TAG, "Failed to clear missed calls notification due to remote exception");
540        }
541    }
542
543    /** Updates call data and notification state while leaving the call log tab. */
544    private void updateOnExit() {
545        updateOnTransition(false);
546    }
547
548    /** Updates call data and notification state while entering the call log tab. */
549    private void updateOnEntry() {
550        updateOnTransition(true);
551    }
552
553    private void updateOnTransition(boolean onEntry) {
554        // We don't want to update any call data when keyguard is on because the user has likely not
555        // seen the new calls yet.
556        // This might be called before onCreate() and thus we need to check null explicitly.
557        if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode()) {
558            // On either of the transitions we reset the new flag and update the notifications.
559            // While exiting we additionally consume all missed calls (by marking them as read).
560            // This will ensure that they no more appear in the "new" section when we return back.
561            mCallLogQueryHandler.markNewCallsAsOld();
562            if (!onEntry) {
563                mCallLogQueryHandler.markMissedCallsAsRead();
564            }
565            removeMissedCallNotifications();
566            updateVoicemailNotifications();
567        }
568    }
569
570    private void updateVoicemailNotifications() {
571        Intent serviceIntent = new Intent(getActivity(), CallLogNotificationsService.class);
572        serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_NOTIFICATIONS);
573        getActivity().startService(serviceIntent);
574    }
575
576    /**
577     * Register a phone call filter to reset the call type when a phone call is place.
578     */
579    private void registerPhoneCallReceiver() {
580        if (mPhoneStateListener != null) {
581            return; // Already registered.
582        }
583        mTelephonyManager = (TelephonyManager) getActivity().getSystemService(
584                Context.TELEPHONY_SERVICE);
585        mPhoneStateListener = new PhoneStateListener() {
586            @Override
587            public void onCallStateChanged(int state, String incomingNumber) {
588                if (state != TelephonyManager.CALL_STATE_OFFHOOK &&
589                        state != TelephonyManager.CALL_STATE_RINGING) {
590                    return;
591                }
592                mHandler.post(new Runnable() {
593                    @Override
594                    public void run() {
595                        if (getActivity() == null || getActivity().isFinishing()) {
596                            return;
597                        }
598                        updateFilterTypeAndHeader(CallLogQueryHandler.CALL_TYPE_ALL);
599                    }
600                 });
601            }
602        };
603        mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
604    }
605
606    /**
607     * Un-registers the phone call receiver.
608     */
609    private void unregisterPhoneCallReceiver() {
610        if (mPhoneStateListener != null) {
611            mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
612            mPhoneStateListener = null;
613        }
614    }
615}
616