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.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF; 19 import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OPPORTUNISTIC; 20 import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME; 21 22 import static com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; 23 24 import android.app.settings.SettingsEnums; 25 import android.content.ActivityNotFoundException; 26 import android.content.ContentResolver; 27 import android.content.Context; 28 import android.content.DialogInterface; 29 import android.content.Intent; 30 import android.net.ConnectivitySettingsManager; 31 import android.os.UserHandle; 32 import android.os.UserManager; 33 import android.provider.Settings; 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.RadioButton; 43 import android.widget.RadioGroup; 44 import android.widget.TextView; 45 46 import androidx.annotation.VisibleForTesting; 47 import androidx.appcompat.app.AlertDialog; 48 import androidx.preference.PreferenceViewHolder; 49 50 import com.android.settings.R; 51 import com.android.settings.overlay.FeatureFactory; 52 import com.android.settings.utils.AnnotationSpan; 53 import com.android.settingslib.CustomDialogPreferenceCompat; 54 import com.android.settingslib.HelpUtils; 55 import com.android.settingslib.RestrictedLockUtils; 56 import com.android.settingslib.RestrictedLockUtilsInternal; 57 58 import com.google.common.net.InternetDomainName; 59 60 import java.util.HashMap; 61 import java.util.Map; 62 63 /** 64 * Dialog to set the Private DNS 65 */ 66 public class PrivateDnsModeDialogPreference extends CustomDialogPreferenceCompat implements 67 DialogInterface.OnClickListener, RadioGroup.OnCheckedChangeListener, TextWatcher { 68 69 public static final String ANNOTATION_URL = "url"; 70 71 private static final String TAG = "PrivateDnsModeDialog"; 72 // DNS_MODE -> RadioButton id 73 private static final Map<Integer, Integer> PRIVATE_DNS_MAP; 74 75 static { 76 PRIVATE_DNS_MAP = new HashMap<>(); PRIVATE_DNS_MAP.put(PRIVATE_DNS_MODE_OFF, R.id.private_dns_mode_off)77 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)78 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)79 PRIVATE_DNS_MAP.put(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME, R.id.private_dns_mode_provider); 80 } 81 82 @VisibleForTesting 83 static final String MODE_KEY = Settings.Global.PRIVATE_DNS_MODE; 84 @VisibleForTesting 85 static final String HOSTNAME_KEY = Settings.Global.PRIVATE_DNS_SPECIFIER; 86 getHostnameFromSettings(ContentResolver cr)87 public static String getHostnameFromSettings(ContentResolver cr) { 88 return Settings.Global.getString(cr, HOSTNAME_KEY); 89 } 90 91 @VisibleForTesting 92 EditText mEditText; 93 @VisibleForTesting 94 RadioGroup mRadioGroup; 95 @VisibleForTesting 96 int mMode; 97 PrivateDnsModeDialogPreference(Context context)98 public PrivateDnsModeDialogPreference(Context context) { 99 super(context); 100 } 101 PrivateDnsModeDialogPreference(Context context, AttributeSet attrs)102 public PrivateDnsModeDialogPreference(Context context, AttributeSet attrs) { 103 super(context, attrs); 104 } 105 PrivateDnsModeDialogPreference(Context context, AttributeSet attrs, int defStyleAttr)106 public PrivateDnsModeDialogPreference(Context context, AttributeSet attrs, int defStyleAttr) { 107 super(context, attrs, defStyleAttr); 108 } 109 PrivateDnsModeDialogPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)110 public PrivateDnsModeDialogPreference(Context context, AttributeSet attrs, int defStyleAttr, 111 int defStyleRes) { 112 super(context, attrs, defStyleAttr, defStyleRes); 113 } 114 115 private final AnnotationSpan.LinkInfo mUrlLinkInfo = new AnnotationSpan.LinkInfo( 116 ANNOTATION_URL, (widget) -> { 117 final Context context = widget.getContext(); 118 final Intent intent = HelpUtils.getHelpIntent(context, 119 context.getString(R.string.help_uri_private_dns), 120 context.getClass().getName()); 121 if (intent != null) { 122 try { 123 widget.startActivityForResult(intent, 0); 124 } catch (ActivityNotFoundException e) { 125 Log.w(TAG, "Activity was not found for intent, " + intent.toString()); 126 } 127 } 128 }); 129 130 @Override onBindViewHolder(PreferenceViewHolder holder)131 public void onBindViewHolder(PreferenceViewHolder holder) { 132 super.onBindViewHolder(holder); 133 if (isDisabledByAdmin()) { 134 // If the preference is disabled by the admin, set the inner item as enabled so 135 // it could act as a click target. The preference itself will have been disabled 136 // by the controller. 137 holder.itemView.setEnabled(true); 138 } 139 } 140 141 @Override onBindDialogView(View view)142 protected void onBindDialogView(View view) { 143 final Context context = getContext(); 144 final ContentResolver contentResolver = context.getContentResolver(); 145 146 mMode = ConnectivitySettingsManager.getPrivateDnsMode(context); 147 148 mEditText = view.findViewById(R.id.private_dns_mode_provider_hostname); 149 mEditText.addTextChangedListener(this); 150 mEditText.setText(getHostnameFromSettings(contentResolver)); 151 152 mRadioGroup = view.findViewById(R.id.private_dns_radio_group); 153 mRadioGroup.setOnCheckedChangeListener(this); 154 mRadioGroup.check(PRIVATE_DNS_MAP.getOrDefault(mMode, R.id.private_dns_mode_opportunistic)); 155 156 // Initial radio button text 157 final RadioButton offRadioButton = view.findViewById(R.id.private_dns_mode_off); 158 offRadioButton.setText(com.android.settingslib.R.string.private_dns_mode_off); 159 final RadioButton opportunisticRadioButton = 160 view.findViewById(R.id.private_dns_mode_opportunistic); 161 opportunisticRadioButton.setText( 162 com.android.settingslib.R.string.private_dns_mode_opportunistic); 163 final RadioButton providerRadioButton = view.findViewById(R.id.private_dns_mode_provider); 164 providerRadioButton.setText(com.android.settingslib.R.string.private_dns_mode_provider); 165 166 final TextView helpTextView = view.findViewById(R.id.private_dns_help_info); 167 helpTextView.setMovementMethod(LinkMovementMethod.getInstance()); 168 final Intent helpIntent = HelpUtils.getHelpIntent(context, 169 context.getString(R.string.help_uri_private_dns), 170 context.getClass().getName()); 171 final AnnotationSpan.LinkInfo linkInfo = new AnnotationSpan.LinkInfo(context, 172 ANNOTATION_URL, helpIntent); 173 if (linkInfo.isActionable()) { 174 helpTextView.setText(AnnotationSpan.linkify( 175 context.getText(R.string.private_dns_help_message), linkInfo)); 176 } else { 177 helpTextView.setText(""); 178 } 179 } 180 181 @Override onClick(DialogInterface dialog, int which)182 public void onClick(DialogInterface dialog, int which) { 183 if (which == DialogInterface.BUTTON_POSITIVE) { 184 final Context context = getContext(); 185 if (mMode == PRIVATE_DNS_MODE_PROVIDER_HOSTNAME) { 186 // Only clickable if hostname is valid, so we could save it safely 187 ConnectivitySettingsManager.setPrivateDnsHostname(context, 188 mEditText.getText().toString()); 189 } 190 191 FeatureFactory.getFeatureFactory().getMetricsFeatureProvider().action(context, 192 SettingsEnums.ACTION_PRIVATE_DNS_MODE, mMode); 193 ConnectivitySettingsManager.setPrivateDnsMode(context, mMode); 194 } 195 } 196 197 @Override onCheckedChanged(RadioGroup group, int checkedId)198 public void onCheckedChanged(RadioGroup group, int checkedId) { 199 if (checkedId == R.id.private_dns_mode_off) { 200 mMode = PRIVATE_DNS_MODE_OFF; 201 } else if (checkedId == R.id.private_dns_mode_opportunistic) { 202 mMode = PRIVATE_DNS_MODE_OPPORTUNISTIC; 203 } else if (checkedId == R.id.private_dns_mode_provider) { 204 mMode = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME; 205 } 206 updateDialogInfo(); 207 } 208 209 @Override beforeTextChanged(CharSequence s, int start, int count, int after)210 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 211 } 212 213 @Override onTextChanged(CharSequence s, int start, int before, int count)214 public void onTextChanged(CharSequence s, int start, int before, int count) { 215 } 216 217 @Override afterTextChanged(Editable s)218 public void afterTextChanged(Editable s) { 219 updateDialogInfo(); 220 } 221 222 @Override performClick()223 public void performClick() { 224 EnforcedAdmin enforcedAdmin = getEnforcedAdmin(); 225 226 if (enforcedAdmin == null) { 227 // If the restriction is not restricted by admin, continue as usual. 228 super.performClick(); 229 } else { 230 // Show a dialog explaining to the user why they cannot change the preference. 231 RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(), enforcedAdmin); 232 } 233 } 234 getEnforcedAdmin()235 private EnforcedAdmin getEnforcedAdmin() { 236 return RestrictedLockUtilsInternal.checkIfRestrictionEnforced( 237 getContext(), UserManager.DISALLOW_CONFIG_PRIVATE_DNS, UserHandle.myUserId()); 238 } 239 isDisabledByAdmin()240 private boolean isDisabledByAdmin() { 241 return getEnforcedAdmin() != null; 242 } 243 getSaveButton()244 private Button getSaveButton() { 245 final AlertDialog dialog = (AlertDialog) getDialog(); 246 if (dialog == null) { 247 return null; 248 } 249 return dialog.getButton(DialogInterface.BUTTON_POSITIVE); 250 } 251 updateDialogInfo()252 private void updateDialogInfo() { 253 final boolean modeProvider = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME == mMode; 254 if (mEditText != null) { 255 mEditText.setEnabled(modeProvider); 256 } 257 final Button saveButton = getSaveButton(); 258 if (saveButton != null) { 259 saveButton.setEnabled(modeProvider 260 ? InternetDomainName.isValid(mEditText.getText().toString()) 261 : true); 262 } 263 } 264 } 265