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