1 /* 2 * Copyright (C) 2016 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.incallui.contactgrid; 18 19 import android.content.Context; 20 import android.graphics.drawable.Animatable; 21 import android.graphics.drawable.Drawable; 22 import android.os.SystemClock; 23 import android.support.annotation.Nullable; 24 import android.support.v4.view.ViewCompat; 25 import android.telephony.PhoneNumberUtils; 26 import android.text.BidiFormatter; 27 import android.text.TextDirectionHeuristics; 28 import android.text.TextUtils; 29 import android.view.View; 30 import android.view.accessibility.AccessibilityEvent; 31 import android.widget.Chronometer; 32 import android.widget.ImageView; 33 import android.widget.Space; 34 import android.widget.TextView; 35 import android.widget.ViewAnimator; 36 import com.android.dialer.common.Assert; 37 import com.android.dialer.common.LogUtil; 38 import com.android.dialer.configprovider.ConfigProviderComponent; 39 import com.android.dialer.glidephotomanager.GlidePhotoManagerComponent; 40 import com.android.dialer.glidephotomanager.PhotoInfo; 41 import com.android.dialer.lettertile.LetterTileDrawable; 42 import com.android.dialer.util.DrawableConverter; 43 import com.android.dialer.widget.BidiTextView; 44 import com.android.incallui.incall.protocol.ContactPhotoType; 45 import com.android.incallui.incall.protocol.PrimaryCallState; 46 import com.android.incallui.incall.protocol.PrimaryInfo; 47 import java.util.List; 48 49 /** Utility to manage the Contact grid */ 50 public class ContactGridManager { 51 52 private final Context context; 53 private final View contactGridLayout; 54 55 // Row 0: Captain Holt ON HOLD 56 // Row 0: Calling... 57 // Row 0: [Wi-Fi icon] Calling via Starbucks Wi-Fi 58 // Row 0: [Wi-Fi icon] Starbucks Wi-Fi 59 // Row 0: Hey Jake, pick up! 60 private final ImageView connectionIconImageView; 61 private final TextView statusTextView; 62 63 // Row 1: Jake Peralta [Contact photo] 64 // Row 1: Walgreens 65 // Row 1: +1 (650) 253-0000 66 private final TextView contactNameTextView; 67 @Nullable private ImageView avatarImageView; 68 69 // Row 2: Mobile +1 (650) 253-0000 70 // Row 2: [HD attempting icon]/[HD icon] 00:15 71 // Row 2: Call ended 72 // Row 2: Hanging up 73 // Row 2: [Alert sign] Suspected spam caller 74 // Row 2: Your emergency callback number: +1 (650) 253-0000 75 private final ImageView workIconImageView; 76 private final ImageView hdIconImageView; 77 private final ImageView forwardIconImageView; 78 private final TextView forwardedNumberView; 79 private final ImageView spamIconImageView; 80 private final ViewAnimator bottomTextSwitcher; 81 private final BidiTextView bottomTextView; 82 private final Chronometer bottomTimerView; 83 private final Space topRowSpace; 84 private int avatarSize; 85 private boolean hideAvatar; 86 private boolean showAnonymousAvatar; 87 private boolean middleRowVisible = true; 88 private boolean isTimerStarted; 89 90 // Row in emergency call: This phone's number: +1 (650) 253-0000 91 private final TextView deviceNumberTextView; 92 private final View deviceNumberDivider; 93 94 private PrimaryInfo primaryInfo = PrimaryInfo.empty(); 95 private PrimaryCallState primaryCallState = PrimaryCallState.empty(); 96 private final LetterTileDrawable letterTile; 97 private boolean isInMultiWindowMode; 98 ContactGridManager( View view, @Nullable ImageView avatarImageView, int avatarSize, boolean showAnonymousAvatar)99 public ContactGridManager( 100 View view, @Nullable ImageView avatarImageView, int avatarSize, boolean showAnonymousAvatar) { 101 context = view.getContext(); 102 Assert.isNotNull(context); 103 104 this.avatarImageView = avatarImageView; 105 this.avatarSize = avatarSize; 106 this.showAnonymousAvatar = showAnonymousAvatar; 107 connectionIconImageView = view.findViewById(R.id.contactgrid_connection_icon); 108 statusTextView = view.findViewById(R.id.contactgrid_status_text); 109 contactNameTextView = view.findViewById(R.id.contactgrid_contact_name); 110 workIconImageView = view.findViewById(R.id.contactgrid_workIcon); 111 hdIconImageView = view.findViewById(R.id.contactgrid_hdIcon); 112 forwardIconImageView = view.findViewById(R.id.contactgrid_forwardIcon); 113 forwardedNumberView = view.findViewById(R.id.contactgrid_forwardNumber); 114 spamIconImageView = view.findViewById(R.id.contactgrid_spamIcon); 115 bottomTextSwitcher = view.findViewById(R.id.contactgrid_bottom_text_switcher); 116 bottomTextView = view.findViewById(R.id.contactgrid_bottom_text); 117 bottomTimerView = view.findViewById(R.id.contactgrid_bottom_timer); 118 topRowSpace = view.findViewById(R.id.contactgrid_top_row_space); 119 120 contactGridLayout = (View) contactNameTextView.getParent(); 121 letterTile = new LetterTileDrawable(context.getResources()); 122 isTimerStarted = false; 123 124 deviceNumberTextView = view.findViewById(R.id.contactgrid_device_number_text); 125 deviceNumberDivider = view.findViewById(R.id.contactgrid_location_divider); 126 } 127 show()128 public void show() { 129 contactGridLayout.setVisibility(View.VISIBLE); 130 } 131 hide()132 public void hide() { 133 contactGridLayout.setVisibility(View.GONE); 134 } 135 setAvatarHidden(boolean hide)136 public void setAvatarHidden(boolean hide) { 137 if (hide != hideAvatar) { 138 hideAvatar = hide; 139 updatePrimaryNameAndPhoto(); 140 } 141 } 142 isAvatarHidden()143 public boolean isAvatarHidden() { 144 return hideAvatar; 145 } 146 getContainerView()147 public View getContainerView() { 148 return contactGridLayout; 149 } 150 setIsMiddleRowVisible(boolean isMiddleRowVisible)151 public void setIsMiddleRowVisible(boolean isMiddleRowVisible) { 152 if (middleRowVisible == isMiddleRowVisible) { 153 return; 154 } 155 middleRowVisible = isMiddleRowVisible; 156 157 contactNameTextView.setVisibility(isMiddleRowVisible ? View.VISIBLE : View.GONE); 158 updateAvatarVisibility(); 159 } 160 setPrimary(PrimaryInfo primaryInfo)161 public void setPrimary(PrimaryInfo primaryInfo) { 162 this.primaryInfo = primaryInfo; 163 updatePrimaryNameAndPhoto(); 164 updateBottomRow(); 165 updateDeviceNumberRow(); 166 } 167 setCallState(PrimaryCallState primaryCallState)168 public void setCallState(PrimaryCallState primaryCallState) { 169 this.primaryCallState = primaryCallState; 170 updatePrimaryNameAndPhoto(); 171 updateBottomRow(); 172 updateTopRow(); 173 updateDeviceNumberRow(); 174 } 175 dispatchPopulateAccessibilityEvent(AccessibilityEvent event)176 public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 177 dispatchPopulateAccessibilityEvent(event, statusTextView); 178 dispatchPopulateAccessibilityEvent(event, contactNameTextView); 179 BottomRow.Info info = BottomRow.getInfo(context, primaryCallState, primaryInfo); 180 if (info.shouldPopulateAccessibilityEvent) { 181 dispatchPopulateAccessibilityEvent(event, bottomTextView); 182 } 183 } 184 setAvatarImageView( @ullable ImageView avatarImageView, int avatarSize, boolean showAnonymousAvatar)185 public void setAvatarImageView( 186 @Nullable ImageView avatarImageView, int avatarSize, boolean showAnonymousAvatar) { 187 this.avatarImageView = avatarImageView; 188 this.avatarSize = avatarSize; 189 this.showAnonymousAvatar = showAnonymousAvatar; 190 updatePrimaryNameAndPhoto(); 191 } 192 onMultiWindowModeChanged(boolean isInMultiWindowMode)193 public void onMultiWindowModeChanged(boolean isInMultiWindowMode) { 194 if (this.isInMultiWindowMode == isInMultiWindowMode) { 195 return; 196 } 197 this.isInMultiWindowMode = isInMultiWindowMode; 198 updateDeviceNumberRow(); 199 } 200 dispatchPopulateAccessibilityEvent(AccessibilityEvent event, View view)201 private void dispatchPopulateAccessibilityEvent(AccessibilityEvent event, View view) { 202 final List<CharSequence> eventText = event.getText(); 203 int size = eventText.size(); 204 view.dispatchPopulateAccessibilityEvent(event); 205 // If no text added write null to keep relative position. 206 if (size == eventText.size()) { 207 eventText.add(null); 208 } 209 } 210 updateAvatarVisibility()211 private boolean updateAvatarVisibility() { 212 if (avatarImageView == null) { 213 return false; 214 } 215 216 if (!middleRowVisible) { 217 avatarImageView.setVisibility(View.GONE); 218 return false; 219 } 220 221 boolean hasPhoto = 222 (primaryInfo.photo() != null || primaryInfo.photoUri() != null) 223 && primaryInfo.photoType() == ContactPhotoType.CONTACT; 224 if (!hasPhoto && !showAnonymousAvatar) { 225 avatarImageView.setVisibility(View.GONE); 226 return false; 227 } 228 229 avatarImageView.setVisibility(View.VISIBLE); 230 return true; 231 } 232 233 /** 234 * Updates row 0. For example: 235 * 236 * <ul> 237 * <li>Captain Holt ON HOLD 238 * <li>Calling... 239 * <li>[Wi-Fi icon] Calling via Starbucks Wi-Fi 240 * <li>[Wi-Fi icon] Starbucks Wi-Fi 241 * <li>Call from 242 * </ul> 243 */ updateTopRow()244 private void updateTopRow() { 245 TopRow.Info info = TopRow.getInfo(context, primaryCallState, primaryInfo); 246 if (TextUtils.isEmpty(info.label)) { 247 // Use INVISIBLE here to prevent the rows below this one from moving up and down. 248 statusTextView.setVisibility(View.INVISIBLE); 249 statusTextView.setText(null); 250 } else { 251 statusTextView.setText(info.label); 252 statusTextView.setVisibility(View.VISIBLE); 253 statusTextView.setSingleLine(info.labelIsSingleLine); 254 // Required to start the marquee 255 // This will send a AccessibilityEvent.TYPE_VIEW_SELECTED, but has no observable effect on 256 // talkback. 257 statusTextView.setSelected(true); 258 } 259 260 if (info.icon == null) { 261 connectionIconImageView.setVisibility(View.GONE); 262 topRowSpace.setVisibility(View.GONE); 263 } else { 264 connectionIconImageView.setVisibility(View.VISIBLE); 265 connectionIconImageView.setImageDrawable(info.icon); 266 if (statusTextView.getVisibility() == View.VISIBLE 267 && !TextUtils.isEmpty(statusTextView.getText())) { 268 topRowSpace.setVisibility(View.VISIBLE); 269 } else { 270 topRowSpace.setVisibility(View.GONE); 271 } 272 } 273 } 274 275 /** 276 * Updates row 1. For example: 277 * 278 * <ul> 279 * <li>Jake Peralta [Contact photo] 280 * <li>Walgreens 281 * <li>+1 (650) 253-0000 282 * </ul> 283 */ updatePrimaryNameAndPhoto()284 private void updatePrimaryNameAndPhoto() { 285 if (TextUtils.isEmpty(primaryInfo.name())) { 286 contactNameTextView.setText(null); 287 } else { 288 contactNameTextView.setText( 289 primaryInfo.nameIsNumber() 290 ? PhoneNumberUtils.createTtsSpannable(primaryInfo.name()) 291 : primaryInfo.name()); 292 293 // Set direction of the name field 294 int nameDirection = View.TEXT_DIRECTION_INHERIT; 295 if (primaryInfo.nameIsNumber()) { 296 nameDirection = View.TEXT_DIRECTION_LTR; 297 } 298 contactNameTextView.setTextDirection(nameDirection); 299 } 300 301 if (avatarImageView != null) { 302 if (hideAvatar) { 303 avatarImageView.setVisibility(View.GONE); 304 } else if (avatarSize > 0 && updateAvatarVisibility()) { 305 if (ConfigProviderComponent.get(context) 306 .getConfigProvider() 307 .getBoolean("enable_glide_photo", false)) { 308 loadPhotoWithGlide(); 309 } else { 310 loadPhotoWithLegacy(); 311 } 312 } 313 } 314 } 315 loadPhotoWithGlide()316 private void loadPhotoWithGlide() { 317 PhotoInfo.Builder photoInfoBuilder = 318 PhotoInfo.newBuilder() 319 .setIsBusiness(primaryInfo.photoType() == ContactPhotoType.BUSINESS) 320 .setIsVoicemail(primaryCallState.isVoiceMailNumber()) 321 .setIsSpam(primaryInfo.isSpam()) 322 .setIsConference(primaryCallState.isConference()); 323 324 // Contact has a name, that is a number. 325 if (primaryInfo.nameIsNumber() && primaryInfo.number() != null) { 326 photoInfoBuilder.setName(primaryInfo.number()); 327 } else if (primaryInfo.name() != null) { 328 photoInfoBuilder.setName(primaryInfo.name()); 329 } 330 331 if (primaryInfo.number() != null) { 332 photoInfoBuilder.setFormattedNumber(primaryInfo.number()); 333 } 334 335 if (primaryInfo.photoUri() != null) { 336 photoInfoBuilder.setPhotoUri(primaryInfo.photoUri().toString()); 337 } 338 339 if (primaryInfo.contactInfoLookupKey() != null) { 340 photoInfoBuilder.setLookupUri(primaryInfo.contactInfoLookupKey()); 341 } 342 343 GlidePhotoManagerComponent.get(context) 344 .glidePhotoManager() 345 .loadContactPhoto(avatarImageView, photoInfoBuilder.build()); 346 } 347 loadPhotoWithLegacy()348 private void loadPhotoWithLegacy() { 349 boolean hasPhoto = 350 primaryInfo.photo() != null && primaryInfo.photoType() == ContactPhotoType.CONTACT; 351 if (hasPhoto) { 352 avatarImageView.setBackground( 353 DrawableConverter.getRoundedDrawable( 354 context, primaryInfo.photo(), avatarSize, avatarSize)); 355 } else { 356 // Contact has a photo, don't render a letter tile. 357 letterTile.setCanonicalDialerLetterTileDetails( 358 primaryInfo.name(), 359 primaryInfo.contactInfoLookupKey(), 360 LetterTileDrawable.SHAPE_CIRCLE, 361 LetterTileDrawable.getContactTypeFromPrimitives( 362 primaryCallState.isVoiceMailNumber(), 363 primaryInfo.isSpam(), 364 primaryCallState.isBusinessNumber(), 365 primaryInfo.numberPresentation(), 366 primaryCallState.isConference())); 367 // By invalidating the avatarImageView we force a redraw of the letter tile. 368 // This is required to properly display the updated letter tile iconography based on the 369 // contact type, because the background drawable reference cached in the view, and the 370 // view is not aware of the mutations made to the background. 371 avatarImageView.invalidate(); 372 avatarImageView.setBackground(letterTile); 373 } 374 } 375 /** 376 * Updates row 2. For example: 377 * 378 * <ul> 379 * <li>Mobile +1 (650) 253-0000 380 * <li>[HD attempting icon]/[HD icon] 00:15 381 * <li>Call ended 382 * <li>Hanging up 383 * </ul> 384 */ updateBottomRow()385 private void updateBottomRow() { 386 BottomRow.Info info = BottomRow.getInfo(context, primaryCallState, primaryInfo); 387 388 bottomTextView.setText(info.label); 389 bottomTextView.setAllCaps(info.isSpamIconVisible); 390 workIconImageView.setVisibility(info.isWorkIconVisible ? View.VISIBLE : View.GONE); 391 if (hdIconImageView.getVisibility() == View.GONE) { 392 if (info.isHdAttemptingIconVisible) { 393 hdIconImageView.setImageResource(R.drawable.asd_hd_icon); 394 hdIconImageView.setVisibility(View.VISIBLE); 395 hdIconImageView.setActivated(false); 396 Drawable drawableCurrent = hdIconImageView.getDrawable().getCurrent(); 397 if (drawableCurrent instanceof Animatable && !((Animatable) drawableCurrent).isRunning()) { 398 ((Animatable) drawableCurrent).start(); 399 } 400 } else if (info.isHdIconVisible) { 401 hdIconImageView.setImageResource(R.drawable.asd_hd_icon); 402 hdIconImageView.setVisibility(View.VISIBLE); 403 hdIconImageView.setActivated(true); 404 } 405 } else if (info.isHdIconVisible) { 406 hdIconImageView.setActivated(true); 407 } else if (!info.isHdAttemptingIconVisible) { 408 hdIconImageView.setVisibility(View.GONE); 409 } 410 spamIconImageView.setVisibility(info.isSpamIconVisible ? View.VISIBLE : View.GONE); 411 412 if (info.isForwardIconVisible) { 413 forwardIconImageView.setVisibility(View.VISIBLE); 414 forwardedNumberView.setVisibility(View.VISIBLE); 415 if (info.isTimerVisible) { 416 bottomTextSwitcher.setVisibility(View.VISIBLE); 417 if (ViewCompat.getLayoutDirection(contactGridLayout) == ViewCompat.LAYOUT_DIRECTION_LTR) { 418 forwardedNumberView.setText(TextUtils.concat(info.label, " • ")); 419 } else { 420 forwardedNumberView.setText(TextUtils.concat(" • ", info.label)); 421 } 422 } else { 423 bottomTextSwitcher.setVisibility(View.GONE); 424 forwardedNumberView.setText(info.label); 425 } 426 } else { 427 forwardIconImageView.setVisibility(View.GONE); 428 forwardedNumberView.setVisibility(View.GONE); 429 bottomTextSwitcher.setVisibility(View.VISIBLE); 430 } 431 432 if (info.isTimerVisible) { 433 bottomTextSwitcher.setDisplayedChild(1); 434 bottomTimerView.setBase( 435 primaryCallState.connectTimeMillis() 436 - System.currentTimeMillis() 437 + SystemClock.elapsedRealtime()); 438 if (!isTimerStarted) { 439 LogUtil.i( 440 "ContactGridManager.updateBottomRow", 441 "starting timer with base: %d", 442 bottomTimerView.getBase()); 443 bottomTimerView.start(); 444 isTimerStarted = true; 445 } 446 } else { 447 bottomTextSwitcher.setDisplayedChild(0); 448 bottomTimerView.stop(); 449 isTimerStarted = false; 450 } 451 } 452 updateDeviceNumberRow()453 private void updateDeviceNumberRow() { 454 // It might not be available, e.g. in video call. 455 if (deviceNumberTextView == null) { 456 return; 457 } 458 if (isInMultiWindowMode || TextUtils.isEmpty(primaryCallState.callbackNumber())) { 459 deviceNumberTextView.setVisibility(View.GONE); 460 deviceNumberDivider.setVisibility(View.GONE); 461 return; 462 } 463 // This is used for carriers like Project Fi to show the callback number for emergency calls. 464 deviceNumberTextView.setText( 465 context.getString( 466 R.string.contact_grid_callback_number, 467 BidiFormatter.getInstance() 468 .unicodeWrap(primaryCallState.callbackNumber(), TextDirectionHeuristics.LTR))); 469 deviceNumberTextView.setVisibility(View.VISIBLE); 470 if (primaryInfo.shouldShowLocation()) { 471 deviceNumberDivider.setVisibility(View.VISIBLE); 472 } 473 } 474 } 475