Merge "Fixed accessibility issues in Wi-Fi password view for SUW" into main

This commit is contained in:
Treehugger Robot
2025-03-13 06:40:20 -07:00
committed by Android (Google) Code Review
9 changed files with 146 additions and 103 deletions

View File

@@ -62,6 +62,8 @@
android:layout_weight="1" android:layout_weight="1"
android:hint="@string/wifi_ssid" android:hint="@string/wifi_ssid"
android:textDirection="locale" android:textDirection="locale"
app:helperTextEnabled="true"
app:helperText="@string/wifi_field_required"
app:endIconMode="clear_text"> app:endIconMode="clear_text">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/ssid" android:id="@+id/ssid"
@@ -345,6 +347,8 @@
android:id="@+id/password_input_layout" android:id="@+id/password_input_layout"
style="@style/Widget.Network.TextInputLayout.WifiConfig" style="@style/Widget.Network.TextInputLayout.WifiConfig"
android:hint="@string/wifi_password" android:hint="@string/wifi_password"
app:helperTextEnabled="true"
app:helperText="@string/wifi_field_required"
app:endIconMode="password_toggle"> app:endIconMode="password_toggle">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/password" android:id="@+id/password"

View File

@@ -2395,7 +2395,7 @@
<!-- Extended message for talkback to say when Advanced Options is collapsed. (e.g., Double-tap to expand) [CHAR LIMIT=NONE] --> <!-- Extended message for talkback to say when Advanced Options is collapsed. (e.g., Double-tap to expand) [CHAR LIMIT=NONE] -->
<string name="wifi_advanced_toggle_description_collapsed">expand</string> <string name="wifi_advanced_toggle_description_collapsed">expand</string>
<!-- Label for the SSID of the network --> <!-- Label for the SSID of the network -->
<string name="wifi_ssid">Network name</string> <string name="wifi_ssid">Network name*</string>
<!-- Hint for a text field to enter the SSID of a hidden wifi network. [CHAR LIMIT=35] --> <!-- Hint for a text field to enter the SSID of a hidden wifi network. [CHAR LIMIT=35] -->
<string name="wifi_ssid_hint">Enter the SSID</string> <string name="wifi_ssid_hint">Enter the SSID</string>
<!-- Label for the security of the connection --> <!-- Label for the security of the connection -->
@@ -2441,7 +2441,9 @@
<!-- Label for the EAP anonymous identity of the network --> <!-- Label for the EAP anonymous identity of the network -->
<string name="wifi_eap_anonymous">Anonymous identity</string> <string name="wifi_eap_anonymous">Anonymous identity</string>
<!-- Label for the password of the secured network --> <!-- Label for the password of the secured network -->
<string name="wifi_password">Password</string> <string name="wifi_password">Password*</string>
<!-- Label for the password of the secured network -->
<string name="wifi_password_invalid">The password is invalid</string>
<!-- Label for the check box to show password --> <!-- Label for the check box to show password -->
<string name="wifi_show_password">Show password</string> <string name="wifi_show_password">Show password</string>
<!-- Label for the radio button to choose wifi ap 2.4 GHz band --> <!-- Label for the radio button to choose wifi ap 2.4 GHz band -->
@@ -2574,6 +2576,8 @@
<string name="wifi_scan_always_turn_on_message_unknown">To improve location accuracy and for other purposes, an unknown app wants to turn on network scanning, even when Wi\u2011Fi is off.\n\nAllow this for all apps that want to scan?</string> <string name="wifi_scan_always_turn_on_message_unknown">To improve location accuracy and for other purposes, an unknown app wants to turn on network scanning, even when Wi\u2011Fi is off.\n\nAllow this for all apps that want to scan?</string>
<string name="wifi_scan_always_confirm_allow">Allow</string> <string name="wifi_scan_always_confirm_allow">Allow</string>
<string name="wifi_scan_always_confirm_deny">Deny</string> <string name="wifi_scan_always_confirm_deny">Deny</string>
<!-- Error message displayed below the Wi-Fi EditText when the filed is required. [CHAR LIMIT=NONE] -->
<string name="wifi_field_required">*required</string>
<!-- Dialog text to tell the user that the selected network does not have Internet access. --> <!-- Dialog text to tell the user that the selected network does not have Internet access. -->
<string name="no_internet_access_text">This network has no internet access. Stay connected?</string> <string name="no_internet_access_text">This network has no internet access. Stay connected?</string>

