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 android.content.Context; 20 import android.content.Intent; 21 import android.content.SharedPreferences; 22 import android.content.res.Resources; 23 import android.database.Cursor; 24 import android.net.Uri; 25 import android.support.v7.widget.RecyclerView; 26 import android.os.Bundle; 27 import android.os.Trace; 28 import android.preference.PreferenceActivity; 29 import android.preference.PreferenceManager; 30 import android.provider.CallLog; 31 import android.support.v7.widget.RecyclerView.ViewHolder; 32 import android.telecom.PhoneAccountHandle; 33 import android.telephony.PhoneNumberUtils; 34 import android.telephony.TelephonyManager; 35 import android.text.TextUtils; 36 import android.util.Log; 37 import android.view.ContextMenu; 38 import android.view.LayoutInflater; 39 import android.view.MenuItem; 40 import android.view.MenuItem.OnMenuItemClickListener; 41 import android.view.View; 42 import android.view.View.AccessibilityDelegate; 43 import android.view.ViewGroup; 44 import android.view.ViewTreeObserver; 45 import android.view.ContextMenu.ContextMenuInfo; 46 import android.view.accessibility.AccessibilityEvent; 47 48 import com.android.contacts.common.CallUtil; 49 import com.android.contacts.common.ClipboardUtils; 50 import com.android.contacts.common.util.PermissionsUtil; 51 import com.android.dialer.DialtactsActivity; 52 import com.android.dialer.PhoneCallDetails; 53 import com.android.dialer.R; 54 import com.android.dialer.contactinfo.ContactInfoCache; 55 import com.android.dialer.contactinfo.ContactInfoCache.OnContactInfoChangedListener; 56 import com.android.dialer.util.DialerUtils; 57 import com.android.dialer.util.PhoneNumberUtil; 58 import com.android.dialer.voicemail.VoicemailPlaybackPresenter; 59 60 import com.google.common.annotations.VisibleForTesting; 61 62 import java.util.HashMap; 63 64 /** 65 * Adapter class to fill in data for the Call Log. 66 */ 67 public class CallLogAdapter extends GroupingListAdapter 68 implements CallLogGroupBuilder.GroupCreator, 69 VoicemailPlaybackPresenter.OnVoicemailDeletedListener { 70 71 /** Interface used to initiate a refresh of the content. */ 72 public interface CallFetcher { 73 public void fetchCalls(); 74 } 75 76 private static final int VIEW_TYPE_SHOW_CALL_HISTORY_LIST_ITEM = 10; 77 private static final int NO_EXPANDED_LIST_ITEM = -1; 78 79 private static final int VOICEMAIL_PROMO_CARD_POSITION = 0; 80 /** 81 * View type for voicemail promo card. Note: Numbering starts at 20 to avoid collision 82 * with {@link com.android.common.widget.GroupingListAdapter#ITEM_TYPE_IN_GROUP}, and 83 * {@link CallLogAdapter#VIEW_TYPE_SHOW_CALL_HISTORY_LIST_ITEM}. 84 */ 85 private static final int VIEW_TYPE_VOICEMAIL_PROMO_CARD = 20; 86 87 /** 88 * The key for the show voicemail promo card preference which will determine whether the promo 89 * card was permanently dismissed or not. 90 */ 91 private static final String SHOW_VOICEMAIL_PROMO_CARD = "show_voicemail_promo_card"; 92 private static final boolean SHOW_VOICEMAIL_PROMO_CARD_DEFAULT = true; 93 94 protected final Context mContext; 95 private final ContactInfoHelper mContactInfoHelper; 96 private final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter; 97 private final CallFetcher mCallFetcher; 98 99 protected ContactInfoCache mContactInfoCache; 100 101 private boolean mIsShowingRecentsTab; 102 103 private static final String KEY_EXPANDED_POSITION = "expanded_position"; 104 private static final String KEY_EXPANDED_ROW_ID = "expanded_row_id"; 105 106 // Tracks the position of the currently expanded list item. 107 private int mCurrentlyExpandedPosition = RecyclerView.NO_POSITION; 108 // Tracks the rowId of the currently expanded list item, so the position can be updated if there 109 // are any changes to the call log entries, such as additions or removals. 110 private long mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM; 111 112 /** 113 * Hashmap, keyed by call Id, used to track the day group for a call. As call log entries are 114 * put into the primary call groups in {@link com.android.dialer.calllog.CallLogGroupBuilder}, 115 * they are also assigned a secondary "day group". This hashmap tracks the day group assigned 116 * to all calls in the call log. This information is used to trigger the display of a day 117 * group header above the call log entry at the start of a day group. 118 * Note: Multiple calls are grouped into a single primary "call group" in the call log, and 119 * the cursor used to bind rows includes all of these calls. When determining if a day group 120 * change has occurred it is necessary to look at the last entry in the call log to determine 121 * its day group. This hashmap provides a means of determining the previous day group without 122 * having to reverse the cursor to the start of the previous day call log entry. 123 */ 124 private HashMap<Long,Integer> mDayGroups = new HashMap<Long, Integer>(); 125 126 private boolean mLoading = true; 127 128 private SharedPreferences mPrefs; 129 130 private boolean mShowPromoCard = false; 131 132 /** Instance of helper class for managing views. */ 133 private final CallLogListItemHelper mCallLogListItemHelper; 134 135 /** Cache for repeated requests to TelecomManager. */ 136 protected final TelecomCallLogCache mTelecomCallLogCache; 137 138 /** Helper to group call log entries. */ 139 private final CallLogGroupBuilder mCallLogGroupBuilder; 140 141 /** 142 * The OnClickListener used to expand or collapse the action buttons of a call log entry. 143 */ 144 private final View.OnClickListener mExpandCollapseListener = new View.OnClickListener() { 145 @Override 146 public void onClick(View v) { 147 CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) v.getTag(); 148 if (viewHolder == null) { 149 return; 150 } 151 152 if (mVoicemailPlaybackPresenter != null) { 153 // Always reset the voicemail playback state on expand or collapse. 154 mVoicemailPlaybackPresenter.resetAll(); 155 } 156 157 if (viewHolder.getAdapterPosition() == mCurrentlyExpandedPosition) { 158 // Hide actions, if the clicked item is the expanded item. 159 viewHolder.showActions(false); 160 161 mCurrentlyExpandedPosition = RecyclerView.NO_POSITION; 162 mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM; 163 } else { 164 expandViewHolderActions(viewHolder); 165 } 166 167 } 168 }; 169 170 /** 171 * Click handler used to dismiss the promo card when the user taps the "ok" button. 172 */ 173 private final View.OnClickListener mOkActionListener = new View.OnClickListener() { 174 @Override 175 public void onClick(View view) { 176 dismissVoicemailPromoCard(); 177 } 178 }; 179 180 /** 181 * Click handler used to send the user to the voicemail settings screen and then dismiss the 182 * promo card. 183 */ 184 private final View.OnClickListener mVoicemailSettingsActionListener = 185 new View.OnClickListener() { 186 @Override 187 public void onClick(View view) { 188 Intent intent = new Intent(TelephonyManager.ACTION_CONFIGURE_VOICEMAIL); 189 mContext.startActivity(intent); 190 dismissVoicemailPromoCard(); 191 } 192 }; 193 194 /** 195 * Listener that is triggered to populate the context menu with actions to perform on the call's 196 * number, when the call log entry is long pressed. 197 */ 198 private final View.OnCreateContextMenuListener mOnCreateContextMenuListener = 199 new View.OnCreateContextMenuListener() { 200 @Override 201 public void onCreateContextMenu(ContextMenu menu, View v, 202 ContextMenuInfo menuInfo) { 203 final CallLogListItemViewHolder vh = 204 (CallLogListItemViewHolder) v.getTag(); 205 if (TextUtils.isEmpty(vh.number)) { 206 return; 207 } 208 209 menu.setHeaderTitle(vh.number); 210 211 final MenuItem copyItem = menu.add( 212 ContextMenu.NONE, 213 R.id.context_menu_copy_to_clipboard, 214 ContextMenu.NONE, 215 R.string.copy_text); 216 217 copyItem.setOnMenuItemClickListener(new OnMenuItemClickListener() { 218 @Override 219 public boolean onMenuItemClick(MenuItem item) { 220 ClipboardUtils.copyText(CallLogAdapter.this.mContext, null, 221 vh.number, true); 222 return true; 223 } 224 }); 225 226 // The edit number before call does not show up if any of the conditions apply: 227 // 1) Number cannot be called 228 // 2) Number is the voicemail number 229 // 3) Number is a SIP address 230 231 if (!PhoneNumberUtil.canPlaceCallsTo(vh.number, vh.numberPresentation) 232 || mTelecomCallLogCache.isVoicemailNumber(vh.accountHandle, vh.number) 233 || PhoneNumberUtil.isSipNumber(vh.number)) { 234 return; 235 } 236 237 final MenuItem editItem = menu.add( 238 ContextMenu.NONE, 239 R.id.context_menu_edit_before_call, 240 ContextMenu.NONE, 241 R.string.recentCalls_editNumberBeforeCall); 242 243 editItem.setOnMenuItemClickListener(new OnMenuItemClickListener() { 244 @Override 245 public boolean onMenuItemClick(MenuItem item) { 246 final Intent intent = new Intent(Intent.ACTION_DIAL, 247 CallUtil.getCallUri(vh.number)); 248 intent.setClass(mContext, DialtactsActivity.class); 249 DialerUtils.startActivityWithErrorToast(mContext, intent); 250 return true; 251 } 252 }); 253 } 254 }; 255 256 private void expandViewHolderActions(CallLogListItemViewHolder viewHolder) { 257 // If another item is expanded, notify it that it has changed. Its actions will be 258 // hidden when it is re-binded because we change mCurrentlyExpandedPosition below. 259 if (mCurrentlyExpandedPosition != RecyclerView.NO_POSITION) { 260 notifyItemChanged(mCurrentlyExpandedPosition); 261 } 262 // Show the actions for the clicked list item. 263 viewHolder.showActions(true); 264 mCurrentlyExpandedPosition = viewHolder.getAdapterPosition(); 265 mCurrentlyExpandedRowId = viewHolder.rowId; 266 } 267 268 /** 269 * Expand the actions on a list item when focused in Talkback mode, to aid discoverability. 270 */ 271 private AccessibilityDelegate mAccessibilityDelegate = new AccessibilityDelegate() { 272 @Override 273 public boolean onRequestSendAccessibilityEvent( 274 ViewGroup host, View child, AccessibilityEvent event) { 275 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) { 276 // Only expand if actions are not already expanded, because triggering the expand 277 // function on clicks causes the action views to lose the focus indicator. 278 CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) host.getTag(); 279 if (mCurrentlyExpandedPosition != viewHolder.getAdapterPosition()) { 280 expandViewHolderActions((CallLogListItemViewHolder) host.getTag()); 281 } 282 } 283 return super.onRequestSendAccessibilityEvent(host, child, event); 284 } 285 }; 286 287 protected final OnContactInfoChangedListener mOnContactInfoChangedListener = 288 new OnContactInfoChangedListener() { 289 @Override 290 public void onContactInfoChanged() { 291 notifyDataSetChanged(); 292 } 293 }; 294 295 public CallLogAdapter( 296 Context context, 297 CallFetcher callFetcher, 298 ContactInfoHelper contactInfoHelper, 299 VoicemailPlaybackPresenter voicemailPlaybackPresenter, 300 boolean isShowingRecentsTab) { 301 super(context); 302 303 mContext = context; 304 mCallFetcher = callFetcher; 305 mContactInfoHelper = contactInfoHelper; 306 mVoicemailPlaybackPresenter = voicemailPlaybackPresenter; 307 if (mVoicemailPlaybackPresenter != null) { 308 mVoicemailPlaybackPresenter.setOnVoicemailDeletedListener(this); 309 } 310 mIsShowingRecentsTab = isShowingRecentsTab; 311 312 mContactInfoCache = new ContactInfoCache( 313 mContactInfoHelper, mOnContactInfoChangedListener); 314 if (!PermissionsUtil.hasContactsPermissions(context)) { 315 mContactInfoCache.disableRequestProcessing(); 316 } 317 318 Resources resources = mContext.getResources(); 319 CallTypeHelper callTypeHelper = new CallTypeHelper(resources); 320 321 mTelecomCallLogCache = new TelecomCallLogCache(mContext); 322 PhoneCallDetailsHelper phoneCallDetailsHelper = 323 new PhoneCallDetailsHelper(mContext, resources, mTelecomCallLogCache); 324 mCallLogListItemHelper = 325 new CallLogListItemHelper(phoneCallDetailsHelper, resources, mTelecomCallLogCache); 326 mCallLogGroupBuilder = new CallLogGroupBuilder(this); 327 mPrefs = PreferenceManager.getDefaultSharedPreferences(context); 328 maybeShowVoicemailPromoCard(); 329 } 330 331 public void onSaveInstanceState(Bundle outState) { 332 outState.putInt(KEY_EXPANDED_POSITION, mCurrentlyExpandedPosition); 333 outState.putLong(KEY_EXPANDED_ROW_ID, mCurrentlyExpandedRowId); 334 } 335 336 public void onRestoreInstanceState(Bundle savedInstanceState) { 337 if (savedInstanceState != null) { 338 mCurrentlyExpandedPosition = 339 savedInstanceState.getInt(KEY_EXPANDED_POSITION, RecyclerView.NO_POSITION); 340 mCurrentlyExpandedRowId = 341 savedInstanceState.getLong(KEY_EXPANDED_ROW_ID, NO_EXPANDED_LIST_ITEM); 342 } 343 } 344 345 /** 346 * Requery on background thread when {@link Cursor} changes. 347 */ 348 @Override 349 protected void onContentChanged() { 350 mCallFetcher.fetchCalls(); 351 } 352 353 public void setLoading(boolean loading) { 354 mLoading = loading; 355 } 356 357 public boolean isEmpty() { 358 if (mLoading) { 359 // We don't want the empty state to show when loading. 360 return false; 361 } else { 362 return getItemCount() == 0; 363 } 364 } 365 366 public void invalidateCache() { 367 mContactInfoCache.invalidate(); 368 } 369 370 public void startCache() { 371 if (PermissionsUtil.hasPermission(mContext, android.Manifest.permission.READ_CONTACTS)) { 372 mContactInfoCache.start(); 373 } 374 } 375 376 public void pauseCache() { 377 mContactInfoCache.stop(); 378 mTelecomCallLogCache.reset(); 379 } 380 381 @Override 382 protected void addGroups(Cursor cursor) { 383 mCallLogGroupBuilder.addGroups(cursor); 384 } 385 386 @Override 387 public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 388 if (viewType == VIEW_TYPE_SHOW_CALL_HISTORY_LIST_ITEM) { 389 return ShowCallHistoryViewHolder.create(mContext, parent); 390 } else if (viewType == VIEW_TYPE_VOICEMAIL_PROMO_CARD) { 391 return createVoicemailPromoCardViewHolder(parent); 392 } 393 return createCallLogEntryViewHolder(parent); 394 } 395 396 /** 397 * Creates a new call log entry {@link ViewHolder}. 398 * 399 * @param parent the parent view. 400 * @return The {@link ViewHolder}. 401 */ 402 private ViewHolder createCallLogEntryViewHolder(ViewGroup parent) { 403 LayoutInflater inflater = LayoutInflater.from(mContext); 404 View view = inflater.inflate(R.layout.call_log_list_item, parent, false); 405 CallLogListItemViewHolder viewHolder = CallLogListItemViewHolder.create( 406 view, 407 mContext, 408 mExpandCollapseListener, 409 mTelecomCallLogCache, 410 mCallLogListItemHelper, 411 mVoicemailPlaybackPresenter); 412 413 viewHolder.callLogEntryView.setTag(viewHolder); 414 viewHolder.callLogEntryView.setAccessibilityDelegate(mAccessibilityDelegate); 415 416 viewHolder.primaryActionView.setOnCreateContextMenuListener(mOnCreateContextMenuListener); 417 viewHolder.primaryActionView.setTag(viewHolder); 418 419 return viewHolder; 420 } 421 422 /** 423 * Binds the views in the entry to the data in the call log. 424 * TODO: This gets called 20-30 times when Dialer starts up for a single call log entry and 425 * should not. It invokes cross-process methods and the repeat execution can get costly. 426 * 427 * @param ViewHolder The view corresponding to this entry. 428 * @param position The position of the entry. 429 */ 430 public void onBindViewHolder(ViewHolder viewHolder, int position) { 431 Trace.beginSection("onBindViewHolder: " + position); 432 433 switch (getItemViewType(position)) { 434 case VIEW_TYPE_SHOW_CALL_HISTORY_LIST_ITEM: 435 break; 436 case VIEW_TYPE_VOICEMAIL_PROMO_CARD: 437 bindVoicemailPromoCardViewHolder(viewHolder); 438 break; 439 default: 440 bindCallLogListViewHolder(viewHolder, position); 441 break; 442 } 443 444 Trace.endSection(); 445 } 446 447 /** 448 * Binds the promo card view holder. 449 * 450 * @param viewHolder The promo card view holder. 451 */ 452 protected void bindVoicemailPromoCardViewHolder(ViewHolder viewHolder) { 453 PromoCardViewHolder promoCardViewHolder = (PromoCardViewHolder) viewHolder; 454 455 promoCardViewHolder.getSettingsTextView().setOnClickListener( 456 mVoicemailSettingsActionListener); 457 promoCardViewHolder.getOkTextView().setOnClickListener(mOkActionListener); 458 } 459 460 /** 461 * Binds the view holder for the call log list item view. 462 * 463 * @param viewHolder The call log list item view holder. 464 * @param position The position of the list item. 465 */ 466 467 private void bindCallLogListViewHolder(ViewHolder viewHolder, int position) { 468 Cursor c = (Cursor) getItem(position); 469 if (c == null) { 470 return; 471 } 472 473 int count = getGroupSize(position); 474 475 final String number = c.getString(CallLogQuery.NUMBER); 476 final int numberPresentation = c.getInt(CallLogQuery.NUMBER_PRESENTATION); 477 final PhoneAccountHandle accountHandle = PhoneAccountUtils.getAccount( 478 c.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME), 479 c.getString(CallLogQuery.ACCOUNT_ID)); 480 final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO); 481 final ContactInfo cachedContactInfo = mContactInfoHelper.getContactInfo(c); 482 final boolean isVoicemailNumber = 483 mTelecomCallLogCache.isVoicemailNumber(accountHandle, number); 484 485 // Note: Binding of the action buttons is done as required in configureActionViews when the 486 // user expands the actions ViewStub. 487 488 ContactInfo info = ContactInfo.EMPTY; 489 if (PhoneNumberUtil.canPlaceCallsTo(number, numberPresentation) && !isVoicemailNumber) { 490 // Lookup contacts with this number 491 info = mContactInfoCache.getValue(number, countryIso, cachedContactInfo); 492 } 493 CharSequence formattedNumber = info.formattedNumber == null 494 ? null : PhoneNumberUtils.createTtsSpannable(info.formattedNumber); 495 496 final PhoneCallDetails details = new PhoneCallDetails( 497 mContext, number, numberPresentation, formattedNumber, isVoicemailNumber); 498 details.accountHandle = accountHandle; 499 details.callTypes = getCallTypes(c, count); 500 details.countryIso = countryIso; 501 details.date = c.getLong(CallLogQuery.DATE); 502 details.duration = c.getLong(CallLogQuery.DURATION); 503 details.features = getCallFeatures(c, count); 504 details.geocode = c.getString(CallLogQuery.GEOCODED_LOCATION); 505 details.transcription = c.getString(CallLogQuery.TRANSCRIPTION); 506 if (details.callTypes[0] == CallLog.Calls.VOICEMAIL_TYPE) { 507 details.isRead = c.getInt(CallLogQuery.IS_READ) == 1; 508 } 509 510 if (!c.isNull(CallLogQuery.DATA_USAGE)) { 511 details.dataUsage = c.getLong(CallLogQuery.DATA_USAGE); 512 } 513 514 if (!TextUtils.isEmpty(info.name)) { 515 details.contactUri = info.lookupUri; 516 details.name = info.name; 517 details.numberType = info.type; 518 details.numberLabel = info.label; 519 details.photoUri = info.photoUri; 520 details.sourceType = info.sourceType; 521 details.objectId = info.objectId; 522 } 523 524 CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder; 525 views.info = info; 526 views.rowId = c.getLong(CallLogQuery.ID); 527 // Store values used when the actions ViewStub is inflated on expansion. 528 views.number = number; 529 views.numberPresentation = numberPresentation; 530 views.callType = c.getInt(CallLogQuery.CALL_TYPE); 531 views.accountHandle = accountHandle; 532 views.voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI); 533 // Stash away the Ids of the calls so that we can support deleting a row in the call log. 534 views.callIds = getCallIds(c, count); 535 536 // Default case: an item in the call log. 537 views.primaryActionView.setVisibility(View.VISIBLE); 538 539 // Check if the day group has changed and display a header if necessary. 540 int currentGroup = getDayGroupForCall(views.rowId); 541 int previousGroup = getPreviousDayGroup(c); 542 if (currentGroup != previousGroup) { 543 views.dayGroupHeader.setVisibility(View.VISIBLE); 544 views.dayGroupHeader.setText(getGroupDescription(currentGroup)); 545 } else { 546 views.dayGroupHeader.setVisibility(View.GONE); 547 } 548 549 mCallLogListItemHelper.setPhoneCallDetails(views, details); 550 551 if (mCurrentlyExpandedRowId == views.rowId) { 552 // In case ViewHolders were added/removed, update the expanded position if the rowIds 553 // match so that we can restore the correct expanded state on rebind. 554 mCurrentlyExpandedPosition = position; 555 } 556 557 views.showActions(mCurrentlyExpandedPosition == position); 558 559 String nameForDefaultImage = null; 560 if (TextUtils.isEmpty(info.name)) { 561 nameForDefaultImage = details.displayNumber; 562 } else { 563 nameForDefaultImage = info.name; 564 } 565 views.setPhoto(info.photoId, info.photoUri, info.lookupUri, nameForDefaultImage, 566 isVoicemailNumber, mContactInfoHelper.isBusiness(info.sourceType)); 567 568 mCallLogListItemHelper.setPhoneCallDetails(views, details); 569 } 570 571 @Override 572 public int getItemCount() { 573 return super.getItemCount() + ((isShowingRecentsTab() || mShowPromoCard) ? 1 : 0); 574 } 575 576 @Override 577 public int getItemViewType(int position) { 578 if (position == getItemCount() - 1 && isShowingRecentsTab()) { 579 return VIEW_TYPE_SHOW_CALL_HISTORY_LIST_ITEM; 580 } else if (position == VOICEMAIL_PROMO_CARD_POSITION && mShowPromoCard) { 581 return VIEW_TYPE_VOICEMAIL_PROMO_CARD; 582 } 583 return super.getItemViewType(position); 584 } 585 586 /** 587 * Retrieves an item at the specified position, taking into account the presence of a promo 588 * card. 589 * 590 * @param position The position to retrieve. 591 * @return The item at that position. 592 */ 593 @Override 594 public Object getItem(int position) { 595 return super.getItem(position - (mShowPromoCard ? 1 : 0)); 596 } 597 598 protected boolean isShowingRecentsTab() { 599 return mIsShowingRecentsTab; 600 } 601 602 @Override 603 public void onVoicemailDeleted(Uri uri) { 604 mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM; 605 mCurrentlyExpandedPosition = RecyclerView.NO_POSITION; 606 } 607 608 /** 609 * Retrieves the day group of the previous call in the call log. Used to determine if the day 610 * group has changed and to trigger display of the day group text. 611 * 612 * @param cursor The call log cursor. 613 * @return The previous day group, or DAY_GROUP_NONE if this is the first call. 614 */ 615 private int getPreviousDayGroup(Cursor cursor) { 616 // We want to restore the position in the cursor at the end. 617 int startingPosition = cursor.getPosition(); 618 int dayGroup = CallLogGroupBuilder.DAY_GROUP_NONE; 619 if (cursor.moveToPrevious()) { 620 long previousRowId = cursor.getLong(CallLogQuery.ID); 621 dayGroup = getDayGroupForCall(previousRowId); 622 } 623 cursor.moveToPosition(startingPosition); 624 return dayGroup; 625 } 626 627 /** 628 * Given a call Id, look up the day group that the call belongs to. The day group data is 629 * populated in {@link com.android.dialer.calllog.CallLogGroupBuilder}. 630 * 631 * @param callId The call to retrieve the day group for. 632 * @return The day group for the call. 633 */ 634 private int getDayGroupForCall(long callId) { 635 if (mDayGroups.containsKey(callId)) { 636 return mDayGroups.get(callId); 637 } 638 return CallLogGroupBuilder.DAY_GROUP_NONE; 639 } 640 641 /** 642 * Returns the call types for the given number of items in the cursor. 643 * <p> 644 * It uses the next {@code count} rows in the cursor to extract the types. 645 * <p> 646 * It position in the cursor is unchanged by this function. 647 */ 648 private int[] getCallTypes(Cursor cursor, int count) { 649 int position = cursor.getPosition(); 650 int[] callTypes = new int[count]; 651 for (int index = 0; index < count; ++index) { 652 callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE); 653 cursor.moveToNext(); 654 } 655 cursor.moveToPosition(position); 656 return callTypes; 657 } 658 659 /** 660 * Determine the features which were enabled for any of the calls that make up a call log 661 * entry. 662 * 663 * @param cursor The cursor. 664 * @param count The number of calls for the current call log entry. 665 * @return The features. 666 */ 667 private int getCallFeatures(Cursor cursor, int count) { 668 int features = 0; 669 int position = cursor.getPosition(); 670 for (int index = 0; index < count; ++index) { 671 features |= cursor.getInt(CallLogQuery.FEATURES); 672 cursor.moveToNext(); 673 } 674 cursor.moveToPosition(position); 675 return features; 676 } 677 678 /** 679 * Sets whether processing of requests for contact details should be enabled. 680 * 681 * This method should be called in tests to disable such processing of requests when not 682 * needed. 683 */ 684 @VisibleForTesting 685 void disableRequestProcessingForTest() { 686 // TODO: Remove this and test the cache directly. 687 mContactInfoCache.disableRequestProcessing(); 688 } 689 690 @VisibleForTesting 691 void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) { 692 // TODO: Remove this and test the cache directly. 693 mContactInfoCache.injectContactInfoForTest(number, countryIso, contactInfo); 694 } 695 696 @Override 697 public void addGroup(int cursorPosition, int size, boolean expanded) { 698 super.addGroup(cursorPosition, size, expanded); 699 } 700 701 /** 702 * Stores the day group associated with a call in the call log. 703 * 704 * @param rowId The row Id of the current call. 705 * @param dayGroup The day group the call belongs in. 706 */ 707 @Override 708 public void setDayGroup(long rowId, int dayGroup) { 709 if (!mDayGroups.containsKey(rowId)) { 710 mDayGroups.put(rowId, dayGroup); 711 } 712 } 713 714 /** 715 * Clears the day group associations on re-bind of the call log. 716 */ 717 @Override 718 public void clearDayGroups() { 719 mDayGroups.clear(); 720 } 721 722 /** 723 * Retrieves the call Ids represented by the current call log row. 724 * 725 * @param cursor Call log cursor to retrieve call Ids from. 726 * @param groupSize Number of calls associated with the current call log row. 727 * @return Array of call Ids. 728 */ 729 private long[] getCallIds(final Cursor cursor, final int groupSize) { 730 // We want to restore the position in the cursor at the end. 731 int startingPosition = cursor.getPosition(); 732 long[] ids = new long[groupSize]; 733 // Copy the ids of the rows in the group. 734 for (int index = 0; index < groupSize; ++index) { 735 ids[index] = cursor.getLong(CallLogQuery.ID); 736 cursor.moveToNext(); 737 } 738 cursor.moveToPosition(startingPosition); 739 return ids; 740 } 741 742 /** 743 * Determines the description for a day group. 744 * 745 * @param group The day group to retrieve the description for. 746 * @return The day group description. 747 */ 748 private CharSequence getGroupDescription(int group) { 749 if (group == CallLogGroupBuilder.DAY_GROUP_TODAY) { 750 return mContext.getResources().getString(R.string.call_log_header_today); 751 } else if (group == CallLogGroupBuilder.DAY_GROUP_YESTERDAY) { 752 return mContext.getResources().getString(R.string.call_log_header_yesterday); 753 } else { 754 return mContext.getResources().getString(R.string.call_log_header_other); 755 } 756 } 757 758 /** 759 * Determines if the voicemail promo card should be shown or not. The voicemail promo card will 760 * be shown as the first item in the voicemail tab. 761 */ 762 private void maybeShowVoicemailPromoCard() { 763 boolean showPromoCard = mPrefs.getBoolean(SHOW_VOICEMAIL_PROMO_CARD, 764 SHOW_VOICEMAIL_PROMO_CARD_DEFAULT); 765 mShowPromoCard = (mVoicemailPlaybackPresenter != null) && showPromoCard; 766 } 767 768 /** 769 * Dismisses the voicemail promo card and refreshes the call log. 770 */ 771 private void dismissVoicemailPromoCard() { 772 mPrefs.edit().putBoolean(SHOW_VOICEMAIL_PROMO_CARD, false).apply(); 773 mShowPromoCard = false; 774 notifyItemRemoved(VOICEMAIL_PROMO_CARD_POSITION); 775 } 776 777 /** 778 * Creates the view holder for the voicemail promo card. 779 * 780 * @param parent The parent view. 781 * @return The {@link ViewHolder}. 782 */ 783 protected ViewHolder createVoicemailPromoCardViewHolder(ViewGroup parent) { 784 LayoutInflater inflater = LayoutInflater.from(mContext); 785 View view = inflater.inflate(R.layout.voicemail_promo_card, parent, false); 786 787 PromoCardViewHolder viewHolder = PromoCardViewHolder.create(view); 788 return viewHolder; 789 } 790 } 791