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