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