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 android.app.Activity; 20 import android.content.ContentUris; 21 import android.content.DialogInterface; 22 import android.content.DialogInterface.OnCancelListener; 23 import android.content.res.Resources; 24 import android.database.Cursor; 25 import android.net.Uri; 26 import android.os.AsyncTask; 27 import android.os.Build.VERSION; 28 import android.os.Build.VERSION_CODES; 29 import android.os.Bundle; 30 import android.os.Trace; 31 import android.provider.CallLog; 32 import android.provider.ContactsContract.CommonDataKinds.Phone; 33 import android.support.annotation.MainThread; 34 import android.support.annotation.NonNull; 35 import android.support.annotation.Nullable; 36 import android.support.annotation.VisibleForTesting; 37 import android.support.annotation.WorkerThread; 38 import android.support.v7.app.AlertDialog; 39 import android.support.v7.widget.RecyclerView; 40 import android.support.v7.widget.RecyclerView.ViewHolder; 41 import android.telecom.PhoneAccountHandle; 42 import android.telephony.PhoneNumberUtils; 43 import android.text.TextUtils; 44 import android.util.ArrayMap; 45 import android.util.ArraySet; 46 import android.util.SparseArray; 47 import android.view.ActionMode; 48 import android.view.LayoutInflater; 49 import android.view.Menu; 50 import android.view.MenuInflater; 51 import android.view.MenuItem; 52 import android.view.View; 53 import android.view.ViewGroup; 54 import com.android.contacts.common.ContactsUtils; 55 import com.android.contacts.common.preference.ContactsPreferences; 56 import com.android.dialer.app.DialtactsActivity; 57 import com.android.dialer.app.R; 58 import com.android.dialer.app.calllog.CallLogFragment.CallLogFragmentListener; 59 import com.android.dialer.app.calllog.CallLogGroupBuilder.GroupCreator; 60 import com.android.dialer.app.calllog.calllogcache.CallLogCache; 61 import com.android.dialer.app.contactinfo.ContactInfoCache; 62 import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter; 63 import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter.OnVoicemailDeletedListener; 64 import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler; 65 import com.android.dialer.calldetails.CallDetailsEntries; 66 import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry; 67 import com.android.dialer.callintent.CallIntentBuilder; 68 import com.android.dialer.calllogutils.CallbackActionHelper.CallbackAction; 69 import com.android.dialer.calllogutils.PhoneCallDetails; 70 import com.android.dialer.common.Assert; 71 import com.android.dialer.common.FragmentUtils.FragmentUtilListener; 72 import com.android.dialer.common.LogUtil; 73 import com.android.dialer.common.concurrent.AsyncTaskExecutor; 74 import com.android.dialer.common.concurrent.AsyncTaskExecutors; 75 import com.android.dialer.compat.android.provider.VoicemailCompat; 76 import com.android.dialer.configprovider.ConfigProviderBindings; 77 import com.android.dialer.duo.Duo; 78 import com.android.dialer.duo.DuoComponent; 79 import com.android.dialer.duo.DuoConstants; 80 import com.android.dialer.duo.DuoListener; 81 import com.android.dialer.enrichedcall.EnrichedCallCapabilities; 82 import com.android.dialer.enrichedcall.EnrichedCallComponent; 83 import com.android.dialer.enrichedcall.EnrichedCallManager; 84 import com.android.dialer.logging.ContactSource; 85 import com.android.dialer.logging.DialerImpression; 86 import com.android.dialer.logging.Logger; 87 import com.android.dialer.logging.UiAction; 88 import com.android.dialer.main.MainActivityPeer; 89 import com.android.dialer.performancereport.PerformanceReport; 90 import com.android.dialer.phonenumbercache.CallLogQuery; 91 import com.android.dialer.phonenumbercache.ContactInfo; 92 import com.android.dialer.phonenumbercache.ContactInfoHelper; 93 import com.android.dialer.phonenumberutil.PhoneNumberHelper; 94 import com.android.dialer.spam.SpamComponent; 95 import com.android.dialer.telecom.TelecomUtil; 96 import com.android.dialer.util.PermissionsUtil; 97 import java.util.ArrayList; 98 import java.util.Map; 99 import java.util.Set; 100 101 /** Adapter class to fill in data for the Call Log. */ 102 public class CallLogAdapter extends GroupingListAdapter 103 implements GroupCreator, OnVoicemailDeletedListener, DuoListener { 104 105 // Types of activities the call log adapter is used for 106 public static final int ACTIVITY_TYPE_CALL_LOG = 1; 107 public static final int ACTIVITY_TYPE_DIALTACTS = 2; 108 private static final int NO_EXPANDED_LIST_ITEM = -1; 109 public static final int ALERT_POSITION = 0; 110 private static final int VIEW_TYPE_ALERT = 1; 111 private static final int VIEW_TYPE_CALLLOG = 2; 112 113 private static final String KEY_EXPANDED_POSITION = "expanded_position"; 114 private static final String KEY_EXPANDED_ROW_ID = "expanded_row_id"; 115 private static final String KEY_ACTION_MODE = "action_mode_selected_items"; 116 117 public static final String LOAD_DATA_TASK_IDENTIFIER = "load_data"; 118 119 public static final String ENABLE_CALL_LOG_MULTI_SELECT = "enable_call_log_multiselect"; 120 public static final boolean ENABLE_CALL_LOG_MULTI_SELECT_FLAG = true; 121 122 protected final Activity activity; 123 protected final VoicemailPlaybackPresenter voicemailPlaybackPresenter; 124 /** Cache for repeated requests to Telecom/Telephony. */ 125 protected final CallLogCache callLogCache; 126 127 private final CallFetcher callFetcher; 128 private final OnActionModeStateChangedListener actionModeStateChangedListener; 129 private final MultiSelectRemoveView multiSelectRemoveView; 130 @NonNull private final FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler; 131 private final int activityType; 132 133 /** Instance of helper class for managing views. */ 134 private final CallLogListItemHelper callLogListItemHelper; 135 /** Helper to group call log entries. */ 136 private final CallLogGroupBuilder callLogGroupBuilder; 137 138 private final AsyncTaskExecutor asyncTaskExecutor = AsyncTaskExecutors.createAsyncTaskExecutor(); 139 private ContactInfoCache contactInfoCache; 140 // Tracks the position of the currently expanded list item. 141 private int currentlyExpandedPosition = RecyclerView.NO_POSITION; 142 // Tracks the rowId of the currently expanded list item, so the position can be updated if there 143 // are any changes to the call log entries, such as additions or removals. 144 private long currentlyExpandedRowId = NO_EXPANDED_LIST_ITEM; 145 146 private final CallLogAlertManager callLogAlertManager; 147 148 public ActionMode actionMode = null; 149 public boolean selectAllMode = false; 150 public boolean deselectAllMode = false; 151 private final SparseArray<String> selectedItems = new SparseArray<>(); 152 153 private final ActionMode.Callback actionModeCallback = 154 new ActionMode.Callback() { 155 156 // Called when the action mode is created; startActionMode() was called 157 @Override 158 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 159 if (activity != null) { 160 announceforAccessibility( 161 activity.getCurrentFocus(), 162 activity.getString(R.string.description_entering_bulk_action_mode)); 163 } 164 actionMode = mode; 165 // Inflate a menu resource providing context menu items 166 MenuInflater inflater = mode.getMenuInflater(); 167 inflater.inflate(R.menu.actionbar_delete, menu); 168 multiSelectRemoveView.showMultiSelectRemoveView(true); 169 actionModeStateChangedListener.onActionModeStateChanged(true); 170 return true; 171 } 172 173 // Called each time the action mode is shown. Always called after onCreateActionMode, but 174 // may be called multiple times if the mode is invalidated. 175 @Override 176 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 177 return false; // Return false if nothing is done 178 } 179 180 // Called when the user selects a contextual menu item 181 @Override 182 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 183 if (item.getItemId() == R.id.action_bar_delete_menu_item) { 184 Logger.get(activity).logImpression(DialerImpression.Type.MULTISELECT_TAP_DELETE_ICON); 185 if (selectedItems.size() > 0) { 186 showDeleteSelectedItemsDialog(); 187 } 188 return true; 189 } else { 190 return false; 191 } 192 } 193 194 // Called when the user exits the action mode 195 @Override 196 public void onDestroyActionMode(ActionMode mode) { 197 if (activity != null) { 198 announceforAccessibility( 199 activity.getCurrentFocus(), 200 activity.getString(R.string.description_leaving_bulk_action_mode)); 201 } 202 selectedItems.clear(); 203 actionMode = null; 204 selectAllMode = false; 205 deselectAllMode = false; 206 multiSelectRemoveView.showMultiSelectRemoveView(false); 207 actionModeStateChangedListener.onActionModeStateChanged(false); 208 notifyDataSetChanged(); 209 } 210 }; 211 212 private void showDeleteSelectedItemsDialog() { 213 SparseArray<String> voicemailsToDeleteOnConfirmation = selectedItems.clone(); 214 new AlertDialog.Builder(activity, R.style.AlertDialogCustom) 215 .setCancelable(true) 216 .setTitle( 217 activity 218 .getResources() 219 .getQuantityString( 220 R.plurals.delete_voicemails_confirmation_dialog_title, selectedItems.size())) 221 .setPositiveButton( 222 R.string.voicemailMultiSelectDeleteConfirm, 223 new DialogInterface.OnClickListener() { 224 @Override 225 public void onClick(final DialogInterface dialog, final int button) { 226 LogUtil.i( 227 "CallLogAdapter.showDeleteSelectedItemsDialog", 228 "onClick, these items to delete " + voicemailsToDeleteOnConfirmation); 229 deleteSelectedItems(voicemailsToDeleteOnConfirmation); 230 actionMode.finish(); 231 dialog.cancel(); 232 Logger.get(activity) 233 .logImpression( 234 DialerImpression.Type.MULTISELECT_DELETE_ENTRY_VIA_CONFIRMATION_DIALOG); 235 } 236 }) 237 .setOnCancelListener( 238 new OnCancelListener() { 239 @Override 240 public void onCancel(DialogInterface dialogInterface) { 241 Logger.get(activity) 242 .logImpression( 243 DialerImpression.Type 244 .MULTISELECT_CANCEL_CONFIRMATION_DIALOG_VIA_CANCEL_TOUCH); 245 dialogInterface.cancel(); 246 } 247 }) 248 .setNegativeButton( 249 R.string.voicemailMultiSelectDeleteCancel, 250 new DialogInterface.OnClickListener() { 251 @Override 252 public void onClick(final DialogInterface dialog, final int button) { 253 Logger.get(activity) 254 .logImpression( 255 DialerImpression.Type 256 .MULTISELECT_CANCEL_CONFIRMATION_DIALOG_VIA_CANCEL_BUTTON); 257 dialog.cancel(); 258 } 259 }) 260 .show(); 261 Logger.get(activity) 262 .logImpression(DialerImpression.Type.MULTISELECT_DISPLAY_DELETE_CONFIRMATION_DIALOG); 263 } 264 265 private void deleteSelectedItems(SparseArray<String> voicemailsToDelete) { 266 for (int i = 0; i < voicemailsToDelete.size(); i++) { 267 String voicemailUri = voicemailsToDelete.get(voicemailsToDelete.keyAt(i)); 268 LogUtil.i("CallLogAdapter.deleteSelectedItems", "deleting uri:" + voicemailUri); 269 CallLogAsyncTaskUtil.deleteVoicemail(activity, Uri.parse(voicemailUri), null); 270 } 271 } 272 273 private final View.OnLongClickListener longPressListener = 274 new View.OnLongClickListener() { 275 @Override 276 public boolean onLongClick(View v) { 277 if (ConfigProviderBindings.get(v.getContext()) 278 .getBoolean(ENABLE_CALL_LOG_MULTI_SELECT, ENABLE_CALL_LOG_MULTI_SELECT_FLAG) 279 && voicemailPlaybackPresenter != null) { 280 if (v.getId() == R.id.primary_action_view || v.getId() == R.id.quick_contact_photo) { 281 if (actionMode == null) { 282 Logger.get(activity) 283 .logImpression( 284 DialerImpression.Type.MULTISELECT_LONG_PRESS_ENTER_MULTI_SELECT_MODE); 285 actionMode = v.startActionMode(actionModeCallback); 286 } 287 Logger.get(activity) 288 .logImpression(DialerImpression.Type.MULTISELECT_LONG_PRESS_TAP_ENTRY); 289 CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) v.getTag(); 290 viewHolder.quickContactView.setVisibility(View.GONE); 291 viewHolder.checkBoxView.setVisibility(View.VISIBLE); 292 expandCollapseListener.onClick(v); 293 return true; 294 } 295 } 296 return true; 297 } 298 }; 299 300 @VisibleForTesting 301 public View.OnClickListener getExpandCollapseListener() { 302 return expandCollapseListener; 303 } 304 305 /** The OnClickListener used to expand or collapse the action buttons of a call log entry. */ 306 private final View.OnClickListener expandCollapseListener = 307 new View.OnClickListener() { 308 @Override 309 public void onClick(View v) { 310 PerformanceReport.recordClick(UiAction.Type.CLICK_CALL_LOG_ITEM); 311 312 CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) v.getTag(); 313 if (viewHolder == null) { 314 return; 315 } 316 if (actionMode != null && viewHolder.voicemailUri != null) { 317 selectAllMode = false; 318 deselectAllMode = false; 319 multiSelectRemoveView.setSelectAllModeToFalse(); 320 int id = getVoicemailId(viewHolder.voicemailUri); 321 if (selectedItems.get(id) != null) { 322 Logger.get(activity) 323 .logImpression(DialerImpression.Type.MULTISELECT_SINGLE_PRESS_UNSELECT_ENTRY); 324 uncheckMarkCallLogEntry(viewHolder, id); 325 } else { 326 Logger.get(activity) 327 .logImpression(DialerImpression.Type.MULTISELECT_SINGLE_PRESS_SELECT_ENTRY); 328 checkMarkCallLogEntry(viewHolder); 329 // select all check box logic 330 if (getItemCount() == selectedItems.size()) { 331 LogUtil.i( 332 "mExpandCollapseListener.onClick", 333 "getitem count %d is equal to items select count %d, check select all box", 334 getItemCount(), 335 selectedItems.size()); 336 multiSelectRemoveView.tapSelectAll(); 337 } 338 } 339 return; 340 } 341 342 if (voicemailPlaybackPresenter != null) { 343 // Always reset the voicemail playback state on expand or collapse. 344 voicemailPlaybackPresenter.resetAll(); 345 } 346 347 // If enriched call capabilities were unknown on the initial load, 348 // viewHolder.isCallComposerCapable may be unset. Check here if we have the capabilities 349 // as a last attempt at getting them before showing the expanded view to the user 350 EnrichedCallCapabilities capabilities = null; 351 352 if (viewHolder.number != null) { 353 capabilities = getEnrichedCallManager().getCapabilities(viewHolder.number); 354 } 355 356 if (capabilities == null) { 357 capabilities = EnrichedCallCapabilities.NO_CAPABILITIES; 358 } 359 360 viewHolder.isCallComposerCapable = capabilities.isCallComposerCapable(); 361 362 if (capabilities.isTemporarilyUnavailable()) { 363 LogUtil.i( 364 "mExpandCollapseListener.onClick", 365 "%s is temporarily unavailable, requesting capabilities", 366 LogUtil.sanitizePhoneNumber(viewHolder.number)); 367 // Refresh the capabilities when temporarily unavailable. 368 // Similarly to when we request capabilities the first time, the 'Share and call' button 369 // won't pop in with the new capabilities. Instead the row needs to be collapsed and 370 // expanded again. 371 getEnrichedCallManager().requestCapabilities(viewHolder.number); 372 } 373 374 if (viewHolder.rowId == currentlyExpandedRowId) { 375 // Hide actions, if the clicked item is the expanded item. 376 viewHolder.showActions(false); 377 378 currentlyExpandedPosition = RecyclerView.NO_POSITION; 379 currentlyExpandedRowId = NO_EXPANDED_LIST_ITEM; 380 } else { 381 if (viewHolder.callType == CallLog.Calls.MISSED_TYPE) { 382 CallLogAsyncTaskUtil.markCallAsRead(activity, viewHolder.callIds); 383 if (activityType == ACTIVITY_TYPE_DIALTACTS) { 384 if (v.getContext() instanceof MainActivityPeer.PeerSupplier) { 385 // This is really bad, but we must do this to prevent a dependency cycle, enforce 386 // best practices in new code, and avoid refactoring DialtactsActivity. 387 ((FragmentUtilListener) 388 ((MainActivityPeer.PeerSupplier) v.getContext()).getPeer()) 389 .getImpl(CallLogFragmentListener.class) 390 .updateTabUnreadCounts(); 391 } else { 392 ((DialtactsActivity) v.getContext()).updateTabUnreadCounts(); 393 } 394 } 395 } 396 expandViewHolderActions(viewHolder); 397 398 if (isDuoCallButtonVisible(viewHolder.videoCallButtonView)) { 399 CallIntentBuilder.increaseLightbringerCallButtonAppearInExpandedCallLogItemCount(); 400 } 401 } 402 } 403 404 private boolean isDuoCallButtonVisible(View videoCallButtonView) { 405 if (videoCallButtonView == null) { 406 return false; 407 } 408 if (videoCallButtonView.getVisibility() != View.VISIBLE) { 409 return false; 410 } 411 IntentProvider intentProvider = (IntentProvider) videoCallButtonView.getTag(); 412 if (intentProvider == null) { 413 return false; 414 } 415 return DuoConstants.PACKAGE_NAME.equals(intentProvider.getIntent(activity).getPackage()); 416 } 417 }; 418 419 @Nullable 420 public RecyclerView.OnScrollListener getOnScrollListener() { 421 return null; 422 } 423 424 private void checkMarkCallLogEntry(CallLogListItemViewHolder viewHolder) { 425 announceforAccessibility( 426 activity.getCurrentFocus(), 427 activity.getString( 428 R.string.description_selecting_bulk_action_mode, viewHolder.nameOrNumber)); 429 viewHolder.quickContactView.setVisibility(View.GONE); 430 viewHolder.checkBoxView.setVisibility(View.VISIBLE); 431 selectedItems.put(getVoicemailId(viewHolder.voicemailUri), viewHolder.voicemailUri); 432 updateActionBar(); 433 } 434 435 private void announceforAccessibility(View view, String announcement) { 436 if (view != null) { 437 view.announceForAccessibility(announcement); 438 } 439 } 440 441 private void updateActionBar() { 442 if (actionMode == null && selectedItems.size() > 0) { 443 Logger.get(activity) 444 .logImpression(DialerImpression.Type.MULTISELECT_ROTATE_AND_SHOW_ACTION_MODE); 445 activity.startActionMode(actionModeCallback); 446 } 447 if (actionMode != null) { 448 actionMode.setTitle( 449 activity 450 .getResources() 451 .getString( 452 R.string.voicemailMultiSelectActionBarTitle, 453 Integer.toString(selectedItems.size()))); 454 } 455 } 456 457 private void uncheckMarkCallLogEntry(CallLogListItemViewHolder viewHolder, int id) { 458 announceforAccessibility( 459 activity.getCurrentFocus(), 460 activity.getString( 461 R.string.description_unselecting_bulk_action_mode, viewHolder.nameOrNumber)); 462 selectedItems.delete(id); 463 viewHolder.checkBoxView.setVisibility(View.GONE); 464 viewHolder.quickContactView.setVisibility(View.VISIBLE); 465 updateActionBar(); 466 } 467 468 private static int getVoicemailId(String voicemailUri) { 469 Assert.checkArgument(voicemailUri != null); 470 Assert.checkArgument(voicemailUri.length() > 0); 471 return (int) ContentUris.parseId(Uri.parse(voicemailUri)); 472 } 473 474 /** 475 * A list of {@link CallLogQuery#ID} that will be hidden. The hide might be temporary so instead 476 * if removing an item, it will be shown as an invisible view. This simplifies the calculation of 477 * item position. 478 */ 479 @NonNull private Set<Long> hiddenRowIds = new ArraySet<>(); 480 /** 481 * Holds a list of URIs that are pending deletion or undo. If the activity ends before the undo 482 * timeout, all of the pending URIs will be deleted. 483 * 484 * <p>TODO(twyen): move this and OnVoicemailDeletedListener to somewhere like {@link 485 * VisualVoicemailCallLogFragment}. The CallLogAdapter does not need to know about what to do with 486 * hidden item or what to hide. 487 */ 488 @NonNull private final Set<Uri> hiddenItemUris = new ArraySet<>(); 489 490 private CallLogListItemViewHolder.OnClickListener blockReportSpamListener; 491 492 /** 493 * Map, keyed by call ID, used to track the callback action for a call. Calls associated with the 494 * same callback action will be put into the same primary call group in {@link 495 * com.android.dialer.app.calllog.CallLogGroupBuilder}. This information is used to set the 496 * callback icon and trigger the corresponding action. 497 */ 498 private final Map<Long, Integer> callbackActions = new ArrayMap<>(); 499 500 /** 501 * Map, keyed by call ID, used to track the day group for a call. As call log entries are put into 502 * the primary call groups in {@link com.android.dialer.app.calllog.CallLogGroupBuilder}, they are 503 * also assigned a secondary "day group". This map tracks the day group assigned to all calls in 504 * the call log. This information is used to trigger the display of a day group header above the 505 * call log entry at the start of a day group. Note: Multiple calls are grouped into a single 506 * primary "call group" in the call log, and the cursor used to bind rows includes all of these 507 * calls. When determining if a day group change has occurred it is necessary to look at the last 508 * entry in the call log to determine its day group. This map provides a means of determining the 509 * previous day group without having to reverse the cursor to the start of the previous day call 510 * log entry. 511 */ 512 private final Map<Long, Integer> dayGroups = new ArrayMap<>(); 513 514 private boolean loading = true; 515 private ContactsPreferences contactsPreferences; 516 517 private boolean isSpamEnabled; 518 519 public CallLogAdapter( 520 Activity activity, 521 ViewGroup alertContainer, 522 CallFetcher callFetcher, 523 MultiSelectRemoveView multiSelectRemoveView, 524 OnActionModeStateChangedListener actionModeStateChangedListener, 525 CallLogCache callLogCache, 526 ContactInfoCache contactInfoCache, 527 VoicemailPlaybackPresenter voicemailPlaybackPresenter, 528 @NonNull FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler, 529 int activityType) { 530 super(); 531 532 this.activity = activity; 533 this.callFetcher = callFetcher; 534 this.actionModeStateChangedListener = actionModeStateChangedListener; 535 this.multiSelectRemoveView = multiSelectRemoveView; 536 this.voicemailPlaybackPresenter = voicemailPlaybackPresenter; 537 if (this.voicemailPlaybackPresenter != null) { 538 this.voicemailPlaybackPresenter.setOnVoicemailDeletedListener(this); 539 } 540 541 this.activityType = activityType; 542 543 this.contactInfoCache = contactInfoCache; 544 545 if (!PermissionsUtil.hasContactsReadPermissions(activity)) { 546 this.contactInfoCache.disableRequestProcessing(); 547 } 548 549 Resources resources = this.activity.getResources(); 550 551 this.callLogCache = callLogCache; 552 553 PhoneCallDetailsHelper phoneCallDetailsHelper = 554 new PhoneCallDetailsHelper(this.activity, resources, this.callLogCache); 555 callLogListItemHelper = 556 new CallLogListItemHelper(phoneCallDetailsHelper, resources, this.callLogCache); 557 callLogGroupBuilder = new CallLogGroupBuilder(this); 558 this.filteredNumberAsyncQueryHandler = Assert.isNotNull(filteredNumberAsyncQueryHandler); 559 560 contactsPreferences = new ContactsPreferences(this.activity); 561 562 blockReportSpamListener = 563 new BlockReportSpamListener( 564 this.activity, 565 ((Activity) this.activity).getFragmentManager(), 566 this, 567 this.filteredNumberAsyncQueryHandler); 568 setHasStableIds(true); 569 570 callLogAlertManager = 571 new CallLogAlertManager(this, LayoutInflater.from(this.activity), alertContainer); 572 } 573 574 private void expandViewHolderActions(CallLogListItemViewHolder viewHolder) { 575 if (!TextUtils.isEmpty(viewHolder.voicemailUri)) { 576 Logger.get(activity).logImpression(DialerImpression.Type.VOICEMAIL_EXPAND_ENTRY); 577 } 578 579 int lastExpandedPosition = currentlyExpandedPosition; 580 // Show the actions for the clicked list item. 581 viewHolder.showActions(true); 582 currentlyExpandedPosition = viewHolder.getAdapterPosition(); 583 currentlyExpandedRowId = viewHolder.rowId; 584 585 // If another item is expanded, notify it that it has changed. Its actions will be 586 // hidden when it is re-binded because we change mCurrentlyExpandedRowId above. 587 if (lastExpandedPosition != RecyclerView.NO_POSITION) { 588 notifyItemChanged(lastExpandedPosition); 589 } 590 } 591 592 public void onSaveInstanceState(Bundle outState) { 593 outState.putInt(KEY_EXPANDED_POSITION, currentlyExpandedPosition); 594 outState.putLong(KEY_EXPANDED_ROW_ID, currentlyExpandedRowId); 595 596 ArrayList<String> listOfSelectedItems = new ArrayList<>(); 597 598 if (selectedItems.size() > 0) { 599 for (int i = 0; i < selectedItems.size(); i++) { 600 int id = selectedItems.keyAt(i); 601 String voicemailUri = selectedItems.valueAt(i); 602 LogUtil.i( 603 "CallLogAdapter.onSaveInstanceState", "index %d, id=%d, uri=%s ", i, id, voicemailUri); 604 listOfSelectedItems.add(voicemailUri); 605 } 606 } 607 outState.putStringArrayList(KEY_ACTION_MODE, listOfSelectedItems); 608 609 LogUtil.i( 610 "CallLogAdapter.onSaveInstanceState", 611 "saved: %d, selectedItemsSize:%d", 612 listOfSelectedItems.size(), 613 selectedItems.size()); 614 } 615 616 public void onRestoreInstanceState(Bundle savedInstanceState) { 617 if (savedInstanceState != null) { 618 currentlyExpandedPosition = 619 savedInstanceState.getInt(KEY_EXPANDED_POSITION, RecyclerView.NO_POSITION); 620 currentlyExpandedRowId = 621 savedInstanceState.getLong(KEY_EXPANDED_ROW_ID, NO_EXPANDED_LIST_ITEM); 622 // Restoring multi selected entries 623 ArrayList<String> listOfSelectedItems = 624 savedInstanceState.getStringArrayList(KEY_ACTION_MODE); 625 if (listOfSelectedItems != null) { 626 LogUtil.i( 627 "CallLogAdapter.onRestoreInstanceState", 628 "restored selectedItemsList:%d", 629 listOfSelectedItems.size()); 630 631 if (!listOfSelectedItems.isEmpty()) { 632 for (int i = 0; i < listOfSelectedItems.size(); i++) { 633 String voicemailUri = listOfSelectedItems.get(i); 634 int id = getVoicemailId(voicemailUri); 635 LogUtil.i( 636 "CallLogAdapter.onRestoreInstanceState", 637 "restoring selected index %d, id=%d, uri=%s ", 638 i, 639 id, 640 voicemailUri); 641 selectedItems.put(id, voicemailUri); 642 } 643 644 LogUtil.i( 645 "CallLogAdapter.onRestoreInstance", 646 "restored selectedItems %s", 647 selectedItems.toString()); 648 updateActionBar(); 649 } 650 } 651 } 652 } 653 654 /** Requery on background thread when {@link Cursor} changes. */ 655 @Override 656 protected void onContentChanged() { 657 callFetcher.fetchCalls(); 658 } 659 660 public void setLoading(boolean loading) { 661 this.loading = loading; 662 } 663 664 public boolean isEmpty() { 665 if (loading) { 666 // We don't want the empty state to show when loading. 667 return false; 668 } else { 669 return getItemCount() == 0; 670 } 671 } 672 673 public void clearFilteredNumbersCache() { 674 filteredNumberAsyncQueryHandler.clearCache(); 675 } 676 677 public void onResume() { 678 if (PermissionsUtil.hasPermission(activity, android.Manifest.permission.READ_CONTACTS)) { 679 contactInfoCache.start(); 680 } 681 contactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY); 682 isSpamEnabled = SpamComponent.get(activity).spam().isSpamEnabled(); 683 getDuo().registerListener(this); 684 notifyDataSetChanged(); 685 } 686 687 public void onPause() { 688 getDuo().unregisterListener(this); 689 pauseCache(); 690 for (Uri uri : hiddenItemUris) { 691 CallLogAsyncTaskUtil.deleteVoicemail(activity, uri, null); 692 } 693 } 694 695 public void onStop() { 696 getEnrichedCallManager().clearCachedData(); 697 } 698 699 public CallLogAlertManager getAlertManager() { 700 return callLogAlertManager; 701 } 702 703 @VisibleForTesting 704 /* package */ void pauseCache() { 705 contactInfoCache.stop(); 706 callLogCache.reset(); 707 } 708 709 @Override 710 protected void addGroups(Cursor cursor) { 711 callLogGroupBuilder.addGroups(cursor); 712 } 713 714 @Override 715 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 716 if (viewType == VIEW_TYPE_ALERT) { 717 return callLogAlertManager.createViewHolder(parent); 718 } 719 return createCallLogEntryViewHolder(parent); 720 } 721 722 /** 723 * Creates a new call log entry {@link ViewHolder}. 724 * 725 * @param parent the parent view. 726 * @return The {@link ViewHolder}. 727 */ 728 private ViewHolder createCallLogEntryViewHolder(ViewGroup parent) { 729 LayoutInflater inflater = LayoutInflater.from(activity); 730 View view = inflater.inflate(R.layout.call_log_list_item, parent, false); 731 CallLogListItemViewHolder viewHolder = 732 CallLogListItemViewHolder.create( 733 view, 734 activity, 735 blockReportSpamListener, 736 expandCollapseListener, 737 longPressListener, 738 actionModeStateChangedListener, 739 callLogCache, 740 callLogListItemHelper, 741 voicemailPlaybackPresenter); 742 743 viewHolder.callLogEntryView.setTag(viewHolder); 744 745 viewHolder.primaryActionView.setTag(viewHolder); 746 viewHolder.quickContactView.setTag(viewHolder); 747 748 return viewHolder; 749 } 750 751 /** 752 * Binds the views in the entry to the data in the call log. TODO: This gets called 20-30 times 753 * when Dialer starts up for a single call log entry and should not. It invokes cross-process 754 * methods and the repeat execution can get costly. 755 * 756 * @param viewHolder The view corresponding to this entry. 757 * @param position The position of the entry. 758 */ 759 @Override 760 public void onBindViewHolder(ViewHolder viewHolder, int position) { 761 Trace.beginSection("onBindViewHolder: " + position); 762 switch (getItemViewType(position)) { 763 case VIEW_TYPE_ALERT: 764 // Do nothing 765 break; 766 default: 767 bindCallLogListViewHolder(viewHolder, position); 768 break; 769 } 770 Trace.endSection(); 771 } 772 773 @Override 774 public void onViewRecycled(ViewHolder viewHolder) { 775 if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) { 776 CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder; 777 updateCheckMarkedStatusOfEntry(views); 778 779 if (views.asyncTask != null) { 780 views.asyncTask.cancel(true); 781 } 782 } 783 } 784 785 @Override 786 public void onViewAttachedToWindow(ViewHolder viewHolder) { 787 if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) { 788 ((CallLogListItemViewHolder) viewHolder).isAttachedToWindow = true; 789 } 790 } 791 792 @Override 793 public void onViewDetachedFromWindow(ViewHolder viewHolder) { 794 if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) { 795 ((CallLogListItemViewHolder) viewHolder).isAttachedToWindow = false; 796 } 797 } 798 799 /** 800 * Binds the view holder for the call log list item view. 801 * 802 * @param viewHolder The call log list item view holder. 803 * @param position The position of the list item. 804 */ 805 private void bindCallLogListViewHolder(final ViewHolder viewHolder, final int position) { 806 Cursor c = (Cursor) getItem(position); 807 if (c == null) { 808 return; 809 } 810 CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder; 811 updateCheckMarkedStatusOfEntry(views); 812 813 views.isLoaded = false; 814 int groupSize = getGroupSize(position); 815 CallDetailsEntries callDetailsEntries = createCallDetailsEntries(c, groupSize); 816 PhoneCallDetails details = createPhoneCallDetails(c, groupSize, views); 817 if (isHiddenRow(views.number, c.getLong(CallLogQuery.ID))) { 818 views.callLogEntryView.setVisibility(View.GONE); 819 views.dayGroupHeader.setVisibility(View.GONE); 820 return; 821 } else { 822 views.callLogEntryView.setVisibility(View.VISIBLE); 823 // dayGroupHeader will be restored after loadAndRender() if it is needed. 824 } 825 if (currentlyExpandedRowId == views.rowId) { 826 views.inflateActionViewStub(); 827 } 828 loadAndRender(views, views.rowId, details, callDetailsEntries); 829 } 830 831 private void updateCheckMarkedStatusOfEntry(CallLogListItemViewHolder views) { 832 if (selectedItems.size() > 0 && views.voicemailUri != null) { 833 int id = getVoicemailId(views.voicemailUri); 834 if (selectedItems.get(id) != null) { 835 checkMarkCallLogEntry(views); 836 } else { 837 uncheckMarkCallLogEntry(views, id); 838 } 839 } 840 } 841 842 private boolean isHiddenRow(@Nullable String number, long rowId) { 843 if (number != null && PhoneNumberUtils.isEmergencyNumber(number)) { 844 return true; 845 } 846 if (hiddenRowIds.contains(rowId)) { 847 return true; 848 } 849 return false; 850 } 851 852 private void loadAndRender( 853 final CallLogListItemViewHolder viewHolder, 854 final long rowId, 855 final PhoneCallDetails details, 856 final CallDetailsEntries callDetailsEntries) { 857 LogUtil.d("CallLogAdapter.loadAndRender", "position: %d", viewHolder.getAdapterPosition()); 858 // Reset block and spam information since this view could be reused which may contain 859 // outdated data. 860 viewHolder.isSpam = false; 861 viewHolder.blockId = null; 862 viewHolder.isSpamFeatureEnabled = false; 863 864 // Attempt to set the isCallComposerCapable field. If capabilities are unknown for this number, 865 // the value will be false while capabilities are requested. mExpandCollapseListener will 866 // attempt to set the field properly in that case 867 viewHolder.isCallComposerCapable = isCallComposerCapable(viewHolder.number); 868 viewHolder.setDetailedPhoneDetails(callDetailsEntries); 869 final AsyncTask<Void, Void, Boolean> loadDataTask = 870 new AsyncTask<Void, Void, Boolean>() { 871 @Override 872 protected Boolean doInBackground(Void... params) { 873 viewHolder.blockId = 874 filteredNumberAsyncQueryHandler.getBlockedIdSynchronous( 875 viewHolder.number, viewHolder.countryIso); 876 details.isBlocked = viewHolder.blockId != null; 877 if (isCancelled()) { 878 return false; 879 } 880 if (isSpamEnabled) { 881 viewHolder.isSpamFeatureEnabled = true; 882 // Only display the call as a spam call if there are incoming calls in the list. 883 // Call log cards with only outgoing calls should never be displayed as spam. 884 viewHolder.isSpam = 885 details.hasIncomingCalls() 886 && SpamComponent.get(activity) 887 .spam() 888 .checkSpamStatusSynchronous(viewHolder.number, viewHolder.countryIso); 889 details.isSpam = viewHolder.isSpam; 890 } 891 return !isCancelled() && loadData(viewHolder, rowId, details); 892 } 893 894 @Override 895 protected void onPostExecute(Boolean success) { 896 viewHolder.isLoaded = true; 897 if (success) { 898 viewHolder.callbackAction = getCallbackAction(viewHolder.rowId); 899 int currentDayGroup = getDayGroup(viewHolder.rowId); 900 if (currentDayGroup != details.previousGroup) { 901 viewHolder.dayGroupHeaderVisibility = View.VISIBLE; 902 viewHolder.dayGroupHeaderText = getGroupDescription(currentDayGroup); 903 } else { 904 viewHolder.dayGroupHeaderVisibility = View.GONE; 905 } 906 render(viewHolder, details, rowId); 907 } 908 } 909 }; 910 911 viewHolder.asyncTask = loadDataTask; 912 asyncTaskExecutor.submit(LOAD_DATA_TASK_IDENTIFIER, loadDataTask); 913 } 914 915 @MainThread 916 private boolean isCallComposerCapable(@Nullable String number) { 917 if (number == null) { 918 return false; 919 } 920 921 EnrichedCallCapabilities capabilities = getEnrichedCallManager().getCapabilities(number); 922 if (capabilities == null) { 923 getEnrichedCallManager().requestCapabilities(number); 924 return false; 925 } 926 return capabilities.isCallComposerCapable(); 927 } 928 929 /** 930 * Initialize PhoneCallDetails by reading all data from cursor. This method must be run on main 931 * thread since cursor is not thread safe. 932 */ 933 @MainThread 934 private PhoneCallDetails createPhoneCallDetails( 935 Cursor cursor, int count, final CallLogListItemViewHolder views) { 936 Assert.isMainThread(); 937 final String number = cursor.getString(CallLogQuery.NUMBER); 938 final String postDialDigits = 939 (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.POST_DIAL_DIGITS) : ""; 940 final String viaNumber = 941 (VERSION.SDK_INT >= VERSION_CODES.N) ? cursor.getString(CallLogQuery.VIA_NUMBER) : ""; 942 final int numberPresentation = cursor.getInt(CallLogQuery.NUMBER_PRESENTATION); 943 final ContactInfo cachedContactInfo = ContactInfoHelper.getContactInfo(cursor); 944 final int transcriptionState = 945 (VERSION.SDK_INT >= VERSION_CODES.O) 946 ? cursor.getInt(CallLogQuery.TRANSCRIPTION_STATE) 947 : VoicemailCompat.TRANSCRIPTION_NOT_STARTED; 948 final PhoneCallDetails details = 949 new PhoneCallDetails(number, numberPresentation, postDialDigits); 950 details.viaNumber = viaNumber; 951 details.countryIso = cursor.getString(CallLogQuery.COUNTRY_ISO); 952 details.date = cursor.getLong(CallLogQuery.DATE); 953 details.duration = cursor.getLong(CallLogQuery.DURATION); 954 details.features = getCallFeatures(cursor, count); 955 details.geocode = cursor.getString(CallLogQuery.GEOCODED_LOCATION); 956 details.transcription = cursor.getString(CallLogQuery.TRANSCRIPTION); 957 details.transcriptionState = transcriptionState; 958 details.callTypes = getCallTypes(cursor, count); 959 960 details.accountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME); 961 details.accountId = cursor.getString(CallLogQuery.ACCOUNT_ID); 962 details.cachedContactInfo = cachedContactInfo; 963 964 if (!cursor.isNull(CallLogQuery.DATA_USAGE)) { 965 details.dataUsage = cursor.getLong(CallLogQuery.DATA_USAGE); 966 } 967 968 views.rowId = cursor.getLong(CallLogQuery.ID); 969 // Stash away the Ids of the calls so that we can support deleting a row in the call log. 970 views.callIds = getCallIds(cursor, count); 971 details.previousGroup = getPreviousDayGroup(cursor); 972 973 // Store values used when the actions ViewStub is inflated on expansion. 974 views.number = number; 975 views.countryIso = details.countryIso; 976 views.postDialDigits = details.postDialDigits; 977 views.numberPresentation = numberPresentation; 978 979 if (details.callTypes[0] == CallLog.Calls.VOICEMAIL_TYPE 980 || details.callTypes[0] == CallLog.Calls.MISSED_TYPE) { 981 details.isRead = cursor.getInt(CallLogQuery.IS_READ) == 1; 982 } 983 views.callType = cursor.getInt(CallLogQuery.CALL_TYPE); 984 views.voicemailUri = cursor.getString(CallLogQuery.VOICEMAIL_URI); 985 details.voicemailUri = views.voicemailUri; 986 987 return details; 988 } 989 990 @MainThread 991 private CallDetailsEntries createCallDetailsEntries(Cursor cursor, int count) { 992 Assert.isMainThread(); 993 int position = cursor.getPosition(); 994 CallDetailsEntries.Builder entries = CallDetailsEntries.newBuilder(); 995 for (int i = 0; i < count; i++) { 996 CallDetailsEntry.Builder entry = 997 CallDetailsEntry.newBuilder() 998 .setCallId(cursor.getLong(CallLogQuery.ID)) 999 .setCallType(cursor.getInt(CallLogQuery.CALL_TYPE)) 1000 .setDataUsage(cursor.getLong(CallLogQuery.DATA_USAGE)) 1001 .setDate(cursor.getLong(CallLogQuery.DATE)) 1002 .setDuration(cursor.getLong(CallLogQuery.DURATION)) 1003 .setFeatures(cursor.getInt(CallLogQuery.FEATURES)); 1004 1005 String phoneAccountComponentName = cursor.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME); 1006 if (DuoConstants.PHONE_ACCOUNT_COMPONENT_NAME 1007 .flattenToString() 1008 .equals(phoneAccountComponentName)) { 1009 entry.setIsDuoCall(true); 1010 } 1011 1012 entries.addEntries(entry.build()); 1013 cursor.moveToNext(); 1014 } 1015 cursor.moveToPosition(position); 1016 return entries.build(); 1017 } 1018 1019 /** 1020 * Load data for call log. Any expensive operation should be put here to avoid blocking main 1021 * thread. Do NOT put any cursor operation here since it's not thread safe. 1022 */ 1023 @WorkerThread 1024 private boolean loadData(CallLogListItemViewHolder views, long rowId, PhoneCallDetails details) { 1025 Assert.isWorkerThread(); 1026 if (rowId != views.rowId) { 1027 LogUtil.i( 1028 "CallLogAdapter.loadData", 1029 "rowId of viewHolder changed after load task is issued, aborting load"); 1030 return false; 1031 } 1032 1033 final PhoneAccountHandle accountHandle = 1034 TelecomUtil.composePhoneAccountHandle(details.accountComponentName, details.accountId); 1035 1036 final boolean isVoicemailNumber = callLogCache.isVoicemailNumber(accountHandle, details.number); 1037 1038 // Note: Binding of the action buttons is done as required in configureActionViews when the 1039 // user expands the actions ViewStub. 1040 1041 ContactInfo info = ContactInfo.EMPTY; 1042 if (PhoneNumberHelper.canPlaceCallsTo(details.number, details.numberPresentation) 1043 && !isVoicemailNumber) { 1044 // Lookup contacts with this number 1045 // Only do remote lookup in first 5 rows. 1046 int position = views.getAdapterPosition(); 1047 info = 1048 contactInfoCache.getValue( 1049 details.number + details.postDialDigits, 1050 details.countryIso, 1051 details.cachedContactInfo, 1052 position 1053 < ConfigProviderBindings.get(activity) 1054 .getLong("number_of_call_to_do_remote_lookup", 5L)); 1055 } 1056 CharSequence formattedNumber = 1057 info.formattedNumber == null 1058 ? null 1059 : PhoneNumberUtils.createTtsSpannable(info.formattedNumber); 1060 details.updateDisplayNumber(activity, formattedNumber, isVoicemailNumber); 1061 1062 views.displayNumber = details.displayNumber; 1063 views.accountHandle = accountHandle; 1064 details.accountHandle = accountHandle; 1065 1066 if (!TextUtils.isEmpty(info.name) || !TextUtils.isEmpty(info.nameAlternative)) { 1067 details.contactUri = info.lookupUri; 1068 details.namePrimary = info.name; 1069 details.nameAlternative = info.nameAlternative; 1070 details.nameDisplayOrder = contactsPreferences.getDisplayOrder(); 1071 details.numberType = info.type; 1072 details.numberLabel = info.label; 1073 details.photoUri = info.photoUri; 1074 details.sourceType = info.sourceType; 1075 details.objectId = info.objectId; 1076 details.contactUserType = info.userType; 1077 } 1078 LogUtil.d( 1079 "CallLogAdapter.loadData", 1080 "position:%d, update geo info: %s, cequint caller id geo: %s, photo uri: %s <- %s", 1081 views.getAdapterPosition(), 1082 details.geocode, 1083 info.geoDescription, 1084 details.photoUri, 1085 info.photoUri); 1086 if (!TextUtils.isEmpty(info.geoDescription)) { 1087 details.geocode = info.geoDescription; 1088 } 1089 1090 views.info = info; 1091 views.numberType = getNumberType(activity.getResources(), details); 1092 1093 callLogListItemHelper.updatePhoneCallDetails(details); 1094 return true; 1095 } 1096 1097 private static String getNumberType(Resources res, PhoneCallDetails details) { 1098 // Label doesn't make much sense if the information is coming from CNAP or Cequint Caller ID. 1099 if (details.sourceType == ContactSource.Type.SOURCE_TYPE_CNAP 1100 || details.sourceType == ContactSource.Type.SOURCE_TYPE_CEQUINT_CALLER_ID) { 1101 return ""; 1102 } 1103 // Returns empty label instead of "custom" if the custom label is empty. 1104 if (details.numberType == Phone.TYPE_CUSTOM && TextUtils.isEmpty(details.numberLabel)) { 1105 return ""; 1106 } 1107 return (String) Phone.getTypeLabel(res, details.numberType, details.numberLabel); 1108 } 1109 1110 /** 1111 * Render item view given position. This is running on UI thread so DO NOT put any expensive 1112 * operation into it. 1113 */ 1114 @MainThread 1115 private void render(CallLogListItemViewHolder views, PhoneCallDetails details, long rowId) { 1116 Assert.isMainThread(); 1117 if (rowId != views.rowId) { 1118 LogUtil.i( 1119 "CallLogAdapter.render", 1120 "rowId of viewHolder changed after load task is issued, aborting render"); 1121 return; 1122 } 1123 1124 // Default case: an item in the call log. 1125 views.primaryActionView.setVisibility(View.VISIBLE); 1126 views.workIconView.setVisibility( 1127 details.contactUserType == ContactsUtils.USER_TYPE_WORK ? View.VISIBLE : View.GONE); 1128 1129 if (selectAllMode && views.voicemailUri != null) { 1130 selectedItems.put(getVoicemailId(views.voicemailUri), views.voicemailUri); 1131 } 1132 if (deselectAllMode && views.voicemailUri != null) { 1133 selectedItems.delete(getVoicemailId(views.voicemailUri)); 1134 } 1135 if (views.voicemailUri != null 1136 && selectedItems.get(getVoicemailId(views.voicemailUri)) != null) { 1137 views.checkBoxView.setVisibility(View.VISIBLE); 1138 views.quickContactView.setVisibility(View.GONE); 1139 } else if (views.voicemailUri != null) { 1140 views.checkBoxView.setVisibility(View.GONE); 1141 views.quickContactView.setVisibility(View.VISIBLE); 1142 } 1143 callLogListItemHelper.setPhoneCallDetails(views, details); 1144 if (currentlyExpandedRowId == views.rowId) { 1145 // In case ViewHolders were added/removed, update the expanded position if the rowIds 1146 // match so that we can restore the correct expanded state on rebind. 1147 currentlyExpandedPosition = views.getAdapterPosition(); 1148 views.showActions(true); 1149 } else { 1150 views.showActions(false); 1151 } 1152 views.dayGroupHeader.setVisibility(views.dayGroupHeaderVisibility); 1153 views.dayGroupHeader.setText(views.dayGroupHeaderText); 1154 } 1155 1156 @Override 1157 public int getItemCount() { 1158 return super.getItemCount() + (callLogAlertManager.isEmpty() ? 0 : 1); 1159 } 1160 1161 @Override 1162 public int getItemViewType(int position) { 1163 if (position == ALERT_POSITION && !callLogAlertManager.isEmpty()) { 1164 return VIEW_TYPE_ALERT; 1165 } 1166 return VIEW_TYPE_CALLLOG; 1167 } 1168 1169 /** 1170 * Retrieves an item at the specified position, taking into account the presence of a promo card. 1171 * 1172 * @param position The position to retrieve. 1173 * @return The item at that position. 1174 */ 1175 @Override 1176 public Object getItem(int position) { 1177 return super.getItem(position - (callLogAlertManager.isEmpty() ? 0 : 1)); 1178 } 1179 1180 @Override 1181 public long getItemId(int position) { 1182 Cursor cursor = (Cursor) getItem(position); 1183 if (cursor != null) { 1184 return cursor.getLong(CallLogQuery.ID); 1185 } else { 1186 return 0; 1187 } 1188 } 1189 1190 @Override 1191 public int getGroupSize(int position) { 1192 return super.getGroupSize(position - (callLogAlertManager.isEmpty() ? 0 : 1)); 1193 } 1194 1195 protected boolean isCallLogActivity() { 1196 return activityType == ACTIVITY_TYPE_CALL_LOG; 1197 } 1198 1199 /** 1200 * In order to implement the "undo" function, when a voicemail is "deleted" i.e. when the user 1201 * clicks the delete button, the deleted item is temporarily hidden from the list. If a user 1202 * clicks delete on a second item before the first item's undo option has expired, the first item 1203 * is immediately deleted so that only one item can be "undoed" at a time. 1204 */ 1205 @Override 1206 public void onVoicemailDeleted(CallLogListItemViewHolder viewHolder, Uri uri) { 1207 hiddenRowIds.add(viewHolder.rowId); 1208 // Save the new hidden item uri in case the activity is suspend before the undo has timed out. 1209 hiddenItemUris.add(uri); 1210 1211 collapseExpandedCard(); 1212 notifyItemChanged(viewHolder.getAdapterPosition()); 1213 // The next item might have to update its day group label 1214 notifyItemChanged(viewHolder.getAdapterPosition() + 1); 1215 } 1216 1217 private void collapseExpandedCard() { 1218 currentlyExpandedRowId = NO_EXPANDED_LIST_ITEM; 1219 currentlyExpandedPosition = RecyclerView.NO_POSITION; 1220 } 1221 1222 /** When the list is changing all stored position is no longer valid. */ 1223 public void invalidatePositions() { 1224 currentlyExpandedPosition = RecyclerView.NO_POSITION; 1225 } 1226 1227 /** When the user clicks "undo", the hidden item is unhidden. */ 1228 @Override 1229 public void onVoicemailDeleteUndo(long rowId, int adapterPosition, Uri uri) { 1230 hiddenItemUris.remove(uri); 1231 hiddenRowIds.remove(rowId); 1232 notifyItemChanged(adapterPosition); 1233 // The next item might have to update its day group label 1234 notifyItemChanged(adapterPosition + 1); 1235 } 1236 1237 /** This callback signifies that a database deletion has completed. */ 1238 @Override 1239 public void onVoicemailDeletedInDatabase(long rowId, Uri uri) { 1240 hiddenItemUris.remove(uri); 1241 } 1242 1243 /** 1244 * Retrieves the day group of the previous call in the call log. Used to determine if the day 1245 * group has changed and to trigger display of the day group text. 1246 * 1247 * @param cursor The call log cursor. 1248 * @return The previous day group, or DAY_GROUP_NONE if this is the first call. 1249 */ 1250 private int getPreviousDayGroup(Cursor cursor) { 1251 // We want to restore the position in the cursor at the end. 1252 int startingPosition = cursor.getPosition(); 1253 moveToPreviousNonHiddenRow(cursor); 1254 if (cursor.isBeforeFirst()) { 1255 cursor.moveToPosition(startingPosition); 1256 return CallLogGroupBuilder.DAY_GROUP_NONE; 1257 } 1258 int result = getDayGroup(cursor.getLong(CallLogQuery.ID)); 1259 cursor.moveToPosition(startingPosition); 1260 return result; 1261 } 1262 1263 private void moveToPreviousNonHiddenRow(Cursor cursor) { 1264 while (cursor.moveToPrevious() && hiddenRowIds.contains(cursor.getLong(CallLogQuery.ID))) {} 1265 } 1266 1267 /** 1268 * Given a call ID, look up its callback action. Callback action data are populated in {@link 1269 * com.android.dialer.app.calllog.CallLogGroupBuilder}. 1270 * 1271 * @param callId The call ID to retrieve the callback action. 1272 * @return The callback action for the call. 1273 */ 1274 @MainThread 1275 private int getCallbackAction(long callId) { 1276 Integer result = callbackActions.get(callId); 1277 if (result != null) { 1278 return result; 1279 } 1280 return CallbackAction.NONE; 1281 } 1282 1283 /** 1284 * Given a call ID, look up the day group the call belongs to. Day group data are populated in 1285 * {@link com.android.dialer.app.calllog.CallLogGroupBuilder}. 1286 * 1287 * @param callId The call ID to retrieve the day group. 1288 * @return The day group for the call. 1289 */ 1290 @MainThread 1291 private int getDayGroup(long callId) { 1292 Integer result = dayGroups.get(callId); 1293 if (result != null) { 1294 return result; 1295 } 1296 return CallLogGroupBuilder.DAY_GROUP_NONE; 1297 } 1298 1299 /** 1300 * Returns the call types for the given number of items in the cursor. 1301 * 1302 * <p>It uses the next {@code count} rows in the cursor to extract the types. 1303 * 1304 * <p>It position in the cursor is unchanged by this function. 1305 */ 1306 private static int[] getCallTypes(Cursor cursor, int count) { 1307 int position = cursor.getPosition(); 1308 int[] callTypes = new int[count]; 1309 for (int index = 0; index < count; ++index) { 1310 callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE); 1311 cursor.moveToNext(); 1312 } 1313 cursor.moveToPosition(position); 1314 return callTypes; 1315 } 1316 1317 /** 1318 * Determine the features which were enabled for any of the calls that make up a call log entry. 1319 * 1320 * @param cursor The cursor. 1321 * @param count The number of calls for the current call log entry. 1322 * @return The features. 1323 */ 1324 private int getCallFeatures(Cursor cursor, int count) { 1325 int features = 0; 1326 int position = cursor.getPosition(); 1327 for (int index = 0; index < count; ++index) { 1328 features |= cursor.getInt(CallLogQuery.FEATURES); 1329 cursor.moveToNext(); 1330 } 1331 cursor.moveToPosition(position); 1332 return features; 1333 } 1334 1335 /** 1336 * Sets whether processing of requests for contact details should be enabled. 1337 * 1338 * <p>This method should be called in tests to disable such processing of requests when not 1339 * needed. 1340 */ 1341 @VisibleForTesting 1342 void disableRequestProcessingForTest() { 1343 // TODO: Remove this and test the cache directly. 1344 contactInfoCache.disableRequestProcessing(); 1345 } 1346 1347 @VisibleForTesting 1348 void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) { 1349 // TODO: Remove this and test the cache directly. 1350 contactInfoCache.injectContactInfoForTest(number, countryIso, contactInfo); 1351 } 1352 1353 /** 1354 * Stores the callback action associated with a call in the call log. 1355 * 1356 * @param rowId The row ID of the current call. 1357 * @param callbackAction The current call's callback action. 1358 */ 1359 @Override 1360 @MainThread 1361 public void setCallbackAction(long rowId, @CallbackAction int callbackAction) { 1362 callbackActions.put(rowId, callbackAction); 1363 } 1364 1365 /** 1366 * Stores the day group associated with a call in the call log. 1367 * 1368 * @param rowId The row ID of the current call. 1369 * @param dayGroup The day group the call belongs in. 1370 */ 1371 @Override 1372 @MainThread 1373 public void setDayGroup(long rowId, int dayGroup) { 1374 dayGroups.put(rowId, dayGroup); 1375 } 1376 1377 /** Clears the day group associations on re-bind of the call log. */ 1378 @Override 1379 @MainThread 1380 public void clearDayGroups() { 1381 dayGroups.clear(); 1382 } 1383 1384 /** 1385 * Retrieves the call Ids represented by the current call log row. 1386 * 1387 * @param cursor Call log cursor to retrieve call Ids from. 1388 * @param groupSize Number of calls associated with the current call log row. 1389 * @return Array of call Ids. 1390 */ 1391 private long[] getCallIds(final Cursor cursor, final int groupSize) { 1392 // We want to restore the position in the cursor at the end. 1393 int startingPosition = cursor.getPosition(); 1394 long[] ids = new long[groupSize]; 1395 // Copy the ids of the rows in the group. 1396 for (int index = 0; index < groupSize; ++index) { 1397 ids[index] = cursor.getLong(CallLogQuery.ID); 1398 cursor.moveToNext(); 1399 } 1400 cursor.moveToPosition(startingPosition); 1401 return ids; 1402 } 1403 1404 /** 1405 * Determines the description for a day group. 1406 * 1407 * @param group The day group to retrieve the description for. 1408 * @return The day group description. 1409 */ 1410 private CharSequence getGroupDescription(int group) { 1411 if (group == CallLogGroupBuilder.DAY_GROUP_TODAY) { 1412 return activity.getResources().getString(R.string.call_log_header_today); 1413 } else if (group == CallLogGroupBuilder.DAY_GROUP_YESTERDAY) { 1414 return activity.getResources().getString(R.string.call_log_header_yesterday); 1415 } else { 1416 return activity.getResources().getString(R.string.call_log_header_other); 1417 } 1418 } 1419 1420 @NonNull 1421 private EnrichedCallManager getEnrichedCallManager() { 1422 return EnrichedCallComponent.get(activity).getEnrichedCallManager(); 1423 } 1424 1425 @NonNull 1426 private Duo getDuo() { 1427 return DuoComponent.get(activity).getDuo(); 1428 } 1429 1430 @Override 1431 public void onDuoStateChanged() { 1432 notifyDataSetChanged(); 1433 } 1434 1435 public void onAllSelected() { 1436 selectAllMode = true; 1437 deselectAllMode = false; 1438 selectedItems.clear(); 1439 for (int i = 0; i < getItemCount(); i++) { 1440 Cursor c = (Cursor) getItem(i); 1441 if (c != null) { 1442 Assert.checkArgument(CallLogQuery.VOICEMAIL_URI == c.getColumnIndex("voicemail_uri")); 1443 String voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI); 1444 selectedItems.put(getVoicemailId(voicemailUri), voicemailUri); 1445 } 1446 } 1447 updateActionBar(); 1448 notifyDataSetChanged(); 1449 } 1450 1451 public void onAllDeselected() { 1452 selectAllMode = false; 1453 deselectAllMode = true; 1454 selectedItems.clear(); 1455 updateActionBar(); 1456 notifyDataSetChanged(); 1457 } 1458 1459 /** Interface used to initiate a refresh of the content. */ 1460 public interface CallFetcher { 1461 1462 void fetchCalls(); 1463 } 1464 1465 /** Interface used to allow single tap multi select for contact photos. */ 1466 public interface OnActionModeStateChangedListener { 1467 1468 void onActionModeStateChanged(boolean isEnabled); 1469 1470 boolean isActionModeStateEnabled(); 1471 } 1472 1473 /** Interface used to hide the fragments. */ 1474 public interface MultiSelectRemoveView { 1475 1476 void showMultiSelectRemoveView(boolean show); 1477 1478 void setSelectAllModeToFalse(); 1479 1480 void tapSelectAll(); 1481 } 1482 } 1483