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