1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.settings.network; 17 18 import static android.net.ConnectivityManager.PRIVATE_DNS_DEFAULT_MODE_FALLBACK; 19 import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OFF; 20 import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OPPORTUNISTIC; 21 import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME; 22 import static android.system.OsConstants.AF_INET; 23 import static android.system.OsConstants.AF_INET6; 24 25 import android.app.AlertDialog; 26 import android.content.ActivityNotFoundException; 27 import android.content.ContentResolver; 28 import android.content.Context; 29 import android.content.DialogInterface; 30 import android.content.Intent; 31 import android.provider.Settings; 32 import android.support.annotation.VisibleForTesting; 33 import android.system.Os; 34 import android.text.Editable; 35 import android.text.TextWatcher; 36 import android.text.method.LinkMovementMethod; 37 import android.util.AttributeSet; 38 import android.util.Log; 39 import android.view.View; 40 import android.widget.Button; 41 import android.widget.EditText; 42 import android.widget.RadioGroup; 43 import android.widget.TextView; 44 45 import com.android.internal.logging.nano.MetricsProto; 46 import com.android.settings.R; 47 import com.android.settings.overlay.FeatureFactory; 48 import com.android.settings.utils.AnnotationSpan; 49 import com.android.settingslib.CustomDialogPreference; 50 import com.android.settingslib.HelpUtils; 51 52 import java.net.InetAddress; 53 import java.util.HashMap; 54 import java.util.Map; 55 56 /** 57 * Dialog to set the Private DNS 58 */ 59 public class PrivateDnsModeDialogPreference extends CustomDialogPreference implements 60 DialogInterface.OnClickListener, RadioGroup.OnCheckedChangeListener, TextWatcher { 61 62 public static final String ANNOTATION_URL = "url"; 63 64 private static final String TAG = "PrivateDnsModeDialog"; 65 // DNS_MODE -> RadioButton id 66 private static final Map<String, Integer> PRIVATE_DNS_MAP; 67 68 static { 69 PRIVATE_DNS_MAP = new HashMap<>(); PRIVATE_DNS_MAP.put(PRIVATE_DNS_MODE_OFF, R.id.private_dns_mode_off)70 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)71 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)72 PRIVATE_DNS_MAP.put(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME, R.id.private_dns_mode_provider); 73 } 74 75 private static final int[] ADDRESS_FAMILIES = new int[]{AF_INET, AF_INET6}; 76 77 @VisibleForTesting 78 static final String MODE_KEY = Settings.Global.PRIVATE_DNS_MODE; 79 @VisibleForTesting 80 static final String HOSTNAME_KEY = Settings.Global.PRIVATE_DNS_SPECIFIER; 81 getModeFromSettings(ContentResolver cr)82 public static String getModeFromSettings(ContentResolver cr) { 83 String mode = Settings.Global.getString(cr, MODE_KEY); 84 if (!PRIVATE_DNS_MAP.containsKey(mode)) { 85 mode = Settings.Global.getString(cr, Settings.Global.PRIVATE_DNS_DEFAULT_MODE); 86 } 87 return PRIVATE_DNS_MAP.containsKey(mode) ? mode : PRIVATE_DNS_DEFAULT_MODE_FALLBACK; 88 } 89 getHostnameFromSettings(ContentResolver cr)90 public static String getHostnameFromSettings(ContentResolver cr) { 91 return Settings.Global.getString(cr, HOSTNAME_KEY); 92 } 93 94 @VisibleForTesting 95 EditText mEditText; 96 @VisibleForTesting 97 RadioGroup mRadioGroup; 98 @VisibleForTesting 99 String mMode; 100 PrivateDnsModeDialogPreference(Context context)101 public PrivateDnsModeDialogPreference(Context context) { 102 super(context); 103 } 104 PrivateDnsModeDialogPreference(Context context, AttributeSet attrs)105 public PrivateDnsModeDialogPreference(Context context, AttributeSet attrs) { 106 super(context, attrs); 107 } 108 PrivateDnsModeDialogPreference(Context context, AttributeSet attrs, int defStyleAttr)109 public PrivateDnsModeDialogPreference(Context context, AttributeSet attrs, int defStyleAttr) { 110 super(context, attrs, defStyleAttr); 111 } 112 PrivateDnsModeDialogPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)113 public PrivateDnsModeDialogPreference(Context context, AttributeSet attrs, int defStyleAttr, 114 int defStyleRes) { 115 super(context, attrs, defStyleAttr, defStyleRes); 116 } 117 118 private final AnnotationSpan.LinkInfo mUrlLinkInfo = new AnnotationSpan.LinkInfo( 119 ANNOTATION_URL, (widget) -> { 120 final Context context = widget.getContext(); 121 final Intent intent = HelpUtils.getHelpIntent(context, 122 context.getString(R.string.help_uri_private_dns), 123 context.getClass().getName()); 124 if (intent != null) { 125 try { 126 widget.startActivityForResult(intent, 0); 127 } catch (ActivityNotFoundException e) { 128 Log.w(TAG, "Activity was not found for intent, " + intent.toString()); 129 } 130 } 131 }); 132 133 @Override onBindDialogView(View view)134 protected void onBindDialogView(View view) { 135 final Context context = getContext(); 136 final ContentResolver contentResolver = context.getContentResolver(); 137 138 mMode = getModeFromSettings(context.getContentResolver()); 139 140 mEditText = view.findViewById(R.id.private_dns_mode_provider_hostname); 141 mEditText.addTextChangedListener(this); 142 mEditText.setText(getHostnameFromSettings(contentResolver)); 143 144 mRadioGroup = view.findViewById(R.id.private_dns_radio_group); 145 mRadioGroup.setOnCheckedChangeListener(this); 146 mRadioGroup.check(PRIVATE_DNS_MAP.getOrDefault(mMode, R.id.private_dns_mode_opportunistic)); 147 148 final TextView helpTextView = view.findViewById(R.id.private_dns_help_info); 149 helpTextView.setMovementMethod(LinkMovementMethod.getInstance()); 150 final Intent helpIntent = HelpUtils.getHelpIntent(context, 151 context.getString(R.string.help_uri_private_dns), 152 context.getClass().getName()); 153 final AnnotationSpan.LinkInfo linkInfo = new AnnotationSpan.LinkInfo(context, 154 ANNOTATION_URL, helpIntent); 155 if (linkInfo.isActionable()) { 156 helpTextView.setText(AnnotationSpan.linkify( 157 context.getText(R.string.private_dns_help_message), linkInfo)); 158 } 159 } 160 161 @Override onClick(DialogInterface dialog, int which)162 public void onClick(DialogInterface dialog, int which) { 163 if (which == DialogInterface.BUTTON_POSITIVE) { 164 final Context context = getContext(); 165 if (mMode.equals(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME)) { 166 // Only clickable if hostname is valid, so we could save it safely 167 Settings.Global.putString(context.getContentResolver(), HOSTNAME_KEY, 168 mEditText.getText().toString()); 169 } 170 171 FeatureFactory.getFactory(context).getMetricsFeatureProvider().action(context, 172 MetricsProto.MetricsEvent.ACTION_PRIVATE_DNS_MODE, mMode); 173 Settings.Global.putString(context.getContentResolver(), MODE_KEY, mMode); 174 } 175 } 176 177 @Override onCheckedChanged(RadioGroup group, int checkedId)178 public void onCheckedChanged(RadioGroup group, int checkedId) { 179 switch (checkedId) { 180 case R.id.private_dns_mode_off: 181 mMode = PRIVATE_DNS_MODE_OFF; 182 break; 183 case R.id.private_dns_mode_opportunistic: 184 mMode = PRIVATE_DNS_MODE_OPPORTUNISTIC; 185 break; 186 case R.id.private_dns_mode_provider: 187 mMode = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME; 188 break; 189 } 190 updateDialogInfo(); 191 } 192 193 @Override beforeTextChanged(CharSequence s, int start, int count, int after)194 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 195 } 196 197 @Override onTextChanged(CharSequence s, int start, int before, int count)198 public void onTextChanged(CharSequence s, int start, int before, int count) { 199 } 200 201 @Override afterTextChanged(Editable s)202 public void afterTextChanged(Editable s) { 203 updateDialogInfo(); 204 } 205 isWeaklyValidatedHostname(String hostname)206 private boolean isWeaklyValidatedHostname(String hostname) { 207 // TODO(b/34953048): Use a validation method that permits more accurate, 208 // but still inexpensive, checking of likely valid DNS hostnames. 209 final String WEAK_HOSTNAME_REGEX = "^[a-zA-Z0-9_.-]+$"; 210 if (!hostname.matches(WEAK_HOSTNAME_REGEX)) { 211 return false; 212 } 213 214 for (int address_family : ADDRESS_FAMILIES) { 215 if (Os.inet_pton(address_family, hostname) != null) { 216 return false; 217 } 218 } 219 220 return true; 221 } 222 getSaveButton()223 private Button getSaveButton() { 224 final AlertDialog dialog = (AlertDialog) getDialog(); 225 if (dialog == null) { 226 return null; 227 } 228 return dialog.getButton(DialogInterface.BUTTON_POSITIVE); 229 } 230 updateDialogInfo()231 private void updateDialogInfo() { 232 final boolean modeProvider = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME.equals(mMode); 233 if (mEditText != null) { 234 mEditText.setEnabled(modeProvider); 235 } 236 final Button saveButton = getSaveButton(); 237 if (saveButton != null) { 238 saveButton.setEnabled(modeProvider 239 ? isWeaklyValidatedHostname(mEditText.getText().toString()) 240 : true); 241 } 242 } 243 } 244