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 Actions first iteration #2489

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Configuration/Entitlements/activate_special_entitlements.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ 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 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
16 changes: 16 additions & 0 deletions HomeAssistant.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,8 @@
420B100C2B1D204400D383D8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 420B100B2B1D204400D383D8 /* Assets.xcassets */; };
424A7F462B188946008C8DF3 /* WidgetBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424A7F452B188946008C8DF3 /* WidgetBackground.swift */; };
424A7F482B188BF3008C8DF3 /* WidgetContentMargin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424A7F472B188BF3008C8DF3 /* WidgetContentMargin.swift */; };
424DD0572B3508F40057E456 /* CarPlaySceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424DD0562B3508F40057E456 /* CarPlaySceneDelegate.swift */; };
424DD05A2B3509170057E456 /* CPFavoriteActionsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 424DD0592B3509170057E456 /* CPFavoriteActionsSection.swift */; };
426740A92B17391000C1DD73 /* Data+Hexadecimal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 426740A72B17390A00C1DD73 /* Data+Hexadecimal.swift */; };
429C72202B28D0EC00BCD558 /* Haptics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 429C721F2B28D0EC00BCD558 /* Haptics.swift */; };
42CA28BB2B1028330093B31A /* SimulatorThreadClientService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 42CA28BA2B1028330093B31A /* SimulatorThreadClientService.swift */; };
Expand Down Expand Up @@ -1626,6 +1628,8 @@
4242A2D12B2B5C9F00E9F001 /* es-MX */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = "es-MX"; path = "es-MX.lproj/AppIntentVocabulary.plist"; sourceTree = "<group>"; };
424A7F452B188946008C8DF3 /* WidgetBackground.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetBackground.swift; sourceTree = "<group>"; };
424A7F472B188BF3008C8DF3 /* WidgetContentMargin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetContentMargin.swift; sourceTree = "<group>"; };
424DD0562B3508F40057E456 /* CarPlaySceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CarPlaySceneDelegate.swift; sourceTree = "<group>"; };
424DD0592B3509170057E456 /* CPFavoriteActionsSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CPFavoriteActionsSection.swift; sourceTree = "<group>"; };
426740A72B17390A00C1DD73 /* Data+Hexadecimal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Hexadecimal.swift"; sourceTree = "<group>"; };
42805A132B0226050095414C /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Base; path = Base.lproj/AppIntentVocabulary.plist; sourceTree = "<group>"; };
429C721F2B28D0EC00BCD558 /* Haptics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Haptics.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2906,6 +2910,7 @@
11EFCDD424F5FA7E00314D85 /* Scenes */ = {
isa = PBXGroup;
children = (
424DD0562B3508F40057E456 /* CarPlaySceneDelegate.swift */,
11EFCDD524F5FA8D00314D85 /* WebViewSceneDelegate.swift */,
11EFCDD724F5FCBE00314D85 /* SettingsSceneDelegate.swift */,
11EFCDD924F5FE0600314D85 /* SceneActivity.swift */,
Expand Down Expand Up @@ -3017,6 +3022,14 @@
name = Frameworks;
sourceTree = "<group>";
};
424DD0582B3509170057E456 /* CarPlay */ = {
isa = PBXGroup;
children = (
424DD0592B3509170057E456 /* CPFavoriteActionsSection.swift */,
);
path = CarPlay;
sourceTree = "<group>";
};
426740A42B17348700C1DD73 /* Assets */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -3414,6 +3427,7 @@
children = (
42CFCD682B1F956D00CCEF4A /* DropSupport */,
B657A8E91CA646EB00121384 /* AppDelegate.swift */,
424DD0582B3509170057E456 /* CarPlay */,
D03D893720E0AF1B00D4F28D /* ClientEvents */,
11A183B22511BCF300CA326A /* LifecycleManager.swift */,
117EB13E2569AD3000049541 /* Notifications */,
Expand Down Expand Up @@ -5611,6 +5625,7 @@
11BD7B4D25B53D7F001826F0 /* AppMacBridgeStatusItemConfiguration.swift in Sources */,
11F55EED25D3B088003977AC /* NotificationDebugNotificationsViewController.swift in Sources */,
B6022223226DBA3800E8DBFE /* OnboardingNavigationViewController.swift in Sources */,
424DD0572B3508F40057E456 /* CarPlaySceneDelegate.swift in Sources */,
11DA6B4B27137A60008ADFAF /* InputAccessoryView.swift in Sources */,
11A71C6D24A4641600D9565F /* ZoneManagerEvent.swift in Sources */,
11F3D7512495434C00C05BBA /* SensorDetailViewController.swift in Sources */,
Expand Down Expand Up @@ -5658,6 +5673,7 @@
11EFCDD624F5FA8D00314D85 /* WebViewSceneDelegate.swift in Sources */,
1185DF94271FBA6100ED7D9A /* OnboardingAuthDetails.swift in Sources */,
B6DA3C7122690B1F00DE811C /* NotificationSoundsViewController.swift in Sources */,
424DD05A2B3509170057E456 /* CPFavoriteActionsSection.swift in Sources */,
D0C88462211ED16300CCB501 /* OnboardingAuth.swift in Sources */,
11EFCDDC24F6065F00314D85 /* AboutSceneDelegate.swift in Sources */,
B6D8A32A2271455300FA765D /* OnboardingErrorViewController.swift in Sources */,
Expand Down
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 14.0, *), connectingSceneSession.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 Expand Up @@ -254,7 +258,7 @@
}

