DO NOT MERGE. Port "respond via SMS message" feature to new Telecomm. (2/4)

Bug: 15275904
Bug: 15196474
Change-Id: I3e2ee62b3e32ad5715457fee1b0e714f88ecea8e
diff --git a/Android.mk b/Android.mk
index 48eece0..852d0a4 100644
--- a/Android.mk
+++ b/Android.mk
@@ -3,6 +3,7 @@
 # Build the Telecomm service.
 include $(CLEAR_VARS)
 
+LOCAL_JAVA_LIBRARIES := telephony-common
 LOCAL_STATIC_JAVA_LIBRARIES := \
         guava \
 
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 2a90f86..c49dca9 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -177,5 +177,12 @@
             </intent-filter>
         </receiver>
 
+        <activity android:name=".RespondViaSmsSettings$Settings"
+                  android:label="@string/respond_via_sms_setting_title"
+                  android:configChanges="orientation|screenSize|keyboardHidden">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+            </intent-filter>
+        </activity>
     </application>
 </manifest>
diff --git a/res/menu/respond_via_message_settings_menu.xml b/res/menu/respond_via_message_settings_menu.xml
new file mode 100644
index 0000000..6f5e246
--- /dev/null
+++ b/res/menu/respond_via_message_settings_menu.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 Google Inc.
+
+     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.
+-->
+
+<!-- Menu for Respond-via-Message settings screen. -->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item android:id="@+id/respond_via_message_reset"
+          android:title="@string/respond_via_sms_menu_reset_default_activity" />
+</menu>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 0c46053..747eca7 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -49,4 +49,30 @@
     <!-- Content description of the speakerphone enabled notification icon for
          accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
     <string name="accessibility_speakerphone_enabled">Speakerphone enabled.</string>
+
+    <!-- Canned response for the "Respond via SMS" feature for incoming calls. [CHAR LIMIT=35] -->
+    <string name="respond_via_sms_canned_response_1">Can\'t talk now. What\'s up?</string>
+    <!-- Canned response for the "Respond via SMS" feature for incoming calls. [CHAR LIMIT=35] -->
+    <string name="respond_via_sms_canned_response_2">I\'ll call you right back.</string>
+    <!-- Canned response for the "Respond via SMS" feature for incoming calls. [CHAR LIMIT=35] -->
+    <string name="respond_via_sms_canned_response_3">I\'ll call you later.</string>
+    <!-- Canned response for the "Respond via SMS" feature for incoming calls. [CHAR LIMIT=35] -->
+    <string name="respond_via_sms_canned_response_4">Can\'t talk now. Call me later?</string>
+
+    <!-- Title of settings screen for managing the "Respond via SMS" feature. [CHAR LIMIT=30] -->
+    <string name="respond_via_sms_setting_title">Quick responses</string>
+    <!-- Slightly more verbose title of settings screen for managing the
+         "Respond via SMS" feature. [CHAR LIMIT=30] -->
+    <string name="respond_via_sms_setting_title_2">Edit quick responses</string>
+    <!-- Settings summary string for the "Respond via SMS" feature. [CHAR LIMIT=40] -->
+    <string name="respond_via_sms_setting_summary"></string>
+    <!-- Dialog title when changing a string for the "Respond via SMS" feature. [CHAR LIMIT=30] -->
+    <string name="respond_via_sms_edittext_dialog_title">Quick response</string>
+    <!-- Menu option in  "Respond via SMS" that allows user to reset the default
+         activity used to handle "Respond via SMS" [CHAR LIMIT=30] -->
+    <string name="respond_via_sms_menu_reset_default_activity">Reset default app</string>
+
+    <!-- "Respond via SMS": Confirmation message shown after sending
+        a text response. [CHAR LIMIT=40] -->
+    <string name="respond_via_sms_confirmation_format">Message sent to <xliff:g id="phone_number">%s</xliff:g>.</string>
 </resources>
