Detect when the NSE isn’t launching

This commit is contained in:
Max Radermacher 2024-02-06 23:39:19 -06:00 committed by GitHub
parent a1c0cd1cd2
commit d0c322fec9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 155 additions and 2 deletions

View File

@ -188,6 +188,7 @@ public class ChatListViewController: OWSViewController, HomeTabViewController {
searchResultsController.viewDidAppear(animated)
showBadgeSheetIfNecessary()
Task { await self.checkForFailedServiceExtensionLaunches() }
hasEverAppeared = true
if viewState.multiSelectState.isActive {
@ -1178,6 +1179,133 @@ public class ChatListViewController: OWSViewController, HomeTabViewController {
paymentsBannerView.addSubview(stack)
stack.autoPinEdgesToSuperviewEdges()
}
// MARK: - Notifications
func checkForFailedServiceExtensionLaunches() async {
guard #available(iOS 17.0, *) else {
return
}
guard RemoteConfig.shouldCheckForServiceExtensionFailures else {
return
}
await messageProcessor.waitForFetchingAndProcessing().awaitable()
// Has the NSE ever launched with the current version?
let appVersion = AppVersionImpl.shared
guard
let mainAppVersion = appVersion.lastCompletedLaunchMainAppVersion,
let nseAppVersion = appVersion.lastCompletedLaunchNSEAppVersion,
let upgradeDate = appVersion.firstMainAppLaunchDateAfterUpdate
else {
return
}
guard nseAppVersion != mainAppVersion else {
return
}
// Has it been at least an hour since we upgraded?
guard -upgradeDate.timeIntervalSinceNow > kHourInterval else {
return
}
// Has the user restarted since the most recent update was installed?
let bootTime: Date? = {
var timeVal = timeval()
var timeValSize = MemoryLayout<timeval>.size
let err = sysctlbyname("kern.boottime", &timeVal, &timeValSize, nil, 0)
guard err == 0, timeValSize == MemoryLayout<timeval>.size else {
return nil
}
return Date(timeIntervalSince1970: TimeInterval(timeVal.tv_sec))
}()
guard let bootTime else {
return
}
guard bootTime < upgradeDate else {
return
}
let keyValueStore = SDSKeyValueStore(collection: "FailedNSELaunches")
let mostRecentDateKey = "mostRecentPromptDate"
let promptCountKey = "promptCount"
let shouldShowPrompt = databaseStorage.read { tx in
// If we've shown the prompt recently, don't show it again.
let promptCount = keyValueStore.getInt(promptCountKey, defaultValue: 0, transaction: tx)
let promptBackoff: TimeInterval = {
switch promptCount {
case 0:
return 0
case 1, 2:
return 24*kHourInterval
case 3:
return 48*kHourInterval
case 4:
return 72*kHourInterval
default:
return 96*kHourInterval
}
}()
let mostRecentDate = keyValueStore.getDate(mostRecentDateKey, transaction: tx)
if let mostRecentDate, -mostRecentDate.timeIntervalSinceNow < promptBackoff {
return false
}
// If we haven't received a message since upgrading, don't show it.
guard
let mostRecentMessage = InteractionFinder.lastInsertedIncomingMessage(transaction: tx),
Date(millisecondsSince1970: mostRecentMessage.receivedAtTimestamp) > upgradeDate
else {
return false
}
return true
}
guard shouldShowPrompt else {
return
}
guard isChatListTopmostViewController() else {
return
}
let actionSheet = ActionSheetController(
title: OWSLocalizedString(
"NOTIFICATIONS_ERROR_TITLE",
comment: "Shown as the title of an alert when notifications can't be shown due to an error."
),
message: String(
format: OWSLocalizedString(
"NOTIFICATIONS_ERROR_MESSAGE",
comment: "Shown as the body of an alert when notifications can't be shown due to an error."
),
UIDevice.current.localizedModel
)
)
actionSheet.addAction(ActionSheetAction(
title: CommonStrings.contactSupport,
handler: { [weak self] _ in
guard let self else { return }
ContactSupportAlert.presentStep2(emailSupportFilter: "NotLaunchingNSE", fromViewController: self)
}
))
actionSheet.addAction(ActionSheetAction(title: CommonStrings.okButton))
let promptDate = Date()
self.present(actionSheet, animated: true)
await databaseStorage.awaitableWrite { tx in
keyValueStore.setDate(promptDate, key: mostRecentDateKey, transaction: tx)
keyValueStore.setInt(
keyValueStore.getInt(promptCountKey, defaultValue: 0, transaction: tx) + 1,
key: promptCountKey,
transaction: tx
)
}
}
}
// MARK: Settings Button

View File

@ -53,7 +53,9 @@ public class ContactSupportAlert: NSObject {
modal.dismiss()
}.catch { error in
guard !modal.wasCancelled else { return }
showError(error, emailSupportFilter: emailSupportFilter, fromViewController: fromViewController)
modal.dismiss(completion: {
showError(error, emailSupportFilter: emailSupportFilter, fromViewController: fromViewController)
})
}
}
}

