Android - implement ChipDnssdStopBrowse (#32801)

* Android - implement ChipDnssdStopBrowse in the core SDK

Fixing style issues

cleanup log statement

* Addressed comments by bzbarsky-apple and sharadb-amazon

* Addressed comments by bzbarsky-apple now using browseIdentifier to stop DNS-SD browse

* Addressed comments by sharadb-amazon - debug logging
diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/DiscoveryExampleFragment.java b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/DiscoveryExampleFragment.java
index b847e17..46401af 100644
--- a/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/DiscoveryExampleFragment.java
+++ b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/DiscoveryExampleFragment.java
@@ -38,9 +38,6 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
 
 public class DiscoveryExampleFragment extends Fragment {
   private static final String TAG = DiscoveryExampleFragment.class.getSimpleName();
@@ -48,9 +45,7 @@
   private static final Long DISCOVERY_TARGET_DEVICE_TYPE = 35L;
   private static final int DISCOVERY_RUNTIME_SEC = 15;
   private TextView matterDiscoveryMessageTextView;
-  private static final ScheduledExecutorService executorService =
-      Executors.newSingleThreadScheduledExecutor();
-  private ScheduledFuture scheduledFutureTask;
+  private TextView matterDiscoveryErrorMessageTextView;
   private static final List<CastingPlayer> castingPlayerList = new ArrayList<>();
   private static ArrayAdapter<CastingPlayer> arrayAdapter;
 
@@ -164,15 +159,17 @@
     matterDiscoveryMessageTextView.setText(
         getString(R.string.matter_discovery_message_initializing_text));
 
+    matterDiscoveryErrorMessageTextView =
+        getActivity().findViewById(R.id.matterDiscoveryErrorTextView);
+    matterDiscoveryErrorMessageTextView.setText(
+        getString(R.string.matter_discovery_error_message_initial));
+
     arrayAdapter = new CastingPlayerArrayAdapter(getActivity(), castingPlayerList);
     final ListView list = getActivity().findViewById(R.id.castingPlayerList);
     list.setAdapter(arrayAdapter);
 
     Log.d(TAG, "onViewCreated() creating callbacks");
 
-    // TODO: In following PRs. Enable startDiscoveryButton and stopDiscoveryButton when
-    //  stopDiscovery is implemented in the core Matter SDK DNS-SD API. Enable in
-    //  fragment_matter_discovery_example.xml
     Button startDiscoveryButton = getView().findViewById(R.id.startDiscoveryButton);
     startDiscoveryButton.setOnClickListener(
         v -> {
@@ -188,8 +185,6 @@
         v -> {
           Log.i(TAG, "onViewCreated() stopDiscoveryButton button clicked. Calling stopDiscovery()");
           stopDiscovery();
-          Log.i(TAG, "onViewCreated() stopDiscoveryButton button clicked. Canceling future task");
-          scheduledFutureTask.cancel(true);
         });
 
     Button clearDiscoveryResultsButton = getView().findViewById(R.id.clearDiscoveryResultsButton);
@@ -198,6 +193,8 @@
           Log.i(
               TAG, "onViewCreated() clearDiscoveryResultsButton button clicked. Clearing results");
           arrayAdapter.clear();
+          matterDiscoveryErrorMessageTextView.setText(
+              getString(R.string.matter_discovery_error_message_initial));
         });
   }
 
@@ -219,11 +216,7 @@
   public void onPause() {
     super.onPause();
     Log.i(TAG, "onPause() called");
-    // stopDiscovery();
-    // Don't crash the app
-    if (scheduledFutureTask != null) {
-      scheduledFutureTask.cancel(true);
-    }
+    stopDiscovery();
   }
 
   /** Interface for notifying the host. */