diff --git a/res/xml/respond_via_sms_settings.xml b/res/xml/respond_via_sms_settings.xml
new file mode 100644
index 0000000..a8eb46c
--- /dev/null
+++ b/res/xml/respond_via_sms_settings.xml
@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+
+<!-- Settings screen that lets the user manage the canned responses
+     for the "Respond via SMS" feature; see RespondViaSmsManager.java -->
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+     android:title="@string/respond_via_sms_setting_title_2">
+
+    <!-- Ultra-simple implementation for now: just provide 4 fixed slots
+         with customizable strings. -->
+
+    <!-- TODO: Potential UI improvements:
+         (1) Allow editing the strings in place, rather than having to tap
+             each one and edit it via the popup dialog.
+         (2) Allow reordering the strings by drag-and-drop.
+         (3) Provide an "Add new string..." option? -->
+
+    <!-- The defaultValues here must agree with the values used with
+         prefs.getString() in RespondViaSmsManager.loadCannedResponses(). -->
+
+    <!-- Use MultiLineTitleEditTextPreference instead of the standard
+         EditTextPreference here, to allow the preference "title" to wrap
+         onto multiple lines if the customized messages are long enough. -->
+
+    <com.android.telecomm.MultiLineTitleEditTextPreference
+        android:key="canned_response_pref_1"
+        android:defaultValue="@string/respond_via_sms_canned_response_1"
+        android:dialogTitle="@string/respond_via_sms_edittext_dialog_title" />
+
+    <com.android.telecomm.MultiLineTitleEditTextPreference
+        android:key="canned_response_pref_2"
+        android:defaultValue="@string/respond_via_sms_canned_response_2"
+        android:dialogTitle="@string/respond_via_sms_edittext_dialog_title" />
+
+    <com.android.telecomm.MultiLineTitleEditTextPreference
+        android:key="canned_response_pref_3"
+        android:defaultValue="@string/respond_via_sms_canned_response_3"
+        android:dialogTitle="@string/respond_via_sms_edittext_dialog_title" />
+
+    <com.android.telecomm.MultiLineTitleEditTextPreference
+        android:key="canned_response_pref_4"
+        android:defaultValue="@string/respond_via_sms_canned_response_4"
+        android:dialogTitle="@string/respond_via_sms_edittext_dialog_title" />
+
+</PreferenceScreen>
diff --git a/src/com/android/telecomm/Call.java b/src/com/android/telecomm/Call.java
index 607aeb1..104ff4e 100644
--- a/src/com/android/telecomm/Call.java
+++ b/src/com/android/telecomm/Call.java
@@ -27,6 +27,7 @@
 import android.telecomm.CallServiceDescriptor;
 import android.telecomm.CallState;
 import android.telecomm.GatewayInfo;
+import android.telecomm.Response;
 import android.telecomm.TelecommConstants;
 import android.telephony.DisconnectCause;
 import android.telephony.PhoneNumberUtils;
@@ -36,11 +37,12 @@
 import com.android.internal.telephony.CallerInfoAsyncQuery;
 import com.android.internal.telephony.CallerInfoAsyncQuery.OnQueryCompleteListener;
 
+import com.android.internal.telephony.SmsApplication;
 import com.android.telecomm.ContactsAsyncHelper.OnImageLoadCompleteListener;
-import com.google.android.collect.Sets;
 import com.google.common.base.Preconditions;
+import com.google.common.collect.Sets;
 
-import java.util.HashSet;
+import java.util.Collections;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Locale;
@@ -68,6 +70,7 @@
         void onConfirmedConferenceCall(Call call);
         void onParentChanged(Call call);
         void onChildrenChanged(Call call);
+        void onCannedSmsResponsesLoaded(Call call);
     }
 
     private static final OnQueryCompleteListener sCallerInfoQueryListener =
@@ -196,6 +199,12 @@
 
     private List<Call> mChildCalls = new LinkedList<>();
 
+    /** Set of text message responses allowed for this call, if applicable. */
+    private List<String> mCannedSmsResponses = Collections.EMPTY_LIST;
+
+    /** Whether an attempt has been made to load the text message responses. */
+    private boolean mCannedSmsResponsesLoadingStarted = false;
+
     /**
      * Creates an empty call object.
      *
@@ -218,6 +227,7 @@
         mGatewayInfo = gatewayInfo;
         mIsIncoming = isIncoming;
         mIsConference = isConference;
+        maybeLoadCannedSmsResponses();
     }
 
     void addListener(Listener listener) {
@@ -261,6 +271,7 @@
         if (mState != newState) {
             Log.v(this, "setState %s -> %s", mState, newState);
             mState = newState;
+            maybeLoadCannedSmsResponses();
         }
     }
 
@@ -506,7 +517,7 @@
                 // TODO(santoscordon): Once we move State handling from CallsManager to Call, we
                 // will not need to set RINGING state prior to calling reject.
                 setState(CallState.RINGING);
-                reject();
+                reject(false, null);
             } else {
                 // TODO(santoscordon): Make this class (not CallsManager) responsible for changing
                 // the call state to RINGING.
@@ -670,8 +681,11 @@
 
     /**
      * Rejects the call if it is ringing.
+     *
+     * @param rejectWithMessage Whether to send a text message as part of the call rejection.
+     * @param textMessage An optional text message to send as part of the rejection.
      */
