Merge "Enable multiple active Ethernet interfaces"
am: 0a6c539901

Change-Id: Idea050a552dd3447cccebab2fc6f041bae06853e
diff --git a/core/java/android/net/EthernetManager.java b/core/java/android/net/EthernetManager.java
index 31a3096..ecccda5 100644
--- a/core/java/android/net/EthernetManager.java
+++ b/core/java/android/net/EthernetManager.java
@@ -18,9 +18,6 @@
 
 import android.annotation.SystemService;
 import android.content.Context;
-import android.net.IEthernetManager;
-import android.net.IEthernetServiceListener;
-import android.net.IpConfiguration;
 import android.os.Handler;
 import android.os.Message;
 import android.os.RemoteException;
@@ -45,18 +42,18 @@
             if (msg.what == MSG_AVAILABILITY_CHANGED) {
                 boolean isAvailable = (msg.arg1 == 1);
                 for (Listener listener : mListeners) {
-                    listener.onAvailabilityChanged(isAvailable);
+                    listener.onAvailabilityChanged((String) msg.obj, isAvailable);
                 }
             }
         }
     };
-    private final ArrayList<Listener> mListeners = new ArrayList<Listener>();
+    private final ArrayList<Listener> mListeners = new ArrayList<>();
     private final IEthernetServiceListener.Stub mServiceListener =
             new IEthernetServiceListener.Stub() {
                 @Override
-                public void onAvailabilityChanged(boolean isAvailable) {
+                public void onAvailabilityChanged(String iface, boolean isAvailable) {
                     mHandler.obtainMessage(
-                            MSG_AVAILABILITY_CHANGED, isAvailable ? 1 : 0, 0, null).sendToTarget();
+                            MSG_AVAILABILITY_CHANGED, isAvailable ? 1 : 0, 0, iface).sendToTarget();
                 }
             };
 
@@ -66,9 +63,10 @@
     public interface Listener {
         /**
          * Called when Ethernet port's availability is changed.
-         * @param isAvailable {@code true} if one or more Ethernet port exists.
+         * @param iface Ethernet interface name
+         * @param isAvailable {@code true} if Ethernet port exists.
          */
-        public void onAvailabilityChanged(boolean isAvailable);
+        void onAvailabilityChanged(String iface, boolean isAvailable);
     }
 
     /**
@@ -86,9 +84,9 @@
      * Get Ethernet configuration.
      * @return the Ethernet Configuration, contained in {@link IpConfiguration}.
      */
