1/*
2 * Copyright (C) 2009 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.settings.vpn;
18
19import com.android.settings.R;
20
21import android.app.AlertDialog;
22import android.app.Dialog;
23import android.content.ComponentName;
24import android.content.BroadcastReceiver;
25import android.content.Context;
26import android.content.DialogInterface;
27import android.content.Intent;
28import android.content.ServiceConnection;
29import android.net.vpn.IVpnService;
30import android.net.vpn.L2tpIpsecProfile;
31import android.net.vpn.L2tpIpsecPskProfile;
32import android.net.vpn.L2tpProfile;
33import android.net.vpn.VpnManager;
34import android.net.vpn.VpnProfile;
35import android.net.vpn.VpnState;
36import android.net.vpn.VpnType;
37import android.os.Bundle;
38import android.os.ConditionVariable;
39import android.os.IBinder;
40import android.os.Parcelable;
41import android.preference.Preference;
42import android.preference.PreferenceActivity;
43import android.preference.PreferenceCategory;
44import android.preference.PreferenceScreen;
45import android.preference.Preference.OnPreferenceClickListener;
46import android.security.Credentials;
47import android.security.KeyStore;
48import android.text.TextUtils;
49import android.util.Log;
50import android.view.ContextMenu;
51import android.view.ContextMenu.ContextMenuInfo;
52import android.view.MenuItem;
53import android.view.View;
54import android.widget.AdapterView.AdapterContextMenuInfo;
55
56import java.io.File;
57import java.io.FileInputStream;
58import java.io.FileOutputStream;
59import java.io.IOException;
60import java.io.ObjectInputStream;
61import java.io.ObjectOutputStream;
62import java.util.ArrayList;
63import java.util.Collections;
64import java.util.Comparator;
65import java.util.LinkedHashMap;
66import java.util.List;
67import java.util.Map;
68
69/**
70 * The preference activity for configuring VPN settings.
71 */
72public class VpnSettings extends PreferenceActivity implements
73        DialogInterface.OnClickListener {
74    // Key to the field exchanged for profile editing.
75    static final String KEY_VPN_PROFILE = "vpn_profile";
76
77    // Key to the field exchanged for VPN type selection.
78    static final String KEY_VPN_TYPE = "vpn_type";
79
80    private static final String TAG = VpnSettings.class.getSimpleName();
81
82    private static final String PREF_ADD_VPN = "add_new_vpn";
83    private static final String PREF_VPN_LIST = "vpn_list";
84
85    private static final String PROFILES_ROOT = VpnManager.getProfilePath() + "/";
86    private static final String PROFILE_OBJ_FILE = ".pobj";
87
88    private static final int REQUEST_ADD_OR_EDIT_PROFILE = 1;
89    private static final int REQUEST_SELECT_VPN_TYPE = 2;
90
91    private static final int CONTEXT_MENU_CONNECT_ID = ContextMenu.FIRST + 0;
92    private static final int CONTEXT_MENU_DISCONNECT_ID = ContextMenu.FIRST + 1;
93    private static final int CONTEXT_MENU_EDIT_ID = ContextMenu.FIRST + 2;
94    private static final int CONTEXT_MENU_DELETE_ID = ContextMenu.FIRST + 3;
95
96    private static final int CONNECT_BUTTON = DialogInterface.BUTTON_POSITIVE;
97    private static final int OK_BUTTON = DialogInterface.BUTTON_POSITIVE;
98
99    private static final int DIALOG_CONNECT = VpnManager.VPN_ERROR_LARGEST + 1;
100    private static final int DIALOG_SECRET_NOT_SET = DIALOG_CONNECT + 1;
101
102    private static final int NO_ERROR = VpnManager.VPN_ERROR_NO_ERROR;
103
104    private static final String KEY_PREFIX_IPSEC_PSK = Credentials.VPN + 'i';
105    private static final String KEY_PREFIX_L2TP_SECRET = Credentials.VPN + 'l';
106
107    private PreferenceScreen mAddVpn;
108    private PreferenceCategory mVpnListContainer;
109
110    // profile name --> VpnPreference
111    private Map<String, VpnPreference> mVpnPreferenceMap;
112    private List<VpnProfile> mVpnProfileList;
113
114    // profile engaged in a connection
115    private VpnProfile mActiveProfile;
116
117    // actor engaged in connecting
118    private VpnProfileActor mConnectingActor;
119
120    // states saved for unlocking keystore
121    private Runnable mUnlockAction;
122
123    private KeyStore mKeyStore = KeyStore.getInstance();
124
125    private VpnManager mVpnManager = new VpnManager(this);
126
127    private ConnectivityReceiver mConnectivityReceiver =
128            new ConnectivityReceiver();
129
130    private int mConnectingErrorCode = NO_ERROR;
131
132    private Dialog mShowingDialog;
133
134    private StatusChecker mStatusChecker = new StatusChecker();
135
136    @Override
137    public void onCreate(Bundle savedInstanceState) {
138        super.onCreate(savedInstanceState);
139        addPreferencesFromResource(R.xml.vpn_settings);
140
141        // restore VpnProfile list and construct VpnPreference map
142        mVpnListContainer = (PreferenceCategory) findPreference(PREF_VPN_LIST);
143
144        // set up the "add vpn" preference
145        mAddVpn = (PreferenceScreen) findPreference(PREF_ADD_VPN);
146        mAddVpn.setOnPreferenceClickListener(
147                new OnPreferenceClickListener() {
148                    public boolean onPreferenceClick(Preference preference) {
149                        startVpnTypeSelection();
150                        return true;
151                    }
152                });
153
154        // for long-press gesture on a profile preference
155        registerForContextMenu(getListView());
156
157        // listen to vpn connectivity event
158        mVpnManager.registerConnectivityReceiver(mConnectivityReceiver);
159
160        retrieveVpnListFromStorage();
161        checkVpnConnectionStatusInBackground();
162    }
163
164    @Override
165    public void onResume() {
166        super.onResume();
167
168        if ((mUnlockAction != null) && isKeyStoreUnlocked()) {
169            Runnable action = mUnlockAction;
170            mUnlockAction = null;
171            runOnUiThread(action);
172        }
173    }
174
175    @Override
176    protected void onDestroy() {
177        super.onDestroy();
178        unregisterForContextMenu(getListView());
179        mVpnManager.unregisterConnectivityReceiver(mConnectivityReceiver);
180        if ((mShowingDialog != null) && mShowingDialog.isShowing()) {
181            mShowingDialog.dismiss();
182        }
183    }
184
185    @Override
186    protected Dialog onCreateDialog (int id) {
187        switch (id) {
188            case DIALOG_CONNECT:
189                return createConnectDialog();
190
191            case DIALOG_SECRET_NOT_SET:
192                return createSecretNotSetDialog();
193
194            case VpnManager.VPN_ERROR_CHALLENGE:
195            case VpnManager.VPN_ERROR_UNKNOWN_SERVER:
196            case VpnManager.VPN_ERROR_PPP_NEGOTIATION_FAILED:
197                return createEditDialog(id);
198
199            default:
200                Log.d(TAG, "create reconnect dialog for event " + id);
201                return createReconnectDialog(id);
202        }
203    }
204
205    private Dialog createConnectDialog() {
206        return new AlertDialog.Builder(this)
207                .setView(mConnectingActor.createConnectView())
208                .setTitle(String.format(getString(R.string.vpn_connect_to),
209                        mActiveProfile.getName()))
210                .setPositiveButton(getString(R.string.vpn_connect_button),
211                        this)
212                .setNegativeButton(getString(android.R.string.cancel),
213                        this)
214                .setOnCancelListener(new DialogInterface.OnCancelListener() {
215                            public void onCancel(DialogInterface dialog) {
216                                removeDialog(DIALOG_CONNECT);
217                                changeState(mActiveProfile, VpnState.IDLE);
218                            }
219                        })
220                .create();
221    }
222
223    private Dialog createReconnectDialog(int id) {
224        int msgId;
225        switch (id) {
226            case VpnManager.VPN_ERROR_AUTH:
227                msgId = R.string.vpn_auth_error_dialog_msg;
228                break;
229
230            case VpnManager.VPN_ERROR_REMOTE_HUNG_UP:
231                msgId = R.string.vpn_remote_hung_up_error_dialog_msg;
232                break;
233
234            case VpnManager.VPN_ERROR_CONNECTION_LOST:
235                msgId = R.string.vpn_reconnect_from_lost;
236                break;
237
238            case VpnManager.VPN_ERROR_REMOTE_PPP_HUNG_UP:
239                msgId = R.string.vpn_remote_ppp_hung_up_error_dialog_msg;
240                break;
241
242            default:
243                msgId = R.string.vpn_confirm_reconnect;
244        }
245        return createCommonDialogBuilder().setMessage(msgId).create();
246    }
247
248    private Dialog createEditDialog(int id) {
249        int msgId;
250        switch (id) {
251            case VpnManager.VPN_ERROR_CHALLENGE:
252                msgId = R.string.vpn_challenge_error_dialog_msg;
253                break;
254
255            case VpnManager.VPN_ERROR_UNKNOWN_SERVER:
256                msgId = R.string.vpn_unknown_server_dialog_msg;
257                break;
258
259            case VpnManager.VPN_ERROR_PPP_NEGOTIATION_FAILED:
260                msgId = R.string.vpn_ppp_negotiation_failed_dialog_msg;
261                break;
262
263            default:
264                return null;
265        }
266        return createCommonEditDialogBuilder().setMessage(msgId).create();
267    }
268
269    private Dialog createSecretNotSetDialog() {
270        return createCommonDialogBuilder()
271                .setMessage(R.string.vpn_secret_not_set_dialog_msg)
272                .setPositiveButton(R.string.vpn_yes_button,
273                        new DialogInterface.OnClickListener() {
274                            public void onClick(DialogInterface dialog, int w) {
275                                startVpnEditor(mActiveProfile);
276                            }
277                        })
278                .create();
279    }
280
281    private AlertDialog.Builder createCommonEditDialogBuilder() {
282        return createCommonDialogBuilder()
283                .setPositiveButton(R.string.vpn_yes_button,
284                        new DialogInterface.OnClickListener() {
285                            public void onClick(DialogInterface dialog, int w) {
286                                VpnProfile p = mActiveProfile;
287                                onIdle();
288                                startVpnEditor(p);
289                            }
290                        });
291    }
292
293    private AlertDialog.Builder createCommonDialogBuilder() {
294        return new AlertDialog.Builder(this)
295                .setTitle(android.R.string.dialog_alert_title)
296                .setIcon(android.R.drawable.ic_dialog_alert)
297                .setPositiveButton(R.string.vpn_yes_button,
298                        new DialogInterface.OnClickListener() {
299                            public void onClick(DialogInterface dialog, int w) {
300                                connectOrDisconnect(mActiveProfile);
301                            }
302                        })
303                .setNegativeButton(R.string.vpn_no_button,
304                        new DialogInterface.OnClickListener() {
305                            public void onClick(DialogInterface dialog, int w) {
306                                onIdle();
307                            }
308                        })
309                .setOnCancelListener(new DialogInterface.OnCancelListener() {
310                            public void onCancel(DialogInterface dialog) {
311                                onIdle();
312                            }
313                        });
314    }
315
316    @Override
317    public void onCreateContextMenu(ContextMenu menu, View v,
318            ContextMenuInfo menuInfo) {
319        super.onCreateContextMenu(menu, v, menuInfo);
320
321        VpnProfile p = getProfile(getProfilePositionFrom(
322                    (AdapterContextMenuInfo) menuInfo));
323        if (p != null) {
324            VpnState state = p.getState();
325            menu.setHeaderTitle(p.getName());
326
327            boolean isIdle = (state == VpnState.IDLE);
328            boolean isNotConnect = (isIdle || (state == VpnState.DISCONNECTING)
329                    || (state == VpnState.CANCELLED));
330            menu.add(0, CONTEXT_MENU_CONNECT_ID, 0, R.string.vpn_menu_connect)
331                    .setEnabled(isIdle && (mActiveProfile == null));
332            menu.add(0, CONTEXT_MENU_DISCONNECT_ID, 0,
333                    R.string.vpn_menu_disconnect)
334                    .setEnabled(state == VpnState.CONNECTED);
335            menu.add(0, CONTEXT_MENU_EDIT_ID, 0, R.string.vpn_menu_edit)
336                    .setEnabled(isNotConnect);
337            menu.add(0, CONTEXT_MENU_DELETE_ID, 0, R.string.vpn_menu_delete)
338                    .setEnabled(isNotConnect);
339        }
340    }
341
342    @Override
343    public boolean onContextItemSelected(MenuItem item) {
344        int position = getProfilePositionFrom(
345                (AdapterContextMenuInfo) item.getMenuInfo());
346        VpnProfile p = getProfile(position);
347
348        switch(item.getItemId()) {
349        case CONTEXT_MENU_CONNECT_ID:
350        case CONTEXT_MENU_DISCONNECT_ID:
351            connectOrDisconnect(p);
352            return true;
353
354        case CONTEXT_MENU_EDIT_ID:
355            startVpnEditor(p);
356            return true;
357
358        case CONTEXT_MENU_DELETE_ID:
359            deleteProfile(position);
360            return true;
361        }
362
363        return super.onContextItemSelected(item);
364    }
365
366    @Override
367    protected void onActivityResult(final int requestCode, final int resultCode,
368            final Intent data) {
369        if ((resultCode == RESULT_CANCELED) || (data == null)) {
370            Log.d(TAG, "no result returned by editor");
371            return;
372        }
373
374        if (requestCode == REQUEST_SELECT_VPN_TYPE) {
375            String typeName = data.getStringExtra(KEY_VPN_TYPE);
376            startVpnEditor(createVpnProfile(typeName));
377        } else if (requestCode == REQUEST_ADD_OR_EDIT_PROFILE) {
378            VpnProfile p = data.getParcelableExtra(KEY_VPN_PROFILE);
379            if (p == null) {
380                Log.e(TAG, "null object returned by editor");
381                return;
382            }
383
384            int index = getProfileIndexFromId(p.getId());
385            if (checkDuplicateName(p, index)) {
386                final VpnProfile profile = p;
387                Util.showErrorMessage(this, String.format(
388                        getString(R.string.vpn_error_duplicate_name),
389                        p.getName()),
390                        new DialogInterface.OnClickListener() {
391                            public void onClick(DialogInterface dialog, int w) {
392                                startVpnEditor(profile);
393                            }
394                        });
395                return;
396            }
397
398            if (needKeyStoreToSave(p)) {
399                Runnable action = new Runnable() {
400                    public void run() {
401                        onActivityResult(requestCode, resultCode, data);
402                    }
403                };
404                if (!unlockKeyStore(p, action)) return;
405            }
406
407            try {
408                if (index < 0) {
409                    addProfile(p);
410                    Util.showShortToastMessage(this, String.format(
411                            getString(R.string.vpn_profile_added), p.getName()));
412                } else {
413                    replaceProfile(index, p);
414                    Util.showShortToastMessage(this, String.format(
415                            getString(R.string.vpn_profile_replaced),
416                            p.getName()));
417                }
418            } catch (IOException e) {
419                final VpnProfile profile = p;
420                Util.showErrorMessage(this, e + ": " + e.getMessage(),
421                        new DialogInterface.OnClickListener() {
422                            public void onClick(DialogInterface dialog, int w) {
423                                startVpnEditor(profile);
424                            }
425                        });
426            }
427        } else {
428            throw new RuntimeException("unknown request code: " + requestCode);
429        }
430    }
431
432    // Called when the buttons on the connect dialog are clicked.
433    //@Override
434    public synchronized void onClick(DialogInterface dialog, int which) {
435        if (which == CONNECT_BUTTON) {
436            Dialog d = (Dialog) dialog;
437            String error = mConnectingActor.validateInputs(d);
438            if (error == null) {
439                mConnectingActor.connect(d);
440                removeDialog(DIALOG_CONNECT);
441                return;
442            } else {
443                dismissDialog(DIALOG_CONNECT);
444                // show error dialog
445                mShowingDialog = new AlertDialog.Builder(this)
446                        .setTitle(android.R.string.dialog_alert_title)
447                        .setIcon(android.R.drawable.ic_dialog_alert)
448                        .setMessage(String.format(getString(
449                                R.string.vpn_error_miss_entering), error))
450                        .setPositiveButton(R.string.vpn_back_button,
451                                new DialogInterface.OnClickListener() {
452                                    public void onClick(DialogInterface dialog,
453                                            int which) {
454                                        showDialog(DIALOG_CONNECT);
455                                    }
456                                })
457                        .create();
458                mShowingDialog.show();
459            }
460        } else {
461            removeDialog(DIALOG_CONNECT);
462            changeState(mActiveProfile, VpnState.IDLE);
463        }
464    }
465
466    private int getProfileIndexFromId(String id) {
467        int index = 0;
468        for (VpnProfile p : mVpnProfileList) {
469            if (p.getId().equals(id)) {
470                return index;
471            } else {
472                index++;
473            }
474        }
475        return -1;
476    }
477
478    // Replaces the profile at index in mVpnProfileList with p.
479    // Returns true if p's name is a duplicate.
480    private boolean checkDuplicateName(VpnProfile p, int index) {
481        List<VpnProfile> list = mVpnProfileList;
482        VpnPreference pref = mVpnPreferenceMap.get(p.getName());
483        if ((pref != null) && (index >= 0) && (index < list.size())) {
484            // not a duplicate if p is to replace the profile at index
485            if (pref.mProfile == list.get(index)) pref = null;
486        }
487        return (pref != null);
488    }
489
490    private int getProfilePositionFrom(AdapterContextMenuInfo menuInfo) {
491        // excludes mVpnListContainer and the preferences above it
492        return menuInfo.position - mVpnListContainer.getOrder() - 1;
493    }
494
495    // position: position in mVpnProfileList
496    private VpnProfile getProfile(int position) {
497        return ((position >= 0) ? mVpnProfileList.get(position) : null);
498    }
499
500    // position: position in mVpnProfileList
501    private void deleteProfile(final int position) {
502        if ((position < 0) || (position >= mVpnProfileList.size())) return;
503        DialogInterface.OnClickListener onClickListener =
504                new DialogInterface.OnClickListener() {
505                    public void onClick(DialogInterface dialog, int which) {
506                        dialog.dismiss();
507                        if (which == OK_BUTTON) {
508                            VpnProfile p = mVpnProfileList.remove(position);
509                            VpnPreference pref =
510                                    mVpnPreferenceMap.remove(p.getName());
511                            mVpnListContainer.removePreference(pref);
512                            removeProfileFromStorage(p);
513                        }
514                    }
515                };
516        mShowingDialog = new AlertDialog.Builder(this)
517                .setTitle(android.R.string.dialog_alert_title)
518                .setIcon(android.R.drawable.ic_dialog_alert)
519                .setMessage(R.string.vpn_confirm_profile_deletion)
520                .setPositiveButton(android.R.string.ok, onClickListener)
521                .setNegativeButton(R.string.vpn_no_button, onClickListener)
522                .create();
523        mShowingDialog.show();
524    }
525
526    // Randomly generates an ID for the profile.
527    // The ID is unique and only set once when the profile is created.
528    private void setProfileId(VpnProfile profile) {
529        String id;
530
531        while (true) {
532            id = String.valueOf(Math.abs(
533                    Double.doubleToLongBits(Math.random())));
534            if (id.length() >= 8) break;
535        }
536        for (VpnProfile p : mVpnProfileList) {
537            if (p.getId().equals(id)) {
538                setProfileId(profile);
539                return;
540            }
541        }
542        profile.setId(id);
543    }
544
545    private void addProfile(VpnProfile p) throws IOException {
546        setProfileId(p);
547        processSecrets(p);
548        saveProfileToStorage(p);
549
550        mVpnProfileList.add(p);
551        addPreferenceFor(p);
552        disableProfilePreferencesIfOneActive();
553    }
554
555    private VpnPreference addPreferenceFor(VpnProfile p) {
556        return addPreferenceFor(p, true);
557    }
558
559    // Adds a preference in mVpnListContainer
560    private VpnPreference addPreferenceFor(
561            VpnProfile p, boolean addToContainer) {
562        VpnPreference pref = new VpnPreference(this, p);
563        mVpnPreferenceMap.put(p.getName(), pref);
564        if (addToContainer) mVpnListContainer.addPreference(pref);
565
566        pref.setOnPreferenceClickListener(
567                new Preference.OnPreferenceClickListener() {
568                    public boolean onPreferenceClick(Preference pref) {
569                        connectOrDisconnect(((VpnPreference) pref).mProfile);
570                        return true;
571                    }
572                });
573        return pref;
574    }
575
576    // index: index to mVpnProfileList
577    private void replaceProfile(int index, VpnProfile p) throws IOException {
578        Map<String, VpnPreference> map = mVpnPreferenceMap;
579        VpnProfile oldProfile = mVpnProfileList.set(index, p);
580        VpnPreference pref = map.remove(oldProfile.getName());
581        if (pref.mProfile != oldProfile) {
582            throw new RuntimeException("inconsistent state!");
583        }
584
585        p.setId(oldProfile.getId());
586
587        processSecrets(p);
588
589        // TODO: remove copyFiles once the setId() code propagates.
590        // Copy config files and remove the old ones if they are in different
591        // directories.
592        if (Util.copyFiles(getProfileDir(oldProfile), getProfileDir(p))) {
593            removeProfileFromStorage(oldProfile);
594        }
595        saveProfileToStorage(p);
596
597        pref.setProfile(p);
598        map.put(p.getName(), pref);
599    }
600
601    private void startVpnTypeSelection() {
602        Intent intent = new Intent(this, VpnTypeSelection.class);
603        startActivityForResult(intent, REQUEST_SELECT_VPN_TYPE);
604    }
605
606    private boolean isKeyStoreUnlocked() {
607        return mKeyStore.test() == KeyStore.NO_ERROR;
608    }
609
610    // Returns true if the profile needs to access keystore
611    private boolean needKeyStoreToSave(VpnProfile p) {
612        switch (p.getType()) {
613            case L2TP_IPSEC_PSK:
614                L2tpIpsecPskProfile pskProfile = (L2tpIpsecPskProfile) p;
615                String presharedKey = pskProfile.getPresharedKey();
616                if (!TextUtils.isEmpty(presharedKey)) return true;
617                // pass through
618
619            case L2TP:
620                L2tpProfile l2tpProfile = (L2tpProfile) p;
621                if (l2tpProfile.isSecretEnabled() &&
622                        !TextUtils.isEmpty(l2tpProfile.getSecretString())) {
623                    return true;
624                }
625                // pass through
626
627            default:
628                return false;
629        }
630    }
631
632    // Returns true if the profile needs to access keystore
633    private boolean needKeyStoreToConnect(VpnProfile p) {
634        switch (p.getType()) {
635            case L2TP_IPSEC:
636            case L2TP_IPSEC_PSK:
637                return true;
638
639            case L2TP:
640                return ((L2tpProfile) p).isSecretEnabled();
641
642            default:
643                return false;
644        }
645    }
646
647    // Returns true if keystore is unlocked or keystore is not a concern
648    private boolean unlockKeyStore(VpnProfile p, Runnable action) {
649        if (isKeyStoreUnlocked()) return true;
650        mUnlockAction = action;
651        Credentials.getInstance().unlock(this);
652        return false;
653    }
654
655    private void startVpnEditor(final VpnProfile profile) {
656        Intent intent = new Intent(this, VpnEditor.class);
657        intent.putExtra(KEY_VPN_PROFILE, (Parcelable) profile);
658        startActivityForResult(intent, REQUEST_ADD_OR_EDIT_PROFILE);
659    }
660
661    private synchronized void connect(final VpnProfile p) {
662        if (needKeyStoreToConnect(p)) {
663            Runnable action = new Runnable() {
664                public void run() {
665                    connect(p);
666                }
667            };
668            if (!unlockKeyStore(p, action)) return;
669        }
670
671        if (!checkSecrets(p)) return;
672        changeState(p, VpnState.CONNECTING);
673        if (mConnectingActor.isConnectDialogNeeded()) {
674            showDialog(DIALOG_CONNECT);
675        } else {
676            mConnectingActor.connect(null);
677        }
678    }
679
680    // Do connect or disconnect based on the current state.
681    private synchronized void connectOrDisconnect(VpnProfile p) {
682        VpnPreference pref = mVpnPreferenceMap.get(p.getName());
683        switch (p.getState()) {
684            case IDLE:
685                connect(p);
686                break;
687
688            case CONNECTING:
689                // do nothing
690                break;
691
692            case CONNECTED:
693            case DISCONNECTING:
694                changeState(p, VpnState.DISCONNECTING);
695                getActor(p).disconnect();
696                break;
697        }
698    }
699
700    private void changeState(VpnProfile p, VpnState state) {
701        VpnState oldState = p.getState();
702        if (oldState == state) return;
703
704        p.setState(state);
705        mVpnPreferenceMap.get(p.getName()).setSummary(
706                getProfileSummaryString(p));
707
708        switch (state) {
709        case CONNECTED:
710            mConnectingActor = null;
711            mActiveProfile = p;
712            disableProfilePreferencesIfOneActive();
713            break;
714
715        case CONNECTING:
716            mConnectingActor = getActor(p);
717            // pass through
718        case DISCONNECTING:
719            mActiveProfile = p;
720            disableProfilePreferencesIfOneActive();
721            break;
722
723        case CANCELLED:
724            changeState(p, VpnState.IDLE);
725            break;
726
727        case IDLE:
728            assert(mActiveProfile == p);
729
730            if (mConnectingErrorCode == NO_ERROR) {
731                onIdle();
732            } else {
733                showDialog(mConnectingErrorCode);
734                mConnectingErrorCode = NO_ERROR;
735            }
736            break;
737        }
738    }
739
740    private void onIdle() {
741        Log.d(TAG, "   onIdle()");
742        mActiveProfile = null;
743        mConnectingActor = null;
744        enableProfilePreferences();
745    }
746
747    private void disableProfilePreferencesIfOneActive() {
748        if (mActiveProfile == null) return;
749
750        for (VpnProfile p : mVpnProfileList) {
751            switch (p.getState()) {
752                case CONNECTING:
753                case DISCONNECTING:
754                case IDLE:
755                    mVpnPreferenceMap.get(p.getName()).setEnabled(false);
756                    break;
757
758                default:
759                    mVpnPreferenceMap.get(p.getName()).setEnabled(true);
760            }
761        }
762    }
763
764    private void enableProfilePreferences() {
765        for (VpnProfile p : mVpnProfileList) {
766            mVpnPreferenceMap.get(p.getName()).setEnabled(true);
767        }
768    }
769
770    static String getProfileDir(VpnProfile p) {
771        return PROFILES_ROOT + p.getId();
772    }
773
774    static void saveProfileToStorage(VpnProfile p) throws IOException {
775        File f = new File(getProfileDir(p));
776        if (!f.exists()) f.mkdirs();
777        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(
778                new File(f, PROFILE_OBJ_FILE)));
779        oos.writeObject(p);
780        oos.close();
781    }
782
783    private void removeProfileFromStorage(VpnProfile p) {
784        Util.deleteFile(getProfileDir(p));
785    }
786
787    private void retrieveVpnListFromStorage() {
788        mVpnPreferenceMap = new LinkedHashMap<String, VpnPreference>();
789        mVpnProfileList = Collections.synchronizedList(
790                new ArrayList<VpnProfile>());
791        mVpnListContainer.removeAll();
792
793        File root = new File(PROFILES_ROOT);
794        String[] dirs = root.list();
795        if (dirs == null) return;
796        for (String dir : dirs) {
797            File f = new File(new File(root, dir), PROFILE_OBJ_FILE);
798            if (!f.exists()) continue;
799            try {
800                VpnProfile p = deserialize(f);
801                if (p == null) continue;
802                if (!checkIdConsistency(dir, p)) continue;
803
804                mVpnProfileList.add(p);
805            } catch (IOException e) {
806                Log.e(TAG, "retrieveVpnListFromStorage()", e);
807            }
808        }
809        Collections.sort(mVpnProfileList, new Comparator<VpnProfile>() {
810            public int compare(VpnProfile p1, VpnProfile p2) {
811                return p1.getName().compareTo(p2.getName());
812            }
813
814            public boolean equals(VpnProfile p) {
815                // not used
816                return false;
817            }
818        });
819        for (VpnProfile p : mVpnProfileList) {
820            Preference pref = addPreferenceFor(p, false);
821        }
822        disableProfilePreferencesIfOneActive();
823    }
824
825    private void checkVpnConnectionStatusInBackground() {
826        new Thread(new Runnable() {
827            public void run() {
828                mStatusChecker.check(mVpnProfileList);
829            }
830        }).start();
831    }
832
833    // A sanity check. Returns true if the profile directory name and profile ID
834    // are consistent.
835    private boolean checkIdConsistency(String dirName, VpnProfile p) {
836        if (!dirName.equals(p.getId())) {
837            Log.d(TAG, "ID inconsistent: " + dirName + " vs " + p.getId());
838            return false;
839        } else {
840            return true;
841        }
842    }
843
844    private VpnProfile deserialize(File profileObjectFile) throws IOException {
845        try {
846            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
847                    profileObjectFile));
848            VpnProfile p = (VpnProfile) ois.readObject();
849            ois.close();
850            return p;
851        } catch (ClassNotFoundException e) {
852            Log.d(TAG, "deserialize a profile", e);
853            return null;
854        }
855    }
856
857    private String getProfileSummaryString(VpnProfile p) {
858        switch (p.getState()) {
859        case CONNECTING:
860            return getString(R.string.vpn_connecting);
861        case DISCONNECTING:
862            return getString(R.string.vpn_disconnecting);
863        case CONNECTED:
864            return getString(R.string.vpn_connected);
865        default:
866            return getString(R.string.vpn_connect_hint);
867        }
868    }
869
870    private VpnProfileActor getActor(VpnProfile p) {
871        return new AuthenticationActor(this, p);
872    }
873
874    private VpnProfile createVpnProfile(String type) {
875        return mVpnManager.createVpnProfile(Enum.valueOf(VpnType.class, type));
876    }
877
878    private boolean checkSecrets(VpnProfile p) {
879        boolean secretMissing = false;
880
881        if (p instanceof L2tpIpsecProfile) {
882            L2tpIpsecProfile certProfile = (L2tpIpsecProfile) p;
883
884            String cert = certProfile.getCaCertificate();
885            if (TextUtils.isEmpty(cert) ||
886                    !mKeyStore.contains(Credentials.CA_CERTIFICATE + cert)) {
887                certProfile.setCaCertificate(null);
888                secretMissing = true;
889            }
890
891            cert = certProfile.getUserCertificate();
892            if (TextUtils.isEmpty(cert) ||
893                    !mKeyStore.contains(Credentials.USER_CERTIFICATE + cert)) {
894                certProfile.setUserCertificate(null);
895                secretMissing = true;
896            }
897        }
898
899        if (p instanceof L2tpIpsecPskProfile) {
900            L2tpIpsecPskProfile pskProfile = (L2tpIpsecPskProfile) p;
901            String presharedKey = pskProfile.getPresharedKey();
902            String key = KEY_PREFIX_IPSEC_PSK + p.getId();
903            if (TextUtils.isEmpty(presharedKey) || !mKeyStore.contains(key)) {
904                pskProfile.setPresharedKey(null);
905                secretMissing = true;
906            }
907        }
908
909        if (p instanceof L2tpProfile) {
910            L2tpProfile l2tpProfile = (L2tpProfile) p;
911            if (l2tpProfile.isSecretEnabled()) {
912                String secret = l2tpProfile.getSecretString();
913                String key = KEY_PREFIX_L2TP_SECRET + p.getId();
914                if (TextUtils.isEmpty(secret) || !mKeyStore.contains(key)) {
915                    l2tpProfile.setSecretString(null);
916                    secretMissing = true;
917                }
918            }
919        }
920
921        if (secretMissing) {
922            mActiveProfile = p;
923            showDialog(DIALOG_SECRET_NOT_SET);
924            return false;
925        } else {
926            return true;
927        }
928    }
929
930    private void processSecrets(VpnProfile p) {
931        switch (p.getType()) {
932            case L2TP_IPSEC_PSK:
933                L2tpIpsecPskProfile pskProfile = (L2tpIpsecPskProfile) p;
934                String presharedKey = pskProfile.getPresharedKey();
935                String key = KEY_PREFIX_IPSEC_PSK + p.getId();
936                if (!TextUtils.isEmpty(presharedKey) &&
937                        !mKeyStore.put(key, presharedKey)) {
938                    Log.e(TAG, "keystore write failed: key=" + key);
939                }
940                pskProfile.setPresharedKey(key);
941                // pass through
942
943            case L2TP_IPSEC:
944            case L2TP:
945                L2tpProfile l2tpProfile = (L2tpProfile) p;
946                key = KEY_PREFIX_L2TP_SECRET + p.getId();
947                if (l2tpProfile.isSecretEnabled()) {
948                    String secret = l2tpProfile.getSecretString();
949                    if (!TextUtils.isEmpty(secret) &&
950                            !mKeyStore.put(key, secret)) {
951                        Log.e(TAG, "keystore write failed: key=" + key);
952                    }
953                    l2tpProfile.setSecretString(key);
954                } else {
955                    mKeyStore.delete(key);
956                }
957                break;
958        }
959    }
960
961    private class VpnPreference extends Preference {
962        VpnProfile mProfile;
963        VpnPreference(Context c, VpnProfile p) {
964            super(c);
965            setProfile(p);
966        }
967
968        void setProfile(VpnProfile p) {
969            mProfile = p;
970            setTitle(p.getName());
971            setSummary(getProfileSummaryString(p));
972        }
973    }
974
975    // to receive vpn connectivity events broadcast by VpnService
976    private class ConnectivityReceiver extends BroadcastReceiver {
977        @Override
978        public void onReceive(Context context, Intent intent) {
979            String profileName = intent.getStringExtra(
980                    VpnManager.BROADCAST_PROFILE_NAME);
981            if (profileName == null) return;
982
983            VpnState s = (VpnState) intent.getSerializableExtra(
984                    VpnManager.BROADCAST_CONNECTION_STATE);
985
986            if (s == null) {
987                Log.e(TAG, "received null connectivity state");
988                return;
989            }
990
991            mConnectingErrorCode = intent.getIntExtra(
992                    VpnManager.BROADCAST_ERROR_CODE, NO_ERROR);
993
994            VpnPreference pref = mVpnPreferenceMap.get(profileName);
995            if (pref != null) {
996                Log.d(TAG, "received connectivity: " + profileName
997                        + ": connected? " + s
998                        + "   err=" + mConnectingErrorCode);
999                changeState(pref.mProfile, s);
1000            } else {
1001                Log.e(TAG, "received connectivity: " + profileName
1002                        + ": connected? " + s + ", but profile does not exist;"
1003                        + " just ignore it");
1004            }
1005        }
1006    }
1007
1008    // managing status check in a background thread
1009    private class StatusChecker {
1010        private List<VpnProfile> mList;
1011
1012        synchronized void check(final List<VpnProfile> list) {
1013            final ConditionVariable cv = new ConditionVariable();
1014            cv.close();
1015            mVpnManager.startVpnService();
1016            ServiceConnection c = new ServiceConnection() {
1017                public synchronized void onServiceConnected(
1018                        ComponentName className, IBinder binder) {
1019                    cv.open();
1020
1021                    IVpnService service = IVpnService.Stub.asInterface(binder);
1022                    for (VpnProfile p : list) {
1023                        try {
1024                            service.checkStatus(p);
1025                        } catch (Throwable e) {
1026                            Log.e(TAG, " --- checkStatus(): " + p.getName(), e);
1027                            changeState(p, VpnState.IDLE);
1028                        }
1029                    }
1030                    VpnSettings.this.unbindService(this);
1031                    showPreferences();
1032                }
1033
1034                public void onServiceDisconnected(ComponentName className) {
1035                    cv.open();
1036
1037                    setDefaultState(list);
1038                    VpnSettings.this.unbindService(this);
1039                    showPreferences();
1040                }
1041            };
1042            if (mVpnManager.bindVpnService(c)) {
1043                if (!cv.block(1000)) {
1044                    Log.d(TAG, "checkStatus() bindService failed");
1045                    setDefaultState(list);
1046                }
1047            } else {
1048                setDefaultState(list);
1049            }
1050        }
1051
1052        private void showPreferences() {
1053            for (VpnProfile p : mVpnProfileList) {
1054                VpnPreference pref = mVpnPreferenceMap.get(p.getName());
1055                mVpnListContainer.addPreference(pref);
1056            }
1057        }
1058
1059        private void setDefaultState(List<VpnProfile> list) {
1060            for (VpnProfile p : list) changeState(p, VpnState.IDLE);
1061            showPreferences();
1062        }
1063    }
1064}
1065