Merge changes from topic "nsd_serviceinfo_network" am: 585b14d79c

Original change: https://android-review.googlesource.com/c/platform/frameworks/base/+/1944534

Change-Id: Ia0a3c75269dd5b125292235360e2e7ba0cefd9ca
diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java
index 0f21e55..ea69e09 100644
--- a/framework-t/src/android/net/nsd/NsdManager.java
+++ b/framework-t/src/android/net/nsd/NsdManager.java
@@ -16,6 +16,8 @@
 
 package android.net.nsd;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.annotation.SdkConstant;
 import android.annotation.SdkConstant.SdkConstantType;
 import android.annotation.SystemService;
@@ -23,6 +25,7 @@
 import android.compat.annotation.ChangeId;
 import android.compat.annotation.EnabledSince;
 import android.content.Context;
+import android.net.Network;
 import android.os.Handler;
 import android.os.HandlerThread;
 import android.os.Looper;
@@ -634,6 +637,14 @@
     }
 
     /**
+     * Same as {@link #discoverServices(String, int, Network, DiscoveryListener)} with a null
+     * {@link Network}.
+     */
+    public void discoverServices(String serviceType, int protocolType, DiscoveryListener listener) {
+        discoverServices(serviceType, protocolType, null, listener);
+    }
+
+    /**
      * Initiate service discovery to browse for instances of a service type. Service discovery
      * consumes network bandwidth and will continue until the application calls
      * {@link #stopServiceDiscovery}.
@@ -657,11 +668,13 @@
      * @param serviceType The service type being discovered. Examples include "_http._tcp" for
      * http services or "_ipp._tcp" for printers
      * @param protocolType The service discovery protocol
+     * @param network Network to discover services on, or null to discover on all available networks
      * @param listener  The listener notifies of a successful discovery and is used
      * to stop discovery on this serviceType through a call on {@link #stopServiceDiscovery}.
      * Cannot be null. Cannot be in use for an active service discovery.
      */
-    public void discoverServices(String serviceType, int protocolType, DiscoveryListener listener) {
+    public void discoverServices(@NonNull String serviceType, int protocolType,
+            @Nullable Network network, @NonNull DiscoveryListener listener) {
         if (TextUtils.isEmpty(serviceType)) {
             throw new IllegalArgumentException("Service type cannot be empty");
         }
@@ -669,6 +682,7 @@
 
         NsdServiceInfo s = new NsdServiceInfo();
         s.setServiceType(serviceType);
+        s.setNetwork(network);
 
         int key = putListener(listener, s);
         try {
diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java
index 0946499..8506db1 100644
--- a/framework-t/src/android/net/nsd/NsdServiceInfo.java
+++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java
@@ -17,7 +17,9 @@
 package android.net.nsd;
 
 import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.compat.annotation.UnsupportedAppUsage;
+import android.net.Network;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.text.TextUtils;
@@ -49,6 +51,9 @@
 
     private int mPort;
 
+    @Nullable
+    private Network mNetwork;
+
     public NsdServiceInfo() {
     }
 
@@ -307,18 +312,37 @@
         return txtRecord;
     }
 
-    public String toString() {
-        StringBuffer sb = new StringBuffer();
+    /**
+     * Get the network where the service can be found.
+     *
+     * This is never null if this {@link NsdServiceInfo} was obtained from
+     * {@link NsdManager#discoverServices} or {@link NsdManager#resolveService}.
+     */
+    @Nullable
+    public Network getNetwork() {
+        return mNetwork;
+    }
 
+    /**
+     * Set the network where the service can be found.
+     * @param network The network, or null to search for, or to announce, the service on all
+     *                connected networks.
+     */
+    public void setNetwork(@Nullable Network network) {
+        mNetwork = network;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
         sb.append("name: ").append(mServiceName)
                 .append(", type: ").append(mServiceType)
                 .append(", host: ").append(mHost)
-                .append(", port: ").append(mPort);
+                .append(", port: ").append(mPort)
+                .append(", network: ").append(mNetwork);
 
         byte[] txtRecord = getTxtRecord();
-        if (txtRecord != null) {
-            sb.append(", txtRecord: ").append(new String(txtRecord, StandardCharsets.UTF_8));
-        }
+        sb.append(", txtRecord: ").append(new String(txtRecord, StandardCharsets.UTF_8));
         return sb.toString();
     }
 
@@ -352,6 +376,8 @@
             }
             dest.writeString(key);
         }
+
+        dest.writeParcelable(mNetwork, 0);
     }
 
     /** Implement the Parcelable interface */
@@ -381,6 +407,7 @@
                     }
                     info.mTxtRecord.put(in.readString(), valueArray);
                 }
+                info.mNetwork = in.readParcelable(null, Network.class);
                 return info;
             }
 
diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java
index eca4ccf..ddf6d2c 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -19,6 +19,9 @@
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
+import android.net.ConnectivityManager;
+import android.net.LinkProperties;
+import android.net.Network;
 import android.net.nsd.INsdManager;
 import android.net.nsd.INsdManagerCallback;
 import android.net.nsd.INsdServiceConnector;
