1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.phone; 18 19 import android.content.ComponentName; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.SharedPreferences; 23 import android.content.res.Resources; 24 import android.net.Uri; 25 import android.os.Handler; 26 import android.os.Looper; 27 import android.os.Message; 28 import android.telephony.PhoneNumberUtils; 29 import android.telephony.TelephonyManager; 30 import android.text.TextUtils; 31 import android.util.Log; 32 import android.widget.Toast; 33 34 import com.android.internal.telephony.Call; 35 import com.android.internal.telephony.Connection; 36 import com.android.internal.telephony.PhoneConstants; 37 import com.android.internal.telephony.SmsApplication; 38 39 import java.util.ArrayList; 40 41 /** 42 * Helper class to manage the "Respond via Message" feature for incoming calls. 43 * 44 * @see com.android.phone.InCallScreen.internalRespondViaSms() 45 */ 46 public class RejectWithTextMessageManager { 47 private static final String TAG = RejectWithTextMessageManager.class.getSimpleName(); 48 private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2); 49 50 /** SharedPreferences file name for our persistent settings. */ 51 private static final String SHARED_PREFERENCES_NAME = "respond_via_sms_prefs"; 52 53 // Preference keys for the 4 "canned responses"; see RespondViaSmsManager$Settings. 54 // Since (for now at least) the number of messages is fixed at 4, and since 55 // SharedPreferences can't deal with arrays anyway, just store the messages 56 // as 4 separate strings. 57 private static final int NUM_CANNED_RESPONSES = 4; 58 private static final String KEY_CANNED_RESPONSE_PREF_1 = "canned_response_pref_1"; 59 private static final String KEY_CANNED_RESPONSE_PREF_2 = "canned_response_pref_2"; 60 private static final String KEY_CANNED_RESPONSE_PREF_3 = "canned_response_pref_3"; 61 private static final String KEY_CANNED_RESPONSE_PREF_4 = "canned_response_pref_4"; 62 63 /** 64 * Read the (customizable) canned responses from SharedPreferences, 65 * or from defaults if the user has never actually brought up 66 * the Settings UI. 67 * 68 * This method does disk I/O (reading the SharedPreferences file) 69 * so don't call it from the main thread. 70 * 71 * @see com.android.phone.RejectWithTextMessageManager.Settings 72 */ 73 public static ArrayList<String> loadCannedResponses() { 74 if (DBG) log("loadCannedResponses()..."); 75 76 final SharedPreferences prefs = PhoneGlobals.getInstance().getSharedPreferences( 77 SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); 78 final Resources res = PhoneGlobals.getInstance().getResources(); 79 80 final ArrayList<String> responses = new ArrayList<String>(NUM_CANNED_RESPONSES); 81 82 // Note the default values here must agree with the corresponding 83 // android:defaultValue attributes in respond_via_sms_settings.xml. 84 85 responses.add(0, prefs.getString(KEY_CANNED_RESPONSE_PREF_1, 86 res.getString(R.string.respond_via_sms_canned_response_1))); 87 responses.add(1, prefs.getString(KEY_CANNED_RESPONSE_PREF_2, 88 res.getString(R.string.respond_via_sms_canned_response_2))); 89 responses.add(2, prefs.getString(KEY_CANNED_RESPONSE_PREF_3, 90 res.getString(R.string.respond_via_sms_canned_response_3))); 91 responses.add(3, prefs.getString(KEY_CANNED_RESPONSE_PREF_4, 92 res.getString(R.string.respond_via_sms_canned_response_4))); 93 return responses; 94 } 95 96 private static void showMessageSentToast(final String phoneNumber) { 97 // ...and show a brief confirmation to the user (since 98 // otherwise it's hard to be sure that anything actually 99 // happened.) 100 // Ugly hack to show a toaster from a service. 101 (new Thread(new Runnable() { 102 @Override 103 public void run() { 104 Looper.prepare(); 105 Handler innerHandler = new Handler() { 106 @Override 107 public void handleMessage(Message message) { 108 final Resources res = PhoneGlobals.getInstance().getResources(); 109 final String formatString = res.getString( 110 R.string.respond_via_sms_confirmation_format); 111 final String confirmationMsg = String.format(formatString, phoneNumber); 112 Toast.makeText(PhoneGlobals.getInstance(), confirmationMsg, 113 Toast.LENGTH_LONG).show(); 114 } 115 116 @Override 117 public void dispatchMessage(Message message) { 118 handleMessage(message); 119 } 120 }; 121 122 Message message = innerHandler.obtainMessage(); 123 innerHandler.dispatchMessage(message); 124 Looper.loop(); 125 } 126 })).start(); 127 128 // TODO: If the device is locked, this toast won't actually ever 129 // be visible! (That's because we're about to dismiss the call 130 // screen, which means that the device will return to the 131 // keyguard. But toasts aren't visible on top of the keyguard.) 132 // Possible fixes: 133 // (1) Is it possible to allow a specific Toast to be visible 134 // on top of the keyguard? 135 // (2) Artificially delay the dismissCallScreen() call by 3 136 // seconds to allow the toast to be seen? 137 // (3) Don't use a toast at all; instead use a transient state 138 // of the InCallScreen (perhaps via the InCallUiState 139 // progressIndication feature), and have that state be 140 // visible for 3 seconds before calling dismissCallScreen(). 141 } 142 143 /** 144 * Reject the call with the specified message. If message is null this call is ignored. 145 */ 146 public static void rejectCallWithMessage(String phoneNumber, String message) { 147 if (message != null) { 148 final ComponentName component = 149 SmsApplication.getDefaultRespondViaMessageApplication( 150 PhoneGlobals.getInstance(), true /*updateIfNeeded*/); 151 if (component != null) { 152 // Build and send the intent 153 final Uri uri = Uri.fromParts(Constants.SCHEME_SMSTO, phoneNumber, null); 154 final Intent intent = new Intent(TelephonyManager.ACTION_RESPOND_VIA_MESSAGE, uri); 155 intent.putExtra(Intent.EXTRA_TEXT, message); 156 showMessageSentToast(phoneNumber); 157 intent.setComponent(component); 158 PhoneGlobals.getInstance().startService(intent); 159 } 160 } 161 } 162 163 /** 164 * @return true if the "Respond via SMS" feature should be enabled 165 * for the specified incoming call. 166 * 167 * The general rule is that we *do* allow "Respond via SMS" except for 168 * the few (relatively rare) cases where we know for sure it won't 169 * work, namely: 170 * - a bogus or blank incoming number 171 * - a call from a SIP address 172 * - a "call presentation" that doesn't allow the number to be revealed 173 * 174 * In all other cases, we allow the user to respond via SMS. 175 * 176 * Note that this behavior isn't perfect; for example we have no way 177 * to detect whether the incoming call is from a landline (with most 178 * networks at least), so we still enable this feature even though 179 * SMSes to that number will silently fail. 180 */ 181 public static boolean allowRespondViaSmsForCall( 182 com.android.services.telephony.common.Call call, Connection conn) { 183 if (DBG) log("allowRespondViaSmsForCall(" + call + ")..."); 184 185 // First some basic sanity checks: 186 if (call == null) { 187 Log.w(TAG, "allowRespondViaSmsForCall: null ringingCall!"); 188 return false; 189 } 190 if (!(call.getState() == com.android.services.telephony.common.Call.State.INCOMING) && 191 !(call.getState() == 192 com.android.services.telephony.common.Call.State.CALL_WAITING)) { 193 // The call is in some state other than INCOMING or WAITING! 194 // (This should almost never happen, but it *could* 195 // conceivably happen if the ringing call got disconnected by 196 // the network just *after* we got it from the CallManager.) 197 Log.w(TAG, "allowRespondViaSmsForCall: ringingCall not ringing! state = " 198 + call.getState()); 199 return false; 200 } 201 202 if (conn == null) { 203 // The call doesn't have any connections! (Again, this can 204 // happen if the ringing call disconnects at the exact right 205 // moment, but should almost never happen in practice.) 206 Log.w(TAG, "allowRespondViaSmsForCall: null Connection!"); 207 return false; 208 } 209 210 // Check the incoming number: 211 final String number = conn.getAddress(); 212 if (DBG) log("- number: '" + number + "'"); 213 if (TextUtils.isEmpty(number)) { 214 Log.w(TAG, "allowRespondViaSmsForCall: no incoming number!"); 215 return false; 216 } 217 if (PhoneNumberUtils.isUriNumber(number)) { 218 // The incoming number is actually a URI (i.e. a SIP address), 219 // not a regular PSTN phone number, and we can't send SMSes to 220 // SIP addresses. 221 // (TODO: That might still be possible eventually, though. Is 222 // there some SIP-specific equivalent to sending a text message?) 223 Log.i(TAG, "allowRespondViaSmsForCall: incoming 'number' is a SIP address."); 224 return false; 225 } 226 227 // Finally, check the "call presentation": 228 int presentation = conn.getNumberPresentation(); 229 if (DBG) log("- presentation: " + presentation); 230 if (presentation == PhoneConstants.PRESENTATION_RESTRICTED) { 231 // PRESENTATION_RESTRICTED means "caller-id blocked". 232 // The user isn't allowed to see the number in the first 233 // place, so obviously we can't let you send an SMS to it. 234 Log.i(TAG, "allowRespondViaSmsForCall: PRESENTATION_RESTRICTED."); 235 return false; 236 } 237 238 // Is there a valid SMS application on the phone? 239 if (SmsApplication.getDefaultRespondViaMessageApplication(PhoneGlobals.getInstance(), 240 true /*updateIfNeeded*/) == null) { 241 return false; 242 } 243 244 // TODO: with some carriers (in certain countries) you *can* actually 245 // tell whether a given number is a mobile phone or not. So in that 246 // case we could potentially return false here if the incoming call is 247 // from a land line. 248 249 // If none of the above special cases apply, it's OK to enable the 250 // "Respond via SMS" feature. 251 return true; 252 } 253 254 private static void log(String msg) { 255 Log.d(TAG, msg); 256 } 257 } 258