-    void reject() {
+    void reject(boolean rejectWithMessage, String textMessage) {
         Preconditions.checkNotNull(mCallService);
 
         // Check to verify that the call is still in the ringing state. A call can change states
@@ -854,6 +868,66 @@
     }
 
     /**
+     * Return whether the user can respond to this {@code Call} via an SMS message.
+     *
+     * @return true if the "Respond via SMS" feature should be enabled
+     * for this 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.
+     */
+    boolean isRespondViaSmsCapable() {
+        if (mState != CallState.RINGING) {
+            return false;
+        }
+
+        if (getHandle() == null) {
+            // No incoming number known or call presentation is "PRESENTATION_RESTRICTED", in
+            // other words, the user should not be able to see the incoming phone number.
+            return false;
+        }
+
+        if (PhoneNumberUtils.isUriNumber(getHandle().toString())) {
+            // The incoming number is actually a URI (i.e. a SIP address),
+            // not a regular PSTN phone number, and we can't send SMSes to
+            // SIP addresses.
+            // (TODO: That might still be possible eventually, though. Is
+            // there some SIP-specific equivalent to sending a text message?)
+            return false;
+        }
+
+        // Is there a valid SMS application on the phone?
+        if (SmsApplication.getDefaultRespondViaMessageApplication(TelecommApp.getInstance(),
+                true /*updateIfNeeded*/) == null) {
+            return false;
+        }
+
+        // TODO: with some carriers (in certain countries) you *can* actually
+        // tell whether a given number is a mobile phone or not. So in that
+        // case we could potentially return false here if the incoming call is
+        // from a land line.
+
+        // If none of the above special cases apply, it's OK to enable the
+        // "Respond via SMS" feature.
+        return true;
+    }
+
+    List<String> getCannedSmsResponses() {
+        return mCannedSmsResponses;
+    }
+
+    /**
      * @return True if the call is ringing, else logs the action name.
      */
     private boolean isRinging(String actionName) {
@@ -934,4 +1008,33 @@
             mCallerInfo.cachedPhotoIcon = photoIcon;
         }
     }
+
+    private void maybeLoadCannedSmsResponses() {
+        if (mIsIncoming && isRespondViaSmsCapable() && !mCannedSmsResponsesLoadingStarted) {
+            Log.d(this, "maybeLoadCannedSmsResponses: starting task to load messages");
+            mCannedSmsResponsesLoadingStarted = true;
+            RespondViaSmsManager.getInstance().loadCannedTextMessages(
+                    new Response<Void, List<String>>() {
+                        @Override
+                        public void onResult(Void request, List<String>... result) {
+                            if (result.length > 0) {
+                                Log.d(this, "maybeLoadCannedSmsResponses: got %s", result[0]);
+                                mCannedSmsResponses = result[0];
+                                for (Listener l : mListeners) {
+                                    l.onCannedSmsResponsesLoaded(Call.this);
+                                }
+                            }
+                        }
+
+                        @Override
+                        public void onError(Void request, int code, String msg) {
+                            Log.w(Call.this, "Error obtaining canned SMS responses: %d %s", code,
+                                    msg);
+                        }
+                    }
+            );
+        } else {
+            Log.d(this, "maybeLoadCannedSmsResponses: doing nothing");
+        }
+    }
 }
diff --git a/src/com/android/telecomm/CallsManager.java b/src/com/android/telecomm/CallsManager.java
index 6057350..5e5f710 100644
--- a/src/com/android/telecomm/CallsManager.java
+++ b/src/com/android/telecomm/CallsManager.java
@@ -57,12 +57,13 @@
                 CallServiceDescriptor oldDescriptor,
                 CallServiceDescriptor newDescriptor);
         void onIncomingCallAnswered(Call call);
-        void onIncomingCallRejected(Call call);
+        void onIncomingCallRejected(Call call, boolean rejectWithMessage, String textMessage);
         void onForegroundCallChanged(Call oldForegroundCall, Call newForegroundCall);
         void onAudioStateChanged(CallAudioState oldAudioState, CallAudioState newAudioState);
         void onRequestingRingback(Call call, boolean ringback);
         void onIsConferenceCapableChanged(Call call, boolean isConferenceCapable);
         void onIsConferencedChanged(Call call);
+        void onCannedSmsResponsesLoaded(Call call);
     }
 
     private static final CallsManager INSTANCE = new CallsManager();
@@ -121,6 +122,7 @@
         mListeners.add(app.getMissedCallNotifier());
         mListeners.add(mDtmfLocalTonePlayer);
         mListeners.add(mHeadsetMediaButton);
+        mListeners.add(RespondViaSmsManager.getInstance());
     }
 
     @Override
@@ -213,6 +215,13 @@
         }
     }
 