View File

@@ -74,6 +74,9 @@ import com.android.settings.network.SubscriptionUtil;
import com.android.settings.utils.AndroidKeystoreAliasLoader; import com.android.settings.utils.AndroidKeystoreAliasLoader;
import com.android.settings.wifi.details2.WifiPrivacyPreferenceController; import com.android.settings.wifi.details2.WifiPrivacyPreferenceController;
import com.android.settings.wifi.dpp.WifiDppUtils; import com.android.settings.wifi.dpp.WifiDppUtils;
import com.android.settings.wifi.utils.TextInputGroup;
import com.android.settings.wifi.utils.TextInputValidator;
import com.android.settings.wifi.utils.WifiPasswordInput;
import com.android.settingslib.Utils; import com.android.settingslib.Utils;
import com.android.settingslib.utils.ThreadUtils; import com.android.settingslib.utils.ThreadUtils;
import com.android.settingslib.wifi.AccessPoint; import com.android.settingslib.wifi.AccessPoint;
@@ -215,7 +218,10 @@ public class WifiConfigController implements TextWatcher,
private String[] mLevels; private String[] mLevels;
private int mMode; private int mMode;
private TextView mSsidView;
private TextInputValidator mValidator = new TextInputValidator();
private TextInputGroup mSsidInput;
private WifiPasswordInput mPasswordInput;
private Context mContext; private Context mContext;
@@ -288,6 +294,12 @@ public class WifiConfigController implements TextWatcher,
wepWarningLayout.setVisibility(View.VISIBLE); wepWarningLayout.setVisibility(View.VISIBLE);
} }
mSsidInput = new TextInputGroup(mView, R.id.ssid_layout, R.id.ssid,
R.string.wifi_ssid_hint);
mPasswordInput = new WifiPasswordInput(mView);
mValidator.addTextInput(mSsidInput);
mValidator.addTextInput(mPasswordInput);
mSsidScanButton = (ImageButton) mView.findViewById(R.id.ssid_scanner_button); mSsidScanButton = (ImageButton) mView.findViewById(R.id.ssid_scanner_button);
mIpSettingsSpinner = (Spinner) mView.findViewById(R.id.ip_settings); mIpSettingsSpinner = (Spinner) mView.findViewById(R.id.ip_settings);
mIpSettingsSpinner.setOnItemSelectedListener(this); mIpSettingsSpinner.setOnItemSelectedListener(this);
@@ -517,45 +529,8 @@ public class WifiConfigController implements TextWatcher,
submit.setEnabled(isSubmittable()); submit.setEnabled(isSubmittable());
} }
boolean isValidPsk(String password) {
if (password.length() == 64 && password.matches("[0-9A-Fa-f]{64}")) {
return true;
} else if (password.length() >= 8 && password.length() <= 63) {
return true;
}
return false;
}
boolean isValidSaePassword(String password) {
if (password.length() >= 1 && password.length() <= 63) {
return true;
}
return false;
}
boolean isSubmittable() { boolean isSubmittable() {
boolean enabled = false; boolean enabled = ipAndProxyFieldsAreValid();
boolean passwordInvalid = false;
if (mPasswordView != null
&& ((mAccessPointSecurity == AccessPoint.SECURITY_WEP
&& mPasswordView.length() == 0)
|| (mAccessPointSecurity == AccessPoint.SECURITY_PSK
&& !isValidPsk(mPasswordView.getText().toString()))
|| (mAccessPointSecurity == AccessPoint.SECURITY_SAE
&& !isValidSaePassword(mPasswordView.getText().toString())))) {
passwordInvalid = true;
}
if ((mAccessPoint == null || !mAccessPoint.isSaved()) && passwordInvalid) {
// If Accesspoint is not saved, apply passwordInvalid check
enabled = false;
} else if (mAccessPoint != null && mAccessPoint.isSaved() && passwordInvalid
&& mPasswordView.length() > 0) {
// If AccessPoint is saved (modifying network) and password is changed, apply
// Invalid password check
enabled = false;
} else {
enabled = ipAndProxyFieldsAreValid();
}
if ((mAccessPointSecurity == AccessPoint.SECURITY_EAP if ((mAccessPointSecurity == AccessPoint.SECURITY_EAP
|| mAccessPointSecurity == AccessPoint.SECURITY_EAP_WPA3_ENTERPRISE || mAccessPointSecurity == AccessPoint.SECURITY_EAP_WPA3_ENTERPRISE
|| mAccessPointSecurity == AccessPoint.SECURITY_EAP_SUITE_B) || mAccessPointSecurity == AccessPoint.SECURITY_EAP_SUITE_B)
@@ -592,12 +567,9 @@ public class WifiConfigController implements TextWatcher,
mView.findViewById(R.id.no_domain_warning).setVisibility(View.GONE); mView.findViewById(R.id.no_domain_warning).setVisibility(View.GONE);
mView.findViewById(R.id.ssid_too_long_warning).setVisibility(View.GONE); mView.findViewById(R.id.ssid_too_long_warning).setVisibility(View.GONE);
if (mSsidView != null) { if (WifiUtils.isSSIDTooLong(mSsidInput.getText())) {
final String ssid = mSsidView.getText().toString();
if (WifiUtils.isSSIDTooLong(ssid)) {
mView.findViewById(R.id.ssid_too_long_warning).setVisibility(View.VISIBLE); mView.findViewById(R.id.ssid_too_long_warning).setVisibility(View.VISIBLE);
} }
}
if (mEapCaCertSpinner != null if (mEapCaCertSpinner != null
&& mView.findViewById(R.id.l_ca_cert).getVisibility() != View.GONE) { && mView.findViewById(R.id.l_ca_cert).getVisibility() != View.GONE) {
if (mEapDomainView != null if (mEapDomainView != null
@@ -626,8 +598,7 @@ public class WifiConfigController implements TextWatcher,
WifiConfiguration config = new WifiConfiguration(); WifiConfiguration config = new WifiConfiguration();
if (mAccessPoint == null) { if (mAccessPoint == null) {
config.SSID = AccessPoint.convertToQuotedString( config.SSID = AccessPoint.convertToQuotedString(mSsidInput.getText());
mSsidView.getText().toString());
// If the user adds a network manually, assume that it is hidden. // If the user adds a network manually, assume that it is hidden.
config.hiddenSSID = mHiddenSettingsSpinner.getSelectedItemPosition() == HIDDEN_NETWORK; config.hiddenSSID = mHiddenSettingsSpinner.getSelectedItemPosition() == HIDDEN_NETWORK;
} else if (!mAccessPoint.isSaved()) { } else if (!mAccessPoint.isSaved()) {
@@ -1677,6 +1648,7 @@ public class WifiConfigController implements TextWatcher,
// Convert menu position to actual Wi-Fi security type // Convert menu position to actual Wi-Fi security type
mAccessPointSecurity = mSecurityInPosition[position]; mAccessPointSecurity = mSecurityInPosition[position];
showSecurityFields(/* refreshEapMethods */ true, /* refreshCertificates */ true); showSecurityFields(/* refreshEapMethods */ true, /* refreshCertificates */ true);
mPasswordInput.setSecurity(mAccessPointSecurity);
if (WifiDppUtils.isSupportEnrolleeQrCodeScanner(mContext, mAccessPointSecurity)) { if (WifiDppUtils.isSupportEnrolleeQrCodeScanner(mContext, mAccessPointSecurity)) {
mSsidScanButton.setVisibility(View.VISIBLE); mSsidScanButton.setVisibility(View.VISIBLE);
@@ -1725,8 +1697,7 @@ public class WifiConfigController implements TextWatcher,
private void configureSecuritySpinner() { private void configureSecuritySpinner() {
mConfigUi.setTitle(R.string.wifi_add_network); mConfigUi.setTitle(R.string.wifi_add_network);
mSsidView = (TextView) mView.findViewById(R.id.ssid); mSsidInput.addTextChangedListener(this);
mSsidView.addTextChangedListener(this);
mSecuritySpinner = ((Spinner) mView.findViewById(R.id.security)); mSecuritySpinner = ((Spinner) mView.findViewById(R.id.security));
mSecuritySpinner.setOnItemSelectedListener(this); mSecuritySpinner.setOnItemSelectedListener(this);
@@ -1894,4 +1865,11 @@ public class WifiConfigController implements TextWatcher,
} }
}); });
} }
/**
* Provides a validator to verify that the Wi-Fi configuration is ready.
*/
public TextInputValidator getValidator() {
return mValidator;
}
} }

View File

@@ -28,7 +28,6 @@ import android.widget.TextView;
import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog;
import com.android.settings.R; import com.android.settings.R;
import com.android.settings.wifi.utils.TextInputGroup;
import com.android.settings.wifi.utils.WifiDialogHelper; import com.android.settings.wifi.utils.WifiDialogHelper;
import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.RestrictedLockUtilsInternal; import com.android.settingslib.RestrictedLockUtilsInternal;
@@ -119,9 +118,7 @@ public class WifiDialog extends AlertDialog implements WifiConfigUiBase,
mController.hideForgetButton(); mController.hideForgetButton();
} }
mDialogHelper = new WifiDialogHelper(this, mDialogHelper = new WifiDialogHelper(this, mController.getValidator());
new TextInputGroup(mView, R.id.ssid_layout, R.id.ssid,
R.string.vpn_field_required));
} }
@SuppressWarnings("MissingSuperCall") // TODO: Fix me @SuppressWarnings("MissingSuperCall") // TODO: Fix me

