CallLogFragment.java revision d0cabd3a4ea4ae4dd7baa7e2e28610b1d969fb02
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.text.TextUtils;
36import android.util.Log;
37import android.view.LayoutInflater;
38import android.view.Menu;
39import android.view.MenuInflater;
40import android.view.MenuItem;
41import android.view.View;
42import android.view.ViewGroup;
43import android.widget.ListView;
44import android.widget.TextView;
45
46import com.android.common.io.MoreCloseables;
47import com.android.contacts.common.CallUtil;
48import com.android.contacts.ContactsUtils;
49import com.android.contacts.R;
50import com.android.dialer.util.EmptyLoader;
51import com.android.dialer.voicemail.VoicemailStatusHelper;
52import com.android.dialer.voicemail.VoicemailStatusHelper.StatusMessage;
53import com.android.dialer.voicemail.VoicemailStatusHelperImpl;
54import com.android.internal.telephony.CallerInfo;
55import com.android.internal.telephony.ITelephony;
56import com.google.common.annotations.VisibleForTesting;
57
58import java.util.List;
59
60/**
61 * Displays a list of call log entries.
62 */
63public class CallLogFragment extends ListFragment
64        implements CallLogQueryHandler.Listener, CallLogAdapter.CallFetcher {
65    private static final String TAG = "CallLogFragment";
66
67    /**
68     * ID of the empty loader to defer other fragments.
69     */
70    private static final int EMPTY_LOADER_ID = 0;
71
72    private CallLogAdapter mAdapter;
73    private CallLogQueryHandler mCallLogQueryHandler;
74    private boolean mScrollToTop;
75
76    /** Whether there is at least one voicemail source installed. */
77    private boolean mVoicemailSourcesAvailable = false;
78    /** Whether we are currently filtering over voicemail. */
79    private boolean mShowingVoicemailOnly = 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 class CustomContentObserver extends ContentObserver {
95        public CustomContentObserver() {
96            super(mHandler);
97        }
98        @Override
99        public void onChange(boolean selfChange) {
100            mRefreshDataRequired = true;
101        }
102    }
103
104    // See issue 6363009
105    private final ContentObserver mCallLogObserver = new CustomContentObserver();
106    private final ContentObserver mContactsObserver = new CustomContentObserver();
107    private boolean mRefreshDataRequired = true;
108
109    // Exactly same variable is in Fragment as a package private.
110    private boolean mMenuVisible = true;
111
112    // Default to all calls.
113    private int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL;
114
115    @Override
116    public void onCreate(Bundle state) {
117        super.onCreate(state);
118
119        mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(), this);
120        mKeyguardManager =
121                (KeyguardManager) getActivity().getSystemService(Context.KEYGUARD_SERVICE);
122        getActivity().getContentResolver().registerContentObserver(
123                CallLog.CONTENT_URI, true, mCallLogObserver);
124        getActivity().getContentResolver().registerContentObserver(
125                ContactsContract.Contacts.CONTENT_URI, true, mContactsObserver);
126        setHasOptionsMenu(true);
127    }
128
129    /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */
130    @Override
131    public void onCallsFetched(Cursor cursor) {
132        if (getActivity() == null || getActivity().isFinishing()) {
133            return;
134        }
135        mAdapter.setLoading(false);
136        mAdapter.changeCursor(cursor);
137        // This will update the state of the "Clear call log" menu item.
138        getActivity().invalidateOptionsMenu();
139        if (mScrollToTop) {
140            final ListView listView = getListView();
141            // The smooth-scroll animation happens over a fixed time period.
142            // As a result, if it scrolls through a large portion of the list,
143            // each frame will jump so far from the previous one that the user
144            // will not experience the illusion of downward motion.  Instead,
145            // if we're not already near the top of the list, we instantly jump
146            // near the top, and animate from there.
147            if (listView.getFirstVisiblePosition() > 5) {
148                listView.setSelection(5);
149            }
150            // Workaround for framework issue: the smooth-scroll doesn't
151            // occur if setSelection() is called immediately before.
152            mHandler.post(new Runnable() {
153               @Override
154               public void run() {
155                   if (getActivity() == null || getActivity().isFinishing()) {
156                       return;
157                   }
158                   listView.smoothScrollToPosition(0);
159               }
160            });
161
162            mScrollToTop = false;
163        }
164        mCallLogFetched = true;
165        destroyEmptyLoaderIfAllDataFetched();
166    }
167
168    /**
169     * Called by {@link CallLogQueryHandler} after a successful query to voicemail status provider.
170     */
171    @Override
172    public void onVoicemailStatusFetched(Cursor statusCursor) {
173        if (getActivity() == null || getActivity().isFinishing()) {
174            return;
175        }
176        updateVoicemailStatusMessage(statusCursor);
177
178        int activeSources = mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor);
179        setVoicemailSourcesAvailable(activeSources != 0);
180        MoreCloseables.closeQuietly(statusCursor);
181        mVoicemailStatusFetched = true;
182        destroyEmptyLoaderIfAllDataFetched();
183    }
184
185    private void destroyEmptyLoaderIfAllDataFetched() {
186        if (mCallLogFetched && mVoicemailStatusFetched && mEmptyLoaderRunning) {
187            mEmptyLoaderRunning = false;
188            getLoaderManager().destroyLoader(EMPTY_LOADER_ID);
189        }
190    }
191
192    /** Sets whether there are any voicemail sources available in the platform. */
193    private void setVoicemailSourcesAvailable(boolean voicemailSourcesAvailable) {
194        if (mVoicemailSourcesAvailable == voicemailSourcesAvailable) return;
195        mVoicemailSourcesAvailable = voicemailSourcesAvailable;
196
197        Activity activity = getActivity();
198        if (activity != null) {
199            // This is so that the options menu content is updated.
200            activity.invalidateOptionsMenu();
201        }
202    }
203
204    @Override
205    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
206        View view = inflater.inflate(R.layout.call_log_fragment, container, false);
207        mVoicemailStatusHelper = new VoicemailStatusHelperImpl();
208        mStatusMessageView = view.findViewById(R.id.voicemail_status);
209        mStatusMessageText = (TextView) view.findViewById(R.id.voicemail_status_message);
210        mStatusMessageAction = (TextView) view.findViewById(R.id.voicemail_status_action);
211        mFilterStatusView = (TextView) view.findViewById(R.id.filter_status);
212        return view;
213    }
214
215    @Override
216    public void onViewCreated(View view, Bundle savedInstanceState) {
217        super.onViewCreated(view, savedInstanceState);
218        String currentCountryIso = ContactsUtils.getCurrentCountryIso(getActivity());
219        mAdapter = new CallLogAdapter(getActivity(), this,
220                new ContactInfoHelper(getActivity(), currentCountryIso));
221        setListAdapter(mAdapter);
222        getListView().setItemsCanFocus(true);
223
224        updateFilterHeader();
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    }
307
308    @Override
309    public void fetchCalls() {
310        mCallLogQueryHandler.fetchCalls(mCallTypeFilter);
311    }
312
313    public void startCallsQuery() {
314        mAdapter.setLoading(true);
315        mCallLogQueryHandler.fetchCalls(mCallTypeFilter);
316        if (mShowingVoicemailOnly) {
317            mShowingVoicemailOnly = false;
318            getActivity().invalidateOptionsMenu();
319        }
320    }
321
322    private void startVoicemailStatusQuery() {
323        mCallLogQueryHandler.fetchVoicemailStatus();
324    }
325
326    @Override
327    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
328        super.onCreateOptionsMenu(menu, inflater);
329        inflater.inflate(R.menu.call_log_options, menu);
330    }
331
332    @Override
333    public void onPrepareOptionsMenu(Menu menu) {
334        final MenuItem itemDeleteAll = menu.findItem(R.id.delete_all);
335        // Check if all the menu items are inflated correctly. As a shortcut, we assume all
336        // menu items are ready if the first item is non-null.
337        if (itemDeleteAll != null) {
338            itemDeleteAll.setEnabled(mAdapter != null && !mAdapter.isEmpty());
339            menu.findItem(R.id.show_voicemails_only).setVisible(mVoicemailSourcesAvailable);
340        }
341    }
342
343    @Override
344    public boolean onOptionsItemSelected(MenuItem item) {
345        switch (item.getItemId()) {
346            case R.id.delete_all:
347                ClearCallLogDialog.show(getFragmentManager());
348                return true;
349
350            case R.id.show_outgoing_only:
351                mCallLogQueryHandler.fetchCalls(Calls.OUTGOING_TYPE);
352                mCallTypeFilter = Calls.OUTGOING_TYPE;
353                updateFilterHeader();
354                return true;
355
356            case R.id.show_incoming_only:
357                mCallLogQueryHandler.fetchCalls(Calls.INCOMING_TYPE);
358                mCallTypeFilter = Calls.INCOMING_TYPE;
359                updateFilterHeader();
360                return true;
361
362            case R.id.show_missed_only:
363                mCallLogQueryHandler.fetchCalls(Calls.MISSED_TYPE);
364                mCallTypeFilter = Calls.MISSED_TYPE;
365                updateFilterHeader();
366                return true;
367
368            case R.id.show_voicemails_only:
369                mCallLogQueryHandler.fetchCalls(Calls.VOICEMAIL_TYPE);
370                mCallTypeFilter = Calls.VOICEMAIL_TYPE;
371                updateFilterHeader();
372                mShowingVoicemailOnly = true;
373                return true;
374
375            case R.id.show_all_calls:
376                mCallLogQueryHandler.fetchCalls(CallLogQueryHandler.CALL_TYPE_ALL);
377                mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL;
378                updateFilterHeader();
379                mShowingVoicemailOnly = false;
380                return true;
381
382            default:
383                return false;
384        }
385    }
386
387    private void updateFilterHeader() {
388        switch (mCallTypeFilter) {
389            case CallLogQueryHandler.CALL_TYPE_ALL:
390                mFilterStatusView.setVisibility(View.GONE);
391                break;
392            case Calls.INCOMING_TYPE:
393                showFilterStatus(R.string.call_log_incoming_header);
394                break;
395            case Calls.OUTGOING_TYPE:
396                showFilterStatus(R.string.call_log_outgoing_header);
397                break;
398            case Calls.MISSED_TYPE:
399                showFilterStatus(R.string.call_log_missed_header);
400                break;
401            case Calls.VOICEMAIL_TYPE:
402                showFilterStatus(R.string.call_log_voicemail_header);
403                break;
404        }
405    }
406
407    private void showFilterStatus(int resId) {
408        mFilterStatusView.setText(resId);
409        mFilterStatusView.setVisibility(View.VISIBLE);
410    }
411
412    public void callSelectedEntry() {
413        int position = getListView().getSelectedItemPosition();
414        if (position < 0) {
415            // In touch mode you may often not have something selected, so
416            // just call the first entry to make sure that [send] [send] calls the
417            // most recent entry.
418            position = 0;
419        }
420        final Cursor cursor = (Cursor)mAdapter.getItem(position);
421        if (cursor != null) {
422            String number = cursor.getString(CallLogQuery.NUMBER);
423            if (TextUtils.isEmpty(number)
424                    || number.equals(CallerInfo.UNKNOWN_NUMBER)
425                    || number.equals(CallerInfo.PRIVATE_NUMBER)
426                    || number.equals(CallerInfo.PAYPHONE_NUMBER)) {
427                // This number can't be called, do nothing
428                return;
429            }
430            Intent intent;
431            // If "number" is really a SIP address, construct a sip: URI.
432            if (PhoneNumberUtils.isUriNumber(number)) {
433                intent = CallUtil.getCallIntent(
434                        Uri.fromParts(CallUtil.SCHEME_SIP, number, null));
435            } else {
436                // We're calling a regular PSTN phone number.
437                // Construct a tel: URI, but do some other possible cleanup first.
438                int callType = cursor.getInt(CallLogQuery.CALL_TYPE);
439                if (!number.startsWith("+") &&
440                       (callType == Calls.INCOMING_TYPE
441                                || callType == Calls.MISSED_TYPE)) {
442                    // If the caller-id matches a contact with a better qualified number, use it
443                    String countryIso = cursor.getString(CallLogQuery.COUNTRY_ISO);
444                    number = mAdapter.getBetterNumberFromContacts(number, countryIso);
445                }
446                intent = CallUtil.getCallIntent(
447                        Uri.fromParts(CallUtil.SCHEME_TEL, number, null));
448            }
449            intent.setFlags(
450                    Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
451            startActivity(intent);
452        }
453    }
454
455    @VisibleForTesting
456    CallLogAdapter getAdapter() {
457        return mAdapter;
458    }
459
460    @Override
461    public void setMenuVisibility(boolean menuVisible) {
462        super.setMenuVisibility(menuVisible);
463        if (mMenuVisible != menuVisible) {
464            mMenuVisible = menuVisible;
465            if (!menuVisible) {
466                updateOnExit();
467            } else if (isResumed()) {
468                refreshData();
469            }
470        }
471    }
472
473    /** Requests updates to the data to be shown. */
474    private void refreshData() {
475        // Prevent unnecessary refresh.
476        if (mRefreshDataRequired) {
477            // Mark all entries in the contact info cache as out of date, so they will be looked up
478            // again once being shown.
479            mAdapter.invalidateCache();
480            startCallsQuery();
481            startVoicemailStatusQuery();
482            updateOnEntry();
483            mRefreshDataRequired = false;
484        }
485    }
486
487    /** Removes the missed call notifications. */
488    private void removeMissedCallNotifications() {
489        try {
490            ITelephony telephony =
491                    ITelephony.Stub.asInterface(ServiceManager.getService("phone"));
492            if (telephony != null) {
493                telephony.cancelMissedCallsNotification();
494            } else {
495                Log.w(TAG, "Telephony service is null, can't call " +
496                        "cancelMissedCallsNotification");
497            }
498        } catch (RemoteException e) {
499            Log.e(TAG, "Failed to clear missed calls notification due to remote exception");
500        }
501    }
502
503    /** Updates call data and notification state while leaving the call log tab. */
504    private void updateOnExit() {
505        updateOnTransition(false);
506    }
507
508    /** Updates call data and notification state while entering the call log tab. */
509    private void updateOnEntry() {
510        updateOnTransition(true);
511    }
512
513    private void updateOnTransition(boolean onEntry) {
514        // We don't want to update any call data when keyguard is on because the user has likely not
515        // seen the new calls yet.
516        // This might be called before onCreate() and thus we need to check null explicitly.
517        if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode()) {
518            // On either of the transitions we reset the new flag and update the notifications.
519            // While exiting we additionally consume all missed calls (by marking them as read).
520            // This will ensure that they no more appear in the "new" section when we return back.
521            mCallLogQueryHandler.markNewCallsAsOld();
522            if (!onEntry) {
523                mCallLogQueryHandler.markMissedCallsAsRead();
524            }
525            removeMissedCallNotifications();
526            updateVoicemailNotifications();
527        }
528    }
529
530    private void updateVoicemailNotifications() {
531        Intent serviceIntent = new Intent(getActivity(), CallLogNotificationsService.class);
532        serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_NOTIFICATIONS);
533        getActivity().startService(serviceIntent);
534    }
535}
536