CallLogFragment.java revision 2f02c8a8d840df38a09b55525a784dc666f622eb
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.telephony.TelephonyManager;
34import android.view.LayoutInflater;
35import android.view.View;
36import android.view.ViewGroup;
37import android.widget.ListView;
38import android.widget.TextView;
39
40import com.android.common.io.MoreCloseables;
41import com.android.contacts.common.CallUtil;
42import com.android.contacts.common.GeoUtil;
43import com.android.contacts.common.util.PhoneNumberHelper;
44import com.android.dialer.R;
45import com.android.dialer.util.EmptyLoader;
46import com.android.dialer.voicemail.VoicemailStatusHelper;
47import com.android.dialer.voicemail.VoicemailStatusHelper.StatusMessage;
48import com.android.dialer.voicemail.VoicemailStatusHelperImpl;
49import com.android.dialerbind.ObjectFactory;
50
51import java.util.List;
52
53/**
54 * Displays a list of call log entries. To filter for a particular kind of call
55 * (all, missed or voicemails), specify it in the constructor.
56 */
57public class CallLogFragment extends ListFragment
58        implements CallLogQueryHandler.Listener, CallLogAdapter.CallFetcher {
59    private static final String TAG = "CallLogFragment";
60
61    /**
62     * ID of the empty loader to defer other fragments.
63     */
64    private static final int EMPTY_LOADER_ID = 0;
65
66    private CallLogAdapter mAdapter;
67    private CallLogQueryHandler mCallLogQueryHandler;
68    private boolean mScrollToTop;
69
70    /** Whether there is at least one voicemail source installed. */
71    private boolean mVoicemailSourcesAvailable = false;
72
73    private VoicemailStatusHelper mVoicemailStatusHelper;
74    private View mStatusMessageView;
75    private TextView mStatusMessageText;
76    private TextView mStatusMessageAction;
77    private KeyguardManager mKeyguardManager;
78
79    private boolean mEmptyLoaderRunning;
80    private boolean mCallLogFetched;
81    private boolean mVoicemailStatusFetched;
82
83    private final Handler mHandler = new Handler();
84
85    private TelephonyManager mTelephonyManager;
86
87    private class CustomContentObserver extends ContentObserver {
88        public CustomContentObserver() {
89            super(mHandler);
90        }
91        @Override
92        public void onChange(boolean selfChange) {
93            mRefreshDataRequired = true;
94        }
95    }
96
97    // See issue 6363009
98    private final ContentObserver mCallLogObserver = new CustomContentObserver();
99    private final ContentObserver mContactsObserver = new CustomContentObserver();
100    private final ContentObserver mVoicemailStatusObserver = new CustomContentObserver();
101    private boolean mRefreshDataRequired = true;
102
103    // Exactly same variable is in Fragment as a package private.
104    private boolean mMenuVisible = true;
105
106    // Default to all calls.
107    private int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL;
108
109    // Log limit - if no limit is specified, then the default in {@link CallLogQueryHandler}
110    // will be used.
111    private int mLogLimit = -1;
112
113    // Date limit (in millis since epoch) - when non-zero, only calls which occurred on or after
114    // the date filter are included.  If zero, no date-based filtering occurs.
115    private long mDateLimit = 0;
116
117    public CallLogFragment() {
118        this(CallLogQueryHandler.CALL_TYPE_ALL, -1);
119    }
120
121    public CallLogFragment(int filterType) {
122        this(filterType, -1);
123    }
124
125    public CallLogFragment(int filterType, int logLimit) {
126        super();
127        mCallTypeFilter = filterType;
128        mLogLimit = logLimit;
129    }
130
131    /**
132     * Creates a call log fragment, filtering to include only calls of the desired type, occurring
133     * after the specified date.
134     * @param filterType type of calls to include.
135     * @param dateLimit limits results to calls occurring on or after the specified date.
136     */
137    public CallLogFragment(int filterType, long dateLimit) {
138        this(filterType, -1, dateLimit);
139    }
140
141    /**
142     * Creates a call log fragment, filtering to include only calls of the desired type, occurring
143     * after the specified date.  Also provides a means to limit the number of results returned.
144     * @param filterType type of calls to include.
145     * @param logLimit limits the number of results to return.
146     * @param dateLimit limits results to calls occurring on or after the specified date.
147     */
148    public CallLogFragment(int filterType, int logLimit, long dateLimit) {
149        this(filterType, logLimit);
150        mDateLimit = dateLimit;
151    }
152
153    @Override
154    public void onCreate(Bundle state) {
155        super.onCreate(state);
156
157        mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(),
158                this, mLogLimit);
159        mKeyguardManager =
160                (KeyguardManager) getActivity().getSystemService(Context.KEYGUARD_SERVICE);
161        getActivity().getContentResolver().registerContentObserver(CallLog.CONTENT_URI, true,
162                mCallLogObserver);
163        getActivity().getContentResolver().registerContentObserver(
164                ContactsContract.Contacts.CONTENT_URI, true, mContactsObserver);
165        getActivity().getContentResolver().registerContentObserver(
166                Status.CONTENT_URI, true, mVoicemailStatusObserver);
167        setHasOptionsMenu(true);
168        updateCallList(mCallTypeFilter, mDateLimit);
169    }
170
171    /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */
172    @Override
173    public void onCallsFetched(Cursor cursor) {
174        if (getActivity() == null || getActivity().isFinishing()) {
175            return;
176        }
177        mAdapter.setLoading(false);
178        mAdapter.changeCursor(cursor);
179        // This will update the state of the "Clear call log" menu item.
180        getActivity().invalidateOptionsMenu();
181        if (mScrollToTop) {
182            final ListView listView = getListView();
183            // The smooth-scroll animation happens over a fixed time period.
184            // As a result, if it scrolls through a large portion of the list,
185            // each frame will jump so far from the previous one that the user
186            // will not experience the illusion of downward motion.  Instead,
187            // if we're not already near the top of the list, we instantly jump
188            // near the top, and animate from there.
189            if (listView.getFirstVisiblePosition() > 5) {
190                listView.setSelection(5);
191            }
192            // Workaround for framework issue: the smooth-scroll doesn't
193            // occur if setSelection() is called immediately before.
194            mHandler.post(new Runnable() {
195               @Override
196               public void run() {
197                   if (getActivity() == null || getActivity().isFinishing()) {
198                       return;
199                   }
200                   listView.smoothScrollToPosition(0);
201               }
202            });
203
204            mScrollToTop = false;
205        }
206        mCallLogFetched = true;
207        destroyEmptyLoaderIfAllDataFetched();
208    }
209
210    /**
211     * Called by {@link CallLogQueryHandler} after a successful query to voicemail status provider.
212     */
213    @Override
214    public void onVoicemailStatusFetched(Cursor statusCursor) {
215        if (getActivity() == null || getActivity().isFinishing()) {
216            return;
217        }
218        updateVoicemailStatusMessage(statusCursor);
219
220        int activeSources = mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor);
221        setVoicemailSourcesAvailable(activeSources != 0);
222        MoreCloseables.closeQuietly(statusCursor);
223        mVoicemailStatusFetched = true;
224        destroyEmptyLoaderIfAllDataFetched();
225    }
226
227    private void destroyEmptyLoaderIfAllDataFetched() {
228        if (mCallLogFetched && mVoicemailStatusFetched && mEmptyLoaderRunning) {
229            mEmptyLoaderRunning = false;
230            getLoaderManager().destroyLoader(EMPTY_LOADER_ID);
231        }
232    }
233
234    /** Sets whether there are any voicemail sources available in the platform. */
235    private void setVoicemailSourcesAvailable(boolean voicemailSourcesAvailable) {
236        if (mVoicemailSourcesAvailable == voicemailSourcesAvailable) return;
237        mVoicemailSourcesAvailable = voicemailSourcesAvailable;
238
239        Activity activity = getActivity();
240        if (activity != null) {
241            // This is so that the options menu content is updated.
242            activity.invalidateOptionsMenu();
243        }
244    }
245
246    @Override
247    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
248        View view = inflater.inflate(R.layout.call_log_fragment, container, false);
249        mVoicemailStatusHelper = new VoicemailStatusHelperImpl();
250        mStatusMessageView = view.findViewById(R.id.voicemail_status);
251        mStatusMessageText = (TextView) view.findViewById(R.id.voicemail_status_message);
252        mStatusMessageAction = (TextView) view.findViewById(R.id.voicemail_status_action);
253        return view;
254    }
255
256    @Override
257    public void onViewCreated(View view, Bundle savedInstanceState) {
258        super.onViewCreated(view, savedInstanceState);
259        updateEmptyMessage(mCallTypeFilter);
260        String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
261        mAdapter = ObjectFactory.newCallLogAdapter(getActivity(), this, new ContactInfoHelper(
262                getActivity(), currentCountryIso), true, true);
263        setListAdapter(mAdapter);
264        getListView().setItemsCanFocus(true);
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 CallLogQueryHandler.CALL_TYPE_ALL:
374                message = getString(R.string.recentCalls_empty);
375                break;
376            default:
377                throw new IllegalArgumentException("Unexpected filter type in CallLogFragment: "
378                        + filterType);
379        }
380        ((TextView) getListView().getEmptyView()).setText(message);
381    }
382
383    public void callSelectedEntry() {
384        int position = getListView().getSelectedItemPosition();
385        if (position < 0) {
386            // In touch mode you may often not have something selected, so
387            // just call the first entry to make sure that [send] [send] calls the
388            // most recent entry.
389            position = 0;
390        }
391        final Cursor cursor = (Cursor)mAdapter.getItem(position);
392        if (cursor != null) {
393            String number = cursor.getString(CallLogQuery.NUMBER);
394            int numberPresentation = cursor.getInt(CallLogQuery.NUMBER_PRESENTATION);
395            if (!PhoneNumberUtilsWrapper.canPlaceCallsTo(number, numberPresentation)) {
396                // This number can't be called, do nothing
397                return;
398            }
399            Intent intent;
400            // If "number" is really a SIP address, construct a sip: URI.
401            if (PhoneNumberHelper.isUriNumber(number)) {
402                intent = CallUtil.getCallIntent(
403                        Uri.fromParts(CallUtil.SCHEME_SIP, number, null));
404            } else {
405                // We're calling a regular PSTN phone number.
406                // Construct a tel: URI, but do some other possible cleanup first.
407                int callType = cursor.getInt(CallLogQuery.CALL_TYPE);
408                if (!number.startsWith("+") &&
409                       (callType == Calls.INCOMING_TYPE
410                                || callType == Calls.MISSED_TYPE)) {
411                    // If the caller-id matches a contact with a better qualified number, use it
412                    String countryIso = cursor.getString(CallLogQuery.COUNTRY_ISO);
413                    number = mAdapter.getBetterNumberFromContacts(number, countryIso);
414                }
415                intent = CallUtil.getCallIntent(
416                        Uri.fromParts(CallUtil.SCHEME_TEL, number, null));
417            }
418            intent.setFlags(
419                    Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
420            startActivity(intent);
421        }
422    }
423
424    CallLogAdapter getAdapter() {
425        return mAdapter;
426    }
427
428    @Override
429    public void setMenuVisibility(boolean menuVisible) {
430        super.setMenuVisibility(menuVisible);
431        if (mMenuVisible != menuVisible) {
432            mMenuVisible = menuVisible;
433            if (!menuVisible) {
434                updateOnExit();
435            } else if (isResumed()) {
436                refreshData();
437            }
438        }
439    }
440
441    /** Requests updates to the data to be shown. */
442    private void refreshData() {
443        // Prevent unnecessary refresh.
444        if (mRefreshDataRequired) {
445            // Mark all entries in the contact info cache as out of date, so they will be looked up
446            // again once being shown.
447            mAdapter.invalidateCache();
448            startCallsQuery();
449            startVoicemailStatusQuery();
450            updateOnEntry();
451            mRefreshDataRequired = false;
452        }
453    }
454
455    /** Updates call data and notification state while leaving the call log tab. */
456    private void updateOnExit() {
457        updateOnTransition(false);
458    }
459
460    /** Updates call data and notification state while entering the call log tab. */
461    private void updateOnEntry() {
462        updateOnTransition(true);
463    }
464
465    // TODO: Move to CallLogActivity
466    private void updateOnTransition(boolean onEntry) {
467        // We don't want to update any call data when keyguard is on because the user has likely not
468        // seen the new calls yet.
469        // This might be called before onCreate() and thus we need to check null explicitly.
470        if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode()) {
471            // On either of the transitions we update the missed call and voicemail notifications.
472            // While exiting we additionally consume all missed calls (by marking them as read).
473            mCallLogQueryHandler.markNewCallsAsOld();
474            if (!onEntry) {
475                mCallLogQueryHandler.markMissedCallsAsRead();
476            }
477            CallLogNotificationsHelper.removeMissedCallNotifications();
478            CallLogNotificationsHelper.updateVoicemailNotifications(getActivity());
479        }
480    }
481}
482