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