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.ConfigProviderBindings; 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 (ConfigProviderBindings.get(getContext()).getBoolean("is_call_log_item_anim_null", false)) { 301 recyclerView.setItemAnimator(null); 302 } 303 recyclerView.setHasFixedSize(true); 304 recyclerView.addOnScrollListener( 305 new RecyclerViewJankLogger( 306 MetricsComponent.get(getContext()).metrics(), Metrics.OLD_CALL_LOG_JANK_EVENT_NAME)); 307 layoutManager = new LinearLayoutManager(getActivity()); 308 recyclerView.setLayoutManager(layoutManager); 309 PerformanceReport.logOnScrollStateChange(recyclerView); 310 emptyListView = (EmptyContentView) view.findViewById(R.id.empty_list_view); 311 emptyListView.setImage(R.drawable.empty_call_log); 312 emptyListView.setActionClickedListener(this); 313 modalAlertView = (ViewGroup) view.findViewById(R.id.modal_message_container); 314 modalAlertManager = 315 new CallLogModalAlertManager(LayoutInflater.from(getContext()), modalAlertView, this); 316 multiSelectUnSelectAllViewContent = 317 view.findViewById(R.id.multi_select_select_all_view_content); 318 selectUnselectAllViewText = (TextView) view.findViewById(R.id.select_all_view_text); 319 selectUnselectAllIcon = (ImageView) view.findViewById(R.id.select_all_view_icon); 320 multiSelectUnSelectAllViewContent.setOnClickListener(null); 321 selectUnselectAllIcon.setOnClickListener(this); 322 selectUnselectAllViewText.setOnClickListener(this); 323 } 324 setupData()325 protected void setupData() { 326 int activityType = 327 isCallLogActivity 328 ? CallLogAdapter.ACTIVITY_TYPE_CALL_LOG 329 : CallLogAdapter.ACTIVITY_TYPE_DIALTACTS; 330 String currentCountryIso = GeoUtil.getCurrentCountryIso(getActivity()); 331 332 contactInfoCache = 333 new ContactInfoCache( 334 ExpirableCacheHeadlessFragment.attach((AppCompatActivity) getActivity()) 335 .getRetainedCache(), 336 new ContactInfoHelper(getActivity(), currentCountryIso), 337 onContactInfoChangedListener); 338 adapter = 339 Bindings.getLegacy(getActivity()) 340 .newCallLogAdapter( 341 getActivity(), 342 recyclerView, 343 this, 344 this, 345 // We aren't calling getParentUnsafe because CallLogActivity doesn't need to 346 // implement this listener 347 FragmentUtils.getParent( 348 this, CallLogAdapter.OnActionModeStateChangedListener.class), 349 new CallLogCache(getActivity()), 350 contactInfoCache, 351 getVoicemailPlaybackPresenter(), 352 new FilteredNumberAsyncQueryHandler(getActivity()), 353 activityType); 354 recyclerView.setAdapter(adapter); 355 if (adapter.getOnScrollListener() != null) { 356 recyclerView.addOnScrollListener(adapter.getOnScrollListener()); 357 } 358 fetchCalls(); 359 } 360 361 @Nullable getVoicemailPlaybackPresenter()362 protected VoicemailPlaybackPresenter getVoicemailPlaybackPresenter() { 363 return null; 364 } 365 366 @Override onActivityCreated(Bundle savedInstanceState)367 public void onActivityCreated(Bundle savedInstanceState) { 368 LogUtil.enterBlock("CallLogFragment.onActivityCreated"); 369 super.onActivityCreated(savedInstanceState); 370 setupData(); 371 updateSelectAllState(savedInstanceState); 372 adapter.onRestoreInstanceState(savedInstanceState); 373 } 374 updateSelectAllState(Bundle savedInstanceState)375 private void updateSelectAllState(Bundle savedInstanceState) { 376 if (savedInstanceState != null) { 377 if (savedInstanceState.getBoolean(KEY_SELECT_ALL_MODE, false)) { 378 updateSelectAllIcon(); 379 } 380 } 381 } 382 383 @Override onViewCreated(View view, Bundle savedInstanceState)384 public void onViewCreated(View view, Bundle savedInstanceState) { 385 super.onViewCreated(view, savedInstanceState); 386 updateEmptyMessage(callTypeFilter); 387 } 388 389 @Override onResume()390 public void onResume() { 391 LogUtil.enterBlock("CallLogFragment.onResume"); 392 super.onResume(); 393 final boolean hasReadCallLogPermission = 394 PermissionsUtil.hasPermission(getActivity(), READ_CALL_LOG); 395 if (!this.hasReadCallLogPermission && hasReadCallLogPermission) { 396 // We didn't have the permission before, and now we do. Force a refresh of the call log. 397 // Note that this code path always happens on a fresh start, but mRefreshDataRequired 398 // is already true in that case anyway. 399 refreshDataRequired = true; 400 updateEmptyMessage(callTypeFilter); 401 } 402 403 ContentResolver resolver = getActivity().getContentResolver(); 404 if (PermissionsUtil.hasCallLogReadPermissions(getContext())) { 405 resolver.registerContentObserver(CallLog.CONTENT_URI, true, callLogObserver); 406 } else { 407 LogUtil.w("CallLogFragment.onCreate", "call log permission not available"); 408 } 409 if (PermissionsUtil.hasContactsReadPermissions(getContext())) { 410 resolver.registerContentObserver( 411 ContactsContract.Contacts.CONTENT_URI, true, contactsObserver); 412 } else { 413 LogUtil.w("CallLogFragment.onCreate", "contacts permission not available."); 414 } 415 416 this.hasReadCallLogPermission = hasReadCallLogPermission; 417 418 /* 419 * Always clear the filtered numbers cache since users could have blocked/unblocked numbers 420 * from the settings page 421 */ 422 adapter.clearFilteredNumbersCache(); 423 refreshData(); 424 adapter.onResume(); 425 426 rescheduleDisplayUpdate(); 427 // onResume() may also be called as a "side" page on the ViewPager, which is not visible. 428 if (getUserVisibleHint()) { 429 onVisible(); 430 } 431 } 432 433 @Override onPause()434 public void onPause() { 435 LogUtil.enterBlock("CallLogFragment.onPause"); 436 getActivity().getContentResolver().unregisterContentObserver(callLogObserver); 437 getActivity().getContentResolver().unregisterContentObserver(contactsObserver); 438 if (getUserVisibleHint()) { 439 onNotVisible(); 440 } 441 cancelDisplayUpdate(); 442 adapter.onPause(); 443 super.onPause(); 444 } 445 446 @Override onStart()447 public void onStart() { 448 LogUtil.enterBlock("CallLogFragment.onStart"); 449 super.onStart(); 450 CequintCallerIdManager cequintCallerIdManager = null; 451 if (CequintCallerIdManager.isCequintCallerIdEnabled(getContext())) { 452 cequintCallerIdManager = CequintCallerIdManager.createInstanceForCallLog(); 453 } 454 contactInfoCache.setCequintCallerIdManager(cequintCallerIdManager); 455 } 456 457 @Override onStop()458 public void onStop() { 459 LogUtil.enterBlock("CallLogFragment.onStop"); 460 super.onStop(); 461 adapter.onStop(); 462 contactInfoCache.stop(); 463 } 464 465 @Override onDestroy()466 public void onDestroy() { 467 LogUtil.enterBlock("CallLogFragment.onDestroy"); 468 if (adapter != null) { 469 adapter.changeCursor(null); 470 } 471 super.onDestroy(); 472 } 473 474 @Override onSaveInstanceState(Bundle outState)475 public void onSaveInstanceState(Bundle outState) { 476 super.onSaveInstanceState(outState); 477 outState.putInt(KEY_FILTER_TYPE, callTypeFilter); 478 outState.putInt(KEY_LOG_LIMIT, logLimit); 479 outState.putLong(KEY_DATE_LIMIT, dateLimit); 480 outState.putBoolean(KEY_IS_CALL_LOG_ACTIVITY, isCallLogActivity); 481 outState.putBoolean(KEY_HAS_READ_CALL_LOG_PERMISSION, hasReadCallLogPermission); 482 outState.putBoolean(KEY_REFRESH_DATA_REQUIRED, refreshDataRequired); 483 outState.putBoolean(KEY_SELECT_ALL_MODE, selectAllMode); 484 if (adapter != null) { 485 adapter.onSaveInstanceState(outState); 486 } 487 } 488 489 @Override fetchCalls()490 public void fetchCalls() { 491 callLogQueryHandler.fetchCalls(callTypeFilter, dateLimit); 492 if (!isCallLogActivity) { 493 FragmentUtils.getParentUnsafe(this, CallLogFragmentListener.class).updateTabUnreadCounts(); 494 } 495 } 496 updateEmptyMessage(int filterType)497 private void updateEmptyMessage(int filterType) { 498 final Context context = getActivity(); 499 if (context == null) { 500 return; 501 } 502 503 if (!PermissionsUtil.hasPermission(context, READ_CALL_LOG)) { 504 emptyListView.setDescription(R.string.permission_no_calllog); 505 emptyListView.setActionLabel(R.string.permission_single_turn_on); 506 return; 507 } 508 509 final int messageId; 510 switch (filterType) { 511 case Calls.MISSED_TYPE: 512 messageId = R.string.call_log_missed_empty; 513 break; 514 case Calls.VOICEMAIL_TYPE: 515 messageId = R.string.call_log_voicemail_empty; 516 break; 517 case CallLogQueryHandler.CALL_TYPE_ALL: 518 messageId = R.string.call_log_all_empty; 519 break; 520 default: 521 throw new IllegalArgumentException( 522 "Unexpected filter type in CallLogFragment: " + filterType); 523 } 524 emptyListView.setDescription(messageId); 525 if (isCallLogActivity) { 526 emptyListView.setActionLabel(EmptyContentView.NO_LABEL); 527 } else if (filterType == CallLogQueryHandler.CALL_TYPE_ALL) { 528 emptyListView.setActionLabel(R.string.call_log_all_empty_action); 529 } else { 530 emptyListView.setActionLabel(EmptyContentView.NO_LABEL); 531 } 532 } 533 getAdapter()534 public CallLogAdapter getAdapter() { 535 return adapter; 536 } 537 538 @Override setMenuVisibility(boolean menuVisible)539 public void setMenuVisibility(boolean menuVisible) { 540 super.setMenuVisibility(menuVisible); 541 if (this.menuVisible != menuVisible) { 542 this.menuVisible = menuVisible; 543 if (menuVisible && isResumed()) { 544 refreshData(); 545 } 546 } 547 } 548 549 /** Requests updates to the data to be shown. */ refreshData()550 private void refreshData() { 551 // Prevent unnecessary refresh. 552 if (refreshDataRequired) { 553 // Mark all entries in the contact info cache as out of date, so they will be looked up 554 // again once being shown. 555 contactInfoCache.invalidate(); 556 adapter.setLoading(true); 557 558 fetchCalls(); 559 callLogQueryHandler.fetchVoicemailStatus(); 560 callLogQueryHandler.fetchMissedCallsUnreadCount(); 561 refreshDataRequired = false; 562 } else { 563 // Refresh the display of the existing data to update the timestamp text descriptions. 564 adapter.notifyDataSetChanged(); 565 } 566 } 567 568 @Override onEmptyViewActionButtonClicked()569 public void onEmptyViewActionButtonClicked() { 570 final Activity activity = getActivity(); 571 if (activity == null) { 572 return; 573 } 574 575 String[] deniedPermissions = 576 PermissionsUtil.getPermissionsCurrentlyDenied( 577 getContext(), PermissionsUtil.allPhoneGroupPermissionsUsedInDialer); 578 if (deniedPermissions.length > 0) { 579 LogUtil.i( 580 "CallLogFragment.onEmptyViewActionButtonClicked", 581 "Requesting permissions: " + Arrays.toString(deniedPermissions)); 582 FragmentCompat.requestPermissions(this, deniedPermissions, PHONE_PERMISSIONS_REQUEST_CODE); 583 } else if (!isCallLogActivity) { 584 LogUtil.i("CallLogFragment.onEmptyViewActionButtonClicked", "showing dialpad"); 585 // Show dialpad if we are not in the call log activity. 586 FragmentUtils.getParentUnsafe(this, HostInterface.class).showDialpad(); 587 } 588 } 589 590 @Override onRequestPermissionsResult( int requestCode, String[] permissions, int[] grantResults)591 public void onRequestPermissionsResult( 592 int requestCode, String[] permissions, int[] grantResults) { 593 if (requestCode == PHONE_PERMISSIONS_REQUEST_CODE) { 594 if (grantResults.length >= 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) { 595 // Force a refresh of the data since we were missing the permission before this. 596 refreshDataRequired = true; 597 } 598 } 599 } 600 601 /** Schedules an update to the relative call times (X mins ago). */ rescheduleDisplayUpdate()602 private void rescheduleDisplayUpdate() { 603 if (!displayUpdateHandler.hasMessages(EVENT_UPDATE_DISPLAY)) { 604 long time = System.currentTimeMillis(); 605 // This value allows us to change the display relatively close to when the time changes 606 // from one minute to the next. 607 long millisUtilNextMinute = MILLIS_IN_MINUTE - (time % MILLIS_IN_MINUTE); 608 displayUpdateHandler.sendEmptyMessageDelayed(EVENT_UPDATE_DISPLAY, millisUtilNextMinute); 609 } 610 } 611 612 /** Cancels any pending update requests to update the relative call times (X mins ago). */ cancelDisplayUpdate()613 private void cancelDisplayUpdate() { 614 displayUpdateHandler.removeMessages(EVENT_UPDATE_DISPLAY); 615 } 616 617 /** Mark all missed calls as read if Keyguard not locked and possible. */ markMissedCallsAsReadAndRemoveNotifications()618 void markMissedCallsAsReadAndRemoveNotifications() { 619 if (callLogQueryHandler != null 620 && !getContext().getSystemService(KeyguardManager.class).isKeyguardLocked()) { 621 callLogQueryHandler.markMissedCallsAsRead(); 622 CallLogNotificationsService.cancelAllMissedCalls(getContext()); 623 } 624 } 625 626 @CallSuper onVisible()627 public void onVisible() { 628 LogUtil.enterBlock("CallLogFragment.onPageSelected"); 629 if (getActivity() != null && FragmentUtils.getParent(this, HostInterface.class) != null) { 630 FragmentUtils.getParentUnsafe(this, HostInterface.class) 631 .enableFloatingButton(!isModalAlertVisible()); 632 } 633 } 634 isModalAlertVisible()635 public boolean isModalAlertVisible() { 636 return modalAlertManager != null && !modalAlertManager.isEmpty(); 637 } 638 639 @CallSuper onNotVisible()640 public void onNotVisible() { 641 LogUtil.enterBlock("CallLogFragment.onPageUnselected"); 642 } 643 644 @Override onShowModalAlert(boolean show)645 public void onShowModalAlert(boolean show) { 646 LogUtil.d( 647 "CallLogFragment.onShowModalAlert", 648 "show: %b, fragment: %s, isVisible: %b", 649 show, 650 this, 651 getUserVisibleHint()); 652 getAdapter().notifyDataSetChanged(); 653 HostInterface hostInterface = FragmentUtils.getParent(this, HostInterface.class); 654 if (show) { 655 recyclerView.setVisibility(View.GONE); 656 modalAlertView.setVisibility(View.VISIBLE); 657 if (hostInterface != null && getUserVisibleHint()) { 658 hostInterface.enableFloatingButton(false); 659 } 660 } else { 661 recyclerView.setVisibility(View.VISIBLE); 662 modalAlertView.setVisibility(View.GONE); 663 if (hostInterface != null && getUserVisibleHint()) { 664 hostInterface.enableFloatingButton(true); 665 } 666 } 667 } 668 669 @Override showMultiSelectRemoveView(boolean show)670 public void showMultiSelectRemoveView(boolean show) { 671 multiSelectUnSelectAllViewContent.setVisibility(show ? View.VISIBLE : View.GONE); 672 multiSelectUnSelectAllViewContent.setAlpha(show ? 0 : 1); 673 multiSelectUnSelectAllViewContent.animate().alpha(show ? 1 : 0).start(); 674 if (show) { 675 FragmentUtils.getParentUnsafe(this, CallLogFragmentListener.class) 676 .showMultiSelectRemoveView(true); 677 } else { 678 // This method is called after onDestroy. In DialtactsActivity, ListsFragment implements this 679 // interface and never goes away with configuration changes so this is safe. MainActivity 680 // removes that extra layer though, so we need to check if the parent is still there. 681 CallLogFragmentListener listener = 682 FragmentUtils.getParent(this, CallLogFragmentListener.class); 683 if (listener != null) { 684 listener.showMultiSelectRemoveView(false); 685 } 686 } 687 } 688 689 @Override setSelectAllModeToFalse()690 public void setSelectAllModeToFalse() { 691 selectAllMode = false; 692 selectUnselectAllIcon.setImageDrawable( 693 getContext().getDrawable(R.drawable.ic_empty_check_mark_white_24dp)); 694 } 695 696 @Override tapSelectAll()697 public void tapSelectAll() { 698 LogUtil.i("CallLogFragment.tapSelectAll", "imitating select all"); 699 selectAllMode = true; 700 updateSelectAllIcon(); 701 } 702 703 @Override onClick(View v)704 public void onClick(View v) { 705 selectAllMode = !selectAllMode; 706 if (selectAllMode) { 707 Logger.get(v.getContext()).logImpression(DialerImpression.Type.MULTISELECT_SELECT_ALL); 708 } else { 709 Logger.get(v.getContext()).logImpression(DialerImpression.Type.MULTISELECT_UNSELECT_ALL); 710 } 711 updateSelectAllIcon(); 712 } 713 updateSelectAllIcon()714 private void updateSelectAllIcon() { 715 if (selectAllMode) { 716 selectUnselectAllIcon.setImageDrawable( 717 getContext().getDrawable(R.drawable.ic_check_mark_blue_24dp)); 718 getAdapter().onAllSelected(); 719 } else { 720 selectUnselectAllIcon.setImageDrawable( 721 getContext().getDrawable(R.drawable.ic_empty_check_mark_white_24dp)); 722 getAdapter().onAllDeselected(); 723 } 724 } 725 726 public interface HostInterface { 727 showDialpad()728 void showDialpad(); 729 enableFloatingButton(boolean enabled)730 void enableFloatingButton(boolean enabled); 731 } 732 733 protected class CustomContentObserver extends ContentObserver { 734 CustomContentObserver()735 public CustomContentObserver() { 736 super(handler); 737 } 738 739 @Override onChange(boolean selfChange)740 public void onChange(boolean selfChange) { 741 refreshDataRequired = true; 742 } 743 } 744 745 /** Useful callback for ListsFragment children to use to call into ListsFragment. */ 746 public interface CallLogFragmentListener { 747 748 /** 749 * External method to update unread count because the unread count changes when the user expands 750 * a voicemail in the call log or when the user expands an unread call in the call history tab. 751 */ updateTabUnreadCounts()752 void updateTabUnreadCounts(); 753 showMultiSelectRemoveView(boolean show)754 void showMultiSelectRemoveView(boolean show); 755 } 756 } 757