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