Home | History | Annotate | Download | only in calllog
      1 /*
      2  * Copyright (C) 2011 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.dialer.app.calllog;
     18 
     19 import android.app.Activity;
     20 import android.content.Context;
     21 import android.content.Intent;
     22 import android.content.res.Resources;
     23 import android.net.Uri;
     24 import android.os.AsyncTask;
     25 import android.provider.CallLog;
     26 import android.provider.CallLog.Calls;
     27 import android.provider.ContactsContract.CommonDataKinds.Phone;
     28 import android.support.annotation.NonNull;
     29 import android.support.annotation.Nullable;
     30 import android.support.annotation.VisibleForTesting;
     31 import android.support.v7.widget.CardView;
     32 import android.support.v7.widget.RecyclerView;
     33 import android.telecom.PhoneAccountHandle;
     34 import android.telecom.TelecomManager;
     35 import android.telephony.PhoneNumberUtils;
     36 import android.text.BidiFormatter;
     37 import android.text.TextDirectionHeuristics;
     38 import android.text.TextUtils;
     39 import android.view.ContextMenu;
     40 import android.view.MenuItem;
     41 import android.view.View;
     42 import android.view.ViewStub;
     43 import android.widget.ImageButton;
     44 import android.widget.ImageView;
     45 import android.widget.QuickContactBadge;
     46 import android.widget.TextView;
     47 import com.android.contacts.common.ClipboardUtils;
     48 import com.android.contacts.common.ContactPhotoManager;
     49 import com.android.contacts.common.compat.PhoneNumberUtilsCompat;
     50 import com.android.contacts.common.dialog.CallSubjectDialog;
     51 import com.android.contacts.common.util.UriUtils;
     52 import com.android.dialer.app.DialtactsActivity;
     53 import com.android.dialer.app.R;
     54 import com.android.dialer.app.calllog.calllogcache.CallLogCache;
     55 import com.android.dialer.app.voicemail.VoicemailPlaybackLayout;
     56 import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter;
     57 import com.android.dialer.blocking.BlockedNumbersMigrator;
     58 import com.android.dialer.blocking.FilteredNumberCompat;
     59 import com.android.dialer.blocking.FilteredNumbersUtil;
     60 import com.android.dialer.callcomposer.CallComposerActivity;
     61 import com.android.dialer.callcomposer.CallComposerContact;
     62 import com.android.dialer.calldetails.CallDetailsEntries;
     63 import com.android.dialer.common.ConfigProviderBindings;
     64 import com.android.dialer.common.LogUtil;
     65 import com.android.dialer.compat.CompatUtils;
     66 import com.android.dialer.lightbringer.Lightbringer;
     67 import com.android.dialer.lightbringer.LightbringerComponent;
     68 import com.android.dialer.logging.ContactSource;
     69 import com.android.dialer.logging.DialerImpression;
     70 import com.android.dialer.logging.Logger;
     71 import com.android.dialer.logging.ScreenEvent;
     72 import com.android.dialer.phonenumbercache.CachedNumberLookupService;
     73 import com.android.dialer.phonenumbercache.ContactInfo;
     74 import com.android.dialer.phonenumbercache.PhoneNumberCache;
     75 import com.android.dialer.phonenumberutil.PhoneNumberHelper;
     76 import com.android.dialer.util.CallUtil;
     77 import com.android.dialer.util.DialerUtils;
     78 
     79 /**
     80  * This is an object containing references to views contained by the call log list item. This
     81  * improves performance by reducing the frequency with which we need to find views by IDs.
     82  *
     83  * <p>This object also contains UI logic pertaining to the view, to isolate it from the
     84  * CallLogAdapter.
     85  */
     86 public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
     87     implements View.OnClickListener,
     88         MenuItem.OnMenuItemClickListener,
     89         View.OnCreateContextMenuListener {
     90   /** The root view of the call log list item */
     91   public final View rootView;
     92   /** The quick contact badge for the contact. */
     93   public final QuickContactBadge quickContactView;
     94   /** The primary action view of the entry. */
     95   public final View primaryActionView;
     96   /** The details of the phone call. */
     97   public final PhoneCallDetailsViews phoneCallDetailsViews;
     98   /** The text of the header for a day grouping. */
     99   public final TextView dayGroupHeader;
    100   /** The view containing the details for the call log row, including the action buttons. */
    101   public final CardView callLogEntryView;
    102   /** The actionable view which places a call to the number corresponding to the call log row. */
    103   public final ImageView primaryActionButtonView;
    104 
    105   private final Context mContext;
    106   private final CallLogCache mCallLogCache;
    107   private final CallLogListItemHelper mCallLogListItemHelper;
    108   private final CachedNumberLookupService mCachedNumberLookupService;
    109   private final VoicemailPlaybackPresenter mVoicemailPlaybackPresenter;
    110   private final OnClickListener mBlockReportListener;
    111   /** Whether the data fields are populated by the worker thread, ready to be shown. */
    112   public boolean isLoaded;
    113   /** The view containing call log item actions. Null until the ViewStub is inflated. */
    114   public View actionsView;
    115   /** The button views below are assigned only when the action section is expanded. */
    116   public VoicemailPlaybackLayout voicemailPlaybackView;
    117 
    118   public View callButtonView;
    119   public View videoCallButtonView;
    120   public View createNewContactButtonView;
    121   public View addToExistingContactButtonView;
    122   public View sendMessageView;
    123   public View blockReportView;
    124   public View blockView;
    125   public View unblockView;
    126   public View reportNotSpamView;
    127   public View detailsButtonView;
    128   public View callWithNoteButtonView;
    129   public View callComposeButtonView;
    130   public View sendVoicemailButtonView;
    131   public ImageView workIconView;
    132   public ImageView checkBoxView;
    133   /**
    134    * The row Id for the first call associated with the call log entry. Used as a key for the map
    135    * used to track which call log entries have the action button section expanded.
    136    */
    137   public long rowId;
    138   /**
    139    * The call Ids for the calls represented by the current call log entry. Used when the user
    140    * deletes a call log entry.
    141    */
    142   public long[] callIds;
    143   /**
    144    * The callable phone number for the current call log entry. Cached here as the call back intent
    145    * is set only when the actions ViewStub is inflated.
    146    */
    147   public String number;
    148   /** The post-dial numbers that are dialed following the phone number. */
    149   public String postDialDigits;
    150   /** The formatted phone number to display. */
    151   public String displayNumber;
    152   /**
    153    * The phone number presentation for the current call log entry. Cached here as the call back
    154    * intent is set only when the actions ViewStub is inflated.
    155    */
    156   public int numberPresentation;
    157   /** The type of the phone number (e.g. main, work, etc). */
    158   public String numberType;
    159   /**
    160    * The country iso for the call. Cached here as the call back intent is set only when the actions
    161    * ViewStub is inflated.
    162    */
    163   public String countryIso;
    164   /**
    165    * The type of call for the current call log entry. Cached here as the call back intent is set
    166    * only when the actions ViewStub is inflated.
    167    */
    168   public int callType;
    169   /**
    170    * ID for blocked numbers database. Set when context menu is created, if the number is blocked.
    171    */
    172   public Integer blockId;
    173   /**
    174    * The account for the current call log entry. Cached here as the call back intent is set only
    175    * when the actions ViewStub is inflated.
    176    */
    177   public PhoneAccountHandle accountHandle;
    178   /**
    179    * If the call has an associated voicemail message, the URI of the voicemail message for playback.
    180    * Cached here as the voicemail intent is only set when the actions ViewStub is inflated.
    181    */
    182   public String voicemailUri;
    183   /**
    184    * The name or number associated with the call. Cached here for use when setting content
    185    * descriptions on buttons in the actions ViewStub when it is inflated.
    186    */
    187   @Nullable public CharSequence nameOrNumber;
    188   /**
    189    * The call type or Location associated with the call. Cached here for use when setting text for a
    190    * voicemail log's call button
    191    */
    192   public CharSequence callTypeOrLocation;
    193   /** The contact info for the contact displayed in this list item. */
    194   public volatile ContactInfo info;
    195   /** Whether spam feature is enabled, which affects UI. */
    196   public boolean isSpamFeatureEnabled;
    197   /** Whether the current log entry is a spam number or not. */
    198   public boolean isSpam;
    199 
    200   public boolean isCallComposerCapable;
    201   public boolean lightbringerReady;
    202 
    203   private View.OnClickListener mExpandCollapseListener;
    204   private final View.OnLongClickListener longPressListener;
    205   private boolean mVoicemailPrimaryActionButtonClicked;
    206 
    207   public int dayGroupHeaderVisibility;
    208   public CharSequence dayGroupHeaderText;
    209   public boolean isAttachedToWindow;
    210 
    211   public AsyncTask<Void, Void, ?> asyncTask;
    212   private CallDetailsEntries callDetailsEntries;
    213 
    214   private CallLogListItemViewHolder(
    215       Context context,
    216       OnClickListener blockReportListener,
    217       View.OnClickListener expandCollapseListener,
    218       View.OnLongClickListener longClickListener,
    219       CallLogCache callLogCache,
    220       CallLogListItemHelper callLogListItemHelper,
    221       VoicemailPlaybackPresenter voicemailPlaybackPresenter,
    222       View rootView,
    223       QuickContactBadge quickContactView,
    224       View primaryActionView,
    225       PhoneCallDetailsViews phoneCallDetailsViews,
    226       CardView callLogEntryView,
    227       TextView dayGroupHeader,
    228       ImageView primaryActionButtonView) {
    229     super(rootView);
    230 
    231     mContext = context;
    232     mExpandCollapseListener = expandCollapseListener;
    233     longPressListener = longClickListener;
    234     mCallLogCache = callLogCache;
    235     mCallLogListItemHelper = callLogListItemHelper;
    236     mVoicemailPlaybackPresenter = voicemailPlaybackPresenter;
    237     mBlockReportListener = blockReportListener;
    238     mCachedNumberLookupService = PhoneNumberCache.get(mContext).getCachedNumberLookupService();
    239 
    240     this.rootView = rootView;
    241     this.quickContactView = quickContactView;
    242     this.primaryActionView = primaryActionView;
    243     this.phoneCallDetailsViews = phoneCallDetailsViews;
    244     this.callLogEntryView = callLogEntryView;
    245     this.dayGroupHeader = dayGroupHeader;
    246     this.primaryActionButtonView = primaryActionButtonView;
    247     this.workIconView = (ImageView) rootView.findViewById(R.id.work_profile_icon);
    248     this.checkBoxView = (ImageView) rootView.findViewById(R.id.quick_contact_checkbox);
    249 
    250     // Set text height to false on the TextViews so they don't have extra padding.
    251     phoneCallDetailsViews.nameView.setElegantTextHeight(false);
    252     phoneCallDetailsViews.callLocationAndDate.setElegantTextHeight(false);
    253 
    254     quickContactView.setOverlay(null);
    255     if (CompatUtils.hasPrioritizedMimeType()) {
    256       quickContactView.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE);
    257     }
    258     primaryActionButtonView.setOnClickListener(this);
    259     primaryActionView.setOnClickListener(mExpandCollapseListener);
    260     if (mVoicemailPlaybackPresenter != null
    261         && ConfigProviderBindings.get(mContext)
    262             .getBoolean(
    263                 CallLogAdapter.ENABLE_CALL_LOG_MULTI_SELECT,
    264                 CallLogAdapter.ENABLE_CALL_LOG_MULTI_SELECT_FLAG)) {
    265       primaryActionView.setOnLongClickListener(longPressListener);
    266       quickContactView.setOnLongClickListener(longPressListener);
    267     } else {
    268       primaryActionView.setOnCreateContextMenuListener(this);
    269     }
    270   }
    271 
    272   public static CallLogListItemViewHolder create(
    273       View view,
    274       Context context,
    275       OnClickListener blockReportListener,
    276       View.OnClickListener expandCollapseListener,
    277       View.OnLongClickListener longClickListener,
    278       CallLogCache callLogCache,
    279       CallLogListItemHelper callLogListItemHelper,
    280       VoicemailPlaybackPresenter voicemailPlaybackPresenter) {
    281 
    282     return new CallLogListItemViewHolder(
    283         context,
    284         blockReportListener,
    285         expandCollapseListener,
    286         longClickListener,
    287         callLogCache,
    288         callLogListItemHelper,
    289         voicemailPlaybackPresenter,
    290         view,
    291         (QuickContactBadge) view.findViewById(R.id.quick_contact_photo),
    292         view.findViewById(R.id.primary_action_view),
    293         PhoneCallDetailsViews.fromView(view),
    294         (CardView) view.findViewById(R.id.call_log_row),
    295         (TextView) view.findViewById(R.id.call_log_day_group_label),
    296         (ImageView) view.findViewById(R.id.primary_action_button));
    297   }
    298 
    299   public static CallLogListItemViewHolder createForTest(Context context) {
    300     Resources resources = context.getResources();
    301     CallLogCache callLogCache = CallLogCache.getCallLogCache(context);
    302     PhoneCallDetailsHelper phoneCallDetailsHelper =
    303         new PhoneCallDetailsHelper(context, resources, callLogCache);
    304 
    305     CallLogListItemViewHolder viewHolder =
    306         new CallLogListItemViewHolder(
    307             context,
    308             null,
    309             null /* expandCollapseListener */,
    310             null,
    311             callLogCache,
    312             new CallLogListItemHelper(phoneCallDetailsHelper, resources, callLogCache),
    313             null /* voicemailPlaybackPresenter */,
    314             new View(context),
    315             new QuickContactBadge(context),
    316             new View(context),
    317             PhoneCallDetailsViews.createForTest(context),
    318             new CardView(context),
    319             new TextView(context),
    320             new ImageView(context));
    321     viewHolder.detailsButtonView = new TextView(context);
    322     viewHolder.actionsView = new View(context);
    323     viewHolder.voicemailPlaybackView = new VoicemailPlaybackLayout(context);
    324     viewHolder.workIconView = new ImageButton(context);
    325     viewHolder.checkBoxView = new ImageButton(context);
    326     return viewHolder;
    327   }
    328 
    329   @Override
    330   public boolean onMenuItemClick(MenuItem item) {
    331     int resId = item.getItemId();
    332     if (resId == R.id.context_menu_copy_to_clipboard) {
    333       ClipboardUtils.copyText(mContext, null, number, true);
    334       return true;
    335     } else if (resId == R.id.context_menu_copy_transcript_to_clipboard) {
    336       ClipboardUtils.copyText(
    337           mContext, null, phoneCallDetailsViews.voicemailTranscriptionView.getText(), true);
    338       return true;
    339     } else if (resId == R.id.context_menu_edit_before_call) {
    340       final Intent intent = new Intent(Intent.ACTION_DIAL, CallUtil.getCallUri(number));
    341       intent.setClass(mContext, DialtactsActivity.class);
    342       DialerUtils.startActivityWithErrorToast(mContext, intent);
    343       return true;
    344     } else if (resId == R.id.context_menu_block_report_spam) {
    345       Logger.get(mContext)
    346           .logImpression(DialerImpression.Type.CALL_LOG_CONTEXT_MENU_BLOCK_REPORT_SPAM);
    347       maybeShowBlockNumberMigrationDialog(
    348           new BlockedNumbersMigrator.Listener() {
    349             @Override
    350             public void onComplete() {
    351               mBlockReportListener.onBlockReportSpam(
    352                   displayNumber, number, countryIso, callType, info.sourceType);
    353             }
    354           });
    355     } else if (resId == R.id.context_menu_block) {
    356       Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_CONTEXT_MENU_BLOCK_NUMBER);
    357       maybeShowBlockNumberMigrationDialog(
    358           new BlockedNumbersMigrator.Listener() {
    359             @Override
    360             public void onComplete() {
    361               mBlockReportListener.onBlock(
    362                   displayNumber, number, countryIso, callType, info.sourceType);
    363             }
    364           });
    365     } else if (resId == R.id.context_menu_unblock) {
    366       Logger.get(mContext)
    367           .logImpression(DialerImpression.Type.CALL_LOG_CONTEXT_MENU_UNBLOCK_NUMBER);
    368       mBlockReportListener.onUnblock(
    369           displayNumber, number, countryIso, callType, info.sourceType, isSpam, blockId);
    370     } else if (resId == R.id.context_menu_report_not_spam) {
    371       Logger.get(mContext)
    372           .logImpression(DialerImpression.Type.CALL_LOG_CONTEXT_MENU_REPORT_AS_NOT_SPAM);
    373       mBlockReportListener.onReportNotSpam(
    374           displayNumber, number, countryIso, callType, info.sourceType);
    375     }
    376     return false;
    377   }
    378 
    379   /**
    380    * Configures the action buttons in the expandable actions ViewStub. The ViewStub is not inflated
    381    * during initial binding, so click handlers, tags and accessibility text must be set here, if
    382    * necessary.
    383    */
    384   public void inflateActionViewStub() {
    385     ViewStub stub = (ViewStub) rootView.findViewById(R.id.call_log_entry_actions_stub);
    386     if (stub != null) {
    387       actionsView = stub.inflate();
    388 
    389       voicemailPlaybackView =
    390           (VoicemailPlaybackLayout) actionsView.findViewById(R.id.voicemail_playback_layout);
    391       voicemailPlaybackView.setViewHolder(this);
    392 
    393       callButtonView = actionsView.findViewById(R.id.call_action);
    394       callButtonView.setOnClickListener(this);
    395 
    396       videoCallButtonView = actionsView.findViewById(R.id.video_call_action);
    397       videoCallButtonView.setOnClickListener(this);
    398 
    399       createNewContactButtonView = actionsView.findViewById(R.id.create_new_contact_action);
    400       createNewContactButtonView.setOnClickListener(this);
    401 
    402       addToExistingContactButtonView =
    403           actionsView.findViewById(R.id.add_to_existing_contact_action);
    404       addToExistingContactButtonView.setOnClickListener(this);
    405 
    406       sendMessageView = actionsView.findViewById(R.id.send_message_action);
    407       sendMessageView.setOnClickListener(this);
    408 
    409       blockReportView = actionsView.findViewById(R.id.block_report_action);
    410       blockReportView.setOnClickListener(this);
    411 
    412       blockView = actionsView.findViewById(R.id.block_action);
    413       blockView.setOnClickListener(this);
    414 
    415       unblockView = actionsView.findViewById(R.id.unblock_action);
    416       unblockView.setOnClickListener(this);
    417 
    418       reportNotSpamView = actionsView.findViewById(R.id.report_not_spam_action);
    419       reportNotSpamView.setOnClickListener(this);
    420 
    421       detailsButtonView = actionsView.findViewById(R.id.details_action);
    422       detailsButtonView.setOnClickListener(this);
    423 
    424       callWithNoteButtonView = actionsView.findViewById(R.id.call_with_note_action);
    425       callWithNoteButtonView.setOnClickListener(this);
    426 
    427       callComposeButtonView = actionsView.findViewById(R.id.call_compose_action);
    428       callComposeButtonView.setOnClickListener(this);
    429 
    430       sendVoicemailButtonView = actionsView.findViewById(R.id.share_voicemail);
    431       sendVoicemailButtonView.setOnClickListener(this);
    432     }
    433   }
    434 
    435   private void updatePrimaryActionButton(boolean isExpanded) {
    436 
    437     if (nameOrNumber == null) {
    438       LogUtil.e("CallLogListItemViewHolder.updatePrimaryActionButton", "name or number is null");
    439     }
    440 
    441     // Calling expandTemplate with a null parameter will cause a NullPointerException.
    442     CharSequence validNameOrNumber = nameOrNumber == null ? "" : nameOrNumber;
    443 
    444     if (!TextUtils.isEmpty(voicemailUri)) {
    445       // Treat as voicemail list item; show play button if not expanded.
    446       if (!isExpanded) {
    447         primaryActionButtonView.setImageResource(R.drawable.quantum_ic_play_arrow_white_24);
    448         primaryActionButtonView.setContentDescription(
    449             TextUtils.expandTemplate(
    450                 mContext.getString(R.string.description_voicemail_action), validNameOrNumber));
    451         primaryActionButtonView.setVisibility(View.VISIBLE);
    452       } else {
    453         primaryActionButtonView.setVisibility(View.GONE);
    454       }
    455     } else {
    456       // Treat as normal list item; show call button, if possible.
    457       if (PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation)) {
    458         boolean isVoicemailNumber = mCallLogCache.isVoicemailNumber(accountHandle, number);
    459         if (isVoicemailNumber) {
    460           // Call to generic voicemail number, in case there are multiple accounts.
    461           primaryActionButtonView.setTag(IntentProvider.getReturnVoicemailCallIntentProvider());
    462         } else {
    463           primaryActionButtonView.setTag(
    464               IntentProvider.getReturnCallIntentProvider(number + postDialDigits));
    465         }
    466 
    467         primaryActionButtonView.setContentDescription(
    468             TextUtils.expandTemplate(
    469                 mContext.getString(R.string.description_call_action), validNameOrNumber));
    470         primaryActionButtonView.setImageResource(R.drawable.quantum_ic_call_white_24);
    471         primaryActionButtonView.setVisibility(View.VISIBLE);
    472       } else {
    473         primaryActionButtonView.setTag(null);
    474         primaryActionButtonView.setVisibility(View.GONE);
    475       }
    476     }
    477   }
    478 
    479   /**
    480    * Binds text titles, click handlers and intents to the voicemail, details and callback action
    481    * buttons.
    482    */
    483   private void bindActionButtons() {
    484     boolean canPlaceCallToNumber = PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation);
    485 
    486     if (isFullyUndialableVoicemail()) {
    487       // Sometimes the voicemail server will report the message is from some non phone number
    488       // source. If the number does not contains any dialable digit treat it as it is from a unknown
    489       // number, remove all action buttons but still show the voicemail playback layout.
    490       callButtonView.setVisibility(View.GONE);
    491       videoCallButtonView.setVisibility(View.GONE);
    492       detailsButtonView.setVisibility(View.GONE);
    493       createNewContactButtonView.setVisibility(View.GONE);
    494       addToExistingContactButtonView.setVisibility(View.GONE);
    495       sendMessageView.setVisibility(View.GONE);
    496       callWithNoteButtonView.setVisibility(View.GONE);
    497       callComposeButtonView.setVisibility(View.GONE);
    498       blockReportView.setVisibility(View.GONE);
    499       blockView.setVisibility(View.GONE);
    500       unblockView.setVisibility(View.GONE);
    501       reportNotSpamView.setVisibility(View.GONE);
    502 
    503       voicemailPlaybackView.setVisibility(View.VISIBLE);
    504       Uri uri = Uri.parse(voicemailUri);
    505       mVoicemailPlaybackPresenter.setPlaybackView(
    506           voicemailPlaybackView,
    507           rowId,
    508           uri,
    509           mVoicemailPrimaryActionButtonClicked,
    510           sendVoicemailButtonView);
    511       mVoicemailPrimaryActionButtonClicked = false;
    512       CallLogAsyncTaskUtil.markVoicemailAsRead(mContext, uri);
    513       return;
    514     }
    515 
    516     if (!TextUtils.isEmpty(voicemailUri) && canPlaceCallToNumber) {
    517       callButtonView.setTag(IntentProvider.getReturnCallIntentProvider(number));
    518       ((TextView) callButtonView.findViewById(R.id.call_action_text))
    519           .setText(
    520               TextUtils.expandTemplate(
    521                   mContext.getString(R.string.call_log_action_call),
    522                   nameOrNumber == null ? "" : nameOrNumber));
    523       TextView callTypeOrLocationView =
    524           ((TextView) callButtonView.findViewById(R.id.call_type_or_location_text));
    525       if (callType == Calls.VOICEMAIL_TYPE && !TextUtils.isEmpty(callTypeOrLocation)) {
    526         callTypeOrLocationView.setText(callTypeOrLocation);
    527         callTypeOrLocationView.setVisibility(View.VISIBLE);
    528       } else {
    529         callTypeOrLocationView.setVisibility(View.GONE);
    530       }
    531       callButtonView.setVisibility(View.VISIBLE);
    532     } else {
    533       callButtonView.setVisibility(View.GONE);
    534     }
    535 
    536     if (hasPlacedCarrierVideoCall() || canSupportCarrierVideoCall()) {
    537       videoCallButtonView.setTag(IntentProvider.getReturnVideoCallIntentProvider(number));
    538       videoCallButtonView.setVisibility(View.VISIBLE);
    539     } else if (lightbringerReady) {
    540       videoCallButtonView.setTag(IntentProvider.getLightbringerIntentProvider(number));
    541       videoCallButtonView.setVisibility(View.VISIBLE);
    542     } else {
    543       videoCallButtonView.setVisibility(View.GONE);
    544     }
    545 
    546     // For voicemail calls, show the voicemail playback layout; hide otherwise.
    547     if (callType == Calls.VOICEMAIL_TYPE
    548         && mVoicemailPlaybackPresenter != null
    549         && !TextUtils.isEmpty(voicemailUri)) {
    550       voicemailPlaybackView.setVisibility(View.VISIBLE);
    551 
    552       Uri uri = Uri.parse(voicemailUri);
    553       mVoicemailPlaybackPresenter.setPlaybackView(
    554           voicemailPlaybackView,
    555           rowId,
    556           uri,
    557           mVoicemailPrimaryActionButtonClicked,
    558           sendVoicemailButtonView);
    559       mVoicemailPrimaryActionButtonClicked = false;
    560       CallLogAsyncTaskUtil.markVoicemailAsRead(mContext, uri);
    561     } else {
    562       voicemailPlaybackView.setVisibility(View.GONE);
    563       sendVoicemailButtonView.setVisibility(View.GONE);
    564     }
    565 
    566     if (callType == Calls.VOICEMAIL_TYPE) {
    567       detailsButtonView.setVisibility(View.GONE);
    568     } else {
    569       detailsButtonView.setVisibility(View.VISIBLE);
    570       detailsButtonView.setTag(
    571           IntentProvider.getCallDetailIntentProvider(callDetailsEntries, buildContact()));
    572     }
    573 
    574     boolean isBlockedOrSpam = blockId != null || (isSpamFeatureEnabled && isSpam);
    575 
    576     if (!isBlockedOrSpam && info != null && UriUtils.isEncodedContactUri(info.lookupUri)) {
    577       createNewContactButtonView.setTag(
    578           IntentProvider.getAddContactIntentProvider(
    579               info.lookupUri, info.name, info.number, info.type, true /* isNewContact */));
    580       createNewContactButtonView.setVisibility(View.VISIBLE);
    581 
    582       addToExistingContactButtonView.setTag(
    583           IntentProvider.getAddContactIntentProvider(
    584               info.lookupUri, info.name, info.number, info.type, false /* isNewContact */));
    585       addToExistingContactButtonView.setVisibility(View.VISIBLE);
    586     } else {
    587       createNewContactButtonView.setVisibility(View.GONE);
    588       addToExistingContactButtonView.setVisibility(View.GONE);
    589     }
    590 
    591     boolean isVoicemailNumber = mCallLogCache.isVoicemailNumber(accountHandle, number);
    592     if (canPlaceCallToNumber && !isBlockedOrSpam && !isVoicemailNumber) {
    593       sendMessageView.setTag(IntentProvider.getSendSmsIntentProvider(number));
    594       sendMessageView.setVisibility(View.VISIBLE);
    595     } else {
    596       sendMessageView.setVisibility(View.GONE);
    597     }
    598 
    599     mCallLogListItemHelper.setActionContentDescriptions(this);
    600 
    601     boolean supportsCallSubject = mCallLogCache.doesAccountSupportCallSubject(accountHandle);
    602     callWithNoteButtonView.setVisibility(
    603         supportsCallSubject && !isVoicemailNumber && info != null ? View.VISIBLE : View.GONE);
    604 
    605     callComposeButtonView.setVisibility(isCallComposerCapable ? View.VISIBLE : View.GONE);
    606 
    607     updateBlockReportActions(isVoicemailNumber);
    608   }
    609 
    610   private boolean isFullyUndialableVoicemail() {
    611     if (callType == Calls.VOICEMAIL_TYPE) {
    612       if (!hasDialableChar(number)) {
    613         return true;
    614       }
    615     }
    616     return false;
    617   }
    618 
    619   private static boolean hasDialableChar(CharSequence number) {
    620     if (TextUtils.isEmpty(number)) {
    621       return false;
    622     }
    623     for (char c : number.toString().toCharArray()) {
    624       if (PhoneNumberUtils.isDialable(c)) {
    625         return true;
    626       }
    627     }
    628     return false;
    629   }
    630 
    631   private boolean hasPlacedCarrierVideoCall() {
    632     if (!phoneCallDetailsViews.callTypeIcons.isVideoShown()) {
    633       return false;
    634     }
    635     if (accountHandle == null) {
    636       return false;
    637     }
    638     if (accountHandle
    639         .getComponentName()
    640         .equals(getLightbringer().getPhoneAccountComponentName(mContext))) {
    641       return false;
    642     }
    643     return true;
    644   }
    645 
    646   private boolean canSupportCarrierVideoCall() {
    647     return mCallLogCache.canRelyOnVideoPresence()
    648         && info != null
    649         && (info.carrierPresence & Phone.CARRIER_PRESENCE_VT_CAPABLE) != 0;
    650   }
    651 
    652   /**
    653    * Show or hide the action views, such as voicemail, details, and add contact.
    654    *
    655    * <p>If the action views have never been shown yet for this view, inflate the view stub.
    656    */
    657   public void showActions(boolean show) {
    658     showOrHideVoicemailTranscriptionView(show);
    659 
    660     if (show) {
    661       if (!isLoaded) {
    662         // b/31268128 for some unidentified reason showActions() can be called before the item is
    663         // loaded, causing NPE on uninitialized fields. Just log and return here, showActions() will
    664         // be called again once the item is loaded.
    665         LogUtil.e(
    666             "CallLogListItemViewHolder.showActions",
    667             "called before item is loaded",
    668             new Exception());
    669         return;
    670       }
    671 
    672       // Inflate the view stub if necessary, and wire up the event handlers.
    673       inflateActionViewStub();
    674       bindActionButtons();
    675       actionsView.setVisibility(View.VISIBLE);
    676       actionsView.setAlpha(1.0f);
    677     } else {
    678       // When recycling a view, it is possible the actionsView ViewStub was previously
    679       // inflated so we should hide it in this case.
    680       if (actionsView != null) {
    681         actionsView.setVisibility(View.GONE);
    682       }
    683     }
    684 
    685     updatePrimaryActionButton(show);
    686   }
    687 
    688   private void showOrHideVoicemailTranscriptionView(boolean isExpanded) {
    689     if (callType != Calls.VOICEMAIL_TYPE) {
    690       return;
    691     }
    692 
    693     final TextView view = phoneCallDetailsViews.voicemailTranscriptionView;
    694     if (!isExpanded || TextUtils.isEmpty(view.getText())) {
    695       view.setVisibility(View.GONE);
    696       return;
    697     }
    698     view.setVisibility(View.VISIBLE);
    699   }
    700 
    701   public void updatePhoto() {
    702     quickContactView.assignContactUri(info.lookupUri);
    703 
    704     if (isSpamFeatureEnabled && isSpam) {
    705       quickContactView.setImageDrawable(mContext.getDrawable(R.drawable.blocked_contact));
    706       return;
    707     }
    708 
    709     final String displayName = TextUtils.isEmpty(info.name) ? displayNumber : info.name;
    710     ContactPhotoManager.getInstance(mContext)
    711         .loadDialerThumbnailOrPhoto(
    712             quickContactView,
    713             info.lookupUri,
    714             info.photoId,
    715             info.photoUri,
    716             displayName,
    717             getContactType());
    718   }
    719 
    720   private int getContactType() {
    721     int contactType = ContactPhotoManager.TYPE_DEFAULT;
    722     if (mCallLogCache.isVoicemailNumber(accountHandle, number)) {
    723       contactType = ContactPhotoManager.TYPE_VOICEMAIL;
    724     } else if (isSpam) {
    725       contactType = ContactPhotoManager.TYPE_SPAM;
    726     } else if (mCachedNumberLookupService != null
    727         && mCachedNumberLookupService.isBusiness(info.sourceType)) {
    728       contactType = ContactPhotoManager.TYPE_BUSINESS;
    729     } else if (numberPresentation == TelecomManager.PRESENTATION_RESTRICTED) {
    730       contactType = ContactPhotoManager.TYPE_GENERIC_AVATAR;
    731     }
    732     return contactType;
    733   }
    734 
    735   @Override
    736   public void onClick(View view) {
    737     if (view.getId() == R.id.primary_action_button) {
    738       CallLogAsyncTaskUtil.markCallAsRead(mContext, callIds);
    739     }
    740 
    741     if (view.getId() == R.id.primary_action_button && !TextUtils.isEmpty(voicemailUri)) {
    742       Logger.get(mContext).logImpression(DialerImpression.Type.VOICEMAIL_PLAY_AUDIO_DIRECTLY);
    743       mVoicemailPrimaryActionButtonClicked = true;
    744       mExpandCollapseListener.onClick(primaryActionView);
    745     } else if (view.getId() == R.id.call_with_note_action) {
    746       CallSubjectDialog.start(
    747           (Activity) mContext,
    748           info.photoId,
    749           info.photoUri,
    750           info.lookupUri,
    751           (String) nameOrNumber /* top line of contact view in call subject dialog */,
    752           number,
    753           TextUtils.isEmpty(info.name) ? null : displayNumber, /* second line of contact
    754                                                                            view in dialog. */
    755           numberType, /* phone number type (e.g. mobile) in second line of contact view */
    756           getContactType(),
    757           accountHandle);
    758     } else if (view.getId() == R.id.block_report_action) {
    759       Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_BLOCK_REPORT_SPAM);
    760       maybeShowBlockNumberMigrationDialog(
    761           new BlockedNumbersMigrator.Listener() {
    762             @Override
    763             public void onComplete() {
    764               mBlockReportListener.onBlockReportSpam(
    765                   displayNumber, number, countryIso, callType, info.sourceType);
    766             }
    767           });
    768     } else if (view.getId() == R.id.block_action) {
    769       Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_BLOCK_NUMBER);
    770       maybeShowBlockNumberMigrationDialog(
    771           new BlockedNumbersMigrator.Listener() {
    772             @Override
    773             public void onComplete() {
    774               mBlockReportListener.onBlock(
    775                   displayNumber, number, countryIso, callType, info.sourceType);
    776             }
    777           });
    778     } else if (view.getId() == R.id.unblock_action) {
    779       Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_UNBLOCK_NUMBER);
    780       mBlockReportListener.onUnblock(
    781           displayNumber, number, countryIso, callType, info.sourceType, isSpam, blockId);
    782     } else if (view.getId() == R.id.report_not_spam_action) {
    783       Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_REPORT_AS_NOT_SPAM);
    784       mBlockReportListener.onReportNotSpam(
    785           displayNumber, number, countryIso, callType, info.sourceType);
    786     } else if (view.getId() == R.id.call_compose_action) {
    787       LogUtil.i("CallLogListItemViewHolder.onClick", "share and call pressed");
    788       Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_SHARE_AND_CALL);
    789       Activity activity = (Activity) mContext;
    790       activity.startActivityForResult(
    791           CallComposerActivity.newIntent(activity, buildContact()),
    792           DialtactsActivity.ACTIVITY_REQUEST_CODE_CALL_COMPOSE);
    793     } else if (view.getId() == R.id.share_voicemail) {
    794       Logger.get(mContext).logImpression(DialerImpression.Type.VVM_SHARE_PRESSED);
    795       mVoicemailPlaybackPresenter.shareVoicemail();
    796     } else {
    797       logCallLogAction(view.getId());
    798       final IntentProvider intentProvider = (IntentProvider) view.getTag();
    799       if (intentProvider != null) {
    800         final Intent intent = intentProvider.getIntent(mContext);
    801         // See IntentProvider.getCallDetailIntentProvider() for why this may be null.
    802         if (intent != null) {
    803           DialerUtils.startActivityWithErrorToast(mContext, intent);
    804         }
    805       }
    806     }
    807   }
    808 
    809   private CallComposerContact buildContact() {
    810     CallComposerContact.Builder contact = CallComposerContact.newBuilder();
    811     contact.setPhotoId(info.photoId);
    812     if (info.photoUri != null) {
    813       contact.setPhotoUri(info.photoUri.toString());
    814     }
    815     if (info.lookupUri != null) {
    816       contact.setContactUri(info.lookupUri.toString());
    817     }
    818     if (nameOrNumber != null) {
    819       contact.setNameOrNumber((String) nameOrNumber);
    820     }
    821     contact.setContactType(getContactType());
    822     contact.setNumber(number);
    823     /* second line of contact view. */
    824     if (!TextUtils.isEmpty(info.name)) {
    825       contact.setDisplayNumber(displayNumber);
    826     }
    827     /* phone number type (e.g. mobile) in second line of contact view */
    828     contact.setNumberLabel(numberType);
    829     return contact.build();
    830   }
    831 
    832   private void logCallLogAction(int id) {
    833     if (id == R.id.send_message_action) {
    834       Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_SEND_MESSAGE);
    835     } else if (id == R.id.add_to_existing_contact_action) {
    836       Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_ADD_TO_CONTACT);
    837     } else if (id == R.id.create_new_contact_action) {
    838       Logger.get(mContext).logImpression(DialerImpression.Type.CALL_LOG_CREATE_NEW_CONTACT);
    839     }
    840   }
    841 
    842   private void maybeShowBlockNumberMigrationDialog(BlockedNumbersMigrator.Listener listener) {
    843     if (!FilteredNumberCompat.maybeShowBlockNumberMigrationDialog(
    844         mContext, ((Activity) mContext).getFragmentManager(), listener)) {
    845       listener.onComplete();
    846     }
    847   }
    848 
    849   private void updateBlockReportActions(boolean isVoicemailNumber) {
    850     // Set block/spam actions.
    851     blockReportView.setVisibility(View.GONE);
    852     blockView.setVisibility(View.GONE);
    853     unblockView.setVisibility(View.GONE);
    854     reportNotSpamView.setVisibility(View.GONE);
    855     String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso);
    856     if (isVoicemailNumber
    857         || !FilteredNumbersUtil.canBlockNumber(mContext, e164Number, number)
    858         || !FilteredNumberCompat.canAttemptBlockOperations(mContext)) {
    859       return;
    860     }
    861     boolean isBlocked = blockId != null;
    862     if (isBlocked) {
    863       unblockView.setVisibility(View.VISIBLE);
    864     } else {
    865       if (isSpamFeatureEnabled) {
    866         if (isSpam) {
    867           blockView.setVisibility(View.VISIBLE);
    868           reportNotSpamView.setVisibility(View.VISIBLE);
    869         } else {
    870           blockReportView.setVisibility(View.VISIBLE);
    871         }
    872       } else {
    873         blockView.setVisibility(View.VISIBLE);
    874       }
    875     }
    876   }
    877 
    878   public void setDetailedPhoneDetails(CallDetailsEntries callDetailsEntries) {
    879     this.callDetailsEntries = callDetailsEntries;
    880   }
    881 
    882   @VisibleForTesting
    883   public CallDetailsEntries getDetailedPhoneDetails() {
    884     return callDetailsEntries;
    885   }
    886 
    887   @NonNull
    888   private Lightbringer getLightbringer() {
    889     return LightbringerComponent.get(mContext).getLightbringer();
    890   }
    891 
    892   @Override
    893   public void onCreateContextMenu(
    894       final ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) {
    895     if (TextUtils.isEmpty(number)) {
    896       return;
    897     }
    898 
    899     if (callType == CallLog.Calls.VOICEMAIL_TYPE) {
    900       menu.setHeaderTitle(mContext.getResources().getText(R.string.voicemail));
    901     } else {
    902       menu.setHeaderTitle(
    903           PhoneNumberUtilsCompat.createTtsSpannable(
    904               BidiFormatter.getInstance().unicodeWrap(number, TextDirectionHeuristics.LTR)));
    905     }
    906 
    907     menu.add(
    908             ContextMenu.NONE,
    909             R.id.context_menu_copy_to_clipboard,
    910             ContextMenu.NONE,
    911             R.string.action_copy_number_text)
    912         .setOnMenuItemClickListener(this);
    913 
    914     // The edit number before call does not show up if any of the conditions apply:
    915     // 1) Number cannot be called
    916     // 2) Number is the voicemail number
    917     // 3) Number is a SIP address
    918 
    919     if (PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation)
    920         && !mCallLogCache.isVoicemailNumber(accountHandle, number)
    921         && !PhoneNumberHelper.isSipNumber(number)) {
    922       menu.add(
    923               ContextMenu.NONE,
    924               R.id.context_menu_edit_before_call,
    925               ContextMenu.NONE,
    926               R.string.action_edit_number_before_call)
    927           .setOnMenuItemClickListener(this);
    928     }
    929 
    930     if (callType == CallLog.Calls.VOICEMAIL_TYPE
    931         && phoneCallDetailsViews.voicemailTranscriptionView.length() > 0) {
    932       menu.add(
    933               ContextMenu.NONE,
    934               R.id.context_menu_copy_transcript_to_clipboard,
    935               ContextMenu.NONE,
    936               R.string.copy_transcript_text)
    937           .setOnMenuItemClickListener(this);
    938     }
    939 
    940     String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso);
    941     boolean isVoicemailNumber = mCallLogCache.isVoicemailNumber(accountHandle, number);
    942     if (!isVoicemailNumber
    943         && FilteredNumbersUtil.canBlockNumber(mContext, e164Number, number)
    944         && FilteredNumberCompat.canAttemptBlockOperations(mContext)) {
    945       boolean isBlocked = blockId != null;
    946       if (isBlocked) {
    947         menu.add(
    948                 ContextMenu.NONE,
    949                 R.id.context_menu_unblock,
    950                 ContextMenu.NONE,
    951                 R.string.call_log_action_unblock_number)
    952             .setOnMenuItemClickListener(this);
    953       } else {
    954         if (isSpamFeatureEnabled) {
    955           if (isSpam) {
    956             menu.add(
    957                     ContextMenu.NONE,
    958                     R.id.context_menu_report_not_spam,
    959                     ContextMenu.NONE,
    960                     R.string.call_log_action_remove_spam)
    961                 .setOnMenuItemClickListener(this);
    962             menu.add(
    963                     ContextMenu.NONE,
    964                     R.id.context_menu_block,
    965                     ContextMenu.NONE,
    966                     R.string.call_log_action_block_number)
    967                 .setOnMenuItemClickListener(this);
    968           } else {
    969             menu.add(
    970                     ContextMenu.NONE,
    971                     R.id.context_menu_block_report_spam,
    972                     ContextMenu.NONE,
    973                     R.string.call_log_action_block_report_number)
    974                 .setOnMenuItemClickListener(this);
    975           }
    976         } else {
    977           menu.add(
    978                   ContextMenu.NONE,
    979                   R.id.context_menu_block,
    980                   ContextMenu.NONE,
    981                   R.string.call_log_action_block_number)
    982               .setOnMenuItemClickListener(this);
    983         }
    984       }
    985     }
    986 
    987     Logger.get(mContext).logScreenView(ScreenEvent.Type.CALL_LOG_CONTEXT_MENU, (Activity) mContext);
    988   }
    989 
    990   public interface OnClickListener {
    991 
    992     void onBlockReportSpam(
    993         String displayNumber,
    994         String number,
    995         String countryIso,
    996         int callType,
    997         ContactSource.Type contactSourceType);
    998 
    999     void onBlock(
   1000         String displayNumber,
   1001         String number,
   1002         String countryIso,
   1003         int callType,
   1004         ContactSource.Type contactSourceType);
   1005 
   1006     void onUnblock(
   1007         String displayNumber,
   1008         String number,
   1009         String countryIso,
   1010         int callType,
   1011         ContactSource.Type contactSourceType,
   1012         boolean isSpam,
   1013         Integer blockId);
   1014 
   1015     void onReportNotSpam(
   1016         String displayNumber,
   1017         String number,
   1018         String countryIso,
   1019         int callType,
   1020         ContactSource.Type contactSourceType);
   1021   }
   1022 }
   1023