Reduce jank in the wifi detail status page.

Currently, when anything changes, the wifi detail status page
removes and then redraws all IP address information. This causes
the whole screen to flicker. Instead, only add and remove things
when they actually change.

In order to do this, convert the IPv6 addresses from a list of
Preference objects to a single newline-separated text field.
This removes the need to keep track of addresses as they are
added and deleted, and also looks a bit better.

Also, minor correctness fixes:
- Get the gateway from the default route, not from the last route
  with a non-null gateway.
- Get the IPv4 subnet mask from the IPv4 address prefix, not from
  the last route with prefix length > 0.

Bug: 62171690
Test: make -j64 RunSettingsRoboTests
Test: IP information does not flicker when signal strength changes
Change-Id: Ia9f2a277e53a2800407ae327701c5b95a9eec20a
diff --git a/res/xml/wifi_network_details_fragment.xml b/res/xml/wifi_network_details_fragment.xml
index 5d5958d..257533f 100644
--- a/res/xml/wifi_network_details_fragment.xml
+++ b/res/xml/wifi_network_details_fragment.xml
@@ -82,8 +82,12 @@
 
     <!-- IPv6 Details -->
     <PreferenceCategory
-            android:key="ipv6_details_category"
+            android:key="ipv6_category"
             android:title="@string/wifi_details_ipv6_address_header"
-            android:selectable="false"/>
+            android:selectable="false">
+        <Preference
+                android:key="ipv6_addresses"
+                android:selectable="false"/>
+    </PreferenceCategory>
 
 </PreferenceScreen>
diff --git a/src/com/android/settings/wifi/WifiDetailPreference.java b/src/com/android/settings/wifi/WifiDetailPreference.java
index 6d34ad1..b62df56 100644
--- a/src/com/android/settings/wifi/WifiDetailPreference.java
+++ b/src/com/android/settings/wifi/WifiDetailPreference.java
@@ -19,6 +19,7 @@
 import android.content.Context;
 import android.support.v7.preference.Preference;
 import android.support.v7.preference.PreferenceViewHolder;
+import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.widget.TextView;
 
@@ -37,6 +38,7 @@
     }
 
     public void setDetailText(String text) {
+        if (TextUtils.equals(mDetailText, text)) return;
         mDetailText = text;
         notifyChanged();
     }
diff --git a/src/com/android/settings/wifi/details/WifiDetailPreferenceController.java b/src/com/android/settings/wifi/details/WifiDetailPreferenceController.java
index a5d922d..b22d7027 100644
--- a/src/com/android/settings/wifi/details/WifiDetailPreferenceController.java
+++ b/src/com/android/settings/wifi/details/WifiDetailPreferenceController.java
@@ -27,6 +27,7 @@
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
 import android.net.IpPrefix;
+import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkBadging;
@@ -67,6 +68,7 @@
 import java.net.UnknownHostException;
 import java.util.List;
 import java.util.StringJoiner;
+import java.util.stream.Collectors;
 
 /**
  * Controller for logic pertaining to displaying Wifi information for the
@@ -100,7 +102,9 @@
     @VisibleForTesting
     static final String KEY_DNS_PREF = "dns";
     @VisibleForTesting
-    static final String KEY_IPV6_ADDRESS_CATEGORY = "ipv6_details_category";
+    static final String KEY_IPV6_CATEGORY = "ipv6_category";
+    @VisibleForTesting
+    static final String KEY_IPV6_ADDRESSES_PREF = "ipv6_addresses";
 
     private AccessPoint mAccessPoint;
     private final ConnectivityManagerWrapper mConnectivityManagerWrapper;
@@ -133,8 +137,9 @@
     private WifiDetailPreference mGatewayPref;
     private WifiDetailPreference mSubnetPref;
     private WifiDetailPreference mDnsPref;
+    private PreferenceCategory mIpv6Category;
+    private Preference mIpv6AddressPref;
 
-    private PreferenceCategory mIpv6AddressCategory;
     private final IntentFilter mFilter;
     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
         @Override
@@ -241,8 +246,8 @@
         mSubnetPref = (WifiDetailPreference) screen.findPreference(KEY_SUBNET_MASK_PREF);
         mDnsPref = (WifiDetailPreference) screen.findPreference(KEY_DNS_PREF);
 
-        mIpv6AddressCategory =
-                (PreferenceCategory) screen.findPreference(KEY_IPV6_ADDRESS_CATEGORY);
+        mIpv6Category = (PreferenceCategory) screen.findPreference(KEY_IPV6_CATEGORY);
+        mIpv6AddressPref = (Preference) screen.findPreference(KEY_IPV6_ADDRESSES_PREF);
 
         mSecurityPref.setDetailText(mAccessPoint.getSecurityString(false /* concise */));
         mForgetButton = (Button) mButtonsPref.findViewById(R.id.left_button);
