• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
ContactGridManager( View view, @Nullable ImageView avatarImageView, int avatarSize, boolean showAnonymousAvatar)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 
show()122   public void show() {
123     contactGridLayout.setVisibility(View.VISIBLE);
124   }
125 
hide()126   public void hide() {
127     contactGridLayout.setVisibility(View.GONE);
128   }
129 
setAvatarHidden(boolean hide)130   public void setAvatarHidden(boolean hide) {
131     if (hide != hideAvatar) {
132       hideAvatar = hide;
133       updatePrimaryNameAndPhoto();
134     }
135   }
136 
isAvatarHidden()137   public boolean isAvatarHidden() {
138     return hideAvatar;
139   }
140 
getContainerView()141   public View getContainerView() {
142     return contactGridLayout;
143   }
144 
setIsMiddleRowVisible(boolean isMiddleRowVisible)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 
setPrimary(PrimaryInfo primaryInfo)155   public void setPrimary(PrimaryInfo primaryInfo) {
156     this.primaryInfo = primaryInfo;
157     updatePrimaryNameAndPhoto();
158     updateBottomRow();
159     updateDeviceNumberRow();
160   }
161 
setCallState(PrimaryCallState primaryCallState)162   public void setCallState(PrimaryCallState primaryCallState) {
163     this.primaryCallState = primaryCallState;
164     updatePrimaryNameAndPhoto();
165     updateBottomRow();
166     updateTopRow();
167     updateDeviceNumberRow();
168   }
169 
dispatchPopulateAccessibilityEvent(AccessibilityEvent event)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 
setAvatarImageView( @ullable ImageView avatarImageView, int avatarSize, boolean showAnonymousAvatar)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 
onMultiWindowModeChanged(boolean isInMultiWindowMode)187   public void onMultiWindowModeChanged(boolean isInMultiWindowMode) {
188     if (this.isInMultiWindowMode == isInMultiWindowMode) {
189       return;
190     }
191     this.isInMultiWindowMode = isInMultiWindowMode;
192     updateDeviceNumberRow();
193   }
194 
dispatchPopulateAccessibilityEvent(AccessibilityEvent event, View view)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 
updateAvatarVisibility()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    */
updateTopRow()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    */
updatePrimaryNameAndPhoto()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    */
updateBottomRow()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 
updateDeviceNumberRow()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