-    public IpConfiguration getConfiguration() {
+    public IpConfiguration getConfiguration(String iface) {
         try {
-            return mService.getConfiguration();
+            return mService.getConfiguration(iface);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -97,21 +95,29 @@
     /**
      * Set Ethernet configuration.
      */
-    public void setConfiguration(IpConfiguration config) {
+    public void setConfiguration(String iface, IpConfiguration config) {
         try {
-            mService.setConfiguration(config);
+            mService.setConfiguration(iface, config);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
     }
 
     /**
-     * Indicates whether the system currently has one or more
-     * Ethernet interfaces.
+     * Indicates whether the system currently has one or more Ethernet interfaces.
      */
     public boolean isAvailable() {
+        return getAvailableInterfaces().length > 0;
+    }
+
+    /**
+     * Indicates whether the system has given interface.
+     *
+     * @param iface Ethernet interface name
+     */
+    public boolean isAvailable(String iface) {
         try {
-            return mService.isAvailable();
+            return mService.isAvailable(iface);
         } catch (RemoteException e) {
             throw e.rethrowFromSystemServer();
         }
@@ -137,6 +143,17 @@
     }
 
     /**
+     * Returns an array of available Ethernet interface names.
+     */
+    public String[] getAvailableInterfaces() {
+        try {
+            return mService.getAvailableInterfaces();
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
      * Removes a listener.
      * @param listener A {@link Listener} to remove.
      * @throws IllegalArgumentException If the listener is null.
diff --git a/core/java/android/net/IEthernetManager.aidl b/core/java/android/net/IEthernetManager.aidl
index 7a92eb9..94960b5 100644
--- a/core/java/android/net/IEthernetManager.aidl
+++ b/core/java/android/net/IEthernetManager.aidl
@@ -26,9 +26,10 @@
 /** {@hide} */
 interface IEthernetManager
 {
-    IpConfiguration getConfiguration();
-    void setConfiguration(in IpConfiguration config);
-    boolean isAvailable();
+    String[] getAvailableInterfaces();
+    IpConfiguration getConfiguration(String iface);
+    void setConfiguration(String iface, in IpConfiguration config);
+    boolean isAvailable(String iface);
     void addListener(in IEthernetServiceListener listener);
     void removeListener(in IEthernetServiceListener listener);
 }
diff --git a/core/java/android/net/IEthernetServiceListener.aidl b/core/java/android/net/IEthernetServiceListener.aidl
index 356690e8..782fa19 100644
--- a/core/java/android/net/IEthernetServiceListener.aidl
+++ b/core/java/android/net/IEthernetServiceListener.aidl
@@ -19,5 +19,5 @@
 /** @hide */
 oneway interface IEthernetServiceListener
 {
-    void onAvailabilityChanged(boolean isAvailable);
+    void onAvailabilityChanged(String iface, boolean isAvailable);
 }
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 5cc727d..1be7fae 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -360,6 +360,23 @@
     <!-- Regex of wired ethernet ifaces -->
     <string translatable="false" name="config_ethernet_iface_regex">eth\\d</string>
 
+
+
+    <!-- Configuration of Ethernet interfaces in the following format:
+         <interface name|mac address>;[Network Capabilities];[IP config]
+         Where
+               [Network Capabilities] Optional. A comma seprated list of network capabilities.
+                   Values must be from NetworkCapabilities#NET_CAPABILITIES_* constants.
+               [IP config] Optional. If empty or not specified - DHCP will be used, otherwise
+                    static IP address with the mask.
+         -->
+    <string-array translatable="false" name="config_ethernet_interfaces">
+        <!--
+        <item>eth1;12,13,14,15;192.168.0.10/24</item>
+        <item>eth2;;192.168.0.11/24</item>
+        -->
+    </string-array>
+
     <!-- If the mobile hotspot feature requires provisioning, a package name and class name
         can be provided to launch a supported application that provisions the devices.
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index fb755a1..77de10d 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -634,6 +634,7 @@
   <java-symbol type="string" name="chooseActivity" />
   <java-symbol type="string" name="config_default_dns_server" />
   <java-symbol type="string" name="config_ethernet_iface_regex" />
+  <java-symbol type="array" name="config_ethernet_interfaces" />
   <java-symbol type="string" name="config_forceVoiceInteractionServicePackage" />
   <java-symbol type="string" name="config_mms_user_agent" />
   <java-symbol type="string" name="config_mms_user_agent_profile_url" />
diff --git a/services/core/java/com/android/server/net/IpConfigStore.java b/services/core/java/com/android/server/net/IpConfigStore.java
index 4d56468..e3e02e3 100644
--- a/services/core/java/com/android/server/net/IpConfigStore.java
+++ b/services/core/java/com/android/server/net/IpConfigStore.java
@@ -24,11 +24,11 @@
 import android.net.ProxyInfo;
 import android.net.RouteInfo;
 import android.net.StaticIpConfiguration;
+import android.util.ArrayMap;
 import android.util.Log;
 import android.util.SparseArray;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.server.net.DelayedDiskWrite;
 
 import java.io.BufferedInputStream;
 import java.io.DataInputStream;
@@ -38,8 +38,8 @@
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
-import java.net.InetAddress;
 import java.net.Inet4Address;
+import java.net.InetAddress;
 
 public class IpConfigStore {
     private static final String TAG = "IpConfigStore";
@@ -60,7 +60,7 @@
     protected static final String EXCLUSION_LIST_KEY = "exclusionList";
     protected static final String EOS = "eos";
 
-    protected static final int IPCONFIG_FILE_VERSION = 2;
+    protected static final int IPCONFIG_FILE_VERSION = 3;
 
     public IpConfigStore(DelayedDiskWrite writer) {
         mWriter = writer;
@@ -70,9 +70,14 @@
         this(new DelayedDiskWrite());
     }
 
+    private static boolean writeConfig(DataOutputStream out, String configKey,
+            IpConfiguration config) throws IOException {
+        return writeConfig(out, configKey, config, IPCONFIG_FILE_VERSION);
+    }
+
     @VisibleForTesting
-    public static boolean writeConfig(DataOutputStream out, int configKey,
-                                IpConfiguration config) throws IOException {
+    public static boolean writeConfig(DataOutputStream out, String configKey,
+                                IpConfiguration config, int version) throws IOException {
         boolean written = false;
 
         try {
@@ -153,7 +158,11 @@
 
             if (written) {
                 out.writeUTF(ID_KEY);
-                out.writeInt(configKey);
+                if (version < 3) {
+                    out.writeInt(Integer.valueOf(configKey));
+                } else {
+                    out.writeUTF(configKey);
+                }
             }
         } catch (NullPointerException e) {
             loge("Failure in writing " + config + e);
@@ -163,18 +172,47 @@
         return written;
     }
 
-    public void writeIpAndProxyConfigurations(String filePath,
+    /**
+     * @Deprecated use {@link #writeIpConfigurations(String, ArrayMap)} instead.
+     * New method uses string as network identifier which could be interface name or MAC address or
+     * other token.
+     */
+    @Deprecated
+    public void writeIpAndProxyConfigurationsToFile(String filePath,
                                               final SparseArray<IpConfiguration> networks) {
-        mWriter.write(filePath, new DelayedDiskWrite.Writer() {
-            public void onWriteCalled(DataOutputStream out) throws IOException{
-                out.writeInt(IPCONFIG_FILE_VERSION);
-                for(int i = 0; i < networks.size(); i++) {
-                    writeConfig(out, networks.keyAt(i), networks.valueAt(i));
-                }
+        mWriter.write(filePath, out -> {
+            out.writeInt(IPCONFIG_FILE_VERSION);
+            for(int i = 0; i < networks.size(); i++) {
+                writeConfig(out, String.valueOf(networks.keyAt(i)), networks.valueAt(i));
             }
         });
     }
 
+    public void writeIpConfigurations(String filePath,
+                                      ArrayMap<String, IpConfiguration> networks) {
+        mWriter.write(filePath, out -> {
+            out.writeInt(IPCONFIG_FILE_VERSION);
+            for(int i = 0; i < networks.size(); i++) {
+                writeConfig(out, networks.keyAt(i), networks.valueAt(i));
+            }
+        });
+    }
+
+    public static ArrayMap<String, IpConfiguration> readIpConfigurations(String filePath) {
+        BufferedInputStream bufferedInputStream;
+        try {
+            bufferedInputStream = new BufferedInputStream(new FileInputStream(filePath));
+        } catch (FileNotFoundException e) {
+            // Return an empty array here because callers expect an empty array when the file is
+            // not present.
+            loge("Error opening configuration file: " + e);
+            return new ArrayMap<>(0);
+        }
+        return readIpConfigurations(bufferedInputStream);
+    }
+
+    /** @Deprecated use {@link #readIpConfigurations(String)} */
+    @Deprecated
     public static SparseArray<IpConfiguration> readIpAndProxyConfigurations(String filePath) {
         BufferedInputStream bufferedInputStream;
         try {
@@ -188,21 +226,40 @@
         return readIpAndProxyConfigurations(bufferedInputStream);
     }
 
+    /** @Deprecated use {@link #readIpConfigurations(InputStream)} */
+    @Deprecated
     public static SparseArray<IpConfiguration> readIpAndProxyConfigurations(
             InputStream inputStream) {
-        SparseArray<IpConfiguration> networks = new SparseArray<IpConfiguration>();
+        ArrayMap<String, IpConfiguration> networks = readIpConfigurations(inputStream);
+        if (networks == null) {
+            return null;
+        }
+
+        SparseArray<IpConfiguration> networksById = new SparseArray<>();
+        for (int i = 0; i < networks.size(); i++) {
+            int id = Integer.valueOf(networks.keyAt(i));
+            networksById.put(id, networks.valueAt(i));
+        }
+
+        return networksById;
+    }
+
+    /** Returns a map of network identity token and {@link IpConfiguration}. */
+    public static ArrayMap<String, IpConfiguration> readIpConfigurations(
+            InputStream inputStream) {
+        ArrayMap<String, IpConfiguration> networks = new ArrayMap<>();
         DataInputStream in = null;
         try {
             in = new DataInputStream(inputStream);
 
             int version = in.readInt();
-            if (version != 2 && version != 1) {
+            if (version != 3 && version != 2 && version != 1) {
                 loge("Bad version on IP configuration file, ignore read");
                 return null;
             }
 
             while (true) {
-                int id = -1;
+                String uniqueToken = null;
                 // Default is DHCP with no proxy
                 IpAssignment ipAssignment = IpAssignment.DHCP;
                 ProxySettings proxySettings = ProxySettings.NONE;
@@ -217,7 +274,12 @@
                     key = in.readUTF();
                     try {
                         if (key.equals(ID_KEY)) {
-                            id = in.readInt();
+                            if (version < 3) {
+                                int id = in.readInt();
+                                uniqueToken = String.valueOf(id);
+                            } else {
+                                uniqueToken = in.readUTF();
+                            }
                         } else if (key.equals(IP_ASSIGNMENT_KEY)) {
                             ipAssignment = IpAssignment.valueOf(in.readUTF());
                         } else if (key.equals(LINK_ADDRESS_KEY)) {
@@ -280,9 +342,9 @@
                     }
                 } while (true);
 
-                if (id != -1) {
+                if (uniqueToken != null) {
                     IpConfiguration config = new IpConfiguration();
-                    networks.put(id, config);
+                    networks.put(uniqueToken, config);
 
                     switch (ipAssignment) {
                         case STATIC:
diff --git a/services/tests/servicestests/src/com/android/server/net/IpConfigStoreTest.java b/services/tests/servicestests/src/com/android/server/net/IpConfigStoreTest.java
new file mode 100644
index 0000000..9f4b754
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/net/IpConfigStoreTest.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2018 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.server.net;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+
+import android.net.IpConfiguration;
+import android.net.IpConfiguration.IpAssignment;
+import android.net.IpConfiguration.ProxySettings;
+import android.net.LinkAddress;
+import android.net.NetworkUtils;
+import android.net.ProxyInfo;
+import android.net.StaticIpConfiguration;
+import android.support.test.runner.AndroidJUnit4;
+import android.util.ArrayMap;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Unit tests for {@link IpConfigStore}
+ */
+@RunWith(AndroidJUnit4.class)
+public class IpConfigStoreTest {
+
+    @Test
+    public void backwardCompatibility2to3() throws IOException {
+        final int KEY_CONFIG = 17;
+        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
+        DataOutputStream outputStream = new DataOutputStream(byteStream);
+
+        IpConfiguration expectedConfig = new IpConfiguration(IpAssignment.DHCP,
+                ProxySettings.NONE, null, null);
+
+        // Emulate writing to old format.
+        writeDhcpConfigV2(outputStream, KEY_CONFIG, expectedConfig);
+
+        InputStream in = new ByteArrayInputStream(byteStream.toByteArray());
+        ArrayMap<String, IpConfiguration> configurations = IpConfigStore.readIpConfigurations(in);
+
+        assertNotNull(configurations);
+        assertEquals(1, configurations.size());
+        IpConfiguration actualConfig = configurations.get(String.valueOf(KEY_CONFIG));
+        assertNotNull(actualConfig);
+        assertEquals(expectedConfig, actualConfig);
+    }
+
+    @Test
+    public void staticIpMultiNetworks() throws Exception {
+        final String IFACE_1 = "eth0";
+        final String IFACE_2 = "eth1";
+        final String IP_ADDR_1 = "192.168.1.10/24";
+        final String IP_ADDR_2 = "192.168.1.20/24";
+        final String DNS_IP_ADDR_1 = "1.2.3.4";
+        final String DNS_IP_ADDR_2 = "5.6.7.8";
+
+        StaticIpConfiguration staticIpConfiguration = new StaticIpConfiguration();
+        staticIpConfiguration.ipAddress = new LinkAddress(IP_ADDR_1);
+        staticIpConfiguration.dnsServers.add(NetworkUtils.numericToInetAddress(DNS_IP_ADDR_1));
+        staticIpConfiguration.dnsServers.add(NetworkUtils.numericToInetAddress(DNS_IP_ADDR_2));
+
+        ProxyInfo proxyInfo = new ProxyInfo("10.10.10.10", 88, "host1,host2");
+
+        IpConfiguration expectedConfig1 = new IpConfiguration(IpAssignment.STATIC,
+                ProxySettings.STATIC, staticIpConfiguration, proxyInfo);
+        IpConfiguration expectedConfig2 = new IpConfiguration(expectedConfig1);
+        expectedConfig2.getStaticIpConfiguration().ipAddress = new LinkAddress(IP_ADDR_2);
+
+        ArrayMap<String, IpConfiguration> expectedNetworks = new ArrayMap<>();
+        expectedNetworks.put(IFACE_1, expectedConfig1);
+        expectedNetworks.put(IFACE_2, expectedConfig2);
+
+        MockedDelayedDiskWrite writer = new MockedDelayedDiskWrite();
+        IpConfigStore store = new IpConfigStore(writer);
+        store.writeIpConfigurations("file/path/not/used/", expectedNetworks);
+
+        InputStream in = new ByteArrayInputStream(writer.byteStream.toByteArray());
+        ArrayMap<String, IpConfiguration> actualNetworks = IpConfigStore.readIpConfigurations(in);
+        assertNotNull(actualNetworks);
+        assertEquals(2, actualNetworks.size());
+        assertEquals(expectedNetworks.get(IFACE_1), actualNetworks.get(IFACE_1));
+        assertEquals(expectedNetworks.get(IFACE_2), actualNetworks.get(IFACE_2));
+    }
+
+    // This is simplified snapshot of code that was used to store values in V2 format (key as int).
+    private static void writeDhcpConfigV2(DataOutputStream out, int configKey,
+            IpConfiguration config) throws IOException {
+        out.writeInt(2);  // VERSION 2
+        switch (config.ipAssignment) {
+            case DHCP:
+                out.writeUTF("ipAssignment");
+                out.writeUTF(config.ipAssignment.toString());
+                break;
+            default:
+                fail("Not supported in test environment");
+        }
+
+        out.writeUTF("id");
+        out.writeInt(configKey);
+        out.writeUTF("eos");
+    }
+
+    /** Synchronously writes into given byte steam */
+    private static class MockedDelayedDiskWrite extends DelayedDiskWrite {
+        final ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
+
+        @Override
+        public void write(String filePath, Writer w) {
+            DataOutputStream outputStream = new DataOutputStream(byteStream);
+
+            try {
+                w.onWriteCalled(outputStream);
+            } catch (IOException e) {
+                fail();
+            }
+        }
+    }
+}