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.Manifest.permission; 20 import android.annotation.SuppressLint; 21 import android.app.Activity; 22 import android.content.ActivityNotFoundException; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.res.Resources; 26 import android.net.Uri; 27 import android.os.AsyncTask; 28 import android.provider.CallLog; 29 import android.provider.CallLog.Calls; 30 import android.provider.ContactsContract.CommonDataKinds.Phone; 31 import android.support.annotation.IntDef; 32 import android.support.annotation.Nullable; 33 import android.support.annotation.RequiresPermission; 34 import android.support.annotation.VisibleForTesting; 35 import android.support.v7.widget.CardView; 36 import android.support.v7.widget.RecyclerView; 37 import android.telecom.PhoneAccount; 38 import android.telecom.PhoneAccountHandle; 39 import android.telecom.TelecomManager; 40 import android.telecom.VideoProfile; 41 import android.telephony.PhoneNumberUtils; 42 import android.telephony.TelephonyManager; 43 import android.text.BidiFormatter; 44 import android.text.TextDirectionHeuristics; 45 import android.text.TextUtils; 46 import android.view.ContextMenu; 47 import android.view.LayoutInflater; 48 import android.view.MenuItem; 49 import android.view.View; 50 import android.view.ViewStub; 51 import android.widget.ImageButton; 52 import android.widget.ImageView; 53 import android.widget.TextView; 54 import android.widget.Toast; 55 import com.android.contacts.common.dialog.CallSubjectDialog; 56 import com.android.dialer.app.DialtactsActivity; 57 import com.android.dialer.app.R; 58 import com.android.dialer.app.calllog.CallLogAdapter.OnActionModeStateChangedListener; 59 import com.android.dialer.app.calllog.calllogcache.CallLogCache; 60 import com.android.dialer.app.voicemail.VoicemailPlaybackLayout; 61 import com.android.dialer.app.voicemail.VoicemailPlaybackPresenter; 62 import com.android.dialer.blocking.BlockedNumbersMigrator; 63 import com.android.dialer.blocking.FilteredNumberCompat; 64 import com.android.dialer.blocking.FilteredNumbersUtil; 65 import com.android.dialer.callcomposer.CallComposerActivity; 66 import com.android.dialer.calldetails.CallDetailsActivity; 67 import com.android.dialer.calldetails.CallDetailsEntries; 68 import com.android.dialer.callintent.CallIntentBuilder; 69 import com.android.dialer.calllogutils.CallbackActionHelper.CallbackAction; 70 import com.android.dialer.clipboard.ClipboardUtils; 71 import com.android.dialer.common.Assert; 72 import com.android.dialer.common.LogUtil; 73 import com.android.dialer.common.concurrent.AsyncTaskExecutors; 74 import com.android.dialer.compat.CompatUtils; 75 import com.android.dialer.configprovider.ConfigProviderBindings; 76 import com.android.dialer.constants.ActivityRequestCodes; 77 import com.android.dialer.contactphoto.ContactPhotoManager; 78 import com.android.dialer.dialercontact.DialerContact; 79 import com.android.dialer.dialercontact.SimDetails; 80 import com.android.dialer.duo.Duo; 81 import com.android.dialer.duo.DuoComponent; 82 import com.android.dialer.duo.DuoConstants; 83 import com.android.dialer.lettertile.LetterTileDrawable; 84 import com.android.dialer.lettertile.LetterTileDrawable.ContactType; 85 import com.android.dialer.logging.ContactSource; 86 import com.android.dialer.logging.ContactSource.Type; 87 import com.android.dialer.logging.DialerImpression; 88 import com.android.dialer.logging.InteractionEvent; 89 import com.android.dialer.logging.Logger; 90 import com.android.dialer.logging.ScreenEvent; 91 import com.android.dialer.logging.UiAction; 92 import com.android.dialer.performancereport.PerformanceReport; 93 import com.android.dialer.phonenumbercache.CachedNumberLookupService; 94 import com.android.dialer.phonenumbercache.ContactInfo; 95 import com.android.dialer.phonenumbercache.PhoneNumberCache; 96 import com.android.dialer.phonenumberutil.PhoneNumberHelper; 97 import com.android.dialer.telecom.TelecomUtil; 98 import com.android.dialer.util.CallUtil; 99 import com.android.dialer.util.DialerUtils; 100 import com.android.dialer.util.UriUtils; 101 import java.lang.annotation.Retention; 102 import java.lang.annotation.RetentionPolicy; 103 import java.lang.ref.WeakReference; 104 105 /** 106 * This is an object containing references to views contained by the call log list item. This 107 * improves performance by reducing the frequency with which we need to find views by IDs. 108 * 109 * <p>This object also contains UI logic pertaining to the view, to isolate it from the 110 * CallLogAdapter. 111 */ 112 public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder 113 implements View.OnClickListener, 114 MenuItem.OnMenuItemClickListener, 115 View.OnCreateContextMenuListener { 116 117 private static final String TASK_DELETE = "task_delete"; 118 119 /** The root view of the call log list item */ 120 public final View rootView; 121 /** The quick contact badge for the contact. */ 122 public final DialerQuickContactBadge quickContactView; 123 /** The primary action view of the entry. */ 124 public final View primaryActionView; 125 /** The details of the phone call. */ 126 public final PhoneCallDetailsViews phoneCallDetailsViews; 127 /** The text of the header for a day grouping. */ 128 public final TextView dayGroupHeader; 129 /** The view containing the details for the call log row, including the action buttons. */ 130 public final CardView callLogEntryView; 131 /** The actionable view which places a call to the number corresponding to the call log row. */ 132 public final ImageView primaryActionButtonView; 133 134 private final Context context; 135 @Nullable private final PhoneAccountHandle defaultPhoneAccountHandle; 136 private final CallLogCache callLogCache; 137 private final CallLogListItemHelper callLogListItemHelper; 138 private final CachedNumberLookupService cachedNumberLookupService; 139 private final VoicemailPlaybackPresenter voicemailPlaybackPresenter; 140 private final OnClickListener blockReportListener; 141 @HostUi private final int hostUi; 142 /** Whether the data fields are populated by the worker thread, ready to be shown. */ 143 public boolean isLoaded; 144 /** The view containing call log item actions. Null until the ViewStub is inflated. */ 145 public View actionsView; 146 /** The button views below are assigned only when the action section is expanded. */ 147 public VoicemailPlaybackLayout voicemailPlaybackView; 148 149 public View callButtonView; 150 public View videoCallButtonView; 151 public View setUpVideoButtonView; 152 public View inviteVideoButtonView; 153 public View createNewContactButtonView; 154 public View addToExistingContactButtonView; 155 public View sendMessageView; 156 public View blockReportView; 157 public View blockView; 158 public View unblockView; 159 public View reportNotSpamView; 160 public View detailsButtonView; 161 public View callWithNoteButtonView; 162 public View callComposeButtonView; 163 public View sendVoicemailButtonView; 164 public ImageView workIconView; 165 public ImageView checkBoxView; 166 /** 167 * The row Id for the first call associated with the call log entry. Used as a key for the map 168 * used to track which call log entries have the action button section expanded. 169 */ 170 public long rowId; 171 /** 172 * The call Ids for the calls represented by the current call log entry. Used when the user 173 * deletes a call log entry. 174 */ 175 public long[] callIds; 176 /** 177 * The callable phone number for the current call log entry. Cached here as the call back intent 178 * is set only when the actions ViewStub is inflated. 179 */ 180 @Nullable public String number; 181 /** The post-dial numbers that are dialed following the phone number. */ 182 public String postDialDigits; 183 /** The formatted phone number to display. */ 184 public String displayNumber; 185 /** 186 * The phone number presentation for the current call log entry. Cached here as the call back 187 * intent is set only when the actions ViewStub is inflated. 188 */ 189 public int numberPresentation; 190 /** The type of the phone number (e.g. main, work, etc). */ 191 public String numberType; 192 /** 193 * The country iso for the call. Cached here as the call back intent is set only when the actions 194 * ViewStub is inflated. 195 */ 196 public String countryIso; 197 /** 198 * The type of call for the current call log entry. Cached here as the call back intent is set 199 * only when the actions ViewStub is inflated. 200 */ 201 public int callType; 202 /** 203 * ID for blocked numbers database. Set when context menu is created, if the number is blocked. 204 */ 205 public Integer blockId; 206 /** 207 * The account for the current call log entry. Cached here as the call back intent is set only 208 * when the actions ViewStub is inflated. 209 */ 210 public PhoneAccountHandle accountHandle; 211 /** 212 * If the call has an associated voicemail message, the URI of the voicemail message for playback. 213 * Cached here as the voicemail intent is only set when the actions ViewStub is inflated. 214 */ 215 public String voicemailUri; 216 /** 217 * The name or number associated with the call. Cached here for use when setting content 218 * descriptions on buttons in the actions ViewStub when it is inflated. 219 */ 220 @Nullable public CharSequence nameOrNumber; 221 /** 222 * The call type or Location associated with the call. Cached here for use when setting text for a 223 * voicemail log's call button 224 */ 225 public CharSequence callTypeOrLocation; 226 /** The contact info for the contact displayed in this list item. */ 227 public volatile ContactInfo info; 228 /** Whether spam feature is enabled, which affects UI. */ 229 public boolean isSpamFeatureEnabled; 230 /** Whether the current log entry is a spam number or not. */ 231 public boolean isSpam; 232 233 public boolean isCallComposerCapable; 234 235 private View.OnClickListener expandCollapseListener; 236 private final OnActionModeStateChangedListener onActionModeStateChangedListener; 237 private final View.OnLongClickListener longPressListener; 238 private boolean voicemailPrimaryActionButtonClicked; 239 240 public int callbackAction; 241 public int dayGroupHeaderVisibility; 242 public CharSequence dayGroupHeaderText; 243 public boolean isAttachedToWindow; 244 245 public AsyncTask<Void, Void, ?> asyncTask; 246 private CallDetailsEntries callDetailsEntries; 247 248 private CallLogListItemViewHolder( 249 Context context, 250 OnClickListener blockReportListener, 251 View.OnClickListener expandCollapseListener, 252 View.OnLongClickListener longClickListener, 253 CallLogAdapter.OnActionModeStateChangedListener actionModeStateChangedListener, 254 CallLogCache callLogCache, 255 CallLogListItemHelper callLogListItemHelper, 256 VoicemailPlaybackPresenter voicemailPlaybackPresenter, 257 View rootView, 258 DialerQuickContactBadge dialerQuickContactView, 259 View primaryActionView, 260 PhoneCallDetailsViews phoneCallDetailsViews, 261 CardView callLogEntryView, 262 TextView dayGroupHeader, 263 ImageView primaryActionButtonView) { 264 super(rootView); 265 266 this.context = context; 267 this.expandCollapseListener = expandCollapseListener; 268 onActionModeStateChangedListener = actionModeStateChangedListener; 269 longPressListener = longClickListener; 270 this.callLogCache = callLogCache; 271 this.callLogListItemHelper = callLogListItemHelper; 272 this.voicemailPlaybackPresenter = voicemailPlaybackPresenter; 273 this.blockReportListener = blockReportListener; 274 cachedNumberLookupService = PhoneNumberCache.get(this.context).getCachedNumberLookupService(); 275 276 // Cache this to avoid having to look it up each time we bind to a call log entry 277 defaultPhoneAccountHandle = 278 TelecomUtil.getDefaultOutgoingPhoneAccount(context, PhoneAccount.SCHEME_TEL); 279 280 this.rootView = rootView; 281 this.quickContactView = dialerQuickContactView; 282 this.primaryActionView = primaryActionView; 283 this.phoneCallDetailsViews = phoneCallDetailsViews; 284 this.callLogEntryView = callLogEntryView; 285 this.dayGroupHeader = dayGroupHeader; 286 this.primaryActionButtonView = primaryActionButtonView; 287 this.workIconView = (ImageView) rootView.findViewById(R.id.work_profile_icon); 288 this.checkBoxView = (ImageView) rootView.findViewById(R.id.quick_contact_checkbox); 289 290 // Set text height to false on the TextViews so they don't have extra padding. 291 phoneCallDetailsViews.nameView.setElegantTextHeight(false); 292 phoneCallDetailsViews.callLocationAndDate.setElegantTextHeight(false); 293 294 if (this.context instanceof CallLogActivity) { 295 hostUi = HostUi.CALL_HISTORY; 296 Logger.get(this.context) 297 .logQuickContactOnTouch( 298 quickContactView, InteractionEvent.Type.OPEN_QUICK_CONTACT_FROM_CALL_HISTORY, true); 299 } else if (this.voicemailPlaybackPresenter == null) { 300 hostUi = HostUi.CALL_LOG; 301 Logger.get(this.context) 302 .logQuickContactOnTouch( 303 quickContactView, InteractionEvent.Type.OPEN_QUICK_CONTACT_FROM_CALL_LOG, true); 304 } else { 305 hostUi = HostUi.VOICEMAIL; 306 Logger.get(this.context) 307 .logQuickContactOnTouch( 308 quickContactView, InteractionEvent.Type.OPEN_QUICK_CONTACT_FROM_VOICEMAIL, false); 309 } 310 311 quickContactView.setOverlay(null); 312 if (CompatUtils.hasPrioritizedMimeType()) { 313 quickContactView.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE); 314 } 315 primaryActionButtonView.setOnClickListener(this); 316 primaryActionView.setOnClickListener(this.expandCollapseListener); 317 if (this.voicemailPlaybackPresenter != null 318 && ConfigProviderBindings.get(this.context) 319 .getBoolean( 320 CallLogAdapter.ENABLE_CALL_LOG_MULTI_SELECT, 321 CallLogAdapter.ENABLE_CALL_LOG_MULTI_SELECT_FLAG)) { 322 primaryActionView.setOnLongClickListener(longPressListener); 323 quickContactView.setOnLongClickListener(longPressListener); 324 quickContactView.setMulitSelectListeners( 325 this.expandCollapseListener, onActionModeStateChangedListener); 326 } else { 327 primaryActionView.setOnCreateContextMenuListener(this); 328 } 329 } 330 331 public static CallLogListItemViewHolder create( 332 View view, 333 Context context, 334 OnClickListener blockReportListener, 335 View.OnClickListener expandCollapseListener, 336 View.OnLongClickListener longClickListener, 337 CallLogAdapter.OnActionModeStateChangedListener actionModeStateChangeListener, 338 CallLogCache callLogCache, 339 CallLogListItemHelper callLogListItemHelper, 340 VoicemailPlaybackPresenter voicemailPlaybackPresenter) { 341 342 return new CallLogListItemViewHolder( 343 context, 344 blockReportListener, 345 expandCollapseListener, 346 longClickListener, 347 actionModeStateChangeListener, 348 callLogCache, 349 callLogListItemHelper, 350 voicemailPlaybackPresenter, 351 view, 352 (DialerQuickContactBadge) view.findViewById(R.id.quick_contact_photo), 353 view.findViewById(R.id.primary_action_view), 354 PhoneCallDetailsViews.fromView(view), 355 (CardView) view.findViewById(R.id.call_log_row), 356 (TextView) view.findViewById(R.id.call_log_day_group_label), 357 (ImageView) view.findViewById(R.id.primary_action_button)); 358 } 359 360 public static CallLogListItemViewHolder createForTest(Context context) { 361 return createForTest(context, null, null, new CallLogCache(context)); 362 } 363 364 public static CallLogListItemViewHolder createForTest( 365 Context context, 366 View.OnClickListener expandCollapseListener, 367 VoicemailPlaybackPresenter voicemailPlaybackPresenter, 368 CallLogCache callLogCache) { 369 Resources resources = context.getResources(); 370 PhoneCallDetailsHelper phoneCallDetailsHelper = 371 new PhoneCallDetailsHelper(context, resources, callLogCache); 372 373 CallLogListItemViewHolder viewHolder = 374 new CallLogListItemViewHolder( 375 context, 376 null, 377 expandCollapseListener /* expandCollapseListener */, 378 null, 379 null, 380 callLogCache, 381 new CallLogListItemHelper(phoneCallDetailsHelper, resources, callLogCache), 382 voicemailPlaybackPresenter, 383 LayoutInflater.from(context).inflate(R.layout.call_log_list_item, null), 384 new DialerQuickContactBadge(context), 385 new View(context), 386 PhoneCallDetailsViews.createForTest(context), 387 new CardView(context), 388 new TextView(context), 389 new ImageView(context)); 390 viewHolder.detailsButtonView = new TextView(context); 391 viewHolder.actionsView = new View(context); 392 viewHolder.voicemailPlaybackView = new VoicemailPlaybackLayout(context); 393 viewHolder.workIconView = new ImageButton(context); 394 viewHolder.checkBoxView = new ImageButton(context); 395 return viewHolder; 396 } 397 398 @Override 399 public boolean onMenuItemClick(MenuItem item) { 400 int resId = item.getItemId(); 401 if (resId == R.id.context_menu_copy_to_clipboard) { 402 ClipboardUtils.copyText(context, null, number, true); 403 return true; 404 } else if (resId == R.id.context_menu_copy_transcript_to_clipboard) { 405 ClipboardUtils.copyText( 406 context, null, phoneCallDetailsViews.voicemailTranscriptionView.getText(), true); 407 return true; 408 } else if (resId == R.id.context_menu_edit_before_call) { 409 final Intent intent = new Intent(Intent.ACTION_DIAL, CallUtil.getCallUri(number)); 410 intent.setClass(context, DialtactsActivity.class); 411 DialerUtils.startActivityWithErrorToast(context, intent); 412 return true; 413 } else if (resId == R.id.context_menu_block_report_spam) { 414 Logger.get(context) 415 .logImpression(DialerImpression.Type.CALL_LOG_CONTEXT_MENU_BLOCK_REPORT_SPAM); 416 maybeShowBlockNumberMigrationDialog( 417 new BlockedNumbersMigrator.Listener() { 418 @Override 419 public void onComplete() { 420 blockReportListener.onBlockReportSpam( 421 displayNumber, number, countryIso, callType, info.sourceType); 422 } 423 }); 424 } else if (resId == R.id.context_menu_block) { 425 Logger.get(context).logImpression(DialerImpression.Type.CALL_LOG_CONTEXT_MENU_BLOCK_NUMBER); 426 maybeShowBlockNumberMigrationDialog( 427 new BlockedNumbersMigrator.Listener() { 428 @Override 429 public void onComplete() { 430 blockReportListener.onBlock( 431 displayNumber, number, countryIso, callType, info.sourceType); 432 } 433 }); 434 } else if (resId == R.id.context_menu_unblock) { 435 Logger.get(context).logImpression(DialerImpression.Type.CALL_LOG_CONTEXT_MENU_UNBLOCK_NUMBER); 436 blockReportListener.onUnblock( 437 displayNumber, number, countryIso, callType, info.sourceType, isSpam, blockId); 438 } else if (resId == R.id.context_menu_report_not_spam) { 439 Logger.get(context) 440 .logImpression(DialerImpression.Type.CALL_LOG_CONTEXT_MENU_REPORT_AS_NOT_SPAM); 441 blockReportListener.onReportNotSpam( 442 displayNumber, number, countryIso, callType, info.sourceType); 443 } else if (resId == R.id.context_menu_delete) { 444 AsyncTaskExecutors.createAsyncTaskExecutor() 445 .submit(TASK_DELETE, new DeleteCallTask(context, callIds)); 446 } 447 return false; 448 } 449 450 /** 451 * Configures the action buttons in the expandable actions ViewStub. The ViewStub is not inflated 452 * during initial binding, so click handlers, tags and accessibility text must be set here, if 453 * necessary. 454 */ 455 public void inflateActionViewStub() { 456 ViewStub stub = (ViewStub) rootView.findViewById(R.id.call_log_entry_actions_stub); 457 if (stub != null) { 458 actionsView = stub.inflate(); 459 460 voicemailPlaybackView = 461 (VoicemailPlaybackLayout) actionsView.findViewById(R.id.voicemail_playback_layout); 462 voicemailPlaybackView.setViewHolder(this); 463 464 callButtonView = actionsView.findViewById(R.id.call_action); 465 callButtonView.setOnClickListener(this); 466 467 videoCallButtonView = actionsView.findViewById(R.id.video_call_action); 468 videoCallButtonView.setOnClickListener(this); 469 470 setUpVideoButtonView = actionsView.findViewById(R.id.set_up_video_action); 471 setUpVideoButtonView.setOnClickListener(this); 472 473 inviteVideoButtonView = actionsView.findViewById(R.id.invite_video_action); 474 inviteVideoButtonView.setOnClickListener(this); 475 476 createNewContactButtonView = actionsView.findViewById(R.id.create_new_contact_action); 477 createNewContactButtonView.setOnClickListener(this); 478 479 addToExistingContactButtonView = 480 actionsView.findViewById(R.id.add_to_existing_contact_action); 481 addToExistingContactButtonView.setOnClickListener(this); 482 483 sendMessageView = actionsView.findViewById(R.id.send_message_action); 484 sendMessageView.setOnClickListener(this); 485 486 blockReportView = actionsView.findViewById(R.id.block_report_action); 487 blockReportView.setOnClickListener(this); 488 489 blockView = actionsView.findViewById(R.id.block_action); 490 blockView.setOnClickListener(this); 491 492 unblockView = actionsView.findViewById(R.id.unblock_action); 493 unblockView.setOnClickListener(this); 494 495 reportNotSpamView = actionsView.findViewById(R.id.report_not_spam_action); 496 reportNotSpamView.setOnClickListener(this); 497 498 detailsButtonView = actionsView.findViewById(R.id.details_action); 499 detailsButtonView.setOnClickListener(this); 500 501 callWithNoteButtonView = actionsView.findViewById(R.id.call_with_note_action); 502 callWithNoteButtonView.setOnClickListener(this); 503 504 callComposeButtonView = actionsView.findViewById(R.id.call_compose_action); 505 callComposeButtonView.setOnClickListener(this); 506 507 sendVoicemailButtonView = actionsView.findViewById(R.id.share_voicemail); 508 sendVoicemailButtonView.setOnClickListener(this); 509 } 510 } 511 512 private void updatePrimaryActionButton(boolean isExpanded) { 513 514 if (nameOrNumber == null) { 515 LogUtil.e("CallLogListItemViewHolder.updatePrimaryActionButton", "name or number is null"); 516 } 517 518 // Calling expandTemplate with a null parameter will cause a NullPointerException. 519 CharSequence validNameOrNumber = nameOrNumber == null ? "" : nameOrNumber; 520 521 if (!TextUtils.isEmpty(voicemailUri)) { 522 // Treat as voicemail list item; show play button if not expanded. 523 if (!isExpanded) { 524 primaryActionButtonView.setImageResource(R.drawable.quantum_ic_play_arrow_white_24); 525 primaryActionButtonView.setContentDescription( 526 TextUtils.expandTemplate( 527 context.getString(R.string.description_voicemail_action), validNameOrNumber)); 528 primaryActionButtonView.setVisibility(View.VISIBLE); 529 } else { 530 primaryActionButtonView.setVisibility(View.GONE); 531 } 532 return; 533 } 534 535 // Treat as normal list item; show call button, if possible. 536 if (!PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation)) { 537 primaryActionButtonView.setTag(null); 538 primaryActionButtonView.setVisibility(View.GONE); 539 return; 540 } 541 542 switch (callbackAction) { 543 case CallbackAction.IMS_VIDEO: 544 primaryActionButtonView.setTag( 545 IntentProvider.getReturnVideoCallIntentProvider(number, accountHandle)); 546 primaryActionButtonView.setContentDescription( 547 TextUtils.expandTemplate( 548 context.getString(R.string.description_video_call_action), validNameOrNumber)); 549 primaryActionButtonView.setImageResource(R.drawable.quantum_ic_videocam_vd_theme_24); 550 primaryActionButtonView.setVisibility(View.VISIBLE); 551 break; 552 case CallbackAction.DUO: 553 if (showDuoPrimaryButton()) { 554 CallIntentBuilder.increaseLightbringerCallButtonAppearInCollapsedCallLogItemCount(); 555 primaryActionButtonView.setTag(IntentProvider.getDuoVideoIntentProvider(number)); 556 } else { 557 primaryActionButtonView.setTag(IntentProvider.getReturnVideoCallIntentProvider(number)); 558 } 559 primaryActionButtonView.setContentDescription( 560 TextUtils.expandTemplate( 561 context.getString(R.string.description_video_call_action), validNameOrNumber)); 562 primaryActionButtonView.setImageResource(R.drawable.quantum_ic_videocam_vd_theme_24); 563 primaryActionButtonView.setVisibility(View.VISIBLE); 564 break; 565 case CallbackAction.VOICE: 566 if (callLogCache.isVoicemailNumber(accountHandle, number)) { 567 // Call to generic voicemail number, in case there are multiple accounts 568 primaryActionButtonView.setTag(IntentProvider.getReturnVoicemailCallIntentProvider(null)); 569 } else if (canSupportAssistedDialing()) { 570 primaryActionButtonView.setTag( 571 IntentProvider.getAssistedDialIntentProvider( 572 number + postDialDigits, 573 context, 574 context.getSystemService(TelephonyManager.class))); 575 } else { 576 primaryActionButtonView.setTag( 577 IntentProvider.getReturnCallIntentProvider(number + postDialDigits)); 578 } 579 580 primaryActionButtonView.setContentDescription( 581 TextUtils.expandTemplate( 582 context.getString(R.string.description_call_action), validNameOrNumber)); 583 primaryActionButtonView.setImageResource(R.drawable.quantum_ic_call_vd_theme_24); 584 primaryActionButtonView.setVisibility(View.VISIBLE); 585 break; 586 default: 587 primaryActionButtonView.setTag(null); 588 primaryActionButtonView.setVisibility(View.GONE); 589 } 590 } 591 592 /** 593 * Binds text titles, click handlers and intents to the voicemail, details and callback action 594 * buttons. 595 */ 596 private void bindActionButtons() { 597 boolean canPlaceCallToNumber = PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation); 598 599 // Hide the call buttons by default. We then set it to be visible when appropriate below. 600 // This saves us having to remember to set it to GONE in multiple places. 601 callButtonView.setVisibility(View.GONE); 602 videoCallButtonView.setVisibility(View.GONE); 603 setUpVideoButtonView.setVisibility(View.GONE); 604 inviteVideoButtonView.setVisibility(View.GONE); 605 606 if (isFullyUndialableVoicemail()) { 607 // Sometimes the voicemail server will report the message is from some non phone number 608 // source. If the number does not contains any dialable digit treat it as it is from a unknown 609 // number, remove all action buttons but still show the voicemail playback layout. 610 detailsButtonView.setVisibility(View.GONE); 611 createNewContactButtonView.setVisibility(View.GONE); 612 addToExistingContactButtonView.setVisibility(View.GONE); 613 sendMessageView.setVisibility(View.GONE); 614 callWithNoteButtonView.setVisibility(View.GONE); 615 callComposeButtonView.setVisibility(View.GONE); 616 blockReportView.setVisibility(View.GONE); 617 blockView.setVisibility(View.GONE); 618 unblockView.setVisibility(View.GONE); 619 reportNotSpamView.setVisibility(View.GONE); 620 621 voicemailPlaybackView.setVisibility(View.VISIBLE); 622 Uri uri = Uri.parse(voicemailUri); 623 voicemailPlaybackPresenter.setPlaybackView( 624 voicemailPlaybackView, 625 rowId, 626 uri, 627 voicemailPrimaryActionButtonClicked, 628 sendVoicemailButtonView); 629 voicemailPrimaryActionButtonClicked = false; 630 CallLogAsyncTaskUtil.markVoicemailAsRead(context, uri); 631 return; 632 } 633 634 TextView callTypeOrLocationView = 635 ((TextView) callButtonView.findViewById(R.id.call_type_or_location_text)); 636 637 if (canPlaceCallToNumber) { 638 if (canSupportAssistedDialing()) { 639 callButtonView.setTag( 640 IntentProvider.getAssistedDialIntentProvider( 641 number, context, context.getSystemService(TelephonyManager.class))); 642 } else { 643 callButtonView.setTag(IntentProvider.getReturnCallIntentProvider(number)); 644 } 645 callTypeOrLocationView.setVisibility(View.GONE); 646 } 647 648 if (!TextUtils.isEmpty(voicemailUri) && canPlaceCallToNumber) { 649 ((TextView) callButtonView.findViewById(R.id.call_action_text)) 650 .setText( 651 TextUtils.expandTemplate( 652 context.getString(R.string.call_log_action_call), 653 nameOrNumber == null ? "" : nameOrNumber)); 654 655 if (callType == Calls.VOICEMAIL_TYPE && !TextUtils.isEmpty(callTypeOrLocation)) { 656 callTypeOrLocationView.setText(callTypeOrLocation); 657 callTypeOrLocationView.setVisibility(View.VISIBLE); 658 } 659 callButtonView.setVisibility(View.VISIBLE); 660 } 661 662 boolean isVoicemailNumber = callLogCache.isVoicemailNumber(accountHandle, number); 663 664 switch (callbackAction) { 665 case CallbackAction.IMS_VIDEO: 666 case CallbackAction.DUO: 667 // For an IMS video call or a Duo call, the secondary action should always be a 668 // voice callback. 669 callButtonView.setVisibility(View.VISIBLE); 670 videoCallButtonView.setVisibility(View.GONE); 671 break; 672 case CallbackAction.VOICE: 673 Duo duo = DuoComponent.get(context).getDuo(); 674 // For a voice call, set the secondary callback action to be an IMS video call if it is 675 // available. Otherwise try to set it as a Duo call. 676 if (CallUtil.isVideoEnabled(context) 677 && (hasPlacedCarrierVideoCall() || canSupportCarrierVideoCall())) { 678 videoCallButtonView.setTag(IntentProvider.getReturnVideoCallIntentProvider(number)); 679 videoCallButtonView.setVisibility(View.VISIBLE); 680 break; 681 } 682 683 if (isVoicemailNumber) { 684 break; 685 } 686 687 boolean identifiedSpamCall = isSpamFeatureEnabled && isSpam; 688 if (duo.isReachable(context, number)) { 689 videoCallButtonView.setTag(IntentProvider.getDuoVideoIntentProvider(number)); 690 videoCallButtonView.setVisibility(View.VISIBLE); 691 } else if (duo.isActivated(context) && !identifiedSpamCall) { 692 if (ConfigProviderBindings.get(context) 693 .getBoolean("enable_call_log_duo_invite_button", false)) { 694 inviteVideoButtonView.setTag(IntentProvider.getDuoInviteIntentProvider(number)); 695 inviteVideoButtonView.setVisibility(View.VISIBLE); 696 Logger.get(context).logImpression(DialerImpression.Type.DUO_CALL_LOG_INVITE_SHOWN); 697 } 698 } else if (duo.isEnabled(context) && !identifiedSpamCall) { 699 if (!duo.isInstalled(context)) { 700 if (ConfigProviderBindings.get(context) 701 .getBoolean("enable_call_log_install_duo_button", false)) { 702 setUpVideoButtonView.setTag(IntentProvider.getInstallDuoIntentProvider()); 703 setUpVideoButtonView.setVisibility(View.VISIBLE); 704 Logger.get(context) 705 .logImpression(DialerImpression.Type.DUO_CALL_LOG_SET_UP_INSTALL_SHOWN); 706 } 707 } else { 708 if (ConfigProviderBindings.get(context) 709 .getBoolean("enable_call_log_activate_duo_button", false)) { 710 setUpVideoButtonView.setTag(IntentProvider.getSetUpDuoIntentProvider()); 711 setUpVideoButtonView.setVisibility(View.VISIBLE); 712 Logger.get(context) 713 .logImpression(DialerImpression.Type.DUO_CALL_LOG_SET_UP_ACTIVATE_SHOWN); 714 } 715 } 716 } 717 break; 718 default: 719 callButtonView.setVisibility(View.GONE); 720 videoCallButtonView.setVisibility(View.GONE); 721 } 722 723 // For voicemail calls, show the voicemail playback layout; hide otherwise. 724 if (callType == Calls.VOICEMAIL_TYPE 725 && voicemailPlaybackPresenter != null 726 && !TextUtils.isEmpty(voicemailUri)) { 727 voicemailPlaybackView.setVisibility(View.VISIBLE); 728 729 Uri uri = Uri.parse(voicemailUri); 730 voicemailPlaybackPresenter.setPlaybackView( 731 voicemailPlaybackView, 732 rowId, 733 uri, 734 voicemailPrimaryActionButtonClicked, 735 sendVoicemailButtonView); 736 voicemailPrimaryActionButtonClicked = false; 737 CallLogAsyncTaskUtil.markVoicemailAsRead(context, uri); 738 } else { 739 voicemailPlaybackView.setVisibility(View.GONE); 740 sendVoicemailButtonView.setVisibility(View.GONE); 741 } 742 743 if (callType == Calls.VOICEMAIL_TYPE) { 744 detailsButtonView.setVisibility(View.GONE); 745 } else { 746 detailsButtonView.setVisibility(View.VISIBLE); 747 boolean canReportCallerId = 748 cachedNumberLookupService != null 749 && cachedNumberLookupService.canReportAsInvalid(info.sourceType, info.objectId); 750 detailsButtonView.setTag( 751 IntentProvider.getCallDetailIntentProvider( 752 callDetailsEntries, buildContact(), canReportCallerId, canSupportAssistedDialing())); 753 } 754 755 boolean isBlockedOrSpam = blockId != null || (isSpamFeatureEnabled && isSpam); 756 757 if (!isBlockedOrSpam && info != null && UriUtils.isEncodedContactUri(info.lookupUri)) { 758 createNewContactButtonView.setTag( 759 IntentProvider.getAddContactIntentProvider( 760 info.lookupUri, info.name, info.number, info.type, true /* isNewContact */)); 761 createNewContactButtonView.setVisibility(View.VISIBLE); 762 763 addToExistingContactButtonView.setTag( 764 IntentProvider.getAddContactIntentProvider( 765 info.lookupUri, info.name, info.number, info.type, false /* isNewContact */)); 766 addToExistingContactButtonView.setVisibility(View.VISIBLE); 767 } else { 768 createNewContactButtonView.setVisibility(View.GONE); 769 addToExistingContactButtonView.setVisibility(View.GONE); 770 } 771 772 if (canPlaceCallToNumber && !isBlockedOrSpam && !isVoicemailNumber) { 773 sendMessageView.setTag(IntentProvider.getSendSmsIntentProvider(number)); 774 sendMessageView.setVisibility(View.VISIBLE); 775 } else { 776 sendMessageView.setVisibility(View.GONE); 777 } 778 779 callLogListItemHelper.setActionContentDescriptions(this); 780 781 boolean supportsCallSubject = callLogCache.doesAccountSupportCallSubject(accountHandle); 782 callWithNoteButtonView.setVisibility( 783 supportsCallSubject && !isVoicemailNumber && info != null ? View.VISIBLE : View.GONE); 784 785 callComposeButtonView.setVisibility(isCallComposerCapable ? View.VISIBLE : View.GONE); 786 787 updateBlockReportActions(isVoicemailNumber); 788 } 789 790 private boolean isFullyUndialableVoicemail() { 791 if (callType == Calls.VOICEMAIL_TYPE) { 792 if (!hasDialableChar(number)) { 793 return true; 794 } 795 } 796 return false; 797 } 798 799 private boolean showDuoPrimaryButton() { 800 return accountHandle != null 801 && accountHandle.getComponentName().equals(DuoConstants.PHONE_ACCOUNT_COMPONENT_NAME) 802 && DuoComponent.get(context).getDuo().isReachable(context, number); 803 } 804 805 private static boolean hasDialableChar(CharSequence number) { 806 if (TextUtils.isEmpty(number)) { 807 return false; 808 } 809 for (char c : number.toString().toCharArray()) { 810 if (PhoneNumberUtils.isDialable(c)) { 811 return true; 812 } 813 } 814 return false; 815 } 816 817 private boolean hasPlacedCarrierVideoCall() { 818 if (!phoneCallDetailsViews.callTypeIcons.isVideoShown()) { 819 return false; 820 } 821 if (accountHandle == null) { 822 return false; 823 } 824 if (defaultPhoneAccountHandle == null) { 825 return false; 826 } 827 return accountHandle.getComponentName().equals(defaultPhoneAccountHandle.getComponentName()); 828 } 829 830 private boolean canSupportAssistedDialing() { 831 return info != null && info.lookupKey != null; 832 } 833 834 private boolean canSupportCarrierVideoCall() { 835 return callLogCache.canRelyOnVideoPresence() 836 && info != null 837 && (info.carrierPresence & Phone.CARRIER_PRESENCE_VT_CAPABLE) != 0; 838 } 839 840 /** 841 * Show or hide the action views, such as voicemail, details, and add contact. 842 * 843 * <p>If the action views have never been shown yet for this view, inflate the view stub. 844 */ 845 public void showActions(boolean show) { 846 showOrHideVoicemailTranscriptionView(show); 847 848 if (show) { 849 if (!isLoaded) { 850 // a bug for some unidentified reason showActions() can be called before the item is 851 // loaded, causing NPE on uninitialized fields. Just log and return here, showActions() will 852 // be called again once the item is loaded. 853 LogUtil.e( 854 "CallLogListItemViewHolder.showActions", 855 "called before item is loaded", 856 new Exception()); 857 return; 858 } 859 860 // Inflate the view stub if necessary, and wire up the event handlers. 861 inflateActionViewStub(); 862 bindActionButtons(); 863 actionsView.setVisibility(View.VISIBLE); 864 actionsView.setAlpha(1.0f); 865 } else { 866 // When recycling a view, it is possible the actionsView ViewStub was previously 867 // inflated so we should hide it in this case. 868 if (actionsView != null) { 869 actionsView.setVisibility(View.GONE); 870 } 871 } 872 873 updatePrimaryActionButton(show); 874 } 875 876 private void showOrHideVoicemailTranscriptionView(boolean isExpanded) { 877 if (callType != Calls.VOICEMAIL_TYPE) { 878 return; 879 } 880 881 View transcriptContainerView = phoneCallDetailsViews.transcriptionView; 882 TextView transcriptView = phoneCallDetailsViews.voicemailTranscriptionView; 883 TextView transcriptBrandingView = phoneCallDetailsViews.voicemailTranscriptionBrandingView; 884 if (!isExpanded) { 885 transcriptContainerView.setVisibility(View.GONE); 886 return; 887 } 888 889 boolean show = false; 890 if (TextUtils.isEmpty(transcriptView.getText())) { 891 transcriptView.setVisibility(View.GONE); 892 } else { 893 transcriptView.setVisibility(View.VISIBLE); 894 show = true; 895 } 896 if (TextUtils.isEmpty(transcriptBrandingView.getText())) { 897 transcriptBrandingView.setVisibility(View.GONE); 898 } else { 899 transcriptBrandingView.setVisibility(View.VISIBLE); 900 show = true; 901 } 902 if (show) { 903 transcriptContainerView.setVisibility(View.VISIBLE); 904 } else { 905 transcriptContainerView.setVisibility(View.GONE); 906 } 907 } 908 909 public void updatePhoto() { 910 quickContactView.assignContactUri(info.lookupUri); 911 912 if (isSpamFeatureEnabled && isSpam) { 913 quickContactView.setImageDrawable(context.getDrawable(R.drawable.blocked_contact)); 914 return; 915 } 916 917 final String displayName = TextUtils.isEmpty(info.name) ? displayNumber : info.name; 918 ContactPhotoManager.getInstance(context) 919 .loadDialerThumbnailOrPhoto( 920 quickContactView, 921 info.lookupUri, 922 info.photoId, 923 info.photoUri, 924 displayName, 925 getContactType()); 926 } 927 928 private @ContactType int getContactType() { 929 return LetterTileDrawable.getContactTypeFromPrimitives( 930 callLogCache.isVoicemailNumber(accountHandle, number), 931 isSpam, 932 cachedNumberLookupService != null && cachedNumberLookupService.isBusiness(info.sourceType), 933 numberPresentation, 934 false); 935 } 936 937 @Override 938 public void onClick(View view) { 939 if (view.getId() == R.id.primary_action_button) { 940 CallLogAsyncTaskUtil.markCallAsRead(context, callIds); 941 } 942 943 if (view.getId() == R.id.primary_action_button && !TextUtils.isEmpty(voicemailUri)) { 944 Logger.get(context).logImpression(DialerImpression.Type.VOICEMAIL_PLAY_AUDIO_DIRECTLY); 945 voicemailPrimaryActionButtonClicked = true; 946 expandCollapseListener.onClick(primaryActionView); 947 return; 948 } 949 950 if (view.getId() == R.id.call_with_note_action) { 951 CallSubjectDialog.start( 952 (Activity) context, 953 info.photoId, 954 info.photoUri, 955 info.lookupUri, 956 (String) nameOrNumber /* top line of contact view in call subject dialog */, 957 number, 958 TextUtils.isEmpty(info.name) ? null : displayNumber, /* second line of contact 959 view in dialog. */ 960 numberType, /* phone number type (e.g. mobile) in second line of contact view */ 961 getContactType(), 962 accountHandle); 963 return; 964 } 965 966 if (view.getId() == R.id.block_report_action) { 967 Logger.get(context).logImpression(DialerImpression.Type.CALL_LOG_BLOCK_REPORT_SPAM); 968 maybeShowBlockNumberMigrationDialog( 969 new BlockedNumbersMigrator.Listener() { 970 @Override 971 public void onComplete() { 972 blockReportListener.onBlockReportSpam( 973 displayNumber, number, countryIso, callType, info.sourceType); 974 } 975 }); 976 return; 977 } 978 979 if (view.getId() == R.id.block_action) { 980 Logger.get(context).logImpression(DialerImpression.Type.CALL_LOG_BLOCK_NUMBER); 981 maybeShowBlockNumberMigrationDialog( 982 new BlockedNumbersMigrator.Listener() { 983 @Override 984 public void onComplete() { 985 blockReportListener.onBlock( 986 displayNumber, number, countryIso, callType, info.sourceType); 987 } 988 }); 989 return; 990 } 991 992 if (view.getId() == R.id.unblock_action) { 993 Logger.get(context).logImpression(DialerImpression.Type.CALL_LOG_UNBLOCK_NUMBER); 994 blockReportListener.onUnblock( 995 displayNumber, number, countryIso, callType, info.sourceType, isSpam, blockId); 996 return; 997 } 998 999 if (view.getId() == R.id.report_not_spam_action) { 1000 Logger.get(context).logImpression(DialerImpression.Type.CALL_LOG_REPORT_AS_NOT_SPAM); 1001 blockReportListener.onReportNotSpam( 1002 displayNumber, number, countryIso, callType, info.sourceType); 1003 return; 1004 } 1005 1006 if (view.getId() == R.id.call_compose_action) { 1007 LogUtil.i("CallLogListItemViewHolder.onClick", "share and call pressed"); 1008 Logger.get(context).logImpression(DialerImpression.Type.CALL_LOG_SHARE_AND_CALL); 1009 Activity activity = (Activity) context; 1010 activity.startActivityForResult( 1011 CallComposerActivity.newIntent(activity, buildContact()), 1012 ActivityRequestCodes.DIALTACTS_CALL_COMPOSER); 1013 return; 1014 } 1015 1016 if (view.getId() == R.id.share_voicemail) { 1017 Logger.get(context).logImpression(DialerImpression.Type.VVM_SHARE_PRESSED); 1018 voicemailPlaybackPresenter.shareVoicemail(); 1019 return; 1020 } 1021 1022 logCallLogAction(view.getId()); 1023 1024 final IntentProvider intentProvider = (IntentProvider) view.getTag(); 1025 if (intentProvider == null) { 1026 return; 1027 } 1028 1029 final Intent intent = intentProvider.getIntent(context); 1030 // See IntentProvider.getCallDetailIntentProvider() for why this may be null. 1031 if (intent == null) { 1032 return; 1033 } 1034 1035 // We check to see if we are starting a Duo intent. The reason is Duo 1036 // intents need to be started using startActivityForResult instead of the usual startActivity 1037 String packageName = intent.getPackage(); 1038 if (DuoConstants.PACKAGE_NAME.equals(packageName)) { 1039 startDuoActivity(intent); 1040 } else if (CallDetailsActivity.isLaunchIntent(intent)) { 1041 PerformanceReport.recordClick(UiAction.Type.OPEN_CALL_DETAIL); 1042 ((Activity) context) 1043 .startActivityForResult(intent, ActivityRequestCodes.DIALTACTS_CALL_DETAILS); 1044 } else { 1045 if (Intent.ACTION_CALL.equals(intent.getAction()) 1046 && intent.getIntExtra(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, -1) 1047 == VideoProfile.STATE_BIDIRECTIONAL) { 1048 Logger.get(context).logImpression(DialerImpression.Type.IMS_VIDEO_REQUESTED_FROM_CALL_LOG); 1049 } else if (intent.getDataString() != null 1050 && intent.getDataString().contains(DuoConstants.PACKAGE_NAME)) { 1051 Logger.get(context).logImpression(DialerImpression.Type.DUO_CALL_LOG_SET_UP_INSTALL); 1052 } 1053 1054 DialerUtils.startActivityWithErrorToast(context, intent); 1055 } 1056 } 1057 1058 private static boolean isNonContactEntry(ContactInfo info) { 1059 if (info == null || info.sourceType != Type.SOURCE_TYPE_DIRECTORY) { 1060 return true; 1061 } 1062 return false; 1063 } 1064 1065 private void startDuoActivity(Intent intent) { 1066 if (DuoConstants.DUO_ACTIVATE_ACTION.equals(intent.getAction())) { 1067 Logger.get(context).logImpression(DialerImpression.Type.DUO_CALL_LOG_SET_UP_ACTIVATE); 1068 } else if (DuoConstants.DUO_INVITE_ACTION.equals(intent.getAction())) { 1069 Logger.get(context).logImpression(DialerImpression.Type.DUO_CALL_LOG_INVITE); 1070 } else if (DuoConstants.DUO_CALL_ACTION.equals(intent.getAction())) { 1071 Logger.get(context) 1072 .logImpression(DialerImpression.Type.LIGHTBRINGER_VIDEO_REQUESTED_FROM_CALL_LOG); 1073 if (isNonContactEntry(info)) { 1074 Logger.get(context) 1075 .logImpression( 1076 DialerImpression.Type.LIGHTBRINGER_NON_CONTACT_VIDEO_REQUESTED_FROM_CALL_LOG); 1077 } 1078 } else { 1079 throw Assert.createIllegalStateFailException( 1080 "Duo intent with invalid action" + intent.getAction()); 1081 } 1082 1083 try { 1084 Activity activity = (Activity) context; 1085 activity.startActivityForResult(intent, ActivityRequestCodes.DIALTACTS_DUO); 1086 } catch (ActivityNotFoundException e) { 1087 Toast.makeText(context, R.string.activity_not_available, Toast.LENGTH_SHORT).show(); 1088 } 1089 } 1090 1091 private DialerContact buildContact() { 1092 DialerContact.Builder contact = DialerContact.newBuilder(); 1093 contact.setPhotoId(info.photoId); 1094 if (info.photoUri != null) { 1095 contact.setPhotoUri(info.photoUri.toString()); 1096 } 1097 if (info.lookupUri != null) { 1098 contact.setContactUri(info.lookupUri.toString()); 1099 } 1100 if (nameOrNumber != null) { 1101 contact.setNameOrNumber((String) nameOrNumber); 1102 } 1103 contact.setContactType(getContactType()); 1104 if (number != null) { 1105 contact.setNumber(number); 1106 } 1107 1108 if (!TextUtils.isEmpty(postDialDigits)) { 1109 contact.setPostDialDigits(postDialDigits); 1110 } 1111 1112 /* second line of contact view. */ 1113 if (!TextUtils.isEmpty(info.name)) { 1114 contact.setDisplayNumber(displayNumber); 1115 } 1116 /* phone number type (e.g. mobile) in second line of contact view */ 1117 contact.setNumberLabel(numberType); 1118 1119 /* third line of contact view. */ 1120 String accountLabel = callLogCache.getAccountLabel(accountHandle); 1121 if (!TextUtils.isEmpty(accountLabel)) { 1122 SimDetails.Builder simDetails = SimDetails.newBuilder().setNetwork(accountLabel); 1123 simDetails.setColor(callLogCache.getAccountColor(accountHandle)); 1124 contact.setSimDetails(simDetails.build()); 1125 } 1126 return contact.build(); 1127 } 1128 1129 private void logCallLogAction(int id) { 1130 if (id == R.id.send_message_action) { 1131 Logger.get(context).logImpression(DialerImpression.Type.CALL_LOG_SEND_MESSAGE); 1132 } else if (id == R.id.add_to_existing_contact_action) { 1133 Logger.get(context).logImpression(DialerImpression.Type.CALL_LOG_ADD_TO_CONTACT); 1134 switch (hostUi) { 1135 case HostUi.CALL_HISTORY: 1136 Logger.get(context) 1137 .logImpression(DialerImpression.Type.ADD_TO_A_CONTACT_FROM_CALL_HISTORY); 1138 break; 1139 case HostUi.CALL_LOG: 1140 Logger.get(context).logImpression(DialerImpression.Type.ADD_TO_A_CONTACT_FROM_CALL_LOG); 1141 break; 1142 case HostUi.VOICEMAIL: 1143 Logger.get(context).logImpression(DialerImpression.Type.ADD_TO_A_CONTACT_FROM_VOICEMAIL); 1144 break; 1145 default: 1146 throw Assert.createIllegalStateFailException(); 1147 } 1148 } else if (id == R.id.create_new_contact_action) { 1149 Logger.get(context).logImpression(DialerImpression.Type.CALL_LOG_CREATE_NEW_CONTACT); 1150 switch (hostUi) { 1151 case HostUi.CALL_HISTORY: 1152 Logger.get(context) 1153 .logImpression(DialerImpression.Type.CREATE_NEW_CONTACT_FROM_CALL_HISTORY); 1154 break; 1155 case HostUi.CALL_LOG: 1156 Logger.get(context).logImpression(DialerImpression.Type.CREATE_NEW_CONTACT_FROM_CALL_LOG); 1157 break; 1158 case HostUi.VOICEMAIL: 1159 Logger.get(context) 1160 .logImpression(DialerImpression.Type.CREATE_NEW_CONTACT_FROM_VOICEMAIL); 1161 break; 1162 default: 1163 throw Assert.createIllegalStateFailException(); 1164 } 1165 } 1166 } 1167 1168 private void maybeShowBlockNumberMigrationDialog(BlockedNumbersMigrator.Listener listener) { 1169 if (!FilteredNumberCompat.maybeShowBlockNumberMigrationDialog( 1170 context, ((Activity) context).getFragmentManager(), listener)) { 1171 listener.onComplete(); 1172 } 1173 } 1174 1175 private void updateBlockReportActions(boolean isVoicemailNumber) { 1176 // Set block/spam actions. 1177 blockReportView.setVisibility(View.GONE); 1178 blockView.setVisibility(View.GONE); 1179 unblockView.setVisibility(View.GONE); 1180 reportNotSpamView.setVisibility(View.GONE); 1181 String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso); 1182 if (isVoicemailNumber 1183 || !FilteredNumbersUtil.canBlockNumber(context, e164Number, number) 1184 || !FilteredNumberCompat.canAttemptBlockOperations(context)) { 1185 return; 1186 } 1187 boolean isBlocked = blockId != null; 1188 if (isBlocked) { 1189 unblockView.setVisibility(View.VISIBLE); 1190 } else { 1191 if (isSpamFeatureEnabled) { 1192 if (isSpam) { 1193 blockView.setVisibility(View.VISIBLE); 1194 reportNotSpamView.setVisibility(View.VISIBLE); 1195 } else { 1196 blockReportView.setVisibility(View.VISIBLE); 1197 } 1198 } else { 1199 blockView.setVisibility(View.VISIBLE); 1200 } 1201 } 1202 } 1203 1204 public void setDetailedPhoneDetails(CallDetailsEntries callDetailsEntries) { 1205 this.callDetailsEntries = callDetailsEntries; 1206 } 1207 1208 @VisibleForTesting 1209 public CallDetailsEntries getDetailedPhoneDetails() { 1210 return callDetailsEntries; 1211 } 1212 1213 @Override 1214 public void onCreateContextMenu( 1215 final ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { 1216 if (TextUtils.isEmpty(number)) { 1217 return; 1218 } 1219 1220 if (callType == CallLog.Calls.VOICEMAIL_TYPE) { 1221 menu.setHeaderTitle(context.getResources().getText(R.string.voicemail)); 1222 } else { 1223 menu.setHeaderTitle( 1224 PhoneNumberUtils.createTtsSpannable( 1225 BidiFormatter.getInstance().unicodeWrap(number, TextDirectionHeuristics.LTR))); 1226 } 1227 1228 menu.add( 1229 ContextMenu.NONE, 1230 R.id.context_menu_copy_to_clipboard, 1231 ContextMenu.NONE, 1232 R.string.action_copy_number_text) 1233 .setOnMenuItemClickListener(this); 1234 1235 // The edit number before call does not show up if any of the conditions apply: 1236 // 1) Number cannot be called 1237 // 2) Number is the voicemail number 1238 // 3) Number is a SIP address 1239 1240 if (PhoneNumberHelper.canPlaceCallsTo(number, numberPresentation) 1241 && !callLogCache.isVoicemailNumber(accountHandle, number) 1242 && !PhoneNumberHelper.isSipNumber(number)) { 1243 menu.add( 1244 ContextMenu.NONE, 1245 R.id.context_menu_edit_before_call, 1246 ContextMenu.NONE, 1247 R.string.action_edit_number_before_call) 1248 .setOnMenuItemClickListener(this); 1249 } 1250 1251 if (callType == CallLog.Calls.VOICEMAIL_TYPE 1252 && phoneCallDetailsViews.voicemailTranscriptionView.length() > 0) { 1253 menu.add( 1254 ContextMenu.NONE, 1255 R.id.context_menu_copy_transcript_to_clipboard, 1256 ContextMenu.NONE, 1257 R.string.copy_transcript_text) 1258 .setOnMenuItemClickListener(this); 1259 } 1260 1261 String e164Number = PhoneNumberUtils.formatNumberToE164(number, countryIso); 1262 boolean isVoicemailNumber = callLogCache.isVoicemailNumber(accountHandle, number); 1263 if (!isVoicemailNumber 1264 && FilteredNumbersUtil.canBlockNumber(context, e164Number, number) 1265 && FilteredNumberCompat.canAttemptBlockOperations(context)) { 1266 boolean isBlocked = blockId != null; 1267 if (isBlocked) { 1268 menu.add( 1269 ContextMenu.NONE, 1270 R.id.context_menu_unblock, 1271 ContextMenu.NONE, 1272 R.string.call_log_action_unblock_number) 1273 .setOnMenuItemClickListener(this); 1274 } else { 1275 if (isSpamFeatureEnabled) { 1276 if (isSpam) { 1277 menu.add( 1278 ContextMenu.NONE, 1279 R.id.context_menu_report_not_spam, 1280 ContextMenu.NONE, 1281 R.string.call_log_action_remove_spam) 1282 .setOnMenuItemClickListener(this); 1283 menu.add( 1284 ContextMenu.NONE, 1285 R.id.context_menu_block, 1286 ContextMenu.NONE, 1287 R.string.call_log_action_block_number) 1288 .setOnMenuItemClickListener(this); 1289 } else { 1290 menu.add( 1291 ContextMenu.NONE, 1292 R.id.context_menu_block_report_spam, 1293 ContextMenu.NONE, 1294 R.string.call_log_action_block_report_number) 1295 .setOnMenuItemClickListener(this); 1296 } 1297 } else { 1298 menu.add( 1299 ContextMenu.NONE, 1300 R.id.context_menu_block, 1301 ContextMenu.NONE, 1302 R.string.call_log_action_block_number) 1303 .setOnMenuItemClickListener(this); 1304 } 1305 } 1306 } 1307 1308 if (callType != CallLog.Calls.VOICEMAIL_TYPE) { 1309 menu.add(ContextMenu.NONE, R.id.context_menu_delete, ContextMenu.NONE, R.string.delete) 1310 .setOnMenuItemClickListener(this); 1311 } 1312 1313 Logger.get(context).logScreenView(ScreenEvent.Type.CALL_LOG_CONTEXT_MENU, (Activity) context); 1314 } 1315 1316 /** Specifies where the view holder belongs. */ 1317 @IntDef({HostUi.CALL_LOG, HostUi.CALL_HISTORY, HostUi.VOICEMAIL}) 1318 @Retention(RetentionPolicy.SOURCE) 1319 private @interface HostUi { 1320 int CALL_LOG = 0; 1321 int CALL_HISTORY = 1; 1322 int VOICEMAIL = 2; 1323 } 1324 1325 public interface OnClickListener { 1326 1327 void onBlockReportSpam( 1328 String displayNumber, 1329 String number, 1330 String countryIso, 1331 int callType, 1332 ContactSource.Type contactSourceType); 1333 1334 void onBlock( 1335 String displayNumber, 1336 String number, 1337 String countryIso, 1338 int callType, 1339 ContactSource.Type contactSourceType); 1340 1341 void onUnblock( 1342 String displayNumber, 1343 String number, 1344 String countryIso, 1345 int callType, 1346 ContactSource.Type contactSourceType, 1347 boolean isSpam, 1348 Integer blockId); 1349 1350 void onReportNotSpam( 1351 String displayNumber, 1352 String number, 1353 String countryIso, 1354 int callType, 1355 ContactSource.Type contactSourceType); 1356 } 1357 1358 private static class DeleteCallTask extends AsyncTask<Void, Void, Void> { 1359 // Using a weak reference to hold the Context so that there is no memory leak. 1360 private final WeakReference<Context> contextWeakReference; 1361 1362 private final String callIdsStr; 1363 1364 DeleteCallTask(Context context, long[] callIdsArray) { 1365 this.contextWeakReference = new WeakReference<>(context); 1366 this.callIdsStr = concatCallIds(callIdsArray); 1367 } 1368 1369 @Override 1370 // Suppress the lint check here as the user will not be able to see call log entries if 1371 // permission.WRITE_CALL_LOG is not granted. 1372 @SuppressLint("MissingPermission") 1373 @RequiresPermission(value = permission.WRITE_CALL_LOG) 1374 protected Void doInBackground(Void... params) { 1375 Context context = contextWeakReference.get(); 1376 if (context == null) { 1377 return null; 1378 } 1379 1380 if (callIdsStr != null) { 1381 context 1382 .getContentResolver() 1383 .delete( 1384 Calls.CONTENT_URI, 1385 CallLog.Calls._ID + " IN (" + callIdsStr + ")" /* where */, 1386 null /* selectionArgs */); 1387 } 1388 1389 return null; 1390 } 1391 1392 @Override 1393 public void onPostExecute(Void result) {} 1394 1395 private String concatCallIds(long[] callIds) { 1396 if (callIds == null || callIds.length == 0) { 1397 return null; 1398 } 1399 1400 StringBuilder str = new StringBuilder(); 1401 for (long callId : callIds) { 1402 if (str.length() != 0) { 1403 str.append(","); 1404 } 1405 str.append(callId); 1406 } 1407 1408 return str.toString(); 1409 } 1410 } 1411 } 1412