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.Context; 27 import android.content.DialogInterface; 28 import android.content.Intent; 29 import android.net.ConnectivitySettingsManager; 30 import android.os.UserHandle; 31 import android.os.UserManager; 32 import android.text.Editable; 33 import android.text.TextWatcher; 34 import android.text.method.LinkMovementMethod; 35 import android.util.AttributeSet; 36 import android.util.Log; 37 import android.view.View; 38 import android.widget.Button; 39 import android.widget.EditText; 40 import android.widget.RadioButton; 41 import android.widget.RadioGroup; 42 import android.widget.TextView; 43 44 import androidx.annotation.VisibleForTesting; 45 import androidx.appcompat.app.AlertDialog; 46 import androidx.preference.PreferenceViewHolder; 47 48 import com.android.settings.R; 49 import com.android.settings.overlay.FeatureFactory; 50 import com.android.settings.utils.AnnotationSpan; 51 import com.android.settingslib.CustomDialogPreferenceCompat; 52 import com.android.settingslib.HelpUtils; 53 import com.android.settingslib.RestrictedLockUtils; 54 import com.android.settingslib.RestrictedLockUtilsInternal; 55 56 import com.google.android.material.textfield.TextInputLayout; 57 import com.google.common.net.InternetDomainName; 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 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<Integer, 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 TextInputLayout mHostnameLayout; 83 @VisibleForTesting 84 EditText mHostnameText; 85 @VisibleForTesting 86 RadioGroup mRadioGroup; 87 @VisibleForTesting 88 int mMode; 89 PrivateDnsModeDialogPreference(Context context)90 public PrivateDnsModeDialogPreference(Context context) { 91 super(context); 92 } 93 PrivateDnsModeDialogPreference(Context context, AttributeSet attrs)94 public PrivateDnsModeDialogPreference(Context context, AttributeSet attrs) { 95 super(context, attrs); 96 } 97 PrivateDnsModeDialogPreference(Context context, AttributeSet attrs, int defStyleAttr)98 public PrivateDnsModeDialogPreference(Context context, AttributeSet attrs, int defStyleAttr) { 99 super(context, attrs, defStyleAttr); 100 } 101 PrivateDnsModeDialogPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)102 public PrivateDnsModeDialogPreference(Context context, AttributeSet attrs, int defStyleAttr, 103 int defStyleRes) { 104 super(context, attrs, defStyleAttr, defStyleRes); 105 } 106 107 private final AnnotationSpan.LinkInfo mUrlLinkInfo = new AnnotationSpan.LinkInfo( 108 ANNOTATION_URL, (widget) -> { 109 final Context context = widget.getContext(); 110 final Intent intent = HelpUtils.getHelpIntent(context, 111 context.getString(R.string.help_uri_private_dns), 112 context.getClass().getName()); 113 if (intent != null) { 114 try { 115 widget.startActivityForResult(intent, 0); 116 } catch (ActivityNotFoundException e) { 117 Log.w(TAG, "Activity was not found for intent, " + intent.toString()); 118 } 119 } 120 }); 121 122 @Override onBindViewHolder(PreferenceViewHolder holder)123 public void onBindViewHolder(PreferenceViewHolder holder) { 124 super.onBindViewHolder(holder); 125 if (isDisabledByAdmin()) { 126 // If the preference is disabled by the admin, set the inner item as enabled so 127 // it could act as a click target. The preference itself will have been disabled 128 // by the controller. 129 holder.itemView.setEnabled(true); 130 } 131 132 setSaveButtonListener(); 133 } 134 135 @Override onBindDialogView(View view)136 protected void onBindDialogView(View view) { 137 final Context context = getContext(); 138 mMode = ConnectivitySettingsManager.getPrivateDnsMode(context); 139 mRadioGroup = view.findViewById(R.id.private_dns_radio_group); 140 mRadioGroup.check(PRIVATE_DNS_MAP.getOrDefault(mMode, R.id.private_dns_mode_opportunistic)); 141 mRadioGroup.setOnCheckedChangeListener(this); 142 143 // Initial radio button text 144 final RadioButton offRadioButton = view.findViewById(R.id.private_dns_mode_off); 145 offRadioButton.setText(com.android.settingslib.R.string.private_dns_mode_off); 146 final RadioButton opportunisticRadioButton = 147 view.findViewById(R.id.private_dns_mode_opportunistic); 148 opportunisticRadioButton.setText( 149 com.android.settingslib.R.string.private_dns_mode_opportunistic); 150 final RadioButton providerRadioButton = view.findViewById(R.id.private_dns_mode_provider); 151 providerRadioButton.setText(com.android.settingslib.R.string.private_dns_mode_provider); 152 153 mHostnameLayout = view.findViewById(R.id.private_dns_mode_provider_hostname_layout); 154 mHostnameText = view.findViewById(R.id.private_dns_mode_provider_hostname); 155 if (mHostnameText != null) { 156 mHostnameText.setText(ConnectivitySettingsManager.getPrivateDnsHostname(context)); 157 mHostnameText.addTextChangedListener(this); 158 } 159 160 final TextView helpTextView = view.findViewById(R.id.private_dns_help_info); 161 helpTextView.setMovementMethod(LinkMovementMethod.getInstance()); 162 final Intent helpIntent = HelpUtils.getHelpIntent(context, 163 context.getString(R.string.help_uri_private_dns), 164 context.getClass().getName()); 165 final AnnotationSpan.LinkInfo linkInfo = new AnnotationSpan.LinkInfo(context, 166 ANNOTATION_URL, helpIntent); 167 if (linkInfo.isActionable()) { 168 helpTextView.setText(AnnotationSpan.linkify( 169 context.getText(R.string.private_dns_help_message), linkInfo)); 170 } else { 171 helpTextView.setText(""); 172 } 173 174 updateDialogInfo(); 175 } 176 177 @Override onCheckedChanged(RadioGroup group, int checkedId)178 public void onCheckedChanged(RadioGroup group, int checkedId) { 179 if (checkedId == R.id.private_dns_mode_off) { 180 mMode = PRIVATE_DNS_MODE_OFF; 181 } else if (checkedId == R.id.private_dns_mode_opportunistic) { 182 mMode = PRIVATE_DNS_MODE_OPPORTUNISTIC; 183 } else if (checkedId == R.id.private_dns_mode_provider) { 184 mMode = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME; 185 } 186 updateDialogInfo(); 187 } 188 189 @Override beforeTextChanged(CharSequence s, int start, int count, int after)190 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 191 } 192 193 @Override onTextChanged(CharSequence s, int start, int before, int count)194 public void onTextChanged(CharSequence s, int start, int before, int count) { 195 } 196 197 @Override afterTextChanged(Editable s)198 public void afterTextChanged(Editable s) { 199 updateDialogInfo(); 200 } 201 202 @Override performClick()203 public void performClick() { 204 EnforcedAdmin enforcedAdmin = getEnforcedAdmin(); 205 206 if (enforcedAdmin == null) { 207 // If the restriction is not restricted by admin, continue as usual. 208 super.performClick(); 209 } else { 210 // Show a dialog explaining to the user why they cannot change the preference. 211 RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(), enforcedAdmin); 212 } 213 } 214 getEnforcedAdmin()215 private EnforcedAdmin getEnforcedAdmin() { 216 return RestrictedLockUtilsInternal.checkIfRestrictionEnforced( 217 getContext(), UserManager.DISALLOW_CONFIG_PRIVATE_DNS, UserHandle.myUserId()); 218 } 219 isDisabledByAdmin()220 private boolean isDisabledByAdmin() { 221 return getEnforcedAdmin() != null; 222 } 223 updateDialogInfo()224 private void updateDialogInfo() { 225 final boolean modeProvider = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME == mMode; 226 if (mHostnameLayout != null) { 227 mHostnameLayout.setEnabled(modeProvider); 228 mHostnameLayout.setErrorEnabled(false); 229 } 230 } 231 setSaveButtonListener()232 private void setSaveButtonListener() { 233 View.OnClickListener onClickListener = v -> doSaveButton(); 234 DialogInterface.OnShowListener onShowListener = dialog -> { 235 if (dialog == null) { 236 Log.e(TAG, "The DialogInterface is null!"); 237 return; 238 } 239 Button saveButton = ((AlertDialog) dialog).getButton(DialogInterface.BUTTON_POSITIVE); 240 if (saveButton == null) { 241 Log.e(TAG, "Can't get the save button!"); 242 return; 243 } 244 saveButton.setOnClickListener(onClickListener); 245 }; 246 setOnShowListener(onShowListener); 247 } 248 249 @VisibleForTesting doSaveButton()250 void doSaveButton() { 251 Context context = getContext(); 252 if (mMode == PRIVATE_DNS_MODE_PROVIDER_HOSTNAME) { 253 if (mHostnameLayout == null || mHostnameText == null) { 254 Log.e(TAG, "Can't find hostname resources!"); 255 return; 256 } 257 if (mHostnameText.getText().isEmpty()) { 258 mHostnameLayout.setError(context.getString(R.string.private_dns_field_require)); 259 Log.w(TAG, "The hostname is empty!"); 260 return; 261 } 262 if (!InternetDomainName.isValid(mHostnameText.getText().toString())) { 263 mHostnameLayout.setError(context.getString(R.string.private_dns_hostname_invalid)); 264 Log.w(TAG, "The hostname is invalid!"); 265 return; 266 } 267 268 ConnectivitySettingsManager.setPrivateDnsHostname(context, 269 mHostnameText.getText().toString()); 270 } 271 272 ConnectivitySettingsManager.setPrivateDnsMode(context, mMode); 273 274 FeatureFactory.getFeatureFactory().getMetricsFeatureProvider() 275 .action(context, SettingsEnums.ACTION_PRIVATE_DNS_MODE, mMode); 276 getDialog().dismiss(); 277 } 278 } 279