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