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 android.app.AlertDialog; 20 import android.content.Context; 21 import android.content.DialogInterface; 22 import android.os.Bundle; 23 import android.os.SystemProperties; 24 import android.security.Credentials; 25 import android.security.KeyStore; 26 import android.text.Editable; 27 import android.text.TextWatcher; 28 import android.view.View; 29 import android.view.WindowManager; 30 import android.widget.AdapterView; 31 import android.widget.ArrayAdapter; 32 import android.widget.CheckBox; 33 import android.widget.CompoundButton; 34 import android.widget.Spinner; 35 import android.widget.TextView; 36 37 import com.android.internal.net.VpnProfile; 38 import com.android.settings.R; 39 40 import java.net.InetAddress; 41 42 /** 43 * Dialog showing information about a VPN configuration. The dialog 44 * can be launched to either edit or prompt for credentials to connect 45 * to a user-added VPN. 46 * 47 * {@see AppDialog} 48 */ 49 class ConfigDialog extends AlertDialog implements TextWatcher, 50 View.OnClickListener, AdapterView.OnItemSelectedListener, 51 CompoundButton.OnCheckedChangeListener { 52 private final KeyStore mKeyStore = KeyStore.getInstance(); 53 private final DialogInterface.OnClickListener mListener; 54 private final VpnProfile mProfile; 55 56 private boolean mEditing; 57 private boolean mExists; 58 59 private View mView; 60 61 private TextView mName; 62 private Spinner mType; 63 private TextView mServer; 64 private TextView mUsername; 65 private TextView mPassword; 66 private TextView mSearchDomains; 67 private TextView mDnsServers; 68 private TextView mRoutes; 69 private CheckBox mMppe; 70 private TextView mL2tpSecret; 71 private TextView mIpsecIdentifier; 72 private TextView mIpsecSecret; 73 private Spinner mIpsecUserCert; 74 private Spinner mIpsecCaCert; 75 private Spinner mIpsecServerCert; 76 private CheckBox mSaveLogin; 77 private CheckBox mShowOptions; 78 private CheckBox mAlwaysOnVpn; 79 private TextView mAlwaysOnInvalidReason; 80 ConfigDialog(Context context, DialogInterface.OnClickListener listener, VpnProfile profile, boolean editing, boolean exists)81 ConfigDialog(Context context, DialogInterface.OnClickListener listener, 82 VpnProfile profile, boolean editing, boolean exists) { 83 super(context); 84 85 mListener = listener; 86 mProfile = profile; 87 mEditing = editing; 88 mExists = exists; 89 } 90 91 @Override onCreate(Bundle savedState)92 protected void onCreate(Bundle savedState) { 93 mView = getLayoutInflater().inflate(R.layout.vpn_dialog, null); 94 setView(mView); 95 96 Context context = getContext(); 97 98 // First, find out all the fields. 99 mName = (TextView) mView.findViewById(R.id.name); 100 mType = (Spinner) mView.findViewById(R.id.type); 101 mServer = (TextView) mView.findViewById(R.id.server); 102 mUsername = (TextView) mView.findViewById(R.id.username); 103 mPassword = (TextView) mView.findViewById(R.id.password); 104 mSearchDomains = (TextView) mView.findViewById(R.id.search_domains); 105 mDnsServers = (TextView) mView.findViewById(R.id.dns_servers); 106 mRoutes = (TextView) mView.findViewById(R.id.routes); 107 mMppe = (CheckBox) mView.findViewById(R.id.mppe); 108 mL2tpSecret = (TextView) mView.findViewById(R.id.l2tp_secret); 109 mIpsecIdentifier = (TextView) mView.findViewById(R.id.ipsec_identifier); 110 mIpsecSecret = (TextView) mView.findViewById(R.id.ipsec_secret); 111 mIpsecUserCert = (Spinner) mView.findViewById(R.id.ipsec_user_cert); 112 mIpsecCaCert = (Spinner) mView.findViewById(R.id.ipsec_ca_cert); 113 mIpsecServerCert = (Spinner) mView.findViewById(R.id.ipsec_server_cert); 114 mSaveLogin = (CheckBox) mView.findViewById(R.id.save_login); 115 mShowOptions = (CheckBox) mView.findViewById(R.id.show_options); 116 mAlwaysOnVpn = (CheckBox) mView.findViewById(R.id.always_on_vpn); 117 mAlwaysOnInvalidReason = (TextView) mView.findViewById(R.id.always_on_invalid_reason); 118 119 // Second, copy values from the profile. 120 mName.setText(mProfile.name); 121 mType.setSelection(mProfile.type); 122 mServer.setText(mProfile.server); 123 if (mProfile.saveLogin) { 124 mUsername.setText(mProfile.username); 125 mPassword.setText(mProfile.password); 126 } 127 mSearchDomains.setText(mProfile.searchDomains); 128 mDnsServers.setText(mProfile.dnsServers); 129 mRoutes.setText(mProfile.routes); 130 mMppe.setChecked(mProfile.mppe); 131 mL2tpSecret.setText(mProfile.l2tpSecret); 132 mIpsecIdentifier.setText(mProfile.ipsecIdentifier); 133 mIpsecSecret.setText(mProfile.ipsecSecret); 134 loadCertificates(mIpsecUserCert, Credentials.USER_PRIVATE_KEY, 0, mProfile.ipsecUserCert); 135 loadCertificates(mIpsecCaCert, Credentials.CA_CERTIFICATE, 136 R.string.vpn_no_ca_cert, mProfile.ipsecCaCert); 137 loadCertificates(mIpsecServerCert, Credentials.USER_CERTIFICATE, 138 R.string.vpn_no_server_cert, mProfile.ipsecServerCert); 139 mSaveLogin.setChecked(mProfile.saveLogin); 140 mAlwaysOnVpn.setChecked(mProfile.key.equals(VpnUtils.getLockdownVpn())); 141 142 // Hide lockdown VPN on devices that require IMS authentication 143 if (SystemProperties.getBoolean("persist.radio.imsregrequired", false)) { 144 mAlwaysOnVpn.setVisibility(View.GONE); 145 } 146 147 // Third, add listeners to required fields. 148 mName.addTextChangedListener(this); 149 mType.setOnItemSelectedListener(this); 150 mServer.addTextChangedListener(this); 151 mUsername.addTextChangedListener(this); 152 mPassword.addTextChangedListener(this); 153 mDnsServers.addTextChangedListener(this); 154 mRoutes.addTextChangedListener(this); 155 mIpsecSecret.addTextChangedListener(this); 156 mIpsecUserCert.setOnItemSelectedListener(this); 157 mShowOptions.setOnClickListener(this); 158 mAlwaysOnVpn.setOnCheckedChangeListener(this); 159 160 // Fourth, determine whether to do editing or connecting. 161 mEditing = mEditing || !validate(true /*editing*/); 162 163 if (mEditing) { 164 setTitle(R.string.vpn_edit); 165 166 // Show common fields. 167 mView.findViewById(R.id.editor).setVisibility(View.VISIBLE); 168 169 // Show type-specific fields. 170 changeType(mProfile.type); 171 172 // Hide 'save login' when we are editing. 173 mSaveLogin.setVisibility(View.GONE); 174 175 // Switch to advanced view immediately if any advanced options are on 176 if (!mProfile.searchDomains.isEmpty() || !mProfile.dnsServers.isEmpty() || 177 !mProfile.routes.isEmpty()) { 178 showAdvancedOptions(); 179 } 180 181 // Create a button to forget the profile if it has already been saved.. 182 if (mExists) { 183 setButton(DialogInterface.BUTTON_NEUTRAL, 184 context.getString(R.string.vpn_forget), mListener); 185 } 186 187 // Create a button to save the profile. 188 setButton(DialogInterface.BUTTON_POSITIVE, 189 context.getString(R.string.vpn_save), mListener); 190 } else { 191 setTitle(context.getString(R.string.vpn_connect_to, mProfile.name)); 192 193 // Create a button to connect the network. 194 setButton(DialogInterface.BUTTON_POSITIVE, 195 context.getString(R.string.vpn_connect), mListener); 196 } 197 198 // Always provide a cancel button. 199 setButton(DialogInterface.BUTTON_NEGATIVE, 200 context.getString(R.string.vpn_cancel), mListener); 201 202 // Let AlertDialog create everything. 203 super.onCreate(savedState); 204 205 // Update UI controls according to the current configuration. 206 updateUiControls(); 207 208 // Workaround to resize the dialog for the input method. 209 getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | 210 WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); 211 } 212 213 @Override onRestoreInstanceState(Bundle savedState)214 public void onRestoreInstanceState(Bundle savedState) { 215 super.onRestoreInstanceState(savedState); 216 217 // Visibility isn't restored by super.onRestoreInstanceState, so re-show the advanced 218 // options here if they were already revealed or set. 219 if (mShowOptions.isChecked()) { 220 showAdvancedOptions(); 221 } 222 } 223 224 @Override afterTextChanged(Editable field)225 public void afterTextChanged(Editable field) { 226 updateUiControls(); 227 } 228 229 @Override beforeTextChanged(CharSequence s, int start, int count, int after)230 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 231 } 232 233 @Override onTextChanged(CharSequence s, int start, int before, int count)234 public void onTextChanged(CharSequence s, int start, int before, int count) { 235 } 236 237 @Override onClick(View view)238 public void onClick(View view) { 239 if (view == mShowOptions) { 240 showAdvancedOptions(); 241 } 242 } 243 244 @Override onItemSelected(AdapterView<?> parent, View view, int position, long id)245 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { 246 if (parent == mType) { 247 changeType(position); 248 } 249 updateUiControls(); 250 } 251 252 @Override onNothingSelected(AdapterView<?> parent)253 public void onNothingSelected(AdapterView<?> parent) { 254 } 255 256 @Override onCheckedChanged(CompoundButton compoundButton, boolean b)257 public void onCheckedChanged(CompoundButton compoundButton, boolean b) { 258 if (compoundButton == mAlwaysOnVpn) { 259 updateUiControls(); 260 } 261 } 262 isVpnAlwaysOn()263 public boolean isVpnAlwaysOn() { 264 return mAlwaysOnVpn.isChecked(); 265 } 266 267 /** 268 * Updates the UI according to the current configuration entered by the user. 269 * 270 * These include: 271 * "Always-on VPN" checkbox 272 * Reason for "Always-on VPN" being disabled, when necessary 273 * "Save account information" checkbox 274 * "Save" and "Connect" buttons 275 */ updateUiControls()276 private void updateUiControls() { 277 VpnProfile profile = getProfile(); 278 279 // Always-on VPN 280 if (profile.isValidLockdownProfile()) { 281 mAlwaysOnVpn.setEnabled(true); 282 mAlwaysOnInvalidReason.setVisibility(View.GONE); 283 } else { 284 mAlwaysOnVpn.setChecked(false); 285 mAlwaysOnVpn.setEnabled(false); 286 if (!profile.isTypeValidForLockdown()) { 287 mAlwaysOnInvalidReason.setText(R.string.vpn_always_on_invalid_reason_type); 288 } else if (!profile.isServerAddressNumeric()) { 289 mAlwaysOnInvalidReason.setText(R.string.vpn_always_on_invalid_reason_server); 290 } else if (!profile.hasDns()) { 291 mAlwaysOnInvalidReason.setText(R.string.vpn_always_on_invalid_reason_no_dns); 292 } else if (!profile.areDnsAddressesNumeric()) { 293 mAlwaysOnInvalidReason.setText(R.string.vpn_always_on_invalid_reason_dns); 294 } else { 295 mAlwaysOnInvalidReason.setText(R.string.vpn_always_on_invalid_reason_other); 296 } 297 mAlwaysOnInvalidReason.setVisibility(View.VISIBLE); 298 } 299 300 // Save account information 301 if (mAlwaysOnVpn.isChecked()) { 302 mSaveLogin.setChecked(true); 303 mSaveLogin.setEnabled(false); 304 } else { 305 mSaveLogin.setChecked(mProfile.saveLogin); 306 mSaveLogin.setEnabled(true); 307 } 308 309 // Save or Connect button 310 getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(validate(mEditing)); 311 } 312 showAdvancedOptions()313 private void showAdvancedOptions() { 314 mView.findViewById(R.id.options).setVisibility(View.VISIBLE); 315 mShowOptions.setVisibility(View.GONE); 316 } 317 changeType(int type)318 private void changeType(int type) { 319 // First, hide everything. 320 mMppe.setVisibility(View.GONE); 321 mView.findViewById(R.id.l2tp).setVisibility(View.GONE); 322 mView.findViewById(R.id.ipsec_psk).setVisibility(View.GONE); 323 mView.findViewById(R.id.ipsec_user).setVisibility(View.GONE); 324 mView.findViewById(R.id.ipsec_peer).setVisibility(View.GONE); 325 326 // Then, unhide type-specific fields. 327 switch (type) { 328 case VpnProfile.TYPE_PPTP: 329 mMppe.setVisibility(View.VISIBLE); 330 break; 331 332 case VpnProfile.TYPE_L2TP_IPSEC_PSK: 333 mView.findViewById(R.id.l2tp).setVisibility(View.VISIBLE); 334 // fall through 335 case VpnProfile.TYPE_IPSEC_XAUTH_PSK: 336 mView.findViewById(R.id.ipsec_psk).setVisibility(View.VISIBLE); 337 break; 338 339 case VpnProfile.TYPE_L2TP_IPSEC_RSA: 340 mView.findViewById(R.id.l2tp).setVisibility(View.VISIBLE); 341 // fall through 342 case VpnProfile.TYPE_IPSEC_XAUTH_RSA: 343 mView.findViewById(R.id.ipsec_user).setVisibility(View.VISIBLE); 344 // fall through 345 case VpnProfile.TYPE_IPSEC_HYBRID_RSA: 346 mView.findViewById(R.id.ipsec_peer).setVisibility(View.VISIBLE); 347 break; 348 } 349 } 350 validate(boolean editing)351 private boolean validate(boolean editing) { 352 if (mAlwaysOnVpn.isChecked() && !getProfile().isValidLockdownProfile()) { 353 return false; 354 } 355 if (!editing) { 356 return mUsername.getText().length() != 0 && mPassword.getText().length() != 0; 357 } 358 if (mName.getText().length() == 0 || mServer.getText().length() == 0 || 359 !validateAddresses(mDnsServers.getText().toString(), false) || 360 !validateAddresses(mRoutes.getText().toString(), true)) { 361 return false; 362 } 363 switch (mType.getSelectedItemPosition()) { 364 case VpnProfile.TYPE_PPTP: 365 case VpnProfile.TYPE_IPSEC_HYBRID_RSA: 366 return true; 367 368 case VpnProfile.TYPE_L2TP_IPSEC_PSK: 369 case VpnProfile.TYPE_IPSEC_XAUTH_PSK: 370 return mIpsecSecret.getText().length() != 0; 371 372 case VpnProfile.TYPE_L2TP_IPSEC_RSA: 373 case VpnProfile.TYPE_IPSEC_XAUTH_RSA: 374 return mIpsecUserCert.getSelectedItemPosition() != 0; 375 } 376 return false; 377 } 378 validateAddresses(String addresses, boolean cidr)379 private boolean validateAddresses(String addresses, boolean cidr) { 380 try { 381 for (String address : addresses.split(" ")) { 382 if (address.isEmpty()) { 383 continue; 384 } 385 // Legacy VPN currently only supports IPv4. 386 int prefixLength = 32; 387 if (cidr) { 388 String[] parts = address.split("/", 2); 389 address = parts[0]; 390 prefixLength = Integer.parseInt(parts[1]); 391 } 392 byte[] bytes = InetAddress.parseNumericAddress(address).getAddress(); 393 int integer = (bytes[3] & 0xFF) | (bytes[2] & 0xFF) << 8 | 394 (bytes[1] & 0xFF) << 16 | (bytes[0] & 0xFF) << 24; 395 if (bytes.length != 4 || prefixLength < 0 || prefixLength > 32 || 396 (prefixLength < 32 && (integer << prefixLength) != 0)) { 397 return false; 398 } 399 } 400 } catch (Exception e) { 401 return false; 402 } 403 return true; 404 } 405 loadCertificates(Spinner spinner, String prefix, int firstId, String selected)406 private void loadCertificates(Spinner spinner, String prefix, int firstId, String selected) { 407 Context context = getContext(); 408 String first = (firstId == 0) ? "" : context.getString(firstId); 409 String[] certificates = mKeyStore.list(prefix); 410 411 if (certificates == null || certificates.length == 0) { 412 certificates = new String[] {first}; 413 } else { 414 String[] array = new String[certificates.length + 1]; 415 array[0] = first; 416 System.arraycopy(certificates, 0, array, 1, certificates.length); 417 certificates = array; 418 } 419 420 ArrayAdapter<String> adapter = new ArrayAdapter<String>( 421 context, android.R.layout.simple_spinner_item, certificates); 422 adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 423 spinner.setAdapter(adapter); 424 425 for (int i = 1; i < certificates.length; ++i) { 426 if (certificates[i].equals(selected)) { 427 spinner.setSelection(i); 428 break; 429 } 430 } 431 } 432 isEditing()433 boolean isEditing() { 434 return mEditing; 435 } 436 getProfile()437 VpnProfile getProfile() { 438 // First, save common fields. 439 VpnProfile profile = new VpnProfile(mProfile.key); 440 profile.name = mName.getText().toString(); 441 profile.type = mType.getSelectedItemPosition(); 442 profile.server = mServer.getText().toString().trim(); 443 profile.username = mUsername.getText().toString(); 444 profile.password = mPassword.getText().toString(); 445 profile.searchDomains = mSearchDomains.getText().toString().trim(); 446 profile.dnsServers = mDnsServers.getText().toString().trim(); 447 profile.routes = mRoutes.getText().toString().trim(); 448 449 // Then, save type-specific fields. 450 switch (profile.type) { 451 case VpnProfile.TYPE_PPTP: 452 profile.mppe = mMppe.isChecked(); 453 break; 454 455 case VpnProfile.TYPE_L2TP_IPSEC_PSK: 456 profile.l2tpSecret = mL2tpSecret.getText().toString(); 457 // fall through 458 case VpnProfile.TYPE_IPSEC_XAUTH_PSK: 459 profile.ipsecIdentifier = mIpsecIdentifier.getText().toString(); 460 profile.ipsecSecret = mIpsecSecret.getText().toString(); 461 break; 462 463 case VpnProfile.TYPE_L2TP_IPSEC_RSA: 464 profile.l2tpSecret = mL2tpSecret.getText().toString(); 465 // fall through 466 case VpnProfile.TYPE_IPSEC_XAUTH_RSA: 467 if (mIpsecUserCert.getSelectedItemPosition() != 0) { 468 profile.ipsecUserCert = (String) mIpsecUserCert.getSelectedItem(); 469 } 470 // fall through 471 case VpnProfile.TYPE_IPSEC_HYBRID_RSA: 472 if (mIpsecCaCert.getSelectedItemPosition() != 0) { 473 profile.ipsecCaCert = (String) mIpsecCaCert.getSelectedItem(); 474 } 475 if (mIpsecServerCert.getSelectedItemPosition() != 0) { 476 profile.ipsecServerCert = (String) mIpsecServerCert.getSelectedItem(); 477 } 478 break; 479 } 480 481 final boolean hasLogin = !profile.username.isEmpty() || !profile.password.isEmpty(); 482 profile.saveLogin = mSaveLogin.isChecked() || (mEditing && hasLogin); 483 return profile; 484 } 485 } 486