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