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