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 
17 package com.android.dialer.calllog;
18 
19 import android.app.Activity;
20 import android.app.Fragment;
21 import android.app.KeyguardManager;
22 import android.content.ContentResolver;
23 import android.content.Context;
24 import android.content.pm.PackageManager;
25 import android.database.ContentObserver;
26 import android.database.Cursor;
27 import android.os.Bundle;
28 import android.os.Handler;
29 import android.os.Message;
30 import android.provider.CallLog;
31 import android.provider.CallLog.Calls;
32 import android.provider.ContactsContract;
33 import android.support.annotation.Nullable;
34 import android.support.v13.app.FragmentCompat;
35 import android.support.v7.widget.LinearLayoutManager;
36 import android.support.v7.widget.RecyclerView;
37 import android.view.LayoutInflater;
38 import android.view.View;
39 import android.view.ViewGroup;
40 
41 import com.android.contacts.common.GeoUtil;
42 import com.android.contacts.common.util.PermissionsUtil;
43 import com.android.dialer.R;
44 import com.android.dialer.list.ListsFragment;
45 import com.android.dialer.util.EmptyLoader;
46 import com.android.dialer.voicemail.VoicemailPlaybackPresenter;
47 import com.android.dialer.widget.EmptyContentView;
48 import com.android.dialer.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener;
49 import com.android.dialerbind.ObjectFactory;
50 
51 import 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  */
57 public 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 {
CustomContentObserver()112         public CustomContentObserver() {
113             super(mHandler);
114         }
115         @Override
onChange(boolean selfChange)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 {
showDialpad()148         public void showDialpad();
149     }
150 
CallLogFragment()151     public CallLogFragment() {
152         this(CallLogQueryHandler.CALL_TYPE_ALL, NO_LOG_LIMIT);
153     }
154 
CallLogFragment(int filterType)155     public CallLogFragment(int filterType) {
156         this(filterType, NO_LOG_LIMIT);
157     }
158 
CallLogFragment(int filterType, boolean isCallLogActivity)159     public CallLogFragment(int filterType, boolean isCallLogActivity) {
160         this(filterType, NO_LOG_LIMIT);
161         mIsCallLogActivity = isCallLogActivity;
162     }
163 
CallLogFragment(int filterType, int logLimit)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      */
CallLogFragment(int filterType, long dateLimit)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      */
CallLogFragment(int filterType, int logLimit, long dateLimit)185     public CallLogFragment(int filterType, int logLimit, long dateLimit) {
186         mCallTypeFilter = filterType;
187         mLogLimit = logLimit;
188         mDateLimit = dateLimit;
189     }
190 
191     @Override
onCreate(Bundle state)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
onCallsFetched(Cursor cursor)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
onVoicemailStatusFetched(Cursor statusCursor)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 
destroyEmptyLoaderIfAllDataFetched()274     private void destroyEmptyLoaderIfAllDataFetched() {
275         if (mCallLogFetched && mVoicemailStatusFetched && mEmptyLoaderRunning) {
276             mEmptyLoaderRunning = false;
277             getLoaderManager().destroyLoader(EMPTY_LOADER_ID);
278         }
279     }
280 
281     @Override
onVoicemailUnreadCountFetched(Cursor cursor)282     public void onVoicemailUnreadCountFetched(Cursor cursor) {}
283 
284     @Override
onMissedCallsUnreadCountFetched(Cursor cursor)285     public void onMissedCallsUnreadCountFetched(Cursor cursor) {}
286 
287     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState)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 
setupView( View view, @Nullable VoicemailPlaybackPresenter voicemailPlaybackPresenter)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
onViewCreated(View view, Bundle savedInstanceState)318     public void onViewCreated(View view, Bundle savedInstanceState) {
319         super.onViewCreated(view, savedInstanceState);
320         updateEmptyMessage(mCallTypeFilter);
321         mAdapter.onRestoreInstanceState(savedInstanceState);
322     }
323 
324     @Override
onStart()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
onResume()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
onPause()355     public void onPause() {
356         cancelDisplayUpdate();
357         mAdapter.onPause();
358         super.onPause();
359     }
360 
361     @Override
onStop()362     public void onStop() {
363         updateOnTransition();
364 
365         super.onStop();
366     }
367 
368     @Override
onDestroy()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
onSaveInstanceState(Bundle outState)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
fetchCalls()389     public void fetchCalls() {
390         mCallLogQueryHandler.fetchCalls(mCallTypeFilter, mDateLimit);
391         if (!mIsCallLogActivity) {
392             ((ListsFragment) getParentFragment()).updateTabUnreadCounts();
393         }
394     }
395 
updateEmptyMessage(int filterType)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 
getAdapter()431     CallLogAdapter getAdapter() {
432         return mAdapter;
433     }
434 
435     @Override
setMenuVisibility(boolean menuVisible)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. */
refreshData()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      */
updateOnTransition()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
onEmptyViewActionButtonClicked()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
onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults)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      */
rescheduleDisplayUpdate()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      */
cancelDisplayUpdate()527     private void cancelDisplayUpdate() {
528         mDisplayUpdateHandler.removeMessages(EVENT_UPDATE_DISPLAY);
529     }
530 }
531