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