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