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