VpnDialog.java revision 2340efc9a3fb4ae7b90cf9e7b3eab8c9ff7aeec8
1/*
2 * Copyright (C) 2011 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.vpn2;
18
19import com.android.settings.R;
20
21import android.app.AlertDialog;
22import android.content.Context;
23import android.content.DialogInterface;
24import android.os.Bundle;
25import android.security.Credentials;
26import android.security.KeyStore;
27import android.text.Editable;
28import android.text.TextWatcher;
29import android.view.View;
30import android.view.WindowManager;
31import android.widget.AdapterView;
32import android.widget.ArrayAdapter;
33import android.widget.Button;
34import android.widget.CheckBox;
35import android.widget.Spinner;
36import android.widget.TextView;
37
38import java.net.InetAddress;
39
40class VpnDialog extends AlertDialog implements TextWatcher,
41        View.OnClickListener, AdapterView.OnItemSelectedListener {
42    private final KeyStore mKeyStore = KeyStore.getInstance();
43    private final DialogInterface.OnClickListener mListener;
44    private final VpnProfile mProfile;
45
46    private boolean mEditing;
47
48    private View mView;
49
50    private TextView mName;
51    private Spinner mType;
52    private TextView mServer;
53    private TextView mUsername;
54    private TextView mPassword;
55    private TextView mSearchDomains;
56    private TextView mDnsServers;
57    private TextView mRoutes;
58    private CheckBox mMppe;
59    private TextView mL2tpSecret;
60    private TextView mIpsecIdentifier;
61    private TextView mIpsecSecret;
62    private Spinner mIpsecUserCert;
63    private Spinner mIpsecCaCert;
64    private Spinner mIpsecServerCert;
65    private CheckBox mSaveLogin;
66
67    VpnDialog(Context context, DialogInterface.OnClickListener listener,
68            VpnProfile profile, boolean editing) {
69        super(context);
70        mListener = listener;
71        mProfile = profile;
72        mEditing = editing;
73    }
74
75    @Override
76    protected void onCreate(Bundle savedState) {
77        mView = getLayoutInflater().inflate(R.layout.vpn_dialog, null);
78        setView(mView);
79        setInverseBackgroundForced(true);
80
81        Context context = getContext();
82
83        // First, find out all the fields.
84        mName = (TextView) mView.findViewById(R.id.name);
85        mType = (Spinner) mView.findViewById(R.id.type);
86        mServer = (TextView) mView.findViewById(R.id.server);
87        mUsername = (TextView) mView.findViewById(R.id.username);
88        mPassword = (TextView) mView.findViewById(R.id.password);
89        mSearchDomains = (TextView) mView.findViewById(R.id.search_domains);
90        mDnsServers = (TextView) mView.findViewById(R.id.dns_servers);
91        mRoutes = (TextView) mView.findViewById(R.id.routes);
92        mMppe = (CheckBox) mView.findViewById(R.id.mppe);
93        mL2tpSecret = (TextView) mView.findViewById(R.id.l2tp_secret);
94        mIpsecIdentifier = (TextView) mView.findViewById(R.id.ipsec_identifier);
95        mIpsecSecret = (TextView) mView.findViewById(R.id.ipsec_secret);
96        mIpsecUserCert = (Spinner) mView.findViewById(R.id.ipsec_user_cert);
97        mIpsecCaCert = (Spinner) mView.findViewById(R.id.ipsec_ca_cert);
98        mIpsecServerCert = (Spinner) mView.findViewById(R.id.ipsec_server_cert);
99        mSaveLogin = (CheckBox) mView.findViewById(R.id.save_login);
100
101        // Second, copy values from the profile.
102        mName.setText(mProfile.name);
103        mType.setSelection(mProfile.type);
104        mServer.setText(mProfile.server);
105        if (mProfile.saveLogin) {
106            mUsername.setText(mProfile.username);
107            mPassword.setText(mProfile.password);
108        }
109        mSearchDomains.setText(mProfile.searchDomains);
110        mDnsServers.setText(mProfile.dnsServers);
111        mRoutes.setText(mProfile.routes);
112        mMppe.setChecked(mProfile.mppe);
113        mL2tpSecret.setText(mProfile.l2tpSecret);
114        mIpsecIdentifier.setText(mProfile.ipsecIdentifier);
115        mIpsecSecret.setText(mProfile.ipsecSecret);
116        loadCertificates(mIpsecUserCert, Credentials.USER_PRIVATE_KEY,
117                0, mProfile.ipsecUserCert);
118        loadCertificates(mIpsecCaCert, Credentials.CA_CERTIFICATE,
119                R.string.vpn_no_ca_cert, mProfile.ipsecCaCert);
120        loadCertificates(mIpsecServerCert, Credentials.USER_CERTIFICATE,
121                R.string.vpn_no_server_cert, mProfile.ipsecServerCert);
122        mSaveLogin.setChecked(mProfile.saveLogin);
123
124        // Third, add listeners to required fields.
125        mName.addTextChangedListener(this);
126        mType.setOnItemSelectedListener(this);
127        mServer.addTextChangedListener(this);
128        mUsername.addTextChangedListener(this);
129        mPassword.addTextChangedListener(this);
130        mDnsServers.addTextChangedListener(this);
131        mRoutes.addTextChangedListener(this);
132        mIpsecSecret.addTextChangedListener(this);
133        mIpsecUserCert.setOnItemSelectedListener(this);
134
135        // Forth, determine to do editing or connecting.
136        boolean valid = validate(true);
137        mEditing = mEditing || !valid;
138
139        if (mEditing) {
140            setTitle(R.string.vpn_edit);
141
142            // Show common fields.
143            mView.findViewById(R.id.editor).setVisibility(View.VISIBLE);
144
145            // Show type-specific fields.
146            changeType(mProfile.type);
147
148            // Show advanced options directly if any of them is set.
149            View showOptions = mView.findViewById(R.id.show_options);
150            if (mProfile.searchDomains.isEmpty() && mProfile.dnsServers.isEmpty() &&
151                    mProfile.routes.isEmpty()) {
152                showOptions.setOnClickListener(this);
153            } else {
154                onClick(showOptions);
155            }
156
157            // Create a button to save the profile.
158            setButton(DialogInterface.BUTTON_POSITIVE,
159                    context.getString(R.string.vpn_save), mListener);
160        } else {
161            setTitle(context.getString(R.string.vpn_connect_to, mProfile.name));
162
163            // Not editing, just show username and password.
164            mView.findViewById(R.id.login).setVisibility(View.VISIBLE);
165
166            // Create a button to connect the network.
167            setButton(DialogInterface.BUTTON_POSITIVE,
168                    context.getString(R.string.vpn_connect), mListener);
169        }
170
171        // Always provide a cancel button.
172        setButton(DialogInterface.BUTTON_NEGATIVE,
173                context.getString(R.string.vpn_cancel), mListener);
174
175        // Let AlertDialog create everything.
176        super.onCreate(null);
177
178        // Disable the action button if necessary.
179        getButton(DialogInterface.BUTTON_POSITIVE)
180                .setEnabled(mEditing ? valid : validate(false));
181
182        // Workaround to resize the dialog for the input method.
183        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE |
184                WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
185    }
186
187    @Override
188    public void afterTextChanged(Editable field) {
189        getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(validate(mEditing));
190    }
191
192    @Override
193    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
194    }
195
196    @Override
197    public void onTextChanged(CharSequence s, int start, int before, int count) {
198    }
199
200    @Override
201    public void onClick(View showOptions) {
202        showOptions.setVisibility(View.GONE);
203        mView.findViewById(R.id.options).setVisibility(View.VISIBLE);
204    }
205
206    @Override
207    public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
208        if (parent == mType) {
209            changeType(position);
210        }
211        getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(validate(mEditing));
212    }
213
214    @Override
215    public void onNothingSelected(AdapterView<?> parent) {
216    }
217
218    private void changeType(int type) {
219        // First, hide everything.
220        mMppe.setVisibility(View.GONE);
221        mView.findViewById(R.id.l2tp).setVisibility(View.GONE);
222        mView.findViewById(R.id.ipsec_psk).setVisibility(View.GONE);
223        mView.findViewById(R.id.ipsec_user).setVisibility(View.GONE);
224        mView.findViewById(R.id.ipsec_peer).setVisibility(View.GONE);
225
226        // Then, unhide type-specific fields.
227        switch (type) {
228            case VpnProfile.TYPE_PPTP:
229                mMppe.setVisibility(View.VISIBLE);
230                break;
231
232            case VpnProfile.TYPE_L2TP_IPSEC_PSK:
233                mView.findViewById(R.id.l2tp).setVisibility(View.VISIBLE);
234                // fall through
235            case VpnProfile.TYPE_IPSEC_XAUTH_PSK:
236                mView.findViewById(R.id.ipsec_psk).setVisibility(View.VISIBLE);
237                break;
238
239            case VpnProfile.TYPE_L2TP_IPSEC_RSA:
240                mView.findViewById(R.id.l2tp).setVisibility(View.VISIBLE);
241                // fall through
242            case VpnProfile.TYPE_IPSEC_XAUTH_RSA:
243                mView.findViewById(R.id.ipsec_user).setVisibility(View.VISIBLE);
244                // fall through
245            case VpnProfile.TYPE_IPSEC_HYBRID_RSA:
246                mView.findViewById(R.id.ipsec_peer).setVisibility(View.VISIBLE);
247                break;
248        }
249    }
250
251    private boolean validate(boolean editing) {
252        if (!editing) {
253            return mUsername.getText().length() != 0 && mPassword.getText().length() != 0;
254        }
255        if (mName.getText().length() == 0 || mServer.getText().length() == 0 ||
256                !validateAddresses(mDnsServers.getText().toString(), false) ||
257                !validateAddresses(mRoutes.getText().toString(), true)) {
258            return false;
259        }
260        switch (mType.getSelectedItemPosition()) {
261            case VpnProfile.TYPE_PPTP:
262            case VpnProfile.TYPE_IPSEC_HYBRID_RSA:
263                return true;
264
265            case VpnProfile.TYPE_L2TP_IPSEC_PSK:
266            case VpnProfile.TYPE_IPSEC_XAUTH_PSK:
267                return mIpsecSecret.getText().length() != 0;
268
269            case VpnProfile.TYPE_L2TP_IPSEC_RSA:
270            case VpnProfile.TYPE_IPSEC_XAUTH_RSA:
271                return mIpsecUserCert.getSelectedItemPosition() != 0;
272        }
273        return false;
274    }
275
276    private boolean validateAddresses(String addresses, boolean cidr) {
277        try {
278            for (String address : addresses.split(" ")) {
279                if (address.isEmpty()) {
280                    continue;
281                }
282                // Legacy VPN currently only supports IPv4.
283                int prefixLength = 32;
284                if (cidr) {
285                    String[] parts = address.split("/", 2);
286                    address = parts[0];
287                    prefixLength = Integer.parseInt(parts[1]);
288                }
289                byte[] bytes = InetAddress.parseNumericAddress(address).getAddress();
290                int integer = (bytes[3] & 0xFF) | (bytes[2] & 0xFF) << 8 |
291                        (bytes[1] & 0xFF) << 16 | (bytes[0] & 0xFF) << 24;
292                if (bytes.length != 4 || prefixLength < 0 || prefixLength > 32 ||
293                        (prefixLength < 32 && (integer << prefixLength) != 0)) {
294                    return false;
295                }
296            }
297        } catch (Exception e) {
298            return false;
299        }
300        return true;
301    }
302
303    private void loadCertificates(Spinner spinner, String prefix, int firstId, String selected) {
304        Context context = getContext();
305        String first = (firstId == 0) ? "" : context.getString(firstId);
306        String[] certificates = mKeyStore.saw(prefix);
307
308        if (certificates == null || certificates.length == 0) {
309            certificates = new String[] {first};
310        } else {
311            String[] array = new String[certificates.length + 1];
312            array[0] = first;
313            System.arraycopy(certificates, 0, array, 1, certificates.length);
314            certificates = array;
315        }
316
317        ArrayAdapter<String> adapter = new ArrayAdapter<String>(
318                context, android.R.layout.simple_spinner_item, certificates);
319        adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
320        spinner.setAdapter(adapter);
321
322        for (int i = 1; i < certificates.length; ++i) {
323            if (certificates[i].equals(selected)) {
324                spinner.setSelection(i);
325                break;
326            }
327        }
328    }
329
330    boolean isEditing() {
331        return mEditing;
332    }
333
334    VpnProfile getProfile() {
335        // First, save common fields.
336        VpnProfile profile = new VpnProfile(mProfile.key);
337        profile.name = mName.getText().toString();
338        profile.type = mType.getSelectedItemPosition();
339        profile.server = mServer.getText().toString().trim();
340        profile.username = mUsername.getText().toString();
341        profile.password = mPassword.getText().toString();
342        profile.searchDomains = mSearchDomains.getText().toString().trim();
343        profile.dnsServers = mDnsServers.getText().toString().trim();
344        profile.routes = mRoutes.getText().toString().trim();
345
346        // Then, save type-specific fields.
347        switch (profile.type) {
348            case VpnProfile.TYPE_PPTP:
349                profile.mppe = mMppe.isChecked();
350                break;
351
352            case VpnProfile.TYPE_L2TP_IPSEC_PSK:
353                profile.l2tpSecret = mL2tpSecret.getText().toString();
354                // fall through
355            case VpnProfile.TYPE_IPSEC_XAUTH_PSK:
356                profile.ipsecIdentifier = mIpsecIdentifier.getText().toString();
357                profile.ipsecSecret = mIpsecSecret.getText().toString();
358                break;
359
360            case VpnProfile.TYPE_L2TP_IPSEC_RSA:
361                profile.l2tpSecret = mL2tpSecret.getText().toString();
362                // fall through
363            case VpnProfile.TYPE_IPSEC_XAUTH_RSA:
364                if (mIpsecUserCert.getSelectedItemPosition() != 0) {
365                    profile.ipsecUserCert = (String) mIpsecUserCert.getSelectedItem();
366                }
367                // fall through
368            case VpnProfile.TYPE_IPSEC_HYBRID_RSA:
369                if (mIpsecCaCert.getSelectedItemPosition() != 0) {
370                    profile.ipsecCaCert = (String) mIpsecCaCert.getSelectedItem();
371                }
372                if (mIpsecServerCert.getSelectedItemPosition() != 0) {
373                    profile.ipsecServerCert = (String) mIpsecServerCert.getSelectedItem();
374                }
375                break;
376        }
377
378        profile.saveLogin = mSaveLogin.isChecked();
379        return profile;
380    }
381}
382