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.content.Context; 20 import android.content.DialogInterface; 21 import android.content.pm.PackageManager; 22 import android.net.ProxyInfo; 23 import android.os.Bundle; 24 import android.os.SystemProperties; 25 import android.text.Editable; 26 import android.text.TextWatcher; 27 import android.util.Log; 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 androidx.appcompat.app.AlertDialog; 38 39 import com.android.internal.net.VpnProfile; 40 import com.android.net.module.util.ProxyUtils; 41 import com.android.settings.R; 42 import com.android.settings.utils.AndroidKeystoreAliasLoader; 43 44 import java.util.Collection; 45 import java.util.List; 46 47 /** 48 * Dialog showing information about a VPN configuration. The dialog 49 * can be launched to either edit or prompt for credentials to connect 50 * to a user-added VPN. 51 * 52 * {@see AppDialog} 53 */ 54 class ConfigDialog extends AlertDialog implements TextWatcher, 55 View.OnClickListener, AdapterView.OnItemSelectedListener, 56 CompoundButton.OnCheckedChangeListener { 57 private static final String TAG = "ConfigDialog"; 58 // Vpn profile constants to match with R.array.vpn_types. 59 private static final List<Integer> VPN_TYPES = List.of( 60 VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS, 61 VpnProfile.TYPE_IKEV2_IPSEC_PSK, 62 VpnProfile.TYPE_IKEV2_IPSEC_RSA 63 ); 64 65 private final DialogInterface.OnClickListener mListener; 66 private final VpnProfile mProfile; 67 68 private boolean mEditing; 69 private boolean mExists; 70 71 private View mView; 72 73 private TextView mName; 74 private Spinner mType; 75 private TextView mServer; 76 private TextView mUsername; 77 private TextView mPassword; 78 private Spinner mProxySettings; 79 private TextView mProxyHost; 80 private TextView mProxyPort; 81 private TextView mIpsecIdentifier; 82 private TextView mIpsecSecret; 83 private Spinner mIpsecUserCert; 84 private Spinner mIpsecCaCert; 85 private Spinner mIpsecServerCert; 86 private CheckBox mSaveLogin; 87 private CheckBox mShowOptions; 88 private CheckBox mAlwaysOnVpn; 89 private TextView mAlwaysOnInvalidReason; 90 ConfigDialog(Context context, DialogInterface.OnClickListener listener, VpnProfile profile, boolean editing, boolean exists)91 ConfigDialog(Context context, DialogInterface.OnClickListener listener, 92 VpnProfile profile, boolean editing, boolean exists) { 93 super(context); 94 95 mListener = listener; 96 mProfile = profile; 97 mEditing = editing; 98 mExists = exists; 99 } 100 101 @Override onCreate(Bundle savedState)102 protected void onCreate(Bundle savedState) { 103 mView = getLayoutInflater().inflate(R.layout.vpn_dialog, null); 104 setView(mView); 105 106 Context context = getContext(); 107 108 // First, find out all the fields. 109 mName = (TextView) mView.findViewById(R.id.name); 110 mType = (Spinner) mView.findViewById(R.id.type); 111 mServer = (TextView) mView.findViewById(R.id.server); 112 mUsername = (TextView) mView.findViewById(R.id.username); 113 mPassword = (TextView) mView.findViewById(R.id.password); 114 mProxySettings = (Spinner) mView.findViewById(R.id.vpn_proxy_settings); 115 mProxyHost = (TextView) mView.findViewById(R.id.vpn_proxy_host); 116 mProxyPort = (TextView) mView.findViewById(R.id.vpn_proxy_port); 117 mIpsecIdentifier = (TextView) mView.findViewById(R.id.ipsec_identifier); 118 mIpsecSecret = (TextView) mView.findViewById(R.id.ipsec_secret); 119 mIpsecUserCert = (Spinner) mView.findViewById(R.id.ipsec_user_cert); 120 mIpsecCaCert = (Spinner) mView.findViewById(R.id.ipsec_ca_cert); 121 mIpsecServerCert = (Spinner) mView.findViewById(R.id.ipsec_server_cert); 122 mSaveLogin = (CheckBox) mView.findViewById(R.id.save_login); 123 mShowOptions = (CheckBox) mView.findViewById(R.id.show_options); 124 mAlwaysOnVpn = (CheckBox) mView.findViewById(R.id.always_on_vpn); 125 mAlwaysOnInvalidReason = (TextView) mView.findViewById(R.id.always_on_invalid_reason); 126 127 // Second, copy values from the profile. 128 mName.setText(mProfile.name); 129 setTypesByFeature(mType); 130 mType.setSelection(convertVpnProfileConstantToTypeIndex(mProfile.type)); 131 mServer.setText(mProfile.server); 132 if (mProfile.saveLogin) { 133 mUsername.setText(mProfile.username); 134 mPassword.setText(mProfile.password); 135 } 136 if (mProfile.proxy != null) { 137 mProxyHost.setText(mProfile.proxy.getHost()); 138 int port = mProfile.proxy.getPort(); 139 mProxyPort.setText(port == 0 ? "" : Integer.toString(port)); 140 } 141 mIpsecIdentifier.setText(mProfile.ipsecIdentifier); 142 mIpsecSecret.setText(mProfile.ipsecSecret); 143 final AndroidKeystoreAliasLoader androidKeystoreAliasLoader = 144 new AndroidKeystoreAliasLoader(null); 145 loadCertificates(mIpsecUserCert, androidKeystoreAliasLoader.getKeyCertAliases(), 0, 146 mProfile.ipsecUserCert); 147 loadCertificates(mIpsecCaCert, androidKeystoreAliasLoader.getCaCertAliases(), 148 R.string.vpn_no_ca_cert, mProfile.ipsecCaCert); 149 loadCertificates(mIpsecServerCert, androidKeystoreAliasLoader.getKeyCertAliases(), 150 R.string.vpn_no_server_cert, mProfile.ipsecServerCert); 151 mSaveLogin.setChecked(mProfile.saveLogin); 152 mAlwaysOnVpn.setChecked(mProfile.key.equals(VpnUtils.getLockdownVpn())); 153 mPassword.setTextAppearance(android.R.style.TextAppearance_DeviceDefault_Medium); 154 155 // Hide lockdown VPN on devices that require IMS authentication 156 if (SystemProperties.getBoolean("persist.radio.imsregrequired", false)) { 157 mAlwaysOnVpn.setVisibility(View.GONE); 158 } 159 160 // Third, add listeners to required fields. 161 mName.addTextChangedListener(this); 162 mType.setOnItemSelectedListener(this); 163 mServer.addTextChangedListener(this); 164 mUsername.addTextChangedListener(this); 165 mPassword.addTextChangedListener(this); 166 mProxySettings.setOnItemSelectedListener(this); 167 mProxyHost.addTextChangedListener(this); 168 mProxyPort.addTextChangedListener(this); 169 mIpsecIdentifier.addTextChangedListener(this); 170 mIpsecSecret.addTextChangedListener(this); 171 mIpsecUserCert.setOnItemSelectedListener(this); 172 mShowOptions.setOnClickListener(this); 173 mAlwaysOnVpn.setOnCheckedChangeListener(this); 174 175 // Fourth, determine whether to do editing or connecting. 176 mEditing = mEditing || !validate(true /*editing*/); 177 178 if (mEditing) { 179 setTitle(R.string.vpn_edit); 180 181 // Show common fields. 182 mView.findViewById(R.id.editor).setVisibility(View.VISIBLE); 183 184 // Show type-specific fields. 185 changeType(mProfile.type); 186 187 // Hide 'save login' when we are editing. 188 mSaveLogin.setVisibility(View.GONE); 189 190 configureAdvancedOptionsVisibility(); 191 192 if (mExists) { 193 // Create a button to forget the profile if it has already been saved.. 194 setButton(DialogInterface.BUTTON_NEUTRAL, 195 context.getString(R.string.vpn_forget), mListener); 196 } 197 198 // Create a button to save the profile. 199 setButton(DialogInterface.BUTTON_POSITIVE, 200 context.getString(R.string.vpn_save), mListener); 201 } else { 202 setTitle(context.getString(R.string.vpn_connect_to, mProfile.name)); 203 204 setUsernamePasswordVisibility(mProfile.type); 205 206 // Create a button to connect the network. 207 setButton(DialogInterface.BUTTON_POSITIVE, 208 context.getString(R.string.vpn_connect), mListener); 209 } 210 211 // Always provide a cancel button. 212 setButton(DialogInterface.BUTTON_NEGATIVE, 213 context.getString(R.string.vpn_cancel), mListener); 214 215 // Let AlertDialog create everything. 216 super.onCreate(savedState); 217 218 // Update UI controls according to the current configuration. 219 updateUiControls(); 220 221 // Workaround to resize the dialog for the input method. 222 getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | 223 WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); 224 } 225 226 @Override onRestoreInstanceState(Bundle savedState)227 public void onRestoreInstanceState(Bundle savedState) { 228 super.onRestoreInstanceState(savedState); 229 230 // Visibility isn't restored by super.onRestoreInstanceState, so re-show the advanced 231 // options here if they were already revealed or set. 232 configureAdvancedOptionsVisibility(); 233 } 234 235 @Override afterTextChanged(Editable field)236 public void afterTextChanged(Editable field) { 237 updateUiControls(); 238 } 239 240 @Override beforeTextChanged(CharSequence s, int start, int count, int after)241 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 242 } 243 244 @Override onTextChanged(CharSequence s, int start, int before, int count)245 public void onTextChanged(CharSequence s, int start, int before, int count) { 246 } 247 248 @Override onClick(View view)249 public void onClick(View view) { 250 if (view == mShowOptions) { 251 configureAdvancedOptionsVisibility(); 252 } 253 } 254 255 @Override onItemSelected(AdapterView<?> parent, View view, int position, long id)256 public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { 257 if (parent == mType) { 258 changeType(VPN_TYPES.get(position)); 259 } else if (parent == mProxySettings) { 260 updateProxyFieldsVisibility(position); 261 } 262 updateUiControls(); 263 } 264 265 @Override onNothingSelected(AdapterView<?> parent)266 public void onNothingSelected(AdapterView<?> parent) { 267 } 268 269 @Override onCheckedChanged(CompoundButton compoundButton, boolean b)270 public void onCheckedChanged(CompoundButton compoundButton, boolean b) { 271 if (compoundButton == mAlwaysOnVpn) { 272 updateUiControls(); 273 } 274 } 275 isVpnAlwaysOn()276 public boolean isVpnAlwaysOn() { 277 return mAlwaysOnVpn.isChecked(); 278 } 279 280 /** 281 * Updates the UI according to the current configuration entered by the user. 282 * 283 * These include: 284 * "Always-on VPN" checkbox 285 * Reason for "Always-on VPN" being disabled, when necessary 286 * Proxy info if manually configured 287 * "Save account information" checkbox 288 * "Save" and "Connect" buttons 289 */ updateUiControls()290 private void updateUiControls() { 291 VpnProfile profile = getProfile(); 292 293 // Always-on VPN 294 if (profile.isValidLockdownProfile()) { 295 mAlwaysOnVpn.setEnabled(true); 296 mAlwaysOnInvalidReason.setVisibility(View.GONE); 297 } else { 298 mAlwaysOnVpn.setChecked(false); 299 mAlwaysOnVpn.setEnabled(false); 300 mAlwaysOnInvalidReason.setText(R.string.vpn_always_on_invalid_reason_other); 301 mAlwaysOnInvalidReason.setVisibility(View.VISIBLE); 302 } 303 304 // Show proxy fields if any proxy field is filled. 305 if (mProfile.proxy != null && (!mProfile.proxy.getHost().isEmpty() || 306 mProfile.proxy.getPort() != 0)) { 307 mProxySettings.setSelection(VpnProfile.PROXY_MANUAL); 308 updateProxyFieldsVisibility(VpnProfile.PROXY_MANUAL); 309 } 310 311 // Save account information 312 if (mAlwaysOnVpn.isChecked()) { 313 mSaveLogin.setChecked(true); 314 mSaveLogin.setEnabled(false); 315 } else { 316 mSaveLogin.setChecked(mProfile.saveLogin); 317 mSaveLogin.setEnabled(true); 318 } 319 320 // Save or Connect button 321 getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(validate(mEditing)); 322 } 323 updateProxyFieldsVisibility(int position)324 private void updateProxyFieldsVisibility(int position) { 325 final int visible = position == VpnProfile.PROXY_MANUAL ? View.VISIBLE : View.GONE; 326 mView.findViewById(R.id.vpn_proxy_fields).setVisibility(visible); 327 } 328 isAdvancedOptionsEnabled()329 private boolean isAdvancedOptionsEnabled() { 330 return mProxyHost.getText().length() > 0 || mProxyPort.getText().length() > 0; 331 } 332 configureAdvancedOptionsVisibility()333 private void configureAdvancedOptionsVisibility() { 334 if (mShowOptions.isChecked() || isAdvancedOptionsEnabled()) { 335 mView.findViewById(R.id.options).setVisibility(View.VISIBLE); 336 mShowOptions.setVisibility(View.GONE); 337 // TODO(b/149070123): Add ability for platform VPNs to support DNS & routes 338 } else { 339 mView.findViewById(R.id.options).setVisibility(View.GONE); 340 mShowOptions.setVisibility(View.VISIBLE); 341 } 342 } 343 changeType(int type)344 private void changeType(int type) { 345 // First, hide everything. 346 mView.findViewById(R.id.ipsec_psk).setVisibility(View.GONE); 347 mView.findViewById(R.id.ipsec_user).setVisibility(View.GONE); 348 mView.findViewById(R.id.ipsec_peer).setVisibility(View.GONE); 349 mView.findViewById(R.id.options_ipsec_identity).setVisibility(View.GONE); 350 351 setUsernamePasswordVisibility(type); 352 353 // Always enable identity for IKEv2/IPsec profiles. 354 mView.findViewById(R.id.options_ipsec_identity).setVisibility(View.VISIBLE); 355 356 // Then, unhide type-specific fields. 357 switch (type) { 358 case VpnProfile.TYPE_IKEV2_IPSEC_PSK: 359 mView.findViewById(R.id.ipsec_psk).setVisibility(View.VISIBLE); 360 mView.findViewById(R.id.options_ipsec_identity).setVisibility(View.VISIBLE); 361 break; 362 case VpnProfile.TYPE_IKEV2_IPSEC_RSA: 363 mView.findViewById(R.id.ipsec_user).setVisibility(View.VISIBLE); 364 // fall through 365 case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS: 366 mView.findViewById(R.id.ipsec_peer).setVisibility(View.VISIBLE); 367 break; 368 } 369 370 configureAdvancedOptionsVisibility(); 371 } 372 validate(boolean editing)373 private boolean validate(boolean editing) { 374 if (mAlwaysOnVpn.isChecked() && !getProfile().isValidLockdownProfile()) { 375 return false; 376 } 377 378 final int position = mType.getSelectedItemPosition(); 379 final int type = VPN_TYPES.get(position); 380 if (!editing && requiresUsernamePassword(type)) { 381 return mUsername.getText().length() != 0 && mPassword.getText().length() != 0; 382 } 383 if (mName.getText().length() == 0 || mServer.getText().length() == 0) { 384 return false; 385 } 386 387 // All IKEv2 methods require an identifier 388 if (mIpsecIdentifier.getText().length() == 0) { 389 return false; 390 } 391 392 if (!validateProxy()) { 393 return false; 394 } 395 396 switch (type) { 397 case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS: 398 return true; 399 400 case VpnProfile.TYPE_IKEV2_IPSEC_PSK: 401 return mIpsecSecret.getText().length() != 0; 402 403 case VpnProfile.TYPE_IKEV2_IPSEC_RSA: 404 return mIpsecUserCert.getSelectedItemPosition() != 0; 405 } 406 return false; 407 } 408 setTypesByFeature(Spinner typeSpinner)409 private void setTypesByFeature(Spinner typeSpinner) { 410 String[] types = getContext().getResources().getStringArray(R.array.vpn_types); 411 if (types.length != VPN_TYPES.size()) { 412 Log.wtf(TAG, "VPN_TYPES array length does not match string array"); 413 } 414 // Although FEATURE_IPSEC_TUNNELS should always be present in android S and beyond, 415 // keep this check here just to be safe. 416 if (!getContext().getPackageManager().hasSystemFeature( 417 PackageManager.FEATURE_IPSEC_TUNNELS)) { 418 Log.wtf(TAG, "FEATURE_IPSEC_TUNNELS missing from system"); 419 } 420 // If the vpn is new or is not already a legacy type, 421 // don't allow the user to set the type to a legacy option. 422 423 // Set the mProfile.type to TYPE_IKEV2_IPSEC_USER_PASS if the VPN not exist 424 if (!mExists) { 425 mProfile.type = VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS; 426 } 427 428 final ArrayAdapter<String> adapter = new ArrayAdapter<String>( 429 getContext(), android.R.layout.simple_spinner_item, types); 430 adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 431 typeSpinner.setAdapter(adapter); 432 } 433 loadCertificates(Spinner spinner, Collection<String> choices, int firstId, String selected)434 private void loadCertificates(Spinner spinner, Collection<String> choices, int firstId, 435 String selected) { 436 Context context = getContext(); 437 String first = (firstId == 0) ? "" : context.getString(firstId); 438 String[] myChoices; 439 440 if (choices == null || choices.size() == 0) { 441 myChoices = new String[] {first}; 442 } else { 443 myChoices = new String[choices.size() + 1]; 444 myChoices[0] = first; 445 int i = 1; 446 for (String c : choices) { 447 myChoices[i++] = c; 448 } 449 } 450 451 ArrayAdapter<String> adapter = new ArrayAdapter<String>( 452 context, android.R.layout.simple_spinner_item, myChoices); 453 adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 454 spinner.setAdapter(adapter); 455 456 for (int i = 1; i < myChoices.length; ++i) { 457 if (myChoices[i].equals(selected)) { 458 spinner.setSelection(i); 459 break; 460 } 461 } 462 } 463 setUsernamePasswordVisibility(int type)464 private void setUsernamePasswordVisibility(int type) { 465 mView.findViewById(R.id.userpass).setVisibility( 466 requiresUsernamePassword(type) ? View.VISIBLE : View.GONE); 467 } 468 requiresUsernamePassword(int type)469 private boolean requiresUsernamePassword(int type) { 470 switch (type) { 471 case VpnProfile.TYPE_IKEV2_IPSEC_RSA: // fall through 472 case VpnProfile.TYPE_IKEV2_IPSEC_PSK: 473 return false; 474 default: 475 return true; 476 } 477 } 478 isEditing()479 boolean isEditing() { 480 return mEditing; 481 } 482 hasProxy()483 boolean hasProxy() { 484 return mProxySettings.getSelectedItemPosition() == VpnProfile.PROXY_MANUAL; 485 } 486 getProfile()487 VpnProfile getProfile() { 488 // First, save common fields. 489 VpnProfile profile = new VpnProfile(mProfile.key); 490 profile.name = mName.getText().toString(); 491 final int position = mType.getSelectedItemPosition(); 492 profile.type = VPN_TYPES.get(position); 493 profile.server = mServer.getText().toString().trim(); 494 profile.username = mUsername.getText().toString(); 495 profile.password = mPassword.getText().toString(); 496 497 // Save fields based on VPN type. 498 profile.ipsecIdentifier = mIpsecIdentifier.getText().toString(); 499 500 if (hasProxy()) { 501 String proxyHost = mProxyHost.getText().toString().trim(); 502 String proxyPort = mProxyPort.getText().toString().trim(); 503 // 0 is a last resort default, but the interface validates that the proxy port is 504 // present and non-zero. 505 int port = 0; 506 if (!proxyPort.isEmpty()) { 507 try { 508 port = Integer.parseInt(proxyPort); 509 } catch (NumberFormatException e) { 510 Log.e(TAG, "Could not parse proxy port integer ", e); 511 } 512 } 513 profile.proxy = ProxyInfo.buildDirectProxy(proxyHost, port); 514 } else { 515 profile.proxy = null; 516 } 517 // Then, save type-specific fields. 518 switch (profile.type) { 519 case VpnProfile.TYPE_IKEV2_IPSEC_PSK: 520 profile.ipsecSecret = mIpsecSecret.getText().toString(); 521 break; 522 523 case VpnProfile.TYPE_IKEV2_IPSEC_RSA: 524 if (mIpsecUserCert.getSelectedItemPosition() != 0) { 525 profile.ipsecUserCert = (String) mIpsecUserCert.getSelectedItem(); 526 profile.ipsecSecret = profile.ipsecUserCert; 527 } 528 // fall through 529 case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS: 530 if (mIpsecCaCert.getSelectedItemPosition() != 0) { 531 profile.ipsecCaCert = (String) mIpsecCaCert.getSelectedItem(); 532 } 533 if (mIpsecServerCert.getSelectedItemPosition() != 0) { 534 profile.ipsecServerCert = (String) mIpsecServerCert.getSelectedItem(); 535 } 536 break; 537 } 538 539 final boolean hasLogin = !profile.username.isEmpty() || !profile.password.isEmpty(); 540 profile.saveLogin = mSaveLogin.isChecked() || (mEditing && hasLogin); 541 return profile; 542 } 543 validateProxy()544 private boolean validateProxy() { 545 if (!hasProxy()) { 546 return true; 547 } 548 549 final String host = mProxyHost.getText().toString().trim(); 550 final String port = mProxyPort.getText().toString().trim(); 551 return ProxyUtils.validate(host, port, "") == ProxyUtils.PROXY_VALID; 552 } 553 convertVpnProfileConstantToTypeIndex(int vpnType)554 private int convertVpnProfileConstantToTypeIndex(int vpnType) { 555 final int typeIndex = VPN_TYPES.indexOf(vpnType); 556 if (typeIndex == -1) { 557 // Existing legacy profile type 558 Log.wtf(TAG, "Invalid existing profile type"); 559 return 0; 560 } 561 return typeIndex; 562 } 563 } 564