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