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