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.RippleDrawable;
26 import android.text.Spannable;
27 import android.text.TextUtils;
28 import android.text.style.TtsSpan;
29 import android.util.AttributeSet;
30 import android.util.TypedValue;
31 import android.view.MotionEvent;
32 import android.view.View;
33 import android.view.ViewGroup;
34 import android.view.ViewPropertyAnimator;
35 import android.view.ViewTreeObserver.OnPreDrawListener;
36 import android.view.accessibility.AccessibilityManager;
37 import android.widget.EditText;
38 import android.widget.ImageButton;
39 import android.widget.LinearLayout;
40 import android.widget.TextView;
41 import com.android.dialer.animation.AnimUtils;
42 import com.android.dialer.common.Assert;
43 import com.android.dialer.common.LogUtil;
44 import com.android.dialer.i18n.LocaleUtils;
45 import java.text.DecimalFormat;
46 import java.text.NumberFormat;
47 import java.util.Locale;
48 
49 /** View that displays a twelve-key phone dialpad. */
50 public class DialpadView extends LinearLayout {
51 
52   private static final String TAG = DialpadView.class.getSimpleName();
53 
54   // Parameters for animation
55   private static final double DELAY_MULTIPLIER = 0.66;
56   private static final double DURATION_MULTIPLIER = 0.8;
57   private static final int KEY_FRAME_DURATION = 33;
58 
59   // Resource IDs for buttons (0-9, *, and #)
60   private static final int[] BUTTON_IDS =
61       new int[] {
62         R.id.zero,
63         R.id.one,
64         R.id.two,
65         R.id.three,
66         R.id.four,
67         R.id.five,
68         R.id.six,
69         R.id.seven,
70         R.id.eight,
71         R.id.nine,
72         R.id.star,
73         R.id.pound
74       };
75 
76   private final AttributeSet attributeSet;
77   private final ColorStateList rippleColor;
78   private final OnPreDrawListenerForKeyLayoutAdjust onPreDrawListenerForKeyLayoutAdjust;
79   private final String[] primaryLettersMapping;
80   private final String[] secondaryLettersMapping;
81   private final boolean isRtl; // whether the dialpad is shown in a right-to-left locale
82   private final int translateDistance;
83 
84   private EditText digits;
85   private TextView digitsHint;
86   private ImageButton delete;
87   private View overflowMenuButton;
88   private ViewGroup rateContainer;
89   private TextView ildCountry;
90   private TextView ildRate;
91   private boolean isLandscapeMode;
92 
DialpadView(Context context)93   public DialpadView(Context context) {
94     this(context, null);
95   }
96 
DialpadView(Context context, AttributeSet attrs)97   public DialpadView(Context context, AttributeSet attrs) {
98     this(context, attrs, 0);
99   }
100 
DialpadView(Context context, AttributeSet attrs, int defStyle)101   public DialpadView(Context context, AttributeSet attrs, int defStyle) {
102     super(context, attrs, defStyle);
103     attributeSet = attrs;
104 
105     TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Dialpad);
106     rippleColor = a.getColorStateList(R.styleable.Dialpad_dialpad_key_button_touch_tint);
107     a.recycle();
108 
109     translateDistance =
110         getResources().getDimensionPixelSize(R.dimen.dialpad_key_button_translate_y);
111     isRtl =
112         TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_RTL;
113 
114     primaryLettersMapping = DialpadCharMappings.getDefaultKeyToCharsMap();
115     secondaryLettersMapping = DialpadCharMappings.getKeyToCharsMap(context);
116 
117     onPreDrawListenerForKeyLayoutAdjust = new OnPreDrawListenerForKeyLayoutAdjust();
118   }
119 
120   @Override
onDetachedFromWindow()121   protected void onDetachedFromWindow() {
122     super.onDetachedFromWindow();
123     getViewTreeObserver().removeOnPreDrawListener(onPreDrawListenerForKeyLayoutAdjust);
124   }
125 
126   @Override
onFinishInflate()127   protected void onFinishInflate() {
128     super.onFinishInflate();
129 
130     // The orientation obtained at this point should be used as the only truth for DialpadView as we
131     // observed inconsistency between configurations obtained here and in
132     // OnPreDrawListenerForKeyLayoutAdjust under rare circumstances.
133     isLandscapeMode =
134         (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE);
135 
136     setupKeypad();
137     digits = (EditText) findViewById(R.id.digits);
138     digitsHint = findViewById(R.id.digits_hint);
139     delete = (ImageButton) findViewById(R.id.deleteButton);
140     overflowMenuButton = findViewById(R.id.dialpad_overflow);
141     rateContainer = (ViewGroup) findViewById(R.id.rate_container);
142     ildCountry = (TextView) rateContainer.findViewById(R.id.ild_country);
143     ildRate = (TextView) rateContainer.findViewById(R.id.ild_rate);
144 
145     AccessibilityManager accessibilityManager =
146         (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
147     if (accessibilityManager.isEnabled()) {
148       // The text view must be selected to send accessibility events.
149       digits.setSelected(true);
150     }
151 
152     // As OnPreDrawListenerForKeyLayoutAdjust makes changes to LayoutParams, it is added here to
153     // ensure it can only be triggered after the layout is inflated.
154     getViewTreeObserver().removeOnPreDrawListener(onPreDrawListenerForKeyLayoutAdjust);
155     getViewTreeObserver().addOnPreDrawListener(onPreDrawListenerForKeyLayoutAdjust);
156   }
157 
setupKeypad()158   private void setupKeypad() {
159     final Resources resources = getContext().getResources();
160     final NumberFormat numberFormat = getNumberFormat();
161 
162     for (int i = 0; i < BUTTON_IDS.length; i++) {
163       DialpadKeyButton dialpadKey = (DialpadKeyButton) findViewById(BUTTON_IDS[i]);
164       TextView numberView = (TextView) dialpadKey.findViewById(R.id.dialpad_key_number);
165 
166       final String numberString;
167       final CharSequence numberContentDescription;
168       if (BUTTON_IDS[i] == R.id.pound) {
169         numberString = resources.getString(R.string.dialpad_pound_number);
170         numberContentDescription = numberString;
171       } else if (BUTTON_IDS[i] == R.id.star) {
172         numberString = resources.getString(R.string.dialpad_star_number);
173         numberContentDescription = numberString;
174       } else if (BUTTON_IDS[i] == R.id.zero) {
175         numberString = numberFormat.format(i);
176         numberContentDescription = numberString;
177       } else {
178         numberString = numberFormat.format(i);
179         // The content description is used for Talkback key presses. The number is
180         // separated by a "," to introduce a slight delay. Convert letters into a verbatim
181         // span so that they are read as letters instead of as one word.
182         String letters = primaryLettersMapping[i];
183         Spannable spannable =
184             Spannable.Factory.getInstance().newSpannable(numberString + "," + letters);
185         spannable.setSpan(
186             (new TtsSpan.VerbatimBuilder(letters)).build(),
187             numberString.length() + 1,
188             numberString.length() + 1 + letters.length(),
189             Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
190         numberContentDescription = spannable;
191       }
192 
193       final RippleDrawable rippleBackground =
194           (RippleDrawable) getContext().getDrawable(R.drawable.btn_dialpad_key);
195       if (rippleColor != null) {
196         rippleBackground.setColor(rippleColor);
197       }
198 
199       numberView.setText(numberString);
200       numberView.setElegantTextHeight(false);
201       dialpadKey.setContentDescription(numberContentDescription);
202       dialpadKey.setBackground(rippleBackground);
203 
204       TextView primaryLettersView = (TextView) dialpadKey.findViewById(R.id.dialpad_key_letters);
205       TextView secondaryLettersView =
206           (TextView) dialpadKey.findViewById(R.id.dialpad_key_secondary_letters);
207       if (primaryLettersView != null) {
208         primaryLettersView.setText(primaryLettersMapping[i]);
209       }
210       if (primaryLettersView != null && secondaryLettersView != null) {
211         if (secondaryLettersMapping == null) {
212           secondaryLettersView.setVisibility(View.GONE);
213         } else {
214           secondaryLettersView.setVisibility(View.VISIBLE);
215           secondaryLettersView.setText(secondaryLettersMapping[i]);
216 
217           // Adjust the font size of the letters if a secondary alphabet is available.
218           TypedArray a =
219               getContext()
220                   .getTheme()
221                   .obtainStyledAttributes(attributeSet, R.styleable.Dialpad, 0, 0);
222           int textSize =
223               a.getDimensionPixelSize(
224                   R.styleable.Dialpad_dialpad_key_letters_size_for_dual_alphabets, 0);
225           a.recycle();
226           primaryLettersView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
227           secondaryLettersView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
228         }
229       }
230     }
231 
232     final DialpadKeyButton one = (DialpadKeyButton) findViewById(R.id.one);
233     one.setLongHoverContentDescription(resources.getText(R.string.description_voicemail_button));
234 
235     final DialpadKeyButton zero = (DialpadKeyButton) findViewById(R.id.zero);
236     zero.setLongHoverContentDescription(resources.getText(R.string.description_image_button_plus));
237   }
238 
getNumberFormat()239   private NumberFormat getNumberFormat() {
240     Locale locale = LocaleUtils.getLocale(getContext());
241 
242     // Return the Persian number format if the current language is Persian.
243     return "fas".equals(locale.getISO3Language())
244         ? DecimalFormat.getInstance(locale)
245         : DecimalFormat.getInstance(Locale.ENGLISH);
246   }
247 
248   /**
249    * Configure whether or not the digits above the dialpad can be edited.
250    *
251    * <p>If we allow editing digits, the backspace button will be shown.
252    */
setCanDigitsBeEdited(boolean canBeEdited)253   public void setCanDigitsBeEdited(boolean canBeEdited) {
254     View deleteButton = findViewById(R.id.deleteButton);
255     deleteButton.setVisibility(canBeEdited ? View.VISIBLE : View.INVISIBLE);
256     View overflowMenuButton = findViewById(R.id.dialpad_overflow);
257     overflowMenuButton.setVisibility(canBeEdited ? View.VISIBLE : View.GONE);
258 
259     EditText digits = (EditText) findViewById(R.id.digits);
260     digits.setClickable(canBeEdited);
261     digits.setLongClickable(canBeEdited);
262     digits.setFocusableInTouchMode(canBeEdited);
263     digits.setCursorVisible(false);
264   }
265 
setCallRateInformation(String countryName, String displayRate)266   public void setCallRateInformation(String countryName, String displayRate) {
267     if (TextUtils.isEmpty(countryName) && TextUtils.isEmpty(displayRate)) {
268       rateContainer.setVisibility(View.GONE);
269       return;
270     }
271     rateContainer.setVisibility(View.VISIBLE);
272     ildCountry.setText(countryName);
273     ildRate.setText(displayRate);
274   }
275 
276   /**
277    * Always returns true for onHoverEvent callbacks, to fix problems with accessibility due to the
278    * dialpad overlaying other fragments.
279    */
280   @Override
onHoverEvent(MotionEvent event)281   public boolean onHoverEvent(MotionEvent event) {
282     return true;
283   }
284 
animateShow()285   public void animateShow() {
286     // This is a hack; without this, the setTranslationY is delayed in being applied, and the
287     // numbers appear at their original position (0) momentarily before animating.
288     final AnimatorListenerAdapter showListener = new AnimatorListenerAdapter() {};
289 
290     for (int i = 0; i < BUTTON_IDS.length; i++) {
291       int delay = (int) (getKeyButtonAnimationDelay(BUTTON_IDS[i]) * DELAY_MULTIPLIER);
292       int duration = (int) (getKeyButtonAnimationDuration(BUTTON_IDS[i]) * DURATION_MULTIPLIER);
293       final DialpadKeyButton dialpadKey = (DialpadKeyButton) findViewById(BUTTON_IDS[i]);
294 
295       ViewPropertyAnimator animator = dialpadKey.animate();
296       if (isLandscapeMode) {
297         // Landscape orientation requires translation along the X axis.
298         // For RTL locales, ensure we translate negative on the X axis.
299         dialpadKey.setTranslationX((isRtl ? -1 : 1) * translateDistance);
300         animator.translationX(0);
301       } else {
302         // Portrait orientation requires translation along the Y axis.
303         dialpadKey.setTranslationY(translateDistance);
304         animator.translationY(0);
305       }
306       animator
307           .setInterpolator(AnimUtils.EASE_OUT_EASE_IN)
308           .setStartDelay(delay)
309           .setDuration(duration)
310           .setListener(showListener)
311           .start();
312     }
313   }
314 
getDigits()315   public EditText getDigits() {
316     return digits;
317   }
318 
getDigitsHint()319   public TextView getDigitsHint() {
320     return digitsHint;
321   }
322 
getDeleteButton()323   public ImageButton getDeleteButton() {
324     return delete;
325   }
326 
getOverflowMenuButton()327   public View getOverflowMenuButton() {
328     return overflowMenuButton;
329   }
330 
331   /**
332    * Get the animation delay for the buttons, taking into account whether the dialpad is in
333    * landscape left-to-right, landscape right-to-left, or portrait.
334    *
335    * @param buttonId The button ID.
336    * @return The animation delay.
337    */
getKeyButtonAnimationDelay(int buttonId)338   private int getKeyButtonAnimationDelay(int buttonId) {
339     if (isLandscapeMode) {
340       if (isRtl) {
341         if (buttonId == R.id.three) {
342           return KEY_FRAME_DURATION * 1;
343         } else if (buttonId == R.id.six) {
344           return KEY_FRAME_DURATION * 2;
345         } else if (buttonId == R.id.nine) {
346           return KEY_FRAME_DURATION * 3;
347         } else if (buttonId == R.id.pound) {
348           return KEY_FRAME_DURATION * 4;
349         } else if (buttonId == R.id.two) {
350           return KEY_FRAME_DURATION * 5;
351         } else if (buttonId == R.id.five) {
352           return KEY_FRAME_DURATION * 6;
353         } else if (buttonId == R.id.eight) {
354           return KEY_FRAME_DURATION * 7;
355         } else if (buttonId == R.id.zero) {
356           return KEY_FRAME_DURATION * 8;
357         } else if (buttonId == R.id.one) {
358           return KEY_FRAME_DURATION * 9;
359         } else if (buttonId == R.id.four) {
360           return KEY_FRAME_DURATION * 10;
361         } else if (buttonId == R.id.seven || buttonId == R.id.star) {
362           return KEY_FRAME_DURATION * 11;
363         }
364       } else {
365         if (buttonId == R.id.one) {
366           return KEY_FRAME_DURATION * 1;
367         } else if (buttonId == R.id.four) {
368           return KEY_FRAME_DURATION * 2;
369         } else if (buttonId == R.id.seven) {
370           return KEY_FRAME_DURATION * 3;
371         } else if (buttonId == R.id.star) {
372           return KEY_FRAME_DURATION * 4;
373         } else if (buttonId == R.id.two) {
374           return KEY_FRAME_DURATION * 5;
375         } else if (buttonId == R.id.five) {
376           return KEY_FRAME_DURATION * 6;
377         } else if (buttonId == R.id.eight) {
378           return KEY_FRAME_DURATION * 7;
379         } else if (buttonId == R.id.zero) {
380           return KEY_FRAME_DURATION * 8;
381         } else if (buttonId == R.id.three) {
382           return KEY_FRAME_DURATION * 9;
383         } else if (buttonId == R.id.six) {
384           return KEY_FRAME_DURATION * 10;
385         } else if (buttonId == R.id.nine || buttonId == R.id.pound) {
386           return KEY_FRAME_DURATION * 11;
387         }
388       }
389     } else {
390       if (buttonId == R.id.one) {
391         return KEY_FRAME_DURATION * 1;
392       } else if (buttonId == R.id.two) {
393         return KEY_FRAME_DURATION * 2;
394       } else if (buttonId == R.id.three) {
395         return KEY_FRAME_DURATION * 3;
396       } else if (buttonId == R.id.four) {
397         return KEY_FRAME_DURATION * 4;
398       } else if (buttonId == R.id.five) {
399         return KEY_FRAME_DURATION * 5;
400       } else if (buttonId == R.id.six) {
401         return KEY_FRAME_DURATION * 6;
402       } else if (buttonId == R.id.seven) {
403         return KEY_FRAME_DURATION * 7;
404       } else if (buttonId == R.id.eight) {
405         return KEY_FRAME_DURATION * 8;
406       } else if (buttonId == R.id.nine) {
407         return KEY_FRAME_DURATION * 9;
408       } else if (buttonId == R.id.star) {
409         return KEY_FRAME_DURATION * 10;
410       } else if (buttonId == R.id.zero || buttonId == R.id.pound) {
411         return KEY_FRAME_DURATION * 11;
412       }
413     }
414 
415     LogUtil.e(TAG, "Attempted to get animation delay for invalid key button id.");
416     return 0;
417   }
418 
419   /**
420    * Get the button animation duration, taking into account whether the dialpad is in landscape
421    * left-to-right, landscape right-to-left, or portrait.
422    *
423    * @param buttonId The button ID.
424    * @return The animation duration.
425    */
getKeyButtonAnimationDuration(int buttonId)426   private int getKeyButtonAnimationDuration(int buttonId) {
427     if (isLandscapeMode) {
428       if (isRtl) {
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 * 8;
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 * 10;
444         }
445       } else {
446         if (buttonId == R.id.one
447             || buttonId == R.id.four
448             || buttonId == R.id.seven
449             || buttonId == R.id.star) {
450           return KEY_FRAME_DURATION * 10;
451         } else if (buttonId == R.id.two
452             || buttonId == R.id.five
453             || buttonId == R.id.eight
454             || buttonId == R.id.zero) {
455           return KEY_FRAME_DURATION * 9;
456         } else if (buttonId == R.id.three
457             || buttonId == R.id.six
458             || buttonId == R.id.nine
459             || buttonId == R.id.pound) {
460           return KEY_FRAME_DURATION * 8;
461         }
462       }
463     } else {
464       if (buttonId == R.id.one
465           || buttonId == R.id.two
466           || buttonId == R.id.three
467           || buttonId == R.id.four
468           || buttonId == R.id.five
469           || buttonId == R.id.six) {
470         return KEY_FRAME_DURATION * 10;
471       } else if (buttonId == R.id.seven || buttonId == R.id.eight || buttonId == R.id.nine) {
472         return KEY_FRAME_DURATION * 9;
473       } else if (buttonId == R.id.star || buttonId == R.id.zero || buttonId == R.id.pound) {
474         return KEY_FRAME_DURATION * 8;
475       }
476     }
477 
478     LogUtil.e(TAG, "Attempted to get animation duration for invalid key button id.");
479     return 0;
480   }
481 
482   /**
483    * An {@link OnPreDrawListener} that adjusts the height/width of each key layout so that they can
484    * be properly aligned.
485    *
486    * <p>When the device is in portrait mode, the layout height for key "1" can be different from
487    * those of other <b>digit</b> keys due to the voicemail icon. Adjustments are needed to ensure
488    * the layouts for all <b>digit</b> keys are of the same height. Key "*" and key "#" are excluded
489    * because their styles are different from other keys'.
490    *
491    * <p>When the device is in landscape mode, keys can have different layout widths due to the
492    * icon/characters associated with them. Adjustments are needed to ensure the layouts for all keys
493    * are of the same width.
494    *
495    * <p>Note that adjustments can only be made after the layouts are measured, which is why the
496    * logic lives in an {@link OnPreDrawListener} that is invoked when the view tree is about to be
497    * drawn.
498    */
499   private class OnPreDrawListenerForKeyLayoutAdjust implements OnPreDrawListener {
500 
501     /**
502      * This method is invoked when the view tree is about to be drawn. At this point, all views in
503      * the tree have been measured and given a frame.
504      *
505      * <p>If the keys have been adjusted, we instruct the current drawing pass to proceed by
506      * returning true. Otherwise, adjustments will be made and the current drawing pass will be
507      * cancelled by returning false.
508      *
509      * <p>It is imperative to schedule another layout pass of the view tree after adjustments are
510      * made so that {@link #onPreDraw()} can be invoked again to check the layouts and proceed with
511      * the drawing pass.
512      */
513     @Override
onPreDraw()514     public boolean onPreDraw() {
515       if (!shouldAdjustKeySizes()) {
516         // Return true to proceed with the current drawing pass.
517         // Note that we must NOT remove this listener here. The fact that we don't need to adjust
518         // keys at the moment doesn't mean they won't need adjustments in the future. For example,
519         // when DialpadFragment is hidden, all keys are of the same size (0) and nothing needs to be
520         // done. Removing the listener will cost us the ability to adjust them when they reappear.
521         // It is only safe to remove the listener after adjusting keys for the first time. See the
522         // comment below for more details.
523         return true;
524       }
525 
526       adjustKeySizes();
527 
528       // After all keys are adjusted for the first time, no more adjustments will be needed during
529       // the rest of DialpadView's lifecycle. It is therefore safe to remove this listener.
530       // Another important reason for removing the listener is that it can be triggered AFTER a
531       // device orientation change but BEFORE DialpadView's onDetachedFromWindow() and
532       // onFinishInflate() are called, i.e., the listener will attempt to adjust the layout before
533       // it is inflated, which results in a crash.
534       getViewTreeObserver().removeOnPreDrawListener(this);
535 
536       return false; // Return false to cancel the current drawing pass.
537     }
538 
shouldAdjustKeySizes()539     private boolean shouldAdjustKeySizes() {
540       return isLandscapeMode ? shouldAdjustKeyWidths() : shouldAdjustDigitKeyHeights();
541     }
542 
543     /**
544      * Return true if not all key layouts have the same width. This method must be called when the
545      * device is in landscape mode.
546      */
shouldAdjustKeyWidths()547     private boolean shouldAdjustKeyWidths() {
548       Assert.checkState(isLandscapeMode);
549 
550       DialpadKeyButton dialpadKeyButton = (DialpadKeyButton) findViewById(BUTTON_IDS[0]);
551       LinearLayout keyLayout =
552           (LinearLayout) dialpadKeyButton.findViewById(R.id.dialpad_key_layout);
553       final int width = keyLayout.getWidth();
554 
555       for (int i = 1; i < BUTTON_IDS.length; i++) {
556         dialpadKeyButton = (DialpadKeyButton) findViewById(BUTTON_IDS[i]);
557         keyLayout = (LinearLayout) dialpadKeyButton.findViewById(R.id.dialpad_key_layout);
558         if (width != keyLayout.getWidth()) {
559           return true;
560         }
561       }
562 
563       return false;
564     }
565 
566     /**
567      * Return true if not all <b>digit</b> key layouts have the same height. This method must be
568      * called when the device is in portrait mode.
569      */
shouldAdjustDigitKeyHeights()570     private boolean shouldAdjustDigitKeyHeights() {
571       Assert.checkState(!isLandscapeMode);
572 
573       DialpadKeyButton dialpadKey = (DialpadKeyButton) findViewById(BUTTON_IDS[0]);
574       LinearLayout keyLayout = (LinearLayout) dialpadKey.findViewById(R.id.dialpad_key_layout);
575       final int height = keyLayout.getHeight();
576 
577       // BUTTON_IDS[i] is the resource ID for button i when 0 <= i && i <= 9.
578       // For example, BUTTON_IDS[3] is the resource ID for button "3" on the dialpad.
579       for (int i = 1; i <= 9; i++) {
580         dialpadKey = (DialpadKeyButton) findViewById(BUTTON_IDS[i]);
581         keyLayout = (LinearLayout) dialpadKey.findViewById(R.id.dialpad_key_layout);
582         if (height != keyLayout.getHeight()) {
583           return true;
584         }
585       }
586 
587       return false;
588     }
589 
adjustKeySizes()590     private void adjustKeySizes() {
591       if (isLandscapeMode) {
592         adjustKeyWidths();
593       } else {
594         adjustDigitKeyHeights();
595       }
596     }
597 
598     /**
599      * Make the heights of all <b>digit</b> keys the same.
600      *
601      * <p>When the device is in portrait mode, we first find the maximum height among digit key
602      * layouts. Then for each key, we adjust the height of the layout containing letters/the
603      * voicemail icon to ensure the height of each digit key is the same.
604      *
605      * <p>A layout pass will be scheduled in this method by {@link
606      * LinearLayout#setLayoutParams(ViewGroup.LayoutParams)}.
607      */
adjustDigitKeyHeights()608     private void adjustDigitKeyHeights() {
609       Assert.checkState(!isLandscapeMode);
610 
611       int maxHeight = 0;
612 
613       // BUTTON_IDS[i] is the resource ID for button i when 0 <= i && i <= 9.
614       // For example, BUTTON_IDS[3] is the resource ID for button "3" on the dialpad.
615       for (int i = 0; i <= 9; i++) {
616         DialpadKeyButton dialpadKey = (DialpadKeyButton) findViewById(BUTTON_IDS[i]);
617         LinearLayout keyLayout = (LinearLayout) dialpadKey.findViewById(R.id.dialpad_key_layout);
618         maxHeight = Math.max(maxHeight, keyLayout.getHeight());
619       }
620 
621       for (int i = 0; i <= 9; i++) {
622         DialpadKeyButton dialpadKey = (DialpadKeyButton) findViewById(BUTTON_IDS[i]);
623         LinearLayout keyLayout = (LinearLayout) dialpadKey.findViewById(R.id.dialpad_key_layout);
624 
625         DialpadTextView numberView =
626             (DialpadTextView) keyLayout.findViewById(R.id.dialpad_key_number);
627         MarginLayoutParams numberViewLayoutParams =
628             (MarginLayoutParams) numberView.getLayoutParams();
629 
630         LinearLayout iconOrLettersLayout =
631             (LinearLayout) keyLayout.findViewById(R.id.dialpad_key_icon_or_letters_layout);
632         iconOrLettersLayout.setLayoutParams(
633             new LayoutParams(
634                 LayoutParams.WRAP_CONTENT /* width */,
635                 maxHeight
636                     - numberView.getHeight()
637                     - numberViewLayoutParams.topMargin
638                     - numberViewLayoutParams.bottomMargin /* height */));
639       }
640     }
641 
642     /**
643      * Make the widths of all keys the same.
644      *
645      * <p>When the device is in landscape mode, we first find the maximum width among key layouts.
646      * Then we adjust the width of each layout's horizontal placeholder so that each key has the
647      * same width.
648      *
649      * <p>A layout pass will be scheduled in this method by {@link
650      * View#setLayoutParams(ViewGroup.LayoutParams)}.
651      */
adjustKeyWidths()652     private void adjustKeyWidths() {
653       Assert.checkState(isLandscapeMode);
654 
655       int maxWidth = 0;
656       for (int buttonId : BUTTON_IDS) {
657         DialpadKeyButton dialpadKey = (DialpadKeyButton) findViewById(buttonId);
658         LinearLayout keyLayout = (LinearLayout) dialpadKey.findViewById(R.id.dialpad_key_layout);
659         maxWidth = Math.max(maxWidth, keyLayout.getWidth());
660       }
661 
662       for (int buttonId : BUTTON_IDS) {
663         DialpadKeyButton dialpadKey = (DialpadKeyButton) findViewById(buttonId);
664         LinearLayout keyLayout = (LinearLayout) dialpadKey.findViewById(R.id.dialpad_key_layout);
665         View horizontalPlaceholder =
666             keyLayout.findViewById(R.id.dialpad_key_horizontal_placeholder);
667         horizontalPlaceholder.setLayoutParams(
668             new LayoutParams(
669                 maxWidth - keyLayout.getWidth() /* width */,
670                 LayoutParams.MATCH_PARENT /* height */));
671       }
672     }
673   }
674 }
675