1 /* 2 * Copyright (C) 2013 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.incallui; 18 19 import android.content.Context; 20 import android.os.Bundle; 21 import android.os.Handler; 22 import android.os.Looper; 23 import android.telephony.PhoneNumberUtils; 24 import android.text.Editable; 25 import android.text.method.DialerKeyListener; 26 import android.util.AttributeSet; 27 import android.view.KeyEvent; 28 import android.view.LayoutInflater; 29 import android.view.MotionEvent; 30 import android.view.View; 31 import android.view.ViewGroup; 32 import android.view.ViewTreeObserver; 33 import android.view.accessibility.AccessibilityManager; 34 import android.widget.EditText; 35 import android.widget.LinearLayout; 36 import android.widget.TextView; 37 38 import com.android.contacts.common.util.MaterialColorMapUtils.MaterialPalette; 39 import com.android.phone.common.dialpad.DialpadKeyButton; 40 import com.android.phone.common.dialpad.DialpadView; 41 42 import java.util.HashMap; 43 44 /** 45 * Fragment for call control buttons 46 */ 47 public class DialpadFragment extends BaseFragment<DialpadPresenter, DialpadPresenter.DialpadUi> 48 implements DialpadPresenter.DialpadUi, View.OnTouchListener, View.OnKeyListener, 49 View.OnHoverListener, View.OnClickListener { 50 51 private static final int ACCESSIBILITY_DTMF_STOP_DELAY_MILLIS = 50; 52 53 private final int[] mButtonIds = new int[] {R.id.zero, R.id.one, R.id.two, R.id.three, 54 R.id.four, R.id.five, R.id.six, R.id.seven, R.id.eight, R.id.nine, R.id.star, 55 R.id.pound}; 56 57 /** 58 * LinearLayout with getter and setter methods for the translationY property using floats, 59 * for animation purposes. 60 */ 61 public static class DialpadSlidingLinearLayout extends LinearLayout { 62 DialpadSlidingLinearLayout(Context context)63 public DialpadSlidingLinearLayout(Context context) { 64 super(context); 65 } 66 DialpadSlidingLinearLayout(Context context, AttributeSet attrs)67 public DialpadSlidingLinearLayout(Context context, AttributeSet attrs) { 68 super(context, attrs); 69 } 70 DialpadSlidingLinearLayout(Context context, AttributeSet attrs, int defStyle)71 public DialpadSlidingLinearLayout(Context context, AttributeSet attrs, int defStyle) { 72 super(context, attrs, defStyle); 73 } 74 getYFraction()75 public float getYFraction() { 76 final int height = getHeight(); 77 if (height == 0) return 0; 78 return getTranslationY() / height; 79 } 80 setYFraction(float yFraction)81 public void setYFraction(float yFraction) { 82 setTranslationY(yFraction * getHeight()); 83 } 84 } 85 86 /** 87 * LinearLayout that always returns true for onHoverEvent callbacks, to fix 88 * problems with accessibility due to the dialpad overlaying other fragments. 89 */ 90 public static class HoverIgnoringLinearLayout extends LinearLayout { 91 HoverIgnoringLinearLayout(Context context)92 public HoverIgnoringLinearLayout(Context context) { 93 super(context); 94 } 95 HoverIgnoringLinearLayout(Context context, AttributeSet attrs)96 public HoverIgnoringLinearLayout(Context context, AttributeSet attrs) { 97 super(context, attrs); 98 } 99 HoverIgnoringLinearLayout(Context context, AttributeSet attrs, int defStyle)100 public HoverIgnoringLinearLayout(Context context, AttributeSet attrs, int defStyle) { 101 super(context, attrs, defStyle); 102 } 103 104 @Override onHoverEvent(MotionEvent event)105 public boolean onHoverEvent(MotionEvent event) { 106 return true; 107 } 108 } 109 110 private EditText mDtmfDialerField; 111 112 /** Hash Map to map a view id to a character*/ 113 private static final HashMap<Integer, Character> mDisplayMap = 114 new HashMap<Integer, Character>(); 115 116 private static final Handler sHandler = new Handler(Looper.getMainLooper()); 117 118 119 /** Set up the static maps*/ 120 static { 121 // Map the buttons to the display characters mDisplayMap.put(R.id.one, '1')122 mDisplayMap.put(R.id.one, '1'); mDisplayMap.put(R.id.two, '2')123 mDisplayMap.put(R.id.two, '2'); mDisplayMap.put(R.id.three, '3')124 mDisplayMap.put(R.id.three, '3'); mDisplayMap.put(R.id.four, '4')125 mDisplayMap.put(R.id.four, '4'); mDisplayMap.put(R.id.five, '5')126 mDisplayMap.put(R.id.five, '5'); mDisplayMap.put(R.id.six, '6')127 mDisplayMap.put(R.id.six, '6'); mDisplayMap.put(R.id.seven, '7')128 mDisplayMap.put(R.id.seven, '7'); mDisplayMap.put(R.id.eight, '8')129 mDisplayMap.put(R.id.eight, '8'); mDisplayMap.put(R.id.nine, '9')130 mDisplayMap.put(R.id.nine, '9'); mDisplayMap.put(R.id.zero, '0')131 mDisplayMap.put(R.id.zero, '0'); mDisplayMap.put(R.id.pound, '#')132 mDisplayMap.put(R.id.pound, '#'); mDisplayMap.put(R.id.star, '*')133 mDisplayMap.put(R.id.star, '*'); 134 } 135 136 // KeyListener used with the "dialpad digits" EditText widget. 137 private DTMFKeyListener mDialerKeyListener; 138 139 private DialpadView mDialpadView; 140 141 private int mCurrentTextColor; 142 143 /** 144 * Our own key listener, specialized for dealing with DTMF codes. 145 * 1. Ignore the backspace since it is irrelevant. 146 * 2. Allow ONLY valid DTMF characters to generate a tone and be 147 * sent as a DTMF code. 148 * 3. All other remaining characters are handled by the superclass. 149 * 150 * This code is purely here to handle events from the hardware keyboard 151 * while the DTMF dialpad is up. 152 */ 153 private class DTMFKeyListener extends DialerKeyListener { 154 DTMFKeyListener()155 private DTMFKeyListener() { 156 super(); 157 } 158 159 /** 160 * Overriden to return correct DTMF-dialable characters. 161 */ 162 @Override getAcceptedChars()163 protected char[] getAcceptedChars(){ 164 return DTMF_CHARACTERS; 165 } 166 167 /** special key listener ignores backspace. */ 168 @Override backspace(View view, Editable content, int keyCode, KeyEvent event)169 public boolean backspace(View view, Editable content, int keyCode, 170 KeyEvent event) { 171 return false; 172 } 173 174 /** 175 * Return true if the keyCode is an accepted modifier key for the 176 * dialer (ALT or SHIFT). 177 */ isAcceptableModifierKey(int keyCode)178 private boolean isAcceptableModifierKey(int keyCode) { 179 switch (keyCode) { 180 case KeyEvent.KEYCODE_ALT_LEFT: 181 case KeyEvent.KEYCODE_ALT_RIGHT: 182 case KeyEvent.KEYCODE_SHIFT_LEFT: 183 case KeyEvent.KEYCODE_SHIFT_RIGHT: 184 return true; 185 default: 186 return false; 187 } 188 } 189 190 /** 191 * Overriden so that with each valid button press, we start sending 192 * a dtmf code and play a local dtmf tone. 193 */ 194 @Override onKeyDown(View view, Editable content, int keyCode, KeyEvent event)195 public boolean onKeyDown(View view, Editable content, 196 int keyCode, KeyEvent event) { 197 // if (DBG) log("DTMFKeyListener.onKeyDown, keyCode " + keyCode + ", view " + view); 198 199 // find the character 200 char c = (char) lookup(event, content); 201 202 // if not a long press, and parent onKeyDown accepts the input 203 if (event.getRepeatCount() == 0 && super.onKeyDown(view, content, keyCode, event)) { 204 205 boolean keyOK = ok(getAcceptedChars(), c); 206 207 // if the character is a valid dtmf code, start playing the tone and send the 208 // code. 209 if (keyOK) { 210 Log.d(this, "DTMFKeyListener reading '" + c + "' from input."); 211 getPresenter().processDtmf(c); 212 } else { 213 Log.d(this, "DTMFKeyListener rejecting '" + c + "' from input."); 214 } 215 return true; 216 } 217 return false; 218 } 219 220 /** 221 * Overriden so that with each valid button up, we stop sending 222 * a dtmf code and the dtmf tone. 223 */ 224 @Override onKeyUp(View view, Editable content, int keyCode, KeyEvent event)225 public boolean onKeyUp(View view, Editable content, 226 int keyCode, KeyEvent event) { 227 // if (DBG) log("DTMFKeyListener.onKeyUp, keyCode " + keyCode + ", view " + view); 228 229 super.onKeyUp(view, content, keyCode, event); 230 231 // find the character 232 char c = (char) lookup(event, content); 233 234 boolean keyOK = ok(getAcceptedChars(), c); 235 236 if (keyOK) { 237 Log.d(this, "Stopping the tone for '" + c + "'"); 238 getPresenter().stopDtmf(); 239 return true; 240 } 241 242 return false; 243 } 244 245 /** 246 * Handle individual keydown events when we DO NOT have an Editable handy. 247 */ onKeyDown(KeyEvent event)248 public boolean onKeyDown(KeyEvent event) { 249 char c = lookup(event); 250 Log.d(this, "DTMFKeyListener.onKeyDown: event '" + c + "'"); 251 252 // if not a long press, and parent onKeyDown accepts the input 253 if (event.getRepeatCount() == 0 && c != 0) { 254 // if the character is a valid dtmf code, start playing the tone and send the 255 // code. 256 if (ok(getAcceptedChars(), c)) { 257 Log.d(this, "DTMFKeyListener reading '" + c + "' from input."); 258 getPresenter().processDtmf(c); 259 return true; 260 } else { 261 Log.d(this, "DTMFKeyListener rejecting '" + c + "' from input."); 262 } 263 } 264 return false; 265 } 266 267 /** 268 * Handle individual keyup events. 269 * 270 * @param event is the event we are trying to stop. If this is null, 271 * then we just force-stop the last tone without checking if the event 272 * is an acceptable dialer event. 273 */ onKeyUp(KeyEvent event)274 public boolean onKeyUp(KeyEvent event) { 275 if (event == null) { 276 //the below piece of code sends stopDTMF event unnecessarily even when a null event 277 //is received, hence commenting it. 278 /*if (DBG) log("Stopping the last played tone."); 279 stopTone();*/ 280 return true; 281 } 282 283 char c = lookup(event); 284 Log.d(this, "DTMFKeyListener.onKeyUp: event '" + c + "'"); 285 286 // TODO: stopTone does not take in character input, we may want to 287 // consider checking for this ourselves. 288 if (ok(getAcceptedChars(), c)) { 289 Log.d(this, "Stopping the tone for '" + c + "'"); 290 getPresenter().stopDtmf(); 291 return true; 292 } 293 294 return false; 295 } 296 297 /** 298 * Find the Dialer Key mapped to this event. 299 * 300 * @return The char value of the input event, otherwise 301 * 0 if no matching character was found. 302 */ lookup(KeyEvent event)303 private char lookup(KeyEvent event) { 304 // This code is similar to {@link DialerKeyListener#lookup(KeyEvent, Spannable) lookup} 305 int meta = event.getMetaState(); 306 int number = event.getNumber(); 307 308 if (!((meta & (KeyEvent.META_ALT_ON | KeyEvent.META_SHIFT_ON)) == 0) || (number == 0)) { 309 int match = event.getMatch(getAcceptedChars(), meta); 310 number = (match != 0) ? match : number; 311 } 312 313 return (char) number; 314 } 315 316 /** 317 * Check to see if the keyEvent is dialable. 318 */ isKeyEventAcceptable(KeyEvent event)319 boolean isKeyEventAcceptable (KeyEvent event) { 320 return (ok(getAcceptedChars(), lookup(event))); 321 } 322 323 /** 324 * Overrides the characters used in {@link DialerKeyListener#CHARACTERS} 325 * These are the valid dtmf characters. 326 */ 327 public final char[] DTMF_CHARACTERS = new char[] { 328 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#', '*' 329 }; 330 } 331 332 @Override onClick(View v)333 public void onClick(View v) { 334 final AccessibilityManager accessibilityManager = (AccessibilityManager) 335 v.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); 336 // When accessibility is on, simulate press and release to preserve the 337 // semantic meaning of performClick(). Required for Braille support. 338 if (accessibilityManager.isEnabled()) { 339 final int id = v.getId(); 340 // Checking the press state prevents double activation. 341 if (!v.isPressed() && mDisplayMap.containsKey(id)) { 342 getPresenter().processDtmf(mDisplayMap.get(id)); 343 sHandler.postDelayed(new Runnable() { 344 @Override 345 public void run() { 346 getPresenter().stopDtmf(); 347 } 348 }, ACCESSIBILITY_DTMF_STOP_DELAY_MILLIS); 349 } 350 } 351 } 352 353 @Override onHover(View v, MotionEvent event)354 public boolean onHover(View v, MotionEvent event) { 355 // When touch exploration is turned on, lifting a finger while inside 356 // the button's hover target bounds should perform a click action. 357 final AccessibilityManager accessibilityManager = (AccessibilityManager) 358 v.getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); 359 360 if (accessibilityManager.isEnabled() 361 && accessibilityManager.isTouchExplorationEnabled()) { 362 final int left = v.getPaddingLeft(); 363 final int right = (v.getWidth() - v.getPaddingRight()); 364 final int top = v.getPaddingTop(); 365 final int bottom = (v.getHeight() - v.getPaddingBottom()); 366 367 switch (event.getActionMasked()) { 368 case MotionEvent.ACTION_HOVER_ENTER: 369 // Lift-to-type temporarily disables double-tap activation. 370 v.setClickable(false); 371 break; 372 case MotionEvent.ACTION_HOVER_EXIT: 373 final int x = (int) event.getX(); 374 final int y = (int) event.getY(); 375 if ((x > left) && (x < right) && (y > top) && (y < bottom)) { 376 v.performClick(); 377 } 378 v.setClickable(true); 379 break; 380 } 381 } 382 383 return false; 384 } 385 386 @Override onKey(View v, int keyCode, KeyEvent event)387 public boolean onKey(View v, int keyCode, KeyEvent event) { 388 Log.d(this, "onKey: keyCode " + keyCode + ", view " + v); 389 390 if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { 391 int viewId = v.getId(); 392 if (mDisplayMap.containsKey(viewId)) { 393 switch (event.getAction()) { 394 case KeyEvent.ACTION_DOWN: 395 if (event.getRepeatCount() == 0) { 396 getPresenter().processDtmf(mDisplayMap.get(viewId)); 397 } 398 break; 399 case KeyEvent.ACTION_UP: 400 getPresenter().stopDtmf(); 401 break; 402 } 403 // do not return true [handled] here, since we want the 404 // press / click animation to be handled by the framework. 405 } 406 } 407 return false; 408 } 409 410 @Override onTouch(View v, MotionEvent event)411 public boolean onTouch(View v, MotionEvent event) { 412 Log.d(this, "onTouch"); 413 int viewId = v.getId(); 414 415 // if the button is recognized 416 if (mDisplayMap.containsKey(viewId)) { 417 switch (event.getAction()) { 418 case MotionEvent.ACTION_DOWN: 419 // Append the character mapped to this button, to the display. 420 // start the tone 421 getPresenter().processDtmf(mDisplayMap.get(viewId)); 422 break; 423 case MotionEvent.ACTION_UP: 424 case MotionEvent.ACTION_CANCEL: 425 // stop the tone on ANY other event, except for MOVE. 426 getPresenter().stopDtmf(); 427 break; 428 } 429 // do not return true [handled] here, since we want the 430 // press / click animation to be handled by the framework. 431 } 432 return false; 433 } 434 435 // TODO(klp) Adds hardware keyboard listener 436 437 @Override createPresenter()438 DialpadPresenter createPresenter() { 439 return new DialpadPresenter(); 440 } 441 442 @Override getUi()443 DialpadPresenter.DialpadUi getUi() { 444 return this; 445 } 446 447 @Override onCreate(Bundle savedInstanceState)448 public void onCreate(Bundle savedInstanceState) { 449 super.onCreate(savedInstanceState); 450 } 451 452 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)453 public View onCreateView(LayoutInflater inflater, ViewGroup container, 454 Bundle savedInstanceState) { 455 final View parent = inflater.inflate( 456 com.android.incallui.R.layout.dtmf_twelve_key_dialer_view, container, false); 457 mDialpadView = (DialpadView) parent.findViewById(R.id.dialpad_view); 458 mDialpadView.setCanDigitsBeEdited(false); 459 mDialpadView.setBackgroundResource(R.color.incall_dialpad_background); 460 mDtmfDialerField = (EditText) parent.findViewById(R.id.digits); 461 if (mDtmfDialerField != null) { 462 mDialerKeyListener = new DTMFKeyListener(); 463 mDtmfDialerField.setKeyListener(mDialerKeyListener); 464 // remove the long-press context menus that support 465 // the edit (copy / paste / select) functions. 466 mDtmfDialerField.setLongClickable(false); 467 mDtmfDialerField.setElegantTextHeight(false); 468 configureKeypadListeners(); 469 } 470 471 return parent; 472 } 473 474 @Override onResume()475 public void onResume() { 476 super.onResume(); 477 updateColors(); 478 } 479 updateColors()480 public void updateColors() { 481 int textColor = InCallPresenter.getInstance().getThemeColors().mPrimaryColor; 482 483 if (mCurrentTextColor == textColor) { 484 return; 485 } 486 487 DialpadKeyButton dialpadKey; 488 for (int i = 0; i < mButtonIds.length; i++) { 489 dialpadKey = (DialpadKeyButton) mDialpadView.findViewById(mButtonIds[i]); 490 ((TextView) dialpadKey.findViewById(R.id.dialpad_key_number)).setTextColor(textColor); 491 } 492 493 mCurrentTextColor = textColor; 494 } 495 496 @Override onDestroyView()497 public void onDestroyView() { 498 mDialerKeyListener = null; 499 super.onDestroyView(); 500 } 501 502 /** 503 * Getter for Dialpad text. 504 * 505 * @return String containing current Dialpad EditText text. 506 */ getDtmfText()507 public String getDtmfText() { 508 return mDtmfDialerField.getText().toString(); 509 } 510 511 /** 512 * Sets the Dialpad text field with some text. 513 * 514 * @param text Text to set Dialpad EditText to. 515 */ setDtmfText(String text)516 public void setDtmfText(String text) { 517 mDtmfDialerField.setText(PhoneNumberUtils.ttsSpanAsPhoneNumber(text)); 518 } 519 520 @Override setVisible(boolean on)521 public void setVisible(boolean on) { 522 if (on) { 523 getView().setVisibility(View.VISIBLE); 524 } else { 525 getView().setVisibility(View.INVISIBLE); 526 } 527 } 528 529 /** 530 * Starts the slide up animation for the Dialpad keys when the Dialpad is revealed. 531 */ animateShowDialpad()532 public void animateShowDialpad() { 533 final DialpadView dialpadView = (DialpadView) getView().findViewById(R.id.dialpad_view); 534 dialpadView.animateShow(); 535 } 536 537 @Override appendDigitsToField(char digit)538 public void appendDigitsToField(char digit) { 539 if (mDtmfDialerField != null) { 540 // TODO: maybe *don't* manually append this digit if 541 // mDialpadDigits is focused and this key came from the HW 542 // keyboard, since in that case the EditText field will 543 // get the key event directly and automatically appends 544 // whetever the user types. 545 // (Or, a cleaner fix would be to just make mDialpadDigits 546 // *not* handle HW key presses. That seems to be more 547 // complicated than just setting focusable="false" on it, 548 // though.) 549 mDtmfDialerField.getText().append(digit); 550 } 551 } 552 553 /** 554 * Called externally (from InCallScreen) to play a DTMF Tone. 555 */ onDialerKeyDown(KeyEvent event)556 /* package */ boolean onDialerKeyDown(KeyEvent event) { 557 Log.d(this, "Notifying dtmf key down."); 558 if (mDialerKeyListener != null) { 559 return mDialerKeyListener.onKeyDown(event); 560 } else { 561 return false; 562 } 563 } 564 565 /** 566 * Called externally (from InCallScreen) to cancel the last DTMF Tone played. 567 */ onDialerKeyUp(KeyEvent event)568 public boolean onDialerKeyUp(KeyEvent event) { 569 Log.d(this, "Notifying dtmf key up."); 570 if (mDialerKeyListener != null) { 571 return mDialerKeyListener.onKeyUp(event); 572 } else { 573 return false; 574 } 575 } 576 configureKeypadListeners()577 private void configureKeypadListeners() { 578 DialpadKeyButton dialpadKey; 579 for (int i = 0; i < mButtonIds.length; i++) { 580 dialpadKey = (DialpadKeyButton) mDialpadView.findViewById(mButtonIds[i]); 581 dialpadKey.setOnTouchListener(this); 582 dialpadKey.setOnKeyListener(this); 583 dialpadKey.setOnHoverListener(this); 584 dialpadKey.setOnClickListener(this); 585 } 586 } 587 } 588