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.app.calllog;
18 
19 import static android.Manifest.permission.READ_CALL_LOG;
20 
21 import android.app.Activity;
22 import android.app.Fragment;
23 import android.app.KeyguardManager;
24 import android.content.ContentResolver;
25 import android.content.Context;
26 import android.content.pm.PackageManager;
27 import android.database.ContentObserver;
28 import android.database.Cursor;
29 import android.os.Bundle;
30 import android.os.Handler;
31 import android.os.Message;
32 import android.provider.CallLog;
33 import android.provider.CallLog.Calls;
34 import android.provider.ContactsContract;
35 import android.support.annotation.CallSuper;
36 import android.support.annotation.Nullable;
37 import android.support.v13.app.FragmentCompat;
38 import android.support.v13.app.FragmentCompat.OnRequestPermissionsResultCallback;
39 import android.support.v7.app.AppCompatActivity;
40 import android.support.v7.widget.LinearLayoutManager;
41 import android.support.v7.widget.RecyclerView;
42 import android.view.LayoutInflater;
43 import android.view.View;
44 import android.view.View.OnClickListener;
45 import android.view.ViewGroup;
46 import android.widget.ImageView;
47 import android.widget.TextView;
48 import com.android.dialer.app.Bindings;
49 import com.android.dialer.app.R;
50 import com.android.dialer.app.calllog.CallLogAdapter.CallFetcher;
51 import com.android.dialer.app.calllog.CallLogAdapter.MultiSelectRemoveView;
52 import com.android.dialer.app.calllog.calllogcache.CallLogCache;
53 import com.android.dialer.app.contactinfo.ContactInfoCache;
54 import com.android.dialer.app.contactinfo.ContactInfoCache.OnContactInfoChangedListener;
55 import com.android.dialer.app.contactinfo.ExpirableCacheHeadlessFragment;
56 import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter;
57 import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
58 import com.android.dialer.common.Assert;
59 import com.android.dialer.common.FragmentUtils;
60 import com.android.dialer.common.LogUtil;
61 import com.android.dialer.configprovider.ConfigProviderComponent;
62 import com.android.dialer.database.CallLogQueryHandler;
63 import com.android.dialer.database.CallLogQueryHandler.Listener;
64 import com.android.dialer.location.GeoUtil;
65 import com.android.dialer.logging.DialerImpression;
66 import com.android.dialer.logging.Logger;
67 import com.android.dialer.metrics.Metrics;
68 import com.android.dialer.metrics.MetricsComponent;
69 import com.android.dialer.metrics.jank.RecyclerViewJankLogger;
70 import com.android.dialer.oem.CequintCallerIdManager;
71 import com.android.dialer.performancereport.PerformanceReport;
72 import com.android.dialer.phonenumbercache.ContactInfoHelper;
73 import com.android.dialer.util.PermissionsUtil;
74 import com.android.dialer.widget.EmptyContentView;
75 import com.android.dialer.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener;
76 import java.util.Arrays;
77 
78 /**
79  * Displays a list of call log entries. To filter for a particular kind of call (all, missed or
80  * voicemails), specify it in the constructor.
81  */
82 public class CallLogFragment extends Fragment
83     implements Listener,
84         CallFetcher,
85         MultiSelectRemoveView,
86         OnEmptyViewActionButtonClickedListener,
87         OnRequestPermissionsResultCallback,
88         CallLogModalAlertManager.Listener,
89         OnClickListener {
90   private static final String KEY_FILTER_TYPE = "filter_type";
91   private static final String KEY_LOG_LIMIT = "log_limit";
92   private static final String KEY_DATE_LIMIT = "date_limit";
93   private static final String KEY_IS_CALL_LOG_ACTIVITY = "is_call_log_activity";
94   private static final String KEY_HAS_READ_CALL_LOG_PERMISSION = "has_read_call_log_permission";
95   private static final String KEY_REFRESH_DATA_REQUIRED = "refresh_data_required";
96   private static final String KEY_SELECT_ALL_MODE = "select_all_mode_checked";
97 
98   // No limit specified for the number of logs to show; use the CallLogQueryHandler's default.
99   private static final int NO_LOG_LIMIT = -1;
100   // No date-based filtering.
101   private static final int NO_DATE_LIMIT = 0;
102 
103   private static final int PHONE_PERMISSIONS_REQUEST_CODE = 1;
104 
105   private static final int EVENT_UPDATE_DISPLAY = 1;
106 
107   private static final long MILLIS_IN_MINUTE = 60 * 1000;
108   private final Handler handler = new Handler();
109   // See issue 6363009
110   private final ContentObserver callLogObserver = new CustomContentObserver();
111   private final ContentObserver contactsObserver = new CustomContentObserver();
112   private View multiSelectUnSelectAllViewContent;
113   private TextView selectUnselectAllViewText;
114   private ImageView selectUnselectAllIcon;
115   private RecyclerView recyclerView;
116   private LinearLayoutManager layoutManager;
117   private CallLogAdapter adapter;
118   private CallLogQueryHandler callLogQueryHandler;
119   private boolean scrollToTop;
120   private EmptyContentView emptyListView;
121   private ContactInfoCache contactInfoCache;
122   private final OnContactInfoChangedListener onContactInfoChangedListener =
123       new OnContactInfoChangedListener() {
124         @Override
125         public void onContactInfoChanged() {
126           if (adapter != null) {
127             adapter.notifyDataSetChanged();
128           }
129         }
130       };
131   private boolean refreshDataRequired;
132   private boolean hasReadCallLogPermission;
133   // Exactly same variable is in Fragment as a package private.
134   private boolean menuVisible = true;
135   // Default to all calls.
136   private int callTypeFilter = CallLogQueryHandler.CALL_TYPE_ALL;
137   // Log limit - if no limit is specified, then the default in {@link CallLogQueryHandler}
138   // will be used.
139   private int logLimit = NO_LOG_LIMIT;
140   // Date limit (in millis since epoch) - when non-zero, only calls which occurred on or after
141   // the date filter are included.  If zero, no date-based filtering occurs.
142   private long dateLimit = NO_DATE_LIMIT;
143   /*
144    * True if this instance of the CallLogFragment shown in the CallLogActivity.
145    */
146   private boolean isCallLogActivity = false;
147   private boolean selectAllMode;
148   private final Handler displayUpdateHandler =
149       new Handler() {
150         @Override
151         public void handleMessage(Message msg) {
152           switch (msg.what) {
153             case EVENT_UPDATE_DISPLAY:
154               refreshData();
155               rescheduleDisplayUpdate();
156               break;
157             default:
158               throw Assert.createAssertionFailException("Invalid message: " + msg);
159           }
160         }
161       };
162   protected CallLogModalAlertManager modalAlertManager;
163   private ViewGroup modalAlertView;
164 
CallLogFragment()165   public CallLogFragment() {
166     this(CallLogQueryHandler.CALL_TYPE_ALL, NO_LOG_LIMIT);
167   }
168 
CallLogFragment(int filterType)169   public CallLogFragment(int filterType) {
170     this(filterType, NO_LOG_LIMIT);
171   }
172 
CallLogFragment(int filterType, boolean isCallLogActivity)173   public CallLogFragment(int filterType, boolean isCallLogActivity) {
174     this(filterType, NO_LOG_LIMIT);
175     this.isCallLogActivity = isCallLogActivity;
176   }
177 
CallLogFragment(int filterType, int logLimit)178   public CallLogFragment(int filterType, int logLimit) {
179     this(filterType, logLimit, NO_DATE_LIMIT);
180   }
181 
182   /**
183    * Creates a call log fragment, filtering to include only calls of the desired type, occurring
184    * after the specified date.
185    *
186    * @param filterType type of calls to include.
187    * @param dateLimit limits results to calls occurring on or after the specified date.
188    */
CallLogFragment(int filterType, long dateLimit)189   public CallLogFragment(int filterType, long dateLimit) {
190     this(filterType, NO_LOG_LIMIT, dateLimit);
191   }
192 
193   /**
194    * Creates a call log fragment, filtering to include only calls of the desired type, occurring
195    * after the specified date. Also provides a means to limit the number of results returned.
196    *
197    * @param filterType type of calls to include.
198    * @param logLimit limits the number of results to return.
199    * @param dateLimit limits results to calls occurring on or after the specified date.
200    */
CallLogFragment(int filterType, int logLimit, long dateLimit)201   public CallLogFragment(int filterType, int logLimit, long dateLimit) {
202     callTypeFilter = filterType;
203     this.logLimit = logLimit;
204     this.dateLimit = dateLimit;
205   }
206 
207   @Override
onCreate(Bundle state)208   public void onCreate(Bundle state) {
209     LogUtil.enterBlock("CallLogFragment.onCreate");
210     super.onCreate(state);
211     refreshDataRequired = true;
212     if (state != null) {
213       callTypeFilter = state.getInt(KEY_FILTER_TYPE, callTypeFilter);
214       logLimit = state.getInt(KEY_LOG_LIMIT, logLimit);
215       dateLimit = state.getLong(KEY_DATE_LIMIT, dateLimit);
216       isCallLogActivity = state.getBoolean(KEY_IS_CALL_LOG_ACTIVITY, isCallLogActivity);
217       hasReadCallLogPermission = state.getBoolean(KEY_HAS_READ_CALL_LOG_PERMISSION, false);
218       refreshDataRequired = state.getBoolean(KEY_REFRESH_DATA_REQUIRED, refreshDataRequired);
219       selectAllMode = state.getBoolean(KEY_SELECT_ALL_MODE, false);
220     }
221 
222     final Activity activity = getActivity();
223     final ContentResolver resolver = activity.getContentResolver();
224     callLogQueryHandler = new CallLogQueryHandler(activity, resolver, this, logLimit);
225     setHasOptionsMenu(true);
226   }
227 
228   /** Called by the CallLogQueryHandler when the list of calls has been fetched or updated. */
229   @Override
onCallsFetched(Cursor cursor)230   public boolean onCallsFetched(Cursor cursor) {
231     if (getActivity() == null || getActivity().isFinishing()) {
232       // Return false; we did not take ownership of the cursor
233       return false;
234     }
235     adapter.invalidatePositions();
236     adapter.setLoading(false);
237     adapter.changeCursor(cursor);
238     // This will update the state of the "Clear call log" menu item.
239     getActivity().invalidateOptionsMenu();
240 
241     if (cursor != null && cursor.getCount() > 0) {
242       recyclerView.setPaddingRelative(
243           recyclerView.getPaddingStart(),
244           0,
245           recyclerView.getPaddingEnd(),
246           getResources().getDimensionPixelSize(R.dimen.floating_action_button_list_bottom_padding));
247       emptyListView.setVisibility(View.GONE);
248     } else {
249       recyclerView.setPaddingRelative(
250           recyclerView.getPaddingStart(), 0, recyclerView.getPaddingEnd(), 0);
251       emptyListView.setVisibility(View.VISIBLE);
252     }
253     if (scrollToTop) {
254       // The smooth-scroll animation happens over a fixed time period.
255       // As a result, if it scrolls through a large portion of the list,
256       // each frame will jump so far from the previous one that the user
257       // will not experience the illusion of downward motion.  Instead,
258       // if we're not already near the top of the list, we instantly jump
259       // near the top, and animate from there.
260       if (layoutManager.findFirstVisibleItemPosition() > 5) {
261         // TODO: Jump to near the top, then begin smooth scroll.
262         recyclerView.smoothScrollToPosition(0);
263       }
264       // Workaround for framework issue: the smooth-scroll doesn't
265       // occur if setSelection() is called immediately before.
266       handler.post(
267           new Runnable() {
268             @Override
269             public void run() {
270               if (getActivity() == null || getActivity().isFinishing()) {
271                 return;
272               }
273               recyclerView.smoothScrollToPosition(0);
274             }
275           });
276 
277       scrollToTop = false;
278     }
279     return true;
280   }
281 
282   @Override
onVoicemailStatusFetched(Cursor statusCursor)283   public void onVoicemailStatusFetched(Cursor statusCursor) {}
284 
285   @Override
onVoicemailUnreadCountFetched(Cursor cursor)286   public void onVoicemailUnreadCountFetched(Cursor cursor) {}
287 
288   @Override
onMissedCallsUnreadCountFetched(Cursor cursor)289   public void onMissedCallsUnreadCountFetched(Cursor cursor) {}
290 
291   @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState)292   public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
293     View view = inflater.inflate(R.layout.call_log_fragment, container, false);
294     setupView(view);
295     return view;
296   }
297 
setupView(View view)298   protected void setupView(View view) {
299     recyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
300     if (ConfigProviderComponent.get(getContext())
301         .getConfigProvider()
302         .getBoolean("is_call_log_item_anim_null", false)) {
303       recyclerView.setItemAnimator(null);
304     }
305     recyclerView.setHasFixedSize(true);
306     recyclerView.addOnScrollListener(
307         new RecyclerViewJankLogger(
308             MetricsComponent.get(getContext()).metrics(), Metrics.OLD_CALL_LOG_JANK_EVENT_NAME));
309     layoutManager = new LinearLayoutManager(getActivity());
310     recyclerView.setLayoutManager(layoutManager);
311     PerformanceReport.logOnScrollStateChange(recyclerView);
312     emptyListView = (EmptyContentView) view.findViewById(R.id.empty_list_view);
313     emptyListView.setImage(R.drawable.empty_call_log);
314     emptyListView.setActionClickedListener(this);
315     modalAlertView = (ViewGroup) view.findViewById(R.id.modal_message_container);
316     modalAlertManager =
317         new CallLogModalAlertManager(LayoutInflater.from(getContext()), modalAlertView, this);
318     multiSelectUnSelectAllViewContent =
319         view.findViewById(R.id.multi_select_select_all_view_content);
320     selectUnselectAllViewText = (TextView) view.findViewById(R.id.select_all_view_text);
321     selectUnselectAllIcon = (ImageView) view.findViewById(R.id.select_all_view_icon);
322     multiSelectUnSelectAllViewContent.setOnClickListener(null);
323     selectUnselectAllIcon.setOnClickListener(this);
324     selectUnselectAllViewText.setOnClickListener(this);
325   }
326 
setupData()327   protected void setupData() {
328     int activityType =
329         isCallLogActivity
330             ? CallLogAdapter.ACTIVITY_TYPE_CALL_LOG
331             : CallLogAdapter.ACTIVITY_TYPE_DIALTACTS;
332     String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity());
333 
334     contactInfoCache =
335         new ContactInfoCache(
336             ExpirableCacheHeadlessFragment.attach((AppCompatActivity) getActivity())
337                 .getRetainedCache(),
338             new ContactInfoHelper(getActivity(), currentCountryIso),
339             onContactInfoChangedListener);
340     adapter =
341         Bindings.getLegacy(getActivity())
342             .newCallLogAdapter(
343                 getActivity(),
344                 recyclerView,
345                 this,
346                 this,
347                 // We aren't calling getParentUnsafe because CallLogActivity doesn't need to
348                 // implement this listener
349                 FragmentUtils.getParent(
350                     this, CallLogAdapter.OnActionModeStateChangedListener.class),
351                 new CallLogCache(getActivity()),
352                 contactInfoCache,
353                 getVoicemailPlaybackPresenter(),
354                 new FilteredNumberAsyncQueryHandler(getActivity()),
355                 activityType);
356     recyclerView.setAdapter(adapter);
357     if (adapter.getOnScrollListener() != null) {
358       recyclerView.addOnScrollListener(adapter.getOnScrollListener());
359     }
360     fetchCalls();
361   }
362 
363   @Nullable
getVoicemailPlaybackPresenter()364   protected VoicemailPlaybackPresenter getVoicemailPlaybackPresenter() {
365     return null;
366   }
367 
368   @Override
onActivityCreated(Bundle savedInstanceState)369   public void onActivityCreated(Bundle savedInstanceState) {
370     LogUtil.enterBlock("CallLogFragment.onActivityCreated");
371     super.onActivityCreated(savedInstanceState);
372     setupData();
373     updateSelectAllState(savedInstanceState);
374     adapter.onRestoreInstanceState(savedInstanceState);
375   }
376 
updateSelectAllState(Bundle savedInstanceState)377   private void updateSelectAllState(Bundle savedInstanceState) {
378     if (savedInstanceState != null) {
379       if (savedInstanceState.getBoolean(KEY_SELECT_ALL_MODE, false)) {
380         updateSelectAllIcon();
381       }
382     }
383   }
384 
385   @Override
onViewCreated(View view, Bundle savedInstanceState)386   public void onViewCreated(View view, Bundle savedInstanceState) {
387     super.onViewCreated(view, savedInstanceState);
388     updateEmptyMessage(callTypeFilter);
389   }
390 
391   @Override
onResume()392   public void onResume() {
393     LogUtil.enterBlock("CallLogFragment.onResume");
394     super.onResume();
395     final boolean hasReadCallLogPermission =
396         PermissionsUtil.hasPermission(getActivity(), READ_CALL_LOG);
397     if (!this.hasReadCallLogPermission && hasReadCallLogPermission) {
398       // We didn't have the permission before, and now we do. Force a refresh of the call log.
399       // Note that this code path always happens on a fresh start, but mRefreshDataRequired
400       // is already true in that case anyway.
401       refreshDataRequired = true;
402       updateEmptyMessage(callTypeFilter);
403     }
404 
405     ContentResolver resolver = getActivity().getContentResolver();
406     if (PermissionsUtil.hasCallLogReadPermissions(getContext())) {
407       resolver.registerContentObserver(CallLog.CONTENT_URI, true, callLogObserver);
408     } else {
409       LogUtil.w("CallLogFragment.onCreate", "call log permission not available");
410     }
411     if (PermissionsUtil.hasContactsReadPermissions(getContext())) {
412       resolver.registerContentObserver(
413           ContactsContract.Contacts.CONTENT_URI, true, contactsObserver);
414     } else {
415       LogUtil.w("CallLogFragment.onCreate", "contacts permission not available.");
416     }
417 
418     this.hasReadCallLogPermission = hasReadCallLogPermission;
419 
420     /*
421      * Always clear the filtered numbers cache since users could have blocked/unblocked numbers
422      * from the settings page
423      */
424     adapter.clearFilteredNumbersCache();
425     refreshData();
426     adapter.onResume();
427 
428     rescheduleDisplayUpdate();
429     // onResume() may also be called as a "side" page on the ViewPager, which is not visible.
430     if (getUserVisibleHint()) {
431       onVisible();
432     }
433   }
434 
435   @Override
onPause()436   public void onPause() {
437     LogUtil.enterBlock("CallLogFragment.onPause");
438     getActivity().getContentResolver().unregisterContentObserver(callLogObserver);
439     getActivity().getContentResolver().unregisterContentObserver(contactsObserver);
440     if (getUserVisibleHint()) {
441       onNotVisible();
442     }
443     cancelDisplayUpdate();
444     adapter.onPause();
445     super.onPause();
446   }
447 
448   @Override
onStart()449   public void onStart() {
450     LogUtil.enterBlock("CallLogFragment.onStart");
451     super.onStart();
452     CequintCallerIdManager cequintCallerIdManager = null;
453     if (CequintCallerIdManager.isCequintCallerIdEnabled(getContext())) {
454       cequintCallerIdManager = new CequintCallerIdManager();
455     }
456     contactInfoCache.setCequintCallerIdManager(cequintCallerIdManager);
457   }
458 
459   @Override
onStop()460   public void onStop() {
461     LogUtil.enterBlock("CallLogFragment.onStop");
462     super.onStop();
463     adapter.onStop();
464     contactInfoCache.stop();
465   }
466 
467   @Override
onDestroy()468   public void onDestroy() {
469     LogUtil.enterBlock("CallLogFragment.onDestroy");
470     if (adapter != null) {
471       adapter.changeCursor(null);
472     }
473     super.onDestroy();
474   }
475 
476   @Override
onSaveInstanceState(Bundle outState)477   public void onSaveInstanceState(Bundle outState) {
478     super.onSaveInstanceState(outState);
479     outState.putInt(KEY_FILTER_TYPE, callTypeFilter);
480     outState.putInt(KEY_LOG_LIMIT, logLimit);
481     outState.putLong(KEY_DATE_LIMIT, dateLimit);
482     outState.putBoolean(KEY_IS_CALL_LOG_ACTIVITY, isCallLogActivity);
483     outState.putBoolean(KEY_HAS_READ_CALL_LOG_PERMISSION, hasReadCallLogPermission);
484     outState.putBoolean(KEY_REFRESH_DATA_REQUIRED, refreshDataRequired);
485     outState.putBoolean(KEY_SELECT_ALL_MODE, selectAllMode);
486     if (adapter != null) {
487       adapter.onSaveInstanceState(outState);
488     }
489   }
490 
491   @Override
fetchCalls()492   public void fetchCalls() {
493     callLogQueryHandler.fetchCalls(callTypeFilter, dateLimit);
494     if (!isCallLogActivity
495         && getActivity() != null
496         && !getActivity().isFinishing()
497         && FragmentUtils.getParent(this, CallLogFragmentListener.class) != null) {
498       FragmentUtils.getParentUnsafe(this, CallLogFragmentListener.class).updateTabUnreadCounts();
499     }
500   }
501 
updateEmptyMessage(int filterType)502   private void updateEmptyMessage(int filterType) {
503     final Context context = getActivity();
504     if (context == null) {
505       return;
506     }
507 
508     if (!PermissionsUtil.hasPermission(context, READ_CALL_LOG)) {
509       emptyListView.setDescription(R.string.permission_no_calllog);
510       emptyListView.setActionLabel(R.string.permission_single_turn_on);
511       return;
512     }
513 
514     final int messageId;
515     switch (filterType) {
516       case Calls.MISSED_TYPE:
517         messageId = R.string.call_log_missed_empty;
518         break;
519       case Calls.VOICEMAIL_TYPE:
520         messageId = R.string.call_log_voicemail_empty;
521         break;
522       case CallLogQueryHandler.CALL_TYPE_ALL:
523         messageId = R.string.call_log_all_empty;
524         break;
525       default:
526         throw new IllegalArgumentException(
527             "Unexpected filter type in CallLogFragment: " + filterType);
528     }
529     emptyListView.setDescription(messageId);
530     if (isCallLogActivity) {
531       emptyListView.setActionLabel(EmptyContentView.NO_LABEL);
532     } else if (filterType == CallLogQueryHandler.CALL_TYPE_ALL) {
533       emptyListView.setActionLabel(R.string.call_log_all_empty_action);
534     } else {
535       emptyListView.setActionLabel(EmptyContentView.NO_LABEL);
536     }
537   }
538 
getAdapter()539   public CallLogAdapter getAdapter() {
540     return adapter;
541   }
542 
543   @Override
setMenuVisibility(boolean menuVisible)544   public void setMenuVisibility(boolean menuVisible) {
545     super.setMenuVisibility(menuVisible);
546     if (this.menuVisible != menuVisible) {
547       this.menuVisible = menuVisible;
548       if (menuVisible && isResumed()) {
549         refreshData();
550       }
551     }
552   }
553 
554   /** Requests updates to the data to be shown. */
refreshData()555   private void refreshData() {
556     // Prevent unnecessary refresh.
557     if (refreshDataRequired) {
558       // Mark all entries in the contact info cache as out of date, so they will be looked up
559       // again once being shown.
560       contactInfoCache.invalidate();
561       adapter.setLoading(true);
562 
563       fetchCalls();
564       callLogQueryHandler.fetchVoicemailStatus();
565       callLogQueryHandler.fetchMissedCallsUnreadCount();
566       refreshDataRequired = false;
567     } else {
568       // Refresh the display of the existing data to update the timestamp text descriptions.
569       adapter.notifyDataSetChanged();
570     }
571   }
572 
573   @Override
onEmptyViewActionButtonClicked()574   public void onEmptyViewActionButtonClicked() {
575     final Activity activity = getActivity();
576     if (activity == null) {
577       return;
578     }
579 
580     String[] deniedPermissions =
581         PermissionsUtil.getPermissionsCurrentlyDenied(
582             getContext(), PermissionsUtil.allPhoneGroupPermissionsUsedInDialer);
583     if (deniedPermissions.length > 0) {
584       LogUtil.i(
585           "CallLogFragment.onEmptyViewActionButtonClicked",
586           "Requesting permissions: " + Arrays.toString(deniedPermissions));
587       FragmentCompat.requestPermissions(this, deniedPermissions, PHONE_PERMISSIONS_REQUEST_CODE);
588     } else if (!isCallLogActivity) {
589       LogUtil.i("CallLogFragment.onEmptyViewActionButtonClicked", "showing dialpad");
590       // Show dialpad if we are not in the call log activity.
591       FragmentUtils.getParentUnsafe(this, HostInterface.class).showDialpad();
592     }
593   }
594 
595   @Override
onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults)596   public void onRequestPermissionsResult(
597       int requestCode, String[] permissions, int[] grantResults) {
598     if (requestCode == PHONE_PERMISSIONS_REQUEST_CODE) {
599       if (grantResults.length >= 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
600         // Force a refresh of the data since we were missing the permission before this.
601         refreshDataRequired = true;
602       }
603     }
604   }
605 
606   /** Schedules an update to the relative call times (X mins ago). */
rescheduleDisplayUpdate()607   private void rescheduleDisplayUpdate() {
608     if (!displayUpdateHandler.hasMessages(EVENT_UPDATE_DISPLAY)) {
609       long time = System.currentTimeMillis();
610       // This value allows us to change the display relatively close to when the time changes
611       // from one minute to the next.
612       long millisUtilNextMinute = MILLIS_IN_MINUTE - (time % MILLIS_IN_MINUTE);
613       displayUpdateHandler.sendEmptyMessageDelayed(EVENT_UPDATE_DISPLAY, millisUtilNextMinute);
614     }
615   }
616 
617   /** Cancels any pending update requests to update the relative call times (X mins ago). */
cancelDisplayUpdate()618   private void cancelDisplayUpdate() {
619     displayUpdateHandler.removeMessages(EVENT_UPDATE_DISPLAY);
620   }
621 
622   /** Mark all missed calls as read if Keyguard not locked and possible. */
markMissedCallsAsReadAndRemoveNotifications()623   void markMissedCallsAsReadAndRemoveNotifications() {
624     if (callLogQueryHandler != null
625         && !getContext().getSystemService(KeyguardManager.class).isKeyguardLocked()) {
626       callLogQueryHandler.markMissedCallsAsRead();
627       CallLogNotificationsService.cancelAllMissedCalls(getContext());
628     }
629   }
630 
631   @CallSuper
onVisible()632   public void onVisible() {
633     LogUtil.enterBlock("CallLogFragment.onPageSelected");
634     if (getActivity() != null && FragmentUtils.getParent(this, HostInterface.class) != null) {
635       FragmentUtils.getParentUnsafe(this, HostInterface.class)
636           .enableFloatingButton(!isModalAlertVisible());
637     }
638   }
639 
isModalAlertVisible()640   public boolean isModalAlertVisible() {
641     return modalAlertManager != null && !modalAlertManager.isEmpty();
642   }
643 
644   @CallSuper
onNotVisible()645   public void onNotVisible() {
646     LogUtil.enterBlock("CallLogFragment.onPageUnselected");
647   }
648 
649   @Override
onShowModalAlert(boolean show)650   public void onShowModalAlert(boolean show) {
651     LogUtil.d(
652         "CallLogFragment.onShowModalAlert",
653         "show: %b, fragment: %s, isVisible: %b",
654         show,
655         this,
656         getUserVisibleHint());
657     getAdapter().notifyDataSetChanged();
658     HostInterface hostInterface = FragmentUtils.getParent(this, HostInterface.class);
659     if (show) {
660       recyclerView.setVisibility(View.GONE);
661       modalAlertView.setVisibility(View.VISIBLE);
662       if (hostInterface != null && getUserVisibleHint()) {
663         hostInterface.enableFloatingButton(false);
664       }
665     } else {
666       recyclerView.setVisibility(View.VISIBLE);
667       modalAlertView.setVisibility(View.GONE);
668       if (hostInterface != null && getUserVisibleHint()) {
669         hostInterface.enableFloatingButton(true);
670       }
671     }
672   }
673 
674   @Override
showMultiSelectRemoveView(boolean show)675   public void showMultiSelectRemoveView(boolean show) {
676     multiSelectUnSelectAllViewContent.setVisibility(show ? View.VISIBLE : View.GONE);
677     multiSelectUnSelectAllViewContent.setAlpha(show ? 0 : 1);
678     multiSelectUnSelectAllViewContent.animate().alpha(show ? 1 : 0).start();
679     if (show) {
680       FragmentUtils.getParentUnsafe(this, CallLogFragmentListener.class)
681           .showMultiSelectRemoveView(true);
682     } else {
683       // This method is called after onDestroy. In DialtactsActivity, ListsFragment implements this
684       // interface and never goes away with configuration changes so this is safe. MainActivity
685       // removes that extra layer though, so we need to check if the parent is still there.
686       CallLogFragmentListener listener =
687           FragmentUtils.getParent(this, CallLogFragmentListener.class);
688       if (listener != null) {
689         listener.showMultiSelectRemoveView(false);
690       }
691     }
692   }
693 
694   @Override
setSelectAllModeToFalse()695   public void setSelectAllModeToFalse() {
696     selectAllMode = false;
697     selectUnselectAllIcon.setImageDrawable(
698         getContext().getDrawable(R.drawable.ic_empty_check_mark_white_24dp));
699   }
700 
701   @Override
tapSelectAll()702   public void tapSelectAll() {
703     LogUtil.i("CallLogFragment.tapSelectAll", "imitating select all");
704     selectAllMode = true;
705     updateSelectAllIcon();
706   }
707 
708   @Override
onClick(View v)709   public void onClick(View v) {
710     selectAllMode = !selectAllMode;
711     if (selectAllMode) {
712       Logger.get(v.getContext()).logImpression(DialerImpression.Type.MULTISELECT_SELECT_ALL);
713     } else {
714       Logger.get(v.getContext()).logImpression(DialerImpression.Type.MULTISELECT_UNSELECT_ALL);
715     }
716     updateSelectAllIcon();
717   }
718 
updateSelectAllIcon()719   private void updateSelectAllIcon() {
720     if (selectAllMode) {
721       selectUnselectAllIcon.setImageDrawable(
722           getContext().getDrawable(R.drawable.ic_check_mark_blue_24dp));
723       getAdapter().onAllSelected();
724     } else {
725       selectUnselectAllIcon.setImageDrawable(
726           getContext().getDrawable(R.drawable.ic_empty_check_mark_white_24dp));
727       getAdapter().onAllDeselected();
728     }
729   }
730 
731   public interface HostInterface {
732 
showDialpad()733     void showDialpad();
734 
enableFloatingButton(boolean enabled)735     void enableFloatingButton(boolean enabled);
736   }
737 
738   protected class CustomContentObserver extends ContentObserver {
739 
CustomContentObserver()740     public CustomContentObserver() {
741       super(handler);
742     }
743 
744     @Override
onChange(boolean selfChange)745     public void onChange(boolean selfChange) {
746       refreshDataRequired = true;
747     }
748   }
749 
750   /** Useful callback for ListsFragment children to use to call into ListsFragment. */
751   public interface CallLogFragmentListener {
752 
753     /**
754      * External method to update unread count because the unread count changes when the user expands
755      * a voicemail in the call log or when the user expands an unread call in the call history tab.
756      */
updateTabUnreadCounts()757     void updateTabUnreadCounts();
758 
showMultiSelectRemoveView(boolean show)759     void showMultiSelectRemoveView(boolean show);
760   }
761 }
762