Implement SiriKit intents for CarPlay messaging (#1664)

* Add SiriKit intent handlers for CarPlay messaging (INSendMessageIntent, INSearchForMessagesIntent, INSetMessageAttributeIntent)

Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/8ef2a78b-83ee-4d9f-82b9-17b766c96312

Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>

* Address code review: consolidate intent routing, support multiple recipients, improve error for long messages

Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/8ef2a78b-83ee-4d9f-82b9-17b766c96312

Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>

* Fix unnecessary nil-coalescing in conversationIdentifier

Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/8ef2a78b-83ee-4d9f-82b9-17b766c96312

Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>

* Restrict INSendMessageIntent to single recipient (channel or direct message, not both)

Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/1798a03a-53b3-4a97-94e1-8281b552217a

Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>

* Fix Mac Catalyst build errors in SiriKit intent handlers

- SendMessageIntentHandler: guard `.noHandlesForValue` with
  #if targetEnvironment(macCatalyst) since the reason enum is iOS-only
- IntentMessageConverters: use .text instead of .tapback; INMessageType.tapback
  is unavailable on Mac Catalyst
- SearchForMessagesIntentHandler: replace .startDate/.endDate (iOS-only) with
  .startDateComponents/.endDateComponents + Calendar.date(from:) which work
  on all platforms

Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/9b61aad5-652c-4330-83b3-2303f10e4f12

Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>

* Add Siri authorization request at startup and NSSiriUsageDescription in Info.plist

Agent-Logs-Url: https://github.com/meshtastic/Meshtastic-Apple/sessions/be245ecb-2f0a-48d4-b931-4df889a6b6cc

Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: garthvh <1795163+garthvh@users.noreply.github.com>
This commit is contained in:
Copilot 2026-04-16 12:12:14 -07:00 committed by GitHub
parent 27b59fbb72
commit 1ae2b4bfef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 494 additions and 1 deletions

View file

@ -315,6 +315,11 @@
DDFFA7472B3A7F3C004730DB /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDFFA7462B3A7F3C004730DB /* Bundle.swift */; };
E3ED80145D0E873011982556 /* TAKServerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B37CCEE8B44A4BA123ED118 /* TAKServerManager.swift */; };
FE508F9AF5AD5DA20AA64DBF /* AccessoryManager+TAK.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82232A3CF2DD284ED5B9B8ED /* AccessoryManager+TAK.swift */; };
43C0CB306098FD005C2D489F /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA63632A10C2DD076FA222BE /* IntentHandler.swift */; };
7717E5954788B23527BACF65 /* IntentMessageConverters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39D80697D5BB72C4566988E0 /* IntentMessageConverters.swift */; };
AB4622DCF4B1D4115ED00312 /* SendMessageIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FCB3877F157D9011FA5C6CF /* SendMessageIntentHandler.swift */; };
B0E4EEF2D2C41A884A5E949C /* SearchForMessagesIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E644AE784C52500A9241481 /* SearchForMessagesIntentHandler.swift */; };
9BC51D7EF97090D149658843 /* SetMessageAttributeIntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = CDA6A18109101FA06A9FBBFB /* SetMessageAttributeIntentHandler.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -707,6 +712,11 @@
DDF924C926FBB953009FE055 /* ConnectedDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectedDevice.swift; sourceTree = "<group>"; };
DDFEB3BA29900C1200EE7472 /* CurrentConditionsCompact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CurrentConditionsCompact.swift; sourceTree = "<group>"; };
DDFFA7462B3A7F3C004730DB /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = "<group>"; };
CA63632A10C2DD076FA222BE /* IntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentHandler.swift; sourceTree = "<group>"; };
39D80697D5BB72C4566988E0 /* IntentMessageConverters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentMessageConverters.swift; sourceTree = "<group>"; };
5FCB3877F157D9011FA5C6CF /* SendMessageIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageIntentHandler.swift; sourceTree = "<group>"; };
0E644AE784C52500A9241481 /* SearchForMessagesIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchForMessagesIntentHandler.swift; sourceTree = "<group>"; };
CDA6A18109101FA06A9FBBFB /* SetMessageAttributeIntentHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetMessageAttributeIntentHandler.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
@ -935,6 +945,18 @@
path = AppIntents;
sourceTree = "<group>";
};
D05BD108B673E15AD4B01BC8 /* Intents */ = {
isa = PBXGroup;
children = (
CA63632A10C2DD076FA222BE /* IntentHandler.swift */,
39D80697D5BB72C4566988E0 /* IntentMessageConverters.swift */,
5FCB3877F157D9011FA5C6CF /* SendMessageIntentHandler.swift */,
0E644AE784C52500A9241481 /* SearchForMessagesIntentHandler.swift */,
CDA6A18109101FA06A9FBBFB /* SetMessageAttributeIntentHandler.swift */,
);
path = Intents;
sourceTree = "<group>";
};
C37572859BC745C4284A9B42 /* TAK */ = {
isa = PBXGroup;
children = (
@ -1230,6 +1252,7 @@
children = (
237AEB8D2E1FE120003B7CE3 /* Accessory */,
BCB6137F2C6728E700485544 /* AppIntents */,
D05BD108B673E15AD4B01BC8 /* Intents */,
DD1BD0EC2C603C5B008C0C70 /* Measurement */,
25F5D5BC2C3F6D7B008036E3 /* Router */,
DD7709392AA1ABA1007A8BF0 /* Tips */,
@ -1680,6 +1703,11 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
43C0CB306098FD005C2D489F /* IntentHandler.swift in Sources */,
7717E5954788B23527BACF65 /* IntentMessageConverters.swift in Sources */,
AB4622DCF4B1D4115ED00312 /* SendMessageIntentHandler.swift in Sources */,
B0E4EEF2D2C41A884A5E949C /* SearchForMessagesIntentHandler.swift in Sources */,
9BC51D7EF97090D149658843 /* SetMessageAttributeIntentHandler.swift in Sources */,
230BC3972E31071E0046BF2A /* AccessoryManager+Discovery.swift in Sources */,
25F26B1F2C2F611300C9CD9D /* AppData.swift in Sources */,
25F26B1E2C2F610D00C9CD9D /* Logger.swift in Sources */,

View file

@ -83,7 +83,9 @@
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>INIntentsSupported</key>
<array>
<string>Intent</string>
<string>INSendMessageIntent</string>
<string>INSearchForMessagesIntent</string>
<string>INSetMessageAttributeIntent</string>
</array>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
@ -119,6 +121,8 @@
<string>We use your location to display it on the mesh map as well as to have GPS coordinates to send to the connected device.</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>We use your location to display it on the mesh map, show and filter by distance as well as to have GPS coordinates to send to the connected device. Route Recording uses location in the background.</string>
<key>NSSiriUsageDescription</key>
<string>Siri is used for messaging to send and receive Meshtastic messages by voice and through CarPlay.</string>
<key>NSSupportsLiveActivities</key>
<true/>
<key>Privacy Bluetooth Always Usage Description</key>

View file

@ -0,0 +1,26 @@
//
// IntentHandler.swift
// Meshtastic
//
// Routes incoming SiriKit intents to the appropriate handler.
// Used by the app delegate for in-app intent handling to support
// CarPlay messaging and Siri voice commands.
//
import Intents
final class IntentHandler: INExtension {
override func handler(for intent: INIntent) -> Any? {
switch intent {
case is INSendMessageIntent:
return SendMessageIntentHandler()
case is INSearchForMessagesIntent:
return SearchForMessagesIntentHandler()
case is INSetMessageAttributeIntent:
return SetMessageAttributeIntentHandler()
default:
return nil
}
}
}

View file

@ -0,0 +1,81 @@
//
// IntentMessageConverters.swift
// Meshtastic
//
// Helpers for converting Core Data entities to SiriKit intent objects (INPerson, INMessage)
// used by the CarPlay messaging intent handlers.
//
import CoreData
import Intents
enum IntentMessageConverters {
/// Converts a `UserEntity` to an `INPerson` for use with SiriKit intents.
static func inPerson(from user: UserEntity) -> INPerson {
let handle = INPersonHandle(value: String(user.num), type: .unknown)
return INPerson(
personHandle: handle,
nameComponents: nil,
displayName: user.longName ?? user.shortName ?? "Node \(user.num)",
image: nil,
contactIdentifier: String(user.num),
customIdentifier: String(user.num)
)
}
/// Converts a `MessageEntity` to an `INMessage` for use with SiriKit search results.
static func inMessage(from message: MessageEntity) -> INMessage {
let sender: INPerson? = message.fromUser.map { inPerson(from: $0) }
let recipients: [INPerson]? = message.toUser.map { [inPerson(from: $0)] }
let dateSent = Date(timeIntervalSince1970: TimeInterval(message.messageTimestamp))
let groupName: INSpeakableString? = message.channel > 0
? INSpeakableString(spokenPhrase: "Channel \(message.channel)")
: nil
return INMessage(
identifier: String(message.messageId),
conversationIdentifier: conversationIdentifier(for: message),
content: message.messagePayload,
dateSent: dateSent,
sender: sender,
recipients: recipients,
groupName: groupName,
messageType: .text
)
}
/// Builds a stable conversation identifier from a message.
/// Channel messages use "channel-<N>", direct messages use "dm-<nodeNum>".
static func conversationIdentifier(for message: MessageEntity) -> String {
if let toUser = message.toUser {
return "dm-\(toUser.num)"
}
return "channel-\(message.channel)"
}
/// Searches for `UserEntity` objects whose name matches the given search term.
static func findUsers(matching searchTerm: String, in context: NSManagedObjectContext) -> [UserEntity] {
let fetchRequest: NSFetchRequest<UserEntity> = UserEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(
format: "longName CONTAINS[cd] %@ OR shortName CONTAINS[cd] %@",
searchTerm, searchTerm
)
return (try? context.fetch(fetchRequest)) ?? []
}
/// Looks up a `ChannelEntity` by matching name.
static func findChannels(matching name: String, in context: NSManagedObjectContext) -> [ChannelEntity] {
let fetchRequest: NSFetchRequest<ChannelEntity> = ChannelEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(
format: "name != nil AND name != '' AND name CONTAINS[cd] %@", name
)
return (try? context.fetch(fetchRequest)) ?? []
}
/// Resolves a channel index from a spoken group name, defaulting to the primary channel.
static func channelIndex(for name: String, in context: NSManagedObjectContext) -> Int {
let channels = findChannels(matching: name, in: context)
return channels.first.map { Int($0.index) } ?? 0
}
}

View file

@ -0,0 +1,106 @@
//
// SearchForMessagesIntentHandler.swift
// Meshtastic
//
// Handles INSearchForMessagesIntent for CarPlay and Siri.
// Queries Core Data for messages matching the intent criteria
// and returns them as INMessage objects.
//
import CoreData
import Intents
import OSLog
final class SearchForMessagesIntentHandler: NSObject, INSearchForMessagesIntentHandling {
/// Maximum number of messages to return in a single search.
private static let maxResults = 20
// MARK: - Handling
func handle(intent: INSearchForMessagesIntent) async -> INSearchForMessagesIntentResponse {
let context = PersistenceController.shared.container.viewContext
let messages: [INMessage] = await MainActor.run {
let fetchRequest: NSFetchRequest<MessageEntity> = MessageEntity.fetchRequest()
var predicates: [NSPredicate] = []
// Exclude admin and emoji messages
predicates.append(NSPredicate(format: "admin == NO"))
predicates.append(NSPredicate(format: "isEmoji == NO"))
// Filter by identifiers (specific message IDs)
if let identifiers = intent.identifiers, !identifiers.isEmpty {
let messageIds = identifiers.compactMap { Int64($0) }
if !messageIds.isEmpty {
predicates.append(NSPredicate(format: "messageId IN %@", messageIds))
}
}
// Filter by sender
if let senders = intent.senders, !senders.isEmpty {
let senderNums = senders.compactMap { $0.personHandle?.value }.compactMap { Int64($0) }
if !senderNums.isEmpty {
predicates.append(NSPredicate(format: "fromUser.num IN %@", senderNums))
}
}
// Filter by date range.
// INDateComponentsRange exposes DateComponents on all platforms;
// .startDate/.endDate are iOS-only and unavailable on Mac Catalyst.
if let dateRange = intent.dateTimeRange {
let calendar = Calendar.current
if let startComponents = dateRange.startDateComponents,
let startDate = calendar.date(from: startComponents) {
let startTimestamp = Int32(startDate.timeIntervalSince1970)
predicates.append(NSPredicate(format: "messageTimestamp >= %d", startTimestamp))
}
if let endComponents = dateRange.endDateComponents,
let endDate = calendar.date(from: endComponents) {
let endTimestamp = Int32(endDate.timeIntervalSince1970)
predicates.append(NSPredicate(format: "messageTimestamp <= %d", endTimestamp))
}
}
// Filter by group/channel name
if let groupNames = intent.speakableGroupNames, !groupNames.isEmpty {
let channelIndices: [Int32] = groupNames.compactMap { groupName in
let channels = IntentMessageConverters.findChannels(
matching: groupName.spokenPhrase, in: context
)
return channels.first.map { Int32($0.index) }
}
if !channelIndices.isEmpty {
predicates.append(NSPredicate(format: "channel IN %@", channelIndices))
}
}
// Filter by read/unread attribute
let attributes = intent.attributes
if attributes.contains(.read) {
predicates.append(NSPredicate(format: "read == YES"))
} else if attributes.contains(.unread) {
predicates.append(NSPredicate(format: "read == NO"))
}
fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: predicates)
fetchRequest.sortDescriptors = [
NSSortDescriptor(key: "messageTimestamp", ascending: false)
]
fetchRequest.fetchLimit = Self.maxResults
fetchRequest.relationshipKeyPathsForPrefetching = ["fromUser", "toUser"]
do {
let results = try context.fetch(fetchRequest)
return results.map { IntentMessageConverters.inMessage(from: $0) }
} catch {
Logger.services.error("CarPlay/Siri: Failed to search messages: \(error.localizedDescription)")
return []
}
}
let response = INSearchForMessagesIntentResponse(code: .success, userActivity: nil)
response.messages = messages
return response
}
}

View file

@ -0,0 +1,148 @@
//
// SendMessageIntentHandler.swift
// Meshtastic
//
// Handles INSendMessageIntent for CarPlay and Siri messaging.
// Meshtastic supports exactly one destination per message: either a single
// direct-message recipient (a mesh node) or a channel (speakableGroupName).
// Multiple recipients are not supported.
//
import CoreData
import Intents
import OSLog
final class SendMessageIntentHandler: NSObject, INSendMessageIntentHandling {
// MARK: - Resolution
func resolveRecipients(for intent: INSendMessageIntent) async -> [INSendMessageRecipientResolutionResult] {
guard let recipients = intent.recipients, !recipients.isEmpty else {
if intent.speakableGroupName != nil {
return []
}
return [.needsValue()]
}
// Meshtastic only supports a single direct-message recipient.
if recipients.count > 1 {
return [.unsupported(forReason: .noAccount)]
}
let context = PersistenceController.shared.container.viewContext
let searchTerm = recipients[0].displayName
let matchingUsers = await MainActor.run {
IntentMessageConverters.findUsers(matching: searchTerm, in: context)
}
if matchingUsers.isEmpty {
return [.unsupported(forReason: .noAccount)]
} else if matchingUsers.count == 1, let user = matchingUsers.first {
return [.success(with: IntentMessageConverters.inPerson(from: user))]
} else {
let persons = matchingUsers.map { IntentMessageConverters.inPerson(from: $0) }
return [.disambiguation(with: persons)]
}
}
func resolveContent(for intent: INSendMessageIntent) async -> INStringResolutionResult {
guard let content = intent.content, !content.isEmpty else {
return .needsValue()
}
guard let data = content.data(using: .utf8), data.count <= 200 else {
return .unsupported()
}
return .success(with: content)
}
func resolveSpeakableGroupName(for intent: INSendMessageIntent) async -> INSpeakableStringResolutionResult {
guard let groupName = intent.speakableGroupName else {
if let recipients = intent.recipients, !recipients.isEmpty {
return .notRequired()
}
return .needsValue()
}
let context = PersistenceController.shared.container.viewContext
let matchingChannels = await MainActor.run {
IntentMessageConverters.findChannels(matching: groupName.spokenPhrase, in: context)
}
if matchingChannels.count == 1, let channel = matchingChannels.first {
let speakable = INSpeakableString(spokenPhrase: channel.name ?? "Channel \(channel.index)")
return .success(with: speakable)
} else if matchingChannels.count > 1 {
let speakables = matchingChannels.map {
INSpeakableString(spokenPhrase: $0.name ?? "Channel \($0.index)")
}
return .disambiguation(with: speakables)
}
#if targetEnvironment(macCatalyst)
return .unsupported()
#else
return .unsupported(forReason: .noHandlesForValue)
#endif
}
// MARK: - Confirmation
func confirm(intent: INSendMessageIntent) async -> INSendMessageIntentResponse {
let connected = await AccessoryManager.shared.isConnected
guard connected else {
return INSendMessageIntentResponse(code: .failureRequiringAppLaunch, userActivity: nil)
}
return INSendMessageIntentResponse(code: .ready, userActivity: nil)
}
// MARK: - Handling
func handle(intent: INSendMessageIntent) async -> INSendMessageIntentResponse {
let connected = await AccessoryManager.shared.isConnected
guard connected else {
return INSendMessageIntentResponse(code: .failureRequiringAppLaunch, userActivity: nil)
}
guard let content = intent.content, !content.isEmpty else {
return INSendMessageIntentResponse(code: .failure, userActivity: nil)
}
do {
if let groupName = intent.speakableGroupName {
// Channel message
let context = PersistenceController.shared.container.viewContext
let channelIndex = await MainActor.run {
IntentMessageConverters.channelIndex(for: groupName.spokenPhrase, in: context)
}
try await AccessoryManager.shared.sendMessage(
message: content,
toUserNum: 0,
channel: Int32(channelIndex),
isEmoji: false,
replyID: 0
)
} else if let recipient = intent.recipients?.first,
let handleValue = recipient.personHandle?.value,
let nodeNum = Int64(handleValue) {
// Direct message to a single node
try await AccessoryManager.shared.sendMessage(
message: content,
toUserNum: nodeNum,
channel: 0,
isEmoji: false,
replyID: 0
)
} else {
return INSendMessageIntentResponse(code: .failure, userActivity: nil)
}
Logger.services.info("CarPlay/Siri: Message sent successfully")
return INSendMessageIntentResponse(code: .success, userActivity: nil)
} catch {
Logger.services.error("CarPlay/Siri: Failed to send message: \(error.localizedDescription)")
return INSendMessageIntentResponse(code: .failure, userActivity: nil)
}
}
}

View file

@ -0,0 +1,85 @@
//
// SetMessageAttributeIntentHandler.swift
// Meshtastic
//
// Handles INSetMessageAttributeIntent for CarPlay and Siri.
// Marks messages as read or unread in Core Data.
//
import CoreData
import Intents
import OSLog
final class SetMessageAttributeIntentHandler: NSObject, INSetMessageAttributeIntentHandling {
// MARK: - Resolution
func resolveAttribute(for intent: INSetMessageAttributeIntent) async -> INMessageAttributeResolutionResult {
let attribute = intent.attribute
guard attribute != .unknown else {
return .needsValue()
}
return .success(with: attribute)
}
// MARK: - Confirmation
func confirm(intent: INSetMessageAttributeIntent) async -> INSetMessageAttributeIntentResponse {
guard let identifiers = intent.identifiers, !identifiers.isEmpty else {
return INSetMessageAttributeIntentResponse(code: .failure, userActivity: nil)
}
return INSetMessageAttributeIntentResponse(code: .ready, userActivity: nil)
}
// MARK: - Handling
func handle(intent: INSetMessageAttributeIntent) async -> INSetMessageAttributeIntentResponse {
guard let identifiers = intent.identifiers, !identifiers.isEmpty else {
return INSetMessageAttributeIntentResponse(code: .failure, userActivity: nil)
}
let attribute = intent.attribute
let context = PersistenceController.shared.container.viewContext
let success: Bool = await MainActor.run {
let messageIds = identifiers.compactMap { Int64($0) }
guard !messageIds.isEmpty else { return false }
let fetchRequest: NSFetchRequest<MessageEntity> = MessageEntity.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "messageId IN %@", messageIds)
do {
let messages = try context.fetch(fetchRequest)
guard !messages.isEmpty else { return false }
for message in messages {
switch attribute {
case .read:
message.read = true
case .unread:
message.read = false
case .flagged, .unflagged:
// Meshtastic does not support message flagging
break
default:
break
}
}
if context.hasChanges {
try context.save()
}
Logger.services.info("CarPlay/Siri: Updated \(messages.count) message(s) to \(String(describing: attribute))")
return true
} catch {
Logger.services.error("CarPlay/Siri: Failed to update message attributes: \(error.localizedDescription)")
return false
}
}
return INSetMessageAttributeIntentResponse(
code: success ? .success : .failure,
userActivity: nil
)
}
}

View file

@ -5,6 +5,7 @@
// Created by Ben on 8/20/23.
//
import Intents
import SwiftUI
import OSLog
@ -29,8 +30,22 @@ class MeshtasticAppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificat
Task { @MainActor in
TAKServerManager.shared.initializeOnStartup()
}
// Request Siri authorization so intent donations work and CarPlay messaging is available.
#if !targetEnvironment(macCatalyst)
INPreferences.requestSiriAuthorization { status in
Logger.services.info("Siri authorization status: \(String(describing: status))")
}
#endif
return true
}
// MARK: - SiriKit Intent Handling
/// Routes incoming SiriKit intents to the appropriate handler for CarPlay and Siri messaging support.
func application(_ application: UIApplication, handlerFor intent: INIntent) -> Any? {
IntentHandler().handler(for: intent)
}
// Lets us show the notification in the app in the foreground
func userNotificationCenter(
_ center: UNUserNotificationCenter,