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