@@ -44,6 +47,9 @@
 import java.io.FileDescriptor;
 import java.io.PrintWriter;
 import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.UnknownHostException;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.concurrent.CountDownLatch;
@@ -60,6 +66,7 @@
 
     private static final boolean DBG = true;
     private static final long CLEANUP_DELAY_MS = 10000;
+    private static final int IFACE_IDX_ANY = 0;
 
     private final Context mContext;
     private final NsdStateMachine mNsdStateMachine;
@@ -297,7 +304,7 @@
 
                         maybeStartDaemon();
                         id = getUniqueId();
-                        if (discoverServices(id, args.serviceInfo.getServiceType())) {
+                        if (discoverServices(id, args.serviceInfo)) {
                             if (DBG) {
                                 Log.d(TAG, "Discover " + msg.arg2 + " " + id
                                         + args.serviceInfo.getServiceType());
@@ -430,13 +437,38 @@
                 }
                 switch (code) {
                     case NativeResponseCode.SERVICE_FOUND:
-                        /* NNN uniqueId serviceName regType domain */
+                        /* NNN uniqueId serviceName regType domain interfaceIdx netId */
                         servInfo = new NsdServiceInfo(cooked[2], cooked[3]);
+                        final int foundNetId;
+                        try {
+                            foundNetId = Integer.parseInt(cooked[6]);
+                        } catch (NumberFormatException e) {
+                            Log.wtf(TAG, "Invalid network received from mdnsd: " + cooked[6]);
+                            break;
+                        }
+                        if (foundNetId == 0L) {
+                            // Ignore services that do not have a Network: they are not usable
+                            // by apps, as they would need privileged permissions to use
+                            // interfaces that do not have an associated Network.
+                            break;
+                        }
+                        servInfo.setNetwork(new Network(foundNetId));
                         clientInfo.onServiceFound(clientId, servInfo);
                         break;
                     case NativeResponseCode.SERVICE_LOST:
-                        /* NNN uniqueId serviceName regType domain */
+                        /* NNN uniqueId serviceName regType domain interfaceIdx netId */
+                        final int lostNetId;
+                        try {
+                            lostNetId = Integer.parseInt(cooked[6]);
+                        } catch (NumberFormatException e) {
+                            Log.wtf(TAG, "Invalid network received from mdnsd: " + cooked[6]);
+                            break;
+                        }
                         servInfo = new NsdServiceInfo(cooked[2], cooked[3]);
+                        // The network could be null if it was torn down when the service is lost
+                        // TODO: avoid returning null in that case, possibly by remembering found
+                        // services on the same interface index and their network at the time
+                        servInfo.setNetwork(lostNetId == 0 ? null : new Network(lostNetId));
                         clientInfo.onServiceLost(clientId, servInfo);
                         break;
                     case NativeResponseCode.SERVICE_DISCOVERY_FAILED:
@@ -461,7 +493,7 @@
                         /* NNN regId errorCode */
                         break;
                     case NativeResponseCode.SERVICE_RESOLVED:
-                        /* NNN resolveId fullName hostName port txtlen txtdata */
+                        /* NNN resolveId fullName hostName port txtlen txtdata interfaceIdx */
                         int index = 0;
                         while (index < cooked[2].length() && cooked[2].charAt(index) != '.') {
                             if (cooked[2].charAt(index) == '\\') {
@@ -473,6 +505,7 @@
                             Log.e(TAG, "Invalid service found " + raw);
                             break;
                         }
+
                         String name = cooked[2].substring(0, index);
                         String rest = cooked[2].substring(index);
                         String type = rest.replace(".local.", "");
@@ -483,12 +516,13 @@
                         clientInfo.mResolvedService.setServiceType(type);
                         clientInfo.mResolvedService.setPort(Integer.parseInt(cooked[4]));
                         clientInfo.mResolvedService.setTxtRecords(cooked[6]);
+                        // Network will be added after SERVICE_GET_ADDR_SUCCESS
 
                         stopResolveService(id);
                         removeRequestMap(clientId, id, clientInfo);
 
                         int id2 = getUniqueId();
-                        if (getAddrInfo(id2, cooked[3], cooked[7])) {
+                        if (getAddrInfo(id2, cooked[3], cooked[7] /* interfaceIdx */)) {
                             storeRequestMap(clientId, id2, clientInfo, NsdManager.RESOLVE_SERVICE);
                         } else {
                             clientInfo.onResolveServiceFailed(
@@ -513,12 +547,31 @@
                                 clientId, NsdManager.FAILURE_INTERNAL_ERROR);
                         break;
                     case NativeResponseCode.SERVICE_GET_ADDR_SUCCESS:
-                        /* NNN resolveId hostname ttl addr */
+                        /* NNN resolveId hostname ttl addr interfaceIdx netId */
+                        Network network = null;
                         try {
-                            clientInfo.mResolvedService.setHost(InetAddress.getByName(cooked[4]));
+                            final int netId = Integer.parseInt(cooked[6]);
+                            network = netId == 0L ? null : new Network(netId);
+                        } catch (NumberFormatException e) {
+                            Log.wtf(TAG, "Invalid network in GET_ADDR_SUCCESS: " + cooked[6], e);
+                        }
+
+                        InetAddress serviceHost = null;
+                        try {
+                            serviceHost = InetAddress.getByName(cooked[4]);
+                        } catch (UnknownHostException e) {
+                            Log.wtf(TAG, "Invalid host in GET_ADDR_SUCCESS", e);
+                        }
+
+                        // If the resolved service is on an interface without a network, consider it
+                        // as a failure: it would not be usable by apps as they would need
+                        // privileged permissions.
+                        if (network != null && serviceHost != null) {
+                            clientInfo.mResolvedService.setHost(serviceHost);
+                            clientInfo.mResolvedService.setNetwork(network);
                             clientInfo.onResolveServiceSucceeded(
                                     clientId, clientInfo.mResolvedService);
-                        } catch (java.net.UnknownHostException e) {
+                        } else {
                             clientInfo.onResolveServiceFailed(
                                     clientId, NsdManager.FAILURE_INTERNAL_ERROR);
                         }
@@ -815,8 +868,15 @@
         return mDaemon.execute("update", regId, t.size(), t.getRawData());
     }
 
-    private boolean discoverServices(int discoveryId, String serviceType) {
-        return mDaemon.execute("discover", discoveryId, serviceType);
+    private boolean discoverServices(int discoveryId, NsdServiceInfo serviceInfo) {
+        final Network network = serviceInfo.getNetwork();
+        final int discoverInterface = getNetworkInterfaceIndex(network);
+        if (network != null && discoverInterface == IFACE_IDX_ANY) {
+            Log.e(TAG, "Interface to discover service on not found");
+            return false;
+        }
+        return mDaemon.execute("discover", discoveryId, serviceInfo.getServiceType(),
+                discoverInterface);
     }
 
     private boolean stopServiceDiscovery(int discoveryId) {
@@ -824,17 +884,61 @@
     }
 
     private boolean resolveService(int resolveId, NsdServiceInfo service) {
-        String name = service.getServiceName();
-        String type = service.getServiceType();
-        return mDaemon.execute("resolve", resolveId, name, type, "local.");
+        final String name = service.getServiceName();
+        final String type = service.getServiceType();
+        final Network network = service.getNetwork();
+        final int resolveInterface = getNetworkInterfaceIndex(network);
+        if (network != null && resolveInterface == IFACE_IDX_ANY) {
+            Log.e(TAG, "Interface to resolve service on not found");
+            return false;
+        }
+        return mDaemon.execute("resolve", resolveId, name, type, "local.", resolveInterface);
+    }
+
+    /**
+     * Guess the interface to use to resolve or discover a service on a specific network.
+     *
+     * This is an imperfect guess, as for example the network may be gone or not yet fully
+     * registered. This is fine as failing is correct if the network is gone, and a client
+     * attempting to resolve/discover on a network not yet setup would have a bad time anyway; also
+     * this is to support the legacy mdnsresponder implementation, which historically resolved
+     * services on an unspecified network.
+     */
+    private int getNetworkInterfaceIndex(Network network) {
+        if (network == null) return IFACE_IDX_ANY;
+
+        final ConnectivityManager cm = mContext.getSystemService(ConnectivityManager.class);
+        if (cm == null) {
+            Log.wtf(TAG, "No ConnectivityManager for resolveService");
+            return IFACE_IDX_ANY;
+        }
+        final LinkProperties lp = cm.getLinkProperties(network);
+        if (lp == null) return IFACE_IDX_ANY;
+
+        // Only resolve on non-stacked interfaces
+        final NetworkInterface iface;
+        try {
+            iface = NetworkInterface.getByName(lp.getInterfaceName());
+        } catch (SocketException e) {
+            Log.e(TAG, "Error querying interface", e);
+            return IFACE_IDX_ANY;
+        }
+
+        if (iface == null) {
+            Log.e(TAG, "Interface not found: " + lp.getInterfaceName());
+            return IFACE_IDX_ANY;
+        }
+
+        return iface.getIndex();
     }
 
     private boolean stopResolveService(int resolveId) {
         return mDaemon.execute("stop-resolve", resolveId);
     }
 
-    private boolean getAddrInfo(int resolveId, String hostname, String interfaceName) {
-        return mDaemon.execute("getaddrinfo", resolveId, hostname, interfaceName);
+    private boolean getAddrInfo(int resolveId, String hostname, String interfaceIdx) {
+        // interfaceIdx is always obtained (as string) from the service resolved callback
+        return mDaemon.execute("getaddrinfo", resolveId, hostname, interfaceIdx);
     }
 
     private boolean stopGetAddrInfo(int resolveId) {