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