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: