@capgo/capacitor-updater 8.0.0 → 8.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CapgoCapacitorUpdater.podspec +7 -5
  2. package/Package.swift +37 -0
  3. package/README.md +1461 -231
  4. package/android/build.gradle +29 -12
  5. package/android/proguard-rules.pro +45 -0
  6. package/android/src/main/AndroidManifest.xml +0 -1
  7. package/android/src/main/java/ee/forgr/capacitor_updater/BundleInfo.java +223 -195
  8. package/android/src/main/java/ee/forgr/capacitor_updater/BundleStatus.java +23 -23
  9. package/android/src/main/java/ee/forgr/capacitor_updater/Callback.java +13 -0
  10. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdaterPlugin.java +2159 -1234
  11. package/android/src/main/java/ee/forgr/capacitor_updater/CapgoUpdater.java +1507 -0
  12. package/android/src/main/java/ee/forgr/capacitor_updater/CryptoCipher.java +330 -121
  13. package/android/src/main/java/ee/forgr/capacitor_updater/DataManager.java +28 -0
  14. package/android/src/main/java/ee/forgr/capacitor_updater/DelayCondition.java +43 -49
  15. package/android/src/main/java/ee/forgr/capacitor_updater/DelayUntilNext.java +4 -4
  16. package/android/src/main/java/ee/forgr/capacitor_updater/DelayUpdateUtils.java +260 -0
  17. package/android/src/main/java/ee/forgr/capacitor_updater/DeviceIdHelper.java +221 -0
  18. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadService.java +808 -117
  19. package/android/src/main/java/ee/forgr/capacitor_updater/DownloadWorkerManager.java +156 -0
  20. package/android/src/main/java/ee/forgr/capacitor_updater/InternalUtils.java +32 -0
  21. package/android/src/main/java/ee/forgr/capacitor_updater/Logger.java +338 -0
  22. package/android/src/main/java/ee/forgr/capacitor_updater/ShakeDetector.java +72 -0
  23. package/android/src/main/java/ee/forgr/capacitor_updater/ShakeMenu.java +169 -0
  24. package/dist/docs.json +2187 -625
  25. package/dist/esm/definitions.d.ts +1286 -249
  26. package/dist/esm/definitions.js.map +1 -1
  27. package/dist/esm/history.d.ts +1 -0
  28. package/dist/esm/history.js +283 -0
  29. package/dist/esm/history.js.map +1 -0
  30. package/dist/esm/index.d.ts +3 -2
  31. package/dist/esm/index.js +5 -4
  32. package/dist/esm/index.js.map +1 -1
  33. package/dist/esm/web.d.ts +36 -41
  34. package/dist/esm/web.js +94 -35
  35. package/dist/esm/web.js.map +1 -1
  36. package/dist/plugin.cjs.js +376 -35
  37. package/dist/plugin.cjs.js.map +1 -1
  38. package/dist/plugin.js +376 -35
  39. package/dist/plugin.js.map +1 -1
  40. package/ios/Sources/CapacitorUpdaterPlugin/AES.swift +69 -0
  41. package/ios/Sources/CapacitorUpdaterPlugin/BigInt.swift +55 -0
  42. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleInfo.swift +37 -10
  43. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/BundleStatus.swift +1 -1
  44. package/ios/Sources/CapacitorUpdaterPlugin/CapacitorUpdaterPlugin.swift +1605 -0
  45. package/ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift +1526 -0
  46. package/ios/Sources/CapacitorUpdaterPlugin/CryptoCipher.swift +267 -0
  47. package/ios/Sources/CapacitorUpdaterPlugin/DelayUpdateUtils.swift +220 -0
  48. package/ios/Sources/CapacitorUpdaterPlugin/DeviceIdHelper.swift +120 -0
  49. package/ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift +311 -0
  50. package/ios/Sources/CapacitorUpdaterPlugin/Logger.swift +310 -0
  51. package/ios/Sources/CapacitorUpdaterPlugin/RSA.swift +274 -0
  52. package/ios/Sources/CapacitorUpdaterPlugin/ShakeMenu.swift +112 -0
  53. package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/UserDefaultsExtension.swift +0 -2
  54. package/package.json +41 -35
  55. package/android/src/main/java/ee/forgr/capacitor_updater/CapacitorUpdater.java +0 -1130
  56. package/ios/Plugin/CapacitorUpdater.swift +0 -858
  57. package/ios/Plugin/CapacitorUpdaterPlugin.h +0 -10
  58. package/ios/Plugin/CapacitorUpdaterPlugin.m +0 -27
  59. package/ios/Plugin/CapacitorUpdaterPlugin.swift +0 -675
  60. package/ios/Plugin/CryptoCipher.swift +0 -240
  61. /package/{LICENCE → LICENSE} +0 -0
  62. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayCondition.swift +0 -0
  63. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/DelayUntilNext.swift +0 -0
  64. /package/ios/{Plugin → Sources/CapacitorUpdaterPlugin}/Info.plist +0 -0
