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