tv-casting-app: simplified android connection API (#31617)

diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/MainActivity.java b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/MainActivity.java
index c0b1bcb..9db8278 100644
--- a/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/MainActivity.java
+++ b/examples/tv-casting-app/android/App/app/src/main/java/com/chip/casting/app/MainActivity.java
@@ -11,6 +11,7 @@
 import com.chip.casting.TvCastingApp;
 import com.chip.casting.util.GlobalCastingConstants;
 import com.chip.casting.util.PreferencesConfigurationManager;
+import com.matter.casting.ConnectionExampleFragment;
 import com.matter.casting.DiscoveryExampleFragment;
 import com.matter.casting.InitializationExample;
 import com.matter.casting.core.CastingPlayer;
@@ -20,7 +21,8 @@
     implements CommissionerDiscoveryFragment.Callback,
         ConnectionFragment.Callback,
         SelectClusterFragment.Callback,
-        DiscoveryExampleFragment.Callback {
+        DiscoveryExampleFragment.Callback,
+        ConnectionExampleFragment.Callback {
 
   private static final String TAG = MainActivity.class.getSimpleName();
 
@@ -58,10 +60,9 @@
   }
 
   @Override
-  public void handleConnectionButtonClicked(CastingPlayer player) {
+  public void handleConnectionButtonClicked(CastingPlayer castingPlayer) {
     Log.i(TAG, "MainActivity.handleConnectionButtonClicked() called");
-    // TODO: In future PR, show fragment that connects to the player.
-    // showFragment(ConnectionFragment.newInstance(CastingPlayer player));
+    showFragment(ConnectionExampleFragment.newInstance(castingPlayer));
   }
 
   @Override
@@ -70,6 +71,14 @@
   }
 
   @Override
+  public void handleConnectionComplete(CastingPlayer castingPlayer) {
+    Log.i(TAG, "MainActivity.handleConnectionComplete() called ");
+
+    // TODO: Implement in following PRs. Select Cluster Fragment.
+    // showFragment(SelectClusterFragment.newInstance(tvCastingApp));
+  }
+
+  @Override
   public void handleContentLauncherSelected() {
     showFragment(ContentLauncherFragment.newInstance(tvCastingApp));
   }
@@ -115,7 +124,7 @@
   private void showFragment(Fragment fragment, boolean showOnBack) {
     Log.d(
         TAG,
-        "showFragment called with " + fragment.getClass().getSimpleName() + " and " + showOnBack);
+        "showFragment() called with " + fragment.getClass().getSimpleName() + " and " + showOnBack);
     FragmentTransaction fragmentTransaction =
         getSupportFragmentManager()
             .beginTransaction()
diff --git a/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ConnectionExampleFragment.java b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ConnectionExampleFragment.java
new file mode 100644
index 0000000..c6462cd
--- /dev/null
+++ b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ConnectionExampleFragment.java
@@ -0,0 +1,147 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   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 com.matter.casting;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import com.R;
+import com.matter.casting.core.CastingPlayer;
+import com.matter.casting.support.DeviceTypeStruct;
+import com.matter.casting.support.EndpointFilter;
+import java.util.ArrayList;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executors;
+
+/** A {@link Fragment} to Verify or establish a connection with a selected Casting Player. */
+public class ConnectionExampleFragment extends Fragment {
+  private static final String TAG = ConnectionExampleFragment.class.getSimpleName();
+  // Time (in sec) to keep the commissioning window open, if commissioning is required.
+  // Must be >= 3 minutes.
+  private static final long MIN_CONNECTION_TIMEOUT_SEC = 3 * 60;
+  private final CastingPlayer targetCastingPlayer;
+  private TextView connectionFragmentStatusTextView;
+  private Button connectionFragmentNextButton;
+
+  public ConnectionExampleFragment(CastingPlayer targetCastingPlayer) {
+    Log.i(TAG, "ConnectionExampleFragment() called with target CastingPlayer");
+    this.targetCastingPlayer = targetCastingPlayer;
+  }
+
+  /**
+   * Use this factory method to create a new instance of this fragment using the provided
+   * parameters.
+   *
+   * @return A new instance of fragment ConnectionExampleFragment.
+   */
+  public static ConnectionExampleFragment newInstance(CastingPlayer castingPlayer) {
+    Log.i(TAG, "newInstance() called");
+    return new ConnectionExampleFragment(castingPlayer);
+  }
+
+  @Override
+  public void onCreate(Bundle savedInstanceState) {
+    super.onCreate(savedInstanceState);
+    Log.i(TAG, "onCreate() called");
+  }
+
+  @Override
+  public View onCreateView(
+      LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+    Log.i(TAG, "onCreateView() called");
+    // Inflate the layout for this fragment
+    return inflater.inflate(R.layout.fragment_matter_connection_example, container, false);
+  }
+
+  @Override
+  public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+    super.onViewCreated(view, savedInstanceState);
+    Log.i(TAG, "onViewCreated() called");
+
+    connectionFragmentStatusTextView = getView().findViewById(R.id.connectionFragmentStatusText);
+    connectionFragmentStatusTextView.setText(
+        "Verifying or establishing connection with Casting Player with device name: "
+            + targetCastingPlayer.getDeviceName());
+
+    connectionFragmentNextButton = getView().findViewById(R.id.connectionFragmentNextButton);
+    Callback callback = (ConnectionExampleFragment.Callback) this.getActivity();
+    connectionFragmentNextButton.setOnClickListener(
+        v -> {
+          Log.i(TAG, "onViewCreated() NEXT clicked. Calling handleConnectionComplete()");
+          callback.handleConnectionComplete(targetCastingPlayer);
+        });
+
+    Executors.newSingleThreadExecutor()
+        .submit(
+            () -> {
+              Log.d(TAG, "onViewCreated() calling verifyOrEstablishConnection()");
+
+              EndpointFilter desiredEndpointFilter =
+                  new EndpointFilter(null, 65521, new ArrayList<DeviceTypeStruct>());
+              // The desired commissioning window timeout and EndpointFilter are optional.
+              CompletableFuture<Void> completableFuture =
+                  targetCastingPlayer.VerifyOrEstablishConnection(
+                      MIN_CONNECTION_TIMEOUT_SEC, desiredEndpointFilter);
+
+              Log.d(TAG, "onViewCreated() verifyOrEstablishConnection() called");
+
+              completableFuture
+                  .thenRun(
+                      () -> {
+                        Log.i(
+                            TAG,
+                            "CompletableFuture.thenRun(), connected to CastingPlayer with deviceId: "
+                                + targetCastingPlayer.getDeviceId());
+                        getActivity()
+                            .runOnUiThread(
+                                () -> {
+                                  connectionFragmentStatusTextView.setText(
+                                      "Connected to Casting Player with device name: "
+                                          + targetCastingPlayer.getDeviceName());
+                                  connectionFragmentNextButton.setEnabled(true);
+                                });
+                      })
+                  .exceptionally(
+                      exc -> {
+                        Log.e(
+                            TAG,
+                            "CompletableFuture.exceptionally(), CastingPLayer connection failed: "
+                                + exc.getMessage());
+                        getActivity()
+                            .runOnUiThread(
+                                () -> {
+                                  connectionFragmentStatusTextView.setText(
+                                      "Casting Player connection failed due to: "
+                                          + exc.getMessage());
+                                });
+                        return null;
+                      });
+            });
+  }
+
+  /** Interface for notifying the host. */
+  public interface Callback {
+    /** Notifies listener to trigger transition on completion of connection */
+    void handleConnectionComplete(CastingPlayer castingPlayer);
+  }
+}
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 4c5d68f..67db95b 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
@@ -338,11 +338,10 @@
           CastingPlayer castingPlayer = playerList.get(i);
           Log.d(
               TAG,
-              "OnItemClickListener.onClick() called for castingPlayer with deviceId: "
+              "OnClickListener.onClick() called for CastingPlayer with deviceId: "
                   + castingPlayer.getDeviceId());
           DiscoveryExampleFragment.Callback callback1 = (DiscoveryExampleFragment.Callback) context;
-          // TODO: In following PRs. Implement CastingPlayer connection
-          // callback1.handleCommissioningButtonClicked(castingPlayer);
+          callback1.handleConnectionButtonClicked(castingPlayer);
         };
     playerDescription.setOnClickListener(clickListener);
     return view;
diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingPlayer.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingPlayer.java
index 71ea476..723f1b8 100644
--- a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingPlayer.java
+++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/CastingPlayer.java
@@ -16,8 +16,10 @@
  */
 package com.matter.casting.core;
 
+import com.matter.casting.support.EndpointFilter;
 import java.net.InetAddress;
 import java.util.List;
+import java.util.concurrent.CompletableFuture;
 
 /**
  * The CastingPlayer interface defines a Matter commissioner that is able to play media to a
@@ -55,24 +57,38 @@
   @Override
   int hashCode();
 
-  // TODO: Implement in following PRs. Related to player connection implementation.
-  //    List<Endpoint> getEndpoints();
-  //
-  //    ConnectionState getConnectionState();
-  //
-  //    CompletableFuture<Void> connect(long timeout);
-  //
-  //    static class ConnectionState extends Observable {
-  //        private boolean connected;
-  //
-  //        void setConnected(boolean connected) {
-  //            this.connected = connected;
-  //            setChanged();
-  //            notifyObservers(this.connected);
-  //        }
-  //
-  //        boolean isConnected() {
-  //            return connected;
-  //        }
-  //    }
+  /**
+   * Verifies that a connection exists with this CastingPlayer, or triggers a new session request.
+   * If the CastingApp does not have the nodeId and fabricIndex of this CastingPlayer cached on
+   * disk, this will execute the user directed commissioning process.
+   *
+   * @param commissioningWindowTimeoutSec (Optional) time (in sec) to keep the commissioning window
+   *     open, if commissioning is required. Needs to be >= MIN_CONNECTION_TIMEOUT_SEC.
+   * @param desiredEndpointFilter (Optional) Attributes (such as VendorId) describing an Endpoint
+   *     that the client wants to interact with after commissioning. If this value is passed in, the
+   *     VerifyOrEstablishConnection will force User Directed Commissioning, in case the desired
+   *     Endpoint is not found in the on device CastingStore.
+   * @return A CompletableFuture that completes when the VerifyOrEstablishConnection is completed.
+   *     The CompletableFuture will be completed with a Void value if the
+   *     VerifyOrEstablishConnection is successful. Otherwise, the CompletableFuture will be
+   *     completed with an Exception. The Exception will be of type
+   *     com.matter.casting.core.CastingException. If the VerifyOrEstablishConnection fails, the
+   *     CastingException will contain the error code and message from the CastingApp.
+   */
+  CompletableFuture<Void> VerifyOrEstablishConnection(
+      long commissioningWindowTimeoutSec, EndpointFilter desiredEndpointFilter);
+
+  /**
+   * Verifies that a connection exists with this CastingPlayer, or triggers a new session request.
+   * If the CastingApp does not have the nodeId and fabricIndex of this CastingPlayer cached on
+   * disk, this will execute the user directed commissioning process.
+   *
+   * @return A CompletableFuture that completes when the VerifyOrEstablishConnection is completed.
+   *     The CompletableFuture will be completed with a Void value if the
+   *     VerifyOrEstablishConnection is successful. Otherwise, the CompletableFuture will be
+   *     completed with an Exception. The Exception will be of type
+   *     com.matter.casting.core.CastingException. If the VerifyOrEstablishConnection fails, the
+   *     CastingException will contain the error code and message from the CastingApp.
+   */
+  CompletableFuture<Void> VerifyOrEstablishConnection();
 }
diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/MatterCastingPlayer.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/MatterCastingPlayer.java
index ecc76a5..d5d93c3 100644
--- a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/MatterCastingPlayer.java
+++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/core/MatterCastingPlayer.java
@@ -16,9 +16,11 @@
  */
 package com.matter.casting.core;
 
+import com.matter.casting.support.EndpointFilter;
 import java.net.InetAddress;
 import java.util.List;
 import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
 
 /**
  * A Matter Casting Player represents a Matter commissioner that is able to play media to a physical
@@ -27,6 +29,13 @@
  * the service discovered/resolved.
  */
 public class MatterCastingPlayer implements CastingPlayer {
+  private static final String TAG = MatterCastingPlayer.class.getSimpleName();
+  /**
+   * Time (in sec) to keep the commissioning window open, if commissioning is required. Must be >= 3
+   * minutes.
+   */
+  public static final long MIN_CONNECTION_TIMEOUT_SEC = 3 * 60;
+
   private boolean connected;
   private String deviceId;
   private String deviceName;
@@ -37,6 +46,7 @@
   private int productId;
   private int vendorId;
   private long deviceType;
+  protected long _cppCastingPlayer;
 
   public MatterCastingPlayer(
       boolean connected,
@@ -137,4 +147,43 @@
     MatterCastingPlayer that = (MatterCastingPlayer) o;
     return Objects.equals(this.deviceId, that.deviceId);
   }
+
+  /**
+   * Verifies that a connection exists with this CastingPlayer, or triggers a new session request.
+   * If the CastingApp does not have the nodeId and fabricIndex of this CastingPlayer cached on
+   * disk, this will execute the user directed commissioning process.
+   *
+   * @param commissioningWindowTimeoutSec (Optional) time (in sec) to keep the commissioning window
+   *     open, if commissioning is required. Needs to be >= MIN_CONNECTION_TIMEOUT_SEC.
+   * @param desiredEndpointFilter (Optional) Attributes (such as VendorId) describing an Endpoint
+   *     that the client wants to interact with after commissioning. If this value is passed in, the
+   *     VerifyOrEstablishConnection will force User Directed Commissioning, in case the desired
+   *     Endpoint is not found in the on device CastingStore.
+   * @return A CompletableFuture that completes when the VerifyOrEstablishConnection is completed.
+   *     The CompletableFuture will be completed with a Void value if the
+   *     VerifyOrEstablishConnection is successful. Otherwise, the CompletableFuture will be
+   *     completed with an Exception. The Exception will be of type
+   *     com.matter.casting.core.CastingException. If the VerifyOrEstablishConnection fails, the
+   *     CastingException will contain the error code and message from the CastingApp.
+   */
+  @Override
+  public native CompletableFuture<Void> VerifyOrEstablishConnection(
+      long commissioningWindowTimeoutSec, EndpointFilter desiredEndpointFilter);
+
+  /**
+   * Verifies that a connection exists with this CastingPlayer, or triggers a new session request.
+   * If the CastingApp does not have the nodeId and fabricIndex of this CastingPlayer cached on
+   * disk, this will execute the user directed commissioning process.
+   *
+   * @return A CompletableFuture that completes when the VerifyOrEstablishConnection is completed.
+   *     The CompletableFuture will be completed with a Void value if the
+   *     VerifyOrEstablishConnection is successful. Otherwise, the CompletableFuture will be
+   *     completed with an Exception. The Exception will be of type
+   *     com.matter.casting.core.CastingException. If the VerifyOrEstablishConnection fails, the
+   *     CastingException will contain the error code and message from the CastingApp.
+   */
+  @Override
+  public CompletableFuture<Void> VerifyOrEstablishConnection() {
+    return VerifyOrEstablishConnection(MIN_CONNECTION_TIMEOUT_SEC, null);
+  }
 }
diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/DeviceTypeStruct.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/DeviceTypeStruct.java
new file mode 100644
index 0000000..4c0271c
--- /dev/null
+++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/DeviceTypeStruct.java
@@ -0,0 +1,28 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   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 com.matter.casting.support;
+
+/** A class to describe a Matter device type. */
+public class DeviceTypeStruct {
+  public long deviceType;
+  public int revision;
+
+  public DeviceTypeStruct(long deviceType, int revision) {
+    this.deviceType = deviceType;
+    this.revision = revision;
+  }
+}
diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/EndpointFilter.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/EndpointFilter.java
new file mode 100644
index 0000000..1152e48
--- /dev/null
+++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/EndpointFilter.java
@@ -0,0 +1,35 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   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 com.matter.casting.support;
+
+import java.util.List;
+
+/** Describes an Endpoint that the client wants to connect to. */
+public class EndpointFilter {
+  // Value of null means unspecified
+  public Integer productId;
+  // Value of null means unspecified
+  public Integer vendorId;
+  public List<DeviceTypeStruct> requiredDeviceTypes;
+
+  public EndpointFilter(
+      Integer productId, Integer vendorId, List<DeviceTypeStruct> requiredDeviceTypes) {
+    this.productId = productId;
+    this.vendorId = vendorId;
+    this.requiredDeviceTypes = requiredDeviceTypes;
+  }
+}
diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayer-JNI.cpp b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayer-JNI.cpp
new file mode 100644
index 0000000..2c0fe91
--- /dev/null
+++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayer-JNI.cpp
@@ -0,0 +1,171 @@
+/*
+ *   Copyright (c) 2024 Project CHIP Authors
+ *   All rights reserved.
+ *
+ *   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.
+ *
+ */
+
+#include "CastingPlayer-JNI.h"
+
+#include "../JNIDACProvider.h"
+#include "../support/CastingPlayerConverter-JNI.h"
+#include "../support/ErrorConverter-JNI.h"
+#include "../support/RotatingDeviceIdUniqueIdProvider-JNI.h"
+#include "core/CastingApp.h"             // from tv-casting-common
+#include "core/CastingPlayer.h"          // from tv-casting-common
+#include "core/CastingPlayerDiscovery.h" // from tv-casting-common
+
+#include <app/clusters/bindings/BindingManager.h>
+#include <app/server/Server.h>
+#include <jni.h>
+#include <lib/support/JniReferences.h>
+#include <lib/support/JniTypeWrappers.h>
+
+using namespace chip;
+
+#define JNI_METHOD(RETURN, METHOD_NAME)                                                                                            \
+    extern "C" JNIEXPORT RETURN JNICALL Java_com_matter_casting_core_MatterCastingPlayer_##METHOD_NAME
+
+namespace matter {
+namespace casting {
+namespace core {
+
+JNI_METHOD(jobject, VerifyOrEstablishConnection)
+(JNIEnv * env, jobject thiz, jlong commissioningWindowTimeoutSec, jobject desiredEndpointFilterJavaObject)
+{
+    chip::DeviceLayer::StackLock lock;
+    ChipLogProgress(AppServer, "CastingPlayer-JNI::VerifyOrEstablishConnection() called with a timeout of: %ld seconds",
+                    static_cast<long>(commissioningWindowTimeoutSec));
+
+    // Convert the CastingPlayer jlong to a CastingPlayer pointer
+    jclass castingPlayerClass         = env->GetObjectClass(thiz);
+    jfieldID _cppCastingPlayerFieldId = env->GetFieldID(castingPlayerClass, "_cppCastingPlayer", "J");
+    VerifyOrReturnValue(
+        _cppCastingPlayerFieldId != nullptr, nullptr,
+        ChipLogError(AppServer, "CastingPlayer-JNI::VerifyOrEstablishConnection() _cppCastingPlayerFieldId == nullptr"));
+
+    jlong _cppCastingPlayerValue  = env->GetLongField(thiz, _cppCastingPlayerFieldId);
+    CastingPlayer * castingPlayer = reinterpret_cast<CastingPlayer *>(_cppCastingPlayerValue);
+    VerifyOrReturnValue(castingPlayer != nullptr, nullptr,
+                        ChipLogError(AppServer, "CastingPlayer-JNI::VerifyOrEstablishConnection() castingPlayer == nullptr"));
+
+    // Create a new Java CompletableFuture
+    jclass completableFutureClass          = env->FindClass("java/util/concurrent/CompletableFuture");
+    jmethodID completableFutureConstructor = env->GetMethodID(completableFutureClass, "<init>", "()V");
+    jobject completableFutureObj           = env->NewObject(completableFutureClass, completableFutureConstructor);
+    jobject completableFutureObjGlobalRef  = env->NewGlobalRef(completableFutureObj);
+    VerifyOrReturnValue(
+        completableFutureObjGlobalRef != nullptr, nullptr,
+        ChipLogError(AppServer, "CastingPlayer-JNI::VerifyOrEstablishConnection() completableFutureObjGlobalRef == nullptr"));
+
+    ConnectCallback callback = [completableFutureObjGlobalRef](CHIP_ERROR err, CastingPlayer * playerPtr) {
+        ChipLogProgress(AppServer, "CastingPlayer-JNI::VerifyOrEstablishConnection() ConnectCallback called");
+        VerifyOrReturn(completableFutureObjGlobalRef != nullptr,
+                       ChipLogError(AppServer, "ConnectCallback, completableFutureObjGlobalRef == nullptr"));
+
+        JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread();
+        VerifyOrReturn(env != nullptr, ChipLogError(AppServer, "ConnectCallback, env == nullptr"));
+        // Ensures proper cleanup of local references to Java objects.
+        JniLocalReferenceManager manager(env);
+        // Ensures proper cleanup of global references to Java objects.
+        JniGlobalRefWrapper globalRefWrapper(completableFutureObjGlobalRef);
+
+        jclass completableFutureClass = env->FindClass("java/util/concurrent/CompletableFuture");
+        VerifyOrReturn(completableFutureClass != nullptr,
+                       ChipLogError(AppServer, "ConnectCallback, completableFutureClass == nullptr");
+                       env->DeleteGlobalRef(completableFutureObjGlobalRef););
+
+        if (err == CHIP_NO_ERROR)
+        {
+            ChipLogProgress(AppServer, "ConnectCallback, Connected to Casting Player with device ID: %s", playerPtr->GetId());
+            jmethodID completeMethod = env->GetMethodID(completableFutureClass, "complete", "(Ljava/lang/Object;)Z");
+            VerifyOrReturn(completeMethod != nullptr, ChipLogError(AppServer, "ConnectCallback, completeMethod == nullptr"));
+
+            chip::DeviceLayer::StackUnlock unlock;
+            env->CallBooleanMethod(completableFutureObjGlobalRef, completeMethod, nullptr);
+        }
+        else
+        {
+            ChipLogError(AppServer, "ConnectCallback, connection error: %" CHIP_ERROR_FORMAT, err.Format());
+            jmethodID completeExceptionallyMethod =
+                env->GetMethodID(completableFutureClass, "completeExceptionally", "(Ljava/lang/Throwable;)Z");
+            VerifyOrReturn(completeExceptionallyMethod != nullptr,
+                           ChipLogError(AppServer, "ConnectCallback, completeExceptionallyMethod == nullptr"));
+
+            // Create a Throwable object (e.g., RuntimeException) to pass to completeExceptionallyMethod
+            jclass throwableClass = env->FindClass("java/lang/RuntimeException");
+            VerifyOrReturn(throwableClass != nullptr, ChipLogError(AppServer, "ConnectCallback, throwableClass == nullptr"));
+            jmethodID throwableConstructor = env->GetMethodID(throwableClass, "<init>", "(Ljava/lang/String;)V");
+            VerifyOrReturn(throwableConstructor != nullptr,
+                           ChipLogError(AppServer, "ConnectCallback, throwableConstructor == nullptr"));
+            jstring errorMessage = env->NewStringUTF(err.Format());
+            VerifyOrReturn(errorMessage != nullptr, ChipLogError(AppServer, "ConnectCallback, errorMessage == nullptr"));
+            jobject throwableObject = env->NewObject(throwableClass, throwableConstructor, errorMessage);
+            VerifyOrReturn(throwableObject != nullptr, ChipLogError(AppServer, "ConnectCallback, throwableObject == nullptr"));
+
+            chip::DeviceLayer::StackUnlock unlock;
+            env->CallBooleanMethod(completableFutureObjGlobalRef, completeExceptionallyMethod, throwableObject);
+        }
+    };
+
+    if (desiredEndpointFilterJavaObject == nullptr)
+    {
+        ChipLogProgress(AppServer,
+                        "CastingPlayer-JNI::VerifyOrEstablishConnection() calling CastingPlayer::VerifyOrEstablishConnection() on "
+                        "Casting Player with device ID: %s",
+                        castingPlayer->GetId());
+        castingPlayer->VerifyOrEstablishConnection(callback, static_cast<unsigned long long int>(commissioningWindowTimeoutSec));
+    }
+    else
+    {
+        // Convert the EndpointFilter Java class to a C++ EndpointFilter
+        jclass endpointFilterJavaClass = env->GetObjectClass(desiredEndpointFilterJavaObject);
+        jfieldID vendorIdFieldId       = env->GetFieldID(endpointFilterJavaClass, "vendorId", "Ljava/lang/Integer;");
+        jfieldID productIdFieldId      = env->GetFieldID(endpointFilterJavaClass, "productId", "Ljava/lang/Integer;");
+        jobject vendorIdIntegerObject  = env->GetObjectField(desiredEndpointFilterJavaObject, vendorIdFieldId);
+        jobject productIdIntegerObject = env->GetObjectField(desiredEndpointFilterJavaObject, productIdFieldId);
+        // jfieldID requiredDeviceTypesFieldId = env->GetFieldID(endpointFilterJavaClass, "requiredDeviceTypes",
+        // "Ljava/util/List;");
+
+        matter::casting::core::EndpointFilter desiredEndpointFilter;
+        // Value of 0 means unspecified
+        desiredEndpointFilter.vendorId  = vendorIdIntegerObject != nullptr
+             ? static_cast<uint16_t>(env->CallIntMethod(
+                  vendorIdIntegerObject, env->GetMethodID(env->GetObjectClass(vendorIdIntegerObject), "intValue", "()I")))
+             : 0;
+        desiredEndpointFilter.productId = productIdIntegerObject != nullptr
+            ? static_cast<uint16_t>(env->CallIntMethod(
+                  productIdIntegerObject, env->GetMethodID(env->GetObjectClass(productIdIntegerObject), "intValue", "()I")))
+            : 0;
+        ChipLogProgress(AppServer, "CastingPlayer-JNI::VerifyOrEstablishConnection() desiredEndpointFilter.vendorId: %d",
+                        desiredEndpointFilter.vendorId);
+        ChipLogProgress(AppServer, "CastingPlayer-JNI::VerifyOrEstablishConnection() desiredEndpointFilter.productId: %d",
+                        desiredEndpointFilter.productId);
+        // TODO: In following PRs. Translate the Java requiredDeviceTypes list to a C++ requiredDeviceTypes vector. For now we're
+        // passing an empty list of DeviceTypeStruct.
+
+        ChipLogProgress(AppServer,
+                        "CastingPlayer-JNI::VerifyOrEstablishConnection() calling "
+                        "CastingPlayer::VerifyOrEstablishConnection() on Casting Player with device ID: %s",
+                        castingPlayer->GetId());
+        castingPlayer->VerifyOrEstablishConnection(callback, static_cast<unsigned long long int>(commissioningWindowTimeoutSec),
+                                                   desiredEndpointFilter);
+    }
+
+    return completableFutureObjGlobalRef;
+}
+
+}; // namespace core
+}; // namespace casting
+}; // namespace matter
diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayer-JNI.h b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayer-JNI.h
new file mode 100644
index 0000000..2870866
--- /dev/null
+++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayer-JNI.h
@@ -0,0 +1,41 @@
+/*
+ *
+ *    Copyright (c) 2024 Project CHIP Authors
+ *    All rights reserved.
+ *
+ *    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.
+ */
+
+#pragma once
+
+#include <jni.h>
+
+namespace matter {
+namespace casting {
+namespace core {
+
+class CastingPlayerJNI
+{
+public:
+private:
+    friend CastingPlayerJNI & CastingAppJNIMgr();
+    static CastingPlayerJNI sInstance;
+};
+
+inline class CastingPlayerJNI & CastingAppJNIMgr()
+{
+    return CastingPlayerJNI::sInstance;
+}
+}; // namespace core
+}; // namespace casting
+}; // namespace matter
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 255ba57..5d531e0 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
@@ -88,6 +88,7 @@
                                     "CastingPlayer jobject"));
 
         JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread();
