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.NonNull; 24 import android.support.annotation.Nullable; 25 import android.support.v4.view.ViewCompat; 26 import android.telecom.TelecomManager; 27 import android.text.TextUtils; 28 import android.view.View; 29 import android.view.accessibility.AccessibilityEvent; 30 import android.widget.Chronometer; 31 import android.widget.ImageView; 32 import android.widget.TextView; 33 import android.widget.ViewAnimator; 34 import com.android.contacts.common.compat.PhoneNumberUtilsCompat; 35 import com.android.contacts.common.lettertiles.LetterTileDrawable; 36 import com.android.dialer.common.Assert; 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 int avatarSize; 78 private boolean hideAvatar; 79 private boolean showAnonymousAvatar; 80 private boolean middleRowVisible = true; 81 82 private PrimaryInfo primaryInfo = PrimaryInfo.createEmptyPrimaryInfo(); 83 private PrimaryCallState primaryCallState = PrimaryCallState.createEmptyPrimaryCallState(); 84 private final LetterTileDrawable letterTile; 85 ContactGridManager( View view, @Nullable ImageView avatarImageView, int avatarSize, boolean showAnonymousAvatar)86 public ContactGridManager( 87 View view, @Nullable ImageView avatarImageView, int avatarSize, boolean showAnonymousAvatar) { 88 context = view.getContext(); 89 Assert.isNotNull(context); 90 91 this.avatarImageView = avatarImageView; 92 this.avatarSize = avatarSize; 93 this.showAnonymousAvatar = showAnonymousAvatar; 94 connectionIconImageView = view.findViewById(R.id.contactgrid_connection_icon); 95 statusTextView = view.findViewById(R.id.contactgrid_status_text); 96 contactNameTextView = view.findViewById(R.id.contactgrid_contact_name); 97 workIconImageView = view.findViewById(R.id.contactgrid_workIcon); 98 hdIconImageView = view.findViewById(R.id.contactgrid_hdIcon); 99 forwardIconImageView = view.findViewById(R.id.contactgrid_forwardIcon); 100 forwardedNumberView = view.findViewById(R.id.contactgrid_forwardNumber); 101 spamIconImageView = view.findViewById(R.id.contactgrid_spamIcon); 102 bottomTextSwitcher = view.findViewById(R.id.contactgrid_bottom_text_switcher); 103 bottomTextView = view.findViewById(R.id.contactgrid_bottom_text); 104 bottomTimerView = view.findViewById(R.id.contactgrid_bottom_timer); 105 106 contactGridLayout = (View) contactNameTextView.getParent(); 107 letterTile = new LetterTileDrawable(context.getResources()); 108 } 109 show()110 public void show() { 111 contactGridLayout.setVisibility(View.VISIBLE); 112 } 113 hide()114 public void hide() { 115 contactGridLayout.setVisibility(View.GONE); 116 } 117 setAvatarHidden(boolean hide)118 public void setAvatarHidden(boolean hide) { 119 if (hide != hideAvatar) { 120 hideAvatar = hide; 121 updatePrimaryNameAndPhoto(); 122 } 123 } 124 isAvatarHidden()125 public boolean isAvatarHidden() { 126 return hideAvatar; 127 } 128 getContainerView()129 public View getContainerView() { 130 return contactGridLayout; 131 } 132 setIsMiddleRowVisible(boolean isMiddleRowVisible)133 public void setIsMiddleRowVisible(boolean isMiddleRowVisible) { 134 if (middleRowVisible == isMiddleRowVisible) { 135 return; 136 } 137 middleRowVisible = isMiddleRowVisible; 138 139 contactNameTextView.setVisibility(isMiddleRowVisible ? View.VISIBLE : View.GONE); 140 updateAvatarVisibility(); 141 } 142 setPrimary(PrimaryInfo primaryInfo)143 public void setPrimary(PrimaryInfo primaryInfo) { 144 this.primaryInfo = primaryInfo; 145 updatePrimaryNameAndPhoto(); 146 updateBottomRow(); 147 } 148 setCallState(PrimaryCallState primaryCallState)149 public void setCallState(PrimaryCallState primaryCallState) { 150 this.primaryCallState = primaryCallState; 151 updatePrimaryNameAndPhoto(); 152 updateBottomRow(); 153 updateTopRow(); 154 } 155 dispatchPopulateAccessibilityEvent(AccessibilityEvent event)156 public void dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { 157 dispatchPopulateAccessibilityEvent(event, statusTextView); 158 dispatchPopulateAccessibilityEvent(event, contactNameTextView); 159 BottomRow.Info info = BottomRow.getInfo(context, primaryCallState, primaryInfo); 160 if (info.shouldPopulateAccessibilityEvent) { 161 dispatchPopulateAccessibilityEvent(event, bottomTextView); 162 } 163 } 164 setAvatarImageView( @ullable ImageView avatarImageView, int avatarSize, boolean showAnonymousAvatar)165 public void setAvatarImageView( 166 @Nullable ImageView avatarImageView, int avatarSize, boolean showAnonymousAvatar) { 167 this.avatarImageView = avatarImageView; 168 this.avatarSize = avatarSize; 169 this.showAnonymousAvatar = showAnonymousAvatar; 170 updatePrimaryNameAndPhoto(); 171 } 172 dispatchPopulateAccessibilityEvent(AccessibilityEvent event, View view)173 private void dispatchPopulateAccessibilityEvent(AccessibilityEvent event, View view) { 174 final List<CharSequence> eventText = event.getText(); 175 int size = eventText.size(); 176 view.dispatchPopulateAccessibilityEvent(event); 177 // If no text added write null to keep relative position. 178 if (size == eventText.size()) { 179 eventText.add(null); 180 } 181 } 182 updateAvatarVisibility()183 private boolean updateAvatarVisibility() { 184 if (avatarImageView == null) { 185 return false; 186 } 187 188 if (!middleRowVisible) { 189 avatarImageView.setVisibility(View.GONE); 190 return false; 191 } 192 193 boolean hasPhoto = 194 primaryInfo.photo != null && primaryInfo.photoType == ContactPhotoType.CONTACT; 195 if (!hasPhoto && !showAnonymousAvatar) { 196 avatarImageView.setVisibility(View.GONE); 197 return false; 198 } 199 200 avatarImageView.setVisibility(View.VISIBLE); 201 return true; 202 } 203 204 /** 205 * Updates row 0. For example: 206 * 207 * <ul> 208 * <li>Captain Holt ON HOLD 209 * <li>Calling... 210 * <li>[Wi-Fi icon] Calling via Starbucks Wi-Fi 211 * <li>[Wi-Fi icon] Starbucks Wi-Fi 212 * <li>Call from 213 * </ul> 214 */ updateTopRow()215 private void updateTopRow() { 216 TopRow.Info info = TopRow.getInfo(context, primaryCallState); 217 if (TextUtils.isEmpty(info.label)) { 218 // Use INVISIBLE here to prevent the rows below this one from moving up and down. 219 statusTextView.setVisibility(View.INVISIBLE); 220 statusTextView.setText(null); 221 } else { 222 statusTextView.setText(info.label); 223 statusTextView.setVisibility(View.VISIBLE); 224 statusTextView.setSingleLine(info.labelIsSingleLine); 225 } 226 227 if (info.icon == null) { 228 connectionIconImageView.setVisibility(View.GONE); 229 } else { 230 connectionIconImageView.setVisibility(View.VISIBLE); 231 connectionIconImageView.setImageDrawable(info.icon); 232 } 233 } 234 235 /** 236 * Returns the appropriate LetterTileDrawable.TYPE_ based on a given call state. 237 * 238 * <p>If no special state is detected, yields TYPE_DEFAULT. 239 */ getContactTypeForPrimaryCallState( @onNull PrimaryCallState callState, @NonNull PrimaryInfo primaryInfo)240 private static @LetterTileDrawable.ContactType int getContactTypeForPrimaryCallState( 241 @NonNull PrimaryCallState callState, @NonNull PrimaryInfo primaryInfo) { 242 if (callState.isVoiceMailNumber) { 243 return LetterTileDrawable.TYPE_VOICEMAIL; 244 } else if (callState.isBusinessNumber) { 245 return LetterTileDrawable.TYPE_BUSINESS; 246 } else if (primaryInfo.numberPresentation == TelecomManager.PRESENTATION_RESTRICTED) { 247 return LetterTileDrawable.TYPE_GENERIC_AVATAR; 248 } else if (callState.isConference) { 249 return LetterTileDrawable.TYPE_CONFERENCE; 250 } else { 251 return LetterTileDrawable.TYPE_DEFAULT; 252 } 253 } 254 255 /** 256 * Updates row 1. For example: 257 * 258 * <ul> 259 * <li>Jake Peralta [Contact photo] 260 * <li>Walgreens 261 * <li>+1 (650) 253-0000 262 * </ul> 263 */ updatePrimaryNameAndPhoto()264 private void updatePrimaryNameAndPhoto() { 265 if (TextUtils.isEmpty(primaryInfo.name)) { 266 contactNameTextView.setText(null); 267 } else { 268 contactNameTextView.setText( 269 primaryInfo.nameIsNumber 270 ? PhoneNumberUtilsCompat.createTtsSpannable(primaryInfo.name) 271 : primaryInfo.name); 272 273 // Set direction of the name field 274 int nameDirection = View.TEXT_DIRECTION_INHERIT; 275 if (primaryInfo.nameIsNumber) { 276 nameDirection = View.TEXT_DIRECTION_LTR; 277 } 278 contactNameTextView.setTextDirection(nameDirection); 279 } 280 281 if (avatarImageView != null) { 282 if (hideAvatar) { 283 avatarImageView.setVisibility(View.GONE); 284 } else if (avatarSize > 0 && updateAvatarVisibility()) { 285 boolean hasPhoto = 286 primaryInfo.photo != null && primaryInfo.photoType == ContactPhotoType.CONTACT; 287 // Contact has a photo, don't render a letter tile. 288 if (hasPhoto) { 289 avatarImageView.setBackground( 290 DrawableConverter.getRoundedDrawable( 291 context, primaryInfo.photo, avatarSize, avatarSize)); 292 // Contact has a name, that isn't a number. 293 } else { 294 letterTile.setCanonicalDialerLetterTileDetails( 295 primaryInfo.name, 296 primaryInfo.contactInfoLookupKey, 297 LetterTileDrawable.SHAPE_CIRCLE, 298 getContactTypeForPrimaryCallState(primaryCallState, primaryInfo)); 299 300 // By invalidating the avatarImageView we force a redraw of the letter tile. 301 // This is required to properly display the updated letter tile iconography based on the 302 // contact type, because the background drawable reference cached in the view, and the 303 // view is not aware of the mutations made to the background. 304 avatarImageView.invalidate(); 305 avatarImageView.setBackground(letterTile); 306 } 307 } 308 } 309 } 310 311 /** 312 * Updates row 2. For example: 313 * 314 * <ul> 315 * <li>Mobile +1 (650) 253-0000 316 * <li>[HD attempting icon]/[HD icon] 00:15 317 * <li>Call ended 318 * <li>Hanging up 319 * </ul> 320 */ updateBottomRow()321 private void updateBottomRow() { 322 BottomRow.Info info = BottomRow.getInfo(context, primaryCallState, primaryInfo); 323 324 bottomTextView.setText(info.label); 325 bottomTextView.setAllCaps(info.isSpamIconVisible); 326 workIconImageView.setVisibility(info.isWorkIconVisible ? View.VISIBLE : View.GONE); 327 if (hdIconImageView.getVisibility() == View.GONE) { 328 if (info.isHdAttemptingIconVisible) { 329 hdIconImageView.setVisibility(View.VISIBLE); 330 hdIconImageView.setActivated(false); 331 Drawable drawableCurrent = hdIconImageView.getDrawable().getCurrent(); 332 if (drawableCurrent instanceof Animatable && !((Animatable) drawableCurrent).isRunning()) { 333 ((Animatable) drawableCurrent).start(); 334 } 335 } else if (info.isHdIconVisible) { 336 hdIconImageView.setVisibility(View.VISIBLE); 337 hdIconImageView.setActivated(true); 338 } 339 } else if (info.isHdIconVisible) { 340 hdIconImageView.setActivated(true); 341 } else if (!info.isHdAttemptingIconVisible) { 342 hdIconImageView.setVisibility(View.GONE); 343 } 344 spamIconImageView.setVisibility(info.isSpamIconVisible ? View.VISIBLE : View.GONE); 345 346 if (info.isForwardIconVisible) { 347 forwardIconImageView.setVisibility(View.VISIBLE); 348 forwardedNumberView.setVisibility(View.VISIBLE); 349 if (info.isTimerVisible) { 350 bottomTextSwitcher.setVisibility(View.VISIBLE); 351 if (ViewCompat.getLayoutDirection(contactGridLayout) == ViewCompat.LAYOUT_DIRECTION_LTR) { 352 forwardedNumberView.setText(TextUtils.concat(info.label, " • ")); 353 } else { 354 forwardedNumberView.setText(TextUtils.concat(" • ", info.label)); 355 } 356 } else { 357 bottomTextSwitcher.setVisibility(View.GONE); 358 forwardedNumberView.setText(info.label); 359 } 360 } else { 361 forwardIconImageView.setVisibility(View.GONE); 362 forwardedNumberView.setVisibility(View.GONE); 363 } 364 365 if (info.isTimerVisible) { 366 bottomTextSwitcher.setDisplayedChild(1); 367 bottomTimerView.setBase( 368 primaryCallState.connectTimeMillis 369 - System.currentTimeMillis() 370 + SystemClock.elapsedRealtime()); 371 bottomTimerView.start(); 372 } else { 373 bottomTextSwitcher.setDisplayedChild(0); 374 bottomTimerView.stop(); 375 } 376 } 377 } 378