diff --git a/framework-t/api/current.txt b/framework-t/api/current.txt index eb77288c7e..ed841b87ed 100644 --- a/framework-t/api/current.txt +++ b/framework-t/api/current.txt @@ -195,12 +195,14 @@ package android.net.nsd { method public void resolveService(android.net.nsd.NsdServiceInfo, android.net.nsd.NsdManager.ResolveListener); method public void resolveService(@NonNull android.net.nsd.NsdServiceInfo, @NonNull java.util.concurrent.Executor, @NonNull android.net.nsd.NsdManager.ResolveListener); method public void stopServiceDiscovery(android.net.nsd.NsdManager.DiscoveryListener); + method public void stopServiceResolution(@NonNull android.net.nsd.NsdManager.ResolveListener); method public void unregisterService(android.net.nsd.NsdManager.RegistrationListener); field public static final String ACTION_NSD_STATE_CHANGED = "android.net.nsd.STATE_CHANGED"; field public static final String EXTRA_NSD_STATE = "nsd_state"; field public static final int FAILURE_ALREADY_ACTIVE = 3; // 0x3 field public static final int FAILURE_INTERNAL_ERROR = 0; // 0x0 field public static final int FAILURE_MAX_LIMIT = 4; // 0x4 + field public static final int FAILURE_OPERATION_NOT_RUNNING = 5; // 0x5 field public static final int NSD_STATE_DISABLED = 1; // 0x1 field public static final int NSD_STATE_ENABLED = 2; // 0x2 field public static final int PROTOCOL_DNS_SD = 1; // 0x1 @@ -224,7 +226,9 @@ package android.net.nsd { public static interface NsdManager.ResolveListener { method public void onResolveFailed(android.net.nsd.NsdServiceInfo, int); + method public default void onResolveStopped(@NonNull android.net.nsd.NsdServiceInfo); method public void onServiceResolved(android.net.nsd.NsdServiceInfo); + method public default void onStopResolutionFailed(@NonNull android.net.nsd.NsdServiceInfo, int); } public final class NsdServiceInfo implements android.os.Parcelable { diff --git a/framework-t/src/android/net/nsd/INsdManagerCallback.aidl b/framework-t/src/android/net/nsd/INsdManagerCallback.aidl index 1a262ec0e9..669efc9730 100644 --- a/framework-t/src/android/net/nsd/INsdManagerCallback.aidl +++ b/framework-t/src/android/net/nsd/INsdManagerCallback.aidl @@ -36,4 +36,6 @@ oneway interface INsdManagerCallback { void onUnregisterServiceSucceeded(int listenerKey); void onResolveServiceFailed(int listenerKey, int error); void onResolveServiceSucceeded(int listenerKey, in NsdServiceInfo info); + void onStopResolutionFailed(int listenerKey, int error); + void onStopResolutionSucceeded(int listenerKey); } diff --git a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl index b06ae55b15..a28fd7d014 100644 --- a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl +++ b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl @@ -32,4 +32,5 @@ interface INsdServiceConnector { void stopDiscovery(int listenerKey); void resolveService(int listenerKey, in NsdServiceInfo serviceInfo); void startDaemon(); + void stopResolution(int listenerKey); } \ No newline at end of file diff --git a/framework-t/src/android/net/nsd/NsdManager.java b/framework-t/src/android/net/nsd/NsdManager.java index 45def3674c..1a5a66788a 100644 --- a/framework-t/src/android/net/nsd/NsdManager.java +++ b/framework-t/src/android/net/nsd/NsdManager.java @@ -16,6 +16,7 @@ package android.net.nsd; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -44,6 +45,8 @@ import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.Objects; import java.util.concurrent.Executor; @@ -230,7 +233,6 @@ public final class NsdManager { /** @hide */ public static final int DAEMON_CLEANUP = 18; - /** @hide */ public static final int DAEMON_STARTUP = 19; @@ -245,6 +247,13 @@ public final class NsdManager { /** @hide */ public static final int MDNS_DISCOVERY_MANAGER_EVENT = 23; + /** @hide */ + public static final int STOP_RESOLUTION = 24; + /** @hide */ + public static final int STOP_RESOLUTION_FAILED = 25; + /** @hide */ + public static final int STOP_RESOLUTION_SUCCEEDED = 26; + /** Dns based service discovery protocol */ public static final int PROTOCOL_DNS_SD = 0x0001; @@ -270,6 +279,9 @@ public final class NsdManager { EVENT_NAMES.put(DAEMON_CLEANUP, "DAEMON_CLEANUP"); EVENT_NAMES.put(DAEMON_STARTUP, "DAEMON_STARTUP"); EVENT_NAMES.put(MDNS_SERVICE_EVENT, "MDNS_SERVICE_EVENT"); + EVENT_NAMES.put(STOP_RESOLUTION, "STOP_RESOLUTION"); + EVENT_NAMES.put(STOP_RESOLUTION_FAILED, "STOP_RESOLUTION_FAILED"); + EVENT_NAMES.put(STOP_RESOLUTION_SUCCEEDED, "STOP_RESOLUTION_SUCCEEDED"); } /** @hide */ @@ -595,6 +607,16 @@ public final class NsdManager { public void onResolveServiceSucceeded(int listenerKey, NsdServiceInfo info) { sendInfo(RESOLVE_SERVICE_SUCCEEDED, listenerKey, info); } + + @Override + public void onStopResolutionFailed(int listenerKey, int error) { + sendError(STOP_RESOLUTION_FAILED, listenerKey, error); + } + + @Override + public void onStopResolutionSucceeded(int listenerKey) { + sendNoArg(STOP_RESOLUTION_SUCCEEDED, listenerKey); + } } /** @@ -618,6 +640,20 @@ public final class NsdManager { */ public static final int FAILURE_MAX_LIMIT = 4; + /** + * Indicates that the stop operation failed because it is not running. + * This failure is passed with {@link ResolveListener#onStopResolutionFailed}. + */ + public static final int FAILURE_OPERATION_NOT_RUNNING = 5; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = { + FAILURE_OPERATION_NOT_RUNNING, + }) + public @interface StopOperationFailureCode { + } + /** Interface for callback invocation for service discovery */ public interface DiscoveryListener { @@ -646,12 +682,49 @@ public final class NsdManager { public void onServiceUnregistered(NsdServiceInfo serviceInfo); } - /** Interface for callback invocation for service resolution */ + /** + * Callback for use with {@link NsdManager#resolveService} to resolve the service info and use + * with {@link NsdManager#stopServiceResolution} to stop resolution. + */ public interface ResolveListener { - public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode); + /** + * Called on the internal thread or with an executor passed to + * {@link NsdManager#resolveService} to report the resolution was failed with an error. + * + * A resolution operation would call either onServiceResolved or onResolveFailed once based + * on the result. + */ + void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode); - public void onServiceResolved(NsdServiceInfo serviceInfo); + /** + * Called on the internal thread or with an executor passed to + * {@link NsdManager#resolveService} to report the resolved service info. + * + * A resolution operation would call either onServiceResolved or onResolveFailed once based + * on the result. + */ + void onServiceResolved(NsdServiceInfo serviceInfo); + + /** + * Called on the internal thread or with an executor passed to + * {@link NsdManager#resolveService} to report the resolution was stopped. + * + * A stop resolution operation would call either onResolveStopped or onStopResolutionFailed + * once based on the result. + */ + default void onResolveStopped(@NonNull NsdServiceInfo serviceInfo) { } + + /** + * Called once on the internal thread or with an executor passed to + * {@link NsdManager#resolveService} to report that stopping resolution failed with an + * error. + * + * A stop resolution operation would call either onResolveStopped or onStopResolutionFailed + * once based on the result. + */ + default void onStopResolutionFailed(@NonNull NsdServiceInfo serviceInfo, + @StopOperationFailureCode int errorCode) { } } @VisibleForTesting @@ -744,6 +817,16 @@ public final class NsdManager { executor.execute(() -> ((ResolveListener) listener).onServiceResolved( (NsdServiceInfo) obj)); break; + case STOP_RESOLUTION_FAILED: + removeListener(key); + executor.execute(() -> ((ResolveListener) listener).onStopResolutionFailed( + ns, errorCode)); + break; + case STOP_RESOLUTION_SUCCEEDED: + removeListener(key); + executor.execute(() -> ((ResolveListener) listener).onResolveStopped( + ns)); + break; default: Log.d(TAG, "Ignored " + message); break; @@ -1079,6 +1162,29 @@ public final class NsdManager { } } + /** + * Stop service resolution initiated with {@link #resolveService}. + * + * A successful stop is notified with a call to {@link ResolveListener#onResolveStopped}. + * + *

Upon failure to stop service resolution for example if resolution is done or the + * requester stops resolution repeatedly, the application is notified + * {@link ResolveListener#onStopResolutionFailed} with {@link #FAILURE_OPERATION_NOT_RUNNING} + * + * @param listener This should be a listener object that was passed to {@link #resolveService}. + * It identifies the resolution that should be stopped and notifies of a + * successful or unsuccessful stop. Throws {@code IllegalArgumentException} if + * the listener was not passed to resolveService before. + */ + public void stopServiceResolution(@NonNull ResolveListener listener) { + int id = getListenerKey(listener); + try { + mService.stopResolution(id); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + private static void checkListener(Object listener) { Objects.requireNonNull(listener, "listener cannot be null"); } diff --git a/service-t/src/com/android/server/NsdService.java b/service-t/src/com/android/server/NsdService.java index 9510c12f5f..ce105cef4a 100644 --- a/service-t/src/com/android/server/NsdService.java +++ b/service-t/src/com/android/server/NsdService.java @@ -405,6 +405,13 @@ public class NsdService extends INsdManager.Stub { clientId, NsdManager.FAILURE_INTERNAL_ERROR); } break; + case NsdManager.STOP_RESOLUTION: + cInfo = getClientInfoForReply(msg); + if (cInfo != null) { + cInfo.onStopResolutionFailed( + clientId, NsdManager.FAILURE_OPERATION_NOT_RUNNING); + } + break; case NsdManager.DAEMON_CLEANUP: maybeStopDaemon(); break; @@ -763,6 +770,29 @@ public class NsdService extends INsdManager.Stub { } break; } + case NsdManager.STOP_RESOLUTION: + if (DBG) Log.d(TAG, "Stop service resolution"); + args = (ListenerArgs) msg.obj; + clientInfo = mClients.get(args.connector); + // If the binder death notification for a INsdManagerCallback was received + // before any calls are received by NsdService, the clientInfo would be + // cleared and cause NPE. Add a null check here to prevent this corner case. + if (clientInfo == null) { + Log.e(TAG, "Unknown connector in stop resolution"); + break; + } + + id = clientInfo.mClientIds.get(clientId); + removeRequestMap(clientId, id, clientInfo); + if (stopResolveService(id)) { + clientInfo.onStopResolutionSucceeded(clientId); + } else { + clientInfo.onStopResolutionFailed( + clientId, NsdManager.FAILURE_OPERATION_NOT_RUNNING); + } + clientInfo.mResolvedService = null; + // TODO: Implement the stop resolution with MdnsDiscoveryManager. + break; case MDNS_SERVICE_EVENT: if (!handleMDnsServiceEvent(msg.arg1, msg.arg2, msg.obj)) { return NOT_HANDLED; @@ -1306,6 +1336,12 @@ public class NsdService extends INsdManager.Stub { new ListenerArgs(this, serviceInfo))); } + @Override + public void stopResolution(int listenerKey) { + mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage( + NsdManager.STOP_RESOLUTION, 0, listenerKey, new ListenerArgs(this, null))); + } + @Override public void startDaemon() { mNsdStateMachine.sendMessage(mNsdStateMachine.obtainMessage( @@ -1643,5 +1679,21 @@ public class NsdService extends INsdManager.Stub { Log.e(TAG, "Error calling onResolveServiceSucceeded", e); } } + + void onStopResolutionFailed(int listenerKey, int error) { + try { + mCb.onStopResolutionFailed(listenerKey, error); + } catch (RemoteException e) { + Log.e(TAG, "Error calling onStopResolutionFailed", e); + } + } + + void onStopResolutionSucceeded(int listenerKey) { + try { + mCb.onStopResolutionSucceeded(listenerKey); + } catch (RemoteException e) { + Log.e(TAG, "Error calling onStopResolutionSucceeded", e); + } + } } } diff --git a/tests/unit/java/com/android/server/NsdServiceTest.java b/tests/unit/java/com/android/server/NsdServiceTest.java index 9edfbe6227..a8f8121a7e 100644 --- a/tests/unit/java/com/android/server/NsdServiceTest.java +++ b/tests/unit/java/com/android/server/NsdServiceTest.java @@ -18,6 +18,7 @@ package com.android.server; import static android.net.InetAddresses.parseNumericAddress; import static android.net.nsd.NsdManager.FAILURE_INTERNAL_ERROR; +import static android.net.nsd.NsdManager.FAILURE_OPERATION_NOT_RUNNING; import static com.android.testutils.ContextUtils.mockService; @@ -69,6 +70,7 @@ import android.os.HandlerThread; import android.os.IBinder; import android.os.Looper; import android.os.Message; +import android.os.RemoteException; import androidx.annotation.NonNull; import androidx.test.filters.SmallTest; @@ -575,6 +577,95 @@ public class NsdServiceTest { anyInt()/* interfaceIdx */); } + @Test + public void testStopServiceResolution() { + final NsdManager client = connectClient(mService); + final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE); + final ResolveListener resolveListener = mock(ResolveListener.class); + client.resolveService(request, resolveListener); + waitForIdle(); + + final ArgumentCaptor resolvIdCaptor = ArgumentCaptor.forClass(Integer.class); + verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(SERVICE_NAME), eq(SERVICE_TYPE), + eq("local.") /* domain */, eq(IFACE_IDX_ANY)); + + final int resolveId = resolvIdCaptor.getValue(); + client.stopServiceResolution(resolveListener); + waitForIdle(); + + verify(mMockMDnsM).stopOperation(resolveId); + verify(resolveListener, timeout(TIMEOUT_MS)).onResolveStopped(argThat(ns -> + request.getServiceName().equals(ns.getServiceName()) + && request.getServiceType().equals(ns.getServiceType()))); + } + + @Test + public void testStopResolutionFailed() { + final NsdManager client = connectClient(mService); + final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE); + final ResolveListener resolveListener = mock(ResolveListener.class); + client.resolveService(request, resolveListener); + waitForIdle(); + + final ArgumentCaptor resolvIdCaptor = ArgumentCaptor.forClass(Integer.class); + verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(SERVICE_NAME), eq(SERVICE_TYPE), + eq("local.") /* domain */, eq(IFACE_IDX_ANY)); + + final int resolveId = resolvIdCaptor.getValue(); + doReturn(false).when(mMockMDnsM).stopOperation(anyInt()); + client.stopServiceResolution(resolveListener); + waitForIdle(); + + verify(mMockMDnsM).stopOperation(resolveId); + verify(resolveListener, timeout(TIMEOUT_MS)).onStopResolutionFailed(argThat(ns -> + request.getServiceName().equals(ns.getServiceName()) + && request.getServiceType().equals(ns.getServiceType())), + eq(FAILURE_OPERATION_NOT_RUNNING)); + } + + @Test @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.TIRAMISU) + public void testStopResolutionDuringGettingAddress() throws RemoteException { + final NsdManager client = connectClient(mService); + final NsdServiceInfo request = new NsdServiceInfo(SERVICE_NAME, SERVICE_TYPE); + final ResolveListener resolveListener = mock(ResolveListener.class); + client.resolveService(request, resolveListener); + waitForIdle(); + + final IMDnsEventListener eventListener = getEventListener(); + final ArgumentCaptor resolvIdCaptor = ArgumentCaptor.forClass(Integer.class); + verify(mMockMDnsM).resolve(resolvIdCaptor.capture(), eq(SERVICE_NAME), eq(SERVICE_TYPE), + eq("local.") /* domain */, eq(IFACE_IDX_ANY)); + + // Resolve service successfully. + final ResolutionInfo resolutionInfo = new ResolutionInfo( + resolvIdCaptor.getValue(), + IMDnsEventListener.SERVICE_RESOLVED, + null /* serviceName */, + null /* serviceType */, + null /* domain */, + SERVICE_FULL_NAME, + DOMAIN_NAME, + PORT, + new byte[0] /* txtRecord */, + IFACE_IDX_ANY); + doReturn(true).when(mMockMDnsM).getServiceAddress(anyInt(), any(), anyInt()); + eventListener.onServiceResolutionStatus(resolutionInfo); + waitForIdle(); + + final ArgumentCaptor getAddrIdCaptor = ArgumentCaptor.forClass(Integer.class); + verify(mMockMDnsM).getServiceAddress(getAddrIdCaptor.capture(), eq(DOMAIN_NAME), + eq(IFACE_IDX_ANY)); + + final int getAddrId = getAddrIdCaptor.getValue(); + client.stopServiceResolution(resolveListener); + waitForIdle(); + + verify(mMockMDnsM).stopOperation(getAddrId); + verify(resolveListener, timeout(TIMEOUT_MS)).onResolveStopped(argThat(ns -> + request.getServiceName().equals(ns.getServiceName()) + && request.getServiceType().equals(ns.getServiceType()))); + } + private void makeServiceWithMdnsDiscoveryManagerEnabled() { doReturn(true).when(mDeps).isMdnsDiscoveryManagerEnabled(any(Context.class)); doReturn(mDiscoveryManager).when(mDeps).makeMdnsDiscoveryManager(any(), any());