+    @Override
+    public void onCannedSmsResponsesLoaded(Call call) {
+        for (CallsManagerListener listener : mListeners) {
+            listener.onCannedSmsResponsesLoaded(call);
+        }
+    }
+
     ImmutableCollection<Call> getCalls() {
         return ImmutableList.copyOf(mCalls);
     }
@@ -338,14 +347,14 @@
      * app through {@link InCallAdapter} after Telecomm notifies it of an incoming call followed by
      * the user opting to reject said call.
      */
-    void rejectCall(Call call) {
+    void rejectCall(Call call, boolean rejectWithMessage, String textMessage) {
         if (!mCalls.contains(call)) {
             Log.i(this, "Request to reject a non-existent call %s", call);
         } else {
             for (CallsManagerListener listener : mListeners) {
-                listener.onIncomingCallRejected(call);
+                listener.onIncomingCallRejected(call, rejectWithMessage, textMessage);
             }
-            call.reject();
+            call.reject(rejectWithMessage, textMessage);
         }
     }
 
diff --git a/src/com/android/telecomm/CallsManagerListenerBase.java b/src/com/android/telecomm/CallsManagerListenerBase.java
index 60fdbf6..55fa0a1 100644
--- a/src/com/android/telecomm/CallsManagerListenerBase.java
+++ b/src/com/android/telecomm/CallsManagerListenerBase.java
@@ -60,7 +60,7 @@
     }
 
     @Override
-    public void onIncomingCallRejected(Call call) {
+    public void onIncomingCallRejected(Call call, boolean rejectWithMessage, String textMessage) {
     }
 
     @Override
@@ -82,4 +82,8 @@
     @Override
     public void onIsConferencedChanged(Call call) {
     }
+
+    @Override
+    public void onCannedSmsResponsesLoaded(Call call) {
+    }
 }
diff --git a/src/com/android/telecomm/InCallAdapter.java b/src/com/android/telecomm/InCallAdapter.java
index 7ae12ab..7937e95 100644
--- a/src/com/android/telecomm/InCallAdapter.java
+++ b/src/com/android/telecomm/InCallAdapter.java
@@ -45,54 +45,111 @@
     private final class InCallAdapterHandler extends Handler {
         @Override
         public void handleMessage(Message msg) {
-            Call call = null;
-            if (msg.obj != null) {
-                call = mCallIdMapper.getCall(msg.obj);
-                if (call == null) {
-                    Log.w(this, "Unknown call id: %s, msg: %d", msg.obj, msg.what);
-                    return;
-                }
-            }
-
+            Call call;
             switch (msg.what) {
                 case MSG_ANSWER_CALL:
-                    mCallsManager.answerCall(call);
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        mCallsManager.answerCall(call);
+                    } else {
+                        Log.w(this, "answerCall, unknown call id: %s", msg.obj);
+                    }
                     break;
                 case MSG_REJECT_CALL:
-                    mCallsManager.rejectCall(call);
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    try {
+                        call = mCallIdMapper.getCall(args.arg1);
+                        boolean rejectWithMessage = args.argi1 == 1;
+                        String textMessage = (String) args.arg2;
+                        if (call != null) {
+                            mCallsManager.rejectCall(call, rejectWithMessage, textMessage);
+                        } else {
+                            Log.w(this, "setRingback, unknown call id: %s", args.arg1);
+                        }
+                    } finally {
+                        args.recycle();
+                    }
                     break;
                 case MSG_PLAY_DTMF_TONE:
-                    mCallsManager.playDtmfTone(call, (char) msg.arg1);
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        mCallsManager.playDtmfTone(call, (char) msg.arg1);
+                    } else {
+                        Log.w(this, "playDtmfTone, unknown call id: %s", msg.obj);
+                    }
                     break;
                 case MSG_STOP_DTMF_TONE:
-                    mCallsManager.stopDtmfTone(call);
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        mCallsManager.stopDtmfTone(call);
+                    } else {
+                        Log.w(this, "stopDtmfTone, unknown call id: %s", msg.obj);
+                    }
                     break;
                 case MSG_POST_DIAL_CONTINUE:
+                    call = mCallIdMapper.getCall(msg.obj);
                     mCallsManager.postDialContinue(call, msg.arg1 == 1);
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        mCallsManager.postDialContinue(call, msg.arg1 == 1);
+                    } else {
+                        Log.w(this, "postDialContinue, unknown call id: %s", msg.obj);
+                    }
                     break;
                 case MSG_DISCONNECT_CALL:
-                    mCallsManager.disconnectCall(call);
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        mCallsManager.disconnectCall(call);
+                    } else {
+                        Log.w(this, "disconnectCall, unknown call id: %s", msg.obj);
+                    }
                     break;
                 case MSG_HOLD_CALL:
-                    mCallsManager.holdCall(call);
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        mCallsManager.holdCall(call);
+                    } else {
+                        Log.w(this, "holdCall, unknown call id: %s", msg.obj);
+                    }
                     break;
                 case MSG_UNHOLD_CALL:
-                    mCallsManager.unholdCall(call);
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        mCallsManager.unholdCall(call);
+                    } else {
+                        Log.w(this, "unholdCall, unknown call id: %s", msg.obj);
+                    }
                     break;
                 case MSG_HANDOFF_CALL:
-                    mCallsManager.startHandoffForCall(call);
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        mCallsManager.startHandoffForCall(call);
+                    } else {
+                        Log.w(this, "startHandoffForCall, unknown call id: %s", msg.obj);
+                    }
                     break;
                 case MSG_MUTE:
-                    mCallsManager.mute(msg.arg1 == 1 ? true : false);
+                    mCallsManager.mute(msg.arg1 == 1);
                     break;
                 case MSG_SET_AUDIO_ROUTE:
                     mCallsManager.setAudioRoute(msg.arg1);
                     break;
                 case MSG_CONFERENCE:
-                    mCallsManager.conference(call);
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        mCallsManager.conference(call);
+                    } else {
+                        Log.w(this, "conference, unknown call id: %s", msg.obj);
+                    }
+
                     break;
                 case MSG_SPLIT_FROM_CONFERENCE:
-                    call.splitFromConference();
+                    call = mCallIdMapper.getCall(msg.obj);
+                    if (call != null) {
+                        call.splitFromConference();
+                    } else {
+                        Log.w(this, "splitFromConference, unknown call id: %s", msg.obj);
+                    }
                     break;
             }
         }
@@ -119,10 +176,14 @@
 
     /** {@inheritDoc} */
     @Override
