mirror of https://github.com/oxen-io/session-ios
Fixed a few closed group and job issues
Fixed a few job migration issues Fixed an issue with the closed group key pair management (wasn't storing keys correctly) Refactored the OWSSound (now Preferences.Sound) Added the logic for the AttachmentDownloadJob and enabled jobs to be cascade deleted via interactions Optimised the HomeViewModel database observation query (fetch specific columns so changes outside those don't trigger updates) Updated to the latest GRDB (ran into a deadlock which should be fixed in a newer version)pull/612/head
parent
11231599db
commit
ed9f4ea6c6
@ -1,166 +0,0 @@
|
||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import SessionUtilitiesKit
|
||||
import SessionSnodeKit
|
||||
import SignalCoreKit
|
||||
|
||||
public final class AttachmentDownloadJob : NSObject, Job, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
|
||||
public let attachmentID: String
|
||||
public let tsMessageID: String
|
||||
public let threadID: String
|
||||
public var delegate: JobDelegate?
|
||||
public var id: String?
|
||||
public var failureCount: UInt = 0
|
||||
public var isDeferred = false
|
||||
|
||||
public enum Error : LocalizedError {
|
||||
case noAttachment
|
||||
case invalidURL
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .noAttachment: return "No such attachment."
|
||||
case .invalidURL: return "Invalid file URL."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Settings
|
||||
public class var collection: String { return "AttachmentDownloadJobCollection" }
|
||||
public static let maxFailureCount: UInt = 20
|
||||
|
||||
// MARK: Initialization
|
||||
public init(attachmentID: String, tsMessageID: String, threadID: String) {
|
||||
self.attachmentID = attachmentID
|
||||
self.tsMessageID = tsMessageID
|
||||
self.threadID = threadID
|
||||
}
|
||||
|
||||
// MARK: Coding
|
||||
public init?(coder: NSCoder) {
|
||||
guard let attachmentID = coder.decodeObject(forKey: "attachmentID") as! String?,
|
||||
let tsMessageID = coder.decodeObject(forKey: "tsIncomingMessageID") as! String?,
|
||||
let threadID = coder.decodeObject(forKey: "threadID") as! String?,
|
||||
let id = coder.decodeObject(forKey: "id") as! String? else { return nil }
|
||||
self.attachmentID = attachmentID
|
||||
self.tsMessageID = tsMessageID
|
||||
self.threadID = threadID
|
||||
self.id = id
|
||||
self.failureCount = coder.decodeObject(forKey: "failureCount") as! UInt? ?? 0
|
||||
self.isDeferred = coder.decodeBool(forKey: "isDeferred")
|
||||
}
|
||||
|
||||
public func encode(with coder: NSCoder) {
|
||||
coder.encode(attachmentID, forKey: "attachmentID")
|
||||
coder.encode(tsMessageID, forKey: "tsIncomingMessageID")
|
||||
coder.encode(threadID, forKey: "threadID")
|
||||
coder.encode(id, forKey: "id")
|
||||
coder.encode(failureCount, forKey: "failureCount")
|
||||
coder.encode(isDeferred, forKey: "isDeferred")
|
||||
}
|
||||
|
||||
// MARK: Running
|
||||
public func execute() {
|
||||
if let id = id {
|
||||
JobQueue.currentlyExecutingJobs.insert(id)
|
||||
}
|
||||
guard !isDeferred else { return }
|
||||
if TSAttachment.fetch(uniqueId: attachmentID) is TSAttachmentStream {
|
||||
// FIXME: It's not clear * how * this happens, but apparently we can get to this point
|
||||
// from time to time with an already downloaded attachment.
|
||||
return handleSuccess()
|
||||
}
|
||||
guard let pointer = TSAttachment.fetch(uniqueId: attachmentID) as? TSAttachmentPointer else {
|
||||
return handleFailure(error: Error.noAttachment)
|
||||
}
|
||||
let storage = SNMessagingKitConfiguration.shared.storage
|
||||
storage.write(with: { transaction in
|
||||
storage.setAttachmentState(to: .downloading, for: pointer, associatedWith: self.tsMessageID, using: transaction)
|
||||
}, completion: { })
|
||||
let temporaryFilePath = URL(fileURLWithPath: OWSTemporaryDirectoryAccessibleAfterFirstAuth() + UUID().uuidString)
|
||||
let handleFailure: (Swift.Error) -> Void = { error in // Intentionally capture self
|
||||
OWSFileSystem.deleteFile(temporaryFilePath.absoluteString)
|
||||
if let error = error as? Error, case .noAttachment = error {
|
||||
storage.write(with: { transaction in
|
||||
storage.setAttachmentState(to: .failed, for: pointer, associatedWith: self.tsMessageID, using: transaction)
|
||||
}, completion: { })
|
||||
self.handlePermanentFailure(error: error)
|
||||
} else if let error = error as? OnionRequestAPI.Error, case .httpRequestFailedAtDestination(let statusCode, _, _) = error,
|
||||
statusCode == 400 {
|
||||
// Otherwise, the attachment will show a state of downloading forever,
|
||||
// and the message won't be able to be marked as read.
|
||||
storage.write(with: { transaction in
|
||||
storage.setAttachmentState(to: .failed, for: pointer, associatedWith: self.tsMessageID, using: transaction)
|
||||
}, completion: { })
|
||||
// This usually indicates a file that has expired on the server, so there's no need to retry.
|
||||
self.handlePermanentFailure(error: error)
|
||||
} else {
|
||||
self.handleFailure(error: error)
|
||||
}
|
||||
}
|
||||
if let tsMessage = TSMessage.fetch(uniqueId: tsMessageID), let v2OpenGroup = storage.getV2OpenGroup(for: tsMessage.uniqueThreadId) {
|
||||
guard let fileAsString = pointer.downloadURL.split(separator: "/").last, let file = UInt64(fileAsString) else {
|
||||
return handleFailure(Error.invalidURL)
|
||||
}
|
||||
OpenGroupAPIV2.download(file, from: v2OpenGroup.room, on: v2OpenGroup.server).done(on: DispatchQueue.global(qos: .userInitiated)) { data in
|
||||
self.handleDownloadedAttachment(data: data, temporaryFilePath: temporaryFilePath, pointer: pointer, failureHandler: handleFailure)
|
||||
}.catch(on: DispatchQueue.global()) { error in
|
||||
handleFailure(error)
|
||||
}
|
||||
} else {
|
||||
guard let fileAsString = pointer.downloadURL.split(separator: "/").last, let file = UInt64(fileAsString) else {
|
||||
return handleFailure(Error.invalidURL)
|
||||
}
|
||||
let useOldServer = pointer.downloadURL.contains(FileServerAPIV2.oldServer)
|
||||
FileServerAPIV2.download(file, useOldServer: useOldServer).done(on: DispatchQueue.global(qos: .userInitiated)) { data in
|
||||
self.handleDownloadedAttachment(data: data, temporaryFilePath: temporaryFilePath, pointer: pointer, failureHandler: handleFailure)
|
||||
}.catch(on: DispatchQueue.global()) { error in
|
||||
handleFailure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func handleDownloadedAttachment(data: Data, temporaryFilePath: URL, pointer: TSAttachmentPointer, failureHandler: (Swift.Error) -> Void) {
|
||||
let storage = SNMessagingKitConfiguration.shared.storage
|
||||
do {
|
||||
try data.write(to: temporaryFilePath, options: .atomic)
|
||||
} catch {
|
||||
return failureHandler(error)
|
||||
}
|
||||
let plaintext: Data
|
||||
if let key = pointer.encryptionKey, let digest = pointer.digest, key.count > 0 && digest.count > 0 {
|
||||
do {
|
||||
plaintext = try Cryptography.decryptAttachment(data, withKey: key, digest: digest, unpaddedSize: pointer.byteCount)
|
||||
} catch {
|
||||
return failureHandler(error)
|
||||
}
|
||||
} else {
|
||||
plaintext = data // Open group attachments are unencrypted
|
||||
}
|
||||
let stream = TSAttachmentStream(pointer: pointer)
|
||||
do {
|
||||
try stream.write(plaintext)
|
||||
} catch {
|
||||
return failureHandler(error)
|
||||
}
|
||||
OWSFileSystem.deleteFile(temporaryFilePath.absoluteString)
|
||||
storage.write(with: { transaction in
|
||||
storage.persist(stream, associatedWith: self.tsMessageID, using: transaction)
|
||||
}, completion: {
|
||||
self.handleSuccess()
|
||||
})
|
||||
}
|
||||
|
||||
private func handleSuccess() {
|
||||
delegate?.handleJobSucceeded(self)
|
||||
}
|
||||
|
||||
private func handlePermanentFailure(error: Swift.Error) {
|
||||
delegate?.handleJobFailedPermanently(self, with: error)
|
||||
}
|
||||
|
||||
private func handleFailure(error: Swift.Error) {
|
||||
delegate?.handleJobFailed(self, with: error)
|
||||
}
|
||||
}
|
@ -0,0 +1,309 @@
|
||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
import SessionUtilitiesKit
|
||||
import SessionSnodeKit
|
||||
import SignalCoreKit
|
||||
|
||||
public enum AttachmentDownloadJob: JobExecutor {
|
||||
public static var maxFailureCount: UInt = 10
|
||||
public static var requiresThreadId: Bool = true
|
||||
public static let requiresInteractionId: Bool = true
|
||||
|
||||
public static func run(
|
||||
_ job: Job,
|
||||
success: @escaping (Job, Bool) -> (),
|
||||
failure: @escaping (Job, Error?, Bool) -> (),
|
||||
deferred: @escaping (Job) -> ()
|
||||
) {
|
||||
guard
|
||||
let threadId: String = job.threadId,
|
||||
let detailsData: Data = job.details,
|
||||
let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData),
|
||||
var attachment: Attachment = GRDBStorage.shared
|
||||
.read({ db in try Attachment.fetchOne(db, id: details.attachmentId) })
|
||||
else {
|
||||
failure(job, JobRunnerError.missingRequiredDetails, false)
|
||||
return
|
||||
}
|
||||
guard attachment.state != .downloaded else {
|
||||
// FIXME: It's not clear * how * this happens, but apparently we can get to this point from time to time with an already downloaded attachment.
|
||||
success(job, false)
|
||||
return
|
||||
}
|
||||
|
||||
// Update to the 'downloading' state
|
||||
attachment = GRDBStorage.shared
|
||||
.write { db in
|
||||
try attachment
|
||||
.with(state: .downloading)
|
||||
.saved(db)
|
||||
}
|
||||
.defaulting(to: attachment)
|
||||
|
||||
let temporaryFilePath: URL = URL(
|
||||
fileURLWithPath: OWSTemporaryDirectoryAccessibleAfterFirstAuth() + UUID().uuidString
|
||||
)
|
||||
let downloadPromise: Promise<Data> = {
|
||||
guard
|
||||
let downloadUrl: String = attachment.downloadUrl,
|
||||
let fileAsString: String = downloadUrl.split(separator: "/").last.map({ String($0) }),
|
||||
let file: UInt64 = UInt64(fileAsString)
|
||||
else {
|
||||
return Promise(error: AttachmentDownloadError.invalidUrl)
|
||||
}
|
||||
|
||||
if let openGroup: OpenGroup = GRDBStorage.shared.read({ db in try OpenGroup.fetchOne(db, id: threadId) }) {
|
||||
return OpenGroupAPIV2.download(file, from: openGroup.room, on: openGroup.server)
|
||||
}
|
||||
|
||||
return FileServerAPIV2.download(file, useOldServer: downloadUrl.contains(FileServerAPIV2.oldServer))
|
||||
}()
|
||||
|
||||
downloadPromise
|
||||
.then { data -> Promise<Void> in
|
||||
try data.write(to: temporaryFilePath, options: .atomic)
|
||||
|
||||
let plaintext: Data = try {
|
||||
guard
|
||||
let key: Data = attachment.encryptionKey,
|
||||
let digest: Data = attachment.digest,
|
||||
key.count > 0,
|
||||
digest.count > 0
|
||||
else { return data } // Open group attachments are unencrypted
|
||||
|
||||
return try Cryptography.decryptAttachment(
|
||||
data,
|
||||
withKey: key,
|
||||
digest: digest,
|
||||
unpaddedSize: UInt32(attachment.byteCount)
|
||||
)
|
||||
}()
|
||||
|
||||
guard try attachment.write(data: plaintext) else {
|
||||
throw AttachmentDownloadError.failedToSaveFile
|
||||
}
|
||||
|
||||
return Promise.value(())
|
||||
}
|
||||
.done {
|
||||
// Remove the temporary file
|
||||
OWSFileSystem.deleteFile(temporaryFilePath.absoluteString)
|
||||
|
||||
// Update the attachment state
|
||||
GRDBStorage.shared.write { db in
|
||||
try attachment
|
||||
.with(
|
||||
state: .downloaded,
|
||||
creationTimestamp: Date().timeIntervalSince1970,
|
||||
localRelativeFilePath: attachment.originalFilePath?
|
||||
.substring(from: Attachment.attachmentsFolder.count)
|
||||
)
|
||||
.save(db)
|
||||
}
|
||||
|
||||
success(job, false)
|
||||
}
|
||||
.catch { error in
|
||||
OWSFileSystem.deleteFile(temporaryFilePath.absoluteString)
|
||||
|
||||
switch error {
|
||||
case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, _, _) where statusCode == 400:
|
||||
// Otherwise, the attachment will show a state of downloading forever,
|
||||
// and the message won't be able to be marked as read
|
||||
GRDBStorage.shared.write { db in
|
||||
try attachment
|
||||
.with(state: .failed)
|
||||
.save(db)
|
||||
}
|
||||
|
||||
// This usually indicates a file that has expired on the server, so there's no need to retry
|
||||
failure(job, error, true)
|
||||
|
||||
default:
|
||||
failure(job, error, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - AttachmentDownloadJob.Details
|
||||
|
||||
extension AttachmentDownloadJob {
|
||||
public struct Details: Codable {
|
||||
public let attachmentId: String
|
||||
}
|
||||
|
||||
public enum AttachmentDownloadError: LocalizedError {
|
||||
case failedToSaveFile
|
||||
case invalidUrl
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .failedToSaveFile: return "Failed to save file"
|
||||
case .invalidUrl: return "Invalid file URL"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: MessageInvalidator.invalidate(tsMessage, with: transaction)
|
||||
|
||||
// public let attachmentID: String
|
||||
// public let tsMessageID: String
|
||||
// public let threadID: String
|
||||
// public var delegate: JobDelegate?
|
||||
// public var id: String?
|
||||
// public var failureCount: UInt = 0
|
||||
// public var isDeferred = false
|
||||
//
|
||||
// public enum Error : LocalizedError {
|
||||
// case noAttachment
|
||||
// case invalidURL
|
||||
//
|
||||
// public var errorDescription: String? {
|
||||
// switch self {
|
||||
// case .noAttachment: return "No such attachment."
|
||||
// case .invalidURL: return "Invalid file URL."
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // MARK: Settings
|
||||
// public class var collection: String { return "AttachmentDownloadJobCollection" }
|
||||
// public static let maxFailureCount: UInt = 20
|
||||
//
|
||||
// // MARK: Initialization
|
||||
// public init(attachmentID: String, tsMessageID: String, threadID: String) {
|
||||
// self.attachmentID = attachmentID
|
||||
// self.tsMessageID = tsMessageID
|
||||
// self.threadID = threadID
|
||||
// }
|
||||
//
|
||||
// // MARK: Coding
|
||||
// public init?(coder: NSCoder) {
|
||||
// guard let attachmentID = coder.decodeObject(forKey: "attachmentID") as! String?,
|
||||
// let tsMessageID = coder.decodeObject(forKey: "tsIncomingMessageID") as! String?,
|
||||
// let threadID = coder.decodeObject(forKey: "threadID") as! String?,
|
||||
// let id = coder.decodeObject(forKey: "id") as! String? else { return nil }
|
||||
// self.attachmentID = attachmentID
|
||||
// self.tsMessageID = tsMessageID
|
||||
// self.threadID = threadID
|
||||
// self.id = id
|
||||
// self.failureCount = coder.decodeObject(forKey: "failureCount") as! UInt? ?? 0
|
||||
// self.isDeferred = coder.decodeBool(forKey: "isDeferred")
|
||||
// }
|
||||
//
|
||||
// public func encode(with coder: NSCoder) {
|
||||
// coder.encode(attachmentID, forKey: "attachmentID")
|
||||
// coder.encode(tsMessageID, forKey: "tsIncomingMessageID")
|
||||
// coder.encode(threadID, forKey: "threadID")
|
||||
// coder.encode(id, forKey: "id")
|
||||
// coder.encode(failureCount, forKey: "failureCount")
|
||||
// coder.encode(isDeferred, forKey: "isDeferred")
|
||||
// }
|
||||
//
|
||||
// // MARK: Running
|
||||
// public func execute() {
|
||||
// if let id = id {
|
||||
// JobQueue.currentlyExecutingJobs.insert(id)
|
||||
// }
|
||||
// guard !isDeferred else { return }
|
||||
// if TSAttachment.fetch(uniqueId: attachmentID) is TSAttachmentStream {
|
||||
// // FIXME: It's not clear * how * this happens, but apparently we can get to this point
|
||||
// // from time to time with an already downloaded attachment.
|
||||
// return handleSuccess()
|
||||
// }
|
||||
// guard let pointer = TSAttachment.fetch(uniqueId: attachmentID) as? TSAttachmentPointer else {
|
||||
// return handleFailure(error: Error.noAttachment)
|
||||
// }
|
||||
// let storage = SNMessagingKitConfiguration.shared.storage
|
||||
// storage.write(with: { transaction in
|
||||
// storage.setAttachmentState(to: .downloading, for: pointer, associatedWith: self.tsMessageID, using: transaction)
|
||||
// }, completion: { })
|
||||
// let temporaryFilePath = URL(fileURLWithPath: OWSTemporaryDirectoryAccessibleAfterFirstAuth() + UUID().uuidString)
|
||||
// let handleFailure: (Swift.Error) -> Void = { error in // Intentionally capture self
|
||||
// OWSFileSystem.deleteFile(temporaryFilePath.absoluteString)
|
||||
// if let error = error as? Error, case .noAttachment = error {
|
||||
// storage.write(with: { transaction in
|
||||
// storage.setAttachmentState(to: .failed, for: pointer, associatedWith: self.tsMessageID, using: transaction)
|
||||
// }, completion: { })
|
||||
// self.handlePermanentFailure(error: error)
|
||||
// } else if let error = error as? OnionRequestAPI.Error, case .httpRequestFailedAtDestination(let statusCode, _, _) = error,
|
||||
// statusCode == 400 {
|
||||
// // Otherwise, the attachment will show a state of downloading forever,
|
||||
// // and the message won't be able to be marked as read.
|
||||
// storage.write(with: { transaction in
|
||||
// storage.setAttachmentState(to: .failed, for: pointer, associatedWith: self.tsMessageID, using: transaction)
|
||||
// }, completion: { })
|
||||
// // This usually indicates a file that has expired on the server, so there's no need to retry.
|
||||
// self.handlePermanentFailure(error: error)
|
||||
// } else {
|
||||
// self.handleFailure(error: error)
|
||||
// }
|
||||
// }
|
||||
// if let tsMessage = TSMessage.fetch(uniqueId: tsMessageID), let v2OpenGroup = storage.getV2OpenGroup(for: tsMessage.uniqueThreadId) {
|
||||
// guard let fileAsString = pointer.downloadURL.split(separator: "/").last, let file = UInt64(fileAsString) else {
|
||||
// return handleFailure(Error.invalidURL)
|
||||
// }
|
||||
// OpenGroupAPIV2.download(file, from: v2OpenGroup.room, on: v2OpenGroup.server).done(on: DispatchQueue.global(qos: .userInitiated)) { data in
|
||||
// self.handleDownloadedAttachment(data: data, temporaryFilePath: temporaryFilePath, pointer: pointer, failureHandler: handleFailure)
|
||||
// }.catch(on: DispatchQueue.global()) { error in
|
||||
// handleFailure(error)
|
||||
// }
|
||||
// } else {
|
||||
// guard let fileAsString = pointer.downloadURL.split(separator: "/").last, let file = UInt64(fileAsString) else {
|
||||
// return handleFailure(Error.invalidURL)
|
||||
// }
|
||||
// let useOldServer = pointer.downloadURL.contains(FileServerAPIV2.oldServer)
|
||||
// FileServerAPIV2.download(file, useOldServer: useOldServer).done(on: DispatchQueue.global(qos: .userInitiated)) { data in
|
||||
// self.handleDownloadedAttachment(data: data, temporaryFilePath: temporaryFilePath, pointer: pointer, failureHandler: handleFailure)
|
||||
// }.catch(on: DispatchQueue.global()) { error in
|
||||
// handleFailure(error)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// private func handleDownloadedAttachment(data: Data, temporaryFilePath: URL, pointer: TSAttachmentPointer, failureHandler: (Swift.Error) -> Void) {
|
||||
// let storage = SNMessagingKitConfiguration.shared.storage
|
||||
// do {
|
||||
// try data.write(to: temporaryFilePath, options: .atomic)
|
||||
// } catch {
|
||||
// return failureHandler(error)
|
||||
// }
|
||||
// let plaintext: Data
|
||||
// if let key = pointer.encryptionKey, let digest = pointer.digest, key.count > 0 && digest.count > 0 {
|
||||
// do {
|
||||
// plaintext = try Cryptography.decryptAttachment(data, withKey: key, digest: digest, unpaddedSize: pointer.byteCount)
|
||||
// } catch {
|
||||
// return failureHandler(error)
|
||||
// }
|
||||
// } else {
|
||||
// plaintext = data // Open group attachments are unencrypted
|
||||
// }
|
||||
// let stream = TSAttachmentStream(pointer: pointer)
|
||||
// do {
|
||||
// try stream.write(plaintext)
|
||||
// } catch {
|
||||
// return failureHandler(error)
|
||||
// }
|
||||
// OWSFileSystem.deleteFile(temporaryFilePath.absoluteString)
|
||||
// storage.write(with: { transaction in
|
||||
// storage.persist(stream, associatedWith: self.tsMessageID, using: transaction)
|
||||
// }, completion: {
|
||||
// self.handleSuccess()
|
||||
// })
|
||||
// }
|
||||
//
|
||||
// private func handleSuccess() {
|
||||
// delegate?.handleJobSucceeded(self)
|
||||
// }
|
||||
//
|
||||
// private func handlePermanentFailure(error: Swift.Error) {
|
||||
// delegate?.handleJobFailedPermanently(self, with: error)
|
||||
// }
|
||||
//
|
||||
// private func handleFailure(error: Swift.Error) {
|
||||
// delegate?.handleJobFailed(self, with: error)
|
||||
// }
|
||||
//}
|
@ -1,79 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <AudioToolbox/AudioServices.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "OWSAudioPlayer.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef NS_ENUM(NSUInteger, OWSSound) {
|
||||
OWSSound_Default = 0,
|
||||
|
||||
// Notification Sounds
|
||||
OWSSound_Aurora,
|
||||
OWSSound_Bamboo,
|
||||
OWSSound_Chord,
|
||||
OWSSound_Circles,
|
||||
OWSSound_Complete,
|
||||
OWSSound_Hello,
|
||||
OWSSound_Input,
|
||||
OWSSound_Keys,
|
||||
OWSSound_Note,
|
||||
OWSSound_Popcorn,
|
||||
OWSSound_Pulse,
|
||||
OWSSound_Synth,
|
||||
OWSSound_SignalClassic,
|
||||
|
||||
// Ringtone Sounds
|
||||
OWSSound_Opening,
|
||||
|
||||
// Calls
|
||||
OWSSound_CallConnecting,
|
||||
OWSSound_CallOutboundRinging,
|
||||
OWSSound_CallBusy,
|
||||
OWSSound_CallFailure,
|
||||
|
||||
// Other
|
||||
OWSSound_MessageSent,
|
||||
OWSSound_None,
|
||||
OWSSound_DefaultiOSIncomingRingtone = OWSSound_Opening,
|
||||
};
|
||||
|
||||
@class OWSAudioPlayer;
|
||||
@class OWSPrimaryStorage;
|
||||
@class TSThread;
|
||||
@class YapDatabaseReadWriteTransaction;
|
||||
|
||||
@interface OWSSounds : NSObject
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
+ (NSString *)displayNameForSound:(OWSSound)sound;
|
||||
|
||||
+ (nullable NSString *)filenameForSound:(OWSSound)sound;
|
||||
+ (nullable NSString *)filenameForSound:(OWSSound)sound quiet:(BOOL)quiet;
|
||||
|
||||
#pragma mark - Notifications
|
||||
|
||||
+ (NSArray<NSNumber *> *)allNotificationSounds;
|
||||
|
||||
+ (OWSSound)globalNotificationSound;
|
||||
+ (void)setGlobalNotificationSound:(OWSSound)sound;
|
||||
+ (void)setGlobalNotificationSound:(OWSSound)sound transaction:(YapDatabaseReadWriteTransaction *)transaction;
|
||||
|
||||
+ (OWSSound)notificationSoundForThread:(TSThread *)thread;
|
||||
+ (SystemSoundID)systemSoundIDForSound:(OWSSound)sound quiet:(BOOL)quiet;
|
||||
+ (void)setNotificationSound:(OWSSound)sound forThread:(TSThread *)thread;
|
||||
|
||||
#pragma mark - AudioPlayer
|
||||
|
||||
+ (nullable OWSAudioPlayer *)audioPlayerForSound:(OWSSound)sound
|
||||
audioBehavior:(OWSAudioBehavior)audioBehavior;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@ -1,365 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSSounds.h"
|
||||
#import "Environment.h"
|
||||
#import "OWSAudioPlayer.h"
|
||||
#import <SessionUtilitiesKit/SessionUtilitiesKit.h>
|
||||
#import <SessionMessagingKit/OWSPrimaryStorage.h>
|
||||
#import <SessionMessagingKit/TSThread.h>
|
||||
#import <SessionMessagingKit/YapDatabaseConnection+OWS.h>
|
||||
#import <YapDatabase/YapDatabase.h>
|
||||
|
||||
NSString *const kOWSSoundsStorageNotificationCollection = @"kOWSSoundsStorageNotificationCollection";
|
||||
NSString *const kOWSSoundsStorageGlobalNotificationKey = @"kOWSSoundsStorageGlobalNotificationKey";
|
||||
|
||||
@interface OWSSystemSound : NSObject
|
||||
|
||||
@property (nonatomic, readonly) SystemSoundID soundID;
|
||||
@property (nonatomic, readonly) NSURL *soundURL;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
- (instancetype)initWithURL:(NSURL *)url NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
@end
|
||||
|
||||
@implementation OWSSystemSound
|
||||
|
||||
- (instancetype)initWithURL:(NSURL *)url
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
_soundURL = url;
|
||||
|
||||
SystemSoundID newSoundID;
|
||||
_soundID = newSoundID;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface OWSSounds ()
|
||||
|
||||
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
|
||||
@property (nonatomic, readonly) AnyLRUCache *cachedSystemSounds;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSSounds
|
||||
|
||||
+ (instancetype)sharedManager
|
||||
{
|
||||
return Environment.shared.sounds;
|
||||
}
|
||||
|
||||
- (instancetype)initWithPrimaryStorage:(OWSPrimaryStorage *)primaryStorage
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
_dbConnection = primaryStorage.newDatabaseConnection;
|
||||
|
||||
// Don't store too many sounds in memory. Most users will only use 1 or 2 sounds anyway.
|
||||
_cachedSystemSounds = [[AnyLRUCache alloc] initWithMaxSize:4];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (NSArray<NSNumber *> *)allNotificationSounds
|
||||
{
|
||||
return @[
|
||||
// None and Note (default) should be first.
|
||||
@(OWSSound_None),
|
||||
@(OWSSound_Note),
|
||||
|
||||
@(OWSSound_Aurora),
|
||||
@(OWSSound_Bamboo),
|
||||
@(OWSSound_Chord),
|
||||
@(OWSSound_Circles),
|
||||
@(OWSSound_Complete),
|
||||
@(OWSSound_Hello),
|
||||
@(OWSSound_Input),
|
||||
@(OWSSound_Keys),
|
||||
@(OWSSound_Popcorn),
|
||||
@(OWSSound_Pulse),
|
||||
@(OWSSound_Synth),
|
||||
];
|
||||
}
|
||||
|
||||
+ (NSString *)displayNameForSound:(OWSSound)sound
|
||||
{
|
||||
// TODO: Should we localize these sound names?
|
||||
switch (sound) {
|
||||
case OWSSound_Default:
|
||||
return @"";
|
||||
|
||||
// Notification Sounds
|
||||
case OWSSound_Aurora:
|
||||
return @"Aurora";
|
||||
case OWSSound_Bamboo:
|
||||
return @"Bamboo";
|
||||
case OWSSound_Chord:
|
||||
return @"Chord";
|
||||
case OWSSound_Circles:
|
||||
return @"Circles";
|
||||
case OWSSound_Complete:
|
||||
return @"Complete";
|
||||
case OWSSound_Hello:
|
||||
return @"Hello";
|
||||
case OWSSound_Input:
|
||||
return @"Input";
|
||||
case OWSSound_Keys:
|
||||
return @"Keys";
|
||||
case OWSSound_Note:
|
||||
return @"Note";
|
||||
case OWSSound_Popcorn:
|
||||
return @"Popcorn";
|
||||
case OWSSound_Pulse:
|
||||
return @"Pulse";
|
||||
case OWSSound_Synth:
|
||||
return @"Synth";
|
||||
case OWSSound_SignalClassic:
|
||||
return @"Signal Classic";
|
||||
|
||||
// Call Audio
|
||||
case OWSSound_Opening:
|
||||
return @"Opening";
|
||||
case OWSSound_CallConnecting:
|
||||
return @"Call Connecting";
|
||||
case OWSSound_CallOutboundRinging:
|
||||
return @"Call Outboung Ringing";
|
||||
case OWSSound_CallBusy:
|
||||
return @"Call Busy";
|
||||
case OWSSound_CallFailure:
|
||||
return @"Call Failure";
|
||||
case OWSSound_MessageSent:
|
||||
return @"Message Sent";
|
||||
|
||||
// Other
|
||||
case OWSSound_None:
|
||||
return NSLocalizedString(@"SOUNDS_NONE",
|
||||
@"Label for the 'no sound' option that allows users to disable sounds for notifications, "
|
||||
@"etc.");
|
||||
}
|
||||
}
|
||||
|
||||
+ (nullable NSString *)filenameForSound:(OWSSound)sound
|
||||
{
|
||||
return [self filenameForSound:sound quiet:NO];
|
||||
}
|
||||
|
||||
+ (nullable NSString *)filenameForSound:(OWSSound)sound quiet:(BOOL)quiet
|
||||
{
|
||||
switch (sound) {
|
||||
case OWSSound_Default:
|
||||
return @"";
|
||||
|
||||
// Notification Sounds
|
||||
case OWSSound_Aurora:
|
||||
return (quiet ? @"aurora-quiet.aifc" : @"aurora.aifc");
|
||||
case OWSSound_Bamboo:
|
||||
return (quiet ? @"bamboo-quiet.aifc" : @"bamboo.aifc");
|
||||
case OWSSound_Chord:
|
||||
return (quiet ? @"chord-quiet.aifc" : @"chord.aifc");
|
||||
case OWSSound_Circles:
|
||||
return (quiet ? @"circles-quiet.aifc" : @"circles.aifc");
|
||||
case OWSSound_Complete:
|
||||
return (quiet ? @"complete-quiet.aifc" : @"complete.aifc");
|
||||
case OWSSound_Hello:
|
||||
return (quiet ? @"hello-quiet.aifc" : @"hello.aifc");
|
||||
case OWSSound_Input:
|
||||
return (quiet ? @"input-quiet.aifc" : @"input.aifc");
|
||||
case OWSSound_Keys:
|
||||
return (quiet ? @"keys-quiet.aifc" : @"keys.aifc");
|
||||
case OWSSound_Note:
|
||||
return (quiet ? @"note-quiet.aifc" : @"note.aifc");
|
||||
case OWSSound_Popcorn:
|
||||
return (quiet ? @"popcorn-quiet.aifc" : @"popcorn.aifc");
|
||||
case OWSSound_Pulse:
|
||||
return (quiet ? @"pulse-quiet.aifc" : @"pulse.aifc");
|
||||
case OWSSound_Synth:
|
||||
return (quiet ? @"synth-quiet.aifc" : @"synth.aifc");
|
||||
case OWSSound_SignalClassic:
|
||||
return (quiet ? @"classic-quiet.aifc" : @"classic.aifc");
|
||||
|
||||
// Ringtone Sounds
|
||||
case OWSSound_Opening:
|
||||
return @"Opening.m4r";
|
||||
|
||||
// Calls
|
||||
case OWSSound_CallConnecting:
|
||||
return @"ringback_tone_ansi.caf";
|
||||
case OWSSound_CallOutboundRinging:
|
||||
return @"ringback_tone_ansi.caf";
|
||||
case OWSSound_CallBusy:
|
||||
return @"busy_tone_ansi.caf";
|
||||
case OWSSound_CallFailure:
|
||||
return @"end_call_tone_cept.caf";
|
||||
case OWSSound_MessageSent:
|
||||
return @"message_sent.aiff";
|
||||
|
||||
// Other
|
||||
case OWSSound_None:
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
+ (nullable NSURL *)soundURLForSound:(OWSSound)sound quiet:(BOOL)quiet
|
||||
{
|
||||
NSString *_Nullable filename = [self filenameForSound:sound quiet:quiet];
|
||||
if (!filename) {
|
||||
return nil;
|
||||
}
|
||||
NSURL *_Nullable url = [[NSBundle mainBundle] URLForResource:filename.stringByDeletingPathExtension
|
||||
withExtension:filename.pathExtension];
|
||||
return url;
|
||||
}
|
||||
|
||||
+ (SystemSoundID)systemSoundIDForSound:(OWSSound)sound quiet:(BOOL)quiet
|
||||
{
|
||||
return [self.sharedManager systemSoundIDForSound:(OWSSound)sound quiet:quiet];
|
||||
}
|
||||
|
||||
- (SystemSoundID)systemSoundIDForSound:(OWSSound)sound quiet:(BOOL)quiet
|
||||
{
|
||||
NSString *cacheKey = [NSString stringWithFormat:@"%lu:%d", (unsigned long)sound, quiet];
|
||||
OWSSystemSound *_Nullable cachedSound = (OWSSystemSound *)[self.cachedSystemSounds getWithKey:cacheKey];
|
||||
|
||||
if (cachedSound) {
|
||||
return cachedSound.soundID;
|
||||
}
|
||||
|
||||
NSURL *soundURL = [self.class soundURLForSound:sound quiet:quiet];
|
||||
OWSSystemSound *newSound = [[OWSSystemSound alloc] initWithURL:soundURL];
|
||||
[self.cachedSystemSounds setWithKey:cacheKey value:newSound];
|
||||
|
||||
return newSound.soundID;
|
||||
}
|
||||
|
||||
#pragma mark - Notifications
|
||||
|
||||
+ (OWSSound)defaultNotificationSound
|
||||
{
|
||||
return OWSSound_Note;
|
||||
}
|
||||
|
||||
+ (OWSSound)globalNotificationSound
|
||||
{
|
||||
OWSSounds *instance = OWSSounds.sharedManager;
|
||||
NSNumber *_Nullable value = [instance.dbConnection objectForKey:kOWSSoundsStorageGlobalNotificationKey
|
||||
inCollection:kOWSSoundsStorageNotificationCollection];
|
||||
// Default to the global default.
|
||||
return (value ? (OWSSound)value.intValue : [self defaultNotificationSound]);
|
||||
}
|
||||
|
||||
+ (void)setGlobalNotificationSound:(OWSSound)sound
|
||||
{
|
||||
[self.sharedManager setGlobalNotificationSound:sound];
|
||||
}
|
||||
|
||||
- (void)setGlobalNotificationSound:(OWSSound)sound
|
||||
{
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) {
|
||||
[self setGlobalNotificationSound:sound transaction:transaction];
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)setGlobalNotificationSound:(OWSSound)sound transaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
{
|
||||
[self.sharedManager setGlobalNotificationSound:sound transaction:transaction];
|
||||
}
|
||||
|
||||
- (void)setGlobalNotificationSound:(OWSSound)sound transaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
{
|
||||
// Fallback push notifications play a sound specified by the server, but we don't want to store this configuration
|
||||
// on the server. Instead, we create a file with the same name as the default to be played when receiving
|
||||
// a fallback notification.
|
||||
NSString *dirPath = [[OWSFileSystem appLibraryDirectoryPath] stringByAppendingPathComponent:@"Sounds"];
|
||||
[OWSFileSystem ensureDirectoryExists:dirPath];
|
||||
|
||||
// This name is specified in the payload by the Signal Service when requesting fallback push notifications.
|
||||
NSString *kDefaultNotificationSoundFilename = @"NewMessage.aifc";
|
||||
NSString *defaultSoundPath = [dirPath stringByAppendingPathComponent:kDefaultNotificationSoundFilename];
|
||||
|
||||
NSURL *_Nullable soundURL = [OWSSounds soundURLForSound:sound quiet:NO];
|
||||
|
||||
NSData *soundData = ^{
|
||||
if (soundURL) {
|
||||
return [NSData dataWithContentsOfURL:soundURL];
|
||||
} else {
|
||||
return [NSData new];
|
||||
}
|
||||
}();
|
||||
|
||||
// Quick way to achieve an atomic "copy" operation that allows overwriting if the user has previously specified
|
||||
// a default notification sound.
|
||||
BOOL success = [soundData writeToFile:defaultSoundPath atomically:YES];
|
||||
|
||||
// The globally configured sound the user has configured is unprotected, so that we can still play the sound if the
|
||||
// user hasn't authenticated after power-cycling their device.
|
||||
[OWSFileSystem protectFileOrFolderAtPath:defaultSoundPath fileProtectionType:NSFileProtectionNone];
|
||||
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
|
||||
[transaction setObject:@(sound)
|
||||
forKey:kOWSSoundsStorageGlobalNotificationKey
|
||||
inCollection:kOWSSoundsStorageNotificationCollection];
|
||||
}
|
||||
|
||||
+ (OWSSound)notificationSoundForThread:(TSThread *)thread
|
||||
{
|
||||
OWSSounds *instance = OWSSounds.sharedManager;
|
||||
NSNumber *_Nullable value =
|
||||
[instance.dbConnection objectForKey:thread.uniqueId inCollection:kOWSSoundsStorageNotificationCollection];
|
||||
// Default to the "global" notification sound, which in turn will default to the global default.
|
||||
return (value ? (OWSSound)value.intValue : [self globalNotificationSound]);
|
||||
}
|
||||
|
||||
+ (void)setNotificationSound:(OWSSound)sound forThread:(TSThread *)thread
|
||||
{
|
||||
OWSSounds *instance = OWSSounds.sharedManager;
|
||||
[instance.dbConnection setObject:@(sound)
|
||||
forKey:thread.uniqueId
|
||||
inCollection:kOWSSoundsStorageNotificationCollection];
|
||||
}
|
||||
|
||||
#pragma mark - AudioPlayer
|
||||
|
||||
+ (BOOL)shouldAudioPlayerLoopForSound:(OWSSound)sound
|
||||
{
|
||||
return (sound == OWSSound_CallConnecting || sound == OWSSound_CallOutboundRinging);
|
||||
}
|
||||
|
||||
+ (nullable OWSAudioPlayer *)audioPlayerForSound:(OWSSound)sound
|
||||
audioBehavior:(OWSAudioBehavior)audioBehavior;
|
||||
{
|
||||
NSURL *_Nullable soundURL = [OWSSounds soundURLForSound:sound quiet:NO];
|
||||
if (!soundURL) {
|
||||
return nil;
|
||||
}
|
||||
OWSAudioPlayer *player = [[OWSAudioPlayer alloc] initWithMediaUrl:soundURL audioBehavior:audioBehavior];
|
||||
if ([self shouldAudioPlayerLoopForSound:sound]) {
|
||||
player.isLooping = YES;
|
||||
}
|
||||
return player;
|
||||
}
|
||||
|
||||
@end
|
@ -1,15 +0,0 @@
|
||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import SignalCoreKit
|
||||
|
||||
extension OWSSound {
|
||||
|
||||
public func notificationSound(isQuiet: Bool) -> UNNotificationSound {
|
||||
guard let filename = OWSSounds.filename(for: self, quiet: isQuiet) else {
|
||||
owsFailDebug("filename was unexpectedly nil")
|
||||
return UNNotificationSound.default
|
||||
}
|
||||
return UNNotificationSound(named: UNNotificationSoundName(rawValue: filename))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue