├── assets
├── pairing.png
└── preparing.png
├── Demo
└── Demo
│ ├── Demo
│ ├── cert.der
│ ├── cert.p12
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ ├── AccentColor.colorset
│ │ │ └── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── Info.plist
│ ├── AppDelegate.swift
│ ├── ViewController.swift
│ ├── SceneDelegate.swift
│ ├── Views.swift
│ └── RemoteTVManager.swift
│ └── Demo.xcodeproj
│ ├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ ├── xcuserdata
│ └── romanodyshew.xcuserdatad
│ │ └── xcschemes
│ │ └── xcschememanagement.plist
│ └── project.pbxproj
├── Sources
└── AndroidTVRemoteControl
│ ├── Network
│ ├── CommandNetwork
│ │ ├── CommandNetwork.swift
│ │ ├── SecodConfiguration.swift
│ │ ├── Ping.swift
│ │ ├── SecondConfigurationResponse.swift
│ │ ├── VolumeLevel.swift
│ │ └── AndroidTVConfigurationMessage.swift
│ ├── RequestDataProtocol.swift
│ └── PairingNetwork
│ │ ├── Configuration.swift
│ │ ├── Pairing.swift
│ │ ├── Secret.swift
│ │ ├── PairingNetwork.swift
│ │ └── Option.swift
│ ├── misc
│ ├── Result.swift
│ └── Logger.swift
│ ├── Commands
│ ├── Direction.swift
│ ├── KeyPress.swift
│ ├── DeepLink.swift
│ └── Key.swift
│ ├── coding
│ ├── Encoder.swift
│ └── Decoder.swift
│ ├── CertManager.swift
│ ├── TLSManager.swift
│ ├── Errors.swift
│ ├── CryptoManager.swift
│ ├── RemoteManager.swift
│ └── PairingManager.swift
├── Tests
└── AndroidTVRemoteControlTests
│ └── AndroidTVRemoteControlTests.swift
├── Package.swift
├── AndroidTVRemoteControl.podspec
├── LICENSE.md
└── README.md
/assets/pairing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/odyshewroman/AndroidTVRemoteControl/HEAD/assets/pairing.png
--------------------------------------------------------------------------------
/assets/preparing.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/odyshewroman/AndroidTVRemoteControl/HEAD/assets/preparing.png
--------------------------------------------------------------------------------
/Demo/Demo/Demo/cert.der:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/odyshewroman/AndroidTVRemoteControl/HEAD/Demo/Demo/Demo/cert.der
--------------------------------------------------------------------------------
/Demo/Demo/Demo/cert.p12:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/odyshewroman/AndroidTVRemoteControl/HEAD/Demo/Demo/Demo/cert.p12
--------------------------------------------------------------------------------
/Demo/Demo/Demo/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Demo/Demo/Demo.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/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/Network/CommandNetwork/SecodConfiguration.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SecodConfiguration.swift
3 | //
4 | //
5 | // Created by Roman Odyshew on 15.10.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | extension CommandNetwork {
11 | struct SecondConfigurationRequest: RequestDataProtocol {
12 | let data = Data([0x12, 0x3, 0x8, 0xEE, 0x4])
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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, timeout: Int = 60) -> 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 | let tcpOptions = NWProtocolTCP.Options()
61 | tcpOptions.connectionTimeout = timeout
62 |
63 | return .Result(NWParameters(tls: options, tcp: tcpOptions))
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.. 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/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/CommandNetwork/SecondConfigurationResponse.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SecondConfigurationResponse.swift
3 | //
4 | //
5 | // Created by Roman Odyshew on 15.10.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | struct SecondConfigurationResponse {
11 | private(set) var powerPart = false
12 | private(set) var currentAppPart = false
13 | private(set) var volumeLevelPart = false
14 |
15 | private(set) var runAppName: String?
16 |
17 | var modelName: String = ""
18 | var readyFullResponse: Bool {
19 | return powerPart && currentAppPart && volumeLevelPart
20 | }
21 |
22 | mutating func parse(_ data: Data) -> 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/Errors.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Errors.swift
3 | //
4 | //
5 | // Created by Roman Odyshew on 15.10.2023.
6 | //
7 |
8 | import Foundation
9 |
10 | public enum AndroidTVRemoteControlError {
11 | // TLS Error
12 | case unexpectedCertData
13 | case extractCFTypeRefError
14 | case secIdentityCreateError
15 |
16 | // Connecting Errors
17 | case toLongNames(description: String)
18 | case connectionCanceled
19 | case pairingNotSuccess(Data)
20 | case optionNotSuccess(Data)
21 | case configurationNotSuccess(Data)
22 | case secretNotSuccess(Data)
23 | case connectionWaitingError(Error)
24 | case connectionFailed(Error)
25 | case receiveDataError(Error)
26 | case sendDataError(Error)
27 |
28 | // Crypto Errors
29 | case invalidCode(description: String)
30 | case wrongCode
31 | case noSecAttributes
32 | case notRSAKey
33 | case notPublicKey
34 | case noKeySizeAttribute
35 | case noValueData
36 | case invalidCertData
37 |
38 | // Certificate Errors
39 | case createCertFromDataError
40 | case noClientPublicCertificate
41 | case noServerPublicCertificate
42 | case secTrustCopyKeyError
43 | case loadCertFromURLError(Error)
44 | case secPKCS12ImportNotSuccess
45 | case createTrustObjectError
46 | case secTrustCreateWithCertificatesNotSuccess(OSStatus)
47 | }
48 |
49 | extension AndroidTVRemoteControlError: Error {
50 | public var localizedDescription: String {
51 | switch self {
52 | case .unexpectedCertData:
53 | return "TLS Error: Unexpected Cert Data"
54 | case .extractCFTypeRefError:
55 | return "TLS Error: Extract CFTypeRef Error"
56 | case .secIdentityCreateError:
57 | return "TLS Error: SecIdentity Create Error"
58 | case .toLongNames(let description):
59 | return "Connection Error: To Long Names - " + description
60 | case .connectionCanceled:
61 | return "Connection Error: connection was canceled"
62 | case .pairingNotSuccess(let data):
63 | return "Connection Error: Pairing Not Success, data: \(Array(data))"
64 | case .optionNotSuccess(let data):
65 | return "Connection Error: option respponse is not success, data: \(Array(data))"
66 | case .configurationNotSuccess(let data):
67 | return "Connection Error: configuration not success, data: \(Array(data))"
68 | case .secretNotSuccess(let data):
69 | return "Connection Error: secret not success, data: \(Array(data))"
70 | case .connectionWaitingError(let error):
71 | return "Connection Error: connection waiting error: " + error.localizedDescription
72 | case .connectionFailed(let error):
73 | return "Connection Error: connection failed - " + error.localizedDescription
74 | case .receiveDataError(let error):
75 | return "Connection Error: receive data error - " + error.localizedDescription
76 | case .sendDataError(let error):
77 | return "Connection Error: send data error - " + error.localizedDescription
78 | case .invalidCode(let description):
79 | return "Crypto Error: invaid code - " + description
80 | case .wrongCode:
81 | return "Crypto Error: wrong code"
82 | case .noSecAttributes:
83 | return "Crypto Error: no SecAttributes"
84 | case .notRSAKey:
85 | return "Crypto Error: no RSAKey"
86 | case .notPublicKey:
87 | return "Crypto Error: no PublicKey"
88 | case .noKeySizeAttribute:
89 | return "Crypto Error: no KeySizeAttribute"
90 | case .noValueData:
91 | return "Crypto Error: no ValueData"
92 | case .invalidCertData:
93 | return "Crypto Error: invalid cert data"
94 | case .createCertFromDataError:
95 | return "Certificate Error: create cert from data error"
96 | case .noClientPublicCertificate:
97 | return "Certificate Error: no client public certificate"
98 | case .noServerPublicCertificate:
99 | return "Certificate Error: no server public certificate"
100 | case .secTrustCopyKeyError:
101 | return "Certificate Error: secTrustCopyKey Error"
102 | case .loadCertFromURLError(let error):
103 | return "Certificate Error: load cert from URL error - " + error.localizedDescription
104 | case .secPKCS12ImportNotSuccess:
105 | return "Certificate Error: secPKCS12Import is not success"
106 | case .createTrustObjectError:
107 | return "Certificate Error: create secTrust error"
108 | case .secTrustCreateWithCertificatesNotSuccess(let status):
109 | return "Certificate Error: secTrust create with certificate is not success, status: \(status)"
110 | }
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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, timeout: Int = 60) {
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, timeout: timeout) {
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/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, timeout: Int = 60) {
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, timeout: timeout) {
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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------