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 showDeleteSelectedItemsDialog()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 deleteSelectedItems(SparseArray<String> voicemailsToDelete)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 getExpandCollapseListener()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 getOnScrollListener()420 public RecyclerView.OnScrollListener getOnScrollListener() { 421 return null; 422 } 423 checkMarkCallLogEntry(CallLogListItemViewHolder viewHolder)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 announceforAccessibility(View view, String announcement)435 private void announceforAccessibility(View view, String announcement) { 436 if (view != null) { 437 view.announceForAccessibility(announcement); 438 } 439 } 440 updateActionBar()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 uncheckMarkCallLogEntry(CallLogListItemViewHolder viewHolder, int id)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 getVoicemailId(String voicemailUri)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 CallLogAdapter( Activity activity, ViewGroup alertContainer, CallFetcher callFetcher, MultiSelectRemoveView multiSelectRemoveView, OnActionModeStateChangedListener actionModeStateChangedListener, CallLogCache callLogCache, ContactInfoCache contactInfoCache, VoicemailPlaybackPresenter voicemailPlaybackPresenter, @NonNull FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler, int activityType)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 expandViewHolderActions(CallLogListItemViewHolder viewHolder)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 onSaveInstanceState(Bundle outState)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 onRestoreInstanceState(Bundle savedInstanceState)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 onContentChanged()656 protected void onContentChanged() { 657 callFetcher.fetchCalls(); 658 } 659 setLoading(boolean loading)660 public void setLoading(boolean loading) { 661 this.loading = loading; 662 } 663 isEmpty()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 clearFilteredNumbersCache()673 public void clearFilteredNumbersCache() { 674 filteredNumberAsyncQueryHandler.clearCache(); 675 } 676 onResume()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 onPause()687 public void onPause() { 688 getDuo().unregisterListener(this); 689 pauseCache(); 690 for (Uri uri : hiddenItemUris) { 691 CallLogAsyncTaskUtil.deleteVoicemail(activity, uri, null); 692 } 693 } 694 onStop()695 public void onStop() { 696 getEnrichedCallManager().clearCachedData(); 697 } 698 getAlertManager()699 public CallLogAlertManager getAlertManager() { 700 return callLogAlertManager; 701 } 702 703 @VisibleForTesting pauseCache()704 /* package */ void pauseCache() { 705 contactInfoCache.stop(); 706 callLogCache.reset(); 707 } 708 709 @Override addGroups(Cursor cursor)710 protected void addGroups(Cursor cursor) { 711 callLogGroupBuilder.addGroups(cursor); 712 } 713 714 @Override onCreateViewHolder(ViewGroup parent, int viewType)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 */ createCallLogEntryViewHolder(ViewGroup parent)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 onBindViewHolder(ViewHolder viewHolder, int position)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 onViewRecycled(ViewHolder viewHolder)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 onViewAttachedToWindow(ViewHolder viewHolder)786 public void onViewAttachedToWindow(ViewHolder viewHolder) { 787 if (viewHolder.getItemViewType() == VIEW_TYPE_CALLLOG) { 788 ((CallLogListItemViewHolder) viewHolder).isAttachedToWindow = true; 789 } 790 } 791 792 @Override onViewDetachedFromWindow(ViewHolder viewHolder)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 */ bindCallLogListViewHolder(final ViewHolder viewHolder, final int position)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 updateCheckMarkedStatusOfEntry(CallLogListItemViewHolder views)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 isHiddenRow(@ullable String number, long rowId)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 loadAndRender( final CallLogListItemViewHolder viewHolder, final long rowId, final PhoneCallDetails details, final CallDetailsEntries callDetailsEntries)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 isCallComposerCapable(@ullable String number)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 createPhoneCallDetails( Cursor cursor, int count, final CallLogListItemViewHolder views)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 createCallDetailsEntries(Cursor cursor, int count)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 loadData(CallLogListItemViewHolder views, long rowId, PhoneCallDetails details)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 getNumberType(Resources res, PhoneCallDetails details)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 render(CallLogListItemViewHolder views, PhoneCallDetails details, long rowId)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 getItemCount()1157 public int getItemCount() { 1158 return super.getItemCount() + (callLogAlertManager.isEmpty() ? 0 : 1); 1159 } 1160 1161 @Override getItemViewType(int position)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 getItem(int position)1176 public Object getItem(int position) { 1177 return super.getItem(position - (callLogAlertManager.isEmpty() ? 0 : 1)); 1178 } 1179 1180 @Override getItemId(int position)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 getGroupSize(int position)1191 public int getGroupSize(int position) { 1192 return super.getGroupSize(position - (callLogAlertManager.isEmpty() ? 0 : 1)); 1193 } 1194 isCallLogActivity()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 onVoicemailDeleted(CallLogListItemViewHolder viewHolder, Uri uri)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 collapseExpandedCard()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. */ invalidatePositions()1223 public void invalidatePositions() { 1224 currentlyExpandedPosition = RecyclerView.NO_POSITION; 1225 } 1226 1227 /** When the user clicks "undo", the hidden item is unhidden. */ 1228 @Override onVoicemailDeleteUndo(long rowId, int adapterPosition, Uri uri)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 onVoicemailDeletedInDatabase(long rowId, Uri uri)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 */ getPreviousDayGroup(Cursor cursor)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 moveToPreviousNonHiddenRow(Cursor cursor)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 getCallbackAction(long callId)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 getDayGroup(long callId)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 */ getCallTypes(Cursor cursor, int count)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 */ getCallFeatures(Cursor cursor, int count)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 disableRequestProcessingForTest()1342 void disableRequestProcessingForTest() { 1343 // TODO: Remove this and test the cache directly. 1344 contactInfoCache.disableRequestProcessing(); 1345 } 1346 1347 @VisibleForTesting injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo)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 setCallbackAction(long rowId, @CallbackAction int callbackAction)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 setDayGroup(long rowId, int dayGroup)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 clearDayGroups()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 */ getCallIds(final Cursor cursor, final int groupSize)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 */ getGroupDescription(int group)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 getEnrichedCallManager()1421 private EnrichedCallManager getEnrichedCallManager() { 1422 return EnrichedCallComponent.get(activity).getEnrichedCallManager(); 1423 } 1424 1425 @NonNull getDuo()1426 private Duo getDuo() { 1427 return DuoComponent.get(activity).getDuo(); 1428 } 1429 1430 @Override onDuoStateChanged()1431 public void onDuoStateChanged() { 1432 notifyDataSetChanged(); 1433 } 1434 onAllSelected()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 onAllDeselected()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 fetchCalls()1462 void fetchCalls(); 1463 } 1464 1465 /** Interface used to allow single tap multi select for contact photos. */ 1466 public interface OnActionModeStateChangedListener { 1467 onActionModeStateChanged(boolean isEnabled)1468 void onActionModeStateChanged(boolean isEnabled); 1469 isActionModeStateEnabled()1470 boolean isActionModeStateEnabled(); 1471 } 1472 1473 /** Interface used to hide the fragments. */ 1474 public interface MultiSelectRemoveView { 1475 showMultiSelectRemoveView(boolean show)1476 void showMultiSelectRemoveView(boolean show); 1477 setSelectAllModeToFalse()1478 void setSelectAllModeToFalse(); 1479 tapSelectAll()1480 void tapSelectAll(); 1481 } 1482 } 1483