@@ -235,6 +228,8 @@
 
   private boolean startDiscovery() {
     Log.i(TAG, "startDiscovery() called");
+    matterDiscoveryErrorMessageTextView.setText(
+        getString(R.string.matter_discovery_error_message_initial));
 
     arrayAdapter.clear();
 
@@ -244,6 +239,8 @@
         matterCastingPlayerDiscovery.addCastingPlayerChangeListener(castingPlayerChangeListener);
     if (err.hasError()) {
       Log.e(TAG, "startDiscovery() addCastingPlayerChangeListener() called, err Add: " + err);
+      matterDiscoveryErrorMessageTextView.setText(
+          getString(R.string.matter_discovery_error_message_stop_before_starting) + err);
       return false;
     }
     // Start discovery
@@ -251,6 +248,8 @@
     err = matterCastingPlayerDiscovery.startDiscovery(DISCOVERY_TARGET_DEVICE_TYPE);
     if (err.hasError()) {
       Log.e(TAG, "startDiscovery() startDiscovery() called, err Start: " + err);
+      matterDiscoveryErrorMessageTextView.setText(
+          getString(R.string.matter_discovery_error_message_start_error) + err);
       return false;
     }
 
@@ -259,28 +258,13 @@
     matterDiscoveryMessageTextView.setText(
         getString(R.string.matter_discovery_message_discovering_text));
 
-    // TODO: In following PRs. Enable this to auto-stop discovery after stopDiscovery is
-    //  implemented in the core Matter SDK DNS-SD API.
-    // Schedule a service to stop discovery and remove the CastingPlayerChangeListener
-    // Safe to call if discovery is not running
-    //    scheduledFutureTask =
-    //        executorService.schedule(
-    //            () -> {
-    //              Log.i(
-    //                  TAG,
-    //                  "startDiscovery() executorService "
-    //                      + DISCOVERY_RUNTIME_SEC
-    //                      + " seconds timer expired. Auto-calling stopDiscovery()");
-    //              stopDiscovery();
-    //            },
-    //            DISCOVERY_RUNTIME_SEC,
-    //            TimeUnit.SECONDS);
-
     return true;
   }
 
   private void stopDiscovery() {
     Log.i(TAG, "stopDiscovery() called");
+    matterDiscoveryErrorMessageTextView.setText(
+        getString(R.string.matter_discovery_error_message_initial));
 
     // Stop discovery
     MatterError err = matterCastingPlayerDiscovery.stopDiscovery();
@@ -288,8 +272,9 @@
       Log.e(
           TAG,
           "stopDiscovery() MatterCastingPlayerDiscovery.stopDiscovery() called, err Stop: " + err);
+      matterDiscoveryErrorMessageTextView.setText(
+          getString(R.string.matter_discovery_error_message_stop_error) + err);
     } else {
-      // TODO: In following PRs. Implement stop discovery in the Android core API.
       Log.d(TAG, "stopDiscovery() MatterCastingPlayerDiscovery.stopDiscovery() success");
     }
 
@@ -309,6 +294,8 @@
           TAG,
           "stopDiscovery() matterCastingPlayerDiscovery.removeCastingPlayerChangeListener() called, err Remove: "
               + err);
+      matterDiscoveryErrorMessageTextView.setText(
+          getString(R.string.matter_discovery_error_message_stop_error) + err);
     }
   }
 }
diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayerDiscovery-JNI.cpp b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayerDiscovery-JNI.cpp
index 5981ab8..df79e60 100644
--- a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayerDiscovery-JNI.cpp
+++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayerDiscovery-JNI.cpp
@@ -73,17 +73,17 @@
 
         VerifyOrReturn(castingPlayerChangeListenerJavaObject.HasValidObjectRef(),
                        ChipLogError(AppServer,
-                                    "CastingPlayerDiscovery-JNI::DiscoveryDelegateImpl::HandleOnAdded() Warning: Not set, "
+                                    "CastingPlayerDiscovery-JNI::DiscoveryDelegateImpl::HandleOnAdded() Not set, "
                                     "CastingPlayerChangeListener == nullptr"));
         VerifyOrReturn(onAddedCallbackJavaMethodID != nullptr,
                        ChipLogError(AppServer,
-                                    "CastingPlayerDiscovery-JNI::DiscoveryDelegateImpl::HandleOnAdded() Warning: Not set, "
+                                    "CastingPlayerDiscovery-JNI::DiscoveryDelegateImpl::HandleOnAdded() Not set, "
                                     "onAddedCallbackJavaMethodID == nullptr"));
 
         jobject matterCastingPlayerJavaObject = support::convertCastingPlayerFromCppToJava(player);
         VerifyOrReturn(matterCastingPlayerJavaObject != nullptr,
                        ChipLogError(AppServer,
-                                    "CastingPlayerDiscovery-JNI::DiscoveryDelegateImpl::HandleOnAdded() Warning: Could not create "
+                                    "CastingPlayerDiscovery-JNI::DiscoveryDelegateImpl::HandleOnAdded() Could not create "
                                     "CastingPlayer jobject"));
 
         JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread();
@@ -100,17 +100,17 @@
 
         VerifyOrReturn(castingPlayerChangeListenerJavaObject.HasValidObjectRef(),
                        ChipLogError(AppServer,
-                                    "CastingPlayerDiscovery-JNI::DiscoveryDelegateImpl::HandleOnUpdated() Warning: Not set, "
+                                    "CastingPlayerDiscovery-JNI::DiscoveryDelegateImpl::HandleOnUpdated() Not set, "
                                     "CastingPlayerChangeListener == nullptr"));
         VerifyOrReturn(onChangedCallbackJavaMethodID != nullptr,
                        ChipLogError(AppServer,
-                                    "CastingPlayerDiscovery-JNI::DiscoveryDelegateImpl::HandleOnUpdated() Warning: Not set, "
+                                    "CastingPlayerDiscovery-JNI::DiscoveryDelegateImpl::HandleOnUpdated() Not set, "
                                     "onChangedCallbackJavaMethodID == nullptr"));
 
         jobject matterCastingPlayerJavaObject = support::convertCastingPlayerFromCppToJava(player);
         VerifyOrReturn(matterCastingPlayerJavaObject != nullptr,
                        ChipLogError(AppServer,
-                                    "CastingPlayerDiscovery-JNI::DiscoveryDelegateImpl::HandleOnUpdated() Warning: Could not "
+                                    "CastingPlayerDiscovery-JNI::DiscoveryDelegateImpl::HandleOnUpdated() Could not "
                                     "create CastingPlayer jobject"));
 
         JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread();
@@ -191,7 +191,7 @@
     if (DiscoveryDelegateImpl::GetInstance()->castingPlayerChangeListenerJavaObject.HasValidObjectRef())
     {
         ChipLogError(AppServer,
-                     "CastingPlayerDiscovery-JNI::addCastingPlayerChangeListener() Warning: Call removeCastingPlayerChangeListener "
+                     "CastingPlayerDiscovery-JNI::addCastingPlayerChangeListener() Call removeCastingPlayerChangeListener "
                      "before adding a new one");
         return support::convertMatterErrorFromCppToJava(CHIP_ERROR_INCORRECT_STATE);
     }
@@ -246,11 +246,18 @@
 
         return support::convertMatterErrorFromCppToJava(CHIP_NO_ERROR);
     }
+    else if (DiscoveryDelegateImpl::GetInstance()->castingPlayerChangeListenerJavaObject.ObjectRef() == nullptr)
+    {
+        ChipLogError(
+            AppServer,
+            "CastingPlayerDiscovery-JNI::removeCastingPlayerChangeListener() Cannot remove listener. No listener was added");
+        return support::convertMatterErrorFromCppToJava(CHIP_NO_ERROR);
+    }
     else
     {
         ChipLogError(AppServer,
-                     "CastingPlayerDiscovery-JNI::removeCastingPlayerChangeListener() Warning: Cannot remove listener. Received a "
-                     "different CastingPlayerChangeListener object");
+                     "CastingPlayerDiscovery-JNI::removeCastingPlayerChangeListener() Cannot remove listener. Received a different "
+                     "CastingPlayerChangeListener object");
         return support::convertMatterErrorFromCppToJava(CHIP_ERROR_INCORRECT_STATE);
     }
 }
