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