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