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