+        chip::DeviceLayer::StackUnlock unlock;
         env->CallVoidMethod(castingPlayerChangeListenerJavaObject, onAddedCallbackJavaMethodID, matterCastingPlayerJavaObject);
     }
 
@@ -113,6 +114,7 @@
                                     "create CastingPlayer jobject"));
 
         JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread();
+        chip::DeviceLayer::StackUnlock unlock;
         env->CallVoidMethod(castingPlayerChangeListenerJavaObject, onChangedCallbackJavaMethodID, matterCastingPlayerJavaObject);
     }
 
diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/CastingPlayerConverter-JNI.cpp b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/CastingPlayerConverter-JNI.cpp
index a993a50..72c3677 100644
--- a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/CastingPlayerConverter-JNI.cpp
+++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/support/CastingPlayerConverter-JNI.cpp
@@ -83,7 +83,11 @@
     {
         ChipLogError(AppServer,
                      "CastingPlayerConverter-JNI.createJCastingPlayer() Warning: Could not create MatterCastingPlayer Java object");
+        return jMatterCastingPlayer;
     }
+    // Set the value of the _cppCastingPlayer field in the Java object to the C++ CastingPlayer pointer.
+    jfieldID longFieldId = env->GetFieldID(matterCastingPlayerJavaClass, "_cppCastingPlayer", "J");
+    env->SetLongField(jMatterCastingPlayer, longFieldId, reinterpret_cast<jlong>(player.get()));
     return jMatterCastingPlayer;
 }
 
