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.calllog; 18 19 import com.google.common.annotations.VisibleForTesting; 20 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.SharedPreferences; 24 import android.content.res.Resources; 25 import android.database.Cursor; 26 import android.net.Uri; 27 import android.os.Bundle; 28 import android.os.Trace; 29 import android.preference.PreferenceManager; 30 import android.provider.CallLog; 31 import android.provider.ContactsContract.CommonDataKinds.Phone; 32 import android.support.v7.widget.RecyclerView; 33 import android.support.v7.widget.RecyclerView.ViewHolder; 34 import android.telecom.PhoneAccountHandle; 35 import android.telephony.PhoneNumberUtils; 36 import android.telephony.TelephonyManager; 37 import android.text.TextUtils; 38 import android.util.ArrayMap; 39 import android.view.LayoutInflater; 40 import android.view.View; 41 import android.view.View.AccessibilityDelegate; 42 import android.view.ViewGroup; 43 import android.view.accessibility.AccessibilityEvent; 44 45 import com.android.contacts.common.ContactsUtils; 46 import com.android.contacts.common.compat.CompatUtils; 47 import com.android.contacts.common.compat.PhoneNumberUtilsCompat; 48 import com.android.contacts.common.preference.ContactsPreferences; 49 import com.android.contacts.common.util.PermissionsUtil; 50 import com.android.dialer.DialtactsActivity; 51 import com.android.dialer.PhoneCallDetails; 52 import com.android.dialer.R; 53 import com.android.dialer.calllog.calllogcache.CallLogCache; 54 import com.android.dialer.contactinfo.ContactInfoCache; 55 import com.android.dialer.contactinfo.ContactInfoCache.OnContactInfoChangedListener; 56 import com.android.dialer.database.FilteredNumberAsyncQueryHandler; 57 import com.android.dialer.database.VoicemailArchiveContract; 58 import com.android.dialer.filterednumber.BlockNumberDialogFragment.Callback; 59 import com.android.dialer.logging.InteractionEvent; 60 import com.android.dialer.logging.Logger; 61 import com.android.dialer.service.ExtendedBlockingButtonRenderer; 62 import com.android.dialer.util.PhoneNumberUtil; 63 import com.android.dialer.voicemail.VoicemailPlaybackPresenter; 64 65 import java.util.HashMap; 66 import java.util.Map; 67 68 /** 69 * Adapter class to fill in data for the Call Log. 70 */ 71 public class CallLogAdapter extends GroupingListAdapter 72 implements CallLogGroupBuilder.GroupCreator, 73 VoicemailPlaybackPresenter.OnVoicemailDeletedListener, 74 ExtendedBlockingButtonRenderer.Listener { 75 76 // Types of activities the call log adapter is used for 77 public static final int ACTIVITY_TYPE_CALL_LOG = 1; 78 public static final int ACTIVITY_TYPE_ARCHIVE = 2; 79 public static final int ACTIVITY_TYPE_DIALTACTS = 3; 80 81 /** Interface used to initiate a refresh of the content. */ 82 public interface CallFetcher { fetchCalls()83 public void fetchCalls(); 84 } 85 86 private static final int NO_EXPANDED_LIST_ITEM = -1; 87 // ConcurrentHashMap doesn't store null values. Use this value for numbers which aren't blocked. 88 private static final int NOT_BLOCKED = -1; 89 90 private static final int VOICEMAIL_PROMO_CARD_POSITION = 0; 91 92 protected static final int VIEW_TYPE_NORMAL = 0; 93 private static final int VIEW_TYPE_VOICEMAIL_PROMO_CARD = 1; 94 95 /** 96 * The key for the show voicemail promo card preference which will determine whether the promo 97 * card was permanently dismissed or not. 98 */ 99 private static final String SHOW_VOICEMAIL_PROMO_CARD = "show_voicemail_promo_card"; 100 private static final boolean SHOW_VOICEMAIL_PROMO_CARD_DEFAULT = true; 101 102 protected final Context mContext; 103 private final ContactInfoHelper mContactInfoHelper; 104 protected final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter; 105 private final CallFetcher mCallFetcher; 106 private final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler; 107 private final Map<String, Boolean> mBlockedNumberCache = new ArrayMap<>(); 108 109 protected ContactInfoCache mContactInfoCache; 110 111 private final int mActivityType; 112 113 private static final String KEY_EXPANDED_POSITION = "expanded_position"; 114 private static final String KEY_EXPANDED_ROW_ID = "expanded_row_id"; 115 116 // Tracks the position of the currently expanded list item. 117 private int mCurrentlyExpandedPosition = RecyclerView.NO_POSITION; 118 // Tracks the rowId of the currently expanded list item, so the position can be updated if there 119 // are any changes to the call log entries, such as additions or removals. 120 private long mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM; 121 private int mHiddenPosition = RecyclerView.NO_POSITION; 122 private Uri mHiddenItemUri = null; 123 private boolean mPendingHide = false; 124 125 /** 126 * Hashmap, keyed by call Id, used to track the day group for a call. As call log entries are 127 * put into the primary call groups in {@link com.android.dialer.calllog.CallLogGroupBuilder}, 128 * they are also assigned a secondary "day group". This hashmap tracks the day group assigned 129 * to all calls in the call log. This information is used to trigger the display of a day 130 * group header above the call log entry at the start of a day group. 131 * Note: Multiple calls are grouped into a single primary "call group" in the call log, and 132 * the cursor used to bind rows includes all of these calls. When determining if a day group 133 * change has occurred it is necessary to look at the last entry in the call log to determine 134 * its day group. This hashmap provides a means of determining the previous day group without 135 * having to reverse the cursor to the start of the previous day call log entry. 136 */ 137 private HashMap<Long, Integer> mDayGroups = new HashMap<>(); 138 139 private boolean mLoading = true; 140 141 private SharedPreferences mPrefs; 142 143 private ContactsPreferences mContactsPreferences; 144 145 protected boolean mShowVoicemailPromoCard = false; 146 147 /** Instance of helper class for managing views. */ 148 private final CallLogListItemHelper mCallLogListItemHelper; 149 150 /** Cache for repeated requests to Telecom/Telephony. */ 151 protected final CallLogCache mCallLogCache; 152 153 /** Helper to group call log entries. */ 154 private final CallLogGroupBuilder mCallLogGroupBuilder; 155 156 /** 157 * The OnClickListener used to expand or collapse the action buttons of a call log entry. 158 */ 159 private final View.OnClickListener mExpandCollapseListener = new View.OnClickListener() { 160 @Override 161 public void onClick(View v) { 162 CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) v.getTag(); 163 if (viewHolder == null) { 164 return; 165 } 166 167 if (mVoicemailPlaybackPresenter != null) { 168 // Always reset the voicemail playback state on expand or collapse. 169 mVoicemailPlaybackPresenter.resetAll(); 170 } 171 172 if (viewHolder.getAdapterPosition() == mCurrentlyExpandedPosition) { 173 // Hide actions, if the clicked item is the expanded item. 174 viewHolder.showActions(false); 175 176 mCurrentlyExpandedPosition = RecyclerView.NO_POSITION; 177 mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM; 178 } else { 179 if (viewHolder.callType == CallLog.Calls.MISSED_TYPE) { 180 CallLogAsyncTaskUtil.markCallAsRead(mContext, viewHolder.callIds); 181 if (mActivityType == ACTIVITY_TYPE_DIALTACTS) { 182 ((DialtactsActivity) v.getContext()).updateTabUnreadCounts(); 183 } 184 } 185 expandViewHolderActions(viewHolder); 186 } 187 188 } 189 }; 190 191 /** 192 * Click handler used to dismiss the promo card when the user taps the "ok" button. 193 */ 194 private final View.OnClickListener mOkActionListener = new View.OnClickListener() { 195 @Override 196 public void onClick(View view) { 197 dismissVoicemailPromoCard(); 198 } 199 }; 200 201 /** 202 * Click handler used to send the user to the voicemail settings screen and then dismiss the 203 * promo card. 204 */ 205 private final View.OnClickListener mVoicemailSettingsActionListener = 206 new View.OnClickListener() { 207 @Override 208 public void onClick(View view) { 209 Intent intent = new Intent(TelephonyManager.ACTION_CONFIGURE_VOICEMAIL); 210 mContext.startActivity(intent); 211 dismissVoicemailPromoCard(); 212 } 213 }; 214 expandViewHolderActions(CallLogListItemViewHolder viewHolder)215 private void expandViewHolderActions(CallLogListItemViewHolder viewHolder) { 216 // If another item is expanded, notify it that it has changed. Its actions will be 217 // hidden when it is re-binded because we change mCurrentlyExpandedPosition below. 218 if (mCurrentlyExpandedPosition != RecyclerView.NO_POSITION) { 219 notifyItemChanged(mCurrentlyExpandedPosition); 220 } 221 // Show the actions for the clicked list item. 222 viewHolder.showActions(true); 223 mCurrentlyExpandedPosition = viewHolder.getAdapterPosition(); 224 mCurrentlyExpandedRowId = viewHolder.rowId; 225 } 226 227 /** 228 * Expand the actions on a list item when focused in Talkback mode, to aid discoverability. 229 */ 230 private AccessibilityDelegate mAccessibilityDelegate = new AccessibilityDelegate() { 231 @Override 232 public boolean onRequestSendAccessibilityEvent( 233 ViewGroup host, View child, AccessibilityEvent event) { 234 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) { 235 // Only expand if actions are not already expanded, because triggering the expand 236 // function on clicks causes the action views to lose the focus indicator. 237 CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) host.getTag(); 238 if (mCurrentlyExpandedPosition != viewHolder.getAdapterPosition()) { 239 if (mVoicemailPlaybackPresenter != null) { 240 // Always reset the voicemail playback state on expand. 241 mVoicemailPlaybackPresenter.resetAll(); 242 } 243 244 expandViewHolderActions((CallLogListItemViewHolder) host.getTag()); 245 } 246 } 247 return super.onRequestSendAccessibilityEvent(host, child, event); 248 } 249 }; 250 251 protected final OnContactInfoChangedListener mOnContactInfoChangedListener = 252 new OnContactInfoChangedListener() { 253 @Override 254 public void onContactInfoChanged() { 255 notifyDataSetChanged(); 256 } 257 }; 258 CallLogAdapter( Context context, CallFetcher callFetcher, ContactInfoHelper contactInfoHelper, VoicemailPlaybackPresenter voicemailPlaybackPresenter, int activityType)259 public CallLogAdapter( 260 Context context, 261 CallFetcher callFetcher, 262 ContactInfoHelper contactInfoHelper, 263 VoicemailPlaybackPresenter voicemailPlaybackPresenter, 264 int activityType) { 265 super(context); 266 267 mContext = context; 268 mCallFetcher = callFetcher; 269 mContactInfoHelper = contactInfoHelper; 270 mVoicemailPlaybackPresenter = voicemailPlaybackPresenter; 271 if (mVoicemailPlaybackPresenter != null) { 272 mVoicemailPlaybackPresenter.setOnVoicemailDeletedListener(this); 273 } 274 275 mActivityType = activityType; 276 277 mContactInfoCache = new ContactInfoCache( 278 mContactInfoHelper, mOnContactInfoChangedListener); 279 if (!PermissionsUtil.hasContactsPermissions(context)) { 280 mContactInfoCache.disableRequestProcessing(); 281 } 282 283 Resources resources = mContext.getResources(); 284 CallTypeHelper callTypeHelper = new CallTypeHelper(resources); 285 286 mCallLogCache = CallLogCache.getCallLogCache(mContext); 287 288 PhoneCallDetailsHelper phoneCallDetailsHelper = 289 new PhoneCallDetailsHelper(mContext, resources, mCallLogCache); 290 mCallLogListItemHelper = 291 new CallLogListItemHelper(phoneCallDetailsHelper, resources, mCallLogCache); 292 mCallLogGroupBuilder = new CallLogGroupBuilder(this); 293 mFilteredNumberAsyncQueryHandler = 294 new FilteredNumberAsyncQueryHandler(mContext.getContentResolver()); 295 296 mPrefs = PreferenceManager.getDefaultSharedPreferences(context); 297 mContactsPreferences = new ContactsPreferences(mContext); 298 maybeShowVoicemailPromoCard(); 299 } 300 onSaveInstanceState(Bundle outState)301 public void onSaveInstanceState(Bundle outState) { 302 outState.putInt(KEY_EXPANDED_POSITION, mCurrentlyExpandedPosition); 303 outState.putLong(KEY_EXPANDED_ROW_ID, mCurrentlyExpandedRowId); 304 } 305 onRestoreInstanceState(Bundle savedInstanceState)306 public void onRestoreInstanceState(Bundle savedInstanceState) { 307 if (savedInstanceState != null) { 308 mCurrentlyExpandedPosition = 309 savedInstanceState.getInt(KEY_EXPANDED_POSITION, RecyclerView.NO_POSITION); 310 mCurrentlyExpandedRowId = 311 savedInstanceState.getLong(KEY_EXPANDED_ROW_ID, NO_EXPANDED_LIST_ITEM); 312 } 313 } 314 315 @Override onBlockedNumber(String number,String countryIso)316 public void onBlockedNumber(String number,String countryIso) { 317 String cacheKey = PhoneNumberUtils.formatNumberToE164(number, countryIso); 318 if (!TextUtils.isEmpty(cacheKey)) { 319 mBlockedNumberCache.put(cacheKey, true); 320 notifyDataSetChanged(); 321 } 322 } 323 324 @Override onUnblockedNumber( String number, String countryIso)325 public void onUnblockedNumber( String number, String countryIso) { 326 String cacheKey = PhoneNumberUtils.formatNumberToE164(number, countryIso); 327 if (!TextUtils.isEmpty(cacheKey)) { 328 mBlockedNumberCache.put(cacheKey, false); 329 notifyDataSetChanged(); 330 } 331 } 332 333 /** 334 * Requery on background thread when {@link Cursor} changes. 335 */ 336 @Override onContentChanged()337 protected void onContentChanged() { 338 mCallFetcher.fetchCalls(); 339 } 340 setLoading(boolean loading)341 public void setLoading(boolean loading) { 342 mLoading = loading; 343 } 344 isEmpty()345 public boolean isEmpty() { 346 if (mLoading) { 347 // We don't want the empty state to show when loading. 348 return false; 349 } else { 350 return getItemCount() == 0; 351 } 352 } 353 invalidateCache()354 public void invalidateCache() { 355 mContactInfoCache.invalidate(); 356 } 357 onResume()358 public void onResume() { 359 if (PermissionsUtil.hasPermission(mContext, android.Manifest.permission.READ_CONTACTS)) { 360 mContactInfoCache.start(); 361 } 362 mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY); 363 } 364 onPause()365 public void onPause() { 366 pauseCache(); 367 368 if (mHiddenItemUri != null) { 369 CallLogAsyncTaskUtil.deleteVoicemail(mContext, mHiddenItemUri, null); 370 } 371 } 372 373 @VisibleForTesting pauseCache()374 /* package */ void pauseCache() { 375 mContactInfoCache.stop(); 376 mCallLogCache.reset(); 377 } 378 379 @Override addGroups(Cursor cursor)380 protected void addGroups(Cursor cursor) { 381 mCallLogGroupBuilder.addGroups(cursor); 382 } 383 384 @Override addVoicemailGroups(Cursor cursor)385 public void addVoicemailGroups(Cursor cursor) { 386 mCallLogGroupBuilder.addVoicemailGroups(cursor); 387 } 388 389 @Override onCreateViewHolder(ViewGroup parent, int viewType)390 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 391 if (viewType == VIEW_TYPE_VOICEMAIL_PROMO_CARD) { 392 return createVoicemailPromoCardViewHolder(parent); 393 } 394 return createCallLogEntryViewHolder(parent); 395 } 396 397 /** 398 * Creates a new call log entry {@link ViewHolder}. 399 * 400 * @param parent the parent view. 401 * @return The {@link ViewHolder}. 402 */ createCallLogEntryViewHolder(ViewGroup parent)403 private ViewHolder createCallLogEntryViewHolder(ViewGroup parent) { 404 LayoutInflater inflater = LayoutInflater.from(mContext); 405 View view = inflater.inflate(R.layout.call_log_list_item, parent, false); 406 CallLogListItemViewHolder viewHolder = CallLogListItemViewHolder.create( 407 view, 408 mContext, 409 this, 410 mExpandCollapseListener, 411 mCallLogCache, 412 mCallLogListItemHelper, 413 mVoicemailPlaybackPresenter, 414 mFilteredNumberAsyncQueryHandler, 415 new Callback() { 416 @Override 417 public void onFilterNumberSuccess() { 418 Logger.logInteraction( 419 InteractionEvent.BLOCK_NUMBER_CALL_LOG); 420 } 421 422 @Override 423 public void onUnfilterNumberSuccess() { 424 Logger.logInteraction( 425 InteractionEvent.UNBLOCK_NUMBER_CALL_LOG); 426 } 427 428 @Override 429 public void onChangeFilteredNumberUndo() {} 430 }, mActivityType == ACTIVITY_TYPE_ARCHIVE); 431 432 viewHolder.callLogEntryView.setTag(viewHolder); 433 viewHolder.callLogEntryView.setAccessibilityDelegate(mAccessibilityDelegate); 434 435 viewHolder.primaryActionView.setTag(viewHolder); 436 437 return viewHolder; 438 } 439 440 /** 441 * Binds the views in the entry to the data in the call log. 442 * TODO: This gets called 20-30 times when Dialer starts up for a single call log entry and 443 * should not. It invokes cross-process methods and the repeat execution can get costly. 444 * 445 * @param viewHolder The view corresponding to this entry. 446 * @param position The position of the entry. 447 */ 448 @Override onBindViewHolder(ViewHolder viewHolder, int position)449 public void onBindViewHolder(ViewHolder viewHolder, int position) { 450 Trace.beginSection("onBindViewHolder: " + position); 451 452 switch (getItemViewType(position)) { 453 case VIEW_TYPE_VOICEMAIL_PROMO_CARD: 454 bindVoicemailPromoCardViewHolder(viewHolder); 455 break; 456 default: 457 bindCallLogListViewHolder(viewHolder, position); 458 break; 459 } 460 461 Trace.endSection(); 462 } 463 464 /** 465 * Binds the promo card view holder. 466 * 467 * @param viewHolder The promo card view holder. 468 */ bindVoicemailPromoCardViewHolder(ViewHolder viewHolder)469 protected void bindVoicemailPromoCardViewHolder(ViewHolder viewHolder) { 470 PromoCardViewHolder promoCardViewHolder = (PromoCardViewHolder) viewHolder; 471 472 promoCardViewHolder.getSecondaryActionView() 473 .setOnClickListener(mVoicemailSettingsActionListener); 474 promoCardViewHolder.getPrimaryActionView().setOnClickListener(mOkActionListener); 475 } 476 477 /** 478 * Binds the view holder for the call log list item view. 479 * 480 * @param viewHolder The call log list item view holder. 481 * @param position The position of the list item. 482 */ 483 bindCallLogListViewHolder(ViewHolder viewHolder, int position)484 private void bindCallLogListViewHolder(ViewHolder viewHolder, int position) { 485 Cursor c = (Cursor) getItem(position); 486 if (c == null) { 487 return; 488 } 489 490 int count = getGroupSize(position); 491 492 final String number = c.getString(CallLogQuery.NUMBER); 493 final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO); 494 final String postDialDigits = CompatUtils.isNCompatible() 495 && mActivityType != ACTIVITY_TYPE_ARCHIVE ? 496 c.getString(CallLogQuery.POST_DIAL_DIGITS) : ""; 497 final String viaNumber = CompatUtils.isNCompatible() 498 && mActivityType != ACTIVITY_TYPE_ARCHIVE ? 499 c.getString(CallLogQuery.VIA_NUMBER) : ""; 500 final int numberPresentation = c.getInt(CallLogQuery.NUMBER_PRESENTATION); 501 final PhoneAccountHandle accountHandle = PhoneAccountUtils.getAccount( 502 c.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME), 503 c.getString(CallLogQuery.ACCOUNT_ID)); 504 final ContactInfo cachedContactInfo = ContactInfoHelper.getContactInfo(c); 505 final boolean isVoicemailNumber = 506 mCallLogCache.isVoicemailNumber(accountHandle, number); 507 508 // Note: Binding of the action buttons is done as required in configureActionViews when the 509 // user expands the actions ViewStub. 510 511 ContactInfo info = ContactInfo.EMPTY; 512 if (PhoneNumberUtil.canPlaceCallsTo(number, numberPresentation) && !isVoicemailNumber) { 513 // Lookup contacts with this number 514 info = mContactInfoCache.getValue(number + postDialDigits, 515 countryIso, cachedContactInfo); 516 } 517 CharSequence formattedNumber = info.formattedNumber == null 518 ? null : PhoneNumberUtilsCompat.createTtsSpannable(info.formattedNumber); 519 520 final PhoneCallDetails details = new PhoneCallDetails( 521 mContext, number, numberPresentation, formattedNumber, 522 postDialDigits, isVoicemailNumber); 523 details.viaNumber = viaNumber; 524 details.accountHandle = accountHandle; 525 details.countryIso = countryIso; 526 details.date = c.getLong(CallLogQuery.DATE); 527 details.duration = c.getLong(CallLogQuery.DURATION); 528 details.features = getCallFeatures(c, count); 529 details.geocode = c.getString(CallLogQuery.GEOCODED_LOCATION); 530 details.transcription = c.getString(CallLogQuery.TRANSCRIPTION); 531 details.callTypes = getCallTypes(c, count); 532 533 if (!c.isNull(CallLogQuery.DATA_USAGE)) { 534 details.dataUsage = c.getLong(CallLogQuery.DATA_USAGE); 535 } 536 537 if (!TextUtils.isEmpty(info.name) || !TextUtils.isEmpty(info.nameAlternative)) { 538 details.contactUri = info.lookupUri; 539 details.namePrimary = info.name; 540 details.nameAlternative = info.nameAlternative; 541 details.nameDisplayOrder = mContactsPreferences.getDisplayOrder(); 542 details.numberType = info.type; 543 details.numberLabel = info.label; 544 details.photoUri = info.photoUri; 545 details.sourceType = info.sourceType; 546 details.objectId = info.objectId; 547 details.contactUserType = info.userType; 548 } 549 550 final CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder; 551 views.info = info; 552 views.rowId = c.getLong(CallLogQuery.ID); 553 // Store values used when the actions ViewStub is inflated on expansion. 554 views.number = number; 555 views.postDialDigits = details.postDialDigits; 556 views.displayNumber = details.displayNumber; 557 views.numberPresentation = numberPresentation; 558 559 views.accountHandle = accountHandle; 560 // Stash away the Ids of the calls so that we can support deleting a row in the call log. 561 views.callIds = getCallIds(c, count); 562 views.isBusiness = mContactInfoHelper.isBusiness(info.sourceType); 563 views.numberType = (String) Phone.getTypeLabel(mContext.getResources(), details.numberType, 564 details.numberLabel); 565 // Default case: an item in the call log. 566 views.primaryActionView.setVisibility(View.VISIBLE); 567 views.workIconView.setVisibility( 568 details.contactUserType == ContactsUtils.USER_TYPE_WORK ? View.VISIBLE : View.GONE); 569 570 // Check if the day group has changed and display a header if necessary. 571 int currentGroup = getDayGroupForCall(views.rowId); 572 int previousGroup = getPreviousDayGroup(c); 573 if (currentGroup != previousGroup) { 574 views.dayGroupHeader.setVisibility(View.VISIBLE); 575 views.dayGroupHeader.setText(getGroupDescription(currentGroup)); 576 } else { 577 views.dayGroupHeader.setVisibility(View.GONE); 578 } 579 580 if (mActivityType == ACTIVITY_TYPE_ARCHIVE) { 581 views.callType = CallLog.Calls.VOICEMAIL_TYPE; 582 views.voicemailUri = VoicemailArchiveContract.VoicemailArchive.buildWithId(c.getInt( 583 c.getColumnIndex(VoicemailArchiveContract.VoicemailArchive._ID))) 584 .toString(); 585 586 } else { 587 if (details.callTypes[0] == CallLog.Calls.VOICEMAIL_TYPE || 588 details.callTypes[0] == CallLog.Calls.MISSED_TYPE) { 589 details.isRead = c.getInt(CallLogQuery.IS_READ) == 1; 590 } 591 views.callType = c.getInt(CallLogQuery.CALL_TYPE); 592 views.voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI); 593 } 594 595 mCallLogListItemHelper.setPhoneCallDetails(views, details); 596 597 if (mCurrentlyExpandedRowId == views.rowId) { 598 // In case ViewHolders were added/removed, update the expanded position if the rowIds 599 // match so that we can restore the correct expanded state on rebind. 600 mCurrentlyExpandedPosition = position; 601 views.showActions(true); 602 } else { 603 views.showActions(false); 604 } 605 views.updatePhoto(); 606 607 mCallLogListItemHelper.setPhoneCallDetails(views, details); 608 } 609 getPreferredDisplayName(ContactInfo contactInfo)610 private String getPreferredDisplayName(ContactInfo contactInfo) { 611 if (mContactsPreferences.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY || 612 TextUtils.isEmpty(contactInfo.nameAlternative)) { 613 return contactInfo.name; 614 } 615 return contactInfo.nameAlternative; 616 } 617 618 @Override getItemCount()619 public int getItemCount() { 620 return super.getItemCount() + (mShowVoicemailPromoCard ? 1 : 0) 621 - (mHiddenPosition != RecyclerView.NO_POSITION ? 1 : 0); 622 } 623 624 @Override getItemViewType(int position)625 public int getItemViewType(int position) { 626 if (position == VOICEMAIL_PROMO_CARD_POSITION && mShowVoicemailPromoCard) { 627 return VIEW_TYPE_VOICEMAIL_PROMO_CARD; 628 } 629 return super.getItemViewType(position); 630 } 631 632 /** 633 * Retrieves an item at the specified position, taking into account the presence of a promo 634 * card. 635 * 636 * @param position The position to retrieve. 637 * @return The item at that position. 638 */ 639 @Override getItem(int position)640 public Object getItem(int position) { 641 return super.getItem(position - (mShowVoicemailPromoCard ? 1 : 0) 642 + ((mHiddenPosition != RecyclerView.NO_POSITION && position >= mHiddenPosition) 643 ? 1 : 0)); 644 } 645 646 @Override getGroupSize(int position)647 public int getGroupSize(int position) { 648 return super.getGroupSize(position - (mShowVoicemailPromoCard ? 1 : 0)); 649 } 650 isCallLogActivity()651 protected boolean isCallLogActivity() { 652 return mActivityType == ACTIVITY_TYPE_CALL_LOG; 653 } 654 655 /** 656 * In order to implement the "undo" function, when a voicemail is "deleted" i.e. when the user 657 * clicks the delete button, the deleted item is temporarily hidden from the list. If a user 658 * clicks delete on a second item before the first item's undo option has expired, the first 659 * item is immediately deleted so that only one item can be "undoed" at a time. 660 */ 661 @Override onVoicemailDeleted(Uri uri)662 public void onVoicemailDeleted(Uri uri) { 663 if (mHiddenItemUri == null) { 664 // Immediately hide the currently expanded card. 665 mHiddenPosition = mCurrentlyExpandedPosition; 666 notifyDataSetChanged(); 667 } else { 668 // This means that there was a previous item that was hidden in the UI but not 669 // yet deleted from the database (call it a "pending delete"). Delete this previous item 670 // now since it is only possible to do one "undo" at a time. 671 CallLogAsyncTaskUtil.deleteVoicemail(mContext, mHiddenItemUri, null); 672 673 // Set pending hide action so that the current item is hidden only after the previous 674 // item is permanently deleted. 675 mPendingHide = true; 676 } 677 678 collapseExpandedCard(); 679 680 // Save the new hidden item uri in case it needs to be deleted from the database when 681 // a user attempts to delete another item. 682 mHiddenItemUri = uri; 683 } 684 collapseExpandedCard()685 private void collapseExpandedCard() { 686 mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM; 687 mCurrentlyExpandedPosition = RecyclerView.NO_POSITION; 688 } 689 690 /** 691 * When the list is changing all stored position is no longer valid. 692 */ invalidatePositions()693 public void invalidatePositions() { 694 mCurrentlyExpandedPosition = RecyclerView.NO_POSITION; 695 mHiddenPosition = RecyclerView.NO_POSITION; 696 } 697 698 /** 699 * When the user clicks "undo", the hidden item is unhidden. 700 */ 701 @Override onVoicemailDeleteUndo()702 public void onVoicemailDeleteUndo() { 703 mHiddenPosition = RecyclerView.NO_POSITION; 704 mHiddenItemUri = null; 705 706 mPendingHide = false; 707 notifyDataSetChanged(); 708 } 709 710 /** 711 * This callback signifies that a database deletion has completed. This means that if there is 712 * an item pending deletion, it will be hidden because the previous item that was in "undo" mode 713 * has been removed from the database. Otherwise it simply resets the hidden state because there 714 * are no pending deletes and thus no hidden items. 715 */ 716 @Override onVoicemailDeletedInDatabase()717 public void onVoicemailDeletedInDatabase() { 718 if (mPendingHide) { 719 mHiddenPosition = mCurrentlyExpandedPosition; 720 mPendingHide = false; 721 } else { 722 // There should no longer be any hidden item because it has been deleted from the 723 // database. 724 mHiddenPosition = RecyclerView.NO_POSITION; 725 mHiddenItemUri = null; 726 } 727 } 728 729 /** 730 * Retrieves the day group of the previous call in the call log. Used to determine if the day 731 * group has changed and to trigger display of the day group text. 732 * 733 * @param cursor The call log cursor. 734 * @return The previous day group, or DAY_GROUP_NONE if this is the first call. 735 */ getPreviousDayGroup(Cursor cursor)736 private int getPreviousDayGroup(Cursor cursor) { 737 // We want to restore the position in the cursor at the end. 738 int startingPosition = cursor.getPosition(); 739 int dayGroup = CallLogGroupBuilder.DAY_GROUP_NONE; 740 if (cursor.moveToPrevious()) { 741 // If the previous entry is hidden (deleted in the UI but not in the database), skip it 742 // and check the card above it. A list with the voicemail promo card at the top will be 743 // 1-indexed because the 0th index is the promo card iteself. 744 int previousViewPosition = mShowVoicemailPromoCard ? startingPosition : 745 startingPosition - 1; 746 if (previousViewPosition != mHiddenPosition || 747 (previousViewPosition == mHiddenPosition && cursor.moveToPrevious())) { 748 long previousRowId = cursor.getLong(CallLogQuery.ID); 749 dayGroup = getDayGroupForCall(previousRowId); 750 } 751 } 752 cursor.moveToPosition(startingPosition); 753 return dayGroup; 754 } 755 756 /** 757 * Given a call Id, look up the day group that the call belongs to. The day group data is 758 * populated in {@link com.android.dialer.calllog.CallLogGroupBuilder}. 759 * 760 * @param callId The call to retrieve the day group for. 761 * @return The day group for the call. 762 */ getDayGroupForCall(long callId)763 private int getDayGroupForCall(long callId) { 764 if (mDayGroups.containsKey(callId)) { 765 return mDayGroups.get(callId); 766 } 767 return CallLogGroupBuilder.DAY_GROUP_NONE; 768 } 769 770 /** 771 * Returns the call types for the given number of items in the cursor. 772 * <p> 773 * It uses the next {@code count} rows in the cursor to extract the types. 774 * <p> 775 * It position in the cursor is unchanged by this function. 776 */ getCallTypes(Cursor cursor, int count)777 private int[] getCallTypes(Cursor cursor, int count) { 778 if (mActivityType == ACTIVITY_TYPE_ARCHIVE) { 779 return new int[] {CallLog.Calls.VOICEMAIL_TYPE}; 780 } 781 int position = cursor.getPosition(); 782 int[] callTypes = new int[count]; 783 for (int index = 0; index < count; ++index) { 784 callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE); 785 cursor.moveToNext(); 786 } 787 cursor.moveToPosition(position); 788 return callTypes; 789 } 790 791 /** 792 * Determine the features which were enabled for any of the calls that make up a call log 793 * entry. 794 * 795 * @param cursor The cursor. 796 * @param count The number of calls for the current call log entry. 797 * @return The features. 798 */ getCallFeatures(Cursor cursor, int count)799 private int getCallFeatures(Cursor cursor, int count) { 800 int features = 0; 801 int position = cursor.getPosition(); 802 for (int index = 0; index < count; ++index) { 803 features |= cursor.getInt(CallLogQuery.FEATURES); 804 cursor.moveToNext(); 805 } 806 cursor.moveToPosition(position); 807 return features; 808 } 809 810 /** 811 * Sets whether processing of requests for contact details should be enabled. 812 * 813 * This method should be called in tests to disable such processing of requests when not 814 * needed. 815 */ 816 @VisibleForTesting disableRequestProcessingForTest()817 void disableRequestProcessingForTest() { 818 // TODO: Remove this and test the cache directly. 819 mContactInfoCache.disableRequestProcessing(); 820 } 821 822 @VisibleForTesting injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo)823 void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) { 824 // TODO: Remove this and test the cache directly. 825 mContactInfoCache.injectContactInfoForTest(number, countryIso, contactInfo); 826 } 827 828 /** 829 * Stores the day group associated with a call in the call log. 830 * 831 * @param rowId The row Id of the current call. 832 * @param dayGroup The day group the call belongs in. 833 */ 834 @Override setDayGroup(long rowId, int dayGroup)835 public void setDayGroup(long rowId, int dayGroup) { 836 if (!mDayGroups.containsKey(rowId)) { 837 mDayGroups.put(rowId, dayGroup); 838 } 839 } 840 841 /** 842 * Clears the day group associations on re-bind of the call log. 843 */ 844 @Override clearDayGroups()845 public void clearDayGroups() { 846 mDayGroups.clear(); 847 } 848 849 /** 850 * Retrieves the call Ids represented by the current call log row. 851 * 852 * @param cursor Call log cursor to retrieve call Ids from. 853 * @param groupSize Number of calls associated with the current call log row. 854 * @return Array of call Ids. 855 */ getCallIds(final Cursor cursor, final int groupSize)856 private long[] getCallIds(final Cursor cursor, final int groupSize) { 857 // We want to restore the position in the cursor at the end. 858 int startingPosition = cursor.getPosition(); 859 long[] ids = new long[groupSize]; 860 // Copy the ids of the rows in the group. 861 for (int index = 0; index < groupSize; ++index) { 862 ids[index] = cursor.getLong(CallLogQuery.ID); 863 cursor.moveToNext(); 864 } 865 cursor.moveToPosition(startingPosition); 866 return ids; 867 } 868 869 /** 870 * Determines the description for a day group. 871 * 872 * @param group The day group to retrieve the description for. 873 * @return The day group description. 874 */ getGroupDescription(int group)875 private CharSequence getGroupDescription(int group) { 876 if (group == CallLogGroupBuilder.DAY_GROUP_TODAY) { 877 return mContext.getResources().getString(R.string.call_log_header_today); 878 } else if (group == CallLogGroupBuilder.DAY_GROUP_YESTERDAY) { 879 return mContext.getResources().getString(R.string.call_log_header_yesterday); 880 } else { 881 return mContext.getResources().getString(R.string.call_log_header_other); 882 } 883 } 884 885 /** 886 * Determines if the voicemail promo card should be shown or not. The voicemail promo card will 887 * be shown as the first item in the voicemail tab. 888 */ maybeShowVoicemailPromoCard()889 private void maybeShowVoicemailPromoCard() { 890 boolean showPromoCard = mPrefs.getBoolean(SHOW_VOICEMAIL_PROMO_CARD, 891 SHOW_VOICEMAIL_PROMO_CARD_DEFAULT); 892 mShowVoicemailPromoCard = mActivityType != ACTIVITY_TYPE_ARCHIVE && 893 (mVoicemailPlaybackPresenter != null) && showPromoCard; 894 } 895 896 /** 897 * Dismisses the voicemail promo card and refreshes the call log. 898 */ dismissVoicemailPromoCard()899 private void dismissVoicemailPromoCard() { 900 mPrefs.edit().putBoolean(SHOW_VOICEMAIL_PROMO_CARD, false).apply(); 901 mShowVoicemailPromoCard = false; 902 notifyItemRemoved(VOICEMAIL_PROMO_CARD_POSITION); 903 } 904 905 /** 906 * Creates the view holder for the voicemail promo card. 907 * 908 * @param parent The parent view. 909 * @return The {@link ViewHolder}. 910 */ createVoicemailPromoCardViewHolder(ViewGroup parent)911 protected ViewHolder createVoicemailPromoCardViewHolder(ViewGroup parent) { 912 LayoutInflater inflater = LayoutInflater.from(mContext); 913 View view = inflater.inflate(R.layout.voicemail_promo_card, parent, false); 914 915 PromoCardViewHolder viewHolder = PromoCardViewHolder.create(view); 916 return viewHolder; 917 } 918 } 919