View File

@@ -25,7 +25,7 @@ import com.google.android.material.textfield.TextInputLayout
/** A widget that wraps the relationship work between a TextInputLayout and an EditText. */ /** A widget that wraps the relationship work between a TextInputLayout and an EditText. */
open class TextInputGroup( open class TextInputGroup(
private val view: View, val view: View,
private val layoutId: Int, private val layoutId: Int,
private val editTextId: Int, private val editTextId: Int,
private val errorMessageId: Int, private val errorMessageId: Int,
@@ -88,7 +88,7 @@ open class TextInputGroup(
val isValid = text.isNotEmpty() val isValid = text.isNotEmpty()
if (!isValid) { if (!isValid) {
Log.w(TAG, "validate failed in ${layout.hint ?: "unknown"}") Log.w(TAG, "validate failed in ${layout.hint ?: "unknown"}")
error = errorMessage.toString() error = errorMessage
} }
return isValid return isValid
} }

View File

@@ -0,0 +1,35 @@
/*
* 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
/**
* The validator used to validate all {@code TextInputGroup} at the same time
*/
class TextInputValidator {
private val textInputList: MutableList<TextInputGroup> = ArrayList()
fun addTextInput(textInputGroup: TextInputGroup) {
textInputList += textInputGroup
}
fun validate(): Boolean {
var isValidate = true
for (input in textInputList) if (!input.validate()) isValidate = false
return isValidate
}
}

View File

@@ -21,14 +21,10 @@ import androidx.appcompat.app.AlertDialog
class WifiDialogHelper( class WifiDialogHelper(
alertDialog: AlertDialog, alertDialog: AlertDialog,
private val ssidInputGroup: TextInputGroup? = null, private val validator: TextInputValidator,
) : AlertDialogHelper(alertDialog) { ) : AlertDialogHelper(alertDialog) {
override fun canDismiss(): Boolean { override fun canDismiss(): Boolean = validator.validate()
val isValid = ssidInputGroup?.validate() ?: true
if (!isValid) Log.w(TAG, "SSID is invalid!")
return isValid
}
companion object { companion object {
const val TAG = "WifiDialogHelper" const val TAG = "WifiDialogHelper"

View File

@@ -0,0 +1,70 @@
/*
* 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.util.Log
import android.view.View
import com.android.settings.R
import com.android.wifitrackerlib.WifiEntry.SECURITY_NONE
import com.android.wifitrackerlib.WifiEntry.SECURITY_PSK
import com.android.wifitrackerlib.WifiEntry.SECURITY_SAE
import com.android.wifitrackerlib.WifiEntry.SECURITY_WEP
/**
* The Wi-Fi password {@code TextInputGroup} that supports input validation.
*/
class WifiPasswordInput(view: View) :
TextInputGroup(view, R.id.password_input_layout, R.id.password, R.string.wifi_field_required) {
var security: Int = SECURITY_NONE
override fun validate(): Boolean {
if (!editText.isShown) return true
return when (security) {
SECURITY_WEP -> super.validate()
SECURITY_PSK -> super.validate() && isValidPsk(text).also { valid ->
if (!valid) {
error = view.context.getString(R.string.wifi_password_invalid)
Log.w(TAG, "validate failed in ${layout.hint ?: "unknown"} for PSK")
}
}
SECURITY_SAE -> super.validate() && isValidSae(text).also { valid ->
if (!valid) {
error = view.context.getString(R.string.wifi_password_invalid)
Log.w(TAG, "validate failed in ${layout.hint ?: "unknown"} for SAE")
}
}
else -> true
}
}
companion object {
const val TAG = "WifiPasswordInput"
fun isValidPsk(password: String): Boolean {
return (password.length == 64 && password.matches("[0-9A-Fa-f]{64}".toRegex())) ||
(password.length in 8..63)
}
fun isValidSae(password: String): Boolean {
return password.length in 1..63
}
}
}

View File

@@ -145,47 +145,6 @@ public class WifiConfigControllerTest {
.isEqualTo(View.GONE); .isEqualTo(View.GONE);
} }
@Test
public void isSubmittable_longPsk_shouldReturnFalse() {
final TextView password = mView.findViewById(R.id.password);
assertThat(password).isNotNull();
password.setText(LONG_PSK);
assertThat(mController.isSubmittable()).isFalse();
}
@Test
public void isSubmittable_shortPsk_shouldReturnFalse() {
final TextView password = mView.findViewById(R.id.password);
assertThat(password).isNotNull();
password.setText(SHORT_PSK);
assertThat(mController.isSubmittable()).isFalse();
}
@Test
public void isSubmittable_goodPsk_shouldReturnTrue() {
final TextView password = mView.findViewById(R.id.password);
assertThat(password).isNotNull();
password.setText(GOOD_PSK);
assertThat(mController.isSubmittable()).isTrue();
}
@Test
public void isSubmittable_hexPsk_shouldReturnTrue() {
final TextView password = mView.findViewById(R.id.password);
assertThat(password).isNotNull();
password.setText(HEX_PSK);
assertThat(mController.isSubmittable()).isTrue();
}
@Test
public void isSubmittable_savedConfigZeroLengthPassword_shouldReturnTrue() {
final TextView password = mView.findViewById(R.id.password);
assertThat(password).isNotNull();
password.setText("");
when(mAccessPoint.isSaved()).thenReturn(true);
assertThat(mController.isSubmittable()).isTrue();
}
@Test @Test
public void isSubmittable_nullAccessPoint_noException() { public void isSubmittable_nullAccessPoint_noException() {
mController = mController =