diff --git a/examples/tv-casting-app/android/App/app/src/main/res/layout/fragment_matter_connection_example.xml b/examples/tv-casting-app/android/App/app/src/main/res/layout/fragment_matter_connection_example.xml
new file mode 100644
index 0000000..8bdd1b4
--- /dev/null
+++ b/examples/tv-casting-app/android/App/app/src/main/res/layout/fragment_matter_connection_example.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<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"
+    tools:context=".matter.casting.ConnectionExampleFragment"
+    android:padding="10sp">
+
+    <TextView
+        android:id="@+id/connectionFragmentStatusText"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="24sp"/>
+
+    <Button
+        android:enabled="false"
+        android:id="@+id/connectionFragmentNextButton"
+        android:layout_below="@id/connectionFragmentStatusText"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/matter_connection_next_button_text" />
+
+</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 f38c641..99547f7 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
@@ -33,4 +33,5 @@
     <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_stopped_text">Discovery Stopped</string>
+    <string name="matter_connection_next_button_text">Next</string>
 </resources>
\ No newline at end of file
diff --git a/examples/tv-casting-app/android/BUILD.gn b/examples/tv-casting-app/android/BUILD.gn
index 33d046d..98547e5 100644
--- a/examples/tv-casting-app/android/BUILD.gn
+++ b/examples/tv-casting-app/android/BUILD.gn
@@ -38,6 +38,8 @@
   sources += [
     "App/app/src/main/jni/cpp/core/CastingApp-JNI.cpp",
     "App/app/src/main/jni/cpp/core/CastingApp-JNI.h",
+    "App/app/src/main/jni/cpp/core/CastingPlayer-JNI.cpp",
+    "App/app/src/main/jni/cpp/core/CastingPlayer-JNI.h",
     "App/app/src/main/jni/cpp/core/CastingPlayerDiscovery-JNI.cpp",
     "App/app/src/main/jni/cpp/core/CastingPlayerDiscovery-JNI.h",
     "App/app/src/main/jni/cpp/support/CastingPlayerConverter-JNI.cpp",
@@ -109,6 +111,8 @@
     "App/app/src/main/jni/com/matter/casting/support/CommissionableData.java",
     "App/app/src/main/jni/com/matter/casting/support/DACProvider.java",
     "App/app/src/main/jni/com/matter/casting/support/DataProvider.java",
+    "App/app/src/main/jni/com/matter/casting/support/DeviceTypeStruct.java",
+    "App/app/src/main/jni/com/matter/casting/support/EndpointFilter.java",
     "App/app/src/main/jni/com/matter/casting/support/MatterError.java",
   ]
 
