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 58 private static final String TAG = SipEditor.class.getSimpleName(); 59 private static final String KEY_PROFILE = "profile"; 60 private static final String GET_METHOD_PREFIX = "get"; 61 private static final char SCRAMBLED = '*'; 62 private static final int NA = 0; 63 64 private PrimaryAccountSelector mPrimaryAccountSelector; 65 private AdvancedSettings mAdvancedSettings; 66 private SipSharedPreferences mSharedPreferences; 67 private boolean mDisplayNameSet; 68 private boolean mHomeButtonClicked; 69 private boolean mUpdateRequired; 70 71 private SipManager mSipManager; 72 private SipProfileDb mProfileDb; 73 private SipProfile mOldProfile; 74 private CallManager mCallManager; 75 private Button mRemoveButton; 76 77 enum PreferenceKey { 78 Username(R.string.username, 0, R.string.default_preference_summary), 79 Password(R.string.password, 0, R.string.default_preference_summary), 80 DomainAddress(R.string.domain_address, 0, R.string.default_preference_summary), 81 DisplayName(R.string.display_name, 0, R.string.display_name_summary), 82 ProxyAddress(R.string.proxy_address, 0, R.string.optional_summary), 83 Port(R.string.port, R.string.default_port, R.string.default_port), 84 Transport(R.string.transport, R.string.default_transport, NA), 85 SendKeepAlive(R.string.send_keepalive, R.string.sip_system_decide, NA), 86 AuthUserName(R.string.auth_username, 0, R.string.optional_summary); 87 88 final int text; 89 final int initValue; 90 final int defaultSummary; 91 Preference preference; 92 93 /** 94 * @param key The key name of the preference. 95 * @param initValue The initial value of the preference. 96 * @param defaultSummary The default summary value of the preference 97 * when the preference value is empty. 98 */ 99 PreferenceKey(int text, int initValue, int defaultSummary) { 100 this.text = text; 101 this.initValue = initValue; 102 this.defaultSummary = defaultSummary; 103 } 104 105 String getValue() { 106 if (preference instanceof EditTextPreference) { 107 return ((EditTextPreference) preference).getText(); 108 } else if (preference instanceof ListPreference) { 109 return ((ListPreference) preference).getValue(); 110 } 111 throw new RuntimeException("getValue() for the preference " + this); 112 } 113 114 void setValue(String value) { 115 if (preference instanceof EditTextPreference) { 116 String oldValue = getValue(); 117 ((EditTextPreference) preference).setText(value); 118 if (this != Password) { 119 Log.v(TAG, this + ": setValue() " + value + ": " + oldValue 120 + " --> " + getValue()); 121 } 122 } else if (preference instanceof ListPreference) { 123 ((ListPreference) preference).setValue(value); 124 } 125 126 if (TextUtils.isEmpty(value)) { 127 preference.setSummary(defaultSummary); 128 } else if (this == Password) { 129 preference.setSummary(scramble(value)); 130 } else if ((this == DisplayName) 131 && value.equals(getDefaultDisplayName())) { 132 preference.setSummary(defaultSummary); 133 } else { 134 preference.setSummary(value); 135 } 136 } 137 } 138 139 @Override 140 public void onResume() { 141 super.onResume(); 142 mHomeButtonClicked = false; 143 if (mCallManager.getState() != Phone.State.IDLE) { 144 mAdvancedSettings.show(); 145 getPreferenceScreen().setEnabled(false); 146 if (mRemoveButton != null) mRemoveButton.setEnabled(false); 147 } else { 148 getPreferenceScreen().setEnabled(true); 149 if (mRemoveButton != null) mRemoveButton.setEnabled(true); 150 } 151 } 152 153 @Override 154 public void onCreate(Bundle savedInstanceState) { 155 Log.v(TAG, "start profile editor"); 156 super.onCreate(savedInstanceState); 157 158 mSipManager = SipManager.newInstance(this); 159 mSharedPreferences = new SipSharedPreferences(this); 160 mProfileDb = new SipProfileDb(this); 161 mCallManager = CallManager.getInstance(); 162 163 setContentView(R.layout.sip_settings_ui); 164 addPreferencesFromResource(R.xml.sip_edit); 165 166 SipProfile p = mOldProfile = (SipProfile) ((savedInstanceState == null) 167 ? getIntent().getParcelableExtra(SipSettings.KEY_SIP_PROFILE) 168 : savedInstanceState.getParcelable(KEY_PROFILE)); 169 170 PreferenceGroup screen = (PreferenceGroup) getPreferenceScreen(); 171 for (int i = 0, n = screen.getPreferenceCount(); i < n; i++) { 172 setupPreference(screen.getPreference(i)); 173 } 174 175 if (p == null) { 176 findViewById(R.id.add_remove_account_bar) 177 .setVisibility(View.GONE); 178 screen.setTitle(R.string.sip_edit_new_title); 179 } else { 180 mRemoveButton = 181 (Button)findViewById(R.id.add_remove_account_button); 182 mRemoveButton.setText(getString(R.string.remove_sip_account)); 183 mRemoveButton.setOnClickListener( 184 new android.view.View.OnClickListener() { 185 public void onClick(View v) { 186 setRemovedProfileAndFinish(); 187 } 188 }); 189 } 190 mAdvancedSettings = new AdvancedSettings(); 191 mPrimaryAccountSelector = new PrimaryAccountSelector(p); 192 193 loadPreferencesFromProfile(p); 194 195 ActionBar actionBar = getActionBar(); 196 if (actionBar != null) { 197 // android.R.id.home will be triggered in onOptionsItemSelected() 198 actionBar.setDisplayHomeAsUpEnabled(true); 199 } 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 android.R.id.home: // See ActionBar#setDisplayHomeAsUpEnabled() 226 // This time just work as "back" or "save" capability. 227 case MENU_SAVE: 228 validateAndSetResult(); 229 return true; 230 231 case MENU_DISCARD: 232 finish(); 233 return true; 234 } 235 return super.onOptionsItemSelected(item); 236 } 237 238 @Override 239 public boolean onKeyDown(int keyCode, KeyEvent event) { 240 switch (keyCode) { 241 case KeyEvent.KEYCODE_BACK: 242 validateAndSetResult(); 243 return true; 244 } 245 return super.onKeyDown(keyCode, event); 246 } 247 248 private void saveAndRegisterProfile(SipProfile p) throws IOException { 249 if (p == null) return; 250 mProfileDb.saveProfile(p); 251 if (p.getAutoRegistration() 252 || mSharedPreferences.isPrimaryAccount(p.getUriString())) { 253 try { 254 mSipManager.open(p, SipUtil.createIncomingCallPendingIntent(), 255 null); 256 } catch (Exception e) { 257 Log.e(TAG, "register failed: " + p.getUriString(), e); 258 } 259 } 260 } 261 262 private void deleteAndUnregisterProfile(SipProfile p) { 263 if (p == null) return; 264 mProfileDb.deleteProfile(p); 265 unregisterProfile(p.getUriString()); 266 } 267 268 private void unregisterProfile(String uri) { 269 try { 270 mSipManager.close(uri); 271 } catch (Exception e) { 272 Log.e(TAG, "unregister failed: " + uri, e); 273 } 274 } 275 276 private void setRemovedProfileAndFinish() { 277 Intent intent = new Intent(this, SipSettings.class); 278 setResult(RESULT_FIRST_USER, intent); 279 Toast.makeText(this, R.string.removing_account, Toast.LENGTH_SHORT) 280 .show(); 281 replaceProfile(mOldProfile, null); 282 // do finish() in replaceProfile() in a background thread 283 } 284 285 private void showAlert(Throwable e) { 286 String msg = e.getMessage(); 287 if (TextUtils.isEmpty(msg)) msg = e.toString(); 288 showAlert(msg); 289 } 290 291 private void showAlert(final String message) { 292 if (mHomeButtonClicked) { 293 Log.v(TAG, "Home button clicked, don't show dialog: " + message); 294 return; 295 } 296 runOnUiThread(new Runnable() { 297 public void run() { 298 new AlertDialog.Builder(SipEditor.this) 299 .setTitle(android.R.string.dialog_alert_title) 300 .setIcon(android.R.drawable.ic_dialog_alert) 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) return true; 441 String value = (newValue == null) ? "" : newValue.toString(); 442 if (TextUtils.isEmpty(value)) { 443 pref.setSummary(getPreferenceKey(pref).defaultSummary); 444 } else if (pref == PreferenceKey.Password.preference) { 445 pref.setSummary(scramble(value)); 446 } else { 447 pref.setSummary(value); 448 } 449 450 if (pref == PreferenceKey.DisplayName.preference) { 451 ((EditTextPreference) pref).setText(value); 452 checkIfDisplayNameSet(); 453 } 454 return true; 455 } 456 457 private PreferenceKey getPreferenceKey(Preference pref) { 458 for (PreferenceKey key : PreferenceKey.values()) { 459 if (key.preference == pref) return key; 460 } 461 throw new RuntimeException("not possible to reach here"); 462 } 463 464 private void loadPreferencesFromProfile(SipProfile p) { 465 if (p != null) { 466 Log.v(TAG, "Edit the existing profile : " + p.getProfileName()); 467 try { 468 Class profileClass = SipProfile.class; 469 for (PreferenceKey key : PreferenceKey.values()) { 470 Method meth = profileClass.getMethod(GET_METHOD_PREFIX 471 + getString(key.text), (Class[])null); 472 if (key == PreferenceKey.SendKeepAlive) { 473 boolean value = ((Boolean) 474 meth.invoke(p, (Object[]) null)).booleanValue(); 475 key.setValue(getString(value 476 ? R.string.sip_always_send_keepalive 477 : R.string.sip_system_decide)); 478 } else { 479 Object value = meth.invoke(p, (Object[])null); 480 key.setValue((value == null) ? "" : value.toString()); 481 } 482 } 483 checkIfDisplayNameSet(); 484 } catch (Exception e) { 485 Log.e(TAG, "Can not load pref from profile", e); 486 } 487 } else { 488 Log.v(TAG, "Edit a new profile"); 489 for (PreferenceKey key : PreferenceKey.values()) { 490 key.preference.setOnPreferenceChangeListener(this); 491 492 // FIXME: android:defaultValue in preference xml file doesn't 493 // work. Even if we setValue() for each preference in the case 494 // of (p != null), the dialog still shows android:defaultValue, 495 // not the value set by setValue(). This happens if 496 // android:defaultValue is not empty. Is it a bug? 497 if (key.initValue != 0) { 498 key.setValue(getString(key.initValue)); 499 } 500 } 501 mDisplayNameSet = false; 502 } 503 } 504 505 private boolean isAlwaysSendKeepAlive() { 506 ListPreference pref = (ListPreference) 507 PreferenceKey.SendKeepAlive.preference; 508 return getString(R.string.sip_always_send_keepalive).equals( 509 pref.getValue()); 510 } 511 512 private void setCheckBox(PreferenceKey key, boolean checked) { 513 CheckBoxPreference pref = (CheckBoxPreference) key.preference; 514 pref.setChecked(checked); 515 } 516 517 private void setupPreference(Preference pref) { 518 pref.setOnPreferenceChangeListener(this); 519 for (PreferenceKey key : PreferenceKey.values()) { 520 String name = getString(key.text); 521 if (name.equals(pref.getKey())) { 522 key.preference = pref; 523 return; 524 } 525 } 526 } 527 528 private void checkIfDisplayNameSet() { 529 String displayName = PreferenceKey.DisplayName.getValue(); 530 mDisplayNameSet = !TextUtils.isEmpty(displayName) 531 && !displayName.equals(getDefaultDisplayName()); 532 Log.d(TAG, "displayName set? " + mDisplayNameSet); 533 if (mDisplayNameSet) { 534 PreferenceKey.DisplayName.preference.setSummary(displayName); 535 } else { 536 PreferenceKey.DisplayName.setValue(""); 537 } 538 } 539 540 private static String getDefaultDisplayName() { 541 return PreferenceKey.Username.getValue(); 542 } 543 544 private static String scramble(String s) { 545 char[] cc = new char[s.length()]; 546 Arrays.fill(cc, SCRAMBLED); 547 return new String(cc); 548 } 549 550 // only takes care of the primary account setting in SipSharedSettings 551 private class PrimaryAccountSelector { 552 private CheckBoxPreference mCheckbox; 553 private final boolean mWasPrimaryAccount; 554 555 // @param profile profile to be edited; null if adding new profile 556 PrimaryAccountSelector(SipProfile profile) { 557 mCheckbox = (CheckBoxPreference) getPreferenceScreen() 558 .findPreference(getString(R.string.set_primary)); 559 boolean noPrimaryAccountSet = 560 !mSharedPreferences.hasPrimaryAccount(); 561 boolean editNewProfile = (profile == null); 562 mWasPrimaryAccount = !editNewProfile 563 && mSharedPreferences.isPrimaryAccount( 564 profile.getUriString()); 565 566 Log.v(TAG, " noPrimaryAccountSet: " + noPrimaryAccountSet); 567 Log.v(TAG, " editNewProfile: " + editNewProfile); 568 Log.v(TAG, " mWasPrimaryAccount: " + mWasPrimaryAccount); 569 570 mCheckbox.setChecked(mWasPrimaryAccount 571 || (editNewProfile && noPrimaryAccountSet)); 572 } 573 574 boolean isSelected() { 575 return mCheckbox.isChecked(); 576 } 577 578 // profile is null if the user removes it 579 void commit(SipProfile profile) { 580 if ((profile != null) && mCheckbox.isChecked()) { 581 mSharedPreferences.setPrimaryAccount(profile.getUriString()); 582 } else if (mWasPrimaryAccount) { 583 mSharedPreferences.unsetPrimaryAccount(); 584 } 585 Log.d(TAG, " primary account changed to : " 586 + mSharedPreferences.getPrimaryAccount()); 587 } 588 } 589 590 private class AdvancedSettings 591 implements Preference.OnPreferenceClickListener { 592 private Preference mAdvancedSettingsTrigger; 593 private Preference[] mPreferences; 594 private boolean mShowing = false; 595 596 AdvancedSettings() { 597 mAdvancedSettingsTrigger = getPreferenceScreen().findPreference( 598 getString(R.string.advanced_settings)); 599 mAdvancedSettingsTrigger.setOnPreferenceClickListener(this); 600 601 loadAdvancedPreferences(); 602 } 603 604 private void loadAdvancedPreferences() { 605 PreferenceGroup screen = (PreferenceGroup) getPreferenceScreen(); 606 607 addPreferencesFromResource(R.xml.sip_advanced_edit); 608 PreferenceGroup group = (PreferenceGroup) screen.findPreference( 609 getString(R.string.advanced_settings_container)); 610 screen.removePreference(group); 611 612 mPreferences = new Preference[group.getPreferenceCount()]; 613 int order = screen.getPreferenceCount(); 614 for (int i = 0, n = mPreferences.length; i < n; i++) { 615 Preference pref = group.getPreference(i); 616 pref.setOrder(order++); 617 setupPreference(pref); 618 mPreferences[i] = pref; 619 } 620 } 621 622 void show() { 623 mShowing = true; 624 mAdvancedSettingsTrigger.setSummary(R.string.advanced_settings_hide); 625 PreferenceGroup screen = (PreferenceGroup) getPreferenceScreen(); 626 for (Preference pref : mPreferences) { 627 screen.addPreference(pref); 628 Log.v(TAG, "add pref " + pref.getKey() + ": order=" + pref.getOrder()); 629 } 630 } 631 632 private void hide() { 633 mShowing = false; 634 mAdvancedSettingsTrigger.setSummary(R.string.advanced_settings_show); 635 PreferenceGroup screen = (PreferenceGroup) getPreferenceScreen(); 636 for (Preference pref : mPreferences) { 637 screen.removePreference(pref); 638 } 639 } 640 641 public boolean onPreferenceClick(Preference preference) { 642 Log.v(TAG, "optional settings clicked"); 643 if (!mShowing) { 644 show(); 645 } else { 646 hide(); 647 } 648 return true; 649 } 650 } 651 } 652