RespondViaSmsManager.java revision 2c8c40738e9b8a8e767aa061721ebaa5b5591a4c
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.telephony.SmsManager; 36import android.util.Log; 37import android.view.MenuItem; 38import android.view.View; 39import android.widget.AdapterView; 40import android.widget.ArrayAdapter; 41import android.widget.ListView; 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 // TODO: at this point we probably should re-check c.getAddress() 140 // and c.getNumberPresentation() for validity. (i.e. recheck the 141 // same cases in InCallTouchUi.showIncomingCallWidget() where we 142 // should have disallowed the "respond via SMS" feature in the 143 // first place.) 144 145 String phoneNumber = c.getAddress(); 146 if (DBG) log("- phoneNumber: " + phoneNumber); // STOPSHIP: don't log PII 147 lv.setOnItemClickListener(new RespondViaSmsItemClickListener(phoneNumber)); 148 149 AlertDialog.Builder builder = new AlertDialog.Builder(mInCallScreen) 150 .setCancelable(true) 151 .setOnCancelListener(new RespondViaSmsCancelListener()) 152 .setView(lv); 153 mPopup = builder.create(); 154 mPopup.show(); 155 } 156 157 /** 158 * Dismiss the "Respond via SMS" popup if it's visible. 159 * 160 * This is safe to call even if the popup is already dismissed, and 161 * even if you never called showRespondViaSmsPopup() in the first 162 * place. 163 */ 164 public void dismissPopup() { 165 if (mPopup != null) { 166 mPopup.dismiss(); // safe even if already dismissed 167 mPopup = null; 168 } 169 } 170 171 /** 172 * OnItemClickListener for the "Respond via SMS" popup. 173 */ 174 public class RespondViaSmsItemClickListener implements AdapterView.OnItemClickListener { 175 // Phone number to send the SMS to. 176 private String mPhoneNumber; 177 178 public RespondViaSmsItemClickListener(String phoneNumber) { 179 mPhoneNumber = phoneNumber; 180 } 181 182 /** 183 * Handles the user selecting an item from the popup. 184 */ 185 public void onItemClick(AdapterView<?> parent, // The ListView 186 View view, // The TextView that was clicked 187 int position, 188 long id) { 189 if (DBG) log("RespondViaSmsItemClickListener.onItemClick(" + position + ")..."); 190 String message = (String) parent.getItemAtPosition(position); 191 if (DBG) log("- message: '" + message + "'"); 192 193 // The "Custom" choice is a special case. 194 // (For now, it's guaranteed to be the last item.) 195 if (position == (parent.getCount() - 1)) { 196 // Take the user to the standard SMS compose UI. 197 launchSmsCompose(mPhoneNumber); 198 } else { 199 // Send the selected message immediately with no user interaction. 200 sendText(mPhoneNumber, message); 201 } 202 203 PhoneApp.getInstance().dismissCallScreen(); 204 } 205 } 206 207 /** 208 * OnCancelListener for the "Respond via SMS" popup. 209 */ 210 public class RespondViaSmsCancelListener implements DialogInterface.OnCancelListener { 211 public RespondViaSmsCancelListener() { 212 } 213 214 /** 215 * Handles the user canceling the popup, either by touching 216 * outside the popup or by pressing Back. 217 */ 218 public void onCancel(DialogInterface dialog) { 219 if (DBG) log("RespondViaSmsCancelListener.onCancel()..."); 220 221 // If the user cancels the popup, this presumably means that 222 // they didn't actually mean to bring up the "Respond via SMS" 223 // UI in the first place (and instead want to go back to the 224 // state where they can either answer or reject the call.) 225 // So restart the ringer and bring back the regular incoming 226 // call UI. 227 228 // This will have no effect if the incoming call isn't still ringing. 229 PhoneApp.getInstance().notifier.restartRinger(); 230 231 // We hid the MultiWaveView widget way back in 232 // InCallTouchUi.onTrigger(), when the user first selected 233 // the "SMS" trigger. 234 // 235 // To bring it back, just force the entire InCallScreen to 236 // update itself based on the current telephony state. 237 // (Assuming the incoming call is still ringing, this will 238 // cause the incoming call widget to reappear.) 239 mInCallScreen.requestUpdateScreen(); 240 } 241 } 242 243 /** 244 * Sends a text message without any interaction from the user. 245 */ 246 private void sendText(String phoneNumber, String message) { 247 // STOPSHIP: disable all logging of PII (everywhere in this file) 248 if (DBG) log("sendText: number " 249 + phoneNumber + ", message '" + message + "'"); 250 251 // TODO: This code should use the new 252 // com.android.mms.intent.action.SENDTO_NO_CONFIRMATION 253 // intent once change https://android-git.corp.google.com/g/114664 254 // gets checked in. 255 // But use the old-school SmsManager API for now. 256 257 final SmsManager smsManager = SmsManager.getDefault(); 258 smsManager.sendTextMessage(phoneNumber, 259 null /* scAddress; null means "use default" */, 260 message, 261 null /* sentIntent */, 262 null /* deliveryIntent */); 263 } 264 265 /** 266 * Brings up the standard SMS compose UI. 267 */ 268 private void launchSmsCompose(String phoneNumber) { 269 if (DBG) log("launchSmsCompose: number " + phoneNumber); 270 271 // TODO: confirm with SMS guys that this is the correct intent to use. 272 Uri uri = Uri.fromParts(Constants.SCHEME_SMS, phoneNumber, null); 273 Intent intent = new Intent(Intent.ACTION_VIEW, uri); 274 275 if (DBG) log("- Launching SMS compose UI: " + intent); 276 mInCallScreen.startActivity(intent); 277 278 // TODO: One open issue here: if the user selects "Custom message" 279 // for an incoming call while the device was locked, and the user 280 // does *not* have a secure keyguard set, we bring up the 281 // non-secure keyguard at this point :-( 282 // Instead, we should immediately go to the SMS compose UI. 283 // 284 // I *believe* the fix is for the SMS compose activity to set the 285 // FLAG_DISMISS_KEYGUARD window flag (which will cause the 286 // keyguard to be dismissed *only* if it is not a secure lock 287 // keyguard.) 288 // 289 // But it there an equivalent way for me to accomplish that here, 290 // without needing to change the SMS app? 291 // 292 // In any case, I'm pretty sure the SMS UI should *not* to set 293 // FLAG_SHOW_WHEN_LOCKED, since we do want the force the user to 294 // enter their lock pattern or PIN at this point if they have a 295 // secure keyguard set. 296 } 297 298 299 /** 300 * Settings activity under "Call settings" to let you manage the 301 * canned responses; see respond_via_sms_settings.xml 302 */ 303 public static class Settings extends PreferenceActivity 304 implements Preference.OnPreferenceChangeListener { 305 @Override 306 protected void onCreate(Bundle icicle) { 307 super.onCreate(icicle); 308 if (DBG) log("Settings: onCreate()..."); 309 310 getPreferenceManager().setSharedPreferencesName(SHARED_PREFERENCES_NAME); 311 312 // This preference screen is ultra-simple; it's just 4 plain 313 // <EditTextPreference>s, one for each of the 4 "canned responses". 314 // 315 // The only nontrivial thing we do here is copy the text value of 316 // each of those EditTextPreferences and use it as the preference's 317 // "title" as well, so that the user will immediately see all 4 318 // strings when they arrive here. 319 // 320 // Also, listen for change events (since we'll need to update the 321 // title any time the user edits one of the strings.) 322 323 addPreferencesFromResource(R.xml.respond_via_sms_settings); 324 325 EditTextPreference pref; 326 pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_1); 327 pref.setTitle(pref.getText()); 328 pref.setOnPreferenceChangeListener(this); 329 330 pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_2); 331 pref.setTitle(pref.getText()); 332 pref.setOnPreferenceChangeListener(this); 333 334 pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_3); 335 pref.setTitle(pref.getText()); 336 pref.setOnPreferenceChangeListener(this); 337 338 pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_4); 339 pref.setTitle(pref.getText()); 340 pref.setOnPreferenceChangeListener(this); 341 342 ActionBar actionBar = getActionBar(); 343 if (actionBar != null) { 344 // android.R.id.home will be triggered in onOptionsItemSelected() 345 actionBar.setDisplayHomeAsUpEnabled(true); 346 } 347 } 348 349 // Preference.OnPreferenceChangeListener implementation 350 public boolean onPreferenceChange(Preference preference, Object newValue) { 351 if (DBG) log("onPreferenceChange: key = " + preference.getKey()); 352 if (DBG) log(" preference = '" + preference + "'"); 353 if (DBG) log(" newValue = '" + newValue + "'"); 354 355 EditTextPreference pref = (EditTextPreference) preference; 356 357 // Copy the new text over to the title, just like in onCreate(). 358 // (Watch out: onPreferenceChange() is called *before* the 359 // Preference itself gets updated, so we need to use newValue here 360 // rather than pref.getText().) 361 pref.setTitle((String) newValue); 362 363 return true; // means it's OK to update the state of the Preference with the new value 364 } 365 366 @Override 367 public boolean onOptionsItemSelected(MenuItem item) { 368 final int itemId = item.getItemId(); 369 if (itemId == android.R.id.home) { // See ActionBar#setDisplayHomeAsUpEnabled() 370 CallFeaturesSetting.goUpToTopLevelSetting(this); 371 return true; 372 } 373 return super.onOptionsItemSelected(item); 374 } 375 } 376 377 /** 378 * Read the (customizable) canned responses from SharedPreferences, 379 * or from defaults if the user has never actually brought up 380 * the Settings UI. 381 * 382 * This method does disk I/O (reading the SharedPreferences file) 383 * so don't call it from the main thread. 384 * 385 * @see RespondViaSmsManager$Settings 386 */ 387 private String[] loadCannedResponses() { 388 if (DBG) log("loadCannedResponses()..."); 389 390 SharedPreferences prefs = 391 mInCallScreen.getSharedPreferences(SHARED_PREFERENCES_NAME, 392 Context.MODE_PRIVATE); 393 final Resources res = mInCallScreen.getResources(); 394 395 String[] responses = new String[NUM_CANNED_RESPONSES]; 396 397 // Note the default values here must agree with the corresponding 398 // android:defaultValue attributes in respond_via_sms_settings.xml. 399 400 responses[0] = prefs.getString(KEY_CANNED_RESPONSE_PREF_1, 401 res.getString(R.string.respond_via_sms_canned_response_1)); 402 responses[1] = prefs.getString(KEY_CANNED_RESPONSE_PREF_2, 403 res.getString(R.string.respond_via_sms_canned_response_2)); 404 responses[2] = prefs.getString(KEY_CANNED_RESPONSE_PREF_3, 405 res.getString(R.string.respond_via_sms_canned_response_3)); 406 responses[3] = prefs.getString(KEY_CANNED_RESPONSE_PREF_4, 407 res.getString(R.string.respond_via_sms_canned_response_4)); 408 return responses; 409 } 410 411 // TODO: Don't call loadCannedResponses() from the UI thread. 412 // 413 // We should either (1) kick off a background task when the call first 414 // starts ringing (probably triggered from the InCallScreen 415 // onNewRingingConnection() method) which would run loadCannedResponses() 416 // and stash the result away in mCannedResponses, or (2) use an 417 // OnSharedPreferenceChangeListener to listen for changes to this 418 // SharedPreferences instance, and use that to kick off the background task. 419 // 420 // In either case: 421 // 422 // - Make sure we recover sanely if mCannedResponses is still null when it's 423 // actually time to show the popup (i.e. if the background task was too 424 // slow, or if the background task never got started for some reason) 425 // 426 // - Make sure that all setting and getting of mCannedResponses happens 427 // inside a synchronized block 428 // 429 // - If we kick off the background task when the call first starts ringing, 430 // consider delaying that until the incoming-call UI actually comes to the 431 // foreground; this way we won't steal any CPU away from the caller-id 432 // query. Maybe do it from InCallScreen.onResume()? 433 // Or InCallTouchUi.showIncomingCallWidget()? 434 435 436 private static void log(String msg) { 437 Log.d(TAG, msg); 438 } 439} 440