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.app.ActivityManager; 20 import android.app.ActionBar; 21 import android.app.AlertDialog; 22 import android.app.Dialog; 23 import android.content.ComponentName; 24 import android.content.Context; 25 import android.content.DialogInterface; 26 import android.content.Intent; 27 import android.content.SharedPreferences; 28 import android.content.pm.ApplicationInfo; 29 import android.content.pm.PackageInfo; 30 import android.content.pm.PackageManager; 31 import android.content.pm.PackageManager; 32 import android.content.pm.ResolveInfo; 33 import android.content.pm.ServiceInfo; 34 import android.content.res.Resources; 35 import android.graphics.drawable.Drawable; 36 import android.net.Uri; 37 import android.os.Bundle; 38 import android.os.SystemProperties; 39 import android.preference.EditTextPreference; 40 import android.preference.Preference; 41 import android.preference.PreferenceActivity; 42 import android.telephony.PhoneNumberUtils; 43 import android.telephony.TelephonyManager; 44 import android.text.TextUtils; 45 import android.util.Log; 46 import android.view.LayoutInflater; 47 import android.view.Menu; 48 import android.view.MenuItem; 49 import android.view.View; 50 import android.view.ViewGroup; 51 import android.widget.AdapterView; 52 import android.widget.ArrayAdapter; 53 import android.widget.BaseAdapter; 54 import android.widget.CheckBox; 55 import android.widget.CompoundButton; 56 import android.widget.ImageView; 57 import android.widget.ListView; 58 import android.widget.TextView; 59 import android.widget.Toast; 60 61 import com.android.internal.telephony.Call; 62 import com.android.internal.telephony.Connection; 63 import com.android.internal.telephony.PhoneConstants; 64 import com.google.android.collect.Lists; 65 66 import java.util.ArrayList; 67 import java.util.Arrays; 68 import java.util.List; 69 70 /** 71 * Helper class to manage the "Respond via Message" feature for incoming calls. 72 * 73 * @see InCallScreen.internalRespondViaSms() 74 */ 75 public class RespondViaSmsManager { 76 private static final String TAG = "RespondViaSmsManager"; 77 private static final boolean DBG = 78 (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1); 79 // Do not check in with VDBG = true, since that may write PII to the system log. 80 private static final boolean VDBG = false; 81 82 private static final String PERMISSION_SEND_RESPOND_VIA_MESSAGE = 83 "android.permission.SEND_RESPOND_VIA_MESSAGE"; 84 85 private int mIconSize = -1; 86 87 /** 88 * Reference to the InCallScreen activity that owns us. This may be 89 * null if we haven't been initialized yet *or* after the InCallScreen 90 * activity has been destroyed. 91 */ 92 private InCallScreen mInCallScreen; 93 94 /** 95 * The popup showing the list of canned responses. 96 * 97 * This is an AlertDialog containing a ListView showing the possible 98 * choices. This may be null if the InCallScreen hasn't ever called 99 * showRespondViaSmsPopup() yet, or if the popup was visible once but 100 * then got dismissed. 101 */ 102 private Dialog mCannedResponsePopup; 103 104 /** 105 * The popup dialog allowing the user to chose which app handles respond-via-sms. 106 * 107 * An AlertDialog showing the Resolve-App UI resource from the framework wchih we then fill in 108 * with the appropriate data set. Can be null when not visible. 109 */ 110 private Dialog mPackageSelectionPopup; 111 112 /** The array of "canned responses"; see loadCannedResponses(). */ 113 private String[] mCannedResponses; 114 115 /** SharedPreferences file name for our persistent settings. */ 116 private static final String SHARED_PREFERENCES_NAME = "respond_via_sms_prefs"; 117 118 // Preference keys for the 4 "canned responses"; see RespondViaSmsManager$Settings. 119 // Since (for now at least) the number of messages is fixed at 4, and since 120 // SharedPreferences can't deal with arrays anyway, just store the messages 121 // as 4 separate strings. 122 private static final int NUM_CANNED_RESPONSES = 4; 123 private static final String KEY_CANNED_RESPONSE_PREF_1 = "canned_response_pref_1"; 124 private static final String KEY_CANNED_RESPONSE_PREF_2 = "canned_response_pref_2"; 125 private static final String KEY_CANNED_RESPONSE_PREF_3 = "canned_response_pref_3"; 126 private static final String KEY_CANNED_RESPONSE_PREF_4 = "canned_response_pref_4"; 127 private static final String KEY_PREFERRED_PACKAGE = "preferred_package_pref"; 128 private static final String KEY_INSTANT_TEXT_DEFAULT_COMPONENT = "instant_text_def_component"; 129 130 /** 131 * RespondViaSmsManager constructor. 132 */ 133 public RespondViaSmsManager() { 134 } 135 136 public void setInCallScreenInstance(InCallScreen inCallScreen) { 137 mInCallScreen = inCallScreen; 138 139 if (mInCallScreen != null) { 140 // Prefetch shared preferences to make the first canned response lookup faster 141 // (and to prevent StrictMode violation) 142 mInCallScreen.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); 143 } 144 } 145 146 /** 147 * Brings up the "Respond via SMS" popup for an incoming call. 148 * 149 * @param ringingCall the current incoming call 150 */ 151 public void showRespondViaSmsPopup(Call ringingCall) { 152 if (DBG) log("showRespondViaSmsPopup()..."); 153 154 // Very quick succession of clicks can cause this to run twice. 155 // Stop here to avoid creating more than one popup. 156 if (isShowingPopup()) { 157 if (DBG) log("Skip showing popup when one is already shown."); 158 return; 159 } 160 161 ListView lv = new ListView(mInCallScreen); 162 163 // Refresh the array of "canned responses". 164 mCannedResponses = loadCannedResponses(); 165 166 // Build the list: start with the canned responses, but manually add 167 // the write-your-own option as the last choice. 168 int numPopupItems = mCannedResponses.length + 1; 169 String[] popupItems = Arrays.copyOf(mCannedResponses, numPopupItems); 170 popupItems[numPopupItems - 1] = mInCallScreen.getResources() 171 .getString(R.string.respond_via_sms_custom_message); 172 173 ArrayAdapter<String> adapter = 174 new ArrayAdapter<String>(mInCallScreen, 175 android.R.layout.simple_list_item_1, 176 android.R.id.text1, 177 popupItems); 178 lv.setAdapter(adapter); 179 180 // Create a RespondViaSmsItemClickListener instance to handle item 181 // clicks from the popup. 182 // (Note we create a fresh instance for each incoming call, and 183 // stash away the call's phone number, since we can't necessarily 184 // assume this call will still be ringing when the user finally 185 // chooses a response.) 186 187 Connection c = ringingCall.getLatestConnection(); 188 if (VDBG) log("- connection: " + c); 189 190 if (c == null) { 191 // Uh oh -- the "ringingCall" doesn't have any connections any more. 192 // (In other words, it's no longer ringing.) This is rare, but can 193 // happen if the caller hangs up right at the exact moment the user 194 // selects the "Respond via SMS" option. 195 // There's nothing to do here (since the incoming call is gone), 196 // so just bail out. 197 Log.i(TAG, "showRespondViaSmsPopup: null connection; bailing out..."); 198 return; 199 } 200 201 // TODO: at this point we probably should re-check c.getAddress() 202 // and c.getNumberPresentation() for validity. (i.e. recheck the 203 // same cases in InCallTouchUi.showIncomingCallWidget() where we 204 // should have disallowed the "respond via SMS" feature in the 205 // first place.) 206 207 String phoneNumber = c.getAddress(); 208 if (VDBG) log("- phoneNumber: " + phoneNumber); 209 lv.setOnItemClickListener(new RespondViaSmsItemClickListener(phoneNumber)); 210 211 AlertDialog.Builder builder = new AlertDialog.Builder(mInCallScreen) 212 .setCancelable(true) 213 .setOnCancelListener(new RespondViaSmsCancelListener()) 214 .setView(lv); 215 mCannedResponsePopup = builder.create(); 216 mCannedResponsePopup.show(); 217 } 218 219 /** 220 * Dismiss currently visible popups. 221 * 222 * This is safe to call even if the popup is already dismissed, and 223 * even if you never called showRespondViaSmsPopup() in the first 224 * place. 225 */ 226 public void dismissPopup() { 227 if (mCannedResponsePopup != null) { 228 mCannedResponsePopup.dismiss(); // safe even if already dismissed 229 mCannedResponsePopup = null; 230 } 231 if (mPackageSelectionPopup != null) { 232 mPackageSelectionPopup.dismiss(); 233 mPackageSelectionPopup = null; 234 } 235 } 236 237 public boolean isShowingPopup() { 238 return (mCannedResponsePopup != null && mCannedResponsePopup.isShowing()) 239 || (mPackageSelectionPopup != null && mPackageSelectionPopup.isShowing()); 240 } 241 242 /** 243 * OnItemClickListener for the "Respond via SMS" popup. 244 */ 245 public class RespondViaSmsItemClickListener implements AdapterView.OnItemClickListener { 246 // Phone number to send the SMS to. 247 private String mPhoneNumber; 248 249 public RespondViaSmsItemClickListener(String phoneNumber) { 250 mPhoneNumber = phoneNumber; 251 } 252 253 /** 254 * Handles the user selecting an item from the popup. 255 */ 256 @Override 257 public void onItemClick(AdapterView<?> parent, // The ListView 258 View view, // The TextView that was clicked 259 int position, 260 long id) { 261 if (DBG) log("RespondViaSmsItemClickListener.onItemClick(" + position + ")..."); 262 String message = (String) parent.getItemAtPosition(position); 263 if (VDBG) log("- message: '" + message + "'"); 264 265 // The "Custom" choice is a special case. 266 // (For now, it's guaranteed to be the last item.) 267 if (position == (parent.getCount() - 1)) { 268 // Take the user to the standard SMS compose UI. 269 launchSmsCompose(mPhoneNumber); 270 onPostMessageSent(); 271 } else { 272 sendTextToDefaultActivity(mPhoneNumber, message); 273 } 274 } 275 } 276 277 278 /** 279 * OnCancelListener for the "Respond via SMS" popup. 280 */ 281 public class RespondViaSmsCancelListener implements DialogInterface.OnCancelListener { 282 public RespondViaSmsCancelListener() { 283 } 284 285 /** 286 * Handles the user canceling the popup, either by touching 287 * outside the popup or by pressing Back. 288 */ 289 @Override 290 public void onCancel(DialogInterface dialog) { 291 if (DBG) log("RespondViaSmsCancelListener.onCancel()..."); 292 293 dismissPopup(); 294 295 final PhoneConstants.State state = PhoneGlobals.getInstance().mCM.getState(); 296 if (state == PhoneConstants.State.IDLE) { 297 // This means the incoming call is already hung up when the user chooses not to 298 // use "Respond via SMS" feature. Let's just exit the whole in-call screen. 299 PhoneGlobals.getInstance().dismissCallScreen(); 300 } else { 301 302 // If the user cancels the popup, this presumably means that 303 // they didn't actually mean to bring up the "Respond via SMS" 304 // UI in the first place (and instead want to go back to the 305 // state where they can either answer or reject the call.) 306 // So restart the ringer and bring back the regular incoming 307 // call UI. 308 309 // This will have no effect if the incoming call isn't still ringing. 310 PhoneGlobals.getInstance().notifier.restartRinger(); 311 312 // We hid the GlowPadView widget way back in 313 // InCallTouchUi.onTrigger(), when the user first selected 314 // the "SMS" trigger. 315 // 316 // To bring it back, just force the entire InCallScreen to 317 // update itself based on the current telephony state. 318 // (Assuming the incoming call is still ringing, this will 319 // cause the incoming call widget to reappear.) 320 mInCallScreen.requestUpdateScreen(); 321 } 322 } 323 } 324 325 private void sendTextToDefaultActivity(String phoneNumber, String message) { 326 if (DBG) log("sendTextToDefaultActivity()..."); 327 final PackageManager packageManager = mInCallScreen.getPackageManager(); 328 329 // Check to see if the default component to receive this intent is already saved 330 // and check to see if it still has the corrent permissions. 331 final SharedPreferences prefs = mInCallScreen.getSharedPreferences(SHARED_PREFERENCES_NAME, 332 Context.MODE_PRIVATE); 333 final String flattenedName = prefs.getString(KEY_INSTANT_TEXT_DEFAULT_COMPONENT, null); 334 if (flattenedName != null) { 335 if (DBG) log("Default package was found." + flattenedName); 336 337 final ComponentName componentName = ComponentName.unflattenFromString(flattenedName); 338 ServiceInfo serviceInfo = null; 339 try { 340 serviceInfo = packageManager.getServiceInfo(componentName, 0); 341 } catch (PackageManager.NameNotFoundException e) { 342 Log.w(TAG, "Default service does not have permission."); 343 } 344 345 if (serviceInfo != null && 346 PERMISSION_SEND_RESPOND_VIA_MESSAGE.equals(serviceInfo.permission)) { 347 sendTextAndExit(phoneNumber, message, componentName, false); 348 return; 349 } else { 350 SharedPreferences.Editor editor = prefs.edit(); 351 editor.remove(KEY_INSTANT_TEXT_DEFAULT_COMPONENT); 352 editor.apply(); 353 } 354 } 355 356 final ArrayList<ComponentName> componentsWithPermission = 357 getPackagesWithInstantTextPermission(); 358 359 final int size = componentsWithPermission.size(); 360 if (size == 0) { 361 Log.e(TAG, "No appropriate package receiving the Intent. Don't send anything"); 362 onPostMessageSent(); 363 } else if (size == 1) { 364 sendTextAndExit(phoneNumber, message, componentsWithPermission.get(0), false); 365 } else { 366 showPackageSelectionDialog(phoneNumber, message, componentsWithPermission); 367 } 368 } 369 370 /** 371 * Queries the System to determine what packages contain services that can handle the instant 372 * text response Action AND have permissions to do so. 373 */ 374 private ArrayList<ComponentName> getPackagesWithInstantTextPermission() { 375 PackageManager packageManager = mInCallScreen.getPackageManager(); 376 377 ArrayList<ComponentName> componentsWithPermission = Lists.newArrayList(); 378 379 // Get list of all services set up to handle the Instant Text intent. 380 final List<ResolveInfo> infos = packageManager.queryIntentServices( 381 getInstantTextIntent("", null, null), 0); 382 383 // Collect all the valid services 384 for (ResolveInfo resolveInfo : infos) { 385 final ServiceInfo serviceInfo = resolveInfo.serviceInfo; 386 if (serviceInfo == null) { 387 Log.w(TAG, "Ignore package without proper service."); 388 continue; 389 } 390 391 // A Service is valid only if it requires the permission 392 // PERMISSION_SEND_RESPOND_VIA_MESSAGE 393 if (PERMISSION_SEND_RESPOND_VIA_MESSAGE.equals(serviceInfo.permission)) { 394 componentsWithPermission.add(new ComponentName(serviceInfo.packageName, 395 serviceInfo.name)); 396 } 397 } 398 399 return componentsWithPermission; 400 } 401 402 private void showPackageSelectionDialog(String phoneNumber, String message, 403 List<ComponentName> components) { 404 if (DBG) log("showPackageSelectionDialog()..."); 405 406 dismissPopup(); 407 408 BaseAdapter adapter = new PackageSelectionAdapter(mInCallScreen, components); 409 410 PackageClickListener clickListener = 411 new PackageClickListener(phoneNumber, message, components); 412 413 final CharSequence title = mInCallScreen.getResources().getText( 414 com.android.internal.R.string.whichApplication); 415 LayoutInflater inflater = 416 (LayoutInflater) mInCallScreen.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 417 418 final View view = inflater.inflate(com.android.internal.R.layout.always_use_checkbox, null); 419 final CheckBox alwaysUse = (CheckBox) view.findViewById( 420 com.android.internal.R.id.alwaysUse); 421 alwaysUse.setText(com.android.internal.R.string.alwaysUse); 422 alwaysUse.setOnCheckedChangeListener(clickListener); 423 424 AlertDialog.Builder builder = new AlertDialog.Builder(mInCallScreen) 425 .setTitle(title) 426 .setCancelable(true) 427 .setOnCancelListener(new RespondViaSmsCancelListener()) 428 .setAdapter(adapter, clickListener) 429 .setView(view); 430 mPackageSelectionPopup = builder.create(); 431 mPackageSelectionPopup.show(); 432 } 433 434 private class PackageSelectionAdapter extends BaseAdapter { 435 private final LayoutInflater mInflater; 436 private final List<ComponentName> mComponents; 437 438 public PackageSelectionAdapter(Context context, List<ComponentName> components) { 439 mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 440 mComponents = components; 441 } 442 443 @Override 444 public int getCount() { 445 return mComponents.size(); 446 } 447 448 @Override 449 public Object getItem(int position) { 450 return mComponents.get(position); 451 } 452 453 @Override 454 public long getItemId(int position) { 455 return position; 456 } 457 458 @Override 459 public View getView(int position, View convertView, ViewGroup parent) { 460 if (convertView == null) { 461 convertView = mInflater.inflate( 462 com.android.internal.R.layout.resolve_list_item, parent, false); 463 } 464 465 final ComponentName component = mComponents.get(position); 466 final String packageName = component.getPackageName(); 467 final PackageManager packageManager = mInCallScreen.getPackageManager(); 468 469 // Set the application label 470 final TextView text = (TextView) convertView.findViewById( 471 com.android.internal.R.id.text1); 472 final TextView text2 = (TextView) convertView.findViewById( 473 com.android.internal.R.id.text2); 474 475 // Reset any previous values 476 text.setText(""); 477 text2.setVisibility(View.GONE); 478 try { 479 final ApplicationInfo appInfo = packageManager.getApplicationInfo(packageName, 0); 480 final CharSequence label = packageManager.getApplicationLabel(appInfo); 481 if (label != null) { 482 text.setText(label); 483 } 484 } catch (PackageManager.NameNotFoundException e) { 485 Log.w(TAG, "Failed to load app label because package was not found."); 486 } 487 488 // Set the application icon 489 final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon); 490 Drawable drawable = null; 491 try { 492 drawable = mInCallScreen.getPackageManager().getApplicationIcon(packageName); 493 } catch (PackageManager.NameNotFoundException e) { 494 Log.w(TAG, "Failed to load icon because it wasn't found."); 495 } 496 if (drawable == null) { 497 drawable = mInCallScreen.getPackageManager().getDefaultActivityIcon(); 498 } 499 icon.setImageDrawable(drawable); 500 ViewGroup.LayoutParams lp = (ViewGroup.LayoutParams) icon.getLayoutParams(); 501 lp.width = lp.height = getIconSize(); 502 503 return convertView; 504 } 505 506 } 507 508 private class PackageClickListener implements DialogInterface.OnClickListener, 509 CompoundButton.OnCheckedChangeListener { 510 /** Phone number to send the SMS to. */ 511 final private String mPhoneNumber; 512 final private String mMessage; 513 final private List<ComponentName> mComponents; 514 private boolean mMakeDefault = false; 515 516 public PackageClickListener(String phoneNumber, String message, 517 List<ComponentName> components) { 518 mPhoneNumber = phoneNumber; 519 mMessage = message; 520 mComponents = components; 521 } 522 523 @Override 524 public void onClick(DialogInterface dialog, int which) { 525 ComponentName component = mComponents.get(which); 526 sendTextAndExit(mPhoneNumber, mMessage, component, mMakeDefault); 527 } 528 529 @Override 530 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 531 Log.i(TAG, "mMakeDefault : " + isChecked); 532 mMakeDefault = isChecked; 533 } 534 } 535 536 private void sendTextAndExit(String phoneNumber, String message, ComponentName component, 537 boolean setDefaultComponent) { 538 // Send the selected message immediately with no user interaction. 539 sendText(phoneNumber, message, component); 540 541 if (setDefaultComponent) { 542 final SharedPreferences prefs = mInCallScreen.getSharedPreferences( 543 SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); 544 prefs.edit() 545 .putString(KEY_INSTANT_TEXT_DEFAULT_COMPONENT, component.flattenToString()) 546 .apply(); 547 } 548 549 // ...and show a brief confirmation to the user (since 550 // otherwise it's hard to be sure that anything actually 551 // happened.) 552 final Resources res = mInCallScreen.getResources(); 553 final String formatString = res.getString(R.string.respond_via_sms_confirmation_format); 554 final String confirmationMsg = String.format(formatString, phoneNumber); 555 Toast.makeText(mInCallScreen, 556 confirmationMsg, 557 Toast.LENGTH_LONG).show(); 558 559 // TODO: If the device is locked, this toast won't actually ever 560 // be visible! (That's because we're about to dismiss the call 561 // screen, which means that the device will return to the 562 // keyguard. But toasts aren't visible on top of the keyguard.) 563 // Possible fixes: 564 // (1) Is it possible to allow a specific Toast to be visible 565 // on top of the keyguard? 566 // (2) Artifically delay the dismissCallScreen() call by 3 567 // seconds to allow the toast to be seen? 568 // (3) Don't use a toast at all; instead use a transient state 569 // of the InCallScreen (perhaps via the InCallUiState 570 // progressIndication feature), and have that state be 571 // visible for 3 seconds before calling dismissCallScreen(). 572 573 onPostMessageSent(); 574 } 575 576 /** 577 * Sends a text message without any interaction from the user. 578 */ 579 private void sendText(String phoneNumber, String message, ComponentName component) { 580 if (VDBG) log("sendText: number " 581 + phoneNumber + ", message '" + message + "'"); 582 583 mInCallScreen.startService(getInstantTextIntent(phoneNumber, message, component)); 584 } 585 586 private void onPostMessageSent() { 587 // At this point the user is done dealing with the incoming call, so 588 // there's no reason to keep it around. (It's also confusing for 589 // the "incoming call" icon in the status bar to still be visible.) 590 // So reject the call now. 591 mInCallScreen.hangupRingingCall(); 592 593 dismissPopup(); 594 595 final PhoneConstants.State state = PhoneGlobals.getInstance().mCM.getState(); 596 if (state == PhoneConstants.State.IDLE) { 597 // There's no other phone call to interact. Exit the entire in-call screen. 598 PhoneGlobals.getInstance().dismissCallScreen(); 599 } else { 600 // The user is still in the middle of other phone calls, so we should keep the 601 // in-call screen. 602 mInCallScreen.requestUpdateScreen(); 603 } 604 } 605 606 /** 607 * Brings up the standard SMS compose UI. 608 */ 609 private void launchSmsCompose(String phoneNumber) { 610 if (VDBG) log("launchSmsCompose: number " + phoneNumber); 611 612 Intent intent = getInstantTextIntent(phoneNumber, null, null); 613 614 if (VDBG) log("- Launching SMS compose UI: " + intent); 615 mInCallScreen.startService(intent); 616 } 617 618 /** 619 * @param phoneNumber Must not be null. 620 * @param message Can be null. If message is null, the returned Intent will be configured to 621 * launch the SMS compose UI. If non-null, the returned Intent will cause the specified message 622 * to be sent with no interaction from the user. 623 * @param component The component that should handle this intent. 624 * @return Service Intent for the instant response. 625 */ 626 private static Intent getInstantTextIntent(String phoneNumber, String message, 627 ComponentName component) { 628 final Uri uri = Uri.fromParts(Constants.SCHEME_SMSTO, phoneNumber, null); 629 Intent intent = new Intent(TelephonyManager.ACTION_RESPOND_VIA_MESSAGE, uri); 630 if (message != null) { 631 intent.putExtra(Intent.EXTRA_TEXT, message); 632 } else { 633 intent.putExtra("exit_on_sent", true); 634 intent.putExtra("showUI", true); 635 } 636 if (component != null) { 637 intent.setComponent(component); 638 } 639 return intent; 640 } 641 642 /** 643 * Settings activity under "Call settings" to let you manage the 644 * canned responses; see respond_via_sms_settings.xml 645 */ 646 public static class Settings extends PreferenceActivity 647 implements Preference.OnPreferenceChangeListener { 648 @Override 649 protected void onCreate(Bundle icicle) { 650 super.onCreate(icicle); 651 if (DBG) log("Settings: onCreate()..."); 652 653 getPreferenceManager().setSharedPreferencesName(SHARED_PREFERENCES_NAME); 654 655 // This preference screen is ultra-simple; it's just 4 plain 656 // <EditTextPreference>s, one for each of the 4 "canned responses". 657 // 658 // The only nontrivial thing we do here is copy the text value of 659 // each of those EditTextPreferences and use it as the preference's 660 // "title" as well, so that the user will immediately see all 4 661 // strings when they arrive here. 662 // 663 // Also, listen for change events (since we'll need to update the 664 // title any time the user edits one of the strings.) 665 666 addPreferencesFromResource(R.xml.respond_via_sms_settings); 667 668 EditTextPreference pref; 669 pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_1); 670 pref.setTitle(pref.getText()); 671 pref.setOnPreferenceChangeListener(this); 672 673 pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_2); 674 pref.setTitle(pref.getText()); 675 pref.setOnPreferenceChangeListener(this); 676 677 pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_3); 678 pref.setTitle(pref.getText()); 679 pref.setOnPreferenceChangeListener(this); 680 681 pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_4); 682 pref.setTitle(pref.getText()); 683 pref.setOnPreferenceChangeListener(this); 684 685 ActionBar actionBar = getActionBar(); 686 if (actionBar != null) { 687 // android.R.id.home will be triggered in onOptionsItemSelected() 688 actionBar.setDisplayHomeAsUpEnabled(true); 689 } 690 } 691 692 // Preference.OnPreferenceChangeListener implementation 693 @Override 694 public boolean onPreferenceChange(Preference preference, Object newValue) { 695 if (DBG) log("onPreferenceChange: key = " + preference.getKey()); 696 if (VDBG) log(" preference = '" + preference + "'"); 697 if (VDBG) log(" newValue = '" + newValue + "'"); 698 699 EditTextPreference pref = (EditTextPreference) preference; 700 701 // Copy the new text over to the title, just like in onCreate(). 702 // (Watch out: onPreferenceChange() is called *before* the 703 // Preference itself gets updated, so we need to use newValue here 704 // rather than pref.getText().) 705 pref.setTitle((String) newValue); 706 707 return true; // means it's OK to update the state of the Preference with the new value 708 } 709 710 @Override 711 public boolean onOptionsItemSelected(MenuItem item) { 712 final int itemId = item.getItemId(); 713 switch (itemId) { 714 case android.R.id.home: 715 // See ActionBar#setDisplayHomeAsUpEnabled() 716 CallFeaturesSetting.goUpToTopLevelSetting(this); 717 return true; 718 case R.id.respond_via_message_reset: 719 // Reset the preferences settings 720 SharedPreferences prefs = getSharedPreferences( 721 SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); 722 SharedPreferences.Editor editor = prefs.edit(); 723 editor.remove(KEY_INSTANT_TEXT_DEFAULT_COMPONENT); 724 editor.apply(); 725 726 return true; 727 default: 728 } 729 return super.onOptionsItemSelected(item); 730 } 731 732 @Override 733 public boolean onCreateOptionsMenu(Menu menu) { 734 getMenuInflater().inflate(R.menu.respond_via_message_settings_menu, menu); 735 return super.onCreateOptionsMenu(menu); 736 } 737 } 738 739 /** 740 * Read the (customizable) canned responses from SharedPreferences, 741 * or from defaults if the user has never actually brought up 742 * the Settings UI. 743 * 744 * This method does disk I/O (reading the SharedPreferences file) 745 * so don't call it from the main thread. 746 * 747 * @see RespondViaSmsManager.Settings 748 */ 749 private String[] loadCannedResponses() { 750 if (DBG) log("loadCannedResponses()..."); 751 752 SharedPreferences prefs = mInCallScreen.getSharedPreferences(SHARED_PREFERENCES_NAME, 753 Context.MODE_PRIVATE); 754 final Resources res = mInCallScreen.getResources(); 755 756 String[] responses = new String[NUM_CANNED_RESPONSES]; 757 758 // Note the default values here must agree with the corresponding 759 // android:defaultValue attributes in respond_via_sms_settings.xml. 760 761 responses[0] = prefs.getString(KEY_CANNED_RESPONSE_PREF_1, 762 res.getString(R.string.respond_via_sms_canned_response_1)); 763 responses[1] = prefs.getString(KEY_CANNED_RESPONSE_PREF_2, 764 res.getString(R.string.respond_via_sms_canned_response_2)); 765 responses[2] = prefs.getString(KEY_CANNED_RESPONSE_PREF_3, 766 res.getString(R.string.respond_via_sms_canned_response_3)); 767 responses[3] = prefs.getString(KEY_CANNED_RESPONSE_PREF_4, 768 res.getString(R.string.respond_via_sms_canned_response_4)); 769 return responses; 770 } 771 772 /** 773 * @return true if the "Respond via SMS" feature should be enabled 774 * for the specified incoming call. 775 * 776 * The general rule is that we *do* allow "Respond via SMS" except for 777 * the few (relatively rare) cases where we know for sure it won't 778 * work, namely: 779 * - a bogus or blank incoming number 780 * - a call from a SIP address 781 * - a "call presentation" that doesn't allow the number to be revealed 782 * 783 * In all other cases, we allow the user to respond via SMS. 784 * 785 * Note that this behavior isn't perfect; for example we have no way 786 * to detect whether the incoming call is from a landline (with most 787 * networks at least), so we still enable this feature even though 788 * SMSes to that number will silently fail. 789 */ 790 public static boolean allowRespondViaSmsForCall(Context context, Call ringingCall) { 791 if (DBG) log("allowRespondViaSmsForCall(" + ringingCall + ")..."); 792 793 // First some basic sanity checks: 794 if (ringingCall == null) { 795 Log.w(TAG, "allowRespondViaSmsForCall: null ringingCall!"); 796 return false; 797 } 798 if (!ringingCall.isRinging()) { 799 // The call is in some state other than INCOMING or WAITING! 800 // (This should almost never happen, but it *could* 801 // conceivably happen if the ringing call got disconnected by 802 // the network just *after* we got it from the CallManager.) 803 Log.w(TAG, "allowRespondViaSmsForCall: ringingCall not ringing! state = " 804 + ringingCall.getState()); 805 return false; 806 } 807 Connection conn = ringingCall.getLatestConnection(); 808 if (conn == null) { 809 // The call doesn't have any connections! (Again, this can 810 // happen if the ringing call disconnects at the exact right 811 // moment, but should almost never happen in practice.) 812 Log.w(TAG, "allowRespondViaSmsForCall: null Connection!"); 813 return false; 814 } 815 816 // Check the incoming number: 817 final String number = conn.getAddress(); 818 if (DBG) log("- number: '" + number + "'"); 819 if (TextUtils.isEmpty(number)) { 820 Log.w(TAG, "allowRespondViaSmsForCall: no incoming number!"); 821 return false; 822 } 823 if (PhoneNumberUtils.isUriNumber(number)) { 824 // The incoming number is actually a URI (i.e. a SIP address), 825 // not a regular PSTN phone number, and we can't send SMSes to 826 // SIP addresses. 827 // (TODO: That might still be possible eventually, though. Is 828 // there some SIP-specific equivalent to sending a text message?) 829 Log.i(TAG, "allowRespondViaSmsForCall: incoming 'number' is a SIP address."); 830 return false; 831 } 832 833 // Finally, check the "call presentation": 834 int presentation = conn.getNumberPresentation(); 835 if (DBG) log("- presentation: " + presentation); 836 if (presentation == PhoneConstants.PRESENTATION_RESTRICTED) { 837 // PRESENTATION_RESTRICTED means "caller-id blocked". 838 // The user isn't allowed to see the number in the first 839 // place, so obviously we can't let you send an SMS to it. 840 Log.i(TAG, "allowRespondViaSmsForCall: PRESENTATION_RESTRICTED."); 841 return false; 842 } 843 844 // Allow the feature only when there's a destination for it. 845 if (context.getPackageManager().resolveService(getInstantTextIntent(number, null, null) , 0) 846 == null) { 847 return false; 848 } 849 850 // TODO: with some carriers (in certain countries) you *can* actually 851 // tell whether a given number is a mobile phone or not. So in that 852 // case we could potentially return false here if the incoming call is 853 // from a land line. 854 855 // If none of the above special cases apply, it's OK to enable the 856 // "Respond via SMS" feature. 857 return true; 858 } 859 860 private int getIconSize() { 861 if (mIconSize < 0) { 862 final ActivityManager am = 863 (ActivityManager) mInCallScreen.getSystemService(Context.ACTIVITY_SERVICE); 864 mIconSize = am.getLauncherLargeIconSize(); 865 } 866 867 return mIconSize; 868 } 869 870 871 private static void log(String msg) { 872 Log.d(TAG, msg); 873 } 874 } 875