Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CarPlay implementation #2320

Merged
merged 42 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
4669e17
CarPlay implementation
LuisFALopes Mar 9, 2023
d4d6a42
Fixed lint warnings
LuisFALopes Apr 17, 2023
4cffca9
Rename friendlyState to localizedState
ItzSwirlz Nov 10, 2023
7f1628c
Update Sources/App/Resources/en.lproj/Localizable.strings
LuisFALopes Nov 21, 2023
a63ffa7
Changed CarPlay scene configuration.
LuisFALopes Nov 21, 2023
2c2bf87
Supported domains refactored;
LuisFALopes Nov 21, 2023
06b30d8
Use the available global UserDefaults.
LuisFALopes Nov 21, 2023
fc50980
entitlement injected via script.
LuisFALopes Dec 27, 2023
c86bac6
Fix script
LuisFALopes Dec 27, 2023
e5d66b4
removed deprecated methods
LuisFALopes Dec 28, 2023
795abc5
Changed getServer to not depend on connected status.
LuisFALopes Dec 28, 2023
02954d4
Removed unnecessary sort
LuisFALopes Dec 28, 2023
d225a8b
Fix linting problems.
LuisFALopes Dec 28, 2023
de8de76
Changed method name to match method output.
LuisFALopes Dec 28, 2023
5f3ecc7
Renamed method.
LuisFALopes Dec 28, 2023
bc1f1ee
Removed code that was checking if the server was connected.
LuisFALopes Dec 29, 2023
771fd52
CarPlay improvements
bgoncal Jan 2, 2024
4595e80
CarPlay Improvements
bgoncal Jan 3, 2024
c4f08b5
Moved pagination buttons to trailingNavigationBarButtons.
LuisFALopes Jan 3, 2024
df16528
Localize domain name
bgoncal Jan 4, 2024
d1479fa
Improve localization
bgoncal Jan 4, 2024
58e6a44
Remove unused strings
bgoncal Jan 4, 2024
f574960
Improve icon logic
bgoncal Jan 4, 2024
2840b76
Improve domain icon
bgoncal Jan 4, 2024
cdddfe1
WIP Add color for lights on
bgoncal Jan 4, 2024
a36419a
Use type safe domain
bgoncal Jan 4, 2024
303df7b
Pass to entities list just it's domain cache
bgoncal Jan 4, 2024
779b9cc
Fix domain sorting
bgoncal Jan 4, 2024
7640f7f
Make states type safe
bgoncal Jan 4, 2024
4049722
Lint
bgoncal Jan 4, 2024
ceb9beb
Add typed request for tap action
bgoncal Jan 4, 2024
b574c62
Improve typed request
bgoncal Jan 4, 2024
25b438a
Fix typed requests
bgoncal Jan 4, 2024
766ea0d
Fix missing input_boolean domain.
LuisFALopes Jan 4, 2024
f723742
Fix update entities state.
LuisFALopes Jan 4, 2024
b82b781
Fix input button service.
LuisFALopes Jan 4, 2024
d4aa317
Add lock operations alert confirmation
bgoncal Jan 4, 2024
b17cc21
Lint
bgoncal Jan 4, 2024
42dc2df
Merge branch 'master' into feature/carplay
bgoncal Jan 4, 2024
ce388d0
Reduce cyclomatic complexity
bgoncal Jan 5, 2024
3c82922
Cancel the entity subscription token when the entities list template …
LuisFALopes Jan 5, 2024
e6dc060
Update the domain list if the entitiesCachedStates changes.
LuisFALopes Jan 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 11 additions & 0 deletions Configuration/Entitlements/activate_special_entitlements.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ if [[ $TARGET_NAME = "App" ]]; then
fi
fi

if [[ $TARGET_NAME = "App" ]]; then
if [[ $CI && $CONFIGURATION != "Release" ]]; then
echo "warning: com.apple.developer.carplay-driving-task disabled for CI"
elif [[ ${ENABLE_CARPLAY} -eq 1 ]]; then
/usr/libexec/PlistBuddy -c "add com.apple.developer.carplay-driving-task bool true" "$ENTITLEMENTS_FILE"
else
echo "warning: com.apple.developer.carplay-driving-task entitlement disabled"
fi
fi


if [[ $TARGET_NAME = "App" ]]; then
if [[ $CI && $CONFIGURATION != "Release" ]]; then
echo "warning: Device name disabled for CI"
Expand Down
2 changes: 2 additions & 0 deletions Configuration/HomeAssistant.xcconfig
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ ENABLE_CRITICAL_ALERTS_QMQYCKL255 = 1
ENABLE_PUSH_PROVIDER_QMQYCKL255 = 1
ENABLE_DEVICE_NAME_QMQYCKL255 = 1
ENABLE_THREAD_NETWORK_CREDENTIALS_QMQYCKL255 = 1
ENABLE_CARPLAY_QMQYCKL255 = 1

// cascades down
PRODUCT_BUNDLE_IDENTIFIER = ${BUNDLE_ID_PREFIX}.HomeAssistant${BUNDLE_ID_SUFFIX}${PROVISIONING_SUFFIX}
Expand All @@ -30,6 +31,7 @@ ENABLE_CRITICAL_ALERTS[sdk=iphoneos*] = $(ENABLE_CRITICAL_ALERTS_$(DEVELOPMENT_T
ENABLE_PUSH_PROVIDER[sdk=iphoneos*] = $(ENABLE_PUSH_PROVIDER_$(DEVELOPMENT_TEAM))
ENABLE_DEVICE_NAME[sdk=iphoneos*] = $(ENABLE_DEVICE_NAME_$(DEVELOPMENT_TEAM))
ENABLE_THREAD_NETWORK_CREDENTIALS[sdk=iphoneos*] = $(ENABLE_THREAD_NETWORK_CREDENTIALS_$(DEVELOPMENT_TEAM))
ENABLE_CARPLAY[sdk=iphoneos*] = $(ENABLE_CARPLAY_$(DEVELOPMENT_TEAM))

// We mutate the entitlements at build time to support other development teams
CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES
Expand Down
76 changes: 76 additions & 0 deletions HomeAssistant.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

12 changes: 8 additions & 4 deletions Sources/App/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,14 @@
configurationForConnecting connectingSceneSession: UISceneSession,
options: UIScene.ConnectionOptions
) -> UISceneConfiguration {
let activity = options.userActivities
.compactMap { SceneActivity(activityIdentifier: $0.activityType) }
.first ?? .webView
return activity.configuration
if #available(iOS 16.0, *), connectingSceneSession.role == UISceneSession.Role.carTemplateApplication {
return SceneActivity.carPlay.configuration

Check warning on line 188 in Sources/App/AppDelegate.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/AppDelegate.swift#L188

Added line #L188 was not covered by tests
} else {
let activity = options.userActivities
.compactMap { SceneActivity(activityIdentifier: $0.activityType) }
.first ?? .webView
return activity.configuration
}
}

