CallLogFragment.java revision a4adb2c035690a1c9600a20c2485886d9d5991d0
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 shown in the CallLogActivity.
145     */
146    private boolean mIsCallLogActivity = false;
147
148    public interface HostInterface {
149        public void showDialpad();
150    }
151
152    public CallLogFragment() {
153        this(CallLogQueryHandler.CALL_TYPE_ALL, NO_LOG_LIMIT);
154    }
155
156    public CallLogFragment(int filterType) {
157        this(filterType, NO_LOG_LIMIT);
158    }
159
160    public CallLogFragment(int filterType, boolean isCallLogActivity) {
161        this(filterType, NO_LOG_LIMIT);
162        mIsCallLogActivity = isCallLogActivity;
163    }
164
165    public CallLogFragment(int filterType, int logLimit) {
166        this(filterType, logLimit, NO_DATE_LIMIT);
167    }
168
169    /**
170     * Creates a call log fragment, filtering to include only calls of the desired type, occurring
171     * after the specified date.
172     * @param filterType type of calls to include.
173     * @param dateLimit limits results to calls occurring on or after the specified date.
174     */
175    public CallLogFragment(int filterType, long dateLimit) {
176        this(filterType, NO_LOG_LIMIT, dateLimit);
177    }
178
179    /**
180     * Creates a call log fragment, filtering to include only calls of the desired type, occurring
181     * after the specified date.  Also provides a means to limit the number of results returned.
182     * @param filterType type of calls to include.
183     * @param logLimit limits the number of results to return.
184     * @param dateLimit limits results to calls occurring on or after the specified date.
185     */
186    public CallLogFragment(int filterType, int logLimit, long dateLimit) {
187        mCallTypeFilter = filterType;
188        mLogLimit = logLimit;
189        mDateLimit = dateLimit;
190    }
191
192    @Override
193    public void onCreate(Bundle state) {
194        super.onCreate(state);
195        if (state != null) {
196            mCallTypeFilter = state.getInt(KEY_FILTER_TYPE, mCallTypeFilter);
197            mLogLimit = state.getInt(KEY_LOG_LIMIT, mLogLimit);
198            mDateLimit = state.getLong(KEY_DATE_LIMIT, mDateLimit);
199        }
200
201        final Activity activity = getActivity();
202        final ContentResolver resolver = activity.getContentResolver();
203        String currentCountryIso = GeoUtil.getCurrentCountryIso(activity);
204        mCallLogQueryHandler = new CallLogQueryHandler(activity, resolver, this, mLogLimit);
205        mKeyguardManager =
206                (KeyguardManager) activity.getSystemService(Context.KEYGUARD_SERVICE);
207        resolver.registerContentObserver(CallLog.CONTENT_URI, true, mCallLogObserver);
208        resolver.registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true,
209                mContactsObserver);
210        resolver.registerContentObserver(Status.CONTENT_URI, true, mVoicemailStatusObserver);
211        setHasOptionsMenu(true);
212
213        if (mCallTypeFilter == Calls.VOICEMAIL_TYPE) {
214            mVoicemailPlaybackPresenter = VoicemailPlaybackPresenter
215                    .getInstance(activity, state);
216        }
217    }
218
219    /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */
220    @Override
221    public boolean onCallsFetched(Cursor cursor) {
222        if (getActivity() == null || getActivity().isFinishing()) {
223            // Return false; we did not take ownership of the cursor
224            return false;
225        }
226        mAdapter.setLoading(false);
227        mAdapter.changeCursor(cursor);
228        // This will update the state of the "Clear call log" menu item.
229        getActivity().invalidateOptionsMenu();
230
231        boolean showListView = cursor != null && cursor.getCount() > 0;
232        mRecyclerView.setVisibility(showListView ? View.VISIBLE : View.GONE);
233        mEmptyListView.setVisibility(!showListView ? View.VISIBLE : View.GONE);
234
235        if (mScrollToTop) {
236            // The smooth-scroll animation happens over a fixed time period.
237            // As a result, if it scrolls through a large portion of the list,
238            // each frame will jump so far from the previous one that the user
239            // will not experience the illusion of downward motion.  Instead,
240            // if we're not already near the top of the list, we instantly jump
241            // near the top, and animate from there.
242            if (mLayoutManager.findFirstVisibleItemPosition() > 5) {
243                // TODO: Jump to near the top, then begin smooth scroll.
244                mRecyclerView.smoothScrollToPosition(0);
245            }
246            // Workaround for framework issue: the smooth-scroll doesn't
247            // occur if setSelection() is called immediately before.
248            mHandler.post(new Runnable() {
249               @Override
250               public void run() {
251                   if (getActivity() == null || getActivity().isFinishing()) {
252                       return;
253                   }
254                   mRecyclerView.smoothScrollToPosition(0);
255               }
256            });
257
258            mScrollToTop = false;
259        }
260        mCallLogFetched = true;
261        destroyEmptyLoaderIfAllDataFetched();
262        return true;
263    }
264
265    /**
266     * Called by {@link CallLogQueryHandler} after a successful query to voicemail status provider.
267     */
268    @Override
269    public void onVoicemailStatusFetched(Cursor statusCursor) {
270        Activity activity = getActivity();
271        if (activity == null || activity.isFinishing()) {
272            return;
273        }
274
275        mVoicemailStatusFetched = true;
276        destroyEmptyLoaderIfAllDataFetched();
277    }
278
279    private void destroyEmptyLoaderIfAllDataFetched() {
280        if (mCallLogFetched && mVoicemailStatusFetched && mEmptyLoaderRunning) {
281            mEmptyLoaderRunning = false;
282            getLoaderManager().destroyLoader(EMPTY_LOADER_ID);
283        }
284    }
285
286    @Override
287    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
288        View view = inflater.inflate(R.layout.call_log_fragment, container, false);
289
290        mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
291        mRecyclerView.setHasFixedSize(true);
292        mLayoutManager = new LinearLayoutManager(getActivity());
293        mRecyclerView.setLayoutManager(mLayoutManager);
294        mEmptyListView = (EmptyContentView) view.findViewById(R.id.empty_list_view);
295        mEmptyListView.setImage(R.drawable.empty_call_log);
296        mEmptyListView.setActionClickedListener(this);
297
298        String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
299        mAdapter = ObjectFactory.newCallLogAdapter(
300                getActivity(),
301                this,
302                new ContactInfoHelper(getActivity(), currentCountryIso),
303                mVoicemailPlaybackPresenter,
304                mIsCallLogActivity);
305        mRecyclerView.setAdapter(mAdapter);
306
307        fetchCalls();
308        return view;
309    }
310
311    @Override
312    public void onViewCreated(View view, Bundle savedInstanceState) {
313        super.onViewCreated(view, savedInstanceState);
314        updateEmptyMessage(mCallTypeFilter);
315        mAdapter.onRestoreInstanceState(savedInstanceState);
316    }
317
318    @Override
319    public void onStart() {
320        // Start the empty loader now to defer other fragments.  We destroy it when both calllog
321        // and the voicemail status are fetched.
322        getLoaderManager().initLoader(EMPTY_LOADER_ID, null,
323                new EmptyLoader.Callback(getActivity()));
324        mEmptyLoaderRunning = true;
325        super.onStart();
326    }
327
328    @Override
329    public void onResume() {
330        super.onResume();
331        final boolean hasReadCallLogPermission =
332                PermissionsUtil.hasPermission(getActivity(), READ_CALL_LOG);
333        if (!mHasReadCallLogPermission && hasReadCallLogPermission) {
334            // We didn't have the permission before, and now we do. Force a refresh of the call log.
335            // Note that this code path always happens on a fresh start, but mRefreshDataRequired
336            // is already true in that case anyway.
337            mRefreshDataRequired = true;
338            updateEmptyMessage(mCallTypeFilter);
339        }
340        mHasReadCallLogPermission = hasReadCallLogPermission;
341        refreshData();
342        mAdapter.startCache();
343    }
344
345    @Override
346    public void onPause() {
347        if (mVoicemailPlaybackPresenter != null) {
348            mVoicemailPlaybackPresenter.onPause();
349        }
350        mAdapter.pauseCache();
351        super.onPause();
352    }
353
354    @Override
355    public void onStop() {
356        updateOnTransition(false /* onEntry */);
357
358        super.onStop();
359    }
360
361    @Override
362    public void onDestroy() {
363        mAdapter.pauseCache();
364        mAdapter.changeCursor(null);
365
366        if (mVoicemailPlaybackPresenter != null) {
367            mVoicemailPlaybackPresenter.onDestroy();
368        }
369
370        getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver);
371        getActivity().getContentResolver().unregisterContentObserver(mContactsObserver);
372        getActivity().getContentResolver().unregisterContentObserver(mVoicemailStatusObserver);
373        super.onDestroy();
374    }
375
376    @Override
377    public void onSaveInstanceState(Bundle outState) {
378        super.onSaveInstanceState(outState);
379        outState.putInt(KEY_FILTER_TYPE, mCallTypeFilter);
380        outState.putInt(KEY_LOG_LIMIT, mLogLimit);
381        outState.putLong(KEY_DATE_LIMIT, mDateLimit);
382
383        mAdapter.onSaveInstanceState(outState);
384
385        if (mVoicemailPlaybackPresenter != null) {
386            mVoicemailPlaybackPresenter.onSaveInstanceState(outState);
387        }
388    }
389
390    @Override
391    public void fetchCalls() {
392        mCallLogQueryHandler.fetchCalls(mCallTypeFilter, mDateLimit);
393    }
394
395    private void updateEmptyMessage(int filterType) {
396        final Context context = getActivity();
397        if (context == null) {
398            return;
399        }
400
401        if (!PermissionsUtil.hasPermission(context, READ_CALL_LOG)) {
402            mEmptyListView.setDescription(R.string.permission_no_calllog);
403            mEmptyListView.setActionLabel(R.string.permission_single_turn_on);
404            return;
405        }
406
407        final int messageId;
408        switch (filterType) {
409            case Calls.MISSED_TYPE:
410                messageId = R.string.recentMissed_empty;
411                break;
412            case Calls.VOICEMAIL_TYPE:
413                messageId = R.string.recentVoicemails_empty;
414                break;
415            case CallLogQueryHandler.CALL_TYPE_ALL:
416                messageId = R.string.recentCalls_empty;
417                break;
418            default:
419                throw new IllegalArgumentException("Unexpected filter type in CallLogFragment: "
420                        + filterType);
421        }
422        mEmptyListView.setDescription(messageId);
423        if (mIsCallLogActivity) {
424            mEmptyListView.setActionLabel(EmptyContentView.NO_LABEL);
425        } else {
426            mEmptyListView.setActionLabel(R.string.recentCalls_empty_action);
427        }
428    }
429
430    CallLogAdapter getAdapter() {
431        return mAdapter;
432    }
433
434    @Override
435    public void setMenuVisibility(boolean menuVisible) {
436        super.setMenuVisibility(menuVisible);
437        if (mMenuVisible != menuVisible) {
438            mMenuVisible = menuVisible;
439            if (!menuVisible) {
440                updateOnTransition(false /* onEntry */);
441            } else if (isResumed()) {
442                refreshData();
443            }
444        }
445    }
446
447    /** Requests updates to the data to be shown. */
448    private void refreshData() {
449        // Prevent unnecessary refresh.
450        if (mRefreshDataRequired) {
451            // Mark all entries in the contact info cache as out of date, so they will be looked up
452            // again once being shown.
453            mAdapter.invalidateCache();
454            mAdapter.setLoading(true);
455
456            fetchCalls();
457            mCallLogQueryHandler.fetchVoicemailStatus();
458
459            updateOnTransition(true /* onEntry */);
460            mRefreshDataRequired = false;
461        } else {
462            // Refresh the display of the existing data to update the timestamp text descriptions.
463            mAdapter.notifyDataSetChanged();
464        }
465    }
466
467    /**
468     * Updates the call data and notification state on entering or leaving the call log tab.
469     *
470     * If we are leaving the call log tab, mark all the missed calls as read.
471     *
472     * TODO: Move to CallLogActivity
473     */
474    private void updateOnTransition(boolean onEntry) {
475        // We don't want to update any call data when keyguard is on because the user has likely not
476        // seen the new calls yet.
477        // This might be called before onCreate() and thus we need to check null explicitly.
478        if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode()) {
479            // On either of the transitions we update the missed call and voicemail notifications.
480            // While exiting we additionally consume all missed calls (by marking them as read).
481            mCallLogQueryHandler.markNewCallsAsOld();
482            if (!onEntry) {
483                mCallLogQueryHandler.markMissedCallsAsRead();
484            }
485            CallLogNotificationsHelper.removeMissedCallNotifications(getActivity());
486            CallLogNotificationsHelper.updateVoicemailNotifications(getActivity());
487        }
488    }
489
490    @Override
491    public void onEmptyViewActionButtonClicked() {
492        final Activity activity = getActivity();
493        if (activity == null) {
494            return;
495        }
496
497        if (!PermissionsUtil.hasPermission(activity, READ_CALL_LOG)) {
498            requestPermissions(new String[] {READ_CALL_LOG}, READ_CALL_LOG_PERMISSION_REQUEST_CODE);
499        } else if (!mIsCallLogActivity) {
500            // Show dialpad if we are the recents fragment.
501            ((HostInterface) activity).showDialpad();
502        }
503    }
504
505    @Override
506    public void onRequestPermissionsResult(int requestCode, String[] permissions,
507            int[] grantResults) {
508        if (requestCode == READ_CALL_LOG_PERMISSION_REQUEST_CODE) {
509            if (grantResults.length >= 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
510                // Force a refresh of the data since we were missing the permission before this.
511                mRefreshDataRequired = true;
512            }
513        }
514    }
515}
516