summaryrefslogtreecommitdiff
path: root/Example Apps/Example Swift/ProxyManager.swift
blob: 9a9d1014039042280d1dd4675a0f59c060f2726c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
//
//  ProxyManager.swift
//  SmartDeviceLink-ExampleSwift
//
//  Copyright © 2017 smartdevicelink. All rights reserved.
//

import UIKit
import SmartDeviceLink
import SmartDeviceLinkSwift

enum ProxyTransportType {
    case tcp
    case iap
}

enum ProxyState {
    case stopped
    case searching
    case connected
}

class ProxyManager: NSObject {
    private var sdlManager: SDLManager!
    private var buttonManager: ButtonManager!
    private var subscribeButtonManager: SubscribeButtonManager!
    private var vehicleDataManager: VehicleDataManager!
    private var performInteractionManager: PerformInteractionManager!
    private var remoteControlManager: RemoteControlManager!
    private var firstHMILevelState: SDLHMILevel
    private var isRemoteControlEnabled: Bool
    weak var delegate: ProxyManagerDelegate?


    // Singleton
    static let sharedManager = ProxyManager()
    private override init() {
        firstHMILevelState = .none
        isRemoteControlEnabled = false;
        super.init()
    }
}

// MARK: - SDL Configuration

extension ProxyManager {
    /// Configures the SDL Manager that handles data transfer beween this app and the car's head unit and starts searching for a connection to a head unit. There are two possible types of transport layers to use: TCP is used to connect wirelessly to SDL Core and is only available for debugging; iAP is used to connect to MFi (Made for iPhone) hardware and is must be used for production builds.
    ///
    /// - Parameter connectionType: The type of transport layer to use.
    func start(with proxyTransportType: ProxyTransportType) {
        delegate?.didChangeProxyState(ProxyState.searching)

        sdlManager = SDLManager(configuration: (proxyTransportType == .iap) ? ProxyManager.iapConfiguration : ProxyManager.tcpConfiguration, delegate: self)
        self.isRemoteControlEnabled = (proxyTransportType == .tcp)
        startManager()
    }

    /// Attempts to close the connection between the this app and the car's head unit. The `SDLManagerDelegate`'s `managerDidDisconnect()` is called when connection is actually closed.
    func stopConnection() {
        guard sdlManager != nil else {
            delegate?.didChangeProxyState(.stopped)
            return
        }

        DispatchQueue.main.async { [weak self] in
            self?.sdlManager.stop()
        }

        delegate?.didChangeProxyState(.stopped)
    }
}

// MARK: - SDL Configuration Helpers

private extension ProxyManager {
    /// Configures an iAP transport layer.
    ///
    /// - Returns: A SDLConfiguration object
    class var iapConfiguration: SDLConfiguration {
        let lifecycleConfiguration = SDLLifecycleConfiguration(appName: ExampleAppName, fullAppId: ExampleFullAppId)
        return setupManagerConfiguration(with: lifecycleConfiguration, enableRemote: false)
    }

    /// Configures a TCP transport layer with the IP address and port of the remote SDL Core instance.
    ///
    /// - Returns: A SDLConfiguration object
    class var tcpConfiguration: SDLConfiguration {
        let lifecycleConfiguration = SDLLifecycleConfiguration(appName: ExampleAppName, fullAppId: ExampleFullAppId, ipAddress: AppUserDefaults.shared.ipAddress!, port: UInt16(AppUserDefaults.shared.port!)!)
        return setupManagerConfiguration(with: lifecycleConfiguration, enableRemote: true)
    }

    /// Helper method for setting additional configuration parameters for both TCP and iAP transport layers.
    ///
    /// - Parameter lifecycleConfiguration: The transport layer configuration
    /// - Returns: A SDLConfiguration object
    class func setupManagerConfiguration(with lifecycleConfiguration: SDLLifecycleConfiguration, enableRemote: Bool) -> SDLConfiguration {
        lifecycleConfiguration.shortAppName = ExampleAppNameShort
        let appIcon = UIImage(named: ExampleAppLogoName)?.withRenderingMode(.alwaysOriginal)
        lifecycleConfiguration.appIcon = appIcon != nil ? SDLArtwork(image: appIcon!, persistent: true, as: .PNG) : nil
        lifecycleConfiguration.appType = .default

