diff --git a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/TargetAppInfo.java b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/TargetAppInfo.java index 1f605a89833bb1..e8b0553b322c47 100644 --- a/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/TargetAppInfo.java +++ b/examples/tv-casting-app/android/App/app/src/main/jni/com/matter/casting/support/TargetAppInfo.java @@ -14,8 +14,8 @@ package com.matter.casting.support; /** - * Feature: Target Content Application - The set of content app Vendor IDs (and optionally, Product - * IDs) that can be used for authentication. + * Feature: Target Content Application - An entry in the IdentificationDeclarationOptions.java + * TargetAppList which contains a TargetVendorId and an optional TargetProductId. */ public class TargetAppInfo { /** Target Target Content Application Vendor ID, null means unspecified */ diff --git a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge.xcodeproj/project.pbxproj b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge.xcodeproj/project.pbxproj index 5df8ce26bf4019..f289f0d519dcea 100644 --- a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge.xcodeproj/project.pbxproj +++ b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge.xcodeproj/project.pbxproj @@ -7,8 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + 393039D52C1CF3C500DE8681 /* MCIdentificationDeclarationOptions_Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 393039D42C1CF3C500DE8681 /* MCIdentificationDeclarationOptions_Internal.h */; }; + 3938C1ED2C1A1C8D009B2B35 /* MCCommissionerDeclaration_Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 3938C1EC2C1A1C8D009B2B35 /* MCCommissionerDeclaration_Internal.h */; }; 39589F162B91556B00BE040C /* MCInteractionModelStructs.h in Headers */ = {isa = PBXBuildFile; fileRef = 39589F152B91556B00BE040C /* MCInteractionModelStructs.h */; }; 39589F182B91557700BE040C /* MCInteractionModelStructs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 39589F172B91557700BE040C /* MCInteractionModelStructs.mm */; }; + 395BF6AF2C18EC7C00C73CE2 /* MCTargetAppInfo.mm in Sources */ = {isa = PBXBuildFile; fileRef = 395BF6AE2C18EC7C00C73CE2 /* MCTargetAppInfo.mm */; }; + 399BCE1E2C13CBAD0075AA85 /* MCTargetAppInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 399BCE1D2C13CBAD0075AA85 /* MCTargetAppInfo.h */; }; 39BF57C52B8CFF790081653C /* MCAttributeObjects.h in Headers */ = {isa = PBXBuildFile; fileRef = 39BF57C42B8CFF790081653C /* MCAttributeObjects.h */; }; 39BF57C72B8CFFB90081653C /* MCAttributeObjects.mm in Sources */ = {isa = PBXBuildFile; fileRef = 39BF57C62B8CFFB90081653C /* MCAttributeObjects.mm */; }; 39BF57C92B8D66540081653C /* NSStringSpanConversion.h in Headers */ = {isa = PBXBuildFile; fileRef = 39BF57C82B8D66540081653C /* NSStringSpanConversion.h */; }; @@ -19,6 +23,12 @@ 39D4D2522B97943D00BF3CFE /* MCCommandObjects.mm in Sources */ = {isa = PBXBuildFile; fileRef = 39D4D2512B97943D00BF3CFE /* MCCommandObjects.mm */; }; 39DB29E92B9A3B1D0071334A /* MCCommandPayloads.h in Headers */ = {isa = PBXBuildFile; fileRef = 39DB29E82B9A3B1D0071334A /* MCCommandPayloads.h */; }; 39DB29EB2B9A3B2D0071334A /* MCCommandPayloads.mm in Sources */ = {isa = PBXBuildFile; fileRef = 39DB29EA2B9A3B2D0071334A /* MCCommandPayloads.mm */; }; + 39F589DA2C176EA4000AAADD /* MCIdentificationDeclarationOptions.h in Headers */ = {isa = PBXBuildFile; fileRef = 39F589D92C176EA4000AAADD /* MCIdentificationDeclarationOptions.h */; }; + 39F589DC2C176EC0000AAADD /* MCIdentificationDeclarationOptions.mm in Sources */ = {isa = PBXBuildFile; fileRef = 39F589DB2C176EC0000AAADD /* MCIdentificationDeclarationOptions.mm */; }; + 39F589DE2C1781C8000AAADD /* MCCommissionerDeclaration.h in Headers */ = {isa = PBXBuildFile; fileRef = 39F589DD2C1781C8000AAADD /* MCCommissionerDeclaration.h */; }; + 39F589E02C1781E4000AAADD /* MCCommissionerDeclaration.mm in Sources */ = {isa = PBXBuildFile; fileRef = 39F589DF2C1781E4000AAADD /* MCCommissionerDeclaration.mm */; }; + 39F589E22C179A09000AAADD /* MCConnectionCallbacks.h in Headers */ = {isa = PBXBuildFile; fileRef = 39F589E12C179A09000AAADD /* MCConnectionCallbacks.h */; }; + 39F589E42C179A18000AAADD /* MCConnectionCallbacks.mm in Sources */ = {isa = PBXBuildFile; fileRef = 39F589E32C179A18000AAADD /* MCConnectionCallbacks.mm */; }; 3C0474062B3F7E5F0012AE95 /* MCEndpointFilter.h in Headers */ = {isa = PBXBuildFile; fileRef = 3C0474052B3F7E5F0012AE95 /* MCEndpointFilter.h */; }; 3C04740C2B4604CF0012AE95 /* MCCryptoUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 3C04740B2B4604CF0012AE95 /* MCCryptoUtils.h */; }; 3C04740E2B4605B40012AE95 /* MCCryptoUtils.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3C04740D2B4605B40012AE95 /* MCCryptoUtils.mm */; }; @@ -81,9 +91,13 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 393039D42C1CF3C500DE8681 /* MCIdentificationDeclarationOptions_Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MCIdentificationDeclarationOptions_Internal.h; sourceTree = ""; }; + 3938C1EC2C1A1C8D009B2B35 /* MCCommissionerDeclaration_Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MCCommissionerDeclaration_Internal.h; sourceTree = ""; }; 39589F152B91556B00BE040C /* MCInteractionModelStructs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MCInteractionModelStructs.h; sourceTree = ""; }; 39589F172B91557700BE040C /* MCInteractionModelStructs.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MCInteractionModelStructs.mm; sourceTree = ""; }; + 395BF6AE2C18EC7C00C73CE2 /* MCTargetAppInfo.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MCTargetAppInfo.mm; sourceTree = ""; }; 399049A62B9FC4ED000C91F0 /* MCCommandPayloads_Internal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MCCommandPayloads_Internal.h; sourceTree = ""; }; + 399BCE1D2C13CBAD0075AA85 /* MCTargetAppInfo.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MCTargetAppInfo.h; sourceTree = ""; }; 39BF57C42B8CFF790081653C /* MCAttributeObjects.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MCAttributeObjects.h; sourceTree = ""; }; 39BF57C62B8CFFB90081653C /* MCAttributeObjects.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MCAttributeObjects.mm; sourceTree = ""; }; 39BF57C82B8D66540081653C /* NSStringSpanConversion.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NSStringSpanConversion.h; sourceTree = ""; }; @@ -94,6 +108,12 @@ 39D4D2512B97943D00BF3CFE /* MCCommandObjects.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MCCommandObjects.mm; sourceTree = ""; }; 39DB29E82B9A3B1D0071334A /* MCCommandPayloads.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MCCommandPayloads.h; sourceTree = ""; }; 39DB29EA2B9A3B2D0071334A /* MCCommandPayloads.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MCCommandPayloads.mm; sourceTree = ""; }; + 39F589D92C176EA4000AAADD /* MCIdentificationDeclarationOptions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MCIdentificationDeclarationOptions.h; sourceTree = ""; }; + 39F589DB2C176EC0000AAADD /* MCIdentificationDeclarationOptions.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MCIdentificationDeclarationOptions.mm; sourceTree = ""; }; + 39F589DD2C1781C8000AAADD /* MCCommissionerDeclaration.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MCCommissionerDeclaration.h; sourceTree = ""; }; + 39F589DF2C1781E4000AAADD /* MCCommissionerDeclaration.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MCCommissionerDeclaration.mm; sourceTree = ""; }; + 39F589E12C179A09000AAADD /* MCConnectionCallbacks.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MCConnectionCallbacks.h; sourceTree = ""; }; + 39F589E32C179A18000AAADD /* MCConnectionCallbacks.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MCConnectionCallbacks.mm; sourceTree = ""; }; 3C0474052B3F7E5F0012AE95 /* MCEndpointFilter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MCEndpointFilter.h; sourceTree = ""; }; 3C04740B2B4604CF0012AE95 /* MCCryptoUtils.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MCCryptoUtils.h; sourceTree = ""; }; 3C04740D2B4605B40012AE95 /* MCCryptoUtils.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = MCCryptoUtils.mm; sourceTree = ""; }; @@ -276,6 +296,16 @@ 3C4F52132B4F31DC00BB8A10 /* MCDeviceTypeStruct.h */, 3C4F52152B4F31FA00BB8A10 /* MCDeviceTypeStruct.m */, 3C0474052B3F7E5F0012AE95 /* MCEndpointFilter.h */, + 399BCE1D2C13CBAD0075AA85 /* MCTargetAppInfo.h */, + 395BF6AE2C18EC7C00C73CE2 /* MCTargetAppInfo.mm */, + 39F589E12C179A09000AAADD /* MCConnectionCallbacks.h */, + 39F589E32C179A18000AAADD /* MCConnectionCallbacks.mm */, + 39F589DD2C1781C8000AAADD /* MCCommissionerDeclaration.h */, + 3938C1EC2C1A1C8D009B2B35 /* MCCommissionerDeclaration_Internal.h */, + 39F589DF2C1781E4000AAADD /* MCCommissionerDeclaration.mm */, + 39F589D92C176EA4000AAADD /* MCIdentificationDeclarationOptions.h */, + 393039D42C1CF3C500DE8681 /* MCIdentificationDeclarationOptions_Internal.h */, + 39F589DB2C176EC0000AAADD /* MCIdentificationDeclarationOptions.mm */, 3C2696FA2B4A5FC50026E771 /* MCEndpointFilter.m */, 3CF71C0D2A992DA2003A5CE5 /* MCDataSource.h */, 3CF71C0F2A99312D003A5CE5 /* MCCommissionableData.h */, @@ -312,6 +342,7 @@ 3C2346212B362B4F00FA276E /* MCCastingPlayer.h in Headers */, 3C2346232B362B9500FA276E /* MCCastingPlayerDiscovery.h in Headers */, 3CF71C0E2A992DA2003A5CE5 /* MCDataSource.h in Headers */, + 3938C1ED2C1A1C8D009B2B35 /* MCCommissionerDeclaration_Internal.h in Headers */, 3C4F52142B4F31DC00BB8A10 /* MCDeviceTypeStruct.h in Headers */, 39BF57CD2B8FC0EF0081653C /* MCClusterObjects.h in Headers */, 39D4D2502B97942D00BF3CFE /* MCCommandObjects.h in Headers */, @@ -319,12 +350,16 @@ 3CD73F1C2A9E8396009D82D1 /* MCCommissionableDataProvider.h in Headers */, 3C4F521E2B4F4B3B00BB8A10 /* MCEndpoint_Internal.h in Headers */, 39BF57C92B8D66540081653C /* NSStringSpanConversion.h in Headers */, + 399BCE1E2C13CBAD0075AA85 /* MCTargetAppInfo.h in Headers */, 39DB29E92B9A3B1D0071334A /* MCCommandPayloads.h in Headers */, + 39F589DE2C1781C8000AAADD /* MCCommissionerDeclaration.h in Headers */, 3C4F52262B50899700BB8A10 /* MCCluster_Internal.h in Headers */, + 39F589DA2C176EA4000AAADD /* MCIdentificationDeclarationOptions.h in Headers */, 3CF71C0A2A992D0D003A5CE5 /* MCCastingApp.h in Headers */, 3C4F521C2B4F4B1B00BB8A10 /* MCCastingPlayer_Internal.h in Headers */, 3CD73F172A9E6884009D82D1 /* MCRotatingDeviceIdUniqueIdProvider.h in Headers */, 3C4F52102B4E18C800BB8A10 /* MCEndpoint.h in Headers */, + 393039D52C1CF3C500DE8681 /* MCIdentificationDeclarationOptions_Internal.h in Headers */, 3C4F52282B51DB3000BB8A10 /* MCCommand.h in Headers */, 39BF57CB2B8E54F80081653C /* NSDataSpanConversion.h in Headers */, 3C621CA92B605C52005CDBA3 /* MCAttribute_Internal.h in Headers */, @@ -336,6 +371,7 @@ 39589F162B91556B00BE040C /* MCInteractionModelStructs.h in Headers */, 3C9437922B3B478E0096E5F4 /* MCErrorUtils.h in Headers */, 3C4F52222B507C9300BB8A10 /* MCCluster.h in Headers */, + 39F589E22C179A09000AAADD /* MCConnectionCallbacks.h in Headers */, 3CCB8741286A593700771BAD /* DiscoveredNodeData.h in Headers */, 3C0474062B3F7E5F0012AE95 /* MCEndpointFilter.h in Headers */, 3CCB87212869085400771BAD /* MatterTvCastingBridge.h in Headers */, @@ -449,18 +485,22 @@ 39BF57CF2B8FC1030081653C /* MCClusterObjects.mm in Sources */, 3CD73F192A9E68A7009D82D1 /* MCRotatingDeviceIdUniqueIdProvider.mm in Sources */, 39589F182B91557700BE040C /* MCInteractionModelStructs.mm in Sources */, + 39F589DC2C176EC0000AAADD /* MCIdentificationDeclarationOptions.mm in Sources */, 39DB29EB2B9A3B2D0071334A /* MCCommandPayloads.mm in Sources */, 3CF8532728E37F1000F07B9F /* MatterError.mm in Sources */, 3C4F522C2B51E02800BB8A10 /* MCCommand.mm in Sources */, + 395BF6AF2C18EC7C00C73CE2 /* MCTargetAppInfo.mm in Sources */, 3C4E53B628E5595A00F293E8 /* ContentLauncherTypes.mm in Sources */, 3C81C75028F7A7D3001CB9D1 /* VideoPlayer.m in Sources */, 3CF71C0C2A992D25003A5CE5 /* MCCastingApp.mm in Sources */, 3CC3979E2BE9857C00465462 /* MCCastingPlayer.mm in Sources */, 3C621CAB2B605C6E005CDBA3 /* MCAttribute.mm in Sources */, + 39F589E42C179A18000AAADD /* MCConnectionCallbacks.mm in Sources */, 3C4F52122B4E18ED00BB8A10 /* MCEndpoint.mm in Sources */, 3C4E53B028E4F28100F293E8 /* MediaPlaybackTypes.mm in Sources */, 3C04740E2B4605B40012AE95 /* MCCryptoUtils.mm in Sources */, 3CD73F1E2A9E83C1009D82D1 /* MCCommissionableDataProvider.mm in Sources */, + 39F589E02C1781E4000AAADD /* MCCommissionerDeclaration.mm in Sources */, 3CD73F222A9EA078009D82D1 /* MCDeviceAttestationCredentials.mm in Sources */, 39BF57C72B8CFFB90081653C /* MCAttributeObjects.mm in Sources */, 3C2346252B362BBB00FA276E /* MCCastingPlayerDiscovery.mm in Sources */, diff --git a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCastingApp.h b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCastingApp.h index 51e93093058640..97c57a062dd727 100644 --- a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCastingApp.h +++ b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCastingApp.h @@ -42,6 +42,16 @@ */ - (NSError * _Nullable)initializeWithDataSource:(id _Nonnull)dataSource; +/** + * @brief Updates the MCCommissionableDataProvider stored in this CastingApp with the new CommissionableData + * to be used for the next commissioning session. Calling this function is mandatory for the + * Casting Player/Commissioner-Generated passcode commissioning flow, since the commissioning session's PAKE + * verifier needs to be updated with the user entered passcode. + * + * @param newCommissionableData the new MCCommissionableData to be used for the next commissioning session. + */ +- (NSError * _Nullable)updateCommissionableDataProvider:(MCCommissionableData * _Nonnull)newCommissionableData; + /** * @brief (async) Starts the Matter server that the MCCastingApp runs on and registers all the necessary delegates */ diff --git a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCastingApp.mm b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCastingApp.mm index d124f7658fdca5..db4f4f85b1d5a3 100644 --- a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCastingApp.mm +++ b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCastingApp.mm @@ -43,6 +43,10 @@ @interface MCCastingApp () // queue used to perform all work performed by the MatterTvCastingBridge @property (atomic) dispatch_queue_t workQueue; +// used to update the commissionableDataProvider post initialization, this is necessary for the +// Commissioner-Generated passcode commissioning feature. +@property (nonatomic, strong) id dataSource; + @end @implementation MCCastingApp @@ -69,7 +73,8 @@ - (dispatch_queue_t)getClientQueue - (NSError *)initializeWithDataSource:(id)dataSource { - ChipLogProgress(AppServer, "MCCastingApp.initializeWithDataSource called"); + ChipLogProgress(AppServer, "MCCastingApp.initializeWithDataSource() called"); + _dataSource = dataSource; // get the clientQueue VerifyOrReturnValue([dataSource clientQueue] != nil, [MCErrorUtils NSErrorFromChipError:CHIP_ERROR_INVALID_ARGUMENT]); @@ -78,6 +83,7 @@ - (NSError *)initializeWithDataSource:(id)dataSource // Initialize cpp Providers VerifyOrReturnValue(_uniqueIdProvider.Initialize(dataSource) == CHIP_NO_ERROR, [MCErrorUtils NSErrorFromChipError:CHIP_ERROR_INVALID_ARGUMENT]); + ChipLogProgress(AppServer, "MCCastingApp.initializeWithDataSource() calling MCCommissionableDataProvider.Initialize()"); _commissionableDataProvider = new matter::casting::support::MCCommissionableDataProvider(); VerifyOrReturnValue(_commissionableDataProvider->Initialize(dataSource) == CHIP_NO_ERROR, [MCErrorUtils NSErrorFromChipError:CHIP_ERROR_INVALID_ARGUMENT]); @@ -92,6 +98,7 @@ - (NSError *)initializeWithDataSource:(id)dataSource == CHIP_NO_ERROR, [MCErrorUtils NSErrorFromChipError:CHIP_ERROR_INVALID_ARGUMENT]); + ChipLogProgress(AppServer, "MCCastingApp.initializeWithDataSource() calling cpp CastingApp::Initialize()"); // Initialize cpp CastingApp VerifyOrReturnValue(matter::casting::core::CastingApp::GetInstance()->Initialize(_appParameters) == CHIP_NO_ERROR, [MCErrorUtils NSErrorFromChipError:CHIP_ERROR_INCORRECT_STATE]); @@ -102,6 +109,26 @@ - (NSError *)initializeWithDataSource:(id)dataSource return [MCErrorUtils NSErrorFromChipError:CHIP_NO_ERROR]; } +- (NSError *)updateCommissionableDataProvider:(MCCommissionableData *)newCommissionableData +{ + ChipLogProgress(AppServer, "MCCastingApp.UpdateCommissionableDataProvider() called"); + + if (_dataSource) { + [_dataSource update:newCommissionableData]; + } + + _commissionableDataProvider = new matter::casting::support::MCCommissionableDataProvider(); + VerifyOrReturnValue(_commissionableDataProvider->Initialize(_dataSource) == CHIP_NO_ERROR, + [MCErrorUtils NSErrorFromChipError:CHIP_ERROR_INVALID_ARGUMENT]); + + ChipLogProgress(AppServer, "MCCastingApp.initializeWithDataSource() calling cpp CastingApp::UpdateCommissionableDataProvider()"); + // Update cpp CastingApp CommissionableDataProvider + VerifyOrReturnValue(matter::casting::core::CastingApp::GetInstance()->UpdateCommissionableDataProvider(_commissionableDataProvider) == CHIP_NO_ERROR, + [MCErrorUtils NSErrorFromChipError:CHIP_ERROR_INCORRECT_STATE]); + + return [MCErrorUtils NSErrorFromChipError:CHIP_NO_ERROR]; +} + - (void)startWithCompletionBlock:(void (^)(NSError *))completion { ChipLogProgress(AppServer, "MCCastingApp.startWithCompletionBlock called"); diff --git a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCastingPlayer.h b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCastingPlayer.h index e796f25da609a0..9ac611ab6b20e6 100644 --- a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCastingPlayer.h +++ b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCastingPlayer.h @@ -15,7 +15,9 @@ * limitations under the License. */ +#import "MCConnectionCallbacks.h" #import "MCEndpointFilter.h" +#import "MCIdentificationDeclarationOptions.h" #import @@ -33,32 +35,89 @@ + (NSInteger)kMinCommissioningWindowTimeoutSec; /** - * @brief (async) 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 completion - called back when the connection process completes. Parameter is nil if it completed successfully - * @param timeout - time (in sec) to keep the commissioning window open, if commissioning is required. - * Needs to be >= CastingPlayer.kMinCommissioningWindowTimeoutSec. - * @param desiredEndpointFilter - 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 cached information about the CastingPlayer - * (if any) + * @brief Verifies that a connection exists with this CastingPlayer, or triggers a new + * commissioning 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 + * (UDC) process by sending an IdentificationDeclaration message to the Commissioner. For + * certain UDC features, where a Commissioner reply is expected, this API needs to be followed + * up with the continueConnecting() API defiend below. See the Matter UDC specification or + * parameter class definitions for details on features not included in the description below. + * @param connectionCallbacks contains the connectionCompleteCallback (Required) and + * commissionerDeclarationCallback (Optional) callbacks defiend in MCConnectionCallbacks. + *

