DTMFTwelveKeyDialer.java revision abc47110c17fa8e8cb6161bc045e87f31eeb7a1c
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
17package com.android.phone;
18
19
20import com.android.internal.telephony.Call;
21import com.android.internal.telephony.CallerInfo;
22import com.android.internal.telephony.CallerInfoAsyncQuery;
23import com.android.internal.telephony.Connection;
24import com.android.internal.telephony.Phone;
25import com.android.internal.widget.SlidingDrawer;
26
27import android.media.AudioManager;
28import android.media.ToneGenerator;
29import android.os.Handler;
30import android.os.Message;
31import android.os.SystemClock;
32import android.provider.Settings;
33import android.telephony.PhoneNumberUtils;
34import android.text.Editable;
35import android.text.method.DialerKeyListener;
36import android.util.Log;
37import android.view.KeyEvent;
38import android.view.MotionEvent;
39import android.view.View;
40import android.view.WindowManager;
41import android.widget.Chronometer;
42import android.widget.EditText;
43import android.widget.ImageView;
44import android.widget.LinearLayout;
45import android.widget.TextView;
46
47import java.util.HashMap;
48
49/**
50 * Dialer class that encapsulates the DTMF twelve key behaviour.
51 * This model backs up the UI behaviour in DTMFTwelveKeyDialerView.java.
52 */
53public class DTMFTwelveKeyDialer implements
54        CallerInfoAsyncQuery.OnQueryCompleteListener,
55        SlidingDrawer.OnDrawerOpenListener,
56        SlidingDrawer.OnDrawerCloseListener,
57        View.OnClickListener,
58        View.OnTouchListener,
59        View.OnKeyListener {
60
61    // debug data
62    private static final String LOG_TAG = "dtmf dialer";
63    private static final boolean DBG = false;
64
65    // events
66    private static final int PHONE_DISCONNECT = 100;
67
68    private static Phone mPhone;
69    private ToneGenerator mToneGenerator;
70    private Object mToneGeneratorLock = new Object();
71
72    // indicate if we want to enable the DTMF tone playback.
73    private boolean mDTMFToneEnabled;
74
75    /** Hash Map to map a character to a tone*/
76    private static final HashMap<Character, Integer> mToneMap =
77        new HashMap<Character, Integer>();
78    /** Hash Map to map a view id to a character*/
79    private static final HashMap<Integer, Character> mDisplayMap =
80        new HashMap<Integer, Character>();
81    /** Set up the static maps*/
82    static {
83        // Map the key characters to tones
84        mToneMap.put('1', ToneGenerator.TONE_DTMF_1);
85        mToneMap.put('2', ToneGenerator.TONE_DTMF_2);
86        mToneMap.put('3', ToneGenerator.TONE_DTMF_3);
87        mToneMap.put('4', ToneGenerator.TONE_DTMF_4);
88        mToneMap.put('5', ToneGenerator.TONE_DTMF_5);
89        mToneMap.put('6', ToneGenerator.TONE_DTMF_6);
90        mToneMap.put('7', ToneGenerator.TONE_DTMF_7);
91        mToneMap.put('8', ToneGenerator.TONE_DTMF_8);
92        mToneMap.put('9', ToneGenerator.TONE_DTMF_9);
93        mToneMap.put('0', ToneGenerator.TONE_DTMF_0);
94        mToneMap.put('#', ToneGenerator.TONE_DTMF_P);
95        mToneMap.put('*', ToneGenerator.TONE_DTMF_S);
96
97        // Map the buttons to the display characters
98        mDisplayMap.put(R.id.one, '1');
99        mDisplayMap.put(R.id.two, '2');
100        mDisplayMap.put(R.id.three, '3');
101        mDisplayMap.put(R.id.four, '4');
102        mDisplayMap.put(R.id.five, '5');
103        mDisplayMap.put(R.id.six, '6');
104        mDisplayMap.put(R.id.seven, '7');
105        mDisplayMap.put(R.id.eight, '8');
106        mDisplayMap.put(R.id.nine, '9');
107        mDisplayMap.put(R.id.zero, '0');
108        mDisplayMap.put(R.id.pound, '#');
109        mDisplayMap.put(R.id.star, '*');
110    }
111
112    // UI elements
113    // Including elements used in the call status, now split
114    // between call state and call timer.
115    private EditText mDigits;
116    private LinearLayout mStatusView;
117    private ImageView mStatusCallIcon;
118    private Chronometer mStatusCallTime;
119    private TextView mStatusCallState;
120    private TextView mStatusCallerName;
121
122    // InCallScreen reference.
123    private InCallScreen mInCallScreen;
124
125    // SlidingDrawer reference.
126    private SlidingDrawer mDialerContainer;
127
128    // view reference
129    private DTMFTwelveKeyDialerView mDialerView;
130
131    // key listner reference, may or may not be attached to a view.
132    private DTMFKeyListener mDialerKeyListener;
133
134    /**
135     * Our own key listener, specialized for dealing with DTMF codes.
136     *   1. Ignore the backspace since it is irrelevant.
137     *   2. Allow ONLY valid DTMF characters to generate a tone and be
138     *      sent as a DTMF code.
139     *   3. All other remaining characters are handled by the superclass.
140     */
141    private class DTMFKeyListener extends DialerKeyListener {
142
143        /**
144         * Overriden to return correct DTMF-dialable characters.
145         */
146        @Override
147        protected char[] getAcceptedChars(){
148            return DTMF_CHARACTERS;
149        }
150
151        /** special key listener ignores backspace. */
152        @Override
153        public boolean backspace(View view, Editable content, int keyCode,
154                KeyEvent event) {
155            return false;
156        }
157
158        /**
159         * Overriden so that with each valid button press, we start sending
160         * a dtmf code and play a local dtmf tone.
161         */
162        @Override
163        public boolean onKeyDown(View view, Editable content,
164                                 int keyCode, KeyEvent event) {
165            // find the character
166            char c = (char) lookup(event, content);
167
168            // if not a long press, and parent onKeyDown accepts the input
169            if (event.getRepeatCount() == 0 && super.onKeyDown(view, content, keyCode, event)) {
170
171                // if the character is a valid dtmf code, start playing the tone and send the
172                // code.
173                if (ok(getAcceptedChars(), c)) {
174                    if (DBG) log("DTMFKeyListener reading '" + c + "' from input.");
175                    playTone(c);
176                } else if (DBG) {
177                    log("DTMFKeyListener rejecting '" + c + "' from input.");
178                }
179                return true;
180            }
181            return false;
182        }
183
184        /**
185         * Overriden so that with each valid button up, we stop sending
186         * a dtmf code and the dtmf tone.
187         */
188        @Override
189        public boolean onKeyUp(View view, Editable content,
190                                 int keyCode, KeyEvent event) {
191
192            super.onKeyUp(view, content, keyCode, event);
193
194            // find the character
195            char c = (char) lookup(event, content);
196
197            if (ok(getAcceptedChars(), c)) {
198                if (DBG) log("Stopping the tone for '" + c + "'");
199                stopTone();
200                return true;
201            }
202
203            return false;
204        }
205
206        /**
207         * Handle individual keydown events when we DO NOT have an Editable handy.
208         */
209        public boolean onKeyDown(KeyEvent event) {
210            char c = lookup (event);
211            if (DBG) log("recieved keydown for '" + c + "'");
212
213            // if not a long press, and parent onKeyDown accepts the input
214            if (event.getRepeatCount() == 0 && c != 0) {
215                // if the character is a valid dtmf code, start playing the tone and send the
216                // code.
217                if (ok(getAcceptedChars(), c)) {
218                    if (DBG) log("DTMFKeyListener reading '" + c + "' from input.");
219                    playTone(c);
220                    return true;
221                } else if (DBG) {
222                    log("DTMFKeyListener rejecting '" + c + "' from input.");
223                }
224            }
225            return false;
226        }
227
228        /**
229         * Handle individual keyup events.
230         *
231         * @param event is the event we are trying to stop.  If this is null,
232         * then we just force-stop the last tone without checking if the event
233         * is an acceptable dialer event.
234         */
235        public boolean onKeyUp(KeyEvent event) {
236            if (event == null) {
237                if (DBG) log("Stopping the last played tone.");
238                stopTone();
239                return true;
240            }
241
242            char c = lookup (event);
243            if (DBG) log("recieved keyup for '" + c + "'");
244
245            // TODO: stopTone does not take in character input, we may want to
246            // consider checking for this ourselves.
247            if (ok(getAcceptedChars(), c)) {
248                if (DBG) log("Stopping the tone for '" + c + "'");
249                stopTone();
250                return true;
251            }
252
253            return false;
254        }
255
256        /**
257         * Find the Dialer Key mapped to this event.
258         *
259         * @return The char value of the input event, otherwise
260         * 0 if no matching character was found.
261         */
262        private char lookup (KeyEvent event) {
263            // This code is similar to {@link DialerKeyListener#lookup(KeyEvent, Spannable) lookup}
264            int meta = event.getMetaState();
265            int number = event.getNumber();
266
267            if (!((meta & (KeyEvent.META_ALT_ON | KeyEvent.META_SHIFT_ON)) == 0) || (number == 0)) {
268                int match = event.getMatch(getAcceptedChars(), meta);
269                number = (match != 0) ? match : number;
270            }
271
272            return (char) number;
273        }
274
275        /**
276         * Check to see if the keyEvent is dialable.
277         */
278        boolean isKeyEventAcceptable (KeyEvent event) {
279            return (ok(getAcceptedChars(), lookup(event)));
280        }
281
282        /**
283         * Overrides the characters used in {@link DialerKeyListener#CHARACTERS}
284         * These are the valid dtmf characters.
285         */
286        public final char[] DTMF_CHARACTERS = new char[] {
287            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#', '*'
288        };
289    }
290
291    /**
292     * Our own handler to take care of the messages from the phone state changes
293     */
294    private Handler mHandler = new Handler () {
295        @Override
296        public void handleMessage(Message msg) {
297            switch (msg.what) {
298                // disconnect action
299                // make sure to close the dialer on ALL disconnect actions.
300                case PHONE_DISCONNECT:
301                    if (DBG) log("disconnect message recieved, shutting down.");
302                    // unregister since we are closing.
303                    mPhone.unregisterForDisconnect(this);
304                    closeDialer(false);
305                    break;
306            }
307        }
308    };
309
310
311    DTMFTwelveKeyDialer (InCallScreen parent) {
312        mInCallScreen = parent;
313        mPhone = ((PhoneApp) mInCallScreen.getApplication()).phone;
314        mDialerContainer = (SlidingDrawer) mInCallScreen.findViewById(R.id.dialer_container);
315        mDialerContainer.setOnDrawerOpenListener(this);
316        mDialerContainer.setOnDrawerCloseListener(this);
317        mDialerKeyListener = new DTMFKeyListener();
318    }
319
320    /**
321     * Null out our reference to the InCallScreen activity.
322     * This indicates that the InCallScreen activity has been destroyed.
323     * At the same time, get rid of listeners since we're not going to
324     * be valid anymore.
325     */
326    void clearInCallScreenReference() {
327        mInCallScreen = null;
328        mDialerKeyListener = null;
329        mDialerContainer.setOnDrawerOpenListener(null);
330        mDialerContainer.setOnDrawerCloseListener(null);
331        closeDialer(false);
332    }
333
334    LinearLayout getView() {
335        return mDialerView;
336    }
337
338    /**
339     * Dialer code that runs when the dialer is brought up.
340     * This includes layout changes, etc, and just prepares the dialer model for use.
341     */
342    void onDialerOpen() {
343        if (DBG) log("initMenu()...");
344
345        // inflate the view.
346        mDialerView = (DTMFTwelveKeyDialerView) mInCallScreen.findViewById(R.id.dtmf_dialer);
347        mDialerView.setDialer(this);
348
349        // Have the WindowManager filter out cheek touch events
350        mInCallScreen.getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES);
351
352        // TODO: Need new assets, Hide the voicemail icon.
353        /*
354        View v = mDialerView.findViewById(R.id.oneVoicemailIcon);
355        if (v != null) {
356            v.setVisibility(View.GONE);
357        }
358        */
359
360        mPhone.registerForDisconnect(mHandler, PHONE_DISCONNECT, null);
361
362        // set to a longer delay while the dialer is up.
363        mInCallScreen.updateWakeState();
364
365        // setup the digit display
366        mDigits = (EditText) mDialerView.findViewById(R.id.digits);
367        mDigits.setKeyListener(new DTMFKeyListener());
368        mDigits.requestFocus();
369
370        // remove the long-press context menus that support
371        // the edit (copy / paste / select) functions.
372        mDigits.setLongClickable(false);
373
374        // setup the status view
375        mStatusView = (LinearLayout) mDialerView.findViewById(R.id.status_view);
376        mStatusView.setOnClickListener(this);
377
378        // get a handle to the relevant UI widgets
379        mStatusCallIcon = (ImageView) mDialerView.findViewById(R.id.status_call_icon);
380        mStatusCallTime = (Chronometer) mDialerView.findViewById(R.id.status_call_time);
381        mStatusCallState = (TextView) mDialerView.findViewById(R.id.status_call_state);
382        mStatusCallerName = (TextView) mDialerView.findViewById(R.id.status_caller_name);
383
384        // Check for the presence of the keypad (portrait mode)
385        View view = mDialerView.findViewById(R.id.one);
386        if (view != null) {
387            if (DBG) log("portrait mode setup");
388            setupKeypad();
389        } else {
390            if (DBG) log("landscape mode setup");
391            // Adding hint text to the field to indicate that keyboard
392            // is needed while in landscape mode.
393            mDigits.setHint(R.string.dialerKeyboardHintText);
394        }
395
396        // update the status screen
397        updateStatus();
398
399        // setup the local tone generator.
400        startDialerSession();
401    }
402
403    /**
404     * Setup the local tone generator.  Should have corresponding calls to
405     * {@link onDialerPause}.
406     */
407    public void startDialerSession() {
408        // see if we need to play local tones.
409        mDTMFToneEnabled = Settings.System.getInt(mInCallScreen.getContentResolver(),
410                Settings.System.DTMF_TONE_WHEN_DIALING, 1) == 1;
411
412        // create the tone generator
413        // if the mToneGenerator creation fails, just continue without it.  It is
414        // a local audio signal, and is not as important as the dtmf tone itself.
415        if (mDTMFToneEnabled) {
416            synchronized (mToneGeneratorLock) {
417                if (mToneGenerator == null) {
418                    try {
419                        mToneGenerator = new ToneGenerator(AudioManager.STREAM_RING, 80);
420                    } catch (RuntimeException e) {
421                        if (DBG) log("Exception caught while creating local tone generator: " + e);
422                        mToneGenerator = null;
423                    }
424                }
425            }
426        }
427    }
428
429    /**
430     * Dialer code that runs when the dialer is closed.
431     * This releases resources acquired when we start the dialer.
432     */
433    public void onDialerClose() {
434        // reset back to a short delay for the poke lock.
435        mInCallScreen.updateWakeState();
436
437        mPhone.unregisterForDisconnect(mHandler);
438
439        if (mStatusCallTime != null) {
440            mStatusCallTime.stop();
441        }
442
443        stopDialerSession();
444    }
445
446    /**
447     * Tear down the local tone generator, corresponds to calls to
448     * {@link onDialerResume}
449     */
450    public void stopDialerSession() {
451        // release the tone generator.
452        synchronized (mToneGeneratorLock) {
453            if (mToneGenerator != null) {
454                mToneGenerator.release();
455                mToneGenerator = null;
456            }
457        }
458    }
459
460    /**
461     * update the status display, code taken mostly from
462     * NotificationMgr.updateInCallNotification
463     */
464    private void updateStatus () {
465        if (DBG) log("updating status display");
466
467        // get statuses
468        final boolean hasActiveCall = !mPhone.getForegroundCall().isIdle();
469        final boolean hasHoldingCall = !mPhone.getBackgroundCall().isIdle();
470
471        // figure out which icon to display
472        int resId = (!hasActiveCall && hasHoldingCall) ?
473                android.R.drawable.stat_sys_phone_call_on_hold :
474                android.R.drawable.stat_sys_phone_call;
475
476        // get the current connected call.
477        Call currentCall = hasActiveCall ? mPhone.getForegroundCall()
478                : mPhone.getBackgroundCall();
479        Connection currentConn = currentCall.getEarliestConnection();
480
481        // update the information about the current connection (chronometer)
482        // only if the current connection exists.
483        if (currentConn != null) {
484            // figure out the elapsed time
485            long callDurationMsec = currentConn.getDurationMillis();
486            long chronometerBaseTime = SystemClock.elapsedRealtime() - callDurationMsec;
487
488            // figure out the call status to display.
489            String statusInfo;
490            if (hasHoldingCall && !hasActiveCall) {
491                statusInfo = mInCallScreen.getString(R.string.onHold);
492
493                // hide the timer while on hold.
494                mStatusCallTime.setVisibility(View.INVISIBLE);
495            } else {
496                statusInfo = mInCallScreen.getString(R.string.ongoing);
497
498                // setup and start the status timer.
499                mStatusCallTime.setVisibility(View.VISIBLE);
500                mStatusCallTime.setBase(chronometerBaseTime);
501                mStatusCallTime.start();
502            }
503
504            // display the call state
505            mStatusCallState.setText(statusInfo);
506
507        } else if (DBG) {
508            log("updateStatus: connection is null, call display not updated.");
509        }
510
511        // this code is independent of the chronometer code; used to
512        // display the caller name.
513        // TODO: it may not make sense for every point to make separate
514        // checks for isConferenceCall, so we need to think about
515        // possibly including this in startGetCallerInfo or some other
516        // common point.
517        if (PhoneUtils.isConferenceCall(currentCall)) {
518            // if this is a conference call, just use that as the caller name.
519            mStatusCallerName.setText(R.string.card_title_conf_call);
520        } else {
521            // get the caller name.
522            PhoneUtils.CallerInfoToken cit =
523                    PhoneUtils.startGetCallerInfo(mInCallScreen, currentCall, this,
524                            mStatusCallerName);
525            mStatusCallerName.setText(
526                    PhoneUtils.getCompactNameFromCallerInfo(cit.currentInfo, mInCallScreen));
527        }
528
529        // set the icon
530        mStatusCallIcon.setImageResource(resId);
531    }
532
533    /**
534     * upon completion of the query, update the name field in the status.
535     */
536    public void onQueryComplete(int token, Object cookie, CallerInfo ci){
537        if (DBG) log("callerinfo query complete, updating ui.");
538
539        ((TextView) cookie).setText(PhoneUtils.getCompactNameFromCallerInfo(ci, mInCallScreen));
540    }
541
542    /**
543     * Called externally (from InCallScreen) to play a DTMF Tone.
544     */
545    public boolean onDialerKeyDown(KeyEvent event) {
546        if (DBG) log("Notifying dtmf key down.");
547        return mDialerKeyListener.onKeyDown(event);
548    }
549
550    /**
551     * Called externally (from InCallScreen) to cancel the last DTMF Tone played.
552     */
553    public boolean onDialerKeyUp(KeyEvent event) {
554        if (DBG) log("Notifying dtmf key up.");
555        return mDialerKeyListener.onKeyUp(event);
556    }
557
558    /**
559     * setup the keys on the dialer activity, using the keymaps.
560     */
561    private void setupKeypad() {
562        // for each view id listed in the displaymap
563        View button;
564        for (int viewId : mDisplayMap.keySet()) {
565            // locate the view
566            button = mDialerView.findViewById(viewId);
567            // Setup the listeners for the buttons
568            button.setOnTouchListener(this);
569            button.setClickable(true);
570            button.setOnKeyListener(this);
571        }
572    }
573
574    /**
575     * catch the back and call buttons to return to the in call activity.
576     */
577    public boolean onKeyDown(int keyCode, KeyEvent event) {
578        switch (keyCode) {
579            // finish for these events
580            case KeyEvent.KEYCODE_BACK:
581            case KeyEvent.KEYCODE_CALL:
582                if (DBG) log("exit requested");
583                closeDialer(true);  // do the "closing" animation
584                return true;
585        }
586        return mInCallScreen.onKeyDown(keyCode, event);
587    }
588
589    /**
590     * catch the back and call buttons to return to the in call activity.
591     */
592    public boolean onKeyUp(int keyCode, KeyEvent event) {
593        return mInCallScreen.onKeyUp(keyCode, event);
594    }
595
596    /**
597     * for clicklistener, process the incoming button presses.
598     */
599    public void onClick(View view) {
600        if (view == mStatusView) {
601            if (DBG) log("exit requested from status view");
602            closeDialer(true);  // do the "closing" animation
603        }
604    }
605
606    /**
607     * Implemented for the TouchListener, process the touch events.
608     */
609    public boolean onTouch(View v, MotionEvent event) {
610        int viewId = v.getId();
611
612        // if the button is recognized
613        if (mDisplayMap.containsKey(viewId)) {
614            switch (event.getAction()) {
615                case MotionEvent.ACTION_DOWN:
616                    // Append the character mapped to this button, to the display.
617                    // start the tone
618                    appendDigit(mDisplayMap.get(viewId));
619                    break;
620                case MotionEvent.ACTION_UP:
621                case MotionEvent.ACTION_CANCEL:
622                    // stop the tone on ANY other event, except for MOVE.
623                    stopTone();
624                    break;
625            }
626            // do not return true [handled] here, since we want the
627            // press / click animation to be handled by the framework.
628        }
629        return false;
630    }
631
632    /**
633     * Implements View.OnKeyListener for the DTMF buttons.  Enables dialing with trackball/dpad.
634     */
635    public boolean onKey(View v, int keyCode, KeyEvent event) {
636        if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
637            int viewId = v.getId();
638            if (mDisplayMap.containsKey(viewId)) {
639                switch (event.getAction()) {
640                case KeyEvent.ACTION_DOWN:
641                    if (event.getRepeatCount() == 0) {
642                        appendDigit(mDisplayMap.get(viewId));
643                    }
644                    break;
645                case KeyEvent.ACTION_UP:
646                    stopTone();
647                    break;
648                }
649                // do not return true [handled] here, since we want the
650                // press / click animation to be handled by the framework.
651            }
652        }
653        return false;
654    }
655
656    /**
657     * @return true if the dialer is currently opened (i.e. expanded).
658     */
659    public boolean isOpened() {
660        return mDialerContainer.isOpened();
661    }
662
663    /**
664     * Forces the dialer into the "open" state.
665     * Does nothing if the dialer is already open.
666     *
667     * @param animate if true, open the dialer with an animation.
668     */
669    public void openDialer(boolean animate) {
670        if (!mDialerContainer.isOpened()) {
671            if (animate) {
672                mDialerContainer.animateToggle();
673            } else {
674                mDialerContainer.toggle();
675            }
676        }
677    }
678
679    /**
680     * Forces the dialer into the "closed" state.
681     * Does nothing if the dialer is already closed.
682     *
683     * @param animate if true, close the dialer with an animation.
684     */
685    public void closeDialer(boolean animate) {
686        if (mDialerContainer.isOpened()) {
687            if (animate) {
688                mDialerContainer.animateToggle();
689            } else {
690                mDialerContainer.toggle();
691            }
692        }
693    }
694
695    /**
696     * Implemented for the SlidingDrawer open listener, prepare the dialer.
697     */
698    public void onDrawerOpened() {
699        onDialerOpen();
700    }
701
702    /**
703     * Implemented for the SlidingDrawer close listener, release the dialer.
704     */
705    public void onDrawerClosed() {
706        onDialerClose();
707    }
708
709    /**
710     * update the text area and playback the tone.
711     */
712    private final void appendDigit(char c) {
713        // if it is a valid key, then update the display and send the dtmf tone.
714        if (PhoneNumberUtils.is12Key(c)) {
715            if (DBG) log("updating display and sending dtmf tone for '" + c + "'");
716
717            if (mDigits != null) {
718                mDigits.getText().append(c);
719            }
720            // play the tone if it exists.
721            if (mToneMap.containsKey(c)) {
722                // begin tone playback.
723                playTone(c);
724            }
725        } else if (DBG) {
726            log("ignoring dtmf request for '" + c + "'");
727        }
728
729    }
730
731    /**
732     * Start playing a DTMF tone, also begin the local tone playback if it is
733     * enabled.
734     *
735     * @param tone a tone code from {@link ToneGenerator}
736     */
737    void playTone(char tone) {
738        if (DBG) log("starting remote tone.");
739        PhoneApp.getInstance().phone.startDtmf(tone);
740
741        // if local tone playback is enabled, start it.
742        if (mDTMFToneEnabled) {
743            synchronized (mToneGeneratorLock) {
744                if (mToneGenerator == null) {
745                    if (DBG) log("playTone: mToneGenerator == null, tone: " + tone);
746                } else {
747                    if (DBG) log("starting local tone " + tone);
748                    mToneGenerator.startTone(mToneMap.get(tone));
749                }
750            }
751        }
752    }
753
754    /**
755     * Stop playing the current DTMF tone.
756     *
757     * The ToneStopper class (similar to that in {@link TwelveKeyDialer#mToneStopper})
758     * has been removed in favor of synchronous start / stop calls since tone duration
759     * is now a function of the input.
760     */
761    void stopTone() {
762        if (DBG) log("stopping remote tone.");
763        PhoneApp.getInstance().phone.stopDtmf();
764
765        // if local tone playback is enabled, stop it.
766        if (DBG) log("trying to stop local tone...");
767        if (mDTMFToneEnabled) {
768            synchronized (mToneGeneratorLock) {
769                if (mToneGenerator == null) {
770                    if (DBG) log("stopTone: mToneGenerator == null");
771                } else {
772                    if (DBG) log("stopping local tone.");
773                    mToneGenerator.stopTone();
774                }
775            }
776        }
777    }
778
779    /**
780     * Check to see if the keyEvent is dialable.
781     */
782    boolean isKeyEventAcceptable (KeyEvent event) {
783        return (mDialerKeyListener != null && mDialerKeyListener.isKeyEventAcceptable(event));
784    }
785
786    /**
787     * static logging method
788     */
789    private static void log(String msg) {
790        Log.d(LOG_TAG, msg);
791    }
792}
793