        // On actual hardware, the app requires permissions to do remote control which this example app will not have.
        // Only use the remote control type on the TCP connection.
        if enableRemote {
            lifecycleConfiguration.additionalAppTypes = [.remoteControl]
        }

        lifecycleConfiguration.language = .enUs
        lifecycleConfiguration.languagesSupported = [.enUs, .esMx, .frCa]
        lifecycleConfiguration.ttsName = [SDLTTSChunk(text: "S D L", type: .text)]

        let green = SDLRGBColor(red: 126, green: 188, blue: 121)
        let white = SDLRGBColor(red: 249, green: 251, blue: 254)
        let grey = SDLRGBColor(red: 186, green: 198, blue: 210)
        let darkGrey = SDLRGBColor(red: 57, green: 78, blue: 96)
        lifecycleConfiguration.dayColorScheme = SDLTemplateColorScheme(primaryRGBColor: green, secondaryRGBColor: grey, backgroundRGBColor: white)
        lifecycleConfiguration.nightColorScheme = SDLTemplateColorScheme(primaryRGBColor: green, secondaryRGBColor: grey, backgroundRGBColor: darkGrey)

        let lockScreenConfiguration = appIcon != nil ? SDLLockScreenConfiguration.enabledConfiguration(withAppIcon: appIcon!, backgroundColor: nil) : SDLLockScreenConfiguration.enabled()
        return SDLConfiguration(lifecycle: lifecycleConfiguration, lockScreen: lockScreenConfiguration, logging: logConfiguration(), fileManager: .default(), encryption: .default())
    }

    /// Sets the type of SDL debug logs that are visible and where to port the logs. There are 4 levels of log filtering, verbose, debug, warning and error. Verbose prints all SDL logs; error prints only the error logs. Adding SDLLogTargetFile to the targest will log to a text file on the iOS device. This file can be accessed via: iTunes > Your Device Name > File Sharing > Your App Name. Make sure `UIFileSharingEnabled` has been added to the application's info.plist and is set to `true`.
    ///
    /// - Returns: A SDLLogConfiguration object
    class func logConfiguration() -> SDLLogConfiguration {
        let logConfig = SDLLogConfiguration.default()
        let exampleLogFileModule = SDLLogFileModule(name: "SDL Swift Example App", files: ["ProxyManager", "AlertManager", "AudioManager", "ButtonManager", "SubscribeButtonManager", "MenuManager", "PerformInteractionManager", "RPCPermissionsManager", "VehicleDataManager", "RemoteControlManager"])
        logConfig.modules.insert(exampleLogFileModule)
        _ = logConfig.targets.insert(SDLLogTargetFile()) // Logs to file
        logConfig.globalLogLevel = .debug // Filters the logs
        return logConfig
    }

    /// Searches for a connection to a SDL enabled accessory. When a connection has been established, the ready handler is called. Even though the app is connected to SDL Core, it does not mean that RPCs can be immediately sent to the accessory as there is no guarentee that SDL Core is ready to receive RPCs. Monitor the `SDLManagerDelegate`'s `hmiLevel:didChangeToLevel:` to determine when to send RPCs.
    func startManager() {
        sdlManager.start(readyHandler: { [unowned self] (success, error) in
            guard success else {
                SDLLog.e("There was an error while starting up: \(String(describing: error))")
                self.stopConnection()
                return
            }

            self.delegate?.didChangeProxyState(ProxyState.connected)

            self.buttonManager = ButtonManager(sdlManager: self.sdlManager, updateScreenHandler: self.refreshUIHandler)
            self.subscribeButtonManager = SubscribeButtonManager(sdlManager: self.sdlManager)
            self.vehicleDataManager = VehicleDataManager(sdlManager: self.sdlManager, refreshUIHandler: self.refreshUIHandler)
            self.performInteractionManager = PerformInteractionManager(sdlManager: self.sdlManager)
            self.remoteControlManager = RemoteControlManager(sdlManager: self.sdlManager, enabled: self.isRemoteControlEnabled, homeButtons: self.buttonManager.allScreenSoftButtons())

            RPCPermissionsManager.setupPermissionsCallbacks(with: self.sdlManager)

            SDLLog.d("SDL file manager storage: \(self.sdlManager.fileManager.bytesAvailable / 1024 / 1024) mb")
        })
    }
}