@@ -315,8 +320,6 @@
         mFrequencyPref.setDetailText(band);
 
         updateIpLayerInfo();
-        mButtonsPref.setVisible(mForgetButton.getVisibility() == View.VISIBLE
-                || mSignInButton.getVisibility() == View.VISIBLE);
     }
 
     private void exitActivity() {
@@ -348,74 +351,69 @@
         mSignalStrengthPref.setDetailText(mSignalStr[summarySignalLevel]);
     }
 
+    private void updatePreference(WifiDetailPreference pref, String detailText) {
+        if (!TextUtils.isEmpty(detailText)) {
+            pref.setDetailText(detailText);
+            pref.setVisible(true);
+        } else {
+            pref.setVisible(false);
+        }
+    }
+
     private void updateIpLayerInfo() {
         mSignInButton.setVisibility(canSignIntoNetwork() ? View.VISIBLE : View.INVISIBLE);
-
-        // Reset all fields
-        mIpv6AddressCategory.removeAll();
-        mIpv6AddressCategory.setVisible(false);
-        mIpAddressPref.setVisible(false);
-        mSubnetPref.setVisible(false);
-        mGatewayPref.setVisible(false);
-        mDnsPref.setVisible(false);
+        mButtonsPref.setVisible(mForgetButton.getVisibility() == View.VISIBLE
+                || mSignInButton.getVisibility() == View.VISIBLE);
 
         if (mNetwork == null || mLinkProperties == null) {
+            mIpAddressPref.setVisible(false);
+            mSubnetPref.setVisible(false);
+            mGatewayPref.setVisible(false);
+            mDnsPref.setVisible(false);
+            mIpv6Category.setVisible(false);
             return;
         }
-        List<InetAddress> addresses = mLinkProperties.getAddresses();
 
-        // Set IPv4 and IPv6 addresses
-        for (int i = 0; i < addresses.size(); i++) {
-            InetAddress addr = addresses.get(i);
-            if (addr instanceof Inet4Address) {
-                mIpAddressPref.setDetailText(addr.getHostAddress());
-                mIpAddressPref.setVisible(true);
-            } else if (addr instanceof Inet6Address) {
-                String ip = addr.getHostAddress();
-                Preference pref = new Preference(mPrefContext);
-                pref.setKey(ip);
-                pref.setTitle(ip);
-                pref.setSelectable(false);
-                mIpv6AddressCategory.addPreference(pref);
-                mIpv6AddressCategory.setVisible(true);
-            }
-        }
-
-        // Set up IPv4 gateway and subnet mask
-        String gateway = null;
+        // Find IPv4 and IPv6 addresses.
+        String ipv4Address = null;
         String subnet = null;
+        StringJoiner ipv6Addresses = new StringJoiner("\n");
+
+        for (LinkAddress addr : mLinkProperties.getLinkAddresses()) {
+            if (addr.getAddress() instanceof Inet4Address) {
+                ipv4Address = addr.getAddress().getHostAddress();
+                subnet = ipv4PrefixLengthToSubnetMask(addr.getPrefixLength());
+            } else if (addr.getAddress() instanceof Inet6Address) {
+                ipv6Addresses.add(addr.getAddress().getHostAddress());
+            }
+        }
+
+        // Find IPv4 default gateway.
+        String gateway = null;
         for (RouteInfo routeInfo : mLinkProperties.getRoutes()) {
-            if (routeInfo.hasGateway() && routeInfo.getGateway() instanceof Inet4Address) {
+            if (routeInfo.isIPv4Default() && routeInfo.hasGateway()) {
                 gateway = routeInfo.getGateway().getHostAddress();
-            }
-            IpPrefix ipPrefix = routeInfo.getDestination();
-            if (ipPrefix != null && ipPrefix.getAddress() instanceof Inet4Address
-                    && ipPrefix.getPrefixLength() > 0) {
-                subnet = ipv4PrefixLengthToSubnetMask(ipPrefix.getPrefixLength());
+                break;
             }
         }
 
-        if (!TextUtils.isEmpty(subnet)) {
-            mSubnetPref.setDetailText(subnet);
-            mSubnetPref.setVisible(true);
-        }
+        // Find IPv4 DNS addresses.
+        String dnsServers = mLinkProperties.getDnsServers().stream()
+                .filter(Inet4Address.class::isInstance)
+                .map(InetAddress::getHostAddress)
+                .collect(Collectors.joining(","));
 
-        if (!TextUtils.isEmpty(gateway)) {
-            mGatewayPref.setDetailText(gateway);
-            mGatewayPref.setVisible(true);
-        }
+        // Update UI.
+        updatePreference(mIpAddressPref, ipv4Address);
+        updatePreference(mSubnetPref, subnet);
+        updatePreference(mGatewayPref, gateway);
+        updatePreference(mDnsPref, dnsServers);
 
-        // Set IPv4 DNS addresses
-        StringJoiner stringJoiner = new StringJoiner(",");
-        for (InetAddress dnsServer : mLinkProperties.getDnsServers()) {
-            if (dnsServer instanceof Inet4Address) {
-                stringJoiner.add(dnsServer.getHostAddress());
-            }
-        }
-        String dnsText = stringJoiner.toString();
-        if (!dnsText.isEmpty()) {
-            mDnsPref.setDetailText(dnsText);
-            mDnsPref.setVisible(true);
+        if (ipv6Addresses.length() > 0) {
+            mIpv6AddressPref.setSummary(ipv6Addresses.toString());
+            mIpv6Category.setVisible(true);
+        } else {
+            mIpv6Category.setVisible(false);
         }
     }
 
diff --git a/tests/robotests/src/com/android/settings/wifi/details/WifiDetailPreferenceControllerTest.java b/tests/robotests/src/com/android/settings/wifi/details/WifiDetailPreferenceControllerTest.java
index 1455618..17801b1 100644
--- a/tests/robotests/src/com/android/settings/wifi/details/WifiDetailPreferenceControllerTest.java
+++ b/tests/robotests/src/com/android/settings/wifi/details/WifiDetailPreferenceControllerTest.java
@@ -79,7 +79,9 @@
 import java.net.Inet6Address;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
+import java.util.Arrays;
 import java.util.List;
+import java.util.stream.Collectors;
 
 @RunWith(SettingsRobolectricTestRunner.class)
 @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
@@ -119,7 +121,8 @@
     @Mock private WifiDetailPreference mockSubnetPref;
     @Mock private WifiDetailPreference mockDnsPref;
     @Mock private Button mockForgetButton;
-    @Mock private PreferenceCategory mockIpv6AddressCategory;
+    @Mock private PreferenceCategory mockIpv6Category;
+    @Mock private WifiDetailPreference mockIpv6AddressesPref;
 
     @Captor private ArgumentCaptor<NetworkCallback> mCallbackCaptor;
     @Captor private ArgumentCaptor<View.OnClickListener> mForgetClickListener;
@@ -211,8 +214,6 @@
 
         when(mockFragment.getActivity()).thenReturn(mockActivity);
 
-        when(mockIpv6AddressCategory.addPreference(mIpv6AddressCaptor.capture())).thenReturn(true);
-
         setupMockedPreferenceScreen();
         mController = newWifiDetailPreferenceController();
     }
@@ -258,8 +259,10 @@
                 .thenReturn(mockSubnetPref);
         when(mockScreen.findPreference(WifiDetailPreferenceController.KEY_DNS_PREF))
                 .thenReturn(mockDnsPref);
-        when(mockScreen.findPreference(WifiDetailPreferenceController.KEY_IPV6_ADDRESS_CATEGORY))
-                .thenReturn(mockIpv6AddressCategory);
+        when(mockScreen.findPreference(WifiDetailPreferenceController.KEY_IPV6_CATEGORY))
+                .thenReturn(mockIpv6Category);
+        when(mockScreen.findPreference(WifiDetailPreferenceController.KEY_IPV6_ADDRESSES_PREF))
+                .thenReturn(mockIpv6AddressesPref);
     }
 
     @Test
