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