Fixed a number of issues with the Notification Service Extension

Fixed an issue where receiving a PN for outgoing messages could break the NotificationServiceExtension
Fixed an issue where the NotificationServiceExtension could startup in an invalid way resulting in subsequent PNs failing to process
Fixed an issue where you could incorrectly receive multiple generic notifications after receiving an incoming call notification
Fixed an issue where the read state syncing might not clear notifications from the notification center
Fixed an issue with parsing Bencoded data
Updated the PN subscription to subscribe to CONVO_INFO_VOLATILE notifications (update read state)
Updated the NotificationServiceExtension to use standard message processing where possible
Updated the NotificationServiceExtension to update the app badge based on a database query
This commit is contained in:
Morgan Pretty 2023-09-08 11:42:04 +10:00
parent ab610578e6
commit a2f1f36d2c
21 changed files with 606 additions and 358 deletions

View File

@ -740,6 +740,7 @@
FD96F3A729DBD43D00401309 /* MockJobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD96F3A629DBD43D00401309 /* MockJobRunner.swift */; }; FD96F3A729DBD43D00401309 /* MockJobRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD96F3A629DBD43D00401309 /* MockJobRunner.swift */; };
FD97B2402A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */; }; FD97B2402A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */; };
FD97B2422A3FEBF30027DD57 /* UnreadMarkerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD97B2412A3FEBF30027DD57 /* UnreadMarkerCell.swift */; }; FD97B2422A3FEBF30027DD57 /* UnreadMarkerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD97B2412A3FEBF30027DD57 /* UnreadMarkerCell.swift */; };
FD9AECA52AAA9609009B3406 /* NotificationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9AECA42AAA9609009B3406 /* NotificationError.swift */; };
FD9B30F3293EA0BF008DEE3E /* BatchResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9B30F2293EA0BF008DEE3E /* BatchResponseSpec.swift */; }; FD9B30F3293EA0BF008DEE3E /* BatchResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9B30F2293EA0BF008DEE3E /* BatchResponseSpec.swift */; };
FD9BDE002A5D22B7005F1EBC /* libSessionUtil.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FD9BDDF82A5D2294005F1EBC /* libSessionUtil.a */; }; FD9BDE002A5D22B7005F1EBC /* libSessionUtil.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FD9BDDF82A5D2294005F1EBC /* libSessionUtil.a */; };
FD9BDE012A5D24EA005F1EBC /* SessionUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; }; FD9BDE012A5D24EA005F1EBC /* SessionUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; };
@ -1856,6 +1857,7 @@
FD96F3A629DBD43D00401309 /* MockJobRunner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockJobRunner.swift; sourceTree = "<group>"; }; FD96F3A629DBD43D00401309 /* MockJobRunner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockJobRunner.swift; sourceTree = "<group>"; };
FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARC4RandomNumberGenerator.swift; sourceTree = "<group>"; }; FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARC4RandomNumberGenerator.swift; sourceTree = "<group>"; };
FD97B2412A3FEBF30027DD57 /* UnreadMarkerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnreadMarkerCell.swift; sourceTree = "<group>"; }; FD97B2412A3FEBF30027DD57 /* UnreadMarkerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnreadMarkerCell.swift; sourceTree = "<group>"; };
FD9AECA42AAA9609009B3406 /* NotificationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationError.swift; sourceTree = "<group>"; };
FD9B30F2293EA0BF008DEE3E /* BatchResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchResponseSpec.swift; sourceTree = "<group>"; }; FD9B30F2293EA0BF008DEE3E /* BatchResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchResponseSpec.swift; sourceTree = "<group>"; };
FD9BDDF82A5D2294005F1EBC /* libSessionUtil.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libSessionUtil.a; sourceTree = BUILT_PRODUCTS_DIR; }; FD9BDDF82A5D2294005F1EBC /* libSessionUtil.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libSessionUtil.a; sourceTree = BUILT_PRODUCTS_DIR; };
FD9DD2702A72516D00ECB68E /* TestExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestExtensions.swift; sourceTree = "<group>"; }; FD9DD2702A72516D00ECB68E /* TestExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestExtensions.swift; sourceTree = "<group>"; };
@ -2408,6 +2410,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
C31C219B255BC92200EC2D66 /* Meta */, C31C219B255BC92200EC2D66 /* Meta */,
FD9AECA42AAA9609009B3406 /* NotificationError.swift */,
7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */, 7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */,
7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */, 7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */,
7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */, 7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */,
@ -5454,6 +5457,7 @@
7BDCFC08242186E700641C39 /* NotificationServiceExtensionContext.swift in Sources */, 7BDCFC08242186E700641C39 /* NotificationServiceExtensionContext.swift in Sources */,
7BC01A3E241F40AB00BC7C55 /* NotificationServiceExtension.swift in Sources */, 7BC01A3E241F40AB00BC7C55 /* NotificationServiceExtension.swift in Sources */,
7B1D74AA27BCC16E0030B423 /* NSENotificationPresenter.swift in Sources */, 7B1D74AA27BCC16E0030B423 /* NSENotificationPresenter.swift in Sources */,
FD9AECA52AAA9609009B3406 /* NotificationError.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@ -6445,7 +6449,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 422; CURRENT_PROJECT_VERSION = 423;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)"; FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -6469,7 +6473,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 2.4.0; MARKETING_VERSION = 2.4.1;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -6517,7 +6521,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 422; CURRENT_PROJECT_VERSION = 423;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
@ -6546,7 +6550,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 2.4.0; MARKETING_VERSION = 2.4.1;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
@ -6582,7 +6586,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 422; CURRENT_PROJECT_VERSION = 423;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)"; FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -6605,7 +6609,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 2.4.0; MARKETING_VERSION = 2.4.1;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
@ -6656,7 +6660,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 422; CURRENT_PROJECT_VERSION = 423;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO; ENABLE_NS_ASSERTIONS = NO;
@ -6684,7 +6688,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
"@executable_path/../../Frameworks", "@executable_path/../../Frameworks",
); );
MARKETING_VERSION = 2.4.0; MARKETING_VERSION = 2.4.1;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES; MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
@ -7616,7 +7620,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 422; CURRENT_PROJECT_VERSION = 423;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -7654,7 +7658,7 @@
"$(SRCROOT)", "$(SRCROOT)",
); );
LLVM_LTO = NO; LLVM_LTO = NO;
MARKETING_VERSION = 2.4.0; MARKETING_VERSION = 2.4.1;
OTHER_LDFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
@ -7687,7 +7691,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer"; CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 422; CURRENT_PROJECT_VERSION = 423;
DEVELOPMENT_TEAM = SUQ8J2PCT7; DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -7725,7 +7729,7 @@
"$(SRCROOT)", "$(SRCROOT)",
); );
LLVM_LTO = NO; LLVM_LTO = NO;
MARKETING_VERSION = 2.4.0; MARKETING_VERSION = 2.4.1;
OTHER_LDFLAGS = "$(inherited)"; OTHER_LDFLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
PRODUCT_NAME = Session; PRODUCT_NAME = Session;

View File

@ -93,7 +93,8 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
public func reportOutgoingCall(_ call: SessionCall) { public func reportOutgoingCall(_ call: SessionCall) {
AssertIsOnMainThread() AssertIsOnMainThread()
UserDefaults.sharedLokiProject?.set(true, forKey: "isCallOngoing") UserDefaults.sharedLokiProject?[.isCallOngoing] = true
UserDefaults.sharedLokiProject?[.lastCallPreOffer] = Date()
call.stateDidChange = { call.stateDidChange = {
if call.hasStartedConnecting { if call.hasStartedConnecting {
@ -123,7 +124,8 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
completion(error) completion(error)
return return
} }
UserDefaults.sharedLokiProject?.set(true, forKey: "isCallOngoing") UserDefaults.sharedLokiProject?[.isCallOngoing] = true
UserDefaults.sharedLokiProject?[.lastCallPreOffer] = Date()
completion(nil) completion(nil)
} }
} }
@ -138,7 +140,9 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
func handleCallEnded() { func handleCallEnded() {
WebRTCSession.current = nil WebRTCSession.current = nil
UserDefaults.sharedLokiProject?.set(false, forKey: "isCallOngoing") UserDefaults.sharedLokiProject?[.isCallOngoing] = false
UserDefaults.sharedLokiProject?[.lastCallPreOffer] = nil
if CurrentAppContext().isInBackground() { if CurrentAppContext().isInBackground() {
(UIApplication.shared.delegate as? AppDelegate)?.stopPollers() (UIApplication.shared.delegate as? AppDelegate)?.stopPollers()
DDLog.flushLog() DDLog.flushLog()

View File

@ -89,7 +89,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
) )
if Environment.shared?.callManager.wrappedValue?.currentCall == nil { if Environment.shared?.callManager.wrappedValue?.currentCall == nil {
UserDefaults.sharedLokiProject?.set(false, forKey: "isCallOngoing") UserDefaults.sharedLokiProject?[.isCallOngoing] = false
UserDefaults.sharedLokiProject?[.lastCallPreOffer] = nil
} }
// No point continuing if we are running tests // No point continuing if we are running tests
@ -416,7 +417,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
// Don't offer the 'Restore' option if it was a 'startupFailed' error as a restore is unlikely to // Don't offer the 'Restore' option if it was a 'startupFailed' error as a restore is unlikely to
// resolve it (most likely the database is locked or the key was somehow lost - safer to get them // resolve it (most likely the database is locked or the key was somehow lost - safer to get them
// to restart and manually reinstall/restore) // to restart and manually reinstall/restore)
case .databaseError(StorageError.startupFailed): break case .databaseError(StorageError.startupFailed), .databaseError(DatabaseError.SQLITE_LOCKED): break
// Offer the 'Restore' option if it was a migration error // Offer the 'Restore' option if it was a migration error
case .databaseError: case .databaseError:
@ -663,39 +664,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
/// we don't block user interaction while it's running /// we don't block user interaction while it's running
DispatchQueue.global(qos: .default).async { DispatchQueue.global(qos: .default).async {
let unreadCount: Int = Storage.shared let unreadCount: Int = Storage.shared
.read { db in .read { db in try Interaction.fetchUnreadCount(db) }
let userPublicKey: String = getUserHexEncodedPublicKey(db)
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
return try Interaction
.filter(Interaction.Columns.wasRead == false)
.filter(Interaction.Variant.variantsToIncrementUnreadCount.contains(Interaction.Columns.variant))
.filter(
// Only count mentions if 'onlyNotifyForMentions' is set
thread[.onlyNotifyForMentions] == false ||
Interaction.Columns.hasMention == true
)
.joining(
required: Interaction.thread
.aliased(thread)
.joining(optional: SessionThread.contact)
.filter(
// Ignore muted threads
SessionThread.Columns.mutedUntilTimestamp == nil ||
SessionThread.Columns.mutedUntilTimestamp < Date().timeIntervalSince1970
)
.filter(
// Ignore message request threads
SessionThread.Columns.variant != SessionThread.Variant.contact ||
!SessionThread.isMessageRequest(userPublicKey: userPublicKey)
)
)
.fetchCount(db)
}
.defaulting(to: 0) .defaulting(to: 0)
DispatchQueue.main.async { DispatchQueue.main.async {
CurrentAppContext().setMainAppBadgeNumber(unreadCount) UIApplication.shared.applicationIconBadgeNumber = unreadCount
} }
} }
} }
@ -904,7 +877,9 @@ private enum StartupError: Error {
var name: String { var name: String {
switch self { switch self {
case .databaseError(StorageError.startupFailed): return "Database startup failed" case .databaseError(StorageError.startupFailed), .databaseError(DatabaseError.SQLITE_LOCKED):
return "Database startup failed"
case .failedToRestore: return "Failed to restore" case .failedToRestore: return "Failed to restore"
case .databaseError: return "Database error" case .databaseError: return "Database error"
case .startupTimeout: return "Startup timeout" case .startupTimeout: return "Startup timeout"
@ -913,7 +888,9 @@ private enum StartupError: Error {
var message: String { var message: String {
switch self { switch self {
case .databaseError(StorageError.startupFailed): return "DATABASE_STARTUP_FAILED".localized() case .databaseError(StorageError.startupFailed), .databaseError(DatabaseError.SQLITE_LOCKED):
return "DATABASE_STARTUP_FAILED".localized()
case .failedToRestore: return "DATABASE_RESTORE_FAILED".localized() case .failedToRestore: return "DATABASE_RESTORE_FAILED".localized()
case .databaseError: return "DATABASE_MIGRATION_FAILED".localized() case .databaseError: return "DATABASE_MIGRATION_FAILED".localized()
case .startupTimeout: return "APP_STARTUP_TIMEOUT".localized() case .startupTimeout: return "APP_STARTUP_TIMEOUT".localized()

View File

@ -179,11 +179,6 @@ final class MainAppContext: NSObject, AppContext {
UIApplication.shared.isIdleTimerDisabled = shouldBeBlocking UIApplication.shared.isIdleTimerDisabled = shouldBeBlocking
} }
func setMainAppBadgeNumber(_ value: Int) {
UIApplication.shared.applicationIconBadgeNumber = value
UserDefaults.sharedLokiProject?.setValue(value, forKey: "currentBadgeNumber")
}
func frontmostViewController() -> UIViewController? { func frontmostViewController() -> UIViewController? {
UIApplication.shared.frontmostViewControllerIgnoringAlerts UIApplication.shared.frontmostViewControllerIgnoringAlerts
} }

View File

@ -240,7 +240,7 @@ final class NukeDataModal: Modal {
// Clear the app badge and notifications // Clear the app badge and notifications
AppEnvironment.shared.notificationPresenter.clearAllNotifications() AppEnvironment.shared.notificationPresenter.clearAllNotifications()
CurrentAppContext().setMainAppBadgeNumber(0) UIApplication.shared.applicationIconBadgeNumber = 0
// Clear out the user defaults // Clear out the user defaults
UserDefaults.removeAll() UserDefaults.removeAll()

View File

@ -465,6 +465,46 @@ public extension Interaction {
// MARK: - GRDB Interactions // MARK: - GRDB Interactions
public extension Interaction { public extension Interaction {
struct ReadInfo: Decodable, FetchableRecord {
let id: Int64
let variant: Interaction.Variant
let timestampMs: Int64
let wasRead: Bool
}
static func fetchUnreadCount(
_ db: Database,
using dependencies: Dependencies = Dependencies()
) throws -> Int {
let userPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
return try Interaction
.filter(Interaction.Columns.wasRead == false)
.filter(Interaction.Variant.variantsToIncrementUnreadCount.contains(Interaction.Columns.variant))
.filter(
// Only count mentions if 'onlyNotifyForMentions' is set
thread[.onlyNotifyForMentions] == false ||
Interaction.Columns.hasMention == true
)
.joining(
required: Interaction.thread
.aliased(thread)
.joining(optional: SessionThread.contact)
.filter(
// Ignore muted threads
SessionThread.Columns.mutedUntilTimestamp == nil ||
SessionThread.Columns.mutedUntilTimestamp < dependencies.dateNow.timeIntervalSince1970
)
.filter(
// Ignore message request threads
SessionThread.Columns.variant != SessionThread.Variant.contact ||
!SessionThread.isMessageRequest(userPublicKey: userPublicKey)
)
)
.fetchCount(db)
}
/// This will update the `wasRead` state the the interaction /// This will update the `wasRead` state the the interaction
/// ///
/// - Parameters /// - Parameters
@ -482,83 +522,16 @@ public extension Interaction {
) throws { ) throws {
guard let interactionId: Int64 = interactionId else { return } guard let interactionId: Int64 = interactionId else { return }
struct InteractionReadInfo: Decodable, FetchableRecord {
let id: Int64
let variant: Interaction.Variant
let timestampMs: Int64
let wasRead: Bool
}
// Once all of the below is done schedule the jobs
func scheduleJobs(
_ db: Database,
threadId: String,
threadVariant: SessionThread.Variant,
interactionInfo: [InteractionReadInfo],
lastReadTimestampMs: Int64
) throws {
// Update the last read timestamp if needed
try SessionUtil.syncThreadLastReadIfNeeded(
db,
threadId: threadId,
threadVariant: threadVariant,
lastReadTimestampMs: lastReadTimestampMs
)
// Add the 'DisappearingMessagesJob' if needed - this will update any expiring
// messages `expiresStartedAtMs` values
JobRunner.upsert(
db,
job: DisappearingMessagesJob.updateNextRunIfNeeded(
db,
interactionIds: interactionInfo.map { $0.id },
startedAtMs: TimeInterval(SnodeAPI.currentOffsetTimestampMs())
)
)
// Clear out any notifications for the interactions we mark as read
Environment.shared?.notificationsManager.wrappedValue?.cancelNotifications(
identifiers: interactionInfo
.map { interactionInfo in
Interaction.notificationIdentifier(
for: interactionInfo.id,
threadId: threadId,
shouldGroupMessagesForThread: false
)
}
.appending(Interaction.notificationIdentifier(
for: 0,
threadId: threadId,
shouldGroupMessagesForThread: true
))
)
// If we want to send read receipts and it's a contact thread then try to add the
// 'SendReadReceiptsJob' for and unread messages that weren't outgoing
if trySendReadReceipt && threadVariant == .contact {
JobRunner.upsert(
db,
job: SendReadReceiptsJob.createOrUpdateIfNeeded(
db,
threadId: threadId,
interactionIds: interactionInfo
.filter { !$0.wasRead && $0.variant != .standardOutgoing }
.map { $0.id }
)
)
}
}
// Since there is no guarantee on the order messages are inserted into the database // Since there is no guarantee on the order messages are inserted into the database
// fetch the timestamp for the interaction and set everything before that as read // fetch the timestamp for the interaction and set everything before that as read
let maybeInteractionInfo: InteractionReadInfo? = try Interaction let maybeInteractionInfo: Interaction.ReadInfo? = try Interaction
.select(.id, .variant, .timestampMs, .wasRead) .select(.id, .variant, .timestampMs, .wasRead)
.filter(id: interactionId) .filter(id: interactionId)
.asRequest(of: InteractionReadInfo.self) .asRequest(of: Interaction.ReadInfo.self)
.fetchOne(db) .fetchOne(db)
// If we aren't including older interactions then update and save the current one // If we aren't including older interactions then update and save the current one
guard includingOlder, let interactionInfo: InteractionReadInfo = maybeInteractionInfo else { guard includingOlder, let interactionInfo: Interaction.ReadInfo = maybeInteractionInfo else {
// Only mark as read and trigger the subsequent jobs if the interaction is // Only mark as read and trigger the subsequent jobs if the interaction is
// actually not read (no point updating and triggering db changes otherwise) // actually not read (no point updating and triggering db changes otherwise)
guard guard
@ -575,19 +548,21 @@ public extension Interaction {
.filter(id: interactionId) .filter(id: interactionId)
.updateAll(db, Columns.wasRead.set(to: true)) .updateAll(db, Columns.wasRead.set(to: true))
try scheduleJobs( try Interaction.scheduleReadJobs(
db, db,
threadId: threadId, threadId: threadId,
threadVariant: threadVariant, threadVariant: threadVariant,
interactionInfo: [ interactionInfo: [
InteractionReadInfo( Interaction.ReadInfo(
id: interactionId, id: interactionId,
variant: variant, variant: variant,
timestampMs: 0, timestampMs: 0,
wasRead: false wasRead: false
) )
], ],
lastReadTimestampMs: timestampMs lastReadTimestampMs: timestampMs,
trySendReadReceipt: trySendReadReceipt,
calledFromConfigHandling: false
) )
return return
} }
@ -596,21 +571,23 @@ public extension Interaction {
.filter(Interaction.Columns.threadId == threadId) .filter(Interaction.Columns.threadId == threadId)
.filter(Interaction.Columns.timestampMs <= interactionInfo.timestampMs) .filter(Interaction.Columns.timestampMs <= interactionInfo.timestampMs)
.filter(Interaction.Columns.wasRead == false) .filter(Interaction.Columns.wasRead == false)
let interactionInfoToMarkAsRead: [InteractionReadInfo] = try interactionQuery let interactionInfoToMarkAsRead: [Interaction.ReadInfo] = try interactionQuery
.select(.id, .variant, .timestampMs, .wasRead) .select(.id, .variant, .timestampMs, .wasRead)
.asRequest(of: InteractionReadInfo.self) .asRequest(of: Interaction.ReadInfo.self)
.fetchAll(db) .fetchAll(db)
// If there are no other interactions to mark as read then just schedule the jobs // If there are no other interactions to mark as read then just schedule the jobs
// for this interaction (need to ensure the disapeparing messages run for sync'ed // for this interaction (need to ensure the disapeparing messages run for sync'ed
// outgoing messages which will always have 'wasRead' as false) // outgoing messages which will always have 'wasRead' as false)
guard !interactionInfoToMarkAsRead.isEmpty else { guard !interactionInfoToMarkAsRead.isEmpty else {
try scheduleJobs( try Interaction.scheduleReadJobs(
db, db,
threadId: threadId, threadId: threadId,
threadVariant: threadVariant, threadVariant: threadVariant,
interactionInfo: [interactionInfo], interactionInfo: [interactionInfo],
lastReadTimestampMs: interactionInfo.timestampMs lastReadTimestampMs: interactionInfo.timestampMs,
trySendReadReceipt: trySendReadReceipt,
calledFromConfigHandling: false
) )
return return
} }
@ -619,12 +596,14 @@ public extension Interaction {
try interactionQuery.updateAll(db, Columns.wasRead.set(to: true)) try interactionQuery.updateAll(db, Columns.wasRead.set(to: true))
// Retrieve the interaction ids we want to update // Retrieve the interaction ids we want to update
try scheduleJobs( try Interaction.scheduleReadJobs(
db, db,
threadId: threadId, threadId: threadId,
threadVariant: threadVariant, threadVariant: threadVariant,
interactionInfo: interactionInfoToMarkAsRead, interactionInfo: interactionInfoToMarkAsRead,
lastReadTimestampMs: interactionInfo.timestampMs lastReadTimestampMs: interactionInfo.timestampMs,
trySendReadReceipt: trySendReadReceipt,
calledFromConfigHandling: false
) )
} }
@ -691,6 +670,71 @@ public extension Interaction {
.asSet() .asSet()
.subtracting(timestampsUpdated) .subtracting(timestampsUpdated)
} }
static func scheduleReadJobs(
_ db: Database,
threadId: String,
threadVariant: SessionThread.Variant,
interactionInfo: [Interaction.ReadInfo],
lastReadTimestampMs: Int64,
trySendReadReceipt: Bool,
calledFromConfigHandling: Bool
) throws {
guard !interactionInfo.isEmpty else { return }
// Update the last read timestamp if needed
if !calledFromConfigHandling {
try SessionUtil.syncThreadLastReadIfNeeded(
db,
threadId: threadId,
threadVariant: threadVariant,
lastReadTimestampMs: lastReadTimestampMs
)
}
// Add the 'DisappearingMessagesJob' if needed - this will update any expiring
// messages `expiresStartedAtMs` values
JobRunner.upsert(
db,
job: DisappearingMessagesJob.updateNextRunIfNeeded(
db,
interactionIds: interactionInfo.map { $0.id },
startedAtMs: TimeInterval(SnodeAPI.currentOffsetTimestampMs())
)
)
// Clear out any notifications for the interactions we mark as read
Environment.shared?.notificationsManager.wrappedValue?.cancelNotifications(
identifiers: interactionInfo
.map { interactionInfo in
Interaction.notificationIdentifier(
for: interactionInfo.id,
threadId: threadId,
shouldGroupMessagesForThread: false
)
}
.appending(Interaction.notificationIdentifier(
for: 0,
threadId: threadId,
shouldGroupMessagesForThread: true
))
)
/// If we want to send read receipts and it's a contact thread then try to add the `SendReadReceiptsJob` for and unread
/// messages that weren't outgoing
if trySendReadReceipt && threadVariant == .contact {
JobRunner.upsert(
db,
job: SendReadReceiptsJob.createOrUpdateIfNeeded(
db,
threadId: threadId,
interactionIds: interactionInfo
.filter { !$0.wasRead && $0.variant != .standardOutgoing }
.map { $0.id }
)
)
}
}
} }
// MARK: - Search Queries // MARK: - Search Queries

View File

@ -41,7 +41,7 @@ extension PushNotificationAPI.NotificationMetadata {
hash: try container.decode(String.self, forKey: .hash), hash: try container.decode(String.self, forKey: .hash),
namespace: try container.decode(Int.self, forKey: .namespace), namespace: try container.decode(Int.self, forKey: .namespace),
dataLength: try container.decode(Int.self, forKey: .dataLength), dataLength: try container.decode(Int.self, forKey: .dataLength),
dataTooLong: ((try? container.decode(Bool.self, forKey: .dataTooLong)) ?? false) dataTooLong: ((try? container.decode(Int.self, forKey: .dataTooLong) != 0) ?? false)
) )
} }
} }

View File

@ -56,7 +56,7 @@ public enum PushNotificationAPI {
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies) let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
let request: SubscribeRequest = SubscribeRequest( let request: SubscribeRequest = SubscribeRequest(
pubkey: currentUserPublicKey, pubkey: currentUserPublicKey,
namespaces: [.default], namespaces: [.default, .configConvoInfoVolatile],
// Note: Unfortunately we always need the message content because without the content // Note: Unfortunately we always need the message content because without the content
// control messages can't be distinguished from visible messages which results in the // control messages can't be distinguished from visible messages which results in the
// 'generic' notification being shown when receiving things like typing indicator updates // 'generic' notification being shown when receiving things like typing indicator updates
@ -372,8 +372,11 @@ public enum PushNotificationAPI {
return (envelope, .legacySuccess) return (envelope, .legacySuccess)
} }
guard let base64EncodedEncString: String = notificationContent.userInfo["enc_payload"] as? String else {
return (nil, .failureNoContent)
}
guard guard
let base64EncodedEncString: String = notificationContent.userInfo["enc_payload"] as? String,
let encData: Data = Data(base64Encoded: base64EncodedEncString), let encData: Data = Data(base64Encoded: base64EncodedEncString),
let notificationsEncryptionKey: Data = try? getOrGenerateEncryptionKey(using: dependencies), let notificationsEncryptionKey: Data = try? getOrGenerateEncryptionKey(using: dependencies),
encData.count > dependencies.crypto.size(.aeadXChaCha20NonceBytes) encData.count > dependencies.crypto.size(.aeadXChaCha20NonceBytes)
@ -401,7 +404,7 @@ public enum PushNotificationAPI {
// If the metadata says that the message was too large then we should show the generic // If the metadata says that the message was too large then we should show the generic
// notification (this is a valid case) // notification (this is a valid case)
guard !notification.info.dataTooLong else { return (nil, .success) } guard !notification.info.dataTooLong else { return (nil, .successTooLong) }
// Check that the body we were given is valid // Check that the body we were given is valid
guard guard

View File

@ -5,7 +5,9 @@ import Foundation
public extension PushNotificationAPI { public extension PushNotificationAPI {
enum ProcessResult { enum ProcessResult {
case success case success
case successTooLong
case failure case failure
case failureNoContent
case legacySuccess case legacySuccess
case legacyFailure case legacyFailure
case legacyForceSilent case legacyForceSilent

View File

@ -77,16 +77,28 @@ internal extension SessionUtil {
} }
// Mark all older interactions as read // Mark all older interactions as read
try Interaction let interactionQuery = Interaction
.filter( .filter(Interaction.Columns.threadId == threadId)
Interaction.Columns.threadId == threadId && .filter(Interaction.Columns.timestampMs <= lastReadTimestampMs)
Interaction.Columns.timestampMs <= lastReadTimestampMs && .filter(Interaction.Columns.wasRead == false)
Interaction.Columns.wasRead == false let interactionInfoToMarkAsRead: [Interaction.ReadInfo] = try interactionQuery
) .select(.id, .variant, .timestampMs, .wasRead)
.asRequest(of: Interaction.ReadInfo.self)
.fetchAll(db)
try interactionQuery
.updateAll( // Handling a config update so don't use `updateAllAndConfig` .updateAll( // Handling a config update so don't use `updateAllAndConfig`
db, db,
Interaction.Columns.wasRead.set(to: true) Interaction.Columns.wasRead.set(to: true)
) )
try Interaction.scheduleReadJobs(
db,
threadId: threadId,
threadVariant: threadInfo.variant,
interactionInfo: interactionInfoToMarkAsRead,
lastReadTimestampMs: lastReadTimestampMs,
trySendReadReceipt: false, // Interactions already read, no need to send
calledFromConfigHandling: true
)
return nil return nil
} }

View File

@ -56,11 +56,9 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
notificationContent.sound = thread.notificationSound notificationContent.sound = thread.notificationSound
.defaulting(to: db[.defaultNotificationSound] ?? Preferences.Sound.defaultNotificationSound) .defaulting(to: db[.defaultNotificationSound] ?? Preferences.Sound.defaultNotificationSound)
.notificationSound(isQuiet: false) .notificationSound(isQuiet: false)
notificationContent.badge = (try? Interaction.fetchUnreadCount(db))
// Badge Number .map { NSNumber(value: $0) }
let newBadgeNumber = CurrentAppContext().appUserDefaults().integer(forKey: "currentBadgeNumber") + 1 .defaulting(to: NSNumber(value: 0))
notificationContent.badge = NSNumber(value: newBadgeNumber)
CurrentAppContext().appUserDefaults().set(newBadgeNumber, forKey: "currentBadgeNumber")
// Title & body // Title & body
let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType] let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType]
@ -157,16 +155,11 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
let notificationContent = UNMutableNotificationContent() let notificationContent = UNMutableNotificationContent()
notificationContent.userInfo = userInfo notificationContent.userInfo = userInfo
notificationContent.sound = thread.notificationSound notificationContent.sound = thread.notificationSound
.defaulting( .defaulting(to: db[.defaultNotificationSound] ?? Preferences.Sound.defaultNotificationSound)
to: db[.defaultNotificationSound]
.defaulting(to: Preferences.Sound.defaultNotificationSound)
)
.notificationSound(isQuiet: false) .notificationSound(isQuiet: false)
notificationContent.badge = (try? Interaction.fetchUnreadCount(db))
// Badge Number .map { NSNumber(value: $0) }
let newBadgeNumber = CurrentAppContext().appUserDefaults().integer(forKey: "currentBadgeNumber") + 1 .defaulting(to: NSNumber(value: 0))
notificationContent.badge = NSNumber(value: newBadgeNumber)
CurrentAppContext().appUserDefaults().set(newBadgeNumber, forKey: "currentBadgeNumber")
notificationContent.title = "Session" notificationContent.title = "Session"
notificationContent.body = "" notificationContent.body = ""

View File

@ -0,0 +1,18 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionMessagingKit
enum NotificationError: LocalizedError {
case processing(PushNotificationAPI.ProcessResult)
case messageProcessing
case messageHandling(MessageReceiverError)
public var errorDescription: String? {
switch self {
case .processing(let result): return "Failed to process notification (\(result))"
case .messageProcessing: return "Failed to process message"
case .messageHandling(let error): return "Failed to handle message (\(error))"
}
}
}

View File

@ -15,11 +15,13 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
private var didPerformSetup = false private var didPerformSetup = false
private var contentHandler: ((UNNotificationContent) -> Void)? private var contentHandler: ((UNNotificationContent) -> Void)?
private var request: UNNotificationRequest? private var request: UNNotificationRequest?
private var openGroupPollCancellable: AnyCancellable?
public static let isFromRemoteKey = "remote" public static let isFromRemoteKey = "remote"
public static let threadIdKey = "Signal.AppNotificationsUserInfoKey.threadId" public static let threadIdKey = "Signal.AppNotificationsUserInfoKey.threadId"
public static let threadVariantRaw = "Signal.AppNotificationsUserInfoKey.threadVariantRaw" public static let threadVariantRaw = "Signal.AppNotificationsUserInfoKey.threadVariantRaw"
public static let threadNotificationCounter = "Session.AppNotificationsUserInfoKey.threadNotificationCounter" public static let threadNotificationCounter = "Session.AppNotificationsUserInfoKey.threadNotificationCounter"
private static let callPreOfferLargeNotificationSupressionDuration: TimeInterval = 30
// MARK: Did receive a remote push notification request // MARK: Did receive a remote push notification request
@ -43,6 +45,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
let isCallOngoing: Bool = (UserDefaults.sharedLokiProject?[.isCallOngoing]) let isCallOngoing: Bool = (UserDefaults.sharedLokiProject?[.isCallOngoing])
.defaulting(to: false) .defaulting(to: false)
let lastCallPreOffer: Date? = UserDefaults.sharedLokiProject?[.lastCallPreOffer]
// Perform main setup // Perform main setup
Storage.resumeDatabaseAccess() Storage.resumeDatabaseAccess()
@ -52,14 +55,13 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
AppReadiness.runNowOrWhenAppDidBecomeReady { AppReadiness.runNowOrWhenAppDidBecomeReady {
let openGroupPollingPublishers: [AnyPublisher<Void, Error>] = self.pollForOpenGroups() let openGroupPollingPublishers: [AnyPublisher<Void, Error>] = self.pollForOpenGroups()
defer { defer {
Publishers self.openGroupPollCancellable = Publishers
.MergeMany(openGroupPollingPublishers) .MergeMany(openGroupPollingPublishers)
.subscribe(on: DispatchQueue.global(qos: .background)) .subscribe(on: DispatchQueue.global(qos: .background))
.subscribe(on: DispatchQueue.main) .subscribe(on: DispatchQueue.main)
.sinkUntilComplete( .sink(
receiveCompletion: { _ in receiveCompletion: { [weak self] _ in self?.completeSilenty() },
self.completeSilenty() receiveValue: { _ in }
}
) )
} }
@ -75,9 +77,21 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
// If we got an explicit failure, or we got a success but no content then show // If we got an explicit failure, or we got a success but no content then show
// the fallback notification // the fallback notification
case .success, .legacySuccess, .failure, .legacyFailure: case .success, .legacySuccess, .failure, .legacyFailure:
return self.handleFailure(for: notificationContent) return self.handleFailure(for: notificationContent, error: .processing(result))
case .legacyForceSilent: return case .successTooLong:
/// If the notification is too long and there is an ongoing call or a recent call pre-offer then we assume the notification
/// is a call `ICE_CANDIDATES` message and just complete silently (because the fallback would be annoying), if not
/// then we do want to show the fallback notification
guard
isCallOngoing ||
(lastCallPreOffer ?? Date.distantPast).timeIntervalSinceNow < NotificationServiceExtension.callPreOfferLargeNotificationSupressionDuration
else { return self.handleFailure(for: notificationContent, error: .processing(result)) }
NSLog("[NotificationServiceExtension] Suppressing large notification too close to a call.")
return
case .legacyForceSilent, .failureNoContent: return
} }
} }
@ -87,10 +101,26 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
Storage.shared.write { db in Storage.shared.write { db in
do { do {
guard let processedMessage: ProcessedMessage = try Message.processRawReceivedMessageAsNotification(db, envelope: envelope) else { guard let processedMessage: ProcessedMessage = try Message.processRawReceivedMessageAsNotification(db, envelope: envelope) else {
self.handleFailure(for: notificationContent) self.handleFailure(for: notificationContent, error: .messageProcessing)
return return
} }
/// Due to the way the `CallMessage` and `SharedConfigMessage` work we need to custom
/// handle their behaviours, for all other message types we want to just use standard messages
switch processedMessage.messageInfo.message {
case is CallMessage, is SharedConfigMessage: break
default:
try MessageReceiver.handle(
db,
threadId: processedMessage.threadId,
threadVariant: processedMessage.threadVariant,
message: processedMessage.messageInfo.message,
serverExpirationTimestamp: processedMessage.messageInfo.serverExpirationTimestamp,
associatedWithProto: processedMessage.proto
)
return
}
// Throw if the message is outdated and shouldn't be processed // Throw if the message is outdated and shouldn't be processed
try MessageReceiver.throwIfMessageOutdated( try MessageReceiver.throwIfMessageOutdated(
db, db,
@ -100,47 +130,6 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
) )
switch processedMessage.messageInfo.message { switch processedMessage.messageInfo.message {
case let visibleMessage as VisibleMessage:
let interactionId: Int64 = try MessageReceiver.handleVisibleMessage(
db,
threadId: processedMessage.threadId,
threadVariant: processedMessage.threadVariant,
message: visibleMessage,
associatedWithProto: processedMessage.proto
)
// Remove the notifications if there is an outgoing messages from a linked device
if
let interaction: Interaction = try? Interaction.fetchOne(db, id: interactionId),
interaction.variant == .standardOutgoing
{
let semaphore = DispatchSemaphore(value: 0)
let center = UNUserNotificationCenter.current()
center.getDeliveredNotifications { notifications in
let matchingNotifications = notifications.filter({ $0.request.content.userInfo[NotificationServiceExtension.threadIdKey] as? String == interaction.threadId })
center.removeDeliveredNotifications(withIdentifiers: matchingNotifications.map({ $0.request.identifier }))
// Hack: removeDeliveredNotifications seems to be async,need to wait for some time before the delivered notifications can be removed.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { semaphore.signal() }
}
semaphore.wait()
}
case let unsendRequest as UnsendRequest:
try MessageReceiver.handleUnsendRequest(
db,
threadId: processedMessage.threadId,
threadVariant: processedMessage.threadVariant,
message: unsendRequest
)
case let closedGroupControlMessage as ClosedGroupControlMessage:
try MessageReceiver.handleClosedGroupControlMessage(
db,
threadId: processedMessage.threadId,
threadVariant: processedMessage.threadVariant,
message: closedGroupControlMessage
)
case let callMessage as CallMessage: case let callMessage as CallMessage:
try MessageReceiver.handleCallMessage( try MessageReceiver.handleCallMessage(
db, db,
@ -187,6 +176,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
break break
} }
try MessageReceiver.insertCallInfoMessage(db, for: callMessage)
self.handleSuccessForIncomingCall(db, for: callMessage) self.handleSuccessForIncomingCall(db, for: callMessage)
case let sharedConfigMessage as SharedConfigMessage: case let sharedConfigMessage as SharedConfigMessage:
@ -210,7 +200,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
if let error = error as? MessageReceiverError, error.isRetryable { if let error = error as? MessageReceiverError, error.isRetryable {
switch error { switch error {
case .invalidGroupPublicKey, .noGroupKeyPair, .outdatedMessage: self.completeSilenty() case .invalidGroupPublicKey, .noGroupKeyPair, .outdatedMessage: self.completeSilenty()
default: self.handleFailure(for: notificationContent) default: self.handleFailure(for: notificationContent, error: .messageHandling(error))
} }
} }
} }
@ -236,6 +226,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
Cryptography.seedRandom() Cryptography.seedRandom()
AppSetup.setupEnvironment( AppSetup.setupEnvironment(
retrySetupIfDatabaseInvalid: true,
appSpecificBlock: { appSpecificBlock: {
Environment.shared?.notificationsManager.mutate { Environment.shared?.notificationsManager.mutate {
$0 = NSENotificationPresenter() $0 = NSENotificationPresenter()
@ -307,14 +298,21 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
override public func serviceExtensionTimeWillExpire() { override public func serviceExtensionTimeWillExpire() {
// Called just before the extension will be terminated by the system. // Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
NSLog("[NotificationServiceExtension] Execution time expired")
openGroupPollCancellable?.cancel()
completeSilenty() completeSilenty()
} }
private func completeSilenty() { private func completeSilenty() {
NSLog("[NotificationServiceExtension] Complete silently") NSLog("[NotificationServiceExtension] Complete silently")
let silentContent: UNMutableNotificationContent = UNMutableNotificationContent()
silentContent.badge = Storage.shared
.read { db in try Interaction.fetchUnreadCount(db) }
.map { NSNumber(value: $0) }
.defaulting(to: NSNumber(value: 0))
Storage.suspendDatabaseAccess() Storage.suspendDatabaseAccess()
self.contentHandler!(.init()) self.contentHandler!(silentContent)
} }
private func handleSuccessForIncomingCall(_ db: Database, for callMessage: CallMessage) { private func handleSuccessForIncomingCall(_ db: Database, for callMessage: CallMessage) {
@ -330,11 +328,12 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
CXProvider.reportNewIncomingVoIPPushPayload(payload) { error in CXProvider.reportNewIncomingVoIPPushPayload(payload) { error in
if let error = error { if let error = error {
self.handleFailureForVoIP(db, for: callMessage) self.handleFailureForVoIP(db, for: callMessage)
SNLog("Failed to notify main app of call message: \(error)") NSLog("[NotificationServiceExtension] Failed to notify main app of call message: \(error)")
} }
else { else {
NSLog("[NotificationServiceExtension] Successfully notified main app of call message.")
UserDefaults.sharedLokiProject?[.lastCallPreOffer] = Date()
self.completeSilenty() self.completeSilenty()
SNLog("Successfully notified main app of call message.")
} }
} }
} }
@ -347,11 +346,9 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
let notificationContent = UNMutableNotificationContent() let notificationContent = UNMutableNotificationContent()
notificationContent.userInfo = [ NotificationServiceExtension.isFromRemoteKey : true ] notificationContent.userInfo = [ NotificationServiceExtension.isFromRemoteKey : true ]
notificationContent.title = "Session" notificationContent.title = "Session"
notificationContent.badge = (try? Interaction.fetchUnreadCount(db))
// Badge Number .map { NSNumber(value: $0) }
let newBadgeNumber = CurrentAppContext().appUserDefaults().integer(forKey: "currentBadgeNumber") + 1 .defaulting(to: NSNumber(value: 0))
notificationContent.badge = NSNumber(value: newBadgeNumber)
CurrentAppContext().appUserDefaults().set(newBadgeNumber, forKey: "currentBadgeNumber")
if let sender: String = callMessage.sender { if let sender: String = callMessage.sender {
let senderDisplayName: String = Profile.displayName(db, id: sender, threadVariant: .contact) let senderDisplayName: String = Profile.displayName(db, id: sender, threadVariant: .contact)
@ -367,20 +364,21 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
UNUserNotificationCenter.current().add(request) { error in UNUserNotificationCenter.current().add(request) { error in
if let error = error { if let error = error {
SNLog("Failed to add notification request due to error:\(error)") NSLog("[NotificationServiceExtension] Failed to add notification request due to error: \(error)")
} }
semaphore.signal() semaphore.signal()
} }
semaphore.wait() semaphore.wait()
SNLog("Add remote notification request") NSLog("[NotificationServiceExtension] Add remote notification request")
} }
private func handleFailure(for content: UNMutableNotificationContent) { private func handleFailure(for content: UNMutableNotificationContent, error: NotificationError) {
NSLog("[NotificationServiceExtension] Show generic failure message due to error: \(error)")
Storage.suspendDatabaseAccess() Storage.suspendDatabaseAccess()
content.body = "You've got a new message"
content.title = "Session" content.title = "Session"
let userInfo: [String:Any] = [ NotificationServiceExtension.isFromRemoteKey : true ] content.body = "APN_Message".localized()
let userInfo: [String: Any] = [ NotificationServiceExtension.isFromRemoteKey: true ]
content.userInfo = userInfo content.userInfo = userInfo
contentHandler!(content) contentHandler!(content)
} }

View File

@ -71,7 +71,6 @@ final class NotificationServiceExtensionContext : NSObject, AppContext {
func ensureSleepBlocking(_ shouldBeBlocking: Bool, blockingObjectsDescription: String) { } func ensureSleepBlocking(_ shouldBeBlocking: Bool, blockingObjectsDescription: String) { }
func frontmostViewController() -> UIViewController? { nil } func frontmostViewController() -> UIViewController? { nil }
func runNowOr(whenMainAppIsActive block: @escaping AppActiveBlock) { } func runNowOr(whenMainAppIsActive block: @escaping AppActiveBlock) { }
func setMainAppBadgeNumber(_ value: Int) { }
func setNetworkActivityIndicatorVisible(_ value: Bool) { } func setNetworkActivityIndicatorVisible(_ value: Bool) { }
func setStatusBarHidden(_ isHidden: Bool, animated isAnimated: Bool) { } func setStatusBarHidden(_ isHidden: Bool, animated isAnimated: Bool) { }
} }

View File

@ -186,10 +186,6 @@ final class ShareAppExtensionContext: NSObject, AppContext {
OWSLogger.debug("Ignoring request to block sleep.") OWSLogger.debug("Ignoring request to block sleep.")
} }
func setMainAppBadgeNumber(_ value: Int) {
owsFailDebug("")
}
func setNetworkActivityIndicatorVisible(_ value: Bool) { func setNetworkActivityIndicatorVisible(_ value: Bool) {
owsFailDebug("") owsFailDebug("")
} }

View File

@ -157,7 +157,7 @@ open class Storage {
guard isValid, let dbWriter: DatabaseWriter = dbWriter else { guard isValid, let dbWriter: DatabaseWriter = dbWriter else {
let error: Error = (startupError ?? StorageError.startupFailed) let error: Error = (startupError ?? StorageError.startupFailed)
SNLog("[Database Error] Statup failed with error: \(error)") SNLog("[Database Error] Statup failed with error: \(error)")
onComplete(.failure(StorageError.startupFailed), false) onComplete(.failure(error), false)
return return
} }
@ -430,12 +430,16 @@ open class Storage {
try? deleteDbKeys() try? deleteDbKeys()
} }
public static func reconfigureDatabase() {
Storage.shared.configureDatabase()
}
public static func resetForCleanMigration() { public static func resetForCleanMigration() {
// Clear existing content // Clear existing content
resetAllStorage() resetAllStorage()
// Reconfigure // Reconfigure
Storage.shared.configureDatabase() reconfigureDatabase()
} }
private static func deleteDatabaseFiles() { private static func deleteDatabaseFiles() {

View File

@ -73,9 +73,6 @@ NSString *NSStringForUIApplicationState(UIApplicationState value);
// Should be a NOOP if isMainApp is NO. // Should be a NOOP if isMainApp is NO.
- (void)ensureSleepBlocking:(BOOL)shouldBeBlocking blockingObjects:(NSArray<id> *)blockingObjects; - (void)ensureSleepBlocking:(BOOL)shouldBeBlocking blockingObjects:(NSArray<id> *)blockingObjects;
// Should only be called if isMainApp is YES.
- (void)setMainAppBadgeNumber:(NSInteger)value;
- (void)setStatusBarHidden:(BOOL)isHidden animated:(BOOL)isAnimated; - (void)setStatusBarHidden:(BOOL)isHidden animated:(BOOL)isAnimated;
@property (nonatomic, readonly) CGFloat statusBarHeight; @property (nonatomic, readonly) CGFloat statusBarHeight;

View File

@ -42,6 +42,7 @@ public enum SNUserDefaults {
case lastOpen case lastOpen
case lastGarbageCollection case lastGarbageCollection
case lastPushNotificationSync case lastPushNotificationSync
case lastCallPreOffer
} }
public enum Double: Swift.String { public enum Double: Swift.String {

View File

@ -2,13 +2,6 @@
import Foundation import Foundation
public protocol BencodableType {
associatedtype ValueType: BencodableType
static var isCollection: Bool { get }
static var isDictionary: Bool { get }
}
public struct BencodeResponse<T: Codable> { public struct BencodeResponse<T: Codable> {
public let info: T public let info: T
public let data: Data? public let data: Data?
@ -58,23 +51,61 @@ public enum Bencode {
using dependencies: Dependencies = Dependencies() using dependencies: Dependencies = Dependencies()
) throws -> BencodeResponse<T> where T: Decodable { ) throws -> BencodeResponse<T> where T: Decodable {
guard guard
let result: [Data] = try? decode([Data].self, from: data), let decodedData: (value: Any, remainingData: Data) = decodeData(data),
let responseData: Data = result.first decodedData.remainingData.isEmpty == true, // Ensure there is no left over data
let resultArray: [Any] = decodedData.value as? [Any],
resultArray.count > 0
else { throw HTTPError.parsingFailed } else { throw HTTPError.parsingFailed }
return BencodeResponse( return BencodeResponse(
info: try responseData.decoded(as: T.self, using: dependencies), info: try Bencode.decode(T.self, decodedValue: resultArray[0], using: dependencies),
data: (result.count > 1 ? result.last : nil) data: {
guard resultArray.count > 1 else { return nil }
switch resultArray.last {
case let bencodeString as BencodeString: return bencodeString.rawValue
default: return resultArray.last as? Data
}
}()
) )
} }
public static func decode<T: BencodableType>(_ type: T.Type, from data: Data) throws -> T { public static func decode<T: Decodable>(
_ type: T.Type,
from data: Data,
using dependencies: Dependencies = Dependencies()
) throws -> T {
guard guard
let decodedData: (value: Any, remainingData: Data) = decodeData(data), let decodedData: (value: Any, remainingData: Data) = decodeData(data),
decodedData.remainingData.isEmpty == true // Ensure there is no left over data decodedData.remainingData.isEmpty == true // Ensure there is no left over data
else { throw HTTPError.parsingFailed } else { throw HTTPError.parsingFailed }
return try recursiveCast(type, from: decodedData.value) return try Bencode.decode(T.self, decodedValue: decodedData.value, using: dependencies)
}
private static func decode<T: Decodable>(
_ type: T.Type,
decodedValue: Any,
using dependencies: Dependencies = Dependencies()
) throws -> T {
switch (decodedValue, T.self) {
case (let directResult as T, _): return directResult
case
(let bencodeString as BencodeString, is String.Type),
(let bencodeString as BencodeString, is Optional<String>.Type):
return try (bencodeString.value as? T ?? { throw HTTPError.parsingFailed }())
case (let bencodeString as BencodeString, _):
return try bencodeString.rawValue.decoded(as: T.self, using: dependencies)
default:
guard
let jsonifiedInfo: Any = try? jsonify(decodedValue),
let infoData: Data = try? JSONSerialization.data(withJSONObject: jsonifiedInfo)
else { throw HTTPError.parsingFailed }
return try infoData.decoded(as: T.self, using: dependencies)
}
} }
// MARK: - Logic // MARK: - Logic
@ -190,74 +221,12 @@ public enum Bencode {
// MARK: - Internal Functions // MARK: - Internal Functions
private static func recursiveCast<T: BencodableType>(_ type: T.Type, from value: Any) throws -> T { private static func jsonify(_ value: Any) throws -> Any {
switch (type.isCollection, type.isDictionary) { switch value {
case (_, true): case let arrayValue as [Any]: return try arrayValue.map { try jsonify($0) } as Any
guard let dictValue: [String: Any] = value as? [String: Any] else { throw HTTPError.parsingFailed } case let dictValue as [String: Any]: return try dictValue.mapValues { try jsonify($0) } as Any
case let bencodeString as BencodeString: return bencodeString.value as Any
return try ( default: return value
dictValue.mapValues { try recursiveCast(type.ValueType.self, from: $0) } as? T ??
{ throw HTTPError.parsingFailed }()
)
case (true, _):
guard let arrayValue: [Any] = value as? [Any] else { throw HTTPError.parsingFailed }
return try (
arrayValue.map { try recursiveCast(type.ValueType.self, from: $0) } as? T ??
{ throw HTTPError.parsingFailed }()
)
default:
switch (value, type) {
case (let bencodeString as BencodeString, is String.Type):
return try (bencodeString.value as? T ?? { throw HTTPError.parsingFailed }())
case (let bencodeString as BencodeString, is Optional<String>.Type):
return try (bencodeString.value as? T ?? { throw HTTPError.parsingFailed }())
case (let bencodeString as BencodeString, _):
return try (bencodeString.rawValue as? T ?? { throw HTTPError.parsingFailed }())
default: return try (value as? T ?? { throw HTTPError.parsingFailed }())
}
} }
} }
} }
// MARK: - BencodableType Extensions
extension Data: BencodableType {
public typealias ValueType = Data
public static var isCollection: Bool { false }
public static var isDictionary: Bool { false }
}
extension Int: BencodableType {
public typealias ValueType = Int
public static var isCollection: Bool { false }
public static var isDictionary: Bool { false }
}
extension String: BencodableType {
public typealias ValueType = String
public static var isCollection: Bool { false }
public static var isDictionary: Bool { false }
}
extension Array: BencodableType where Element: BencodableType {
public typealias ValueType = Element
public static var isCollection: Bool { true }
public static var isDictionary: Bool { false }
}
extension Dictionary: BencodableType where Key == String, Value: BencodableType {
public typealias ValueType = Value
public static var isCollection: Bool { false }
public static var isDictionary: Bool { true }
}

View File

@ -13,11 +13,37 @@ class BencodeSpec: QuickSpec {
let stringValue: String let stringValue: String
} }
struct TestType2: Codable, Equatable {
let stringValue: String
let boolValue: Bool
}
struct TestType3: Codable, Equatable {
let stringValue: String
let boolValue: Bool
init(_ stringValue: String, _ boolValue: Bool) {
self.stringValue = stringValue
self.boolValue = boolValue
}
init(from decoder: Decoder) throws {
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
self = TestType3(
try container.decode(String.self, forKey: .stringValue),
((try? container.decode(Bool.self, forKey: .boolValue)) ?? false)
)
}
}
// MARK: - Spec // MARK: - Spec
override func spec() { override func spec() {
describe("Bencode") { describe("Bencode") {
// MARK: - when decoding
context("when decoding") { context("when decoding") {
// MARK: -- should decode a basic string
it("should decode a basic string") { it("should decode a basic string") {
let basicStringData: Data = "5:howdy".data(using: .utf8)! let basicStringData: Data = "5:howdy".data(using: .utf8)!
let result = try? Bencode.decode(String.self, from: basicStringData) let result = try? Bencode.decode(String.self, from: basicStringData)
@ -25,6 +51,7 @@ class BencodeSpec: QuickSpec {
expect(result).to(equal("howdy")) expect(result).to(equal("howdy"))
} }
// MARK: -- should decode a basic integer
it("should decode a basic integer") { it("should decode a basic integer") {
let basicIntegerData: Data = "i3e".data(using: .utf8)! let basicIntegerData: Data = "i3e".data(using: .utf8)!
let result = try? Bencode.decode(Int.self, from: basicIntegerData) let result = try? Bencode.decode(Int.self, from: basicIntegerData)
@ -32,6 +59,7 @@ class BencodeSpec: QuickSpec {
expect(result).to(equal(3)) expect(result).to(equal(3))
} }
// MARK: -- should decode a list of integers
it("should decode a list of integers") { it("should decode a list of integers") {
let basicIntListData: Data = "li1ei2ee".data(using: .utf8)! let basicIntListData: Data = "li1ei2ee".data(using: .utf8)!
let result = try? Bencode.decode([Int].self, from: basicIntListData) let result = try? Bencode.decode([Int].self, from: basicIntListData)
@ -39,57 +67,246 @@ class BencodeSpec: QuickSpec {
expect(result).to(equal([1, 2])) expect(result).to(equal([1, 2]))
} }
// MARK: -- should decode a basic dict
it("should decode a basic dict") { it("should decode a basic dict") {
let basicDictData: Data = "d4:spaml1:a1:bee".data(using: .utf8)! let basicDictData: Data = "d4:spaml1:a1:bee".data(using: .utf8)!
let result = try? Bencode.decode([String: [String]].self, from: basicDictData) let result = try? Bencode.decode([String: [String]].self, from: basicDictData)
expect(result).to(equal(["spam": ["a", "b"]])) expect(result).to(equal(["spam": ["a", "b"]]))
} }
// MARK: -- decodes a decodable type
it("decodes a decodable type") {
let data: Data = "d8:intValuei100e11:stringValue4:Test".data(using: .utf8)!
let result: TestType? = try? Bencode.decode(TestType.self, from: data)
expect(result).to(equal(TestType(intValue: 100, stringValue: "Test")))
}
// MARK: -- decodes a stringified decodable type
it("decodes a stringified decodable type") {
let data: Data = "37:{\"intValue\":100,\"stringValue\":\"Test\"}".data(using: .utf8)!
let result: TestType? = try? Bencode.decode(TestType.self, from: data)
expect(result).to(equal(TestType(intValue: 100, stringValue: "Test")))
}
} }
// MARK: - when decoding a response
context("when decoding a response") { context("when decoding a response") {
it("decodes successfully") { // MARK: -- with a decodable type
let data: Data = "l37:{\"intValue\":100,\"stringValue\":\"Test\"}5:\u{01}\u{02}\u{03}\u{04}\u{05}e" context("with a decodable type") {
.data(using: .utf8)! // MARK: ---- decodes successfully
let result: BencodeResponse<TestType>? = try? Bencode.decodeResponse(from: data) it("decodes successfully") {
let data: Data = "ld8:intValuei100e11:stringValue4:Teste5:\u{01}\u{02}\u{03}\u{04}\u{05}e"
.data(using: .utf8)!
let result: BencodeResponse<TestType>? = try? Bencode.decodeResponse(from: data)
expect(result)
.to(equal(
BencodeResponse(
info: TestType(
intValue: 100,
stringValue: "Test"
),
data: Data([1, 2, 3, 4, 5])
)
))
}
expect(result) // MARK: -- decodes successfully with no body
.to(equal( it("decodes successfully with no body") {
BencodeResponse( let data: Data = "ld8:intValuei100e11:stringValue4:Teste"
info: TestType( .data(using: .utf8)!
intValue: 100, let result: BencodeResponse<TestType>? = try? Bencode.decodeResponse(from: data)
stringValue: "Test"
), expect(result)
data: Data([1, 2, 3, 4, 5]) .to(equal(
) BencodeResponse(
)) info: TestType(
intValue: 100,
stringValue: "Test"
),
data: nil
)
))
}
// MARK: ---- throws a parsing error when given an invalid length
it("throws a parsing error when given an invalid length") {
let data: Data = "ld12:intValuei100e11:stringValue4:Teste5:\u{01}\u{02}\u{03}\u{04}\u{05}e"
.data(using: .utf8)!
expect {
let result: BencodeResponse<TestType> = try Bencode.decodeResponse(from: data)
_ = result
}.to(throwError(HTTPError.parsingFailed))
}
// MARK: ---- throws a parsing error when given an invalid key
it("throws a parsing error when given an invalid key") {
let data: Data = "ld7:INVALIDi100e11:stringValue4:Teste5:\u{01}\u{02}\u{03}\u{04}\u{05}e"
.data(using: .utf8)!
expect {
let result: BencodeResponse<TestType> = try Bencode.decodeResponse(from: data)
_ = result
}.to(throwError(HTTPError.parsingFailed))
}
// MARK: ---- decodes correctly when trying to decode an int to a bool with custom handling
it("decodes correctly when trying to decode an int to a bool with custom handling") {
let data: Data = "ld9:boolValuei1e11:stringValue4:testee"
.data(using: .utf8)!
expect {
let result: BencodeResponse<TestType3> = try Bencode.decodeResponse(from: data)
_ = result
}.toNot(throwError(HTTPError.parsingFailed))
}
// MARK: ---- throws a parsing error when trying to decode an int to a bool
it("throws a parsing error when trying to decode an int to a bool") {
let data: Data = "ld9:boolValuei1e11:stringValue4:testee"
.data(using: .utf8)!
expect {
let result: BencodeResponse<TestType2> = try Bencode.decodeResponse(from: data)
_ = result
}.to(throwError(HTTPError.parsingFailed))
}
} }
it("decodes successfully with no body") { // MARK: -- with stringified json info
let data: Data = "l37:{\"intValue\":100,\"stringValue\":\"Test\"}e" context("with stringified json info") {
.data(using: .utf8)! // MARK: -- decodes successfully
let result: BencodeResponse<TestType>? = try? Bencode.decodeResponse(from: data) it("decodes successfully") {
let data: Data = "l37:{\"intValue\":100,\"stringValue\":\"Test\"}5:\u{01}\u{02}\u{03}\u{04}\u{05}e"
.data(using: .utf8)!
let result: BencodeResponse<TestType>? = try? Bencode.decodeResponse(from: data)
expect(result)
.to(equal(
BencodeResponse(
info: TestType(
intValue: 100,
stringValue: "Test"
),
data: Data([1, 2, 3, 4, 5])
)
))
}
expect(result) // MARK: -- decodes successfully with no body
.to(equal( it("decodes successfully with no body") {
BencodeResponse( let data: Data = "l37:{\"intValue\":100,\"stringValue\":\"Test\"}e"
info: TestType( .data(using: .utf8)!
intValue: 100, let result: BencodeResponse<TestType>? = try? Bencode.decodeResponse(from: data)
stringValue: "Test"
), expect(result)
data: nil .to(equal(
) BencodeResponse(
)) info: TestType(
intValue: 100,
stringValue: "Test"
),
data: nil
)
))
}
// MARK: -- throws a parsing error when invalid
it("throws a parsing error when invalid") {
let data: Data = "l36:{\"INVALID\":100,\"stringValue\":\"Test\"}5:\u{01}\u{02}\u{03}\u{04}\u{05}e"
.data(using: .utf8)!
expect {
let result: BencodeResponse<TestType> = try Bencode.decodeResponse(from: data)
_ = result
}.to(throwError(HTTPError.parsingFailed))
}
} }
it("throws a parsing error when invalid") { // MARK: -- with a string value
let data: Data = "l36:{\"INVALID\":100,\"stringValue\":\"Test\"}5:\u{01}\u{02}\u{03}\u{04}\u{05}e" context("with a string value") {
.data(using: .utf8)! // MARK: ---- decodes successfully
it("decodes successfully") {
expect { let data: Data = "l4:Test5:\u{01}\u{02}\u{03}\u{04}\u{05}e".data(using: .utf8)!
let result: BencodeResponse<TestType> = try Bencode.decodeResponse(from: data) let result: BencodeResponse<String>? = try? Bencode.decodeResponse(from: data)
_ = result
}.to(throwError(HTTPError.parsingFailed)) expect(result)
.to(equal(
BencodeResponse(
info: "Test",
data: Data([1, 2, 3, 4, 5])
)
))
}
// MARK: ---- decodes successfully with no body
it("decodes successfully with no body") {
let data: Data = "l4:Teste".data(using: .utf8)!
let result: BencodeResponse<String>? = try? Bencode.decodeResponse(from: data)
expect(result)
.to(equal(
BencodeResponse(
info: "Test",
data: nil
)
))
}
// MARK: ---- throws a parsing error when invalid
it("throws a parsing error when invalid") {
let data: Data = "l10:Teste".data(using: .utf8)!
expect {
let result: BencodeResponse<String> = try Bencode.decodeResponse(from: data)
_ = result
}.to(throwError(HTTPError.parsingFailed))
}
}
// MARK: -- with an int value
context("with an int value") {
// MARK: ---- decodes successfully
it("decodes successfully") {
let data: Data = "li100e5:\u{01}\u{02}\u{03}\u{04}\u{05}e".data(using: .utf8)!
let result: BencodeResponse<Int>? = try? Bencode.decodeResponse(from: data)
expect(result)
.to(equal(
BencodeResponse(
info: 100,
data: Data([1, 2, 3, 4, 5])
)
))
}
// MARK: ---- decodes successfully with no body
it("decodes successfully with no body") {
let data: Data = "li100ee".data(using: .utf8)!
let result: BencodeResponse<Int>? = try? Bencode.decodeResponse(from: data)
expect(result)
.to(equal(
BencodeResponse(
info: 100,
data: nil
)
))
}
// MARK: ---- throws a parsing error when invalid
it("throws a parsing error when invalid") {
let data: Data = "l4:Teste".data(using: .utf8)!
expect {
let result: BencodeResponse<Int> = try Bencode.decodeResponse(from: data)
_ = result
}.to(throwError(HTTPError.parsingFailed))
}
} }
} }
} }

View File

@ -11,11 +11,34 @@ public enum AppSetup {
private static let hasRun: Atomic<Bool> = Atomic(false) private static let hasRun: Atomic<Bool> = Atomic(false)
public static func setupEnvironment( public static func setupEnvironment(
retrySetupIfDatabaseInvalid: Bool = false,
appSpecificBlock: @escaping () -> (), appSpecificBlock: @escaping () -> (),
migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil, migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil,
migrationsCompletion: @escaping (Result<Void, Error>, Bool) -> () migrationsCompletion: @escaping (Result<Void, Error>, Bool) -> ()
) { ) {
guard !AppSetup.hasRun.wrappedValue else { return } // If we've already run the app setup then only continue under certain circumstances
guard !AppSetup.hasRun.wrappedValue else {
let storageIsValid: Bool = Storage.shared.isValid
switch (retrySetupIfDatabaseInvalid, storageIsValid) {
case (true, false):
Storage.reconfigureDatabase()
AppSetup.hasRun.mutate { $0 = false }
AppSetup.setupEnvironment(
retrySetupIfDatabaseInvalid: false, // Don't want to get stuck in a loop
appSpecificBlock: appSpecificBlock,
migrationProgressChanged: migrationProgressChanged,
migrationsCompletion: migrationsCompletion
)
default:
migrationsCompletion(
(storageIsValid ? .success(()) : .failure(StorageError.startupFailed)),
false
)
}
return
}
AppSetup.hasRun.mutate { $0 = true } AppSetup.hasRun.mutate { $0 = true }
@ -64,14 +87,6 @@ public enum AppSetup {
migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil, migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil,
migrationsCompletion: @escaping (Result<Void, Error>, Bool) -> () migrationsCompletion: @escaping (Result<Void, Error>, Bool) -> ()
) { ) {
// If the database can't be initialised into a valid state then error
guard Storage.shared.isValid else {
DispatchQueue.main.async {
migrationsCompletion(Result.failure(StorageError.databaseInvalid), false)
}
return
}
var backgroundTask: OWSBackgroundTask? = (backgroundTask ?? OWSBackgroundTask(labelStr: #function)) var backgroundTask: OWSBackgroundTask? = (backgroundTask ?? OWSBackgroundTask(labelStr: #function))
Storage.shared.perform( Storage.shared.perform(