For obvious bootstrapping reasons, DNS settings have always used
IP address string literals in input fields.
However, since we can use the network-assigned nameservers to bootstrap
our way to multiple IP addresses of multiple families (!), hostnames
provide a clear simplicity and future-proofing advantage.
Permitting IP address literals means not only making sure that we can
validate X.509v3 certificates for IP addresses, but coping with the
inevitable broken configurations where users may have configured IPv4
addresses but no IPv6 addresses. This will unnecessarily complicate
life on IPv6-only networks.
=)
Test: as follows
- built
- flashed
- booted
- tried to enter IP string literals
- make -j50 RunSettingsRoboTests ROBOTEST_FILTER=PrivateDnsModeDialogPreferenceTest
Bug: 34953048
Bug: 64133961
Bug: 73641539
Change-Id: I7a58e86ed640ff5600906fb3d8cb9a2c75598831
226 lines
8.3 KiB
Java
226 lines
8.3 KiB
Java
/*
|
|
* 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 static android.system.OsConstants.AF_INET;
|
|
import static android.system.OsConstants.AF_INET6;
|
|
|
|
import android.app.AlertDialog;
|
|
import android.content.ActivityNotFoundException;
|
|
import android.content.ContentResolver;
|
|
import android.content.Context;
|
|
import android.content.DialogInterface;
|
|
import android.content.Intent;
|
|
import android.provider.Settings;
|
|
import android.support.annotation.VisibleForTesting;
|
|
import android.system.Os;
|
|
import android.text.Editable;
|
|
import android.text.TextWatcher;
|
|
import android.text.method.LinkMovementMethod;
|
|
import android.util.AttributeSet;
|
|
import android.util.Log;
|
|
import android.view.View;
|
|
import android.widget.Button;
|
|
import android.widget.EditText;
|
|
import android.widget.RadioGroup;
|
|
import android.widget.TextView;
|
|
|
|
import com.android.internal.logging.nano.MetricsProto;
|
|
import com.android.settings.R;
|
|
import com.android.settings.overlay.FeatureFactory;
|
|
import com.android.settings.utils.AnnotationSpan;
|
|
import com.android.settingslib.CustomDialogPreference;
|
|
import com.android.settingslib.HelpUtils;
|
|
|
|
import java.net.InetAddress;
|
|
import java.util.HashMap;
|
|
import java.util.Map;
|
|
|
|
/**
|
|
* Dialog to set the private dns
|
|
*/
|
|
public class PrivateDnsModeDialogPreference extends CustomDialogPreference implements
|
|
DialogInterface.OnClickListener, RadioGroup.OnCheckedChangeListener, TextWatcher {
|
|
|
|
public static final String ANNOTATION_URL = "url";
|
|
|
|
private static final String TAG = "PrivateDnsModeDialog";
|
|
// DNS_MODE -> RadioButton id
|
|
private static final Map<String, Integer> 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);
|
|
}
|
|
|
|
private static final int[] ADDRESS_FAMILIES = new int[]{AF_INET, AF_INET6};
|
|
|
|
@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
|
|
String mMode;
|
|
|
|
public PrivateDnsModeDialogPreference(Context context) {
|
|
super(context);
|
|
}
|
|
|
|
public PrivateDnsModeDialogPreference(Context context, AttributeSet attrs) {
|
|
super(context, attrs);
|
|
}
|
|
|
|
public PrivateDnsModeDialogPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
super(context, attrs, defStyleAttr);
|
|
}
|
|
|
|
public PrivateDnsModeDialogPreference(Context context, AttributeSet attrs, int defStyleAttr,
|
|
int defStyleRes) {
|
|
super(context, attrs, defStyleAttr, defStyleRes);
|
|
}
|
|
|
|
private final AnnotationSpan.LinkInfo mUrlLinkInfo = new AnnotationSpan.LinkInfo(
|
|
ANNOTATION_URL, (widget) -> {
|
|
final Context context = widget.getContext();
|
|
final Intent intent = HelpUtils.getHelpIntent(context,
|
|
context.getString(R.string.help_uri_private_dns),
|
|
context.getClass().getName());
|
|
if (intent != null) {
|
|
try {
|
|
widget.startActivityForResult(intent, 0);
|
|
} catch (ActivityNotFoundException e) {
|
|
Log.w(TAG, "Activity was not found for intent, " + intent.toString());
|
|
}
|
|
}
|
|
});
|
|
|
|
@Override
|
|
protected void onBindDialogView(View view) {
|
|
final Context context = getContext();
|
|
final ContentResolver contentResolver = context.getContentResolver();
|
|
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));
|
|
|
|
final TextView helpTextView = view.findViewById(R.id.private_dns_help_info);
|
|
helpTextView.setMovementMethod(LinkMovementMethod.getInstance());
|
|
final Intent helpIntent = HelpUtils.getHelpIntent(context,
|
|
context.getString(R.string.help_uri_private_dns),
|
|
context.getClass().getName());
|
|
final AnnotationSpan.LinkInfo linkInfo = new AnnotationSpan.LinkInfo(context,
|
|
ANNOTATION_URL, helpIntent);
|
|
if (linkInfo.isActionable()) {
|
|
helpTextView.setText(AnnotationSpan.linkify(
|
|
context.getText(R.string.private_dns_help_message), linkInfo));
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onClick(DialogInterface dialog, int which) {
|
|
final Context context = getContext();
|
|
if (mMode.equals(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME)) {
|
|
// Only clickable if hostname is valid, so we could save it safely
|
|
Settings.Global.putString(context.getContentResolver(), HOSTNAME_KEY,
|
|
mEditText.getText().toString());
|
|
}
|
|
|
|
FeatureFactory.getFactory(context).getMetricsFeatureProvider().action(context,
|
|
MetricsProto.MetricsEvent.ACTION_PRIVATE_DNS_MODE, mMode);
|
|
Settings.Global.putString(context.getContentResolver(), MODE_KEY, mMode);
|
|
}
|
|
|
|
@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) {
|
|
updateDialogInfo();
|
|
}
|
|
|
|
private boolean isWeaklyValidatedHostname(String hostname) {
|
|
// TODO(b/34953048): Use a validation method that permits more accurate,
|
|
// but still inexpensive, checking of likely valid DNS hostnames.
|
|
final String WEAK_HOSTNAME_REGEX = "^[a-zA-Z0-9_.-]+$";
|
|
if (!hostname.matches(WEAK_HOSTNAME_REGEX)) {
|
|
return false;
|
|
}
|
|
|
|
for (int address_family : ADDRESS_FAMILIES) {
|
|
if (Os.inet_pton(address_family, hostname) != null) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private Button getSaveButton() {
|
|
final AlertDialog dialog = (AlertDialog) getDialog();
|
|
if (dialog == null) {
|
|
return null;
|
|
}
|
|
return dialog.getButton(DialogInterface.BUTTON_POSITIVE);
|
|
}
|
|
|
|
private void updateDialogInfo() {
|
|
final boolean modeProvider = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME.equals(mMode);
|
|
if (mEditText != null) {
|
|
mEditText.setEnabled(modeProvider);
|
|
}
|
|
final Button saveButton = getSaveButton();
|
|
if (saveButton != null) {
|
|
saveButton.setEnabled(modeProvider
|
|
? isWeaklyValidatedHostname(mEditText.getText().toString())
|
|
: true);
|
|
}
|
|
}
|
|
}
|