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