CallLogFragment.java revision 69705be50fc4c84b35df88c879dd9a4b4de655b3
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.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ValueAnimator;
22import android.app.Activity;
23import android.app.DialogFragment;
24import android.app.Fragment;
25import android.app.KeyguardManager;
26import android.content.Context;
27import android.content.Intent;
28import android.database.ContentObserver;
29import android.database.Cursor;
30import android.graphics.Rect;
31import android.os.Bundle;
32import android.os.Handler;
33import android.provider.CallLog;
34import android.provider.CallLog.Calls;
35import android.provider.ContactsContract;
36import android.provider.VoicemailContract.Status;
37import android.support.v7.widget.RecyclerView;
38import android.support.v7.widget.LinearLayoutManager;
39import android.view.LayoutInflater;
40import android.view.View;
41import android.view.ViewGroup;
42import android.view.View.OnClickListener;
43import android.view.ViewGroup.LayoutParams;
44import android.widget.ListView;
45import android.widget.TextView;
46
47import com.android.contacts.common.GeoUtil;
48import com.android.contacts.common.util.ViewUtil;
49import com.android.dialer.R;
50import com.android.dialer.list.ListsFragment.HostInterface;
51import com.android.dialer.util.DialerUtils;
52import com.android.dialer.util.EmptyLoader;
53import com.android.dialer.voicemail.VoicemailStatusHelper;
54import com.android.dialer.voicemail.VoicemailStatusHelper.StatusMessage;
55import com.android.dialer.voicemail.VoicemailStatusHelperImpl;
56import com.android.dialerbind.ObjectFactory;
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 Fragment
65        implements CallLogQueryHandler.Listener, CallLogAdapter.OnReportButtonClickListener,
66        CallLogAdapter.CallFetcher {
67    private static final String TAG = "CallLogFragment";
68
69    private static final String REPORT_DIALOG_TAG = "report_dialog";
70
71    /**
72     * ID of the empty loader to defer other fragments.
73     */
74    private static final int EMPTY_LOADER_ID = 0;
75
76    private static final String KEY_FILTER_TYPE = "filter_type";
77    private static final String KEY_LOG_LIMIT = "log_limit";
78    private static final String KEY_DATE_LIMIT = "date_limit";
79
80    // No limit specified for the number of logs to show; use the CallLogQueryHandler's default.
81    private static final int NO_LOG_LIMIT = -1;
82    // No date-based filtering.
83    private static final int NO_DATE_LIMIT = 0;
84
85    private RecyclerView mRecyclerView;
86    private LinearLayoutManager mLayoutManager;
87    private CallLogAdapter mAdapter;
88    private CallLogQueryHandler mCallLogQueryHandler;
89    private boolean mScrollToTop;
90
91    /** Whether there is at least one voicemail source installed. */
92    private boolean mVoicemailSourcesAvailable = false;
93
94    private VoicemailStatusHelper mVoicemailStatusHelper;
95    private View mStatusMessageView;
96    private View mEmptyListView;
97    private TextView mStatusMessageText;
98    private TextView mStatusMessageAction;
99    private KeyguardManager mKeyguardManager;
100
101    private boolean mEmptyLoaderRunning;
102    private boolean mCallLogFetched;
103    private boolean mVoicemailStatusFetched;
104
105    private final Handler mHandler = new Handler();
106
107    private class CustomContentObserver extends ContentObserver {
108        public CustomContentObserver() {
109            super(mHandler);
110        }
111        @Override
112        public void onChange(boolean selfChange) {
113            mRefreshDataRequired = true;
114        }
115    }
116
117    // See issue 6363009
118    private final ContentObserver mCallLogObserver = new CustomContentObserver();
119    private final ContentObserver mContactsObserver = new CustomContentObserver();
120    private final ContentObserver mVoicemailStatusObserver = new CustomContentObserver();
121    private boolean mRefreshDataRequired = true;
122
123    // Exactly same variable is in Fragment as a package private.
124    private boolean mMenuVisible = true;
125
126    // Default to all calls.
127    private int mCallTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL;
128
129    // Log limit - if no limit is specified, then the default in {@link CallLogQueryHandler}
130    // will be used.
131    private int mLogLimit = NO_LOG_LIMIT;
132
133    // Date limit (in millis since epoch) - when non-zero, only calls which occurred on or after
134    // the date filter are included.  If zero, no date-based filtering occurs.
135    private long mDateLimit = NO_DATE_LIMIT;
136
137    public CallLogFragment() {
138        this(CallLogQueryHandler.CALL_TYPE_ALL, NO_LOG_LIMIT);
139    }
140
141    public CallLogFragment(int filterType) {
142        this(filterType, NO_LOG_LIMIT);
143    }
144
145    public CallLogFragment(int filterType, int logLimit) {
146        super();
147        mCallTypeFilter = filterType;
148        mLogLimit = logLimit;
149    }
150
151    /**
152     * Creates a call log fragment, filtering to include only calls of the desired type, occurring
153     * after the specified date.
154     * @param filterType type of calls to include.
155     * @param dateLimit limits results to calls occurring on or after the specified date.
156     */
157    public CallLogFragment(int filterType, long dateLimit) {
158        this(filterType, NO_LOG_LIMIT, dateLimit);
159    }
160
161    /**
162     * Creates a call log fragment, filtering to include only calls of the desired type, occurring
163     * after the specified date.  Also provides a means to limit the number of results returned.
164     * @param filterType type of calls to include.
165     * @param logLimit limits the number of results to return.
166     * @param dateLimit limits results to calls occurring on or after the specified date.
167     */
168    public CallLogFragment(int filterType, int logLimit, long dateLimit) {
169        this(filterType, logLimit);
170        mDateLimit = dateLimit;
171    }
172
173    @Override
174    public void onCreate(Bundle state) {
175        super.onCreate(state);
176        if (state != null) {
177            mCallTypeFilter = state.getInt(KEY_FILTER_TYPE, mCallTypeFilter);
178            mLogLimit = state.getInt(KEY_LOG_LIMIT, mLogLimit);
179            mDateLimit = state.getLong(KEY_DATE_LIMIT, mDateLimit);
180        }
181
182        String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
183        mCallLogQueryHandler = new CallLogQueryHandler(getActivity().getContentResolver(),
184                this, mLogLimit);
185        mKeyguardManager =
186                (KeyguardManager) getActivity().getSystemService(Context.KEYGUARD_SERVICE);
187        getActivity().getContentResolver().registerContentObserver(
188                CallLog.CONTENT_URI, true, mCallLogObserver);
189        getActivity().getContentResolver().registerContentObserver(
190                ContactsContract.Contacts.CONTENT_URI, true, mContactsObserver);
191        getActivity().getContentResolver().registerContentObserver(
192                Status.CONTENT_URI, true, mVoicemailStatusObserver);
193        setHasOptionsMenu(true);
194        fetchCalls();
195    }
196
197    /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */
198    @Override
199    public boolean onCallsFetched(Cursor cursor) {
200        if (getActivity() == null || getActivity().isFinishing()) {
201            // Return false; we did not take ownership of the cursor
202            return false;
203        }
204        mAdapter.setLoading(false);
205        mAdapter.changeCursor(cursor);
206        // This will update the state of the "Clear call log" menu item.
207        getActivity().invalidateOptionsMenu();
208
209        boolean showListView = cursor.getCount() > 0;
210        mRecyclerView.setVisibility(showListView ? View.VISIBLE : View.GONE);
211        mEmptyListView.setVisibility(!showListView ? View.VISIBLE : View.GONE);
212
213        if (mScrollToTop) {
214            // The smooth-scroll animation happens over a fixed time period.
215            // As a result, if it scrolls through a large portion of the list,
216            // each frame will jump so far from the previous one that the user
217            // will not experience the illusion of downward motion.  Instead,
218            // if we're not already near the top of the list, we instantly jump
219            // near the top, and animate from there.
220            if (mLayoutManager.findFirstVisibleItemPosition() > 5) {
221                // TODO: Jump to near the top, then begin smooth scroll.
222                mRecyclerView.smoothScrollToPosition(0);
223            }
224            // Workaround for framework issue: the smooth-scroll doesn't
225            // occur if setSelection() is called immediately before.
226            mHandler.post(new Runnable() {
227               @Override
228               public void run() {
229                   if (getActivity() == null || getActivity().isFinishing()) {
230                       return;
231                   }
232                   mRecyclerView.smoothScrollToPosition(0);
233               }
234            });
235
236            mScrollToTop = false;
237        }
238        mCallLogFetched = true;
239        destroyEmptyLoaderIfAllDataFetched();
240        return true;
241    }
242
243    /**
244     * Called by {@link CallLogQueryHandler} after a successful query to voicemail status provider.
245     */
246    @Override
247    public void onVoicemailStatusFetched(Cursor statusCursor) {
248        Activity activity = getActivity();
249        if (activity == null || activity.isFinishing()) {
250            return;
251        }
252        updateVoicemailStatusMessage(statusCursor);
253
254        // If there are any changes to the presence of active voicemail services, invalidate the
255        // options menu so that it will be updated.
256        boolean hasActiveVoicemailSources =
257                mVoicemailStatusHelper.getNumberActivityVoicemailSources(statusCursor) != 0;
258        if (mVoicemailSourcesAvailable != hasActiveVoicemailSources) {
259            mVoicemailSourcesAvailable = hasActiveVoicemailSources;
260            activity.invalidateOptionsMenu();
261        }
262
263        mVoicemailStatusFetched = true;
264        destroyEmptyLoaderIfAllDataFetched();
265    }
266
267    private void destroyEmptyLoaderIfAllDataFetched() {
268        if (mCallLogFetched && mVoicemailStatusFetched && mEmptyLoaderRunning) {
269            mEmptyLoaderRunning = false;
270            getLoaderManager().destroyLoader(EMPTY_LOADER_ID);
271        }
272    }
273
274    @Override
275    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
276        View view = inflater.inflate(R.layout.call_log_fragment, container, false);
277
278        mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
279        mRecyclerView.setHasFixedSize(true);
280        mLayoutManager = new LinearLayoutManager(getActivity());
281        mRecyclerView.setLayoutManager(mLayoutManager);
282
283        String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
284        mAdapter = ObjectFactory.newCallLogAdapter(getActivity(), this,
285                new ContactInfoHelper(getActivity(), currentCountryIso), this);
286        if (mLogLimit != NO_LOG_LIMIT || mDateLimit != NO_DATE_LIMIT) {
287            mAdapter.setShowCallHistoryListItem(true);
288        }
289        mRecyclerView.setAdapter(mAdapter);
290
291        mVoicemailStatusHelper = new VoicemailStatusHelperImpl();
292        mStatusMessageView = view.findViewById(R.id.voicemail_status);
293        mStatusMessageText = (TextView) view.findViewById(R.id.voicemail_status_message);
294        mStatusMessageAction = (TextView) view.findViewById(R.id.voicemail_status_action);
295        return view;
296    }
297
298    @Override
299    public void onViewCreated(View view, Bundle savedInstanceState) {
300        super.onViewCreated(view, savedInstanceState);
301        mEmptyListView = view.findViewById(R.id.empty_list_view);
302
303        updateEmptyMessage(mCallTypeFilter);
304    }
305
306    /**
307     * Based on the new intent, decide whether the list should be configured
308     * to scroll up to display the first item.
309     */
310    public void configureScreenFromIntent(Intent newIntent) {
311        // Typically, when switching to the call-log we want to show the user
312        // the same section of the list that they were most recently looking
313        // at.  However, under some circumstances, we want to automatically
314        // scroll to the top of the list to present the newest call items.
315        // For example, immediately after a call is finished, we want to
316        // display information about that call.
317        mScrollToTop = Calls.CONTENT_TYPE.equals(newIntent.getType());
318    }
319
320    @Override
321    public void onStart() {
322        // Start the empty loader now to defer other fragments.  We destroy it when both calllog
323        // and the voicemail status are fetched.
324        getLoaderManager().initLoader(EMPTY_LOADER_ID, null,
325                new EmptyLoader.Callback(getActivity()));
326        mEmptyLoaderRunning = true;
327        super.onStart();
328    }
329
330    @Override
331    public void onResume() {
332        super.onResume();
333        refreshData();
334    }
335
336    private void updateVoicemailStatusMessage(Cursor statusCursor) {
337        List<StatusMessage> messages = mVoicemailStatusHelper.getStatusMessages(statusCursor);
338        if (messages.size() == 0) {
339            mStatusMessageView.setVisibility(View.GONE);
340        } else {
341            mStatusMessageView.setVisibility(View.VISIBLE);
342            // TODO: Change the code to show all messages. For now just pick the first message.
343            final StatusMessage message = messages.get(0);
344            if (message.showInCallLog()) {
345                mStatusMessageText.setText(message.callLogMessageId);
346            }
347            if (message.actionMessageId != -1) {
348                mStatusMessageAction.setText(message.actionMessageId);
349            }
350            if (message.actionUri != null) {
351                mStatusMessageAction.setVisibility(View.VISIBLE);
352                mStatusMessageAction.setOnClickListener(new View.OnClickListener() {
353                    @Override
354                    public void onClick(View v) {
355                        getActivity().startActivity(
356                                new Intent(Intent.ACTION_VIEW, message.actionUri));
357                    }
358                });
359            } else {
360                mStatusMessageAction.setVisibility(View.GONE);
361            }
362        }
363    }
364
365    @Override
366    public void onPause() {
367        super.onPause();
368        mAdapter.pauseCache();
369    }
370
371    @Override
372    public void onStop() {
373        super.onStop();
374
375        updateOnTransition(false /* onEntry */);
376    }
377
378    @Override
379    public void onDestroy() {
380        super.onDestroy();
381        mAdapter.pauseCache();
382        mAdapter.changeCursor(null);
383        getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver);
384        getActivity().getContentResolver().unregisterContentObserver(mContactsObserver);
385        getActivity().getContentResolver().unregisterContentObserver(mVoicemailStatusObserver);
386    }
387
388    @Override
389    public void onSaveInstanceState(Bundle outState) {
390        super.onSaveInstanceState(outState);
391        outState.putInt(KEY_FILTER_TYPE, mCallTypeFilter);
392        outState.putInt(KEY_LOG_LIMIT, mLogLimit);
393        outState.putLong(KEY_DATE_LIMIT, mDateLimit);
394    }
395
396    @Override
397    public void fetchCalls() {
398        mCallLogQueryHandler.fetchCalls(mCallTypeFilter, mDateLimit);
399    }
400
401    private void updateEmptyMessage(int filterType) {
402        final int messageId;
403        switch (filterType) {
404            case Calls.MISSED_TYPE:
405                messageId = R.string.recentMissed_empty;
406                break;
407            case Calls.VOICEMAIL_TYPE:
408                messageId = R.string.recentVoicemails_empty;
409                break;
410            case CallLogQueryHandler.CALL_TYPE_ALL:
411                messageId = R.string.recentCalls_empty;
412                break;
413            default:
414                throw new IllegalArgumentException("Unexpected filter type in CallLogFragment: "
415                        + filterType);
416        }
417        DialerUtils.configureEmptyListView(
418                mEmptyListView, R.drawable.empty_call_log, messageId, getResources());
419    }
420
421    CallLogAdapter getAdapter() {
422        return mAdapter;
423    }
424
425    @Override
426    public void setMenuVisibility(boolean menuVisible) {
427        super.setMenuVisibility(menuVisible);
428        if (mMenuVisible != menuVisible) {
429            mMenuVisible = menuVisible;
430            if (!menuVisible) {
431                updateOnTransition(false /* onEntry */);
432            } else if (isResumed()) {
433                refreshData();
434            }
435        }
436    }
437
438    /** Requests updates to the data to be shown. */
439    private void refreshData() {
440        // Prevent unnecessary refresh.
441        if (mRefreshDataRequired) {
442            // Mark all entries in the contact info cache as out of date, so they will be looked up
443            // again once being shown.
444            mAdapter.invalidateCache();
445            mAdapter.setLoading(true);
446
447            fetchCalls();
448            mCallLogQueryHandler.fetchVoicemailStatus();
449
450            updateOnTransition(true /* onEntry */);
451            mRefreshDataRequired = false;
452        } else {
453            // Refresh the display of the existing data to update the timestamp text descriptions.
454            mAdapter.notifyDataSetChanged();
455        }
456    }
457
458    /**
459     * Updates the call data and notification state on entering or leaving the call log tab.
460     *
461     * If we are leaving the call log tab, mark all the missed calls as read.
462     *
463     * TODO: Move to CallLogActivity
464     */
465    private void updateOnTransition(boolean onEntry) {
466        // We don't want to update any call data when keyguard is on because the user has likely not
467        // seen the new calls yet.
468        // This might be called before onCreate() and thus we need to check null explicitly.
469        if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode()) {
470            // On either of the transitions we update the missed call and voicemail notifications.
471            // While exiting we additionally consume all missed calls (by marking them as read).
472            mCallLogQueryHandler.markNewCallsAsOld();
473            if (!onEntry) {
474                mCallLogQueryHandler.markMissedCallsAsRead();
475            }
476            CallLogNotificationsHelper.removeMissedCallNotifications(getActivity());
477            CallLogNotificationsHelper.updateVoicemailNotifications(getActivity());
478        }
479    }
480
481    public void onBadDataReported(String number) {
482        if (number == null) {
483            return;
484        }
485        mAdapter.invalidateCache();
486        mAdapter.notifyDataSetChanged();
487    }
488
489    public void onReportButtonClick(String number) {
490        DialogFragment df = ObjectFactory.getReportDialogFragment(number);
491        if (df != null) {
492            df.setTargetFragment(this, 0);
493            df.show(getActivity().getFragmentManager(), REPORT_DIALOG_TAG);
494        }
495    }
496}
497