Merge "Add CANP name and presentation related fields to call"
diff --git a/Android.mk b/Android.mk
index 24d5b71..261cb3f 100644
--- a/Android.mk
+++ b/Android.mk
@@ -16,7 +16,7 @@
         src/com/android/phone/INetworkQueryServiceCallback.aidl
 
 LOCAL_PACKAGE_NAME := TeleService
-LOCAL_OVERRIDES_PACKAGES := Phone GooglePhone
+LOCAL_OVERRIDES_PACKAGES := Phone
 
 LOCAL_CERTIFICATE := platform
 LOCAL_PRIVILEGED_MODULE := true
diff --git a/common/src/com/android/services/telephony/common/AudioMode.java b/common/src/com/android/services/telephony/common/AudioMode.java
new file mode 100644
index 0000000..f296a93
--- /dev/null
+++ b/common/src/com/android/services/telephony/common/AudioMode.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2013 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.services.telephony.common;
+
+/**
+ * Container class for audio modes.
+ */
+public class AudioMode {
+    // These can be used as a bit mask
+    public static int EARPIECE = 0x00000001;
+    public static int BLUETOOTH = 0x00000002;
+
+    public static int ALL_MODES = EARPIECE | BLUETOOTH;
+
+    public static String toString(int mode) {
+        if ((mode & ~ALL_MODES) != 0x0) {
+            return "UNKNOWN";
+        }
+
+        StringBuffer buffer = new StringBuffer();
+        if ((mode & EARPIECE) == EARPIECE) {
+            listAppend(buffer, "EARPIECE");
+        }
+        if ((mode & BLUETOOTH) == BLUETOOTH) {
+            listAppend(buffer, "BLUETOOTH");
+        }
+
+        return buffer.toString();
+    }
+
+    private static void listAppend(StringBuffer buffer, String str) {
+        if (buffer.length() > 0) {
+            buffer.append(", ");
+        }
+        buffer.append(str);
+    }
+}
diff --git a/common/src/com/android/services/telephony/common/ICallCommandService.aidl b/common/src/com/android/services/telephony/common/ICallCommandService.aidl
index a8cfba3..defeeef 100644
--- a/common/src/com/android/services/telephony/common/ICallCommandService.aidl
+++ b/common/src/com/android/services/telephony/common/ICallCommandService.aidl
@@ -32,7 +32,7 @@
     /**
      * Reject a ringing call.
      */
-    void rejectCall(int callId);
+    void rejectCall(int callId, boolean rejectWithMessage, String message);
 
     /**
      * Disconnect an active call.
@@ -51,16 +51,23 @@
 
     /**
      * Turn on or off speaker.
+     * TODO(klp): Remove in favor of setAudioMode
      */
     void speaker(boolean onOff);
 
     /**
      * Start playing DTMF tone for the specified digit.
      */
-    void playDtmfTone(char digit);
+    void playDtmfTone(char digit, boolean timedShortTone);
 
     /**
      * Stop playing DTMF tone for the specified digit.
      */
     void stopDtmfTone();
+
+    /**
+     * Sets the audio mode for the active phone call.
+     * {@see AudioMode}
+     */
+    void setAudioMode(int mode);
 }
diff --git a/common/src/com/android/services/telephony/common/ICallHandlerService.aidl b/common/src/com/android/services/telephony/common/ICallHandlerService.aidl
index d458749..33055c5 100644
--- a/common/src/com/android/services/telephony/common/ICallHandlerService.aidl
+++ b/common/src/com/android/services/telephony/common/ICallHandlerService.aidl
@@ -37,6 +37,11 @@
     void setCallCommandService(ICallCommandService callCommandService);
 
     /**
+     * Called when there is an incoming call.
+     */
+    void onIncoming(in Call call, in List<String> textReponses);
+
+    /**
      * Called when the state of a call changes.
      */
     void onUpdate(in List<Call> call, boolean fullUpdate);
@@ -45,4 +50,16 @@
      * Called when a call disconnects.
      */
     void onDisconnect(in Call call);
+
+    /**
+     * Called when the audio mode changes.
+     * {@see AudioMode}
+     */
+    void onAudioModeChange(in int mode);
+
+    /**
+     * Called when the supported audio modes change.
+     * {@see AudioMode}
+     */
+    void onAudioModeSupportChange(in int modeMask);
 }
