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