1 /* 2 * Copyright (C) 2010 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.services.telephony.sip; 18 19 import android.app.AlertDialog; 20 import android.app.Dialog; 21 import android.app.DialogFragment; 22 import android.content.Intent; 23 import android.net.sip.SipProfile; 24 import android.os.Bundle; 25 import android.os.Parcelable; 26 import android.preference.CheckBoxPreference; 27 import android.preference.EditTextPreference; 28 import android.preference.ListPreference; 29 import android.preference.Preference; 30 import android.preference.PreferenceActivity; 31 import android.preference.PreferenceGroup; 32 import android.text.TextUtils; 33 import android.util.Log; 34 import android.view.KeyEvent; 35 import android.view.Menu; 36 import android.view.MenuItem; 37 import android.widget.Button; 38 import android.widget.Toast; 39 40 import java.io.IOException; 41 import java.lang.reflect.Method; 42 import java.util.Arrays; 43 44 /** 45 * The activity class for editing a new or existing SIP profile. 46 */ 47 public class SipEditor extends PreferenceActivity 48 implements Preference.OnPreferenceChangeListener { 49 private static final String PREFIX = "[SipEditor] "; 50 private static final boolean VERBOSE = false; /* STOP SHIP if true */ 51 52 private static final int MENU_SAVE = Menu.FIRST; 53 private static final int MENU_DISCARD = Menu.FIRST + 1; 54 private static final int MENU_REMOVE = Menu.FIRST + 2; 55 56 private static final String KEY_PROFILE = "profile"; 57 private static final String GET_METHOD_PREFIX = "get"; 58 private static final char SCRAMBLED = '*'; 59 private static final int NA = 0; 60 61 private AdvancedSettings mAdvancedSettings; 62 private SipPreferences mSipPreferences; 63 private boolean mDisplayNameSet; 64 private boolean mHomeButtonClicked; 65 private boolean mUpdateRequired; 66 67 private SipProfileDb mProfileDb; 68 private SipProfile mOldProfile; 69 private Button mRemoveButton; 70 private SipAccountRegistry mSipAccountRegistry; 71 72 /** 73 * Dialog fragment class to be used for displaying an alert dialog. 74 */ 75 public static class AlertDialogFragment extends DialogFragment { 76 private static final String KEY_MESSAGE = "message"; 77 78 /** 79 * Initialize the AlertDialogFragment instance. 80 * 81 * @param message the dialog message to display. 82 * @return the AlertDialogFragment. 83 */ newInstance(String message)84 public static AlertDialogFragment newInstance(String message) { 85 AlertDialogFragment frag = new AlertDialogFragment(); 86 Bundle args = new Bundle(); 87 args.putString(KEY_MESSAGE, message); 88 frag.setArguments(args); 89 return frag; 90 } 91 92 @Override onCreateDialog(Bundle savedInstanceState)93 public Dialog onCreateDialog(Bundle savedInstanceState) { 94 String message = getArguments().getString(KEY_MESSAGE); 95 96 return new AlertDialog.Builder(getActivity()) 97 .setTitle(android.R.string.dialog_alert_title) 98 .setIconAttribute(android.R.attr.alertDialogIcon) 99 .setMessage(message) 100 .setPositiveButton(R.string.alert_dialog_ok, null) 101 .create(); 102 } 103 } 104 105 enum PreferenceKey { 106 Username(R.string.username, 0, R.string.default_preference_summary_username), 107 Password(R.string.password, 0, R.string.default_preference_summary_password), 108 DomainAddress(R.string.domain_address, 0, 109 R.string.default_preference_summary_domain_address), 110 DisplayName(R.string.display_name, 0, R.string.display_name_summary), 111 ProxyAddress(R.string.proxy_address, 0, R.string.optional_summary), 112 Port(R.string.port, R.string.default_port, R.string.default_port), 113 Transport(R.string.transport, R.string.default_transport, NA), 114 SendKeepAlive(R.string.send_keepalive, R.string.sip_system_decide, NA), 115 AuthUserName(R.string.auth_username, 0, R.string.optional_summary); 116 117 final int text; 118 final int initValue; 119 final int defaultSummary; 120 Preference preference; 121 122 /** 123 * @param key The key name of the preference. 124 * @param initValue The initial value of the preference. 125 * @param defaultSummary The default summary value of the preference 126 * when the preference value is empty. 127 */ PreferenceKey(int text, int initValue, int defaultSummary)128 PreferenceKey(int text, int initValue, int defaultSummary) { 129 this.text = text; 130 this.initValue = initValue; 131 this.defaultSummary = defaultSummary; 132 } 133 getValue()134 String getValue() { 135 if (preference instanceof EditTextPreference) { 136 return ((EditTextPreference) preference).getText(); 137 } else if (preference instanceof ListPreference) { 138 return ((ListPreference) preference).getValue(); 139 } 140 throw new RuntimeException("getValue() for the preference " + this); 141 } 142 setValue(String value)143 void setValue(String value) { 144 if (preference instanceof EditTextPreference) { 145 String oldValue = getValue(); 146 ((EditTextPreference) preference).setText(value); 147 if (this != Password) { 148 if (VERBOSE) { 149 log(this + ": setValue() " + value + ": " + oldValue + " --> " + 150 getValue()); 151 } 152 } 153 } else if (preference instanceof ListPreference) { 154 ((ListPreference) preference).setValue(value); 155 } 156 157 if (TextUtils.isEmpty(value)) { 158 preference.setSummary(defaultSummary); 159 } else if (this == Password) { 160 preference.setSummary(scramble(value)); 161 } else if ((this == DisplayName) 162 && value.equals(getDefaultDisplayName())) { 163 preference.setSummary(defaultSummary); 164 } else { 165 preference.setSummary(value); 166 } 167 } 168 } 169 170 @Override onResume()171 public void onResume() { 172 super.onResume(); 173 mHomeButtonClicked = false; 174 if (!SipUtil.isPhoneIdle(this)) { 175 mAdvancedSettings.show(); 176 getPreferenceScreen().setEnabled(false); 177 if (mRemoveButton != null) mRemoveButton.setEnabled(false); 178 } else { 179 getPreferenceScreen().setEnabled(true); 180 if (mRemoveButton != null) mRemoveButton.setEnabled(true); 181 } 182 } 183 184 @Override onCreate(Bundle savedInstanceState)185 public void onCreate(Bundle savedInstanceState) { 186 if (VERBOSE) log("onCreate, start profile editor"); 187 super.onCreate(savedInstanceState); 188 189 mSipPreferences = new SipPreferences(this); 190 mProfileDb = new SipProfileDb(this); 191 mSipAccountRegistry = SipAccountRegistry.getInstance(); 192 193 setContentView(R.layout.sip_settings_ui); 194 addPreferencesFromResource(R.xml.sip_edit); 195 196 SipProfile p = mOldProfile = (SipProfile) ((savedInstanceState == null) 197 ? getIntent().getParcelableExtra(SipSettings.KEY_SIP_PROFILE) 198 : savedInstanceState.getParcelable(KEY_PROFILE)); 199 200 PreferenceGroup screen = (PreferenceGroup) getPreferenceScreen(); 201 for (int i = 0, n = screen.getPreferenceCount(); i < n; i++) { 202 setupPreference(screen.getPreference(i)); 203 } 204 205 if (p == null) { 206 screen.setTitle(R.string.sip_edit_new_title); 207 } 208 209 mAdvancedSettings = new AdvancedSettings(); 210 211 loadPreferencesFromProfile(p); 212 } 213 214 @Override onPause()215 public void onPause() { 216 if (VERBOSE) log("onPause, finishing: " + isFinishing()); 217 if (!isFinishing()) { 218 mHomeButtonClicked = true; 219 validateAndSetResult(); 220 } 221 super.onPause(); 222 } 223 224 @Override onCreateOptionsMenu(Menu menu)225 public boolean onCreateOptionsMenu(Menu menu) { 226 super.onCreateOptionsMenu(menu); 227 menu.add(0, MENU_DISCARD, 0, R.string.sip_menu_discard) 228 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); 229 menu.add(0, MENU_SAVE, 0, R.string.sip_menu_save) 230 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); 231 menu.add(0, MENU_REMOVE, 0, R.string.remove_sip_account) 232 .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); 233 return true; 234 } 235 236 @Override onPrepareOptionsMenu(Menu menu)237 public boolean onPrepareOptionsMenu(Menu menu) { 238 MenuItem removeMenu = menu.findItem(MENU_REMOVE); 239 removeMenu.setVisible(mOldProfile != null); 240 menu.findItem(MENU_SAVE).setEnabled(mUpdateRequired); 241 return super.onPrepareOptionsMenu(menu); 242 } 243 244 @Override onOptionsItemSelected(MenuItem item)245 public boolean onOptionsItemSelected(MenuItem item) { 246 switch (item.getItemId()) { 247 case MENU_SAVE: 248 validateAndSetResult(); 249 return true; 250 251 case MENU_DISCARD: 252 finish(); 253 return true; 254 255 case MENU_REMOVE: { 256 setRemovedProfileAndFinish(); 257 return true; 258 } 259 case android.R.id.home: { 260 finish(); 261 return true; 262 } 263 } 264 return super.onOptionsItemSelected(item); 265 } 266 267 @Override onKeyDown(int keyCode, KeyEvent event)268 public boolean onKeyDown(int keyCode, KeyEvent event) { 269 switch (keyCode) { 270 case KeyEvent.KEYCODE_BACK: 271 validateAndSetResult(); 272 return true; 273 } 274 return super.onKeyDown(keyCode, event); 275 } 276 277 /** 278 * Saves a {@link SipProfile} and registers the associated 279 * {@link android.telecom.PhoneAccount}. 280 * 281 * @param p The {@link SipProfile} to register. 282 * @param enableProfile {@code true} if profile should be enabled, too. 283 * @throws IOException Exception resulting from profile save. 284 */ saveAndRegisterProfile(SipProfile p, boolean enableProfile)285 private void saveAndRegisterProfile(SipProfile p, boolean enableProfile) throws IOException { 286 if (p == null) return; 287 mProfileDb.saveProfile(p); 288 mSipAccountRegistry.startSipService(this, p.getProfileName(), enableProfile); 289 } 290 291 /** 292 * Deletes a {@link SipProfile} and un-registers the associated 293 * {@link android.telecom.PhoneAccount}. 294 * 295 * @param p The {@link SipProfile} to delete. 296 */ deleteAndUnregisterProfile(SipProfile p)297 private void deleteAndUnregisterProfile(SipProfile p) throws IOException { 298 if (p == null) return; 299 mProfileDb.deleteProfile(p); 300 mSipAccountRegistry.stopSipService(this, p.getProfileName()); 301 } 302 setRemovedProfileAndFinish()303 private void setRemovedProfileAndFinish() { 304 Intent intent = new Intent(this, SipSettings.class); 305 setResult(RESULT_FIRST_USER, intent); 306 Toast.makeText(this, R.string.removing_account, Toast.LENGTH_SHORT) 307 .show(); 308 replaceProfile(mOldProfile, null); 309 // do finish() in replaceProfile() in a background thread 310 } 311 showAlert(Throwable e)312 private void showAlert(Throwable e) { 313 String msg = e.getMessage(); 314 if (TextUtils.isEmpty(msg)) msg = e.toString(); 315 showAlert(msg); 316 } 317 showAlert(final String message)318 private void showAlert(final String message) { 319 if (mHomeButtonClicked) { 320 if (VERBOSE) log("Home button clicked, don't show dialog: " + message); 321 return; 322 } 323 324 AlertDialogFragment newFragment = AlertDialogFragment.newInstance(message); 325 newFragment.show(getFragmentManager(), null); 326 } 327 isEditTextEmpty(PreferenceKey key)328 private boolean isEditTextEmpty(PreferenceKey key) { 329 EditTextPreference pref = (EditTextPreference) key.preference; 330 return TextUtils.isEmpty(pref.getText()) 331 || pref.getSummary().equals(getString(key.defaultSummary)); 332 } 333 validateAndSetResult()334 private void validateAndSetResult() { 335 boolean allEmpty = true; 336 CharSequence firstEmptyFieldTitle = null; 337 for (PreferenceKey key : PreferenceKey.values()) { 338 Preference p = key.preference; 339 if (p instanceof EditTextPreference) { 340 EditTextPreference pref = (EditTextPreference) p; 341 boolean fieldEmpty = isEditTextEmpty(key); 342 if (allEmpty && !fieldEmpty) allEmpty = false; 343 344 // use default value if display name is empty 345 if (fieldEmpty) { 346 switch (key) { 347 case DisplayName: 348 pref.setText(getDefaultDisplayName()); 349 break; 350 case AuthUserName: 351 case ProxyAddress: 352 // optional; do nothing 353 break; 354 case Port: 355 pref.setText(getString(R.string.default_port)); 356 break; 357 default: 358 if (firstEmptyFieldTitle == null) { 359 firstEmptyFieldTitle = pref.getTitle(); 360 } 361 } 362 } else if (key == PreferenceKey.Port) { 363 int port; 364 try { 365 port = Integer.parseInt(PreferenceKey.Port.getValue()); 366 } catch (NumberFormatException e) { 367 showAlert(getString(R.string.not_a_valid_port)); 368 return; 369 } 370 if ((port < 1000) || (port > 65534)) { 371 showAlert(getString(R.string.not_a_valid_port)); 372 return; 373 } 374 } 375 } 376 } 377 378 if (!mUpdateRequired) { 379 finish(); 380 return; 381 } else if (allEmpty) { 382 showAlert(getString(R.string.all_empty_alert)); 383 return; 384 } else if (firstEmptyFieldTitle != null) { 385 showAlert(getString(R.string.empty_alert, firstEmptyFieldTitle)); 386 return; 387 } 388 try { 389 SipProfile profile = createSipProfile(); 390 Intent intent = new Intent(this, SipSettings.class); 391 intent.putExtra(SipSettings.KEY_SIP_PROFILE, (Parcelable) profile); 392 setResult(RESULT_OK, intent); 393 Toast.makeText(this, R.string.saving_account, Toast.LENGTH_SHORT).show(); 394 395 replaceProfile(mOldProfile, profile); 396 // do finish() in replaceProfile() in a background thread 397 } catch (Exception e) { 398 log("validateAndSetResult, can not create new SipProfile, exception: " + e); 399 showAlert(e); 400 } 401 } 402 replaceProfile(final SipProfile oldProfile, final SipProfile newProfile)403 private void replaceProfile(final SipProfile oldProfile, final SipProfile newProfile) { 404 // Replace profile in a background thread as it takes time to access the 405 // storage; do finish() once everything goes fine. 406 // newProfile may be null if the old profile is to be deleted rather 407 // than being modified. 408 new Thread(new Runnable() { 409 public void run() { 410 try { 411 deleteAndUnregisterProfile(oldProfile); 412 boolean autoEnableNewProfile = oldProfile == null; 413 saveAndRegisterProfile(newProfile, autoEnableNewProfile); 414 finish(); 415 } catch (Exception e) { 416 log("replaceProfile, can not save/register new SipProfile, exception: " + e); 417 showAlert(e); 418 } 419 } 420 }, "SipEditor").start(); 421 } 422 getProfileName()423 private String getProfileName() { 424 return PreferenceKey.Username.getValue() + "@" 425 + PreferenceKey.DomainAddress.getValue(); 426 } 427 createSipProfile()428 private SipProfile createSipProfile() throws Exception { 429 return new SipProfile.Builder( 430 PreferenceKey.Username.getValue(), 431 PreferenceKey.DomainAddress.getValue()) 432 .setProfileName(getProfileName()) 433 .setPassword(PreferenceKey.Password.getValue()) 434 .setOutboundProxy(PreferenceKey.ProxyAddress.getValue()) 435 .setProtocol(PreferenceKey.Transport.getValue()) 436 .setDisplayName(PreferenceKey.DisplayName.getValue()) 437 .setPort(Integer.parseInt(PreferenceKey.Port.getValue())) 438 .setSendKeepAlive(isAlwaysSendKeepAlive()) 439 .setAutoRegistration( 440 mSipPreferences.isReceivingCallsEnabled()) 441 .setAuthUserName(PreferenceKey.AuthUserName.getValue()) 442 .build(); 443 } 444 onPreferenceChange(Preference pref, Object newValue)445 public boolean onPreferenceChange(Preference pref, Object newValue) { 446 if (!mUpdateRequired) { 447 mUpdateRequired = true; 448 } 449 450 if (pref instanceof CheckBoxPreference) { 451 invalidateOptionsMenu(); 452 return true; 453 } 454 String value = (newValue == null) ? "" : newValue.toString(); 455 if (TextUtils.isEmpty(value)) { 456 pref.setSummary(getPreferenceKey(pref).defaultSummary); 457 } else if (pref == PreferenceKey.Password.preference) { 458 pref.setSummary(scramble(value)); 459 } else { 460 pref.setSummary(value); 461 } 462 463 if (pref == PreferenceKey.DisplayName.preference) { 464 ((EditTextPreference) pref).setText(value); 465 checkIfDisplayNameSet(); 466 } 467 468 // SAVE menu should be enabled once the user modified some preference. 469 invalidateOptionsMenu(); 470 return true; 471 } 472 getPreferenceKey(Preference pref)473 private PreferenceKey getPreferenceKey(Preference pref) { 474 for (PreferenceKey key : PreferenceKey.values()) { 475 if (key.preference == pref) return key; 476 } 477 throw new RuntimeException("not possible to reach here"); 478 } 479 loadPreferencesFromProfile(SipProfile p)480 private void loadPreferencesFromProfile(SipProfile p) { 481 if (p != null) { 482 if (VERBOSE) log("loadPreferencesFromProfile, existing profile: " + p.getProfileName()); 483 try { 484 Class profileClass = SipProfile.class; 485 for (PreferenceKey key : PreferenceKey.values()) { 486 Method meth = profileClass.getMethod(GET_METHOD_PREFIX 487 + getString(key.text), (Class[])null); 488 if (key == PreferenceKey.SendKeepAlive) { 489 boolean value = ((Boolean) meth.invoke(p, (Object[]) null)).booleanValue(); 490 key.setValue(getString(value 491 ? R.string.sip_always_send_keepalive 492 : R.string.sip_system_decide)); 493 } else { 494 Object value = meth.invoke(p, (Object[])null); 495 key.setValue((value == null) ? "" : value.toString()); 496 } 497 } 498 checkIfDisplayNameSet(); 499 } catch (Exception e) { 500 log("loadPreferencesFromProfile, can not load pref from profile, exception: " + e); 501 } 502 } else { 503 if (VERBOSE) log("loadPreferencesFromProfile, edit a new profile"); 504 for (PreferenceKey key : PreferenceKey.values()) { 505 key.preference.setOnPreferenceChangeListener(this); 506 507 // FIXME: android:defaultValue in preference xml file doesn't 508 // work. Even if we setValue() for each preference in the case 509 // of (p != null), the dialog still shows android:defaultValue, 510 // not the value set by setValue(). This happens if 511 // android:defaultValue is not empty. Is it a bug? 512 if (key.initValue != 0) { 513 key.setValue(getString(key.initValue)); 514 } 515 } 516 mDisplayNameSet = false; 517 } 518 } 519 isAlwaysSendKeepAlive()520 private boolean isAlwaysSendKeepAlive() { 521 ListPreference pref = (ListPreference) PreferenceKey.SendKeepAlive.preference; 522 return getString(R.string.sip_always_send_keepalive).equals(pref.getValue()); 523 } 524 setCheckBox(PreferenceKey key, boolean checked)525 private void setCheckBox(PreferenceKey key, boolean checked) { 526 CheckBoxPreference pref = (CheckBoxPreference) key.preference; 527 pref.setChecked(checked); 528 } 529 setupPreference(Preference pref)530 private void setupPreference(Preference pref) { 531 pref.setOnPreferenceChangeListener(this); 532 for (PreferenceKey key : PreferenceKey.values()) { 533 String name = getString(key.text); 534 if (name.equals(pref.getKey())) { 535 key.preference = pref; 536 return; 537 } 538 } 539 } 540 checkIfDisplayNameSet()541 private void checkIfDisplayNameSet() { 542 String displayName = PreferenceKey.DisplayName.getValue(); 543 mDisplayNameSet = !TextUtils.isEmpty(displayName) 544 && !displayName.equals(getDefaultDisplayName()); 545 if (VERBOSE) log("checkIfDisplayNameSet, displayName set: " + mDisplayNameSet); 546 if (mDisplayNameSet) { 547 PreferenceKey.DisplayName.preference.setSummary(displayName); 548 } else { 549 PreferenceKey.DisplayName.setValue(""); 550 } 551 } 552 getDefaultDisplayName()553 private static String getDefaultDisplayName() { 554 return PreferenceKey.Username.getValue(); 555 } 556 scramble(String s)557 private static String scramble(String s) { 558 char[] cc = new char[s.length()]; 559 Arrays.fill(cc, SCRAMBLED); 560 return new String(cc); 561 } 562 563 private class AdvancedSettings implements Preference.OnPreferenceClickListener { 564 private Preference mAdvancedSettingsTrigger; 565 private Preference[] mPreferences; 566 private boolean mShowing = false; 567 AdvancedSettings()568 AdvancedSettings() { 569 mAdvancedSettingsTrigger = getPreferenceScreen().findPreference( 570 getString(R.string.advanced_settings)); 571 mAdvancedSettingsTrigger.setOnPreferenceClickListener(this); 572 573 loadAdvancedPreferences(); 574 } 575 loadAdvancedPreferences()576 private void loadAdvancedPreferences() { 577 PreferenceGroup screen = (PreferenceGroup) getPreferenceScreen(); 578 579 addPreferencesFromResource(R.xml.sip_advanced_edit); 580 PreferenceGroup group = (PreferenceGroup) screen.findPreference( 581 getString(R.string.advanced_settings_container)); 582 screen.removePreference(group); 583 584 mPreferences = new Preference[group.getPreferenceCount()]; 585 int order = screen.getPreferenceCount(); 586 for (int i = 0, n = mPreferences.length; i < n; i++) { 587 Preference pref = group.getPreference(i); 588 pref.setOrder(order++); 589 setupPreference(pref); 590 mPreferences[i] = pref; 591 } 592 } 593 show()594 void show() { 595 mShowing = true; 596 mAdvancedSettingsTrigger.setSummary(R.string.advanced_settings_hide); 597 PreferenceGroup screen = (PreferenceGroup) getPreferenceScreen(); 598 for (Preference pref : mPreferences) { 599 screen.addPreference(pref); 600 if (VERBOSE) { 601 log("AdvancedSettings.show, pref: " + pref.getKey() + ", order: " + 602 pref.getOrder()); 603 } 604 } 605 } 606 hide()607 private void hide() { 608 mShowing = false; 609 mAdvancedSettingsTrigger.setSummary(R.string.advanced_settings_show); 610 PreferenceGroup screen = (PreferenceGroup) getPreferenceScreen(); 611 for (Preference pref : mPreferences) { 612 screen.removePreference(pref); 613 } 614 } 615 onPreferenceClick(Preference preference)616 public boolean onPreferenceClick(Preference preference) { 617 if (VERBOSE) log("AdvancedSettings.onPreferenceClick"); 618 if (!mShowing) { 619 show(); 620 } else { 621 hide(); 622 } 623 return true; 624 } 625 } 626 log(String msg)627 private static void log(String msg) { 628 Log.d(SipUtil.LOG_TAG, PREFIX + msg); 629 } 630 } 631