| /* |
| * 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; |
| } |
| } |