@@ -276,8 +283,7 @@
             jboolean added = env->CallBooleanMethod(arrayList, addMethod, matterCastingPlayerJavaObject);
             if (!((bool) added))
             {
-                ChipLogError(AppServer,
-                             "CastingPlayerDiscovery-JNI::getCastingPlayers() Warning: Unable to add CastingPlayer with ID: %s",
+                ChipLogError(AppServer, "CastingPlayerDiscovery-JNI::getCastingPlayers() Unable to add CastingPlayer with ID: %s",
                              player->GetId());
             }
         }
diff --git a/examples/tv-casting-app/android/App/app/src/main/res/layout/fragment_matter_discovery_example.xml b/examples/tv-casting-app/android/App/app/src/main/res/layout/fragment_matter_discovery_example.xml
index 1c3eb13..3f3e3d9 100644
--- a/examples/tv-casting-app/android/App/app/src/main/res/layout/fragment_matter_discovery_example.xml
+++ b/examples/tv-casting-app/android/App/app/src/main/res/layout/fragment_matter_discovery_example.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
@@ -15,14 +15,14 @@
         android:padding="10sp">
 
         <Button
-            android:enabled="false"
+            android:enabled="true"
             android:id="@+id/startDiscoveryButton"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
             android:text="@string/matter_start_discovery_button_text" />
 
         <Button
-            android:enabled="false"
+            android:enabled="true"
             android:id="@+id/stopDiscoveryButton"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
@@ -49,5 +49,13 @@
             android:fadeScrollbars = "false" />
     </LinearLayout>
 
+    <TextView
+        android:id="@+id/matterDiscoveryErrorTextView"
+        android:layout_width="match_parent"
+        android:layout_height = "wrap_content"
+        android:text="string/matter_discovery_error_default_text"
+        android:textAlignment="center"
+        android:textAppearance="@style/TextAppearance.AppCompat.Small"
+        android:layout_alignParentBottom="true"/>
 
-</FrameLayout>
\ No newline at end of file
+</RelativeLayout>
\ No newline at end of file
diff --git a/examples/tv-casting-app/android/App/app/src/main/res/values/strings.xml b/examples/tv-casting-app/android/App/app/src/main/res/values/strings.xml
index de7849c..d0cc982 100644
--- a/examples/tv-casting-app/android/App/app/src/main/res/values/strings.xml
+++ b/examples/tv-casting-app/android/App/app/src/main/res/values/strings.xml
@@ -31,8 +31,12 @@
     <string name="matter_stop_discovery_button_text">Stop Discovery"</string>
     <string name="matter_clear_results_button_text">Clear Results"</string>
     <string name="matter_discovery_message_initializing_text">Initializing</string>
-    <string name="matter_discovery_message_discovering_text">Casting Players on-network:</string>
+    <string name="matter_discovery_message_discovering_text">Discovering Casting Players on-network:</string>
     <string name="matter_discovery_message_stopped_text">Discovery Stopped</string>
+    <string name="matter_discovery_error_message_initial">No errors to report</string>
+    <string name="matter_discovery_error_message_start_error">Start discovery error. -</string>
+    <string name="matter_discovery_error_message_stop_before_starting">Start discovery error. Discovery ongoing, stop before starting. -</string>
+    <string name="matter_discovery_error_message_stop_error">Stop discovery error. -</string>
     <string name="matter_connection_next_button_text">Next</string>
     <string name="matter_action_selector_text">Select an action</string>
     <string name="matter_select_content_launcher_launch_url_button_text">Content Launcher Launch URL</string>