@@ -414,17 +417,17 @@
     @Test
     public void noLinkProperties_allIpDetailsHidden() {
         when(mockConnectivityManager.getLinkProperties(mockNetwork)).thenReturn(null);
-        reset(mockIpv6AddressCategory, mockIpAddressPref, mockSubnetPref, mockGatewayPref,
+        reset(mockIpv6Category, mockIpAddressPref, mockSubnetPref, mockGatewayPref,
                 mockDnsPref);
 
         mController.displayPreference(mockScreen);
 
-        verify(mockIpv6AddressCategory).setVisible(false);
+        verify(mockIpv6Category).setVisible(false);
         verify(mockIpAddressPref).setVisible(false);
         verify(mockSubnetPref).setVisible(false);
         verify(mockGatewayPref).setVisible(false);
         verify(mockDnsPref).setVisible(false);
-        verify(mockIpv6AddressCategory, never()).setVisible(true);
+        verify(mockIpv6Category, never()).setVisible(true);
         verify(mockIpAddressPref, never()).setVisible(true);
         verify(mockSubnetPref, never()).setVisible(true);
         verify(mockGatewayPref, never()).setVisible(true);
@@ -442,23 +445,11 @@
         mCallbackCaptor.getValue().onLinkPropertiesChanged(mockNetwork, new LinkProperties(lp));
     }
 
-    // Check that all IP information is deleted before being redrawn.
-    // TODO: switch the code to redrawing in place and remove this method.
-    private void verifyIpLayerInfoCleared(InOrder inOrder) {
-        inOrder.verify(mockIpv6AddressCategory).removeAll();
-        inOrder.verify(mockIpv6AddressCategory).setVisible(false);
-        inOrder.verify(mockIpAddressPref).setVisible(false);
-        inOrder.verify(mockSubnetPref).setVisible(false);
-        inOrder.verify(mockGatewayPref).setVisible(false);
-        inOrder.verify(mockDnsPref).setVisible(false);
-    }
-
     private void verifyDisplayedIpv6Addresses(InOrder inOrder, LinkAddress... addresses) {
-        for (LinkAddress l: addresses) {
-            inOrder.verify(mockIpv6AddressCategory).addPreference(mIpv6AddressCaptor.capture());
-            assertThat(mIpv6AddressCaptor.getValue().getTitle()).isEqualTo(asString(l));
-        }
-        inOrder.verify(mockIpv6AddressCategory).setVisible(true);
+        String text = Arrays.stream(addresses)
+                .map(address -> asString(address))
+                .collect(Collectors.joining("\n"));
+        inOrder.verify(mockIpv6AddressesPref).setSummary(text);
     }
 
     @Test
@@ -467,25 +458,23 @@
         mController.onResume();
 
         InOrder inOrder = inOrder(mockIpAddressPref, mockGatewayPref, mockSubnetPref,
-                mockDnsPref, mockIpv6AddressCategory);
+                mockDnsPref, mockIpv6Category, mockIpv6AddressesPref);
 
         LinkProperties lp = new LinkProperties();
 
         lp.addLinkAddress(Constants.IPV6_LINKLOCAL);
         updateLinkProperties(lp);
