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