@@ -0,0 +1,311 @@
1
+ /*
2
+ * This Source Code Form is subject to the terms of the Mozilla Public
3
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
4
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/.
5
+ */
6
+
7
+ import Foundation
8
+
9
+ extension Collection {
10
+ subscript(safe index: Index) -> Element? {
11
+ return indices.contains(index) ? self[index] : nil
12
+ }
13
+ }
14
+ extension URL {
15
+ var isDirectory: Bool {
16
+ (try? resourceValues(forKeys: [.isDirectoryKey]))?.isDirectory == true
17
+ }
18
+ var exist: Bool {
19
+ return FileManager().fileExists(atPath: self.path)
20
+ }
21
+ }
22
+ struct SetChannelDec: Decodable {
23
+ let status: String?
24
+ let error: String?
25
+ let message: String?
26
+ }
27
+ public class SetChannel: NSObject {
28
+ var status: String = ""
29
+ var error: String = ""
30
+ var message: String = ""
31
+ }
32
+ extension SetChannel {
33
+ func toDict() -> [String: Any] {
34
+ var dict: [String: Any] = [String: Any]()
35
+ let otherSelf: Mirror = Mirror(reflecting: self)
36
+ for child: Mirror.Child in otherSelf.children {
37
+ if let key: String = child.label {
38
+ dict[key] = child.value
39
+ }
40
+ }
41
+ return dict
42
+ }
43
+ }
44
+ struct GetChannelDec: Decodable {
45
+ let channel: String?
46
+ let status: String?
47
+ let error: String?
48
+ let message: String?
49
+ let allowSet: Bool?
50
+ }
51
+ public class GetChannel: NSObject {
52
+ var channel: String = ""
53
+ var status: String = ""
54
+ var error: String = ""
55
+ var message: String = ""
56
+ var allowSet: Bool = true
57
+ }
58
+ extension GetChannel {
59
+ func toDict() -> [String: Any] {
60
+ var dict: [String: Any] = [String: Any]()
61
+ let otherSelf: Mirror = Mirror(reflecting: self)
62
+ for child: Mirror.Child in otherSelf.children {
63
+ if let key: String = child.label {
64
+ dict[key] = child.value
65
+ }
66
+ }
67
+ return dict
68
+ }
69
+ }
70
+ struct ChannelInfo: Codable {
71
+ let id: String?
72
+ let name: String?
73
+ let `public`: Bool?
74
+ let allow_self_set: Bool?
75
+ }
76
+ struct ListChannelsDec: Decodable {
77
+ let channels: [ChannelInfo]?
78
+ let error: String?
79
+
80
+ init(from decoder: Decoder) throws {
81
+ let container = try decoder.singleValueContainer()
82
+
83
+ if let channelsArray = try? container.decode([ChannelInfo].self) {
84
+ // Backend returns direct array
85
+ self.channels = channelsArray
86
+ self.error = nil
87
+ } else {
88
+ // Handle error response
89
+ let errorContainer = try decoder.container(keyedBy: CodingKeys.self)
90
+ self.channels = nil
91
+ self.error = try? errorContainer.decode(String.self, forKey: .error)
92
+ }
93
+ }
94
+
95
+ private enum CodingKeys: String, CodingKey {
96
+ case error
97
+ }
98
+ }
99
+ public class ListChannels: NSObject {
100
+ var channels: [[String: Any]] = []
101
+ var error: String = ""
102
+ }
103
+ extension ListChannels {
104
+ func toDict() -> [String: Any] {
105
+ var dict: [String: Any] = [String: Any]()
106
+ let otherSelf: Mirror = Mirror(reflecting: self)
107
+ for child: Mirror.Child in otherSelf.children {
108
+ if let key: String = child.label {
109
+ dict[key] = child.value
110
+ }
111
+ }
112
+ return dict
113
+ }
114
+ }
115
+ struct InfoObject: Codable {
116
+ let platform: String?
117
+ let device_id: String?
118
+ let app_id: String?
119
+ let custom_id: String?
120
+ let version_build: String?
121
+ let version_code: String?
122
+ let version_os: String?
123
+ var version_name: String?
124
+ var old_version_name: String?
125
+ let plugin_version: String?
126
+ let is_emulator: Bool?
127
+ let is_prod: Bool?
128
+ var action: String?
129
+ var channel: String?
130
+ var defaultChannel: String?
131
+ }
132
+
133
+ public struct ManifestEntry: Codable {
134
+ let file_name: String?
135
+ let file_hash: String?
136
+ let download_url: String?
137
+ }
138
+
139
+ extension ManifestEntry {
140
+ func toDict() -> [String: Any] {
141
+ var dict: [String: Any] = [String: Any]()
142
+ let mirror = Mirror(reflecting: self)
143
+ for child in mirror.children {
144
+ if let key = child.label {
145
+ dict[key] = child.value
146
+ }
147
+ }
148
+ return dict
149
+ }
150
+ }
151
+
152
+ struct AppVersionDec: Decodable {
153
+ let version: String?
154
+ let checksum: String?
155
+ let url: String?
156
+ let message: String?
157
+ let error: String?
158
+ let session_key: String?
159
+ let major: Bool?
160
+ let breaking: Bool?
161
+ let data: [String: String]?
162
+ let manifest: [ManifestEntry]?
163
+ let link: String?
164
+ let comment: String?
165
+ // The HTTP status code is captured separately in CapgoUpdater; this struct only mirrors JSON.
166
+ }
167
+
168
+ public class AppVersion: NSObject {
169
+ var version: String = ""
170
+ var checksum: String = ""
171
+ var url: String = ""
172
+ var message: String?
173
+ var error: String?
174
+ var sessionKey: String?
175
+ var major: Bool?
176
+ var breaking: Bool?
177
+ var data: [String: String]?
178
+ var manifest: [ManifestEntry]?
179
+ var link: String?
180
+ var comment: String?
181
+ var statusCode: Int = 0
182
+ }
183
+
184
+ extension AppVersion {
185
+ func toDict() -> [String: Any] {
186
+ var dict: [String: Any] = [String: Any]()
187
+ let otherSelf: Mirror = Mirror(reflecting: self)
188
+ for child: Mirror.Child in otherSelf.children {
189
+ if let key: String = child.label {
190
+ if key == "manifest", let manifestEntries = child.value as? [ManifestEntry] {
191
+ dict[key] = manifestEntries.map { $0.toDict() }
192
+ } else {
193
+ dict[key] = child.value
194
+ }
195
+ }
196
+ }
197
+ return dict
198
+ }
199
+ }
200
+
201
+ extension OperatingSystemVersion {
202
+ func getFullVersion(separator: String = ".") -> String {
203
+ return "\(majorVersion)\(separator)\(minorVersion)\(separator)\(patchVersion)"
204
+ }
205
+ }
206
+ extension Bundle {
207
+ var versionName: String? {
208
+ return infoDictionary?["CFBundleShortVersionString"] as? String
209
+ }
210
+ var versionCode: String? {
211
+ return infoDictionary?["CFBundleVersion"] as? String
212
+ }
213
+ }
214
+
215
+ extension ISO8601DateFormatter {
216
+ convenience init(_ formatOptions: Options) {
217
+ self.init()
218
+ self.formatOptions = formatOptions
219
+ }
220
+ }
221
+ extension Formatter {
222
+ static let iso8601withFractionalSeconds: ISO8601DateFormatter = ISO8601DateFormatter([.withInternetDateTime, .withFractionalSeconds])
223
+ }
224
+ extension Date {
225
+ var iso8601withFractionalSeconds: String { return Formatter.iso8601withFractionalSeconds.string(from: self) }
226
+ }
227
+ extension String {
228
+
229
+ var fileURL: URL {
230
+ return URL(fileURLWithPath: self)
231
+ }
232
+
233
+ var lastPathComponent: String {
234
+ get {
235
+ return fileURL.lastPathComponent
236
+ }
237
+ }
238
+ var iso8601withFractionalSeconds: Date? {
239
+ return Formatter.iso8601withFractionalSeconds.date(from: self)
240
+ }
241
+ func trim(using characterSet: CharacterSet = .whitespacesAndNewlines) -> String {
242
+ return trimmingCharacters(in: characterSet)
243
+ }
244
+ }
245
+
246
+ enum CustomError: Error {
247
+ // Throw when an unzip fail
248
+ case cannotUnzip
249
+ case cannotWrite
250
+ case cannotDecode
251
+ case cannotUnflat
252
+ case cannotCreateDirectory
253
+ case cannotDeleteDirectory
254
+ case cannotDecryptSessionKey
255
+ case invalidBase64
256
+
257
+ // Throw in all other cases
258
+ case unexpected(code: Int)
259
+ }
260
+
261
+ extension CustomError: LocalizedError {
262
+ public var errorDescription: String? {
263
+ switch self {
264
+ case .cannotUnzip:
265
+ return NSLocalizedString(
266
+ "The file cannot be unzip",
267
+ comment: "Invalid zip"
268
+ )
269
+ case .cannotCreateDirectory:
270
+ return NSLocalizedString(
271
+ "The folder cannot be created",
272
+ comment: "Invalid folder"
273
+ )
274
+ case .cannotDeleteDirectory:
275
+ return NSLocalizedString(
276
+ "The folder cannot be deleted",
277
+ comment: "Invalid folder"
278
+ )
279
+ case .cannotUnflat:
280
+ return NSLocalizedString(
281
+ "The file cannot be unflat",
282
+ comment: "Invalid folder"
283
+ )
284
+ case .unexpected:
285
+ return NSLocalizedString(
286
+ "An unexpected error occurred.",
287
+ comment: "Unexpected Error"
288
+ )
289
+ case .cannotDecode:
290
+ return NSLocalizedString(
291
+ "Decoding the zip failed with this key",
292
+ comment: "Invalid public key"
293
+ )
294
+ case .cannotWrite:
295
+ return NSLocalizedString(
296
+ "Cannot write to the destination",
297
+ comment: "Invalid destination"
298
+ )
299
+ case .cannotDecryptSessionKey:
300
+ return NSLocalizedString(
301
+ "Decrypting the session key failed",
302
+ comment: "Invalid session key"
303
+ )
304
+ case .invalidBase64:
305
+ return NSLocalizedString(
306
+ "Decrypting the base64 failed",
307
+ comment: "Invalid checksum key"
308
+ )
309
+ }
310
+ }
311
+ }
@@ -0,0 +1,310 @@
1
+ import Capacitor
2
+ import Foundation
3
+ import os.log
4
+ import WebKit
5
+
6
+ public class Logger {
7
+ public enum LogLevel: Int {
8
+ case silent = 0
9
+ case error
10
+ case warn
11
+ case info
12
+ case debug
13
+
14
+ public static subscript(_ str: String) -> LogLevel? {
15
+ var index = 0
16
+
17
+ while let item = LogLevel(rawValue: index) {
18
+ if str == "\(item)" {
19
+ return item
20
+ }
21
+
22
+ index += 1
23
+ }
24
+
25
+ return nil
26
+ }
27
+
28
+ public func asOSLogType() -> OSLogType {
29
+ switch self {
30
+ case .debug:
31
+ return OSLogType.debug
32
+ case .info:
33
+ return OSLogType.info
34
+ case .warn:
35
+ return OSLogType.default
36
+ case .error:
37
+ return OSLogType.error
38
+ case .silent:
39
+ return OSLogType.info // Compiler wants this, it will never be used
40
+ }
41
+ }
42
+ public func asString() -> String {
43
+ switch self {
44
+ case .debug:
45
+ return "debug"
46
+ case .info:
47
+ return "info"
48
+ case .warn:
49
+ return "warn"
50
+ case .error:
51
+ return "error"
52
+ case .silent:
53
+ return "info"
54
+ }
55
+ }
56
+ }
57
+
58
+ public struct Options {
59
+ public var level: LogLevel
60
+ public var labels: [String: String]
61
+ public var useSyslog: Bool
62
+
63
+ public init(level: LogLevel = LogLevel.info, labels: [String: String] = [:], useSyslog: Bool = false) {
64
+ self.level = level
65
+ self.labels = labels
66
+ self.useSyslog = useSyslog
67
+ }
68
+ }
69
+
70
+ private var _labels: [LogLevel: String] = [
71
+ LogLevel.silent: "",
72
+ LogLevel.error: "🔴",
73
+ LogLevel.warn: "🟠",
74
+ LogLevel.info: "🟢",
75
+ LogLevel.debug: "🔎"
76
+ ]
77
+
78
+ public var labels: [String: String] {
79
+ get {
80
+ var result: [String: String] = [:]
81
+
82
+ for (level, label) in _labels {
83
+ result[String(describing: level)] = label
84
+ }
85
+
86
+ return result
87
+ }
88
+
89
+ set {
90
+ for (level, label) in newValue {
91
+ if let logLevel = LogLevel[level] {
92
+ _labels[logLevel] = label
93
+ }
94
+ }
95
+ }
96
+ }
97
+
98
+ public var level = LogLevel.info
99
+
100
+ public var levelName: String {
101
+ get {
102
+ String(describing: level)
103
+ }
104
+ set {
105
+ if let newLevel = LogLevel[newValue] {
106
+ level = newLevel
107
+ }
108
+ }
109
+ }
110
+
111
+ private var _tag = ""
112
+
113
+ public var tag: String {
114
+ get {
115
+ _tag
116
+ }
117
+ set {
118
+ if !newValue.isEmpty {
119
+ _tag = newValue
120
+ }
121
+ }
122
+ }
123
+
124
+ private let kDefaultTimerLabel = "default"
125
+ private var timers: [String: Date] = [:]
126
+ public var useSyslog = false
127
+ public var webView: WKWebView?
128
+
129
+ public func setWebView(webView: WKWebView) {
130
+ self.webView = webView
131
+ }
132
+
133
+ public init(
134
+ withTag tag: String,
135
+ config: InstanceConfiguration? = nil,
136
+ options: Options? = nil
137
+ ) {
138
+ self.tag = tag
139
+ if let config = config {
140
+ // The logger plugin's name is LoggerBridge, we want to look at the config
141
+ // named "Logger", so we can't use plugin.getConfigValue().
142
+ if let configLevel = getConfigValue("level", from: config) as? String,
143
+ let logLevel = LogLevel[configLevel] {
144
+ level = logLevel
145
+ }
146
+
147
+ if let configLabels = getConfigValue("labels", from: config) as? [String: String] {
148
+ labels = configLabels
149
+ }
150
+
151
+ if let configSyslog = getConfigValue("useSyslog", from: config) as? Bool {
152
+ useSyslog = configSyslog
153
+ }
154
+ }
155
+
156
+ if let options = options {
157
+ self.level = options.level
158
+ self.labels = options.labels
159
+ self.useSyslog = options.useSyslog
160
+ }
161
+ }
162
+
163
+ private func getConfigValue(_ configKey: String, from config: InstanceConfiguration) -> Any? {
164
+ if let config = config.pluginConfigurations as? JSObject {
165
+ return config[keyPath: KeyPath(stringLiteral: "Logger.\(configKey)")]
166
+ }
167
+
168
+ return nil
169
+ }
170
+
171
+ public func error(_ message: String) {
172
+ log(atLevel: LogLevel.error, message: message)
173
+ }
174
+
175
+ public func warn(_ message: String) {
176
+ log(atLevel: LogLevel.warn, message: message)
177
+ }
178
+
179
+ public func info(_ message: String) {
180
+ log(atLevel: LogLevel.info, message: message)
181
+ }
182
+
183
+ public func log(_ message: String) {
184
+ log(atLevel: LogLevel.info, message: message)
185
+ }
186
+
187
+ public func debug(_ message: String) {
188
+ log(atLevel: LogLevel.debug, message: message)
189
+ }
190
+
191
+ public func dir(_ value: Any?) {
192
+ // Check the log level here to avoid conversion to string
193
+ if canLog(atLevel: LogLevel.info) {
194
+ log(atLevel: LogLevel.info, message: String(describing: value))
195
+ }
196
+ }
197
+
198
+ public func log(atLevel level: LogLevel, message: String) {
199
+ // This will never fail, but we have to keep swift happy
200
+ if let label = _labels[level] {
201
+ log(atLevel: level, label: label, tag: tag, message: message)
202
+ if let webView = self.webView {
203
+ DispatchQueue.main.async {
204
+ let combined = "\(label) \(self.tag) : \(message)"
205
+ let jsArg = self.toJSStringLiteral(combined)
206
+ webView.evaluateJavaScript("console.\(level.asString())(\(jsArg))", completionHandler: nil)
207
+ }
208
+ }
209
+ }
210
+ }
211
+
212
+ private func toJSStringLiteral(_ value: String) -> String {
213
+ // Prefer JSON encoding to produce a valid JS string literal
214
+ if let data = try? JSONEncoder().encode(value),
215
+ let encoded = String(data: data, encoding: .utf8) {
216
+ return encoded
217
+ }
218
+
219
+ // Fallback manual escaping (unlikely to be used)
220
+ var s = value
221
+ s = s.replacingOccurrences(of: "\\", with: "\\\\")
222
+ s = s.replacingOccurrences(of: "\"", with: "\\\"")
223
+ s = s.replacingOccurrences(of: "'", with: "\\'")
224
+ s = s.replacingOccurrences(of: "\n", with: "\\n")
225
+ s = s.replacingOccurrences(of: "\r", with: "\\r")
226
+ s = s.replacingOccurrences(of: "\u{2028}", with: "\\u2028")
227
+ s = s.replacingOccurrences(of: "\u{2029}", with: "\\u2029")
228
+ return "\"\(s)\""
229
+ }
230
+
231
+ public func log(atLevel level: LogLevel, label: String?, tag: String, message: String) {
232
+ // This will never fail, but we have to keep swift happy
233
+ if let label = label ?? _labels[level] {
234
+ print(atLevel: level, label: label, tag: tag, message: message)
235
+ // eval
236
+ }
237
+ }
238
+
239
+ private func canLog(atLevel level: LogLevel) -> Bool {
240
+ self.level.rawValue >= level.rawValue
241
+ }
242
+
243
+ private func print(atLevel level: LogLevel, label: String, tag: String, message: String) {
244
+ guard canLog(atLevel: level) else {
245
+ return
246
+ }
247
+
248
+ var msg = message
249
+
250
+ if !label.isEmpty {
251
+ // If the label is ASCII, put it after the tag, otherwise before.
252
+ if label[label.startIndex].isASCII {
253
+ msg = "[\(tag)] \(label): \(message)"
254
+ } else {
255
+ msg = "\(label) [\(tag)]: \(message)"
256
+ }
257
+ } else {
258
+ msg = "[\(tag)]: \(message)"
259
+ }
260
+
261
+ if useSyslog {
262
+ os_log("%{public}@", type: level.asOSLogType(), msg)
263
+ } else {
264
+ Swift.print(msg)
265
+ }
266
+ }
267
+
268
+ public func time(_ label: String?) {
269
+ timers[label ?? ""] = Date()
270
+ }
271
+
272
+ public func timeLog(_ label: String?) {
273
+ if let timer = timers[label ?? ""] {
274
+ info(formatTimeInterval(timer.timeIntervalSinceNow))
275
+ } else {
276
+ warn("timer \(label ?? kDefaultTimerLabel) does not exist")
277
+ }
278
+ }
279
+
280
+ public func timeEnd(_ label: String?) {
281
+ timeLog(label)
282
+ timers.removeValue(forKey: label ?? kDefaultTimerLabel)
283
+ }
284
+
285
+ private func formatTimeInterval(_ interval: TimeInterval) -> String {
286
+ let int = Int(interval)
287
+ let millis = Int(((1 + interval.remainder(dividingBy: 1)) * 1000).rounded())
288
+ let seconds = int % 60
289
+ let minutes = (int / 60) % 60
290
+ let hours = (int / 3600)
291
+
292
+ if seconds < 1 {
293
+ return "\(millis)ms"
294
+ }
295
+
296
+ if minutes < 1 {
297
+ return "\(seconds).\(String(format: "%0.3d", millis))s"
298
+ }
299
+
300
+ if hours < 1 {
301
+ return "\(minutes):\(String(format: "%0.2d", seconds)).\(String(format: "%0.3d", millis)) (min:sec.ms)"
302
+ }
303
+
304
+ return "\(hours):\(String(format: "%0.2d", minutes)):\(String(format: "%0.2d", seconds)) (hr:min:sec)"
305
+ }
306
+
307
+ public func trace() {
308
+ info(String(format: "%@", Thread.callStackSymbols))
309
+ }
310
+ }