For example: During CastingPlayer/Commissioner-Generated passcode commissioning, the + * Commissioner replies with a CommissionerDeclaration message with PasscodeDialogDisplayed + * and CommissionerPasscode set to true. Given these Commissioner state details, the client is + * expected to perform some actions, detailed in the continueConnecting() API below, and then + * call the continueConnecting() API to complete the process. + * @param timeout time (in sec) to keep the commissioning window open, if commissioning is required. + * Needs to be >= CastingPlayer.kMinCommissioningWindowTimeoutSec. + * @param identificationDeclarationOptions (Optional) Parameters in the IdentificationDeclaration + * message sent by the Commissionee to the Commissioner. These parameters specify the + * information relating to the requested commissioning session. + *

For example: To invoke the CastingPlayer/Commissioner-Generated passcode commissioning + * flow, the client would call this API with IdentificationDeclarationOptions containing + * CommissionerPasscode set to true. See IdentificationDeclarationOptions.java for a complete + * list of optional parameters. + *

Furthermore, attributes (such as VendorId) describe the TargetApp/Endpoint that the client + * wants to interact with after commissioning. If this value is passed in, + * verifyOrEstablishConnection() will force UDC, in case the desired TargetApp is not found in + * the on-device cached information/CastingStore. + * @return nil if request submitted successfully, otherwise a NSError object corresponding to the error. */ -- (void)verifyOrEstablishConnectionWithCompletionBlock:(void (^_Nonnull)(NSError * _Nullable))completion timeout:(long long)timeout desiredEndpointFilter:(MCEndpointFilter * _Nullable)desiredEndpointFilter; +- (NSError *)verifyOrEstablishConnectionWithCallbacks:(MCConnectionCallbacks * _Nonnull)connectionCallbacks + timeout:(long)timeout + identificationDeclarationOptions:(MCIdentificationDeclarationOptions * _Nullable)identificationDeclarationOptions; /** - * @brief (async) Verifies that a connection exists with this MCCastingPlayer, or triggers a new session request. If the - * MCCastingApp does not have the nodeId and fabricIndex of this MCCastingPlayer cached on disk, this will execute the user - * directed commissioning process. - * - * @param completion - called back when the connection process completes. Parameter is nil if it completed successfully - * @param desiredEndpointFilter - Attributes (such as VendorId) describing an MCEndpoint 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 cached information about the MCCastingPlayer - * (if any) + * @brief The simplified version of the verifyOrEstablishConnectionWithCallbacks() API above. + * @return nil if request submitted successfully, otherwise a NSError object corresponding to the error. + */ +- (NSError *)verifyOrEstablishConnectionWithCallbacks:(MCConnectionCallbacks * _Nonnull)connectionCallbacks + identificationDeclarationOptions:(MCIdentificationDeclarationOptions * _Nullable)identificationDeclarationOptions; + +/** + * @brief The simplified version of the verifyOrEstablishConnectionWithCallbacks() API above. + * @return nil if request submitted successfully, otherwise a NSError object corresponding to the error. + */ +- (NSError *)verifyOrEstablishConnectionWithCallbacks:(MCConnectionCallbacks * _Nonnull)connectionCallbacks; + +/** + * @brief This is a continuation of the CastingPlayer/Commissioner-Generated passcode + * commissioning flow started via the verifyOrEstablishConnection() API above. It continues + * the UDC process by sending a second IdentificationDeclaration message to Commissioner + * containing CommissionerPasscode and CommissionerPasscodeReady set to true. At this point it + * is assumed that the following have occurred: + *

1. Client (Commissionee) has sent the first IdentificationDeclaration message, via + * verifyOrEstablishConnection(), to the Commissioner containing CommissionerPasscode set to + * true. + *

2. Commissioner generated and displayed a passcode. + *

3. The Commissioner replied with a CommissionerDecelration message with + * PasscodeDialogDisplayed and CommissionerPasscode set to true. + *

4. Client has handled the Commissioner's CommissionerDecelration message. + *

5. Client prompted user to input Passcode from Commissioner. + *

6. Client has updated the CastingApp's MCCommissionableDataProvider with the user entered + * passcode via the following function call: + * MCCastingApp.updateCommissionableDataProvider(MCCommissionableData). This updates the + * commissioning session's PAKE verifier with the user entered passcode. + *

Note: The same connectionCallbacks and commissioningWindowTimeoutSec parameters passed + * into verifyOrEstablishConnection() will be used. + * @return nil if request submitted successfully, otherwise a NSError object corresponding to the error. + */ +- (NSError *)continueConnecting; + +/** + * @brief This cancels the CastingPlayer/Commissioner-Generated passcode commissioning flow + * started via the VerifyOrEstablishConnection() API above. It constructs and sends an + * IdentificationDeclaration message to the CastingPlayer/Commissioner containing + * CancelPasscode set to true. It is used to indicate that the user, and thus the + * Client/Commissionee, have cancelled the commissioning process. This indicates that the + * CastingPlayer/Commissioner can dismiss any dialogs corresponding to commissioning, such as + * a Passcode input dialog or a Passcode display dialog. + *

Note: stopConnecting() does not call the connectionCompleteCallback() callback passed to the + * VerifyOrEstablishConnection() API above since no connection is established. + * @return nil if request submitted successfully, otherwise a NSError object corresponding to the error. */ -- (void)verifyOrEstablishConnectionWithCompletionBlock:(void (^_Nonnull)(NSError * _Nullable))completion desiredEndpointFilter:(MCEndpointFilter * _Nullable)desiredEndpointFilter; +- (NSError *)stopConnecting; /** * @brief Sets the internal connection state of this MCCastingPlayer to "disconnected" @@ -79,6 +138,7 @@ * @brief Returns the NSArray of MCEndpoints associated with this MCCastingPlayer */ - (NSArray * _Nonnull)endpoints; +- (void)logAllEndpoints; - (nonnull instancetype)init UNAVAILABLE_ATTRIBUTE; + (nonnull instancetype)new UNAVAILABLE_ATTRIBUTE; diff --git a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCastingPlayer.mm b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCastingPlayer.mm index f36c51728100ab..868ef822bd799d 100644 --- a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCastingPlayer.mm +++ b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCastingPlayer.mm @@ -18,11 +18,15 @@ #import "MCCastingPlayer.h" #import "MCCastingApp.h" +#import "MCCommissionerDeclaration_Internal.h" +#import "MCConnectionCallbacks.h" #import "MCEndpoint_Internal.h" #import "MCErrorUtils.h" +#import "MCIdentificationDeclarationOptions_Internal.h" #import "core/CastingPlayer.h" // from tv-casting-common #import "core/ConnectionCallbacks.h" // from tv-casting-common +#import "core/IdentificationDeclarationOptions.h" // from tv-casting-common #import @@ -41,53 +45,118 @@ + (NSInteger)kMinCommissioningWindowTimeoutSec return kMinCommissioningWindowTimeoutSec; } -- (void)verifyOrEstablishConnectionWithCompletionBlock:(void (^_Nonnull)(NSError * _Nullable))completion desiredEndpointFilter:(MCEndpointFilter * _Nullable)desiredEndpointFilter +- (NSError *)verifyOrEstablishConnectionWithCallbacks:(MCConnectionCallbacks * _Nonnull)connectionCallbacks { - [self verifyOrEstablishConnectionWithCompletionBlock:completion timeout:kMinCommissioningWindowTimeoutSec desiredEndpointFilter:desiredEndpointFilter]; + ChipLogProgress(AppServer, "MCCastingPlayer.verifyOrEstablishConnectionWithCallbacks() called, MCConnectionCallbacks parameter only"); + return [self verifyOrEstablishConnectionWithCallbacks:connectionCallbacks + timeout:kMinCommissioningWindowTimeoutSec + identificationDeclarationOptions:nil]; } -- (void)verifyOrEstablishConnectionWithCompletionBlock:(void (^_Nonnull)(NSError * _Nullable))completion timeout:(long long)timeout desiredEndpointFilter:(MCEndpointFilter * _Nullable)desiredEndpointFilter +- (NSError *)verifyOrEstablishConnectionWithCallbacks:(MCConnectionCallbacks * _Nonnull)connectionCallbacks + identificationDeclarationOptions:(MCIdentificationDeclarationOptions * _Nullable)identificationDeclarationOptions { - ChipLogProgress(AppServer, "MCCastingPlayer.verifyOrEstablishConnectionWithCompletionBlock() called"); - VerifyOrReturn([[MCCastingApp getSharedInstance] isRunning], ChipLogError(AppServer, "MCCastingApp NOT running")); + ChipLogProgress(AppServer, "MCCastingPlayer.verifyOrEstablishConnectionWithCallbacks() called, MCConnectionCallbacks and MCIdentificationDeclarationOptions parameters"); + return [self verifyOrEstablishConnectionWithCallbacks:connectionCallbacks + timeout:kMinCommissioningWindowTimeoutSec + identificationDeclarationOptions:identificationDeclarationOptions]; +} + +- (NSError *)verifyOrEstablishConnectionWithCallbacks:(MCConnectionCallbacks * _Nonnull)connectionCallbacks + timeout:(long)timeout + identificationDeclarationOptions:(MCIdentificationDeclarationOptions * _Nullable)identificationDeclarationOptions +{ + ChipLogProgress(AppServer, "MCCastingPlayer.verifyOrEstablishConnectionWithCallbacks() called, MCConnectionCallbacks, timeout and MCIdentificationDeclarationOptions parameters"); + VerifyOrReturnValue([[MCCastingApp getSharedInstance] isRunning], + [MCErrorUtils NSErrorFromChipError:CHIP_ERROR_INCORRECT_STATE], + ChipLogError(AppServer, "MCCastingPlayer.verifyOrEstablishConnectionWithCallbacks() MCCastingApp NOT running")); dispatch_queue_t workQueue = [[MCCastingApp getSharedInstance] getWorkQueue]; dispatch_sync(workQueue, ^{ - matter::casting::core::IdentificationDeclarationOptions idOptions; - - // TODO: In the following PRs. Replace EndpointFilter objC class with IdentificationDeclarationOptions objC class. - __block matter::casting::core::EndpointFilter cppDesiredEndpointFilter; - if (desiredEndpointFilter != nil) { - chip::Protocols::UserDirectedCommissioning::TargetAppInfo targetAppInfo; - targetAppInfo.vendorId = desiredEndpointFilter.vendorId; - targetAppInfo.productId = desiredEndpointFilter.productId; - - CHIP_ERROR result = idOptions.addTargetAppInfo(targetAppInfo); - if (result != CHIP_NO_ERROR) { - ChipLogError(AppServer, "MCCastingPlayer.verifyOrEstablishConnectionWithCompletionBlock() failed to add targetAppInfo: %" CHIP_ERROR_FORMAT, result.Format()); - } + matter::casting::core::IdentificationDeclarationOptions cppIdOptions; + if (identificationDeclarationOptions != nil) { + cppIdOptions = [identificationDeclarationOptions getCppIdentificationDeclarationOptions]; + } else { + ChipLogProgress(AppServer, "MCCastingPlayer.verifyOrEstablishConnectionWithCallbacks()client did not set the optional MCIdentificationDeclarationOptions using default options"); } void (^connectCallback)(CHIP_ERROR, matter::casting::core::CastingPlayer *) = ^(CHIP_ERROR err, matter::casting::core::CastingPlayer * castingPlayer) { - ChipLogProgress(AppServer, "MCCastingPlayer.verifyOrEstablishConnectionWithCompletionBlock() ConnectCallback()"); + ChipLogProgress(AppServer, "MCCastingPlayer.verifyOrEstablishConnectionWithCallbacks() connectCallback() called"); + dispatch_queue_t clientQueue = [[MCCastingApp getSharedInstance] getClientQueue]; + dispatch_async(clientQueue, ^{ + if (connectionCallbacks.connectionCompleteCallback) { + connectionCallbacks.connectionCompleteCallback(err == CHIP_NO_ERROR ? nil : [MCErrorUtils NSErrorFromChipError:err]); + } else { + ChipLogError(AppServer, "MCCastingPlayer.verifyOrEstablishConnectionWithCallbacks() connectCallback(), client failed to set the connectionCompleteCallback() callback"); + } + }); + }; + void (^commissionerDeclarationCallback)(const chip::Transport::PeerAddress & source, const chip::Protocols::UserDirectedCommissioning::CommissionerDeclaration cppCommissionerDeclaration) = ^(const chip::Transport::PeerAddress & source, const chip::Protocols::UserDirectedCommissioning::CommissionerDeclaration cppCommissionerDeclaration) { + ChipLogProgress(AppServer, "MCCastingPlayer.verifyOrEstablishConnectionWithCallbacks() commissionerDeclarationCallback() called with cpp CommissionerDeclaration"); dispatch_queue_t clientQueue = [[MCCastingApp getSharedInstance] getClientQueue]; dispatch_async(clientQueue, ^{ - completion(err == CHIP_NO_ERROR ? nil : [MCErrorUtils NSErrorFromChipError:err]); + if (connectionCallbacks.commissionerDeclarationCallback) { + auto cppCommissionerDeclarationPtr = std::make_shared(cppCommissionerDeclaration); + MCCommissionerDeclaration * objcCommissionerDeclaration = [[MCCommissionerDeclaration alloc] initWithCppCommissionerDeclaration:cppCommissionerDeclarationPtr]; + connectionCallbacks.commissionerDeclarationCallback(objcCommissionerDeclaration); + } else { + ChipLogError(AppServer, "MCCastingPlayer.verifyOrEstablishConnectionWithCallbacks() commissionerDeclarationCallback(), client failed to set the c commissionerDeclarationCallback() callback"); + } }); }; - matter::casting::core::ConnectionCallbacks connectionCallbacks; - connectionCallbacks.mOnConnectionComplete = connectCallback; + matter::casting::core::ConnectionCallbacks cppConnectionCallbacks; + cppConnectionCallbacks.mOnConnectionComplete = connectCallback; + if (connectionCallbacks.commissionerDeclarationCallback) { + cppConnectionCallbacks.mCommissionerDeclarationCallback = commissionerDeclarationCallback; + } else { + ChipLogProgress(AppServer, "MCCastingPlayer.verifyOrEstablishConnectionWithCallbacks(), client did not set the optional commissionerDeclarationCallback()"); + } + + ChipLogProgress(AppServer, "MCCastingPlayer.verifyOrEstablishConnectionWithCallbacks() calling cpp CastingPlayer.VerifyOrEstablishConnection()"); + _cppCastingPlayer->VerifyOrEstablishConnection(cppConnectionCallbacks, timeout, cppIdOptions); + }); + return nil; +} + +- (NSError *)continueConnecting +{ + ChipLogProgress(AppServer, "MCCastingPlayer.continueConnecting() called"); + VerifyOrReturnValue([[MCCastingApp getSharedInstance] isRunning], [MCErrorUtils NSErrorFromChipError:CHIP_ERROR_INCORRECT_STATE], ChipLogError(AppServer, "MCCastingPlayer.continueConnecting() MCCastingApp NOT running")); + + __block CHIP_ERROR err = CHIP_NO_ERROR; + dispatch_queue_t workQueue = [[MCCastingApp getSharedInstance] getWorkQueue]; + dispatch_sync(workQueue, ^{ + err = _cppCastingPlayer->ContinueConnecting(); + }); + if (err != CHIP_NO_ERROR) { + ChipLogError(AppServer, "MCCastingPlayer.continueConnecting() call to cppCastingPlayer->ContinueConnecting() failed due to %" CHIP_ERROR_FORMAT, err.Format()); + return [MCErrorUtils NSErrorFromChipError:err]; + } + return nil; +} + +- (NSError *)stopConnecting +{ + ChipLogProgress(AppServer, "MCCastingPlayer.stopConnecting() called"); + VerifyOrReturnValue([[MCCastingApp getSharedInstance] isRunning], [MCErrorUtils NSErrorFromChipError:CHIP_ERROR_INCORRECT_STATE], ChipLogError(AppServer, "MCCastingPlayer.stopConnecting() MCCastingApp NOT running")); - // TODO: In the following PRs. Add optional CommissionerDeclarationHandler callback parameter for the Commissioner-Generated passcode commissioning flow. - _cppCastingPlayer->VerifyOrEstablishConnection(connectionCallbacks, timeout, idOptions); + __block CHIP_ERROR err = CHIP_NO_ERROR; + dispatch_queue_t workQueue = [[MCCastingApp getSharedInstance] getWorkQueue]; + dispatch_sync(workQueue, ^{ + err = _cppCastingPlayer->StopConnecting(); }); + if (err != CHIP_NO_ERROR) { + ChipLogError(AppServer, "MCCastingPlayer.continueConnecting() call to cppCastingPlayer->StopConnecting() failed due to %" CHIP_ERROR_FORMAT, err.Format()); + return [MCErrorUtils NSErrorFromChipError:err]; + } + return nil; } - (void)disconnect { - ChipLogProgress(AppServer, "MCCastingPlayer.disconnect called"); - VerifyOrReturn([[MCCastingApp getSharedInstance] isRunning], ChipLogError(AppServer, "MCCastingApp NOT running")); + ChipLogProgress(AppServer, "MCCastingPlayer.disconnect() called"); + VerifyOrReturn([[MCCastingApp getSharedInstance] isRunning], ChipLogError(AppServer, "MCCastingPlayer.disconnect() MCCastingApp NOT running")); dispatch_queue_t workQueue = [[MCCastingApp getSharedInstance] getWorkQueue]; dispatch_sync(workQueue, ^{ @@ -119,7 +188,7 @@ + (MCCastingPlayer * _Nullable)getTargetCastingPlayer - (NSString * _Nonnull)description { - return [NSString stringWithFormat:@"%@ with Product ID: %hu and Vendor ID: %hu. Resolved IPAddr?: %@. Supports Commissioner Generated Passcode?: %@.", + return [NSString stringWithFormat:@"%@ with Product ID: %hu and Vendor ID: %hu. Resolved IPAddr?: %@. Supports Commissioner-Generated Passcode?: %@.", self.deviceName, self.productId, self.vendorId, self.ipAddresses != nil && self.ipAddresses.count > 0 ? @"YES" : @"NO", self.supportsCommissionerGeneratedPasscode ? @"YES" : @"NO"]; } @@ -217,4 +286,12 @@ - (NSUInteger)hash return result; } +- (void)logAllEndpoints +{ + NSArray * endpointsArray = [self endpoints]; + for (MCEndpoint * endpoint in endpointsArray) { + [endpoint logDetail]; + } +} + @end diff --git a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCommissionableDataProvider.mm b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCommissionableDataProvider.mm index 83159b2801f09a..5498ac98e9c38c 100644 --- a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCommissionableDataProvider.mm +++ b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCommissionableDataProvider.mm @@ -50,9 +50,11 @@ CHIP_ERROR GeneratePaseSalt(std::vector & spake2pSaltVector) CHIP_ERROR MCCommissionableDataProvider::Initialize(id dataSource) { + ChipLogProgress(Support, "MCCommissionableDataProvider Initialize()"); VerifyOrReturnLogError(dataSource != nil, CHIP_ERROR_INVALID_ARGUMENT); VerifyOrReturnLogError(mDataSource == nullptr, CHIP_ERROR_INCORRECT_STATE); + ChipLogProgress(Support, "MCCommissionableDataProvider Initialize() calling MCCommissionableData castingAppDidReceiveRequestForCommissionableData()"); mDataSource = dataSource; MCCommissionableData * commissionableData = [mDataSource castingAppDidReceiveRequestForCommissionableData:@"MCCommissionableDataProvider.Initialize()"]; diff --git a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCommissionerDeclaration.h b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCommissionerDeclaration.h new file mode 100644 index 00000000000000..de9cc147fd2cdd --- /dev/null +++ b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCommissionerDeclaration.h @@ -0,0 +1,70 @@ +/** + * + * Copyright (c) 2024 Project CHIP Authors + * + * 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. + */ +#import + +#ifndef MCCommissionerDeclaration_h +#define MCCommissionerDeclaration_h + +/** + * Represents the Commissioner Declaration message sent by a User Directed Commissioning server + * (CastingPlayer/Commissioner) to a UDC client (Casting Client/Commissionee). + */ +@interface MCCommissionerDeclaration : NSObject + +/** The allowed values for the ErrorCode field are the following */ +typedef NS_ENUM(NSInteger, CdError) { + kNoError = 0, + kCommissionableDiscoveryFailed = 1, + kPaseConnectionFailed = 2, + kPaseAuthFailed = 3, + kDacValidationFailed = 4, + kAlreadyOnFabric = 5, + kOperationalDiscoveryFailed = 6, + kCaseConnectionFailed = 7, + kCaseAuthFailed = 8, + kConfigurationFailed = 9, + kBindingConfigurationFailed = 10, + kCommissionerPasscodeNotSupported = 11, + kInvalidIdentificationDeclarationParams = 12, + kAppInstallConsentPending = 13, + kAppInstalling = 14, + kAppInstallFailed = 15, + kAppInstalledRetryNeeded = 16, + kCommissionerPasscodeDisabled = 17, + kUnexpectedCommissionerPasscodeReady = 18 +}; + +- (instancetype)initWithOptions:(NSInteger)errorCode + needsPasscode:(BOOL)needsPasscode + noAppsFound:(BOOL)noAppsFound + passcodeDialogDisplayed:(BOOL)passcodeDialogDisplayed + commissionerPasscode:(BOOL)commissionerPasscode + qRCodeDisplayed:(BOOL)qRCodeDisplayed; + +- (CdError)getErrorCode; +- (BOOL)getNeedsPasscode; +- (BOOL)getNoAppsFound; +- (BOOL)getPasscodeDialogDisplayed; +- (BOOL)getCommissionerPasscode; +- (BOOL)getQRCodeDisplayed; + +- (NSString *)description; +- (void)logDetail; + +@end + +#endif /* MCCommissionerDeclaration_h */ diff --git a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCommissionerDeclaration.mm b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCommissionerDeclaration.mm new file mode 100644 index 00000000000000..89aaee905dfcb1 --- /dev/null +++ b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCommissionerDeclaration.mm @@ -0,0 +1,173 @@ +/** + * + * Copyright (c) 2024 Project CHIP Authors + * + * 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. + */ + +#import "MCCommissionerDeclaration_Internal.h" +#import "core/Types.h" + +@interface MCCommissionerDeclaration () + +/** Feature: All - Indicates errors incurred during commissioning. */ +@property (nonatomic) CdError errorCode; + /** + * Feature: Coordinate PIN Dialogs - When NoPasscode field set to true, and the Commissioner + * determines that a Passcode code will be needed for commissioning. + */ +@property (nonatomic) BOOL needsPasscode; + /** + * Feature: Target Content Application - No apps with AccountLogin cluster implementation were + * found for the last IdentificationDeclaration request. Only apps which provide access to the + * vendor id of the Commissionee will be considered. + */ +@property (nonatomic) BOOL noAppsFound; + /** + * Feature: Coordinate PIN Dialogs - A Passcode input dialog is now displayed for the user on the + * Commissioner. + */ +@property (nonatomic) BOOL passcodeDialogDisplayed; + /** + * Feature: Commissioner-Generated Passcode - A Passcode is now displayed for the user by the + * CastingPlayer/Commissioner. + */ +@property (nonatomic) BOOL commissionerPasscode; + /** + * Feature: Commissioner-Generated Passcode - The user experience conveying a Passcode to the user + * also displays a QR code. + */ +@property (nonatomic) BOOL qRCodeDisplayed; + +@property (nonatomic, readwrite) matter::casting::memory::Strong cppCommissionerDeclaration; + +@end + +@implementation MCCommissionerDeclaration + +- (instancetype _Nonnull)initWithCppCommissionerDeclaration:(std::shared_ptr)cppCommissionerDeclaration +{ + if (self = [super init]) { + _cppCommissionerDeclaration = cppCommissionerDeclaration; + _errorCode = static_cast(cppCommissionerDeclaration->GetErrorCode()); + _needsPasscode = cppCommissionerDeclaration->GetNeedsPasscode(); + _noAppsFound = cppCommissionerDeclaration->GetNoAppsFound(); + _passcodeDialogDisplayed = cppCommissionerDeclaration->GetPasscodeDialogDisplayed(); + _commissionerPasscode = cppCommissionerDeclaration->GetCommissionerPasscode(); + _qRCodeDisplayed = cppCommissionerDeclaration->GetQRCodeDisplayed(); + } + return self; +} + +- (instancetype)initWithOptions:(NSInteger)errorCode + needsPasscode:(BOOL)needsPasscode + noAppsFound:(BOOL)noAppsFound + passcodeDialogDisplayed:(BOOL)passcodeDialogDisplayed + commissionerPasscode:(BOOL)commissionerPasscode + qRCodeDisplayed:(BOOL)qRCodeDisplayed { + self = [super init]; + if (self) { + _errorCode = (CdError)errorCode; + _needsPasscode = needsPasscode; + _noAppsFound = noAppsFound; + _passcodeDialogDisplayed = passcodeDialogDisplayed; + _commissionerPasscode = commissionerPasscode; + _qRCodeDisplayed = qRCodeDisplayed; + } + return self; +} + +- (CdError)getErrorCode { + return _errorCode; +} + +- (BOOL)getNeedsPasscode { + return _needsPasscode; +} + +- (BOOL)getNoAppsFound { + return _noAppsFound; +} + +- (BOOL)getPasscodeDialogDisplayed { + return _passcodeDialogDisplayed; +} + +- (BOOL)getCommissionerPasscode { + return _commissionerPasscode; +} + +- (BOOL)getQRCodeDisplayed { + return _qRCodeDisplayed; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"MCCommissionerDeclaration::errorCode: %@\nMCCommissionerDeclaration::needsPasscode: %d\nMCCommissionerDeclaration::noAppsFound: %d\nMCCommissionerDeclaration::passcodeDialogDisplayed: %d\nMCCommissionerDeclaration::commissionerPasscode: %d\nMCCommissionerDeclaration::qRCodeDisplayed: %d", + [self stringForErrorCode:self.errorCode], + self.needsPasscode, + self.noAppsFound, + self.passcodeDialogDisplayed, + self.commissionerPasscode, + self.qRCodeDisplayed]; +} + +- (NSString *)stringForErrorCode:(CdError)errorCode { + switch (errorCode) { + case kNoError: + return @"kNoError"; + case kCommissionableDiscoveryFailed: + return @"kCommissionableDiscoveryFailed"; + case kPaseConnectionFailed: + return @"kPaseConnectionFailed"; + case kPaseAuthFailed: + return @"kPaseAuthFailed"; + case kDacValidationFailed: + return @"kDacValidationFailed"; + case kAlreadyOnFabric: + return @"kAlreadyOnFabric"; + case kOperationalDiscoveryFailed: + return @"kOperationalDiscoveryFailed"; + case kCaseConnectionFailed: + return @"kCaseConnectionFailed"; + case kCaseAuthFailed: + return @"kCaseAuthFailed"; + case kConfigurationFailed: + return @"kConfigurationFailed"; + case kBindingConfigurationFailed: + return @"kBindingConfigurationFailed"; + case kCommissionerPasscodeNotSupported: + return @"kCommissionerPasscodeNotSupported"; + case kInvalidIdentificationDeclarationParams: + return @"kInvalidIdentificationDeclarationParams"; + case kAppInstallConsentPending: + return @"kAppInstallConsentPending"; + case kAppInstalling: + return @"kAppInstalling"; + case kAppInstallFailed: + return @"kAppInstallFailed"; + case kAppInstalledRetryNeeded: + return @"kAppInstalledRetryNeeded"; + case kCommissionerPasscodeDisabled: + return @"kCommissionerPasscodeDisabled"; + case kUnexpectedCommissionerPasscodeReady: + return @"kUnexpectedCommissionerPasscodeReady"; + default: + return @"Unknown Error"; + } +} + +- (void)logDetail { + ChipLogDetail(AppServer, "MCCommissionerDeclaration::logDetail()\n%@", [self description]); +} + +@end diff --git a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCommissionerDeclaration_Internal.h b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCommissionerDeclaration_Internal.h new file mode 100644 index 00000000000000..96acced6be94bb --- /dev/null +++ b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCCommissionerDeclaration_Internal.h @@ -0,0 +1,32 @@ +/** + * + * Copyright (c) 2024 Project CHIP Authors + * + * 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. + */ + +#import "MCCommissionerDeclaration.h" +#import "core/Types.h" + +#import + +#ifndef MCCommissionerDeclaration_Internal_h +#define MCCommissionerDeclaration_Internal_h + +@interface MCCommissionerDeclaration () + +- (instancetype _Nonnull)initWithCppCommissionerDeclaration:(std::shared_ptr)cppCommissionerDeclaration; + +@end + +#endif /* MCCommissionerDeclaration_Internal_h */ diff --git a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCConnectionCallbacks.h b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCConnectionCallbacks.h new file mode 100644 index 00000000000000..08e2d3b807470b --- /dev/null +++ b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCConnectionCallbacks.h @@ -0,0 +1,55 @@ +/** + * + * Copyright (c) 2024 Project CHIP Authors + * + * 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. + */ + +#import "MCCommissionerDeclaration.h" + +#import + +#ifndef MCConnectionCallbacks_h +#define MCConnectionCallbacks_h + +/** @brief A container class for User Directed Commissioning (UDC) callbacks. */ +@interface MCConnectionCallbacks : NSObject + + +/** +* @param connectionCompleteCallback is required. +* @param commissionerDeclarationCallback is optional. +* +* @return A new instance of MCConnectionCallbacks. + */ +- (instancetype _Nonnull)initWithCallbacks:(void (^_Nonnull)(NSError * _Nonnull))connectionCompleteCallback + commissionerDeclarationCallback:(void (^_Nullable)(MCCommissionerDeclaration * _Nonnull))commissionerDeclarationCallback; + +/** + * (Required) The callback called when the connection process has ended, regardless of whether it was successful or not. + */ +@property void (^_Nullable connectionCompleteCallback)(NSError * _Nonnull); + + + /** + * (Optional) The callback called when the Client/Commissionee receives a CommissionerDeclaration + * message from the CastingPlayer/Commissioner. This callback is needed to support UDC features + * where a reply from the Commissioner is expected. It provides information indicating the + * Commissioner’s pre-commissioning state. + */ +@property void (^_Nullable commissionerDeclarationCallback)(MCCommissionerDeclaration * _Nonnull); + + +@end + +#endif /* MCConnectionCallbacks_h */ diff --git a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCConnectionCallbacks.mm b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCConnectionCallbacks.mm new file mode 100644 index 00000000000000..a6b17b24c3f775 --- /dev/null +++ b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCConnectionCallbacks.mm @@ -0,0 +1,32 @@ +/** + * + * Copyright (c) 2024 Project CHIP Authors + * + * 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. + */ + +#import "MCConnectionCallbacks.h" + +@implementation MCConnectionCallbacks + +- (instancetype _Nonnull)initWithCallbacks:(void (^_Nonnull)(NSError * _Nonnull))connectionCompleteCallback + commissionerDeclarationCallback:(void (^_Nullable)(MCCommissionerDeclaration * _Nonnull))commissionerDeclarationCallback { + self = [super init]; + if (self) { + self.connectionCompleteCallback = connectionCompleteCallback; + self.commissionerDeclarationCallback = commissionerDeclarationCallback; + } + return self; +} + +@end diff --git a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCDataSource.h b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCDataSource.h index 6339de82bc2695..61c03d67acfa9f 100644 --- a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCDataSource.h +++ b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCDataSource.h @@ -29,6 +29,16 @@ */ - (dispatch_queue_t _Nonnull)clientQueue; +/** + * @brief Updates the MCCommissionableData object stored inside MCAppParametersDataSource, which was used to + * initialize the MCCastingApp. This function needs to be implemented by the client in use cases where the + * MCCommissionableData needs to be updated post-initialization. For example, when the Commissioner-Generated + * Passcode feature is used. + * + * @param newCommissionableData The new MCCommissionableData object to be used for the next commissioning session. + */ +- (void)update:(MCCommissionableData * _Nonnull)newCommissionableData; + /** * @brief Provide UniqueId used to generate the RotatingDeviceId advertised during commissioning by the MCCastingApp * Must be at least 16 bytes (i.e. ConfigurationManager::kMinRotatingDeviceIDUniqueIDLength) diff --git a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCEndpoint.h b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCEndpoint.h index db340f097764f4..8dc540039e3ef5 100644 --- a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCEndpoint.h +++ b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCEndpoint.h @@ -41,6 +41,9 @@ - (BOOL)hasCluster:(MCEndpointClusterType)type; - (MCCluster * _Nullable)clusterForType:(MCEndpointClusterType)type; +- (NSString * _Nonnull)description; +- (void)logDetail; + @end #endif /* MCEndpoint_h */ diff --git a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCEndpoint.mm b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCEndpoint.mm index 76d1eaf70724f9..718e52b593a829 100644 --- a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCEndpoint.mm +++ b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCEndpoint.mm @@ -154,4 +154,14 @@ - (NSUInteger)hash return result; } + +- (NSString *)description +{ + return [NSString stringWithFormat:@"", [self identifier], [self vendorId], [self productId]]; +} + +- (void)logDetail +{ + ChipLogDetail(AppServer, "MCEndpoint Details: %@", [self description]); +} @end diff --git a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCIdentificationDeclarationOptions.h b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCIdentificationDeclarationOptions.h new file mode 100644 index 00000000000000..d870588fa19531 --- /dev/null +++ b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCIdentificationDeclarationOptions.h @@ -0,0 +1,53 @@ +/** + * + * Copyright (c) 2024 Project CHIP Authors + * + * 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. + */ +#import +#import "MCTargetAppInfo.h" + +#ifndef MCIdentificationDeclarationOptions_h +#define MCIdentificationDeclarationOptions_h + +/** + * This class contains the optional parameters used in the IdentificationDeclaration Message, sent + * by the Commissionee to the Commissioner. The options specify information relating to the + * requested UDC commissioning session. + */ +@interface MCIdentificationDeclarationOptions : NSObject + +- (instancetype)init; + +- (instancetype)initWithCommissionerPasscodeOnly:(BOOL)commissionerPasscode; + +// Getter methods +- (BOOL)getNoPasscode; +- (BOOL)getCdUponPasscodeDialog; +- (BOOL)getCommissionerPasscode; +- (BOOL)getCommissionerPasscodeReady; +- (BOOL)getCancelPasscode; + + /** + * @brief Adds a TargetAppInfo to the IdentificationDeclarationOptions.java TargetAppInfos list, + * up to a maximum of CHIP_DEVICE_CONFIG_UDC_MAX_TARGET_APPS. + */ +- (BOOL)addTargetAppInfo:(MCTargetAppInfo *)targetAppInfo NS_SWIFT_NAME(addTargetAppInfo(_:)); +- (NSArray *)getTargetAppInfoList; + +- (NSString *)description; +- (void)logDetail; + +@end + +#endif /* MCIdentificationDeclarationOptions_h */ diff --git a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCIdentificationDeclarationOptions.mm b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCIdentificationDeclarationOptions.mm new file mode 100644 index 00000000000000..83ca1343c23aff --- /dev/null +++ b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCIdentificationDeclarationOptions.mm @@ -0,0 +1,167 @@ +/** + * + * Copyright (c) 2024 Project CHIP Authors + * + * 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. + */ +#import "MCIdentificationDeclarationOptions.h" + +#import "core/Types.h" // from tv-casting-common +#import "core/IdentificationDeclarationOptions.h" // from tv-casting-common +#import "include/CHIPProjectAppConfig.h" // from tv-casting-common + +@interface MCIdentificationDeclarationOptions () + +// Private properties: + + /** + * Feature: Target Content Application - Flag to instruct the Commissioner not to display a + * Passcode input dialog, and instead send a CommissionerDeclaration message if a commissioning + * Passcode is needed. + */ +@property (nonatomic) BOOL noPasscode; + /** + * Feature: Coordinate Passcode Dialogs - Flag to instruct the Commissioner to send a + * CommissionerDeclaration message when the Passcode input dialog on the Commissioner has been + * shown to the user. + */ +@property (nonatomic) BOOL cdUponPasscodeDialog; + /** + * Feature: Commissioner-Generated Passcode - Flag to instruct the Commissioner to use the + * Commissioner-generated Passcode for commissioning. + */ +@property (nonatomic) BOOL commissionerPasscode; + /** + * Feature: Commissioner-Generated Passcode - Flag to indicate whether or not the Commissionee has + * obtained the Commissioner Passcode from the user and is therefore ready for commissioning. + */ +@property (nonatomic) BOOL commissionerPasscodeReady; + /** + * Feature: Coordinate Passcode Dialogs Flag - to indicate when the Commissionee user has decided + * to exit the commissioning process. + */ +@property (nonatomic) BOOL cancelPasscode; + /** + * Feature: Target Content Application - The set of content app Vendor IDs (and optionally, + * Product IDs) that can be used for authentication. Also, if TargetAppInfo is passed in, + * VerifyOrEstablishConnection() will force User Directed Commissioning, in case the desired + * TargetApp is not found in the on-device CastingStore. + */ +@property (nonatomic, strong) NSMutableArray *targetAppInfos; + +@end + +@implementation MCIdentificationDeclarationOptions + +- (instancetype)init { + self = [super init]; + if (self) { + _noPasscode = NO; + _cdUponPasscodeDialog = NO; + _commissionerPasscode = NO; + _commissionerPasscodeReady = NO; + _cancelPasscode = NO; + _targetAppInfos = [[NSMutableArray alloc] init]; + } + return self; +} + +- (instancetype)initWithCommissionerPasscodeOnly:(BOOL)commissionerPasscode { + self = [super init]; + if (self) { + _noPasscode = NO; + _cdUponPasscodeDialog = NO; + _commissionerPasscode = commissionerPasscode; + _commissionerPasscodeReady = NO; + _cancelPasscode = NO; + _targetAppInfos = [[NSMutableArray alloc] init]; + } + return self; +} + +// Getter methods +- (BOOL)getNoPasscode { + return _noPasscode; +} + +- (BOOL)getCdUponPasscodeDialog { + return _cdUponPasscodeDialog; +} + +- (BOOL)getCommissionerPasscode { + return _commissionerPasscode; +} + +- (BOOL)getCommissionerPasscodeReady { + return _commissionerPasscodeReady; +} + +- (BOOL)getCancelPasscode { + return _cancelPasscode; +} + +- (BOOL)addTargetAppInfo:(MCTargetAppInfo *)targetAppInfo { + if (self.targetAppInfos.count >= CHIP_DEVICE_CONFIG_UDC_MAX_TARGET_APPS) { + ChipLogError(AppServer, "MCIdentificationDeclarationOptions addTargetAppInfo() failed to add TargetAppInfo, max targetAppInfos list size is: %d", CHIP_DEVICE_CONFIG_UDC_MAX_TARGET_APPS); + return NO; + } + [self.targetAppInfos addObject:targetAppInfo]; + return YES; +} + +- (NSArray *)getTargetAppInfoList { + return [self.targetAppInfos copy]; +} + +- (NSString *)description { + NSMutableString *sb = [NSMutableString stringWithFormat:@"MCIdentificationDeclarationOptions::noPasscode: %d\n", self.noPasscode]; + [sb appendFormat:@"MCIdentificationDeclarationOptions::cdUponPasscodeDialog: %d\n", self.cdUponPasscodeDialog]; + [sb appendFormat:@"MCIdentificationDeclarationOptions::commissionerPasscode: %d\n", self.commissionerPasscode]; + [sb appendFormat:@"MCIdentificationDeclarationOptions::commissionerPasscodeReady: %d\n", self.commissionerPasscodeReady]; + [sb appendFormat:@"MCIdentificationDeclarationOptions::cancelPasscode: %d\n", self.cancelPasscode]; + [sb appendString:@"MCIdentificationDeclarationOptions::targetAppInfos list: \n"]; + + for (MCTargetAppInfo *targetAppInfo in self.targetAppInfos) { + [sb appendFormat:@"\t\tTargetAppInfo - Vendor ID: %d, Product ID: %d\n", targetAppInfo.vendorId, targetAppInfo.productId]; + } + + return [sb copy]; +} + +- (void)logDetail { + ChipLogDetail(AppServer, "MCIdentificationDeclarationOptions::logDetail()\n%@", [self description]); +} + +- (matter::casting::core::IdentificationDeclarationOptions)getCppIdentificationDeclarationOptions { + matter::casting::core::IdentificationDeclarationOptions cppIdOptions; + cppIdOptions.mNoPasscode = [self getNoPasscode]; + cppIdOptions.mCdUponPasscodeDialog = [self getCdUponPasscodeDialog]; + cppIdOptions.mCommissionerPasscode = [self getCommissionerPasscode]; + cppIdOptions.mCommissionerPasscodeReady = [self getCommissionerPasscodeReady]; + cppIdOptions.mCancelPasscode = [self getCancelPasscode]; + + NSArray *targetAppInfos = [self getTargetAppInfoList]; + for (MCTargetAppInfo *appInfo in targetAppInfos) { + chip::Protocols::UserDirectedCommissioning::TargetAppInfo targetAppInfo; + targetAppInfo.vendorId = appInfo.vendorId; + targetAppInfo.productId = appInfo.productId; + CHIP_ERROR err = cppIdOptions.addTargetAppInfo(targetAppInfo); + if (err != CHIP_NO_ERROR) { + ChipLogError(AppServer, "MCIdentificationDeclarationOptions.getCppIdentificationDeclarationOptions() Failed to add TargetAppInfo with Vendor ID: %d, Product ID: %d", appInfo.vendorId, appInfo.productId); + } + } + + return cppIdOptions; +} + +@end diff --git a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCIdentificationDeclarationOptions_Internal.h b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCIdentificationDeclarationOptions_Internal.h new file mode 100644 index 00000000000000..6f466cbbb59158 --- /dev/null +++ b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCIdentificationDeclarationOptions_Internal.h @@ -0,0 +1,32 @@ +/** + * + * Copyright (c) 2024 Project CHIP Authors + * + * 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. + */ + +#import "MCIdentificationDeclarationOptions.h" +#import "core/IdentificationDeclarationOptions.h" // from tv-casting-common + +#import + +#ifndef MCIdentificationDeclarationOptions_Internal_h +#define MCIdentificationDeclarationOptions_Internal_h + +@interface MCIdentificationDeclarationOptions () + +- (matter::casting::core::IdentificationDeclarationOptions)getCppIdentificationDeclarationOptions; + +@end + +#endif /* MCIdentificationDeclarationOptions_Internal_h */ diff --git a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCTargetAppInfo.h b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCTargetAppInfo.h new file mode 100644 index 00000000000000..7714b580752f1f --- /dev/null +++ b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCTargetAppInfo.h @@ -0,0 +1,39 @@ +/** + * + * Copyright (c) 2024 Project CHIP Authors + * + * 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. + */ + +#import + +#ifndef MCTargetAppInfo_h +#define MCTargetAppInfo_h + +/** + * @brief Feature: Target Content Application - An entry in the TargetAppList which contains + * a Target vendorId and an optional Target productId. + */ +@interface MCTargetAppInfo : NSObject + +/** Target Target Content Application Vendor ID, 0 means unspecified */ +@property (nonatomic) uint16_t vendorId; + +/** Target Target Content Application Product ID, 0 means unspecified */ +@property (nonatomic) uint16_t productId; + +- (instancetype)init; + +@end + +#endif /* MCTargetAppInfo_h */ \ No newline at end of file diff --git a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCTargetAppInfo.mm b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCTargetAppInfo.mm new file mode 100644 index 00000000000000..235603e7dcdfee --- /dev/null +++ b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MCTargetAppInfo.mm @@ -0,0 +1,31 @@ +/** + * + * Copyright (c) 2024 Project CHIP Authors + * + * 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. + */ + +#import "MCTargetAppInfo.h" + +@implementation MCTargetAppInfo + +- (instancetype)init { + self = [super init]; + if (self) { + _vendorId = 0; + _productId = 0; + } + return self; +} + +@end diff --git a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MatterTvCastingBridge.h b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MatterTvCastingBridge.h index 5e83cea90b0728..ed168df027fe48 100644 --- a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MatterTvCastingBridge.h +++ b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/MatterTvCastingBridge.h @@ -33,11 +33,15 @@ FOUNDATION_EXPORT const unsigned char MatterTvCastingBridgeVersionString[]; #import "MCCluster.h" #import "MCCommand.h" #import "MCCommissionableData.h" +#import "MCCommissionerDeclaration.h" +#import "MCConnectionCallbacks.h" #import "MCCryptoUtils.h" #import "MCDataSource.h" #import "MCDeviceAttestationCredentials.h" #import "MCEndpoint.h" #import "MCEndpointFilter.h" +#import "MCIdentificationDeclarationOptions.h" +#import "MCTargetAppInfo.h" #import "zap-generated/MCAttributeObjects.h" #import "zap-generated/MCClusterObjects.h" #import "zap-generated/MCCommandObjects.h" diff --git a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/compat-shim/CastingServerBridge.mm b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/compat-shim/CastingServerBridge.mm index ca7ccb3162fa8c..dd522ef38a4a59 100644 --- a/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/compat-shim/CastingServerBridge.mm +++ b/examples/tv-casting-app/darwin/MatterTvCastingBridge/MatterTvCastingBridge/compat-shim/CastingServerBridge.mm @@ -24,8 +24,10 @@ #import "MCCastingApp.h" #import "MCCastingPlayerDiscovery.h" #import "MCCastingPlayer_Internal.h" +#import "MCConnectionCallbacks.h" #import "MCEndpoint.h" #import "MCErrorUtils.h" +#import "MCIdentificationDeclarationOptions.h" #import "MatterCallbacks.h" #import "OnboardingPayload.h" @@ -226,11 +228,20 @@ - (void)sendUserDirectedCommissioningRequest:(DiscoveredNodeData * _Nonnull)comm { ChipLogProgress(AppServer, "CastingServerBridge().sendUserDirectedCommissioningRequest() called with desiredContentAppVendorId: %d", desiredContentAppVendorId); - MCEndpointFilter * filter = [MCEndpointFilter new]; - filter.vendorId = desiredContentAppVendorId; + MCIdentificationDeclarationOptions * identificationDeclarationOptions = [[MCIdentificationDeclarationOptions alloc] init]; + MCTargetAppInfo * targetAppInfo = [[MCTargetAppInfo alloc] init]; + targetAppInfo.vendorId = desiredContentAppVendorId; + BOOL success = [identificationDeclarationOptions addTargetAppInfo:targetAppInfo]; + if (success) { + ChipLogProgress(AppServer, "CastingServerBridge().sendUserDirectedCommissioningRequest() Target app info added successfully"); + } else { + ChipLogProgress(AppServer, "CastingServerBridge().sendUserDirectedCommissioningRequest() Failed to add target app info"); + } + [identificationDeclarationOptions logDetail]; - [commissioner.getCastingPlayer verifyOrEstablishConnectionWithCompletionBlock:^(NSError * _Nullable err) { + void (^connectionCompletionBlock)(NSError * _Nullable) = ^(NSError * _Nullable err) { dispatch_async(clientQueue, ^{ + ChipLogError(AppServer, "CastingServerBridge().sendUserDirectedCommissioningRequest() connectionCompleteCallback() completed with error: %@", err.description); if (err == nil) { if (self->_commissioningCallbackHandlers != nil && self->_commissioningCallbackHandlers.commissioningCompleteCallback != nil) { self->_commissioningCallbackHandlers.commissioningCompleteCallback(MATTER_NO_ERROR); @@ -249,7 +260,11 @@ - (void)sendUserDirectedCommissioningRequest:(DiscoveredNodeData * _Nonnull)comm } } }); - } desiredEndpointFilter:filter]; + }; + + MCConnectionCallbacks * connectionCallbacks = [[MCConnectionCallbacks alloc] initWithCallbacks:connectionCompletionBlock commissionerDeclarationCallback:nil]; + + [commissioner.getCastingPlayer verifyOrEstablishConnectionWithCallbacks:connectionCallbacks identificationDeclarationOptions:identificationDeclarationOptions]; dispatch_async(clientQueue, ^{ udcRequestSentHandler(MATTER_NO_ERROR); diff --git a/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCApplicationBasicReadVendorIDExampleViewModel.swift b/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCApplicationBasicReadVendorIDExampleViewModel.swift index 32e20e4b324ca3..fea1772f4ee920 100644 --- a/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCApplicationBasicReadVendorIDExampleViewModel.swift +++ b/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCApplicationBasicReadVendorIDExampleViewModel.swift @@ -30,66 +30,80 @@ class MCApplicationBasicReadVendorIDExampleViewModel: ObservableObject { func read(castingPlayer: MCCastingPlayer) { + self.Log.info("MCApplicationBasicReadVendorIDExampleViewModel.read()") + castingPlayer.logAllEndpoints() + var selectedEndpoint: MCEndpoint? + // select the MCEndpoint on the MCCastingPlayer to invoke the command on - if let endpoint: MCEndpoint = castingPlayer.endpoints().filter({ $0.vendorId().intValue == sampleEndpointVid}).first + if let endpoint = castingPlayer.endpoints().filter({ $0.vendorId().intValue == sampleEndpointVid }).first { + selectedEndpoint = endpoint + // For the example Commissioner-Generated passcode commissioning flow, run demo interactions with the Endpoint with + // ID 1. For this flow, we commissioned with the Target Content Application with Vendor ID 1111. Since this target + // content application does not report its Endpoint's Vendor IDs, we find the desired endpoint based on the Endpoint + // ID. See connectedhomeip/examples/tv-app/tv-common/include/AppTv.h. + } else if let endpoint = castingPlayer.endpoints().filter({ $0.identifier().intValue == 1 }).first { + self.Log.info("MCApplicationBasicReadVendorIDExampleViewModel.read() No endpoint matching the sampleEndpointVid: \(String(describing: self.sampleEndpointVid)), but found endpoint with identifier: 1") + selectedEndpoint = endpoint + } + + guard let endpoint = selectedEndpoint else { + self.Log.error("No endpoint matching the example VID or identifier 1 found") + DispatchQueue.main.async { + self.status = "No endpoint matching the example VID or identifier 1 found" + } + return + } + + self.Log.info("MCApplicationBasicReadVendorIDExampleViewModel.read() selected endpoint: \(endpoint.description)") + + // validate that the selected endpoint supports the ApplicationBasic cluster + if(!endpoint.hasCluster(MCEndpointClusterTypeApplicationBasic)) { - // validate that the selected endpoint supports the ApplicationBasic cluster - if(!endpoint.hasCluster(MCEndpointClusterTypeApplicationBasic)) + self.Log.error("No ApplicationBasic cluster supporting endpoint found") + DispatchQueue.main.async { - self.Log.error("No ApplicationBasic cluster supporting endpoint found") - DispatchQueue.main.async - { - self.status = "No ApplicationBasic cluster supporting endpoint found" - } - return + self.status = "No ApplicationBasic cluster supporting endpoint found" } - - // get ApplicationBasic cluster from the endpoint - let applicationBasiccluster: MCApplicationBasicCluster = endpoint.cluster(for: MCEndpointClusterTypeApplicationBasic) as! MCApplicationBasicCluster + return + } + + // get ApplicationBasic cluster from the endpoint + let applicationBasiccluster: MCApplicationBasicCluster = endpoint.cluster(for: MCEndpointClusterTypeApplicationBasic) as! MCApplicationBasicCluster - // get the vendorIDAttribute from the applicationBasiccluster - let vendorIDAttribute: MCApplicationBasicClusterVendorIDAttribute? = applicationBasiccluster.vendorIDAttribute() - if(vendorIDAttribute == nil) + // get the vendorIDAttribute from the applicationBasiccluster + let vendorIDAttribute: MCApplicationBasicClusterVendorIDAttribute? = applicationBasiccluster.vendorIDAttribute() + if(vendorIDAttribute == nil) + { + self.Log.error("VendorID attribute not supported on cluster") + DispatchQueue.main.async { - self.Log.error("VendorID attribute not supported on cluster") - DispatchQueue.main.async - { - self.status = "VendorID attribute not supported on cluster" - } - return - } - - - // call read on vendorIDAttribute and pass in a completion block - vendorIDAttribute!.read(nil) { context, before, after, err in - DispatchQueue.main.async - { - if(err != nil) - { - self.Log.error("Error when reading VendorID value \(String(describing: err))") - self.status = "Error when reading VendorID value \(String(describing: err))" - return - } - - if(before != nil) - { - self.Log.info("Read VendorID value: \(String(describing: after)) Before: \(String(describing: before))") - self.status = "Read VendorID value: \(String(describing: after)) Before: \(String(describing: before))" - } - else - { - self.Log.info("Read VendorID value: \(String(describing: after))") - self.status = "Read VendorID value: \(String(describing: after))" - } - } + self.status = "VendorID attribute not supported on cluster" } + return } - else - { - self.Log.error("No endpoint matching the example VID found") + + + // call read on vendorIDAttribute and pass in a completion block + vendorIDAttribute!.read(nil) { context, before, after, err in DispatchQueue.main.async { - self.status = "No endpoint matching the example VID found" + if(err != nil) + { + self.Log.error("Error when reading VendorID value \(String(describing: err))") + self.status = "Error when reading VendorID value \(String(describing: err))" + return + } + + if(before != nil) + { + self.Log.info("Read VendorID value: \(String(describing: after)) Before: \(String(describing: before))") + self.status = "Read VendorID value: \(String(describing: after)) Before: \(String(describing: before))" + } + else + { + self.Log.info("Read VendorID value: \(String(describing: after))") + self.status = "Read VendorID value: \(String(describing: after))" + } } } } diff --git a/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCConnectionExampleView.swift b/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCConnectionExampleView.swift index 87448bf46b4790..9ecf0f87511aa4 100644 --- a/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCConnectionExampleView.swift +++ b/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCConnectionExampleView.swift @@ -17,19 +17,32 @@ import SwiftUI +import os.log struct MCConnectionExampleView: View { + let Log = Logger(subsystem: "com.matter.casting", + category: "MCConnectionExampleView") var selectedCastingPlayer: MCCastingPlayer? + var useCommissionerGeneratedPasscode: Bool @StateObject var viewModel = MCConnectionExampleViewModel(); - init(_selectedCastingPlayer: MCCastingPlayer?) { + init(_selectedCastingPlayer: MCCastingPlayer?, _useCommissionerGeneratedPasscode: Bool) { self.selectedCastingPlayer = _selectedCastingPlayer + self.useCommissionerGeneratedPasscode = _useCommissionerGeneratedPasscode } var body: some View { VStack(alignment: .leading) { - Text("Verifying or Establishing Connection to Casting Player: \(self.selectedCastingPlayer!.deviceName())").padding() + if self.useCommissionerGeneratedPasscode { + if selectedCastingPlayer?.supportsCommissionerGeneratedPasscode() == true { + Text("Verifying or Establishing Connection, using Casting Player/Commissioner-Generated passcode, with Casting Player: \(self.selectedCastingPlayer!.deviceName())\n\nEnter the passcode displayed on the Casting Player when prompted.").padding() + } else { + Text("Sorry, the selected Casting Player does not support Casting Player/Commissioner-Generated passcode commissioning. \n\nCasting Player selected: \(self.selectedCastingPlayer!.deviceName()) \n\nRoute back to select a different Casting Player.").padding() + } + } else { + Text("Verifying or Establishing Connection to Casting Player: \(self.selectedCastingPlayer!.deviceName())").padding() + } if let connectionSuccess = viewModel.connectionSuccess { if let connectionStatus = viewModel.connectionStatus @@ -56,13 +69,23 @@ struct MCConnectionExampleView: View { .navigationTitle("Connecting...") .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .top) .onAppear(perform: { - viewModel.connect(selectedCastingPlayer: self.selectedCastingPlayer) + if self.useCommissionerGeneratedPasscode { + if selectedCastingPlayer?.supportsCommissionerGeneratedPasscode() == true { + self.Log.info("MCConnectionExampleView calling MCConnectionExampleViewModel.connect() with useCommissionerGeneratedPasscode: \(String(describing: self.useCommissionerGeneratedPasscode))") + viewModel.connect(selectedCastingPlayer: self.selectedCastingPlayer, useCommissionerGeneratedPasscode: self.useCommissionerGeneratedPasscode) + } else { + self.Log.error("MCConnectionExampleView Sorry, the selected Casting Player does not support Commissioner-Generated passcode commissioning. Casting Player: \(self.selectedCastingPlayer!.deviceName()), Route back to try again.") + } + } else { + self.Log.info("MCConnectionExampleView calling MCConnectionExampleViewModel.connect() with useCommissionerGeneratedPasscode: \(String(describing: self.useCommissionerGeneratedPasscode))") + viewModel.connect(selectedCastingPlayer: self.selectedCastingPlayer, useCommissionerGeneratedPasscode: self.useCommissionerGeneratedPasscode) + } }) } } struct MCConnectionExampleView_Previews: PreviewProvider { static var previews: some View { - MCConnectionExampleView(_selectedCastingPlayer: nil) + MCConnectionExampleView(_selectedCastingPlayer: nil, _useCommissionerGeneratedPasscode: false) } } diff --git a/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCConnectionExampleViewModel.swift b/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCConnectionExampleViewModel.swift index 13cdad3d8e60f1..81f362c991305a 100644 --- a/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCConnectionExampleViewModel.swift +++ b/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCConnectionExampleViewModel.swift @@ -15,9 +15,9 @@ * limitations under the License. */ - import Foundation import os.log +import UIKit class MCConnectionExampleViewModel: ObservableObject { let Log = Logger(subsystem: "com.matter.casting", @@ -25,29 +25,169 @@ class MCConnectionExampleViewModel: ObservableObject { // VendorId of the MCEndpoint on the MCCastingPlayer that the MCCastingApp desires to interact with after connection let kDesiredEndpointVendorId: UInt16 = 65521; + + // VendorId of the MCEndpoint on the MCCastingPlayer that the MCCastingApp desires to interact with after connecting + // using the MCCastingPlayer/Commissioner-Generated passcode (CGP) commissioning flow. Use this Target Content + // Application Vendor ID, which is configured on the tv-app. This Target Content Application Vendor ID (1111), does + // not implement the AccountLogin cluster, which would otherwise auto commission using the Commissionee-Generated + // passcode upon recieving the IdentificationDeclaration Message. See + // connectedhomeip/examples/tv-app/tv-common/include/AppTv.h. + let kDesiredEndpointVendorIdCGP: UInt16 = 1111; @Published var connectionSuccess: Bool?; @Published var connectionStatus: String?; - func connect(selectedCastingPlayer: MCCastingPlayer?) { - let desiredEndpointFilter: MCEndpointFilter = MCEndpointFilter() - desiredEndpointFilter.vendorId = kDesiredEndpointVendorId - selectedCastingPlayer?.verifyOrEstablishConnection(completionBlock: { err in - self.Log.error("MCConnectionExampleViewModel connect() completed with \(err)") - DispatchQueue.main.async - { - if(err == nil) - { + func connect(selectedCastingPlayer: MCCastingPlayer?, useCommissionerGeneratedPasscode: Bool) { + self.Log.info("MCConnectionExampleViewModel.connect() useCommissionerGeneratedPasscode: \(String(describing: useCommissionerGeneratedPasscode))") + + let connectionCompleteCallback: (Swift.Error?) -> Void = { err in + self.Log.error("MCConnectionExampleViewModel connect() completed with: \(err)") + DispatchQueue.main.async { + if err == nil { self.connectionSuccess = true - self.connectionStatus = "Connected!" - } - else - { + if useCommissionerGeneratedPasscode { + self.connectionStatus = "Successfully connected to Casting Player using the Casting Player/Commissioner-Generated passcode!" + self.Log.info("MCConnectionExampleViewModel connect() Successfully connected to Casting Player using the Casting Player/CommissioneR-Generated passcode!") + } else { + self.connectionStatus = "Successfully connected to Casting Player!" + self.Log.info("MCConnectionExampleViewModel connect() Successfully connected to Casting Player using the Casting App/CommissioneE-Generated passcode!") + } + } else { self.connectionSuccess = false - self.connectionStatus = "Connection failed with \(String(describing: err))" + self.connectionStatus = "Connection to Casting Player failed with: \(String(describing: err))" } } - }, desiredEndpointFilter: desiredEndpointFilter) + } + + let commissionerDeclarationCallback: (MCCommissionerDeclaration) -> Void = { commissionerDeclarationMessage in + DispatchQueue.main.async { + self.Log.info("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, recived a message form the MCCastingPlayer:\n\(commissionerDeclarationMessage)") + if commissionerDeclarationMessage.getCommissionerPasscode() { + self.Log.info("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, calling getTopMostViewController()") + if let topViewController = self.getTopMostViewController() { + self.Log.info("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, calling displayPasscodeInputDialog()") + self.displayPasscodeInputDialog(on: topViewController, continueConnecting: { passcode in + self.Log.info("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, Continuing to connect with user entered MCCastingPlayer/Commissioner-Generated passcode: \(passcode)") + if let castingApp = MCCastingApp.getSharedInstance() { + let commissionableData: MCCommissionableData = MCCommissionableData( + passcode: UInt32(passcode) ?? 0, + discriminator: 0, + spake2pIterationCount: 1000, + spake2pVerifier: nil, + spake2pSalt: nil + ) + // Update the CommissionableData DataProvider with the user entered CastingPlayer/Commissioner-Generated + // setup passcode. This is mandatory for the Commissioner-Generated passcode commissioning flow since + // the commissioning session's PAKE verifier needs to be updated with the entered passcode. + let errUpdate = castingApp.updateCommissionableDataProvider(commissionableData) + if errUpdate != nil { + self.connectionStatus = "MCCastingPlayer.updateCommissionableDataProvider() failed with: \(String(describing: errUpdate)) \n\nRoute back and try again." + self.Log.error("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, MCCastingPlayer.updateCommissionableDataProvider() failed due to: \(errUpdate)") + } + self.Log.info("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, calling MCCastingPlayer.continueConnecting()") + let errContinue = selectedCastingPlayer?.continueConnecting() + if errContinue == nil { + self.connectionStatus = "Continuing to connect with user entered passcode: \(passcode)" + } else { + self.connectionStatus = "Continue Connecting to Casting Player failed with: \(String(describing: errContinue)) \n\nRoute back and try again." + self.Log.error("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, MCCastingPlayer.continueConnecting() failed due to: \(errContinue)") + } + } + }, cancelConnecting: { + self.Log.info("MCConnectionExampleViewModel connect() commissionerDeclarationCallback, Connection attempt cancelled by the user, calling MCCastingPlayer.stopConnecting()") + let err = selectedCastingPlayer?.stopConnecting() + 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)") + } + }) + } + } + } + } + + let identificationDeclarationOptions: MCIdentificationDeclarationOptions + let targetAppInfo: MCTargetAppInfo = MCTargetAppInfo() + let connectionCallbacks: MCConnectionCallbacks + + if useCommissionerGeneratedPasscode { + identificationDeclarationOptions = MCIdentificationDeclarationOptions(commissionerPasscodeOnly: true) + targetAppInfo.vendorId = kDesiredEndpointVendorIdCGP + connectionCallbacks = MCConnectionCallbacks( + callbacks: connectionCompleteCallback, + commissionerDeclarationCallback: commissionerDeclarationCallback + ) + } else { + identificationDeclarationOptions = MCIdentificationDeclarationOptions() + targetAppInfo.vendorId = kDesiredEndpointVendorId + connectionCallbacks = MCConnectionCallbacks( + callbacks: connectionCompleteCallback, + commissionerDeclarationCallback: nil + ) + } + + identificationDeclarationOptions.addTargetAppInfo(targetAppInfo) + identificationDeclarationOptions.logDetail() + + self.Log.info("MCConnectionExampleViewModel.connect() calling MCCastingPlayer.verifyOrEstablishConnection()") + let err = selectedCastingPlayer?.verifyOrEstablishConnection(with: connectionCallbacks, identificationDeclarationOptions: identificationDeclarationOptions) + if err != nil { + self.Log.error("MCConnectionExampleViewModel connect(), MCCastingPlayer.verifyOrEstablishConnection() failed due to: \(err)") + } + } + + // Function to display the passcode input dialog + func displayPasscodeInputDialog(on viewController: UIViewController, continueConnecting: @escaping (String) -> Void, cancelConnecting: @escaping () -> Void) { + self.Log.info("MCConnectionExampleViewModel displayPasscodeInputDialog()") + + // Create the alert controller + let alertController = UIAlertController(title: "Enter Passcode", message: nil, preferredStyle: .alert) + + // Add the text field with the default passcode + alertController.addTextField { textField in + textField.placeholder = "Enter Passcode" + textField.text = "12345678" + // textField.isSecureTextEntry = true // Makes the passcode invisible + } + + // Add the "Continue Connecting" button + let continueAction = UIAlertAction(title: "Continue Connecting", style: .default) { _ in + if let passcode = alertController.textFields?.first?.text { + self.Log.info("MCConnectionExampleViewModel displayPasscodeInputDialog() User entered passcode: \(passcode)") + continueConnecting(passcode) + } + } + alertController.addAction(continueAction) + + // Add the "Cancel" button + let cancelAction = UIAlertAction(title: "Cancel", style: .cancel) { _ in + self.Log.info("MCConnectionExampleViewModel displayPasscodeInputDialog() User cancelled the passcode input dialog.") + cancelConnecting() + } + alertController.addAction(cancelAction) + + // Present the alert controller + viewController.present(alertController, animated: true, completion: nil) + } + + // Function to get the top-most view controller + func getTopMostViewController() -> UIViewController? { + self.Log.info("MCConnectionExampleViewModel getTopMostViewController()") + guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first(where: { $0.isKeyWindow }) else { + return nil + } + + var topController = window.rootViewController + while let presentedController = topController?.presentedViewController { + topController = presentedController + } + + return topController } } diff --git a/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCContentLauncherLaunchURLExampleViewModel.swift b/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCContentLauncherLaunchURLExampleViewModel.swift index 302ec886b4e115..9923e2301ea30f 100644 --- a/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCContentLauncherLaunchURLExampleViewModel.swift +++ b/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCContentLauncherLaunchURLExampleViewModel.swift @@ -30,65 +30,79 @@ class MCContentLauncherLaunchURLExampleViewModel: ObservableObject { func invokeCommand(castingPlayer: MCCastingPlayer, contentUrl: String, displayString: String) { + self.Log.info("MCContentLauncherLaunchURLExampleViewModel.invokeCommand()") + castingPlayer.logAllEndpoints() + var selectedEndpoint: MCEndpoint? + // select the MCEndpoint on the MCCastingPlayer to invoke the command on - if let endpoint: MCEndpoint = castingPlayer.endpoints().filter({ $0.vendorId().intValue == sampleEndpointVid}).first - { - // validate that the selected endpoint supports the ContentLauncher cluster - if(!endpoint.hasCluster(MCEndpointClusterTypeContentLauncher)) - { - self.Log.error("No ContentLauncher cluster supporting endpoint found") - DispatchQueue.main.async - { - self.status = "No ContentLauncher cluster supporting endpoint found" - } - return + if let endpoint = castingPlayer.endpoints().filter({ $0.vendorId().intValue == sampleEndpointVid }).first { + selectedEndpoint = endpoint + // For the example Commissioner-Generated passcode commissioning flow, run demo interactions with the Endpoint with + // ID 1. For this flow, we commissioned with the Target Content Application with Vendor ID 1111. Since this target + // content application does not report its Endpoint's Vendor IDs, we find the desired endpoint based on the Endpoint + // ID. See connectedhomeip/examples/tv-app/tv-common/include/AppTv.h. + } else if let endpoint = castingPlayer.endpoints().filter({ $0.identifier().intValue == 1 }).first { + self.Log.info("MCContentLauncherLaunchURLExampleViewModel.invokeCommand() No endpoint matching the sampleEndpointVid: \(String(describing: self.sampleEndpointVid)), but found endpoint with identifier: 1") + selectedEndpoint = endpoint + } + + guard let endpoint = selectedEndpoint else { + self.Log.error("No endpoint matching the example VID or identifier 1 found") + DispatchQueue.main.async { + self.status = "No endpoint matching the example VID or identifier 1 found" } - - // get ContentLauncher cluster from the endpoint - let contentLaunchercluster: MCContentLauncherCluster = endpoint.cluster(for: MCEndpointClusterTypeContentLauncher) as! MCContentLauncherCluster + return + } - // get the launchURLCommand from the contentLauncherCluster - let launchURLCommand: MCContentLauncherClusterLaunchURLCommand? = contentLaunchercluster.launchURLCommand() - if(launchURLCommand == nil) + self.Log.info("MCContentLauncherLaunchURLExampleViewModel.invokeCommand() selected endpoint: \(endpoint.description)") + + // validate that the selected endpoint supports the ContentLauncher cluster + if(!endpoint.hasCluster(MCEndpointClusterTypeContentLauncher)) + { + self.Log.error("No ContentLauncher cluster supporting endpoint found") + DispatchQueue.main.async { - self.Log.error("LaunchURL not supported on cluster") - DispatchQueue.main.async - { - self.status = "LaunchURL not supported on cluster" - } - return + self.status = "No ContentLauncher cluster supporting endpoint found" } - - // create the LaunchURL request - let request: MCContentLauncherClusterLaunchURLParams = MCContentLauncherClusterLaunchURLParams() - request.contentURL = contentUrl - request.displayString = displayString - - // call invoke on launchURLCommand while passing in a completion block - launchURLCommand!.invoke(request, context: nil, completion: { context, err, response in - DispatchQueue.main.async - { - if(err == nil) - { - self.Log.info("LaunchURLCommand invoke completion success with \(String(describing: response))") - self.status = "Success. Response data: \(String(describing: response?.data))" - } - else - { - self.Log.error("LaunchURLCommand invoke completion failure with \(String(describing: err))") - self.status = "Failure: \(String(describing: err))" - } - } - }, - timedInvokeTimeoutMs: 5000) // time out after 5000ms + return } - else + + // get ContentLauncher cluster from the endpoint + let contentLaunchercluster: MCContentLauncherCluster = endpoint.cluster(for: MCEndpointClusterTypeContentLauncher) as! MCContentLauncherCluster + + // get the launchURLCommand from the contentLauncherCluster + let launchURLCommand: MCContentLauncherClusterLaunchURLCommand? = contentLaunchercluster.launchURLCommand() + if(launchURLCommand == nil) { - self.Log.error("No endpoint matching the example VID found") + self.Log.error("LaunchURL not supported on cluster") DispatchQueue.main.async { - self.status = "No endpoint matching the example VID found" + self.status = "LaunchURL not supported on cluster" } + return } + + // create the LaunchURL request + let request: MCContentLauncherClusterLaunchURLParams = MCContentLauncherClusterLaunchURLParams() + request.contentURL = contentUrl + request.displayString = displayString + + // call invoke on launchURLCommand while passing in a completion block + launchURLCommand!.invoke(request, context: nil, completion: { context, err, response in + DispatchQueue.main.async + { + if(err == nil) + { + self.Log.info("LaunchURLCommand invoke completion success with \(String(describing: response))") + self.status = "Success. Response data: \(String(describing: response?.data))" + } + else + { + self.Log.error("LaunchURLCommand invoke completion failure with \(String(describing: err))") + self.status = "Failure: \(String(describing: err))" + } + } + }, + timedInvokeTimeoutMs: 5000) // time out after 5000ms } } diff --git a/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCDiscoveryExampleView.swift b/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCDiscoveryExampleView.swift index 769b84c2a35963..d697d512269404 100644 --- a/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCDiscoveryExampleView.swift +++ b/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCDiscoveryExampleView.swift @@ -25,25 +25,30 @@ extension MCCastingPlayer : Identifiable { struct MCDiscoveryExampleView: View { @StateObject var viewModel = MCDiscoveryExampleViewModel() + @State private var selectedCastingPlayer: MCCastingPlayer? + // Enable navigating to the MCConnectionExampleView with or without the use + // Commissioner-Generated Passcode (CGP) flag. + @State private var navigateWithUseCGP = false + @State private var navigateWithoutUseCGP = false var body: some View { - VStack(alignment: .leading) { + VStack(alignment: .center, spacing: 16) { Button("Start Discovery", action: viewModel.startDiscovery) - .frame(width: 350, height: 30, alignment: .center) + .frame(width: 350, height: 30) .border(Color.black, width: 1) .background(Color.blue) .foregroundColor(Color.white) .padding(1) Button("Stop Discovery", action: viewModel.stopDiscovery) - .frame(width: 350, height: 30, alignment: .center) + .frame(width: 350, height: 30) .border(Color.black, width: 1) .background(Color.blue) .foregroundColor(Color.white) .padding(1) Button("Clear Results", action: viewModel.clearResults) - .frame(width: 350, height: 30, alignment: .center) + .frame(width: 350, height: 30) .border(Color.black, width: 1) .background(Color.blue) .foregroundColor(Color.white) @@ -55,28 +60,64 @@ struct MCDiscoveryExampleView: View { } else if(!viewModel.displayedCastingPlayers.isEmpty) { - Text("Select a Casting player...") + Text("Select a Casting player:") ForEach(viewModel.displayedCastingPlayers) { castingPlayer in - NavigationLink( - destination: { - MCConnectionExampleView(_selectedCastingPlayer: castingPlayer) - }, - label: { - Text(castingPlayer.description) - .frame(minHeight: 50) - .padding() - } - ) - .frame(width: 350, alignment: .center) - .border(Color.black, width: 1) - .background(Color.blue) - .foregroundColor(Color.white) + ZStack { + NavigationLink( + destination: MCConnectionExampleView(_selectedCastingPlayer: castingPlayer, _useCommissionerGeneratedPasscode: false), + isActive: Binding( + get: { navigateWithoutUseCGP && selectedCastingPlayer?.id == castingPlayer.id }, + set: { newValue in + if !newValue { + navigateWithoutUseCGP = false + selectedCastingPlayer = nil + } + } + ), + label: { EmptyView() } + ) + NavigationLink( + destination: MCConnectionExampleView(_selectedCastingPlayer: castingPlayer, _useCommissionerGeneratedPasscode: true), + isActive: Binding( + get: { navigateWithUseCGP && selectedCastingPlayer?.id == castingPlayer.id }, + set: { newValue in + if !newValue { + navigateWithUseCGP = false + selectedCastingPlayer = nil + } + } + ), + label: { EmptyView() } + ) + Text(castingPlayer.description) + .frame(minHeight: 50) + .multilineTextAlignment(.center) + .padding(10) + .background(Color.blue) + .foregroundColor(Color.white) + .border(Color.black, width: 1) + .onTapGesture { + selectedCastingPlayer = castingPlayer + navigateWithoutUseCGP = true + } + .gesture( + LongPressGesture() + .onEnded { _ in + selectedCastingPlayer = castingPlayer + navigateWithUseCGP = true + } + ) + } + .frame(width: 350) .padding(1) } } + Spacer() + Text("Long click on a Casting Player to connect using CastingPlayer/Commissioner-Generated passcode commissioning flow (if supported).").font(.system(size: 12)).padding(1) } .navigationTitle("Casting Player Discovery") .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .top) + .multilineTextAlignment(.center) } } diff --git a/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCInitializationExample.swift b/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCInitializationExample.swift index daae3ab449418f..7d22a37e34e8ae 100644 --- a/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCInitializationExample.swift +++ b/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCInitializationExample.swift @@ -24,6 +24,33 @@ class MCAppParametersDataSource : NSObject, MCDataSource let Log = Logger(subsystem: "com.matter.casting", category: "MCAppParametersDataSource") + // Dummy values for demonstration only. + private var commissionableData: MCCommissionableData = MCCommissionableData( + passcode: 20202021, + discriminator: 3874, + // Default to the minimum PBKDF iterations (1,000) for this example implementation. For TV devices and TV casting app production + // implementations, you should use a higher number of PBKDF iterations to enhance security. The default minimum iterations are + // not sufficient against brute-force and rainbow table attacks. Increasing the number of iterations will increase the + // computational time required to derive the key. This can slow down the authentication process, especially on devices with + // limited processing power like a Raspberry Pi 4. For a production implementation, you should measure the actual performance on + // the target device. + // + // 1,000 - Hypothetical key derivation time: ~20 milliseconds (ms). + // 100,000 - Hypothetical key derivation time: ~2 seconds. + spake2pIterationCount: 1000, + spake2pVerifier: nil, + spake2pSalt: nil + ) + + /** + * This function needs to be implemented by the client in use cases where the MCCommissionableData needs to be updated + * post-initialization. For example, when the Commissioner-Generated Passcode feature is used. + */ + func update(_ newCommissionableData: MCCommissionableData) { + Log.info("MCAppParametersDataSource.updateCommissionableData()") + self.commissionableData = newCommissionableData + } + func clientQueue() -> DispatchQueue { return DispatchQueue.main; } @@ -34,13 +61,8 @@ class MCAppParametersDataSource : NSObject, MCDataSource } func castingAppDidReceiveRequestForCommissionableData(_ sender: Any) -> MCCommissionableData { - // dummy values for demonstration only - return MCCommissionableData( - passcode: 20202021, - discriminator: 3874, - spake2pIterationCount: 1000, - spake2pVerifier: nil, - spake2pSalt: nil) + Log.info("MCAppParametersDataSource castingAppDidReceiveRequestForCommissionableData()") + return commissionableData } // dummy DAC values for demonstration only @@ -99,10 +121,10 @@ class MCAppParametersDataSource : NSObject, MCDataSource class MCInitializationExample { let Log = Logger(subsystem: "com.matter.casting", category: "MCInitializationExample") - func initialize() -> Error? { if let castingApp = MCCastingApp.getSharedInstance() { + Log.info("MCInitializationExample initialize() calling MCCastingApp.initializeWithDataSource()") return castingApp.initialize(with: MCAppParametersDataSource()) } else diff --git a/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCMediaPlaybackSubscribeToCurrentStateExampleViewModel.swift b/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCMediaPlaybackSubscribeToCurrentStateExampleViewModel.swift index e7d3581dd07350..bd6f7fb6f6b1a2 100644 --- a/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCMediaPlaybackSubscribeToCurrentStateExampleViewModel.swift +++ b/examples/tv-casting-app/darwin/TvCasting/TvCasting/MCMediaPlaybackSubscribeToCurrentStateExampleViewModel.swift @@ -30,69 +30,83 @@ class MCMediaPlaybackSubscribeToCurrentStateExampleViewModel: ObservableObject { func subscribe(castingPlayer: MCCastingPlayer) { + self.Log.info("MCMediaPlaybackSubscribeToCurrentStateExampleViewModel.subscribe()") + castingPlayer.logAllEndpoints() + var selectedEndpoint: MCEndpoint? + // select the MCEndpoint on the MCCastingPlayer to invoke the command on - if let endpoint: MCEndpoint = castingPlayer.endpoints().filter({ $0.vendorId().intValue == sampleEndpointVid}).first - { - // validate that the selected endpoint supports the MediaPlayback cluster - if(!endpoint.hasCluster(MCEndpointClusterTypeMediaPlayback)) - { - self.Log.error("No MediaPlayback cluster supporting endpoint found") - DispatchQueue.main.async - { - self.status = "No MediaPlayback cluster supporting endpoint found" - } - return + if let endpoint = castingPlayer.endpoints().filter({ $0.vendorId().intValue == sampleEndpointVid }).first { + selectedEndpoint = endpoint + // For the example Commissioner-Generated passcode commissioning flow, run demo interactions with the Endpoint with + // ID 1. For this flow, we commissioned with the Target Content Application with Vendor ID 1111. Since this target + // content application does not report its Endpoint's Vendor IDs, we find the desired endpoint based on the Endpoint + // ID. See connectedhomeip/examples/tv-app/tv-common/include/AppTv.h. + } else if let endpoint = castingPlayer.endpoints().filter({ $0.identifier().intValue == 1 }).first { + self.Log.info("MCMediaPlaybackSubscribeToCurrentStateExampleViewModel.subscribe() No endpoint matching the sampleEndpointVid: \(String(describing: self.sampleEndpointVid)), but found endpoint with identifier: 1") + selectedEndpoint = endpoint + } + + guard let endpoint = selectedEndpoint else { + self.Log.error("No endpoint matching the example VID or identifier 1 found") + DispatchQueue.main.async { + self.status = "No endpoint matching the example VID or identifier 1 found" } - - // get MediaPlayback cluster from the endpoint - let mediaPlaybackCluster: MCMediaPlaybackCluster = endpoint.cluster(for: MCEndpointClusterTypeMediaPlayback) as! MCMediaPlaybackCluster + return + } + + self.Log.info("MCMediaPlaybackSubscribeToCurrentStateExampleViewModel.subscribe() selected endpoint: \(endpoint.description)") - // get the currentStateAttribute from the mediaPlaybackCluster - let currentStateAttribute: MCMediaPlaybackClusterCurrentStateAttribute? = mediaPlaybackCluster.currentStateAttribute() - if(currentStateAttribute == nil) + // validate that the selected endpoint supports the MediaPlayback cluster + if(!endpoint.hasCluster(MCEndpointClusterTypeMediaPlayback)) + { + self.Log.error("No MediaPlayback cluster supporting endpoint found") + DispatchQueue.main.async { - self.Log.error("CurrentState attribute not supported on cluster") - DispatchQueue.main.async - { - self.status = "CurrentState attribute not supported on cluster" - } - return + self.status = "No MediaPlayback cluster supporting endpoint found" } - - - // call read on currentStateAttribute and pass in a completion block - currentStateAttribute!.subscribe(nil, completion: { context, before, after, err in - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "HH:mm:ss" - let currentTime = dateFormatter.string(from: Date()) - DispatchQueue.main.async - { - if(err != nil) - { - self.Log.error("Error when reading CurrentState value \(String(describing: err)) at \(currentTime)") - self.status = "Error when reading CurrentState value \(String(describing: err)) at \(currentTime)" - return - } - if(before != nil) - { - self.Log.info("Read CurrentState value: \(String(describing: after)) Before: \(String(describing: before)) at \(currentTime)") - self.status = "Read CurrentState value: \(String(describing: after)) Before: \(String(describing: before)) at \(currentTime)" - } - else - { - self.Log.info("Read CurrentState value: \(String(describing: after)) at \(currentTime)") - self.status = "Read CurrentState value: \(String(describing: after)) at \(currentTime)" - } - } - }, minInterval: 0, maxInterval: 1) + return } - else + + // get MediaPlayback cluster from the endpoint + let mediaPlaybackCluster: MCMediaPlaybackCluster = endpoint.cluster(for: MCEndpointClusterTypeMediaPlayback) as! MCMediaPlaybackCluster + + // get the currentStateAttribute from the mediaPlaybackCluster + let currentStateAttribute: MCMediaPlaybackClusterCurrentStateAttribute? = mediaPlaybackCluster.currentStateAttribute() + if(currentStateAttribute == nil) { - self.Log.error("No endpoint matching the example VID found") + self.Log.error("CurrentState attribute not supported on cluster") DispatchQueue.main.async { - self.status = "No endpoint matching the example VID found" + self.status = "CurrentState attribute not supported on cluster" } + return } + + + // call read on currentStateAttribute and pass in a completion block + currentStateAttribute!.subscribe(nil, completion: { context, before, after, err in + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "HH:mm:ss" + let currentTime = dateFormatter.string(from: Date()) + DispatchQueue.main.async + { + if(err != nil) + { + self.Log.error("Error when reading CurrentState value \(String(describing: err)) at \(currentTime)") + self.status = "Error when reading CurrentState value \(String(describing: err)) at \(currentTime)" + return + } + if(before != nil) + { + self.Log.info("Read CurrentState value: \(String(describing: after)) Before: \(String(describing: before)) at \(currentTime)") + self.status = "Read CurrentState value: \(String(describing: after)) Before: \(String(describing: before)) at \(currentTime)" + } + else + { + self.Log.info("Read CurrentState value: \(String(describing: after)) at \(currentTime)") + self.status = "Read CurrentState value: \(String(describing: after)) at \(currentTime)" + } + } + }, minInterval: 0, maxInterval: 1) } }