diff --git a/res/layout/vpn_dialog.xml b/res/layout/vpn_dialog.xml index fadd2025f14..f0e7b836c64 100644 --- a/res/layout/vpn_dialog.xml +++ b/res/layout/vpn_dialog.xml @@ -53,6 +53,8 @@ android:id="@+id/name_layout" android:hint="@string/vpn_name" app:endIconMode="clear_text" + app:helperTextEnabled="true" + app:helperText="@string/vpn_required" app:errorEnabled="true"> generic error. [CHAR LIMIT=120] --> The information entered doesn\'t support always-on VPN + + (optional) + + (required) + + The field is required Cancel diff --git a/src/com/android/settings/vpn2/ConfigDialog.java b/src/com/android/settings/vpn2/ConfigDialog.java index 1c001cb8bab..8dbcf94b023 100644 --- a/src/com/android/settings/vpn2/ConfigDialog.java +++ b/src/com/android/settings/vpn2/ConfigDialog.java @@ -40,6 +40,7 @@ import com.android.internal.net.VpnProfile; import com.android.net.module.util.ProxyUtils; import com.android.settings.R; import com.android.settings.utils.AndroidKeystoreAliasLoader; +import com.android.settings.wifi.utils.TextInputGroup; import java.util.Collection; import java.util.List; @@ -70,16 +71,17 @@ class ConfigDialog extends AlertDialog implements TextWatcher, private View mView; - private TextView mName; + private TextInputGroup mNameInput; private Spinner mType; - private TextView mServer; - private TextView mUsername; + private TextInputGroup mServerInput; + private TextInputGroup mUsernameInput; + private TextInputGroup mPasswordInput; private TextView mPassword; private Spinner mProxySettings; private TextView mProxyHost; private TextView mProxyPort; - private TextView mIpsecIdentifier; - private TextView mIpsecSecret; + private TextInputGroup mIpsecIdentifierInput; + private TextInputGroup mIpsecSecretInput; private Spinner mIpsecUserCert; private Spinner mIpsecCaCert; private Spinner mIpsecServerCert; @@ -106,16 +108,22 @@ class ConfigDialog extends AlertDialog implements TextWatcher, Context context = getContext(); // First, find out all the fields. - mName = (TextView) mView.findViewById(R.id.name); + mNameInput = new TextInputGroup(mView, R.id.name_layout, R.id.name, + R.string.vpn_field_required); mType = (Spinner) mView.findViewById(R.id.type); - mServer = (TextView) mView.findViewById(R.id.server); - mUsername = (TextView) mView.findViewById(R.id.username); - mPassword = (TextView) mView.findViewById(R.id.password); + mServerInput = new TextInputGroup(mView, R.id.server_layout, R.id.server, + R.string.vpn_field_required); + mUsernameInput = new TextInputGroup(mView, R.id.username_layout, R.id.username, + R.string.vpn_field_required); + mPasswordInput = new TextInputGroup(mView, R.id.password_layout, R.id.password, + R.string.vpn_field_required); mProxySettings = (Spinner) mView.findViewById(R.id.vpn_proxy_settings); mProxyHost = (TextView) mView.findViewById(R.id.vpn_proxy_host); mProxyPort = (TextView) mView.findViewById(R.id.vpn_proxy_port); - mIpsecIdentifier = (TextView) mView.findViewById(R.id.ipsec_identifier); - mIpsecSecret = (TextView) mView.findViewById(R.id.ipsec_secret); + mIpsecIdentifierInput = new TextInputGroup(mView, R.id.ipsec_identifier_layout, + R.id.ipsec_identifier, R.string.vpn_field_required); + mIpsecSecretInput = new TextInputGroup(mView, R.id.ipsec_secret_layout, R.id.ipsec_secret, + R.string.vpn_field_required); mIpsecUserCert = (Spinner) mView.findViewById(R.id.ipsec_user_cert); mIpsecCaCert = (Spinner) mView.findViewById(R.id.ipsec_ca_cert); mIpsecServerCert = (Spinner) mView.findViewById(R.id.ipsec_server_cert); @@ -125,21 +133,21 @@ class ConfigDialog extends AlertDialog implements TextWatcher, mAlwaysOnInvalidReason = (TextView) mView.findViewById(R.id.always_on_invalid_reason); // Second, copy values from the profile. - mName.setText(mProfile.name); + mNameInput.setText(mProfile.name); setTypesByFeature(mType); mType.setSelection(convertVpnProfileConstantToTypeIndex(mProfile.type)); - mServer.setText(mProfile.server); + mServerInput.setText(mProfile.server); if (mProfile.saveLogin) { - mUsername.setText(mProfile.username); - mPassword.setText(mProfile.password); + mUsernameInput.setText(mProfile.username); + mPasswordInput.setText(mProfile.password); } if (mProfile.proxy != null) { mProxyHost.setText(mProfile.proxy.getHost()); int port = mProfile.proxy.getPort(); mProxyPort.setText(port == 0 ? "" : Integer.toString(port)); } - mIpsecIdentifier.setText(mProfile.ipsecIdentifier); - mIpsecSecret.setText(mProfile.ipsecSecret); + mIpsecIdentifierInput.setText(mProfile.ipsecIdentifier); + mIpsecSecretInput.setText(mProfile.ipsecSecret); final AndroidKeystoreAliasLoader androidKeystoreAliasLoader = new AndroidKeystoreAliasLoader(null); loadCertificates(mIpsecUserCert, androidKeystoreAliasLoader.getKeyCertAliases(), 0, @@ -150,7 +158,8 @@ class ConfigDialog extends AlertDialog implements TextWatcher, R.string.vpn_no_server_cert, mProfile.ipsecServerCert); mSaveLogin.setChecked(mProfile.saveLogin); mAlwaysOnVpn.setChecked(mProfile.key.equals(VpnUtils.getLockdownVpn())); - mPassword.setTextAppearance(android.R.style.TextAppearance_DeviceDefault_Medium); + mPasswordInput.getEditText() + .setTextAppearance(android.R.style.TextAppearance_DeviceDefault_Medium); // Hide lockdown VPN on devices that require IMS authentication if (SystemProperties.getBoolean("persist.radio.imsregrequired", false)) { @@ -158,16 +167,16 @@ class ConfigDialog extends AlertDialog implements TextWatcher, } // Third, add listeners to required fields. - mName.addTextChangedListener(this); + mNameInput.addTextChangedListener(this); mType.setOnItemSelectedListener(this); - mServer.addTextChangedListener(this); - mUsername.addTextChangedListener(this); - mPassword.addTextChangedListener(this); + mServerInput.addTextChangedListener(this); + mUsernameInput.addTextChangedListener(this); + mPasswordInput.addTextChangedListener(this); mProxySettings.setOnItemSelectedListener(this); mProxyHost.addTextChangedListener(this); mProxyPort.addTextChangedListener(this); - mIpsecIdentifier.addTextChangedListener(this); - mIpsecSecret.addTextChangedListener(this); + mIpsecIdentifierInput.addTextChangedListener(this); + mIpsecSecretInput.addTextChangedListener(this); mIpsecUserCert.setOnItemSelectedListener(this); mShowOptions.setOnClickListener(this); mAlwaysOnVpn.setOnCheckedChangeListener(this); @@ -202,6 +211,8 @@ class ConfigDialog extends AlertDialog implements TextWatcher, setTitle(context.getString(R.string.vpn_connect_to, mProfile.name)); setUsernamePasswordVisibility(mProfile.type); + mUsernameInput.setHelperText(context.getString(R.string.vpn_required)); + mPasswordInput.setHelperText(context.getString(R.string.vpn_required)); // Create a button to connect the network. setButton(DialogInterface.BUTTON_POSITIVE, @@ -260,6 +271,10 @@ class ConfigDialog extends AlertDialog implements TextWatcher, updateProxyFieldsVisibility(position); } updateUiControls(); + mNameInput.setError(""); + mServerInput.setError(""); + mIpsecIdentifierInput.setError(""); + mIpsecSecretInput.setError(""); } @Override @@ -375,30 +390,16 @@ class ConfigDialog extends AlertDialog implements TextWatcher, return false; } - final int position = mType.getSelectedItemPosition(); - final int type = VPN_TYPES.get(position); - if (!editing && requiresUsernamePassword(type)) { - return mUsername.getText().length() != 0 && mPassword.getText().length() != 0; - } - if (mName.getText().length() == 0 || mServer.getText().length() == 0) { - return false; - } - - // All IKEv2 methods require an identifier - if (mIpsecIdentifier.getText().length() == 0) { - return false; - } - if (!validateProxy()) { return false; } - switch (type) { + switch (getVpnType()) { case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS: return true; case VpnProfile.TYPE_IKEV2_IPSEC_PSK: - return mIpsecSecret.getText().length() != 0; + return true; case VpnProfile.TYPE_IKEV2_IPSEC_RSA: return mIpsecUserCert.getSelectedItemPosition() != 0; @@ -406,6 +407,29 @@ class ConfigDialog extends AlertDialog implements TextWatcher, return false; } + public boolean validate() { + boolean isValidate = true; + int type = getVpnType(); + if (!mEditing && requiresUsernamePassword(type)) { + if (!mUsernameInput.validate()) isValidate = false; + if (!mPasswordInput.validate()) isValidate = false; + return isValidate; + } + + if (!mNameInput.validate()) isValidate = false; + if (!mServerInput.validate()) isValidate = false; + if (!mIpsecIdentifierInput.validate()) isValidate = false; + if (type == VpnProfile.TYPE_IKEV2_IPSEC_PSK && !mIpsecSecretInput.validate()) { + isValidate = false; + } + if (!isValidate) Log.w(TAG, "Failed to validate VPN profile!"); + return isValidate; + } + + private int getVpnType() { + return VPN_TYPES.get(mType.getSelectedItemPosition()); + } + private void setTypesByFeature(Spinner typeSpinner) { String[] types = getContext().getResources().getStringArray(R.array.vpn_types); if (types.length != VPN_TYPES.size()) { @@ -487,15 +511,14 @@ class ConfigDialog extends AlertDialog implements TextWatcher, VpnProfile getProfile() { // First, save common fields. VpnProfile profile = new VpnProfile(mProfile.key); - profile.name = mName.getText().toString(); - final int position = mType.getSelectedItemPosition(); - profile.type = VPN_TYPES.get(position); - profile.server = mServer.getText().toString().trim(); - profile.username = mUsername.getText().toString(); - profile.password = mPassword.getText().toString(); + profile.name = mNameInput.getText(); + profile.type = getVpnType(); + profile.server = mServerInput.getText().trim(); + profile.username = mUsernameInput.getText(); + profile.password = mPasswordInput.getText(); // Save fields based on VPN type. - profile.ipsecIdentifier = mIpsecIdentifier.getText().toString(); + profile.ipsecIdentifier = mIpsecIdentifierInput.getText(); if (hasProxy()) { String proxyHost = mProxyHost.getText().toString().trim(); @@ -517,7 +540,7 @@ class ConfigDialog extends AlertDialog implements TextWatcher, // Then, save type-specific fields. switch (profile.type) { case VpnProfile.TYPE_IKEV2_IPSEC_PSK: - profile.ipsecSecret = mIpsecSecret.getText().toString(); + profile.ipsecSecret = mIpsecSecretInput.getText(); break; case VpnProfile.TYPE_IKEV2_IPSEC_RSA: diff --git a/src/com/android/settings/vpn2/ConfigDialogFragment.java b/src/com/android/settings/vpn2/ConfigDialogFragment.java index 559003aa4c0..6bffef7c6d5 100644 --- a/src/com/android/settings/vpn2/ConfigDialogFragment.java +++ b/src/com/android/settings/vpn2/ConfigDialogFragment.java @@ -124,6 +124,7 @@ public class ConfigDialogFragment extends InstrumentedDialogFragment implements VpnProfile profile = dialog.getProfile(); if (button == DialogInterface.BUTTON_POSITIVE) { + if (!dialog.validate()) return; // Possibly throw up a dialog to explain lockdown VPN. final boolean shouldLockdown = dialog.isVpnAlwaysOn(); final boolean shouldConnect = shouldLockdown || !dialog.isEditing(); diff --git a/src/com/android/settings/wifi/WifiConfigController2.java b/src/com/android/settings/wifi/WifiConfigController2.java index 1bf1102dde1..a080fc8c5bc 100644 --- a/src/com/android/settings/wifi/WifiConfigController2.java +++ b/src/com/android/settings/wifi/WifiConfigController2.java @@ -77,7 +77,7 @@ import com.android.settings.utils.AndroidKeystoreAliasLoader; import com.android.settings.wifi.details2.WifiPrivacyPreferenceController; import com.android.settings.wifi.details2.WifiPrivacyPreferenceController2; import com.android.settings.wifi.dpp.WifiDppUtils; -import com.android.settings.wifi.utils.SsidInputGroup; +import com.android.settings.wifi.utils.TextInputGroup; import com.android.settingslib.Utils; import com.android.settingslib.utils.ThreadUtils; import com.android.wifi.flags.Flags; @@ -229,7 +229,7 @@ public class WifiConfigController2 implements TextWatcher, private final boolean mHideMeteredAndPrivacy; private final WifiManager mWifiManager; private final AndroidKeystoreAliasLoader mAndroidKeystoreAliasLoader; - private SsidInputGroup mSsidInputGroup; + private TextInputGroup mSsidInputGroup; private final Context mContext; @@ -299,7 +299,8 @@ public class WifiConfigController2 implements TextWatcher, wepWarningLayout.setVisibility(View.VISIBLE); } - mSsidInputGroup = new SsidInputGroup(mContext, mView, R.id.ssid_layout, R.id.ssid); + mSsidInputGroup = new TextInputGroup(mView, R.id.ssid_layout, R.id.ssid, + R.string.wifi_ssid_hint); mSsidScanButton = (ImageButton) mView.findViewById(R.id.ssid_scanner_button); mIpSettingsSpinner = (Spinner) mView.findViewById(R.id.ip_settings); mIpSettingsSpinner.setOnItemSelectedListener(this); diff --git a/src/com/android/settings/wifi/WifiDialog.java b/src/com/android/settings/wifi/WifiDialog.java index 40d22e60fa2..38c99b6a759 100644 --- a/src/com/android/settings/wifi/WifiDialog.java +++ b/src/com/android/settings/wifi/WifiDialog.java @@ -28,7 +28,7 @@ import android.widget.TextView; import androidx.appcompat.app.AlertDialog; import com.android.settings.R; -import com.android.settings.wifi.utils.SsidInputGroup; +import com.android.settings.wifi.utils.TextInputGroup; import com.android.settings.wifi.utils.WifiDialogHelper; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtilsInternal; @@ -120,7 +120,8 @@ public class WifiDialog extends AlertDialog implements WifiConfigUiBase, } mDialogHelper = new WifiDialogHelper(this, - new SsidInputGroup(getContext(), mView, R.id.ssid_layout, R.id.ssid)); + new TextInputGroup(mView, R.id.ssid_layout, R.id.ssid, + R.string.vpn_field_required)); } @SuppressWarnings("MissingSuperCall") // TODO: Fix me diff --git a/src/com/android/settings/wifi/utils/SsidInputGroup.kt b/src/com/android/settings/wifi/utils/SsidInputGroup.kt deleted file mode 100644 index 5d8f8d418e3..00000000000 --- a/src/com/android/settings/wifi/utils/SsidInputGroup.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2025 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.settings.wifi.utils - -import android.content.Context -import android.view.View -import com.android.settings.R - -/** TextInputGroup for Wi-Fi SSID. */ -class SsidInputGroup(private val context: Context, view: View, layoutId: Int, editTextId: Int) : - TextInputGroup(view, layoutId, editTextId) { - - fun validate(): Boolean { - if (getText().isEmpty()) { - setError(context.getString(R.string.wifi_ssid_hint)) - return false - } - return true - } -} diff --git a/src/com/android/settings/wifi/utils/TextInputGroup.kt b/src/com/android/settings/wifi/utils/TextInputGroup.kt index 8006dad3bc4..53c80ffb241 100644 --- a/src/com/android/settings/wifi/utils/TextInputGroup.kt +++ b/src/com/android/settings/wifi/utils/TextInputGroup.kt @@ -18,6 +18,7 @@ package com.android.settings.wifi.utils import android.text.Editable import android.text.TextWatcher +import android.util.Log import android.view.View import android.widget.EditText import com.google.android.material.textfield.TextInputLayout @@ -27,13 +28,17 @@ open class TextInputGroup( private val view: View, private val layoutId: Int, private val editTextId: Int, + private val errorMessageId: Int, ) { - private val View.layout: TextInputLayout? - get() = findViewById(layoutId) + val layout: TextInputLayout + get() = view.requireViewById(layoutId) - private val View.editText: EditText? - get() = findViewById(editTextId) + val editText: EditText + get() = view.requireViewById(editTextId) + + val errorMessage: String + get() = view.context.getString(errorMessageId) private val textWatcher = object : TextWatcher { @@ -42,7 +47,7 @@ open class TextInputGroup( override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} override fun afterTextChanged(s: Editable?) { - view.layout?.isErrorEnabled = false + layout.isErrorEnabled = false } } @@ -51,18 +56,37 @@ open class TextInputGroup( } fun addTextChangedListener(watcher: TextWatcher) { - view.editText?.addTextChangedListener(watcher) + editText.addTextChangedListener(watcher) } - fun getText(): String { - return view.editText?.text?.toString() ?: "" + var text: String + get() = editText.text?.toString() ?: "" + set(value) { + editText.setText(value) + } + + var helperText: String + get() = layout.helperText?.toString() ?: "" + set(value) { + layout.setHelperText(value) + } + + var error: String + get() = layout.error?.toString() ?: "" + set(value) { + layout.setError(value) + } + + open fun validate(): Boolean { + val isValid = text.isNotEmpty() + if (!isValid) { + Log.w(TAG, "validate failed in ${layout.hint ?: "unknown"}") + error = errorMessage.toString() + } + return isValid } - fun setText(text: String) { - view.editText?.setText(text) - } - - fun setError(errorMessage: String?) { - view.layout?.apply { error = errorMessage } + companion object { + const val TAG = "TextInputGroup" } } diff --git a/src/com/android/settings/wifi/utils/WifiDialogHelper.kt b/src/com/android/settings/wifi/utils/WifiDialogHelper.kt index 3b23b1a7e50..aa41b969a6e 100644 --- a/src/com/android/settings/wifi/utils/WifiDialogHelper.kt +++ b/src/com/android/settings/wifi/utils/WifiDialogHelper.kt @@ -21,7 +21,7 @@ import androidx.appcompat.app.AlertDialog class WifiDialogHelper( alertDialog: AlertDialog, - private val ssidInputGroup: SsidInputGroup? = null, + private val ssidInputGroup: TextInputGroup? = null, ) : AlertDialogHelper(alertDialog) { override fun canDismiss(): Boolean {