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 c0b1bcb5a860df..9db82781d855b8 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 @@ public class MainActivity extends AppCompatActivity 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 @@ public void handleCommissioningButtonClicked(DiscoveredNodeData commissioner) { } @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 @@ -69,6 +70,14 @@ public void handleCommissioningComplete() { showFragment(SelectClusterFragment.newInstance(tvCastingApp)); } + @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 boolean initJni() { 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 00000000000000..c6462cd52690e5 --- /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()); + // The desired commissioning window timeout and EndpointFilter are optional. + CompletableFuture 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 4c5d68fdb60654..67db95be2637df 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 @@ public View getView(int i, View view, ViewGroup viewGroup) { 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 71ea4767a03994..723f1b8e93b1a5 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 @@ public interface CastingPlayer { @Override int hashCode(); - // TODO: Implement in following PRs. Related to player connection implementation. - // List getEndpoints(); - // - // ConnectionState getConnectionState(); - // - // CompletableFuture 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 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 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 ecc76a575a115b..d5d93c3204ec34 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 @@ public class MatterCastingPlayer implements CastingPlayer { private int productId; private int vendorId; private long deviceType; + protected long _cppCastingPlayer; public MatterCastingPlayer( boolean connected, @@ -137,4 +147,43 @@ public boolean equals(Object o) { 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 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 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 00000000000000..4c0271c69ecdf2 --- /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 00000000000000..1152e48b95890c --- /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 requiredDeviceTypes; + + public EndpointFilter( + Integer productId, Integer vendorId, List 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 00000000000000..2c0fe914232778 --- /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 +#include +#include +#include +#include + +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(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(_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, "", "()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, "", "(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(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(env->CallIntMethod( + vendorIdIntegerObject, env->GetMethodID(env->GetObjectClass(vendorIdIntegerObject), "intValue", "()I"))) + : 0; + desiredEndpointFilter.productId = productIdIntegerObject != nullptr + ? static_cast(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(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 00000000000000..2870866895c868 --- /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 + +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 255ba570082e16..5d531e0164e4db 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 @@ class DiscoveryDelegateImpl : public DiscoveryDelegate "CastingPlayer jobject")); JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); + chip::DeviceLayer::StackUnlock unlock; env->CallVoidMethod(castingPlayerChangeListenerJavaObject, onAddedCallbackJavaMethodID, matterCastingPlayerJavaObject); } @@ -113,6 +114,7 @@ class DiscoveryDelegateImpl : public DiscoveryDelegate "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 a993a501bd6aa0..72c3677f357707 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 @@ jobject createJCastingPlayer(matter::casting::memory::StrongGetFieldID(matterCastingPlayerJavaClass, "_cppCastingPlayer", "J"); + env->SetLongField(jMatterCastingPlayer, longFieldId, reinterpret_cast(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 00000000000000..8bdd1b4bb741aa --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/res/layout/fragment_matter_connection_example.xml @@ -0,0 +1,23 @@ + + + + + +