func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any? {
IntentHandlerFactory.handler(for: intent)

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

View check run for this annotation

Codecov / codecov/patch

Sources/App/AppDelegate.swift#L261

Added line #L261 was not covered by tests
}

// MARK: - Private helpers
Expand Down Expand Up @@ -497,7 +501,7 @@
}

@objc private func menuRelatedSettingDidChange(_ note: Notification) {
UIMenuSystem.main.setNeedsRebuild()

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

View check run for this annotation

Codecov / codecov/patch

Sources/App/AppDelegate.swift#L504

Added line #L504 was not covered by tests
}

// swiftlint:disable:next file_length
Expand Down
95 changes: 95 additions & 0 deletions Sources/App/CarPlay/CPFavoriteActionsSection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import CarPlay
import Foundation
import RealmSwift
import Shared

@available(iOS 15.0, *)
final class CPFavoriteActionsSection {
private var noActionsView: CPInformationTemplate = {
CPInformationTemplate(
title: L10n.About.Logo.title,
layout: .leading,
items: [
.init(title: L10n.CarPlay.NoActions.title, detail: nil),
],
actions: []
)
}()

Check warning on line 17 in Sources/App/CarPlay/CPFavoriteActionsSection.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/CarPlay/CPFavoriteActionsSection.swift#L8-L17

Added lines #L8 - L17 were not covered by tests

func list(for actions: Results<Action>) -> CPTemplate {
if actions.isEmpty {
noActionsView
} else {
CPListTemplate(
title: L10n.SettingsDetails.Actions.title, sections: [
section(actions: actions),
]
)
}
}

Check warning on line 29 in Sources/App/CarPlay/CPFavoriteActionsSection.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/CarPlay/CPFavoriteActionsSection.swift#L19-L29

Added lines #L19 - L29 were not covered by tests

private func section(actions: Results<Action>) -> CPListSection {
let items: [CPListItem] = actions.map { action in
let materialDesignIcon = MaterialDesignIcons(named: action.IconName)
.image(ofSize: CPListItem.maximumImageSize, color: UIColor(hex: action.IconColor))
let croppedIcon = cropImageToSquare(image: materialDesignIcon)!
let carPlayIcon = carPlayImage(from: croppedIcon)!
let item = CPListItem(
text: action.Name,
detailText: action.Text,
image: carPlayIcon
)
item.handler = { _, completion in
guard let server = Current.servers.server(for: action) else {
completion()
return
}
Current.api(for: server).HandleAction(actionID: action.ID, source: .CarPlay).pipe { result in
switch result {
case .fulfilled:
break
case let .rejected(error):
Current.Log.info(error)
}
completion()
}
}
return item
}

return CPListSection(items: items)
}

Check warning on line 61 in Sources/App/CarPlay/CPFavoriteActionsSection.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/CarPlay/CPFavoriteActionsSection.swift#L31-L61

Added lines #L31 - L61 were not covered by tests
}

// MARK: - CarPlay Image Resize

@available(iOS 15.0, *)
extension CPFavoriteActionsSection {
private func carPlayImage(from image: UIImage) -> UIImage? {
let imageAsset = UIImageAsset()
imageAsset.register(image, with: .current)
return imageAsset.image(with: .current)
}

Check warning on line 72 in Sources/App/CarPlay/CPFavoriteActionsSection.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/CarPlay/CPFavoriteActionsSection.swift#L68-L72

Added lines #L68 - L72 were not covered by tests

private func resizeImage(image: UIImage, newSize: CGSize) -> UIImage? {
let renderer = UIGraphicsImageRenderer(size: newSize)
let resizedImage = renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: newSize))
}
return resizedImage
}

Check warning on line 80 in Sources/App/CarPlay/CPFavoriteActionsSection.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/CarPlay/CPFavoriteActionsSection.swift#L74-L80

Added lines #L74 - L80 were not covered by tests

private func cropImageToSquare(image: UIImage) -> UIImage? {
let originalSize = image.size
let squareSize = min(originalSize.width, originalSize.height)
let newSize = CGSize(width: squareSize, height: squareSize)
let origin = CGPoint(x: (originalSize.width - squareSize) / 2, y: (originalSize.height - squareSize) / 2)

let renderer = UIGraphicsImageRenderer(size: newSize)
let croppedImage = renderer.image { _ in
image.draw(at: CGPoint(x: -origin.x, y: -origin.y))
}

return croppedImage
}

