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