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