Show the status of private DNS.
This works as follows :
Off → "Off"
Opportunistic, inactive → "Automatic"
Opportunistic, active → "On"
(stealing a string from notifications for this)
Strict, not resolved and/or not validated → "Couldn't connect"
Strict, resolved and validated → Set up hostname
Bug: 73641539
Test: manual, and updated tests pass
Change-Id: Id1132467288d51aa9cb81a04db65dee438ddfad9
diff --git a/src/com/android/settings/network/PrivateDnsPreferenceController.java b/src/com/android/settings/network/PrivateDnsPreferenceController.java
index 50224ca..47aa4dc 100644
--- a/src/com/android/settings/network/PrivateDnsPreferenceController.java
+++ b/src/com/android/settings/network/PrivateDnsPreferenceController.java
@@ -24,6 +24,10 @@
import android.content.ContentResolver;
import android.content.res.Resources;
import android.database.ContentObserver;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.LinkProperties;
+import android.net.Network;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
@@ -31,6 +35,7 @@
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceScreen;
+import com.android.internal.util.ArrayUtils;
import com.android.settings.R;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.PreferenceControllerMixin;
@@ -38,6 +43,8 @@
import com.android.settingslib.core.lifecycle.events.OnStop;
import com.android.settingslib.core.lifecycle.LifecycleObserver;
+import java.net.InetAddress;
+import java.util.List;
public class PrivateDnsPreferenceController extends BasePreferenceController
implements PreferenceControllerMixin, LifecycleObserver, OnStart, OnStop {
@@ -50,12 +57,15 @@
private final Handler mHandler;
private final ContentObserver mSettingsObserver;
+ private final ConnectivityManager mConnectivityManager;
+ private LinkProperties mLatestLinkProperties;
private Preference mPreference;
public PrivateDnsPreferenceController(Context context) {
super(context, KEY_PRIVATE_DNS_SETTINGS);
mHandler = new Handler(Looper.getMainLooper());
mSettingsObserver = new PrivateDnsSettingsObserver(mHandler);
+ mConnectivityManager = context.getSystemService(ConnectivityManager.class);
}
@Override
@@ -80,11 +90,17 @@
for (Uri uri : SETTINGS_URIS) {
mContext.getContentResolver().registerContentObserver(uri, false, mSettingsObserver);
}
+ final Network defaultNetwork = mConnectivityManager.getActiveNetwork();
+ if (defaultNetwork != null) {
+ mLatestLinkProperties = mConnectivityManager.getLinkProperties(defaultNetwork);
+ }
+ mConnectivityManager.registerDefaultNetworkCallback(mNetworkCallback, mHandler);
}
@Override
public void onStop() {
mContext.getContentResolver().unregisterContentObserver(mSettingsObserver);
+ mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
}
@Override
@@ -92,13 +108,23 @@
final Resources res = mContext.getResources();
final ContentResolver cr = mContext.getContentResolver();
final String mode = PrivateDnsModeDialogPreference.getModeFromSettings(cr);
+ final LinkProperties lp = mLatestLinkProperties;
+ final List<InetAddress> dnses = (lp == null) ? null : lp.getValidatedPrivateDnsServers();
+ final boolean dnsesResolved = !ArrayUtils.isEmpty(dnses);
switch (mode) {
case PRIVATE_DNS_MODE_OFF:
return res.getString(R.string.private_dns_mode_off);
case PRIVATE_DNS_MODE_OPPORTUNISTIC:
- return res.getString(R.string.private_dns_mode_opportunistic);
+ // TODO (b/79122154) : create a string specifically for this, instead of
+ // hijacking a string from notifications. This is necessary at this time
+ // because string freeze is in the past and this string has the right
+ // content at this moment.
+ return dnsesResolved ? res.getString(R.string.switch_on_text)
+ : res.getString(R.string.private_dns_mode_opportunistic);
case PRIVATE_DNS_MODE_PROVIDER_HOSTNAME:
- return PrivateDnsModeDialogPreference.getHostnameFromSettings(cr);
+ return dnsesResolved
+ ? PrivateDnsModeDialogPreference.getHostnameFromSettings(cr)
+ : res.getString(R.string.private_dns_mode_provider_failure);
}
return "";
}
@@ -111,8 +137,25 @@
@Override
public void onChange(boolean selfChange) {
if (mPreference != null) {
- PrivateDnsPreferenceController.this.updateState(mPreference);
+ updateState(mPreference);
}
}
}
+
+ private final NetworkCallback mNetworkCallback = new NetworkCallback() {
+ @Override
+ public void onLinkPropertiesChanged(Network network, LinkProperties lp) {
+ mLatestLinkProperties = lp;
+ if (mPreference != null) {
+ updateState(mPreference);
+ }
+ }
+ @Override
+ public void onLost(Network network) {
+ mLatestLinkProperties = null;
+ if (mPreference != null) {
+ updateState(mPreference);
+ }
+ }
+ };
}
diff --git a/tests/robotests/src/com/android/settings/network/PrivateDnsPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/network/PrivateDnsPreferenceControllerTest.java
index 83d4bd5..ce40ab6 100644
--- a/tests/robotests/src/com/android/settings/network/PrivateDnsPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/network/PrivateDnsPreferenceControllerTest.java
@@ -24,17 +24,29 @@
import static android.provider.Settings.Global.PRIVATE_DNS_MODE;
import static android.provider.Settings.Global.PRIVATE_DNS_SPECIFIER;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.nullable;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.CALLS_REAL_METHODS;
import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.withSettings;
import static org.mockito.Mockito.when;
import android.arch.lifecycle.LifecycleOwner;
import android.content.Context;
import android.content.ContentResolver;
import android.database.ContentObserver;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.os.Handler;
import android.provider.Settings;
import android.support.v7.preference.Preference;
import android.support.v7.preference.PreferenceScreen;
@@ -46,22 +58,45 @@
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowContentResolver;
+import org.robolectric.shadows.ShadowServiceManager;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
@RunWith(SettingsRobolectricTestRunner.class)
public class PrivateDnsPreferenceControllerTest {
private final static String HOSTNAME = "dns.example.com";
+ private final static List<InetAddress> NON_EMPTY_ADDRESS_LIST;
+ static {
+ try {
+ NON_EMPTY_ADDRESS_LIST = Arrays.asList(
+ InetAddress.getByAddress(new byte[] { 8, 8, 8, 8 }));
+ } catch (UnknownHostException e) {
+ throw new RuntimeException("Invalid hardcoded IP addresss: " + e);
+ }
+ }
@Mock
private PreferenceScreen mScreen;
@Mock
+ private ConnectivityManager mConnectivityManager;
+ @Mock
+ private Network mNetwork;
+ @Mock
private Preference mPreference;
+ @Captor
+ private ArgumentCaptor<NetworkCallback> mCallbackCaptor;
private PrivateDnsPreferenceController mController;
private Context mContext;
private ContentResolver mContentResolver;
@@ -75,15 +110,41 @@
mContext = spy(RuntimeEnvironment.application);
mContentResolver = mContext.getContentResolver();
mShadowContentResolver = Shadow.extract(mContentResolver);
+ when(mContext.getSystemService(Context.CONNECTIVITY_SERVICE))
+ .thenReturn(mConnectivityManager);
+ doNothing().when(mConnectivityManager).registerDefaultNetworkCallback(
+ mCallbackCaptor.capture(), nullable(Handler.class));
when(mScreen.findPreference(anyString())).thenReturn(mPreference);
mController = spy(new PrivateDnsPreferenceController(mContext));
+
mLifecycleOwner = () -> mLifecycle;
mLifecycle = new Lifecycle(mLifecycleOwner);
mLifecycle.addObserver(mController);
}
+ private void updateLinkProperties(LinkProperties lp) {
+ NetworkCallback nc = mCallbackCaptor.getValue();
+ // The network callback that has been captured by the captor is the `mNetworkCallback'
+ // member of mController. mController being a spy, it has copied that member from the
+ // original object it was spying on, which means the object returned by the captor
+ // has a reference to the original object instead of the mock as its outer instance
+ // and will call methods and modify members of the original object instead of the spy,
+ // so methods subsequently called on the spy will not be aware of the changes. To work
+ // around this, the following code will create a new instance of the same class with
+ // the same code, but it sets the spy as the outer instance.
+ // A more recent version of Mockito would have made possible to create the spy with
+ // spy(PrivateDnsPreferenceController.class, withSettings().useConstructor(mContext))
+ // and that would have solved the problem by removing the original object entirely
+ // in a more elegant manner, but useConstructor(Object...) is only available starting
+ // with Mockito 2.7.14. Other solutions involve modifying the code under test for
+ // the sake of the test.
+ nc = mock(nc.getClass(), withSettings().useConstructor().outerInstance(mController)
+ .defaultAnswer(CALLS_REAL_METHODS));
+ nc.onLinkPropertiesChanged(mNetwork, lp);
+ }
+
@Test
public void goThroughLifecycle_shouldRegisterUnregisterSettingsObserver() {
mLifecycle.handleLifecycleEvent(ON_START);
@@ -113,20 +174,50 @@
@Test
public void getSummary_PrivateDnsModeOpportunistic() {
+ mLifecycle.handleLifecycleEvent(ON_START);
setPrivateDnsMode(PRIVATE_DNS_MODE_OPPORTUNISTIC);
setPrivateDnsProviderHostname(HOSTNAME);
mController.updateState(mPreference);
verify(mController, atLeastOnce()).getSummary();
verify(mPreference).setSummary(getResourceString(R.string.private_dns_mode_opportunistic));
+
+ LinkProperties lp = mock(LinkProperties.class);
+ when(lp.getValidatedPrivateDnsServers()).thenReturn(NON_EMPTY_ADDRESS_LIST);
+ updateLinkProperties(lp);
+ mController.updateState(mPreference);
+ verify(mPreference).setSummary(getResourceString(R.string.switch_on_text));
+
+ reset(mPreference);
+ lp = mock(LinkProperties.class);
+ when(lp.getValidatedPrivateDnsServers()).thenReturn(Collections.emptyList());
+ updateLinkProperties(lp);
+ mController.updateState(mPreference);
+ verify(mPreference).setSummary(getResourceString(R.string.private_dns_mode_opportunistic));
}
@Test
public void getSummary_PrivateDnsModeProviderHostname() {
+ mLifecycle.handleLifecycleEvent(ON_START);
setPrivateDnsMode(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME);
setPrivateDnsProviderHostname(HOSTNAME);
mController.updateState(mPreference);
verify(mController, atLeastOnce()).getSummary();
+ verify(mPreference).setSummary(
+ getResourceString(R.string.private_dns_mode_provider_failure));
+
+ LinkProperties lp = mock(LinkProperties.class);
+ when(lp.getValidatedPrivateDnsServers()).thenReturn(NON_EMPTY_ADDRESS_LIST);
+ updateLinkProperties(lp);
+ mController.updateState(mPreference);
verify(mPreference).setSummary(HOSTNAME);
+
+ reset(mPreference);
+ lp = mock(LinkProperties.class);
+ when(lp.getValidatedPrivateDnsServers()).thenReturn(Collections.emptyList());
+ updateLinkProperties(lp);
+ mController.updateState(mPreference);
+ verify(mPreference).setSummary(
+ getResourceString(R.string.private_dns_mode_provider_failure));
}
private void setPrivateDnsMode(String mode) {