blob: 9cde0b65d7af12712a22f703473c03fbb592a0e7 [file] [log] [blame]
/*
* Copyright (C) 2011 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.dialer.calllog;
import com.android.dialer.filterednumber.BlockNumberDialogFragment;
import com.android.dialer.service.ExtendedCallInfoService;
import com.android.dialerbind.ObjectFactory;
import com.google.common.annotations.VisibleForTesting;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.Trace;
import android.preference.PreferenceManager;
import android.provider.CallLog;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.telecom.PhoneAccountHandle;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.AccessibilityDelegate;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import com.android.contacts.common.ContactsUtils;
import com.android.contacts.common.compat.CompatUtils;
import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
import com.android.contacts.common.preference.ContactsPreferences;
import com.android.contacts.common.util.PermissionsUtil;
import com.android.dialer.DialtactsActivity;
import com.android.dialer.PhoneCallDetails;
import com.android.dialer.R;
import com.android.dialer.calllog.calllogcache.CallLogCache;
import com.android.dialer.contactinfo.ContactInfoCache;
import com.android.dialer.contactinfo.ContactInfoCache.OnContactInfoChangedListener;
import com.android.dialer.database.FilteredNumberAsyncQueryHandler;
import com.android.dialer.database.VoicemailArchiveContract;
import com.android.dialer.logging.InteractionEvent;
import com.android.dialer.logging.Logger;
import com.android.dialer.util.PhoneNumberUtil;
import com.android.dialer.voicemail.VoicemailPlaybackPresenter;
import java.util.HashMap;
/**
* Adapter class to fill in data for the Call Log.
*/
public class CallLogAdapter extends GroupingListAdapter
implements CallLogGroupBuilder.GroupCreator,
VoicemailPlaybackPresenter.OnVoicemailDeletedListener {
// Types of activities the call log adapter is used for
public static final int ACTIVITY_TYPE_CALL_LOG = 1;
public static final int ACTIVITY_TYPE_ARCHIVE = 2;
public static final int ACTIVITY_TYPE_DIALTACTS = 3;
/** Interface used to initiate a refresh of the content. */
public interface CallFetcher {
public void fetchCalls();
}
private static final int NO_EXPANDED_LIST_ITEM = -1;
// ConcurrentHashMap doesn't store null values. Use this value for numbers which aren't blocked.
private static final int NOT_BLOCKED = -1;
private static final int VOICEMAIL_PROMO_CARD_POSITION = 0;
protected static final int VIEW_TYPE_NORMAL = 0;
private static final int VIEW_TYPE_VOICEMAIL_PROMO_CARD = 1;
/**
* The key for the show voicemail promo card preference which will determine whether the promo
* card was permanently dismissed or not.
*/
private static final String SHOW_VOICEMAIL_PROMO_CARD = "show_voicemail_promo_card";
private static final boolean SHOW_VOICEMAIL_PROMO_CARD_DEFAULT = true;
protected final Context mContext;
private final ContactInfoHelper mContactInfoHelper;
protected final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
private final CallFetcher mCallFetcher;
private final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler;
protected ContactInfoCache mContactInfoCache;
private final int mActivityType;
private static final String KEY_EXPANDED_POSITION = "expanded_position";
private static final String KEY_EXPANDED_ROW_ID = "expanded_row_id";
// Tracks the position of the currently expanded list item.
private int mCurrentlyExpandedPosition = RecyclerView.NO_POSITION;
// Tracks the rowId of the currently expanded list item, so the position can be updated if there
// are any changes to the call log entries, such as additions or removals.
private long mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
private int mHiddenPosition = RecyclerView.NO_POSITION;
private Uri mHiddenItemUri = null;
private boolean mPendingHide = false;
private BlockNumberDialogFragment.Callback mBlockedNumberDialogCallback =
new BlockNumberDialogFragment.Callback() {
@Override
public void onFilterNumberSuccess() {
Logger.logInteraction(
InteractionEvent.BLOCK_NUMBER_CALL_LOG);
notifyDataSetChanged();
}
@Override
public void onUnfilterNumberSuccess() {
Logger.logInteraction(
InteractionEvent.UNBLOCK_NUMBER_CALL_LOG);
notifyDataSetChanged();
}
@Override
public void onChangeFilteredNumberUndo() {
}
};
private CallLogListItemViewHolder.OnClickListener mBlockReportSpamListener;
/**
* Hashmap, keyed by call Id, used to track the day group for a call. As call log entries are
* put into the primary call groups in {@link com.android.dialer.calllog.CallLogGroupBuilder},
* they are also assigned a secondary "day group". This hashmap tracks the day group assigned
* to all calls in the call log. This information is used to trigger the display of a day
* group header above the call log entry at the start of a day group.
* Note: Multiple calls are grouped into a single primary "call group" in the call log, and
* the cursor used to bind rows includes all of these calls. When determining if a day group
* change has occurred it is necessary to look at the last entry in the call log to determine
* its day group. This hashmap provides a means of determining the previous day group without
* having to reverse the cursor to the start of the previous day call log entry.
*/
private HashMap<Long, Integer> mDayGroups = new HashMap<>();
private boolean mLoading = true;
private SharedPreferences mPrefs;
private ContactsPreferences mContactsPreferences;
protected boolean mShowVoicemailPromoCard = false;
/** Instance of helper class for managing views. */
private final CallLogListItemHelper mCallLogListItemHelper;
/** Cache for repeated requests to Telecom/Telephony. */
protected final CallLogCache mCallLogCache;
/** Helper to group call log entries. */
private final CallLogGroupBuilder mCallLogGroupBuilder;
private ExtendedCallInfoService mExtendedCallInfoService;
/**
* The OnClickListener used to expand or collapse the action buttons of a call log entry.
*/
private final View.OnClickListener mExpandCollapseListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) v.getTag();
if (viewHolder == null) {
return;
}
if (mVoicemailPlaybackPresenter != null) {
// Always reset the voicemail playback state on expand or collapse.
mVoicemailPlaybackPresenter.resetAll();
}
if (viewHolder.getAdapterPosition() == mCurrentlyExpandedPosition) {
// Hide actions, if the clicked item is the expanded item.
viewHolder.showActions(false);
mCurrentlyExpandedPosition = RecyclerView.NO_POSITION;
mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
} else {
if (viewHolder.callType == CallLog.Calls.MISSED_TYPE) {
CallLogAsyncTaskUtil.markCallAsRead(mContext, viewHolder.callIds);
if (mActivityType == ACTIVITY_TYPE_DIALTACTS) {
((DialtactsActivity) v.getContext()).updateTabUnreadCounts();
}
}
expandViewHolderActions(viewHolder);
}
}
};
/**
* Click handler used to dismiss the promo card when the user taps the "ok" button.
*/
private final View.OnClickListener mOkActionListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
dismissVoicemailPromoCard();
}
};
/**
* Click handler used to send the user to the voicemail settings screen and then dismiss the
* promo card.
*/
private final View.OnClickListener mVoicemailSettingsActionListener =
new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent(TelephonyManager.ACTION_CONFIGURE_VOICEMAIL);
mContext.startActivity(intent);
dismissVoicemailPromoCard();
}
};
private void expandViewHolderActions(CallLogListItemViewHolder viewHolder) {
// If another item is expanded, notify it that it has changed. Its actions will be
// hidden when it is re-binded because we change mCurrentlyExpandedPosition below.
if (mCurrentlyExpandedPosition != RecyclerView.NO_POSITION) {
notifyItemChanged(mCurrentlyExpandedPosition);
}
// Show the actions for the clicked list item.
viewHolder.showActions(true);
mCurrentlyExpandedPosition = viewHolder.getAdapterPosition();
mCurrentlyExpandedRowId = viewHolder.rowId;
}
/**
* Expand the actions on a list item when focused in Talkback mode, to aid discoverability.
*/
private AccessibilityDelegate mAccessibilityDelegate = new AccessibilityDelegate() {
@Override
public boolean onRequestSendAccessibilityEvent(
ViewGroup host, View child, AccessibilityEvent event) {
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
// Only expand if actions are not already expanded, because triggering the expand
// function on clicks causes the action views to lose the focus indicator.
CallLogListItemViewHolder viewHolder = (CallLogListItemViewHolder) host.getTag();
if (mCurrentlyExpandedPosition != viewHolder.getAdapterPosition()) {
if (mVoicemailPlaybackPresenter != null) {
// Always reset the voicemail playback state on expand.
mVoicemailPlaybackPresenter.resetAll();
}
expandViewHolderActions((CallLogListItemViewHolder) host.getTag());
}
}
return super.onRequestSendAccessibilityEvent(host, child, event);
}
};
protected final OnContactInfoChangedListener mOnContactInfoChangedListener =
new OnContactInfoChangedListener() {
@Override
public void onContactInfoChanged() {
notifyDataSetChanged();
}
};
public CallLogAdapter(
Context context,
CallFetcher callFetcher,
ContactInfoHelper contactInfoHelper,
VoicemailPlaybackPresenter voicemailPlaybackPresenter,
int activityType) {
super(context);
mContext = context;
mCallFetcher = callFetcher;
mContactInfoHelper = contactInfoHelper;
mVoicemailPlaybackPresenter = voicemailPlaybackPresenter;
if (mVoicemailPlaybackPresenter != null) {
mVoicemailPlaybackPresenter.setOnVoicemailDeletedListener(this);
}
mActivityType = activityType;
mContactInfoCache = new ContactInfoCache(
mContactInfoHelper, mOnContactInfoChangedListener);
if (!PermissionsUtil.hasContactsPermissions(context)) {
mContactInfoCache.disableRequestProcessing();
}
Resources resources = mContext.getResources();
CallTypeHelper callTypeHelper = new CallTypeHelper(resources);
mCallLogCache = CallLogCache.getCallLogCache(mContext);
PhoneCallDetailsHelper phoneCallDetailsHelper =
new PhoneCallDetailsHelper(mContext, resources, mCallLogCache);
mCallLogListItemHelper =
new CallLogListItemHelper(phoneCallDetailsHelper, resources, mCallLogCache);
mCallLogGroupBuilder = new CallLogGroupBuilder(this);
mFilteredNumberAsyncQueryHandler =
new FilteredNumberAsyncQueryHandler(mContext.getContentResolver());
mPrefs = PreferenceManager.getDefaultSharedPreferences(context);
mContactsPreferences = new ContactsPreferences(mContext);
maybeShowVoicemailPromoCard();
mExtendedCallInfoService = ObjectFactory.newExtendedCallInfoService(context);
mBlockReportSpamListener = new BlockReportSpamListener(
((Activity) mContext).getFragmentManager(), this,
mExtendedCallInfoService, mFilteredNumberAsyncQueryHandler);
setHasStableIds(true);
}
public void onSaveInstanceState(Bundle outState) {
outState.putInt(KEY_EXPANDED_POSITION, mCurrentlyExpandedPosition);
outState.putLong(KEY_EXPANDED_ROW_ID, mCurrentlyExpandedRowId);
}
public void onRestoreInstanceState(Bundle savedInstanceState) {
if (savedInstanceState != null) {
mCurrentlyExpandedPosition =
savedInstanceState.getInt(KEY_EXPANDED_POSITION, RecyclerView.NO_POSITION);
mCurrentlyExpandedRowId =
savedInstanceState.getLong(KEY_EXPANDED_ROW_ID, NO_EXPANDED_LIST_ITEM);
}
}
/**
* Requery on background thread when {@link Cursor} changes.
*/
@Override
protected void onContentChanged() {
mCallFetcher.fetchCalls();
}
public void setLoading(boolean loading) {
mLoading = loading;
}
public boolean isEmpty() {
if (mLoading) {
// We don't want the empty state to show when loading.
return false;
} else {
return getItemCount() == 0;
}
}
public void invalidateCache() {
mContactInfoCache.invalidate();
}
public void onResume() {
if (PermissionsUtil.hasPermission(mContext, android.Manifest.permission.READ_CONTACTS)) {
mContactInfoCache.start();
}
mContactsPreferences.refreshValue(ContactsPreferences.DISPLAY_ORDER_KEY);
}
public void onPause() {
pauseCache();
if (mHiddenItemUri != null) {
CallLogAsyncTaskUtil.deleteVoicemail(mContext, mHiddenItemUri, null);
}
}
@VisibleForTesting
/* package */ void pauseCache() {
mContactInfoCache.stop();
mCallLogCache.reset();
}
@Override
protected void addGroups(Cursor cursor) {
mCallLogGroupBuilder.addGroups(cursor);
}
@Override
public void addVoicemailGroups(Cursor cursor) {
mCallLogGroupBuilder.addVoicemailGroups(cursor);
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == VIEW_TYPE_VOICEMAIL_PROMO_CARD) {
return createVoicemailPromoCardViewHolder(parent);
}
return createCallLogEntryViewHolder(parent);
}
/**
* Creates a new call log entry {@link ViewHolder}.
*
* @param parent the parent view.
* @return The {@link ViewHolder}.
*/
private ViewHolder createCallLogEntryViewHolder(ViewGroup parent) {
LayoutInflater inflater = LayoutInflater.from(mContext);
View view = inflater.inflate(R.layout.call_log_list_item, parent, false);
CallLogListItemViewHolder viewHolder = CallLogListItemViewHolder.create(
view,
mContext,
mBlockReportSpamListener,
mExpandCollapseListener,
mCallLogCache,
mCallLogListItemHelper,
mVoicemailPlaybackPresenter,
mFilteredNumberAsyncQueryHandler,
mBlockedNumberDialogCallback,
mActivityType == ACTIVITY_TYPE_ARCHIVE);
viewHolder.callLogEntryView.setTag(viewHolder);
viewHolder.callLogEntryView.setAccessibilityDelegate(mAccessibilityDelegate);
viewHolder.primaryActionView.setTag(viewHolder);
return viewHolder;
}
/**
* Binds the views in the entry to the data in the call log.
* TODO: This gets called 20-30 times when Dialer starts up for a single call log entry and
* should not. It invokes cross-process methods and the repeat execution can get costly.
*
* @param viewHolder The view corresponding to this entry.
* @param position The position of the entry.
*/
@Override
public void onBindViewHolder(ViewHolder viewHolder, int position) {
Trace.beginSection("onBindViewHolder: " + position);
switch (getItemViewType(position)) {
case VIEW_TYPE_VOICEMAIL_PROMO_CARD:
bindVoicemailPromoCardViewHolder(viewHolder);
break;
default:
bindCallLogListViewHolder(viewHolder, position);
break;
}
Trace.endSection();
}
/**
* Binds the promo card view holder.
*
* @param viewHolder The promo card view holder.
*/
protected void bindVoicemailPromoCardViewHolder(ViewHolder viewHolder) {
PromoCardViewHolder promoCardViewHolder = (PromoCardViewHolder) viewHolder;
promoCardViewHolder.getSecondaryActionView()
.setOnClickListener(mVoicemailSettingsActionListener);
promoCardViewHolder.getPrimaryActionView().setOnClickListener(mOkActionListener);
}
/**
* Binds the view holder for the call log list item view.
*
* @param viewHolder The call log list item view holder.
* @param position The position of the list item.
*/
private void bindCallLogListViewHolder(final ViewHolder viewHolder, final int position) {
Cursor c = (Cursor) getItem(position);
if (c == null) {
return;
}
final String number = c.getString(CallLogQuery.NUMBER);
final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO);
final CallLogListItemViewHolder views = (CallLogListItemViewHolder) viewHolder;
boolean success = mFilteredNumberAsyncQueryHandler.isBlockedNumber(
new FilteredNumberAsyncQueryHandler.OnCheckBlockedListener() {
@Override
public void onCheckComplete(Integer id) {
views.blockId = id;
if (mExtendedCallInfoService == null) {
loadDataAndRender(views);
} else {
views.isSpamFeatureEnabled = true;
mExtendedCallInfoService.getExtendedCallInfo(number, countryIso,
new ExtendedCallInfoService.Listener() {
@Override
public void onComplete(boolean isSpam) {
views.isSpam = isSpam;
loadDataAndRender(views);
}
});
}
}
}, number, countryIso);
if (!success) {
loadDataAndRender(views);
}
}
private void loadDataAndRender(CallLogListItemViewHolder views) {
int position = views.getAdapterPosition();
Cursor c = (Cursor) getItem(position);
if (c == null) {
return;
}
int count = getGroupSize(position);
final String number = c.getString(CallLogQuery.NUMBER);
final String countryIso = c.getString(CallLogQuery.COUNTRY_ISO);
final String postDialDigits = CompatUtils.isNCompatible()
&& mActivityType != ACTIVITY_TYPE_ARCHIVE ?
c.getString(CallLogQuery.POST_DIAL_DIGITS) : "";
final String viaNumber = CompatUtils.isNCompatible()
&& mActivityType != ACTIVITY_TYPE_ARCHIVE ?
c.getString(CallLogQuery.VIA_NUMBER) : "";
final int numberPresentation = c.getInt(CallLogQuery.NUMBER_PRESENTATION);
final PhoneAccountHandle accountHandle = PhoneAccountUtils.getAccount(
c.getString(CallLogQuery.ACCOUNT_COMPONENT_NAME),
c.getString(CallLogQuery.ACCOUNT_ID));
final ContactInfo cachedContactInfo = ContactInfoHelper.getContactInfo(c);
final boolean isVoicemailNumber =
mCallLogCache.isVoicemailNumber(accountHandle, number);
// Note: Binding of the action buttons is done as required in configureActionViews when the
// user expands the actions ViewStub.
ContactInfo info = ContactInfo.EMPTY;
if (PhoneNumberUtil.canPlaceCallsTo(number, numberPresentation) && !isVoicemailNumber) {
// Lookup contacts with this number
info = mContactInfoCache.getValue(number + postDialDigits,
countryIso, cachedContactInfo);
}
CharSequence formattedNumber = info.formattedNumber == null
? null : PhoneNumberUtilsCompat.createTtsSpannable(info.formattedNumber);
final PhoneCallDetails details = new PhoneCallDetails(
mContext, number, numberPresentation, formattedNumber,
postDialDigits, isVoicemailNumber);
details.viaNumber = viaNumber;
details.accountHandle = accountHandle;
details.countryIso = countryIso;
details.date = c.getLong(CallLogQuery.DATE);
details.duration = c.getLong(CallLogQuery.DURATION);
details.features = getCallFeatures(c, count);
details.geocode = c.getString(CallLogQuery.GEOCODED_LOCATION);
details.transcription = c.getString(CallLogQuery.TRANSCRIPTION);
details.callTypes = getCallTypes(c, count);
if (!c.isNull(CallLogQuery.DATA_USAGE)) {
details.dataUsage = c.getLong(CallLogQuery.DATA_USAGE);
}
if (!TextUtils.isEmpty(info.name) || !TextUtils.isEmpty(info.nameAlternative)) {
details.contactUri = info.lookupUri;
details.namePrimary = info.name;
details.nameAlternative = info.nameAlternative;
details.nameDisplayOrder = mContactsPreferences.getDisplayOrder();
details.numberType = info.type;
details.numberLabel = info.label;
details.photoUri = info.photoUri;
details.sourceType = info.sourceType;
details.objectId = info.objectId;
details.contactUserType = info.userType;
}
views.info = info;
views.rowId = c.getLong(CallLogQuery.ID);
// Store values used when the actions ViewStub is inflated on expansion.
views.number = number;
views.postDialDigits = details.postDialDigits;
views.displayNumber = details.displayNumber;
views.numberPresentation = numberPresentation;
views.accountHandle = accountHandle;
// Stash away the Ids of the calls so that we can support deleting a row in the call log.
views.callIds = getCallIds(c, count);
views.isBusiness = mContactInfoHelper.isBusiness(info.sourceType);
views.numberType = (String) Phone.getTypeLabel(mContext.getResources(), details.numberType,
details.numberLabel);
// Default case: an item in the call log.
views.primaryActionView.setVisibility(View.VISIBLE);
views.workIconView.setVisibility(
details.contactUserType == ContactsUtils.USER_TYPE_WORK ? View.VISIBLE : View.GONE);
// Check if the day group has changed and display a header if necessary.
int currentGroup = getDayGroupForCall(views.rowId);
int previousGroup = getPreviousDayGroup(c);
if (currentGroup != previousGroup) {
views.dayGroupHeader.setVisibility(View.VISIBLE);
views.dayGroupHeader.setText(getGroupDescription(currentGroup));
} else {
views.dayGroupHeader.setVisibility(View.GONE);
}
if (mActivityType == ACTIVITY_TYPE_ARCHIVE) {
views.callType = CallLog.Calls.VOICEMAIL_TYPE;
views.voicemailUri = VoicemailArchiveContract.VoicemailArchive.buildWithId(c.getInt(
c.getColumnIndex(VoicemailArchiveContract.VoicemailArchive._ID)))
.toString();
} else {
if (details.callTypes[0] == CallLog.Calls.VOICEMAIL_TYPE ||
details.callTypes[0] == CallLog.Calls.MISSED_TYPE) {
details.isRead = c.getInt(CallLogQuery.IS_READ) == 1;
}
views.callType = c.getInt(CallLogQuery.CALL_TYPE);
views.voicemailUri = c.getString(CallLogQuery.VOICEMAIL_URI);
}
// Reversely pass spam information from views since details is not constructed when spam
// information comes back. This is used to render phone call details.
details.isSpam = views.isSpam;
render(views, details);
}
private void render(CallLogListItemViewHolder views, PhoneCallDetails details) {
mCallLogListItemHelper.setPhoneCallDetails(views, details);
if (mCurrentlyExpandedRowId == views.rowId) {
// In case ViewHolders were added/removed, update the expanded position if the rowIds
// match so that we can restore the correct expanded state on rebind.
mCurrentlyExpandedPosition = views.getAdapterPosition();
views.showActions(true);
} else {
views.showActions(false);
}
}
private String getPreferredDisplayName(ContactInfo contactInfo) {
if (mContactsPreferences.getDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY ||
TextUtils.isEmpty(contactInfo.nameAlternative)) {
return contactInfo.name;
}
return contactInfo.nameAlternative;
}
@Override
public int getItemCount() {
return super.getItemCount() + (mShowVoicemailPromoCard ? 1 : 0)
- (mHiddenPosition != RecyclerView.NO_POSITION ? 1 : 0);
}
@Override
public int getItemViewType(int position) {
if (position == VOICEMAIL_PROMO_CARD_POSITION && mShowVoicemailPromoCard) {
return VIEW_TYPE_VOICEMAIL_PROMO_CARD;
}
return super.getItemViewType(position);
}
/**
* Retrieves an item at the specified position, taking into account the presence of a promo
* card.
*
* @param position The position to retrieve.
* @return The item at that position.
*/
@Override
public Object getItem(int position) {
return super.getItem(position - (mShowVoicemailPromoCard ? 1 : 0)
+ ((mHiddenPosition != RecyclerView.NO_POSITION && position >= mHiddenPosition)
? 1 : 0));
}
@Override
public long getItemId(int position) {
Cursor cursor = (Cursor) getItem(position);
if (cursor != null) {
return cursor.getLong(CallLogQuery.ID);
} else {
return 0;
}
}
@Override
public int getGroupSize(int position) {
return super.getGroupSize(position - (mShowVoicemailPromoCard ? 1 : 0));
}
protected boolean isCallLogActivity() {
return mActivityType == ACTIVITY_TYPE_CALL_LOG;
}
/**
* In order to implement the "undo" function, when a voicemail is "deleted" i.e. when the user
* clicks the delete button, the deleted item is temporarily hidden from the list. If a user
* clicks delete on a second item before the first item's undo option has expired, the first
* item is immediately deleted so that only one item can be "undoed" at a time.
*/
@Override
public void onVoicemailDeleted(Uri uri) {
if (mHiddenItemUri == null) {
// Immediately hide the currently expanded card.
mHiddenPosition = mCurrentlyExpandedPosition;
notifyDataSetChanged();
} else {
// This means that there was a previous item that was hidden in the UI but not
// yet deleted from the database (call it a "pending delete"). Delete this previous item
// now since it is only possible to do one "undo" at a time.
CallLogAsyncTaskUtil.deleteVoicemail(mContext, mHiddenItemUri, null);
// Set pending hide action so that the current item is hidden only after the previous
// item is permanently deleted.
mPendingHide = true;
}
collapseExpandedCard();
// Save the new hidden item uri in case it needs to be deleted from the database when
// a user attempts to delete another item.
mHiddenItemUri = uri;
}
private void collapseExpandedCard() {
mCurrentlyExpandedRowId = NO_EXPANDED_LIST_ITEM;
mCurrentlyExpandedPosition = RecyclerView.NO_POSITION;
}
/**
* When the list is changing all stored position is no longer valid.
*/
public void invalidatePositions() {
mCurrentlyExpandedPosition = RecyclerView.NO_POSITION;
mHiddenPosition = RecyclerView.NO_POSITION;
}
/**
* When the user clicks "undo", the hidden item is unhidden.
*/
@Override
public void onVoicemailDeleteUndo() {
mHiddenPosition = RecyclerView.NO_POSITION;
mHiddenItemUri = null;
mPendingHide = false;
notifyDataSetChanged();
}
/**
* This callback signifies that a database deletion has completed. This means that if there is
* an item pending deletion, it will be hidden because the previous item that was in "undo" mode
* has been removed from the database. Otherwise it simply resets the hidden state because there
* are no pending deletes and thus no hidden items.
*/
@Override
public void onVoicemailDeletedInDatabase() {
if (mPendingHide) {
mHiddenPosition = mCurrentlyExpandedPosition;
mPendingHide = false;
} else {
// There should no longer be any hidden item because it has been deleted from the
// database.
mHiddenPosition = RecyclerView.NO_POSITION;
mHiddenItemUri = null;
}
}
/**
* Retrieves the day group of the previous call in the call log. Used to determine if the day
* group has changed and to trigger display of the day group text.
*
* @param cursor The call log cursor.
* @return The previous day group, or DAY_GROUP_NONE if this is the first call.
*/
private int getPreviousDayGroup(Cursor cursor) {
// We want to restore the position in the cursor at the end.
int startingPosition = cursor.getPosition();
int dayGroup = CallLogGroupBuilder.DAY_GROUP_NONE;
if (cursor.moveToPrevious()) {
// If the previous entry is hidden (deleted in the UI but not in the database), skip it
// and check the card above it. A list with the voicemail promo card at the top will be
// 1-indexed because the 0th index is the promo card iteself.
int previousViewPosition = mShowVoicemailPromoCard ? startingPosition :
startingPosition - 1;
if (previousViewPosition != mHiddenPosition ||
(previousViewPosition == mHiddenPosition && cursor.moveToPrevious())) {
long previousRowId = cursor.getLong(CallLogQuery.ID);
dayGroup = getDayGroupForCall(previousRowId);
}
}
cursor.moveToPosition(startingPosition);
return dayGroup;
}
/**
* Given a call Id, look up the day group that the call belongs to. The day group data is
* populated in {@link com.android.dialer.calllog.CallLogGroupBuilder}.
*
* @param callId The call to retrieve the day group for.
* @return The day group for the call.
*/
private int getDayGroupForCall(long callId) {
if (mDayGroups.containsKey(callId)) {
return mDayGroups.get(callId);
}
return CallLogGroupBuilder.DAY_GROUP_NONE;
}
/**
* Returns the call types for the given number of items in the cursor.
* <p>
* It uses the next {@code count} rows in the cursor to extract the types.
* <p>
* It position in the cursor is unchanged by this function.
*/
private int[] getCallTypes(Cursor cursor, int count) {
if (mActivityType == ACTIVITY_TYPE_ARCHIVE) {
return new int[] {CallLog.Calls.VOICEMAIL_TYPE};
}
int position = cursor.getPosition();
int[] callTypes = new int[count];
for (int index = 0; index < count; ++index) {
callTypes[index] = cursor.getInt(CallLogQuery.CALL_TYPE);
cursor.moveToNext();
}
cursor.moveToPosition(position);
return callTypes;
}
/**
* Determine the features which were enabled for any of the calls that make up a call log
* entry.
*
* @param cursor The cursor.
* @param count The number of calls for the current call log entry.
* @return The features.
*/
private int getCallFeatures(Cursor cursor, int count) {
int features = 0;
int position = cursor.getPosition();
for (int index = 0; index < count; ++index) {
features |= cursor.getInt(CallLogQuery.FEATURES);
cursor.moveToNext();
}
cursor.moveToPosition(position);
return features;
}
/**
* Sets whether processing of requests for contact details should be enabled.
*
* This method should be called in tests to disable such processing of requests when not
* needed.
*/
@VisibleForTesting
void disableRequestProcessingForTest() {
// TODO: Remove this and test the cache directly.
mContactInfoCache.disableRequestProcessing();
}
@VisibleForTesting
void injectContactInfoForTest(String number, String countryIso, ContactInfo contactInfo) {
// TODO: Remove this and test the cache directly.
mContactInfoCache.injectContactInfoForTest(number, countryIso, contactInfo);
}
/**
* Stores the day group associated with a call in the call log.
*
* @param rowId The row Id of the current call.
* @param dayGroup The day group the call belongs in.
*/
@Override
public void setDayGroup(long rowId, int dayGroup) {
if (!mDayGroups.containsKey(rowId)) {
mDayGroups.put(rowId, dayGroup);
}
}
/**
* Clears the day group associations on re-bind of the call log.
*/
@Override
public void clearDayGroups() {
mDayGroups.clear();
}
/**
* Retrieves the call Ids represented by the current call log row.
*
* @param cursor Call log cursor to retrieve call Ids from.
* @param groupSize Number of calls associated with the current call log row.
* @return Array of call Ids.
*/
private long[] getCallIds(final Cursor cursor, final int groupSize) {
// We want to restore the position in the cursor at the end.
int startingPosition = cursor.getPosition();
long[] ids = new long[groupSize];
// Copy the ids of the rows in the group.
for (int index = 0; index < groupSize; ++index) {
ids[index] = cursor.getLong(CallLogQuery.ID);
cursor.moveToNext();
}
cursor.moveToPosition(startingPosition);
return ids;
}
/**
* Determines the description for a day group.
*
* @param group The day group to retrieve the description for.
* @return The day group description.
*/
private CharSequence getGroupDescription(int group) {
if (group == CallLogGroupBuilder.DAY_GROUP_TODAY) {
return mContext.getResources().getString(R.string.call_log_header_today);
} else if (group == CallLogGroupBuilder.DAY_GROUP_YESTERDAY) {
return mContext.getResources().getString(R.string.call_log_header_yesterday);
} else {
return mContext.getResources().getString(R.string.call_log_header_other);
}
}
/**
* Determines if the voicemail promo card should be shown or not. The voicemail promo card will
* be shown as the first item in the voicemail tab.
*/
private void maybeShowVoicemailPromoCard() {
boolean showPromoCard = mPrefs.getBoolean(SHOW_VOICEMAIL_PROMO_CARD,
SHOW_VOICEMAIL_PROMO_CARD_DEFAULT);
mShowVoicemailPromoCard = mActivityType != ACTIVITY_TYPE_ARCHIVE &&
(mVoicemailPlaybackPresenter != null) && showPromoCard;
}
/**
* Dismisses the voicemail promo card and refreshes the call log.
*/
private void dismissVoicemailPromoCard() {
mPrefs.edit().putBoolean(SHOW_VOICEMAIL_PROMO_CARD, false).apply();
mShowVoicemailPromoCard = false;
notifyItemRemoved(VOICEMAIL_PROMO_CARD_POSITION);
}
/**
* Creates the view holder for the voicemail promo card.
*
* @param parent The parent view.
* @return The {@link ViewHolder}.
*/
protected ViewHolder createVoicemailPromoCardViewHolder(ViewGroup parent) {
LayoutInflater inflater = LayoutInflater.from(mContext);
View view = inflater.inflate(R.layout.voicemail_promo_card, parent, false);
PromoCardViewHolder viewHolder = PromoCardViewHolder.create(view);
return viewHolder;
}
}