Merge "Update string name to "desktop windowing" from "Desktop view" for developer options." into main
diff --git a/res/layout/vpn_dialog.xml b/res/layout/vpn_dialog.xml
index fadd202..f0e7b83 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">
                     <com.google.android.material.textfield.TextInputEditText
                         style="@style/vpn_value"
@@ -73,6 +75,8 @@
                     android:id="@+id/server_layout"
                     android:hint="@string/vpn_server"
                     app:endIconMode="clear_text"
+                    app:helperTextEnabled="true"
+                    app:helperText="@string/vpn_required"
                     app:errorEnabled="true">
                     <com.google.android.material.textfield.TextInputEditText
                         style="@style/vpn_value"
@@ -90,7 +94,7 @@
                         android:hint="@string/vpn_ipsec_identifier"
                         app:endIconMode="clear_text"
                         app:helperTextEnabled="true"
-                        app:helperText="@string/vpn_not_used"
+                        app:helperText="@string/vpn_required"
                         app:errorEnabled="true">
                         <com.google.android.material.textfield.TextInputEditText
                             style="@style/vpn_value"
@@ -108,6 +112,8 @@
                         android:id="@+id/ipsec_secret_layout"
                         android:hint="@string/vpn_ipsec_secret"
                         app:endIconMode="password_toggle"
+                        app:helperTextEnabled="true"
+                        app:helperText="@string/vpn_required"
                         app:errorEnabled="true">
                         <com.google.android.material.textfield.TextInputEditText
                             style="@style/vpn_value"
@@ -183,7 +189,7 @@
                         android:hint="@string/proxy_hostname_label"
                         app:endIconMode="clear_text"
                         app:helperTextEnabled="true"
-                        app:helperText="@string/proxy_hostname_hint"
+                        app:helperText="@string/vpn_optional"
                         app:errorEnabled="true">
                         <com.google.android.material.textfield.TextInputEditText
                             style="@style/vpn_value"
@@ -197,7 +203,7 @@
                         android:hint="@string/proxy_port_label"
                         app:endIconMode="clear_text"
                         app:helperTextEnabled="true"
-                        app:helperText="@string/proxy_port_hint"
+                        app:helperText="@string/vpn_optional"
                         app:errorEnabled="true">
                         <com.google.android.material.textfield.TextInputEditText
                             style="@style/vpn_value"
@@ -217,6 +223,8 @@
                     android:id="@+id/username_layout"
                     android:hint="@string/vpn_username"
                     app:endIconMode="clear_text"
+                    app:helperTextEnabled="true"
+                    app:helperText="@string/vpn_optional"
                     app:errorEnabled="true">
                     <com.google.android.material.textfield.TextInputEditText
                         style="@style/vpn_value"
@@ -228,6 +236,8 @@
                     android:id="@+id/password_layout"
                     android:hint="@string/vpn_password"
                     app:endIconMode="password_toggle"
+                    app:helperTextEnabled="true"
+                    app:helperText="@string/vpn_optional"
                     app:errorEnabled="true">
                     <com.google.android.material.textfield.TextInputEditText
                         style="@style/vpn_value"
diff --git a/res/values/strings.xml b/res/values/strings.xml
index c7bdcee..e551749 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -7290,6 +7290,12 @@
         generic error. [CHAR LIMIT=120] -->
     <string name="vpn_always_on_invalid_reason_other">The information entered doesn\'t support
         always-on VPN</string>
+    <!-- Hint for an optional field in a VPN profile. [CHAR LIMIT=40] -->
+    <string name="vpn_optional">(optional)</string>
+    <!-- Hint for a required field in a VPN profile. [CHAR LIMIT=40] -->
+    <string name="vpn_required">(required)</string>
+    <!-- Error message displayed below the VPN EditText when the filed is required. [CHAR LIMIT=NONE] -->
+    <string name="vpn_field_required">The field is required</string>
 
     <!-- Button label to cancel changing a VPN profile. [CHAR LIMIT=40] -->
     <string name="vpn_cancel">Cancel</string>
@@ -14297,4 +14303,6 @@
     <string name="supervision_add_forgot_pin_preference_title">Forgot PIN</string>
     <!-- Title for web content filters entry [CHAR LIMIT=60] -->
     <string name="supervision_web_content_filters_title">Web content filters</string>
