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