/* * 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.app.calllog; import android.app.Activity; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.net.Uri; import android.os.AsyncTask; import android.provider.CallLog; import android.provider.CallLog.Calls; import android.provider.ContactsContract.CommonDataKinds.Phone; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.v7.widget.CardView; import android.support.v7.widget.RecyclerView; import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; import android.telephony.PhoneNumberUtils; import android.text.BidiFormatter; import android.text.TextDirectionHeuristics; import android.text.TextUtils; import android.view.ContextMenu; import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewStub; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import com.android.contacts.common.ClipboardUtils; import com.android.contacts.common.ContactPhotoManager; import com.android.contacts.common.compat.PhoneNumberUtilsCompat; import com.android.contacts.common.dialog.CallSubjectDialog; import com.android.contacts.common.lettertiles.LetterTileDrawable; import com.android.contacts.common.lettertiles.LetterTileDrawable.ContactType; import com.android.contacts.common.util.UriUtils; import com.android.dialer.app.DialtactsActivity; import com.android.dialer.app.R; import com.android.dialer.app.calllog.CallLogAdapter.OnActionModeStateChangedListener; import com.android.dialer.app.calllog.calllogcache.CallLogCache; import com.android.dialer.app.voicemail.VoicemailPlaybackLayout; import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter; import com.android.dialer.blocking.BlockedNumbersMigrator; import com.android.dialer.blocking.FilteredNumberCompat; import com.android.dialer.blocking.FilteredNumbersUtil; import com.android.dialer.callcomposer.CallComposerActivity; import com.android.dialer.calldetails.CallDetailsActivity; import com.android.dialer.calldetails.CallDetailsEntries; import com.android.dialer.common.Assert; import com.android.dialer.common.LogUtil; import com.android.dialer.compat.CompatUtils; import com.android.dialer.configprovider.ConfigProviderBindings; import com.android.dialer.dialercontact.DialerContact; import com.android.dialer.dialercontact.SimDetails; import com.android.dialer.lightbringer.Lightbringer; import com.android.dialer.lightbringer.LightbringerComponent; import com.android.dialer.logging.ContactSource; import com.android.dialer.logging.DialerImpression; import com.android.dialer.logging.InteractionEvent; import com.android.dialer.logging.Logger; import com.android.dialer.logging.ScreenEvent; import com.android.dialer.logging.UiAction; import com.android.dialer.performancereport.PerformanceReport; import com.android.dialer.phonenumbercache.CachedNumberLookupService; import com.android.dialer.phonenumbercache.ContactInfo; import com.android.dialer.phonenumbercache.PhoneNumberCache; import com.android.dialer.phonenumberutil.PhoneNumberHelper; import com.android.dialer.telecom.TelecomUtil; import com.android.dialer.util.CallUtil; import com.android.dialer.util.DialerUtils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * 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 DialerQuickContactBadge 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;
private final Context mContext;
@Nullable private final PhoneAccountHandle mDefaultPhoneAccountHandle;
private final CallLogCache mCallLogCache;
private final CallLogListItemHelper mCallLogListItemHelper;
private final CachedNumberLookupService mCachedNumberLookupService;
private final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
private final OnClickListener mBlockReportListener;
@HostUi private final int hostUi;
/** Whether the data fields are populated by the worker thread, ready to be shown. */
public boolean isLoaded;
/** 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 blockReportView;
public View blockView;
public View unblockView;
public View reportNotSpamView;
public View detailsButtonView;
public View callWithNoteButtonView;
public View callComposeButtonView;
public View sendVoicemailButtonView;
public ImageView workIconView;
public ImageView checkBoxView;
/**
* 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 post-dial numbers that are dialed following the phone number. */
public String postDialDigits;
/** 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.
*/
@Nullable public CharSequence nameOrNumber;
/**
* The call type or Location associated with the call. Cached here for use when setting text for a
* voicemail log's call button
*/
public CharSequence callTypeOrLocation;
/** The contact info for the contact displayed in this list item. */
public volatile ContactInfo info;
/** Whether spam feature is enabled, which affects UI. */
public boolean isSpamFeatureEnabled;
/** Whether the current log entry is a spam number or not. */
public boolean isSpam;
public boolean isCallComposerCapable;
public boolean lightbringerReady;
private View.OnClickListener mExpandCollapseListener;
private final OnActionModeStateChangedListener onActionModeStateChangedListener;
private final View.OnLongClickListener longPressListener;
private boolean mVoicemailPrimaryActionButtonClicked;
public int dayGroupHeaderVisibility;
public CharSequence dayGroupHeaderText;
public boolean isAttachedToWindow;
public AsyncTask If the action views have never been shown yet for this view, inflate the view stub.
*/
public void showActions(boolean show) {
showOrHideVoicemailTranscriptionView(show);
if (show) {
if (!isLoaded) {
// b/31268128 for some unidentified reason showActions() can be called before the item is
// loaded, causing NPE on uninitialized fields. Just log and return here, showActions() will
// be called again once the item is loaded.
LogUtil.e(
"CallLogListItemViewHolder.showActions",
"called before item is loaded",
new Exception());
return;
}
// Inflate the view stub if necessary, and wire up the event handlers.
inflateActionViewStub();
bindActionButtons();
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);
}
private void showOrHideVoicemailTranscriptionView(boolean isExpanded) {
if (callType != Calls.VOICEMAIL_TYPE) {
return;
}
TextView transcriptView = phoneCallDetailsViews.voicemailTranscriptionView;
TextView transcriptBrandingView = phoneCallDetailsViews.voicemailTranscriptionBrandingView;
if (!isExpanded || TextUtils.isEmpty(transcriptView.getText())) {
Assert.checkArgument(TextUtils.isEmpty(transcriptBrandingView.getText()));
transcriptView.setVisibility(View.GONE);
transcriptBrandingView.setVisibility(View.GONE);
return;
}
transcriptView.setVisibility(View.VISIBLE);
if (TextUtils.isEmpty(transcriptBrandingView.getText())) {
phoneCallDetailsViews.voicemailTranscriptionBrandingView.setVisibility(View.GONE);
} else {
phoneCallDetailsViews.voicemailTranscriptionBrandingView.setVisibility(View.VISIBLE);
}
}
public void updatePhoto() {
quickContactView.assignContactUri(info.lookupUri);
if (isSpamFeatureEnabled && isSpam) {
quickContactView.setImageDrawable(mContext.getDrawable(R.drawable.blocked_contact));
return;
}
final String displayName = TextUtils.isEmpty(info.name) ? displayNumber : info.name;
ContactPhotoManager.getInstance(mContext)
.loadDialerThumbnailOrPhoto(
quickContactView,
info.lookupUri,
info.photoId,
info.photoUri,
displayName,
getContactType());
}
private @ContactType int getContactType() {
return LetterTileDrawable.getContactTypeFromPrimitives(
mCallLogCache.isVoicemailNumber(accountHandle, number),
isSpam,
mCachedNumberLookupService != null
&& mCachedNumberLookupService.isBusiness(info.sourceType),
numberPresentation,
false);
}
@Override
public void onClick(View view) {
if (view.getId() == R.id.primary_action_button) {
CallLogAsyncTaskUtil.markCallAsRead(mContext, callIds);
}
if (view.getId() == R.id.primary_action_button && !TextUtils.isEmpty(voicemailUri)) {
Logger.get(mContext).logImpression(DialerImpression.Type.VOICEMAIL_PLAY_AUDIO_DIRECTLY);
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 */,
number,
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 */
getContactType(),
accountHandle);
} else if (view.getId() == R.id.block_report_action) {
Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_BLOCK_REPORT_SPAM);
maybeShowBlockNumberMigrationDialog(
new BlockedNumbersMigrator.Listener() {
@Override
public void onComplete() {
mBlockReportListener.onBlockReportSpam(
displayNumber, number, countryIso, callType, info.sourceType);
}
});
} else if (view.getId() == R.id.block_action) {
Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_BLOCK_NUMBER);
maybeShowBlockNumberMigrationDialog(
new BlockedNumbersMigrator.Listener() {
@Override
public void onComplete() {
mBlockReportListener.onBlock(
displayNumber, number, countryIso, callType, info.sourceType);
}
});
} else if (view.getId() == R.id.unblock_action) {
Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_UNBLOCK_NUMBER);
mBlockReportListener.onUnblock(
displayNumber, number, countryIso, callType, info.sourceType, isSpam, blockId);
} else if (view.getId() == R.id.report_not_spam_action) {
Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_REPORT_AS_NOT_SPAM);
mBlockReportListener.onReportNotSpam(
displayNumber, number, countryIso, callType, info.sourceType);
} else if (view.getId() == R.id.call_compose_action) {
LogUtil.i("CallLogListItemViewHolder.onClick", "share and call pressed");
Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_SHARE_AND_CALL);
Activity activity = (Activity) mContext;
activity.startActivityForResult(
CallComposerActivity.newIntent(activity, buildContact()),
DialtactsActivity.ACTIVITY_REQUEST_CODE_CALL_COMPOSE);
} else if (view.getId() == R.id.share_voicemail) {
Logger.get(mContext).logImpression(DialerImpression.Type.VVM_SHARE_PRESSED);
mVoicemailPlaybackPresenter.shareVoicemail();
} else {
logCallLogAction(view.getId());
final IntentProvider intentProvider = (IntentProvider) view.getTag();
if (intentProvider == null) {
return;
}
final Intent intent = intentProvider.getIntent(mContext);
// See IntentProvider.getCallDetailIntentProvider() for why this may be null.
if (intent == null) {
return;
}
// We check to see if we are starting a Lightbringer intent. The reason is Lightbringer
// intents need to be started using startActivityForResult instead of the usual startActivity
String packageName = intent.getPackage();
if (packageName != null && packageName.equals(getLightbringer().getPackageName())) {
startLightbringerActivity(intent);
} else if (CallDetailsActivity.isLaunchIntent(intent)) {
PerformanceReport.recordClick(UiAction.Type.OPEN_CALL_DETAIL);
((Activity) mContext)
.startActivityForResult(intent, DialtactsActivity.ACTIVITY_REQUEST_CODE_CALL_DETAILS);
} else {
DialerUtils.startActivityWithErrorToast(mContext, intent);
}
}
}
private void startLightbringerActivity(Intent intent) {
try {
Activity activity = (Activity) mContext;
activity.startActivityForResult(intent, DialtactsActivity.ACTIVITY_REQUEST_CODE_LIGHTBRINGER);
} catch (ActivityNotFoundException e) {
Toast.makeText(mContext, R.string.activity_not_available, Toast.LENGTH_SHORT).show();
}
}
private DialerContact buildContact() {
DialerContact.Builder contact = DialerContact.newBuilder();
contact.setPhotoId(info.photoId);
if (info.photoUri != null) {
contact.setPhotoUri(info.photoUri.toString());
}
if (info.lookupUri != null) {
contact.setContactUri(info.lookupUri.toString());
}
if (nameOrNumber != null) {
contact.setNameOrNumber((String) nameOrNumber);
}
contact.setContactType(getContactType());
contact.setNumber(number);
/* second line of contact view. */
if (!TextUtils.isEmpty(info.name)) {
contact.setDisplayNumber(displayNumber);
}
/* phone number type (e.g. mobile) in second line of contact view */
contact.setNumberLabel(numberType);
/* third line of contact view. */
String accountLabel = mCallLogCache.getAccountLabel(accountHandle);
if (!TextUtils.isEmpty(accountLabel)) {
SimDetails.Builder simDetails = SimDetails.newBuilder().setNetwork(accountLabel);
int color = mCallLogCache.getAccountColor(accountHandle);
if (color == PhoneAccount.NO_HIGHLIGHT_COLOR) {
simDetails.setColor(R.color.secondary_text_color);
} else {
simDetails.setColor(color);
}
contact.setSimDetails(simDetails.build());
}
return contact.build();
}
private void logCallLogAction(int id) {
if (id == R.id.send_message_action) {
Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_SEND_MESSAGE);
} else if (id == R.id.add_to_existing_contact_action) {
Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_ADD_TO_CONTACT);
switch (hostUi) {
case HostUi.CALL_HISTORY:
Logger.get(mContext)
.logImpression(DialerImpression.Type.ADD_TO_A_CONTACT_FROM_CALL_HISTORY);
break;
case HostUi.CALL_LOG:
Logger.get(mContext).logImpression(DialerImpression.Type.ADD_TO_A_CONTACT_FROM_CALL_LOG);
break;
case HostUi.VOICEMAIL:
Logger.get(mContext).logImpression(DialerImpression.Type.ADD_TO_A_CONTACT_FROM_VOICEMAIL);
break;
default:
throw Assert.createIllegalStateFailException();
}
} else if (id == R.id.create_new_contact_action) {
Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_CREATE_NEW_CONTACT);
switch (hostUi) {
case HostUi.CALL_HISTORY:
Logger.get(mContext)
.logImpression(DialerImpression.Type.CREATE_NEW_CONTACT_FROM_CALL_HISTORY);
break;
case HostUi.CALL_LOG:
Logger.get(mContext)
.logImpression(DialerImpression.Type.CREATE_NEW_CONTACT_FROM_CALL_LOG);
break;
case HostUi.VOICEMAIL:
Logger.get(mContext)
.logImpression(DialerImpression.Type.CREATE_NEW_CONTACT_FROM_VOICEMAIL);
break;
default:
throw Assert.createIllegalStateFailException();
}
}
}
private void maybeShowBlockNumberMigrationDialog(BlockedNumbersMigrator.Listener listener) {
if (!FilteredNumberCompat.maybeShowBlockNumberMigrationDialog(
mContext, ((Activity) mContext).getFragmentManager(), listener)) {
listener.onComplete();
}
}
private void updateBlockReportActions(boolean isVoicemailNumber) {
// Set block/spam actions.
blockReportView.setVisibility(View.GONE);
blockView.setVisibility(View.GONE);
unblockView.setVisibility(View.GONE);
reportNotSpamView.setVisibility(View.GONE);
String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso);
if (isVoicemailNumber
|| !FilteredNumbersUtil.canBlockNumber(mContext, e164Number, number)
|| !FilteredNumberCompat.canAttemptBlockOperations(mContext)) {
return;
}
boolean isBlocked = blockId != null;
if (isBlocked) {
unblockView.setVisibility(View.VISIBLE);
} else {
if (isSpamFeatureEnabled) {
if (isSpam) {
blockView.setVisibility(View.VISIBLE);
reportNotSpamView.setVisibility(View.VISIBLE);
} else {
blockReportView.setVisibility(View.VISIBLE);
}
} else {
blockView.setVisibility(View.VISIBLE);
}
}
}
public void setDetailedPhoneDetails(CallDetailsEntries callDetailsEntries) {
this.callDetailsEntries = callDetailsEntries;
}
@VisibleForTesting
public CallDetailsEntries getDetailedPhoneDetails() {
return callDetailsEntries;
}
@NonNull
private Lightbringer getLightbringer() {
return LightbringerComponent.get(mContext).getLightbringer();
}
@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(
PhoneNumberUtilsCompat.createTtsSpannable(
BidiFormatter.getInstance().unicodeWrap(number, TextDirectionHeuristics.LTR)));
}
menu.add(
ContextMenu.NONE,
R.id.context_menu_copy_to_clipboard,
ContextMenu.NONE,
R.string.action_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 (PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation)
&& !mCallLogCache.isVoicemailNumber(accountHandle, number)
&& !PhoneNumberHelper.isSipNumber(number)) {
menu.add(
ContextMenu.NONE,
R.id.context_menu_edit_before_call,
ContextMenu.NONE,
R.string.action_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);
}
String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso);
boolean isVoicemailNumber = mCallLogCache.isVoicemailNumber(accountHandle, number);
if (!isVoicemailNumber
&& FilteredNumbersUtil.canBlockNumber(mContext, e164Number, number)
&& FilteredNumberCompat.canAttemptBlockOperations(mContext)) {
boolean isBlocked = blockId != null;
if (isBlocked) {
menu.add(
ContextMenu.NONE,
R.id.context_menu_unblock,
ContextMenu.NONE,
R.string.call_log_action_unblock_number)
.setOnMenuItemClickListener(this);
} else {
if (isSpamFeatureEnabled) {
if (isSpam) {
menu.add(
ContextMenu.NONE,
R.id.context_menu_report_not_spam,
ContextMenu.NONE,
R.string.call_log_action_remove_spam)
.setOnMenuItemClickListener(this);
menu.add(
ContextMenu.NONE,
R.id.context_menu_block,
ContextMenu.NONE,
R.string.call_log_action_block_number)
.setOnMenuItemClickListener(this);
} else {
menu.add(
ContextMenu.NONE,
R.id.context_menu_block_report_spam,
ContextMenu.NONE,
R.string.call_log_action_block_report_number)
.setOnMenuItemClickListener(this);
}
} else {
menu.add(
ContextMenu.NONE,
R.id.context_menu_block,
ContextMenu.NONE,
R.string.call_log_action_block_number)
.setOnMenuItemClickListener(this);
}
}
}
Logger.get(mContext).logScreenView(ScreenEvent.Type.CALL_LOG_CONTEXT_MENU, (Activity) mContext);
}
/** Specifies where the view holder belongs. */
@IntDef({HostUi.CALL_LOG, HostUi.CALL_HISTORY, HostUi.VOICEMAIL})
@Retention(RetentionPolicy.SOURCE)
private @interface HostUi {
int CALL_LOG = 0;
int CALL_HISTORY = 1;
int VOICEMAIL = 2;
}
public interface OnClickListener {
void onBlockReportSpam(
String displayNumber,
String number,
String countryIso,
int callType,
ContactSource.Type contactSourceType);
void onBlock(
String displayNumber,
String number,
String countryIso,
int callType,
ContactSource.Type contactSourceType);
void onUnblock(
String displayNumber,
String number,
String countryIso,
int callType,
ContactSource.Type contactSourceType,
boolean isSpam,
Integer blockId);
void onReportNotSpam(
String displayNumber,
String number,
String countryIso,
int callType,
ContactSource.Type contactSourceType);
}
}