diff --git a/src/platform/android/DnssdImpl.cpp b/src/platform/android/DnssdImpl.cpp
index 5f2e79b..5a27b76 100644
--- a/src/platform/android/DnssdImpl.cpp
+++ b/src/platform/android/DnssdImpl.cpp
@@ -44,6 +44,7 @@
 JniGlobalReference sMdnsCallbackObject;
 jmethodID sResolveMethod          = nullptr;
 jmethodID sBrowseMethod           = nullptr;
+jmethodID sStopBrowseMethod       = nullptr;
 jmethodID sGetTextEntryKeysMethod = nullptr;
 jmethodID sGetTextEntryDataMethod = nullptr;
 jclass sMdnsCallbackClass         = nullptr;
@@ -203,13 +204,31 @@
         return CHIP_JNI_ERROR_EXCEPTION_THROWN;
     }
 
-    *browseIdentifier = reinterpret_cast<intptr_t>(nullptr);
+    auto sdCtx        = chip::Platform::New<BrowseContext>(callback);
+    *browseIdentifier = reinterpret_cast<intptr_t>(sdCtx);
+
     return CHIP_NO_ERROR;
 }
 
 CHIP_ERROR ChipDnssdStopBrowse(intptr_t browseIdentifier)
 {
-    return CHIP_ERROR_NOT_IMPLEMENTED;
+    VerifyOrReturnError(sBrowserObject.HasValidObjectRef() && sStopBrowseMethod != nullptr, CHIP_ERROR_INVALID_ARGUMENT);
+
+    JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread();
+    auto ctx     = reinterpret_cast<BrowseContext *>(browseIdentifier);
+
+    env->CallVoidMethod(sBrowserObject.ObjectRef(), sStopBrowseMethod, reinterpret_cast<jlong>(ctx->callback));
+
+    if (env->ExceptionCheck())
+    {
+        ChipLogError(Discovery, "Java exception in ChipDnssdStopBrowse");
+        env->ExceptionDescribe();
+        env->ExceptionClear();
+        return CHIP_JNI_ERROR_EXCEPTION_THROWN;
+    }
+    chip::Platform::Delete(ctx);
+
+    return CHIP_NO_ERROR;
 }
 
 template <size_t N>
@@ -306,6 +325,8 @@
 
     sBrowseMethod = env->GetMethodID(browserClass, "browse", "(Ljava/lang/String;JJLchip/platform/ChipMdnsCallback;)V");
 