-    public void rejectCall(String callId) {
-        Log.d(this, "rejectCall(%s)", callId);
+    public void rejectCall(String callId, boolean rejectWithMessage, String textMessage) {
+        Log.d(this, "rejectCall(%s,%b,%s)", callId, rejectWithMessage, textMessage);
         mCallIdMapper.checkValidCallId(callId);
-        mHandler.obtainMessage(MSG_REJECT_CALL, callId).sendToTarget();
+        SomeArgs args = SomeArgs.obtain();
+        args.arg1 = callId;
+        args.argi1 = rejectWithMessage ? 1 : 0;
+        args.arg2 = textMessage;
+        mHandler.obtainMessage(MSG_REJECT_CALL, args).sendToTarget();
     }
 
     /** {@inheritDoc} */
diff --git a/src/com/android/telecomm/InCallController.java b/src/com/android/telecomm/InCallController.java
index 434c709..3dd0539 100644
--- a/src/com/android/telecomm/InCallController.java
+++ b/src/com/android/telecomm/InCallController.java
@@ -29,14 +29,13 @@
 import android.telecomm.CallServiceDescriptor;
 import android.telecomm.CallState;
 import android.telecomm.InCallCall;
-import android.telecomm.CallState;
 
 import com.android.internal.telecomm.IInCallService;
 import com.google.common.collect.ImmutableCollection;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
-import java.util.Set;
 
 /**
  * Binds to {@link IInCallService} and provides the service to {@link CallsManager} through which it
@@ -92,10 +91,12 @@
             bind();
         } else {
             Log.i(this, "Adding call: %s", call);
-            mCallIdMapper.addCall(call);
-            try {
-                mInCallService.addCall(toInCallCall(call));
-            } catch (RemoteException ignored) {
+            if (mCallIdMapper.getCallId(call) == null) {
+                mCallIdMapper.addCall(call);
+                try {
+                    mInCallService.addCall(toInCallCall(call));
+                } catch (RemoteException ignored) {
+                }
             }
         }
     }
@@ -168,6 +169,11 @@
         updateCall(call);
     }
 
+    @Override
+    public void onCannedSmsResponsesLoaded(Call call) {
+        updateCall(call);
+    }
+
     void bringToForeground(boolean showDialpad) {
         if (mInCallService != null) {
             try {
@@ -308,9 +314,14 @@
             }
         }
 
+        if (call.isRespondViaSmsCapable()) {
+            capabilities |= CallCapabilities.RESPOND_VIA_TEXT;
+        }
+
         return new InCallCall(callId, state, call.getDisconnectCause(), call.getDisconnectMessage(),
-                capabilities, connectTimeMillis, call.getHandle(), call.getGatewayInfo(),
-                descriptor, call.getHandoffCallServiceDescriptor(), parentCallId, childCallIds);
+                call.getCannedSmsResponses(), capabilities, connectTimeMillis, call.getHandle(),
+                call.getGatewayInfo(), descriptor, call.getHandoffCallServiceDescriptor(),
+                parentCallId, childCallIds);
     }
 
 }
diff --git a/src/com/android/telecomm/MultiLineTitleEditTextPreference.java b/src/com/android/telecomm/MultiLineTitleEditTextPreference.java
new file mode 100644
index 0000000..d40cb21
--- /dev/null
+++ b/src/com/android/telecomm/MultiLineTitleEditTextPreference.java
@@ -0,0 +1,73 @@
+/*
+ * 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.telecomm;
+
+import android.content.Context;
+import android.preference.EditTextPreference;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.TextView;
+
+/**
+ * Ultra-simple subclass of EditTextPreference that allows the "title" to wrap
+ * onto multiple lines.
+ *
+ * (By default, the title of an EditTextPreference is singleLine="true"; see
+ * preference_holo.xml under frameworks/base.  But in the "Respond via SMS"
+ * settings UI we want titles to be multi-line, since the customized messages
+ * might be fairly long, and should be able to wrap.)
+ *
+ * TODO: This is pretty cumbersome; it would be nicer for the framework to
+ * either allow modifying the title's attributes in XML, or at least provide
+ * some way from Java (given an EditTextPreference) to reach inside and get a
+ * handle to the "title" TextView.
+ *
+ * TODO: Also, it would reduce clutter if this could be an inner class in
+ * RespondViaSmsManager.java, but then there would be no way to reference the
+ * class from XML.  That's because
+ *    <com.android.telecomm.MultiLineTitleEditTextPreference ... />
+ * isn't valid XML syntax due to the "$" character.  And Preference
+ * elements don't have a "class" attribute, so you can't do something like
+ * <view class="com.android.telecomm.Foo$Bar"> as you can with regular views.
+ */
+public class MultiLineTitleEditTextPreference extends EditTextPreference {
+    public MultiLineTitleEditTextPreference(Context context, AttributeSet attrs, int defStyle) {
+        super(context, attrs, defStyle);
+    }
+
+    public MultiLineTitleEditTextPreference(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public MultiLineTitleEditTextPreference(Context context) {
+        super(context);
+    }
+
+    // The "title" TextView inside an EditTextPreference defaults to
+    // singleLine="true" (see preference_holo.xml under frameworks/base.)
+    // We override onBindView() purely to look up that TextView and call
+    // setSingleLine(false) on it.
+    @Override
+    protected void onBindView(View view) {
+        super.onBindView(view);
+
+        TextView textView = (TextView) view.findViewById(com.android.internal.R.id.title);
+        if (textView != null) {
+            textView.setSingleLine(false);
+        }
+    }
+}
diff --git a/src/com/android/telecomm/RespondViaSmsManager.java b/src/com/android/telecomm/RespondViaSmsManager.java
new file mode 100644
index 0000000..bf1a2a5
--- /dev/null
+++ b/src/com/android/telecomm/RespondViaSmsManager.java
@@ -0,0 +1,189 @@
+/*
+ * 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.telecomm;
+
+import com.android.internal.os.SomeArgs;
+import com.android.internal.telephony.SmsApplication;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.telecomm.Response;
+import android.telephony.TelephonyManager;
+import android.widget.Toast;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Helper class to manage the "Respond via Message" feature for incoming calls.
+ */
+public class RespondViaSmsManager extends CallsManagerListenerBase {
+    private static final String SCHEME_SMSTO = "smsto";
+
+    /** 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 int MSG_CANNED_TEXT_MESSAGES_READY = 1;
+    private static final int MSG_SHOW_SENT_TOAST = 2;
+
+    private static final RespondViaSmsManager sInstance = new RespondViaSmsManager();
+
+    private final Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_CANNED_TEXT_MESSAGES_READY:
+                    SomeArgs args = (SomeArgs) msg.obj;
+                    try {
+                        Response<Void, List<String>> response =
+                                (Response<Void, List<String>>) args.arg1;
+                        List<String> textMessages =
+                                (List<String>) args.arg2;
+                        if (textMessages != null) {
+                            response.onResult(null, textMessages);
+                        } else {
+                            response.onError(null, 0, null);
+                        }
+                    } finally {
+                        args.recycle();
+                    }
+                    break;
+                case MSG_SHOW_SENT_TOAST:
+                    showMessageSentToast((String) msg.obj);
+                    break;
+            }
+        }
+    };
+
+    public static RespondViaSmsManager getInstance() { return sInstance; }
+
+    private RespondViaSmsManager() {}
+
+    /**
+     * Read the (customizable) canned responses from SharedPreferences,
+     * or from defaults if the user has never actually brought up
+     * the Settings UI.
+     *
+     * The interface of this method is asynchronous since it does disk I/O.
+     *
+     * @param response An object to receive an async reply, which will be called from
+     *                 the main thread.
+     */
+    public void loadCannedTextMessages(final Response<Void, List<String>> response) {
+        new Thread() {
+            @Override
+            public void run() {
+                Log.d(RespondViaSmsManager.this, "loadCannedResponses() starting");
+                final SharedPreferences prefs = TelecommApp.getInstance().getSharedPreferences(
+                        SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+                final Resources res = TelecommApp.getInstance().getInstance().getResources();
+
+                final ArrayList<String> textMessages = 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.
+
+                textMessages.add(0, prefs.getString(KEY_CANNED_RESPONSE_PREF_1,
+                        res.getString(R.string.respond_via_sms_canned_response_1)));
+                textMessages.add(1, prefs.getString(KEY_CANNED_RESPONSE_PREF_2,
+                        res.getString(R.string.respond_via_sms_canned_response_2)));
+                textMessages.add(2, prefs.getString(KEY_CANNED_RESPONSE_PREF_3,
+                        res.getString(R.string.respond_via_sms_canned_response_3)));
+                textMessages.add(3, prefs.getString(KEY_CANNED_RESPONSE_PREF_4,
+                        res.getString(R.string.respond_via_sms_canned_response_4)));
+
+                Log.d(RespondViaSmsManager.this,
+                        "loadCannedResponses() completed, found responses: %s",
+                        textMessages.toString());
+
+                SomeArgs args = SomeArgs.obtain();
+                args.arg1 = response;
+                args.arg2 = textMessages;
+                mHandler.obtainMessage(MSG_CANNED_TEXT_MESSAGES_READY, args).sendToTarget();
+            }
+        }.start();
+    }
+
+    @Override
+    public void onIncomingCallRejected(Call call, boolean rejectWithMessage, String textMessage) {
+        if (rejectWithMessage) {
+            rejectCallWithMessage(call.getHandle().getSchemeSpecificPart(), textMessage);
+        }
+    }
+
+    private void showMessageSentToast(final String phoneNumber) {
+        // ...and show a brief confirmation to the user (since
+        // otherwise it's hard to be sure that anything actually
+        // happened.)
+        final Resources res = TelecommApp.getInstance().getResources();
+        final String formatString = res.getString(
+                R.string.respond_via_sms_confirmation_format);
+        final String confirmationMsg = String.format(formatString, phoneNumber);
+        Toast.makeText(TelecommApp.getInstance(), confirmationMsg,
+                Toast.LENGTH_LONG).show();
+
+        // 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) Artificially 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().
+    }
+
+    /**
+     * Reject the call with the specified message. If message is null this call is ignored.
+     */
+    private void rejectCallWithMessage(String phoneNumber, String textMessage) {
+        if (textMessage != null) {
+            final ComponentName component =
+                    SmsApplication.getDefaultRespondViaMessageApplication(
+                            TelecommApp.getInstance(), true /*updateIfNeeded*/);
+            if (component != null) {
+                // Build and send the intent
+                final Uri uri = Uri.fromParts(SCHEME_SMSTO, phoneNumber, null);
+                final Intent intent = new Intent(TelephonyManager.ACTION_RESPOND_VIA_MESSAGE, uri);
+                intent.putExtra(Intent.EXTRA_TEXT, textMessage);
+                mHandler.obtainMessage(MSG_SHOW_SENT_TOAST, phoneNumber).sendToTarget();
+                intent.setComponent(component);
+                TelecommApp.getInstance().startService(intent);
+            }
+        }
+    }
+}
diff --git a/src/com/android/telecomm/RespondViaSmsSettings.java b/src/com/android/telecomm/RespondViaSmsSettings.java
new file mode 100644
index 0000000..1adf45c
--- /dev/null
+++ b/src/com/android/telecomm/RespondViaSmsSettings.java
@@ -0,0 +1,170 @@
+/*
+ * 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.telecomm;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.preference.EditTextPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+
+/**
+ * Helper class to manage the "Respond via SMS Message" feature for incoming calls.
+ */
+public class RespondViaSmsSettings {
+    /** 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";
+
+    // TODO: This class is newly copied into Telecomm (com.android.telecomm) from it previous
+    // location in Telephony (com.android.phone). User's preferences stored in the old location
+    // will be lost. We need code here to migrate KLP -> LMP settings values.
+
+    /**
+     * Settings activity under "Call settings" to let you manage the
+     * canned responses; see respond_via_sms_settings.xml
+     */
+    public static class Settings extends PreferenceActivity
+            implements Preference.OnPreferenceChangeListener {
+        @Override
+        protected void onCreate(Bundle icicle) {
+            super.onCreate(icicle);
+            Log.d(this, "Settings: onCreate()...");
+
+            getPreferenceManager().setSharedPreferencesName(SHARED_PREFERENCES_NAME);
+
+            // This preference screen is ultra-simple; it's just 4 plain
+            // <EditTextPreference>s, one for each of the 4 "canned responses".
+            //
+            // The only nontrivial thing we do here is copy the text value of
+            // each of those EditTextPreferences and use it as the preference's
+            // "title" as well, so that the user will immediately see all 4
+            // strings when they arrive here.
+            //
+            // Also, listen for change events (since we'll need to update the
+            // title any time the user edits one of the strings.)
+
+            addPreferencesFromResource(R.xml.respond_via_sms_settings);
+
+            EditTextPreference pref;
+            pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_1);
+            pref.setTitle(pref.getText());
+            pref.setOnPreferenceChangeListener(this);
+
+            pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_2);
+            pref.setTitle(pref.getText());
+            pref.setOnPreferenceChangeListener(this);
+
+            pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_3);
+            pref.setTitle(pref.getText());
+            pref.setOnPreferenceChangeListener(this);
+
+            pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_4);
+            pref.setTitle(pref.getText());
+            pref.setOnPreferenceChangeListener(this);
+
+            ActionBar actionBar = getActionBar();
+            if (actionBar != null) {
+                // android.R.id.home will be triggered in onOptionsItemSelected()
+                actionBar.setDisplayHomeAsUpEnabled(true);
+            }
+        }
+
+        // Preference.OnPreferenceChangeListener implementation
+        @Override
+        public boolean onPreferenceChange(Preference preference, Object newValue) {
+            Log.d(this, "onPreferenceChange: key = %s", preference.getKey());
+            Log.d(this, "  preference = '%s'", preference);
+            Log.d(this, "  newValue = '%s'", newValue);
+
+            EditTextPreference pref = (EditTextPreference) preference;
+
+            // Copy the new text over to the title, just like in onCreate().
+            // (Watch out: onPreferenceChange() is called *before* the
+            // Preference itself gets updated, so we need to use newValue here
+            // rather than pref.getText().)
+            pref.setTitle((String) newValue);
+
+            return true;  // means it's OK to update the state of the Preference with the new value
+        }
+
+        @Override
+        public boolean onOptionsItemSelected(MenuItem item) {
+            final int itemId = item.getItemId();
+            switch (itemId) {
+                case android.R.id.home:
+                    goUpToTopLevelSetting(this);
+                    return true;
+                case R.id.respond_via_message_reset:
+                    // Reset the preferences settings
+                    SharedPreferences prefs = getSharedPreferences(
+                            SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+                    SharedPreferences.Editor editor = prefs.edit();
+                    editor.remove(KEY_INSTANT_TEXT_DEFAULT_COMPONENT);
+                    editor.apply();
+
+                    return true;
+                default:
+            }
+            return super.onOptionsItemSelected(item);
+        }
+
+        @Override
+        public boolean onCreateOptionsMenu(Menu menu) {
+            getMenuInflater().inflate(R.menu.respond_via_message_settings_menu, menu);
+            return super.onCreateOptionsMenu(menu);
+        }
+    }
+
+    /**
+     * Finish current Activity and go up to the top level Settings.
+     */
+    public static void goUpToTopLevelSetting(Activity activity) {
+        Intent intent = new Intent();
+        try {
+            intent.setClassName(
+                    activity.createPackageContext("com.android.phone", 0),
+                    "com.android.phone.CallFeaturesSetting");
+        } catch (PackageManager.NameNotFoundException e) {
+            Log.w(RespondViaSmsSettings.class,
+                    "Exception building package context com.android.phone", e);
+            return;
+        }
+        intent.setAction(Intent.ACTION_MAIN);
+        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        activity.startActivity(intent);
+        activity.finish();
+     }
+}
diff --git a/src/com/android/telecomm/Ringer.java b/src/com/android/telecomm/Ringer.java
index 5b5beff..0b54a2b 100644
--- a/src/com/android/telecomm/Ringer.java
+++ b/src/com/android/telecomm/Ringer.java
@@ -105,7 +105,7 @@
     }
 
     @Override
-    public void onIncomingCallRejected(Call call) {
+    public void onIncomingCallRejected(Call call, boolean rejectWithMessage, String textMessage) {
         onRespondedToIncomingCall(call);
     }