func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
Expand Down
11 changes: 11 additions & 0 deletions Sources/App/Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@
<true/>
<key>UISceneConfigurations</key>
<dict>
<key>CPTemplateApplicationSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>CPTemplateApplicationScene</string>
<key>UISceneConfigurationName</key>
<string>CarPlay</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).CarPlaySceneDelegate</string>
</dict>
</array>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
Expand Down
12 changes: 11 additions & 1 deletion Sources/App/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"about.home_assistant_on_facebook.title" = "Home Assistant on Facebook";
"about.home_assistant_on_twitter.title" = "Home Assistant on Twitter";
"about.logo.app_title" = "Home Assistant Companion";
"about.logo.title" = "Home Assistant";
"about.logo.tagline" = "Awaken Your Home";
"about.review.title" = "Leave a review";
"about.title" = "About";
Expand All @@ -32,6 +33,7 @@
"alerts.auth_required.message" = "The server has rejected your credentials, and you must sign in again to continue.";
"alerts.auth_required.title" = "You must sign in to continue";
"alerts.confirm.cancel" = "Cancel";
"alerts.confirm.confirm" = "Confirm";
"alerts.confirm.ok" = "OK";
"alerts.deprecations.notification_category.message" = "You must migrate to actions defined in the notification itself before %1$@.";
"alerts.deprecations.notification_category.title" = "Notification Categories are deprecated";
Expand Down Expand Up @@ -784,4 +786,12 @@ Home Assistant is free and open source home automation software with a focus on
"widgets.open_page.description" = "Open a frontend page in Home Assistant.";
"widgets.open_page.not_configured" = "No Pages Available";
"widgets.open_page.title" = "Open Page";
"yes_label" = "Yes";
"yes_label" = "Yes";
"carplay.navigation.button.next" = "Next";
"carplay.navigation.button.previous" = "Previous";
"carplay.labels.servers" = "Servers";
"carplay.labels.empty_domain_list" = "No domains available";
"carplay.labels.no_servers_available" = "No servers available. Add a server in the app.";
"carplay.labels.already_added_server" = "Already added";
"carplay.lock.confirmation.title" = "Are you sure you want to perform lock action on %@?";
"carplay.unlock.confirmation.title" = "Are you sure you want to perform unlock action on %@?";
221 changes: 221 additions & 0 deletions Sources/App/Scenes/CarPlaySceneDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import CarPlay
import Communicator
import HAKit
import PromiseKit
import Shared

public protocol EntitiesStateSubscription {
func subscribe()
func unsubscribe()
}

@available(iOS 16.0, *)
class CarPlaySceneDelegate: UIResponder {
private var interfaceController: CPInterfaceController?
private var entities: HACache<Set<HAEntity>>?
private var domainsListTemplate: DomainsListTemplate?
private var serverId: Identifier<Server>?

private let carPlayPreferredServerKey = "carPlay-server"

Check warning on line 19 in Sources/App/Scenes/CarPlaySceneDelegate.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/Scenes/CarPlaySceneDelegate.swift#L19

Added line #L19 was not covered by tests

private func setServer(server: Server) {
serverId = server.identifier
prefs.set(server.identifier.rawValue, forKey: carPlayPreferredServerKey)
setDomainListTemplate(for: server)
updateServerListButton()
}

Check warning on line 26 in Sources/App/Scenes/CarPlaySceneDelegate.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/Scenes/CarPlaySceneDelegate.swift#L21-L26

Added lines #L21 - L26 were not covered by tests

private func updateServerListButton() {
domainsListTemplate?.setServerListButton(show: Current.servers.all.count > 1)
}

Check warning on line 30 in Sources/App/Scenes/CarPlaySceneDelegate.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/Scenes/CarPlaySceneDelegate.swift#L28-L30

Added lines #L28 - L30 were not covered by tests

@objc private func updateServerList() {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.updateServerListButton()
if self.serverId == nil {
/// No server is selected
guard let server = self.getServer() else {
Current.Log.info("No server connected")
return
}
self.setServer(server: server)
}
}
}

Check warning on line 45 in Sources/App/Scenes/CarPlaySceneDelegate.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/Scenes/CarPlaySceneDelegate.swift#L32-L45

Added lines #L32 - L45 were not covered by tests

private func showNoServerAlert() {
guard interfaceController?.presentedTemplate == nil else {
return
}

let loginAlertAction = CPAlertAction(title: L10n.Carplay.Labels.alreadyAddedServer, style: .default) { _ in
if !Current.servers.all.isEmpty {
self.interfaceController?.dismissTemplate(animated: true, completion: nil)
}
}
let alertTemplate = CPAlertTemplate(
titleVariants: [L10n.Carplay.Labels.noServersAvailable],
actions: [loginAlertAction]
)
interfaceController?.presentTemplate(alertTemplate, animated: true, completion: nil)
}

Check warning on line 62 in Sources/App/Scenes/CarPlaySceneDelegate.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/Scenes/CarPlaySceneDelegate.swift#L47-L62

Added lines #L47 - L62 were not covered by tests

private func setDomainListTemplate(for server: Server) {
guard let interfaceController else { return }

let entities = Current.api(for: server).connection.caches.states

domainsListTemplate = DomainsListTemplate(
title: server.info.name,
entities: entities,
serverButtonHandler: { [weak self] _ in
self?.setServerListTemplate()
},
server: server
)

guard let domainsListTemplate else { return }

domainsListTemplate.interfaceController = interfaceController

interfaceController.setRootTemplate(domainsListTemplate.template, animated: true, completion: nil)
domainsListTemplate.updateSections()
}

Check warning on line 84 in Sources/App/Scenes/CarPlaySceneDelegate.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/Scenes/CarPlaySceneDelegate.swift#L64-L84

Added lines #L64 - L84 were not covered by tests

private func setServerListTemplate() {
var serverList: [CPListItem] = []
for server in Current.servers.all {
let serverItem = CPListItem(
text: server.info.name,
detailText: "\(server.info.connection.activeURLType.description) - \(server.info.connection.activeURL().absoluteString)"
)
serverItem.handler = { [weak self] _, completion in
self?.setServer(server: server)
if let templates = self?.interfaceController?.templates, templates.count > 1 {
self?.interfaceController?.popTemplate(animated: true, completion: nil)
}
completion()
}
serverItem.accessoryType = serverId == server.identifier ? .cloud : .none
serverList.append(serverItem)
}
let section = CPListSection(items: serverList)
let serverListTemplate = CPListTemplate(title: L10n.Carplay.Labels.servers, sections: [section])
interfaceController?.pushTemplate(serverListTemplate, animated: true, completion: nil)
}

Check warning on line 106 in Sources/App/Scenes/CarPlaySceneDelegate.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/Scenes/CarPlaySceneDelegate.swift#L86-L106

Added lines #L86 - L106 were not covered by tests

private func setEmptyTemplate(interfaceController: CPInterfaceController) {
interfaceController.setRootTemplate(CPInformationTemplate(
title: L10n.About.Logo.title,
layout: .leading,
items: [],
actions: []
), animated: true, completion: nil)
}

Check warning on line 115 in Sources/App/Scenes/CarPlaySceneDelegate.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/Scenes/CarPlaySceneDelegate.swift#L108-L115

Added lines #L108 - L115 were not covered by tests

/// Get server for ID or first server available
private func getServer(id: Identifier<Server>? = nil) -> Server? {
guard let id = id else {
return Current.servers.all.first
}
return Current.servers.server(for: id)
}

Check warning on line 123 in Sources/App/Scenes/CarPlaySceneDelegate.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/Scenes/CarPlaySceneDelegate.swift#L118-L123

Added lines #L118 - L123 were not covered by tests
}

// MARK: - CPTemplateApplicationSceneDelegate

