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