diff --git a/examples/tv-casting-app/tv-casting-common/core/CastingPlayer.cpp b/examples/tv-casting-app/tv-casting-common/core/CastingPlayer.cpp
index 8ea90c2..41d9feb 100644
--- a/examples/tv-casting-app/tv-casting-common/core/CastingPlayer.cpp
+++ b/examples/tv-casting-app/tv-casting-common/core/CastingPlayer.cpp
@@ -32,7 +32,7 @@
 void CastingPlayer::VerifyOrEstablishConnection(ConnectCallback onCompleted, unsigned long long int commissioningWindowTimeoutSec,
                                                 EndpointFilter desiredEndpointFilter)
 {
-    ChipLogProgress(AppServer, "CastingPlayer::VerifyOrEstablishConnection called");
+    ChipLogProgress(AppServer, "CastingPlayer::VerifyOrEstablishConnection() called");
 
     std::vector<core::CastingPlayer>::iterator it;
     std::vector<core::CastingPlayer> cachedCastingPlayers = support::CastingStore::GetInstance()->ReadAll();
@@ -41,9 +41,10 @@
 
     // ensure the app was not already in the process of connecting to this CastingPlayer
     err = (mConnectionState != CASTING_PLAYER_CONNECTING ? CHIP_NO_ERROR : CHIP_ERROR_INCORRECT_STATE);
-    VerifyOrExit(mConnectionState != CASTING_PLAYER_CONNECTING,
-                 ChipLogError(AppServer,
-                              "CastingPlayer::VerifyOrEstablishConnection called while already connecting to this CastingPlayer"));
+    VerifyOrExit(
+        mConnectionState != CASTING_PLAYER_CONNECTING,
+        ChipLogError(AppServer,
+                     "CastingPlayer::VerifyOrEstablishConnection() called while already connecting to this CastingPlayer"));
     mConnectionState               = CASTING_PLAYER_CONNECTING;
     mOnCompleted                   = onCompleted;
     mCommissioningWindowTimeoutSec = commissioningWindowTimeoutSec;
@@ -64,14 +65,15 @@
             if (ContainsDesiredEndpoint(&cachedCastingPlayers[index], desiredEndpointFilter))
             {
                 ChipLogProgress(
-                    AppServer, "CastingPlayer::VerifyOrEstablishConnection calling FindOrEstablishSession on cached CastingPlayer");
+                    AppServer,
+                    "CastingPlayer::VerifyOrEstablishConnection() calling FindOrEstablishSession on cached CastingPlayer");
                 *this = cachedCastingPlayers[index];
 
                 FindOrEstablishSession(
                     nullptr,
                     [](void * context, chip::Messaging::ExchangeManager & exchangeMgr, const chip::SessionHandle & sessionHandle) {
                         ChipLogProgress(AppServer,
-                                        "CastingPlayer::VerifyOrEstablishConnection Connection to CastingPlayer successful");
+                                        "CastingPlayer::VerifyOrEstablishConnection() Connection to CastingPlayer successful");
                         CastingPlayer::GetTargetCastingPlayer()->mConnectionState = CASTING_PLAYER_CONNECTED;
 
                         // this async call will Load all the endpoints with their respective attributes into the TargetCastingPlayer
@@ -80,7 +82,7 @@
                         support::EndpointListLoader::GetInstance()->Load();
                     },
                     [](void * context, const chip::ScopedNodeId & peerId, CHIP_ERROR error) {
-                        ChipLogError(AppServer, "CastingPlayer::VerifyOrEstablishConnection Connection to CastingPlayer failed");
+                        ChipLogError(AppServer, "CastingPlayer::VerifyOrEstablishConnection() Connection to CastingPlayer failed");
                         CastingPlayer::GetTargetCastingPlayer()->mConnectionState = CASTING_PLAYER_NOT_CONNECTED;
                         CHIP_ERROR e = support::CastingStore::GetInstance()->Delete(*CastingPlayer::GetTargetCastingPlayer());
                         if (e != CHIP_NO_ERROR)
@@ -102,7 +104,7 @@
     // will require User Directed Commissioning.
     if (chip::Server::GetInstance().GetFailSafeContext().IsFailSafeArmed())
     {
-        ChipLogProgress(AppServer, "CastingPlayer::VerifyOrEstablishConnection Forcing expiry of armed FailSafe timer");
+        ChipLogProgress(AppServer, "CastingPlayer::VerifyOrEstablishConnection() Forcing expiry of armed FailSafe timer");
         // ChipDeviceEventHandler will handle the kFailSafeTimerExpired event by Opening the Basic Commissioning Window and Sending
         // the User Directed Commissioning Request
         chip::Server::GetInstance().GetFailSafeContext().ForceFailSafeTimerExpiry();
@@ -120,7 +122,7 @@
 exit:
     if (err != CHIP_NO_ERROR)
     {
-        ChipLogError(AppServer, "CastingPlayer::VerifyOrEstablishConnection failed with %" CHIP_ERROR_FORMAT, err.Format());
+        ChipLogError(AppServer, "CastingPlayer::VerifyOrEstablishConnection() failed with %" CHIP_ERROR_FORMAT, err.Format());
         support::ChipDeviceEventHandler::SetUdcStatus(false);
         mConnectionState               = CASTING_PLAYER_NOT_CONNECTED;
         mCommissioningWindowTimeoutSec = kCommissioningWindowTimeoutSec;
diff --git a/src/lib/support/JniTypeWrappers.h b/src/lib/support/JniTypeWrappers.h
index 642e1b4..0b4999b 100644
--- a/src/lib/support/JniTypeWrappers.h
+++ b/src/lib/support/JniTypeWrappers.h
@@ -180,6 +180,22 @@
     jclass mClassRef;
 };
 
+// Manages an pre-existing global reference to a jobject.
+class JniGlobalRefWrapper
+{
+public:
+    explicit JniGlobalRefWrapper(jobject mGlobalRef) : mGlobalRef(mGlobalRef) {}
+    ~JniGlobalRefWrapper()
+    {
+        chip::JniReferences::GetInstance().GetEnvForCurrentThread()->DeleteGlobalRef(mGlobalRef);
+        mGlobalRef = nullptr;
+    }
+    jobject classRef() { return mGlobalRef; }
+
+private:
+    jobject mGlobalRef = nullptr;
+};
+
 class JniLocalReferenceManager
 {
 public: