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