Android: Synchronizing on the use of NsdManager.resolveService() (#23515)

* Android: Synchronizing on the use of NsdManager.resolveService()

* Incorporating comments from andy31415@
diff --git a/src/platform/android/java/chip/platform/NsdManagerServiceResolver.java b/src/platform/android/java/chip/platform/NsdManagerServiceResolver.java
index a3a4ecf..11b6241 100644
--- a/src/platform/android/java/chip/platform/NsdManagerServiceResolver.java
+++ b/src/platform/android/java/chip/platform/NsdManagerServiceResolver.java
@@ -25,9 +25,13 @@
 import android.os.Handler;
 import android.os.Looper;
 import android.util.Log;
+import androidx.annotation.Nullable;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 public class NsdManagerServiceResolver implements ServiceResolver {
   private static final String TAG = NsdManagerServiceResolver.class.getSimpleName();
@@ -37,8 +41,15 @@
   private Handler mainThreadHandler;
   private List<NsdManager.RegistrationListener> registrationListeners = new ArrayList<>();
   private final CopyOnWriteArrayList<String> mMFServiceName = new CopyOnWriteArrayList<>();
+  @Nullable private final NsdManagerResolverAvailState nsdManagerResolverAvailState;
 
-  public NsdManagerServiceResolver(Context context) {
+  /**
+   * @param context application context
+   * @param nsdManagerResolverAvailState Passing NsdManagerResolverAvailState allows
+   *     NsdManagerServiceResolver to synchronize on the usage of NsdManager's resolveService() API
+   */
+  public NsdManagerServiceResolver(
+      Context context, @Nullable NsdManagerResolverAvailState nsdManagerResolverAvailState) {
     this.nsdManager = (NsdManager) context.getSystemService(Context.NSD_SERVICE);
     this.mainThreadHandler = new Handler(Looper.getMainLooper());
 
@@ -46,6 +57,11 @@
         ((WifiManager) context.getSystemService(Context.WIFI_SERVICE))
             .createMulticastLock("chipMulticastLock");
     this.multicastLock.setReferenceCounted(true);
+    this.nsdManagerResolverAvailState = nsdManagerResolverAvailState;
+  }
+
+  public NsdManagerServiceResolver(Context context) {
+    this(context, null);
   }
 
   @Override
@@ -78,10 +94,18 @@
             Log.d(TAG, "resolve: Timing out");
             if (multicastLock.isHeld()) {
               multicastLock.release();
+
+              if (nsdManagerResolverAvailState != null) {
+                nsdManagerResolverAvailState.signalFree();
+              }
             }
           }
         };
 
+    if (nsdManagerResolverAvailState != null) {
+      nsdManagerResolverAvailState.acquireResolver();
+    }
+
     this.nsdManager.resolveService(
         serviceInfo,
         new NsdManager.ResolveListener() {
@@ -95,6 +119,10 @@
 
             if (multicastLock.isHeld()) {
               multicastLock.release();
+
+              if (nsdManagerResolverAvailState != null) {
+                nsdManagerResolverAvailState.signalFree();
+              }
             }
             mainThreadHandler.removeCallbacks(timeoutRunnable);
           }
@@ -120,10 +148,15 @@
 
             if (multicastLock.isHeld()) {
               multicastLock.release();
+
+              if (nsdManagerResolverAvailState != null) {
+                nsdManagerResolverAvailState.signalFree();
+              }
             }
             mainThreadHandler.removeCallbacks(timeoutRunnable);
           }
         });
+
     mainThreadHandler.postDelayed(timeoutRunnable, RESOLVE_SERVICE_TIMEOUT);
   }
 
@@ -223,4 +256,51 @@
     registrationListeners.clear();
     mMFServiceName.clear();
   }
+
+  /**
+   * The Android NsdManager calls back on the NsdManager.ResolveListener with a
+   * FAILURE_ALREADY_ACTIVE(3) if any application code calls resolveService() on it while the
+   * resolve operation is already active (from another call made previously). An object of
+   * NsdManagerResolverAvailState allows NsdManagerServiceResolver to synchronize on the usage of
+   * NsdManager's resolveService() API
+   */
+  public static class NsdManagerResolverAvailState {
+    private static final String TAG = NsdManagerResolverAvailState.class.getSimpleName();
+
+    private Lock lock = new ReentrantLock();
+    private Condition condition = lock.newCondition();
+    private boolean busy = false;
+
+    /**
+     * Waits if the NsdManager is already busy with resolving a service. Otherwise, it marks it as
+     * busy and returns
+     */
+    public void acquireResolver() {
+      lock.lock();
+      try {
+        while (busy) {
+          Log.d(TAG, "Found NsdManager Resolver busy, waiting");
+          condition.await();
+        }
+        Log.d(TAG, "Found NsdManager Resolver free, using it and marking it as busy");
+        busy = true;
+      } catch (InterruptedException e) {
+        Log.e(TAG, "Failure while waiting for condition: " + e);
+      } finally {
+        lock.unlock();
+      }
+    }
+
+    /** Signals the NsdManager resolver as free */
+    public void signalFree() {
+      lock.lock();
+      try {
+        Log.d(TAG, "Signaling NsdManager Resolver as free");
+        busy = false;
+        condition.signal();
+      } finally {
+        lock.unlock();
+      }
+    }
+  }
 }