CallLogFragment.java revision 146a4cdf57f0d4d0cd85e808f1df2bdea639b24c
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.provider.CallLog;
30import android.provider.CallLog.Calls;
31import android.provider.ContactsContract;
32import android.provider.VoicemailContract.Status;
33import android.view.LayoutInflater;
34import android.view.View;
35import android.view.ViewGroup;
36import android.widget.ListView;
37import android.widget.TextView;
38
39import com.android.common.io.MoreCloseables;
40import com.android.contacts.common.CallUtil;
41import com.android.contacts.common.GeoUtil;
42import com.android.contacts.common.util.PhoneNumberHelper;
43import com.android.dialer.R;
44import com.android.dialer.util.EmptyLoader;
45import com.android.dialer.voicemail.VoicemailStatusHelper;
46import com.android.dialer.voicemail.VoicemailStatusHelper.StatusMessage;
47import com.android.dialer.voicemail.VoicemailStatusHelperImpl;
48import com.android.dialerbind.ObjectFactory;
49
50import java.util.List;
51
52/**
53 * Displays a list of call log entries. To filter for a particular kind of call
54 * (all, missed or voicemails), specify it in the constructor.
55 */
56public class CallLogFragment extends ListFragment
57        implements CallLogQueryHandler.Listener, CallLogAdapter.CallFetcher {
58    private static final String TAG = "CallLogFragment";
59
60    /**
61     * ID of the empty loader to defer other fragments.
62     */
63    private static final int EMPTY_LOADER_ID = 0;
64
65    private CallLogAdapter mAdapter;
66    private CallLogQueryHandler mCallLogQueryHandler;
67    private boolean mScrollToTop;
68
69    /** Whether there is at least one voicemail source installed. */
70    private boolean mVoicemailSourcesAvailable = false;
71
72    private VoicemailStatusHelper mVoicemailStatusHelper;
73    private View mStatusMessageView;
74    private TextView mStatusMessageText;
75    private TextView mStatusMessageAction;
76    private KeyguardManager mKeyguardManager;
77    private View mFooterView;
78
79    private boolean mEmptyLoaderRunning;
80    private boolean mCallLogFetched;
81    private boolean mVoicemailStatusFetched;
82
83    private final Handler mHandler = new Handler();
84
85    private class CustomContentObserver extends ContentObserver {
86        public CustomContentObserver() {
87            super(mHandler);
88        }
89        @Override
90        public void onChange(boolean selfChange) {
91            mRefreshDataRequired = true;
92        }
93    }
94
95    // See issue 6363009
96    private final ContentObserver mCallLogObserver = new CustomContentObserver();
97    private final ContentObserver mContactsObserver = new CustomContentObserver();
98    private final ContentObserver mVoicemailStatusObserver = new CustomContentObserver();
99    private boolean mRefreshDataRequired = true;
100
101    // Exactly same variable is in Fragment as a package private.
102    private boolean mMenuVisible = true;
103
104    // Default to all calls.
105    private int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL;
106
107    // Log limit - if no limit is specified, then the default in {@link CallLogQueryHandler}
108    // will be used.
109    private int mLogLimit = -1;
110
111    // Date limit (in millis since epoch) - when non-zero, only calls which occurred on or after
112    // the date filter are included.  If zero, no date-based filtering occurs.
113    private long mDateLimit = 0;
114
115    public CallLogFragment() {
116        this(CallLogQueryHandler.CALL_TYPE_ALL, -1);
117    }
118
119    public CallLogFragment(int filterType) {
120        this(filterType, -1);
121    }
122
123    public CallLogFragment(int filterType, int logLimit) {
124        super();
125        mCallTypeFilter = filterType;
126        mLogLimit = logLimit;
127    }
128
129    /**
130     * Creates a call log fragment, filtering to include only calls of the desired type, occurring
131     * after the specified date.
132     * @param filterType type of calls to include.
133     * @param dateLimit limits results to calls occurring on or after the specified date.
134     */
135    public CallLogFragment(int filterType, long dateLimit) {
136        this(filterType, -1, dateLimit);
137    }
138
139    /**
140     * Creates a call log fragment, filtering to include only calls of the desired type, occurring
141     * after the specified date.  Also provides a means to limit the number of results returned.
142     * @param filterType type of calls to include.
143     * @param logLimit limits the number of results to return.
144     * @param dateLimit limits results to calls occurring on or after the specified date.
145     */
146    public CallLogFragment(int filterType, int logLimit, long dateLimit) {
147        this(filterType, logLimit);
148        mDateLimit = dateLimit;
149    }
150
151    @Override
152    public void onCreate(Bundle state) {
153        super.onCreate(state);
154
155        String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
156        mAdapter = ObjectFactory.newCallLogAdapter(getActivity(), this, new ContactInfoHelper(
157                getActivity(), currentCountryIso), true);
158        setListAdapter(mAdapter);
159        mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(),
160                this, mLogLimit);
161        mKeyguardManager =
162                (KeyguardManager) getActivity().getSystemService(Context.KEYGUARD_SERVICE);
163        getActivity().getContentResolver().registerContentObserver(CallLog.CONTENT_URI, true,
164                mCallLogObserver);
165        getActivity().getContentResolver().registerContentObserver(
166                ContactsContract.Contacts.CONTENT_URI, true, mContactsObserver);
167        getActivity().getContentResolver().registerContentObserver(
168                Status.CONTENT_URI, true, mVoicemailStatusObserver);
169        setHasOptionsMenu(true);
170        updateCallList(mCallTypeFilter, mDateLimit);
171    }
172
173    /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */
174    @Override
175    public void onCallsFetched(Cursor cursor) {
176        if (getActivity() == null || getActivity().isFinishing()) {
177            return;
178        }
179        mAdapter.setLoading(false);
180        mAdapter.changeCursor(cursor);
181        // This will update the state of the "Clear call log" menu item.
182        getActivity().invalidateOptionsMenu();
183        if (mScrollToTop) {
184            final ListView listView = getListView();
185            // The smooth-scroll animation happens over a fixed time period.
186            // As a result, if it scrolls through a large portion of the list,
187            // each frame will jump so far from the previous one that the user
188            // will not experience the illusion of downward motion.  Instead,
189            // if we're not already near the top of the list, we instantly jump
190            // near the top, and animate from there.
191            if (listView.getFirstVisiblePosition() > 5) {
192                listView.setSelection(5);
193            }
194            // Workaround for framework issue: the smooth-scroll doesn't
195            // occur if setSelection() is called immediately before.
196            mHandler.post(new Runnable() {
197               @Override
198               public void run() {
199                   if (getActivity() == null || getActivity().isFinishing()) {
200                       return;
201                   }
202                   listView.smoothScrollToPosition(0);
203               }
204            });
205
206            mScrollToTop = false;
207        }
208        mCallLogFetched = true;
209        destroyEmptyLoaderIfAllDataFetched();
210    }
211
212    /**
213     * Called by {@link CallLogQueryHandler} after a successful query to voicemail status provider.
214     */
215    @Override
216    public void onVoicemailStatusFetched(Cursor statusCursor) {
217        if (getActivity() == null || getActivity().isFinishing()) {
218            return;
219        }
220        updateVoicemailStatusMessage(statusCursor);
221
222        int activeSources = mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor);
223        setVoicemailSourcesAvailable(activeSources != 0);
224        MoreCloseables.closeQuietly(statusCursor);
225        mVoicemailStatusFetched = true;
226        destroyEmptyLoaderIfAllDataFetched();
227    }
228
229    private void destroyEmptyLoaderIfAllDataFetched() {
230        if (mCallLogFetched && mVoicemailStatusFetched && mEmptyLoaderRunning) {
231            mEmptyLoaderRunning = false;
232            getLoaderManager().destroyLoader(EMPTY_LOADER_ID);
233        }
234    }
235
236    /** Sets whether there are any voicemail sources available in the platform. */
237    private void setVoicemailSourcesAvailable(boolean voicemailSourcesAvailable) {
238        if (mVoicemailSourcesAvailable == voicemailSourcesAvailable) return;
239        mVoicemailSourcesAvailable = voicemailSourcesAvailable;
240
241        Activity activity = getActivity();
242        if (activity != null) {
243            // This is so that the options menu content is updated.
244            activity.invalidateOptionsMenu();
245        }
246    }
247
248    @Override
249    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
250        View view = inflater.inflate(R.layout.call_log_fragment, container, false);
251        mVoicemailStatusHelper = new VoicemailStatusHelperImpl();
252        mStatusMessageView = view.findViewById(R.id.voicemail_status);
253        mStatusMessageText = (TextView) view.findViewById(R.id.voicemail_status_message);
254        mStatusMessageAction = (TextView) view.findViewById(R.id.voicemail_status_action);
255
256        return view;
257    }
258
259    @Override
260    public void onViewCreated(View view, Bundle savedInstanceState) {
261        super.onViewCreated(view, savedInstanceState);
262        updateEmptyMessage(mCallTypeFilter);
263        getListView().setItemsCanFocus(true);
264        assignFooterViewToListView();
265    }
266
267    /**
268     * Based on the new intent, decide whether the list should be configured
269     * to scroll up to display the first item.
270     */
271    public void configureScreenFromIntent(Intent newIntent) {
272        // Typically, when switching to the call-log we want to show the user
273        // the same section of the list that they were most recently looking
274        // at.  However, under some circumstances, we want to automatically
275        // scroll to the top of the list to present the newest call items.
276        // For example, immediately after a call is finished, we want to
277        // display information about that call.
278        mScrollToTop = Calls.CONTENT_TYPE.equals(newIntent.getType());
279    }
280
281    @Override
282    public void onStart() {
283        // Start the empty loader now to defer other fragments.  We destroy it when both calllog
284        // and the voicemail status are fetched.
285        getLoaderManager().initLoader(EMPTY_LOADER_ID, null,
286                new EmptyLoader.Callback(getActivity()));
287        mEmptyLoaderRunning = true;
288        super.onStart();
289    }
290
291    @Override
292    public void onResume() {
293        super.onResume();
294        refreshData();
295    }
296
297    private void updateVoicemailStatusMessage(Cursor statusCursor) {
298        List<StatusMessage> messages = mVoicemailStatusHelper.getStatusMessages(statusCursor);
299        if (messages.size() == 0) {
300            mStatusMessageView.setVisibility(View.GONE);
301        } else {
302            mStatusMessageView.setVisibility(View.VISIBLE);
303            // TODO: Change the code to show all messages. For now just pick the first message.
304            final StatusMessage message = messages.get(0);
305            if (message.showInCallLog()) {
306                mStatusMessageText.setText(message.callLogMessageId);
307            }
308            if (message.actionMessageId != -1) {
309                mStatusMessageAction.setText(message.actionMessageId);
310            }
311            if (message.actionUri != null) {
312                mStatusMessageAction.setVisibility(View.VISIBLE);
313                mStatusMessageAction.setOnClickListener(new View.OnClickListener() {
314                    @Override
315                    public void onClick(View v) {
316                        getActivity().startActivity(
317                                new Intent(Intent.ACTION_VIEW, message.actionUri));
318                    }
319                });
320            } else {
321                mStatusMessageAction.setVisibility(View.GONE);
322            }
323        }
324    }
325
326    @Override
327    public void onPause() {
328        super.onPause();
329        // Kill the requests thread
330        mAdapter.stopRequestProcessing();
331    }
332
333    @Override
334    public void onStop() {
335        super.onStop();
336        updateOnExit();
337    }
338
339    @Override
340    public void onDestroy() {
341        super.onDestroy();
342        mAdapter.stopRequestProcessing();
343        mAdapter.changeCursor(null);
344        getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver);
345        getActivity().getContentResolver().unregisterContentObserver(mContactsObserver);
346        getActivity().getContentResolver().unregisterContentObserver(mVoicemailStatusObserver);
347    }
348
349    @Override
350    public void fetchCalls() {
351        mCallLogQueryHandler.fetchCalls(mCallTypeFilter, mDateLimit);
352    }
353
354    public void startCallsQuery() {
355        mAdapter.setLoading(true);
356        mCallLogQueryHandler.fetchCalls(mCallTypeFilter, mDateLimit);
357    }
358
359    private void startVoicemailStatusQuery() {
360        mCallLogQueryHandler.fetchVoicemailStatus();
361    }
362
363    private void updateCallList(int filterType, long dateLimit) {
364        mCallLogQueryHandler.fetchCalls(filterType, dateLimit);
365    }
366
367    private void updateEmptyMessage(int filterType) {
368        final String message;
369        switch (filterType) {
370            case Calls.MISSED_TYPE:
371                message = getString(R.string.recentMissed_empty);
372                break;
373            case Calls.VOICEMAIL_TYPE:
374                message = getString(R.string.recentVoicemails_empty);
375                break;
376            case CallLogQueryHandler.CALL_TYPE_ALL:
377                message = getString(R.string.recentCalls_empty);
378                break;
379            default:
380                throw new IllegalArgumentException("Unexpected filter type in CallLogFragment: "
381                        + filterType);
382        }
383        ((TextView) getListView().getEmptyView()).setText(message);
384    }
385
386    public void callSelectedEntry() {
387        int position = getListView().getSelectedItemPosition();
388        if (position < 0) {
389            // In touch mode you may often not have something selected, so
390            // just call the first entry to make sure that [send] [send] calls the
391            // most recent entry.
392            position = 0;
393        }
394        final Cursor cursor = (Cursor)mAdapter.getItem(position);
395        if (cursor != null) {
396            String number = cursor.getString(CallLogQuery.NUMBER);
397            int numberPresentation = cursor.getInt(CallLogQuery.NUMBER_PRESENTATION);
398            if (!PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation)) {
399                // This number can't be called, do nothing
400                return;
401            }
402            Intent intent;
403            // If "number" is really a SIP address, construct a sip: URI.
404            if (PhoneNumberHelper.isUriNumber(number)) {
405                intent = CallUtil.getCallIntent(
406                        Uri.fromParts(CallUtil.SCHEME_SIP, number, null));
407            } else {
408                // We're calling a regular PSTN phone number.
409                // Construct a tel: URI, but do some other possible cleanup first.
410                int callType = cursor.getInt(CallLogQuery.CALL_TYPE);
411                if (!number.startsWith("+") &&
412                       (callType == Calls.INCOMING_TYPE
413                                || callType == Calls.MISSED_TYPE)) {
414                    // If the caller-id matches a contact with a better qualified number, use it
415                    String countryIso = cursor.getString(CallLogQuery.COUNTRY_ISO);
416                    number = mAdapter.getBetterNumberFromContacts(number, countryIso);
417                }
418                intent = CallUtil.getCallIntent(
419                        Uri.fromParts(CallUtil.SCHEME_TEL, number, null));
420            }
421            intent.setFlags(
422                    Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
423            startActivity(intent);
424        }
425    }
426
427    CallLogAdapter getAdapter() {
428        return mAdapter;
429    }
430
431    @Override
432    public void setMenuVisibility(boolean menuVisible) {
433        super.setMenuVisibility(menuVisible);
434        if (mMenuVisible != menuVisible) {
435            mMenuVisible = menuVisible;
436            if (!menuVisible) {
437                updateOnExit();
438            } else if (isResumed()) {
439                refreshData();
440            }
441        }
442    }
443
444    /** Requests updates to the data to be shown. */
445    private void refreshData() {
446        // Prevent unnecessary refresh.
447        if (mRefreshDataRequired) {
448            // Mark all entries in the contact info cache as out of date, so they will be looked up
449            // again once being shown.
450            mAdapter.invalidateCache();
451            startCallsQuery();
452            startVoicemailStatusQuery();
453            updateOnEntry();
454            mRefreshDataRequired = false;
455        }
456    }
457
458    /** Updates call data and notification state while leaving the call log tab. */
459    private void updateOnExit() {
460        updateOnTransition(false);
461    }
462
463    /** Updates call data and notification state while entering the call log tab. */
464    private void updateOnEntry() {
465        updateOnTransition(true);
466    }
467
468    // TODO: Move to CallLogActivity
469    private void updateOnTransition(boolean onEntry) {
470        // We don't want to update any call data when keyguard is on because the user has likely not
471        // seen the new calls yet.
472        // This might be called before onCreate() and thus we need to check null explicitly.
473        if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode()) {
474            // On either of the transitions we update the missed call and voicemail notifications.
475            // While exiting we additionally consume all missed calls (by marking them as read).
476            mCallLogQueryHandler.markNewCallsAsOld();
477            if (!onEntry) {
478                mCallLogQueryHandler.markMissedCallsAsRead();
479            }
480            CallLogNotificationsHelper.removeMissedCallNotifications();
481            CallLogNotificationsHelper.updateVoicemailNotifications(getActivity());
482        }
483    }
484
485    /**
486     * Assigns a view to be used as a footer view in the call log.
487     *
488     * @param view View to be used as the footer view.
489     */
490    public void setFooterView(View view) {
491        mFooterView = view;
492        // Content view not created yet, defer addition of the footerview to onCreate
493        if (getView() == null) {
494            return;
495        }
496        assignFooterViewToListView();
497    }
498
499    private void assignFooterViewToListView() {
500        if (mFooterView == null) {
501            return;
502        }
503        final ListView listView = getListView();
504        listView.removeFooterView(mFooterView);
505        listView.addFooterView(mFooterView);
506    }
507}
508