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