Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit b3938a02 authored by Weng Su's avatar Weng Su
Browse files

Fix accessibility issues in Private DNS Settings

- Keep the Save button enabled at all times

- Show error in the Hostname view to remind the user
  - "The field is required" error
  - "The hostname you typed isn't valid" error

Bug: 386323822
Flag: EXEMPT bugfix
Test: Manual testing
atest -c PrivateDnsModeDialogPreferenceTest \
         PrivateDnsPreferenceControllerTest

Change-Id: I63973bd5001b838d7f27827e6a6d4ac96ac78ca9
parent cc2b6ab4
Loading
Loading
Loading
Loading
+19 −10
Original line number Diff line number Diff line
@@ -16,8 +16,10 @@

<ScrollView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    android:layout_height="wrap_content"
    android:theme="@style/Theme.AppCompat.DayNight">

    <LinearLayout
        android:layout_width="match_parent"
@@ -43,17 +45,24 @@
                android:id="@+id/private_dns_mode_provider"
                layout="@layout/preference_widget_dialog_radiobutton"/>

            <EditText
            <com.google.android.material.textfield.TextInputLayout
                android:id="@+id/private_dns_mode_provider_hostname_layout"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:hint="@string/private_dns_title"
                android:theme="@style/Theme.Settings"
                app:endIconMode="clear_text"
                app:errorEnabled="true">

                <com.google.android.material.textfield.TextInputEditText
                    android:id="@+id/private_dns_mode_provider_hostname"
                android:hint="@string/private_dns_mode_provider_hostname_hint"
                style="@android:style/Widget.CompoundButton.RadioButton"
                android:imeOptions="actionDone"
                android:inputType="textFilter|textUri|textMultiLine"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:imeOptions="actionDone"
                    android:inputType="textFilter|textUri|textMultiLine"
                    android:layout_marginStart="3dp"
                android:layout_marginEnd="8dp"
                android:minHeight="@dimen/developer_option_dialog_min_height"/>
                    android:layout_marginEnd="8dp"/>
            </com.google.android.material.textfield.TextInputLayout>
        </RadioGroup>

        <include
+6 −0
Original line number Diff line number Diff line
@@ -2937,6 +2937,12 @@
    <string name="emergency_address_title">Emergency address</string>
    <!-- Summary of Update Emergency Address preference, explaining usage of emergency address [CHAR LIMIT=NONE] -->
    <string name="emergency_address_summary">Used as your location when you make an emergency call over Wi\u2011Fi</string>
    <!-- Title of a preference for private DNS provider hostname [CHAR LIMIT=40] -->
    <string name="private_dns_title">Hostname</string>
    <!-- Message of private dns hostname that the field is required. [CHAR LIMIT=NONE] -->
    <string name="private_dns_field_require">The field is required</string>
    <!-- The error if the private dns hostname is not valid -->
    <string name="private_dns_hostname_invalid">The hostname you typed isn\u2019t valid</string>
    <!-- Message of private dns that provides a help link. [CHAR LIMIT=NONE] -->
    <string name="private_dns_help_message"><annotation id="url">Learn more</annotation> about Private DNS features</string>
    <!-- Message to display when private dns is on. [CHAR LIMIT=10] -->
+64 −50
Original line number Diff line number Diff line
@@ -23,14 +23,12 @@ import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;

import android.app.settings.SettingsEnums;
import android.content.ActivityNotFoundException;
import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.ConnectivitySettingsManager;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
import android.text.Editable;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
@@ -55,6 +53,7 @@ import com.android.settingslib.HelpUtils;
import com.android.settingslib.RestrictedLockUtils;
import com.android.settingslib.RestrictedLockUtilsInternal;

import com.google.android.material.textfield.TextInputLayout;
import com.google.common.net.InternetDomainName;

import java.util.HashMap;
@@ -64,7 +63,7 @@ import java.util.Map;
 * Dialog to set the Private DNS
 */
