Add support for text messages
Change-Id: I3883de331fd881133e29755fde3c7b772823ad70
diff --git a/src/com/android/phone/CallCommandService.java b/src/com/android/phone/CallCommandService.java
index 35c507a..6a3848c 100644
--- a/src/com/android/phone/CallCommandService.java
+++ b/src/com/android/phone/CallCommandService.java
@@ -38,14 +38,17 @@
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, AudioRouter audioRouter) {
+ DTMFTonePlayer dtmfTonePlayer, AudioRouter audioRouter,
+ RejectWithTextMessageManager rejectWithTextMessageManager) {
mContext = context;
mCallManager = callManager;
mCallModeler = callModeler;
mDtmfTonePlayer = dtmfTonePlayer;
mAudioRouter = audioRouter;
+ mRejectWithTextMessageManager = rejectWithTextMessageManager;
}
/**
@@ -67,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) {
diff --git a/src/com/android/phone/CallHandlerServiceProxy.java b/src/com/android/phone/CallHandlerServiceProxy.java
index 2762c98..165e693 100644
--- a/src/com/android/phone/CallHandlerServiceProxy.java
+++ b/src/com/android/phone/CallHandlerServiceProxy.java
@@ -34,6 +34,7 @@
import com.android.services.telephony.common.ICallHandlerService;
import com.android.services.telephony.common.ICallCommandService;
+import java.util.ArrayList;
import java.util.List;
/**
@@ -78,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 {
diff --git a/src/com/android/phone/CallModeler.java b/src/com/android/phone/CallModeler.java
index 517a5a9..c70da9a 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;
@@ -76,10 +75,13 @@
private final HashMap<Connection, Call> mCallMap = Maps.newHashMap();
private final AtomicInteger mNextCallId = new AtomicInteger(CALL_ID_START_VALUE);
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);
}
@@ -144,7 +146,10 @@
call.setState(Call.State.INCOMING);
for (int i = 0; i < mListeners.size(); ++i) {
- mListeners.get(i).onUpdate(Lists.newArrayList(call), false);
+ if (call != null) {
+ mListeners.get(i).onIncoming(call,
+ mRejectWithTextMessageManager.loadCannedResponses());
+ }
}
}
@@ -269,6 +274,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 39fc4d9..0e27a99 100644
--- a/src/com/android/phone/DTMFTonePlayer.java
+++ b/src/com/android/phone/DTMFTonePlayer.java
@@ -31,6 +31,7 @@
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;
@@ -115,6 +116,10 @@
}
@Override
+ public void onIncoming(Call call, ArrayList<String> textResponses) {
+ }
+
+ @Override
public void onUpdate(List<Call> calls, boolean full) {
logD("Call updated");
checkCallState();
diff --git a/src/com/android/phone/PhoneGlobals.java b/src/com/android/phone/PhoneGlobals.java
index 914db15..3e81175 100644
--- a/src/com/android/phone/PhoneGlobals.java
+++ b/src/com/android/phone/PhoneGlobals.java
@@ -176,6 +176,7 @@
private CallModeler callModeler;
private CallStateMonitor callStateMonitor;
private DTMFTonePlayer dtmfTonePlayer;
+ private RejectWithTextMessageManager rejectWithTextMessageManager;
private IBluetoothHeadsetPhone mBluetoothPhone;
private Ringer ringer;
@@ -545,8 +546,11 @@
// 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);
@@ -556,7 +560,7 @@
// Service used by in-call UI to control calls
callCommandService = new CallCommandService(this, mCM, callModeler, dtmfTonePlayer,
- audioRouter);
+ 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);
+ }
+}