Home | History | Annotate | Download | only in dialpadview
      1 /*
      2  * Copyright (C) 2014 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.dialer.dialpadview;
     18 
     19 import android.animation.AnimatorListenerAdapter;
     20 import android.content.Context;
     21 import android.content.res.ColorStateList;
     22 import android.content.res.Configuration;
     23 import android.content.res.Resources;
     24 import android.content.res.TypedArray;
     25 import android.graphics.drawable.Drawable;
     26 import android.graphics.drawable.RippleDrawable;
     27 import android.os.Build;
     28 import android.text.Spannable;
     29 import android.text.TextUtils;
     30 import android.text.style.TtsSpan;
     31 import android.util.AttributeSet;
     32 import android.util.Log;
     33 import android.view.MotionEvent;
     34 import android.view.View;
     35 import android.view.ViewGroup;
     36 import android.view.ViewPropertyAnimator;
     37 import android.view.accessibility.AccessibilityManager;
     38 import android.widget.EditText;
     39 import android.widget.ImageButton;
     40 import android.widget.LinearLayout;
     41 import android.widget.TextView;
     42 import com.android.dialer.animation.AnimUtils;
     43 import java.text.DecimalFormat;
     44 import java.text.NumberFormat;
     45 import java.util.Locale;
     46 
     47 /** View that displays a twelve-key phone dialpad. */
     48 public class DialpadView extends LinearLayout {
     49 
     50   private static final String TAG = DialpadView.class.getSimpleName();
     51 
     52   private static final double DELAY_MULTIPLIER = 0.66;
     53   private static final double DURATION_MULTIPLIER = 0.8;
     54   // For animation.
     55   private static final int KEY_FRAME_DURATION = 33;
     56   /** {@code True} if the dialpad is in landscape orientation. */
     57   private final boolean mIsLandscape;
     58   /** {@code True} if the dialpad is showing in a right-to-left locale. */
     59   private final boolean mIsRtl;
     60 
     61   private final int[] mButtonIds =
     62       new int[] {
     63         R.id.zero,
     64         R.id.one,
     65         R.id.two,
     66         R.id.three,
     67         R.id.four,
     68         R.id.five,
     69         R.id.six,
     70         R.id.seven,
     71         R.id.eight,
     72         R.id.nine,
     73         R.id.star,
     74         R.id.pound
     75       };
     76   private EditText mDigits;
     77   private ImageButton mDelete;
     78   private View mOverflowMenuButton;
     79   private ColorStateList mRippleColor;
     80   private ViewGroup mRateContainer;
     81   private TextView mIldCountry;
     82   private TextView mIldRate;
     83   private boolean mCanDigitsBeEdited;
     84   private int mTranslateDistance;
     85 
     86   public DialpadView(Context context) {
     87     this(context, null);
     88   }
     89 
     90   public DialpadView(Context context, AttributeSet attrs) {
     91     this(context, attrs, 0);
     92   }
     93 
     94   public DialpadView(Context context, AttributeSet attrs, int defStyle) {
     95     super(context, attrs, defStyle);
     96 
     97     TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Dialpad);
     98     mRippleColor = a.getColorStateList(R.styleable.Dialpad_dialpad_key_button_touch_tint);
     99     a.recycle();
    100 
    101     mTranslateDistance =
    102         getResources().getDimensionPixelSize(R.dimen.dialpad_key_button_translate_y);
    103 
    104     mIsLandscape =
    105         getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
    106     mIsRtl =
    107         TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL;
    108   }
    109 
    110   @Override
    111   protected void onFinishInflate() {
    112     setupKeypad();
    113     mDigits = (EditText) findViewById(R.id.digits);
    114     mDelete = (ImageButton) findViewById(R.id.deleteButton);
    115     mOverflowMenuButton = findViewById(R.id.dialpad_overflow);
    116     mRateContainer = (ViewGroup) findViewById(R.id.rate_container);
    117     mIldCountry = (TextView) mRateContainer.findViewById(R.id.ild_country);
    118     mIldRate = (TextView) mRateContainer.findViewById(R.id.ild_rate);
    119 
    120     AccessibilityManager accessibilityManager =
    121         (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
    122     if (accessibilityManager.isEnabled()) {
    123       // The text view must be selected to send accessibility events.
    124       mDigits.setSelected(true);
    125     }
    126   }
    127 
    128   private void setupKeypad() {
    129     final int[] letterIds =
    130         new int[] {
    131           R.string.dialpad_0_letters,
    132           R.string.dialpad_1_letters,
    133           R.string.dialpad_2_letters,
    134           R.string.dialpad_3_letters,
    135           R.string.dialpad_4_letters,
    136           R.string.dialpad_5_letters,
    137           R.string.dialpad_6_letters,
    138           R.string.dialpad_7_letters,
    139           R.string.dialpad_8_letters,
    140           R.string.dialpad_9_letters,
    141           R.string.dialpad_star_letters,
    142           R.string.dialpad_pound_letters
    143         };
    144 
    145     final Resources resources = getContext().getResources();
    146 
    147     DialpadKeyButton dialpadKey;
    148     TextView numberView;
    149     TextView lettersView;
    150 
    151     final Locale currentLocale = resources.getConfiguration().locale;
    152     final NumberFormat nf;
    153     // We translate dialpad numbers only for "fa" and not any other locale
    154     // ("ar" anybody ?).
    155     if ("fa".equals(currentLocale.getLanguage())) {
    156       nf = DecimalFormat.getInstance(resources.getConfiguration().locale);
    157     } else {
    158       nf = DecimalFormat.getInstance(Locale.ENGLISH);
    159     }
    160 
    161     for (int i = 0; i < mButtonIds.length; i++) {
    162       dialpadKey = (DialpadKeyButton) findViewById(mButtonIds[i]);
    163       numberView = (TextView) dialpadKey.findViewById(R.id.dialpad_key_number);
    164       lettersView = (TextView) dialpadKey.findViewById(R.id.dialpad_key_letters);
    165 
    166       final String numberString;
    167       final CharSequence numberContentDescription;
    168       if (mButtonIds[i] == R.id.pound) {
    169         numberString = resources.getString(R.string.dialpad_pound_number);
    170         numberContentDescription = numberString;
    171       } else if (mButtonIds[i] == R.id.star) {
    172         numberString = resources.getString(R.string.dialpad_star_number);
    173         numberContentDescription = numberString;
    174       } else {
    175         numberString = nf.format(i);
    176         // The content description is used for Talkback key presses. The number is
    177         // separated by a "," to introduce a slight delay. Convert letters into a verbatim
    178         // span so that they are read as letters instead of as one word.
    179         String letters = resources.getString(letterIds[i]);
    180         Spannable spannable =
    181             Spannable.Factory.getInstance().newSpannable(numberString + "," + letters);
    182         spannable.setSpan(
    183             (new TtsSpan.VerbatimBuilder(letters)).build(),
    184             numberString.length() + 1,
    185             numberString.length() + 1 + letters.length(),
    186             Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
    187         numberContentDescription = spannable;
    188       }
    189 
    190       final RippleDrawable rippleBackground =
    191           (RippleDrawable) getDrawableCompat(getContext(), R.drawable.btn_dialpad_key);
    192       if (mRippleColor != null) {
    193         rippleBackground.setColor(mRippleColor);
    194       }
    195 
    196       numberView.setText(numberString);
    197       numberView.setElegantTextHeight(false);
    198       dialpadKey.setContentDescription(numberContentDescription);
    199       dialpadKey.setBackground(rippleBackground);
    200 
    201       if (lettersView != null) {
    202         lettersView.setText(resources.getString(letterIds[i]));
    203       }
    204     }
    205 
    206     final DialpadKeyButton one = (DialpadKeyButton) findViewById(R.id.one);
    207     one.setLongHoverContentDescription(resources.getText(R.string.description_voicemail_button));
    208 
    209     final DialpadKeyButton zero = (DialpadKeyButton) findViewById(R.id.zero);
    210     zero.setLongHoverContentDescription(resources.getText(R.string.description_image_button_plus));
    211   }
    212 
    213   private Drawable getDrawableCompat(Context context, int id) {
    214     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    215       return context.getDrawable(id);
    216     } else {
    217       return context.getResources().getDrawable(id);
    218     }
    219   }
    220 
    221   public void setShowVoicemailButton(boolean show) {
    222     View view = findViewById(R.id.dialpad_key_voicemail);
    223     if (view != null) {
    224       view.setVisibility(show ? View.VISIBLE : View.INVISIBLE);
    225     }
    226   }
    227 
    228   /**
    229    * Whether or not the digits above the dialer can be edited.
    230    *
    231    * @param canBeEdited If true, the backspace button will be shown and the digits EditText will be
    232    *     configured to allow text manipulation.
    233    */
    234   public void setCanDigitsBeEdited(boolean canBeEdited) {
    235     View deleteButton = findViewById(R.id.deleteButton);
    236     deleteButton.setVisibility(canBeEdited ? View.VISIBLE : View.INVISIBLE);
    237     View overflowMenuButton = findViewById(R.id.dialpad_overflow);
    238     overflowMenuButton.setVisibility(canBeEdited ? View.VISIBLE : View.GONE);
    239 
    240     EditText digits = (EditText) findViewById(R.id.digits);
    241     digits.setClickable(canBeEdited);
    242     digits.setLongClickable(canBeEdited);
    243     digits.setFocusableInTouchMode(canBeEdited);
    244     digits.setCursorVisible(false);
    245 
    246     mCanDigitsBeEdited = canBeEdited;
    247   }
    248 
    249   public void setCallRateInformation(String countryName, String displayRate) {
    250     if (TextUtils.isEmpty(countryName) && TextUtils.isEmpty(displayRate)) {
    251       mRateContainer.setVisibility(View.GONE);
    252       return;
    253     }
    254     mRateContainer.setVisibility(View.VISIBLE);
    255     mIldCountry.setText(countryName);
    256     mIldRate.setText(displayRate);
    257   }
    258 
    259   public boolean canDigitsBeEdited() {
    260     return mCanDigitsBeEdited;
    261   }
    262 
    263   /**
    264    * Always returns true for onHoverEvent callbacks, to fix problems with accessibility due to the
    265    * dialpad overlaying other fragments.
    266    */
    267   @Override
    268   public boolean onHoverEvent(MotionEvent event) {
    269     return true;
    270   }
    271 
    272   public void animateShow() {
    273     // This is a hack; without this, the setTranslationY is delayed in being applied, and the
    274     // numbers appear at their original position (0) momentarily before animating.
    275     final AnimatorListenerAdapter showListener = new AnimatorListenerAdapter() {};
    276 
    277     for (int i = 0; i < mButtonIds.length; i++) {
    278       int delay = (int) (getKeyButtonAnimationDelay(mButtonIds[i]) * DELAY_MULTIPLIER);
    279       int duration = (int) (getKeyButtonAnimationDuration(mButtonIds[i]) * DURATION_MULTIPLIER);
    280       final DialpadKeyButton dialpadKey = (DialpadKeyButton) findViewById(mButtonIds[i]);
    281 
    282       ViewPropertyAnimator animator = dialpadKey.animate();
    283       if (mIsLandscape) {
    284         // Landscape orientation requires translation along the X axis.
    285         // For RTL locales, ensure we translate negative on the X axis.
    286         dialpadKey.setTranslationX((mIsRtl ? -1 : 1) * mTranslateDistance);
    287         animator.translationX(0);
    288       } else {
    289         // Portrait orientation requires translation along the Y axis.
    290         dialpadKey.setTranslationY(mTranslateDistance);
    291         animator.translationY(0);
    292       }
    293       animator
    294           .setInterpolator(AnimUtils.EASE_OUT_EASE_IN)
    295           .setStartDelay(delay)
    296           .setDuration(duration)
    297           .setListener(showListener)
    298           .start();
    299     }
    300   }
    301 
    302   public EditText getDigits() {
    303     return mDigits;
    304   }
    305 
    306   public ImageButton getDeleteButton() {
    307     return mDelete;
    308   }
    309 
    310   public View getOverflowMenuButton() {
    311     return mOverflowMenuButton;
    312   }
    313 
    314   /**
    315    * Get the animation delay for the buttons, taking into account whether the dialpad is in
    316    * landscape left-to-right, landscape right-to-left, or portrait.
    317    *
    318    * @param buttonId The button ID.
    319    * @return The animation delay.
    320    */
    321   private int getKeyButtonAnimationDelay(int buttonId) {
    322     if (mIsLandscape) {
    323       if (mIsRtl) {
    324         if (buttonId == R.id.three) {
    325           return KEY_FRAME_DURATION * 1;
    326         } else if (buttonId == R.id.six) {
    327           return KEY_FRAME_DURATION * 2;
    328         } else if (buttonId == R.id.nine) {
    329           return KEY_FRAME_DURATION * 3;
    330         } else if (buttonId == R.id.pound) {
    331           return KEY_FRAME_DURATION * 4;
    332         } else if (buttonId == R.id.two) {
    333           return KEY_FRAME_DURATION * 5;
    334         } else if (buttonId == R.id.five) {
    335           return KEY_FRAME_DURATION * 6;
    336         } else if (buttonId == R.id.eight) {
    337           return KEY_FRAME_DURATION * 7;
    338         } else if (buttonId == R.id.zero) {
    339           return KEY_FRAME_DURATION * 8;
    340         } else if (buttonId == R.id.one) {
    341           return KEY_FRAME_DURATION * 9;
    342         } else if (buttonId == R.id.four) {
    343           return KEY_FRAME_DURATION * 10;
    344         } else if (buttonId == R.id.seven || buttonId == R.id.star) {
    345           return KEY_FRAME_DURATION * 11;
    346         }
    347       } else {
    348         if (buttonId == R.id.one) {
    349           return KEY_FRAME_DURATION * 1;
    350         } else if (buttonId == R.id.four) {
    351           return KEY_FRAME_DURATION * 2;
    352         } else if (buttonId == R.id.seven) {
    353           return KEY_FRAME_DURATION * 3;
    354         } else if (buttonId == R.id.star) {
    355           return KEY_FRAME_DURATION * 4;
    356         } else if (buttonId == R.id.two) {
    357           return KEY_FRAME_DURATION * 5;
    358         } else if (buttonId == R.id.five) {
    359           return KEY_FRAME_DURATION * 6;
    360         } else if (buttonId == R.id.eight) {
    361           return KEY_FRAME_DURATION * 7;
    362         } else if (buttonId == R.id.zero) {
    363           return KEY_FRAME_DURATION * 8;
    364         } else if (buttonId == R.id.three) {
    365           return KEY_FRAME_DURATION * 9;
    366         } else if (buttonId == R.id.six) {
    367           return KEY_FRAME_DURATION * 10;
    368         } else if (buttonId == R.id.nine || buttonId == R.id.pound) {
    369           return KEY_FRAME_DURATION * 11;
    370         }
    371       }
    372     } else {
    373       if (buttonId == R.id.one) {
    374         return KEY_FRAME_DURATION * 1;
    375       } else if (buttonId == R.id.two) {
    376         return KEY_FRAME_DURATION * 2;
    377       } else if (buttonId == R.id.three) {
    378         return KEY_FRAME_DURATION * 3;
    379       } else if (buttonId == R.id.four) {
    380         return KEY_FRAME_DURATION * 4;
    381       } else if (buttonId == R.id.five) {
    382         return KEY_FRAME_DURATION * 5;
    383       } else if (buttonId == R.id.six) {
    384         return KEY_FRAME_DURATION * 6;
    385       } else if (buttonId == R.id.seven) {
    386         return KEY_FRAME_DURATION * 7;
    387       } else if (buttonId == R.id.eight) {
    388         return KEY_FRAME_DURATION * 8;
    389       } else if (buttonId == R.id.nine) {
    390         return KEY_FRAME_DURATION * 9;
    391       } else if (buttonId == R.id.star) {
    392         return KEY_FRAME_DURATION * 10;
    393       } else if (buttonId == R.id.zero || buttonId == R.id.pound) {
    394         return KEY_FRAME_DURATION * 11;
    395       }
    396     }
    397 
    398     Log.wtf(TAG, "Attempted to get animation delay for invalid key button id.");
    399     return 0;
    400   }
    401 
    402   /**
    403    * Get the button animation duration, taking into account whether the dialpad is in landscape
    404    * left-to-right, landscape right-to-left, or portrait.
    405    *
    406    * @param buttonId The button ID.
    407    * @return The animation duration.
    408    */
    409   private int getKeyButtonAnimationDuration(int buttonId) {
    410     if (mIsLandscape) {
    411       if (mIsRtl) {
    412         if (buttonId == R.id.one
    413             || buttonId == R.id.four
    414             || buttonId == R.id.seven
    415             || buttonId == R.id.star) {
    416           return KEY_FRAME_DURATION * 8;
    417         } else if (buttonId == R.id.two
    418             || buttonId == R.id.five
    419             || buttonId == R.id.eight
    420             || buttonId == R.id.zero) {
    421           return KEY_FRAME_DURATION * 9;
    422         } else if (buttonId == R.id.three
    423             || buttonId == R.id.six
    424             || buttonId == R.id.nine
    425             || buttonId == R.id.pound) {
    426           return KEY_FRAME_DURATION * 10;
    427         }
    428       } else {
    429         if (buttonId == R.id.one
    430             || buttonId == R.id.four
    431             || buttonId == R.id.seven
    432             || buttonId == R.id.star) {
    433           return KEY_FRAME_DURATION * 10;
    434         } else if (buttonId == R.id.two
    435             || buttonId == R.id.five
    436             || buttonId == R.id.eight
    437             || buttonId == R.id.zero) {
    438           return KEY_FRAME_DURATION * 9;
    439         } else if (buttonId == R.id.three
    440             || buttonId == R.id.six
    441             || buttonId == R.id.nine
    442             || buttonId == R.id.pound) {
    443           return KEY_FRAME_DURATION * 8;
    444         }
    445       }
    446     } else {
    447       if (buttonId == R.id.one
    448           || buttonId == R.id.two
    449           || buttonId == R.id.three
    450           || buttonId == R.id.four
    451           || buttonId == R.id.five
    452           || buttonId == R.id.six) {
    453         return KEY_FRAME_DURATION * 10;
    454       } else if (buttonId == R.id.seven || buttonId == R.id.eight || buttonId == R.id.nine) {
    455         return KEY_FRAME_DURATION * 9;
    456       } else if (buttonId == R.id.star || buttonId == R.id.zero || buttonId == R.id.pound) {
    457         return KEY_FRAME_DURATION * 8;
    458       }
    459     }
    460 
    461     Log.wtf(TAG, "Attempted to get animation duration for invalid key button id.");
    462     return 0;
    463   }
    464 }
    465