+    <!-- Generic content description that is attached to the preview illustration at the top of an Accessibility feature toggle page. [CHAR LIMIT=NONE] -->
+    <string name="accessibility_illustration_content_description"><xliff:g id="feature" example="Select to Speak">%1$s</xliff:g> animation</string>
 </resources>
diff --git a/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java b/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java
index 66c32df..d8c3985 100644
--- a/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java
+++ b/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragment.java
@@ -68,6 +68,7 @@
 import com.android.settings.flags.Flags;
 import com.android.settings.widget.SettingsMainSwitchBar;
 import com.android.settings.widget.SettingsMainSwitchPreference;
+import com.android.settingslib.utils.ThreadUtils;
 import com.android.settingslib.widget.IllustrationPreference;
 import com.android.settingslib.widget.TopIntroPreference;
 
@@ -311,6 +312,11 @@
         return getString(R.string.accessibility_shortcut_title, mFeatureName);
     }
 
+    @VisibleForTesting
+    CharSequence getContentDescriptionForAnimatedIllustration() {
+        return getString(R.string.accessibility_illustration_content_description, mFeatureName);
+    }
+
     protected void onPreferenceToggled(String preferenceKey, boolean enabled) {
     }
 
@@ -427,22 +433,38 @@
 
         return drawable;
     }
-
     private void initAnimatedImagePreference() {
-        if (mImageUri == null) {
+        initAnimatedImagePreference(mImageUri, new IllustrationPreference(getPrefContext()));
+    }
+
+    @VisibleForTesting
+    void initAnimatedImagePreference(
+            @Nullable Uri imageUri,
+            @NonNull IllustrationPreference preference) {
+        if (imageUri == null) {
             return;
         }
 
         final int displayHalfHeight =
                 AccessibilityUtil.getDisplayBounds(getPrefContext()).height() / 2;
-        final IllustrationPreference illustrationPreference =
-                new IllustrationPreference(getPrefContext());
-        illustrationPreference.setImageUri(mImageUri);
-        illustrationPreference.setSelectable(false);
-        illustrationPreference.setMaxHeight(displayHalfHeight);
-        illustrationPreference.setKey(KEY_ANIMATED_IMAGE);
-
-        getPreferenceScreen().addPreference(illustrationPreference);
+        preference.setImageUri(imageUri);
+        preference.setSelectable(false);
+        preference.setMaxHeight(displayHalfHeight);
+        preference.setKey(KEY_ANIMATED_IMAGE);
+        preference.setOnBindListener(view -> {
+            // isAnimatable is decided in
+            // {@link IllustrationPreference#onBindViewHolder(PreferenceViewHolder)}. Therefore, we
+            // wait until the view is bond to set the content description for it.
+            // The content description is added for an animation illustration only. Since the static
+            // images are decorative.
+            ThreadUtils.getUiThreadHandler().post(() -> {
+                if (preference.isAnimatable()) {
+                    preference.setContentDescription(
+                            getContentDescriptionForAnimatedIllustration());
+                }
+            });
+        });
+        getPreferenceScreen().addPreference(preference);
     }
 
     @VisibleForTesting
diff --git a/src/com/android/settings/vpn2/ConfigDialog.java b/src/com/android/settings/vpn2/ConfigDialog.java
index 1c001cb..8dbcf94 100644
--- a/src/com/android/settings/vpn2/ConfigDialog.java
+++ b/src/com/android/settings/vpn2/ConfigDialog.java
@@ -40,6 +40,7 @@
 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 @@
 
     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 @@
         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 @@
         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 @@
                 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 @@
         }
 
         // 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 @@
             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 @@
             updateProxyFieldsVisibility(position);
         }
         updateUiControls();
+        mNameInput.setError("");
+        mServerInput.setError("");
+        mIpsecIdentifierInput.setError("");
+        mIpsecSecretInput.setError("");
     }
 
     @Override
@@ -375,30 +390,16 @@
             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 @@
         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 @@
     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 @@
         // 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 559003a..6bffef7 100644
--- a/src/com/android/settings/vpn2/ConfigDialogFragment.java
+++ b/src/com/android/settings/vpn2/ConfigDialogFragment.java
@@ -124,6 +124,7 @@
         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 1bf1102..a080fc8 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.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 @@
     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 @@
             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 40d22e6..38c99b6 100644
--- a/src/com/android/settings/wifi/WifiDialog.java
+++ b/src/com/android/settings/wifi/WifiDialog.java
@@ -28,7 +28,7 @@
 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 @@
         }
 
         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 5d8f8d4..0000000