View File

@ -4366,6 +4366,12 @@
/* Title for notification permission reminder megaphone */
"NOTIFICATION_PERMISSION_REMINDER_MEGAPHONE_TITLE" = "Turn on Notifications?";
/* Shown as the body of an alert when notifications can't be shown due to an error. */
"NOTIFICATIONS_ERROR_MESSAGE" = "Restart your %1$@ if youre not receiving Signal notifications.";
/* Shown as the title of an alert when notifications can't be shown due to an error. */
"NOTIFICATIONS_ERROR_TITLE" = "Couldnt Show Notifications";
/* No comment provided by engineer. */
"NOTIFICATIONS_FOOTER_WARNING" = "Actions include “Mark as Read,” “Reply,” and “Call Back.”";

View File

@ -42,6 +42,7 @@ public protocol AppVersion {
var lastCompletedLaunchMainAppVersion: String? { get }
var lastCompletedLaunchSAEAppVersion: String? { get }
var lastCompletedLaunchNSEAppVersion: String? { get }
var firstMainAppLaunchDateAfterUpdate: Date? { get }
var buildDate: Date { get }
@ -62,6 +63,7 @@ public class AppVersionImpl: AppVersion {
private let lastCompletedMainAppLaunchVersionKey = "kNSUserDefaults_LastCompletedLaunchAppVersion_MainApp"
private let lastCompletedSAELaunchVersionKey = "kNSUserDefaults_LastCompletedLaunchAppVersion_SAE"
private let lastCompletedNSELaunchVersionKey = "kNSUserDefaults_LastCompletedLaunchAppVersion_NSE"
private let firstMainAppLaunchDateAfterUpdateKey = "FirstMainAppLaunchDateAfterUpdate"
public static let shared: AppVersion = {
let result = AppVersionImpl(
@ -126,8 +128,10 @@ public class AppVersionImpl: AppVersion {
public private(set) var lastCompletedLaunchMainAppVersion: String? {
get { userDefaults.string(forKey: lastCompletedMainAppLaunchVersionKey) }
set {
let didChange = lastCompletedLaunchMainAppVersion != newValue
userDefaults.setOrRemove(newValue, forKey: lastCompletedLaunchVersionKey)
userDefaults.setOrRemove(newValue, forKey: lastCompletedMainAppLaunchVersionKey)
if didChange { userDefaults.set(Date(), forKey: firstMainAppLaunchDateAfterUpdateKey) }
}
}
public private(set) var lastCompletedLaunchSAEAppVersion: String? {
@ -144,6 +148,9 @@ public class AppVersionImpl: AppVersion {
userDefaults.setOrRemove(newValue, forKey: lastCompletedNSELaunchVersionKey)
}
}
public var firstMainAppLaunchDateAfterUpdate: Date? {
return userDefaults.object(forKey: firstMainAppLaunchDateAfterUpdateKey) as? Date
}
public let buildDate: Date
@ -341,6 +348,8 @@ public class MockAppVerion: AppVersion {
public var lastCompletedLaunchNSEAppVersion: String?
public var firstMainAppLaunchDateAfterUpdate: Date?
public var buildDate: Date = Date()
public func compare(_ lhs: String, with rhs: String) -> ComparisonResult {

View File

@ -256,6 +256,10 @@ public class RemoteConfig: BaseFlags {
return isEnabled(.enableGifSearch, defaultValue: true)
}
public static var shouldCheckForServiceExtensionFailures: Bool {
return !isEnabled(.serviceExtensionFailureKillSwitch)
}
// MARK: UInt values
private static func getUIntValue(
@ -475,6 +479,7 @@ private enum IsEnabledFlag: String, FlagType {
case cdsDisableCompatibilityMode = "cds.disableCompatibilityMode"
case canDonateWithSepa = "ios.canDonateWithSepa"
case enableGifSearch = "global.gifSearch"
case serviceExtensionFailureKillSwitch = "ios.serviceExtensionFailureKillSwitch"
var isSticky: Bool {
switch self {
@ -494,12 +499,15 @@ private enum IsEnabledFlag: String, FlagType {
case .ringrtcNwPathMonitorTrialKillSwitch: fallthrough
case .cdsDisableCompatibilityMode: fallthrough
case .canDonateWithSepa: fallthrough
case .enableGifSearch:
case .enableGifSearch: fallthrough
case .serviceExtensionFailureKillSwitch:
return false
}
}
var isHotSwappable: Bool {
switch self {
case .serviceExtensionFailureKillSwitch:
return true
case .automaticSessionResetKillSwitch: fallthrough
case .paymentsResetKillSwitch: fallthrough
case .messageResendKillSwitch: fallthrough