/* * 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 android.app.Activity; import android.content.Context; import android.content.res.Resources; import android.content.Intent; import android.net.Uri; import android.provider.CallLog; import android.provider.CallLog.Calls; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.support.v7.widget.CardView; import android.support.v7.widget.RecyclerView; import android.telecom.PhoneAccountHandle; import android.text.TextUtils; import android.view.ContextMenu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.ViewStub; import android.widget.QuickContactBadge; import android.widget.ImageView; import android.widget.TextView; import com.android.contacts.common.CallUtil; import com.android.contacts.common.ClipboardUtils; import com.android.contacts.common.ContactPhotoManager; import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest; import com.android.contacts.common.dialog.CallSubjectDialog; import com.android.contacts.common.testing.NeededForTesting; import com.android.contacts.common.util.UriUtils; import com.android.dialer.DialtactsActivity; import com.android.dialer.R; import com.android.dialer.database.FilteredNumberAsyncQueryHandler; import com.android.dialer.filterednumber.FilterNumberDialogFragment; import com.android.dialer.util.DialerUtils; import com.android.dialer.util.PhoneNumberUtil; import com.android.dialer.voicemail.VoicemailPlaybackPresenter; import com.android.dialer.voicemail.VoicemailPlaybackLayout; /** * This is an object containing references to views contained by the call log list item. This * improves performance by reducing the frequency with which we need to find views by IDs. * * This object also contains UI logic pertaining to the view, to isolate it from the CallLogAdapter. */ public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, MenuItem.OnMenuItemClickListener, View.OnCreateContextMenuListener { /** The root view of the call log list item */ public final View rootView; /** The quick contact badge for the contact. */ public final QuickContactBadge quickContactView; /** The primary action view of the entry. */ public final View primaryActionView; /** The details of the phone call. */ public final PhoneCallDetailsViews phoneCallDetailsViews; /** The text of the header for a day grouping. */ public final TextView dayGroupHeader; /** The view containing the details for the call log row, including the action buttons. */ public final CardView callLogEntryView; /** The actionable view which places a call to the number corresponding to the call log row. */ public final ImageView primaryActionButtonView; /** The view containing call log item actions. Null until the ViewStub is inflated. */ public View actionsView; /** The button views below are assigned only when the action section is expanded. */ public VoicemailPlaybackLayout voicemailPlaybackView; public View callButtonView; public View videoCallButtonView; public View createNewContactButtonView; public View addToExistingContactButtonView; public View sendMessageView; public View detailsButtonView; public View callWithNoteButtonView; /** * The row Id for the first call associated with the call log entry. Used as a key for the * map used to track which call log entries have the action button section expanded. */ public long rowId; /** * The call Ids for the calls represented by the current call log entry. Used when the user * deletes a call log entry. */ public long[] callIds; /** * The callable phone number for the current call log entry. Cached here as the call back * intent is set only when the actions ViewStub is inflated. */ public String number; /** * The formatted phone number to display. */ public String displayNumber; /** * The phone number presentation for the current call log entry. Cached here as the call back * intent is set only when the actions ViewStub is inflated. */ public int numberPresentation; /** * The type of the phone number (e.g. main, work, etc). */ public String numberType; /** * The country iso for the call. Cached here as the call back * intent is set only when the actions ViewStub is inflated. */ public String countryIso; /** * The type of call for the current call log entry. Cached here as the call back * intent is set only when the actions ViewStub is inflated. */ public int callType; /** * ID for blocked numbers database. * Set when context menu is created, if the number is blocked. */ public Integer blockId; /** * The account for the current call log entry. Cached here as the call back * intent is set only when the actions ViewStub is inflated. */ public PhoneAccountHandle accountHandle; /** * If the call has an associated voicemail message, the URI of the voicemail message for * playback. Cached here as the voicemail intent is only set when the actions ViewStub is * inflated. */ public String voicemailUri; /** * The name or number associated with the call. Cached here for use when setting content * descriptions on buttons in the actions ViewStub when it is inflated. */ public CharSequence nameOrNumber; /** * Whether this row is for a business or not. */ public boolean isBusiness; /** * The contact info for the contact displayed in this list item. */ public ContactInfo info; private static final int VOICEMAIL_TRANSCRIPTION_MAX_LINES = 10; private final Context mContext; private final TelecomCallLogCache mTelecomCallLogCache; private final CallLogListItemHelper mCallLogListItemHelper; private final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter; private final FilteredNumberAsyncQueryHandler mFilteredNumberAsyncQueryHandler; private final int mPhotoSize; private View.OnClickListener mExpandCollapseListener; private boolean mVoicemailPrimaryActionButtonClicked; private CallLogListItemViewHolder( Context context, View.OnClickListener expandCollapseListener, TelecomCallLogCache telecomCallLogCache, CallLogListItemHelper callLogListItemHelper, VoicemailPlaybackPresenter voicemailPlaybackPresenter, FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler, View rootView, QuickContactBadge quickContactView, View primaryActionView, PhoneCallDetailsViews phoneCallDetailsViews, CardView callLogEntryView, TextView dayGroupHeader, ImageView primaryActionButtonView) { super(rootView); mContext = context; mExpandCollapseListener = expandCollapseListener; mTelecomCallLogCache = telecomCallLogCache; mCallLogListItemHelper = callLogListItemHelper; mVoicemailPlaybackPresenter = voicemailPlaybackPresenter; mFilteredNumberAsyncQueryHandler = filteredNumberAsyncQueryHandler; this.rootView = rootView; this.quickContactView = quickContactView; this.primaryActionView = primaryActionView; this.phoneCallDetailsViews = phoneCallDetailsViews; this.callLogEntryView = callLogEntryView; this.dayGroupHeader = dayGroupHeader; this.primaryActionButtonView = primaryActionButtonView; Resources resources = mContext.getResources(); mPhotoSize = mContext.getResources().getDimensionPixelSize(R.dimen.contact_photo_size); // Set text height to false on the TextViews so they don't have extra padding. phoneCallDetailsViews.nameView.setElegantTextHeight(false); phoneCallDetailsViews.callLocationAndDate.setElegantTextHeight(false); quickContactView.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE); primaryActionButtonView.setOnClickListener(this); primaryActionView.setOnClickListener(mExpandCollapseListener); primaryActionView.setOnCreateContextMenuListener(this); } public static CallLogListItemViewHolder create( View view, Context context, View.OnClickListener expandCollapseListener, TelecomCallLogCache telecomCallLogCache, CallLogListItemHelper callLogListItemHelper, VoicemailPlaybackPresenter voicemailPlaybackPresenter, FilteredNumberAsyncQueryHandler filteredNumberAsyncQueryHandler) { return new CallLogListItemViewHolder( context, expandCollapseListener, telecomCallLogCache, callLogListItemHelper, voicemailPlaybackPresenter, filteredNumberAsyncQueryHandler, view, (QuickContactBadge) view.findViewById(R.id.quick_contact_photo), view.findViewById(R.id.primary_action_view), PhoneCallDetailsViews.fromView(view), (CardView) view.findViewById(R.id.call_log_row), (TextView) view.findViewById(R.id.call_log_day_group_label), (ImageView) view.findViewById(R.id.primary_action_button)); } @Override public void onCreateContextMenu( final ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { if (TextUtils.isEmpty(number)) { return; } if (callType == CallLog.Calls.VOICEMAIL_TYPE) { menu.setHeaderTitle(mContext.getResources().getText(R.string.voicemail)); } else { menu.setHeaderTitle(number); } menu.add(ContextMenu.NONE, R.id.context_menu_copy_to_clipboard, ContextMenu.NONE, R.string.copy_number_text) .setOnMenuItemClickListener(this); // The edit number before call does not show up if any of the conditions apply: // 1) Number cannot be called // 2) Number is the voicemail number // 3) Number is a SIP address if (PhoneNumberUtil.canPlaceCallsTo(number, numberPresentation) && !mTelecomCallLogCache.isVoicemailNumber(accountHandle, number) && !PhoneNumberUtil.isSipNumber(number)) { menu.add(ContextMenu.NONE, R.id.context_menu_edit_before_call, ContextMenu.NONE, R.string.call_log_edit_number_before_call) .setOnMenuItemClickListener(this); } if (callType == CallLog.Calls.VOICEMAIL_TYPE && phoneCallDetailsViews.voicemailTranscriptionView.length() > 0) { menu.add(ContextMenu.NONE, R.id.context_menu_copy_transcript_to_clipboard, ContextMenu.NONE, R.string.copy_transcript_text) .setOnMenuItemClickListener(this); } mFilteredNumberAsyncQueryHandler.startBlockedQuery( new FilteredNumberAsyncQueryHandler.OnCheckBlockedListener() { @Override public void onCheckComplete(Integer id) { blockId = id; int blockTitleId = blockId == null ? R.string.call_log_block_number : R.string.call_log_unblock_number; final MenuItem blockItem = menu.add( ContextMenu.NONE, R.id.context_menu_block_number, ContextMenu.NONE, blockTitleId); blockItem.setOnMenuItemClickListener( CallLogListItemViewHolder.this); } }, info.normalizedNumber, number, countryIso); } @Override public boolean onMenuItemClick(MenuItem item) { switch (item.getItemId()) { case R.id.context_menu_block_number: FilterNumberDialogFragment newFragment = FilterNumberDialogFragment.newInstance(blockId, info.normalizedNumber, number, countryIso, displayNumber); newFragment.setParentView( ((Activity) mContext).findViewById(R.id.floating_action_button_container)); newFragment.show(((Activity) mContext).getFragmentManager(), FilterNumberDialogFragment.BLOCK_DIALOG_FRAGMENT); return true; case R.id.context_menu_copy_to_clipboard: ClipboardUtils.copyText(mContext, null, number, true); return true; case R.id.context_menu_copy_transcript_to_clipboard: ClipboardUtils.copyText(mContext, null, phoneCallDetailsViews.voicemailTranscriptionView.getText(), true); return true; case R.id.context_menu_edit_before_call: final Intent intent = new Intent( Intent.ACTION_DIAL, CallUtil.getCallUri(number)); intent.setClass(mContext, DialtactsActivity.class); DialerUtils.startActivityWithErrorToast(mContext, intent); return true; } return false; } /** * Configures the action buttons in the expandable actions ViewStub. The ViewStub is not * inflated during initial binding, so click handlers, tags and accessibility text must be set * here, if necessary. * * @param callLogItem The call log list item view. */ public void inflateActionViewStub() { ViewStub stub = (ViewStub) rootView.findViewById(R.id.call_log_entry_actions_stub); if (stub != null) { actionsView = (ViewGroup) stub.inflate(); voicemailPlaybackView = (VoicemailPlaybackLayout) actionsView .findViewById(R.id.voicemail_playback_layout); callButtonView = actionsView.findViewById(R.id.call_action); callButtonView.setOnClickListener(this); videoCallButtonView = actionsView.findViewById(R.id.video_call_action); videoCallButtonView.setOnClickListener(this); createNewContactButtonView = actionsView.findViewById(R.id.create_new_contact_action); createNewContactButtonView.setOnClickListener(this); addToExistingContactButtonView = actionsView.findViewById(R.id.add_to_existing_contact_action); addToExistingContactButtonView.setOnClickListener(this); sendMessageView = actionsView.findViewById(R.id.send_message_action); sendMessageView.setOnClickListener(this); detailsButtonView = actionsView.findViewById(R.id.details_action); detailsButtonView.setOnClickListener(this); callWithNoteButtonView = actionsView.findViewById(R.id.call_with_note_action); callWithNoteButtonView.setOnClickListener(this); } bindActionButtons(); } private void updatePrimaryActionButton(boolean isExpanded) { if (!TextUtils.isEmpty(voicemailUri)) { // Treat as voicemail list item; show play button if not expanded. if (!isExpanded) { primaryActionButtonView.setImageResource(R.drawable.ic_play_arrow_24dp); primaryActionButtonView.setVisibility(View.VISIBLE); } else { primaryActionButtonView.setVisibility(View.GONE); } } else { // Treat as normal list item; show call button, if possible. if (PhoneNumberUtil.canPlaceCallsTo(number, numberPresentation)) { boolean isVoicemailNumber = mTelecomCallLogCache.isVoicemailNumber(accountHandle, number); if (isVoicemailNumber) { // Call to generic voicemail number, in case there are multiple accounts. primaryActionButtonView.setTag( IntentProvider.getReturnVoicemailCallIntentProvider()); } else { primaryActionButtonView.setTag( IntentProvider.getReturnCallIntentProvider(number)); } primaryActionButtonView.setContentDescription(TextUtils.expandTemplate( mContext.getString(R.string.description_call_action), nameOrNumber)); primaryActionButtonView.setImageResource(R.drawable.ic_call_24dp); primaryActionButtonView.setVisibility(View.VISIBLE); } else { primaryActionButtonView.setTag(null); primaryActionButtonView.setVisibility(View.GONE); } } } /** * Binds text titles, click handlers and intents to the voicemail, details and callback action * buttons. */ private void bindActionButtons() { boolean canPlaceCallToNumber = PhoneNumberUtil.canPlaceCallsTo(number, numberPresentation); if (!TextUtils.isEmpty(voicemailUri) && canPlaceCallToNumber) { callButtonView.setTag(IntentProvider.getReturnCallIntentProvider(number)); ((TextView) callButtonView.findViewById(R.id.call_action_text)) .setText(TextUtils.expandTemplate( mContext.getString(R.string.call_log_action_call), nameOrNumber)); callButtonView.setVisibility(View.VISIBLE); } else { callButtonView.setVisibility(View.GONE); } // If one of the calls had video capabilities, show the video call button. if (mTelecomCallLogCache.isVideoEnabled() && canPlaceCallToNumber && phoneCallDetailsViews.callTypeIcons.isVideoShown()) { videoCallButtonView.setTag(IntentProvider.getReturnVideoCallIntentProvider(number)); videoCallButtonView.setVisibility(View.VISIBLE); } else { videoCallButtonView.setVisibility(View.GONE); } // For voicemail calls, show the voicemail playback layout; hide otherwise. if (callType == Calls.VOICEMAIL_TYPE && mVoicemailPlaybackPresenter != null) { voicemailPlaybackView.setVisibility(View.VISIBLE); Uri uri = Uri.parse(voicemailUri); mVoicemailPlaybackPresenter.setPlaybackView( voicemailPlaybackView, uri, mVoicemailPrimaryActionButtonClicked); mVoicemailPrimaryActionButtonClicked = false; CallLogAsyncTaskUtil.markVoicemailAsRead(mContext, uri); } else { voicemailPlaybackView.setVisibility(View.GONE); } detailsButtonView.setVisibility(View.VISIBLE); detailsButtonView.setTag( IntentProvider.getCallDetailIntentProvider(rowId, callIds, null)); if (info != null && UriUtils.isEncodedContactUri(info.lookupUri)) { createNewContactButtonView.setTag(IntentProvider.getAddContactIntentProvider( info.lookupUri, info.name, info.number, info.type, true /* isNewContact */)); createNewContactButtonView.setVisibility(View.VISIBLE); addToExistingContactButtonView.setTag(IntentProvider.getAddContactIntentProvider( info.lookupUri, info.name, info.number, info.type, false /* isNewContact */)); addToExistingContactButtonView.setVisibility(View.VISIBLE); } else { createNewContactButtonView.setVisibility(View.GONE); addToExistingContactButtonView.setVisibility(View.GONE); } if (canPlaceCallToNumber) { sendMessageView.setTag(IntentProvider.getSendSmsIntentProvider(number)); sendMessageView.setVisibility(View.VISIBLE); } else { sendMessageView.setVisibility(View.GONE); } mCallLogListItemHelper.setActionContentDescriptions(this); boolean supportsCallSubject = mTelecomCallLogCache.doesAccountSupportCallSubject(accountHandle); boolean isVoicemailNumber = mTelecomCallLogCache.isVoicemailNumber(accountHandle, number); callWithNoteButtonView.setVisibility( supportsCallSubject && !isVoicemailNumber ? View.VISIBLE : View.GONE); } /** * Show or hide the action views, such as voicemail, details, and add contact. * * If the action views have never been shown yet for this view, inflate the view stub. */ public void showActions(boolean show) { expandVoicemailTranscriptionView(show); if (show) { // Inflate the view stub if necessary, and wire up the event handlers. inflateActionViewStub(); actionsView.setVisibility(View.VISIBLE); actionsView.setAlpha(1.0f); } else { // When recycling a view, it is possible the actionsView ViewStub was previously // inflated so we should hide it in this case. if (actionsView != null) { actionsView.setVisibility(View.GONE); } } updatePrimaryActionButton(show); } public void expandVoicemailTranscriptionView(boolean isExpanded) { if (callType != Calls.VOICEMAIL_TYPE) { return; } final TextView view = phoneCallDetailsViews.voicemailTranscriptionView; if (TextUtils.isEmpty(view.getText())) { return; } view.setMaxLines(isExpanded ? VOICEMAIL_TRANSCRIPTION_MAX_LINES : 1); view.setSingleLine(!isExpanded); } public void updatePhoto() { quickContactView.setOverlay(null); quickContactView.assignContactUri(info.lookupUri); if (blockId != null) { quickContactView.setImageDrawable(mContext.getDrawable(R.drawable.blocked_contact)); return; } final boolean isVoicemail = mTelecomCallLogCache.isVoicemailNumber(accountHandle, number); int contactType = ContactPhotoManager.TYPE_DEFAULT; if (isVoicemail) { contactType = ContactPhotoManager.TYPE_VOICEMAIL; } else if (isBusiness) { contactType = ContactPhotoManager.TYPE_BUSINESS; } final String lookupKey = info.lookupUri != null ? UriUtils.getLookupKeyFromUri(info.lookupUri) : null; final String displayName = TextUtils.isEmpty(info.name) ? displayNumber : info.name; final DefaultImageRequest request = new DefaultImageRequest( displayName, lookupKey, contactType, true /* isCircular */); if (info.photoId == 0 && info.photoUri != null) { ContactPhotoManager.getInstance(mContext).loadPhoto(quickContactView, info.photoUri, mPhotoSize, false /* darkTheme */, true /* isCircular */, request); } else { ContactPhotoManager.getInstance(mContext).loadThumbnail(quickContactView, info.photoId, false /* darkTheme */, true /* isCircular */, request); } } @Override public void onClick(View view) { if (view.getId() == R.id.primary_action_button && !TextUtils.isEmpty(voicemailUri)) { mVoicemailPrimaryActionButtonClicked = true; mExpandCollapseListener.onClick(primaryActionView); } else if (view.getId() == R.id.call_with_note_action) { CallSubjectDialog.start( (Activity) mContext, info.photoId, info.photoUri, info.lookupUri, (String) nameOrNumber /* top line of contact view in call subject dialog */, isBusiness, number, /* callable number used for ACTION_CALL intent */ TextUtils.isEmpty(info.name) ? null : displayNumber, /* second line of contact view in dialog. */ numberType, /* phone number type (e.g. mobile) in second line of contact view */ accountHandle); } else { final IntentProvider intentProvider = (IntentProvider) view.getTag(); if (intentProvider != null) { final Intent intent = intentProvider.getIntent(mContext); // See IntentProvider.getCallDetailIntentProvider() for why this may be null. if (intent != null) { DialerUtils.startActivityWithErrorToast(mContext, intent); } } } } @NeededForTesting public static CallLogListItemViewHolder createForTest(Context context) { Resources resources = context.getResources(); TelecomCallLogCache telecomCallLogCache = new TelecomCallLogCache(context); PhoneCallDetailsHelper phoneCallDetailsHelper = new PhoneCallDetailsHelper( context, resources, telecomCallLogCache); CallLogListItemViewHolder viewHolder = new CallLogListItemViewHolder( context, null /* expandCollapseListener */, telecomCallLogCache, new CallLogListItemHelper(phoneCallDetailsHelper, resources, telecomCallLogCache), null /* voicemailPlaybackPresenter */, null /* filteredNumberAsyncQueryHandler */, new View(context), new QuickContactBadge(context), new View(context), PhoneCallDetailsViews.createForTest(context), new CardView(context), new TextView(context), new ImageView(context)); viewHolder.detailsButtonView = new TextView(context); viewHolder.actionsView = new View(context); viewHolder.voicemailPlaybackView = new VoicemailPlaybackLayout(context); return viewHolder; } }