Signal-iOS/Signal/src/ViewControllers/DebugUI/DebugUINotifications.swift
Michael Kirk 45c4066a80 Fix deadlock with thread.name
For contact threads, thread.name opened a sneaky transaction, which would
sometimes cause a deadlock.

This commit moves the name method to the contact manager and offers an explicit
transaction flavor. There is still a sneaky transaction flavor used in some
places where it's deemed dangerous to plumb through a transaction without
further scrutiny.
2019-08-23 18:45:23 -06:00

235 lines
9.2 KiB
Swift

//
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
//
import Foundation
import SignalServiceKit
import SignalMessaging
import PromiseKit
#if DEBUG
class DebugUINotifications: DebugUIPage {
// MARK: Dependencies
var notificationPresenter: NotificationPresenter {
return AppEnvironment.shared.notificationPresenter
}
var messageSender: MessageSender {
return SSKEnvironment.shared.messageSender
}
var contactsManager: OWSContactsManager {
return Environment.shared.contactsManager
}
var databaseStorage: SDSDatabaseStorage {
return SSKEnvironment.shared.databaseStorage
}
// MARK: Overrides
override func name() -> String {
return "Notifications"
}
override func section(thread: TSThread?) -> OWSTableSection? {
guard let thread = thread else {
owsFailDebug("Notifications must specify thread.")
return nil
}
var sectionItems: [OWSTableItem] = []
if let contactThread = thread as? TSContactThread {
sectionItems += [
OWSTableItem(title: "All Notifications in Sequence") { [weak self] in
self?.notifyForEverythingInSequence(contactThread: contactThread).retainUntilComplete()
},
OWSTableItem(title: "Incoming Call") { [weak self] in
self?.notifyForIncomingCall(thread: contactThread).retainUntilComplete()
},
OWSTableItem(title: "Call Missed") { [weak self] in
self?.notifyForMissedCall(thread: contactThread).retainUntilComplete()
},
OWSTableItem(title: "Call Rejected: New Safety Number") { [weak self] in
self?.notifyForMissedCallBecauseOfNewIdentity(thread: contactThread).retainUntilComplete()
},
OWSTableItem(title: "Call Rejected: No Longer Verified") { [weak self] in
self?.notifyForMissedCallBecauseOfNoLongerVerifiedIdentity(thread: contactThread).retainUntilComplete()
}
]
}
sectionItems += [
OWSTableItem(title: "Last Incoming Message") { [weak self] in
self?.notifyForIncomingMessage(thread: thread).retainUntilComplete()
},
OWSTableItem(title: "Notify For Error Message") { [weak self] in
self?.notifyForErrorMessage(thread: thread).retainUntilComplete()
},
OWSTableItem(title: "Notify For Threadless Error Message") { [weak self] in
self?.notifyUserForThreadlessErrorMessage().retainUntilComplete()
},
OWSTableItem(title: "Notify of New Signal Users") { [weak self] in
self?.notifyOfNewUsers().retainUntilComplete()
}
]
return OWSTableSection(title: "Notifications have delay: \(kNotificationDelay)s", items: sectionItems)
}
// MARK: Helpers
// After enqueing the notification you may want to background the app or lock the screen before it triggers, so
// we give a little delay.
let kNotificationDelay: TimeInterval = 5
func delayedNotificationDispatch(block: @escaping () -> Void) -> Guarantee<Void> {
Logger.info("⚠️ will present notification after \(kNotificationDelay) second delay")
// Notifications won't sound if the app is suspended.
let taskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
return after(seconds: kNotificationDelay).done {
block()
}.then {
after(seconds: 2.0)
}.done {
// We don't want to endBackgroundTask until *after* the notifications manager is done,
// but it dispatches async without a completion handler, so we just wait a while extra.
// This is fragile, but it's only for debug UI.
UIApplication.shared.endBackgroundTask(taskIdentifier)
}
}
func delayedNotificationDispatchWithFakeCall(thread: TSContactThread, callBlock: @escaping (SignalCall) -> Void) -> Guarantee<Void> {
let call = SignalCall.incomingCall(localId: UUID(), remoteAddress: thread.contactAddress, signalingId: 0)
return delayedNotificationDispatch {
callBlock(call)
}
}
// MARK: Notification Methods
func notifyForEverythingInSequence(contactThread: TSContactThread) -> Guarantee<Void> {
let taskIdentifier = UIApplication.shared.beginBackgroundTask(expirationHandler: nil)
return firstly {
self.notifyForIncomingCall(thread: contactThread)
}.then {
self.notifyForMissedCall(thread: contactThread)
}.then {
self.notifyForMissedCallBecauseOfNewIdentity(thread: contactThread)
}.then {
self.notifyForMissedCallBecauseOfNoLongerVerifiedIdentity(thread: contactThread)
}.then {
self.notifyForIncomingMessage(thread: contactThread)
}.then {
self.notifyForErrorMessage(thread: contactThread)
}.then {
self.notifyUserForThreadlessErrorMessage()
}.then {
self.notifyOfNewUsers()
}.done {
UIApplication.shared.endBackgroundTask(taskIdentifier)
}
}
func notifyForIncomingCall(thread: TSContactThread) -> Guarantee<Void> {
return delayedNotificationDispatchWithFakeCall(thread: thread) { call in
let callerName = self.contactsManager.displayName(for: thread.contactAddress)
self.notificationPresenter.presentIncomingCall(call, callerName: callerName)
}
}
func notifyForMissedCall(thread: TSContactThread) -> Guarantee<Void> {
return delayedNotificationDispatchWithFakeCall(thread: thread) { call in
let callerName = self.contactsManager.displayName(for: thread.contactAddress)
self.notificationPresenter.presentMissedCall(call, callerName: callerName)
}
}
func notifyForMissedCallBecauseOfNewIdentity(thread: TSContactThread) -> Guarantee<Void> {
return delayedNotificationDispatchWithFakeCall(thread: thread) { call in
let callerName = self.contactsManager.displayName(for: thread.contactAddress)
self.notificationPresenter.presentMissedCallBecauseOfNewIdentity(call: call, callerName: callerName)
}
}
func notifyForMissedCallBecauseOfNoLongerVerifiedIdentity(thread: TSContactThread) -> Guarantee<Void> {
return delayedNotificationDispatchWithFakeCall(thread: thread) { call in
let callerName = self.contactsManager.displayName(for: thread.contactAddress)
self.notificationPresenter.presentMissedCallBecauseOfNoLongerVerifiedIdentity(call: call, callerName: callerName)
}
}
func notifyForIncomingMessage(thread: TSThread) -> Guarantee<Void> {
return delayedNotificationDispatch {
self.databaseStorage.write { transaction in
let factory = IncomingMessageFactory()
factory.threadCreator = { _ in return thread }
let incomingMessage = factory.create(transaction: transaction)
self.notificationPresenter.notifyUser(for: incomingMessage,
in: thread,
transaction: transaction)
}
}
}
func notifyForErrorMessage(thread: TSThread) -> Guarantee<Void> {
return delayedNotificationDispatch {
let errorMessage = TSErrorMessage(timestamp: NSDate.ows_millisecondTimeStamp(),
in: thread,
failedMessageType: TSErrorMessageType.invalidMessage)
self.databaseStorage.write { transaction in
self.notificationPresenter.notifyUser(for: errorMessage, thread: thread, transaction: transaction)
}
}
}
func notifyUserForThreadlessErrorMessage() -> Guarantee<Void> {
return delayedNotificationDispatch {
self.databaseStorage.write { transaction in
let errorMessage = ThreadlessErrorMessage.corruptedMessageInUnknownThread()
self.notificationPresenter.notifyUser(for: errorMessage,
transaction: transaction)
}
}
}
func notifyOfNewUsers() -> Guarantee<Void> {
return delayedNotificationDispatch {
let recipients: Set<SignalRecipient> = self.databaseStorage.readReturningResult { transaction in
let allRecipients = SignalRecipient.anyFetchAll(transaction: transaction)
let activeRecipients = allRecipients.filter { recipient in
guard recipient.devices.count > 0 else {
return false
}
guard !recipient.address.isLocalAddress else {
return false
}
return true
}
return Set(activeRecipients)
}
NewAccountDiscovery.shared.discovered(newRecipients: recipients, forNewThreadsOnly: false)
}
}
}
#endif