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