public class PrivateDnsModeDialogPreference extends CustomDialogPreferenceCompat implements
        DialogInterface.OnClickListener, RadioGroup.OnCheckedChangeListener, TextWatcher {
        RadioGroup.OnCheckedChangeListener, TextWatcher {

    public static final String ANNOTATION_URL = "url";

@@ -80,16 +79,9 @@ public class PrivateDnsModeDialogPreference extends CustomDialogPreferenceCompat
    }

    @VisibleForTesting
    static final String MODE_KEY = Settings.Global.PRIVATE_DNS_MODE;
    TextInputLayout mHostnameLayout;
    @VisibleForTesting
    static final String HOSTNAME_KEY = Settings.Global.PRIVATE_DNS_SPECIFIER;

    public static String getHostnameFromSettings(ContentResolver cr) {
        return Settings.Global.getString(cr, HOSTNAME_KEY);
    }

    @VisibleForTesting
    EditText mEditText;
    EditText mHostnameText;
    @VisibleForTesting
    RadioGroup mRadioGroup;
    @VisibleForTesting
@@ -136,22 +128,17 @@ public class PrivateDnsModeDialogPreference extends CustomDialogPreferenceCompat
            // by the controller.
            holder.itemView.setEnabled(true);
        }

        setSaveButtonListener();
    }

    @Override
    protected void onBindDialogView(View view) {
        final Context context = getContext();
        final ContentResolver contentResolver = context.getContentResolver();

        mMode = ConnectivitySettingsManager.getPrivateDnsMode(context);

        mEditText = view.findViewById(R.id.private_dns_mode_provider_hostname);
        mEditText.addTextChangedListener(this);
        mEditText.setText(getHostnameFromSettings(contentResolver));

        mRadioGroup = view.findViewById(R.id.private_dns_radio_group);
        mRadioGroup.setOnCheckedChangeListener(this);
        mRadioGroup.check(PRIVATE_DNS_MAP.getOrDefault(mMode, R.id.private_dns_mode_opportunistic));
        mRadioGroup.setOnCheckedChangeListener(this);

        // Initial radio button text
        final RadioButton offRadioButton = view.findViewById(R.id.private_dns_mode_off);
@@ -163,6 +150,13 @@ public class PrivateDnsModeDialogPreference extends CustomDialogPreferenceCompat
        final RadioButton providerRadioButton = view.findViewById(R.id.private_dns_mode_provider);
        providerRadioButton.setText(com.android.settingslib.R.string.private_dns_mode_provider);

        mHostnameLayout = view.findViewById(R.id.private_dns_mode_provider_hostname_layout);
        mHostnameText = view.findViewById(R.id.private_dns_mode_provider_hostname);
        if (mHostnameText != null) {
            mHostnameText.setText(ConnectivitySettingsManager.getPrivateDnsHostname(context));
            mHostnameText.addTextChangedListener(this);
        }

        final TextView helpTextView = view.findViewById(R.id.private_dns_help_info);
        helpTextView.setMovementMethod(LinkMovementMethod.getInstance());
        final Intent helpIntent = HelpUtils.getHelpIntent(context,
@@ -176,22 +170,8 @@ public class PrivateDnsModeDialogPreference extends CustomDialogPreferenceCompat
        } else {
            helpTextView.setText("");
        }
    }

    @Override
    public void onClick(DialogInterface dialog, int which) {
        if (which == DialogInterface.BUTTON_POSITIVE) {
            final Context context = getContext();
            if (mMode == PRIVATE_DNS_MODE_PROVIDER_HOSTNAME) {
                // Only clickable if hostname is valid, so we could save it safely
                ConnectivitySettingsManager.setPrivateDnsHostname(context,
                        mEditText.getText().toString());
            }

            FeatureFactory.getFeatureFactory().getMetricsFeatureProvider().action(context,
                    SettingsEnums.ACTION_PRIVATE_DNS_MODE, mMode);
            ConnectivitySettingsManager.setPrivateDnsMode(context, mMode);
        }
        updateDialogInfo();
    }

    @Override
