Adds hardware keyboard support for DTMF

Bug: 10808399
Change-Id: Id12acee7baf11335916a38c50a266f83f578e0bd
diff --git a/InCallUI/src/com/android/incallui/DialpadFragment.java b/InCallUI/src/com/android/incallui/DialpadFragment.java
index 428750d..5087561 100644
--- a/InCallUI/src/com/android/incallui/DialpadFragment.java
+++ b/InCallUI/src/com/android/incallui/DialpadFragment.java
@@ -18,6 +18,8 @@
 
 import android.content.Context;
 import android.os.Bundle;
+import android.text.Editable;
+import android.text.method.DialerKeyListener;
 import android.view.KeyEvent;
 import android.view.LayoutInflater;
 import android.view.MotionEvent;
@@ -58,6 +60,198 @@
         mDisplayMap.put(R.id.star, '*');
     }
 
+    // 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) {
+                    Log.d(this, "DTMFKeyListener reading '" + c + "' from input.");
+                    getPresenter().processDtmf(c);
+                } else {
+                    Log.d(this, "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) {
+                Log.d(this, "Stopping the tone for '" + c + "'");
+                getPresenter().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);
+            Log.d(this, "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)) {
+                    Log.d(this, "DTMFKeyListener reading '" + c + "' from input.");
+                    getPresenter().processDtmf(c);
+                    return true;
+                } else {
+                    Log.d(this, "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);
+            Log.d(this, "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)) {
+                Log.d(this, "Stopping the tone for '" + c + "'");
+                getPresenter().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', '#', '*'
+        };
+    }
+
     @Override
     public void onClick(View v) {
         Log.d(this, "onClick");
@@ -180,6 +374,8 @@
                 com.android.incallui.R.layout.dtmf_twelve_key_dialer_view, container, false);
         mDtmfDialerField = (EditText) parent.findViewById(R.id.dtmfDialerField);
         if (mDtmfDialerField != null) {
+            mDialerKeyListener = new DTMFKeyListener();
+            mDtmfDialerField.setKeyListener(mDialerKeyListener);
             // remove the long-press context menus that support
             // the edit (copy / paste / select) functions.
             mDtmfDialerField.setLongClickable(false);
@@ -190,6 +386,12 @@
     }
 
     @Override
+    public void onDestroyView() {
+        mDialerKeyListener = null;
+        super.onDestroyView();
+    }
+
+    @Override
     public void setVisible(boolean on) {
         if (on) {
             getView().setVisibility(View.VISIBLE);
@@ -215,6 +417,30 @@
     }
 
     /**
+     * Called externally (from InCallScreen) to play a DTMF Tone.
+     */
+    /* package */ boolean onDialerKeyDown(KeyEvent event) {
+        Log.d(this, "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) {
+        Log.d(this, "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(View parent) {
diff --git a/InCallUI/src/com/android/incallui/InCallActivity.java b/InCallUI/src/com/android/incallui/InCallActivity.java
index d2a7f21..41e01ff 100644
--- a/InCallUI/src/com/android/incallui/InCallActivity.java
+++ b/InCallUI/src/com/android/incallui/InCallActivity.java
@@ -82,7 +82,7 @@
 
     @Override
     protected void onResume() {
-        Log.d(this, "onResume()...");
+        Log.i(this, "onResume()...");
         super.onResume();
 
         mIsForegroundActivity = true;
@@ -102,6 +102,9 @@
         super.onPause();
 
         mIsForegroundActivity = false;
+
+        mDialpadFragment.onDialerKeyUp(null);
+
         InCallPresenter.getInstance().onUiShowing(false);
     }
 
@@ -189,6 +192,18 @@
     }
 
     @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        // push input to the dialer.
+        if ((mDialpadFragment.isVisible()) && (mDialpadFragment.onDialerKeyUp(event))){
+            return true;
+        } else if (keyCode == KeyEvent.KEYCODE_CALL) {
+            // Always consume CALL to be sure the PhoneWindow won't do anything with it
+            return true;
+        }
+        return super.onKeyUp(keyCode, event);
+    }
+
+    @Override
     public boolean onKeyDown(int keyCode, KeyEvent event) {
         switch (keyCode) {
             case KeyEvent.KEYCODE_CALL:
@@ -238,9 +253,33 @@
                 break;
         }
 
+        if (event.getRepeatCount() == 0 && handleDialerKeyDown(keyCode, event)) {
+            return true;
+        }
+
         return super.onKeyDown(keyCode, event);
     }
 
+    private boolean handleDialerKeyDown(int keyCode, KeyEvent event) {
+        Log.v(this, "handleDialerKeyDown: keyCode " + keyCode + ", event " + event + "...");
+
+        // As soon as the user starts typing valid dialable keys on the
+        // keyboard (presumably to type DTMF tones) we start passing the
+        // key events to the DTMFDialer's onDialerKeyDown.
+        if (mDialpadFragment.isVisible()) {
+            return mDialpadFragment.onDialerKeyDown(event);
+
+            // TODO: If the dialpad isn't currently visible, maybe
+            // consider automatically bringing it up right now?
+            // (Just to make sure the user sees the digits widget...)
+            // But this probably isn't too critical since it's awkward to
+            // use the hard keyboard while in-call in the first place,
+            // especially now that the in-call UI is portrait-only...
+        }
+
+        return false;
+    }
+
     @Override
     public void onConfigurationChanged(Configuration config) {
         InCallPresenter.getInstance().getProximitySensor().onConfigurationChanged(config);