Check warning on line 94 in Sources/App/CarPlay/CPFavoriteActionsSection.swift

View check run for this annotation

Codecov / codecov/patch

Sources/App/CarPlay/CPFavoriteActionsSection.swift#L82-L94

Added lines #L82 - L94 were not covered by tests
}
11 changes: 11 additions & 0 deletions Sources/App/Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,17 @@
<string>About</string>
</dict>
</array>
<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>
</dict>
</dict>
<key>CFBundleVersion</key>
Expand Down
4 changes: 3 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 Down Expand Up @@ -784,4 +785,5 @@ 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.no_actions.title" = "Open iOS Companion App to create actions for CarPlay.";
44 changes: 44 additions & 0 deletions Sources/App/Scenes/CarPlaySceneDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import CarPlay
import Foundation
import Realm
import RealmSwift
import Shared

@available(iOS 15.0, *)
final class CarPlaySceneDelegate: NSObject, CPTemplateApplicationSceneDelegate {
private var interfaceController: CPInterfaceController?
private var rootTemplate: CPTemplate?
private let realm = Current.realm()

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

View check run for this annotation

Codecov / codecov/patch

Sources/App/Scenes/CarPlaySceneDelegate.swift#L11

Added line #L11 was not covered by tests
private var actionsToken: RLMNotificationToken?

func templateApplicationScene(
_ templateApplicationScene: CPTemplateApplicationScene,
didConnect interfaceController: CPInterfaceController
) {
self.interfaceController = interfaceController

let actions = realm.objects(Action.self)
.sorted(byKeyPath: "Position")
.filter("Scene == nil")

actionsToken = actions.observe { [weak self] _ in
self?.setActions(actions: actions)
}

setActions(actions: actions)
}

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

View check run for this annotation

Codecov / codecov/patch

Sources/App/Scenes/CarPlaySceneDelegate.swift#L17-L29

Added lines #L17 - L29 were not covered by tests

private func setActions(actions: Results<Action>) {
/* It's called 'tab' because the plan is to put this inside a tab bar
in the next iterations */
let actionsTab = CPFavoriteActionsSection().list(for: actions)

self.rootTemplate = actionsTab
guard let rootTemplate = rootTemplate else { return }
interfaceController?.setRootTemplate(
rootTemplate,
animated: true,
completion: nil
)
}

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

View check run for this annotation

Codecov / codecov/patch

Sources/App/Scenes/CarPlaySceneDelegate.swift#L31-L43

Added lines #L31 - L43 were not covered by tests
}
10 changes: 9 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,16 @@
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 .carPlay:
return .init(name: configurationName, sessionRole: .carTemplateApplication)

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

View check run for this annotation

Codecov / codecov/patch

Sources/App/Scenes/SceneActivity.swift#L42

Added line #L42 was not covered by tests
default:
return .init(name: configurationName, sessionRole: .windowApplication)
}
}
}
1 change: 1 addition & 0 deletions Sources/Shared/API/HAAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,7 @@
return when(resolved: realm.reentrantWrite {
let accuracyAuthorization: CLAccuracyAuthorization

if #available(watchOS 7, *) {

Check warning on line 518 in Sources/Shared/API/HAAPI.swift

View check run for this annotation

Codecov / codecov/patch

Sources/Shared/API/HAAPI.swift#L518

Added line #L518 was not covered by tests
accuracyAuthorization = CLLocationManager().accuracyAuthorization
} else {
accuracyAuthorization = .fullAccuracy
Expand Down Expand Up @@ -563,6 +563,7 @@
case Preview = "preview"
case SiriShortcut = "siriShortcut"
case URLHandler = "urlHandler"
case CarPlay = "carPlay"

public var description: String {
rawValue
Expand Down
9 changes: 9 additions & 0 deletions Sources/Shared/Resources/Swiftgen/Strings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ public enum L10n {
public static var appTitle: String { return L10n.tr("Localizable", "about.logo.app_title") }
/// Awaken Your Home
public static var tagline: String { return L10n.tr("Localizable", "about.logo.tagline") }
/// Home Assistant
public static var title: String { return L10n.tr("Localizable", "about.logo.title") }
}
public enum Review {
/// Leave a review
Expand Down Expand Up @@ -229,6 +231,13 @@ public enum L10n {
}
}

public enum CarPlay {
public enum NoActions {
/// Open iOS Companion App to create actions for CarPlay.
public static var title: String { return L10n.tr("Localizable", "carPlay.no_actions.title") }
}
}

public enum ClError {
public enum Description {
/// Deferred mode is not supported for the requested accuracy.
Expand Down
Loading