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