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