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