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

feat: Apiv3 v 10.0.0 branch #122

Merged
merged 18 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
- name: Build
run: xcodebuild -scheme InfomaniakCore build -destination "platform=iOS Simulator,name=iPhone 13,OS=latest"
- name: Test
run: xcodebuild -scheme InfomaniakCore test -destination "platform=iOS Simulator,name=iPhone 13,OS=latest"
run: xcodebuild -scheme InfomaniakCore-Package test -destination "platform=iOS Simulator,name=iPhone 13,OS=latest"

build_and_test_macOS:
name: Build and Test project on macOS
Expand Down
14 changes: 13 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ let package = Package(
.library(
name: "InfomaniakCore",
targets: ["InfomaniakCore"]
),
.library(
name: "InfomaniakCoreDB",
targets: ["InfomaniakCoreDB"]
)
],
dependencies: [
Expand All @@ -38,9 +42,17 @@ let package = Package(
.product(name: "OSInfo", package: "OSInfo"),
]
),
.target(
name: "InfomaniakCoreDB",
dependencies: [
"InfomaniakCore",
.product(name: "InfomaniakDI", package: "ios-dependency-injection"),
.product(name: "RealmSwift", package: "realm-swift"),
]
),
.testTarget(
name: "InfomaniakCoreTests",
dependencies: ["InfomaniakCore","ZIPFoundation"],
dependencies: ["InfomaniakCore", "InfomaniakCoreDB" ,"ZIPFoundation"],
resources: [Resource.copy("Resources/Matterhorn_as_seen_from_Zermatt,_Wallis,_Switzerland,_2012_August,Wikimedia_Commons.heic"),
Resource.copy("Resources/Matterhorn_as_seen_from_Zermatt,_Wallis,_Switzerland,_2012_August,Wikimedia_Commons.jpg"),
Resource.copy("Resources/dummy.pdf")]
Expand Down
22 changes: 17 additions & 5 deletions Sources/InfomaniakCore/Account/UserDefaults+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,19 @@ public extension UserDefaults {
self.rawValue = rawValue
}

// TODO: Clean hotfix
static let legacyIsFirstLaunch = Keys(rawValue: "isFirstLaunch")
static let currentUserId = Keys(rawValue: "currentUserId")
static let appRestorationVersion = Keys(rawValue: "appRestorationVersion")
}

func key(_ key: Keys) -> String {
return key.rawValue
}
}

// MARK: - Public extension

