/* * Copyright (C) 2017 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.network; import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OFF; import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OPPORTUNISTIC; import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME; import android.app.AlertDialog; import android.app.Dialog; import android.app.FragmentManager; import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.os.Bundle; import android.provider.Settings; import android.support.annotation.VisibleForTesting; import android.text.Editable; import android.text.TextWatcher; import android.view.LayoutInflater; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.RadioGroup; import com.android.settings.R; import com.android.settings.core.instrumentation.InstrumentedDialogFragment; import java.util.HashMap; import java.util.Map; /** * Dialog to set the private dns */ public class PrivateDnsModeDialogFragment extends InstrumentedDialogFragment implements DialogInterface.OnClickListener, RadioGroup.OnCheckedChangeListener, TextWatcher { private static final String TAG = "PrivateDnsModeDialogFragment"; // DNS_MODE -> RadioButton id private static final Map PRIVATE_DNS_MAP; static { PRIVATE_DNS_MAP = new HashMap<>(); PRIVATE_DNS_MAP.put(PRIVATE_DNS_MODE_OFF, R.id.private_dns_mode_off); PRIVATE_DNS_MAP.put(PRIVATE_DNS_MODE_OPPORTUNISTIC, R.id.private_dns_mode_opportunistic); PRIVATE_DNS_MAP.put(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME, R.id.private_dns_mode_provider); } @VisibleForTesting static final String MODE_KEY = Settings.Global.PRIVATE_DNS_MODE; @VisibleForTesting static final String HOSTNAME_KEY = Settings.Global.PRIVATE_DNS_SPECIFIER; @VisibleForTesting EditText mEditText; @VisibleForTesting RadioGroup mRadioGroup; @VisibleForTesting Button mSaveButton; @VisibleForTesting String mMode; public static void show(FragmentManager fragmentManager) { if (fragmentManager.findFragmentByTag(TAG) == null) { final PrivateDnsModeDialogFragment fragment = new PrivateDnsModeDialogFragment(); fragment.show(fragmentManager, TAG); } } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { final Context context = getContext(); final AlertDialog dialog = new AlertDialog.Builder(context) .setTitle(R.string.select_private_dns_configuration_title) .setView(buildPrivateDnsView(context)) .setPositiveButton(R.string.save, this) .setNegativeButton(R.string.dlg_cancel, null) .create(); dialog.setOnShowListener(dialogInterface -> { mSaveButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); updateDialogInfo(); }); return dialog; } private View buildPrivateDnsView(final Context context) { final ContentResolver contentResolver = context.getContentResolver(); mMode = Settings.Global.getString(contentResolver, MODE_KEY); final View view = LayoutInflater.from(context).inflate(R.layout.private_dns_mode_dialog, null); mEditText = view.findViewById(R.id.private_dns_mode_provider_hostname); mEditText.addTextChangedListener(this); mEditText.setText(Settings.Global.getString(contentResolver, HOSTNAME_KEY)); 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)); return view; } @Override public void onClick(DialogInterface dialog, int which) { //TODO(b/34953048): add metric action if (mMode.equals(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME)) { // Only clickable if hostname is valid, so we could save it safely Settings.Global.putString(getContext().getContentResolver(), HOSTNAME_KEY, mEditText.getText().toString()); } Settings.Global.putString(getContext().getContentResolver(), MODE_KEY, mMode); } @Override public int getMetricsCategory() { //TODO(b/68030013): add metric id return 0; } @Override public void onCheckedChanged(RadioGroup group, int checkedId) { switch (checkedId) { case R.id.private_dns_mode_off: mMode = PRIVATE_DNS_MODE_OFF; break; case R.id.private_dns_mode_opportunistic: mMode = PRIVATE_DNS_MODE_OPPORTUNISTIC; break; case R.id.private_dns_mode_provider: mMode = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME; break; } updateDialogInfo(); } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { if (mSaveButton != null) { mSaveButton.setEnabled(isWeaklyValidatedHostname(mEditText.getText().toString())); } } private boolean isWeaklyValidatedHostname(String hostname) { // TODO(b/34953048): Find and use a better validation method. Specifically: // [1] this should reject IP string literals, and // [2] do the best, simplest, future-proof verification that // the input approximates a DNS hostname. final String WEAK_HOSTNAME_REGEX = "^[a-zA-Z0-9_.-]+$"; return hostname.matches(WEAK_HOSTNAME_REGEX); } private void updateDialogInfo() { final boolean modeProvider = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME.equals(mMode); if (mEditText != null) { mEditText.setEnabled(modeProvider); } if (mSaveButton != null) { mSaveButton.setEnabled( modeProvider ? isWeaklyValidatedHostname(mEditText.getText().toString()) : true); } } }