Skip to content
Open
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
27 changes: 19 additions & 8 deletions OptimobileShared/MediaHelper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,28 @@
import Foundation

enum MediaHelper {
/// Use ``Region.US`` as fallback region.
static let mediaResizerBaseUrl: String = "https://i-us-east-1.app.delivery"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This URL was used here as a fallback URL, in case if no access to the persistent key-value storage.
Now, this code has the storage as a dependency in class init, and also will not be executed in the runtime.

Copy link
Member Author

@eligutovsky eligutovsky Dec 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it would be helpful to replace the notification content with the text "Error: Check Optimove AppGroup capabilities" if there is no storage available? This could help us reduce the number of integration issues related to missing images.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some playground, I found that optimobile Core data throwing an no db file.

enum Error: LocalizedError {
case noMediaUrlFound
case invalidPictureUrl(String)
}

static func getCompletePictureUrl(pictureUrlString: String, width: UInt) throws -> URL {
if pictureUrlString.hasPrefix("https://") || pictureUrlString.hasPrefix("http://") {
guard let url = URL(string: pictureUrlString) else {
throw Error.invalidPictureUrl(pictureUrlString)
}
return url
}

static func getCompletePictureUrl(pictureUrl: String, width: UInt) -> URL? {
if pictureUrl.hasPrefix("https://") || pictureUrl.hasPrefix("http://") {
return URL(string: pictureUrl)
guard let mediaUrl = KeyValPersistenceHelper.object(forKey: OptimobileUserDefaultsKey.MEDIA_BASE_URL.rawValue) as? String else {
throw Error.noMediaUrlFound
}

let baseUrl = KeyValPersistenceHelper.object(forKey: OptimobileUserDefaultsKey.MEDIA_BASE_URL.rawValue) as? String ?? mediaResizerBaseUrl
let urlString = "\(mediaUrl)/\(width)x/\(pictureUrlString)"
guard let url = URL(string: urlString) else {
throw Error.invalidPictureUrl(urlString)
}

let completeString = "\(baseUrl)/\(width)x/\(pictureUrl)"
return URL(string: completeString)
return url
}
}
6 changes: 6 additions & 0 deletions OptimobileShared/PendingNotification.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,10 @@ struct PendingNotification: Codable {
let id: Int
let deliveredAt: Date
let identifier: String

init(id: Int, deliveredAt: Date = .init(), identifier: String) {
self.id = id
self.deliveredAt = deliveredAt
self.identifier = identifier
}
}
101 changes: 32 additions & 69 deletions OptimoveNotificationServiceExtension/Sources/CategoryManager.swift
Original file line number Diff line number Diff line change
@@ -1,92 +1,55 @@
// Copyright © 2022 Optimove. All rights reserved.

import Foundation
import UserNotifications

let MAX_DYNAMIC_CATEGORIES = 128
let DYNAMIC_CATEGORY_IDENTIFIER = "__kumulos_category_%d__"

@available(iOS 10.0, *)
class CategoryManager {
let categoryReadLock = DispatchSemaphore(value: 0)
let dynamicCategoryLock = DispatchSemaphore(value: 1)
Copy link
Member Author

@eligutovsky eligutovsky Dec 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These locks were replaced with the Swift async. You will find the usage below.


fileprivate static var instance: CategoryManager?

static var sharedInstance: CategoryManager {
if instance == nil {
instance = CategoryManager()
}

return instance!
enum CategoryManager {
enum Constants {
static let MAX_DYNAMIC_CATEGORIES = 128
static let DYNAMIC_CATEGORY = OptimobileUserDefaultsKey.DYNAMIC_CATEGORY.rawValue
}

static func getCategoryIdForMessageId(messageId: Int) -> String {
return String(format: DYNAMIC_CATEGORY_IDENTIFIER, messageId)
static func getCategoryId(messageId: Int) -> String {
return "__kumulos_category_\(messageId)__"
}

static func registerCategory(category: UNNotificationCategory) {
var categorySet = sharedInstance.getExistingCategories()
var storedDynamicCategories = sharedInstance.getExistingDynamicCategoriesList()
static func registerCategory(_ category: UNNotificationCategory) async {
var systemCategories = await UNUserNotificationCenter.current().notificationCategories()
var storedCategoryIds = readCategoryIds()

systemCategories.insert(category)
storedCategoryIds.insert(category.identifier)

categorySet.insert(category)
storedDynamicCategories.append(category.identifier)
let (categories, categoryIds) = maybePruneCategories(categories: systemCategories, categoryIds: storedCategoryIds)

sharedInstance.pruneCategoriesAndSave(categories: categorySet, dynamicCategories: storedDynamicCategories)
UNUserNotificationCenter.current().setNotificationCategories(categories)
writeCategoryIds(categoryIds)

// Force a reload of the categories
_ = sharedInstance.getExistingCategories()
await UNUserNotificationCenter.current().notificationCategories()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awaiting for a result instead of manual mutex signaling

}

private func getExistingCategories() -> Set<UNNotificationCategory> {
var returnedCategories = Set<UNNotificationCategory>()

UNUserNotificationCenter.current().getNotificationCategories { (categories: Set<UNNotificationCategory>) in
returnedCategories = Set<UNNotificationCategory>(categories)

self.categoryReadLock.signal()
}

_ = categoryReadLock.wait(timeout: DispatchTime.now() + DispatchTimeInterval.seconds(5))
Copy link
Member Author

@eligutovsky eligutovsky Dec 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not certain about deleting this.
The mentioned extension operates as a distinct process and is granted a maximum of 30 seconds to complete its current task. In case the time limit is exceeded, the operating system will invoke another function for this instance and we will have one last opportunity to invoke the callback from the initial method's callback function.


return returnedCategories
static func readCategoryIds() -> Set<String> {
let array = UserDefaults.standard.object(forKey: Constants.DYNAMIC_CATEGORY) as? [String] ?? []
return Set(array)
}

private func getExistingDynamicCategoriesList() -> [String] {
dynamicCategoryLock.wait()
defer {
dynamicCategoryLock.signal()
}

if let existingArray = UserDefaults.standard.object(forKey: OptimobileUserDefaultsKey.DYNAMIC_CATEGORY.rawValue) {
return existingArray as! [String]
}

let newArray = [String]()

UserDefaults.standard.set(newArray, forKey: OptimobileUserDefaultsKey.DYNAMIC_CATEGORY.rawValue)

return newArray
static func writeCategoryIds(_ ids: Set<String>) {
UserDefaults.standard.set(Array(ids), forKey: Constants.DYNAMIC_CATEGORY)
Copy link
Member Author

@eligutovsky eligutovsky Dec 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The standard iOS persistence will be replaced with the storage abstraction in this bulk of PRs

}

private func pruneCategoriesAndSave(categories: Set<UNNotificationCategory>, dynamicCategories: [String]) {
if dynamicCategories.count <= MAX_DYNAMIC_CATEGORIES {
UNUserNotificationCenter.current().setNotificationCategories(categories)
UserDefaults.standard.set(dynamicCategories, forKey: OptimobileUserDefaultsKey.DYNAMIC_CATEGORY.rawValue)
return
static func maybePruneCategories(
categories: Set<UNNotificationCategory>,
categoryIds: Set<String>,
limit: Int = Constants.MAX_DYNAMIC_CATEGORIES
) -> (categories: Set<UNNotificationCategory>, categoryIds: Set<String>) {
if categoryIds.count <= limit, categories.count <= limit {
return (categories: categories, categoryIds: categoryIds)
}

let categoriesToRemove = dynamicCategories.prefix(dynamicCategories.count - MAX_DYNAMIC_CATEGORIES)

let prunedCategories = categories.filter { category -> Bool in
categoriesToRemove.firstIndex(of: category.identifier) == nil
}

let prunedDynamicCategories = dynamicCategories.filter { cat -> Bool in
categoriesToRemove.firstIndex(of: cat) == nil
}
let categoriesToRemove = categoryIds.prefix(categoryIds.count - limit)
let prunedCategories = categories.filter { !categoriesToRemove.contains($0.identifier) }
let prunedCategoryIds = categoryIds.subtracting(categoriesToRemove)

UNUserNotificationCenter.current().setNotificationCategories(prunedCategories)
UserDefaults.standard.set(prunedDynamicCategories, forKey: OptimobileUserDefaultsKey.DYNAMIC_CATEGORY.rawValue)
return (categories: prunedCategories, categoryIds: prunedCategoryIds)
}
}
Loading