Merge "New API to stop service resolution" am: fd02056713 am: b0a6390b7d

Original change: https://android-review.googlesource.com/c/platform/packages/modules/Connectivity/+/2369237

Change-Id: I5ec56d59a1188038797ae5ddb9d9a094ccdb584b
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/framework-t/api/current.txt b/framework-t/api/current.txt
index eb77288..ed841b8 100644
--- a/framework-t/api/current.txt
+++ b/framework-t/api/current.txt
@@ -195,12 +195,14 @@
     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 @@
 
   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 1a262ec..669efc9 100644
--- a/framework-t/src/android/net/nsd/INsdManagerCallback.aidl
+++ b/framework-t/src/android/net/nsd/INsdManagerCallback.aidl
@@ -36,4 +36,6 @@
     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 b06ae55..a28fd7d 100644
--- a/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
+++ b/framework-t/src/android/net/nsd/INsdServiceConnector.aidl
@@ -32,4 +32,5 @@
     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 45def36..1a5a667 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 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 @@
 
     /** @hide */
     public static final int DAEMON_CLEANUP                          = 18;
-
     /** @hide */
     public static final int DAEMON_STARTUP                          = 19;
 
@@ -245,6 +247,13 @@
     /** @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 @@
         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 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 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 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 @@
                     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 @@
         }
     }
 
+    /**
+     * Stop service resolution initiated with {@link #resolveService}.
+     *
+     * A successful stop is notified with a call to {@link ResolveListener#onResolveStopped}.
+     *
+     * <p> 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 9510c12..ce105ce 100644
--- a/service-t/src/com/android/server/NsdService.java
+++ b/service-t/src/com/android/server/NsdService.java
@@ -405,6 +405,13 @@
                                     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 @@
                         }
                         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;
@@ -1307,6 +1337,12 @@
         }
 
         @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(
                     NsdManager.DAEMON_STARTUP, new ListenerArgs(this, null)));
@@ -1643,5 +1679,21 @@
                 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 9edfbe6..a8f8121 100644
--- a/tests/unit/java/com/android/server/NsdServiceTest.java
+++ b/tests/unit/java/com/android/server/NsdServiceTest.java
@@ -18,6 +18,7 @@
 
 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.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 @@
                 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<Integer> 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<Integer> 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<Integer> 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<Integer> 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());