// MARK: - SDLManagerDelegate

extension ProxyManager: SDLManagerDelegate {
    /// Called when the connection between this app and the module has closed.
    func managerDidDisconnect() {
        if delegate?.proxyState != .some(.stopped) {
            delegate?.didChangeProxyState(ProxyState.searching)
        }
        
        firstHMILevelState = .none
    }

    /// Called when the state of the SDL app has changed. The state limits the type of RPC that can be sent. Refer to the class documentation for each RPC to determine what state(s) the RPC can be sent.
    ///
    /// - Parameters:
    ///   - oldLevel: The old HMI Level
    ///   - newLevel: The new HMI Level
    func hmiLevel(_ oldLevel: SDLHMILevel, didChangeToLevel newLevel: SDLHMILevel) {
        if newLevel != .none && firstHMILevelState == .none {
            // This is our first time in a non-NONE state
            firstHMILevelState = newLevel

            // Subscribe to vehicle data.
            vehicleDataManager.subscribeToVehicleOdometer()

            // Start Remote Control Connection
            remoteControlManager.start()

            //Handle initial launch
            showInitialData()
        }

        switch newLevel {
        case .full:
            // The SDL app is in the foreground. Always try to show the initial state to guard against some possible weird states. Duplicates will be ignored by Core.
            subscribeButtonManager.subscribeToPresetButtons()
        case .limited: break // An active NAV or MEDIA SDL app is in the background
        case .background: break // The SDL app is not in the foreground
        case .none:
            // The SDL app is not yet running or is terminated
            firstHMILevelState = .none
            break
        default: break
        }
    }

    /// Called when the SDL app's HMI context changes.
    /// - Parameters:
    ///   - oldContext: The old HMI context
    ///   - newContext: The new HMI context
    func systemContext(_ oldContext: SDLSystemContext?, didChangeToContext newContext: SDLSystemContext) {
        switch newContext {
        case SDLSystemContext.alert: break // The SDL app's screen is obscured by an alert
        case SDLSystemContext.hmiObscured: break // The SDL app's screen is obscured
        case SDLSystemContext.main: break // The SDL app's main screen is open
        case SDLSystemContext.menu: break // The SDL app's menu is open
        case SDLSystemContext.voiceRecognitionSession: break // A voice recognition session is in progress
        default: break
        }
    }

    /// Called when the audio state of the SDL app has changed. The audio state only needs to be monitored if the app is streaming audio.
    ///
    /// - Parameters:
    ///   - oldState: The old audio streaming state
    ///   - newState: The new audio streaming state
    func audioStreamingState(_ oldState: SDLAudioStreamingState?, didChangeToState newState: SDLAudioStreamingState) {
        switch newState {
        case .audible: break        // The SDL app's audio can be heard
        case .notAudible: break     // The SDL app's audio cannot be heard
        case .attenuated: break     // The SDL app's audio volume has been lowered to let the system speak over the audio. This usually happens with voice recognition commands.
        default: break
        }
    }

    /// Called when the car's head unit language is different from the default langage set in the SDLConfiguration AND the head unit language is supported by the app (as set in `languagesSupported` of SDLConfiguration). This method is only called when a connection to Core is first established. If desired, you can update the app's name and text-to-speech name to reflect the head unit's language.
    ///
    /// - Parameter language: The head unit's current VR+TTS language
    /// - Parameter hmiLanguage: The head unit's current HMI language
    /// - Returns: A SDLLifecycleConfigurationUpdate object
    func managerShouldUpdateLifecycle(toLanguage language: SDLLanguage, hmiLanguage: SDLLanguage) -> SDLLifecycleConfigurationUpdate? {
        let update = SDLLifecycleConfigurationUpdate()
        switch hmiLanguage {
        case .enUs:
            update.appName = ExampleAppName
        case .esMx:
            update.appName = ExampleAppNameSpanish
        case .frCa:
            update.appName = ExampleAppNameFrench
        default:
            return nil
        }
        
        update.ttsName = [SDLTTSChunk(text: update.appName!, type: .text)]
        
        return update
    }

