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