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        mAdapter.startCache();
342    }
343
344    @Override
345    public void onPause() {
346        if (mVoicemailPlaybackPresenter != null) {
347            mVoicemailPlaybackPresenter.onPause();
348        }
349        mAdapter.pauseCache();
350        super.onPause();
351    }
352
353    @Override
354    public void onStop() {
355        updateOnTransition(false /* onEntry */);
356
357        super.onStop();
358    }
359
360    @Override
361    public void onDestroy() {
362        mAdapter.pauseCache();
363        mAdapter.changeCursor(null);
364
365        if (mVoicemailPlaybackPresenter != null) {
366            mVoicemailPlaybackPresenter.onDestroy();
367        }
368
369        getActivity().getContentResolver().unregisterContentObserver(mCallLogObserver);
370        getActivity().getContentResolver().unregisterContentObserver(mContactsObserver);
371        getActivity().getContentResolver().unregisterContentObserver(mVoicemailStatusObserver);
372        super.onDestroy();
373    }
374
375    @Override
376    public void onSaveInstanceState(Bundle outState) {
377        super.onSaveInstanceState(outState);
378        outState.putInt(KEY_FILTER_TYPE, mCallTypeFilter);
379        outState.putInt(KEY_LOG_LIMIT, mLogLimit);
380        outState.putLong(KEY_DATE_LIMIT, mDateLimit);
381
382        mAdapter.onSaveInstanceState(outState);
383
384        if (mVoicemailPlaybackPresenter != null) {
385            mVoicemailPlaybackPresenter.onSaveInstanceState(outState);
386        }
387    }
388
389    @Override
390    public void fetchCalls() {
391        mCallLogQueryHandler.fetchCalls(mCallTypeFilter, mDateLimit);
392    }
393
394    private void updateEmptyMessage(int filterType) {
395        final Context context = getActivity();
396        if (context == null) {
397            return;
398        }
399
400        if (!PermissionsUtil.hasPermission(context, READ_CALL_LOG)) {
401            mEmptyListView.setDescription(R.string.permission_no_calllog);
402            mEmptyListView.setActionLabel(R.string.permission_single_turn_on);
403            return;
404        }
405
406        final int messageId;
407        switch (filterType) {
408            case Calls.MISSED_TYPE:
409                messageId = R.string.recentMissed_empty;
410                break;
411            case Calls.VOICEMAIL_TYPE:
412                messageId = R.string.recentVoicemails_empty;
413                break;
414            case CallLogQueryHandler.CALL_TYPE_ALL:
415                messageId = R.string.recentCalls_empty;
416                break;
417            default:
418                throw new IllegalArgumentException("Unexpected filter type in CallLogFragment: "
419                        + filterType);
420        }
421        mEmptyListView.setDescription(messageId);
422        if (mIsRecentsFragment) {
423            mEmptyListView.setActionLabel(R.string.recentCalls_empty_action);
424        } else {
425            mEmptyListView.setActionLabel(EmptyContentView.NO_LABEL);
426        }
427    }
428
429    CallLogAdapter getAdapter() {
430        return mAdapter;
431    }
432
433    @Override
434    public void setMenuVisibility(boolean menuVisible) {
435        super.setMenuVisibility(menuVisible);
436        if (mMenuVisible != menuVisible) {
437            mMenuVisible = menuVisible;
438            if (!menuVisible) {
439                updateOnTransition(false /* onEntry */);
440            } else if (isResumed()) {
441                refreshData();
442            }
443        }
444    }
445
446    /** Requests updates to the data to be shown. */
447    private void refreshData() {
448        // Prevent unnecessary refresh.
449        if (mRefreshDataRequired) {
450            // Mark all entries in the contact info cache as out of date, so they will be looked up
451            // again once being shown.
452            mAdapter.invalidateCache();
453            mAdapter.setLoading(true);
454
455            fetchCalls();
456            mCallLogQueryHandler.fetchVoicemailStatus();
457
458            updateOnTransition(true /* onEntry */);
459            mRefreshDataRequired = false;
460        } else {
461            // Refresh the display of the existing data to update the timestamp text descriptions.
462            mAdapter.notifyDataSetChanged();
463        }
464    }
465
466    /**
467     * Updates the call data and notification state on entering or leaving the call log tab.
468     *
469     * If we are leaving the call log tab, mark all the missed calls as read.
470     *
471     * TODO: Move to CallLogActivity
472     */
473    private void updateOnTransition(boolean onEntry) {
474        // We don't want to update any call data when keyguard is on because the user has likely not
475        // seen the new calls yet.
476        // This might be called before onCreate() and thus we need to check null explicitly.
477        if (mKeyguardManager != null && !mKeyguardManager.inKeyguardRestrictedInputMode()) {
478            // On either of the transitions we update the missed call and voicemail notifications.
479            // While exiting we additionally consume all missed calls (by marking them as read).
480            mCallLogQueryHandler.markNewCallsAsOld();
481            if (!onEntry) {
482                mCallLogQueryHandler.markMissedCallsAsRead();
483            }
484            CallLogNotificationsHelper.removeMissedCallNotifications(getActivity());
485            CallLogNotificationsHelper.updateVoicemailNotifications(getActivity());
486        }
487    }
488
489    @Override
490    public void onEmptyViewActionButtonClicked() {
491        final Activity activity = getActivity();
492        if (activity == null) {
493            return;
494        }
495
496        if (!PermissionsUtil.hasPermission(activity, READ_CALL_LOG)) {
497            requestPermissions(new String[] {READ_CALL_LOG}, READ_CALL_LOG_PERMISSION_REQUEST_CODE);
498        } else if (mIsRecentsFragment) {
499            // Show dialpad if we are the recents fragment.
500            ((HostInterface) activity).showDialpad();
501        }
502    }
503
504    @Override
505    public void onRequestPermissionsResult(int requestCode, String[] permissions,
506            int[] grantResults) {
507        if (requestCode == READ_CALL_LOG_PERMISSION_REQUEST_CODE) {
508            if (grantResults.length >= 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
509                // Force a refresh of the data since we were missing the permission before this.
510                mRefreshDataRequired = true;
511            }
512        }
513    }
514}
515