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