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