Blanket copy of PhoneApp to services/Telephony.

First phase of splitting out InCallUI from PhoneApp.

Change-Id: I237341c4ff00e96c677caa4580b251ef3432931b
diff --git a/src/com/android/phone/DTMFTwelveKeyDialer.java b/src/com/android/phone/DTMFTwelveKeyDialer.java
new file mode 100644
index 0000000..4afac55
--- /dev/null
+++ b/src/com/android/phone/DTMFTwelveKeyDialer.java
@@ -0,0 +1,1119 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.phone;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.ToneGenerator;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.Settings;
+import android.telephony.PhoneNumberUtils;
+import android.text.Editable;
+import android.text.SpannableString;
+import android.text.method.DialerKeyListener;
+import android.text.style.RelativeSizeSpan;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.View.OnHoverListener;
+import android.view.accessibility.AccessibilityManager;
+import android.view.ViewStub;
+import android.widget.EditText;
+
+import com.android.internal.telephony.CallManager;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.TelephonyCapabilities;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Queue;
+
+
+/**
+ * Dialer class that encapsulates the DTMF twelve key behaviour.
+ * This model backs up the UI behaviour in DTMFTwelveKeyDialerView.java.
+ */
+public class DTMFTwelveKeyDialer implements View.OnTouchListener, View.OnKeyListener,
+        View.OnHoverListener, View.OnClickListener {
+    private static final String LOG_TAG = "DTMFTwelveKeyDialer";
+    private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
+
+    // events
+    private static final int PHONE_DISCONNECT = 100;
+    private static final int DTMF_SEND_CNF = 101;
+    private static final int DTMF_STOP = 102;
+
+    /** Accessibility manager instance used to check touch exploration state. */
+    private final AccessibilityManager mAccessibilityManager;
+
+    private CallManager mCM;
+    private ToneGenerator mToneGenerator;
+    private final Object mToneGeneratorLock = new Object();
+
+    // indicate if we want to enable the local tone playback.
+    private boolean mLocalToneEnabled;
+
+    // indicates that we are using automatically shortened DTMF tones
+    boolean mShortTone;
+
+    // indicate if the confirmation from TelephonyFW is pending.
+    private boolean mDTMFBurstCnfPending = false;
+
+    // Queue to queue the short dtmf characters.
+    private Queue<Character> mDTMFQueue = new LinkedList<Character>();
+
+    //  Short Dtmf tone duration
+    private static final int DTMF_DURATION_MS = 120;
+
+
+    /** Hash Map to map a character to a tone*/
+    private static final HashMap<Character, Integer> mToneMap =
+        new HashMap<Character, Integer>();
+    /** Hash Map to map a view id to a character*/
+    private static final HashMap<Integer, Character> mDisplayMap =
+        new HashMap<Integer, Character>();
+    /** Set up the static maps*/
+    static {
+        // Map the key characters to tones
+        mToneMap.put('1', ToneGenerator.TONE_DTMF_1);
+        mToneMap.put('2', ToneGenerator.TONE_DTMF_2);
+        mToneMap.put('3', ToneGenerator.TONE_DTMF_3);
+        mToneMap.put('4', ToneGenerator.TONE_DTMF_4);
+        mToneMap.put('5', ToneGenerator.TONE_DTMF_5);
+        mToneMap.put('6', ToneGenerator.TONE_DTMF_6);
+        mToneMap.put('7', ToneGenerator.TONE_DTMF_7);
+        mToneMap.put('8', ToneGenerator.TONE_DTMF_8);
+        mToneMap.put('9', ToneGenerator.TONE_DTMF_9);
+        mToneMap.put('0', ToneGenerator.TONE_DTMF_0);
+        mToneMap.put('#', ToneGenerator.TONE_DTMF_P);
+        mToneMap.put('*', ToneGenerator.TONE_DTMF_S);
+
+        // Map the buttons to the display characters
+        mDisplayMap.put(R.id.one, '1');
+        mDisplayMap.put(R.id.two, '2');
+        mDisplayMap.put(R.id.three, '3');
+        mDisplayMap.put(R.id.four, '4');
+        mDisplayMap.put(R.id.five, '5');
+        mDisplayMap.put(R.id.six, '6');
+        mDisplayMap.put(R.id.seven, '7');
+        mDisplayMap.put(R.id.eight, '8');
+        mDisplayMap.put(R.id.nine, '9');
+        mDisplayMap.put(R.id.zero, '0');
+        mDisplayMap.put(R.id.pound, '#');
+        mDisplayMap.put(R.id.star, '*');
+    }
+
+    /** EditText field used to display the DTMF digits sent so far.
+        Note this is null in some modes (like during the CDMA OTA call,
+        where there's no onscreen "digits" display.) */
+    private EditText mDialpadDigits;
+
+    // InCallScreen reference.
+    private InCallScreen mInCallScreen;
+
+    /**
+     * The DTMFTwelveKeyDialerView we use to display the dialpad.
+     *
+     * Only one of mDialerView or mDialerStub will have a legitimate object; the other one will be
+     * null at that moment. Either of following scenarios will occur:
+     *
+     * - If the constructor with {@link DTMFTwelveKeyDialerView} is called, mDialerView will
+     *   obtain that object, and mDialerStub will be null. mDialerStub won't be used in this case.
+     *
+     * - If the constructor with {@link ViewStub} is called, mDialerView will be null at that
+     *   moment, and mDialerStub will obtain the ViewStub object.
+     *   When the dialer is required by the user (i.e. until {@link #openDialer(boolean)} being
+     *   called), mDialerStub will inflate the dialer, and make mDialerStub itself null.
+     *   mDialerStub won't be used afterward.
+     */
+    private DTMFTwelveKeyDialerView mDialerView;
+
+    /**
+     * {@link ViewStub} holding {@link DTMFTwelveKeyDialerView}. See the comments for mDialerView.
+     */
+    private ViewStub mDialerStub;
+
+    // KeyListener used with the "dialpad digits" EditText widget.
+    private DTMFKeyListener mDialerKeyListener;
+
+    /**
+     * Our own key listener, specialized for dealing with DTMF codes.
+     *   1. Ignore the backspace since it is irrelevant.
+     *   2. Allow ONLY valid DTMF characters to generate a tone and be
+     *      sent as a DTMF code.
+     *   3. All other remaining characters are handled by the superclass.
+     *
+     * This code is purely here to handle events from the hardware keyboard
+     * while the DTMF dialpad is up.
+     */
+    private class DTMFKeyListener extends DialerKeyListener {
+
+        private DTMFKeyListener() {
+            super();
+        }
+
+        /**
+         * Overriden to return correct DTMF-dialable characters.
+         */
+        @Override
+        protected char[] getAcceptedChars(){
+            return DTMF_CHARACTERS;
+        }
+
+        /** special key listener ignores backspace. */
+        @Override
+        public boolean backspace(View view, Editable content, int keyCode,
+                KeyEvent event) {
+            return false;
+        }
+
+        /**
+         * Return true if the keyCode is an accepted modifier key for the
+         * dialer (ALT or SHIFT).
+         */
+        private boolean isAcceptableModifierKey(int keyCode) {
+            switch (keyCode) {
+                case KeyEvent.KEYCODE_ALT_LEFT:
+                case KeyEvent.KEYCODE_ALT_RIGHT:
+                case KeyEvent.KEYCODE_SHIFT_LEFT:
+                case KeyEvent.KEYCODE_SHIFT_RIGHT:
+                    return true;
+                default:
+                    return false;
+            }
+        }
+
+        /**
+         * Overriden so that with each valid button press, we start sending
+         * a dtmf code and play a local dtmf tone.
+         */
+        @Override
+        public boolean onKeyDown(View view, Editable content,
+                                 int keyCode, KeyEvent event) {
+            // if (DBG) log("DTMFKeyListener.onKeyDown, keyCode " + keyCode + ", view " + view);
+
+            // find the character
+            char c = (char) lookup(event, content);
+
+            // if not a long press, and parent onKeyDown accepts the input
+            if (event.getRepeatCount() == 0 && super.onKeyDown(view, content, keyCode, event)) {
+
+                boolean keyOK = ok(getAcceptedChars(), c);
+
+                // if the character is a valid dtmf code, start playing the tone and send the
+                // code.
+                if (keyOK) {
+                    if (DBG) log("DTMFKeyListener reading '" + c + "' from input.");
+                    processDtmf(c);
+                } else if (DBG) {
+                    log("DTMFKeyListener rejecting '" + c + "' from input.");
+                }
+                return true;
+            }
+            return false;
+        }
+
+        /**
+         * Overriden so that with each valid button up, we stop sending
+         * a dtmf code and the dtmf tone.
+         */
+        @Override
+        public boolean onKeyUp(View view, Editable content,
+                                 int keyCode, KeyEvent event) {
+            // if (DBG) log("DTMFKeyListener.onKeyUp, keyCode " + keyCode + ", view " + view);
+
+            super.onKeyUp(view, content, keyCode, event);
+
+            // find the character
+            char c = (char) lookup(event, content);
+
+            boolean keyOK = ok(getAcceptedChars(), c);
+
+            if (keyOK) {
+                if (DBG) log("Stopping the tone for '" + c + "'");
+                stopTone();
+                return true;
+            }
+
+            return false;
+        }
+
+        /**
+         * Handle individual keydown events when we DO NOT have an Editable handy.
+         */
+        public boolean onKeyDown(KeyEvent event) {
+            char c = lookup(event);
+            if (DBG) log("DTMFKeyListener.onKeyDown: event '" + c + "'");
+
+            // if not a long press, and parent onKeyDown accepts the input
+            if (event.getRepeatCount() == 0 && c != 0) {
+                // if the character is a valid dtmf code, start playing the tone and send the
+                // code.
+                if (ok(getAcceptedChars(), c)) {
+                    if (DBG) log("DTMFKeyListener reading '" + c + "' from input.");
+                    processDtmf(c);
+                    return true;
+                } else if (DBG) {
+                    log("DTMFKeyListener rejecting '" + c + "' from input.");
+                }
+            }
+            return false;
+        }
+
+        /**
+         * Handle individual keyup events.
+         *
+         * @param event is the event we are trying to stop.  If this is null,
+         * then we just force-stop the last tone without checking if the event
+         * is an acceptable dialer event.
+         */
+        public boolean onKeyUp(KeyEvent event) {
+            if (event == null) {
+                //the below piece of code sends stopDTMF event unnecessarily even when a null event
+                //is received, hence commenting it.
+                /*if (DBG) log("Stopping the last played tone.");
+                stopTone();*/
+                return true;
+            }
+
+            char c = lookup(event);
+            if (DBG) log("DTMFKeyListener.onKeyUp: event '" + c + "'");
+
+            // TODO: stopTone does not take in character input, we may want to
+            // consider checking for this ourselves.
+            if (ok(getAcceptedChars(), c)) {
+                if (DBG) log("Stopping the tone for '" + c + "'");
+                stopTone();
+                return true;
+            }
+
+            return false;
+        }
+
+        /**
+         * Find the Dialer Key mapped to this event.
+         *
+         * @return The char value of the input event, otherwise
+         * 0 if no matching character was found.
+         */
+        private char lookup(KeyEvent event) {
+            // This code is similar to {@link DialerKeyListener#lookup(KeyEvent, Spannable) lookup}
+            int meta = event.getMetaState();
+            int number = event.getNumber();
+
+            if (!((meta & (KeyEvent.META_ALT_ON | KeyEvent.META_SHIFT_ON)) == 0) || (number == 0)) {
+                int match = event.getMatch(getAcceptedChars(), meta);
+                number = (match != 0) ? match : number;
+            }
+
+            return (char) number;
+        }
+
+        /**
+         * Check to see if the keyEvent is dialable.
+         */
+        boolean isKeyEventAcceptable (KeyEvent event) {
+            return (ok(getAcceptedChars(), lookup(event)));
+        }
+
+        /**
+         * Overrides the characters used in {@link DialerKeyListener#CHARACTERS}
+         * These are the valid dtmf characters.
+         */
+        public final char[] DTMF_CHARACTERS = new char[] {
+            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#', '*'
+        };
+    }
+
+    /**
+     * Our own handler to take care of the messages from the phone state changes
+     */
+    private final Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                // disconnect action
+                // make sure to close the dialer on ALL disconnect actions.
+                case PHONE_DISCONNECT:
+                    if (DBG) log("disconnect message recieved, shutting down.");
+                    // unregister since we are closing.
+                    mCM.unregisterForDisconnect(this);
+                    closeDialer(false);
+                    break;
+                case DTMF_SEND_CNF:
+                    if (DBG) log("dtmf confirmation received from FW.");
+                    // handle burst dtmf confirmation
+                    handleBurstDtmfConfirmation();
+                    break;
+                case DTMF_STOP:
+                    if (DBG) log("dtmf stop received");
+                    stopTone();
+                    break;
+            }
+        }
+    };
+
+
+    /**
+     * DTMFTwelveKeyDialer constructor with {@link DTMFTwelveKeyDialerView}
+     *
+     * @param parent the InCallScreen instance that owns us.
+     * @param dialerView the DTMFTwelveKeyDialerView we should use to display the dialpad.
+     */
+    public DTMFTwelveKeyDialer(InCallScreen parent,
+                                DTMFTwelveKeyDialerView dialerView) {
+        this(parent);
+
+        // The passed-in DTMFTwelveKeyDialerView *should* always be
+        // non-null, now that the in-call UI uses only portrait mode.
+        if (dialerView == null) {
+            Log.e(LOG_TAG, "DTMFTwelveKeyDialer: null dialerView!", new IllegalStateException());
+            // ...continue as best we can, although things will
+            // be pretty broken without the mDialerView UI elements!
+        }
+        mDialerView = dialerView;
+        if (DBG) log("- Got passed-in mDialerView: " + mDialerView);
+
+        if (mDialerView != null) {
+            setupDialerView();
+        }
+    }
+
+    /**
+     * DTMFTwelveKeyDialer constructor with {@link ViewStub}.
+     *
+     * When the dialer is required for the first time (e.g. when {@link #openDialer(boolean)} is
+     * called), the object will inflate the ViewStub by itself, assuming the ViewStub will return
+     * {@link DTMFTwelveKeyDialerView} on {@link ViewStub#inflate()}.
+     *
+     * @param parent the InCallScreen instance that owns us.
+     * @param dialerStub ViewStub which will return {@link DTMFTwelveKeyDialerView} on
+     * {@link ViewStub#inflate()}.
+     */
+    public DTMFTwelveKeyDialer(InCallScreen parent, ViewStub dialerStub) {
+        this(parent);
+
+        mDialerStub = dialerStub;
+        if (DBG) log("- Got passed-in mDialerStub: " + mDialerStub);
+
+        // At this moment mDialerView is still null. We delay calling setupDialerView().
+    }
+
+    /**
+     * Private constructor used for initialization calls common to all public
+     * constructors.
+     *
+     * @param parent the InCallScreen instance that owns us.
+     */
+    private DTMFTwelveKeyDialer(InCallScreen parent) {
+        if (DBG) log("DTMFTwelveKeyDialer constructor... this = " + this);
+
+        mInCallScreen = parent;
+        mCM = PhoneGlobals.getInstance().mCM;
+        mAccessibilityManager = (AccessibilityManager) parent.getSystemService(
+                Context.ACCESSIBILITY_SERVICE);
+    }
+
+    /**
+     * Prepare the dialer view and relevant variables.
+     */
+    private void setupDialerView() {
+        if (DBG) log("setupDialerView()");
+        mDialerView.setDialer(this);
+
+        // In the normal in-call DTMF dialpad, mDialpadDigits is an
+        // EditText used to display the digits the user has typed so
+        // far.  But some other modes (like the OTA call) have no
+        // "digits" display at all, in which case mDialpadDigits will
+        // be null.
+        mDialpadDigits = (EditText) mDialerView.findViewById(R.id.dtmfDialerField);
+        if (mDialpadDigits != null) {
+            mDialerKeyListener = new DTMFKeyListener();
+            mDialpadDigits.setKeyListener(mDialerKeyListener);
+
+            // remove the long-press context menus that support
+            // the edit (copy / paste / select) functions.
+            mDialpadDigits.setLongClickable(false);
+        }
+
+        // Hook up touch / key listeners for the buttons in the onscreen
+        // keypad.
+        setupKeypad(mDialerView);
+    }
+
+    /**
+     * Null out our reference to the InCallScreen activity.
+     * This indicates that the InCallScreen activity has been destroyed.
+     * At the same time, get rid of listeners since we're not going to
+     * be valid anymore.
+     */
+    /* package */ void clearInCallScreenReference() {
+        if (DBG) log("clearInCallScreenReference()...");
+        mInCallScreen = null;
+        mDialerKeyListener = null;
+        mHandler.removeMessages(DTMF_SEND_CNF);
+        synchronized (mDTMFQueue) {
+            mDTMFBurstCnfPending = false;
+            mDTMFQueue.clear();
+        }
+        closeDialer(false);
+    }
+
+    /**
+     * Dialer code that runs when the dialer is brought up.
+     * This includes layout changes, etc, and just prepares the dialer model for use.
+     */
+    private void onDialerOpen(boolean animate) {
+        if (DBG) log("onDialerOpen()...");
+
+        // Any time the dialer is open, listen for "disconnect" events (so
+        // we can close ourself.)
+        mCM.registerForDisconnect(mHandler, PHONE_DISCONNECT, null);
+
+        // On some devices the screen timeout is set to a special value
+        // while the dialpad is up.
+        PhoneGlobals.getInstance().updateWakeState();
+
+        // Give the InCallScreen a chance to do any necessary UI updates.
+        if (mInCallScreen != null) {
+            mInCallScreen.onDialerOpen(animate);
+        } else {
+            Log.e(LOG_TAG, "InCallScreen object was null during onDialerOpen()");
+        }
+    }
+
+    /**
+     * Allocates some resources we keep around during a "dialer session".
+     *
+     * (Currently, a "dialer session" just means any situation where we
+     * might need to play local DTMF tones, which means that we need to
+     * keep a ToneGenerator instance around.  A ToneGenerator instance
+     * keeps an AudioTrack resource busy in AudioFlinger, so we don't want
+     * to keep it around forever.)
+     *
+     * Call {@link stopDialerSession} to release the dialer session
+     * resources.
+     */
+    public void startDialerSession() {
+        if (DBG) log("startDialerSession()... this = " + this);
+
+        // see if we need to play local tones.
+        if (PhoneGlobals.getInstance().getResources().getBoolean(R.bool.allow_local_dtmf_tones)) {
+            mLocalToneEnabled = Settings.System.getInt(mInCallScreen.getContentResolver(),
+                    Settings.System.DTMF_TONE_WHEN_DIALING, 1) == 1;
+        } else {
+            mLocalToneEnabled = false;
+        }
+        if (DBG) log("- startDialerSession: mLocalToneEnabled = " + mLocalToneEnabled);
+
+        // create the tone generator
+        // if the mToneGenerator creation fails, just continue without it.  It is
+        // a local audio signal, and is not as important as the dtmf tone itself.
+        if (mLocalToneEnabled) {
+            synchronized (mToneGeneratorLock) {
+                if (mToneGenerator == null) {
+                    try {
+                        mToneGenerator = new ToneGenerator(AudioManager.STREAM_DTMF, 80);
+                    } catch (RuntimeException e) {
+                        if (DBG) log("Exception caught while creating local tone generator: " + e);
+                        mToneGenerator = null;
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Dialer code that runs when the dialer is closed.
+     * This releases resources acquired when we start the dialer.
+     */
+    private void onDialerClose(boolean animate) {
+        if (DBG) log("onDialerClose()...");
+
+        // reset back to a short delay for the poke lock.
+        PhoneGlobals app = PhoneGlobals.getInstance();
+        app.updateWakeState();
+
+        mCM.unregisterForDisconnect(mHandler);
+
+        // Give the InCallScreen a chance to do any necessary UI updates.
+        if (mInCallScreen != null) {
+            mInCallScreen.onDialerClose(animate);
+        } else {
+            Log.e(LOG_TAG, "InCallScreen object was null during onDialerClose()");
+        }
+    }
+
+    /**
+     * Releases resources we keep around during a "dialer session"
+     * (see {@link startDialerSession}).
+     *
+     * It's safe to call this even without a corresponding
+     * startDialerSession call.
+     */
+    public void stopDialerSession() {
+        // release the tone generator.
+        synchronized (mToneGeneratorLock) {
+            if (mToneGenerator != null) {
+                mToneGenerator.release();
+                mToneGenerator = null;
+            }
+        }
+    }
+
+    /**
+     * Called externally (from InCallScreen) to play a DTMF Tone.
+     */
+    public boolean onDialerKeyDown(KeyEvent event) {
+        if (DBG) log("Notifying dtmf key down.");
+        if (mDialerKeyListener != null) {
+            return mDialerKeyListener.onKeyDown(event);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Called externally (from InCallScreen) to cancel the last DTMF Tone played.
+     */
+    public boolean onDialerKeyUp(KeyEvent event) {
+        if (DBG) log("Notifying dtmf key up.");
+        if (mDialerKeyListener != null) {
+            return mDialerKeyListener.onKeyUp(event);
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * setup the keys on the dialer activity, using the keymaps.
+     */
+    private void setupKeypad(DTMFTwelveKeyDialerView dialerView) {
+        // for each view id listed in the displaymap
+        View button;
+        for (int viewId : mDisplayMap.keySet()) {
+            // locate the view
+            button = dialerView.findViewById(viewId);
+            // Setup the listeners for the buttons
+            button.setOnTouchListener(this);
+            button.setClickable(true);
+            button.setOnKeyListener(this);
+            button.setOnHoverListener(this);
+            button.setOnClickListener(this);
+        }
+    }
+
+    /**
+     * catch the back and call buttons to return to the in call activity.
+     */
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        // if (DBG) log("onKeyDown:  keyCode " + keyCode);
+        switch (keyCode) {
+            // finish for these events
+            case KeyEvent.KEYCODE_BACK:
+            case KeyEvent.KEYCODE_CALL:
+                if (DBG) log("exit requested");
+                closeDialer(true);  // do the "closing" animation
+                return true;
+        }
+        return mInCallScreen.onKeyDown(keyCode, event);
+    }
+
+    /**
+     * catch the back and call buttons to return to the in call activity.
+     */
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        // if (DBG) log("onKeyUp:  keyCode " + keyCode);
+        return mInCallScreen.onKeyUp(keyCode, event);
+    }
+
+    /**
+     * Implemented for {@link android.view.View.OnHoverListener}. Handles touch
+     * events for accessibility when touch exploration is enabled.
+     */
+    @Override
+    public boolean onHover(View v, MotionEvent event) {
+        // When touch exploration is turned on, lifting a finger while inside
+        // the button's hover target bounds should perform a click action.
+        if (mAccessibilityManager.isEnabled()
+                && mAccessibilityManager.isTouchExplorationEnabled()) {
+            final int left = v.getPaddingLeft();
+            final int right = (v.getWidth() - v.getPaddingRight());
+            final int top = v.getPaddingTop();
+            final int bottom = (v.getHeight() - v.getPaddingBottom());
+
+            switch (event.getActionMasked()) {
+                case MotionEvent.ACTION_HOVER_ENTER:
+                    // Lift-to-type temporarily disables double-tap activation.
+                    v.setClickable(false);
+                    break;
+                case MotionEvent.ACTION_HOVER_EXIT:
+                    final int x = (int) event.getX();
+                    final int y = (int) event.getY();
+                    if ((x > left) && (x < right) && (y > top) && (y < bottom)) {
+                        v.performClick();
+                    }
+                    v.setClickable(true);
+                    break;
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    public void onClick(View v) {
+        // When accessibility is on, simulate press and release to preserve the
+        // semantic meaning of performClick(). Required for Braille support.
+        if (mAccessibilityManager.isEnabled()) {
+            final int id = v.getId();
+            // Checking the press state prevents double activation.
+            if (!v.isPressed() && mDisplayMap.containsKey(id)) {
+                processDtmf(mDisplayMap.get(id), true /* timedShortTone */);
+            }
+        }
+    }
+
+    /**
+     * Implemented for the TouchListener, process the touch events.
+     */
+    @Override
+    public boolean onTouch(View v, MotionEvent event) {
+        int viewId = v.getId();
+
+        // if the button is recognized
+        if (mDisplayMap.containsKey(viewId)) {
+            switch (event.getAction()) {
+                case MotionEvent.ACTION_DOWN:
+                    // Append the character mapped to this button, to the display.
+                    // start the tone
+                    processDtmf(mDisplayMap.get(viewId));
+                    break;
+                case MotionEvent.ACTION_UP:
+                case MotionEvent.ACTION_CANCEL:
+                    // stop the tone on ANY other event, except for MOVE.
+                    stopTone();
+                    break;
+            }
+            // do not return true [handled] here, since we want the
+            // press / click animation to be handled by the framework.
+        }
+        return false;
+    }
+
+    /**
+     * Implements View.OnKeyListener for the DTMF buttons.  Enables dialing with trackball/dpad.
+     */
+    @Override
+    public boolean onKey(View v, int keyCode, KeyEvent event) {
+        // if (DBG) log("onKey:  keyCode " + keyCode + ", view " + v);
+
+        if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
+            int viewId = v.getId();
+            if (mDisplayMap.containsKey(viewId)) {
+                switch (event.getAction()) {
+                case KeyEvent.ACTION_DOWN:
+                    if (event.getRepeatCount() == 0) {
+                        processDtmf(mDisplayMap.get(viewId));
+                    }
+                    break;
+                case KeyEvent.ACTION_UP:
+                    stopTone();
+                    break;
+                }
+                // do not return true [handled] here, since we want the
+                // press / click animation to be handled by the framework.
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns true if the dialer is in "open" state, meaning it is already visible *and* it
+     * isn't fading out. Note that during fade-out animation the View will return VISIBLE but
+     * will become GONE soon later, so you would want to use this method instead of
+     * {@link View#getVisibility()}.
+     *
+     * Fade-in animation, on the other hand, will set the View's visibility VISIBLE soon after
+     * the request, so we don't need to take care much of it. In other words,
+     * {@link #openDialer(boolean)} soon makes the visibility VISIBLE and thus this method will
+     * return true just after the method call.
+     *
+     * Note: during the very early stage of "open" state, users may not see the dialpad yet because
+     * of its fading-in animation, while they will see it shortly anyway. Similarly, during the
+     * early stage of "closed" state (opposite of "open" state), users may still see the dialpad
+     * due to fading-out animation, but it will vanish shortly and thus we can treat it as "closed",
+     * or "not open". To make the transition clearer, we call the state "open", not "shown" nor
+     * "visible".
+     */
+    public boolean isOpened() {
+        // Return whether or not the dialer view is visible.
+        // (Note that if we're in the middle of a fade-out animation, that
+        // also counts as "not visible" even though mDialerView itself is
+        // technically still VISIBLE.)
+        return (mDialerView != null
+                &&(mDialerView.getVisibility() == View.VISIBLE)
+                && !AnimationUtils.Fade.isFadingOut(mDialerView));
+    }
+
+    /**
+     * Forces the dialer into the "open" state.
+     * Does nothing if the dialer is already open.
+     *
+     * The "open" state includes the state the dialer is fading in.
+     * {@link InCallScreen#onDialerOpen(boolean)} will change visibility state and do
+     * actual animation.
+     *
+     * @param animate if true, open the dialer with an animation.
+     *
+     * @see #isOpened
+     */
+    public void openDialer(boolean animate) {
+        if (DBG) log("openDialer()...");
+
+        if (mDialerView == null && mDialerStub != null) {
+            if (DBG) log("Dialer isn't ready. Inflate it from ViewStub.");
+            mDialerView = (DTMFTwelveKeyDialerView) mDialerStub.inflate();
+            setupDialerView();
+            mDialerStub = null;
+        }
+
+        if (!isOpened()) {
+            // Make the dialer view visible.
+            if (animate) {
+                AnimationUtils.Fade.show(mDialerView);
+            } else {
+                mDialerView.setVisibility(View.VISIBLE);
+            }
+            onDialerOpen(animate);
+        }
+    }
+
+    /**
+     * Forces the dialer into the "closed" state.
+     * Does nothing if the dialer is already closed.
+     *
+     * {@link InCallScreen#onDialerOpen(boolean)} will change visibility state and do
+     * actual animation.
+     *
+     * @param animate if true, close the dialer with an animation.
+     *
+     * @see #isOpened
+     */
+    public void closeDialer(boolean animate) {
+        if (DBG) log("closeDialer()...");
+
+        if (isOpened()) {
+            // Hide the dialer view.
+            if (animate) {
+                AnimationUtils.Fade.hide(mDialerView, View.GONE);
+            } else {
+                mDialerView.setVisibility(View.GONE);
+            }
+            onDialerClose(animate);
+        }
+    }
+
+    /**
+     * Processes the specified digit as a DTMF key, by playing the
+     * appropriate DTMF tone, and appending the digit to the EditText
+     * field that displays the DTMF digits sent so far.
+     *
+     * @see #processDtmf(char, boolean)
+     */
+    private final void processDtmf(char c) {
+        processDtmf(c, false);
+    }
+
+    /**
+     * Processes the specified digit as a DTMF key, by playing the appropriate
+     * DTMF tone (or short tone if requested), and appending the digit to the
+     * EditText field that displays the DTMF digits sent so far.
+     */
+    private final void processDtmf(char c, boolean timedShortTone) {
+        // if it is a valid key, then update the display and send the dtmf tone.
+        if (PhoneNumberUtils.is12Key(c)) {
+            if (DBG) log("updating display and sending dtmf tone for '" + c + "'");
+
+            // Append this key to the "digits" widget.
+            if (mDialpadDigits != null) {
+                // TODO: maybe *don't* manually append this digit if
+                // mDialpadDigits is focused and this key came from the HW
+                // keyboard, since in that case the EditText field will
+                // get the key event directly and automatically appends
+                // whetever the user types.
+                // (Or, a cleaner fix would be to just make mDialpadDigits
+                // *not* handle HW key presses.  That seems to be more
+                // complicated than just setting focusable="false" on it,
+                // though.)
+                mDialpadDigits.getText().append(c);
+            }
+
+            // Play the tone if it exists.
+            if (mToneMap.containsKey(c)) {
+                // begin tone playback.
+                startTone(c, timedShortTone);
+            }
+        } else if (DBG) {
+            log("ignoring dtmf request for '" + c + "'");
+        }
+
+        // Any DTMF keypress counts as explicit "user activity".
+        PhoneGlobals.getInstance().pokeUserActivity();
+    }
+
+    /**
+     * Clears out the display of "DTMF digits typed so far" that's kept in
+     * mDialpadDigits.
+     *
+     * The InCallScreen is responsible for calling this method any time a
+     * new call becomes active (or, more simply, any time a call ends).
+     * This is how we make sure that the "history" of DTMF digits you type
+     * doesn't persist from one call to the next.
+     *
+     * TODO: it might be more elegent if the dialpad itself could remember
+     * the call that we're associated with, and clear the digits if the
+     * "current call" has changed since last time.  (This would require
+     * some unique identifier that's different for each call.  We can't
+     * just use the foreground Call object, since that's a singleton that
+     * lasts the whole life of the phone process.  Instead, maybe look at
+     * the Connection object that comes back from getEarliestConnection()?
+     * Or getEarliestConnectTime()?)
+     *
+     * Or to be even fancier, we could keep a mapping of *multiple*
+     * "active calls" to DTMF strings.  That way you could have two lines
+     * in use and swap calls multiple times, and we'd still remember the
+     * digits for each call.  (But that's such an obscure use case that
+     * it's probably not worth the extra complexity.)
+     */
+    public void clearDigits() {
+        if (DBG) log("clearDigits()...");
+
+        if (mDialpadDigits != null) {
+            mDialpadDigits.setText("");
+        }
+
+        setDialpadContext("");
+    }
+
+    /**
+     * Set the context text (hint) to show in the dialpad Digits EditText.
+     *
+     * This is currently only used for displaying a value for "Voice Mail"
+     * calls since they default to the dialpad and we want to give users better
+     * context when they dial voicemail.
+     *
+     * TODO: Is there value in extending this functionality for all contacts
+     * and not just Voice Mail calls?
+     * TODO: This should include setting the digits as well as the context
+     * once we start saving the digits properly...and properly in this case
+     * ideally means moving some of processDtmf() out of this class.
+     */
+    public void setDialpadContext(String contextValue) {
+        if (mDialpadDigits != null) {
+            if (contextValue == null) {
+              contextValue = "";
+            }
+            final SpannableString hint = new SpannableString(contextValue);
+            hint.setSpan(new RelativeSizeSpan(0.8f), 0, hint.length(), 0);
+            mDialpadDigits.setHint(hint);
+        }
+    }
+
+    /**
+     * Plays the local tone based the phone type.
+     */
+    public void startTone(char c, boolean timedShortTone) {
+        // Only play the tone if it exists.
+        if (!mToneMap.containsKey(c)) {
+            return;
+        }
+
+        if (!mInCallScreen.okToDialDTMFTones()) {
+            return;
+        }
+
+        // Read the settings as it may be changed by the user during the call
+        Phone phone = mCM.getFgPhone();
+        mShortTone = useShortDtmfTones(phone, phone.getContext());
+
+        // Before we go ahead and start a tone, we need to make sure that any pending
+        // stop-tone message is processed.
+        if (mHandler.hasMessages(DTMF_STOP)) {
+            mHandler.removeMessages(DTMF_STOP);
+            stopTone();
+        }
+
+        if (DBG) log("startDtmfTone()...");
+
+        // For Short DTMF we need to play the local tone for fixed duration
+        if (mShortTone) {
+            sendShortDtmfToNetwork(c);
+        } else {
+            // Pass as a char to be sent to network
+            if (DBG) log("send long dtmf for " + c);
+            mCM.startDtmf(c);
+
+            // If it is a timed tone, queue up the stop command in DTMF_DURATION_MS.
+            if (timedShortTone) {
+                mHandler.sendMessageDelayed(mHandler.obtainMessage(DTMF_STOP), DTMF_DURATION_MS);
+            }
+        }
+        startLocalToneIfNeeded(c);
+    }
+
+
+    /**
+     * Plays the local tone based the phone type, optionally forcing a short
+     * tone.
+     */
+    public void startLocalToneIfNeeded(char c) {
+        // if local tone playback is enabled, start it.
+        // Only play the tone if it exists.
+        if (!mToneMap.containsKey(c)) {
+            return;
+        }
+        if (mLocalToneEnabled) {
+            synchronized (mToneGeneratorLock) {
+                if (mToneGenerator == null) {
+                    if (DBG) log("startDtmfTone: mToneGenerator == null, tone: " + c);
+                } else {
+                    if (DBG) log("starting local tone " + c);
+                    int toneDuration = -1;
+                    if (mShortTone) {
+                        toneDuration = DTMF_DURATION_MS;
+                    }
+                    mToneGenerator.startTone(mToneMap.get(c), toneDuration);
+                }
+            }
+        }
+    }
+
+    /**
+     * Check to see if the keyEvent is dialable.
+     */
+    boolean isKeyEventAcceptable (KeyEvent event) {
+        return (mDialerKeyListener != null && mDialerKeyListener.isKeyEventAcceptable(event));
+    }
+
+    /**
+     * static logging method
+     */
+    private static void log(String msg) {
+        Log.d(LOG_TAG, msg);
+    }
+
+    /**
+     * Stops the local tone based on the phone type.
+     */
+    public void stopTone() {
+        // We do not rely on InCallScreen#okToDialDTMFTones() here since it is ok to stop tones
+        // without starting them.
+
+        if (!mShortTone) {
+            if (DBG) log("stopping remote tone.");
+            mCM.stopDtmf();
+            stopLocalToneIfNeeded();
+        }
+    }
+
+    /**
+     * Stops the local tone based on the phone type.
+     */
+    public void stopLocalToneIfNeeded() {
+        if (!mShortTone) {
+            // if local tone playback is enabled, stop it.
+            if (DBG) log("trying to stop local tone...");
+            if (mLocalToneEnabled) {
+                synchronized (mToneGeneratorLock) {
+                    if (mToneGenerator == null) {
+                        if (DBG) log("stopLocalTone: mToneGenerator == null");
+                    } else {
+                        if (DBG) log("stopping local tone.");
+                        mToneGenerator.stopTone();
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Sends the dtmf character over the network for short DTMF settings
+     * When the characters are entered in quick succession,
+     * the characters are queued before sending over the network.
+     */
+    private void sendShortDtmfToNetwork(char dtmfDigit) {
+        synchronized (mDTMFQueue) {
+            if (mDTMFBurstCnfPending == true) {
+                // Insert the dtmf char to the queue
+                mDTMFQueue.add(new Character(dtmfDigit));
+            } else {
+                String dtmfStr = Character.toString(dtmfDigit);
+                mCM.sendBurstDtmf(dtmfStr, 0, 0, mHandler.obtainMessage(DTMF_SEND_CNF));
+                // Set flag to indicate wait for Telephony confirmation.
+                mDTMFBurstCnfPending = true;
+            }
+        }
+    }
+
+    /**
+     * Handles Burst Dtmf Confirmation from the Framework.
+     */
+    void handleBurstDtmfConfirmation() {
+        Character dtmfChar = null;
+        synchronized (mDTMFQueue) {
+            mDTMFBurstCnfPending = false;
+            if (!mDTMFQueue.isEmpty()) {
+                dtmfChar = mDTMFQueue.remove();
+                Log.i(LOG_TAG, "The dtmf character removed from queue" + dtmfChar);
+            }
+        }
+        if (dtmfChar != null) {
+            sendShortDtmfToNetwork(dtmfChar);
+        }
+    }
+
+    /**
+     * On GSM devices, we never use short tones.
+     * On CDMA devices, it depends upon the settings.
+     */
+    private static boolean useShortDtmfTones(Phone phone, Context context) {
+        int phoneType = phone.getPhoneType();
+        if (phoneType == PhoneConstants.PHONE_TYPE_GSM) {
+            return false;
+        } else if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+            int toneType = android.provider.Settings.System.getInt(
+                    context.getContentResolver(),
+                    Settings.System.DTMF_TONE_TYPE_WHEN_DIALING,
+                    Constants.DTMF_TONE_TYPE_NORMAL);
+            if (toneType == Constants.DTMF_TONE_TYPE_NORMAL) {
+                return true;
+            } else {
+                return false;
+            }
+        } else if (phoneType == PhoneConstants.PHONE_TYPE_SIP) {
+            return false;
+        } else {
+            throw new IllegalStateException("Unexpected phone type: " + phoneType);
+        }
+    }
+
+}