diff --git a/src/com/android/settings/network/PrivateDnsPreferenceController.java b/src/com/android/settings/network/PrivateDnsPreferenceController.java index 50224caba1a..47aa4dcffe4 100644 --- a/src/com/android/settings/network/PrivateDnsPreferenceController.java +++ b/src/com/android/settings/network/PrivateDnsPreferenceController.java @@ -24,6 +24,10 @@ import android.content.Context; 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.provider.Settings; 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.OnStart; 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 @@ public class PrivateDnsPreferenceController extends BasePreferenceController 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 @@ public class PrivateDnsPreferenceController extends BasePreferenceController 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 @@ public class PrivateDnsPreferenceController extends BasePreferenceController final Resources res = mContext.getResources(); final ContentResolver cr = mContext.getContentResolver(); final String mode = PrivateDnsModeDialogPreference.getModeFromSettings(cr); + final LinkProperties lp = mLatestLinkProperties; + final List 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 @@ public class PrivateDnsPreferenceController extends BasePreferenceController @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 83d4bd530b7..ce40ab69364 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.net.ConnectivityManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME 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 com.android.settingslib.core.lifecycle.Lifecycle; 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 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 mCallbackCaptor; private PrivateDnsPreferenceController mController; private Context mContext; private ContentResolver mContentResolver; @@ -75,15 +110,41 @@ public class PrivateDnsPreferenceControllerTest { 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 @@ public class PrivateDnsPreferenceControllerTest { @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) {