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