    /// Called when connected module information becomes available
    /// - Parameter systemInfo: The connected module's information
    /// - Returns: True to continue connecting, false to disconnect immediately
    func didReceiveSystemInfo(_ systemInfo: SDLSystemInfo) -> Bool {
        SDLLog.d("Example app got system info: \(systemInfo)")
        return true
    }
}

// MARK: - SDL UI

private extension ProxyManager {
    /// Handler for refreshing the UI
    var refreshUIHandler: RefreshUIHandler? {
        return { [unowned self] () in
            self.updateScreen()
        }
    }

    /// Set the template and create the UI
    func showInitialData() {
        // Send static menu items and soft buttons
        createMenuAndGlobalVoiceCommands()
        sdlManager.screenManager.softButtonObjects = buttonManager.allScreenSoftButtons()

        guard sdlManager.hmiLevel == .full else { return }

        sdlManager.screenManager.changeLayout(SDLTemplateConfiguration(predefinedLayout: .nonMedia), withCompletionHandler: nil)

        updateScreen()
    }

    /// Update the UI's textfields, images and soft buttons
    func updateScreen() {
        guard sdlManager.hmiLevel == .full else { return }

        let screenManager = sdlManager.screenManager
        let isTextVisible = buttonManager.textEnabled
        let areImagesVisible = buttonManager.imagesEnabled

        screenManager.beginUpdates()
        screenManager.textAlignment = .left
        screenManager.title = isTextVisible ? "Home" : nil
        screenManager.textField1 = isTextVisible ? SmartDeviceLinkText : nil
        screenManager.textField2 = isTextVisible ? "Swift \(ExampleAppText)" : nil
        screenManager.textField3 = isTextVisible ? vehicleDataManager.vehicleOdometerData : nil

        // Primary graphic
        if imageFieldSupported(imageFieldName: .graphic) {
            screenManager.primaryGraphic = areImagesVisible ? SDLArtwork(image: UIImage(named: ExampleAppLogoName)!.withRenderingMode(.alwaysOriginal), persistent: true, as: .PNG) : nil
        }
        
        // Secondary graphic
        if imageFieldSupported(imageFieldName: .secondaryGraphic) {
            screenManager.secondaryGraphic = areImagesVisible ? SDLArtwork(image: UIImage(named: CarBWIconImageName)!, persistent: true, as: .PNG) : nil
        }
        
        screenManager.endUpdates(completionHandler: { (error) in
            guard error != nil else { return }
            SDLLog.e("Textfields, graphics and soft buttons failed to update: \(error!.localizedDescription)")
        })
    }

    /// Send static menu data
    func createMenuAndGlobalVoiceCommands() {
        // Send the root menu items
        let screenManager = sdlManager.screenManager
        let menuItems = MenuManager.allMenuItems(with: sdlManager, choiceSetManager: performInteractionManager, remoteManager: remoteControlManager)
        let voiceMenuItems = MenuManager.allVoiceMenuItems(with: sdlManager)

        if !menuItems.isEmpty { screenManager.menu = menuItems }
        if !voiceMenuItems.isEmpty { screenManager.voiceCommands = voiceMenuItems }
    }

    /// Checks if SDL Core's HMI current template supports the template image field (i.e. primary graphic, secondary graphic, etc.)
    ///
    /// - Parameter imageFieldName: The name for the image field
    /// - Returns:                  True if the image field is supported, false if not
    func imageFieldSupported(imageFieldName: SDLImageFieldName) -> Bool {
        return sdlManager.systemCapabilityManager.defaultMainWindowCapability?.imageFields?.first { $0.name == imageFieldName } != nil ? true : false
    }
}