From 135f900f3e728ccc93b26a7ff1fc0844a774efe8 Mon Sep 17 00:00:00 2001 From: Philip Gregor Date: Mon, 22 Jan 2024 15:54:22 -0800 Subject: [PATCH 1/8] tv-casting-app: simplified android connection API --- .../com/chip/casting/app/MainActivity.java | 22 ++- .../casting/ConnectionExampleFragment.java | 127 ++++++++++++++++++ .../casting/DiscoveryExampleFragment.java | 5 +- .../matter/casting/core/CastingPlayer.java | 44 +++++- .../casting/core/MatterCastingPlayer.java | 49 +++++++ .../casting/support/DeviceTypeStruct.java | 32 +++++ .../casting/support/EndpointFilter.java | 39 ++++++ .../main/jni/cpp/core/CastingPlayer-JNI.cpp | 118 ++++++++++++++++ .../src/main/jni/cpp/core/CastingPlayer-JNI.h | 41 ++++++ .../support/CastingPlayerConverter-JNI.cpp | 4 + .../fragment_matter_connection_example.xml | 23 ++++ .../App/app/src/main/res/values/strings.xml | 1 + examples/tv-casting-app/android/BUILD.gn | 4 + .../tv-casting-common/core/CastingPlayer.cpp | 20 +-- 14 files changed, 507 insertions(+), 22 deletions(-) create mode 100644 examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ConnectionExampleFragment.java create mode 100644 examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/DeviceTypeStruct.java create mode 100644 examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/EndpointFilter.java create mode 100644 examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayer-JNI.cpp create mode 100644 examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayer-JNI.h create mode 100644 examples/tv-casting-app/android/App/app/src/main/res/layout/fragment_matter_connection_example.xml 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..4b18a64229fa8b 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,17 @@ public void handleCommissioningComplete() { showFragment(SelectClusterFragment.newInstance(tvCastingApp)); } + @Override + public void handleConnectionComplete(CastingPlayer castingPlayer) { + Log.i( + TAG, + "MainActivity.handleConnectionComplete() called with CastingPlayer with deviceId: " + + castingPlayer.getDeviceId()); + + // TODO: Implement in following PRs. Select Cluster Fragment. + // showFragment(SelectClusterFragment.newInstance(tvCastingApp)); + } + @Override public void handleContentLauncherSelected() { showFragment(ContentLauncherFragment.newInstance(tvCastingApp)); @@ -115,7 +127,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..70f30f49793ad8 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/java/com/matter/casting/ConnectionExampleFragment.java @@ -0,0 +1,127 @@ +/* + * 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 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 selectedCastingPlayer; + private TextView connectionFragmentStatusTextView; + private Button connectionFragmentNextButton; + + public ConnectionExampleFragment(CastingPlayer selectedCastingPlayer) { + Log.i(TAG, "ConnectionExampleFragment() called with CastingPlayer with deviceId: " + selectedCastingPlayer.getDeviceId()); + this.selectedCastingPlayer = selectedCastingPlayer; + } + + /** + * 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.connectionWindowStatus); + connectionFragmentStatusTextView.setText("Verifying or establishing connection with Casting Player with device name: " + selectedCastingPlayer.getDeviceName()); + + connectionFragmentNextButton = getView().findViewById(R.id.connectionWindowNextButton); + Callback callback = (ConnectionExampleFragment.Callback) this.getActivity(); + connectionFragmentNextButton.setOnClickListener( + v -> { + Log.i(TAG, "onViewCreated() connectionWindowNextButton button clicked. Calling MainActivity.handleConnectionComplete()"); + callback.handleConnectionComplete(selectedCastingPlayer); + }); + + Executors.newSingleThreadExecutor().submit( () -> { + Log.d(TAG, "onViewCreated() calling verifyOrEstablishConnection() on CastingPlayer with deviceId: " + selectedCastingPlayer.getDeviceId()); + + CompletableFuture completableFuture = selectedCastingPlayer.VerifyOrEstablishConnection(); + // Optionally, we can specify the desired commissioning window duration and Endpoint Filter. + //EndpointFilter desiredEndpointFilter = new EndpointFilter(0, 0, new ArrayList()); + //CompletableFuture completableFuture = selectedCastingPlayer.VerifyOrEstablishConnection(MIN_CONNECTION_TIMEOUT_SEC, desiredEndpointFilter); + + Log.d(TAG, "onViewCreated() verifyOrEstablishConnection() called"); + if (completableFuture == null) { + Log.e(TAG, "onViewCreated() verifyOrEstablishConnection() Warning: completableFuture == null"); + } + + completableFuture.thenRun( + () -> { + Log.i(TAG, "onViewCreated() CompletableFuture.thenRun(), Connected to CastingPlayer with deviceId: " + selectedCastingPlayer.getDeviceId()); + getActivity().runOnUiThread( + () -> { + connectionFragmentStatusTextView.setText("Connected to Casting Player with device name: " + selectedCastingPlayer.getDeviceName()); + connectionFragmentNextButton.setEnabled(true); + }); + }).exceptionally( + exc -> { + Log.e(TAG, "onViewCreated() CompletableFuture.exceptionally(), CastingPlayer connection failed due to exception: " + 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 commissioning */ + 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..5f89bcde80dd5c 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,13 +57,45 @@ public interface CastingPlayer { @Override int hashCode(); - // TODO: Implement in following PRs. Related to player connection implementation. - // List getEndpoints(); - // + /** + * 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(); + + // TODO: Implement in following PRs. + // ConnectionState getConnectionState(); // - // CompletableFuture connect(long timeout); - // // static class ConnectionState extends Observable { // private boolean connected; // 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..e4a6a30e585bcd --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/DeviceTypeStruct.java @@ -0,0 +1,32 @@ +/* + * 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..e4195a32e5310c --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/EndpointFilter.java @@ -0,0 +1,39 @@ +/* + * 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 0 means unspecified + public int productId; + // Value of 0 means unspecified + public int vendorId; + public List requiredDeviceTypes; + + public EndpointFilter( + int productId, + int 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..9ff6fd05434d77 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/CastingPlayer-JNI.cpp @@ -0,0 +1,118 @@ +/* + * 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() Warning: _cppCastingPlayerFieldId == nullptr")); + + jlong _cppCastingPlayerValue = env->GetLongField(thiz, _cppCastingPlayerFieldId); + CastingPlayer* castingPlayer = reinterpret_cast(_cppCastingPlayerValue); + VerifyOrReturnValue(castingPlayer != nullptr, nullptr, ChipLogError(AppServer, "CastingPlayer-JNI::VerifyOrEstablishConnection() Warning: 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() Warning: completableFutureObjGlobalRef == nullptr")); + + ConnectCallback callback = [completableFutureObjGlobalRef](CHIP_ERROR err, CastingPlayer* playerPtr) { + ChipLogProgress(AppServer, "CastingPlayer-JNI::VerifyOrEstablishConnection() ConnectCallback called"); + if (completableFutureObjGlobalRef == nullptr) { + // Prevents an app crash at CallBooleanMethod + ChipLogError(AppServer, "CastingPlayer-JNI::VerifyOrEstablishConnection() ConnectCallback, Warning: completableFutureObjGlobalRef == nullptr"); + } else { + JNIEnv * env = JniReferences::GetInstance().GetEnvForCurrentThread(); + jclass completableFutureClass = env->FindClass("java/util/concurrent/CompletableFuture"); + + if (err == CHIP_NO_ERROR) { + ChipLogProgress(AppServer, "CastingPlayer-JNI::VerifyOrEstablishConnection() ConnectCallback, Casting Player connection successful!"); + jmethodID completeMethod = env->GetMethodID(completableFutureClass, "complete", "(Ljava/lang/Object;)Z"); + env->CallBooleanMethod(completableFutureObjGlobalRef, completeMethod, NULL); + } else { + ChipLogError(AppServer, "CastingPlayer-JNI::VerifyOrEstablishConnection() ConnectCallback, Warning: connection error: %" CHIP_ERROR_FORMAT, err.Format()); + jmethodID completeExceptionallyMethod = env->GetMethodID(completableFutureClass, "completeExceptionally", "(Ljava/lang/Throwable;)Z"); + // Create a Throwable object (e.g., RuntimeException) to pass to completeExceptionallyMethod + jclass throwableClass = env->FindClass("java/lang/RuntimeException"); + jmethodID throwableConstructor = env->GetMethodID(throwableClass, "", "(Ljava/lang/String;)V"); + jstring errorMessage = env->NewStringUTF(err.Format()); + jobject throwableObject = env->NewObject(throwableClass, throwableConstructor, errorMessage); + env->CallBooleanMethod(completableFutureObjGlobalRef, completeExceptionallyMethod, throwableObject); + } + env->DeleteGlobalRef(completableFutureObjGlobalRef); + } + }; + + 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 { + ChipLogProgress(AppServer, "CastingPlayer-JNI::VerifyOrEstablishConnection() calling CastingPlayer::VerifyOrEstablishConnection(desiredEndpointFilter) on Casting Player with device ID: %s", castingPlayer->GetId()); + // Convert the EndpointFilter Java class to a C++ EndpointFilter + jclass endpointFilterJavaClass = env->GetObjectClass(desiredEndpointFilterJavaObject); + jfieldID vendorIdFieldId = env->GetFieldID(endpointFilterJavaClass, "productId", "I"); + jfieldID productIdFieldId = env->GetFieldID(endpointFilterJavaClass, "vendorId", "I"); + jfieldID requiredDeviceTypesFieldId = env->GetFieldID(endpointFilterJavaClass, "requiredDeviceTypes", "Ljava/util/List;"); + + matter::casting::core::EndpointFilter desiredEndpointFilter; + desiredEndpointFilter.vendorId = static_cast(env->GetIntField(desiredEndpointFilterJavaObject, vendorIdFieldId)); + desiredEndpointFilter.productId = static_cast(env->GetIntField(desiredEndpointFilterJavaObject, vendorIdFieldId)); + // TODO: In following PRs. Translate the Java requiredDeviceTypes list to a C++ requiredDeviceTypes vector. For now we're + // passing an empty list of. + + 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/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..f17e38339e1150 --- /dev/null +++ b/examples/tv-casting-app/android/App/app/src/main/res/layout/fragment_matter_connection_example.xml @@ -0,0 +1,23 @@ + + + + + +