Add private dns dialog in wifi settings

This option lives in menu. A dialog will show up once this menu
is clicked.

We reuse most of the logic in go/aog/524415 with the following
updates:
1. Use radioGroup instead of directly control radioButton
2. remove bunch of methods if they are only used once.

Following cl will:
1. Remove this feature in development page
2. Add the help link at the bottom of the dialog
3. Disable "Save" button when hostname is invalid.

Bug: 68030013
Test: add for later

Change-Id: I4c6d359dc9c55675858c20e47953ef677b31c3b5
This commit is contained in:
jackqdyulei
2017-11-08 13:29:23 -08:00
parent d0965a21e4
commit 05cb931a31
8 changed files with 398 additions and 14 deletions

View File

@@ -16,6 +16,7 @@
<RadioGroup
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/private_dns_radio_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dip">
@@ -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"/>
<RadioButton
android:id="@+id/private_dns_mode_opportunistic"
android:text="@string/private_dns_mode_opportunistic"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dip"
/>
android:layout_margin="8dip"/>
<RadioButton
android:id="@+id/private_dns_mode_provider"
android:text="@string/private_dns_mode_provider"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dip"
/>
android:layout_margin="8dip"/>
<EditText
android:id="@+id/private_dns_mode_provider_hostname"
@@ -52,8 +50,7 @@
android:inputType="textFilter|textUri"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dip"
android:layout_marginEnd="8dip"
/>
android:layout_marginStart="40dip"
android:layout_marginEnd="8dip"/>
</RadioGroup>

View File

@@ -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

View File

@@ -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 -> {

View File

@@ -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;
});
}
}
}

View File

@@ -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<String, Integer> 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);
}
}

View File

@@ -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);
}

View File

@@ -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));
}
}

View File

@@ -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);
}
}