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 initialize(); 101 } 102 PrivateDnsModeDialogPreference(Context context, AttributeSet attrs)103 public PrivateDnsModeDialogPreference(Context context, AttributeSet attrs) { 104 super(context, attrs); 105 initialize(); 106 } 107 PrivateDnsModeDialogPreference(Context context, AttributeSet attrs, int defStyleAttr)108 public PrivateDnsModeDialogPreference(Context context, AttributeSet attrs, int defStyleAttr) { 109 super(context, attrs, defStyleAttr); 110 initialize(); 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 initialize(); 117 } 118 119 private final AnnotationSpan.LinkInfo mUrlLinkInfo = new AnnotationSpan.LinkInfo( 120 ANNOTATION_URL, (widget) -> { 121 final Context context = widget.getContext(); 122 final Intent intent = HelpUtils.getHelpIntent(context, 123 context.getString(R.string.help_uri_private_dns), 124 context.getClass().getName()); 125 if (intent != null) { 126 try { 127 widget.startActivityForResult(intent, 0); 128 } catch (ActivityNotFoundException e) { 129 Log.w(TAG, "Activity was not found for intent, " + intent.toString()); 130 } 131 } 132 }); 133 initialize()134 private void initialize() { 135 // Add the "Restricted" icon resource so that if the preference is disabled by the 136 // admin, an information button will be shown. 137 setWidgetLayoutResource(R.layout.restricted_icon); 138 } 139 140 @Override onBindViewHolder(PreferenceViewHolder holder)141 public void onBindViewHolder(PreferenceViewHolder holder) { 142 super.onBindViewHolder(holder); 143 if (isDisabledByAdmin()) { 144 // If the preference is disabled by the admin, set the inner item as enabled so 145 // it could act as a click target. The preference itself will have been disabled 146 // by the controller. 147 holder.itemView.setEnabled(true); 148 } 149 150 final View restrictedIcon = holder.findViewById(R.id.restricted_icon); 151 if (restrictedIcon != null) { 152 // Show the "Restricted" icon if, and only if, the preference was disabled by 153 // the admin. 154 restrictedIcon.setVisibility(isDisabledByAdmin() ? View.VISIBLE : View.GONE); 155 } 156 } 157 158 @Override onBindDialogView(View view)159 protected void onBindDialogView(View view) { 160 final Context context = getContext(); 161 final ContentResolver contentResolver = context.getContentResolver(); 162 163 mMode = ConnectivitySettingsManager.getPrivateDnsMode(context); 164 165 mEditText = view.findViewById(R.id.private_dns_mode_provider_hostname); 166 mEditText.addTextChangedListener(this); 167 mEditText.setText(getHostnameFromSettings(contentResolver)); 168 169 mRadioGroup = view.findViewById(R.id.private_dns_radio_group); 170 mRadioGroup.setOnCheckedChangeListener(this); 171 mRadioGroup.check(PRIVATE_DNS_MAP.getOrDefault(mMode, R.id.private_dns_mode_opportunistic)); 172 173 // Initial radio button text 174 final RadioButton offRadioButton = view.findViewById(R.id.private_dns_mode_off); 175 offRadioButton.setText(R.string.private_dns_mode_off); 176 final RadioButton opportunisticRadioButton = 177 view.findViewById(R.id.private_dns_mode_opportunistic); 178 opportunisticRadioButton.setText(R.string.private_dns_mode_opportunistic); 179 final RadioButton providerRadioButton = view.findViewById(R.id.private_dns_mode_provider); 180 providerRadioButton.setText(R.string.private_dns_mode_provider); 181 182 final TextView helpTextView = view.findViewById(R.id.private_dns_help_info); 183 helpTextView.setMovementMethod(LinkMovementMethod.getInstance()); 184 final Intent helpIntent = HelpUtils.getHelpIntent(context, 185 context.getString(R.string.help_uri_private_dns), 186 context.getClass().getName()); 187 final AnnotationSpan.LinkInfo linkInfo = new AnnotationSpan.LinkInfo(context, 188 ANNOTATION_URL, helpIntent); 189 if (linkInfo.isActionable()) { 190 helpTextView.setText(AnnotationSpan.linkify( 191 context.getText(R.string.private_dns_help_message), linkInfo)); 192 } 193 } 194 195 @Override onClick(DialogInterface dialog, int which)196 public void onClick(DialogInterface dialog, int which) { 197 if (which == DialogInterface.BUTTON_POSITIVE) { 198 final Context context = getContext(); 199 if (mMode == PRIVATE_DNS_MODE_PROVIDER_HOSTNAME) { 200 // Only clickable if hostname is valid, so we could save it safely 201 ConnectivitySettingsManager.setPrivateDnsHostname(context, 202 mEditText.getText().toString()); 203 } 204 205 FeatureFactory.getFactory(context).getMetricsFeatureProvider().action(context, 206 SettingsEnums.ACTION_PRIVATE_DNS_MODE, mMode); 207 ConnectivitySettingsManager.setPrivateDnsMode(context, mMode); 208 } 209 } 210 211 @Override onCheckedChanged(RadioGroup group, int checkedId)212 public void onCheckedChanged(RadioGroup group, int checkedId) { 213 if (checkedId == R.id.private_dns_mode_off) { 214 mMode = PRIVATE_DNS_MODE_OFF; 215 } else if (checkedId == R.id.private_dns_mode_opportunistic) { 216 mMode = PRIVATE_DNS_MODE_OPPORTUNISTIC; 217 } else if (checkedId == R.id.private_dns_mode_provider) { 218 mMode = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME; 219 } 220 updateDialogInfo(); 221 } 222 223 @Override beforeTextChanged(CharSequence s, int start, int count, int after)224 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 225 } 226 227 @Override onTextChanged(CharSequence s, int start, int before, int count)228 public void onTextChanged(CharSequence s, int start, int before, int count) { 229 } 230 231 @Override afterTextChanged(Editable s)232 public void afterTextChanged(Editable s) { 233 updateDialogInfo(); 234 } 235 236 @Override performClick()237 public void performClick() { 238 EnforcedAdmin enforcedAdmin = getEnforcedAdmin(); 239 240 if (enforcedAdmin == null) { 241 // If the restriction is not restricted by admin, continue as usual. 242 super.performClick(); 243 } else { 244 // Show a dialog explaining to the user why they cannot change the preference. 245 RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(), enforcedAdmin); 246 } 247 } 248 getEnforcedAdmin()249 private EnforcedAdmin getEnforcedAdmin() { 250 return RestrictedLockUtilsInternal.checkIfRestrictionEnforced( 251 getContext(), UserManager.DISALLOW_CONFIG_PRIVATE_DNS, UserHandle.myUserId()); 252 } 253 isDisabledByAdmin()254 private boolean isDisabledByAdmin() { 255 return getEnforcedAdmin() != null; 256 } 257 getSaveButton()258 private Button getSaveButton() { 259 final AlertDialog dialog = (AlertDialog) getDialog(); 260 if (dialog == null) { 261 return null; 262 } 263 return dialog.getButton(DialogInterface.BUTTON_POSITIVE); 264 } 265 updateDialogInfo()266 private void updateDialogInfo() { 267 final boolean modeProvider = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME == mMode; 268 if (mEditText != null) { 269 mEditText.setEnabled(modeProvider); 270 } 271 final Button saveButton = getSaveButton(); 272 if (saveButton != null) { 273 saveButton.setEnabled(modeProvider 274 ? InternetDomainName.isValid(mEditText.getText().toString()) 275 : true); 276 } 277 } 278 } 279