diff --git a/Podfile.lock b/Podfile.lock index 47b6951ec..153b82414 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -48,6 +48,7 @@ PODS: - CocoaLumberjack - libPhoneNumber-iOS - Mantle + - Reachability - SAMKeychain - SocketRocket - TwistedOakCollapsingFutures @@ -154,7 +155,7 @@ SPEC CHECKSUMS: PureLayout: 4d550abe49a94f24c2808b9b95db9131685fe4cd Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 SAMKeychain: 1865333198217411f35327e8da61b43de79b635b - SignalServiceKit: 0fa1aa668c13b51bca399ce970bc1f1d9297184b + SignalServiceKit: db63e60f9a9f851e9ae48214bee230be92bb7e57 SocketRocket: dbb1554b8fc288ef8ef370d6285aeca7361be31e SQLCipher: 43d12c0eb9c57fb438749618fc3ce0065509a559 TwistedOakCollapsingFutures: f359b90f203e9ab13dfb92c9ff41842a7fe1cd0c diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index e7e8f9329..178f6b89a 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -159,6 +159,8 @@ static NSString *const kURLHostVerifyPrefix = @"verify"; DDLogInfo(@"%@ application: didFinishLaunchingWithOptions completed.", self.tag); + [OWSAnalytics appLaunchDidBegin]; + return YES; } @@ -794,6 +796,7 @@ static NSString *const kURLHostVerifyPrefix = @"verify"; [DeviceSleepManager.sharedInstance removeBlockWithBlockObject:self]; + [OWSAnalytics appLaunchDidComplete]; [AppVersion.instance appLaunchDidComplete]; [self ensureRootViewController]; diff --git a/Signal/src/ViewControllers/ConversationView/MessagesViewController.m b/Signal/src/ViewControllers/ConversationView/MessagesViewController.m index 6189765ce..6701c5331 100644 --- a/Signal/src/ViewControllers/ConversationView/MessagesViewController.m +++ b/Signal/src/ViewControllers/ConversationView/MessagesViewController.m @@ -67,6 +67,7 @@ #import #import #import +#import #import #import #import @@ -100,7 +101,7 @@ static const int JSQ_TOOLBAR_ICON_HEIGHT = 22; static const int JSQ_TOOLBAR_ICON_WIDTH = 22; static const int JSQ_IMAGE_INSET = 5; -static NSTimeInterval const kTSMessageSentDateShowTimeInterval = 5 * 60; +static NSTimeInterval const kTSMessageSentDateShowTimeInterval = 5 * kMinuteInterval; NSString *const OWSMessagesViewControllerDidAppearNotification = @"OWSMessagesViewControllerDidAppear"; diff --git a/Signal/src/ViewControllers/DebugUI/DebugUIDiskUsage.m b/Signal/src/ViewControllers/DebugUI/DebugUIDiskUsage.m index bcf5e5493..6324ada08 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUIDiskUsage.m +++ b/Signal/src/ViewControllers/DebugUI/DebugUIDiskUsage.m @@ -5,6 +5,7 @@ #import "DebugUIDiskUsage.h" #import "OWSTableViewController.h" #import "Signal-Swift.h" +#import #import #import #import @@ -85,11 +86,7 @@ NS_ASSUME_NONNULL_BEGIN + (void)deleteOldMessages_3Months { - NSTimeInterval kMinute = 60.f; - NSTimeInterval kHour = 60 * kMinute; - NSTimeInterval kDay = 24 * kHour; - NSTimeInterval kMonth = 30 * kDay; - [self deleteOldMessages:kMonth * 3]; + [self deleteOldMessages:kMonthInterval * 3]; } + (void)deleteOldMessages:(NSTimeInterval)maxAgeSeconds diff --git a/Signal/src/environment/DebugLogger.m b/Signal/src/environment/DebugLogger.m index e4076ca12..3a8706e00 100644 --- a/Signal/src/environment/DebugLogger.m +++ b/Signal/src/environment/DebugLogger.m @@ -4,6 +4,7 @@ #import "DebugLogger.h" #import "OWSScrubbingLogFormatter.h" +#import #pragma mark Logging - Production logging wants us to write some logs to a file in case we need it for debugging. @@ -29,7 +30,7 @@ // Logging to file, because it's in the Cache folder, they are not uploaded in iTunes/iCloud backups. self.fileLogger = [DDFileLogger new]; // 24 hour rolling. - self.fileLogger.rollingFrequency = 60 * 60 * 24; + self.fileLogger.rollingFrequency = kDayInterval; // Keep last 3 days of logs - or last 3 logs (if logs rollover due to max file size). self.fileLogger.logFileManager.maximumNumberOfLogFiles = 3; // Raise the max file size per log file to 3 MB. diff --git a/Signal/src/util/AppUpdateNag.m b/Signal/src/util/AppUpdateNag.m index bec8a5a3f..111c8afae 100644 --- a/Signal/src/util/AppUpdateNag.m +++ b/Signal/src/util/AppUpdateNag.m @@ -6,6 +6,7 @@ #import "RegistrationViewController.h" #import "Signal-Swift.h" #import +#import #import NSString *const TSStorageManagerAppUpgradeNagCollection = @"TSStorageManagerAppUpgradeNagCollection"; @@ -70,10 +71,7 @@ NSString *const TSStorageManagerAppUpgradeNagDate = @"TSStorageManagerAppUpgrade NSDate *lastNagDate = [[TSStorageManager sharedManager] dateForKey:TSStorageManagerAppUpgradeNagDate inCollection:TSStorageManagerAppUpgradeNagCollection]; - const NSTimeInterval kMinute = 60.f; - const NSTimeInterval kHour = 60 * kMinute; - const NSTimeInterval kDay = 24 * kHour; - const NSTimeInterval kNagFrequency = kDay * 14; + const NSTimeInterval kNagFrequency = kDayInterval * 14; BOOL canNag = (!lastNagDate || fabs(lastNagDate.timeIntervalSinceNow) > kNagFrequency); if (!canNag) { return; diff --git a/Signal/src/util/DateUtil.m b/Signal/src/util/DateUtil.m index b7bb2156a..578e62693 100644 --- a/Signal/src/util/DateUtil.m +++ b/Signal/src/util/DateUtil.m @@ -1,7 +1,9 @@ -#import "DateUtil.h" +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// -#define ONE_DAY_TIME_INTERVAL (double)60 * 60 * 24 -#define ONE_WEEK_TIME_INTERVAL (double)60 * 60 * 24 * 7 +#import "DateUtil.h" +#import static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE"; @@ -31,11 +33,11 @@ static NSString *const DATE_FORMAT_WEEKDAY = @"EEEE"; } + (BOOL)dateIsOlderThanOneDay:(NSDate *)date { - return [[NSDate date] timeIntervalSinceDate:date] > ONE_DAY_TIME_INTERVAL; + return [[NSDate date] timeIntervalSinceDate:date] > kDayInterval; } + (BOOL)dateIsOlderThanOneWeek:(NSDate *)date { - return [[NSDate date] timeIntervalSinceDate:date] > ONE_WEEK_TIME_INTERVAL; + return [[NSDate date] timeIntervalSinceDate:date] > kWeekInterval; } + (BOOL)date:(NSDate *)date isEqualToDateIgnoringTime:(NSDate *)anotherDate { diff --git a/SignalServiceKit.podspec b/SignalServiceKit.podspec index 15cdc2eae..7c221a201 100644 --- a/SignalServiceKit.podspec +++ b/SignalServiceKit.podspec @@ -42,4 +42,5 @@ An Objective-C library for communicating with the Signal messaging service. s.dependency 'libPhoneNumber-iOS' s.dependency 'SAMKeychain' s.dependency 'TwistedOakCollapsingFutures' + s.dependency 'Reachability' end diff --git a/SignalServiceKit/Example/TSKitiOSTestApp/Podfile.lock b/SignalServiceKit/Example/TSKitiOSTestApp/Podfile.lock index 9c62eecb9..50160c58b 100644 --- a/SignalServiceKit/Example/TSKitiOSTestApp/Podfile.lock +++ b/SignalServiceKit/Example/TSKitiOSTestApp/Podfile.lock @@ -34,6 +34,7 @@ PODS: - Mantle/extobjc (= 2.1.0) - Mantle/extobjc (2.1.0) - ProtocolBuffers (1.9.11) + - Reachability (3.2) - SAMKeychain (1.5.2) - SignalServiceKit (0.9.0): - '25519' @@ -42,6 +43,7 @@ PODS: - CocoaLumberjack - libPhoneNumber-iOS - Mantle + - Reachability - SAMKeychain - SocketRocket - TwistedOakCollapsingFutures @@ -111,7 +113,7 @@ EXTERNAL SOURCES: AxolotlKit: :git: https://github.com/WhisperSystems/SignalProtocolKit.git SignalServiceKit: - :path: "../../../SignalServiceKit.podspec" + :path: ../../../SignalServiceKit.podspec SocketRocket: :git: https://github.com/facebook/SocketRocket.git @@ -132,8 +134,9 @@ SPEC CHECKSUMS: libPhoneNumber-iOS: f721ae4d5854bce60934f9fb9b0b28e8e68913cb Mantle: 2fa750afa478cd625a94230fbf1c13462f29395b ProtocolBuffers: d509225eb2ea43d9582a59e94348fcf86e2abd65 + Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 SAMKeychain: 1865333198217411f35327e8da61b43de79b635b - SignalServiceKit: 0fa1aa668c13b51bca399ce970bc1f1d9297184b + SignalServiceKit: db63e60f9a9f851e9ae48214bee230be92bb7e57 SocketRocket: dbb1554b8fc288ef8ef370d6285aeca7361be31e SQLCipher: 43d12c0eb9c57fb438749618fc3ce0065509a559 TwistedOakCollapsingFutures: f359b90f203e9ab13dfb92c9ff41842a7fe1cd0c diff --git a/SignalServiceKit/Example/TSKitiOSTestApp/TSKitiOSTestApp.xcodeproj/project.pbxproj b/SignalServiceKit/Example/TSKitiOSTestApp/TSKitiOSTestApp.xcodeproj/project.pbxproj index c6880e3a6..9c2748493 100644 --- a/SignalServiceKit/Example/TSKitiOSTestApp/TSKitiOSTestApp.xcodeproj/project.pbxproj +++ b/SignalServiceKit/Example/TSKitiOSTestApp/TSKitiOSTestApp.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 308D7DFA789594CEA62740D9 /* libPods-TSKitiOSTestAppTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C0DC1A83C39CBC09FB2405A3 /* libPods-TSKitiOSTestAppTests.a */; }; + 34D99C891F2250FF00D284D6 /* OWSAnalyticsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 34D99C881F2250FF00D284D6 /* OWSAnalyticsTests.m */; }; 45046FE01D95A6130015EFF2 /* TSMessagesManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 45046FDF1D95A6130015EFF2 /* TSMessagesManagerTest.m */; }; 450E3C9A1D96DD2600BF4EB6 /* OWSDisappearingMessagesJobTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 450E3C991D96DD2600BF4EB6 /* OWSDisappearingMessagesJobTest.m */; }; 4516E3E81DD153CC00DC4206 /* TSGroupThreadTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 4516E3E71DD153CC00DC4206 /* TSGroupThreadTest.m */; }; @@ -64,6 +65,7 @@ /* Begin PBXFileReference section */ 1A50A62A8930EE2BC9B8AC11 /* Pods-TSKitiOSTestApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TSKitiOSTestApp.release.xcconfig"; path = "Pods/Target Support Files/Pods-TSKitiOSTestApp/Pods-TSKitiOSTestApp.release.xcconfig"; sourceTree = ""; }; 31DFDA8F9523F5B15EA2376B /* Pods-TSKitiOSTestApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TSKitiOSTestApp.debug.xcconfig"; path = "Pods/Target Support Files/Pods-TSKitiOSTestApp/Pods-TSKitiOSTestApp.debug.xcconfig"; sourceTree = ""; }; + 34D99C881F2250FF00D284D6 /* OWSAnalyticsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSAnalyticsTests.m; sourceTree = ""; }; 36DA6C703F99948D553F4E3F /* Pods-TSKitiOSTestAppTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-TSKitiOSTestAppTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-TSKitiOSTestAppTests/Pods-TSKitiOSTestAppTests.debug.xcconfig"; sourceTree = ""; }; 45046FDF1D95A6130015EFF2 /* TSMessagesManagerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TSMessagesManagerTest.m; path = ../../../tests/Messages/TSMessagesManagerTest.m; sourceTree = ""; }; 450E3C991D96DD2600BF4EB6 /* OWSDisappearingMessagesJobTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSDisappearingMessagesJobTest.m; path = ../../../tests/Messages/OWSDisappearingMessagesJobTest.m; sourceTree = ""; }; @@ -201,6 +203,7 @@ children = ( 45458B731CC342B600A02153 /* CryptographyTests.m */, 45458B741CC342B600A02153 /* MessagePaddingTests.m */, + 34D99C881F2250FF00D284D6 /* OWSAnalyticsTests.m */, ); name = Util; path = ../../../tests/Util; @@ -563,6 +566,7 @@ 45E741B61E5D14E800735842 /* OWSIncomingMessageFinderTest.m in Sources */, 452EE6D51D4AC43300E934BA /* OWSOrphanedDataCleanerTest.m in Sources */, 450E3C9A1D96DD2600BF4EB6 /* OWSDisappearingMessagesJobTest.m in Sources */, + 34D99C891F2250FF00D284D6 /* OWSAnalyticsTests.m in Sources */, 452EE6CF1D4A754C00E934BA /* TSThreadTest.m in Sources */, 45C6A09A1D2F029B007D8AC0 /* TSMessageTest.m in Sources */, 453E1FCF1DA8313100DDD7B7 /* OWSMessageSenderTest.m in Sources */, diff --git a/SignalServiceKit/src/Account/TSPreKeyManager.m b/SignalServiceKit/src/Account/TSPreKeyManager.m index 45e61699e..a7b8a1102 100644 --- a/SignalServiceKit/src/Account/TSPreKeyManager.m +++ b/SignalServiceKit/src/Account/TSPreKeyManager.m @@ -3,6 +3,7 @@ // #import "TSPreKeyManager.h" +#import "NSDate+OWS.h" #import "NSURLSessionDataTask+StatusCode.h" #import "OWSIdentityManager.h" #import "TSNetworkManager.h" @@ -13,17 +14,17 @@ // Time before deletion of signed prekeys (measured in seconds) // // Currently we retain signed prekeys for at least 7 days. -static const CGFloat kSignedPreKeysDeletionTime = 7 * 24 * 60 * 60; +static const NSTimeInterval kSignedPreKeysDeletionTime = 7 * kDayInterval; // Time before rotation of signed prekeys (measured in seconds) // // Currently we rotate signed prekeys every 2 days (48 hours). -static const CGFloat kSignedPreKeyRotationTime = 2 * 24 * 60 * 60; +static const NSTimeInterval kSignedPreKeyRotationTime = 2 * kDayInterval; // How often we check prekey state on app activation. // // Currently we check prekey state every 12 hours. -static const CGFloat kPreKeyCheckFrequencySeconds = 12 * 60 * 60; +static const NSTimeInterval kPreKeyCheckFrequencySeconds = 12 * kHourInterval; // We generate 100 one-time prekeys at a time. We should replenish // whenever ~2/3 of them have been consumed. @@ -40,7 +41,7 @@ static const NSUInteger kMaxPrekeyUpdateFailureCount = 5; // before the message sending is disabled. // // Current value is 10 days (240 hours). -static const CGFloat kSignedPreKeyUpdateFailureMaxFailureDuration = 10 * 24 * 60 * 60; +static const NSTimeInterval kSignedPreKeyUpdateFailureMaxFailureDuration = 10 * kDayInterval; #pragma mark - @@ -180,7 +181,11 @@ static const CGFloat kSignedPreKeyUpdateFailureMaxFailureDuration = 10 * 24 * 60 [TSPreKeyManager clearPreKeyUpdateFailureCount]; } failure:^(NSURLSessionDataTask *task, NSError *error) { - OWSAnalyticsError(@"Prekey update failed (%@): %@", description, error); + if (modeCopy == RefreshPreKeysMode_SignedAndOneTime) { + OWSProdErrorWNSError(@"error_prekeys_update_failed_signed_and_onetime", error); + } else { + OWSProdErrorWNSError(@"error_prekeys_update_failed_just_signed", error); + } // Mark the prekeys as _NOT_ checked on failure. [self markPreKeysAsNotChecked]; @@ -388,10 +393,12 @@ static const CGFloat kSignedPreKeyUpdateFailureMaxFailureDuration = 10 * 24 * 60 } } - OWSAnalyticsInfo(@"%@ Deleting old signed prekey: %@, wasAcceptedByService: %d", - self.tag, - [dateFormatter stringFromDate:signedPrekey.generatedAt], - signedPrekey.wasAcceptedByService); + OWSProdInfoWParams(@"prekeys_deleted_old_signed_prekey", ^{ + return (@{ + @"generated" : [dateFormatter stringFromDate:signedPrekey.generatedAt], + @"accepted" : @(signedPrekey.wasAcceptedByService), + }); + }); oldSignedPreKeyCount--; [storageManager removeSignedPreKey:signedPrekey.Id]; diff --git a/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.m b/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.m index 60a2c6b2e..484420ea3 100644 --- a/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.m +++ b/SignalServiceKit/src/Contacts/OWSDisappearingMessagesConfiguration.m @@ -1,11 +1,14 @@ -// Created by Michael Kirk on 9/23/16. -// Copyright © 2016 Open Whisper Systems. All rights reserved. +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// #import "OWSDisappearingMessagesConfiguration.h" +#import "NSDate+OWS.h" NS_ASSUME_NONNULL_BEGIN -const uint32_t OWSDisappearingMessagesConfigurationDefaultExpirationDuration = 60 * 60 * 24; // 1 day. +// 1 day. +const uint32_t OWSDisappearingMessagesConfigurationDefaultExpirationDuration = kDayInterval; @interface OWSDisappearingMessagesConfiguration () diff --git a/SignalServiceKit/src/Messages/OWSDisappearingMessagesJob.m b/SignalServiceKit/src/Messages/OWSDisappearingMessagesJob.m index 6483fcb95..269ac80ea 100644 --- a/SignalServiceKit/src/Messages/OWSDisappearingMessagesJob.m +++ b/SignalServiceKit/src/Messages/OWSDisappearingMessagesJob.m @@ -4,6 +4,7 @@ #import "OWSDisappearingMessagesJob.h" #import "ContactsManagerProtocol.h" +#import "NSDate+OWS.h" #import "NSDate+millisecondTimeStamp.h" #import "NSTimer+OWS.h" #import "OWSDisappearingConfigurationUpdateInfoMessage.h" @@ -300,7 +301,7 @@ NS_ASSUME_NONNULL_BEGIN - (NSTimeInterval)maxDelaySeconds { // Don't run less often than once per N minutes. - return 5 * 60.f; + return 5 * kMinuteInterval; } // Waits the maximum amount of time to run again. diff --git a/SignalServiceKit/src/Messages/OWSMessageSender.m b/SignalServiceKit/src/Messages/OWSMessageSender.m index b931f1b62..0e26cc45e 100644 --- a/SignalServiceKit/src/Messages/OWSMessageSender.m +++ b/SignalServiceKit/src/Messages/OWSMessageSender.m @@ -836,7 +836,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; AssertIsOnSendingQueue(); if ([TSPreKeyManager isAppLockedDueToPreKeyUpdateFailures]) { - OWSAnalyticsError(@"Message send failed due to prekey update failures"); + OWSProdError(@"message_send_error_failed_due_to_prekey_update_failures"); // Retry prekey update every time user tries to send a message while app // is disabled due to prekey update failures. @@ -878,7 +878,7 @@ NSString *const OWSMessageSenderRateLimitedException = @"RateLimitedException"; // We expect it to happen whenever Bob reinstalls, and Alice messages Bob before // she can pull down his latest identity. // If it's happening a lot, we should rethink our profile fetching strategy. - OWSAnalyticsInfo(@"Message send failed due to untrusted key."); + OWSProdInfo(@"message_send_error_failed_due_to_untrusted_key"); NSString *localizedErrorDescriptionFormat = NSLocalizedString(@"FAILED_SENDING_BECAUSE_UNTRUSTED_IDENTITY_KEY", diff --git a/SignalServiceKit/src/Storage/OWSOrphanedDataCleaner.m b/SignalServiceKit/src/Storage/OWSOrphanedDataCleaner.m index 421f72aa2..23997a5b8 100644 --- a/SignalServiceKit/src/Storage/OWSOrphanedDataCleaner.m +++ b/SignalServiceKit/src/Storage/OWSOrphanedDataCleaner.m @@ -3,6 +3,7 @@ // #import "OWSOrphanedDataCleaner.h" +#import "NSDate+OWS.h" #import "TSAttachmentStream.h" #import "TSInteraction.h" #import "TSMessage.h" @@ -134,7 +135,7 @@ NS_ASSUME_NONNULL_BEGIN #ifdef SSK_BUILDING_FOR_TESTS const NSTimeInterval kMinimumOrphanAge = 0.f; #else - const NSTimeInterval kMinimumOrphanAge = 15 * 60.f; + const NSTimeInterval kMinimumOrphanAge = 15 * kMinuteInterval; #endif if (!shouldCleanup) { diff --git a/SignalServiceKit/src/Storage/TSStorageManager.m b/SignalServiceKit/src/Storage/TSStorageManager.m index 18969ae7c..539aaf33d 100644 --- a/SignalServiceKit/src/Storage/TSStorageManager.m +++ b/SignalServiceKit/src/Storage/TSStorageManager.m @@ -117,14 +117,14 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass"; // The best we can try to do is to discard the current database // and behave like a clean install. - OWSAnalyticsCritical(@"Could not load database"); + OWSProdError(@"storage_error_could_not_load_database"); // Try to reset app by deleting database. // Disabled resetting storage until we have better data on why this happens. // [self resetSignalStorage]; if (![self tryToLoadDatabase]) { - OWSAnalyticsCritical(@"Could not load database (second attempt)"); + OWSProdError(@"storage_error_could_not_load_database_second_attempt"); [NSException raise:TSStorageManagerExceptionNameNoDatabase format:@"Failed to initialize database."]; } @@ -353,8 +353,7 @@ static NSString *keychainDBPassAccount = @"TSDatabasePass"; BOOL shouldHavePassword = [NSFileManager.defaultManager fileExistsAtPath:[self dbPath]]; if (shouldHavePassword) { - OWSAnalyticsCriticalWithParameters(@"Could not retrieve database password from keychain", - @{ @"ErrorCode" : @(keyFetchError.code) }); + OWSProdErrorWNSError(@"storage_error_could_not_load_database_second_attempt", keyFetchError); } // Try to reset app by deleting database. diff --git a/SignalServiceKit/src/Util/NSDate+OWS.h b/SignalServiceKit/src/Util/NSDate+OWS.h new file mode 100755 index 000000000..cba1a0ded --- /dev/null +++ b/SignalServiceKit/src/Util/NSDate+OWS.h @@ -0,0 +1,14 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +NS_ASSUME_NONNULL_BEGIN + +// These NSTimeInterval constants provide simplified durations for readability. +#define kMinuteInterval 60 +#define kHourInterval (60 * kMinuteInterval) +#define kDayInterval (24 * kHourInterval) +#define kWeekInterval (7 * kDayInterval) +#define kMonthInterval (30 * kDayInterval) + +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Util/OWSAnalytics.h b/SignalServiceKit/src/Util/OWSAnalytics.h index b43717430..ffddb8818 100755 --- a/SignalServiceKit/src/Util/OWSAnalytics.h +++ b/SignalServiceKit/src/Util/OWSAnalytics.h @@ -1,9 +1,10 @@ // -// OWSAnalytics.h -// // Copyright (c) 2017 Open Whisper Systems. All rights reserved. // +NS_ASSUME_NONNULL_BEGIN + +// TODO: We probably don't need all of these levels. typedef NS_ENUM(NSUInteger, OWSAnalyticsSeverity) { OWSAnalyticsSeverityDebug = 0, OWSAnalyticsSeverityInfo = 1, @@ -32,41 +33,121 @@ typedef NS_ENUM(NSUInteger, OWSAnalyticsSeverity) { @interface OWSAnalytics : NSObject // description: A non-empty string without any leading whitespace. +// This should conform to our analytics event naming conventions. +// "category_event_name", e.g. "database_error_no_database_file_found". // parameters: Optional. // If non-nil, the keys should all be non-empty NSStrings. // Values should be NSStrings or NSNumbers. -+ (void)logEvent:(NSString *)description ++ (void)logEvent:(NSString *)eventName severity:(OWSAnalyticsSeverity)severity - parameters:(NSDictionary *)parameters - location:(const char *)location; + parameters:(nullable NSDictionary *)parameters + location:(const char *)location + line:(int)line; + ++ (void)appLaunchDidBegin; + ++ (void)appLaunchDidComplete; @end -#define OWSAnalyticsLogEvent(severityLevel, frmt, ...) \ - [OWSAnalytics logEvent:[NSString stringWithFormat:frmt, ##__VA_ARGS__] \ - severity:severityLevel \ - parameters:nil \ - location:__PRETTY_FUNCTION__]; +typedef NSDictionary *_Nonnull (^OWSProdAssertParametersBlock)(); + +#define kOWSProdAssertParameterDescription @"description" +#define kOWSProdAssertParameterNSErrorDomain @"nserror_domain" +#define kOWSProdAssertParameterNSErrorCode @"nserror_code" +#define kOWSProdAssertParameterNSErrorDescription @"nserror_description" + +// These methods should be used to assert errors for which we want to fire analytics events. +// +// In production, returns __Value, the assert value, so that we can handle this case. +// In debug builds, asserts. +// +// parametersBlock is of type OWSProdAssertParametersBlock. +// The "C" variants (e.g. OWSProdAssert() vs. OWSProdCAssert() should be used in free functions, +// where there is no self. +// +#define OWSProdAssertWParamsTemplate(__value, __analyticsEventName, __parametersBlock, __assertMacro) \ + { \ + if (!(BOOL)(__value)) { \ + NSDictionary *__eventParameters = (__parametersBlock ? __parametersBlock() : nil); \ + [DDLog flushLog]; \ + [OWSAnalytics logEvent:__analyticsEventName \ + severity:OWSAnalyticsSeverityError \ + parameters:__eventParameters \ + location:__PRETTY_FUNCTION__ \ + line:__LINE__]; \ + } \ + __assertMacro(__value); \ + return (BOOL)(__value); \ + } + +#define OWSProdAssertWParams(__value, __analyticsEventName, __parametersBlock) \ + OWSProdAssertWParamsTemplate(__value, __analyticsEventName, __parametersBlock, OWSAssert) + +#define OWSProdCAssertWParams(__value, __analyticsEventName, __parametersBlock) \ + OWSProdAssertWParamsTemplate(__value, __analyticsEventName, __parametersBlock, OWSCAssert) + +#define OWSProdAssert(__value, __analyticsEventName) OWSProdAssertWParams(__value, __analyticsEventName, nil) + +#define OWSProdCAssert(__value, __analyticsEventName) OWSProdCAssertWParams(__value, __analyticsEventName, nil) + +#define OWSProdFailWParamsTemplate(__analyticsEventName, __parametersBlock, __failMacro) \ + { \ + NSDictionary *__eventParameters \ + = (__parametersBlock ? ((OWSProdAssertParametersBlock)__parametersBlock)() : nil); \ + [OWSAnalytics logEvent:__analyticsEventName \ + severity:OWSAnalyticsSeverityCritical \ + parameters:__eventParameters \ + location:__PRETTY_FUNCTION__ \ + line:__LINE__]; \ + __failMacro(__analyticsEventName); \ + } + +#define OWSProdFailWParams(__analyticsEventName, __parametersBlock) \ + OWSProdFailWParamsTemplate(__analyticsEventName, __parametersBlock, OWSFail) +#define OWSProdCFailWParams(__analyticsEventName, __parametersBlock) \ + OWSProdFailWParamsTemplate(__analyticsEventName, __parametersBlock, OWSCFail) + +#define OWSProdFail(__analyticsEventName) OWSProdFailWParams(__analyticsEventName, nil) + +#define OWSProdCFail(__analyticsEventName) OWSProdCFailWParams(__analyticsEventName, nil) + +#define AnalyticsParametersFromNSError(__nserror) \ + ^{ \ + return (@{ \ + kOWSProdAssertParameterNSErrorDomain : __nserror.domain, \ + kOWSProdAssertParameterNSErrorCode : @(__nserror.code), \ + kOWSProdAssertParameterNSErrorDescription : __nserror.description, \ + }); \ + } + +#define OWSProdFailWNSError(__analyticsEventName, __nserror) \ + OWSProdFailWParams(__analyticsEventName, AnalyticsParametersFromNSError(__nserror)) + +#define OWSProdEventWParams(__severityLevel, __analyticsEventName, __parametersBlock) \ + { \ + NSDictionary *__eventParameters \ + = (__parametersBlock ? ((OWSProdAssertParametersBlock)__parametersBlock)() : nil); \ + [OWSAnalytics logEvent:__analyticsEventName \ + severity:OWSAnalyticsSeverityCritical \ + parameters:__eventParameters \ + location:__PRETTY_FUNCTION__ \ + line:__LINE__]; \ + } + +#define OWSProdErrorWParams(__analyticsEventName, __parametersBlock) \ + OWSProdEventWParams(OWSAnalyticsSeverityCritical, __analyticsEventName, __parametersBlock) -#define OWSAnalyticsLogEventWithParameters(severityLevel, frmt, params) \ - [OWSAnalytics logEvent:frmt severity:severityLevel parameters:params location:__PRETTY_FUNCTION__]; +#define OWSProdError(__analyticsEventName) OWSProdEventWParams(OWSAnalyticsSeverityCritical, __analyticsEventName, nil) -#define OWSAnalyticsDebug(frmt, ...) OWSAnalyticsLogEvent(OWSAnalyticsSeverityDebug, frmt, ##__VA_ARGS__) -#define OWSAnalyticsDebugWithParameters(description, params) \ - OWSAnalyticsLogEventWithParameters(OWSAnalyticsSeverityDebug, description, params) +#define OWSProdInfoWParams(__analyticsEventName, __parametersBlock) \ + OWSProdEventWParams(OWSAnalyticsSeverityInfo, __analyticsEventName, __parametersBlock) -#define OWSAnalyticsInfo(frmt, ...) OWSAnalyticsLogEvent(OWSAnalyticsSeverityInfo, frmt, ##__VA_ARGS__) -#define OWSAnalyticsInfoWithParameters(description, params) \ - OWSAnalyticsLogEventWithParameters(OWSAnalyticsSeverityInfo, description, params) +#define OWSProdInfo(__analyticsEventName) OWSProdEventWParams(OWSAnalyticsSeverityInfo, __analyticsEventName, nil) -#define OWSAnalyticsWarn(frmt, ...) OWSAnalyticsLogEvent(OWSAnalyticsSeverityWarn, frmt, ##__VA_ARGS__) -#define OWSAnalyticsWarnWithParameters(description, params) \ - OWSAnalyticsLogEventWithParameters(OWSAnalyticsSeverityWarn, description, params) +#define OWSProdCFail(__analyticsEventName) OWSProdCFailWParams(__analyticsEventName, nil) -#define OWSAnalyticsError(frmt, ...) OWSAnalyticsLogEvent(OWSAnalyticsSeverityError, frmt, ##__VA_ARGS__) -#define OWSAnalyticsErrorWithParameters(description, params) \ - OWSAnalyticsLogEventWithParameters(OWSAnalyticsSeverityError, description, params) +#define OWSProdErrorWNSError(__analyticsEventName, __nserror) \ + OWSProdErrorWParams(__analyticsEventName, AnalyticsParametersFromNSError(__nserror)) -#define OWSAnalyticsCritical(frmt, ...) OWSAnalyticsLogEvent(OWSAnalyticsSeverityCritical, frmt, ##__VA_ARGS__) -#define OWSAnalyticsCriticalWithParameters(description, params) \ - OWSAnalyticsLogEventWithParameters(OWSAnalyticsSeverityCritical, description, params) +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/Util/OWSAnalytics.m b/SignalServiceKit/src/Util/OWSAnalytics.m index c48ac0fe3..043a008c3 100755 --- a/SignalServiceKit/src/Util/OWSAnalytics.m +++ b/SignalServiceKit/src/Util/OWSAnalytics.m @@ -1,12 +1,53 @@ // -// OWSAnalytics.m -// // Copyright (c) 2017 Open Whisper Systems. All rights reserved. // +#import "OWSAnalytics.h" +#import "AppVersion.h" +#import "TSStorageManager.h" #import +#import -#import "OWSAnalytics.h" +NS_ASSUME_NONNULL_BEGIN + +#if TARGET_IPHONE_SIMULATOR + +#define NO_SIGNAL_ANALYTICS + +#else + +#ifdef DEBUG + +// TODO: Disable analytics for debug builds. +//#define NO_SIGNAL_ANALYTICS + +#endif + +#endif + +NSString *const kOWSAnalytics_EventsCollection = @"kOWSAnalytics_EventsCollection"; + +NSString *const kOWSAnalytics_Collection = @"kOWSAnalytics_Collection"; +NSString *const kOWSAnalytics_KeyLaunchCount = @"kOWSAnalytics_KeyLaunchCount"; +NSString *const kOWSAnalytics_KeyLaunchCompleteCount = @"kOWSAnalytics_KeyLaunchCompleteCount"; + +// Percentage of analytics events to discard. 0 <= x <= 100. +const int kOWSAnalytics_DiscardFrequency = 0; + +@interface OWSAnalytics () + +@property (nonatomic, readonly) TSStorageManager *storageManager; +@property (nonatomic, readonly) Reachability *reachability; +@property (nonatomic, readonly) YapDatabaseConnection *dbConnection; + +@property (atomic) BOOL hasRequestInFlight; + +@property (atomic) NSNumber *launchCount; +@property (atomic) NSNumber *launchCompleteCount; + +@end + +#pragma mark - @implementation OWSAnalytics @@ -16,27 +57,223 @@ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [self new]; - // TODO: If we ever log these events to disk, - // we may want to protect these file(s) like TSStorageManager. + instance = [[self alloc] initDefault]; }); return instance; } -+ (void)logEvent:(NSString *)description - severity:(OWSAnalyticsSeverity)severity - parameters:(NSDictionary *)parameters - location:(const char *)location +- (instancetype)initDefault +{ + TSStorageManager *storageManager = [TSStorageManager sharedManager]; + + return [self initWithStorageManager:storageManager]; +} + +- (instancetype)initWithStorageManager:(TSStorageManager *)storageManager +{ + self = [super init]; + + if (!self) { + return self; + } + + OWSAssert(storageManager); + + _storageManager = storageManager; + // Use a newDatabaseConnection so as not to block other reads in the launch path. + _dbConnection = storageManager.newDatabaseConnection; + _reachability = [Reachability reachabilityForInternetConnection]; + + [self observeNotifications]; + + OWSSingletonAssert(); + + return self; +} + +- (void)observeNotifications +{ + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(reachabilityChanged) + name:kReachabilityChangedNotification + object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(applicationDidBecomeActive) + name:UIApplicationDidBecomeActiveNotification + object:nil]; +} + +- (void)dealloc +{ + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} + +- (void)reachabilityChanged +{ + OWSAssert([NSThread isMainThread]); + + [self tryToSyncEvents]; +} + +- (void)applicationDidBecomeActive { + OWSAssert([NSThread isMainThread]); - [[self sharedInstance] logEvent:description severity:severity parameters:parameters location:location]; + [self tryToSyncEvents]; } -- (void)logEvent:(NSString *)description +- (void)tryToSyncEvents +{ + // Don't try to sync if: + // + // * There's no network available. + // * There's already a sync request in flight. + if (!self.reachability.isReachable || self.hasRequestInFlight) { + return; + } + + dispatch_async(self.serialQueue, ^{ + __block NSString *firstEventKey = nil; + __block NSDictionary *firstEventDictionary = nil; + [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + // Take any event. We don't need to deliver them in any particular order. + [transaction enumerateKeysInCollection:kOWSAnalytics_EventsCollection + usingBlock:^(NSString *key, BOOL *_Nonnull stop) { + firstEventKey = key; + *stop = YES; + }]; + if (!firstEventKey) { + return; + } + + firstEventDictionary = [transaction objectForKey:firstEventKey inCollection:kOWSAnalytics_EventsCollection]; + OWSAssert(firstEventDictionary); + OWSAssert([firstEventDictionary isKindOfClass:[NSDictionary class]]); + }]; + + if (!firstEventDictionary) { + return; + } + + DDLogDebug(@"%@ trying to deliver event: %@", self.tag, firstEventKey); + self.hasRequestInFlight = YES; + // Until we integrate with an analytics platform, behave as though all event delivery succeeds. + dispatch_async(dispatch_get_main_queue(), ^{ + self.hasRequestInFlight = NO; + + BOOL success = YES; + if (success) { + [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + // Remove from queue. + [transaction removeObjectForKey:firstEventKey inCollection:kOWSAnalytics_EventsCollection]; + }]; + } + + // Wait a second between network requests / retries. + dispatch_after( + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + [self tryToSyncEvents]; + }); + }); + }); +} + +- (dispatch_queue_t)serialQueue +{ + static dispatch_queue_t queue = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + queue = dispatch_queue_create("org.whispersystems.analytics.serial", DISPATCH_QUEUE_SERIAL); + }); + return queue; +} + +- (NSDictionary *)eventSuperProperties +{ + NSMutableDictionary *result = [NSMutableDictionary new]; + if (AppVersion.instance.firstAppVersion) { + result[@"app_version_first"] = AppVersion.instance.firstAppVersion; + } + if (AppVersion.instance.lastAppVersion) { + result[@"app_version_last"] = AppVersion.instance.lastAppVersion; + } + if (AppVersion.instance.currentAppVersion) { + result[@"app_version_current"] = AppVersion.instance.currentAppVersion; + } + NSNumber *launchCount = self.launchCount; + if (launchCount) { + result[@"launch_count"] = @([self orderOfMagnitudeOf:launchCount.longValue]); + } + // TODO: Order of magnitude: thread count. + // TODO: Order of magnitude: total message count. + + return result; +} + +- (long)orderOfMagnitudeOf:(long)value +{ + return [OWSAnalytics orderOfMagnitudeOf:value]; +} + ++ (long)orderOfMagnitudeOf:(long)value +{ + if (value <= 0) { + return 0; + } + return (long)round(pow(10, floor(log10(value)))); +} + +- (void)addEvent:(NSString *)eventName properties:(NSDictionary *)properties +{ + OWSAssert(eventName.length > 0); + + uint32_t discardValue = arc4random_uniform(101); + if (discardValue < kOWSAnalytics_DiscardFrequency) { + DDLogVerbose(@"Discarding event: %@", eventName); + return; + } + +#ifndef NO_SIGNAL_ANALYTICS + dispatch_async(self.serialQueue, ^{ + // Add super properties. + NSMutableDictionary *eventProperties = (properties ? [properties mutableCopy] : [NSMutableDictionary new]); + [eventProperties addEntriesFromDictionary:self.eventSuperProperties]; + + NSDictionary *eventDictionary = [eventProperties copy]; + OWSAssert(eventDictionary); + NSString *eventKey = [NSUUID UUID].UUIDString; + DDLogDebug(@"%@ enqueuing event: %@", self.tag, eventKey); + + [self.dbConnection readWriteWithBlock:^(YapDatabaseReadWriteTransaction *transaction) { + const int kMaxQueuedEvents = 5000; + if ([transaction numberOfKeysInCollection:kOWSAnalytics_EventsCollection] > kMaxQueuedEvents) { + DDLogError(@"%@ Event queue overflow.", self.tag); + return; + } + + [transaction setObject:eventDictionary forKey:eventKey inCollection:kOWSAnalytics_EventsCollection]; + }]; + + [self tryToSyncEvents]; + }); +#endif +} + ++ (void)logEvent:(NSString *)eventName severity:(OWSAnalyticsSeverity)severity - parameters:(NSDictionary *)parameters + parameters:(nullable NSDictionary *)parameters location:(const char *)location + line:(int)line { + [[self sharedInstance] logEvent:eventName severity:severity parameters:parameters location:location line:line]; +} +- (void)logEvent:(NSString *)eventName + severity:(OWSAnalyticsSeverity)severity + parameters:(nullable NSDictionary *)parameters + location:(const char *)location + line:(int)line +{ DDLogFlag logFlag; BOOL async = YES; switch (severity) { @@ -64,13 +301,81 @@ } // Log the event. + NSString *logString = [NSString stringWithFormat:@"%s:%d %@", location, line, eventName]; if (!parameters) { - LOG_MAYBE(async, LOG_LEVEL_DEF, logFlag, 0, nil, location, @"%@", description); + LOG_MAYBE(async, LOG_LEVEL_DEF, logFlag, 0, nil, location, @"%@", logString); } else { - LOG_MAYBE(async, LOG_LEVEL_DEF, logFlag, 0, nil, location, @"%@ %@", description, parameters); + LOG_MAYBE(async, LOG_LEVEL_DEF, logFlag, 0, nil, location, @"%@ %@", logString, parameters); + } + if (!async) { + [DDLog flushLog]; } - // Do nothing. We don't yet serialize or transmit analytics events. + NSMutableDictionary *eventProperties = (parameters ? [parameters mutableCopy] : [NSMutableDictionary new]); + eventProperties[@"event_location"] = [NSString stringWithFormat:@"%s:%d", location, line]; + [self addEvent:eventName properties:eventProperties]; +} + +#pragma mark - Logging + ++ (void)appLaunchDidBegin +{ + [self.sharedInstance appLaunchDidBegin]; +} + +- (void)appLaunchDidBegin +{ + OWSProdInfo(@"app_launch"); + + [self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) { + NSNumber *oldLaunchCount = + [transaction objectForKey:kOWSAnalytics_KeyLaunchCount inCollection:kOWSAnalytics_Collection]; + NSNumber *newLaunchCount = @(oldLaunchCount.longValue + 1); + self.launchCount = newLaunchCount; + + NSNumber *oldLaunchCompleteCount = + [transaction objectForKey:kOWSAnalytics_KeyLaunchCompleteCount inCollection:kOWSAnalytics_Collection]; + self.launchCompleteCount = @(oldLaunchCompleteCount.longValue); + }]; + [TSStorageManager.sharedManager.newDatabaseConnection + asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { + [transaction setObject:self.launchCount + forKey:kOWSAnalytics_KeyLaunchCount + inCollection:kOWSAnalytics_Collection]; + }]; +} + ++ (void)appLaunchDidComplete +{ + [self.sharedInstance appLaunchDidComplete]; +} + +- (void)appLaunchDidComplete +{ + OWSProdInfo(@"app_launch_complete"); + + self.launchCompleteCount = @(self.launchCompleteCount.longValue + 1); + + [TSStorageManager.sharedManager.newDatabaseConnection + asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { + [transaction setObject:self.launchCompleteCount + forKey:kOWSAnalytics_KeyLaunchCompleteCount + inCollection:kOWSAnalytics_Collection]; + }]; +} + +#pragma mark - Logging + ++ (NSString *)tag +{ + return [NSString stringWithFormat:@"[%@]", self.class]; +} + +- (NSString *)tag +{ + return self.class.tag; } @end + +NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/tests/Util/OWSAnalyticsTests.m b/SignalServiceKit/tests/Util/OWSAnalyticsTests.m new file mode 100644 index 000000000..8a13136e1 --- /dev/null +++ b/SignalServiceKit/tests/Util/OWSAnalyticsTests.m @@ -0,0 +1,49 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + +#import "NSData+Base64.h" +#import "OWSAnalytics.h" +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface OWSAnalyticsTests : XCTestCase + +@end + +@interface OWSAnalytics (Test) + ++ (long)orderOfMagnitudeOf:(long)value; + +@end + +@implementation OWSAnalyticsTests + +- (void)testOrderOfMagnitudeOf +{ + XCTAssertEqual(0, [OWSAnalytics orderOfMagnitudeOf:-1]); + XCTAssertEqual(0, [OWSAnalytics orderOfMagnitudeOf:0]); + XCTAssertEqual(1, [OWSAnalytics orderOfMagnitudeOf:1]); + XCTAssertEqual(1, [OWSAnalytics orderOfMagnitudeOf:5]); + XCTAssertEqual(1, [OWSAnalytics orderOfMagnitudeOf:9]); + XCTAssertEqual(10, [OWSAnalytics orderOfMagnitudeOf:10]); + XCTAssertEqual(10, [OWSAnalytics orderOfMagnitudeOf:11]); + XCTAssertEqual(10, [OWSAnalytics orderOfMagnitudeOf:19]); + XCTAssertEqual(10, [OWSAnalytics orderOfMagnitudeOf:99]); + XCTAssertEqual(100, [OWSAnalytics orderOfMagnitudeOf:100]); + XCTAssertEqual(100, [OWSAnalytics orderOfMagnitudeOf:303]); + XCTAssertEqual(100, [OWSAnalytics orderOfMagnitudeOf:999]); + XCTAssertEqual(1000, [OWSAnalytics orderOfMagnitudeOf:1000]); + XCTAssertEqual(1000, [OWSAnalytics orderOfMagnitudeOf:3030]); + XCTAssertEqual(10000, [OWSAnalytics orderOfMagnitudeOf:10000]); + XCTAssertEqual(10000, [OWSAnalytics orderOfMagnitudeOf:30303]); + XCTAssertEqual(10000, [OWSAnalytics orderOfMagnitudeOf:99999]); + XCTAssertEqual(100000, [OWSAnalytics orderOfMagnitudeOf:100000]); + XCTAssertEqual(100000, [OWSAnalytics orderOfMagnitudeOf:303030]); + XCTAssertEqual(100000, [OWSAnalytics orderOfMagnitudeOf:999999]); +} + +@end + +NS_ASSUME_NONNULL_END