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