diff --git a/res/layout/private_dns_mode_dialog.xml b/res/layout/private_dns_mode_dialog.xml
index 43470559817..16152a4e122 100644
--- a/res/layout/private_dns_mode_dialog.xml
+++ b/res/layout/private_dns_mode_dialog.xml
@@ -16,6 +16,7 @@
@@ -25,24 +26,21 @@
android:text="@string/private_dns_mode_off"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_margin="8dip"
- />
+ android:layout_margin="8dip"/>
+ android:layout_margin="8dip"/>
+ android:layout_margin="8dip"/>
+ android:layout_marginStart="40dip"
+ android:layout_marginEnd="8dip"/>
diff --git a/src/com/android/settings/network/NetworkDashboardFragment.java b/src/com/android/settings/network/NetworkDashboardFragment.java
index e982a289e9d..2a04d274820 100644
--- a/src/com/android/settings/network/NetworkDashboardFragment.java
+++ b/src/com/android/settings/network/NetworkDashboardFragment.java
@@ -48,8 +48,11 @@ public class NetworkDashboardFragment extends DashboardFragment implements
MobilePlanPreferenceHost {
private static final String TAG = "NetworkDashboardFrag";
+ private static final int MENU_NETWORK_RESET = Menu.FIRST;
+ private static final int MENU_PRIVATE_DNS = Menu.FIRST + 1;
private NetworkResetActionMenuController mNetworkResetController;
+ private PrivateDnsMenuController mPrivateDnsMenuController;
@Override
public int getMetricsCategory() {
@@ -69,7 +72,9 @@ public class NetworkDashboardFragment extends DashboardFragment implements
@Override
public void onAttach(Context context) {
super.onAttach(context);
- mNetworkResetController = new NetworkResetActionMenuController(context);
+ mNetworkResetController = new NetworkResetActionMenuController(context, MENU_NETWORK_RESET);
+ mPrivateDnsMenuController = new PrivateDnsMenuController(getFragmentManager(),
+ MENU_PRIVATE_DNS);
}
@Override
@@ -81,6 +86,7 @@ public class NetworkDashboardFragment extends DashboardFragment implements
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
super.onCreateOptionsMenu(menu, inflater);
mNetworkResetController.buildMenuItem(menu);
+ mPrivateDnsMenuController.buildMenuItem(menu);
}
@Override
diff --git a/src/com/android/settings/network/NetworkResetActionMenuController.java b/src/com/android/settings/network/NetworkResetActionMenuController.java
index 254834b9808..75db65eccca 100644
--- a/src/com/android/settings/network/NetworkResetActionMenuController.java
+++ b/src/com/android/settings/network/NetworkResetActionMenuController.java
@@ -27,19 +27,20 @@ import com.android.settings.Utils;
public class NetworkResetActionMenuController {
- private static final int MENU_NETWORK_RESET = Menu.FIRST + 200;
private final Context mContext;
private final NetworkResetRestrictionChecker mRestrictionChecker;
+ private final int mMenuId;
- public NetworkResetActionMenuController(Context context) {
+ public NetworkResetActionMenuController(Context context, int menuId) {
mContext = context;
mRestrictionChecker = new NetworkResetRestrictionChecker(context);
+ mMenuId = menuId;
}
public void buildMenuItem(Menu menu) {
MenuItem item = null;
if (isAvailable() && menu != null) {
- item = menu.add(0, MENU_NETWORK_RESET, 0, R.string.reset_network_title);
+ item = menu.add(0, mMenuId, 0, R.string.reset_network_title);
}
if (item != null) {
item.setOnMenuItemClickListener(target -> {
diff --git a/src/com/android/settings/network/PrivateDnsMenuController.java b/src/com/android/settings/network/PrivateDnsMenuController.java
new file mode 100644
index 00000000000..a8117755565
--- /dev/null
+++ b/src/com/android/settings/network/PrivateDnsMenuController.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.network;
+
+import android.app.FragmentManager;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import com.android.settings.R;
+
+public class PrivateDnsMenuController {
+ private final FragmentManager mFragmentManager;
+ private final int mMenuId;
+
+ public PrivateDnsMenuController(FragmentManager fragmentManager, int menuId) {
+ mFragmentManager = fragmentManager;
+ mMenuId = menuId;
+ }
+
+ public void buildMenuItem(Menu menu) {
+ if (menu != null) {
+ MenuItem item = menu.add(0 /* groupId */, mMenuId, 0 /* order */,
+ R.string.select_private_dns_configuration_title);
+ item.setOnMenuItemClickListener(target -> {
+ PrivateDnsModeDialogFragment.show(mFragmentManager);
+ return true;
+ });
+ }
+ }
+}
diff --git a/src/com/android/settings/network/PrivateDnsModeDialogFragment.java b/src/com/android/settings/network/PrivateDnsModeDialogFragment.java
new file mode 100644
index 00000000000..cb3079e6798
--- /dev/null
+++ b/src/com/android/settings/network/PrivateDnsModeDialogFragment.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.settings.network;
+
+import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OFF;
+import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
+import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.FragmentManager;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.support.annotation.VisibleForTesting;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.RadioGroup;
+
+import com.android.settings.R;
+import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Dialog to set the private dns
+ */
+public class PrivateDnsModeDialogFragment extends InstrumentedDialogFragment implements
+ DialogInterface.OnClickListener, RadioGroup.OnCheckedChangeListener, TextWatcher {
+
+ private static final String TAG = "PrivateDnsModeDialogFragment";
+ // DNS_MODE -> RadioButton id
+ private static final Map PRIVATE_DNS_MAP;
+
+ static {
+ PRIVATE_DNS_MAP = new HashMap<>();
+ 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);
+ PRIVATE_DNS_MAP.put(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME, R.id.private_dns_mode_provider);
+ }
+
+ @VisibleForTesting
+ static final String MODE_KEY = Settings.Global.PRIVATE_DNS_MODE;
+ @VisibleForTesting
+ static final String HOSTNAME_KEY = Settings.Global.PRIVATE_DNS_SPECIFIER;
+
+ @VisibleForTesting
+ EditText mEditText;
+ @VisibleForTesting
+ RadioGroup mRadioGroup;
+ @VisibleForTesting
+ String mMode;
+
+ public static void show(FragmentManager fragmentManager) {
+ if (fragmentManager.findFragmentByTag(TAG) == null) {
+ final PrivateDnsModeDialogFragment fragment = new PrivateDnsModeDialogFragment();
+ fragment.show(fragmentManager, TAG);
+ }
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ final Context context = getContext();
+
+ return new AlertDialog.Builder(context)
+ .setTitle(R.string.select_private_dns_configuration_title)
+ .setView(buildPrivateDnsView(context))
+ .setPositiveButton(R.string.save, this)
+ .setNegativeButton(R.string.dlg_cancel, null)
+ .create();
+ }
+
+ private View buildPrivateDnsView(final Context context) {
+ final ContentResolver contentResolver = context.getContentResolver();
+ final String mode = Settings.Global.getString(contentResolver, MODE_KEY);
+ final View view = LayoutInflater.from(context).inflate(R.layout.private_dns_mode_dialog,
+ null);
+
+ mEditText = view.findViewById(R.id.private_dns_mode_provider_hostname);
+ mEditText.addTextChangedListener(this);
+ mEditText.setText(Settings.Global.getString(contentResolver, HOSTNAME_KEY));
+
+ mRadioGroup = view.findViewById(R.id.private_dns_radio_group);
+ mRadioGroup.setOnCheckedChangeListener(this);
+ mRadioGroup.check(PRIVATE_DNS_MAP.getOrDefault(mode, R.id.private_dns_mode_opportunistic));
+
+ return view;
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ //TODO(b/34953048): add metric action
+ if (mMode.equals(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME)) {
+ // Only clickable if hostname is valid, so we could save it safely
+ Settings.Global.putString(getContext().getContentResolver(), HOSTNAME_KEY,
+ mEditText.getText().toString());
+ }
+
+ Settings.Global.putString(getContext().getContentResolver(), MODE_KEY, mMode);
+ }
+
+ @Override
+ public int getMetricsCategory() {
+ //TODO(b/68030013): add metric id
+ return 0;
+ }
+
+ @Override
+ public void onCheckedChanged(RadioGroup group, int checkedId) {
+ switch (checkedId) {
+ case R.id.private_dns_mode_off:
+ mMode = PRIVATE_DNS_MODE_OFF;
+ mEditText.setEnabled(false);
+ break;
+ case R.id.private_dns_mode_opportunistic:
+ mMode = PRIVATE_DNS_MODE_OPPORTUNISTIC;
+ mEditText.setEnabled(false);
+ break;
+ case R.id.private_dns_mode_provider:
+ mMode = PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
+ mEditText.setEnabled(true);
+ break;
+ }
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ // TODO(b/68030013): Disable the "positive button" ("Save") when appearsValid is false.
+ final boolean valid = isWeaklyValidatedHostname(s.toString());
+ }
+
+ private boolean isWeaklyValidatedHostname(String hostname) {
+ // TODO(b/34953048): Find and use a better validation method. Specifically:
+ // [1] this should reject IP string literals, and
+ // [2] do the best, simplest, future-proof verification that
+ // the input approximates a DNS hostname.
+ final String WEAK_HOSTNAME_REGEX = "^[a-zA-Z0-9_.-]+$";
+ return hostname.matches(WEAK_HOSTNAME_REGEX);
+ }
+
+}
diff --git a/tests/robotests/src/com/android/settings/network/NetworkResetActionMenuControllerTest.java b/tests/robotests/src/com/android/settings/network/NetworkResetActionMenuControllerTest.java
index 0b2be3a2ad4..896e802a14d 100644
--- a/tests/robotests/src/com/android/settings/network/NetworkResetActionMenuControllerTest.java
+++ b/tests/robotests/src/com/android/settings/network/NetworkResetActionMenuControllerTest.java
@@ -43,6 +43,7 @@ import org.robolectric.util.ReflectionHelpers;
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
public class NetworkResetActionMenuControllerTest {
+ private static final int MENU_ID = Menu.FIRST;
private Context mContext;
private NetworkResetActionMenuController mController;
@Mock
@@ -56,7 +57,7 @@ public class NetworkResetActionMenuControllerTest {
public void setUp() {
MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application;
- mController = new NetworkResetActionMenuController(mContext);
+ mController = new NetworkResetActionMenuController(mContext, MENU_ID);
ReflectionHelpers.setField(mController, "mRestrictionChecker", mRestrictionChecker);
when(mMenu.add(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(mMenuItem);
}
diff --git a/tests/robotests/src/com/android/settings/network/PrivateDnsMenuControllerTest.java b/tests/robotests/src/com/android/settings/network/PrivateDnsMenuControllerTest.java
new file mode 100644
index 00000000000..0be66e05104
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/network/PrivateDnsMenuControllerTest.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.network;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.FragmentManager;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import com.android.settings.R;
+import com.android.settings.TestConfig;
+import com.android.settings.testutils.SettingsRobolectricTestRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.annotation.Config;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class PrivateDnsMenuControllerTest {
+ private static final int MENU_ID = 0;
+
+ private PrivateDnsMenuController mController;
+ @Mock
+ private Menu mMenu;
+ @Mock
+ private MenuItem mMenuItem;
+ @Mock
+ private FragmentManager mFragmentManager;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mController = new PrivateDnsMenuController(mFragmentManager, MENU_ID);
+ when(mMenu.add(anyInt(), anyInt(), anyInt(), anyInt())).thenReturn(mMenuItem);
+ }
+
+ @Test
+ public void buildMenuItem_available_shouldAddToMenu() {
+ mController.buildMenuItem(mMenu);
+
+ verify(mMenu).add(0 /* groupId */, MENU_ID, 0 /* order */,
+ R.string.select_private_dns_configuration_title);
+ verify(mMenuItem).setOnMenuItemClickListener(any(MenuItem.OnMenuItemClickListener.class));
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/network/PrivateDnsModeDialogFragmentTest.java b/tests/robotests/src/com/android/settings/network/PrivateDnsModeDialogFragmentTest.java
new file mode 100644
index 00000000000..3468702f76e
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/network/PrivateDnsModeDialogFragmentTest.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.network;
+
+import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OFF;
+import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
+import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+
+import android.content.Context;
+import android.provider.Settings;
+
+import com.android.settings.R;
+import com.android.settings.TestConfig;
+import com.android.settings.testutils.SettingsRobolectricTestRunner;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.RuntimeEnvironment;
+import org.robolectric.annotation.Config;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+public class PrivateDnsModeDialogFragmentTest {
+ private static final String HOST_NAME = "192.168.1.1";
+
+ private Context mContext;
+ private PrivateDnsModeDialogFragment mFragment;
+
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mContext = RuntimeEnvironment.application;
+ mFragment = spy(new PrivateDnsModeDialogFragment());
+ doReturn(mContext).when(mFragment).getContext();
+ mFragment.onCreateDialog(null);
+ }
+
+ @Test
+ public void testOnCheckedChanged_dnsModeOff_disableEditText() {
+ mFragment.onCheckedChanged(null, R.id.private_dns_mode_off);
+
+ assertThat(mFragment.mMode).isEqualTo(PRIVATE_DNS_MODE_OFF);
+ assertThat(mFragment.mEditText.isEnabled()).isFalse();
+ }
+
+ @Test
+ public void testOnCheckedChanged_dnsModeOpportunistic_disableEditText() {
+ mFragment.onCheckedChanged(null, R.id.private_dns_mode_opportunistic);
+
+ assertThat(mFragment.mMode).isEqualTo(PRIVATE_DNS_MODE_OPPORTUNISTIC);
+ assertThat(mFragment.mEditText.isEnabled()).isFalse();
+ }
+
+ @Test
+ public void testOnCheckedChanged_dnsModeProvider_enableEditText() {
+ mFragment.onCheckedChanged(null, R.id.private_dns_mode_provider);
+
+ assertThat(mFragment.mMode).isEqualTo(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME);
+ assertThat(mFragment.mEditText.isEnabled()).isTrue();
+ }
+
+ @Test
+ public void testOnCreateDialog_containsCorrectData() {
+ Settings.Global.putString(mContext.getContentResolver(),
+ PrivateDnsModeDialogFragment.MODE_KEY, PRIVATE_DNS_MODE_OPPORTUNISTIC);
+ Settings.Global.putString(mContext.getContentResolver(),
+ PrivateDnsModeDialogFragment.HOSTNAME_KEY, HOST_NAME);
+
+ mFragment.onCreateDialog(null);
+
+ assertThat(mFragment.mEditText.getText().toString()).isEqualTo(HOST_NAME);
+ assertThat(mFragment.mRadioGroup.getCheckedRadioButtonId()).isEqualTo(
+ R.id.private_dns_mode_opportunistic);
+ }
+
+}