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