@@ -241,24 +221,58 @@ public class PrivateDnsModeDialogPreference extends CustomDialogPreferenceCompat
        return getEnforcedAdmin() != null;
    }

    private Button getSaveButton() {
        final AlertDialog dialog = (AlertDialog) getDialog();
    private void updateDialogInfo() {
        final boolean modeProvider = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME == mMode;
        if (mHostnameLayout != null) {
            mHostnameLayout.setEnabled(modeProvider);
            mHostnameLayout.setErrorEnabled(false);
        }
    }

    private void setSaveButtonListener() {
        View.OnClickListener onClickListener = v -> doSaveButton();
        DialogInterface.OnShowListener onShowListener = dialog -> {
            if (dialog == null) {
            return null;
                Log.e(TAG, "The DialogInterface is null!");
                return;
            }
            Button saveButton = ((AlertDialog) dialog).getButton(DialogInterface.BUTTON_POSITIVE);
            if (saveButton == null) {
                Log.e(TAG, "Can't get the save button!");
                return;
            }
        return dialog.getButton(DialogInterface.BUTTON_POSITIVE);
            saveButton.setOnClickListener(onClickListener);
        };
        setOnShowListener(onShowListener);
    }

    private void updateDialogInfo() {
        final boolean modeProvider = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME == mMode;
        if (mEditText != null) {
            mEditText.setEnabled(modeProvider);
    @VisibleForTesting
    void doSaveButton() {
        Context context = getContext();
        if (mMode == PRIVATE_DNS_MODE_PROVIDER_HOSTNAME) {
            if (mHostnameLayout == null || mHostnameText == null) {
                Log.e(TAG, "Can't find hostname resources!");
                return;
            }
            if (mHostnameText.getText().isEmpty()) {
                mHostnameLayout.setError(context.getString(R.string.private_dns_field_require));
                Log.w(TAG, "The hostname is empty!");
                return;
            }
        final Button saveButton = getSaveButton();
        if (saveButton != null) {
            saveButton.setEnabled(modeProvider
                    ? InternetDomainName.isValid(mEditText.getText().toString())
                    : true);
            if (!InternetDomainName.isValid(mHostnameText.getText().toString())) {
                mHostnameLayout.setError(context.getString(R.string.private_dns_hostname_invalid));
                Log.w(TAG, "The hostname is invalid!");
                return;
            }

            ConnectivitySettingsManager.setPrivateDnsHostname(context,
                    mHostnameText.getText().toString());
        }

        ConnectivitySettingsManager.setPrivateDnsMode(context, mMode);

        FeatureFactory.getFeatureFactory().getMetricsFeatureProvider()
                .action(context, SettingsEnums.ACTION_PRIVATE_DNS_MODE, mMode);
        getDialog().dismiss();
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -135,7 +135,7 @@ public class PrivateDnsPreferenceController extends BasePreferenceController
                                com.android.settingslib.R.string.private_dns_mode_opportunistic);
            case PRIVATE_DNS_MODE_PROVIDER_HOSTNAME:
                return dnsesResolved
                        ? PrivateDnsModeDialogPreference.getHostnameFromSettings(cr)
                        ? ConnectivitySettingsManager.getPrivateDnsHostname(mContext)
                        : res.getString(
                                com.android.settingslib.R.string.private_dns_mode_provider_failure);
        }
+55 −40
Original line number Diff line number Diff line
@@ -21,14 +21,12 @@ import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OPPORTUNI
import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;

import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import android.content.Context;
import android.content.DialogInterface;
import android.net.ConnectivitySettingsManager;
import android.view.LayoutInflater;
import android.view.View;
@@ -88,31 +86,31 @@ public class PrivateDnsModeDialogPreferenceTest {
    }

    @Test
    public void testOnCheckedChanged_dnsModeOff_disableEditText() {
    public void onCheckedChanged_dnsModeOff_disableHostnameText() {
        mPreference.onCheckedChanged(null, R.id.private_dns_mode_off);

        assertThat(mPreference.mMode).isEqualTo(PRIVATE_DNS_MODE_OFF);
        assertThat(mPreference.mEditText.isEnabled()).isFalse();
        assertThat(mPreference.mHostnameText.isEnabled()).isFalse();
    }

    @Test
    public void testOnCheckedChanged_dnsModeOpportunistic_disableEditText() {
    public void onCheckedChanged_dnsModeOpportunistic_disableHostnameText() {
        mPreference.onCheckedChanged(null, R.id.private_dns_mode_opportunistic);

        assertThat(mPreference.mMode).isEqualTo(PRIVATE_DNS_MODE_OPPORTUNISTIC);
        assertThat(mPreference.mEditText.isEnabled()).isFalse();
        assertThat(mPreference.mHostnameText.isEnabled()).isFalse();
    }

    @Test
    public void testOnCheckedChanged_dnsModeProvider_enableEditText() {
    public void onCheckedChanged_dnsModeProvider_enableHostnameText() {
        mPreference.onCheckedChanged(null, R.id.private_dns_mode_provider);

        assertThat(mPreference.mMode).isEqualTo(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME);
        assertThat(mPreference.mEditText.isEnabled()).isTrue();
        assertThat(mPreference.mHostnameText.isEnabled()).isTrue();
    }

    @Test
    public void testOnBindDialogView_containsCorrectData() {
    public void onBindDialogView_containsCorrectData() {
        // Don't set settings to the default value ("opportunistic") as that
        // risks masking failure to read the mode from settings.
        ConnectivitySettingsManager.setPrivateDnsMode(mContext, PRIVATE_DNS_MODE_OFF);
@@ -123,57 +121,74 @@ public class PrivateDnsModeDialogPreferenceTest {
                new LinearLayout(mContext), false);
        mPreference.onBindDialogView(view);

        assertThat(mPreference.mEditText.getText().toString()).isEqualTo(HOST_NAME);
        assertThat(mPreference.mHostnameText.getText().toString()).isEqualTo(HOST_NAME);
        assertThat(mPreference.mRadioGroup.getCheckedRadioButtonId()).isEqualTo(
                R.id.private_dns_mode_off);
    }

    @Test
    public void testOnCheckedChanged_switchMode_saveButtonHasCorrectState() {
        final String[] INVALID_HOST_NAMES = new String[] {
                INVALID_HOST_NAME,
                "2001:db8::53",  // IPv6 string literal
                "192.168.1.1",   // IPv4 string literal
        };
    public void doSaveButton_changeToOffMode_saveData() {
        // Set the default settings to OPPORTUNISTIC
        ConnectivitySettingsManager.setPrivateDnsMode(mContext, PRIVATE_DNS_MODE_OPPORTUNISTIC);

        for (String invalid : INVALID_HOST_NAMES) {
            // Set invalid hostname
            mPreference.mEditText.setText(invalid);
        mPreference.mMode = PRIVATE_DNS_MODE_OFF;
        mPreference.doSaveButton();

            mPreference.onCheckedChanged(null, R.id.private_dns_mode_off);
            assertWithMessage("off: " + invalid).that(mSaveButton.isEnabled()).isTrue();

            mPreference.onCheckedChanged(null, R.id.private_dns_mode_opportunistic);
            assertWithMessage("opportunistic: " + invalid).that(mSaveButton.isEnabled()).isTrue();

            mPreference.onCheckedChanged(null, R.id.private_dns_mode_provider);
            assertWithMessage("provider: " + invalid).that(mSaveButton.isEnabled()).isFalse();
        }
        // Change to OPPORTUNISTIC
        assertThat(ConnectivitySettingsManager.getPrivateDnsMode(mContext))
                .isEqualTo(PRIVATE_DNS_MODE_OFF);
    }

    @Test
    public void testOnClick_positiveButtonClicked_saveData() {
    public void doSaveButton_changeToOpportunisticMode_saveData() {
        // Set the default settings to OFF
        ConnectivitySettingsManager.setPrivateDnsMode(mContext, PRIVATE_DNS_MODE_OFF);

        mPreference.mMode = PRIVATE_DNS_MODE_OPPORTUNISTIC;
        mPreference.onClick(null, DialogInterface.BUTTON_POSITIVE);
        mPreference.doSaveButton();

        // Change to OPPORTUNISTIC
        assertThat(ConnectivitySettingsManager.getPrivateDnsMode(mContext)).isEqualTo(
                PRIVATE_DNS_MODE_OPPORTUNISTIC);
        assertThat(ConnectivitySettingsManager.getPrivateDnsMode(mContext))
                .isEqualTo(PRIVATE_DNS_MODE_OPPORTUNISTIC);
    }

    @Test
    public void testOnClick_negativeButtonClicked_doNothing() {
    public void doSaveButton_changeToProviderHostnameMode_saveData() {
        // Set the default settings to OFF
        ConnectivitySettingsManager.setPrivateDnsMode(mContext, PRIVATE_DNS_MODE_OFF);

        mPreference.mMode = PRIVATE_DNS_MODE_OPPORTUNISTIC;
        mPreference.onClick(null, DialogInterface.BUTTON_NEGATIVE);
        mPreference.onCheckedChanged(null, R.id.private_dns_mode_provider);
        mPreference.mHostnameText.setText(HOST_NAME);
        mPreference.doSaveButton();

        // Change to PROVIDER_HOSTNAME
        assertThat(ConnectivitySettingsManager.getPrivateDnsMode(mContext))
                .isEqualTo(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME);
        assertThat(ConnectivitySettingsManager.getPrivateDnsHostname(mContext))
                .isEqualTo(HOST_NAME);
    }

    @Test
    public void doSaveButton_providerHostnameIsEmpty_setHostnameError() {
        ConnectivitySettingsManager.setPrivateDnsMode(mContext, PRIVATE_DNS_MODE_PROVIDER_HOSTNAME);
        ConnectivitySettingsManager.setPrivateDnsHostname(mContext, HOST_NAME);
        mPreference.onCheckedChanged(null, R.id.private_dns_mode_provider);

        mPreference.mHostnameText.setText("");
        mPreference.doSaveButton();

        assertThat(mPreference.mHostnameLayout.isErrorEnabled()).isTrue();
    }

    @Test
    public void doSaveButton_providerHostnameIsInvalid_setHostnameError() {
        ConnectivitySettingsManager.setPrivateDnsMode(mContext, PRIVATE_DNS_MODE_PROVIDER_HOSTNAME);
        ConnectivitySettingsManager.setPrivateDnsHostname(mContext, HOST_NAME);
        mPreference.onCheckedChanged(null, R.id.private_dns_mode_provider);

        mPreference.mHostnameText.setText(INVALID_HOST_NAME);
        mPreference.doSaveButton();

        // Still equal to OFF
        assertThat(ConnectivitySettingsManager.getPrivateDnsMode(mContext)).isEqualTo(
                PRIVATE_DNS_MODE_OFF);
        assertThat(mPreference.mHostnameLayout.isErrorEnabled()).isTrue();
    }
}