From 9a95762379039c8ba512bb7ed8e4bfae252bffbc Mon Sep 17 00:00:00 2001 From: Eldhose Mathokkil Babu Date: Tue, 14 May 2024 08:46:22 -0700 Subject: [PATCH 1/2] Initial Live acitivty integration changes --- .../LiveActivities/LiveActivityManager.swift | 69 +++++++++ .../LiveActivityRegistrationRequest.swift | 59 ++++++++ .../LiveActivityTokenManager.swift | 141 +++++++++++++++++ .../LiveActivityTypeWrapper.swift | 39 +++++ .../LiveActivityTypeWrapperImpl.swift | 142 ++++++++++++++++++ 5 files changed, 450 insertions(+) create mode 100644 FirebaseMessaging/Sources/LiveActivities/LiveActivityManager.swift create mode 100644 FirebaseMessaging/Sources/LiveActivities/LiveActivityRegistrationRequest.swift create mode 100644 FirebaseMessaging/Sources/LiveActivities/LiveActivityTokenManager.swift create mode 100644 FirebaseMessaging/Sources/LiveActivities/LiveActivityTypeWrapper.swift create mode 100644 FirebaseMessaging/Sources/LiveActivities/LiveActivityTypeWrapperImpl.swift diff --git a/FirebaseMessaging/Sources/LiveActivities/LiveActivityManager.swift b/FirebaseMessaging/Sources/LiveActivities/LiveActivityManager.swift new file mode 100644 index 00000000000..4c7b2f19bef --- /dev/null +++ b/FirebaseMessaging/Sources/LiveActivities/LiveActivityManager.swift @@ -0,0 +1,69 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation +import ActivityKit +import UIKit + +/** + Live activity manager class for FCM SDK + + Functionalities: + - Keeps track of live activity updates (starting and ending) + - Keeps track of live activity token updates and push to start token updates. + - Uploads the updated tokens to FCM backend when needed. + + */ +@available(iOS 16.1, *) +public class LiveActivityManager{ + + // To keep track of registered Live Acitvities so that they can be invalidated + private static var acitivityWrappers = [LiveActivityTypeWrapper]() + + // Class to manage Live Activity tokens + private static let tokenManager:LiveActivityTokenManager = LiveActivityTokenManager.getInstance() + + // Log tag for printing logs + public static let LOG_TAG = "LAM# " + + public static func liveActivityRegsistration() -> RegistrationRequest{ + return RegistrationRequest() + } + + static func invalidateActivities(){ + Task{ + var refreshedIds :[String] = [] + for activityWrapper in acitivityWrappers{ + activityWrapper.invalidateActivities() + refreshedIds.append(contentsOf:activityWrapper.getActiveActivityIds()) + } + + await tokenManager.invalidateWith(activityIds: refreshedIds) + + NSLog(LOG_TAG + "Invalidated") + } + } + + static func setActivityWrappers(wrappers: [LiveActivityTypeWrapper]){ + acitivityWrappers = wrappers + } + + public static func getLiveAcitivityTokens() async -> [String:String] { + let tokens = await tokenManager.getTokens() + return tokens.mapValues { $0 }; + } + +} diff --git a/FirebaseMessaging/Sources/LiveActivities/LiveActivityRegistrationRequest.swift b/FirebaseMessaging/Sources/LiveActivities/LiveActivityRegistrationRequest.swift new file mode 100644 index 00000000000..641404b53cb --- /dev/null +++ b/FirebaseMessaging/Sources/LiveActivities/LiveActivityRegistrationRequest.swift @@ -0,0 +1,59 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation +import ActivityKit + +/** + Builder class to accept Live activity Registration requests + */ +@available(iOS 16.1, *) +public class RegistrationRequest{ + + let tokenManager:LiveActivityTokenManager = LiveActivityTokenManager.getInstance() + var acitivityDict = [String:LiveActivityTypeWrapper]() + + public func add(type: T.Type) -> RegistrationRequest{ + let key = String(describing: type) + acitivityDict[key] = LiveActivityTypeWrapperImpl() + return self + } + + /** + Registers the live activities and returns the Push to start id if supported. + + PTS id is returned only if iOS 17.2 or above and atleast one Live activity type is registered with FCM + */ + public func register() -> String?{ + var wrappers = [LiveActivityTypeWrapper]() + var ptsTokenId:String? = nil + + if(!acitivityDict.isEmpty){ + ptsTokenId = tokenManager.ptsTokenId + acitivityDict.first?.value.initPTSToken(ptsTokenId: ptsTokenId) + + for wrapper in acitivityDict.values{ + wrappers.append(wrapper) + wrapper.listenForActivityUpdates() + } + } + + LiveActivityManager.setActivityWrappers(wrappers: wrappers) + LiveActivityManager.invalidateActivities() + + return ptsTokenId + } +} diff --git a/FirebaseMessaging/Sources/LiveActivities/LiveActivityTokenManager.swift b/FirebaseMessaging/Sources/LiveActivities/LiveActivityTokenManager.swift new file mode 100644 index 00000000000..e37cad02d8b --- /dev/null +++ b/FirebaseMessaging/Sources/LiveActivities/LiveActivityTokenManager.swift @@ -0,0 +1,141 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/** + Token manager class which is synchronized. + */ +@available(iOS 16.1, *) +actor LiveActivityTokenManager{ + + private let TOKENS_DEFAULTS_KEY = "TOKENS_DEFAULTS_KEY" + private var activityTokens : [String:String] + private let userDefaults = UserDefaults(suiteName: "LAM")! + private static let shared = LiveActivityTokenManager() + public let ptsTokenId:String? + + /** + Returns the singleton instance of token manager. + */ + public static func getInstance() -> LiveActivityTokenManager{ + return shared + } + + private init(){ + self.activityTokens = userDefaults.dictionary(forKey: TOKENS_DEFAULTS_KEY) as? [String: String] ?? [:] + + if #available(iOS 17.2, *) { + //Initializing PTS token id only if iOS 17.2 or above + let PTS_TOKEN_ID_DEFAULTS_KEY = "PTS_TOKEN_ID_DEFAULTS_KEY" + var ptsId = userDefaults.string(forKey: PTS_TOKEN_ID_DEFAULTS_KEY) + if(ptsId == nil || ptsId == ""){ + ptsId = UUID().uuidString + userDefaults.set(ptsId, forKey: PTS_TOKEN_ID_DEFAULTS_KEY) + } + ptsTokenId = ptsId + }else{ + ptsTokenId = nil + } + } + + func getPTSTokenId() -> String?{ + return ptsTokenId + } + + func getTokens() -> [String:String]{ + return activityTokens + } + + func saveTokensToUserDefaults(){ + userDefaults.set(activityTokens, forKey: TOKENS_DEFAULTS_KEY) + } + + func haveTokenFor(activityId:String) -> Bool{ + return activityTokens.keys.contains(activityId) + } + + func checkAndUpdateTokenFor(activityId:String,activityToken:String){ + if(activityId=="" || activityToken == ""){ + return + } + + if (haveTokenFor(activityId: activityId)){ + let oldToken = activityTokens[activityId] + + if(oldToken == nil || oldToken == "" || oldToken != activityToken){ + //Token needs update + updateToken(id: activityId, token: activityToken, reason: .ActivityTokenUpdated) + } + }else{ + //New token + updateToken(id: activityId, token: activityToken, reason: .ActivityTokenAdded) + } + + } + + func checkAndRemoveTokenFor(activityId:String){ + if(activityTokens.keys.contains(activityId)){ + // Remove token + let token = activityTokens[activityId]! + updateToken(id: activityId, token: token, reason: .ActivityTokenRemoved) + } + } + + func invalidateWith(activityIds: [String]){ + //Checking and removing ended acitivities + for acitivtyId in activityTokens.keys{ + if(acitivtyId == ptsTokenId){ + //PTS tokens are meant to be updated and not removed. + continue + } + if(!activityIds.contains(where: {$0==acitivtyId})){ + checkAndRemoveTokenFor(activityId: acitivtyId) + } + } + } + + func updateToken(id:String,token:String,reason:TokenUpdateReason){ + NSLog(LiveActivityManager.LOG_TAG + "Token Update : " + String(describing: reason)) + if(reason == .ActivityTokenRemoved){ + activityTokens.removeValue(forKey: id) + saveTokensToUserDefaults() + // No need for FCM backend update. So returning. + return + } + + activityTokens[id] = token + saveTokensToUserDefaults() + + uploadToken(id: id, token: token) + } + + func uploadToken(id:String,token:String){ + NSLog(LiveActivityManager.LOG_TAG + "Token Upload:: Id: " + id + " token: " + token) + //TODO: Code for token upload to FCM backend. + + + } + + /** + Reasons for token update + */ + enum TokenUpdateReason { + case ActivityTokenRemoved + case ActivityTokenAdded + case ActivityTokenUpdated + } +} diff --git a/FirebaseMessaging/Sources/LiveActivities/LiveActivityTypeWrapper.swift b/FirebaseMessaging/Sources/LiveActivities/LiveActivityTypeWrapper.swift new file mode 100644 index 00000000000..89eeb880828 --- /dev/null +++ b/FirebaseMessaging/Sources/LiveActivities/LiveActivityTypeWrapper.swift @@ -0,0 +1,39 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation + +/** + Protocol class invalidate and fetch details of Live activity instance fir a specific live activity type. + */ +protocol LiveActivityTypeWrapper { + /** + Invalidate the local live activity records by syncing with Activitykit apis. + */ + func invalidateActivities() + /** + Gets the list of active live activity ids + */ + func getActiveActivityIds() -> [String] + /** + Initializes PTS token + */ + func initPTSToken(ptsTokenId:String?) + /** + Listens for live activity updates + */ + func listenForActivityUpdates() +} diff --git a/FirebaseMessaging/Sources/LiveActivities/LiveActivityTypeWrapperImpl.swift b/FirebaseMessaging/Sources/LiveActivities/LiveActivityTypeWrapperImpl.swift new file mode 100644 index 00000000000..bbdbef1e056 --- /dev/null +++ b/FirebaseMessaging/Sources/LiveActivities/LiveActivityTypeWrapperImpl.swift @@ -0,0 +1,142 @@ +/* + * Copyright 2017 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation +import ActivityKit + +/** + Wrapper class to hold Live activity type. + + This class is responsible for listening to live activity updates and token changes. + */ +@available(iOS 16.1, *) +class LiveActivityTypeWrapperImpl: LiveActivityTypeWrapper { + + private let tokenManager = LiveActivityTokenManager.getInstance() + private var listeningStatus = [String:Bool]() + + //Helper functions + + func getFormatedToken(token:Data) -> String{ + return token.reduce("") { + $0 + String(format: "%02x", $1) + } + } + + func checkAndAddActivity(activity:Activity){ + if (hasListenerSetFor(activityId: activity.id)){ + //We have already subscribed to activity and token updates for this live activity instance. + return + } + + listeningStatus[activity.id] = true + + Task{ + Task{ + if(activity.pushToken != nil){ + let activityToken = getFormatedToken(token: activity.pushToken!) + checkAndUpdateToken(activityId: activity.id, activityToken: activityToken) + } + for await pushToken in activity.pushTokenUpdates { + let activityToken = getFormatedToken(token: pushToken) + checkAndUpdateToken(activityId: activity.id, activityToken: activityToken) + } + } + Task{ + for await activityState in activity.activityStateUpdates { + if(activityState == ActivityState.ended || activity.activityState==ActivityState.dismissed){ + checkAndRemoveActivityToken(activityId: activity.id) + } + } + } + } + } + + func hasListenerSetFor(activityId:String) -> Bool{ + if(listeningStatus.keys.contains(activityId)){ + return listeningStatus[activityId]! + } + + return false + } + + func checkAndUpdateToken(activityId:String,activityToken:String) { + Task{ + await tokenManager.checkAndUpdateTokenFor(activityId: activityId, activityToken: activityToken) + } + } + + func checkAndRemoveActivityToken(activityId:String){ + Task{ + await tokenManager.checkAndRemoveTokenFor(activityId:activityId) + } + } + + //Protocol Functions + + /** + Requests for Push to start live activity token and listen for its updates. + */ + func initPTSToken(ptsTokenId:String?){ + if #available(iOS 17.2, *) { + Task{ + let ptsToken = Activity.pushToStartToken + if(ptsToken != nil){ + let ptsTokenString = getFormatedToken(token: ptsToken!) + checkAndUpdateToken(activityId: ptsTokenId!, activityToken: ptsTokenString) + } + + for await pushToken in Activity.pushToStartTokenUpdates { + let activityToken = getFormatedToken(token: pushToken) + //PTS token is saved against registration id + checkAndUpdateToken(activityId: ptsTokenId!, activityToken: activityToken) + } + } + } + } + + /** + Listens for live activity updates happening in the app for the current type. + */ + func listenForActivityUpdates(){ + Task{ + for await activity in Activity.activityUpdates { + if(activity.activityState==ActivityState.ended || activity.activityState==ActivityState.dismissed){ + checkAndRemoveActivityToken(activityId: activity.id) + }else if(activity.activityState==ActivityState.active){ + checkAndAddActivity(activity: activity) + } + } + } + } + + func invalidateActivities() { + Task{ + let activities = Activity.activities + + // Checking and adding new activities + for activity in activities { + checkAndAddActivity(activity:activity) + } + } + } + + func getActiveActivityIds() -> [String] { + let activities = Activity.activities + let activityIds = activities.map { $0.id } + return activityIds + } +} From b892c74fb2a96bbb0b7800e200e834c936e59378 Mon Sep 17 00:00:00 2001 From: Eldhose Mathokkil Babu Date: Tue, 14 May 2024 08:47:28 -0700 Subject: [PATCH 2/2] podspec changes to include live activity code changes --- FirebaseMessaging.podspec | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/FirebaseMessaging.podspec b/FirebaseMessaging.podspec index 29c94be68fa..897185d47df 100644 --- a/FirebaseMessaging.podspec +++ b/FirebaseMessaging.podspec @@ -38,6 +38,7 @@ device, and it is completely free. base_dir = "FirebaseMessaging/" s.source_files = [ base_dir + 'Sources/**/*', + base_dir + 'Sources/LiveActivities/*.{swift,h,m}', base_dir + 'Sources/Protogen/nanopb/*.h', base_dir + 'Interop/*.h', 'Interop/Analytics/Public/*.h', @@ -46,6 +47,9 @@ device, and it is completely free. ] s.public_header_files = base_dir + 'Sources/Public/FirebaseMessaging/*.h' s.library = 'sqlite3' +# s.prepare_command = <<-CMD +# echo "Bridging header has been created" +# CMD s.pod_target_xcconfig = { 'GCC_C_LANGUAGE_STANDARD' => 'c99', 'GCC_PREPROCESSOR_DEFINITIONS' => @@ -53,6 +57,7 @@ device, and it is completely free. 'PB_FIELD_32BIT=1 PB_NO_PACKED_STRUCTS=1 PB_ENABLE_MALLOC=1', # Unit tests do library imports using repo-root relative paths. 'HEADER_SEARCH_PATHS' => '"${PODS_TARGET_SRCROOT}"', +# 'SWIFT_OBJC_BRIDGING_HEADER' => '$(PODS_TARGET_SRCROOT)/FirebaseMessaging/Sources/LiveActivities/FirebaseMessaging-Bridging-Header.h', } s.ios.framework = 'SystemConfiguration' s.tvos.framework = 'SystemConfiguration'