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 index dcc148357ebe26..6320c48d0dc3fb 100644 --- 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 @@ -262,6 +262,21 @@ public void handle(CommissionerDeclaration cd) { }); } + @Override + public void onStop() { + super.onStop(); + // Only stop connection if we are still connecting to the device + if (targetCastingPlayer.getConnectionState() == CastingPlayer.ConnectionState.CONNECTING) { + // NOTE, once stopConnecting() is called, the targetCastingPlayer's native object is freed + MatterError err = targetCastingPlayer.stopConnecting(); + if (err.hasError()) { + Log.e( + TAG, + "Going back before connection finishes but stopConnecting() failed due to: " + err); + } + } + } + private void displayPasscodeInputDialog(Context context) { AlertDialog.Builder builder = new AlertDialog.Builder(context); @@ -336,18 +351,9 @@ public void onClick(DialogInterface dialog, int which) { connectionFragmentStatusTextView.setText( "Casting Player CONTINUE CONNECTING failed due to: " + finalErr - + "\n\n"); + + ". Route back to disconnect & try again. \n\n"); }); - Log.e( - TAG, - "displayPasscodeInputDialog() continueConnecting() failed, calling stopConnecting() due to: " - + err); - // Since continueConnecting() failed, Attempt to cancel the connection attempt with - // the CastingPlayer/Commissioner. - err = targetCastingPlayer.stopConnecting(); - if (err.hasError()) { - Log.e(TAG, "displayPasscodeInputDialog() stopConnecting() failed due to: " + err); - } + Log.e(TAG, "displayPasscodeInputDialog() continueConnecting() failed due to: " + err); } } }); @@ -359,20 +365,9 @@ public void onClick(DialogInterface dialog, int which) { public void onClick(DialogInterface dialog, int which) { Log.i( TAG, - "displayPasscodeInputDialog() user cancelled the CastingPlayer/Commissioner-Generated Passcode input dialog. Calling stopConnecting()"); + "displayPasscodeInputDialog() user cancelled the CastingPlayer/Commissioner-Generated Passcode input dialog"); connectionFragmentStatusTextView.setText( - "Connection attempt with Casting Player cancelled by the Casting Client/Commissionee user. \n\nRoute back to exit. \n\n"); - MatterError err = targetCastingPlayer.stopConnecting(); - if (err.hasError()) { - MatterError finalErr = err; - getActivity() - .runOnUiThread( - () -> { - connectionFragmentStatusTextView.setText( - "Casting Player CANCEL failed due to: " + finalErr + "\n\n"); - }); - Log.e(TAG, "displayPasscodeInputDialog() stopConnecting() failed due to: " + err); - } + "Connection attempt with Casting Player cancelled by the Casting Client/Commissionee user. \n\nRoute back to disconnect & exit. \n\n"); dialog.cancel(); } }); 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 d7ba6f00f9888f..7dba783ff8dcbb 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 @@ -29,6 +29,19 @@ * about the service discovered/resolved. */ public interface CastingPlayer { + + /** @brief CastingPlayer's Conenction State */ + public enum ConnectionState { + /** State when CastingPlayer is not connected */ + NOT_CONNECTED, + + /** State when CastingPlayer is attempting to connect */ + CONNECTING, + + /** State when CastingPlayer is connnected */ + CONNECTED, + } + boolean isConnected(); String getDeviceId(); @@ -152,4 +165,16 @@ MatterError verifyOrEstablishConnection( /** @brief Sets the internal connection state of this CastingPlayer to "disconnected" */ void disconnect(); + + /** + * @brief Get CastingPlayer's current ConnectionState. + * @return Current ConnectionState. + */ + ConnectionState getConnectionState(); + + /** + * @brief Get the Current ConnectionState of a CastingPlayer from the native layer. + * @returns A String representation of the CastingPlayer's current connectation. + */ + String getConnectionStateNative(); } 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 29ee17e26db8be..6bfc36c4e7cbeb 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 @@ -32,6 +32,7 @@ */ 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. @@ -267,4 +268,28 @@ public MatterError continueConnecting() { @Override public native void disconnect(); + + /** + * @brief Get CastingPlayer's current ConnectionState. + * @throws IllegalArgumentException or NullPointerException when native layer returns invalid + * state. + * @return Current ConnectionState. + */ + @Override + public ConnectionState getConnectionState() { + return ConnectionState.valueOf(getConnectionStateNative()); + } + + /* + * @brief Get the Current ConnectionState of a CastingPlayer from the native layer. + * + * @returns A String representation of the CastingPlayer's current connectation. + * Possible return values are + * - "NOT_CONNECTED" + * - "CONNECTING" + * - "CONNECTED" + * - Error message or NULL on error + */ + @Override + public native String getConnectionStateNative(); } diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterCastingPlayer-JNI.cpp b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterCastingPlayer-JNI.cpp index 5219dbca3a88af..f16700b45997f8 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterCastingPlayer-JNI.cpp +++ b/examples/tv-casting-app/android/App/app/src/main/jni/cpp/core/MatterCastingPlayer-JNI.cpp @@ -165,6 +165,45 @@ JNI_METHOD(void, disconnect) castingPlayer->Disconnect(); } +JNI_METHOD(jstring, getConnectionStateNative) +(JNIEnv * env, jobject thiz) +{ + char error_str[50]; + jobject jstr_obj = nullptr; + + if (NULL == env) + { + LogErrorOnFailure( + chip::JniReferences::GetInstance().CharToStringUTF(CharSpan::fromCharString("JNIEnv interface is NULL"), jstr_obj)); + return static_cast(jstr_obj); + } + + chip::DeviceLayer::StackLock lock; + ChipLogProgress(AppServer, "MatterCastingPlayer-JNI::getConnectionState()"); + + CastingPlayer * castingPlayer = support::convertCastingPlayerFromJavaToCpp(thiz); + VerifyOrReturnValue(castingPlayer != nullptr, env->NewStringUTF("Cast Player is nullptr")); + + matter::casting::core::ConnectionState state = castingPlayer->GetConnectionState(); + switch (state) + { + case matter::casting::core::ConnectionState::CASTING_PLAYER_NOT_CONNECTED: + LogErrorOnFailure(chip::JniReferences::GetInstance().CharToStringUTF(CharSpan::fromCharString("NOT_CONNECTED"), jstr_obj)); + break; + case matter::casting::core::ConnectionState::CASTING_PLAYER_CONNECTING: + LogErrorOnFailure(chip::JniReferences::GetInstance().CharToStringUTF(CharSpan::fromCharString("CONNECTING"), jstr_obj)); + break; + case matter::casting::core::ConnectionState::CASTING_PLAYER_CONNECTED: + LogErrorOnFailure(chip::JniReferences::GetInstance().CharToStringUTF(CharSpan::fromCharString("CONNECTED"), jstr_obj)); + break; + default: + snprintf(error_str, sizeof(error_str), "Unsupported Connection State: %d", state); + LogErrorOnFailure(chip::JniReferences::GetInstance().CharToStringUTF(CharSpan::fromCharString(error_str), jstr_obj)); + break; + } + return static_cast(jstr_obj); +} + JNI_METHOD(jobject, getEndpoints) (JNIEnv * env, jobject thiz) { diff --git a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCastingPlayer.h b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCastingPlayer.h index a8439935a34360..bfd47804dea371 100644 --- a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCastingPlayer.h +++ b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCastingPlayer.h @@ -26,6 +26,16 @@ @class MCEndpoint; +/** + * @brief Represents CastingPlayer ConnectionState. + * @note Should be kept up to date with matter::casting::core::ConnectionState. + */ +typedef enum { + MC_CASTING_PLAYER_NOT_CONNECTED, + MC_CASTING_PLAYER_CONNECTING, + MC_CASTING_PLAYER_CONNECTED, +} MCCastingPlayerConnectionState; + /** * @brief MCCastingPlayer represents a Matter commissioner that is able to play media to a physical * output or to a display screen which is part of the device. @@ -148,6 +158,13 @@ */ - (void)disconnect; +/** + * @brief Get the CastingPlayer's connection state + * @param state The current connection state that will be return. + * @return nil if request submitted successfully, otherwise a NSError object corresponding to the error. + */ +- (NSError * _Nullable)getConnectionState:(MCCastingPlayerConnectionState * _Nonnull)state; + - (NSString * _Nonnull)identifier; - (NSString * _Nonnull)deviceName; - (uint16_t)vendorId; diff --git a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCastingPlayer.mm b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCastingPlayer.mm index 793f76ff7ee791..3a812704c774cd 100644 --- a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCastingPlayer.mm +++ b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCastingPlayer.mm @@ -190,6 +190,35 @@ - (void)disconnect }); } +- (NSError * _Nullable)getConnectionState:(MCCastingPlayerConnectionState * _Nonnull)state; +{ + ChipLogProgress(AppServer, "MCCastingPlayer.getConnectionState() called"); + VerifyOrReturnValue([[MCCastingApp getSharedInstance] isRunning], [MCErrorUtils NSErrorFromChipError:CHIP_ERROR_INCORRECT_STATE], ChipLogError(AppServer, "MCCastingPlayer.getConnectionState() MCCastingApp NOT running")); + + __block matter::casting::core::ConnectionState native_state = matter::casting::core::ConnectionState::CASTING_PLAYER_NOT_CONNECTED; + dispatch_queue_t workQueue = [[MCCastingApp getSharedInstance] getWorkQueue]; + dispatch_sync(workQueue, ^{ + native_state = _cppCastingPlayer->GetConnectionState(); + }); + + switch (native_state) { + case matter::casting::core::ConnectionState::CASTING_PLAYER_NOT_CONNECTED: + *state = MC_CASTING_PLAYER_NOT_CONNECTED; + break; + case matter::casting::core::ConnectionState::CASTING_PLAYER_CONNECTING: + *state = MC_CASTING_PLAYER_CONNECTING; + break; + case matter::casting::core::ConnectionState::CASTING_PLAYER_CONNECTED: + *state = MC_CASTING_PLAYER_CONNECTED; + break; + default: + [NSException raise:@"Unhandled matter::casting::core::ConnectionState" format:@"%d is not handled", native_state]; + break; + } + + return nil; +} + - (instancetype _Nonnull)initWithCppCastingPlayer:(matter::casting::memory::Strong)cppCastingPlayer { if (self = [super init]) { diff --git a/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCConnectionExampleView.swift b/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCConnectionExampleView.swift index 5760fae84aa28c..8e3788c5b961c6 100644 --- a/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCConnectionExampleView.swift +++ b/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCConnectionExampleView.swift @@ -88,6 +88,9 @@ struct MCConnectionExampleView: View { viewModel.connect(selectedCastingPlayer: self.selectedCastingPlayer, useCommissionerGeneratedPasscode: self.useCommissionerGeneratedPasscode) } }) + .onDisappear(perform: { + viewModel.cancelConnectionAttempt(selectedCastingPlayer: self.selectedCastingPlayer) + }) } } diff --git a/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCConnectionExampleViewModel.swift b/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCConnectionExampleViewModel.swift index 815c79fc381589..8848cf456cd521 100644 --- a/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCConnectionExampleViewModel.swift +++ b/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCConnectionExampleViewModel.swift @@ -42,6 +42,29 @@ class MCConnectionExampleViewModel: ObservableObject { @Published var errorCodeDescription: String? + func cancelConnectionAttempt(selectedCastingPlayer: MCCastingPlayer?) { + DispatchQueue.main.async { + // Only stop connection if we are pending passcode confirmation + var connectionState = MC_CASTING_PLAYER_NOT_CONNECTED; + let err = selectedCastingPlayer?.getConnectionState(&connectionState) + if err != nil { + self.Log.error("MCConnectionExampleViewModel cancelConnect() MCCastingPlayer.getConnectionState() failed due to: \(err)") + } + + if connectionState == MC_CASTING_PLAYER_CONNECTING { + self.Log.info("MCConnectionExampleViewModel cancelConnect(). User navigating back from ConnectionView") + let err = selectedCastingPlayer?.stopConnecting() + if err == nil { + self.connectionStatus = "User cancelled the connection attempt with CastingPlayer.stopConnecting()." + self.Log.info("MCConnectionExampleViewModel cancelConnect() MCCastingPlayer.stopConnecting() succeeded.") + } else { + self.connectionStatus = "Cancel connection failed due to: \(String(describing: err))." + self.Log.error("MCConnectionExampleViewModel cancelConnect() MCCastingPlayer.stopConnecting() failed due to: \(err)") + } + } + } + } + func connect(selectedCastingPlayer: MCCastingPlayer?, useCommissionerGeneratedPasscode: Bool) { self.Log.info("MCConnectionExampleViewModel.connect() useCommissionerGeneratedPasscode: \(String(describing: useCommissionerGeneratedPasscode))") @@ -108,17 +131,9 @@ class MCConnectionExampleViewModel: ObservableObject { dataSource.update(newCommissionableData) self.Log.info("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, Updated MCAppParametersDataSource instance with new MCCommissionableData.") } else { - self.Log.error("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, InitializationExample.getAppParametersDataSource() failed, calling stopConnecting()") - self.connectionStatus = "Failed to update the MCAppParametersDataSource with the user entered passcode: \n\nRoute back and try again." + self.Log.error("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, InitializationExample.getAppParametersDataSource() failed") + self.connectionStatus = "Failed to update the MCAppParametersDataSource with the user entered passcode: \n\nRoute back to disconnect and try again." self.connectionSuccess = false - // Since we failed to update the passcode, Attempt to cancel the connection attempt with - // the CastingPlayer/Commissioner. - let err = selectedCastingPlayer?.stopConnecting() - if err == nil { - self.Log.info("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, InitializationExample.getAppParametersDataSource() failed, then stopConnecting() succeeded") - } else { - self.Log.error("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, InitializationExample.getAppParametersDataSource() failed, then stopConnecting() failed due to: \(err)") - } return } @@ -127,28 +142,13 @@ class MCConnectionExampleViewModel: ObservableObject { if errContinue == nil { self.connectionStatus = "Continuing to connect with user entered passcode: \(userEnteredPasscode)" } else { - self.connectionStatus = "Continue Connecting to Casting Player failed with: \(String(describing: errContinue)) \n\nRoute back and try again." + self.connectionStatus = "Continue Connecting to Casting Player failed with: \(String(describing: errContinue)) \n\nRoute back to disconnect and try again." self.Log.error("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, MCCastingPlayer.continueConnecting() failed due to: \(errContinue)") - // Since continueConnecting() failed, Attempt to cancel the connection attempt with - // the CastingPlayer/Commissioner. - let err = selectedCastingPlayer?.stopConnecting() - if err == nil { - self.Log.info("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, MCCastingPlayer.continueConnecting() failed, then stopConnecting() succeeded") - } else { - self.Log.error("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, MCCastingPlayer.continueConnecting() failed, then stopConnecting() failed due to: \(err)") - } } }, cancelConnecting: { - self.Log.info("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, Connection attempt cancelled by the user, calling MCCastingPlayer.stopConnecting()") - let err = selectedCastingPlayer?.stopConnecting() + self.Log.info("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, Connection attempt cancelled by the user") self.connectionSuccess = false - if err == nil { - self.connectionStatus = "User cancelled the connection attempt with CastingPlayer. \n\nRoute back to exit." - self.Log.info("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, User cancelled the connection attempt with MCCastingPlayer, MCCastingPlayer.stopConnecting() succeeded.") - } else { - self.connectionStatus = "Cancel connection failed due to: \(String(describing: err)) \n\nRoute back to exit." - self.Log.error("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, MCCastingPlayer.stopConnecting() failed due to: \(err)") - } + self.connectionStatus = "User cancelled the connection attempt with CastingPlayer. \n\nRoute back to disconnect & exit." }) } } 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 37d442f7142aa2..113a559f4bced6 100644 --- a/examples/tv-casting-app/tv-casting-common/core/CastingPlayer.cpp +++ b/examples/tv-casting-app/tv-casting-common/core/CastingPlayer.cpp @@ -228,11 +228,6 @@ CHIP_ERROR CastingPlayer::ContinueConnecting() CHIP_ERROR CastingPlayer::StopConnecting() { - VerifyOrReturnValue( - mIdOptions.mCommissionerPasscode, CHIP_ERROR_INCORRECT_STATE, - ChipLogError(AppServer, - "CastingPlayer::StopConnecting() mIdOptions.mCommissionerPasscode == false, ContinueConnecting() should only " - "be called when the CastingPlayer/Commissioner-Generated passcode commissioning flow is in progress.");); // Calling the internal StopConnecting() API with the shouldSendIdentificationDeclarationMessage set to true to notify the // CastingPlayer/Commissioner that the commissioning session was cancelled by the Casting Client/Commissionee user. This will // result in the Casting Client/Commissionee sending a CancelPasscode IdentificationDeclaration message to the CastingPlayer. diff --git a/examples/tv-casting-app/tv-casting-common/core/CastingPlayer.h b/examples/tv-casting-app/tv-casting-common/core/CastingPlayer.h index b2422b43f5dee6..558ae301bc06fc 100644 --- a/examples/tv-casting-app/tv-casting-common/core/CastingPlayer.h +++ b/examples/tv-casting-app/tv-casting-common/core/CastingPlayer.h @@ -210,11 +210,15 @@ class CastingPlayer : public std::enable_shared_from_this * otherwise. StopConnecting() can only be called by the client during the CastingPlayer/Commissioner-Generated passcode * commissioning flow. Calling StopConnecting() during the Client/Commissionee-Generated commissioning flow will return a * CHIP_ERROR_INCORRECT_STATE error. + * + * @note This method will free the calling object as a side effect. */ CHIP_ERROR StopConnecting(); /** * @brief Sets the internal connection state of this CastingPlayer to "disconnected" + * + * @note This method will free the calling object as a side effect. */ void Disconnect(); @@ -278,7 +282,11 @@ class CastingPlayer : public std::enable_shared_from_this /** * @brief Return the current state of the CastingPlayer */ - ConnectionState GetConnectionState() const { return mConnectionState; } + ConnectionState GetConnectionState() const + { + ChipLogError(AppServer, "CastingPlayer::GetConnectionState() state: %d", mConnectionState); + return mConnectionState; + } private: std::vector> mEndpoints; @@ -318,6 +326,8 @@ class CastingPlayer : public std::enable_shared_from_this /** * @brief resets this CastingPlayer's state and calls mOnCompleted with the CHIP_ERROR. Also, after calling mOnCompleted, it * clears mOnCompleted by setting it to a nullptr. + * + * @note This method will free the calling object as a side effect. */ void resetState(CHIP_ERROR err); diff --git a/examples/tv-casting-app/tv-casting-common/core/CastingPlayerDiscovery.h b/examples/tv-casting-app/tv-casting-common/core/CastingPlayerDiscovery.h index 3a98472bd8c6d1..0a6fdb9da56ec5 100644 --- a/examples/tv-casting-app/tv-casting-common/core/CastingPlayerDiscovery.h +++ b/examples/tv-casting-app/tv-casting-common/core/CastingPlayerDiscovery.h @@ -108,6 +108,7 @@ class CastingPlayerDiscovery static CastingPlayerDiscovery * GetInstance(); ~CastingPlayerDiscovery() { + ChipLogError(AppServer, "CastingPlayerDiscovery destructor() called"); mCastingPlayers.clear(); mCastingPlayersInternal.clear(); } diff --git a/examples/tv-casting-app/tv-casting-common/support/ChipDeviceEventHandler.cpp b/examples/tv-casting-app/tv-casting-common/support/ChipDeviceEventHandler.cpp index 91c0da0173e693..b83201fc5c22cf 100644 --- a/examples/tv-casting-app/tv-casting-common/support/ChipDeviceEventHandler.cpp +++ b/examples/tv-casting-app/tv-casting-common/support/ChipDeviceEventHandler.cpp @@ -37,6 +37,18 @@ void ChipDeviceEventHandler::Handle(const chip::DeviceLayer::ChipDeviceEvent * e { ChipLogProgress(AppServer, "ChipDeviceEventHandler::Handle() called"); + // Make sure we have not disconnected from the TargetCastingPlayer when handling incoming messages. + // Sometimes the tv-app will still send messages after we clean up the TargetCastingPlayer. + // Exmaple: Call stopConnecting() after immediately after calling continueConnectiong() + // + // A proper fix would be to either not let user navigate back when running post commission or update commissioner, commissionee + // communication protocal to better handle asynchronous disconnects + if (CastingPlayer::GetTargetCastingPlayer() == nullptr) + { + ChipLogError(AppServer, "ChipDeviceEventHandler::Handler() TargetCastingPlayer is null for event: %u", event->Type); + return; + } + bool runPostCommissioning = false; chip::NodeId targetNodeId = 0; chip::FabricIndex targetFabricIndex = 0;