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