--- 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 8006dad..53c80ff 100644
--- a/src/com/android/settings/wifi/utils/TextInputGroup.kt
+++ b/src/com/android/settings/wifi/utils/TextInputGroup.kt
@@ -18,6 +18,7 @@
 
 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 @@
     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 @@
             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 @@
     }
 
     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 3b23b1a..aa41b96 100644
--- a/src/com/android/settings/wifi/utils/WifiDialogHelper.kt
+++ b/src/com/android/settings/wifi/utils/WifiDialogHelper.kt
@@ -21,7 +21,7 @@
 
 class WifiDialogHelper(
     alertDialog: AlertDialog,
-    private val ssidInputGroup: SsidInputGroup? = null,
+    private val ssidInputGroup: TextInputGroup? = null,
 ) : AlertDialogHelper(alertDialog) {
 
     override fun canDismiss(): Boolean {
diff --git a/tests/robotests/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragmentTest.java b/tests/robotests/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragmentTest.java
index 571075c..f72b591 100644
--- a/tests/robotests/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragmentTest.java
+++ b/tests/robotests/src/com/android/settings/accessibility/ToggleFeaturePreferenceFragmentTest.java
@@ -39,6 +39,7 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.icu.text.CaseMap;
+import android.net.Uri;
 import android.os.Bundle;
 import android.platform.test.annotations.DisableFlags;
 import android.platform.test.annotations.EnableFlags;
@@ -56,12 +57,14 @@
 import androidx.preference.Preference;
 import androidx.preference.PreferenceManager;
 import androidx.preference.PreferenceScreen;
+import androidx.preference.PreferenceViewHolder;
 import androidx.test.core.app.ApplicationProvider;
 
 import com.android.settings.R;
 import com.android.settings.flags.Flags;
 import com.android.settings.testutils.shadow.ShadowAccessibilityManager;
 import com.android.settings.testutils.shadow.ShadowFragment;
+import com.android.settingslib.widget.IllustrationPreference;
 import com.android.settingslib.widget.TopIntroPreference;
 
 import com.google.android.setupcompat.util.WizardManagerHelper;
@@ -79,6 +82,7 @@
 import org.robolectric.annotation.Config;
 import org.robolectric.shadow.api.Shadow;
 import org.robolectric.shadows.ShadowApplication;
+import org.robolectric.shadows.ShadowLooper;
 
 import java.util.List;
 import java.util.Locale;
@@ -316,6 +320,45 @@
     }
 
     @Test
+    public void initAnimatedImagePreference_isAnimatable_setContentDescription() {
+        mFragment.mFeatureName = "Test Feature";
+        final View view =
+                LayoutInflater.from(mContext).inflate(
+                        com.android.settingslib.widget.preference.illustration
+                                .R.layout.illustration_preference,
+                        null);
+        IllustrationPreference preference = spy(new IllustrationPreference(mFragment.getContext()));
+        when(preference.isAnimatable()).thenReturn(true);
+        mFragment.initAnimatedImagePreference(mock(Uri.class), preference);
+
+        preference.onBindViewHolder(PreferenceViewHolder.createInstanceForTests(view));
+        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
+
+        String expectedContentDescription = mFragment.getString(
+                R.string.accessibility_illustration_content_description, mFragment.mFeatureName);
+        assertThat(preference.getContentDescription().toString())
+                .isEqualTo(expectedContentDescription);
+    }
+
+    @Test
+    public void initAnimatedImagePreference_isNotAnimatable_notSetContentDescription() {
+        mFragment.mFeatureName = "Test Feature";
+        final View view =
+                LayoutInflater.from(mContext).inflate(
+                        com.android.settingslib.widget.preference.illustration
+                                .R.layout.illustration_preference,
+                        null);
+        IllustrationPreference preference = spy(new IllustrationPreference(mFragment.getContext()));
+        when(preference.isAnimatable()).thenReturn(false);
+        mFragment.initAnimatedImagePreference(mock(Uri.class), preference);
+
+        preference.onBindViewHolder(PreferenceViewHolder.createInstanceForTests(view));
+        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
+
+        verify(preference, never()).setContentDescription(any());
+    }
+
+    @Test
     @EnableFlags(Flags.FLAG_ACCESSIBILITY_SHOW_APP_INFO_BUTTON)
     public void createAppInfoPreference_withValidComponentName() {
         when(mPackageManager.isPackageAvailable(PLACEHOLDER_PACKAGE_NAME)).thenReturn(true);