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