1 /* 2 * Copyright (C) 2008 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.phone; 18 19 import android.content.Context; 20 import android.media.AudioManager; 21 import android.media.ToneGenerator; 22 import android.os.Handler; 23 import android.os.Message; 24 import android.provider.Settings; 25 import android.telephony.PhoneNumberUtils; 26 import android.text.Editable; 27 import android.text.SpannableString; 28 import android.text.method.DialerKeyListener; 29 import android.text.style.RelativeSizeSpan; 30 import android.util.Log; 31 import android.view.KeyEvent; 32 import android.view.MotionEvent; 33 import android.view.View; 34 import android.view.ViewConfiguration; 35 import android.view.View.OnHoverListener; 36 import android.view.accessibility.AccessibilityManager; 37 import android.view.ViewStub; 38 import android.widget.EditText; 39 40 import com.android.internal.telephony.CallManager; 41 import com.android.internal.telephony.Phone; 42 import com.android.internal.telephony.PhoneConstants; 43 import com.android.internal.telephony.TelephonyCapabilities; 44 45 import java.util.HashMap; 46 import java.util.LinkedList; 47 import java.util.Queue; 48 49 50 /** 51 * Dialer class that encapsulates the DTMF twelve key behaviour. 52 * This model backs up the UI behaviour in DTMFTwelveKeyDialerView.java. 53 */ 54 public class DTMFTwelveKeyDialer implements View.OnTouchListener, View.OnKeyListener, 55 View.OnHoverListener, View.OnClickListener { 56 private static final String LOG_TAG = "DTMFTwelveKeyDialer"; 57 private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2); 58 59 // events 60 private static final int PHONE_DISCONNECT = 100; 61 private static final int DTMF_SEND_CNF = 101; 62 private static final int DTMF_STOP = 102; 63 64 /** Accessibility manager instance used to check touch exploration state. */ 65 private final AccessibilityManager mAccessibilityManager; 66 67 private CallManager mCM; 68 private ToneGenerator mToneGenerator; 69 private final Object mToneGeneratorLock = new Object(); 70 71 // indicate if we want to enable the local tone playback. 72 private boolean mLocalToneEnabled; 73 74 // indicates that we are using automatically shortened DTMF tones 75 boolean mShortTone; 76 77 // indicate if the confirmation from TelephonyFW is pending. 78 private boolean mDTMFBurstCnfPending = false; 79 80 // Queue to queue the short dtmf characters. 81 private Queue<Character> mDTMFQueue = new LinkedList<Character>(); 82 83 // Short Dtmf tone duration 84 private static final int DTMF_DURATION_MS = 120; 85 86 87 /** Hash Map to map a character to a tone*/ 88 private static final HashMap<Character, Integer> mToneMap = 89 new HashMap<Character, Integer>(); 90 /** Hash Map to map a view id to a character*/ 91 private static final HashMap<Integer, Character> mDisplayMap = 92 new HashMap<Integer, Character>(); 93 /** Set up the static maps*/ 94 static { 95 // Map the key characters to tones 96 mToneMap.put('1', ToneGenerator.TONE_DTMF_1); 97 mToneMap.put('2', ToneGenerator.TONE_DTMF_2); 98 mToneMap.put('3', ToneGenerator.TONE_DTMF_3); 99 mToneMap.put('4', ToneGenerator.TONE_DTMF_4); 100 mToneMap.put('5', ToneGenerator.TONE_DTMF_5); 101 mToneMap.put('6', ToneGenerator.TONE_DTMF_6); 102 mToneMap.put('7', ToneGenerator.TONE_DTMF_7); 103 mToneMap.put('8', ToneGenerator.TONE_DTMF_8); 104 mToneMap.put('9', ToneGenerator.TONE_DTMF_9); 105 mToneMap.put('0', ToneGenerator.TONE_DTMF_0); 106 mToneMap.put('#', ToneGenerator.TONE_DTMF_P); 107 mToneMap.put('*', ToneGenerator.TONE_DTMF_S); 108 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 /** EditText field used to display the DTMF digits sent so far. 125 Note this is null in some modes (like during the CDMA OTA call, 126 where there's no onscreen "digits" display.) */ 127 private EditText mDialpadDigits; 128 129 // InCallScreen reference. 130 private InCallScreen mInCallScreen; 131 132 /** 133 * The DTMFTwelveKeyDialerView we use to display the dialpad. 134 * 135 * Only one of mDialerView or mDialerStub will have a legitimate object; the other one will be 136 * null at that moment. Either of following scenarios will occur: 137 * 138 * - If the constructor with {@link DTMFTwelveKeyDialerView} is called, mDialerView will 139 * obtain that object, and mDialerStub will be null. mDialerStub won't be used in this case. 140 * 141 * - If the constructor with {@link ViewStub} is called, mDialerView will be null at that 142 * moment, and mDialerStub will obtain the ViewStub object. 143 * When the dialer is required by the user (i.e. until {@link #openDialer(boolean)} being 144 * called), mDialerStub will inflate the dialer, and make mDialerStub itself null. 145 * mDialerStub won't be used afterward. 146 */ 147 private DTMFTwelveKeyDialerView mDialerView; 148 149 /** 150 * {@link ViewStub} holding {@link DTMFTwelveKeyDialerView}. See the comments for mDialerView. 151 */ 152 private ViewStub mDialerStub; 153 154 // KeyListener used with the "dialpad digits" EditText widget. 155 private DTMFKeyListener mDialerKeyListener; 156 157 /** 158 * Our own key listener, specialized for dealing with DTMF codes. 159 * 1. Ignore the backspace since it is irrelevant. 160 * 2. Allow ONLY valid DTMF characters to generate a tone and be 161 * sent as a DTMF code. 162 * 3. All other remaining characters are handled by the superclass. 163 * 164 * This code is purely here to handle events from the hardware keyboard 165 * while the DTMF dialpad is up. 166 */ 167 private class DTMFKeyListener extends DialerKeyListener { 168 169 private DTMFKeyListener() { 170 super(); 171 } 172 173 /** 174 * Overriden to return correct DTMF-dialable characters. 175 */ 176 @Override 177 protected char[] getAcceptedChars(){ 178 return DTMF_CHARACTERS; 179 } 180 181 /** special key listener ignores backspace. */ 182 @Override 183 public boolean backspace(View view, Editable content, int keyCode, 184 KeyEvent event) { 185 return false; 186 } 187 188 /** 189 * Return true if the keyCode is an accepted modifier key for the 190 * dialer (ALT or SHIFT). 191 */ 192 private boolean isAcceptableModifierKey(int keyCode) { 193 switch (keyCode) { 194 case KeyEvent.KEYCODE_ALT_LEFT: 195 case KeyEvent.KEYCODE_ALT_RIGHT: 196 case KeyEvent.KEYCODE_SHIFT_LEFT: 197 case KeyEvent.KEYCODE_SHIFT_RIGHT: 198 return true; 199 default: 200 return false; 201 } 202 } 203 204 /** 205 * Overriden so that with each valid button press, we start sending 206 * a dtmf code and play a local dtmf tone. 207 */ 208 @Override 209 public boolean onKeyDown(View view, Editable content, 210 int keyCode, KeyEvent event) { 211 // if (DBG) log("DTMFKeyListener.onKeyDown, keyCode " + keyCode + ", view " + view); 212 213 // find the character 214 char c = (char) lookup(event, content); 215 216 // if not a long press, and parent onKeyDown accepts the input 217 if (event.getRepeatCount() == 0 && super.onKeyDown(view, content, keyCode, event)) { 218 219 boolean keyOK = ok(getAcceptedChars(), c); 220 221 // if the character is a valid dtmf code, start playing the tone and send the 222 // code. 223 if (keyOK) { 224 if (DBG) log("DTMFKeyListener reading '" + c + "' from input."); 225 processDtmf(c); 226 } else if (DBG) { 227 log("DTMFKeyListener rejecting '" + c + "' from input."); 228 } 229 return true; 230 } 231 return false; 232 } 233 234 /** 235 * Overriden so that with each valid button up, we stop sending 236 * a dtmf code and the dtmf tone. 237 */ 238 @Override 239 public boolean onKeyUp(View view, Editable content, 240 int keyCode, KeyEvent event) { 241 // if (DBG) log("DTMFKeyListener.onKeyUp, keyCode " + keyCode + ", view " + view); 242 243 super.onKeyUp(view, content, keyCode, event); 244 245 // find the character 246 char c = (char) lookup(event, content); 247 248 boolean keyOK = ok(getAcceptedChars(), c); 249 250 if (keyOK) { 251 if (DBG) log("Stopping the tone for '" + c + "'"); 252 stopTone(); 253 return true; 254 } 255 256 return false; 257 } 258 259 /** 260 * Handle individual keydown events when we DO NOT have an Editable handy. 261 */ 262 public boolean onKeyDown(KeyEvent event) { 263 char c = lookup(event); 264 if (DBG) log("DTMFKeyListener.onKeyDown: event '" + c + "'"); 265 266 // if not a long press, and parent onKeyDown accepts the input 267 if (event.getRepeatCount() == 0 && c != 0) { 268 // if the character is a valid dtmf code, start playing the tone and send the 269 // code. 270 if (ok(getAcceptedChars(), c)) { 271 if (DBG) log("DTMFKeyListener reading '" + c + "' from input."); 272 processDtmf(c); 273 return true; 274 } else if (DBG) { 275 log("DTMFKeyListener rejecting '" + c + "' from input."); 276 } 277 } 278 return false; 279 } 280 281 /** 282 * Handle individual keyup events. 283 * 284 * @param event is the event we are trying to stop. If this is null, 285 * then we just force-stop the last tone without checking if the event 286 * is an acceptable dialer event. 287 */ 288 public boolean onKeyUp(KeyEvent event) { 289 if (event == null) { 290 //the below piece of code sends stopDTMF event unnecessarily even when a null event 291 //is received, hence commenting it. 292 /*if (DBG) log("Stopping the last played tone."); 293 stopTone();*/ 294 return true; 295 } 296 297 char c = lookup(event); 298 if (DBG) log("DTMFKeyListener.onKeyUp: event '" + c + "'"); 299 300 // TODO: stopTone does not take in character input, we may want to 301 // consider checking for this ourselves. 302 if (ok(getAcceptedChars(), c)) { 303 if (DBG) log("Stopping the tone for '" + c + "'"); 304 stopTone(); 305 return true; 306 } 307 308 return false; 309 } 310 311 /** 312 * Find the Dialer Key mapped to this event. 313 * 314 * @return The char value of the input event, otherwise 315 * 0 if no matching character was found. 316 */ 317 private char lookup(KeyEvent event) { 318 // This code is similar to {@link DialerKeyListener#lookup(KeyEvent, Spannable) lookup} 319 int meta = event.getMetaState(); 320 int number = event.getNumber(); 321 322 if (!((meta & (KeyEvent.META_ALT_ON | KeyEvent.META_SHIFT_ON)) == 0) || (number == 0)) { 323 int match = event.getMatch(getAcceptedChars(), meta); 324 number = (match != 0) ? match : number; 325 } 326 327 return (char) number; 328 } 329 330 /** 331 * Check to see if the keyEvent is dialable. 332 */ 333 boolean isKeyEventAcceptable (KeyEvent event) { 334 return (ok(getAcceptedChars(), lookup(event))); 335 } 336 337 /** 338 * Overrides the characters used in {@link DialerKeyListener#CHARACTERS} 339 * These are the valid dtmf characters. 340 */ 341 public final char[] DTMF_CHARACTERS = new char[] { 342 '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#', '*' 343 }; 344 } 345 346 /** 347 * Our own handler to take care of the messages from the phone state changes 348 */ 349 private final Handler mHandler = new Handler() { 350 @Override 351 public void handleMessage(Message msg) { 352 switch (msg.what) { 353 // disconnect action 354 // make sure to close the dialer on ALL disconnect actions. 355 case PHONE_DISCONNECT: 356 if (DBG) log("disconnect message recieved, shutting down."); 357 // unregister since we are closing. 358 mCM.unregisterForDisconnect(this); 359 closeDialer(false); 360 break; 361 case DTMF_SEND_CNF: 362 if (DBG) log("dtmf confirmation received from FW."); 363 // handle burst dtmf confirmation 364 handleBurstDtmfConfirmation(); 365 break; 366 case DTMF_STOP: 367 if (DBG) log("dtmf stop received"); 368 stopTone(); 369 break; 370 } 371 } 372 }; 373 374 375 /** 376 * DTMFTwelveKeyDialer constructor with {@link DTMFTwelveKeyDialerView} 377 * 378 * @param parent the InCallScreen instance that owns us. 379 * @param dialerView the DTMFTwelveKeyDialerView we should use to display the dialpad. 380 */ 381 public DTMFTwelveKeyDialer(InCallScreen parent, 382 DTMFTwelveKeyDialerView dialerView) { 383 this(parent); 384 385 // The passed-in DTMFTwelveKeyDialerView *should* always be 386 // non-null, now that the in-call UI uses only portrait mode. 387 if (dialerView == null) { 388 Log.e(LOG_TAG, "DTMFTwelveKeyDialer: null dialerView!", new IllegalStateException()); 389 // ...continue as best we can, although things will 390 // be pretty broken without the mDialerView UI elements! 391 } 392 mDialerView = dialerView; 393 if (DBG) log("- Got passed-in mDialerView: " + mDialerView); 394 395 if (mDialerView != null) { 396 setupDialerView(); 397 } 398 } 399 400 /** 401 * DTMFTwelveKeyDialer constructor with {@link ViewStub}. 402 * 403 * When the dialer is required for the first time (e.g. when {@link #openDialer(boolean)} is 404 * called), the object will inflate the ViewStub by itself, assuming the ViewStub will return 405 * {@link DTMFTwelveKeyDialerView} on {@link ViewStub#inflate()}. 406 * 407 * @param parent the InCallScreen instance that owns us. 408 * @param dialerStub ViewStub which will return {@link DTMFTwelveKeyDialerView} on 409 * {@link ViewStub#inflate()}. 410 */ 411 public DTMFTwelveKeyDialer(InCallScreen parent, ViewStub dialerStub) { 412 this(parent); 413 414 mDialerStub = dialerStub; 415 if (DBG) log("- Got passed-in mDialerStub: " + mDialerStub); 416 417 // At this moment mDialerView is still null. We delay calling setupDialerView(). 418 } 419 420 /** 421 * Private constructor used for initialization calls common to all public 422 * constructors. 423 * 424 * @param parent the InCallScreen instance that owns us. 425 */ 426 private DTMFTwelveKeyDialer(InCallScreen parent) { 427 if (DBG) log("DTMFTwelveKeyDialer constructor... this = " + this); 428 429 mInCallScreen = parent; 430 mCM = PhoneGlobals.getInstance().mCM; 431 mAccessibilityManager = (AccessibilityManager) parent.getSystemService( 432 Context.ACCESSIBILITY_SERVICE); 433 } 434 435 /** 436 * Prepare the dialer view and relevant variables. 437 */ 438 private void setupDialerView() { 439 if (DBG) log("setupDialerView()"); 440 mDialerView.setDialer(this); 441 442 // In the normal in-call DTMF dialpad, mDialpadDigits is an 443 // EditText used to display the digits the user has typed so 444 // far. But some other modes (like the OTA call) have no 445 // "digits" display at all, in which case mDialpadDigits will 446 // be null. 447 mDialpadDigits = (EditText) mDialerView.findViewById(R.id.dtmfDialerField); 448 if (mDialpadDigits != null) { 449 mDialerKeyListener = new DTMFKeyListener(); 450 mDialpadDigits.setKeyListener(mDialerKeyListener); 451 452 // remove the long-press context menus that support 453 // the edit (copy / paste / select) functions. 454 mDialpadDigits.setLongClickable(false); 455 } 456 457 // Hook up touch / key listeners for the buttons in the onscreen 458 // keypad. 459 setupKeypad(mDialerView); 460 } 461 462 /** 463 * Null out our reference to the InCallScreen activity. 464 * This indicates that the InCallScreen activity has been destroyed. 465 * At the same time, get rid of listeners since we're not going to 466 * be valid anymore. 467 */ 468 /* package */ void clearInCallScreenReference() { 469 if (DBG) log("clearInCallScreenReference()..."); 470 mInCallScreen = null; 471 mDialerKeyListener = null; 472 mHandler.removeMessages(DTMF_SEND_CNF); 473 synchronized (mDTMFQueue) { 474 mDTMFBurstCnfPending = false; 475 mDTMFQueue.clear(); 476 } 477 closeDialer(false); 478 } 479 480 /** 481 * Dialer code that runs when the dialer is brought up. 482 * This includes layout changes, etc, and just prepares the dialer model for use. 483 */ 484 private void onDialerOpen(boolean animate) { 485 if (DBG) log("onDialerOpen()..."); 486 487 // Any time the dialer is open, listen for "disconnect" events (so 488 // we can close ourself.) 489 mCM.registerForDisconnect(mHandler, PHONE_DISCONNECT, null); 490 491 // On some devices the screen timeout is set to a special value 492 // while the dialpad is up. 493 PhoneGlobals.getInstance().updateWakeState(); 494 495 // Give the InCallScreen a chance to do any necessary UI updates. 496 if (mInCallScreen != null) { 497 mInCallScreen.onDialerOpen(animate); 498 } else { 499 Log.e(LOG_TAG, "InCallScreen object was null during onDialerOpen()"); 500 } 501 } 502 503 /** 504 * Allocates some resources we keep around during a "dialer session". 505 * 506 * (Currently, a "dialer session" just means any situation where we 507 * might need to play local DTMF tones, which means that we need to 508 * keep a ToneGenerator instance around. A ToneGenerator instance 509 * keeps an AudioTrack resource busy in AudioFlinger, so we don't want 510 * to keep it around forever.) 511 * 512 * Call {@link stopDialerSession} to release the dialer session 513 * resources. 514 */ 515 public void startDialerSession() { 516 if (DBG) log("startDialerSession()... this = " + this); 517 518 // see if we need to play local tones. 519 if (PhoneGlobals.getInstance().getResources().getBoolean(R.bool.allow_local_dtmf_tones)) { 520 mLocalToneEnabled = Settings.System.getInt(mInCallScreen.getContentResolver(), 521 Settings.System.DTMF_TONE_WHEN_DIALING, 1) == 1; 522 } else { 523 mLocalToneEnabled = false; 524 } 525 if (DBG) log("- startDialerSession: mLocalToneEnabled = " + mLocalToneEnabled); 526 527 // create the tone generator 528 // if the mToneGenerator creation fails, just continue without it. It is 529 // a local audio signal, and is not as important as the dtmf tone itself. 530 if (mLocalToneEnabled) { 531 synchronized (mToneGeneratorLock) { 532 if (mToneGenerator == null) { 533 try { 534 mToneGenerator = new ToneGenerator(AudioManager.STREAM_DTMF, 80); 535 } catch (RuntimeException e) { 536 if (DBG) log("Exception caught while creating local tone generator: " + e); 537 mToneGenerator = null; 538 } 539 } 540 } 541 } 542 } 543 544 /** 545 * Dialer code that runs when the dialer is closed. 546 * This releases resources acquired when we start the dialer. 547 */ 548 private void onDialerClose(boolean animate) { 549 if (DBG) log("onDialerClose()..."); 550 551 // reset back to a short delay for the poke lock. 552 PhoneGlobals app = PhoneGlobals.getInstance(); 553 app.updateWakeState(); 554 555 mCM.unregisterForDisconnect(mHandler); 556 557 // Give the InCallScreen a chance to do any necessary UI updates. 558 if (mInCallScreen != null) { 559 mInCallScreen.onDialerClose(animate); 560 } else { 561 Log.e(LOG_TAG, "InCallScreen object was null during onDialerClose()"); 562 } 563 } 564 565 /** 566 * Releases resources we keep around during a "dialer session" 567 * (see {@link startDialerSession}). 568 * 569 * It's safe to call this even without a corresponding 570 * startDialerSession call. 571 */ 572 public void stopDialerSession() { 573 // release the tone generator. 574 synchronized (mToneGeneratorLock) { 575 if (mToneGenerator != null) { 576 mToneGenerator.release(); 577 mToneGenerator = null; 578 } 579 } 580 } 581 582 /** 583 * Called externally (from InCallScreen) to play a DTMF Tone. 584 */ 585 public boolean onDialerKeyDown(KeyEvent event) { 586 if (DBG) log("Notifying dtmf key down."); 587 if (mDialerKeyListener != null) { 588 return mDialerKeyListener.onKeyDown(event); 589 } else { 590 return false; 591 } 592 } 593 594 /** 595 * Called externally (from InCallScreen) to cancel the last DTMF Tone played. 596 */ 597 public boolean onDialerKeyUp(KeyEvent event) { 598 if (DBG) log("Notifying dtmf key up."); 599 if (mDialerKeyListener != null) { 600 return mDialerKeyListener.onKeyUp(event); 601 } else { 602 return false; 603 } 604 } 605 606 /** 607 * setup the keys on the dialer activity, using the keymaps. 608 */ 609 private void setupKeypad(DTMFTwelveKeyDialerView dialerView) { 610 // for each view id listed in the displaymap 611 View button; 612 for (int viewId : mDisplayMap.keySet()) { 613 // locate the view 614 button = dialerView.findViewById(viewId); 615 // Setup the listeners for the buttons 616 button.setOnTouchListener(this); 617 button.setClickable(true); 618 button.setOnKeyListener(this); 619 button.setOnHoverListener(this); 620 button.setOnClickListener(this); 621 } 622 } 623 624 /** 625 * catch the back and call buttons to return to the in call activity. 626 */ 627 public boolean onKeyDown(int keyCode, KeyEvent event) { 628 // if (DBG) log("onKeyDown: keyCode " + keyCode); 629 switch (keyCode) { 630 // finish for these events 631 case KeyEvent.KEYCODE_BACK: 632 case KeyEvent.KEYCODE_CALL: 633 if (DBG) log("exit requested"); 634 closeDialer(true); // do the "closing" animation 635 return true; 636 } 637 return mInCallScreen.onKeyDown(keyCode, event); 638 } 639 640 /** 641 * catch the back and call buttons to return to the in call activity. 642 */ 643 public boolean onKeyUp(int keyCode, KeyEvent event) { 644 // if (DBG) log("onKeyUp: keyCode " + keyCode); 645 return mInCallScreen.onKeyUp(keyCode, event); 646 } 647 648 /** 649 * Implemented for {@link android.view.View.OnHoverListener}. Handles touch 650 * events for accessibility when touch exploration is enabled. 651 */ 652 @Override 653 public boolean onHover(View v, MotionEvent event) { 654 // When touch exploration is turned on, lifting a finger while inside 655 // the button's hover target bounds should perform a click action. 656 if (mAccessibilityManager.isEnabled() 657 && mAccessibilityManager.isTouchExplorationEnabled()) { 658 final int left = v.getPaddingLeft(); 659 final int right = (v.getWidth() - v.getPaddingRight()); 660 final int top = v.getPaddingTop(); 661 final int bottom = (v.getHeight() - v.getPaddingBottom()); 662 663 switch (event.getActionMasked()) { 664 case MotionEvent.ACTION_HOVER_ENTER: 665 // Lift-to-type temporarily disables double-tap activation. 666 v.setClickable(false); 667 break; 668 case MotionEvent.ACTION_HOVER_EXIT: 669 final int x = (int) event.getX(); 670 final int y = (int) event.getY(); 671 if ((x > left) && (x < right) && (y > top) && (y < bottom)) { 672 v.performClick(); 673 } 674 v.setClickable(true); 675 break; 676 } 677 } 678 679 return false; 680 } 681 682 @Override 683 public void onClick(View v) { 684 // When accessibility is on, simulate press and release to preserve the 685 // semantic meaning of performClick(). Required for Braille support. 686 if (mAccessibilityManager.isEnabled()) { 687 final int id = v.getId(); 688 // Checking the press state prevents double activation. 689 if (!v.isPressed() && mDisplayMap.containsKey(id)) { 690 processDtmf(mDisplayMap.get(id), true /* timedShortTone */); 691 } 692 } 693 } 694 695 /** 696 * Implemented for the TouchListener, process the touch events. 697 */ 698 @Override 699 public boolean onTouch(View v, MotionEvent event) { 700 int viewId = v.getId(); 701 702 // if the button is recognized 703 if (mDisplayMap.containsKey(viewId)) { 704 switch (event.getAction()) { 705 case MotionEvent.ACTION_DOWN: 706 // Append the character mapped to this button, to the display. 707 // start the tone 708 processDtmf(mDisplayMap.get(viewId)); 709 break; 710 case MotionEvent.ACTION_UP: 711 case MotionEvent.ACTION_CANCEL: 712 // stop the tone on ANY other event, except for MOVE. 713 stopTone(); 714 break; 715 } 716 // do not return true [handled] here, since we want the 717 // press / click animation to be handled by the framework. 718 } 719 return false; 720 } 721 722 /** 723 * Implements View.OnKeyListener for the DTMF buttons. Enables dialing with trackball/dpad. 724 */ 725 @Override 726 public boolean onKey(View v, int keyCode, KeyEvent event) { 727 // if (DBG) log("onKey: keyCode " + keyCode + ", view " + v); 728 729 if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) { 730 int viewId = v.getId(); 731 if (mDisplayMap.containsKey(viewId)) { 732 switch (event.getAction()) { 733 case KeyEvent.ACTION_DOWN: 734 if (event.getRepeatCount() == 0) { 735 processDtmf(mDisplayMap.get(viewId)); 736 } 737 break; 738 case KeyEvent.ACTION_UP: 739 stopTone(); 740 break; 741 } 742 // do not return true [handled] here, since we want the 743 // press / click animation to be handled by the framework. 744 } 745 } 746 return false; 747 } 748 749 /** 750 * Returns true if the dialer is in "open" state, meaning it is already visible *and* it 751 * isn't fading out. Note that during fade-out animation the View will return VISIBLE but 752 * will become GONE soon later, so you would want to use this method instead of 753 * {@link View#getVisibility()}. 754 * 755 * Fade-in animation, on the other hand, will set the View's visibility VISIBLE soon after 756 * the request, so we don't need to take care much of it. In other words, 757 * {@link #openDialer(boolean)} soon makes the visibility VISIBLE and thus this method will 758 * return true just after the method call. 759 * 760 * Note: during the very early stage of "open" state, users may not see the dialpad yet because 761 * of its fading-in animation, while they will see it shortly anyway. Similarly, during the 762 * early stage of "closed" state (opposite of "open" state), users may still see the dialpad 763 * due to fading-out animation, but it will vanish shortly and thus we can treat it as "closed", 764 * or "not open". To make the transition clearer, we call the state "open", not "shown" nor 765 * "visible". 766 */ 767 public boolean isOpened() { 768 // Return whether or not the dialer view is visible. 769 // (Note that if we're in the middle of a fade-out animation, that 770 // also counts as "not visible" even though mDialerView itself is 771 // technically still VISIBLE.) 772 return (mDialerView != null 773 &&(mDialerView.getVisibility() == View.VISIBLE) 774 && !AnimationUtils.Fade.isFadingOut(mDialerView)); 775 } 776 777 /** 778 * Forces the dialer into the "open" state. 779 * Does nothing if the dialer is already open. 780 * 781 * The "open" state includes the state the dialer is fading in. 782 * {@link InCallScreen#onDialerOpen(boolean)} will change visibility state and do 783 * actual animation. 784 * 785 * @param animate if true, open the dialer with an animation. 786 * 787 * @see #isOpened 788 */ 789 public void openDialer(boolean animate) { 790 if (DBG) log("openDialer()..."); 791 792 if (mDialerView == null && mDialerStub != null) { 793 if (DBG) log("Dialer isn't ready. Inflate it from ViewStub."); 794 mDialerView = (DTMFTwelveKeyDialerView) mDialerStub.inflate(); 795 setupDialerView(); 796 mDialerStub = null; 797 } 798 799 if (!isOpened()) { 800 // Make the dialer view visible. 801 if (animate) { 802 AnimationUtils.Fade.show(mDialerView); 803 } else { 804 mDialerView.setVisibility(View.VISIBLE); 805 } 806 onDialerOpen(animate); 807 } 808 } 809 810 /** 811 * Forces the dialer into the "closed" state. 812 * Does nothing if the dialer is already closed. 813 * 814 * {@link InCallScreen#onDialerOpen(boolean)} will change visibility state and do 815 * actual animation. 816 * 817 * @param animate if true, close the dialer with an animation. 818 * 819 * @see #isOpened 820 */ 821 public void closeDialer(boolean animate) { 822 if (DBG) log("closeDialer()..."); 823 824 if (isOpened()) { 825 // Hide the dialer view. 826 if (animate) { 827 AnimationUtils.Fade.hide(mDialerView, View.GONE); 828 } else { 829 mDialerView.setVisibility(View.GONE); 830 } 831 onDialerClose(animate); 832 } 833 } 834 835 /** 836 * Processes the specified digit as a DTMF key, by playing the 837 * appropriate DTMF tone, and appending the digit to the EditText 838 * field that displays the DTMF digits sent so far. 839 * 840 * @see #processDtmf(char, boolean) 841 */ 842 private final void processDtmf(char c) { 843 processDtmf(c, false); 844 } 845 846 /** 847 * Processes the specified digit as a DTMF key, by playing the appropriate 848 * DTMF tone (or short tone if requested), and appending the digit to the 849 * EditText field that displays the DTMF digits sent so far. 850 */ 851 private final void processDtmf(char c, boolean timedShortTone) { 852 // if it is a valid key, then update the display and send the dtmf tone. 853 if (PhoneNumberUtils.is12Key(c)) { 854 if (DBG) log("updating display and sending dtmf tone for '" + c + "'"); 855 856 // Append this key to the "digits" widget. 857 if (mDialpadDigits != null) { 858 // TODO: maybe *don't* manually append this digit if 859 // mDialpadDigits is focused and this key came from the HW 860 // keyboard, since in that case the EditText field will 861 // get the key event directly and automatically appends 862 // whetever the user types. 863 // (Or, a cleaner fix would be to just make mDialpadDigits 864 // *not* handle HW key presses. That seems to be more 865 // complicated than just setting focusable="false" on it, 866 // though.) 867 mDialpadDigits.getText().append(c); 868 } 869 870 // Play the tone if it exists. 871 if (mToneMap.containsKey(c)) { 872 // begin tone playback. 873 startTone(c, timedShortTone); 874 } 875 } else if (DBG) { 876 log("ignoring dtmf request for '" + c + "'"); 877 } 878 879 // Any DTMF keypress counts as explicit "user activity". 880 PhoneGlobals.getInstance().pokeUserActivity(); 881 } 882 883 /** 884 * Clears out the display of "DTMF digits typed so far" that's kept in 885 * mDialpadDigits. 886 * 887 * The InCallScreen is responsible for calling this method any time a 888 * new call becomes active (or, more simply, any time a call ends). 889 * This is how we make sure that the "history" of DTMF digits you type 890 * doesn't persist from one call to the next. 891 * 892 * TODO: it might be more elegent if the dialpad itself could remember 893 * the call that we're associated with, and clear the digits if the 894 * "current call" has changed since last time. (This would require 895 * some unique identifier that's different for each call. We can't 896 * just use the foreground Call object, since that's a singleton that 897 * lasts the whole life of the phone process. Instead, maybe look at 898 * the Connection object that comes back from getEarliestConnection()? 899 * Or getEarliestConnectTime()?) 900 * 901 * Or to be even fancier, we could keep a mapping of *multiple* 902 * "active calls" to DTMF strings. That way you could have two lines 903 * in use and swap calls multiple times, and we'd still remember the 904 * digits for each call. (But that's such an obscure use case that 905 * it's probably not worth the extra complexity.) 906 */ 907 public void clearDigits() { 908 if (DBG) log("clearDigits()..."); 909 910 if (mDialpadDigits != null) { 911 mDialpadDigits.setText(""); 912 } 913 914 setDialpadContext(""); 915 } 916 917 /** 918 * Set the context text (hint) to show in the dialpad Digits EditText. 919 * 920 * This is currently only used for displaying a value for "Voice Mail" 921 * calls since they default to the dialpad and we want to give users better 922 * context when they dial voicemail. 923 * 924 * TODO: Is there value in extending this functionality for all contacts 925 * and not just Voice Mail calls? 926 * TODO: This should include setting the digits as well as the context 927 * once we start saving the digits properly...and properly in this case 928 * ideally means moving some of processDtmf() out of this class. 929 */ 930 public void setDialpadContext(String contextValue) { 931 if (mDialpadDigits != null) { 932 if (contextValue == null) { 933 contextValue = ""; 934 } 935 final SpannableString hint = new SpannableString(contextValue); 936 hint.setSpan(new RelativeSizeSpan(0.8f), 0, hint.length(), 0); 937 mDialpadDigits.setHint(hint); 938 } 939 } 940 941 /** 942 * Plays the local tone based the phone type. 943 */ 944 public void startTone(char c, boolean timedShortTone) { 945 // Only play the tone if it exists. 946 if (!mToneMap.containsKey(c)) { 947 return; 948 } 949 950 if (!mInCallScreen.okToDialDTMFTones()) { 951 return; 952 } 953 954 // Read the settings as it may be changed by the user during the call 955 Phone phone = mCM.getFgPhone(); 956 mShortTone = useShortDtmfTones(phone, phone.getContext()); 957 958 // Before we go ahead and start a tone, we need to make sure that any pending 959 // stop-tone message is processed. 960 if (mHandler.hasMessages(DTMF_STOP)) { 961 mHandler.removeMessages(DTMF_STOP); 962 stopTone(); 963 } 964 965 if (DBG) log("startDtmfTone()..."); 966 967 // For Short DTMF we need to play the local tone for fixed duration 968 if (mShortTone) { 969 sendShortDtmfToNetwork(c); 970 } else { 971 // Pass as a char to be sent to network 972 if (DBG) log("send long dtmf for " + c); 973 mCM.startDtmf(c); 974 975 // If it is a timed tone, queue up the stop command in DTMF_DURATION_MS. 976 if (timedShortTone) { 977 mHandler.sendMessageDelayed(mHandler.obtainMessage(DTMF_STOP), DTMF_DURATION_MS); 978 } 979 } 980 startLocalToneIfNeeded(c); 981 } 982 983 984 /** 985 * Plays the local tone based the phone type, optionally forcing a short 986 * tone. 987 */ 988 public void startLocalToneIfNeeded(char c) { 989 // if local tone playback is enabled, start it. 990 // Only play the tone if it exists. 991 if (!mToneMap.containsKey(c)) { 992 return; 993 } 994 if (mLocalToneEnabled) { 995 synchronized (mToneGeneratorLock) { 996 if (mToneGenerator == null) { 997 if (DBG) log("startDtmfTone: mToneGenerator == null, tone: " + c); 998 } else { 999 if (DBG) log("starting local tone " + c); 1000 int toneDuration = -1; 1001 if (mShortTone) { 1002 toneDuration = DTMF_DURATION_MS; 1003 } 1004 mToneGenerator.startTone(mToneMap.get(c), toneDuration); 1005 } 1006 } 1007 } 1008 } 1009 1010 /** 1011 * Check to see if the keyEvent is dialable. 1012 */ 1013 boolean isKeyEventAcceptable (KeyEvent event) { 1014 return (mDialerKeyListener != null && mDialerKeyListener.isKeyEventAcceptable(event)); 1015 } 1016 1017 /** 1018 * static logging method 1019 */ 1020 private static void log(String msg) { 1021 Log.d(LOG_TAG, msg); 1022 } 1023 1024 /** 1025 * Stops the local tone based on the phone type. 1026 */ 1027 public void stopTone() { 1028 // We do not rely on InCallScreen#okToDialDTMFTones() here since it is ok to stop tones 1029 // without starting them. 1030 1031 if (!mShortTone) { 1032 if (DBG) log("stopping remote tone."); 1033 mCM.stopDtmf(); 1034 stopLocalToneIfNeeded(); 1035 } 1036 } 1037 1038 /** 1039 * Stops the local tone based on the phone type. 1040 */ 1041 public void stopLocalToneIfNeeded() { 1042 if (!mShortTone) { 1043 // if local tone playback is enabled, stop it. 1044 if (DBG) log("trying to stop local tone..."); 1045 if (mLocalToneEnabled) { 1046 synchronized (mToneGeneratorLock) { 1047 if (mToneGenerator == null) { 1048 if (DBG) log("stopLocalTone: mToneGenerator == null"); 1049 } else { 1050 if (DBG) log("stopping local tone."); 1051 mToneGenerator.stopTone(); 1052 } 1053 } 1054 } 1055 } 1056 } 1057 1058 /** 1059 * Sends the dtmf character over the network for short DTMF settings 1060 * When the characters are entered in quick succession, 1061 * the characters are queued before sending over the network. 1062 */ 1063 private void sendShortDtmfToNetwork(char dtmfDigit) { 1064 synchronized (mDTMFQueue) { 1065 if (mDTMFBurstCnfPending == true) { 1066 // Insert the dtmf char to the queue 1067 mDTMFQueue.add(new Character(dtmfDigit)); 1068 } else { 1069 String dtmfStr = Character.toString(dtmfDigit); 1070 mCM.sendBurstDtmf(dtmfStr, 0, 0, mHandler.obtainMessage(DTMF_SEND_CNF)); 1071 // Set flag to indicate wait for Telephony confirmation. 1072 mDTMFBurstCnfPending = true; 1073 } 1074 } 1075 } 1076 1077 /** 1078 * Handles Burst Dtmf Confirmation from the Framework. 1079 */ 1080 void handleBurstDtmfConfirmation() { 1081 Character dtmfChar = null; 1082 synchronized (mDTMFQueue) { 1083 mDTMFBurstCnfPending = false; 1084 if (!mDTMFQueue.isEmpty()) { 1085 dtmfChar = mDTMFQueue.remove(); 1086 Log.i(LOG_TAG, "The dtmf character removed from queue" + dtmfChar); 1087 } 1088 } 1089 if (dtmfChar != null) { 1090 sendShortDtmfToNetwork(dtmfChar); 1091 } 1092 } 1093 1094 /** 1095 * On GSM devices, we never use short tones. 1096 * On CDMA devices, it depends upon the settings. 1097 */ 1098 private static boolean useShortDtmfTones(Phone phone, Context context) { 1099 int phoneType = phone.getPhoneType(); 1100 if (phoneType == PhoneConstants.PHONE_TYPE_GSM) { 1101 return false; 1102 } else if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) { 1103 int toneType = android.provider.Settings.System.getInt( 1104 context.getContentResolver(), 1105 Settings.System.DTMF_TONE_TYPE_WHEN_DIALING, 1106 Constants.DTMF_TONE_TYPE_NORMAL); 1107 if (toneType == Constants.DTMF_TONE_TYPE_NORMAL) { 1108 return true; 1109 } else { 1110 return false; 1111 } 1112 } else if (phoneType == PhoneConstants.PHONE_TYPE_SIP) { 1113 return false; 1114 } else { 1115 throw new IllegalStateException("Unexpected phone type: " + phoneType); 1116 } 1117 } 1118 1119 } 1120