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