├── AndroidTVRemoteControl.podspec ├── Demo └── Demo │ ├── Demo.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcuserdata │ │ └── romanodyshew.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist │ └── Demo │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Info.plist │ ├── RemoteTVManager.swift │ ├── SceneDelegate.swift │ ├── ViewController.swift │ ├── Views.swift │ ├── cert.der │ └── cert.p12 ├── LICENSE.md ├── Package.swift ├── README.md ├── Sources └── AndroidTVRemoteControl │ ├── CertManager.swift │ ├── Commands │ ├── DeepLink.swift │ ├── Direction.swift │ ├── Key.swift │ └── KeyPress.swift │ ├── CryptoManager.swift │ ├── Errors.swift │ ├── Network │ ├── CommandNetwork │ │ ├── AndroidTVConfigurationMessage.swift │ │ ├── CommandNetwork.swift │ │ ├── Ping.swift │ │ ├── SecodConfiguration.swift │ │ ├── SecondConfigurationResponse.swift │ │ └── VolumeLevel.swift │ ├── PairingNetwork │ │ ├── Configuration.swift │ │ ├── Option.swift │ │ ├── Pairing.swift │ │ ├── PairingNetwork.swift │ │ └── Secret.swift │ └── RequestDataProtocol.swift │ ├── PairingManager.swift │ ├── RemoteManager.swift │ ├── TLSManager.swift │ ├── coding │ ├── Decoder.swift │ └── Encoder.swift │ └── misc │ ├── Logger.swift │ └── Result.swift ├── Tests └── AndroidTVRemoteControlTests │ └── AndroidTVRemoteControlTests.swift └── assets ├── pairing.png └── preparing.png /AndroidTVRemoteControl.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'AndroidTVRemoteControl' 3 | s.version = '2.4.16' 4 | s.summary = 'Implementation of the remote control protocol v2 for Android TV.' 5 | s.homepage = 'https://github.com/odyshewroman/AndroidTVRemoteControl' 6 | s.license = { :type => 'MIT', :file => 'LICENSE.md' } 7 | s.author = { 'Odyshew Roman' => 'odyshewroman@gmail.com' } 8 | s.source = { :git => 'https://github.com/odyshewroman/AndroidTVRemoteControl.git', :tag => s.version.to_s } 9 | s.ios.deployment_target = '13.0' 10 | s.swift_version = '5.0' 11 | s.source_files = 'Sources/AndroidTVRemoteControl/**/*' 12 | end 13 | -------------------------------------------------------------------------------- /Demo/Demo/Demo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | C591B96C2AE1CEED00A50131 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C591B96B2AE1CEED00A50131 /* AppDelegate.swift */; }; 11 | C591B96E2AE1CEED00A50131 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C591B96D2AE1CEED00A50131 /* SceneDelegate.swift */; }; 12 | C591B9702AE1CEED00A50131 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C591B96F2AE1CEED00A50131 /* ViewController.swift */; }; 13 | C591B9752AE1CEEE00A50131 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C591B9742AE1CEEE00A50131 /* Assets.xcassets */; }; 14 | C591B9802AE2356800A50131 /* RemoteTVManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C591B97F2AE2356800A50131 /* RemoteTVManager.swift */; }; 15 | C591B9B82AE23C7A00A50131 /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = C591B9B12AE23C7A00A50131 /* Errors.swift */; }; 16 | C591B9B92AE23C7A00A50131 /* TLSManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C591B9B22AE23C7A00A50131 /* TLSManager.swift */; }; 17 | C591B9BA2AE23C7A00A50131 /* PairingManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C591B9B32AE23C7A00A50131 /* PairingManager.swift */; }; 18 | C591B9BB2AE23C7A00A50131 /* CryptoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C591B9B42AE23C7A00A50131 /* CryptoManager.swift */; }; 19 | C591B9BD2AE23C7A00A50131 /* CertManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C591B9B62AE23C7A00A50131 /* CertManager.swift */; }; 20 | C591B9BE2AE23C7A00A50131 /* RemoteManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C591B9B72AE23C7A00A50131 /* RemoteManager.swift */; }; 21 | C591B9C12AE23CA300A50131 /* RequestDataProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C591B9C02AE23CA300A50131 /* RequestDataProtocol.swift */; }; 22 | C591B9C82AE23CBE00A50131 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C591B9C32AE23CBE00A50131 /* Configuration.swift */; }; 23 | C591B9C92AE23CBE00A50131 /* Option.swift in Sources */ = {isa = PBXBuildFile; fileRef = C591B9C42AE23CBE00A50131 /* Option.swift */; }; 24 | C591B9CA2AE23CBE00A50131 /* Pairing.swift in Sources */ = {isa = PBXBuildFile; fileRef = C591B9C52AE23CBE00A50131 /* Pairing.swift */; }; 25 | C591B9CB2AE23CBE00A50131 /* PairingNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = C591B9C62AE23CBE00A50131 /* PairingNetwork.swift */; }; 26 | C591B9CC2AE23CBE00A50131 /* Secret.swift in Sources */ = {isa = PBXBuildFile; fileRef = C591B9C72AE23CBE00A50131 /* Secret.swift */; }; 27 | C591B9D32AE23CD600A50131 /* CommandNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = C591B9CE2AE23CD600A50131 /* CommandNetwork.swift */; }; 28 | C591B9D42AE23CD600A50131 /* Ping.swift in Sources */ = {isa = PBXBuildFile; fileRef = C591B9CF2AE23CD600A50131 /* Ping.swift */; }; 29 | C591B9D52AE23CD600A50131 /* SecodConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C591B9D02AE23CD600A50131 /* SecodConfiguration.swift */; }; 30 | C591B9D62AE23CD600A50131 /* SecondConfigurationResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = C591B9D12AE23CD600A50131 /* SecondConfigurationResponse.swift */; }; 31 | C591B9D72AE23CD600A50131 /* AndroidTVConfigurationMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C591B9D22AE23CD600A50131 /* AndroidTVConfigurationMessage.swift */; }; 32 | C591B9DA2AE23E7F00A50131 /* cert.der in Resources */ = {isa = PBXBuildFile; fileRef = C591B9D82AE23E7F00A50131 /* cert.der */; }; 33 | C591B9DB2AE23E7F00A50131 /* cert.p12 in Resources */ = {isa = PBXBuildFile; fileRef = C591B9D92AE23E7F00A50131 /* cert.p12 */; }; 34 | C5B25B4D2AFBCCCC00BE8743 /* Direction.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B25B492AFBCCCC00BE8743 /* Direction.swift */; }; 35 | C5B25B4E2AFBCCCC00BE8743 /* Key.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B25B4A2AFBCCCC00BE8743 /* Key.swift */; }; 36 | C5B25B4F2AFBCCCC00BE8743 /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B25B4B2AFBCCCC00BE8743 /* DeepLink.swift */; }; 37 | C5B25B502AFBCCCC00BE8743 /* KeyPress.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B25B4C2AFBCCCC00BE8743 /* KeyPress.swift */; }; 38 | C5D3739F2AE2EE4500BE7F37 /* Views.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5D3739E2AE2EE4500BE7F37 /* Views.swift */; }; 39 | C5E26BD62B2025D4007D7E08 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5E26BD42B2025D4007D7E08 /* Result.swift */; }; 40 | C5E26BD72B2025D4007D7E08 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5E26BD52B2025D4007D7E08 /* Logger.swift */; }; 41 | C5E26BDB2B2F33A6007D7E08 /* Decoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5E26BD92B2F33A6007D7E08 /* Decoder.swift */; }; 42 | C5E26BDC2B2F33A6007D7E08 /* Encoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5E26BDA2B2F33A6007D7E08 /* Encoder.swift */; }; 43 | C5EFFD182B0B9B0B00B35F8B /* VolumeLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5EFFD172B0B9B0B00B35F8B /* VolumeLevel.swift */; }; 44 | /* End PBXBuildFile section */ 45 | 46 | /* Begin PBXFileReference section */ 47 | C591B9682AE1CEED00A50131 /* Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Demo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 48 | C591B96B2AE1CEED00A50131 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 49 | C591B96D2AE1CEED00A50131 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 50 | C591B96F2AE1CEED00A50131 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 51 | C591B9742AE1CEEE00A50131 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 52 | C591B9792AE1CEEE00A50131 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 53 | C591B97F2AE2356800A50131 /* RemoteTVManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteTVManager.swift; sourceTree = ""; }; 54 | C591B9B12AE23C7A00A50131 /* Errors.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Errors.swift; path = ../../../Sources/AndroidTVRemoteControl/Errors.swift; sourceTree = ""; }; 55 | C591B9B22AE23C7A00A50131 /* TLSManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TLSManager.swift; path = ../../../Sources/AndroidTVRemoteControl/TLSManager.swift; sourceTree = ""; }; 56 | C591B9B32AE23C7A00A50131 /* PairingManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PairingManager.swift; path = ../../../Sources/AndroidTVRemoteControl/PairingManager.swift; sourceTree = ""; }; 57 | C591B9B42AE23C7A00A50131 /* CryptoManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CryptoManager.swift; path = ../../../Sources/AndroidTVRemoteControl/CryptoManager.swift; sourceTree = ""; }; 58 | C591B9B62AE23C7A00A50131 /* CertManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CertManager.swift; path = ../../../Sources/AndroidTVRemoteControl/CertManager.swift; sourceTree = ""; }; 59 | C591B9B72AE23C7A00A50131 /* RemoteManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RemoteManager.swift; path = ../../../Sources/AndroidTVRemoteControl/RemoteManager.swift; sourceTree = ""; }; 60 | C591B9C02AE23CA300A50131 /* RequestDataProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = RequestDataProtocol.swift; path = ../../../Sources/AndroidTVRemoteControl/Network/RequestDataProtocol.swift; sourceTree = ""; }; 61 | C591B9C32AE23CBE00A50131 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Configuration.swift; path = ../../../Sources/AndroidTVRemoteControl/Network/PairingNetwork/Configuration.swift; sourceTree = ""; }; 62 | C591B9C42AE23CBE00A50131 /* Option.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Option.swift; path = ../../../Sources/AndroidTVRemoteControl/Network/PairingNetwork/Option.swift; sourceTree = ""; }; 63 | C591B9C52AE23CBE00A50131 /* Pairing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Pairing.swift; path = ../../../Sources/AndroidTVRemoteControl/Network/PairingNetwork/Pairing.swift; sourceTree = ""; }; 64 | C591B9C62AE23CBE00A50131 /* PairingNetwork.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = PairingNetwork.swift; path = ../../../Sources/AndroidTVRemoteControl/Network/PairingNetwork/PairingNetwork.swift; sourceTree = ""; }; 65 | C591B9C72AE23CBE00A50131 /* Secret.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Secret.swift; path = ../../../Sources/AndroidTVRemoteControl/Network/PairingNetwork/Secret.swift; sourceTree = ""; }; 66 | C591B9CE2AE23CD600A50131 /* CommandNetwork.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = CommandNetwork.swift; path = ../../../Sources/AndroidTVRemoteControl/Network/CommandNetwork/CommandNetwork.swift; sourceTree = ""; }; 67 | C591B9CF2AE23CD600A50131 /* Ping.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Ping.swift; path = ../../../Sources/AndroidTVRemoteControl/Network/CommandNetwork/Ping.swift; sourceTree = ""; }; 68 | C591B9D02AE23CD600A50131 /* SecodConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SecodConfiguration.swift; path = ../../../Sources/AndroidTVRemoteControl/Network/CommandNetwork/SecodConfiguration.swift; sourceTree = ""; }; 69 | C591B9D12AE23CD600A50131 /* SecondConfigurationResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SecondConfigurationResponse.swift; path = ../../../Sources/AndroidTVRemoteControl/Network/CommandNetwork/SecondConfigurationResponse.swift; sourceTree = ""; }; 70 | C591B9D22AE23CD600A50131 /* AndroidTVConfigurationMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AndroidTVConfigurationMessage.swift; path = ../../../Sources/AndroidTVRemoteControl/Network/CommandNetwork/AndroidTVConfigurationMessage.swift; sourceTree = ""; }; 71 | C591B9D82AE23E7F00A50131 /* cert.der */ = {isa = PBXFileReference; lastKnownFileType = file; path = cert.der; sourceTree = ""; }; 72 | C591B9D92AE23E7F00A50131 /* cert.p12 */ = {isa = PBXFileReference; lastKnownFileType = file; path = cert.p12; sourceTree = ""; }; 73 | C5B25B492AFBCCCC00BE8743 /* Direction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Direction.swift; path = ../../../Sources/AndroidTVRemoteControl/Commands/Direction.swift; sourceTree = ""; }; 74 | C5B25B4A2AFBCCCC00BE8743 /* Key.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Key.swift; path = ../../../Sources/AndroidTVRemoteControl/Commands/Key.swift; sourceTree = ""; }; 75 | C5B25B4B2AFBCCCC00BE8743 /* DeepLink.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DeepLink.swift; path = ../../../Sources/AndroidTVRemoteControl/Commands/DeepLink.swift; sourceTree = ""; }; 76 | C5B25B4C2AFBCCCC00BE8743 /* KeyPress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = KeyPress.swift; path = ../../../Sources/AndroidTVRemoteControl/Commands/KeyPress.swift; sourceTree = ""; }; 77 | C5D3739E2AE2EE4500BE7F37 /* Views.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Views.swift; sourceTree = ""; }; 78 | C5E26BD42B2025D4007D7E08 /* Result.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Result.swift; path = ../../../Sources/AndroidTVRemoteControl/misc/Result.swift; sourceTree = ""; }; 79 | C5E26BD52B2025D4007D7E08 /* Logger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Logger.swift; path = ../../../Sources/AndroidTVRemoteControl/misc/Logger.swift; sourceTree = ""; }; 80 | C5E26BD92B2F33A6007D7E08 /* Decoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Decoder.swift; path = ../../../Sources/AndroidTVRemoteControl/coding/Decoder.swift; sourceTree = ""; }; 81 | C5E26BDA2B2F33A6007D7E08 /* Encoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Encoder.swift; path = ../../../Sources/AndroidTVRemoteControl/coding/Encoder.swift; sourceTree = ""; }; 82 | C5EFFD172B0B9B0B00B35F8B /* VolumeLevel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = VolumeLevel.swift; path = ../../../Sources/AndroidTVRemoteControl/Network/CommandNetwork/VolumeLevel.swift; sourceTree = ""; }; 83 | /* End PBXFileReference section */ 84 | 85 | /* Begin PBXFrameworksBuildPhase section */ 86 | C591B9652AE1CEED00A50131 /* Frameworks */ = { 87 | isa = PBXFrameworksBuildPhase; 88 | buildActionMask = 2147483647; 89 | files = ( 90 | ); 91 | runOnlyForDeploymentPostprocessing = 0; 92 | }; 93 | /* End PBXFrameworksBuildPhase section */ 94 | 95 | /* Begin PBXGroup section */ 96 | C591B95F2AE1CEED00A50131 = { 97 | isa = PBXGroup; 98 | children = ( 99 | C591B96A2AE1CEED00A50131 /* Demo */, 100 | C591B9692AE1CEED00A50131 /* Products */, 101 | ); 102 | sourceTree = ""; 103 | }; 104 | C591B9692AE1CEED00A50131 /* Products */ = { 105 | isa = PBXGroup; 106 | children = ( 107 | C591B9682AE1CEED00A50131 /* Demo.app */, 108 | ); 109 | name = Products; 110 | sourceTree = ""; 111 | }; 112 | C591B96A2AE1CEED00A50131 /* Demo */ = { 113 | isa = PBXGroup; 114 | children = ( 115 | C591B9B02AE23C4700A50131 /* AndroidTVRemoteControl */, 116 | C591B96B2AE1CEED00A50131 /* AppDelegate.swift */, 117 | C591B96D2AE1CEED00A50131 /* SceneDelegate.swift */, 118 | C5D3739E2AE2EE4500BE7F37 /* Views.swift */, 119 | C591B96F2AE1CEED00A50131 /* ViewController.swift */, 120 | C591B97F2AE2356800A50131 /* RemoteTVManager.swift */, 121 | C591B9742AE1CEEE00A50131 /* Assets.xcassets */, 122 | C591B9D82AE23E7F00A50131 /* cert.der */, 123 | C591B9D92AE23E7F00A50131 /* cert.p12 */, 124 | C591B9792AE1CEEE00A50131 /* Info.plist */, 125 | ); 126 | path = Demo; 127 | sourceTree = ""; 128 | }; 129 | C591B9B02AE23C4700A50131 /* AndroidTVRemoteControl */ = { 130 | isa = PBXGroup; 131 | children = ( 132 | C5E26BD82B2F3381007D7E08 /* coding */, 133 | C5E26BD32B2025B2007D7E08 /* misc */, 134 | C5B25B482AFBCCA700BE8743 /* Commands */, 135 | C591B9BF2AE23C8D00A50131 /* Network */, 136 | C591B9B62AE23C7A00A50131 /* CertManager.swift */, 137 | C591B9B42AE23C7A00A50131 /* CryptoManager.swift */, 138 | C591B9B12AE23C7A00A50131 /* Errors.swift */, 139 | C591B9B32AE23C7A00A50131 /* PairingManager.swift */, 140 | C591B9B72AE23C7A00A50131 /* RemoteManager.swift */, 141 | C591B9B22AE23C7A00A50131 /* TLSManager.swift */, 142 | ); 143 | name = AndroidTVRemoteControl; 144 | sourceTree = ""; 145 | }; 146 | C591B9BF2AE23C8D00A50131 /* Network */ = { 147 | isa = PBXGroup; 148 | children = ( 149 | C591B9CD2AE23CC100A50131 /* CommandNetwork */, 150 | C591B9C22AE23CA700A50131 /* PairingNetwork */, 151 | C591B9C02AE23CA300A50131 /* RequestDataProtocol.swift */, 152 | ); 153 | name = Network; 154 | sourceTree = ""; 155 | }; 156 | C591B9C22AE23CA700A50131 /* PairingNetwork */ = { 157 | isa = PBXGroup; 158 | children = ( 159 | C591B9C32AE23CBE00A50131 /* Configuration.swift */, 160 | C591B9C42AE23CBE00A50131 /* Option.swift */, 161 | C591B9C52AE23CBE00A50131 /* Pairing.swift */, 162 | C591B9C62AE23CBE00A50131 /* PairingNetwork.swift */, 163 | C591B9C72AE23CBE00A50131 /* Secret.swift */, 164 | ); 165 | name = PairingNetwork; 166 | sourceTree = ""; 167 | }; 168 | C591B9CD2AE23CC100A50131 /* CommandNetwork */ = { 169 | isa = PBXGroup; 170 | children = ( 171 | C5EFFD172B0B9B0B00B35F8B /* VolumeLevel.swift */, 172 | C591B9D22AE23CD600A50131 /* AndroidTVConfigurationMessage.swift */, 173 | C591B9CE2AE23CD600A50131 /* CommandNetwork.swift */, 174 | C591B9CF2AE23CD600A50131 /* Ping.swift */, 175 | C591B9D02AE23CD600A50131 /* SecodConfiguration.swift */, 176 | C591B9D12AE23CD600A50131 /* SecondConfigurationResponse.swift */, 177 | ); 178 | name = CommandNetwork; 179 | sourceTree = ""; 180 | }; 181 | C5B25B482AFBCCA700BE8743 /* Commands */ = { 182 | isa = PBXGroup; 183 | children = ( 184 | C5B25B4B2AFBCCCC00BE8743 /* DeepLink.swift */, 185 | C5B25B492AFBCCCC00BE8743 /* Direction.swift */, 186 | C5B25B4A2AFBCCCC00BE8743 /* Key.swift */, 187 | C5B25B4C2AFBCCCC00BE8743 /* KeyPress.swift */, 188 | ); 189 | name = Commands; 190 | sourceTree = ""; 191 | }; 192 | C5E26BD32B2025B2007D7E08 /* misc */ = { 193 | isa = PBXGroup; 194 | children = ( 195 | C5E26BD52B2025D4007D7E08 /* Logger.swift */, 196 | C5E26BD42B2025D4007D7E08 /* Result.swift */, 197 | ); 198 | name = misc; 199 | sourceTree = ""; 200 | }; 201 | C5E26BD82B2F3381007D7E08 /* coding */ = { 202 | isa = PBXGroup; 203 | children = ( 204 | C5E26BD92B2F33A6007D7E08 /* Decoder.swift */, 205 | C5E26BDA2B2F33A6007D7E08 /* Encoder.swift */, 206 | ); 207 | name = coding; 208 | sourceTree = ""; 209 | }; 210 | /* End PBXGroup section */ 211 | 212 | /* Begin PBXNativeTarget section */ 213 | C591B9672AE1CEED00A50131 /* Demo */ = { 214 | isa = PBXNativeTarget; 215 | buildConfigurationList = C591B97C2AE1CEEE00A50131 /* Build configuration list for PBXNativeTarget "Demo" */; 216 | buildPhases = ( 217 | C591B9642AE1CEED00A50131 /* Sources */, 218 | C591B9652AE1CEED00A50131 /* Frameworks */, 219 | C591B9662AE1CEED00A50131 /* Resources */, 220 | ); 221 | buildRules = ( 222 | ); 223 | dependencies = ( 224 | ); 225 | name = Demo; 226 | productName = Demo; 227 | productReference = C591B9682AE1CEED00A50131 /* Demo.app */; 228 | productType = "com.apple.product-type.application"; 229 | }; 230 | /* End PBXNativeTarget section */ 231 | 232 | /* Begin PBXProject section */ 233 | C591B9602AE1CEED00A50131 /* Project object */ = { 234 | isa = PBXProject; 235 | attributes = { 236 | BuildIndependentTargetsInParallel = 1; 237 | LastSwiftUpdateCheck = 1430; 238 | LastUpgradeCheck = 1430; 239 | TargetAttributes = { 240 | C591B9672AE1CEED00A50131 = { 241 | CreatedOnToolsVersion = 14.3; 242 | }; 243 | }; 244 | }; 245 | buildConfigurationList = C591B9632AE1CEED00A50131 /* Build configuration list for PBXProject "Demo" */; 246 | compatibilityVersion = "Xcode 14.0"; 247 | developmentRegion = en; 248 | hasScannedForEncodings = 0; 249 | knownRegions = ( 250 | en, 251 | Base, 252 | ); 253 | mainGroup = C591B95F2AE1CEED00A50131; 254 | productRefGroup = C591B9692AE1CEED00A50131 /* Products */; 255 | projectDirPath = ""; 256 | projectRoot = ""; 257 | targets = ( 258 | C591B9672AE1CEED00A50131 /* Demo */, 259 | ); 260 | }; 261 | /* End PBXProject section */ 262 | 263 | /* Begin PBXResourcesBuildPhase section */ 264 | C591B9662AE1CEED00A50131 /* Resources */ = { 265 | isa = PBXResourcesBuildPhase; 266 | buildActionMask = 2147483647; 267 | files = ( 268 | C591B9DA2AE23E7F00A50131 /* cert.der in Resources */, 269 | C591B9752AE1CEEE00A50131 /* Assets.xcassets in Resources */, 270 | C591B9DB2AE23E7F00A50131 /* cert.p12 in Resources */, 271 | ); 272 | runOnlyForDeploymentPostprocessing = 0; 273 | }; 274 | /* End PBXResourcesBuildPhase section */ 275 | 276 | /* Begin PBXSourcesBuildPhase section */ 277 | C591B9642AE1CEED00A50131 /* Sources */ = { 278 | isa = PBXSourcesBuildPhase; 279 | buildActionMask = 2147483647; 280 | files = ( 281 | C591B9D62AE23CD600A50131 /* SecondConfigurationResponse.swift in Sources */, 282 | C591B9702AE1CEED00A50131 /* ViewController.swift in Sources */, 283 | C5B25B4F2AFBCCCC00BE8743 /* DeepLink.swift in Sources */, 284 | C591B9C92AE23CBE00A50131 /* Option.swift in Sources */, 285 | C5D3739F2AE2EE4500BE7F37 /* Views.swift in Sources */, 286 | C5E26BD72B2025D4007D7E08 /* Logger.swift in Sources */, 287 | C591B9CB2AE23CBE00A50131 /* PairingNetwork.swift in Sources */, 288 | C591B9BE2AE23C7A00A50131 /* RemoteManager.swift in Sources */, 289 | C591B9B92AE23C7A00A50131 /* TLSManager.swift in Sources */, 290 | C591B9D32AE23CD600A50131 /* CommandNetwork.swift in Sources */, 291 | C591B9CA2AE23CBE00A50131 /* Pairing.swift in Sources */, 292 | C5B25B502AFBCCCC00BE8743 /* KeyPress.swift in Sources */, 293 | C591B9D42AE23CD600A50131 /* Ping.swift in Sources */, 294 | C5B25B4D2AFBCCCC00BE8743 /* Direction.swift in Sources */, 295 | C591B9BB2AE23C7A00A50131 /* CryptoManager.swift in Sources */, 296 | C591B9C12AE23CA300A50131 /* RequestDataProtocol.swift in Sources */, 297 | C591B9B82AE23C7A00A50131 /* Errors.swift in Sources */, 298 | C591B9CC2AE23CBE00A50131 /* Secret.swift in Sources */, 299 | C5B25B4E2AFBCCCC00BE8743 /* Key.swift in Sources */, 300 | C591B9D52AE23CD600A50131 /* SecodConfiguration.swift in Sources */, 301 | C591B9C82AE23CBE00A50131 /* Configuration.swift in Sources */, 302 | C591B9D72AE23CD600A50131 /* AndroidTVConfigurationMessage.swift in Sources */, 303 | C591B9BD2AE23C7A00A50131 /* CertManager.swift in Sources */, 304 | C5E26BDC2B2F33A6007D7E08 /* Encoder.swift in Sources */, 305 | C591B9BA2AE23C7A00A50131 /* PairingManager.swift in Sources */, 306 | C5E26BD62B2025D4007D7E08 /* Result.swift in Sources */, 307 | C5EFFD182B0B9B0B00B35F8B /* VolumeLevel.swift in Sources */, 308 | C591B9802AE2356800A50131 /* RemoteTVManager.swift in Sources */, 309 | C591B96C2AE1CEED00A50131 /* AppDelegate.swift in Sources */, 310 | C5E26BDB2B2F33A6007D7E08 /* Decoder.swift in Sources */, 311 | C591B96E2AE1CEED00A50131 /* SceneDelegate.swift in Sources */, 312 | ); 313 | runOnlyForDeploymentPostprocessing = 0; 314 | }; 315 | /* End PBXSourcesBuildPhase section */ 316 | 317 | /* Begin XCBuildConfiguration section */ 318 | C591B97A2AE1CEEE00A50131 /* Debug */ = { 319 | isa = XCBuildConfiguration; 320 | buildSettings = { 321 | ALWAYS_SEARCH_USER_PATHS = NO; 322 | CLANG_ANALYZER_NONNULL = YES; 323 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 324 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 325 | CLANG_ENABLE_MODULES = YES; 326 | CLANG_ENABLE_OBJC_ARC = YES; 327 | CLANG_ENABLE_OBJC_WEAK = YES; 328 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 329 | CLANG_WARN_BOOL_CONVERSION = YES; 330 | CLANG_WARN_COMMA = YES; 331 | CLANG_WARN_CONSTANT_CONVERSION = YES; 332 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 333 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 334 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 335 | CLANG_WARN_EMPTY_BODY = YES; 336 | CLANG_WARN_ENUM_CONVERSION = YES; 337 | CLANG_WARN_INFINITE_RECURSION = YES; 338 | CLANG_WARN_INT_CONVERSION = YES; 339 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 340 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 341 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 342 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 343 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 344 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 345 | CLANG_WARN_STRICT_PROTOTYPES = YES; 346 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 347 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 348 | CLANG_WARN_UNREACHABLE_CODE = YES; 349 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 350 | COPY_PHASE_STRIP = NO; 351 | DEBUG_INFORMATION_FORMAT = dwarf; 352 | ENABLE_STRICT_OBJC_MSGSEND = YES; 353 | ENABLE_TESTABILITY = YES; 354 | GCC_C_LANGUAGE_STANDARD = gnu11; 355 | GCC_DYNAMIC_NO_PIC = NO; 356 | GCC_NO_COMMON_BLOCKS = YES; 357 | GCC_OPTIMIZATION_LEVEL = 0; 358 | GCC_PREPROCESSOR_DEFINITIONS = ( 359 | "DEBUG=1", 360 | "$(inherited)", 361 | ); 362 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 363 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 364 | GCC_WARN_UNDECLARED_SELECTOR = YES; 365 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 366 | GCC_WARN_UNUSED_FUNCTION = YES; 367 | GCC_WARN_UNUSED_VARIABLE = YES; 368 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 369 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 370 | MTL_FAST_MATH = YES; 371 | ONLY_ACTIVE_ARCH = YES; 372 | SDKROOT = iphoneos; 373 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 374 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 375 | }; 376 | name = Debug; 377 | }; 378 | C591B97B2AE1CEEE00A50131 /* Release */ = { 379 | isa = XCBuildConfiguration; 380 | buildSettings = { 381 | ALWAYS_SEARCH_USER_PATHS = NO; 382 | CLANG_ANALYZER_NONNULL = YES; 383 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 384 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 385 | CLANG_ENABLE_MODULES = YES; 386 | CLANG_ENABLE_OBJC_ARC = YES; 387 | CLANG_ENABLE_OBJC_WEAK = YES; 388 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 389 | CLANG_WARN_BOOL_CONVERSION = YES; 390 | CLANG_WARN_COMMA = YES; 391 | CLANG_WARN_CONSTANT_CONVERSION = YES; 392 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 393 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 394 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 395 | CLANG_WARN_EMPTY_BODY = YES; 396 | CLANG_WARN_ENUM_CONVERSION = YES; 397 | CLANG_WARN_INFINITE_RECURSION = YES; 398 | CLANG_WARN_INT_CONVERSION = YES; 399 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 400 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 401 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 402 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 403 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 404 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 405 | CLANG_WARN_STRICT_PROTOTYPES = YES; 406 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 407 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 408 | CLANG_WARN_UNREACHABLE_CODE = YES; 409 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 410 | COPY_PHASE_STRIP = NO; 411 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 412 | ENABLE_NS_ASSERTIONS = NO; 413 | ENABLE_STRICT_OBJC_MSGSEND = YES; 414 | GCC_C_LANGUAGE_STANDARD = gnu11; 415 | GCC_NO_COMMON_BLOCKS = YES; 416 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 417 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 418 | GCC_WARN_UNDECLARED_SELECTOR = YES; 419 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 420 | GCC_WARN_UNUSED_FUNCTION = YES; 421 | GCC_WARN_UNUSED_VARIABLE = YES; 422 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 423 | MTL_ENABLE_DEBUG_INFO = NO; 424 | MTL_FAST_MATH = YES; 425 | SDKROOT = iphoneos; 426 | SWIFT_COMPILATION_MODE = wholemodule; 427 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 428 | VALIDATE_PRODUCT = YES; 429 | }; 430 | name = Release; 431 | }; 432 | C591B97D2AE1CEEE00A50131 /* Debug */ = { 433 | isa = XCBuildConfiguration; 434 | buildSettings = { 435 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 436 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 437 | CODE_SIGN_STYLE = Automatic; 438 | CURRENT_PROJECT_VERSION = 1; 439 | GENERATE_INFOPLIST_FILE = YES; 440 | INFOPLIST_FILE = Demo/Info.plist; 441 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 442 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 443 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; 444 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; 445 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 446 | LD_RUNPATH_SEARCH_PATHS = ( 447 | "$(inherited)", 448 | "@executable_path/Frameworks", 449 | ); 450 | MARKETING_VERSION = 1.0; 451 | PRODUCT_BUNDLE_IDENTIFIER = AndroidTVRemoteControl.Demo; 452 | PRODUCT_NAME = "$(TARGET_NAME)"; 453 | SWIFT_EMIT_LOC_STRINGS = YES; 454 | SWIFT_VERSION = 5.0; 455 | TARGETED_DEVICE_FAMILY = "1,2"; 456 | }; 457 | name = Debug; 458 | }; 459 | C591B97E2AE1CEEE00A50131 /* Release */ = { 460 | isa = XCBuildConfiguration; 461 | buildSettings = { 462 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 463 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 464 | CODE_SIGN_STYLE = Automatic; 465 | CURRENT_PROJECT_VERSION = 1; 466 | GENERATE_INFOPLIST_FILE = YES; 467 | INFOPLIST_FILE = Demo/Info.plist; 468 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 469 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 470 | INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; 471 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; 472 | IPHONEOS_DEPLOYMENT_TARGET = 14.0; 473 | LD_RUNPATH_SEARCH_PATHS = ( 474 | "$(inherited)", 475 | "@executable_path/Frameworks", 476 | ); 477 | MARKETING_VERSION = 1.0; 478 | PRODUCT_BUNDLE_IDENTIFIER = AndroidTVRemoteControl.Demo; 479 | PRODUCT_NAME = "$(TARGET_NAME)"; 480 | SWIFT_EMIT_LOC_STRINGS = YES; 481 | SWIFT_VERSION = 5.0; 482 | TARGETED_DEVICE_FAMILY = "1,2"; 483 | }; 484 | name = Release; 485 | }; 486 | /* End XCBuildConfiguration section */ 487 | 488 | /* Begin XCConfigurationList section */ 489 | C591B9632AE1CEED00A50131 /* Build configuration list for PBXProject "Demo" */ = { 490 | isa = XCConfigurationList; 491 | buildConfigurations = ( 492 | C591B97A2AE1CEEE00A50131 /* Debug */, 493 | C591B97B2AE1CEEE00A50131 /* Release */, 494 | ); 495 | defaultConfigurationIsVisible = 0; 496 | defaultConfigurationName = Release; 497 | }; 498 | C591B97C2AE1CEEE00A50131 /* Build configuration list for PBXNativeTarget "Demo" */ = { 499 | isa = XCConfigurationList; 500 | buildConfigurations = ( 501 | C591B97D2AE1CEEE00A50131 /* Debug */, 502 | C591B97E2AE1CEEE00A50131 /* Release */, 503 | ); 504 | defaultConfigurationIsVisible = 0; 505 | defaultConfigurationName = Release; 506 | }; 507 | /* End XCConfigurationList section */ 508 | }; 509 | rootObject = C591B9602AE1CEED00A50131 /* Project object */; 510 | } 511 | -------------------------------------------------------------------------------- /Demo/Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Demo/Demo/Demo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Demo/Demo/Demo.xcodeproj/xcuserdata/romanodyshew.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Demo.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Demo/Demo/Demo/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Demo 4 | // 5 | // Created by Roman Odyshew on 20.10.2023. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 14 | // Override point for customization after application launch. 15 | return true 16 | } 17 | 18 | // MARK: UISceneSession Lifecycle 19 | 20 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 21 | // Called when a new scene session is being created. 22 | // Use this method to select a configuration to create the new scene with. 23 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 24 | } 25 | 26 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 27 | // Called when the user discards a scene session. 28 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 29 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Demo/Demo/Demo/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Demo/Demo/Demo/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Demo/Demo/Demo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Demo/Demo/Demo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Demo/Demo/Demo/RemoteTVManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RemoteTVManager.swift 3 | // Demo 4 | // 5 | // Created by Roman Odyshew on 20.10.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | class RemoteTVManager { 11 | private let queue = DispatchQueue(label: "queue") 12 | 13 | private let pairingManager: PairingManager 14 | private let remoteManager: RemoteManager 15 | 16 | public var pairingStateChanged: ((String)->())? 17 | public var remoteStateChanged: ((String)->())? 18 | 19 | init() { 20 | let cryptoManager = CryptoManager() 21 | 22 | cryptoManager.clientPublicCertificate = { 23 | guard let url = Bundle.main.url(forResource: "cert", withExtension: "der") else { 24 | return .Error(.loadCertFromURLError(MyError.certNotFound)) 25 | } 26 | 27 | return CertManager().getSecKey(url) 28 | } 29 | 30 | let tlsManager = TLSManager { 31 | guard let url = Bundle.main.url(forResource: "cert", withExtension: "p12") else { 32 | return .Error(.loadCertFromURLError(MyError.certNotFound)) 33 | } 34 | 35 | return CertManager().cert(url, "") 36 | } 37 | 38 | tlsManager.secTrustClosure = { secTrust in 39 | cryptoManager.serverPublicCertificate = { 40 | if #available(iOS 14.0, *) { 41 | guard let key = SecTrustCopyKey(secTrust) else { 42 | return .Error(.secTrustCopyKeyError) 43 | } 44 | return .Result(key) 45 | } else { 46 | guard let key = SecTrustCopyPublicKey(secTrust) else { 47 | return .Error(.secTrustCopyKeyError) 48 | } 49 | return .Result(key) 50 | } 51 | } 52 | } 53 | 54 | pairingManager = PairingManager(tlsManager, cryptoManager, DefaultLogger()) 55 | remoteManager = RemoteManager(tlsManager, CommandNetwork.DeviceInfo("client", "iPhone", "1.0.0", "example_app", "235"), DefaultLogger()) 56 | } 57 | 58 | func connect(host: String) { 59 | queue.async { 60 | self.remoteManager.stateChanged = { [weak self] remoteState in 61 | self?.remoteStateChanged?(remoteState.toString()) 62 | 63 | if case .error(.connectionWaitingError) = remoteState { 64 | self?.pairingManager.stateChanged = { pairingState in 65 | self?.pairingStateChanged?(pairingState.toString()) 66 | 67 | if case .successPaired = pairingState { 68 | self?.remoteManager.connect(host) 69 | } 70 | } 71 | 72 | self?.pairingManager.connect(host, "client", "iPhone") 73 | } 74 | } 75 | 76 | self.remoteManager.connect(host) 77 | } 78 | } 79 | 80 | func sendCode(code: String) { 81 | queue.async { 82 | self.pairingManager.sendSecret(code) 83 | } 84 | } 85 | 86 | func runNetflix() { 87 | queue.async { 88 | self.remoteManager.send(DeepLink("https://www.netflix.com/title")) 89 | } 90 | } 91 | 92 | func volUp() { 93 | queue.async { 94 | self.remoteManager.send(KeyPress(.KEYCODE_VOLUME_UP)) 95 | } 96 | } 97 | 98 | func volDown() { 99 | queue.async { 100 | self.remoteManager.send(KeyPress(.KEYCODE_VOLUME_DOWN)) 101 | } 102 | } 103 | } 104 | 105 | public enum MyError: Error { 106 | case certNotFound 107 | } 108 | 109 | extension RemoteManager.RemoteState { 110 | func toString() -> String { 111 | switch self { 112 | case .idle: 113 | return "idle" 114 | case .connectionSetUp: 115 | return "connection Set Up" 116 | case .connectionPrepairing: 117 | return "connection Prepairing" 118 | case .connected: 119 | return "connected" 120 | case .fisrtConfigMessageReceived(let info): 121 | return "fisrt Config Message Received: vendor: \(info.vendor) model: \(info.model)" 122 | case .firstConfigSent: 123 | return "first Config Sent" 124 | case .secondConfigSent: 125 | return "second Config Sent" 126 | case .paired(let runningApp): 127 | return "Paired! Current runned app " + (runningApp ?? "") 128 | case .error(let error): 129 | return "Error: " + error.toString() 130 | } 131 | } 132 | } 133 | 134 | extension PairingManager.PairingState { 135 | func toString() -> String { 136 | switch self { 137 | case .idle: 138 | return "idle" 139 | case .extractTLSparams: 140 | return "Extract TLS params" 141 | case .connectionSetUp: 142 | return "Connection Set Up" 143 | case .connectionPrepairing: 144 | return "Connection Prepairing" 145 | case .connected: 146 | return "Connected" 147 | case .pairingRequestSent: 148 | return "Pairing Request Sent" 149 | case .pairingResponseSuccess: 150 | return "Pairing Response Success" 151 | case .optionRequestSent: 152 | return "Option Request Sent" 153 | case .optionResponseSuccess: 154 | return "Option Response Success" 155 | case .confirmationRequestSent: 156 | return "Confirmation Request Sent" 157 | case .confirmationResponseSuccess: 158 | return "Confirmation Response Success" 159 | case .waitingCode: 160 | return "Waiting Code" 161 | case .secretSent: 162 | return "Secret Sent" 163 | case .successPaired: 164 | return "Success Paired" 165 | case .error(let error): 166 | return "Error: " + error.toString() 167 | } 168 | } 169 | } 170 | 171 | extension AndroidTVRemoteControlError { 172 | func toString() -> String { 173 | switch self { 174 | case .unexpectedCertData: 175 | return "unexpected Cert Data" 176 | case .extractCFTypeRefError: 177 | return "extract CFTypeRef Error" 178 | case .secIdentityCreateError: 179 | return "sec Identity Create Error" 180 | case .toLongNames(let description): 181 | return "to Long Names" + description 182 | case .connectionCanceled: 183 | return "connection Canceled" 184 | case .pairingNotSuccess: 185 | return "pairing Not Success" 186 | case .optionNotSuccess: 187 | return "option Not Success" 188 | case .configurationNotSuccess: 189 | return "configuration Not Success" 190 | case .secretNotSuccess: 191 | return "secret Not Success" 192 | case .connectionWaitingError(let error): 193 | return "connection Waiting Error: " + error.localizedDescription 194 | case .connectionFailed: 195 | return "connection Failed" 196 | case .receiveDataError: 197 | return "receive Data Error" 198 | case .sendDataError: 199 | return "send Data Error" 200 | case .invalidCode(let description): 201 | return "invalid Code " + description 202 | case .wrongCode: 203 | return "wrong Code" 204 | case .noSecAttributes: 205 | return "no SecAttributes" 206 | case .notRSAKey: 207 | return "not RSA Key" 208 | case .notPublicKey: 209 | return "not Public Key" 210 | case .noKeySizeAttribute: 211 | return "no Key Size Attribute" 212 | case .noValueData: 213 | return "no Value Data" 214 | case .invalidCertData: 215 | return "invalid Cert Data" 216 | case .createCertFromDataError: 217 | return "create Cert From Data Error" 218 | case .noClientPublicCertificate: 219 | return "no Client Public Certificate" 220 | case .noServerPublicCertificate: 221 | return "no Server Public Certificate" 222 | case .secTrustCopyKeyError: 223 | return "sec Trust Copy Key Error" 224 | case .loadCertFromURLError: 225 | return "load Cert From URL Error" 226 | case .secPKCS12ImportNotSuccess: 227 | return "secPKCS12Import Not Success" 228 | case .createTrustObjectError: 229 | return "create Trust Object Error" 230 | case .secTrustCreateWithCertificatesNotSuccess: 231 | return "secTrust Create With Certificates Not Success" 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /Demo/Demo/Demo/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Demo 4 | // 5 | // Created by Roman Odyshew on 20.10.2023. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 15 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 16 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 17 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 18 | guard let windowScene = (scene as? UIWindowScene) else { return } 19 | window = UIWindow(windowScene: windowScene) 20 | window?.rootViewController = ViewController() 21 | window?.makeKeyAndVisible() 22 | } 23 | 24 | func sceneDidDisconnect(_ scene: UIScene) { 25 | // Called as the scene is being released by the system. 26 | // This occurs shortly after the scene enters the background, or when its session is discarded. 27 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 28 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 29 | } 30 | 31 | func sceneDidBecomeActive(_ scene: UIScene) { 32 | // Called when the scene has moved from an inactive state to an active state. 33 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 34 | } 35 | 36 | func sceneWillResignActive(_ scene: UIScene) { 37 | // Called when the scene will move from an active state to an inactive state. 38 | // This may occur due to temporary interruptions (ex. an incoming phone call). 39 | } 40 | 41 | func sceneWillEnterForeground(_ scene: UIScene) { 42 | // Called as the scene transitions from the background to the foreground. 43 | // Use this method to undo the changes made on entering the background. 44 | } 45 | 46 | func sceneDidEnterBackground(_ scene: UIScene) { 47 | // Called as the scene transitions from the foreground to the background. 48 | // Use this method to save data, release shared resources, and store enough scene-specific state information 49 | // to restore the scene back to its current state. 50 | } 51 | 52 | 53 | } 54 | 55 | -------------------------------------------------------------------------------- /Demo/Demo/Demo/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Demo 4 | // 5 | // Created by Roman Odyshew on 20.10.2023. 6 | // 7 | 8 | import UIKit 9 | 10 | class ViewController: UIViewController { 11 | private let views = Views() 12 | private let remoteManager = RemoteTVManager() 13 | 14 | init() { 15 | super.init(nibName: nil, bundle: nil) 16 | 17 | remoteManager.pairingStateChanged = { [weak self] state in 18 | DispatchQueue.main.async { 19 | self?.views.pairingStateLabel.text = "Pairing state: " + state 20 | self?.views.sendCodeButton.isEnabled = state == "Waiting Code" 21 | } 22 | } 23 | 24 | remoteManager.remoteStateChanged = { [weak self] state in 25 | DispatchQueue.main.async { 26 | self?.views.remoteStateLabel.text = "Remote state: " + state 27 | } 28 | } 29 | } 30 | 31 | required init?(coder: NSCoder) { 32 | fatalError("init(coder:) has not been implemented") 33 | } 34 | 35 | override func viewDidLoad() { 36 | super.viewDidLoad() 37 | 38 | views.viewDidLoad(view) 39 | views.sendCodeButton.isEnabled = false 40 | views.connectButton.addTarget(self, action: #selector(connect), for: .touchUpInside) 41 | views.sendCodeButton.addTarget(self, action: #selector(sendCode), for: .touchUpInside) 42 | views.runNetflixButton.addTarget(self, action: #selector(runNetflix), for: .touchUpInside) 43 | views.volUpButton.addTarget(self, action: #selector(volUp), for: .touchUpInside) 44 | views.volDownButton.addTarget(self, action: #selector(volDown), for: .touchUpInside) 45 | } 46 | 47 | @objc private func connect() { 48 | // set your Android TV device ip 49 | remoteManager.connect(host: "") 50 | } 51 | 52 | @objc private func volUp() { 53 | remoteManager.volUp() 54 | } 55 | 56 | @objc private func volDown() { 57 | remoteManager.volDown() 58 | } 59 | 60 | @objc private func sendCode() { 61 | guard let code = views.codeTextField.text else { 62 | return 63 | } 64 | remoteManager.sendCode(code: code) 65 | } 66 | 67 | @objc private func runNetflix() { 68 | remoteManager.runNetflix() 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Demo/Demo/Demo/Views.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Views.swift 3 | // Demo 4 | // 5 | // Created by Roman Odyshew on 21.10.2023. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | extension ViewController { 12 | class Views { 13 | let connectButton = UIButton() 14 | let codeTextField = UITextField() 15 | let sendCodeButton = UIButton() 16 | let runNetflixButton = UIButton() 17 | 18 | let volUpButton = UIButton() 19 | let volDownButton = UIButton() 20 | 21 | let pairingStateLabel = UILabel() 22 | let remoteStateLabel = UILabel() 23 | 24 | func viewDidLoad(_ view: UIView) { 25 | // Do any additional setup after loading the view. 26 | view.backgroundColor = .white 27 | view.addSubview(pairingStateLabel) 28 | view.addSubview(remoteStateLabel) 29 | view.addSubview(connectButton) 30 | view.addSubview(codeTextField) 31 | view.addSubview(sendCodeButton) 32 | view.addSubview(runNetflixButton) 33 | view.addSubview(volUpButton) 34 | view.addSubview(volDownButton) 35 | 36 | pairingStateLabel.numberOfLines = 0 37 | remoteStateLabel.numberOfLines = 0 38 | 39 | pairingStateLabel.translatesAutoresizingMaskIntoConstraints = false 40 | remoteStateLabel.translatesAutoresizingMaskIntoConstraints = false 41 | connectButton.translatesAutoresizingMaskIntoConstraints = false 42 | codeTextField.translatesAutoresizingMaskIntoConstraints = false 43 | sendCodeButton.translatesAutoresizingMaskIntoConstraints = false 44 | runNetflixButton.translatesAutoresizingMaskIntoConstraints = false 45 | volUpButton.translatesAutoresizingMaskIntoConstraints = false 46 | volDownButton.translatesAutoresizingMaskIntoConstraints = false 47 | 48 | pairingStateLabel.textAlignment = .center 49 | remoteStateLabel.textAlignment = .center 50 | 51 | connectButton.backgroundColor = UIColor.gray.withAlphaComponent(0.7) 52 | connectButton.layer.cornerRadius = 8 53 | sendCodeButton.backgroundColor = UIColor.gray.withAlphaComponent(0.7) 54 | sendCodeButton.layer.cornerRadius = 8 55 | runNetflixButton.backgroundColor = UIColor.gray.withAlphaComponent(0.7) 56 | runNetflixButton.layer.cornerRadius = 8 57 | volUpButton.backgroundColor = UIColor.gray.withAlphaComponent(0.7) 58 | volUpButton.layer.cornerRadius = 8 59 | volDownButton.backgroundColor = UIColor.gray.withAlphaComponent(0.7) 60 | volDownButton.layer.cornerRadius = 8 61 | 62 | codeTextField.layer.borderWidth = 2.0 63 | codeTextField.layer.borderColor = UIColor.darkGray.cgColor 64 | 65 | NSLayoutConstraint.activate([ 66 | pairingStateLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 100), 67 | pairingStateLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), 68 | pairingStateLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 10), 69 | pairingStateLabel.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -10), 70 | 71 | remoteStateLabel.topAnchor.constraint(equalTo: pairingStateLabel.bottomAnchor, constant: 30), 72 | remoteStateLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), 73 | remoteStateLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 10), 74 | remoteStateLabel.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -10), 75 | 76 | connectButton.topAnchor.constraint(equalTo: remoteStateLabel.bottomAnchor, constant: 30), 77 | connectButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), 78 | connectButton.heightAnchor.constraint(equalToConstant: 40), 79 | connectButton.widthAnchor.constraint(equalToConstant: 120), 80 | 81 | codeTextField.topAnchor.constraint(equalTo: connectButton.bottomAnchor, constant: 30), 82 | codeTextField.centerXAnchor.constraint(equalTo: view.centerXAnchor), 83 | codeTextField.heightAnchor.constraint(equalToConstant: 35), 84 | codeTextField.widthAnchor.constraint(equalToConstant: 100), 85 | 86 | sendCodeButton.topAnchor.constraint(equalTo: codeTextField.bottomAnchor, constant: 30), 87 | sendCodeButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), 88 | sendCodeButton.heightAnchor.constraint(equalToConstant: 40), 89 | sendCodeButton.widthAnchor.constraint(equalToConstant: 120), 90 | 91 | runNetflixButton.topAnchor.constraint(equalTo: sendCodeButton.bottomAnchor, constant: 30), 92 | runNetflixButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), 93 | runNetflixButton.heightAnchor.constraint(equalToConstant: 40), 94 | runNetflixButton.widthAnchor.constraint(equalToConstant: 120), 95 | 96 | volUpButton.centerYAnchor.constraint(equalTo: runNetflixButton.centerYAnchor), 97 | volUpButton.leftAnchor.constraint(equalTo: runNetflixButton.rightAnchor, constant: 15), 98 | volUpButton.heightAnchor.constraint(equalToConstant: 40), 99 | volUpButton.widthAnchor.constraint(equalToConstant: 80), 100 | 101 | volDownButton.centerYAnchor.constraint(equalTo: runNetflixButton.centerYAnchor), 102 | volDownButton.rightAnchor.constraint(equalTo: runNetflixButton.leftAnchor, constant: -15), 103 | volDownButton.heightAnchor.constraint(equalToConstant: 40), 104 | volDownButton.widthAnchor.constraint(equalToConstant: 80), 105 | ]) 106 | 107 | connectButton.setTitle("Connect", for: .normal) 108 | codeTextField.placeholder = "Enter Code" 109 | sendCodeButton.setTitle("Send Code", for: .normal) 110 | 111 | sendCodeButton.setTitleColor(.gray, for: .disabled) 112 | sendCodeButton.setTitleColor(.white, for: .normal) 113 | runNetflixButton.setTitle("Run Netflix", for: .normal) 114 | volUpButton.setTitle("Vol +", for: .normal) 115 | volDownButton.setTitle("Vol -", for: .normal) 116 | 117 | 118 | pairingStateLabel.text = "pairingStateLabel" 119 | remoteStateLabel.text = "remoteStateLabel" 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Demo/Demo/Demo/cert.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odyshewroman/AndroidTVRemoteControl/6021df93ee16f0535ca9de350ab16f4d5bb8163c/Demo/Demo/Demo/cert.der -------------------------------------------------------------------------------- /Demo/Demo/Demo/cert.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odyshewroman/AndroidTVRemoteControl/6021df93ee16f0535ca9de350ab16f4d5bb8163c/Demo/Demo/Demo/cert.p12 -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 odyshewroman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.8 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "AndroidTVRemoteControl", 7 | platforms: [.iOS(.v13)], 8 | products: [ 9 | .library( 10 | name: "AndroidTVRemoteControl", 11 | targets: ["AndroidTVRemoteControl"]), 12 | ], 13 | targets: [ 14 | .target( 15 | name: "AndroidTVRemoteControl"), 16 | .testTarget( 17 | name: "AndroidTVRemoteControlTests", 18 | dependencies: ["AndroidTVRemoteControl"]), 19 | ], 20 | swiftLanguageVersions: [.v4, .v4_2, .v5] 21 | ) 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AndroidTVRemoteControl 2 | 3 | [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) 4 | [![SPM Compatible](https://img.shields.io/badge/SPM-Compatible-brightgreen.svg)](https://swift.org/package-manager) 5 | [![CocoaPods](https://img.shields.io/cocoapods/v/AndroidTVRemoteControl.svg)](https://cocoapods.org/pods/AndroidTVRemoteControl) 6 | 7 | Please suport me [![Support](https://img.shields.io/badge/Support-Buy%20Me%20a%20Coffee-brightgreen.svg)](https://www.buymeacoffee.com/odyshewroman) 8 | 9 | This project it's implementation pairing and connection to Android TV OS devices, using protovol v2, and follows the approach described here [Google TV (aka Android TV) Remote Control (v2)](https://github.com/Aymkdn/assistant-freebox-cloud/wiki/Google-TV-(aka-Android-TV)-Remote-Control-(v2)). 10 | 11 | ## Compatibility 12 | 13 | The AndroidTVRemoteControl is supported on iOS version 13 or later and Swift 4 and above. 14 | 15 | ## Installation 16 | 17 | 18 | ### [Swift Package Manager](https://github.com/apple/swift-package-manager) 19 | 20 | In Xcode go to: ```File -> Swift Packages -> Add Package Dependency...``` 21 | 22 | Enter the AndroidTVRemoteControl GitHub repository - ```https://github.com/odyshewroman/AndroidTVRemoteControl``` 23 | 24 | Select the version 25 | 26 | Import AndroidTVRemoteControl module and start to use AndroidTVRemoteControl 27 | 28 | ### Cocoapods 29 | To install AndroidTVRemoteControl with CocoaPods, add the following lines to your `Podfile`: 30 | 31 | ```ruby 32 | pod "AndroidTVRemoteControl" 33 | ``` 34 | 35 | ## Usage 36 | 37 | First of all, you need a certificate to establish a TLS connection with an Android TV OS device. Since the connection is made over a local network, a self-signed certificate is suitable for this purpose. 38 | 39 | The entire process is divided into two parts - **Pairing** and **Sending** commands (and yes, there is also an internal pairing process there). 40 | 41 | Next, you need to create an object - CryptoManager, passing your logic for obtaining the public key of the certificate within the closure. 42 | Then, create a TLSManager and pass the logic for obtaining a CFArray - an array containing a dictionary for every item extracted from the certificate. You also need to set a closure where you will pass the SecTrust obtained from the Android TV OS device when connecting to it to the CryptoManager. 43 | 44 | 45 | 46 | ### Pairing 47 | 48 | Now you can create a **PairingManager**, passing **TLSManager** and **CryptoManager** as parameters. Set a closure to handle the pairing process states and call the connect method. When you receive the `waitingCode` state, on the Android TV OS device screen, three hex numbers (6 characters, you can validate - user input should only contain digits 0-9 and characters A-F) will be displayed. Upon receiving a string with these characters, you need to call the `sendSecret` method on the **PairingManager**. In a successful case, you will receive the `successPaired` state. 49 | 50 | ### Remote 51 | 52 | **Congratulations, you've successfully connected to the Android TV OS device!** If you've paired with this device before, you can skip the pairing and code input step and proceed directly to the command-sending process. 53 | 54 | For sending commands, you'll need the **RemoteManager** object. Additionally, set up a closure to handle the connection process states and call the `connect` method. When the *connected* state is reached, you can start sending your messages to the Android TV OS device using the `send` method. 55 | 56 | 57 | -------------------------------------------------------------------------------- /Sources/AndroidTVRemoteControl/CertManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CertManager.swift 3 | // 4 | // 5 | // Created by Roman Odyshew on 15.10.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public class CertManager { 11 | public init() {} 12 | 13 | public func cert(_ url: URL, _ password: String) -> Result { 14 | let p12Data: Data 15 | do { 16 | p12Data = try Data(contentsOf: url) 17 | } catch let error { 18 | return .Error(.loadCertFromURLError(error)) 19 | } 20 | 21 | let importOptions = [kSecImportExportPassphrase as String: password] 22 | var rawItems: CFArray? 23 | let status = SecPKCS12Import(p12Data as CFData, importOptions as CFDictionary, &rawItems) 24 | 25 | guard status == errSecSuccess else { 26 | return .Error(.secPKCS12ImportNotSuccess) 27 | } 28 | 29 | return .Result(rawItems) 30 | } 31 | 32 | public func getSecKey(_ url: URL) -> Result { 33 | guard let certificateData = NSData(contentsOf:url), 34 | let certificate = SecCertificateCreateWithData(nil, certificateData) else { 35 | return .Error(.createCertFromDataError) 36 | } 37 | 38 | var trust: SecTrust? 39 | let policy = SecPolicyCreateBasicX509() 40 | let status = SecTrustCreateWithCertificates(certificate, policy, &trust) 41 | 42 | guard status == errSecSuccess else { 43 | return .Error(.secTrustCreateWithCertificatesNotSuccess(status)) 44 | } 45 | 46 | guard let secTrust = trust else { 47 | return (.Error(.createTrustObjectError)) 48 | } 49 | 50 | if #available(iOS 14.0, *) { 51 | guard let key = SecTrustCopyKey(secTrust) else { 52 | return .Error(.secTrustCopyKeyError) 53 | } 54 | return .Result(key) 55 | } else { 56 | guard let key = SecTrustCopyPublicKey(secTrust) else { 57 | return .Error(.secTrustCopyKeyError) 58 | } 59 | return .Result(key) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/AndroidTVRemoteControl/Commands/DeepLink.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DeepLink.swift 3 | // 4 | // 5 | // Created by Roman Odyshew on 08.11.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct DeepLink { 11 | let url: String 12 | 13 | public init(_ url: String) { 14 | self.url = url 15 | } 16 | 17 | public init(_ url: URL) { 18 | self.url = url.absoluteString 19 | } 20 | } 21 | 22 | extension DeepLink: RequestDataProtocol { 23 | public var data: Data { 24 | 25 | var data = Data([0xd2, 0x05]) 26 | data.append(contentsOf: Encoder.encodeVarint(UInt(1 + Encoder.encodeVarint(UInt(url.count)).count + url.count))) 27 | data.append(contentsOf: [0xa]) 28 | data.append(contentsOf: Encoder.encodeVarint(UInt(url.count))) 29 | data.append(contentsOf: url.utf8) 30 | return data 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/AndroidTVRemoteControl/Commands/Direction.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Direction.swift 3 | // 4 | // 5 | // Created by Roman Odyshew on 07.11.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum Direction: UInt8 { 11 | case UNKNOWN_DIRECTION = 0 12 | case START_LONG = 1 13 | case END_LONG = 2 14 | case SHORT = 3 15 | } 16 | -------------------------------------------------------------------------------- /Sources/AndroidTVRemoteControl/Commands/Key.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Key.swift 3 | // 4 | // 5 | // Created by Roman Odyshew on 07.11.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum Key: UInt { 11 | // [82, 5, 8, 132, 2, 16, 3] 12 | // [82, 4, 8, 66, 16, 3] 13 | // [0x52, 0x04, 0x08, code, 0x10, 0x03] 14 | 15 | case KEYCODE_UNKNOWN = 0 16 | case KEYCODE_SOFT_LEFT = 1 17 | case KEYCODE_SOFT_RIGHT = 2 18 | case KEYCODE_HOME = 3 19 | case KEYCODE_BACK = 4 20 | case KEYCODE_CALL = 5 21 | case KEYCODE_ENDCALL = 6 22 | case KEYCODE_0 = 7 23 | case KEYCODE_1 = 8 24 | case KEYCODE_2 = 9 25 | case KEYCODE_3 = 10 26 | case KEYCODE_4 = 11 27 | case KEYCODE_5 = 12 28 | case KEYCODE_6 = 13 29 | case KEYCODE_7 = 14 30 | case KEYCODE_8 = 15 31 | case KEYCODE_9 = 16 32 | case KEYCODE_STAR = 17 33 | case KEYCODE_POUND = 18 34 | case KEYCODE_DPAD_UP = 19 35 | case KEYCODE_DPAD_DOWN = 20 36 | case KEYCODE_DPAD_LEFT = 21 37 | case KEYCODE_DPAD_RIGHT = 22 38 | case KEYCODE_DPAD_CENTER = 23 39 | case KEYCODE_VOLUME_UP = 24 40 | case KEYCODE_VOLUME_DOWN = 25 41 | case KEYCODE_POWER = 26 42 | case KEYCODE_CAMERA = 27 43 | case KEYCODE_CLEAR = 28 44 | case KEYCODE_A = 29 45 | case KEYCODE_B = 30 46 | case KEYCODE_C = 31 47 | case KEYCODE_D = 32 48 | case KEYCODE_E = 33 49 | case KEYCODE_F = 34 50 | case KEYCODE_G = 35 51 | case KEYCODE_H = 36 52 | case KEYCODE_I = 37 53 | case KEYCODE_J = 38 54 | case KEYCODE_K = 39 55 | case KEYCODE_L = 40 56 | case KEYCODE_M = 41 57 | case KEYCODE_N = 42 58 | case KEYCODE_O = 43 59 | case KEYCODE_P = 44 60 | case KEYCODE_Q = 45 61 | case KEYCODE_R = 46 62 | case KEYCODE_S = 47 63 | case KEYCODE_T = 48 64 | case KEYCODE_U = 49 65 | case KEYCODE_V = 50 66 | case KEYCODE_W = 51 67 | case KEYCODE_X = 52 68 | case KEYCODE_Y = 53 69 | case KEYCODE_Z = 54 70 | case KEYCODE_COMMA = 55 71 | case KEYCODE_PERIOD = 56 72 | case KEYCODE_ALT_LEFT = 57 73 | case KEYCODE_ALT_RIGHT = 58 74 | case KEYCODE_SHIFT_LEFT = 59 75 | case KEYCODE_SHIFT_RIGHT = 60 76 | case KEYCODE_TAB = 61 77 | case KEYCODE_SPACE = 62 78 | case KEYCODE_SYM = 63 79 | case KEYCODE_EXPLORER = 64 80 | case KEYCODE_ENVELOPE = 65 81 | case KEYCODE_ENTER = 66 82 | case KEYCODE_DEL = 67 83 | case KEYCODE_GRAVE = 68 84 | case KEYCODE_MINUS = 69 85 | case KEYCODE_EQUALS = 70 86 | case KEYCODE_LEFT_BRACKET = 71 87 | case KEYCODE_RIGHT_BRACKET = 72 88 | case KEYCODE_BACKSLASH = 73 89 | case KEYCODE_SEMICOLON = 74 90 | case KEYCODE_APOSTROPHE = 75 91 | case KEYCODE_SLASH = 76 92 | case KEYCODE_AT = 77 93 | case KEYCODE_NUM = 78 94 | case KEYCODE_HEADSETHOOK = 79 95 | case KEYCODE_FOCUS = 80 96 | case KEYCODE_PLUS = 81 97 | case KEYCODE_MENU = 82 98 | case KEYCODE_NOTIFICATION = 83 99 | case KEYCODE_SEARCH = 84 100 | case KEYCODE_MEDIA_PLAY_PAUSE = 85 101 | case KEYCODE_MEDIA_STOP = 86 102 | case KEYCODE_MEDIA_NEXT = 87 103 | case KEYCODE_MEDIA_PREVIOUS = 88 104 | case KEYCODE_MEDIA_REWIND = 89 105 | case KEYCODE_MEDIA_FAST_FORWARD = 90 106 | case KEYCODE_MUTE = 91 107 | case KEYCODE_PAGE_UP = 92 108 | case KEYCODE_PAGE_DOWN = 93 109 | case KEYCODE_PICTSYMBOLS = 94 110 | case KEYCODE_SWITCH_CHARSET = 95 111 | case KEYCODE_BUTTON_A = 96 112 | case KEYCODE_BUTTON_B = 97 113 | case KEYCODE_BUTTON_C = 98 114 | case KEYCODE_BUTTON_X = 99 115 | case KEYCODE_BUTTON_Y = 100 116 | case KEYCODE_BUTTON_Z = 101 117 | case KEYCODE_BUTTON_L1 = 102 118 | case KEYCODE_BUTTON_R1 = 103 119 | case KEYCODE_BUTTON_L2 = 104 120 | case KEYCODE_BUTTON_R2 = 105 121 | case KEYCODE_BUTTON_THUMBL = 106 122 | case KEYCODE_BUTTON_THUMBR = 107 123 | case KEYCODE_BUTTON_START = 108 124 | case KEYCODE_BUTTON_SELECT = 109 125 | case KEYCODE_BUTTON_MODE = 110 126 | case KEYCODE_ESCAPE = 111 127 | case KEYCODE_FORWARD_DEL = 112 128 | case KEYCODE_CTRL_LEFT = 113 129 | case KEYCODE_CTRL_RIGHT = 114 130 | case KEYCODE_CAPS_LOCK = 115 131 | case KEYCODE_SCROLL_LOCK = 116 132 | case KEYCODE_META_LEFT = 117 133 | case KEYCODE_META_RIGHT = 118 134 | case KEYCODE_FUNCTION = 119 135 | case KEYCODE_SYSRQ = 120 136 | case KEYCODE_BREAK = 121 137 | case KEYCODE_MOVE_HOME = 122 138 | case KEYCODE_MOVE_END = 123 139 | case KEYCODE_INSERT = 124 140 | case KEYCODE_FORWARD = 125 141 | case KEYCODE_MEDIA_PLAY = 126 142 | case KEYCODE_MEDIA_PAUSE = 127 143 | case KEYCODE_MEDIA_CLOSE = 128 144 | case KEYCODE_MEDIA_EJECT = 129 145 | case KEYCODE_MEDIA_RECORD = 130 146 | case KEYCODE_F1 = 131 147 | case KEYCODE_F2 = 132 148 | case KEYCODE_F3 = 133 149 | case KEYCODE_F4 = 134 150 | case KEYCODE_F5 = 135 151 | case KEYCODE_F6 = 136 152 | case KEYCODE_F7 = 137 153 | case KEYCODE_F8 = 138 154 | case KEYCODE_F9 = 139 155 | case KEYCODE_F10 = 140 156 | case KEYCODE_F11 = 141 157 | case KEYCODE_F12 = 142 158 | case KEYCODE_NUM_LOCK = 143 159 | case KEYCODE_NUMPAD_0 = 144 160 | case KEYCODE_NUMPAD_1 = 145 161 | case KEYCODE_NUMPAD_2 = 146 162 | case KEYCODE_NUMPAD_3 = 147 163 | case KEYCODE_NUMPAD_4 = 148 164 | case KEYCODE_NUMPAD_5 = 149 165 | case KEYCODE_NUMPAD_6 = 150 166 | case KEYCODE_NUMPAD_7 = 151 167 | case KEYCODE_NUMPAD_8 = 152 168 | case KEYCODE_NUMPAD_9 = 153 169 | case KEYCODE_NUMPAD_DIVIDE = 154 170 | case KEYCODE_NUMPAD_MULTIPLY = 155 171 | case KEYCODE_NUMPAD_SUBTRACT = 156 172 | case KEYCODE_NUMPAD_ADD = 157 173 | case KEYCODE_NUMPAD_DOT = 158 174 | case KEYCODE_NUMPAD_COMMA = 159 175 | case KEYCODE_NUMPAD_ENTER = 160 176 | case KEYCODE_NUMPAD_EQUALS = 161 177 | case KEYCODE_NUMPAD_LEFT_PAREN = 162 178 | case KEYCODE_NUMPAD_RIGHT_PAREN = 163 179 | case KEYCODE_VOLUME_MUTE = 164 180 | case KEYCODE_INFO = 165 181 | case KEYCODE_CHANNEL_UP = 166 182 | case KEYCODE_CHANNEL_DOWN = 167 183 | case KEYCODE_ZOOM_IN = 168 184 | case KEYCODE_ZOOM_OUT = 169 185 | case KEYCODE_TV = 170 186 | case KEYCODE_WINDOW = 171 187 | case KEYCODE_GUIDE = 172 188 | case KEYCODE_DVR = 173 189 | case KEYCODE_BOOKMARK = 174 190 | case KEYCODE_CAPTIONS = 175 191 | case KEYCODE_SETTINGS = 176 192 | case KEYCODE_TV_POWER = 177 193 | case KEYCODE_TV_INPUT = 178 194 | case KEYCODE_STB_POWER = 179 195 | case KEYCODE_STB_INPUT = 180 196 | case KEYCODE_AVR_POWER = 181 197 | case KEYCODE_AVR_INPUT = 182 198 | case KEYCODE_PROG_RED = 183 199 | case KEYCODE_PROG_GREEN = 184 200 | case KEYCODE_PROG_YELLOW = 185 201 | case KEYCODE_PROG_BLUE = 186 202 | case KEYCODE_APP_SWITCH = 187 203 | case KEYCODE_BUTTON_1 = 188 204 | case KEYCODE_BUTTON_2 = 189 205 | case KEYCODE_BUTTON_3 = 190 206 | case KEYCODE_BUTTON_4 = 191 207 | case KEYCODE_BUTTON_5 = 192 208 | case KEYCODE_BUTTON_6 = 193 209 | case KEYCODE_BUTTON_7 = 194 210 | case KEYCODE_BUTTON_8 = 195 211 | case KEYCODE_BUTTON_9 = 196 212 | case KEYCODE_BUTTON_10 = 197 213 | case KEYCODE_BUTTON_11 = 198 214 | case KEYCODE_BUTTON_12 = 199 215 | case KEYCODE_BUTTON_13 = 200 216 | case KEYCODE_BUTTON_14 = 201 217 | case KEYCODE_BUTTON_15 = 202 218 | case KEYCODE_BUTTON_16 = 203 219 | case KEYCODE_LANGUAGE_SWITCH = 204 220 | case KEYCODE_MANNER_MODE = 205 221 | case KEYCODE_3D_MODE = 206 222 | case KEYCODE_CONTACTS = 207 223 | case KEYCODE_CALENDAR = 208 224 | case KEYCODE_MUSIC = 209 225 | case KEYCODE_CALCULATOR = 210 226 | case KEYCODE_ZENKAKU_HANKAKU = 211 227 | case KEYCODE_EISU = 212 228 | case KEYCODE_MUHENKAN = 213 229 | case KEYCODE_HENKAN = 214 230 | case KEYCODE_KATAKANA_HIRAGANA = 215 231 | case KEYCODE_YEN = 216 232 | case KEYCODE_RO = 217 233 | case KEYCODE_KANA = 218 234 | case KEYCODE_ASSIST = 219 235 | case KEYCODE_BRIGHTNESS_DOWN = 220 236 | case KEYCODE_BRIGHTNESS_UP = 221 237 | case KEYCODE_MEDIA_AUDIO_TRACK = 222 238 | case KEYCODE_SLEEP = 223 239 | case KEYCODE_WAKEUP = 224 240 | case KEYCODE_PAIRING = 225 241 | case KEYCODE_MEDIA_TOP_MENU = 226 242 | case KEYCODE_11 = 227 243 | case KEYCODE_12 = 228 244 | case KEYCODE_LAST_CHANNEL = 229 245 | case KEYCODE_TV_DATA_SERVICE = 230 246 | case KEYCODE_VOICE_ASSIST = 231 247 | case KEYCODE_TV_RADIO_SERVICE = 232 248 | case KEYCODE_TV_TELETEXT = 233 249 | case KEYCODE_TV_NUMBER_ENTRY = 234 250 | case KEYCODE_TV_TERRESTRIAL_ANALOG = 235 251 | case KEYCODE_TV_TERRESTRIAL_DIGITAL = 236 252 | case KEYCODE_TV_SATELLITE = 237 253 | case KEYCODE_TV_SATELLITE_BS = 238 254 | case KEYCODE_TV_SATELLITE_CS = 239 255 | case KEYCODE_TV_SATELLITE_SERVICE = 240 256 | case KEYCODE_TV_NETWORK = 241 257 | case KEYCODE_TV_ANTENNA_CABLE = 242 258 | case KEYCODE_TV_INPUT_HDMI_1 = 243 259 | case KEYCODE_TV_INPUT_HDMI_2 = 244 260 | case KEYCODE_TV_INPUT_HDMI_3 = 245 261 | case KEYCODE_TV_INPUT_HDMI_4 = 246 262 | case KEYCODE_TV_INPUT_COMPOSITE_1 = 247 263 | case KEYCODE_TV_INPUT_COMPOSITE_2 = 248 264 | case KEYCODE_TV_INPUT_COMPONENT_1 = 249 265 | case KEYCODE_TV_INPUT_COMPONENT_2 = 250 266 | case KEYCODE_TV_INPUT_VGA_1 = 251 267 | case KEYCODE_TV_AUDIO_DESCRIPTION = 252 268 | case KEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP = 253 269 | case KEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN = 254 270 | case KEYCODE_TV_ZOOM_MODE = 255 271 | case KEYCODE_TV_CONTENTS_MENU = 256 272 | case KEYCODE_TV_MEDIA_CONTEXT_MENU = 257 273 | case KEYCODE_TV_TIMER_PROGRAMMING = 258 274 | case KEYCODE_HELP = 259 275 | case KEYCODE_NAVIGATE_PREVIOUS = 260 276 | case KEYCODE_NAVIGATE_NEXT = 261 277 | case KEYCODE_NAVIGATE_IN = 262 278 | case KEYCODE_NAVIGATE_OUT = 263 279 | case KEYCODE_STEM_PRIMARY = 264 280 | case KEYCODE_STEM_1 = 265 281 | case KEYCODE_STEM_2 = 266 282 | case KEYCODE_STEM_3 = 267 283 | case KEYCODE_DPAD_UP_LEFT = 268 284 | case KEYCODE_DPAD_DOWN_LEFT = 269 285 | case KEYCODE_DPAD_UP_RIGHT = 270 286 | case KEYCODE_DPAD_DOWN_RIGHT = 271 287 | case KEYCODE_MEDIA_SKIP_FORWARD = 272 288 | case KEYCODE_MEDIA_SKIP_BACKWARD = 273 289 | case KEYCODE_MEDIA_STEP_FORWARD = 274 290 | case KEYCODE_MEDIA_STEP_BACKWARD = 275 291 | case KEYCODE_SOFT_SLEEP = 276 292 | case KEYCODE_CUT = 277 293 | case KEYCODE_COPY = 278 294 | case KEYCODE_PASTE = 279 295 | case KEYCODE_SYSTEM_NAVIGATION_UP = 280 296 | case KEYCODE_SYSTEM_NAVIGATION_DOWN = 281 297 | case KEYCODE_SYSTEM_NAVIGATION_LEFT = 282 298 | case KEYCODE_SYSTEM_NAVIGATION_RIGHT = 283 299 | case KEYCODE_ALL_APPS = 284 300 | case KEYCODE_REFRESH = 285 301 | case KEYCODE_THUMBS_UP = 286 302 | case KEYCODE_THUMBS_DOWN = 287 303 | case KEYCODE_PROFILE_SWITCH = 288 304 | case KEYCODE_VIDEO_APP_1 = 289 305 | case KEYCODE_VIDEO_APP_2 = 290 306 | case KEYCODE_VIDEO_APP_3 = 291 307 | case KEYCODE_VIDEO_APP_4 = 292 308 | case KEYCODE_VIDEO_APP_5 = 293 309 | case KEYCODE_VIDEO_APP_6 = 294 310 | case KEYCODE_VIDEO_APP_7 = 295 311 | case KEYCODE_VIDEO_APP_8 = 296 312 | case KEYCODE_FEATURED_APP_1 = 297 313 | case KEYCODE_FEATURED_APP_2 = 298 314 | case KEYCODE_FEATURED_APP_3 = 299 315 | case KEYCODE_FEATURED_APP_4 = 300 316 | case KEYCODE_DEMO_APP_1 = 301 317 | case KEYCODE_DEMO_APP_2 = 302 318 | case KEYCODE_DEMO_APP_3 = 303 319 | case KEYCODE_DEMO_APP_4 = 304 320 | } 321 | -------------------------------------------------------------------------------- /Sources/AndroidTVRemoteControl/Commands/KeyPress.swift: -------------------------------------------------------------------------------- 1 | // 2 | // KeyPress.swift 3 | // 4 | // 5 | // Created by Roman Odyshew on 07.11.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct KeyPress { 11 | let key: Key 12 | let direction: Direction 13 | 14 | public init(_ key: Key, _ direction: Direction = .SHORT) { 15 | self.key = key 16 | self.direction = direction 17 | } 18 | } 19 | 20 | extension KeyPress: RequestDataProtocol { 21 | public var data: Data { 22 | let encodedKey = Encoder.encodeVarint(key.rawValue) 23 | var data = Data() 24 | data.append(contentsOf: [0x52, UInt8(3 + encodedKey.count), 0x08]) 25 | data.append(contentsOf: encodedKey) 26 | data.append(contentsOf: [0x10, direction.rawValue]) 27 | 28 | return data 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/AndroidTVRemoteControl/CryptoManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CryptoManager.swift 3 | // 4 | // 5 | // Created by Roman Odyshew on 15.10.2023. 6 | // 7 | 8 | import Foundation 9 | import CryptoKit 10 | 11 | public class CryptoManager { 12 | public var clientPublicCertificate: (() -> Result)? 13 | public var serverPublicCertificate: (() -> Result)? 14 | 15 | public init() {} 16 | 17 | func getEncodedCert(_ code: String) -> Result<[UInt8]> { 18 | if code.count != 6 { 19 | return .Error(.invalidCode(description: "The code should contain 6 characters")) 20 | } 21 | 22 | guard let firstNumber = UInt8(String(code.prefix(2)), radix: 16), 23 | let secondNumber = UInt8(String(code.dropFirst(2).prefix(2)), radix: 16), 24 | let thirdNumber = UInt8(String(code.suffix(2)), radix: 16) 25 | else { 26 | return .Error(.invalidCode(description: "The code should contain only hex characters")) 27 | } 28 | 29 | let codeBytes: [UInt8] = [secondNumber, thirdNumber] 30 | 31 | let clientComponents: (mod: Data, exp: Data) 32 | let serverComponents: (mod: Data, exp: Data) 33 | 34 | guard let clientCert = clientPublicCertificate?() else { 35 | return .Error(.noClientPublicCertificate) 36 | } 37 | 38 | guard let serverCert = serverPublicCertificate?() else { 39 | return .Error(.noServerPublicCertificate) 40 | } 41 | 42 | switch clientCert { 43 | case .Result(let secKey): 44 | switch getCertComponents(secKey) { 45 | case .Result(let data): 46 | clientComponents = data 47 | case .Error(let error): 48 | return .Error(error) 49 | } 50 | case .Error(let error): 51 | return .Error(error) 52 | } 53 | 54 | switch serverCert { 55 | case .Result(let secKey): 56 | switch getCertComponents(secKey) { 57 | case .Result(let data): 58 | serverComponents = data 59 | case .Error(let error): 60 | return .Error(error) 61 | } 62 | case .Error(let error): 63 | return .Error(error) 64 | } 65 | 66 | var shaHash = CryptoKit.SHA256() 67 | shaHash.update(data: clientComponents.mod) 68 | shaHash.update(data: clientComponents.exp) 69 | shaHash.update(data: serverComponents.mod) 70 | shaHash.update(data: serverComponents.exp) 71 | shaHash.update(data: Data(codeBytes)) 72 | 73 | let hashData: [UInt8] = shaHash.finalize().map { $0 } 74 | 75 | guard hashData.first == firstNumber else { 76 | return .Error(.wrongCode) 77 | } 78 | 79 | return .Result(hashData) 80 | } 81 | 82 | private func getCertComponents(_ secKey: SecKey) -> Result<(mod: Data, exp: Data)> { 83 | let keyAndData: (pubData: Data, keySize: Int) 84 | 85 | switch getPublicCertData(secKey) { 86 | case .Result(let result): 87 | keyAndData = result 88 | case .Error(let error): 89 | return .Error(error) 90 | } 91 | 92 | let modulus = extractModulus(keyAndData.pubData, keyAndData.keySize) 93 | let exponent = extractExponent(keyAndData.pubData) 94 | 95 | return .Result((mod: modulus, exp: exponent)) 96 | } 97 | 98 | private func getPublicCertData(_ publicKey: SecKey) -> Result<(pubData: Data, keySize: Int)> { 99 | guard let publicKeyAttributes = SecKeyCopyAttributes(publicKey) as? [String: Any] else { 100 | return .Error(.noSecAttributes) 101 | } 102 | 103 | // Check that this is really an RSA key 104 | guard let keyType = publicKeyAttributes[kSecAttrKeyType as String] as? String, 105 | keyType == kSecAttrKeyTypeRSA as String else { 106 | return .Error(.notRSAKey) 107 | } 108 | 109 | // Check that this is really a public key 110 | guard let keyClass = publicKeyAttributes[kSecAttrKeyClass as String] as? String, 111 | keyClass == kSecAttrKeyClassPublic as String 112 | else { 113 | return .Error(.notPublicKey) 114 | } 115 | 116 | guard let keySize = publicKeyAttributes[kSecAttrKeySizeInBits as String] as? Int else { 117 | return .Error(.noKeySizeAttribute) 118 | } 119 | 120 | guard let pubData = publicKeyAttributes[kSecValueData as String] as? Data else { 121 | return .Error(.noValueData) 122 | } 123 | 124 | if pubData.count < 13 { 125 | return .Error(.invalidCertData) 126 | } 127 | 128 | return .Result((pubData, keySize)) 129 | } 130 | 131 | private func extractModulus(_ publicKeyData: Data, _ keySize: Int) -> Data { 132 | var modulus = publicKeyData.subdata(in: 8..<(publicKeyData.count - 5)) 133 | 134 | if modulus.count > keySize / 8 { // --> 257 bytes 135 | modulus.removeFirst(1) 136 | } 137 | 138 | return modulus 139 | } 140 | 141 | private func extractExponent(_ publicKeyData: Data) -> Data { 142 | return publicKeyData.subdata(in: (publicKeyData.count - 3).. 10 else { 17 | return nil 18 | } 19 | 20 | var flags: UInt16 = UInt16(data[5]) << 8 21 | flags += UInt16(data[4]) 22 | 23 | self.flags = flags 24 | 25 | guard data[8] == 0xa, 26 | let deviceInfo = DeviceInfo(Data(data.dropFirst(9))) else { 27 | return nil 28 | } 29 | 30 | self.deviceInfo = deviceInfo 31 | } 32 | } 33 | 34 | struct FirstConfigurationRequest: RequestDataProtocol { 35 | let deviceInfo: DeviceInfo 36 | 37 | var data: Data { 38 | var data = Data([0xa]) 39 | 40 | let modelLength = Encoder.encodeVarint(UInt(deviceInfo.model.count)) 41 | let vendorLength = Encoder.encodeVarint(UInt(deviceInfo.vendor.count)) 42 | let buildLength = Encoder.encodeVarint(UInt(deviceInfo.appBuild.count)) 43 | let appNameLength = Encoder.encodeVarint(UInt(deviceInfo.appName.count)) 44 | let versionLength = Encoder.encodeVarint(UInt(deviceInfo.version.count)) 45 | 46 | let subLength = 7 + deviceInfo.length + modelLength.count + vendorLength.count + buildLength.count + appNameLength.count + versionLength.count 47 | let length = subLength + 4 + Encoder.encodeVarint(UInt(subLength)).count 48 | 49 | data.append(contentsOf: Encoder.encodeVarint(UInt(length))) 50 | data.append(contentsOf: [0x08, 0xEE, 0x04, 0x12]) 51 | 52 | data.append(contentsOf: Encoder.encodeVarint(UInt(subLength))) 53 | data.append(contentsOf: [0xa]) 54 | data.append(contentsOf: modelLength) 55 | data.append(contentsOf: deviceInfo.model.utf8) 56 | data.append(contentsOf: [0x12]) 57 | data.append(contentsOf: vendorLength) 58 | data.append(contentsOf: deviceInfo.vendor.utf8) 59 | data.append(contentsOf: [0x18, 0x01, 0x22]) 60 | data.append(contentsOf: buildLength) 61 | data.append(contentsOf: deviceInfo.appBuild.utf8) 62 | data.append(contentsOf: [0x2a]) 63 | data.append(contentsOf: appNameLength) 64 | data.append(contentsOf: deviceInfo.appName.utf8) 65 | data.append(contentsOf: [0x32]) 66 | data.append(contentsOf: versionLength) 67 | data.append(contentsOf: deviceInfo.version.utf8) 68 | return data 69 | } 70 | } 71 | 72 | public struct DeviceInfo { 73 | public let model: String 74 | public let vendor: String 75 | public let version: String 76 | public let appName: String 77 | public let appBuild: String 78 | 79 | var length: Int { 80 | return model.count + vendor.count + version.count + appBuild.count + appName.count 81 | } 82 | 83 | public init(_ model: String, _ vendor: String, _ version: String, _ appName: String, _ appBuild: String) { 84 | self.model = model 85 | self.vendor = vendor 86 | self.version = version 87 | self.appName = appName 88 | self.appBuild = appBuild 89 | } 90 | 91 | init?(_ data: Data) { 92 | let length = data.count 93 | var index = 0 94 | guard let model = Self.extractString(data, index) else { 95 | return nil 96 | } 97 | 98 | self.model = model 99 | 100 | index += 1 + model.count 101 | guard index < length, data[index] == 0x12 else { 102 | return nil 103 | } 104 | 105 | index += 1 106 | guard let vendor = Self.extractString(data, index) else { 107 | return nil 108 | } 109 | self.vendor = vendor 110 | index += 1 + vendor.count 111 | 112 | guard index + 2 < length, [data[index], data[index + 1], data[index + 2]] == [0x18, 0x1, 0x22] else { 113 | return nil 114 | } 115 | 116 | index += 3 117 | guard let version = Self.extractString(data, index) else { 118 | return nil 119 | } 120 | self.version = version 121 | 122 | index += 1 + version.count 123 | guard index < length, data[index] == 0x2a else { 124 | return nil 125 | } 126 | 127 | index += 1 128 | guard let appName = Self.extractString(data, index) else { 129 | return nil 130 | } 131 | self.appName = appName 132 | 133 | index += appName.count + 1 134 | guard index < length, data[index] == 0x32 else { 135 | return nil 136 | } 137 | 138 | index += 1 139 | let appBuild = Self.extractString(data, index) ?? "-1" 140 | self.appBuild = appBuild 141 | } 142 | 143 | private static func extractString(_ data: Data, _ index: Int) -> String? { 144 | guard data.count > index else { return nil } 145 | 146 | let size = Int(data[index]) 147 | guard data.count > index + size else { return nil } 148 | 149 | let startIndex = index + 1 150 | let endIndex = startIndex + size 151 | let subData = data[startIndex..= 3 else { 166 | return false 167 | } 168 | 169 | return Array(data.suffix(3)) == [0x02, 0x12, 0x0] 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /Sources/AndroidTVRemoteControl/Network/CommandNetwork/CommandNetwork.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CommandNetwork.swift 3 | // 4 | // 5 | // Created by Roman Odyshew on 15.10.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public struct CommandNetwork { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /Sources/AndroidTVRemoteControl/Network/CommandNetwork/Ping.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Ping.swift 3 | // 4 | // 5 | // Created by Roman Odyshew on 15.10.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension CommandNetwork { 11 | struct Ping { 12 | let val1: [UInt8] 13 | let val2: [UInt8] 14 | 15 | init?(_ data: Data) { 16 | let arrayData = Array(data) 17 | self.init(arrayData) 18 | } 19 | 20 | init?(_ data: [UInt8]) { 21 | guard !data.isEmpty, 22 | data[0] == data.count - 1, 23 | data.indices.contains(1), data[1] == 66, 24 | data.indices.contains(2), data[2] == data[0] - 2, 25 | data.indices.contains(3), data[3] == 8 else { 26 | return nil 27 | } 28 | 29 | let startIndex = 4 30 | if data[2] == 0x02 { 31 | val1 = Array(data.suffix(from: startIndex)) 32 | val2 = [] 33 | return 34 | } 35 | 36 | guard var endIndex = data.firstIndex(of: 16) else { 37 | return nil 38 | } 39 | 40 | guard endIndex > 3, data.count > endIndex else { 41 | return nil 42 | } 43 | 44 | if data.indices.contains(endIndex + 1), data[endIndex + 1] == 16 { 45 | endIndex += 1 46 | } 47 | 48 | val1 = Array(data[startIndex.. Ping? { 53 | return self.extract(from: Array(data)) 54 | } 55 | 56 | static func extract(from bytes: [UInt8]) -> Ping? { 57 | let indexes = bytes.indices.filter { bytes[$0] == 66 } 58 | 59 | guard indexes.count > 0 else { 60 | return nil 61 | } 62 | 63 | for i in indexes { 64 | if i == 0 { continue } 65 | 66 | let size = Int(bytes[i-1]) 67 | if i + size > bytes.count { 68 | continue 69 | } 70 | 71 | 72 | if let ping = Ping(Array(bytes[i-1.. Bool { 23 | let dataArray = Array(data) 24 | return parse(dataArray) 25 | } 26 | 27 | // The data arrives in portions in arbitrary order, and we attempt to parse parts related to the current state of power, 28 | // the currently running application, and the volume level 29 | mutating func parse(_ data: [UInt8]) -> Bool { 30 | var result: Bool = false 31 | 32 | if !powerPart { 33 | powerPart = parsePowerPart(data) 34 | result = powerPart 35 | } 36 | 37 | if !currentAppPart { 38 | runAppName = parseCurrentApp(data) 39 | currentAppPart = runAppName != nil 40 | result = result || currentAppPart 41 | } 42 | 43 | if !volumeLevelPart { 44 | volumeLevelPart = VolumeLevel(data) != nil 45 | result = result || volumeLevelPart 46 | } 47 | 48 | return result 49 | } 50 | 51 | // incoming data format: [5, 194, 2, 2, 8, ] 52 | private func parsePowerPart(_ data: [UInt8]) -> Bool { 53 | let pattern: [UInt8] = [194, 2, 2, 8] 54 | 55 | guard data.count >= pattern.count else { 56 | return false 57 | } 58 | 59 | guard let index = data.firstIndex(of: 194) else { 60 | return false 61 | } 62 | 63 | let powerPartLength = index + pattern.count 64 | guard powerPartLength <= data.count else { 65 | return false 66 | } 67 | 68 | if Array(data[index.. String? { 78 | guard let index = data.firstIndex(of: 162), index > 0 else { 79 | return nil 80 | } 81 | 82 | let length = Int(data[index - 1]) 83 | if length < 7 { 84 | return nil 85 | } 86 | 87 | if data.count < index + length { 88 | return nil 89 | } 90 | 91 | guard data.indices.contains(index + 1), data[index + 1] == 1, 92 | data.indices.contains(index + 3), data[index + 3] == 10 else { 93 | return nil 94 | } 95 | 96 | guard var index = data.firstIndex(of: 98), data.indices.contains(index + 1) else { 97 | return nil 98 | } 99 | 100 | index += 1 101 | let appNameLength = Int(data[index]) 102 | if data.count <= index + appNameLength { 103 | return nil 104 | } 105 | 106 | index += 1 107 | let appNameArray = data[index.., (optional), 16, , 26, model_name_length, model_name_string, 32, , 40, unknown, 48, max_volume_level, 56, current_volume_level, 64, unknown] 11 | // Fields after '32, ' is otional 12 | // example [27, 146, 3, 24, 8, 2, 16, 2, 26, 8, 65, 105, 80, 108, 117, 115, 50, 75, 32, 2, 40, 0, 48, 100, 56, 74, 64, 0] 13 | // example [23, 146, 3, 20, 8, 50, 16, 9, 26, 12, 78, 101, 120, 117, 115, 32, 80, 108, 97, 121, 101, 114, 32, 0] 14 | struct VolumeLevel { 15 | var unknown1: UInt8 16 | var unknown2: UInt8 17 | var modelName: String 18 | var unknown3: UInt8 19 | var unknown4: UInt8? 20 | var volumeMax: UInt8? 21 | var volumeLevel: UInt8? 22 | var unknown5: UInt8? 23 | 24 | init?(_ data: Data) { 25 | self.init(Array(data)) 26 | } 27 | 28 | init?(_ data: [UInt8]) { 29 | guard var index = data.firstIndex(of: 146), index > 0 else { 30 | return nil 31 | } 32 | 33 | let length = Int(data[index - 1]) 34 | 35 | guard length >= 12, 36 | data.count >= index + length 37 | else { 38 | return nil 39 | } 40 | 41 | index += 1 42 | guard data.indices.contains(index), data[index] == 3, 43 | data.indices.contains(index + 2), data[index + 2] == 8 else { 44 | return nil 45 | } 46 | 47 | guard data.indices.contains(index + 3) else { 48 | return nil 49 | } 50 | unknown1 = data[index + 3] 51 | 52 | index += 4 53 | if !data.indices.contains(index) || data[index] != 16 { 54 | index += 1 55 | } 56 | 57 | guard data.indices.contains(index), data[index] == 16 else { 58 | return nil 59 | } 60 | 61 | guard data.indices.contains(index + 1) else { 62 | return nil 63 | } 64 | unknown2 = data[index + 1] 65 | 66 | let modelNameSizeIndex = index + 3 67 | 68 | guard data.indices.contains(modelNameSizeIndex) else { 69 | return nil 70 | } 71 | 72 | let modelNameSize = Int(data[modelNameSizeIndex]) 73 | 74 | guard modelNameSizeIndex + modelNameSize < data.count else { 75 | return nil 76 | } 77 | 78 | guard let modelName = String(bytes: data[modelNameSizeIndex + 1...modelNameSizeIndex + modelNameSize], encoding: .utf8) else { 79 | return nil 80 | } 81 | 82 | self.modelName = modelName 83 | 84 | index = modelNameSizeIndex + modelNameSize + 1 85 | 86 | guard data.indices.contains(index) && data[index] == 32 else { 87 | return nil 88 | } 89 | 90 | index += 1 91 | 92 | guard data.indices.contains(index) else { 93 | return nil 94 | } 95 | 96 | unknown3 = data[index] 97 | 98 | index += 1 99 | guard data.indices.contains(index), data[index] == 40 else { 100 | return 101 | } 102 | 103 | index += 1 104 | guard data.indices.contains(index) else { 105 | return 106 | } 107 | 108 | unknown4 = data[index] 109 | 110 | index += 1 111 | guard data.indices.contains(index), data[index] == 48 else { 112 | return 113 | } 114 | 115 | index += 1 116 | guard data.indices.contains(index) else { 117 | return 118 | } 119 | 120 | volumeMax = data[index] 121 | 122 | index += 1 123 | guard data.indices.contains(index), data[index] == 56 else { 124 | return 125 | } 126 | 127 | index += 1 128 | guard data.indices.contains(index) else { 129 | return 130 | } 131 | 132 | volumeLevel = data[index] 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Sources/AndroidTVRemoteControl/Network/PairingNetwork/Configuration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Configuration.swift 3 | // 4 | // 5 | // Created by Roman Odyshew on 15.10.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension PairingNetwork { 11 | struct ConfigurationResponse { 12 | var length: Data? 13 | var data: Data? 14 | 15 | var isSuccess: Bool { 16 | guard length != nil, let data = data else { 17 | return false 18 | } 19 | 20 | var successArray: [UInt8] = [UInt8](ProtocolVersion2().data) 21 | successArray.append(contentsOf: Status.ok.data) 22 | successArray.append(contentsOf: [0xfa, 0x01]) 23 | 24 | return data.starts(with: successArray) 25 | } 26 | } 27 | } 28 | 29 | extension PairingNetwork { 30 | struct ConfigurationRequest: RequestDataProtocol { 31 | let config = ParingConfiguration(clientRole: .input, encoding: ParingEncoding(symbolLength: 6, type: .hexadecimal)) 32 | let status: Status = .ok 33 | let protocolVersion = ProtocolVersion2() 34 | 35 | var data: Data { 36 | var data = Data() 37 | 38 | data.append(protocolVersion.data) 39 | data.append(status.data) 40 | 41 | data.append(contentsOf: [0xf2, 0x01, config.length]) 42 | data.append(config.data) 43 | 44 | return data 45 | } 46 | } 47 | 48 | struct ParingConfiguration { 49 | let clientRole: RoleType 50 | let encoding: ParingEncoding 51 | 52 | var data: Data { 53 | var data: Data = Data([0xa, encoding.length]) 54 | data.append(encoding.data) 55 | data.append(contentsOf: [0x10, 0x1]) 56 | 57 | return data 58 | } 59 | 60 | var length: UInt8 { 61 | return UInt8(data.count) 62 | } 63 | } 64 | 65 | enum RoleType: UInt8 { 66 | case unknown = 0 67 | case input = 1 68 | case output = 2 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/AndroidTVRemoteControl/Network/PairingNetwork/Option.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Option.swift 3 | // 4 | // 5 | // Created by Roman Odyshew on 15.10.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension PairingNetwork { 11 | struct OptionRequest: RequestDataProtocol { 12 | let option: ParingOption 13 | let status: Status 14 | let protocolVersion = ProtocolVersion2() 15 | 16 | var data: Data { 17 | var data = Data() 18 | data.append(protocolVersion.data) 19 | data.append(status.data) 20 | 21 | data.append(contentsOf: [0xa2, 0x01, option.length]) 22 | data.append(option.data) 23 | 24 | return data 25 | } 26 | 27 | init() { 28 | status = .ok 29 | option = ParingOption(inputEncodings: [ParingEncoding(symbolLength: 6, type: .hexadecimal)], 30 | outputEncodings: [], 31 | preferredRole: .input) 32 | } 33 | } 34 | 35 | struct OptionResponse { 36 | var length: Data? 37 | var data: Data? 38 | 39 | var isSuccess: Bool { 40 | guard length != nil, let data = data else { 41 | return false 42 | } 43 | 44 | var successArray: [UInt8] = [UInt8](ProtocolVersion2().data) 45 | successArray.append(contentsOf: Status.ok.data) 46 | successArray.append(contentsOf: [0xa2, 0x01]) 47 | 48 | return data.starts(with: successArray) 49 | } 50 | } 51 | } 52 | 53 | extension PairingNetwork.OptionRequest { 54 | struct ParingOption { 55 | var inputEncodings: [PairingNetwork.ParingEncoding] = [] 56 | var outputEncodings: [PairingNetwork.ParingEncoding] = [] 57 | var preferredRole: RoleType = .input 58 | 59 | var data: Data { 60 | var data: Data = Data() 61 | 62 | if inputEncodings.count > 0 { 63 | for inputEncoding in inputEncodings { 64 | data.append(contentsOf: [0xa, inputEncoding.length]) 65 | data.append(inputEncoding.data) 66 | } 67 | } 68 | 69 | if outputEncodings.count > 0 { 70 | for outputEncoding in outputEncodings { 71 | data.append(contentsOf: [0x12, outputEncoding.length]) 72 | data.append(outputEncoding.data) 73 | } 74 | } 75 | 76 | if preferredRole.rawValue != 0 { 77 | data.append(contentsOf: [0x18, preferredRole.rawValue]) 78 | } 79 | 80 | return data 81 | } 82 | 83 | var length: UInt8 { 84 | var length: UInt8 = 0 85 | 86 | if inputEncodings.count > 0 { 87 | for inputEncoding in inputEncodings { 88 | length += 2 89 | length += inputEncoding.length 90 | } 91 | } 92 | 93 | if outputEncodings.count > 0 { 94 | for outputEncoding in outputEncodings { 95 | length += 2 96 | length += outputEncoding.length 97 | } 98 | } 99 | 100 | if preferredRole.rawValue != 0 { 101 | length += 2 102 | } 103 | 104 | return length 105 | } 106 | } 107 | 108 | enum RoleType: UInt8 { 109 | case unknown = 0 110 | case input = 1 111 | case output = 2 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Sources/AndroidTVRemoteControl/Network/PairingNetwork/Pairing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Pairing.swift 3 | // 4 | // 5 | // Created by Roman Odyshew on 15.10.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension PairingNetwork { 11 | struct PairingRequest: RequestDataProtocol { 12 | let clientName: String 13 | let serviceName: String 14 | 15 | let protocolVersion = ProtocolVersion2() 16 | let statusCode: Status = .ok 17 | 18 | var data: Data { 19 | var data = Data() 20 | data.append(statusCode.data) 21 | data.append(protocolVersion.data) 22 | data.append(contentsOf: [0x52]) 23 | 24 | if serviceName.isEmpty && clientName.isEmpty { 25 | data.append(contentsOf: [0]) 26 | return data 27 | } 28 | 29 | var array: [UInt8] = [] 30 | 31 | if !serviceName.isEmpty { 32 | array.append(0xa) 33 | array.append(contentsOf: Encoder.encodeVarint(UInt(serviceName.utf8.count))) 34 | array.append(contentsOf: serviceName.utf8) 35 | } 36 | 37 | if !clientName.isEmpty { 38 | array.append(0x12) 39 | array.append(contentsOf: Encoder.encodeVarint(UInt(clientName.utf8.count))) 40 | array.append(contentsOf: clientName.utf8) 41 | } 42 | 43 | data.append(contentsOf: Encoder.encodeVarint(UInt(array.count))) 44 | data.append(contentsOf: array) 45 | return data 46 | } 47 | } 48 | } 49 | 50 | extension PairingNetwork { 51 | struct PairingResponse { 52 | var length: Data? 53 | var data: Data? 54 | 55 | var isSuccess: Bool { 56 | guard length != nil, let data = data else { 57 | return false 58 | } 59 | 60 | var successdData = ProtocolVersion2().data 61 | successdData.append(Status.ok.data) 62 | successdData.append(contentsOf: [0x5a, 0x0]) 63 | return data == successdData 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/AndroidTVRemoteControl/Network/PairingNetwork/PairingNetwork.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PairingNetwork.swift 3 | // 4 | // 5 | // Created by Roman Odyshew on 15.10.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | struct PairingNetwork { 11 | struct ProtocolVersion2 { 12 | let data = Data([0x08, 0x02]) 13 | let size = 2 14 | } 15 | 16 | enum Status: Int { 17 | case unknown = 0 18 | case ok = 200 19 | case error = 400 20 | case badConfiguration = 401 21 | case badSecret = 402 22 | 23 | var data: Data { 24 | switch self { 25 | case .unknown: 26 | return Data() 27 | case .ok: 28 | return Data([0x10, 0xc8, 0x01]) 29 | case .error: 30 | return Data([0x10, 0x90, 0x02]) 31 | case .badConfiguration: 32 | return Data([0x10, 0x91, 0x02]) 33 | case .badSecret: 34 | return Data([0x10, 0x92, 0x02]) 35 | } 36 | } 37 | 38 | var size: Int { 39 | switch self { 40 | case .unknown: 41 | return 0 42 | default: 43 | return 3 44 | } 45 | } 46 | } 47 | 48 | enum EncodingType: UInt8 { 49 | case unknown = 0 50 | case alphanumeric = 1 51 | case numeric = 2 52 | case hexadecimal = 3 53 | case qrcode = 4 54 | } 55 | 56 | struct ParingEncoding { 57 | var symbolLength: UInt8 58 | var type: EncodingType 59 | 60 | var data: Data { 61 | var array: [UInt8] = [] 62 | 63 | if type.rawValue != 0 { 64 | array = [0x08, type.rawValue] 65 | } 66 | 67 | if symbolLength > 0 { 68 | array.append(16) 69 | if symbolLength < 128 { 70 | array.append(UInt8(symbolLength)) 71 | } else { 72 | let part2 = symbolLength / 128 73 | let part1 = symbolLength - (part2 - 1) * 128 74 | 75 | array.append(UInt8(part1)) 76 | array.append(UInt8(part2)) 77 | } 78 | } 79 | 80 | return Data(array) 81 | } 82 | 83 | var length: UInt8 { 84 | return UInt8(data.count) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /Sources/AndroidTVRemoteControl/Network/PairingNetwork/Secret.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Secret.swift 3 | // 4 | // 5 | // Created by Roman Odyshew on 15.10.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | extension PairingNetwork { 11 | struct SecretRequest: RequestDataProtocol { 12 | let status: Status = .ok 13 | let protocolVersion = ProtocolVersion2() 14 | let encodedCert: [UInt8] 15 | 16 | private let unknownFields: [UInt8] = [0xc2, 0x02] 17 | 18 | var data: Data { 19 | var data = Data() 20 | 21 | data.append(protocolVersion.data) 22 | data.append(status.data) 23 | 24 | data.append(contentsOf: unknownFields) 25 | data.append(contentsOf: [UInt8(encodedCert.count + 2), 0xa, UInt8(encodedCert.count)]) 26 | data.append(contentsOf: encodedCert) 27 | 28 | return data 29 | } 30 | } 31 | } 32 | 33 | extension PairingNetwork { 34 | struct SecretResponse { 35 | var data: Data? 36 | var code: String 37 | 38 | var isSuccess: Bool { 39 | guard data != nil else { 40 | return false 41 | } 42 | 43 | guard let size = Decoder.decodeVarint(Array(data!)) else { 44 | return false 45 | } 46 | 47 | guard let data = data, size.value == UInt(data.count - size.bytesCount) else { 48 | return false 49 | } 50 | 51 | guard code.count > 1, let firstNumber = UInt8(String(code.prefix(2)), radix: 16) else { 52 | return false 53 | } 54 | 55 | let subData: [UInt8] = [0xca, 0x02, 0x22, 0x0a] 56 | let subCount = data.count - size.bytesCount - ProtocolVersion2().size - Status.ok.size - subData.count - 1 57 | if subCount < 0 { 58 | return false 59 | } 60 | 61 | var successArray: [UInt8] = Encoder.encodeVarint(UInt(data.count - size.bytesCount)) 62 | successArray.append(contentsOf: ProtocolVersion2().data) 63 | successArray.append(contentsOf: Status.ok.data) 64 | successArray.append(contentsOf: subData) 65 | successArray.append(contentsOf: [UInt8(subCount), firstNumber]) 66 | 67 | return data.starts(with: successArray) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/AndroidTVRemoteControl/Network/RequestDataProtocol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RequestDataProtocol.swift 3 | // 4 | // 5 | // Created by Roman Odyshew on 15.10.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol RequestDataProtocol { 11 | var data: Data { get } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/AndroidTVRemoteControl/PairingManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PairingManager.swift 3 | // 4 | // 5 | // Created by Roman Odyshew on 15.10.2023. 6 | // 7 | 8 | import Foundation 9 | import Network 10 | import CryptoKit 11 | 12 | public class PairingManager { 13 | private let stateQueue = DispatchQueue(label: "pairing.state") 14 | private let connectQueue = DispatchQueue(label: "pairing.connect") 15 | 16 | private var pairingResponse = PairingNetwork.PairingResponse() 17 | private var optionResponse = PairingNetwork.OptionResponse() 18 | private var configResponse = PairingNetwork.ConfigurationResponse() 19 | 20 | private var connection: NWConnection? 21 | private var cryptoManager: CryptoManager 22 | private let tlsManager: TLSManager 23 | 24 | private var clientName = "client" 25 | private var serviceName = "service" 26 | private var code: String = "" 27 | 28 | public var logger: Logger? 29 | private let logPrefix = "Pairing: " 30 | 31 | public var stateChanged: ((PairingState)->())? 32 | 33 | private var pairingState: PairingState = .idle { 34 | didSet { 35 | let state = pairingState 36 | 37 | stateQueue.async { 38 | switch state { 39 | case .idle: 40 | self.logger?.infoLog(self.logPrefix + "idle") 41 | case .extractTLSparams: 42 | self.logger?.infoLog(self.logPrefix + "extract TLS parameters") 43 | case .connectionSetUp: 44 | self.logger?.infoLog(self.logPrefix + "connection set up") 45 | case .connectionPrepairing: 46 | self.logger?.infoLog(self.logPrefix + "connection prepairing") 47 | case .connected: 48 | self.logger?.infoLog(self.logPrefix + "connected") 49 | case .pairingRequestSent: 50 | self.logger?.infoLog(self.logPrefix + "pairing request has been sent") 51 | case .pairingResponseSuccess: 52 | self.logger?.infoLog(self.logPrefix + "pairing sesponse success") 53 | case .optionRequestSent: 54 | self.logger?.infoLog(self.logPrefix + "option request sent") 55 | case .optionResponseSuccess: 56 | self.logger?.infoLog(self.logPrefix + "option response success") 57 | case .confirmationRequestSent: 58 | self.logger?.infoLog(self.logPrefix + "confirmation request has been sent") 59 | case .confirmationResponseSuccess: 60 | self.logger?.infoLog(self.logPrefix + "confirmation response success") 61 | case .waitingCode: 62 | self.logger?.infoLog(self.logPrefix + "waiting code") 63 | case .secretSent: 64 | self.logger?.infoLog(self.logPrefix + "secret has been sent") 65 | case .successPaired: 66 | self.logger?.infoLog(self.logPrefix + "success paired") 67 | case .error(let error): 68 | self.logger?.errorLog(self.logPrefix + error.localizedDescription) 69 | } 70 | 71 | self.stateChanged?(state) 72 | } 73 | } 74 | } 75 | 76 | public init(_ tlsManager: TLSManager, _ cryptoManager: CryptoManager, _ logger: Logger? = nil) { 77 | self.tlsManager = tlsManager 78 | self.cryptoManager = cryptoManager 79 | self.logger = logger 80 | } 81 | 82 | public func connect(_ host: String, _ clientName: String, _ serviceName: String) { 83 | if host.isEmpty { 84 | logger?.errorLog(logPrefix + "host shouldn't be empty!") 85 | } 86 | 87 | self.clientName = clientName 88 | self.serviceName = serviceName 89 | 90 | pairingState = .extractTLSparams 91 | 92 | let tlsParams: NWParameters 93 | 94 | switch tlsManager.getNWParams(connectQueue) { 95 | case .Result(let params): 96 | tlsParams = params 97 | case .Error(let error): 98 | pairingState = .error(error) 99 | return 100 | } 101 | 102 | connection = NWConnection( 103 | host: NWEndpoint.Host(host), 104 | port: NWEndpoint.Port(integerLiteral: 6467), 105 | using: tlsParams) 106 | 107 | connection?.stateUpdateHandler = handleConnectionState 108 | logger?.infoLog(logPrefix + "connecting " + host + ":6467") 109 | connection?.start(queue: connectQueue) 110 | } 111 | 112 | public func disconnect() { 113 | logger?.infoLog(logPrefix + "disconnect") 114 | connection?.stateUpdateHandler = nil 115 | connection?.cancel() 116 | connection = nil 117 | } 118 | 119 | public func sendSecret(_ code: String) { 120 | // Set the code for secret transmission 121 | logger?.debugLog("code: " + code) 122 | self.code = code 123 | let secret: [UInt8] 124 | switch cryptoManager.getEncodedCert(code) { 125 | case .Result(let data): 126 | secret = data 127 | case .Error(let error): 128 | pairingState = .error(error) 129 | disconnect() 130 | return 131 | } 132 | 133 | send(PairingNetwork.SecretRequest(encodedCert: secret)) 134 | pairingState = .secretSent 135 | 136 | receive() 137 | } 138 | 139 | private func handleConnectionState(_ state: NWConnection.State) { 140 | switch state { 141 | case .setup: 142 | pairingState = .connectionSetUp 143 | case .waiting(let error): 144 | pairingState = .error(.connectionWaitingError(error)) 145 | disconnect() 146 | case .preparing: 147 | pairingState = .connectionPrepairing 148 | case .ready: 149 | pairingState = .connected 150 | 151 | pairingResponse = PairingNetwork.PairingResponse() 152 | logger?.debugLog(logPrefix + "Sending pairing request") 153 | send(PairingNetwork.PairingRequest(clientName: clientName, serviceName: serviceName)) 154 | pairingState = .pairingRequestSent 155 | 156 | receive() 157 | case .failed(let error): 158 | pairingState = .error(.connectionFailed(error)) 159 | disconnect() 160 | case .cancelled: 161 | pairingState = .error(.connectionCanceled) 162 | disconnect() 163 | default: 164 | break 165 | } 166 | } 167 | 168 | private func receive() { 169 | connection?.receive(minimumIncompleteLength: 1, maximumLength: 256) { [weak self] (data, context, isComplete, error) in 170 | guard let `self` = self else { return } 171 | 172 | if let error = error { 173 | self.pairingState = .error(.receiveDataError(error)) 174 | return 175 | } 176 | 177 | guard let data = data, !data.isEmpty, isComplete == false else { 178 | self.logger?.infoLog(self.logPrefix + "Empty or completion data received") 179 | return 180 | } 181 | 182 | self.logger?.debugLog(self.logPrefix + "recived: \(Array(data))") 183 | 184 | switch self.pairingState { 185 | case .pairingRequestSent: 186 | guard pairingResponse.length != nil else { 187 | self.logger?.debugLog(self.logPrefix + "it's lentgh of pairing response") 188 | pairingResponse.length = data 189 | self.receive() 190 | return 191 | } 192 | 193 | pairingResponse.data = data 194 | guard pairingResponse.isSuccess else { 195 | self.pairingState = .error(.pairingNotSuccess(data)) 196 | return 197 | } 198 | 199 | self.logger?.debugLog(self.logPrefix + "it's pairing response data") 200 | self.pairingState = .pairingResponseSuccess 201 | 202 | optionResponse = PairingNetwork.OptionResponse() 203 | logger?.debugLog(self.logPrefix + "Sending option request") 204 | send(PairingNetwork.OptionRequest()) 205 | self.pairingState = .optionRequestSent 206 | self.receive() 207 | return 208 | 209 | case .optionRequestSent: 210 | guard optionResponse.length != nil else { 211 | self.logger?.debugLog(self.logPrefix + "it's lentgh of option response") 212 | optionResponse.length = data 213 | self.receive() 214 | return 215 | } 216 | 217 | self.logger?.debugLog(self.logPrefix + "it's option response data") 218 | optionResponse.data = data 219 | guard optionResponse.isSuccess else { 220 | self.pairingState = .error(.optionNotSuccess(data)) 221 | return 222 | } 223 | 224 | self.pairingState = .optionResponseSuccess 225 | configResponse = PairingNetwork.ConfigurationResponse() 226 | logger?.debugLog(self.logPrefix + "Sending configuration request") 227 | send(PairingNetwork.ConfigurationRequest()) 228 | self.pairingState = .confirmationRequestSent 229 | self.receive() 230 | return 231 | 232 | case .confirmationRequestSent: 233 | guard configResponse.length != nil else { 234 | self.logger?.debugLog(self.logPrefix + "it's lentgh of confirmation response") 235 | configResponse.length = data 236 | self.receive() 237 | return 238 | } 239 | 240 | self.logger?.debugLog(self.logPrefix + "it's confirmation response data") 241 | configResponse.data = data 242 | guard configResponse.isSuccess else { 243 | self.pairingState = .error(.configurationNotSuccess(data)) 244 | return 245 | } 246 | 247 | self.pairingState = .confirmationResponseSuccess 248 | self.pairingState = .waitingCode 249 | return 250 | case .secretSent: 251 | let secretResponse = PairingNetwork.SecretResponse(data: data, code: code) 252 | if secretResponse.isSuccess { 253 | self.pairingState = .successPaired 254 | } 255 | 256 | self.pairingState = secretResponse.isSuccess ? .successPaired : .error(.secretNotSuccess(data)) 257 | self.disconnect() 258 | default: 259 | return 260 | } 261 | } 262 | } 263 | 264 | private func send(_ request: RequestDataProtocol) { 265 | send(Data(Encoder.encodeVarint(UInt(request.data.count))), request.data) 266 | } 267 | 268 | private func send(_ data: Data, _ nextData: Data? = nil) { 269 | logger?.debugLog(logPrefix + "Sending data: \(Array(data))") 270 | connection?.send(content: data, completion: .contentProcessed({ [weak self] (error) in 271 | guard let `self` = self else { 272 | return 273 | } 274 | 275 | if let error = error { 276 | self.pairingState = .error(.sendDataError(error)) 277 | self.disconnect() 278 | return 279 | } 280 | 281 | self.logger?.debugLog(self.logPrefix + "Success sent") 282 | if let nextMessage = nextData { 283 | self.send(nextMessage) 284 | } 285 | })) 286 | } 287 | } 288 | 289 | extension PairingManager { 290 | public enum PairingState { 291 | case idle 292 | case extractTLSparams 293 | case connectionSetUp 294 | case connectionPrepairing 295 | case connected 296 | case pairingRequestSent 297 | case pairingResponseSuccess 298 | case optionRequestSent 299 | case optionResponseSuccess 300 | case confirmationRequestSent 301 | case confirmationResponseSuccess 302 | case waitingCode 303 | case secretSent 304 | case successPaired 305 | case error(AndroidTVRemoteControlError) 306 | } 307 | } 308 | -------------------------------------------------------------------------------- /Sources/AndroidTVRemoteControl/RemoteManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RemoteManager.swift 3 | // 4 | // 5 | // Created by Roman Odyshew on 15.10.2023. 6 | // 7 | 8 | import Foundation 9 | import Network 10 | 11 | public class RemoteManager { 12 | private let stateQueue = DispatchQueue(label: "remote.state") 13 | private let remoteQueue = DispatchQueue(label: "remote.connect") 14 | private let receiveQueue = DispatchQueue(label: "remote.receive") 15 | 16 | private var connection: NWConnection? 17 | private let tlsManager: TLSManager 18 | 19 | private var data = Data() 20 | private var secondConfigurationResponse = SecondConfigurationResponse() 21 | 22 | public var stateChanged: ((RemoteState)->())? 23 | public var receiveData: ((Data?, Error?)->Void)? 24 | public var deviceInfo: CommandNetwork.DeviceInfo 25 | 26 | public var logger: Logger? 27 | private let logPrefix = "Remote: " 28 | 29 | private var remoteState: RemoteState = .idle { 30 | didSet { 31 | let state = remoteState 32 | 33 | stateQueue.async { 34 | switch state { 35 | case .error(let error): 36 | self.logger?.errorLog(self.logPrefix + error.localizedDescription) 37 | case .connected: 38 | self.logger?.infoLog(self.logPrefix + "connected") 39 | case .idle: 40 | self.logger?.infoLog(self.logPrefix + "idle") 41 | case .connectionSetUp: 42 | self.logger?.infoLog(self.logPrefix + "connection set up") 43 | case .connectionPrepairing: 44 | self.logger?.infoLog(self.logPrefix + "connection preparing") 45 | case .fisrtConfigMessageReceived(let info): 46 | self.logger?.infoLog(self.logPrefix + String(format: "fisrt configuration message has been received: %@ %@ %@ %@ %@", info.vendor, info.model, info.appName, info.appBuild, info.version)) 47 | case .firstConfigSent: 48 | self.logger?.infoLog(self.logPrefix + "fisrt configuration has been sent") 49 | case .secondConfigSent: 50 | self.logger?.infoLog(self.logPrefix + "second configuration has been sent") 51 | case .paired(runningApp: let runningApp): 52 | self.logger?.infoLog(self.logPrefix + "paired, current running app: " + (runningApp ?? "Unknown")) 53 | } 54 | 55 | self.stateChanged?(state) 56 | } 57 | } 58 | } 59 | 60 | public init(_ tlsManager: TLSManager, _ deviceInfo: CommandNetwork.DeviceInfo, _ logger: Logger? = nil) { 61 | self.tlsManager = tlsManager 62 | self.deviceInfo = deviceInfo 63 | self.logger = logger 64 | } 65 | 66 | public func connect(_ host: String) { 67 | if host.isEmpty { 68 | logger?.errorLog(logPrefix + "host shouldn't be empty!") 69 | } 70 | 71 | let tlsParams: NWParameters 72 | 73 | switch tlsManager.getNWParams(remoteQueue) { 74 | case .Result(let params): 75 | tlsParams = params 76 | case .Error(let error): 77 | remoteState = .error(error) 78 | return 79 | } 80 | 81 | connection = NWConnection( 82 | host: NWEndpoint.Host(host), 83 | port: NWEndpoint.Port(integerLiteral: 6466), 84 | using: tlsParams) 85 | 86 | connection?.stateUpdateHandler = handleConnectionState 87 | logger?.infoLog(logPrefix + "connecting " + host + ":6466") 88 | secondConfigurationResponse = SecondConfigurationResponse() 89 | connection?.start(queue: remoteQueue) 90 | } 91 | 92 | public func disconnect() { 93 | logger?.infoLog(logPrefix + "disconnect") 94 | connection?.stateUpdateHandler = nil 95 | connection?.cancel() 96 | connection = nil 97 | } 98 | 99 | public func send(_ request: RequestDataProtocol) { 100 | send(Data(Encoder.encodeVarint(UInt(request.data.count))), request.data) 101 | } 102 | 103 | public func send(_ data: Data, _ nextData: Data? = nil) { 104 | logger?.debugLog(logPrefix + "Sending data: \(Array(data))") 105 | connection?.send(content: data, completion: .contentProcessed({ [weak self] (error) in 106 | guard let `self` = self else { 107 | return 108 | } 109 | 110 | if let error = error { 111 | self.remoteState = .error(.sendDataError(error)) 112 | self.disconnect() 113 | return 114 | } 115 | 116 | self.logger?.debugLog(self.logPrefix + "Success sent") 117 | if let nextMessage = nextData { 118 | self.send(nextMessage) 119 | } 120 | })) 121 | } 122 | 123 | private func handleConnectionState(_ state: NWConnection.State) { 124 | switch state { 125 | case .setup: 126 | remoteState = .connectionSetUp 127 | case .waiting(let error): 128 | remoteState = .error(.connectionWaitingError(error)) 129 | disconnect() 130 | case .preparing: 131 | remoteState = .connectionPrepairing 132 | case .ready: 133 | remoteState = .connected 134 | receive() 135 | case .failed(let error): 136 | remoteState = .error(.connectionFailed(error)) 137 | disconnect() 138 | case .cancelled: 139 | remoteState = .error(.connectionCanceled) 140 | disconnect() 141 | default: 142 | break 143 | } 144 | } 145 | 146 | private func receive() { 147 | connection?.receive(minimumIncompleteLength: 1, maximumLength: 512) { [weak self] (data, context, isComplete, error) in 148 | guard let `self` = self else { return } 149 | 150 | self.receiveQueue.async { 151 | self.receiveData?(data, error) 152 | } 153 | 154 | if let error = error { 155 | remoteState = .error(.receiveDataError(error)) 156 | return 157 | } 158 | 159 | guard let data = data, !data.isEmpty, isComplete == false else { 160 | self.logger?.infoLog(self.logPrefix + "Empty or completion data received") 161 | self.receive() 162 | return 163 | } 164 | 165 | self.data.append(data) 166 | self.handleData() 167 | } 168 | } 169 | 170 | private func handleData() { 171 | logger?.debugLog(logPrefix + "handle: \(Array(data))") 172 | if handlePing() { 173 | receive() 174 | return 175 | } 176 | 177 | switch remoteState { 178 | case .connected: 179 | guard let configMessage = CommandNetwork.AndroidTVConfigurationMessage(data) else { 180 | logger?.debugLog(logPrefix + "it's not configuration message") 181 | receive() 182 | return 183 | } 184 | 185 | data.removeAll() 186 | remoteState = .fisrtConfigMessageReceived(configMessage.deviceInfo) 187 | 188 | secondConfigurationResponse.modelName = configMessage.deviceInfo.model 189 | 190 | logger?.debugLog(logPrefix + "Sending first configuration request") 191 | send(CommandNetwork.FirstConfigurationRequest(deviceInfo: deviceInfo)) 192 | remoteState = .firstConfigSent 193 | receive() 194 | 195 | case .firstConfigSent: 196 | guard CommandNetwork.FirstConfigurationResponse(data: data).isSuccess else { 197 | logger?.debugLog(logPrefix + "it's not first configuration response") 198 | receive() 199 | return 200 | } 201 | 202 | logger?.debugLog(logPrefix + "first configuration response was received") 203 | data.removeAll() 204 | logger?.debugLog(logPrefix + "Sending second configuration request") 205 | send(CommandNetwork.SecondConfigurationRequest()) 206 | remoteState = .secondConfigSent 207 | receive() 208 | 209 | case .secondConfigSent: 210 | guard secondConfigurationResponse.parse(data) else { 211 | logger?.debugLog(logPrefix + "it's not second configuration response") 212 | receive() 213 | return 214 | } 215 | 216 | if secondConfigurationResponse.currentAppPart { 217 | logger?.debugLog(logPrefix + "second configuration response CURRENT APP - OK") 218 | } 219 | 220 | if secondConfigurationResponse.powerPart { 221 | logger?.debugLog(logPrefix + "second configuration response POWER - OK") 222 | } 223 | 224 | if secondConfigurationResponse.volumeLevelPart { 225 | logger?.debugLog(logPrefix + "second configuration response VOLUME LEVEL - OK") 226 | } 227 | 228 | data.removeAll() 229 | guard secondConfigurationResponse.readyFullResponse else { 230 | receive() 231 | return 232 | } 233 | 234 | remoteState = .paired(runningApp: secondConfigurationResponse.runAppName) 235 | receive() 236 | default: 237 | logger?.debugLog(logPrefix + "unrecognized data") 238 | if VolumeLevel(data) != nil { 239 | data.removeAll() 240 | } 241 | receive() 242 | return 243 | } 244 | } 245 | 246 | private func handlePing() -> Bool { 247 | guard let ping = CommandNetwork.Ping.extract(from: data) else { 248 | return false 249 | } 250 | 251 | logger?.debugLog(logPrefix + "ping has bin handled") 252 | data.removeAll() 253 | let pong = CommandNetwork.Pong(ping.val1) 254 | logger?.debugLog(logPrefix + "sending pong") 255 | send(pong.data) 256 | return true 257 | } 258 | } 259 | 260 | extension RemoteManager { 261 | public enum RemoteState { 262 | case idle 263 | case connectionSetUp 264 | case connectionPrepairing 265 | case connected 266 | case fisrtConfigMessageReceived(CommandNetwork.DeviceInfo) 267 | case firstConfigSent 268 | case secondConfigSent 269 | case paired(runningApp: String?) 270 | case error(AndroidTVRemoteControlError) 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /Sources/AndroidTVRemoteControl/TLSManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TLSManager.swift 3 | // 4 | // 5 | // Created by Roman Odyshew on 15.10.2023. 6 | // 7 | 8 | import Foundation 9 | import Network 10 | 11 | public class TLSManager { 12 | public var certificateProvider: () -> Result 13 | public var secTrustClosure: ((SecTrust)->())? 14 | 15 | public init(_ certificateProviderClosure: @escaping () -> Result) { 16 | self.certificateProvider = certificateProviderClosure 17 | } 18 | 19 | func getNWParams(_ queue: DispatchQueue) -> Result { 20 | var rawItems: CFArray? 21 | 22 | switch certificateProvider() { 23 | case .Result(let items): 24 | rawItems = items 25 | case .Error(let error): 26 | return .Error(error) 27 | } 28 | 29 | guard let items = rawItems as? Array> else { 30 | return .Error(.unexpectedCertData) 31 | } 32 | 33 | // Extract the CFTypeRef representing the SecIdentity and check that cfIdentity type is SecIdentityGetTypeID, cause we should use force unwrap 34 | guard let cfIdentity = items.first?[kSecImportItemIdentity as String] as? CFTypeRef, 35 | CFGetTypeID(cfIdentity) == SecIdentityGetTypeID() else { 36 | return .Error(.extractCFTypeRefError) 37 | } 38 | 39 | let clientIdentity = cfIdentity as! SecIdentity 40 | 41 | guard let secIdentity: sec_identity_t = sec_identity_create(clientIdentity) else { 42 | return .Error(.secIdentityCreateError) 43 | } 44 | 45 | let options = NWProtocolTLS.Options() 46 | 47 | sec_protocol_options_set_verify_block(options.securityProtocolOptions, { [weak self] (_, sec_trust, completionHandler) in 48 | let serverTrust = sec_trust_copy_ref(sec_trust).takeRetainedValue() 49 | 50 | self?.secTrustClosure?(serverTrust) 51 | 52 | // not check and accept all certificates 53 | completionHandler(true) 54 | }, queue) 55 | 56 | sec_protocol_options_set_challenge_block(options.securityProtocolOptions, { (_, completionHandler) in 57 | completionHandler(secIdentity) 58 | }, queue) 59 | 60 | return .Result(NWParameters(tls: options)) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/AndroidTVRemoteControl/coding/Decoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Decoder.swift 3 | // 4 | // 5 | // Created by Roman Odyshew on 16.12.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | class Decoder { 11 | static func decodeVarint(_ data: Data) -> (value: UInt, bytesCount: Int)? { 12 | return Self.decodeVarint(Array(data)) 13 | } 14 | 15 | static func decodeVarint(_ data: [UInt8]) -> (value: UInt, bytesCount: Int)? { 16 | guard data.first != nil else { 17 | return nil 18 | } 19 | 20 | var shift: UInt = 0 21 | var value: UInt = 0 22 | 23 | for byte in data { 24 | value |= (UInt(byte) & 0x7f) << shift 25 | if byte & 0x80 == 0 { 26 | return (value, Int(shift) / 7 + 1) 27 | } 28 | 29 | shift += 7 30 | 31 | if shift > 31 { 32 | return nil 33 | } 34 | } 35 | 36 | return (value, Int(shift) / 7 + 1) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/AndroidTVRemoteControl/coding/Encoder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Encoder.swift 3 | // 4 | // 5 | // Created by Roman Odyshew on 07.11.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | class Encoder { 11 | static func encodeVarint(_ value: UInt) -> [UInt8] { 12 | guard value > 127 else { 13 | return [UInt8(value)] 14 | } 15 | 16 | var encodedBytes: [UInt8] = [] 17 | var val = value 18 | 19 | while val != 0 { 20 | var byte = UInt8(val & 0x7F) 21 | val >>= 7 22 | if val != 0 { 23 | byte |= 0x80 24 | } 25 | encodedBytes.append(byte) 26 | } 27 | 28 | return encodedBytes 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/AndroidTVRemoteControl/misc/Logger.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Logger.swift 3 | // 4 | // 5 | // Created by Roman Odyshew on 05.12.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol Logger { 11 | func debugLog(_ str: String) 12 | 13 | func infoLog(_ str: String) 14 | 15 | func errorLog(_ str: String) 16 | } 17 | 18 | public class DefaultLogger: Logger { 19 | public init() {} 20 | 21 | public func debugLog(_ str: String) { 22 | log("Debug: " + str) 23 | } 24 | 25 | public func infoLog(_ str: String) { 26 | log("Info: " + str) 27 | } 28 | 29 | public func errorLog(_ str: String) { 30 | log("Error: " + str) 31 | } 32 | 33 | private func log(_ str: String) { 34 | NSLog(str) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/AndroidTVRemoteControl/misc/Result.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result.swift 3 | // 4 | // 5 | // Created by Roman Odyshew on 15.10.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum Result { 11 | case Result(T) 12 | case Error(AndroidTVRemoteControlError) 13 | } 14 | -------------------------------------------------------------------------------- /Tests/AndroidTVRemoteControlTests/AndroidTVRemoteControlTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import AndroidTVRemoteControl 3 | 4 | final class AndroidTVRemoteControlTests: XCTestCase { 5 | func testExample() throws { 6 | // XCTest Documenation 7 | // https://developer.apple.com/documentation/xctest 8 | 9 | // Defining Test Cases and Test Methods 10 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /assets/pairing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odyshewroman/AndroidTVRemoteControl/6021df93ee16f0535ca9de350ab16f4d5bb8163c/assets/pairing.png -------------------------------------------------------------------------------- /assets/preparing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/odyshewroman/AndroidTVRemoteControl/6021df93ee16f0535ca9de350ab16f4d5bb8163c/assets/preparing.png --------------------------------------------------------------------------------