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

[Bug] Add Dispatch Queues to all executors #1454

Merged
merged 8 commits into from
Jul 1, 2024
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import OneSignalOSCore
public class MockDispatchQueue: OSDispatchQueue {
let requestDispatch = DispatchQueue(label: "MockDispatchQueue")
var numDispatches = 0

public init() {}

public func async(execute work: @escaping @convention(block) () -> Void) {
Expand All @@ -46,8 +46,8 @@ public class MockDispatchQueue: OSDispatchQueue {
self.numDispatches += 1
}
}
public func waitForDispatches(_ numDispatches: Int) -> Void {

public func waitForDispatches(_ numDispatches: Int) {
while self.numDispatches < numDispatches {
requestDispatch.sync {
Thread.sleep(forTimeInterval: TimeInterval(1))
Expand Down
12 changes: 10 additions & 2 deletions iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,17 @@ import OneSignalCore
*/
@objc
public class MockOneSignalClient: NSObject, IOneSignalClient {
public let executionQueue: DispatchQueue = DispatchQueue(label: "com.onesignal.execution")
public let executionQueue: DispatchQueue = DispatchQueue(label: "com.onesignal.execution", attributes: .concurrent)
let lock = NSLock()

var mockResponses: [String: [String: Any]] = [:]
var mockFailureResponses: [String: NSError] = [:]
public var lastHTTPRequest: OneSignalRequest?
public var networkRequestCount = 0
public var executedRequests: [OneSignalRequest] = []
public var executeInstantaneously = false
/// Set to true to make it unnecessary to setup mock responses for every request possible
public var fireSuccessForAllRequests = false

var remoteParamsResponse: [String: Any]?
var shouldUseProvisionalAuthorization = false // new in iOS 12 (aka Direct to History)
Expand Down Expand Up @@ -90,7 +93,9 @@ public class MockOneSignalClient: NSObject, IOneSignalClient {
public func execute(_ request: OneSignalRequest, onSuccess successBlock: @escaping OSResultSuccessBlock, onFailure failureBlock: @escaping OSFailureBlock) {
print("🧪 MockOneSignalClient execute called")

executedRequests.append(request)
lock.withLock {
executedRequests.append(request)
}

if executeInstantaneously {
finishExecutingRequest(request, onSuccess: successBlock, onFailure: failureBlock)
Expand Down Expand Up @@ -135,6 +140,9 @@ public class MockOneSignalClient: NSObject, IOneSignalClient {
successBlock(mockResponses[stringifiedRequest])
} else if (mockFailureResponses[stringifiedRequest]) != nil {
failureBlock(mockFailureResponses[stringifiedRequest])
} else if fireSuccessForAllRequests {
allRequestsHandled = false
successBlock([:])
} else {
allRequestsHandled = false
print("🧪 cannot find a mock response for request: \(stringifiedRequest)")
Expand Down
16 changes: 8 additions & 8 deletions iOS_SDK/OneSignalSDK/OneSignalCoreMocks/OneSignalCoreMocks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ public class OneSignalCoreMocks: NSObject {
let expectation = XCTestExpectation(description: "Wait for \(seconds) seconds")
_ = XCTWaiter.wait(for: [expectation], timeout: seconds)
}

@objc public static func backgroundApp() {
if (OSBundleUtils.isAppUsingUIScene()) {
if OSBundleUtils.isAppUsingUIScene() {
if #available(iOS 13.0, *) {
NotificationCenter.default.post(name: UIScene.willDeactivateNotification, object: nil)
NotificationCenter.default.post(name: UIScene.didEnterBackgroundNotification, object: nil)
Expand All @@ -63,9 +63,9 @@ public class OneSignalCoreMocks: NSObject {
NotificationCenter.default.post(name: UIApplication.didEnterBackgroundNotification, object: nil)
}
}

@objc public static func foregroundApp() {
if (OSBundleUtils.isAppUsingUIScene()) {
if OSBundleUtils.isAppUsingUIScene() {
if #available(iOS 13.0, *) {
NotificationCenter.default.post(name: UIScene.willEnterForegroundNotification, object: nil)
NotificationCenter.default.post(name: UIScene.didActivateNotification, object: nil)
Expand All @@ -75,19 +75,19 @@ public class OneSignalCoreMocks: NSObject {
NotificationCenter.default.post(name: UIApplication.didBecomeActiveNotification, object: nil)
}
}

@objc public static func resignActive() {
if (OSBundleUtils.isAppUsingUIScene()) {
if OSBundleUtils.isAppUsingUIScene() {
if #available(iOS 13.0, *) {
NotificationCenter.default.post(name: UIScene.willDeactivateNotification, object: nil)
}
} else {
NotificationCenter.default.post(name: UIApplication.willResignActiveNotification, object: nil)
}
}

@objc public static func becomeActive() {
if (OSBundleUtils.isAppUsingUIScene()) {
if OSBundleUtils.isAppUsingUIScene() {
if #available(iOS 13.0, *) {
NotificationCenter.default.post(name: UIScene.didActivateNotification, object: nil)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import OneSignalCoreMocks
import UIKit

final class OneSignalNotificationsTests: XCTestCase {

var notifTypes: Int32 = 0
var token: String = ""

Expand All @@ -38,7 +38,7 @@ final class OneSignalNotificationsTests: XCTestCase {
// Ensure that badge count == 0
XCTAssertEqual(UIApplication.shared.applicationIconBadgeNumber, 0)
}

func testDontclearBadgesWhenAppBecomesActive() throws {
// NotificationManager Start to register lifecycle listener
OSNotificationsManager.start()
Expand All @@ -51,25 +51,24 @@ final class OneSignalNotificationsTests: XCTestCase {
// Ensure that badge count == 0
XCTAssertEqual(UIApplication.shared.applicationIconBadgeNumber, 1)
}

func testUpdateNotificationTypesOnAppEntersForeground() throws {
// NotificationManager Start to register lifecycle listener
OSNotificationsManager.start()

OSNotificationsManager.delegate = self

XCTAssertEqual(self.notifTypes, 0)

// Then background the app
OneSignalCoreMocks.backgroundApp()

// Foreground the app for within 30 seconds
OneSignalCoreMocks.foregroundApp()

// Ensure that the delegate is updated with the new notification type
XCTAssertEqual(self.notifTypes, ERROR_PUSH_NEVER_PROMPTED)
}


}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
var addRequestQueue: [OSRequestAddAliases] = []
var removeRequestQueue: [OSRequestRemoveAlias] = []

// The Identity executor dispatch queue, serial. This synchronizes access to the delta and request queues.
private let dispatchQueue = DispatchQueue(label: "OneSignal.OSIdentityOperationExecutor", target: .global())

init() {
// Read unfinished deltas from cache, if any...
if var deltaQueue = OneSignalUserDefaults.initShared().getSavedCodeableData(forKey: OS_IDENTITY_EXECUTOR_DELTA_QUEUE_KEY, defaultValue: []) as? [OSDelta] {
Expand Down Expand Up @@ -101,53 +104,60 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
}

func enqueueDelta(_ delta: OSDelta) {
OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSIdentityOperationExecutor enqueueDelta: \(delta)")
deltaQueue.append(delta)
self.dispatchQueue.async {
OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSIdentityOperationExecutor enqueueDelta: \(delta)")
self.deltaQueue.append(delta)
}
}

func cacheDeltaQueue() {
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_DELTA_QUEUE_KEY, withValue: self.deltaQueue)
self.dispatchQueue.async {
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_DELTA_QUEUE_KEY, withValue: self.deltaQueue)
}
}

func processDeltaQueue(inBackground: Bool) {
if !deltaQueue.isEmpty {
OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSIdentityOperationExecutor processDeltaQueue with queue: \(deltaQueue)")
}
for delta in deltaQueue {
guard let model = delta.model as? OSIdentityModel,
let aliases = delta.value as? [String: String]
else {
// Log error
continue
self.dispatchQueue.async {
if !self.deltaQueue.isEmpty {
OneSignalLog.onesignalLog(.LL_VERBOSE, message: "OSIdentityOperationExecutor processDeltaQueue with queue: \(self.deltaQueue)")
}
for delta in self.deltaQueue {
guard let model = delta.model as? OSIdentityModel,
let aliases = delta.value as? [String: String]
else {
// Log error
continue
}

switch delta.name {
case OS_ADD_ALIAS_DELTA:
let request = OSRequestAddAliases(aliases: aliases, identityModel: model)
addRequestQueue.append(request)
switch delta.name {
case OS_ADD_ALIAS_DELTA:
let request = OSRequestAddAliases(aliases: aliases, identityModel: model)
self.addRequestQueue.append(request)

case OS_REMOVE_ALIAS_DELTA:
for (label, _) in aliases {
let request = OSRequestRemoveAlias(labelToRemove: label, identityModel: model)
removeRequestQueue.append(request)
}
case OS_REMOVE_ALIAS_DELTA:
for (label, _) in aliases {
let request = OSRequestRemoveAlias(labelToRemove: label, identityModel: model)
self.removeRequestQueue.append(request)
}

default:
OneSignalLog.onesignalLog(.LL_DEBUG, message: "OSIdentityOperationExecutor met incompatible OSDelta type: \(delta)")
default:
OneSignalLog.onesignalLog(.LL_DEBUG, message: "OSIdentityOperationExecutor met incompatible OSDelta type: \(delta)")
}
}
}

self.deltaQueue = [] // TODO: Check that we can simply clear all the deltas in the deltaQueue
self.deltaQueue = [] // TODO: Check that we can simply clear all the deltas in the deltaQueue

// persist executor's requests (including new request) to storage
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue)
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue)
// persist executor's requests (including new request) to storage
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue)
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue)

OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_DELTA_QUEUE_KEY, withValue: self.deltaQueue) // This should be empty, can remove instead?
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_DELTA_QUEUE_KEY, withValue: self.deltaQueue) // This should be empty, can remove instead?

processRequestQueue(inBackground: inBackground)
self.processRequestQueue(inBackground: inBackground)
}
}

/// This method is called by `processDeltaQueue` only and does not need to be added to the dispatchQueue.
func processRequestQueue(inBackground: Bool) {
let requestQueue: [OneSignalRequest] = addRequestQueue + removeRequestQueue

Expand Down Expand Up @@ -188,38 +198,42 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
OneSignalCoreImpl.sharedClient().execute(request) { _ in
// No hydration from response
// On success, remove request from cache
self.addRequestQueue.removeAll(where: { $0 == request})
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue)
if inBackground {
OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier)
self.dispatchQueue.async {
self.addRequestQueue.removeAll(where: { $0 == request})
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue)
if inBackground {
OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier)
}
}
} onFailure: { error in
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSIdentityOperationExecutor add aliases request failed with error: \(error.debugDescription)")
if let nsError = error as? NSError {
let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code)
if responseType == .missing {
// Remove from cache and queue
self.addRequestQueue.removeAll(where: { $0 == request})
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue)
// Logout if the user in the SDK is the same
guard OneSignalUserManagerImpl.sharedInstance.isCurrentUser(request.identityModel)
else {
if inBackground {
OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier)
self.dispatchQueue.async {
if let nsError = error as? NSError {
let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code)
if responseType == .missing {
// Remove from cache and queue
self.addRequestQueue.removeAll(where: { $0 == request})
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue)
// Logout if the user in the SDK is the same
guard OneSignalUserManagerImpl.sharedInstance.isCurrentUser(request.identityModel)
else {
if inBackground {
OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier)
}
return
}
return
// The subscription has been deleted along with the user, so remove the subscription_id but keep the same push subscription model
OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModel?.subscriptionId = nil
OneSignalUserManagerImpl.sharedInstance._logout()
} else if responseType != .retryable {
// Fail, no retry, remove from cache and queue
self.addRequestQueue.removeAll(where: { $0 == request})
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue)
}
// The subscription has been deleted along with the user, so remove the subscription_id but keep the same push subscription model
OneSignalUserManagerImpl.sharedInstance.pushSubscriptionModel?.subscriptionId = nil
OneSignalUserManagerImpl.sharedInstance._logout()
} else if responseType != .retryable {
// Fail, no retry, remove from cache and queue
self.addRequestQueue.removeAll(where: { $0 == request})
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_ADD_REQUEST_QUEUE_KEY, withValue: self.addRequestQueue)
}
}
if inBackground {
OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier)
if inBackground {
OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier)
}
}
}
}
Expand All @@ -243,25 +257,28 @@ class OSIdentityOperationExecutor: OSOperationExecutor {
OneSignalCoreImpl.sharedClient().execute(request) { _ in
// There is nothing to hydrate
// On success, remove request from cache
self.removeRequestQueue.removeAll(where: { $0 == request})
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue)
if inBackground {
OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier)
self.dispatchQueue.async {
self.removeRequestQueue.removeAll(where: { $0 == request})
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue)
if inBackground {
OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier)
}
}
} onFailure: { error in
OneSignalLog.onesignalLog(.LL_ERROR, message: "OSIdentityOperationExecutor remove alias request failed with error: \(error.debugDescription)")

if let nsError = error as? NSError {
let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code)
if responseType != .retryable {
// Fail, no retry, remove from cache and queue
// A response of .missing could mean the alias doesn't exist on this user OR this user has been deleted
self.removeRequestQueue.removeAll(where: { $0 == request})
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue)
self.dispatchQueue.async {
if let nsError = error as? NSError {
let responseType = OSNetworkingUtils.getResponseStatusType(nsError.code)
if responseType != .retryable {
// Fail, no retry, remove from cache and queue
// A response of .missing could mean the alias doesn't exist on this user OR this user has been deleted
self.removeRequestQueue.removeAll(where: { $0 == request})
OneSignalUserDefaults.initShared().saveCodeableData(forKey: OS_IDENTITY_EXECUTOR_REMOVE_REQUEST_QUEUE_KEY, withValue: self.removeRequestQueue)
}
}
if inBackground {
OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier)
}
}
if inBackground {
OSBackgroundTaskManager.endBackgroundTask(backgroundTaskIdentifier)
}
}
}
Expand Down
Loading
Loading