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