@available(iOS 16.0, *)
extension CarPlaySceneDelegate: CPTemplateApplicationSceneDelegate {
func templateApplicationScene(
_ templateApplicationScene: CPTemplateApplicationScene,
didConnect interfaceController: CPInterfaceController
) {
self.interfaceController = interfaceController
self.interfaceController?.delegate = self

if let serverIdentifier = prefs.string(forKey: carPlayPreferredServerKey),
let selectedServer = Current.servers.server(forServerIdentifier: serverIdentifier) {
setServer(server: selectedServer)
} else if let server = getServer() {
setServer(server: server)
} else {
setEmptyTemplate(interfaceController: interfaceController)
}

updateServerList()

NotificationCenter.default.addObserver(
self,
selector: #selector(updateServerList),
name: HAConnectionState.didTransitionToStateNotification,
object: nil
)

NotificationCenter.default.addObserver(
self,
selector: #selector(updateServerList),
name: HomeAssistantAPI.didConnectNotification,
object: nil
)

/// Observer for servers list changes
Current.servers.add(observer: self)

if Current.servers.all.isEmpty {
showNoServerAlert()
}
}

Check warning on line 168 in Sources/App/Scenes/CarPlaySceneDelegate.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/Scenes/CarPlaySceneDelegate.swift#L133-L168

Added lines #L133 - L168 were not covered by tests

func templateApplicationScene(
_ templateApplicationScene: CPTemplateApplicationScene,
didDisconnect interfaceController: CPInterfaceController,
from window: CPWindow
) {
NotificationCenter.default.removeObserver(self)
Current.servers.remove(observer: self)
}

Check warning on line 177 in Sources/App/Scenes/CarPlaySceneDelegate.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/Scenes/CarPlaySceneDelegate.swift#L174-L177

Added lines #L174 - L177 were not covered by tests
}

// MARK: - ServerObserver

@available(iOS 16.0, *)
extension CarPlaySceneDelegate: ServerObserver {
func serversDidChange(_ serverManager: ServerManager) {
defer {
updateServerListButton()
}

guard let server = getServer(id: serverId) else {
serverId = nil

if let server = getServer() {
setServer(server: server)
} else if interfaceController?.presentedTemplate != nil {
interfaceController?.dismissTemplate(animated: true, completion: nil)
} else {
showNoServerAlert()
}

return
}
setServer(server: server)
}

Check warning on line 203 in Sources/App/Scenes/CarPlaySceneDelegate.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/Scenes/CarPlaySceneDelegate.swift#L184-L203

Added lines #L184 - L203 were not covered by tests
}

@available(iOS 16.0, *)
extension CarPlaySceneDelegate: CPInterfaceControllerDelegate {
func templateWillDisappear(_ aTemplate: CPTemplate, animated: Bool) {
domainsListTemplate?.templateWillDisappear(template: aTemplate)
}

Check warning on line 210 in Sources/App/Scenes/CarPlaySceneDelegate.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/Scenes/CarPlaySceneDelegate.swift#L208-L210

Added lines #L208 - L210 were not covered by tests

func templateWillAppear(_ aTemplate: CPTemplate, animated: Bool) {
domainsListTemplate?.templateWillAppear(template: aTemplate)
}

Check warning on line 214 in Sources/App/Scenes/CarPlaySceneDelegate.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/Scenes/CarPlaySceneDelegate.swift#L212-L214

Added lines #L212 - L214 were not covered by tests
}

protocol CarPlayTemplateProvider {
var template: CPTemplate { get set }
func templateWillDisappear(template: CPTemplate)
func templateWillAppear(template: CPTemplate)
}
8 changes: 7 additions & 1 deletion Sources/App/Scenes/SceneActivity.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
case webView
case settings
case about
case carPlay

init(activityIdentifier: String) {
self = Self.allCases.first(where: { $0.activityIdentifier == activityIdentifier }) ?? .webView
Expand All @@ -22,6 +23,7 @@
case .settings: return "ha.settings"
case .webView: return "ha.webview"
case .about: return "ha.about"
case .carPlay: return "ha.carPlay"

Check warning on line 26 in Sources/App/Scenes/SceneActivity.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/Scenes/SceneActivity.swift#L26

Added line #L26 was not covered by tests
}
}

Expand All @@ -30,10 +32,14 @@
case .webView: return "WebView"
case .settings: return "Settings"
case .about: return "About"
case .carPlay: return "CarPlay"
}
}

var configuration: UISceneConfiguration {
.init(name: configurationName, sessionRole: .windowApplication)
switch self {
case .webView, .settings, .about: return .init(name: configurationName, sessionRole: .windowApplication)
case .carPlay: return .init(name: configurationName, sessionRole: .carTemplateApplication)
}
}
}