-        verifyIpLayerInfoCleared(inOrder);
         verifyDisplayedIpv6Addresses(inOrder, Constants.IPV6_LINKLOCAL);
+        inOrder.verify(mockIpv6Category).setVisible(true);
 
         lp.addRoute(Constants.IPV4_DEFAULT);
         updateLinkProperties(lp);
-        verifyIpLayerInfoCleared(inOrder);
         inOrder.verify(mockGatewayPref).setDetailText(Constants.IPV4_GATEWAY.getHostAddress());
         inOrder.verify(mockGatewayPref).setVisible(true);
 
         lp.addLinkAddress(Constants.IPV4_ADDR);
         lp.addRoute(Constants.IPV4_SUBNET);
         updateLinkProperties(lp);
-        verifyIpLayerInfoCleared(inOrder);
         inOrder.verify(mockIpAddressPref).setDetailText(asString(Constants.IPV4_ADDR));
         inOrder.verify(mockIpAddressPref).setVisible(true);
         inOrder.verify(mockSubnetPref).setDetailText("255.255.255.128");
@@ -494,7 +483,6 @@
         lp.addLinkAddress(Constants.IPV6_GLOBAL1);
         lp.addLinkAddress(Constants.IPV6_GLOBAL2);
         updateLinkProperties(lp);
-        verifyIpLayerInfoCleared(inOrder);
         verifyDisplayedIpv6Addresses(inOrder,
                 Constants.IPV6_LINKLOCAL,
                 Constants.IPV6_GLOBAL1,
@@ -502,7 +490,6 @@
 
         lp.removeLinkAddress(Constants.IPV6_GLOBAL1);
         updateLinkProperties(lp);
-        verifyIpLayerInfoCleared(inOrder);
         verifyDisplayedIpv6Addresses(inOrder,
                 Constants.IPV6_LINKLOCAL,
                 Constants.IPV6_GLOBAL2);
@@ -630,11 +617,13 @@
         mController.displayPreference(mockScreen);
 
         List <Preference> addrs = mIpv6AddressCaptor.getAllValues();
-        assertThat(addrs.size()).isEqualTo(3);
 
-        assertThat((String) addrs.get(0).getTitle()).isEqualTo(asString(Constants.IPV6_LINKLOCAL));
-        assertThat((String) addrs.get(1).getTitle()).isEqualTo(asString(Constants.IPV6_GLOBAL1));
-        assertThat((String) addrs.get(2).getTitle()).isEqualTo(asString(Constants.IPV6_GLOBAL2));
+        String expectedAddresses = String.join("\n",
+                asString(Constants.IPV6_LINKLOCAL),
+                asString(Constants.IPV6_GLOBAL1),
+                asString(Constants.IPV6_GLOBAL2));
+
+        verify(mockIpv6AddressesPref).setSummary(expectedAddresses);
     }
 
     @Test
@@ -643,7 +632,7 @@
 
         mController.displayPreference(mockScreen);
 
-        assertThat(mockIpv6AddressCategory.isSelectable()).isFalse();
+        assertThat(mockIpv6AddressesPref.isSelectable()).isFalse();
     }
 
     @Test