diff --git a/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java b/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java index 61b34d0bcf..d9c9d74032 100644 --- a/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java +++ b/framework-t/src/android/net/ConnectivityFrameworkInitializerTiramisu.java @@ -20,7 +20,9 @@ import android.annotation.SystemApi; import android.app.SystemServiceRegistry; import android.app.usage.NetworkStatsManager; import android.content.Context; +import android.net.mdns.aidl.IMDns; import android.net.nsd.INsdManager; +import android.net.nsd.MDnsManager; import android.net.nsd.NsdManager; /** @@ -78,5 +80,14 @@ public final class ConnectivityFrameworkInitializerTiramisu { return new EthernetManager(context, service); } ); + + SystemServiceRegistry.registerStaticService( + MDnsManager.MDNS_SERVICE, + MDnsManager.class, + (serviceBinder) -> { + IMDns service = IMDns.Stub.asInterface(serviceBinder); + return new MDnsManager(service); + } + ); } } diff --git a/framework-t/src/android/net/nsd/MDnsManager.java b/framework-t/src/android/net/nsd/MDnsManager.java new file mode 100644 index 0000000000..c11e60c49d --- /dev/null +++ b/framework-t/src/android/net/nsd/MDnsManager.java @@ -0,0 +1,200 @@ +/* + * Copyright (C) 2022 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 android.net.nsd; + +import android.annotation.NonNull; +import android.net.mdns.aidl.DiscoveryInfo; +import android.net.mdns.aidl.GetAddressInfo; +import android.net.mdns.aidl.IMDns; +import android.net.mdns.aidl.IMDnsEventListener; +import android.net.mdns.aidl.RegistrationInfo; +import android.net.mdns.aidl.ResolutionInfo; +import android.os.RemoteException; +import android.os.ServiceSpecificException; +import android.util.Log; + +/** + * A manager class for mdns service. + * + * @hide + */ +public class MDnsManager { + private static final String TAG = MDnsManager.class.getSimpleName(); + private final IMDns mMdns; + + /** Service name for this. */ + public static final String MDNS_SERVICE = "mdns"; + + private static final int NO_RESULT = -1; + private static final int NETID_UNSET = 0; + + public MDnsManager(IMDns mdns) { + mMdns = mdns; + } + + /** + * Start the MDNSResponder daemon. + */ + public void startDaemon() { + try { + mMdns.startDaemon(); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Start mdns failed.", e); + } + } + + /** + * Stop the MDNSResponder daemon. + */ + public void stopDaemon() { + try { + mMdns.stopDaemon(); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Stop mdns failed.", e); + } + } + + /** + * Start registering a service. + * + * @param id The operation ID. + * @param serviceName The service name to be registered. + * @param registrationType The service type to be registered. + * @param port The port on which the service accepts connections. + * @param txtRecord The txt record. Refer to {@code NsdServiceInfo#setTxtRecords} for details. + * @param interfaceIdx The interface index on which to register the service. + * @return {@code true} if registration is successful, else {@code false}. + */ + public boolean registerService(int id, @NonNull String serviceName, + @NonNull String registrationType, int port, @NonNull byte[] txtRecord, + int interfaceIdx) { + final RegistrationInfo info = new RegistrationInfo(id, NO_RESULT, serviceName, + registrationType, port, txtRecord, interfaceIdx); + try { + mMdns.registerService(info); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Register service failed.", e); + return false; + } + return true; + } + + /** + * Start discovering services. + * + * @param id The operation ID. + * @param registrationType The service type to be discovered. + * @param interfaceIdx The interface index on which to discover for services. + * @return {@code true} if discovery is started successfully, else {@code false}. + */ + public boolean discover(int id, @NonNull String registrationType, int interfaceIdx) { + final DiscoveryInfo info = new DiscoveryInfo(id, NO_RESULT, "" /* serviceName */, + registrationType, "" /* domainName */, interfaceIdx, NETID_UNSET); + try { + mMdns.discover(info); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Discover service failed.", e); + return false; + } + return true; + } + + /** + * Start resolving the target service. + * + * @param id The operation ID. + * @param serviceName The service name to be resolved. + * @param registrationType The service type to be resolved. + * @param domain The service domain to be resolved. + * @param interfaceIdx The interface index on which to resolve the service. + * @return {@code true} if resolution is started successfully, else {@code false}. + */ + public boolean resolve(int id, @NonNull String serviceName, @NonNull String registrationType, + @NonNull String domain, int interfaceIdx) { + final ResolutionInfo info = new ResolutionInfo(id, NO_RESULT, serviceName, + registrationType, domain, "" /* serviceFullName */, "" /* hostname */, 0 /* port */, + new byte[0] /* txtRecord */, interfaceIdx); + try { + mMdns.resolve(info); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Resolve service failed.", e); + return false; + } + return true; + } + + /** + * Start getting the target service address. + * + * @param id The operation ID. + * @param hostname The fully qualified domain name of the host to be queried for. + * @param interfaceIdx The interface index on which to issue the query. + * @return {@code true} if getting address is started successful, else {@code false}. + */ + public boolean getServiceAddress(int id, @NonNull String hostname, int interfaceIdx) { + final GetAddressInfo info = new GetAddressInfo(id, NO_RESULT, hostname, + "" /* address */, interfaceIdx, NETID_UNSET); + try { + mMdns.getServiceAddress(info); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Get service address failed.", e); + return false; + } + return true; + } + + /** + * Stop an operation which was requested before. + * + * @param id the operation id to be stopped. + * @return {@code true} if operation is stopped successfully, else {@code false}. + */ + public boolean stopOperation(int id) { + try { + mMdns.stopOperation(id); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Stop operation failed.", e); + return false; + } + return true; + } + + /** + * Register an event listener. + * + * @param listener The listener to be registered. + */ + public void registerEventListener(@NonNull IMDnsEventListener listener) { + try { + mMdns.registerEventListener(listener); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Register listener failed.", e); + } + } + + /** + * Unregister an event listener. + * + * @param listener The listener to be unregistered. + */ + public void unregisterEventListener(@NonNull IMDnsEventListener listener) { + try { + mMdns.unregisterEventListener(listener); + } catch (RemoteException | ServiceSpecificException e) { + Log.e(TAG, "Unregister listener failed.", e); + } + } +} diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java index 209f372fd3..33b44c8210 100644 --- a/framework-t/src/android/net/nsd/NsdManager.java +++ b/framework-t/src/android/net/nsd/NsdManager.java @@ -235,7 +235,7 @@ public final class NsdManager { public static final int DISABLE = 21; /** @hide */ - public static final int NATIVE_DAEMON_EVENT = 22; + public static final int MDNS_SERVICE_EVENT = 22; /** @hide */ public static final int REGISTER_CLIENT = 23; @@ -268,7 +268,7 @@ public final class NsdManager { EVENT_NAMES.put(DAEMON_STARTUP, "DAEMON_STARTUP"); EVENT_NAMES.put(ENABLE, "ENABLE"); EVENT_NAMES.put(DISABLE, "DISABLE"); - EVENT_NAMES.put(NATIVE_DAEMON_EVENT, "NATIVE_DAEMON_EVENT"); + EVENT_NAMES.put(MDNS_SERVICE_EVENT, "MDNS_SERVICE_EVENT"); } /** @hide */ diff --git a/framework-t/src/android/net/nsd/NsdServiceInfo.java b/framework-t/src/android/net/nsd/NsdServiceInfo.java index 8506db1fbe..2621594e98 100644 --- a/framework-t/src/android/net/nsd/NsdServiceInfo.java +++ b/framework-t/src/android/net/nsd/NsdServiceInfo.java @@ -24,7 +24,6 @@ import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; import android.util.ArrayMap; -import android.util.Base64; import android.util.Log; import java.io.UnsupportedEncodingException; @@ -106,13 +105,11 @@ public final class NsdServiceInfo implements Parcelable { /** * Unpack txt information from a base-64 encoded byte array. * - * @param rawRecords The raw base64 encoded records string read from netd. + * @param txtRecordsRawBytes The raw base64 encoded byte array. * * @hide */ - public void setTxtRecords(@NonNull String rawRecords) { - byte[] txtRecordsRawBytes = Base64.decode(rawRecords, Base64.DEFAULT); - + public void setTxtRecords(@NonNull byte[] txtRecordsRawBytes) { // There can be multiple TXT records after each other. Each record has to following format: // // byte type required meaning diff --git a/framework/Android.bp b/framework/Android.bp index f31a7d5421..3703df86cc 100644 --- a/framework/Android.bp +++ b/framework/Android.bp @@ -86,6 +86,7 @@ java_defaults { "net-utils-device-common", ], static_libs: [ + "mdns_aidl_interface-lateststable-java", "modules-utils-backgroundthread", "modules-utils-build", "modules-utils-preconditions", diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java index ddf6d2c4ab..995f8aeed1 100644 --- a/service-t/src/com/android/server/NsdService.java +++ b/service-t/src/com/android/server/NsdService.java @@ -16,15 +16,24 @@ package com.android.server; +import static android.net.ConnectivityManager.NETID_UNSET; +import static android.net.nsd.NsdManager.MDNS_SERVICE_EVENT; + 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.mdns.aidl.DiscoveryInfo; +import android.net.mdns.aidl.GetAddressInfo; +import android.net.mdns.aidl.IMDnsEventListener; +import android.net.mdns.aidl.RegistrationInfo; +import android.net.mdns.aidl.ResolutionInfo; import android.net.nsd.INsdManager; import android.net.nsd.INsdManagerCallback; import android.net.nsd.INsdServiceConnector; +import android.net.nsd.MDnsManager; import android.net.nsd.NsdManager; import android.net.nsd.NsdServiceInfo; import android.os.Handler; @@ -33,7 +42,6 @@ import android.os.IBinder; import android.os.Message; import android.os.RemoteException; import android.os.UserHandle; -import android.util.Base64; import android.util.Log; import android.util.Pair; import android.util.SparseArray; @@ -42,7 +50,6 @@ import android.util.SparseIntArray; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.State; import com.android.internal.util.StateMachine; -import com.android.net.module.util.DnsSdTxtRecord; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -50,9 +57,7 @@ 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; /** * Network Service Discovery Service handles remote service discovery operation requests by @@ -64,14 +69,18 @@ public class NsdService extends INsdManager.Stub { private static final String TAG = "NsdService"; private static final String MDNS_TAG = "mDnsConnector"; - private static final boolean DBG = true; + private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG); private static final long CLEANUP_DELAY_MS = 10000; private static final int IFACE_IDX_ANY = 0; private final Context mContext; private final NsdStateMachine mNsdStateMachine; - private final DaemonConnection mDaemon; - private final NativeCallbackReceiver mDaemonCallback; + private final MDnsManager mMDnsManager; + private final MDnsEventCallback mMDnsEventCallback; + // WARNING : Accessing this value in any thread is not safe, it must only be changed in the + // state machine thread. If change this outside state machine, it will need to introduce + // synchronization. + private boolean mIsDaemonStarted = false; /** * Clients receiving asynchronous messages @@ -100,10 +109,26 @@ public class NsdService extends INsdManager.Stub { } private void maybeStartDaemon() { - mDaemon.maybeStart(); + if (mIsDaemonStarted) { + if (DBG) Log.d(TAG, "Daemon is already started."); + return; + } + mMDnsManager.registerEventListener(mMDnsEventCallback); + mMDnsManager.startDaemon(); + mIsDaemonStarted = true; maybeScheduleStop(); } + private void maybeStopDaemon() { + if (!mIsDaemonStarted) { + if (DBG) Log.d(TAG, "Daemon has not been started."); + return; + } + mMDnsManager.unregisterEventListener(mMDnsEventCallback); + mMDnsManager.stopDaemon(); + mIsDaemonStarted = false; + } + private boolean isAnyRequestActive() { return mIdToClientInfoMap.size() != 0; } @@ -198,7 +223,7 @@ public class NsdService extends INsdManager.Stub { } break; case NsdManager.DAEMON_CLEANUP: - mDaemon.maybeStop(); + maybeStopDaemon(); break; // This event should be only sent by the legacy (target SDK < S) clients. // Mark the sending client as legacy. @@ -211,7 +236,6 @@ public class NsdService extends INsdManager.Stub { maybeStartDaemon(); } break; - case NsdManager.NATIVE_DAEMON_EVENT: default: Log.e(TAG, "Unhandled " + msg); return NOT_HANDLED; @@ -397,9 +421,8 @@ public class NsdService extends INsdManager.Stub { clientId, NsdManager.FAILURE_INTERNAL_ERROR); } break; - case NsdManager.NATIVE_DAEMON_EVENT: - NativeEvent event = (NativeEvent) msg.obj; - if (!handleNativeEvent(event.code, event.raw, event.cooked)) { + case MDNS_SERVICE_EVENT: + if (!handleMDnsServiceEvent(msg.arg1, msg.arg2, msg.obj)) { return NOT_HANDLED; } break; @@ -409,13 +432,11 @@ public class NsdService extends INsdManager.Stub { return HANDLED; } - private boolean handleNativeEvent(int code, String raw, String[] cooked) { + private boolean handleMDnsServiceEvent(int code, int id, Object obj) { NsdServiceInfo servInfo; - int id = Integer.parseInt(cooked[1]); ClientInfo clientInfo = mIdToClientInfoMap.get(id); if (clientInfo == null) { - String name = NativeResponseCode.nameOf(code); - Log.e(TAG, String.format("id %d for %s has no client mapping", id, name)); + Log.e(TAG, String.format("id %d for %d has no client mapping", id, code)); return false; } @@ -425,27 +446,20 @@ public class NsdService extends INsdManager.Stub { // This can happen because of race conditions. For example, // SERVICE_FOUND may race with STOP_SERVICE_DISCOVERY, // and we may get in this situation. - String name = NativeResponseCode.nameOf(code); - Log.d(TAG, String.format( - "Notification %s for listener id %d that is no longer active", - name, id)); + Log.d(TAG, String.format("%d for listener id %d that is no longer active", + code, id)); return false; } if (DBG) { - String name = NativeResponseCode.nameOf(code); - Log.d(TAG, String.format("Native daemon message %s: %s", name, raw)); + Log.d(TAG, String.format("MDns service event code:%d id=%d", code, id)); } switch (code) { - case NativeResponseCode.SERVICE_FOUND: - /* 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; - } + case IMDnsEventListener.SERVICE_FOUND: { + final DiscoveryInfo info = (DiscoveryInfo) obj; + final String name = info.serviceName; + final String type = info.registrationType; + servInfo = new NsdServiceInfo(name, type); + final int foundNetId = info.netId; 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 @@ -455,74 +469,65 @@ public class NsdService extends INsdManager.Stub { servInfo.setNetwork(new Network(foundNetId)); clientInfo.onServiceFound(clientId, servInfo); break; - case NativeResponseCode.SERVICE_LOST: - /* 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]); + } + case IMDnsEventListener.SERVICE_LOST: { + final DiscoveryInfo info = (DiscoveryInfo) obj; + final String name = info.serviceName; + final String type = info.registrationType; + final int lostNetId = info.netId; + servInfo = new NsdServiceInfo(name, type); // 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: - /* NNN uniqueId errorCode */ + } + case IMDnsEventListener.SERVICE_DISCOVERY_FAILED: clientInfo.onDiscoverServicesFailed( clientId, NsdManager.FAILURE_INTERNAL_ERROR); break; - case NativeResponseCode.SERVICE_REGISTERED: - /* NNN regId serviceName regType */ - servInfo = new NsdServiceInfo(cooked[2], null); + case IMDnsEventListener.SERVICE_REGISTERED: { + final RegistrationInfo info = (RegistrationInfo) obj; + final String name = info.serviceName; + servInfo = new NsdServiceInfo(name, null /* serviceType */); clientInfo.onRegisterServiceSucceeded(clientId, servInfo); break; - case NativeResponseCode.SERVICE_REGISTRATION_FAILED: - /* NNN regId errorCode */ + } + case IMDnsEventListener.SERVICE_REGISTRATION_FAILED: clientInfo.onRegisterServiceFailed( clientId, NsdManager.FAILURE_INTERNAL_ERROR); break; - case NativeResponseCode.SERVICE_UPDATED: - /* NNN regId */ - break; - case NativeResponseCode.SERVICE_UPDATE_FAILED: - /* NNN regId errorCode */ - break; - case NativeResponseCode.SERVICE_RESOLVED: - /* NNN resolveId fullName hostName port txtlen txtdata interfaceIdx */ + case IMDnsEventListener.SERVICE_RESOLVED: { + final ResolutionInfo info = (ResolutionInfo) obj; int index = 0; - while (index < cooked[2].length() && cooked[2].charAt(index) != '.') { - if (cooked[2].charAt(index) == '\\') { + final String fullName = info.serviceFullName; + while (index < fullName.length() && fullName.charAt(index) != '.') { + if (fullName.charAt(index) == '\\') { ++index; } ++index; } - if (index >= cooked[2].length()) { - Log.e(TAG, "Invalid service found " + raw); + if (index >= fullName.length()) { + Log.e(TAG, "Invalid service found " + fullName); break; } - String name = cooked[2].substring(0, index); - String rest = cooked[2].substring(index); + String name = fullName.substring(0, index); + String rest = fullName.substring(index); String type = rest.replace(".local.", ""); - name = unescape(name); - clientInfo.mResolvedService.setServiceName(name); clientInfo.mResolvedService.setServiceType(type); - clientInfo.mResolvedService.setPort(Integer.parseInt(cooked[4])); - clientInfo.mResolvedService.setTxtRecords(cooked[6]); + clientInfo.mResolvedService.setPort(info.port); + clientInfo.mResolvedService.setTxtRecords(info.txtRecord); // 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] /* interfaceIdx */)) { + final int id2 = getUniqueId(); + if (getAddrInfo(id2, info.hostname, info.interfaceIdx)) { storeRequestMap(clientId, id2, clientInfo, NsdManager.RESOLVE_SERVICE); } else { clientInfo.onResolveServiceFailed( @@ -530,7 +535,8 @@ public class NsdService extends INsdManager.Stub { clientInfo.mResolvedService = null; } break; - case NativeResponseCode.SERVICE_RESOLUTION_FAILED: + } + case IMDnsEventListener.SERVICE_RESOLUTION_FAILED: /* NNN resolveId errorCode */ stopResolveService(id); removeRequestMap(clientId, id, clientInfo); @@ -538,7 +544,7 @@ public class NsdService extends INsdManager.Stub { clientInfo.onResolveServiceFailed( clientId, NsdManager.FAILURE_INTERNAL_ERROR); break; - case NativeResponseCode.SERVICE_GET_ADDR_FAILED: + case IMDnsEventListener.SERVICE_GET_ADDR_FAILED: /* NNN resolveId errorCode */ stopGetAddrInfo(id); removeRequestMap(clientId, id, clientInfo); @@ -546,19 +552,15 @@ public class NsdService extends INsdManager.Stub { clientInfo.onResolveServiceFailed( clientId, NsdManager.FAILURE_INTERNAL_ERROR); break; - case NativeResponseCode.SERVICE_GET_ADDR_SUCCESS: + case IMDnsEventListener.SERVICE_GET_ADDR_SUCCESS: { /* NNN resolveId hostname ttl addr interfaceIdx netId */ - Network network = null; - try { - 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); - } - + final GetAddressInfo info = (GetAddressInfo) obj; + final String address = info.address; + final int netId = info.netId; + final Network network = netId == NETID_UNSET ? null : new Network(netId); InetAddress serviceHost = null; try { - serviceHost = InetAddress.getByName(cooked[4]); + serviceHost = InetAddress.getByName(address); } catch (UnknownHostException e) { Log.wtf(TAG, "Invalid host in GET_ADDR_SUCCESS", e); } @@ -579,6 +581,7 @@ public class NsdService extends INsdManager.Stub { removeRequestMap(clientId, id, clientInfo); clientInfo.mResolvedService = null; break; + } default: return false; } @@ -587,50 +590,66 @@ public class NsdService extends INsdManager.Stub { } } - private String unescape(String s) { - StringBuilder sb = new StringBuilder(s.length()); - for (int i = 0; i < s.length(); ++i) { - char c = s.charAt(i); - if (c == '\\') { - if (++i >= s.length()) { - Log.e(TAG, "Unexpected end of escape sequence in: " + s); - break; - } - c = s.charAt(i); - if (c != '.' && c != '\\') { - if (i + 2 >= s.length()) { - Log.e(TAG, "Unexpected end of escape sequence in: " + s); - break; - } - c = (char) ((c-'0') * 100 + (s.charAt(i+1)-'0') * 10 + (s.charAt(i+2)-'0')); - i += 2; - } - } - sb.append(c); - } - return sb.toString(); - } - @VisibleForTesting - NsdService(Context ctx, Handler handler, DaemonConnectionSupplier fn, long cleanupDelayMs) { + NsdService(Context ctx, Handler handler, long cleanupDelayMs) { mCleanupDelayMs = cleanupDelayMs; mContext = ctx; mNsdStateMachine = new NsdStateMachine(TAG, handler); mNsdStateMachine.start(); - mDaemonCallback = new NativeCallbackReceiver(); - mDaemon = fn.get(mDaemonCallback); + mMDnsManager = ctx.getSystemService(MDnsManager.class); + mMDnsEventCallback = new MDnsEventCallback(mNsdStateMachine); } public static NsdService create(Context context) throws InterruptedException { HandlerThread thread = new HandlerThread(TAG); thread.start(); Handler handler = new Handler(thread.getLooper()); - NsdService service = - new NsdService(context, handler, DaemonConnection::new, CLEANUP_DELAY_MS); - service.mDaemonCallback.awaitConnection(); + NsdService service = new NsdService(context, handler, CLEANUP_DELAY_MS); return service; } + private static class MDnsEventCallback extends IMDnsEventListener.Stub { + private final StateMachine mStateMachine; + + MDnsEventCallback(StateMachine sm) { + mStateMachine = sm; + } + + @Override + public void onServiceRegistrationStatus(final RegistrationInfo status) { + mStateMachine.sendMessage( + MDNS_SERVICE_EVENT, status.result, status.id, status); + } + + @Override + public void onServiceDiscoveryStatus(final DiscoveryInfo status) { + mStateMachine.sendMessage( + MDNS_SERVICE_EVENT, status.result, status.id, status); + } + + @Override + public void onServiceResolutionStatus(final ResolutionInfo status) { + mStateMachine.sendMessage( + MDNS_SERVICE_EVENT, status.result, status.id, status); + } + + @Override + public void onGettingServiceAddressStatus(final GetAddressInfo status) { + mStateMachine.sendMessage( + MDNS_SERVICE_EVENT, status.result, status.id, status); + } + + @Override + public int getInterfaceVersion() throws RemoteException { + return this.VERSION; + } + + @Override + public String getInterfaceHash() throws RemoteException { + return this.HASH; + } + } + @Override public INsdServiceConnector connect(INsdManagerCallback cb) { mContext.enforceCallingOrSelfPermission(android.Manifest.permission.INTERNET, "NsdService"); @@ -711,140 +730,6 @@ public class NsdService extends INsdManager.Stub { return mUniqueId; } - /* These should be in sync with system/netd/server/ResponseCode.h */ - static final class NativeResponseCode { - public static final int SERVICE_DISCOVERY_FAILED = 602; - public static final int SERVICE_FOUND = 603; - public static final int SERVICE_LOST = 604; - - public static final int SERVICE_REGISTRATION_FAILED = 605; - public static final int SERVICE_REGISTERED = 606; - - public static final int SERVICE_RESOLUTION_FAILED = 607; - public static final int SERVICE_RESOLVED = 608; - - public static final int SERVICE_UPDATED = 609; - public static final int SERVICE_UPDATE_FAILED = 610; - - public static final int SERVICE_GET_ADDR_FAILED = 611; - public static final int SERVICE_GET_ADDR_SUCCESS = 612; - - private static final SparseArray CODE_NAMES = new SparseArray<>(); - static { - CODE_NAMES.put(SERVICE_DISCOVERY_FAILED, "SERVICE_DISCOVERY_FAILED"); - CODE_NAMES.put(SERVICE_FOUND, "SERVICE_FOUND"); - CODE_NAMES.put(SERVICE_LOST, "SERVICE_LOST"); - CODE_NAMES.put(SERVICE_REGISTRATION_FAILED, "SERVICE_REGISTRATION_FAILED"); - CODE_NAMES.put(SERVICE_REGISTERED, "SERVICE_REGISTERED"); - CODE_NAMES.put(SERVICE_RESOLUTION_FAILED, "SERVICE_RESOLUTION_FAILED"); - CODE_NAMES.put(SERVICE_RESOLVED, "SERVICE_RESOLVED"); - CODE_NAMES.put(SERVICE_UPDATED, "SERVICE_UPDATED"); - CODE_NAMES.put(SERVICE_UPDATE_FAILED, "SERVICE_UPDATE_FAILED"); - CODE_NAMES.put(SERVICE_GET_ADDR_FAILED, "SERVICE_GET_ADDR_FAILED"); - CODE_NAMES.put(SERVICE_GET_ADDR_SUCCESS, "SERVICE_GET_ADDR_SUCCESS"); - } - - static String nameOf(int code) { - String name = CODE_NAMES.get(code); - if (name == null) { - return Integer.toString(code); - } - return name; - } - } - - private class NativeEvent { - final int code; - final String raw; - final String[] cooked; - - NativeEvent(int code, String raw, String[] cooked) { - this.code = code; - this.raw = raw; - this.cooked = cooked; - } - } - - class NativeCallbackReceiver implements INativeDaemonConnectorCallbacks { - private final CountDownLatch connected = new CountDownLatch(1); - - public void awaitConnection() throws InterruptedException { - connected.await(); - } - - @Override - public void onDaemonConnected() { - connected.countDown(); - } - - @Override - public boolean onCheckHoldWakeLock(int code) { - return false; - } - - @Override - public boolean onEvent(int code, String raw, String[] cooked) { - // TODO: NDC translates a message to a callback, we could enhance NDC to - // directly interact with a state machine through messages - NativeEvent event = new NativeEvent(code, raw, cooked); - mNsdStateMachine.sendMessage(NsdManager.NATIVE_DAEMON_EVENT, event); - return true; - } - } - - interface DaemonConnectionSupplier { - DaemonConnection get(NativeCallbackReceiver callback); - } - - @VisibleForTesting - public static class DaemonConnection { - final NativeDaemonConnector mNativeConnector; - boolean mIsStarted = false; - - DaemonConnection(NativeCallbackReceiver callback) { - mNativeConnector = new NativeDaemonConnector(callback, "mdns", 10, MDNS_TAG, 25, null); - new Thread(mNativeConnector, MDNS_TAG).start(); - } - - /** - * Executes the specified cmd on the daemon. - */ - public boolean execute(Object... args) { - if (DBG) { - Log.d(TAG, "mdnssd " + Arrays.toString(args)); - } - try { - mNativeConnector.execute("mdnssd", args); - } catch (NativeDaemonConnectorException e) { - Log.e(TAG, "Failed to execute mdnssd " + Arrays.toString(args), e); - return false; - } - return true; - } - - /** - * Starts the daemon if it is not already started. - */ - public void maybeStart() { - if (mIsStarted) { - return; - } - execute("start-service"); - mIsStarted = true; - } - - /** - * Stops the daemon if it is started. - */ - public void maybeStop() { - if (!mIsStarted) { - return; - } - execute("stop-service"); - mIsStarted = false; - } - } - private boolean registerService(int regId, NsdServiceInfo service) { if (DBG) { Log.d(TAG, "registerService: " + regId + " " + service); @@ -853,34 +738,26 @@ public class NsdService extends INsdManager.Stub { String type = service.getServiceType(); int port = service.getPort(); byte[] textRecord = service.getTxtRecord(); - String record = Base64.encodeToString(textRecord, Base64.DEFAULT).replace("\n", ""); - return mDaemon.execute("register", regId, name, type, port, record); + return mMDnsManager.registerService(regId, name, type, port, textRecord, IFACE_IDX_ANY); } private boolean unregisterService(int regId) { - return mDaemon.execute("stop-register", regId); - } - - private boolean updateService(int regId, DnsSdTxtRecord t) { - if (t == null) { - return false; - } - return mDaemon.execute("update", regId, t.size(), t.getRawData()); + return mMDnsManager.stopOperation(regId); } private boolean discoverServices(int discoveryId, NsdServiceInfo serviceInfo) { final Network network = serviceInfo.getNetwork(); + final String type = serviceInfo.getServiceType(); 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); + return mMDnsManager.discover(discoveryId, type, discoverInterface); } private boolean stopServiceDiscovery(int discoveryId) { - return mDaemon.execute("stop-discover", discoveryId); + return mMDnsManager.stopOperation(discoveryId); } private boolean resolveService(int resolveId, NsdServiceInfo service) { @@ -892,7 +769,7 @@ public class NsdService extends INsdManager.Stub { Log.e(TAG, "Interface to resolve service on not found"); return false; } - return mDaemon.execute("resolve", resolveId, name, type, "local.", resolveInterface); + return mMDnsManager.resolve(resolveId, name, type, "local.", resolveInterface); } /** @@ -933,16 +810,15 @@ public class NsdService extends INsdManager.Stub { } private boolean stopResolveService(int resolveId) { - return mDaemon.execute("stop-resolve", resolveId); + return mMDnsManager.stopOperation(resolveId); } - 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 getAddrInfo(int resolveId, String hostname, int interfaceIdx) { + return mMDnsManager.getServiceAddress(resolveId, hostname, interfaceIdx); } private boolean stopGetAddrInfo(int resolveId) { - return mDaemon.execute("stop-getaddrinfo", resolveId); + return mMDnsManager.stopOperation(resolveId); } @Override diff --git a/service/jarjar-rules.txt b/service/jarjar-rules.txt index e90b29b598..4b21569b23 100644 --- a/service/jarjar-rules.txt +++ b/service/jarjar-rules.txt @@ -108,5 +108,8 @@ rule com.android.networkstack.apishim.** com.android.connectivity.@0 # From filegroup framework-connectivity-protos rule android.service.*Proto com.android.connectivity.@0 +# From mdns-aidl-interface +rule android.net.mdns.aidl.** android.net.connectivity.@0 + # Remaining are connectivity sources in com.android.server and com.android.server.connectivity: # TODO: move to a subpackage of com.android.connectivity (such as com.android.connectivity.server) diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java index 5086943b23..3c228d0609 100644 --- a/tests/unit/java/com/android/server/NsdServiceTest.java +++ b/tests/unit/java/com/android/server/NsdServiceTest.java @@ -19,10 +19,12 @@ package com.android.server; import static libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges; import static libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; -import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.any; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -36,6 +38,7 @@ import android.content.ContentResolver; import android.content.Context; import android.net.nsd.INsdManagerCallback; import android.net.nsd.INsdServiceConnector; +import android.net.nsd.MDnsManager; import android.net.nsd.NsdManager; import android.net.nsd.NsdServiceInfo; import android.os.Binder; @@ -49,9 +52,6 @@ import android.os.Message; import androidx.annotation.NonNull; import androidx.test.filters.SmallTest; -import com.android.server.NsdService.DaemonConnection; -import com.android.server.NsdService.DaemonConnectionSupplier; -import com.android.server.NsdService.NativeCallbackReceiver; import com.android.testutils.DevSdkIgnoreRule; import com.android.testutils.DevSdkIgnoreRunner; import com.android.testutils.HandlerUtils; @@ -63,10 +63,8 @@ import org.junit.Test; import org.junit.rules.TestRule; import org.junit.runner.RunWith; import org.mockito.AdditionalAnswers; -import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.mockito.Spy; import java.util.LinkedList; import java.util.Queue; @@ -92,8 +90,7 @@ public class NsdServiceTest { public TestRule compatChangeRule = new PlatformCompatChangeRule(); @Mock Context mContext; @Mock ContentResolver mResolver; - NativeCallbackReceiver mDaemonCallback; - @Spy DaemonConnection mDaemon = new DaemonConnection(mDaemonCallback); + @Mock MDnsManager mMockMDnsM; HandlerThread mThread; TestHandler mHandler; @@ -112,9 +109,17 @@ public class NsdServiceTest { MockitoAnnotations.initMocks(this); mThread = new HandlerThread("mock-service-handler"); mThread.start(); - doReturn(true).when(mDaemon).execute(any()); mHandler = new TestHandler(mThread.getLooper()); when(mContext.getContentResolver()).thenReturn(mResolver); + doReturn(MDnsManager.MDNS_SERVICE).when(mContext) + .getSystemServiceName(MDnsManager.class); + doReturn(mMockMDnsM).when(mContext).getSystemService(MDnsManager.MDNS_SERVICE); + doReturn(true).when(mMockMDnsM).registerService( + anyInt(), anyString(), anyString(), anyInt(), any(), anyInt()); + doReturn(true).when(mMockMDnsM).stopOperation(anyInt()); + doReturn(true).when(mMockMDnsM).discover(anyInt(), anyString(), anyInt()); + doReturn(true).when(mMockMDnsM).resolve( + anyInt(), anyString(), anyString(), anyString(), anyInt()); } @After @@ -135,24 +140,25 @@ public class NsdServiceTest { waitForIdle(); final INsdManagerCallback cb1 = getCallback(); final IBinder.DeathRecipient deathRecipient1 = verifyLinkToDeath(cb1); - verify(mDaemon, times(1)).maybeStart(); - verifyDaemonCommands("start-service"); + verify(mMockMDnsM, times(1)).registerEventListener(any()); + verify(mMockMDnsM, times(1)).startDaemon(); connectClient(service); waitForIdle(); final INsdManagerCallback cb2 = getCallback(); final IBinder.DeathRecipient deathRecipient2 = verifyLinkToDeath(cb2); - verify(mDaemon, times(1)).maybeStart(); + // Daemon has been started, it should not try to start it again. + verify(mMockMDnsM, times(1)).registerEventListener(any()); + verify(mMockMDnsM, times(1)).startDaemon(); deathRecipient1.binderDied(); // Still 1 client remains, daemon shouldn't be stopped. waitForIdle(); - verify(mDaemon, never()).maybeStop(); + verify(mMockMDnsM, never()).stopDaemon(); deathRecipient2.binderDied(); // All clients are disconnected, the daemon should be stopped. verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS); - verifyDaemonCommands("stop-service"); } @Test @@ -160,28 +166,30 @@ public class NsdServiceTest { public void testNoDaemonStartedWhenClientsConnect() throws Exception { final NsdService service = makeService(); - // Creating an NsdManager will not cause any cmds executed, which means - // no daemon is started. + // Creating an NsdManager will not cause daemon startup. connectClient(service); waitForIdle(); - verify(mDaemon, never()).execute(any()); + verify(mMockMDnsM, never()).registerEventListener(any()); + verify(mMockMDnsM, never()).startDaemon(); final INsdManagerCallback cb1 = getCallback(); final IBinder.DeathRecipient deathRecipient1 = verifyLinkToDeath(cb1); - // Creating another NsdManager will not cause any cmds executed. + // Creating another NsdManager will not cause daemon startup either. connectClient(service); waitForIdle(); - verify(mDaemon, never()).execute(any()); + verify(mMockMDnsM, never()).registerEventListener(any()); + verify(mMockMDnsM, never()).startDaemon(); final INsdManagerCallback cb2 = getCallback(); final IBinder.DeathRecipient deathRecipient2 = verifyLinkToDeath(cb2); - // If there is no active request, try to clean up the daemon - // every time the client disconnects. + // If there is no active request, try to clean up the daemon but should not do it because + // daemon has not been started. deathRecipient1.binderDied(); - verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS); - reset(mDaemon); + verify(mMockMDnsM, never()).unregisterEventListener(any()); + verify(mMockMDnsM, never()).stopDaemon(); deathRecipient2.binderDied(); - verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS); + verify(mMockMDnsM, never()).unregisterEventListener(any()); + verify(mMockMDnsM, never()).stopDaemon(); } private IBinder.DeathRecipient verifyLinkToDeath(INsdManagerCallback cb) @@ -200,8 +208,8 @@ public class NsdServiceTest { waitForIdle(); final INsdManagerCallback cb1 = getCallback(); final IBinder.DeathRecipient deathRecipient = verifyLinkToDeath(cb1); - verify(mDaemon, never()).maybeStart(); - verify(mDaemon, never()).execute(any()); + verify(mMockMDnsM, never()).registerEventListener(any()); + verify(mMockMDnsM, never()).startDaemon(); NsdServiceInfo request = new NsdServiceInfo("a_name", "a_type"); request.setPort(2201); @@ -210,29 +218,31 @@ public class NsdServiceTest { NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class); client.registerService(request, PROTOCOL, listener1); waitForIdle(); - verify(mDaemon, times(1)).maybeStart(); - verifyDaemonCommands("start-service", "register 2 a_name a_type 2201"); + verify(mMockMDnsM, times(1)).registerEventListener(any()); + verify(mMockMDnsM, times(1)).startDaemon(); + verify(mMockMDnsM, times(1)).registerService( + eq(2), eq("a_name"), eq("a_type"), eq(2201), any(), eq(0)); // Client discovery request NsdManager.DiscoveryListener listener2 = mock(NsdManager.DiscoveryListener.class); client.discoverServices("a_type", PROTOCOL, listener2); waitForIdle(); - verify(mDaemon, times(1)).maybeStart(); - verifyDaemonCommand("discover 3 a_type 0"); + verify(mMockMDnsM, times(1)).discover(eq(3), eq("a_type"), eq(0)); // Client resolve request NsdManager.ResolveListener listener3 = mock(NsdManager.ResolveListener.class); client.resolveService(request, listener3); waitForIdle(); - verify(mDaemon, times(1)).maybeStart(); - verifyDaemonCommand("resolve 4 a_name a_type local. 0"); + verify(mMockMDnsM, times(1)).resolve( + eq(4), eq("a_name"), eq("a_type"), eq("local."), eq(0)); // Client disconnects, stop the daemon after CLEANUP_DELAY_MS. deathRecipient.binderDied(); verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS); // checks that request are cleaned - verifyDaemonCommands("stop-register 2", "stop-discover 3", - "stop-resolve 4", "stop-service"); + verify(mMockMDnsM, times(1)).stopOperation(eq(2)); + verify(mMockMDnsM, times(1)).stopOperation(eq(3)); + verify(mMockMDnsM, times(1)).stopOperation(eq(4)); } @Test @@ -246,20 +256,23 @@ public class NsdServiceTest { NsdManager.RegistrationListener listener1 = mock(NsdManager.RegistrationListener.class); client.registerService(request, PROTOCOL, listener1); waitForIdle(); - verify(mDaemon, times(1)).maybeStart(); + verify(mMockMDnsM, times(1)).registerEventListener(any()); + verify(mMockMDnsM, times(1)).startDaemon(); final INsdManagerCallback cb1 = getCallback(); final IBinder.DeathRecipient deathRecipient = verifyLinkToDeath(cb1); - verifyDaemonCommands("start-service", "register 2 a_name a_type 2201"); + verify(mMockMDnsM, times(1)).registerService( + eq(2), eq("a_name"), eq("a_type"), eq(2201), any(), eq(0)); client.unregisterService(listener1); - verifyDaemonCommand("stop-register 2"); + waitForIdle(); + verify(mMockMDnsM, times(1)).stopOperation(eq(2)); verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS); - verifyDaemonCommand("stop-service"); - reset(mDaemon); + reset(mMockMDnsM); deathRecipient.binderDied(); - // Client disconnects, after CLEANUP_DELAY_MS, maybeStop the daemon. - verifyDelayMaybeStopDaemon(CLEANUP_DELAY_MS); + // Client disconnects, daemon should not be stopped after CLEANUP_DELAY_MS. + verify(mMockMDnsM, never()).unregisterEventListener(any()); + verify(mMockMDnsM, never()).stopDaemon(); } private void waitForIdle() { @@ -267,11 +280,7 @@ public class NsdServiceTest { } NsdService makeService() { - DaemonConnectionSupplier supplier = (callback) -> { - mDaemonCallback = callback; - return mDaemon; - }; - final NsdService service = new NsdService(mContext, mHandler, supplier, CLEANUP_DELAY_MS) { + final NsdService service = new NsdService(mContext, mHandler, CLEANUP_DELAY_MS) { @Override public INsdServiceConnector connect(INsdManagerCallback baseCb) { // Wrap the callback in a transparent mock, to mock asBinder returning a @@ -285,7 +294,6 @@ public class NsdServiceTest { return super.connect(cb); } }; - verify(mDaemon, never()).execute(any(String.class)); return service; } @@ -297,34 +305,15 @@ public class NsdServiceTest { return new NsdManager(mContext, service); } - void verifyDelayMaybeStopDaemon(long cleanupDelayMs) { + void verifyDelayMaybeStopDaemon(long cleanupDelayMs) throws Exception { waitForIdle(); // Stop daemon shouldn't be called immediately. - verify(mDaemon, never()).maybeStop(); + verify(mMockMDnsM, never()).unregisterEventListener(any()); + verify(mMockMDnsM, never()).stopDaemon(); + // Clean up the daemon after CLEANUP_DELAY_MS. - verify(mDaemon, timeout(cleanupDelayMs + TIMEOUT_MS)).maybeStop(); - } - - void verifyDaemonCommands(String... wants) { - verifyDaemonCommand(String.join(" ", wants), wants.length); - } - - void verifyDaemonCommand(String want) { - verifyDaemonCommand(want, 1); - } - - void verifyDaemonCommand(String want, int n) { - waitForIdle(); - final ArgumentCaptor argumentsCaptor = ArgumentCaptor.forClass(Object.class); - verify(mDaemon, times(n)).execute(argumentsCaptor.capture()); - String got = ""; - for (Object o : argumentsCaptor.getAllValues()) { - got += o + " "; - } - assertEquals(want, got.trim()); - // rearm deamon for next command verification - reset(mDaemon); - doReturn(true).when(mDaemon).execute(any()); + verify(mMockMDnsM, timeout(cleanupDelayMs + TIMEOUT_MS)).unregisterEventListener(any()); + verify(mMockMDnsM, timeout(cleanupDelayMs + TIMEOUT_MS)).stopDaemon(); } public static class TestHandler extends Handler {