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