public extension UserDefaults {
var currentUserId: Int {
get {
return integer(forKey: key(.currentUserId))
Expand All @@ -44,19 +48,27 @@ public extension UserDefaults {
}
}

// TODO: Clean hotfix
var legacyIsFirstLaunch: Bool {
get {
if object(forKey: key(.legacyIsFirstLaunch)) != nil {
return bool(forKey: key(.legacyIsFirstLaunch))
} else {
guard let isFirstLaunch = object(forKey: key(.legacyIsFirstLaunch)) as? Bool else {
return true
}

return isFirstLaunch
}
set {
set(newValue, forKey: key(.legacyIsFirstLaunch))
}
}

var appRestorationVersion: Int? {
get {
return object(forKey: key(.appRestorationVersion)) as? Int
}
set {
set(newValue, forKey: key(.appRestorationVersion))
}
}
}

// MARK: - Internal extension
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public final class SendableArray<T>: @unchecked Sendable, Sequence {
/// Internal collection
private(set) var content: [T]

public init(content: [T] = Array<T>()) {
public init(content: [T] = [T]()) {
self.content = content
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
Infomaniak Core - iOS
Copyright (C) 2024 Infomaniak Network SA

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import Foundation

/// Making a property thread safe, while not requiring `await`. Conforms to Sendable.
///
/// Please prefer using first party structured concurrency. Use this for prototyping or dealing with race conditions.
@propertyWrapper
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public final class SendableProperty<Property>: @unchecked Sendable {
/// Serial locking queue
let lock = DispatchQueue(label: "com.infomaniak.core.SendableProperty.lock")

/// Store property
var property: Property?

public init() { }

public var wrappedValue: Property? {
get {
lock.sync {
return self.property
}
}
set {
lock.sync {
self.property = newValue
}
}
}

/// The property wrapper itself for debugging and testing
public var projectedValue: SendableProperty {
self
}
}
134 changes: 134 additions & 0 deletions Sources/InfomaniakCore/SuddenTermination/ExpiringActivity.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
Infomaniak Core - iOS
Copyright (C) 2024 Infomaniak Network SA

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import Foundation

/// Delegation mechanism to notify the end of an `ExpiringActivity`
public protocol ExpiringActivityDelegate: AnyObject {
/// Called when the system is requiring us to terminate an expiring activity
func backgroundActivityExpiring()
}

/// Something that can perform arbitrary short background tasks, with a super simple API.
public protocol ExpiringActivityable {
/// Common init method
/// - Parameters:
/// - id: Something to identify the background activity in debug
/// - qos: QoS used by the underlying queues
/// - delegate: The delegate to notify we should terminate
init(id: String, qos: DispatchQoS, delegate: ExpiringActivityDelegate?)

/// init method
/// - Parameters:
/// - id: Something to identify the background activity in debug
/// - delegate: The delegate to notify we should terminate
init(id: String, delegate: ExpiringActivityDelegate?)

/// Register with the system an expiring activity
func start()

/// Terminate all the expiring activities
func endAll()

/// True if the system asked to stop the background activity
var shouldTerminate: Bool { get }
}

public final class ExpiringActivity: ExpiringActivityable {
private let qos: DispatchQoS

private let queue: DispatchQueue

private let processInfo = ProcessInfo.processInfo

var locks = [TolerantDispatchGroup]()

let id: String

public var shouldTerminate = false

weak var delegate: ExpiringActivityDelegate?

// MARK: Lifecycle

public init(id: String, qos: DispatchQoS, delegate: ExpiringActivityDelegate?) {
self.id = id
self.qos = qos
self.delegate = delegate
queue = DispatchQueue(label: "com.infomaniak.ExpiringActivity.sync", qos: qos)
}

public convenience init(id: String = "\(#function)-\(UUID().uuidString)", delegate: ExpiringActivityDelegate? = nil) {
self.init(id: id, qos: .userInitiated, delegate: delegate)
}

deinit {
queue.sync {
assert(locks.isEmpty, "please make sure to call 'endAll()' once explicitly before releasing this object")
}
}

public func start() {
let group = TolerantDispatchGroup(qos: qos)

queue.sync {
self.locks.append(group)
}

#if os(macOS)
// We block a non cooperative queue that matches current QoS
DispatchQueue.global(qos: qos.qosClass).async {
self.processInfo.performActivity(options: .suddenTerminationDisabled, reason: self.id) {
// No expiration handler as we are running on macOS
group.enter()
group.wait()
}
}
#else
// Make sure to not lock an unexpected thread that would deinit()
processInfo.performExpiringActivity(withReason: id) { [weak self] shouldTerminate in
guard let self else {
return
}

if shouldTerminate {
self.shouldTerminate = true
delegate?.backgroundActivityExpiring()

// At this point we should release the block, but we prefer to wait until the end 🫡⛵️🪦
// There is a chance endAll() is called while we wait in should terminate
group.wait()
return
}

group.enter()
group.wait()
}
#endif
}

public func endAll() {
queue.sync {
// Release locks, oldest first
for group in locks.reversed() {
group.leave()
}
locks.removeAll()
}
}
}
36 changes: 36 additions & 0 deletions Sources/InfomaniakCoreDB/Extensions/Realm+SafeWrite.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
Infomaniak Core - iOS
Copyright (C) 2024 Infomaniak Network SA

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import Foundation
import RealmSwift

public extension Realm {
/// Only emits a write signal if not already within a write transaction
func safeWrite(_ block: () throws -> Void) throws {
if isInWriteTransaction {
try block()
} else {
try write(block)
}
}

/// Shorthand for object from primary key
func getObject<RealmObject: Object, KeyType>(id: KeyType) -> RealmObject? {
object(ofType: RealmObject.self, forPrimaryKey: id)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
Infomaniak Core - iOS
Copyright (C) 2023 Infomaniak Network SA
Copyright (C) 2024 Infomaniak Network SA

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
Expand Down
39 changes: 39 additions & 0 deletions Sources/InfomaniakCoreDB/RealmConfigurable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
Infomaniak Core - iOS
Copyright (C) 2024 Infomaniak Network SA

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import Foundation
import RealmSwift

/// Something that can access a realm
///
/// Do not expose a realm directly in your app, use Transactionable instead
///
public protocol RealmAccessible {
/// Fetches an up to date realm for a given configuration, or fail in a controlled manner
func getRealm() -> Realm
}

/// Something that can provide a realm configuration
public protocol RealmConfigurable {
/// Configuration for a given realm
var realmConfiguration: Realm.Configuration { get }

/// Set `isExcludedFromBackup = true` to the folder where realm is located to exclude a realm cache from an iCloud backup
/// - Important: Avoid calling this method too often as this can be expensive, prefer calling it once at init time
func excludeRealmFromBackup()
}
Loading
Loading