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