diff --git a/src/com/android/phone/AudioRouter.java b/src/com/android/phone/AudioRouter.java
new file mode 100644
index 0000000..2b8201c
--- /dev/null
+++ b/src/com/android/phone/AudioRouter.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2013 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 com.google.common.collect.Lists;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.os.SystemProperties;
+import android.util.Log;
+
+import com.android.phone.BluetoothManager.BluetoothIndicatorListener;
+import com.android.services.telephony.common.AudioMode;
+
+import java.util.List;
+
+/**
+ * Responsible for Routing in-call audio and maintaining routing state.
+ */
+/* package */ class AudioRouter implements BluetoothIndicatorListener {
+
+    private static String LOG_TAG = AudioRouter.class.getSimpleName();
+    private static final boolean DBG =
+            (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
+    private static final boolean VDBG = (PhoneGlobals.DBG_LEVEL >= 2);
+
+    private final Context mContext;
+    private final BluetoothManager mBluetoothManager;
+    private final List<AudioModeListener> mListeners = Lists.newArrayList();
+    private int mAudioMode = AudioMode.EARPIECE;
+    private int mPreviousMode = AudioMode.EARPIECE;
+
+    public AudioRouter(Context context, BluetoothManager bluetoothManager) {
+        mContext = context;
+        mBluetoothManager = bluetoothManager;
+
+        init();
+    }
+
+    /**
+     * Return the current audio mode.
+     */
+    public int getAudioMode() {
+        return mAudioMode;
+    }
+
+    /**
+     * Add a listener to audio mode changes.
+     */
+    public void addAudioModeListener(AudioModeListener listener) {
+        if (!mListeners.contains(listener)) {
+            mListeners.add(listener);
+        }
+    }
+
+    /**
+     * Remove  listener.
+     */
+    public void removeAudioModeListener(AudioModeListener listener) {
+        if (mListeners.contains(listener)) {
+            mListeners.remove(listener);
+        }
+    }
+
+    /**
+     * Sets the audio mode to the mode that is passed in.
+     */
+    public void setAudioMode(int mode) {
+        if (AudioMode.BLUETOOTH == mode) {
+
+            // dont set mAudioMode because we will get a notificaiton through
+            // onBluetoothIndicationChange if successful
+            toggleBluetooth(true);
+        } else if (AudioMode.EARPIECE == mode) {
+            toggleBluetooth(false);
+        }
+    }
+
+    /**
+     * Called when the bluetooth connection changes.
+     * We adjust the audio mode according to the state we receive.
+     */
+    @Override
+    public void onBluetoothIndicationChange(boolean isConnected, BluetoothManager btManager) {
+        int newMode = mAudioMode;
+
+        if (isConnected) {
+            newMode = AudioMode.BLUETOOTH;
+        } else {
+            newMode = AudioMode.EARPIECE;
+        }
+
+        changeAudioModeTo(newMode);
+    }
+
+    private void toggleBluetooth(boolean on) {
+        if (on) {
+            mBluetoothManager.connectBluetoothAudio();
+        } else {
+            mBluetoothManager.disconnectBluetoothAudio();
+        }
+    }
+
+    private void init() {
+        mBluetoothManager.addBluetoothIndicatorListener(this);
+    }
+
+    private void changeAudioModeTo(int mode) {
+        if (mAudioMode != mode) {
+            Log.i(LOG_TAG, "Audio mode changing to " + AudioMode.toString(mode));
+
+            mPreviousMode = mAudioMode;
+            mAudioMode = mode;
+
+            notifyListeners();
+        }
+    }
+
+    private void notifyListeners() {
+        for (int i = 0; i < mListeners.size(); i++) {
+            mListeners.get(i).onAudioModeChange(mPreviousMode, mAudioMode);
+        }
+    }
+
+    public interface AudioModeListener {
+        void onAudioModeChange(int previousMode, int newMode);
+    }
+}
diff --git a/src/com/android/phone/BluetoothManager.java b/src/com/android/phone/BluetoothManager.java
index a297472..8d20c25 100644
--- a/src/com/android/phone/BluetoothManager.java
+++ b/src/com/android/phone/BluetoothManager.java
@@ -221,12 +221,18 @@
         notifyListeners(mShowBluetoothIndication);
     }
 
