Home | History | Annotate | Download | only in contactgrid
      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