Home | History | Annotate | Download | only in sip
      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