RespondViaSmsManager.java revision f4ef613d8e6f203248d459cd77a3a7074c8f817c
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 17package com.android.phone; 18 19import com.android.internal.telephony.Call; 20import com.android.internal.telephony.Connection; 21import com.android.internal.telephony.Phone; 22 23import android.app.ActionBar; 24import android.app.AlertDialog; 25import android.app.Dialog; 26import android.content.Context; 27import android.content.DialogInterface; 28import android.content.Intent; 29import android.content.SharedPreferences; 30import android.content.res.Resources; 31import android.net.Uri; 32import android.os.Bundle; 33import android.os.SystemProperties; 34import android.preference.EditTextPreference; 35import android.preference.Preference; 36import android.preference.PreferenceActivity; 37import android.telephony.PhoneNumberUtils; 38import android.text.TextUtils; 39import android.util.Log; 40import android.view.MenuItem; 41import android.view.View; 42import android.widget.AdapterView; 43import android.widget.ArrayAdapter; 44import android.widget.ListView; 45import android.widget.Toast; 46 47import java.util.Arrays; 48 49/** 50 * Helper class to manage the "Respond via SMS" feature for incoming calls. 51 * @see InCallScreen.internalRespondViaSms() 52 */ 53public class RespondViaSmsManager { 54 private static final String TAG = "RespondViaSmsManager"; 55 private static final boolean DBG = 56 (PhoneApp.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1); 57 // Do not check in with VDBG = true, since that may write PII to the system log. 58 private static final boolean VDBG = false; 59 60 /** 61 * Reference to the InCallScreen activity that owns us. This may be 62 * null if we haven't been initialized yet *or* after the InCallScreen 63 * activity has been destroyed. 64 */ 65 private InCallScreen mInCallScreen; 66 67 /** 68 * The popup showing the list of canned responses. 69 * 70 * This is an AlertDialog containing a ListView showing the possible 71 * choices. This may be null if the InCallScreen hasn't ever called 72 * showRespondViaSmsPopup() yet, or if the popup was visible once but 73 * then got dismissed. 74 */ 75 private Dialog mPopup; 76 77 /** The array of "canned responses"; see loadCannedResponses(). */ 78 private String[] mCannedResponses; 79 80 /** SharedPreferences file name for our persistent settings. */ 81 private static final String SHARED_PREFERENCES_NAME = "respond_via_sms_prefs"; 82 83 // Preference keys for the 4 "canned responses"; see RespondViaSmsManager$Settings. 84 // Since (for now at least) the number of messages is fixed at 4, and since 85 // SharedPreferences can't deal with arrays anyway, just store the messages 86 // as 4 separate strings. 87 private static final int NUM_CANNED_RESPONSES = 4; 88 private static final String KEY_CANNED_RESPONSE_PREF_1 = "canned_response_pref_1"; 89 private static final String KEY_CANNED_RESPONSE_PREF_2 = "canned_response_pref_2"; 90 private static final String KEY_CANNED_RESPONSE_PREF_3 = "canned_response_pref_3"; 91 private static final String KEY_CANNED_RESPONSE_PREF_4 = "canned_response_pref_4"; 92 93 94 /** 95 * RespondViaSmsManager constructor. 96 */ 97 public RespondViaSmsManager() { 98 } 99 100 public void setInCallScreenInstance(InCallScreen inCallScreen) { 101 mInCallScreen = inCallScreen; 102 103 // Prefetch shared preferences to make the first canned response lookup faster 104 // (and to prevent StrictMode violation) 105 mInCallScreen.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); 106 } 107 108 /** 109 * Brings up the "Respond via SMS" popup for an incoming call. 110 * 111 * @param ringingCall the current incoming call 112 */ 113 public void showRespondViaSmsPopup(Call ringingCall) { 114 if (DBG) log("showRespondViaSmsPopup()..."); 115 116 ListView lv = new ListView(mInCallScreen); 117 118 // Refresh the array of "canned responses". 119 mCannedResponses = loadCannedResponses(); 120 121 // Build the list: start with the canned responses, but manually add 122 // "Custom message..." as the last choice. 123 int numPopupItems = mCannedResponses.length + 1; 124 String[] popupItems = Arrays.copyOf(mCannedResponses, numPopupItems); 125 popupItems[numPopupItems - 1] = mInCallScreen.getResources() 126 .getString(R.string.respond_via_sms_custom_message); 127 128 ArrayAdapter<String> adapter = 129 new ArrayAdapter<String>(mInCallScreen, 130 android.R.layout.simple_list_item_1, 131 android.R.id.text1, 132 popupItems); 133 lv.setAdapter(adapter); 134 135 // Create a RespondViaSmsItemClickListener instance to handle item 136 // clicks from the popup. 137 // (Note we create a fresh instance for each incoming call, and 138 // stash away the call's phone number, since we can't necessarily 139 // assume this call will still be ringing when the user finally 140 // chooses a response.) 141 142 Connection c = ringingCall.getLatestConnection(); 143 if (VDBG) log("- connection: " + c); 144 145 if (c == null) { 146 // Uh oh -- the "ringingCall" doesn't have any connections any more. 147 // (In other words, it's no longer ringing.) This is rare, but can 148 // happen if the caller hangs up right at the exact moment the user 149 // selects the "Respond via SMS" option. 150 // There's nothing to do here (since the incoming call is gone), 151 // so just bail out. 152 Log.i(TAG, "showRespondViaSmsPopup: null connection; bailing out..."); 153 return; 154 } 155 156 // TODO: at this point we probably should re-check c.getAddress() 157 // and c.getNumberPresentation() for validity. (i.e. recheck the 158 // same cases in InCallTouchUi.showIncomingCallWidget() where we 159 // should have disallowed the "respond via SMS" feature in the 160 // first place.) 161 162 String phoneNumber = c.getAddress(); 163 if (VDBG) log("- phoneNumber: " + phoneNumber); 164 lv.setOnItemClickListener(new RespondViaSmsItemClickListener(phoneNumber)); 165 166 AlertDialog.Builder builder = new AlertDialog.Builder(mInCallScreen) 167 .setCancelable(true) 168 .setOnCancelListener(new RespondViaSmsCancelListener()) 169 .setView(lv); 170 mPopup = builder.create(); 171 mPopup.show(); 172 } 173 174 /** 175 * Dismiss the "Respond via SMS" popup if it's visible. 176 * 177 * This is safe to call even if the popup is already dismissed, and 178 * even if you never called showRespondViaSmsPopup() in the first 179 * place. 180 */ 181 public void dismissPopup() { 182 if (mPopup != null) { 183 mPopup.dismiss(); // safe even if already dismissed 184 mPopup = null; 185 } 186 } 187 188 public boolean isShowingPopup() { 189 return mPopup != null && mPopup.isShowing(); 190 } 191 192 /** 193 * OnItemClickListener for the "Respond via SMS" popup. 194 */ 195 public class RespondViaSmsItemClickListener implements AdapterView.OnItemClickListener { 196 // Phone number to send the SMS to. 197 private String mPhoneNumber; 198 199 public RespondViaSmsItemClickListener(String phoneNumber) { 200 mPhoneNumber = phoneNumber; 201 } 202 203 /** 204 * Handles the user selecting an item from the popup. 205 */ 206 @Override 207 public void onItemClick(AdapterView<?> parent, // The ListView 208 View view, // The TextView that was clicked 209 int position, 210 long id) { 211 if (DBG) log("RespondViaSmsItemClickListener.onItemClick(" + position + ")..."); 212 String message = (String) parent.getItemAtPosition(position); 213 if (VDBG) log("- message: '" + message + "'"); 214 215 // The "Custom" choice is a special case. 216 // (For now, it's guaranteed to be the last item.) 217 if (position == (parent.getCount() - 1)) { 218 // Take the user to the standard SMS compose UI. 219 launchSmsCompose(mPhoneNumber); 220 } else { 221 // Send the selected message immediately with no user interaction. 222 sendText(mPhoneNumber, message); 223 224 // ...and show a brief confirmation to the user (since 225 // otherwise it's hard to be sure that anything actually 226 // happened.) 227 final Resources res = mInCallScreen.getResources(); 228 String formatString = res.getString(R.string.respond_via_sms_confirmation_format); 229 String confirmationMsg = String.format(formatString, mPhoneNumber); 230 Toast.makeText(mInCallScreen, 231 confirmationMsg, 232 Toast.LENGTH_LONG).show(); 233 234 // TODO: If the device is locked, this toast won't actually ever 235 // be visible! (That's because we're about to dismiss the call 236 // screen, which means that the device will return to the 237 // keyguard. But toasts aren't visible on top of the keyguard.) 238 // Possible fixes: 239 // (1) Is it possible to allow a specific Toast to be visible 240 // on top of the keyguard? 241 // (2) Artifically delay the dismissCallScreen() call by 3 242 // seconds to allow the toast to be seen? 243 // (3) Don't use a toast at all; instead use a transient state 244 // of the InCallScreen (perhaps via the InCallUiState 245 // progressIndication feature), and have that state be 246 // visible for 3 seconds before calling dismissCallScreen(). 247 } 248 249 // At this point the user is done dealing with the incoming call, so 250 // there's no reason to keep it around. (It's also confusing for 251 // the "incoming call" icon in the status bar to still be visible.) 252 // So reject the call now. 253 mInCallScreen.hangupRingingCall(); 254 255 dismissPopup(); 256 257 final Phone.State state = PhoneApp.getInstance().mCM.getState(); 258 if (state == Phone.State.IDLE) { 259 // There's no other phone call to interact. Exit the entire in-call screen. 260 PhoneApp.getInstance().dismissCallScreen(); 261 } else { 262 // The user is still in the middle of other phone calls, so we should keep the 263 // in-call screen. 264 mInCallScreen.requestUpdateScreen(); 265 } 266 } 267 } 268 269 /** 270 * OnCancelListener for the "Respond via SMS" popup. 271 */ 272 public class RespondViaSmsCancelListener implements DialogInterface.OnCancelListener { 273 public RespondViaSmsCancelListener() { 274 } 275 276 /** 277 * Handles the user canceling the popup, either by touching 278 * outside the popup or by pressing Back. 279 */ 280 @Override 281 public void onCancel(DialogInterface dialog) { 282 if (DBG) log("RespondViaSmsCancelListener.onCancel()..."); 283 284 dismissPopup(); 285 286 final Phone.State state = PhoneApp.getInstance().mCM.getState(); 287 if (state == Phone.State.IDLE) { 288 // This means the incoming call is already hung up when the user chooses not to 289 // use "Respond via SMS" feature. Let's just exit the whole in-call screen. 290 PhoneApp.getInstance().dismissCallScreen(); 291 } else { 292 293 // If the user cancels the popup, this presumably means that 294 // they didn't actually mean to bring up the "Respond via SMS" 295 // UI in the first place (and instead want to go back to the 296 // state where they can either answer or reject the call.) 297 // So restart the ringer and bring back the regular incoming 298 // call UI. 299 300 // This will have no effect if the incoming call isn't still ringing. 301 PhoneApp.getInstance().notifier.restartRinger(); 302 303 // We hid the MultiWaveView widget way back in 304 // InCallTouchUi.onTrigger(), when the user first selected 305 // the "SMS" trigger. 306 // 307 // To bring it back, just force the entire InCallScreen to 308 // update itself based on the current telephony state. 309 // (Assuming the incoming call is still ringing, this will 310 // cause the incoming call widget to reappear.) 311 mInCallScreen.requestUpdateScreen(); 312 } 313 } 314 } 315 316 /** 317 * Sends a text message without any interaction from the user. 318 */ 319 private void sendText(String phoneNumber, String message) { 320 if (VDBG) log("sendText: number " 321 + phoneNumber + ", message '" + message + "'"); 322 323 Uri uri = Uri.fromParts(Constants.SCHEME_SMSTO, phoneNumber, null); 324 Intent intent = new Intent("com.android.mms.intent.action.SENDTO_NO_CONFIRMATION", uri); 325 intent.putExtra(Intent.EXTRA_TEXT, message); 326 mInCallScreen.startService(intent); 327 } 328 329 /** 330 * Brings up the standard SMS compose UI. 331 */ 332 private void launchSmsCompose(String phoneNumber) { 333 if (VDBG) log("launchSmsCompose: number " + phoneNumber); 334 335 Uri uri = Uri.fromParts(Constants.SCHEME_SMSTO, phoneNumber, null); 336 Intent intent = new Intent("com.android.mms.intent.action.SENDTO_NO_CONFIRMATION", uri); 337 intent.putExtra("exit_on_sent", true); 338 intent.putExtra("showUI", true); 339 340 if (VDBG) log("- Launching SMS compose UI: " + intent); 341 mInCallScreen.startService(intent); 342 } 343 344 345 /** 346 * Settings activity under "Call settings" to let you manage the 347 * canned responses; see respond_via_sms_settings.xml 348 */ 349 public static class Settings extends PreferenceActivity 350 implements Preference.OnPreferenceChangeListener { 351 @Override 352 protected void onCreate(Bundle icicle) { 353 super.onCreate(icicle); 354 if (DBG) log("Settings: onCreate()..."); 355 356 getPreferenceManager().setSharedPreferencesName(SHARED_PREFERENCES_NAME); 357 358 // This preference screen is ultra-simple; it's just 4 plain 359 // <EditTextPreference>s, one for each of the 4 "canned responses". 360 // 361 // The only nontrivial thing we do here is copy the text value of 362 // each of those EditTextPreferences and use it as the preference's 363 // "title" as well, so that the user will immediately see all 4 364 // strings when they arrive here. 365 // 366 // Also, listen for change events (since we'll need to update the 367 // title any time the user edits one of the strings.) 368 369 addPreferencesFromResource(R.xml.respond_via_sms_settings); 370 371 EditTextPreference pref; 372 pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_1); 373 pref.setTitle(pref.getText()); 374 pref.setOnPreferenceChangeListener(this); 375 376 pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_2); 377 pref.setTitle(pref.getText()); 378 pref.setOnPreferenceChangeListener(this); 379 380 pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_3); 381 pref.setTitle(pref.getText()); 382 pref.setOnPreferenceChangeListener(this); 383 384 pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_4); 385 pref.setTitle(pref.getText()); 386 pref.setOnPreferenceChangeListener(this); 387 388 ActionBar actionBar = getActionBar(); 389 if (actionBar != null) { 390 // android.R.id.home will be triggered in onOptionsItemSelected() 391 actionBar.setDisplayHomeAsUpEnabled(true); 392 } 393 } 394 395 // Preference.OnPreferenceChangeListener implementation 396 @Override 397 public boolean onPreferenceChange(Preference preference, Object newValue) { 398 if (DBG) log("onPreferenceChange: key = " + preference.getKey()); 399 if (VDBG) log(" preference = '" + preference + "'"); 400 if (VDBG) log(" newValue = '" + newValue + "'"); 401 402 EditTextPreference pref = (EditTextPreference) preference; 403 404 // Copy the new text over to the title, just like in onCreate(). 405 // (Watch out: onPreferenceChange() is called *before* the 406 // Preference itself gets updated, so we need to use newValue here 407 // rather than pref.getText().) 408 pref.setTitle((String) newValue); 409 410 return true; // means it's OK to update the state of the Preference with the new value 411 } 412 413 @Override 414 public boolean onOptionsItemSelected(MenuItem item) { 415 final int itemId = item.getItemId(); 416 if (itemId == android.R.id.home) { // See ActionBar#setDisplayHomeAsUpEnabled() 417 CallFeaturesSetting.goUpToTopLevelSetting(this); 418 return true; 419 } 420 return super.onOptionsItemSelected(item); 421 } 422 } 423 424 /** 425 * Read the (customizable) canned responses from SharedPreferences, 426 * or from defaults if the user has never actually brought up 427 * the Settings UI. 428 * 429 * This method does disk I/O (reading the SharedPreferences file) 430 * so don't call it from the main thread. 431 * 432 * @see RespondViaSmsManager.Settings 433 */ 434 private String[] loadCannedResponses() { 435 if (DBG) log("loadCannedResponses()..."); 436 437 SharedPreferences prefs = 438 mInCallScreen.getSharedPreferences(SHARED_PREFERENCES_NAME, 439 Context.MODE_PRIVATE); 440 final Resources res = mInCallScreen.getResources(); 441 442 String[] responses = new String[NUM_CANNED_RESPONSES]; 443 444 // Note the default values here must agree with the corresponding 445 // android:defaultValue attributes in respond_via_sms_settings.xml. 446 447 responses[0] = prefs.getString(KEY_CANNED_RESPONSE_PREF_1, 448 res.getString(R.string.respond_via_sms_canned_response_1)); 449 responses[1] = prefs.getString(KEY_CANNED_RESPONSE_PREF_2, 450 res.getString(R.string.respond_via_sms_canned_response_2)); 451 responses[2] = prefs.getString(KEY_CANNED_RESPONSE_PREF_3, 452 res.getString(R.string.respond_via_sms_canned_response_3)); 453 responses[3] = prefs.getString(KEY_CANNED_RESPONSE_PREF_4, 454 res.getString(R.string.respond_via_sms_canned_response_4)); 455 return responses; 456 } 457 458 /** 459 * @return true if the "Respond via SMS" feature should be enabled 460 * for the specified incoming call. 461 * 462 * The general rule is that we *do* allow "Respond via SMS" except for 463 * the few (relatively rare) cases where we know for sure it won't 464 * work, namely: 465 * - a bogus or blank incoming number 466 * - a call from a SIP address 467 * - a "call presentation" that doesn't allow the number to be revealed 468 * 469 * In all other cases, we allow the user to respond via SMS. 470 * 471 * Note that this behavior isn't perfect; for example we have no way 472 * to detect whether the incoming call is from a landline (with most 473 * networks at least), so we still enable this feature even though 474 * SMSes to that number will silently fail. 475 */ 476 public static boolean allowRespondViaSmsForCall(Call ringingCall) { 477 if (DBG) log("allowRespondViaSmsForCall(" + ringingCall + ")..."); 478 479 // First some basic sanity checks: 480 if (ringingCall == null) { 481 Log.w(TAG, "allowRespondViaSmsForCall: null ringingCall!"); 482 return false; 483 } 484 if (!ringingCall.isRinging()) { 485 // The call is in some state other than INCOMING or WAITING! 486 // (This should almost never happen, but it *could* 487 // conceivably happen if the ringing call got disconnected by 488 // the network just *after* we got it from the CallManager.) 489 Log.w(TAG, "allowRespondViaSmsForCall: ringingCall not ringing! state = " 490 + ringingCall.getState()); 491 return false; 492 } 493 Connection conn = ringingCall.getLatestConnection(); 494 if (conn == null) { 495 // The call doesn't have any connections! (Again, this can 496 // happen if the ringing call disconnects at the exact right 497 // moment, but should almost never happen in practice.) 498 Log.w(TAG, "allowRespondViaSmsForCall: null Connection!"); 499 return false; 500 } 501 502 // Check the incoming number: 503 final String number = conn.getAddress(); 504 if (DBG) log("- number: '" + number + "'"); 505 if (TextUtils.isEmpty(number)) { 506 Log.w(TAG, "allowRespondViaSmsForCall: no incoming number!"); 507 return false; 508 } 509 if (PhoneNumberUtils.isUriNumber(number)) { 510 // The incoming number is actually a URI (i.e. a SIP address), 511 // not a regular PSTN phone number, and we can't send SMSes to 512 // SIP addresses. 513 // (TODO: That might still be possible eventually, though. Is 514 // there some SIP-specific equivalent to sending a text message?) 515 Log.i(TAG, "allowRespondViaSmsForCall: incoming 'number' is a SIP address."); 516 return false; 517 } 518 519 // Finally, check the "call presentation": 520 int presentation = conn.getNumberPresentation(); 521 if (DBG) log("- presentation: " + presentation); 522 if (presentation == Connection.PRESENTATION_RESTRICTED) { 523 // PRESENTATION_RESTRICTED means "caller-id blocked". 524 // The user isn't allowed to see the number in the first 525 // place, so obviously we can't let you send an SMS to it. 526 Log.i(TAG, "allowRespondViaSmsForCall: PRESENTATION_RESTRICTED."); 527 return false; 528 } 529 530 // TODO: with some carriers (in certain countries) you *can* actually 531 // tell whether a given number is a mobile phone or not. So in that 532 // case we could potentially return false here if the incoming call is 533 // from a land line. 534 535 // If none of the above special cases apply, it's OK to enable the 536 // "Respond via SMS" feature. 537 return true; 538 } 539 540 541 private static void log(String msg) { 542 Log.d(TAG, msg); 543 } 544} 545