+    sStopBrowseMethod = env->GetMethodID(browserClass, "stopDiscover", "(J)V");
+
     if (sResolveMethod == nullptr)
     {
         ChipLogError(Discovery, "Failed to access Resolver 'resolve' method");
diff --git a/src/platform/android/DnssdImpl.h b/src/platform/android/DnssdImpl.h
index a160157..f106ec6 100644
--- a/src/platform/android/DnssdImpl.h
+++ b/src/platform/android/DnssdImpl.h
@@ -18,6 +18,7 @@
 #pragma once
 
 #include <jni.h>
+#include <lib/dnssd/platform/Dnssd.h>
 
 namespace chip {
 namespace Dnssd {
@@ -37,5 +38,12 @@
 
 void HandleBrowse(jobjectArray instanceName, jstring serviceType, jlong callbackHandle, jlong contextHandle);
 
+struct BrowseContext
+{
+    DnssdBrowseCallback callback;
+
+    BrowseContext(DnssdBrowseCallback cb) { callback = cb; };
+};
+
 } // namespace Dnssd
 } // namespace chip
diff --git a/src/platform/android/java/chip/platform/NsdManagerServiceBrowser.java b/src/platform/android/java/chip/platform/NsdManagerServiceBrowser.java
index f98acdf..28fc698 100644
--- a/src/platform/android/java/chip/platform/NsdManagerServiceBrowser.java
+++ b/src/platform/android/java/chip/platform/NsdManagerServiceBrowser.java
@@ -24,6 +24,7 @@
 import android.net.wifi.WifiManager.MulticastLock;
 import android.os.Handler;
 import android.os.Looper;
+import android.os.MessageQueue;
 import android.util.Log;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -68,7 +69,14 @@
         new Runnable() {
           @Override
           public void run() {
-            stopDiscover(callbackHandle, chipMdnsCallback);
+            Log.i(
+                TAG,
+                "Browse for service '"
+                    + serviceType
+                    + "' expired after timeout: "
+                    + timeout
+                    + " ms");
+            stopDiscover(callbackHandle);
           }
         };
     startDiscover(serviceType, callbackHandle, contextHandle, chipMdnsCallback);
@@ -81,23 +89,30 @@
       final long contextHandle,
       final ChipMdnsCallback chipMdnsCallback) {
     if (callbackMap.containsKey(callbackHandle)) {
-      Log.d(TAG, "Invalid callbackHandle");
+      Log.w(TAG, "Starting service discovery failed. Invalid callbackHandle: " + callbackHandle);
       return;
     }
 
     NsdManagerDiscovery discovery =
         new NsdManagerDiscovery(serviceType, callbackHandle, contextHandle);
     multicastLock.acquire();
+    discovery.setChipMdnsCallback(chipMdnsCallback);
 
-    Log.d(TAG, "Starting service discovering for '" + serviceType + "'");
+    Log.d(
+        TAG,
+        "Starting service discovery for '"
+            + serviceType
+            + "' with callbackHandle: "
+            + callbackHandle);
 
     this.nsdManager.discoverServices(serviceType, NsdManager.PROTOCOL_DNS_SD, discovery);
     callbackMap.put(callbackHandle, discovery);
   }
 
-  public void stopDiscover(final long callbackHandle, final ChipMdnsCallback chipMdnsCallback) {
+  public void stopDiscover(final long callbackHandle) {
+    Log.d(TAG, "Stopping service discovery with callbackHandle: " + callbackHandle);
     if (!callbackMap.containsKey(callbackHandle)) {
-      Log.d(TAG, "Invalid callbackHandle");
+      Log.w(TAG, "Stopping service discovery failed. Callback handle not found.");
       return;
     }
 
@@ -106,8 +121,13 @@
       multicastLock.release();
     }
 
+    MessageQueue queue = mainThreadHandler.getLooper().getQueue();
+    if (!queue.isIdle()) {
+      Log.d(TAG, "Canceling scheduled browse timeout runnable for '" + discovery.serviceType + "'");
+      mainThreadHandler.removeCallbacksAndMessages(null);
+    }
+
     this.nsdManager.stopServiceDiscovery(discovery);
-    discovery.handleServiceBrowse(chipMdnsCallback);
   }
 
   public class NsdManagerDiscovery implements NsdManager.DiscoveryListener {
@@ -115,6 +135,7 @@
     private long callbackHandle;
     private long contextHandle;
     private ArrayList<String> serviceNameList = new ArrayList<>();
+    private ChipMdnsCallback chipMdnsCallback;
 
     public NsdManagerDiscovery(String serviceType, long callbackHandle, long contextHandle) {
       this.serviceType = serviceType;
@@ -150,7 +171,8 @@
 
     @Override
     public void onDiscoveryStopped(String serviceType) {
-      Log.w(TAG, "Succeed to stop discovery service '" + serviceType);
+      Log.w(TAG, "Successfully stopped discovery service '" + serviceType);
+      this.handleServiceBrowse(chipMdnsCallback);
     }
 
     public void handleServiceBrowse(ChipMdnsCallback chipMdnsCallback) {
@@ -160,5 +182,9 @@
           callbackHandle,
           contextHandle);
     }
+
+    public void setChipMdnsCallback(final ChipMdnsCallback chipMdnsCallback) {
+      this.chipMdnsCallback = chipMdnsCallback;
+    }
   }
 }