-    /* package */ void addBluetoothIndicatorListener(BluetoothIndicatorListener listener) {
+    public void addBluetoothIndicatorListener(BluetoothIndicatorListener listener) {
         if (!mListeners.contains(listener)) {
             mListeners.add(listener);
         }
     }
 
+    public void removeBluetoothIndicatorListener(BluetoothIndicatorListener listener) {
+        if (mListeners.contains(listener)) {
+            mListeners.remove(listener);
+        }
+    }
+
     private void notifyListeners(boolean showBluetoothOn) {
         for (int i = 0; i < mListeners.size(); i++) {
             mListeners.get(i).onBluetoothIndicationChange(showBluetoothOn, this);
@@ -396,6 +402,6 @@
     }
 
     /* package */ interface BluetoothIndicatorListener {
-        public void onBluetoothIndicationChange(boolean showAsConnected, BluetoothManager manager);
+        public void onBluetoothIndicationChange(boolean isConnected, BluetoothManager manager);
     }
 }
diff --git a/src/com/android/phone/CallCommandService.java b/src/com/android/phone/CallCommandService.java
index 7f85404..6a3848c 100644
--- a/src/com/android/phone/CallCommandService.java
+++ b/src/com/android/phone/CallCommandService.java
@@ -21,6 +21,7 @@
 
 import com.android.internal.telephony.CallManager;
 import com.android.phone.CallModeler.CallResult;
+import com.android.services.telephony.common.AudioMode;
 import com.android.services.telephony.common.Call;
 import com.android.services.telephony.common.ICallCommandService;
 
@@ -36,13 +37,18 @@
     private final CallManager mCallManager;
     private final CallModeler mCallModeler;
     private final DTMFTonePlayer mDtmfTonePlayer;
+    private final AudioRouter mAudioRouter;
+    private final RejectWithTextMessageManager mRejectWithTextMessageManager;
 
     public CallCommandService(Context context, CallManager callManager, CallModeler callModeler,
-            DTMFTonePlayer dtmfTonePlayer) {
+            DTMFTonePlayer dtmfTonePlayer, AudioRouter audioRouter,
+            RejectWithTextMessageManager rejectWithTextMessageManager) {
         mContext = context;
         mCallManager = callManager;
         mCallModeler = callModeler;
         mDtmfTonePlayer = dtmfTonePlayer;
+        mAudioRouter = audioRouter;
+        mRejectWithTextMessageManager = rejectWithTextMessageManager;
     }
 
     /**
@@ -64,10 +70,20 @@
      * TODO(klp): Add a confirmation callback parameter.
      */
     @Override
-    public void rejectCall(int callId) {
+    public void rejectCall(int callId, boolean rejectWithMessage, String message) {
         try {
             CallResult result = mCallModeler.getCallWithId(callId);
             if (result != null) {
+                if (rejectWithMessage) {
+                    if (message != null) {
+                        mRejectWithTextMessageManager.rejectCallWithMessage(
+                                result.getConnection().getCall(), message);
+                    } else {
+                        mRejectWithTextMessageManager.rejectCallWithNewMessage(
+                                result.getConnection().getCall());
+                    }
+                }
+                Log.v(TAG, "Hanging up");
                 PhoneUtils.hangupRingingCall(result.getConnection().getCall());
             }
         } catch (Exception e) {
@@ -110,7 +126,8 @@
     @Override
     public void mute(boolean onOff) {
         try {
-            PhoneUtils.setMute(onOff);
+            //PhoneUtils.setMute(onOff);
+            mAudioRouter.setAudioMode(onOff ? AudioMode.BLUETOOTH : AudioMode.EARPIECE);
         } catch (Exception e) {
             Log.e(TAG, "Error during mute().", e);
         }
@@ -127,9 +144,9 @@
     }
 
     @Override
-    public void playDtmfTone(char digit) {
+    public void playDtmfTone(char digit, boolean timedShortTone) {
         try {
-            mDtmfTonePlayer.playDtmfTone(digit);
+            mDtmfTonePlayer.playDtmfTone(digit, timedShortTone);
         } catch (Exception e) {
             Log.e(TAG, "Error playing DTMF tone.", e);
         }
@@ -143,4 +160,13 @@
             Log.e(TAG, "Error stopping DTMF tone.", e);
         }
     }
+
+    @Override
+    public void setAudioMode(int mode) {
+        try {
+            mAudioRouter.setAudioMode(mode);
+        } catch (Exception e) {
+            Log.e(TAG, "Error setting the audio mode.", e);
+        }
+    }
 }
diff --git a/src/com/android/phone/CallHandlerServiceProxy.java b/src/com/android/phone/CallHandlerServiceProxy.java
index 5d6f68d..165e693 100644
--- a/src/com/android/phone/CallHandlerServiceProxy.java
+++ b/src/com/android/phone/CallHandlerServiceProxy.java
@@ -28,16 +28,20 @@
 import android.os.SystemProperties;
 import android.util.Log;
 
+import com.android.phone.AudioRouter.AudioModeListener;
+import com.android.services.telephony.common.AudioMode;
 import com.android.services.telephony.common.Call;
 import com.android.services.telephony.common.ICallHandlerService;
 import com.android.services.telephony.common.ICallCommandService;
 
+import java.util.ArrayList;
 import java.util.List;
 
 /**
  * This class is responsible for passing through call state changes to the CallHandlerService.
  */
-public class CallHandlerServiceProxy extends Handler implements CallModeler.Listener {
+public class CallHandlerServiceProxy extends Handler implements CallModeler.Listener,
+        AudioModeListener {
 
     private static final String TAG = CallHandlerServiceProxy.class.getSimpleName();
     private static final boolean DBG =
@@ -57,7 +61,7 @@
         mCallModeler = callModeler;
 
         setupServiceConnection();
-        mCallModeler.setListener(this);
+        mCallModeler.addListener(this);
 
         // start the whole process
         onUpdate(mCallModeler.getFullList(), true);
@@ -75,6 +79,17 @@
     }
 
     @Override
+    public void onIncoming(Call call, ArrayList<String> textResponses) {
+        if (mCallHandlerService != null) {
+            try {
+                mCallHandlerService.onIncoming(call, textResponses);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Remote exception handling onUpdate", e);
+            }
+        }
+    }
+
+    @Override
     public void onUpdate(List<Call> calls, boolean fullUpdate) {
         if (mCallHandlerService != null) {
             try {
@@ -85,6 +100,21 @@
         }
     }
 
+    @Override
+    public void onAudioModeChange(int previousMode, int newMode) {
+        // Just do a simple log for now.
+        Log.i(TAG, "Updating with new audio mode: " + AudioMode.toString(newMode) +
+                " from " + AudioMode.toString(previousMode));
+
+        if (mCallHandlerService != null) {
+            try {
+                mCallHandlerService.onAudioModeChange(newMode);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Remote exception handling onAudioModeChange", e);
+            }
+        }
+    }
+
     /**
      * Sets up the connection with ICallHandlerService
      */
diff --git a/src/com/android/phone/CallModeler.java b/src/com/android/phone/CallModeler.java
index c5eda4e..f309fa0 100644
--- a/src/com/android/phone/CallModeler.java
+++ b/src/com/android/phone/CallModeler.java
@@ -23,7 +23,6 @@
 import android.os.AsyncResult;
 import android.os.Handler;
 import android.os.Message;
-import android.util.Log;
 
 import com.android.internal.telephony.CallManager;
 import com.android.internal.telephony.Connection;
@@ -75,11 +74,14 @@
     private final CallManager mCallManager;
     private final HashMap<Connection, Call> mCallMap = Maps.newHashMap();
     private final AtomicInteger mNextCallId = new AtomicInteger(CALL_ID_START_VALUE);
-    private Listener mListener;
+    private final ArrayList<Listener> mListeners = new ArrayList<Listener>();
+    private RejectWithTextMessageManager mRejectWithTextMessageManager;
 
-    public CallModeler(CallStateMonitor callStateMonitor, CallManager callManager) {
+    public CallModeler(CallStateMonitor callStateMonitor, CallManager callManager,
+            RejectWithTextMessageManager rejectWithTextMessageManager) {
         mCallStateMonitor = callStateMonitor;
         mCallManager = callManager;
+        mRejectWithTextMessageManager = rejectWithTextMessageManager;
 
         mCallStateMonitor.addListener(this);
     }
@@ -101,14 +103,12 @@
         }
     }
 
-    public void setListener(Listener listener) {
+    public void addListener(Listener listener) {
         Preconditions.checkNotNull(listener);
-        Preconditions.checkState(mListener == null);
-
-        // only support setting listener once.
-        // We only have one listener anyway and supporting multiple means maintaining state for
-        // each of the listeners so that we can do proper diffs.
-        mListener = listener;
+        Preconditions.checkNotNull(mListeners);
+        if (!mListeners.contains(listener)) {
+            mListeners.add(listener);
+        }
     }
 
     public List<Call> getFullList() {
@@ -145,8 +145,11 @@
         final Call call = getCallFromConnection(conn, true);
         call.setState(Call.State.INCOMING);
 
-        if (call != null && mListener != null) {
-            mListener.onUpdate(Lists.newArrayList(call), false);
+        for (int i = 0; i < mListeners.size(); ++i) {
+            if (call != null) {
+              mListeners.get(i).onIncoming(call,
+                      mRejectWithTextMessageManager.loadCannedResponses());
+            }
         }
     }
 
@@ -158,8 +161,8 @@
         if (call != null) {
             mCallMap.remove(conn);
 
-            if (mListener != null) {
-                mListener.onDisconnect(call);
+            for (int i = 0; i < mListeners.size(); ++i) {
+                mListeners.get(i).onDisconnect(call);
             }
         }
     }
@@ -171,8 +174,8 @@
         final List<Call> updatedCalls = Lists.newArrayList();
         doUpdate(false, updatedCalls);
 
-        if (mListener != null) {
-            mListener.onUpdate(updatedCalls, false);
+        for (int i = 0; i < mListeners.size(); ++i) {
+            mListeners.get(i).onUpdate(updatedCalls, false);
         }
     }
 
@@ -274,6 +277,7 @@
      */
     public interface Listener {
         void onDisconnect(Call call);
+        void onIncoming(Call call, ArrayList<String> textReponses);
         void onUpdate(List<Call> calls, boolean fullUpdate);
     }
 
diff --git a/src/com/android/phone/DTMFTonePlayer.java b/src/com/android/phone/DTMFTonePlayer.java
index 682c74a..0e27a99 100644
--- a/src/com/android/phone/DTMFTonePlayer.java
+++ b/src/com/android/phone/DTMFTonePlayer.java
@@ -18,6 +18,7 @@
 
 import com.google.common.collect.ImmutableMap;
 
+import android.content.Context;
 import android.media.AudioManager;
 import android.media.ToneGenerator;
 import android.os.Handler;
@@ -27,11 +28,15 @@
 
 import com.android.internal.telephony.CallManager;
 import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
 import com.android.services.telephony.common.Call;
 
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.Queue;
 
 /**
  * Playing DTMF tones through the CallManager.
@@ -40,7 +45,8 @@
     private static final String LOG_TAG = DTMFTonePlayer.class.getSimpleName();
     private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
 
-    private static final int DTMF_STOP = 100;
+    private static final int DTMF_SEND_CNF = 100;
+    private static final int DTMF_STOP = 101;
 
     /** Hash Map to map a character to a tone*/
     private static final Map<Character, Integer> mToneMap =
@@ -65,18 +71,57 @@
     private ToneGenerator mToneGenerator;
     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;
+
+    /**
+     * 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) {
+                case DTMF_SEND_CNF:
+                    logD("dtmf confirmation received from FW.");
+                    // handle burst dtmf confirmation
+                    handleBurstDtmfConfirmation();
+                    break;
+                case DTMF_STOP:
+                    logD("dtmf stop received");
+                    stopDtmfTone();
+                    break;
+            }
+        }
+    };
+
     public DTMFTonePlayer(CallManager callManager, CallModeler callModeler) {
         mCallManager = callManager;
         mCallModeler = callModeler;
+        mCallModeler.addListener(this);
     }
 
     @Override
     public void onDisconnect(Call call) {
+        logD("Call disconnected");
         checkCallState();
     }
 
     @Override
+    public void onIncoming(Call call, ArrayList<String> textResponses) {
+    }
+
+    @Override
     public void onUpdate(List<Call> calls, boolean full) {
+        logD("Call updated");
         checkCallState();
     }
 
@@ -137,12 +182,18 @@
                 mToneGenerator = null;
             }
         }
+
+        mHandler.removeMessages(DTMF_SEND_CNF);
+        synchronized (mDTMFQueue) {
+            mDTMFBurstCnfPending = false;
+            mDTMFQueue.clear();
+        }
     }
 
     /**
      * Starts playback of the dtmf tone corresponding to the parameter.
      */
-    public void playDtmfTone(char c) {
+    public void playDtmfTone(char c, boolean timedShortTone) {
         // Only play the tone if it exists.
         if (!mToneMap.containsKey(c)) {
             return;
@@ -152,21 +203,79 @@
             return;
         }
 
+        PhoneGlobals.getInstance().pokeUserActivity();
+
         // Read the settings as it may be changed by the user during the call
         Phone phone = mCallManager.getFgPhone();
 
+        // 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);
+            stopDtmfTone();
+        }
+
+        mShortTone = useShortDtmfTones(phone, phone.getContext());
         logD("startDtmfTone()...");
 
-        // Pass as a char to be sent to network
-        logD("send long dtmf for " + c);
-        mCallManager.startDtmf(c);
+        // 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
+            logD("send long dtmf for " + c);
+            mCallManager.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);
     }
 
+    /**
+     * 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);
+                mCallManager.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);
+        }
+    }
+
     public void stopDtmfTone() {
-        mCallManager.stopDtmf();
-        stopLocalToneIfNeeded();
+        if (!mShortTone) {
+            mCallManager.stopDtmf();
+            stopLocalToneIfNeeded();
+        }
     }
 
     /**
@@ -181,6 +290,9 @@
                 } else {
                     logD("starting local tone " + c);
                     int toneDuration = -1;
+                    if (mShortTone) {
+                        toneDuration = DTMF_DURATION_MS;
+                    }
                     mToneGenerator.startTone(mToneMap.get(c), toneDuration);
                 }
             }
@@ -191,15 +303,17 @@
      * Stops the local tone based on the phone type.
      */
     public void stopLocalToneIfNeeded() {
-        // if local tone playback is enabled, stop it.
-        logD("trying to stop local tone...");
-        if (mLocalToneEnabled) {
-            synchronized (mToneGeneratorLock) {
-                if (mToneGenerator == null) {
-                    logD("stopLocalTone: mToneGenerator == null");
-                } else {
-                    logD("stopping local tone.");
-                    mToneGenerator.stopTone();
+        if (!mShortTone) {
+            // if local tone playback is enabled, stop it.
+            logD("trying to stop local tone...");
+            if (mLocalToneEnabled) {
+                synchronized (mToneGeneratorLock) {
+                    if (mToneGenerator == null) {
+                        logD("stopLocalTone: mToneGenerator == null");
+                    } else {
+                        logD("stopping local tone.");
+                        mToneGenerator.stopTone();
+                    }
                 }
             }
         }
@@ -221,10 +335,36 @@
     }
 
     /**
+     * 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);
+        }
+    }
+
+    /**
      * Checks to see if there are any active calls. If there are, then we want to allocate the tone
      * resources for playing DTMF tone, otherwise release them.
      */
     private void checkCallState() {
+        logD("checkCallState");
         if (mCallModeler.hasOutstandingActiveCall()) {
             startDialerSession();
         } else {
diff --git a/src/com/android/phone/PhoneGlobals.java b/src/com/android/phone/PhoneGlobals.java
index 9db596b..3e81175 100644
--- a/src/com/android/phone/PhoneGlobals.java
+++ b/src/com/android/phone/PhoneGlobals.java
@@ -169,12 +169,14 @@
     Phone phone;
     PhoneInterfaceManager phoneMgr;
 
+    private AudioRouter audioRouter;
     private BluetoothManager bluetoothManager;
     private CallCommandService callCommandService;
     private CallHandlerServiceProxy callHandlerServiceProxy;
     private CallModeler callModeler;
     private CallStateMonitor callStateMonitor;
     private DTMFTonePlayer dtmfTonePlayer;
+    private RejectWithTextMessageManager rejectWithTextMessageManager;
     private IBluetoothHeadsetPhone mBluetoothPhone;
     private Ringer ringer;
 
@@ -544,14 +546,21 @@
             // Monitors call activity from the telephony layer
             callStateMonitor = new CallStateMonitor(mCM);
 
+            // Rejects calls with TextMessages
+            rejectWithTextMessageManager = new RejectWithTextMessageManager();
+
             // Creates call models for use with CallHandlerService.
-            callModeler = new CallModeler(callStateMonitor, mCM);
+            callModeler = new CallModeler(callStateMonitor, mCM, rejectWithTextMessageManager);
 
             // Plays DTMF Tones
             dtmfTonePlayer = new DTMFTonePlayer(mCM, callModeler);
 
+            // Audio router
+            audioRouter = new AudioRouter(this, bluetoothManager);
+
             // Service used by in-call UI to control calls
-            callCommandService = new CallCommandService(this, mCM, callModeler, dtmfTonePlayer);
+            callCommandService = new CallCommandService(this, mCM, callModeler, dtmfTonePlayer,
+                    audioRouter, rejectWithTextMessageManager);
 
             // Sends call state to the UI
             callHandlerServiceProxy = new CallHandlerServiceProxy(this, callModeler,
diff --git a/src/com/android/phone/RejectWithTextMessageManager.java b/src/com/android/phone/RejectWithTextMessageManager.java
new file mode 100644
index 0000000..f032169
--- /dev/null
+++ b/src/com/android/phone/RejectWithTextMessageManager.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright (C) 2011 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.app.ActivityManager;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Handler;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.Connection;
+import com.android.internal.telephony.PhoneConstants;
+import com.google.android.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helper class to manage the "Respond via Message" feature for incoming calls.
+ *
+ * @see com.android.phone.InCallScreen.internalRespondViaSms()
+ */
+public class RejectWithTextMessageManager {
+
+    private static final String TAG = RejectWithTextMessageManager.class.getSimpleName();
+    private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
+
+    private static final String PERMISSION_SEND_RESPOND_VIA_MESSAGE =
+            "android.permission.SEND_RESPOND_VIA_MESSAGE";
+
+    /** The array of "canned responses"; see loadCannedResponses(). */
+    private String[] mCannedResponses;
+
+    /** SharedPreferences file name for our persistent settings. */
+    private static final String SHARED_PREFERENCES_NAME = "respond_via_sms_prefs";
+
+    // Preference keys for the 4 "canned responses"; see RespondViaSmsManager$Settings.
+    // Since (for now at least) the number of messages is fixed at 4, and since
+    // SharedPreferences can't deal with arrays anyway, just store the messages
+    // as 4 separate strings.
+    private static final int NUM_CANNED_RESPONSES = 4;
+    private static final String KEY_CANNED_RESPONSE_PREF_1 = "canned_response_pref_1";
+    private static final String KEY_CANNED_RESPONSE_PREF_2 = "canned_response_pref_2";
+    private static final String KEY_CANNED_RESPONSE_PREF_3 = "canned_response_pref_3";
+    private static final String KEY_CANNED_RESPONSE_PREF_4 = "canned_response_pref_4";
+    private static final String KEY_PREFERRED_PACKAGE = "preferred_package_pref";
+    private static final String KEY_INSTANT_TEXT_DEFAULT_COMPONENT = "instant_text_def_component";
+
+    /**
+     * Brings up the standard SMS compose UI.
+     */
+    private void launchSmsCompose(String phoneNumber) {
+        if (DBG) log("launchSmsCompose: number " + phoneNumber);
+
+        final Intent intent = getInstantTextIntent(phoneNumber, null, getSmsService());
+
+        if (DBG) log("- Launching SMS compose UI: " + intent);
+        PhoneGlobals.getInstance().startService(intent);
+    }
+
+    /**
+     * Read the (customizable) canned responses from SharedPreferences,
+     * or from defaults if the user has never actually brought up
+     * the Settings UI.
+     *
+     * This method does disk I/O (reading the SharedPreferences file)
+     * so don't call it from the main thread.
+     *
+     * @see com.android.phone.RejectWithTextMessageManager.Settings
+     */
+    public ArrayList<String> loadCannedResponses() {
+        if (DBG) log("loadCannedResponses()...");
+
+        final SharedPreferences prefs = PhoneGlobals.getInstance().getSharedPreferences(
+                SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+        final Resources res = PhoneGlobals.getInstance().getResources();
+
+        final ArrayList<String> responses = new ArrayList<String>(NUM_CANNED_RESPONSES);
+
+        // Note the default values here must agree with the corresponding
+        // android:defaultValue attributes in respond_via_sms_settings.xml.
+
+        responses.add(0, prefs.getString(KEY_CANNED_RESPONSE_PREF_1,
+                                       res.getString(R.string.respond_via_sms_canned_response_1)));
+        responses.add(1, prefs.getString(KEY_CANNED_RESPONSE_PREF_2,
+                                       res.getString(R.string.respond_via_sms_canned_response_2)));
+        responses.add(2, prefs.getString(KEY_CANNED_RESPONSE_PREF_3,
+                                       res.getString(R.string.respond_via_sms_canned_response_3)));
+        responses.add(3, prefs.getString(KEY_CANNED_RESPONSE_PREF_4,
+                                       res.getString(R.string.respond_via_sms_canned_response_4)));
+        return responses;
+    }
+
+    /**
+     * @return true if the "Respond via SMS" feature should be enabled
+     * for the specified incoming call.
+     *
+     * The general rule is that we *do* allow "Respond via SMS" except for
+     * the few (relatively rare) cases where we know for sure it won't
+     * work, namely:
+     *   - a bogus or blank incoming number
+     *   - a call from a SIP address
+     *   - a "call presentation" that doesn't allow the number to be revealed
+     *
+     * In all other cases, we allow the user to respond via SMS.
+     *
+     * Note that this behavior isn't perfect; for example we have no way
+     * to detect whether the incoming call is from a landline (with most
+     * networks at least), so we still enable this feature even though
+     * SMSes to that number will silently fail.
+     */
+    public static boolean allowRespondViaSmsForCall(Context context, Call ringingCall) {
+        // TODO(klp) implement this!
+        return true;
+    }
+
+    /**
+     * Sends a text message without any interaction from the user.
+     */
+    private void sendText(String phoneNumber, String message, ComponentName component) {
+        if (DBG) log("sendText: number "
+                      + phoneNumber + ", message '" + message + "'");
+
+        PhoneGlobals.getInstance().startService(getInstantTextIntent(phoneNumber, message,
+                component));
+    }
+
+    private void sendTextAndExit(String phoneNumber, String message, ComponentName component,
+            boolean setDefaultComponent) {
+        // Send the selected message immediately with no user interaction.
+        sendText(phoneNumber, message, component);
+
+        if (setDefaultComponent) {
+            final SharedPreferences prefs = PhoneGlobals.getInstance().getSharedPreferences(
+                    SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+            prefs.edit()
+                    .putString(KEY_INSTANT_TEXT_DEFAULT_COMPONENT, component.flattenToString())
+                    .apply();
+        }
+
+        // ...and show a brief confirmation to the user (since
+        // otherwise it's hard to be sure that anything actually
+        // happened.)
+        // TODO(klp): Ask the InCallUI to show a confirmation
+
+
+        // TODO: If the device is locked, this toast won't actually ever
+        // be visible!  (That's because we're about to dismiss the call
+        // screen, which means that the device will return to the
+        // keyguard.  But toasts aren't visible on top of the keyguard.)
+        // Possible fixes:
+        // (1) Is it possible to allow a specific Toast to be visible
+        //     on top of the keyguard?
+        // (2) Artifically delay the dismissCallScreen() call by 3
+        //     seconds to allow the toast to be seen?
+        // (3) Don't use a toast at all; instead use a transient state
+        //     of the InCallScreen (perhaps via the InCallUiState
+        //     progressIndication feature), and have that state be
+        //     visible for 3 seconds before calling dismissCallScreen().
+    }
+
+    /**
+     * Queries the System to determine what packages contain services that can handle the instant
+     * text response Action AND have permissions to do so.
+     */
+    private ArrayList<ComponentName> getPackagesWithInstantTextPermission() {
+        final PackageManager packageManager = PhoneGlobals.getInstance().getPackageManager();
+
+        final ArrayList<ComponentName> componentsWithPermission = new ArrayList<ComponentName>();
+
+        // Get list of all services set up to handle the Instant Text intent.
+        final List<ResolveInfo> infos = packageManager.queryIntentServices(
+                getInstantTextIntent("", null, null), 0);
+
+        // Collect all the valid services
+        for (ResolveInfo resolveInfo : infos) {
+            final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+            if (serviceInfo == null) {
+                Log.w(TAG, "Ignore package without proper service.");
+                continue;
+            }
+
+            // A Service is valid only if it requires the permission
+            // PERMISSION_SEND_RESPOND_VIA_MESSAGE
+            if (PERMISSION_SEND_RESPOND_VIA_MESSAGE.equals(serviceInfo.permission)) {
+                componentsWithPermission.add(new ComponentName(serviceInfo.packageName,
+                    serviceInfo.name));
+            }
+        }
+
+        return componentsWithPermission;
+    }
+
+    /**
+     * @param phoneNumber Must not be null.
+     * @param message Can be null. If message is null, the returned Intent will be configured to
+     * launch the SMS compose UI. If non-null, the returned Intent will cause the specified message
+     * to be sent with no interaction from the user.
+     * @param component The component that should handle this intent.
+     * @return Service Intent for the instant response.
+     */
+    private static Intent getInstantTextIntent(String phoneNumber, String message,
+            ComponentName component) {
+        final Uri uri = Uri.fromParts(Constants.SCHEME_SMSTO, phoneNumber, null);
+        final Intent intent = new Intent(TelephonyManager.ACTION_RESPOND_VIA_MESSAGE, uri);
+        if (message != null) {
+            intent.putExtra(Intent.EXTRA_TEXT, message);
+        } else {
+            intent.putExtra("exit_on_sent", true);
+            intent.putExtra("showUI", true);
+        }
+        if (component != null) {
+            intent.setComponent(component);
+        }
+        return intent;
+    }
+
+    public void rejectCallWithNewMessage(Call call) {
+        launchSmsCompose(call.getLatestConnection().getAddress());
+    }
+
+    private ComponentName getSmsService() {
+        if (DBG) log("sendTextToDefaultActivity()...");
+        final PackageManager packageManager = PhoneGlobals.getInstance().getPackageManager();
+
+        // Check to see if the default component to receive this intent is already saved
+        // and check to see if it still has the corrent permissions.
+        final SharedPreferences prefs = PhoneGlobals.getInstance().
+                getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+        final String flattenedName = prefs.getString(KEY_INSTANT_TEXT_DEFAULT_COMPONENT, null);
+        if (flattenedName != null) {
+            if (DBG) log("Default package was found." + flattenedName);
+
+            final ComponentName componentName = ComponentName.unflattenFromString(flattenedName);
+            ServiceInfo serviceInfo = null;
+            try {
+                serviceInfo = packageManager.getServiceInfo(componentName, 0);
+            } catch (PackageManager.NameNotFoundException e) {
+                Log.w(TAG, "Default service does not have permission.");
+            }
+
+            if (serviceInfo != null &&
+                    PERMISSION_SEND_RESPOND_VIA_MESSAGE.equals(serviceInfo.permission)) {
+                return componentName;
+            } else {
+                SharedPreferences.Editor editor = prefs.edit();
+                editor.remove(KEY_INSTANT_TEXT_DEFAULT_COMPONENT);
+                editor.apply();
+            }
+        }
+
+        final ArrayList<ComponentName> componentsWithPermission =
+            getPackagesWithInstantTextPermission();
+
+        final int size = componentsWithPermission.size();
+        if (size == 0) {
+            Log.e(TAG, "No appropriate package receiving the Intent. Don't send anything");
+            return null;
+        } else if (size == 1) {
+            return componentsWithPermission.get(0);
+        } else {
+            Log.v(TAG, "Choosing from one of the apps");
+            // TODO(klp): Add an app picker.
+            return componentsWithPermission.get(0);
+        }
+    }
+
+
+    public void rejectCallWithMessage(Call call, String message) {
+        final ComponentName componentName = getSmsService();
+
+        if (componentName != null) {
+            sendTextAndExit(call.getLatestConnection().getAddress(), message, componentName,
+                    false);
+        }
+    }
+
+    private static void log(String msg) {
+        Log.d(TAG, msg);
+    }
+}