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