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