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