From 1bfe6918951c95158a74e8d0afd085d1b764b987 Mon Sep 17 00:00:00 2001 From: Michael Kirk Date: Fri, 18 Jan 2019 10:54:09 -0700 Subject: [PATCH] In app notifications for iOS10+ Extract shared notification presention/response Implement adapters which use that logic for modern UNUserNotification and legacy UINotifications --- Signal.xcodeproj/project.pbxproj | 50 +- Signal/src/AppDelegate.m | 118 ++- Signal/src/Jobs/MessageFetcherJob.swift | 5 +- Signal/src/Models/AccountManager.swift | 5 - Signal/src/Signal-Bridging-Header.h | 5 +- .../Notifications/AppNotifications.swift | 672 ++++++++++++++++++ .../LegacyNotificationsAdaptee.swift | 290 ++++++++ .../Notifications/NotificationsAdapter.swift | 61 -- .../UserNotificationsAdaptee.swift | 383 ++++++---- .../AdvancedSettingsTableViewController.m | 3 +- .../AppSettings/AppSettingsViewController.m | 1 - .../ConversationInputToolbar.m | 4 +- .../DebugUI/DebugUINotifications.swift | 18 +- .../HomeView/HomeViewController.m | 5 +- Signal/src/call/CallService.swift | 12 +- Signal/src/call/NonCallKitCallUIAdaptee.swift | 10 +- .../Speakerbox/CallKitCallUIAdaptee.swift | 6 +- .../call/UserInterface/CallUIAdapter.swift | 14 +- .../OWSCallNotificationsAdaptee.h | 26 - Signal/src/environment/AppEnvironment.swift | 10 +- Signal/src/environment/NotificationsManager.h | 26 - Signal/src/environment/NotificationsManager.m | 505 ------------- .../environment/PushRegistrationManager.swift | 51 +- Signal/src/network/PushManager.h | 75 -- Signal/src/network/PushManager.m | 504 ------------- Signal/test/push/PushManagerTest.m | 33 - SignalMessaging/Views/CommonStrings.swift | 21 +- SignalServiceKit/src/SignalServiceKit.h | 3 +- .../src/TestUtils/Factories.swift | 20 +- .../src/TestUtils/MockSSKEnvironment.m | 5 +- .../TestUtils}/NoopNotificationsManager.swift | 4 +- .../TestUtils/OWSFakeNotificationsManager.h | 17 - .../TestUtils/OWSFakeNotificationsManager.m | 39 - 33 files changed, 1395 insertions(+), 1606 deletions(-) create mode 100644 Signal/src/UserInterface/Notifications/AppNotifications.swift create mode 100644 Signal/src/UserInterface/Notifications/LegacyNotificationsAdaptee.swift delete mode 100644 Signal/src/UserInterface/Notifications/NotificationsAdapter.swift delete mode 100644 Signal/src/call/UserInterface/OWSCallNotificationsAdaptee.h delete mode 100644 Signal/src/environment/NotificationsManager.h delete mode 100644 Signal/src/environment/NotificationsManager.m delete mode 100644 Signal/src/network/PushManager.h delete mode 100644 Signal/src/network/PushManager.m delete mode 100644 Signal/test/push/PushManagerTest.m rename {SignalMessaging/environment => SignalServiceKit/src/TestUtils}/NoopNotificationsManager.swift (89%) delete mode 100644 SignalServiceKit/src/TestUtils/OWSFakeNotificationsManager.h delete mode 100644 SignalServiceKit/src/TestUtils/OWSFakeNotificationsManager.m diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj index c82e49cfe..a292ebe16 100644 --- a/Signal.xcodeproj/project.pbxproj +++ b/Signal.xcodeproj/project.pbxproj @@ -85,7 +85,6 @@ 346129951FD1E30000532771 /* OWSDatabaseMigration.h in Headers */ = {isa = PBXBuildFile; fileRef = 346129931FD1E30000532771 /* OWSDatabaseMigration.h */; settings = {ATTRIBUTES = (Public, ); }; }; 346129961FD1E30000532771 /* OWSDatabaseMigration.m in Sources */ = {isa = PBXBuildFile; fileRef = 346129941FD1E30000532771 /* OWSDatabaseMigration.m */; }; 346129991FD1E4DA00532771 /* SignalApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 346129971FD1E4D900532771 /* SignalApp.m */; }; - 3461299C1FD1EA9E00532771 /* NotificationsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 3461299B1FD1EA9E00532771 /* NotificationsManager.m */; }; 346129A51FD1F09100532771 /* OWSContactsManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 346129A21FD1F09100532771 /* OWSContactsManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; 346129A61FD1F09100532771 /* OWSContactsManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 346129A31FD1F09100532771 /* OWSContactsManager.m */; }; 346129A91FD1F0E000532771 /* OWSFormat.h in Headers */ = {isa = PBXBuildFile; fileRef = 346129A81FD1F0DF00532771 /* OWSFormat.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -137,7 +136,6 @@ 347850691FD9B78A007B8332 /* AppSetup.m in Sources */ = {isa = PBXBuildFile; fileRef = 347850651FD9B789007B8332 /* AppSetup.m */; }; 3478506A1FD9B78A007B8332 /* AppSetup.h in Headers */ = {isa = PBXBuildFile; fileRef = 347850661FD9B789007B8332 /* AppSetup.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3478506B1FD9B78A007B8332 /* NoopCallMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347850671FD9B78A007B8332 /* NoopCallMessageHandler.swift */; }; - 3478506C1FD9B78A007B8332 /* NoopNotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347850681FD9B78A007B8332 /* NoopNotificationsManager.swift */; }; 347850711FDAEB17007B8332 /* OWSUserProfile.m in Sources */ = {isa = PBXBuildFile; fileRef = 3478506F1FDAEB16007B8332 /* OWSUserProfile.m */; }; 347850721FDAEB17007B8332 /* OWSUserProfile.h in Headers */ = {isa = PBXBuildFile; fileRef = 347850701FDAEB16007B8332 /* OWSUserProfile.h */; settings = {ATTRIBUTES = (Public, ); }; }; 34843B2421432293004DED45 /* SignalBaseTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 34843B2221432292004DED45 /* SignalBaseTest.m */; }; @@ -321,7 +319,7 @@ 45194F931FD7215C00333B2C /* OWSContactOffersInteraction.m in Sources */ = {isa = PBXBuildFile; fileRef = 34C42D631F4734ED0072EC04 /* OWSContactOffersInteraction.m */; }; 45194F941FD7216000333B2C /* TSUnreadIndicatorInteraction.h in Headers */ = {isa = PBXBuildFile; fileRef = 34C42D641F4734ED0072EC04 /* TSUnreadIndicatorInteraction.h */; settings = {ATTRIBUTES = (Public, ); }; }; 45194F951FD7216600333B2C /* TSUnreadIndicatorInteraction.m in Sources */ = {isa = PBXBuildFile; fileRef = 34C42D651F4734ED0072EC04 /* TSUnreadIndicatorInteraction.m */; }; - 451A13B11E13DED2000A50FD /* NotificationsAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451A13B01E13DED2000A50FD /* NotificationsAdapter.swift */; }; + 451A13B11E13DED2000A50FD /* AppNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451A13B01E13DED2000A50FD /* AppNotifications.swift */; }; 451F8A341FD710C3005CB9DA /* ConversationSearcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 451777C71FD61554001225FF /* ConversationSearcher.swift */; }; 451F8A351FD710DE005CB9DA /* Searcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45360B8C1F9521F800FA666C /* Searcher.swift */; }; 451F8A3B1FD71297005CB9DA /* UIUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = B97940261832BD2400BD66CB /* UIUtil.m */; }; @@ -474,6 +472,7 @@ 4CC1ECF9211A47CE00CC13BE /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4CC1ECF8211A47CD00CC13BE /* StoreKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 4CC1ECFB211A553000CC13BE /* AppUpdateNag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC1ECFA211A553000CC13BE /* AppUpdateNag.swift */; }; 4CEB78C92178EBAB00F315D2 /* OWSSessionResetJobRecord.m in Sources */ = {isa = PBXBuildFile; fileRef = 4CEB78C82178EBAB00F315D2 /* OWSSessionResetJobRecord.m */; }; + 4CFE6B6C21F92BA700006701 /* LegacyNotificationsAdaptee.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFE6B6B21F92BA700006701 /* LegacyNotificationsAdaptee.swift */; }; 70377AAB1918450100CAF501 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 70377AAA1918450100CAF501 /* MobileCoreServices.framework */; }; 768A1A2B17FC9CD300E00ED8 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 768A1A2A17FC9CD300E00ED8 /* libz.dylib */; }; 76C87F19181EFCE600C4ACAB /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */; }; @@ -510,7 +509,6 @@ B633C5C41A1D190B0059AC12 /* mute_on@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = B633C5421A1D190B0059AC12 /* mute_on@2x.png */; }; B633C5CE1A1D190B0059AC12 /* quit@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = B633C54C1A1D190B0059AC12 /* quit@2x.png */; }; B633C5D21A1D190B0059AC12 /* savephoto@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = B633C5501A1D190B0059AC12 /* savephoto@2x.png */; }; - B660F6D21C29868000687D6E /* PushManagerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B660F69C1C29868000687D6E /* PushManagerTest.m */; }; B660F6D41C29868000687D6E /* whisperFake.cer in Resources */ = {isa = PBXBuildFile; fileRef = B660F69F1C29868000687D6E /* whisperFake.cer */; }; B660F6DB1C29868000687D6E /* FunctionalUtilTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B660F6AD1C29868000687D6E /* FunctionalUtilTest.m */; }; B660F6E01C29868000687D6E /* UtilTest.m in Sources */ = {isa = PBXBuildFile; fileRef = B660F6B41C29868000687D6E /* UtilTest.m */; }; @@ -518,7 +516,6 @@ B67EBF5D19194AC60084CCFD /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = B67EBF5C19194AC60084CCFD /* Settings.bundle */; }; B69CD25119773E79005CE69A /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B69CD25019773E79005CE69A /* XCTest.framework */; }; B6B226971BE4B7D200860F4D /* ContactsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6B226961BE4B7D200860F4D /* ContactsUI.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; - B6B9ECFC198B31BA00C620D3 /* PushManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B6B9ECFB198B31BA00C620D3 /* PushManager.m */; }; B6F509971AA53F760068F56A /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B6F509951AA53F760068F56A /* Localizable.strings */; }; B6FE7EB71ADD62FA00A6D22F /* PushKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B6FE7EB61ADD62FA00A6D22F /* PushKit.framework */; }; B90418E6183E9DD40038554A /* DateUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = B90418E5183E9DD40038554A /* DateUtil.m */; }; @@ -739,8 +736,6 @@ 346129941FD1E30000532771 /* OWSDatabaseMigration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSDatabaseMigration.m; sourceTree = ""; }; 346129971FD1E4D900532771 /* SignalApp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SignalApp.m; sourceTree = ""; }; 346129981FD1E4DA00532771 /* SignalApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SignalApp.h; sourceTree = ""; }; - 3461299A1FD1EA9E00532771 /* NotificationsManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NotificationsManager.h; sourceTree = ""; }; - 3461299B1FD1EA9E00532771 /* NotificationsManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NotificationsManager.m; sourceTree = ""; }; 346129A21FD1F09100532771 /* OWSContactsManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSContactsManager.h; sourceTree = ""; }; 346129A31FD1F09100532771 /* OWSContactsManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSContactsManager.m; sourceTree = ""; }; 346129A81FD1F0DF00532771 /* OWSFormat.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSFormat.h; sourceTree = ""; }; @@ -794,7 +789,6 @@ 347850651FD9B789007B8332 /* AppSetup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppSetup.m; sourceTree = ""; }; 347850661FD9B789007B8332 /* AppSetup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppSetup.h; sourceTree = ""; }; 347850671FD9B78A007B8332 /* NoopCallMessageHandler.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoopCallMessageHandler.swift; sourceTree = ""; }; - 347850681FD9B78A007B8332 /* NoopNotificationsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NoopNotificationsManager.swift; sourceTree = ""; }; 3478506F1FDAEB16007B8332 /* OWSUserProfile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSUserProfile.m; sourceTree = ""; }; 347850701FDAEB16007B8332 /* OWSUserProfile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSUserProfile.h; sourceTree = ""; }; 34843B2221432292004DED45 /* SignalBaseTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SignalBaseTest.m; sourceTree = ""; }; @@ -1028,7 +1022,7 @@ 451166BF1FD86B98000739BA /* AccountManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountManager.swift; sourceTree = ""; }; 451764291DE939FD00EDB8B9 /* ContactCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactCell.swift; sourceTree = ""; }; 451777C71FD61554001225FF /* ConversationSearcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationSearcher.swift; sourceTree = ""; }; - 451A13B01E13DED2000A50FD /* NotificationsAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = NotificationsAdapter.swift; path = UserInterface/Notifications/NotificationsAdapter.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 451A13B01E13DED2000A50FD /* AppNotifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; lineEnding = 0; name = AppNotifications.swift; path = UserInterface/Notifications/AppNotifications.swift; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; 452037CF1EE84975004E4CDF /* DebugUISessionState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DebugUISessionState.h; sourceTree = ""; }; 452037D01EE84975004E4CDF /* DebugUISessionState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DebugUISessionState.m; sourceTree = ""; }; 4520D8D41D417D8E00123472 /* Photos.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Photos.framework; path = System/Library/Frameworks/Photos.framework; sourceTree = SDKROOT; }; @@ -1144,7 +1138,6 @@ 45DF5DF11DDB843F00C936C7 /* CompareSafetyNumbersActivity.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompareSafetyNumbersActivity.swift; sourceTree = ""; }; 45E282DE1D08E67800ADD4C8 /* gl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = gl; path = translations/gl.lproj/Localizable.strings; sourceTree = ""; }; 45E282DF1D08E6CC00ADD4C8 /* id */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = id; path = translations/id.lproj/Localizable.strings; sourceTree = ""; }; - 45E2E91E1E13EE3500457AA0 /* OWSCallNotificationsAdaptee.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; name = OWSCallNotificationsAdaptee.h; path = UserInterface/OWSCallNotificationsAdaptee.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; 45E5A6981F61E6DD001E4A8A /* MarqueeLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MarqueeLabel.swift; sourceTree = ""; }; 45E7A6A61E71CA7E00D44FB5 /* DisplayableTextFilterTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DisplayableTextFilterTest.swift; sourceTree = ""; }; 45F170AB1E2F0351003FC1F2 /* OWSAudioSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSAudioSession.swift; sourceTree = ""; }; @@ -1197,6 +1190,7 @@ 4CC1ECFA211A553000CC13BE /* AppUpdateNag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdateNag.swift; sourceTree = ""; }; 4CEB78C72178EBAB00F315D2 /* OWSSessionResetJobRecord.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OWSSessionResetJobRecord.h; sourceTree = ""; }; 4CEB78C82178EBAB00F315D2 /* OWSSessionResetJobRecord.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OWSSessionResetJobRecord.m; sourceTree = ""; }; + 4CFE6B6B21F92BA700006701 /* LegacyNotificationsAdaptee.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LegacyNotificationsAdaptee.swift; path = UserInterface/Notifications/LegacyNotificationsAdaptee.swift; sourceTree = ""; }; 4CFF4C0920F55BBA005DA313 /* MenuActionsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuActionsViewController.swift; sourceTree = ""; }; 69349DE607F5BA6036C9AC60 /* Pods-SignalShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SignalShareExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SignalShareExtension/Pods-SignalShareExtension.debug.xcconfig"; sourceTree = ""; }; 70377AAA1918450100CAF501 /* MobileCoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MobileCoreServices.framework; path = System/Library/Frameworks/MobileCoreServices.framework; sourceTree = SDKROOT; }; @@ -1244,7 +1238,6 @@ B646D10F1AA54626004133BA /* fil */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fil; path = translations/fil.lproj/Localizable.strings; sourceTree = ""; }; B646D1141AA54674004133BA /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = translations/hu.lproj/Localizable.strings; sourceTree = ""; }; B657DDC91911A40500F45B0C /* Signal.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = Signal.entitlements; sourceTree = ""; }; - B660F69C1C29868000687D6E /* PushManagerTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PushManagerTest.m; sourceTree = ""; }; B660F69E1C29868000687D6E /* SignalTests-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "SignalTests-Info.plist"; sourceTree = ""; }; B660F69F1C29868000687D6E /* whisperFake.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = whisperFake.cer; sourceTree = ""; }; B660F6A01C29868000687D6E /* TestUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TestUtil.h; sourceTree = ""; }; @@ -1269,8 +1262,6 @@ B69C2D1B1AA5448300A640C2 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = translations/cs.lproj/Localizable.strings; sourceTree = ""; }; B69CD25019773E79005CE69A /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; B6B226961BE4B7D200860F4D /* ContactsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ContactsUI.framework; path = System/Library/Frameworks/ContactsUI.framework; sourceTree = SDKROOT; }; - B6B9ECFA198B31BA00C620D3 /* PushManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = PushManager.h; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objcpp; }; - B6B9ECFB198B31BA00C620D3 /* PushManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; lineEnding = 0; path = PushManager.m; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; B6BC3D0C1AA544B100C2907F /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = translations/da.lproj/Localizable.strings; sourceTree = ""; }; B6F509961AA53F760068F56A /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = translations/en.lproj/Localizable.strings; sourceTree = ""; }; B6FE7EB61ADD62FA00A6D22F /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = System/Library/Frameworks/PushKit.framework; sourceTree = SDKROOT; }; @@ -1641,7 +1632,6 @@ 346129411FD1D74B00532771 /* Environment.m */, 346129921FD1E30000532771 /* migrations */, 347850671FD9B78A007B8332 /* NoopCallMessageHandler.swift */, - 347850681FD9B78A007B8332 /* NoopNotificationsManager.swift */, 45F170AB1E2F0351003FC1F2 /* OWSAudioSession.swift */, 34074F60203D0CBE004596AE /* OWSSounds.h */, 34074F5F203D0CBD004596AE /* OWSSounds.m */, @@ -1800,13 +1790,6 @@ path = Backup; sourceTree = ""; }; - 3496957521A301A300DCFE74 /* New Group */ = { - isa = PBXGroup; - children = ( - ); - path = "New Group"; - sourceTree = ""; - }; 34B3F8331E8DF1700035BE1A /* ViewControllers */ = { isa = PBXGroup; children = ( @@ -2040,8 +2023,9 @@ 450DF2071E0DD29E003D14BE /* Notifications */ = { isa = PBXGroup; children = ( - 451A13B01E13DED2000A50FD /* NotificationsAdapter.swift */, + 451A13B01E13DED2000A50FD /* AppNotifications.swift */, 450DF2081E0DD2C6003D14BE /* UserNotificationsAdaptee.swift */, + 4CFE6B6B21F92BA700006701 /* LegacyNotificationsAdaptee.swift */, ); name = Notifications; sourceTree = ""; @@ -2168,7 +2152,6 @@ isa = PBXGroup; children = ( 45FBC57A1DF8575700E9B410 /* CallKit */, - 45E2E91E1E13EE3500457AA0 /* OWSCallNotificationsAdaptee.h */, 45794E851E00620000066731 /* CallUIAdapter.swift */, 45F659811E1BE77000444429 /* NonCallKitCallUIAdaptee.swift */, ); @@ -2279,8 +2262,6 @@ children = ( 34D99CE3217509C1000AFB39 /* AppEnvironment.swift */, 4505C2BD1E648E6E00CEBF41 /* ExperienceUpgrades */, - 3461299A1FD1EA9E00532771 /* NotificationsManager.h */, - 3461299B1FD1EA9E00532771 /* NotificationsManager.m */, 4539B5851F79348F007141FF /* PushRegistrationManager.swift */, 346129981FD1E4DA00532771 /* SignalApp.h */, 346129971FD1E4D900532771 /* SignalApp.m */, @@ -2293,8 +2274,6 @@ children = ( 3430FE171F7751D4000EC51B /* GiphyAPI.swift */, 34D1F0511F7E8EA30066283D /* GiphyDownloader.swift */, - B6B9ECFA198B31BA00C620D3 /* PushManager.h */, - B6B9ECFB198B31BA00C620D3 /* PushManager.m */, ); path = network; sourceTree = ""; @@ -2302,7 +2281,6 @@ 76EB04C818170B33006006FC /* util */ = { isa = PBXGroup; children = ( - 3496957521A301A300DCFE74 /* New Group */, 4CC1ECFA211A553000CC13BE /* AppUpdateNag.swift */, B90418E4183E9DD40038554A /* DateUtil.h */, 3496956121A301A100DCFE74 /* Backup */, @@ -2415,7 +2393,6 @@ B660F6751C29867F00687D6E /* contact */, 34843B29214FE295004DED45 /* mocks */, 458E38381D6699110094BD24 /* Models */, - B660F69B1C29868000687D6E /* push */, 34843B2321432293004DED45 /* SignalBaseTest.h */, 34843B2221432292004DED45 /* SignalBaseTest.m */, 4589670F1DC117CC00E9DD21 /* SignalTests-Bridging-Header.h */, @@ -2445,14 +2422,6 @@ path = contact; sourceTree = ""; }; - B660F69B1C29868000687D6E /* push */ = { - isa = PBXGroup; - children = ( - B660F69C1C29868000687D6E /* PushManagerTest.m */, - ); - path = push; - sourceTree = ""; - }; B660F69D1C29868000687D6E /* Supporting Files */ = { isa = PBXGroup; children = ( @@ -3317,7 +3286,6 @@ 342950882124CB0A0000B063 /* OWSSearchBar.m in Sources */, 342950822124C9750000B063 /* OWSTextField.m in Sources */, 34AC0A13211B39EA00997B47 /* DisappearingTimerConfigurationView.swift in Sources */, - 3478506C1FD9B78A007B8332 /* NoopNotificationsManager.swift in Sources */, 4CA46F4D219CFDAA0038ABDE /* GalleryRailView.swift in Sources */, 34480B621FD0A98800BC14EF /* UIColor+OWS.m in Sources */, 4C20B2B720CA0034001BAC90 /* ThreadViewModel.swift in Sources */, @@ -3441,7 +3409,7 @@ 454A84042059C787008B8C75 /* MediaTileViewController.swift in Sources */, 340FC8B4204DAC8D007AEB0F /* OWSBackupSettingsViewController.m in Sources */, 34D1F0871F8678AA0066283D /* ConversationViewItem.m in Sources */, - 451A13B11E13DED2000A50FD /* NotificationsAdapter.swift in Sources */, + 451A13B11E13DED2000A50FD /* AppNotifications.swift in Sources */, 34D99CE4217509C2000AFB39 /* AppEnvironment.swift in Sources */, 348570A820F67575004FF32B /* OWSMessageHeaderView.m in Sources */, 450DF2091E0DD2C6003D14BE /* UserNotificationsAdaptee.swift in Sources */, @@ -3460,7 +3428,6 @@ 4585C4681ED8F8D200896AEA /* SafetyNumberConfirmationAlert.swift in Sources */, 4C20B2B920CA10DE001BAC90 /* ConversationSearchViewController.swift in Sources */, 450D19131F85236600970622 /* RemoteVideoView.m in Sources */, - B6B9ECFC198B31BA00C620D3 /* PushManager.m in Sources */, 34129B8621EF877A005457A8 /* LinkPreviewView.swift in Sources */, 34386A54207D271D009F5D9C /* NeverClearView.swift in Sources */, 45DF5DF21DDB843F00C936C7 /* CompareSafetyNumbersActivity.swift in Sources */, @@ -3542,6 +3509,7 @@ 45D308AD2049A439000189E4 /* PinEntryView.m in Sources */, 340FC8B1204DAC8D007AEB0F /* BlockListViewController.m in Sources */, 45B5360E206DD8BB00D61655 /* UIResponder+OWS.swift in Sources */, + 4CFE6B6C21F92BA700006701 /* LegacyNotificationsAdaptee.swift in Sources */, 3441FD9F21A3604F00BB9542 /* BackupRestoreViewController.swift in Sources */, 45F659821E1BE77000444429 /* NonCallKitCallUIAdaptee.swift in Sources */, 4C5250D221E7BD7D00CE3D95 /* PhoneNumberValidator.swift in Sources */, @@ -3586,7 +3554,6 @@ 340FC8B9204DAC8D007AEB0F /* UpdateGroupViewController.m in Sources */, 4574A5D61DD6704700C6B692 /* CallService.swift in Sources */, 340FC8A8204DAC8D007AEB0F /* CodeVerificationViewController.m in Sources */, - 3461299C1FD1EA9E00532771 /* NotificationsManager.m in Sources */, 4521C3C01F59F3BA00B4C582 /* TextFieldHelper.swift in Sources */, 34D2CCDF206939B400CB1A14 /* DebugUIMessagesAction.m in Sources */, 340FC8AC204DAC8D007AEB0F /* PrivacySettingsTableViewController.m in Sources */, @@ -3641,7 +3608,6 @@ 4C5250D421E7C51900CE3D95 /* PhoneNumberValidatorTest.swift in Sources */, 452D1AF12081059C00A67F7F /* StringAdditionsTest.swift in Sources */, 4C4BC6C32102D697004040C9 /* ContactDiscoveryOperationTest.swift in Sources */, - B660F6D21C29868000687D6E /* PushManagerTest.m in Sources */, 455AC69E1F4F8B0300134004 /* ImageCacheTest.swift in Sources */, 34E8A8D12085238A00B272B1 /* ProtoParsingTest.m in Sources */, ); diff --git a/Signal/src/AppDelegate.m b/Signal/src/AppDelegate.m index 635a425b1..af258f619 100644 --- a/Signal/src/AppDelegate.m +++ b/Signal/src/AppDelegate.m @@ -12,7 +12,6 @@ #import "OWSOrphanDataCleaner.h" #import "OWSScreenLockUI.h" #import "Pastelog.h" -#import "PushManager.h" #import "RegistrationViewController.h" #import "Signal-Swift.h" #import "SignalApp.h" @@ -61,7 +60,7 @@ static NSString *const kURLHostVerifyPrefix = @"verify"; static NSTimeInterval launchStartedAt; -@interface AppDelegate () +@interface AppDelegate () @property (nonatomic) BOOL hasInitialRootViewController; @property (nonatomic) BOOL areVersionMigrationsComplete; @@ -175,6 +174,11 @@ static NSTimeInterval launchStartedAt; return AppEnvironment.shared.backup; } +- (OWSNotificationPresenter *)notificationPresenter +{ + return AppEnvironment.shared.notificationPresenter; +} + #pragma mark - - (void)applicationDidEnterBackground:(UIApplication *)application { @@ -285,6 +289,14 @@ static NSTimeInterval launchStartedAt; mainWindow.rootViewController = [LoadingViewController new]; [mainWindow makeKeyAndVisible]; + if (@available(iOS 11, *)) { + // This must happen in appDidFinishLaunching or earlier to ensure we don't + // miss notifications. + // Setting the delegate also seems to prevent us from getting the legacy notification + // notification callbacks upon launch e.g. 'didReceiveLocalNotification' + UNUserNotificationCenter.currentNotificationCenter.delegate = self; + } + // Accept push notification when app is not open NSDictionary *remoteNotif = launchOptions[UIApplicationLaunchOptionsRemoteNotificationKey]; if (remoteNotif) { @@ -580,8 +592,8 @@ static NSTimeInterval launchStartedAt; return; } - OWSLogInfo(@"registered user notification settings"); - [self.pushRegistrationManager didRegisterUserNotificationSettings]; + OWSLogInfo(@"registered legacy notification settings"); + [self.notificationPresenter didRegisterLegacyNotificationSettings]; } - (BOOL)application:(UIApplication *)application @@ -648,11 +660,6 @@ static NSTimeInterval launchStartedAt; [self handleActivation]; }]; - // There is a sequence of actions a user can take where we present a conversation from a notification - // multiple times, producing an undesirable "stack" of multiple conversation view controllers. - // So we ensure that we only present conversations once per activate. - [PushManager sharedManager].hasPresentedConversationSinceLastDeactivation = NO; - // Clear all notifications whenever we become active. // When opening the app from a notification, // AppDelegate.didReceiveLocalNotification will always @@ -733,8 +740,7 @@ static NSTimeInterval launchStartedAt; dispatch_async(dispatch_get_main_queue(), ^{ [self.socketManager requestSocketOpen]; [Environment.shared.contactsManager fetchSystemContactsOnceIfAlreadyAuthorized]; - // This will fetch new messages, if we're using domain fronting. - [[PushManager sharedManager] applicationDidBecomeActive]; + [[AppEnvironment.shared.messageFetcherJob run] retainUntilComplete]; if (![UIApplication sharedApplication].isRegisteredForRemoteNotifications) { OWSLogInfo(@"Retrying to register for remote notifications since user hasn't registered yet."); @@ -1111,8 +1117,9 @@ static NSTimeInterval launchStartedAt; return; } - // It is safe to continue even if the app isn't ready. - [[PushManager sharedManager] application:application didReceiveRemoteNotification:userInfo]; + [AppReadiness runNowOrWhenAppDidBecomeReady:^{ + [[AppEnvironment.shared.messageFetcherJob run] retainUntilComplete]; + }]; } - (void)application:(UIApplication *)application @@ -1129,10 +1136,12 @@ static NSTimeInterval launchStartedAt; return; } - // It is safe to continue even if the app isn't ready. - [[PushManager sharedManager] application:application - didReceiveRemoteNotification:userInfo - fetchCompletionHandler:completionHandler]; + [AppReadiness runNowOrWhenAppDidBecomeReady:^{ + [[AppEnvironment.shared.messageFetcherJob run] retainUntilComplete]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 20 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ + completionHandler(UIBackgroundFetchResultNewData); + }); + }]; } - (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification { @@ -1150,7 +1159,12 @@ static NSTimeInterval launchStartedAt; return; } - [[PushManager sharedManager] application:application didReceiveLocalNotification:notification]; + [LegacyNotificationActionHandler.shared + handleNotificationResponseWithActionIdentifier:LegacyNotificationActionHandler.kDefaultActionIdentifier + notification:notification + responseInfo:@{} + completionHandler:^{ + }]; }]; } @@ -1180,10 +1194,10 @@ static NSTimeInterval launchStartedAt; return; } - [[PushManager sharedManager] application:application - handleActionWithIdentifier:identifier - forLocalNotification:notification - completionHandler:completionHandler]; + [LegacyNotificationActionHandler.shared handleNotificationResponseWithActionIdentifier:identifier + notification:notification + responseInfo:@{} + completionHandler:completionHandler]; }]; } @@ -1216,11 +1230,10 @@ static NSTimeInterval launchStartedAt; return; } - [[PushManager sharedManager] application:application - handleActionWithIdentifier:identifier - forLocalNotification:notification - withResponseInfo:responseInfo - completionHandler:completionHandler]; + [LegacyNotificationActionHandler.shared handleNotificationResponseWithActionIdentifier:identifier + notification:notification + responseInfo:responseInfo + completionHandler:completionHandler]; }]; } @@ -1243,6 +1256,7 @@ static NSTimeInterval launchStartedAt; job = nil; }); }); + [job retainUntilComplete]; }]; } @@ -1301,7 +1315,7 @@ static NSTimeInterval launchStartedAt; // Fetch messages as soon as possible after launching. In particular, when // launching from the background, without this, we end up waiting some extra // seconds before receiving an actionable push notification. - __unused AnyPromise *messagePromise = [AppEnvironment.shared.messageFetcherJob run]; + [[AppEnvironment.shared.messageFetcherJob run] retainUntilComplete]; // This should happen at any launch, background or foreground. __unused AnyPromise *pushTokenpromise = @@ -1481,4 +1495,52 @@ static NSTimeInterval launchStartedAt; } } +#pragma mark - UNUserNotificationsDelegate + +// The method will be called on the delegate only if the application is in the foreground. If the method is not +// implemented or the handler is not called in a timely manner then the notification will not be presented. The +// application can choose to have the notification presented as a sound, badge, alert and/or in the notification list. +// This decision should be based on whether the information in the notification is otherwise visible to the user. +- (void)userNotificationCenter:(UNUserNotificationCenter *)center + willPresentNotification:(UNNotification *)notification + withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler + __IOS_AVAILABLE(10.0)__TVOS_AVAILABLE(10.0)__WATCHOS_AVAILABLE(3.0)__OSX_AVAILABLE(10.14) +{ + OWSLogInfo(@""); + [AppReadiness runNowOrWhenAppDidBecomeReady:^() { + // TODO move this into adaptee ? + // we need to respect the in-app notification sound settings, either here + // or maybe it's simpler to do that when building the notification. e.g. if the app + // is in the forground when the notification was sent, we could just *not* add the sound. + UNNotificationPresentationOptions options = UNNotificationPresentationOptionAlert + | UNNotificationPresentationOptionBadge | UNNotificationPresentationOptionSound; + completionHandler(options); + }]; +} + +// The method will be called on the delegate when the user responded to the notification by opening the application, +// dismissing the notification or choosing a UNNotificationAction. The delegate must be set before the application +// returns from application:didFinishLaunchingWithOptions:. +- (void)userNotificationCenter:(UNUserNotificationCenter *)center + didReceiveNotificationResponse:(UNNotificationResponse *)response + withCompletionHandler:(void (^)(void))completionHandler __IOS_AVAILABLE(10.0)__WATCHOS_AVAILABLE(3.0) + __OSX_AVAILABLE(10.14)__TVOS_PROHIBITED +{ + OWSLogInfo(@""); + [AppReadiness runNowOrWhenAppDidBecomeReady:^() { + [UserNotificationActionHandler.shared handleNotificationResponse:response completionHandler:completionHandler]; + }]; +} + +// The method will be called on the delegate when the application is launched in response to the user's request to view +// in-app notification settings. Add UNAuthorizationOptionProvidesAppNotificationSettings as an option in +// requestAuthorizationWithOptions:completionHandler: to add a button to inline notification settings view and the +// notification settings view in Settings. The notification will be nil when opened from Settings. +- (void)userNotificationCenter:(UNUserNotificationCenter *)center + openSettingsForNotification:(nullable UNNotification *)notification __IOS_AVAILABLE(12.0) + __OSX_AVAILABLE(10.14)__WATCHOS_PROHIBITED __TVOS_PROHIBITED +{ + OWSLogInfo(@""); +} + @end diff --git a/Signal/src/Jobs/MessageFetcherJob.swift b/Signal/src/Jobs/MessageFetcherJob.swift index 6fd5c4e92..ed8819cb6 100644 --- a/Signal/src/Jobs/MessageFetcherJob.swift +++ b/Signal/src/Jobs/MessageFetcherJob.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // import Foundation @@ -28,8 +28,7 @@ public class MessageFetcherJob: NSObject { return SSKEnvironment.shared.messageReceiver } -private - var signalService: OWSSignalService { + private var signalService: OWSSignalService { return OWSSignalService.sharedInstance() } diff --git a/Signal/src/Models/AccountManager.swift b/Signal/src/Models/AccountManager.swift index 3a393de3f..eb7d36cad 100644 --- a/Signal/src/Models/AccountManager.swift +++ b/Signal/src/Models/AccountManager.swift @@ -15,11 +15,6 @@ public class AccountManager: NSObject { // MARK: - Dependencies - var pushManager: PushManager { - // dependency injection hack since PushManager has *alot* of dependencies, and would induce a cycle. - return PushManager.shared() - } - var profileManager: OWSProfileManager { return OWSProfileManager.shared() } diff --git a/Signal/src/Signal-Bridging-Header.h b/Signal/src/Signal-Bridging-Header.h index 325b378bf..a8f7d2804 100644 --- a/Signal/src/Signal-Bridging-Header.h +++ b/Signal/src/Signal-Bridging-Header.h @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import @@ -20,7 +20,6 @@ #import "HomeViewController.h" #import "MediaDetailViewController.h" #import "NotificationSettingsViewController.h" -#import "NotificationsManager.h" #import "OWSAddToContactViewController.h" #import "OWSAnyTouchGestureRecognizer.h" #import "OWSAudioMessageView.h" @@ -30,7 +29,6 @@ #import "OWSBezierPathView.h" #import "OWSBubbleShapeView.h" #import "OWSBubbleView.h" -#import "OWSCallNotificationsAdaptee.h" #import "OWSDatabaseMigration.h" #import "OWSMessageBubbleView.h" #import "OWSMessageCell.h" @@ -42,7 +40,6 @@ #import "PinEntryView.h" #import "PrivacySettingsTableViewController.h" #import "ProfileViewController.h" -#import "PushManager.h" #import "RegistrationViewController.h" #import "RemoteVideoView.h" #import "SignalApp.h" diff --git a/Signal/src/UserInterface/Notifications/AppNotifications.swift b/Signal/src/UserInterface/Notifications/AppNotifications.swift new file mode 100644 index 000000000..f9a498b25 --- /dev/null +++ b/Signal/src/UserInterface/Notifications/AppNotifications.swift @@ -0,0 +1,672 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import Foundation +import PromiseKit + +/// There are two primary components in our system notification integration: +/// +/// 1. The `NotificationPresenter` shows system notifications to the user. +/// 2. The `NotificationActionHandler` handles the users interactions with these +/// notifications. +/// +/// The NotificationPresenter is driven by the adapter pattern to provide a unified interface to +/// presenting notifications on iOS9, which uses UINotifications vs iOS10+ which supports +/// UNUserNotifications. +/// +/// The `NotificationActionHandler`s also need slightly different integrations for UINotifications +/// vs. UNUserNotifications, but because they are integrated at separate system defined callbacks, +/// there is no need for an Adapter, and instead the appropriate NotificationActionHandler is +/// wired directly into the appropriate callback point. + +enum AppNotificationCategory: CaseIterable { + case incomingMessage + case incomingMessageFromNoLongerVerifiedIdentity + case errorMessage + case threadlessErrorMessage + case incomingCall + case missedCall + case missedCallFromNoLongerVerifiedIdentity +} + +enum AppNotificationAction: CaseIterable { + case answerCall + case callBack + case declineCall + case markAsRead + case reply + case showThread +} + +struct AppNotificationUserInfoKey { + static let threadId = "Signal.AppNotificationsUserInfoKey.threadId" + static let callBackNumber = "Signal.AppNotificationsUserInfoKey.callBackNumber" + static let localCallId = "Signal.AppNotificationsUserInfoKey.localCallId" +} + +extension AppNotificationCategory { + var identifier: String { + switch self { + case .incomingMessage: + return "Signal.AppNotificationCategory.incomingMessage" + case .incomingMessageFromNoLongerVerifiedIdentity: + return "Signal.AppNotificationCategory.incomingMessageFromNoLongerVerifiedIdentity" + case .errorMessage: + return "Signal.AppNotificationCategory.errorMessage" + case .threadlessErrorMessage: + return "Signal.AppNotificationCategory.threadlessErrorMessage" + case .incomingCall: + return "Signal.AppNotificationCategory.incomingCall" + case .missedCall: + return "Signal.AppNotificationCategory.missedCall" + case .missedCallFromNoLongerVerifiedIdentity: + return "Signal.AppNotificationCategory.missedCallFromNoLongerVerifiedIdentity" + } + } + + var actions: [AppNotificationAction] { + switch self { + case .incomingMessage: + return [.markAsRead, .reply] + case .incomingMessageFromNoLongerVerifiedIdentity: + return [.markAsRead, .showThread] + case .errorMessage: + return [.showThread] + case .threadlessErrorMessage: + return [] + case .incomingCall: + return [.answerCall, .declineCall] + case .missedCall: + return [.callBack, .showThread] + case .missedCallFromNoLongerVerifiedIdentity: + return [.showThread] + } + } +} + +extension AppNotificationAction { + var identifier: String { + switch self { + case .answerCall: + return "Signal.AppNotifications.Action.answerCall" + case .callBack: + return "Signal.AppNotifications.Action.callBack" + case .declineCall: + return "Signal.AppNotifications.Action.declineCall" + case .markAsRead: + return "Signal.AppNotifications.Action.markAsRead" + case .reply: + return "Signal.AppNotifications.Action.reply" + case .showThread: + return "Signal.AppNotifications.Action.showThread" + } + } +} + +// Delay notification of incoming messages when it's likely to be read by a linked device to +// avoid notifying a user on their phone while a conversation is actively happening on desktop. +let kNotificationDelayForRemoteRead: TimeInterval = 5 + +protocol NotificationPresenterAdaptee: class { + + func registerNotificationSettings() -> Promise + + func notify(category: AppNotificationCategory, body: String, userInfo: [AnyHashable: Any], sound: OWSSound?) + func notify(category: AppNotificationCategory, body: String, userInfo: [AnyHashable: Any], sound: OWSSound?, replacingIdentifier: String?) + + func cancelNotifications(threadId: String) + func clearAllNotifications() + + var shouldPlaySoundForNotification: Bool { get } + var hasReceivedSyncMessageRecently: Bool { get } +} + +extension NotificationPresenterAdaptee { + var hasReceivedSyncMessageRecently: Bool { + return OWSDeviceManager.shared().hasReceivedSyncMessage(inLastSeconds: 60) + } +} + +@objc(OWSNotificationPresenter) +public class NotificationPresenter: NSObject, NotificationsProtocol { + + private let adaptee: NotificationPresenterAdaptee + + @objc + public override init() { + if #available(iOS 10, *) { + self.adaptee = UserNotificationPresenterAdaptee() + } else { + self.adaptee = LegacyNotificationPresenterAdaptee() + } + + super.init() + + AppReadiness.runNowOrWhenAppDidBecomeReady { + NotificationCenter.default.addObserver(self, selector: #selector(self.handleMessageRead), name: .incomingMessageMarkedAsRead, object: nil) + } + SwiftSingletons.register(self) + } + + // MARK: - Dependencies + + var identityManager: OWSIdentityManager { + return OWSIdentityManager.shared() + } + + var previewType: NotificationType { + return Environment.shared.preferences.notificationPreviewType() + } + + // MARK: - + + // It is not safe to assume push token requests will be acknowledged until the user has + // registered their notification settings. + // + // e.g. in the case that Background Fetch is disabled, token requests will be ignored until + // we register user notification settings. + // + // For modern UNUserNotificationSettings, the registration takes a callback, so "waiting" for + // notification settings registration is straight forward, however for legacy UIUserNotification + // settings, the settings request is confirmed in the AppDelegate, where we call this method + // to inform the adaptee it's safe to proceed. + @objc + public func didRegisterLegacyNotificationSettings() { + guard let legacyAdaptee = adaptee as? LegacyNotificationPresenterAdaptee else { + owsFailDebug("unexpected notifications adaptee: \(adaptee)") + return + } + legacyAdaptee.didRegisterUserNotificationSettings() + } + + @objc + func handleMessageRead(notification: Notification) { + AssertIsOnMainThread() + + switch notification.object { + case let incomingMessage as TSIncomingMessage: + Logger.debug("canceled notification for message: \(incomingMessage)") + cancelNotifications(threadId: incomingMessage.uniqueThreadId) + default: + break + } + } + + // MARK: - Presenting Notifications + + func registerNotificationSettings() -> Promise { + return adaptee.registerNotificationSettings() + } + + func presentIncomingCall(_ call: SignalCall, callerName: String) { + let alertMessage: String + switch previewType { + case .noNameNoPreview: + alertMessage = CallStrings.incomingCallWithoutCallerNameNotification + case .nameNoPreview, .namePreview: + alertMessage = String(format: CallStrings.incomingCallNotificationFormat, callerName) + } + let notificationBody = "☎️".rtlSafeAppend(" ").rtlSafeAppend(alertMessage) + + let remotePhoneNumber = call.remotePhoneNumber + let thread = TSContactThread.getOrCreateThread(contactId: remotePhoneNumber) + + guard let threadId = thread.uniqueId else { + owsFailDebug("threadId was unexpectedly nil") + return + } + + let userInfo = [ + AppNotificationUserInfoKey.threadId: threadId, + AppNotificationUserInfoKey.localCallId: call.localId.uuidString + ] + + let sound = OWSSound.defaultiOSIncomingRingtone + + DispatchQueue.main.async { + self.adaptee.notify(category: .incomingCall, + body: notificationBody, + userInfo: userInfo, + sound: sound, + replacingIdentifier: call.localId.uuidString) + } + } + + func presentMissedCall(_ call: SignalCall, callerName: String) { + let notificationBody: String + switch previewType { + case .noNameNoPreview: + notificationBody = CallStrings.missedCallNotificationBodyWithoutCallerName + case .nameNoPreview, .namePreview: + notificationBody = String(format: CallStrings.missedCallNotificationBodyWithCallerName, callerName) + } + + let remotePhoneNumber = call.remotePhoneNumber + let thread = TSContactThread.getOrCreateThread(contactId: remotePhoneNumber) + + guard let threadId = thread.uniqueId else { + owsFailDebug("threadId was unexpectedly nil") + return + } + + let sound: OWSSound? + if shouldPlaySoundForNotification { + sound = OWSSounds.notificationSound(for: thread) + } else { + sound = nil + } + + let userInfo = [ + AppNotificationUserInfoKey.threadId: threadId, + AppNotificationUserInfoKey.localCallId: call.localId.uuidString + ] + + DispatchQueue.main.async { + self.adaptee.notify(category: .missedCall, + body: notificationBody, + userInfo: userInfo, + sound: sound, + replacingIdentifier: call.localId.uuidString) + } + } + + public func presentMissedCallBecauseOfNoLongerVerifiedIdentity(call: SignalCall, callerName: String) { + let notificationBody: String + switch previewType { + case .noNameNoPreview: + notificationBody = CallStrings.missedCallWithIdentityChangeNotificationBodyWithoutCallerName + case .nameNoPreview, .namePreview: + notificationBody = String(format: CallStrings.missedCallWithIdentityChangeNotificationBodyWithCallerName, callerName) + } + + let remotePhoneNumber = call.remotePhoneNumber + let thread = TSContactThread.getOrCreateThread(contactId: remotePhoneNumber) + guard let threadId = thread.uniqueId else { + owsFailDebug("threadId was unexpectedly nil") + return + } + + let sound: OWSSound? + if shouldPlaySoundForNotification { + sound = OWSSounds.notificationSound(for: thread) + } else { + sound = nil + } + + let userInfo = [ + AppNotificationUserInfoKey.threadId: threadId + ] + + DispatchQueue.main.async { + self.adaptee.notify(category: .missedCallFromNoLongerVerifiedIdentity, + body: notificationBody, + userInfo: userInfo, + sound: sound, + replacingIdentifier: call.localId.uuidString) + } + } + + public func presentMissedCallBecauseOfNewIdentity(call: SignalCall, callerName: String) { + + let notificationBody: String + switch previewType { + case .noNameNoPreview: + notificationBody = CallStrings.missedCallWithIdentityChangeNotificationBodyWithoutCallerName + case .nameNoPreview, .namePreview: + notificationBody = String(format: CallStrings.missedCallWithIdentityChangeNotificationBodyWithCallerName, callerName) + } + + let remotePhoneNumber = call.remotePhoneNumber + let thread = TSContactThread.getOrCreateThread(contactId: remotePhoneNumber) + + guard let threadId = thread.uniqueId else { + owsFailDebug("threadId was unexpectedly nil") + return + } + + let sound: OWSSound? + if shouldPlaySoundForNotification { + sound = OWSSounds.notificationSound(for: thread) + } else { + sound = nil + } + + let userInfo = [ + AppNotificationUserInfoKey.threadId: threadId, + AppNotificationUserInfoKey.callBackNumber: remotePhoneNumber + ] + + DispatchQueue.main.async { + self.adaptee.notify(category: .missedCall, + body: notificationBody, + userInfo: userInfo, + sound: sound, + replacingIdentifier: call.localId.uuidString) + } + } + + // MJK TODO DI contactsManager + public func notifyUser(for incomingMessage: TSIncomingMessage, in thread: TSThread, contactsManager: ContactsManagerProtocol, transaction: YapDatabaseReadTransaction) { + + guard !thread.isMuted else { + return + } + + // While batch processing, some of the necessary changes have not been commited. + let rawMessageText = incomingMessage.previewText(with: transaction) + + // iOS strips anything that looks like a printf formatting character from + // the notification body, so if we want to dispay a literal "%" in a notification + // it must be escaped. + // see https://developer.apple.com/documentation/uikit/uilocalnotification/1616646-alertbody + // for more details. + let messageText = DisplayableText.filterNotificationText(rawMessageText) + + let senderName = contactsManager.displayName(forPhoneIdentifier: incomingMessage.authorId) + + let notificationBody: String + + switch previewType { + case .noNameNoPreview: + notificationBody = NSLocalizedString("APN_Message", comment: "") + case .nameNoPreview: + switch thread { + case is TSContactThread: + // TODO - should this be a format string? seems weird we're hardcoding in a ":" + let fromText = NSLocalizedString("APN_MESSAGE_FROM", comment: "") + notificationBody = String(format: "%@: %@", fromText, senderName) + case is TSGroupThread: + var groupName = thread.name() + if groupName.count < 1 { + groupName = MessageStrings.newGroupDefaultTitle + } + // TODO - should this be a format string? seems weird we're hardcoding in the quotes + let fromText = NSLocalizedString("APN_MESSAGE_IN_GROUP", comment: "") + notificationBody = String(format: "%@ \"%@\"", fromText, groupName) + default: + owsFailDebug("unexpected thread: \(thread)") + return + } + case .namePreview: + switch thread { + case is TSContactThread: + notificationBody = String(format: "%@: %@", senderName, messageText ?? "") + case is TSGroupThread: + var groupName = thread.name() + if groupName.count < 1 { + groupName = MessageStrings.newGroupDefaultTitle + } + let threadName = String(format: "\"%@\"", groupName) + + let bodyFormat = NSLocalizedString("APN_MESSAGE_IN_GROUP_DETAILED", comment: "") + notificationBody = String(format: bodyFormat, senderName, threadName, messageText ?? "") + default: + owsFailDebug("unexpected thread: \(thread)") + return + } + } + + let sound: OWSSound? + if shouldPlaySoundForNotification { + sound = OWSSounds.notificationSound(for: thread) + } else { + sound = nil + } + + guard let threadId = thread.uniqueId else { + owsFailDebug("threadId was unexpectedly nil") + return + } + + // Don't reply from lockscreen if anyone in this conversation is + // "no longer verified". + var category = AppNotificationCategory.incomingMessage + for recipientId in thread.recipientIdentifiers { + if self.identityManager.verificationState(forRecipientId: recipientId) == .noLongerVerified { + category = AppNotificationCategory.incomingMessageFromNoLongerVerifiedIdentity + break + } + } + + let userInfo = [ + AppNotificationUserInfoKey.threadId: threadId + ] + + DispatchQueue.main.async { + self.adaptee.notify(category: category, body: notificationBody, userInfo: userInfo, sound: sound) + } + } + + public func notifyForFailedSend(inThread thread: TSThread) { + let notificationFormat = NSLocalizedString("NOTIFICATION_SEND_FAILED", comment: "subsequent notification body when replying from notification fails") + let notificationBody = String(format: notificationFormat, thread.name()) + + let sound: OWSSound? + if shouldPlaySoundForNotification { + sound = OWSSounds.notificationSound(for: thread) + } else { + sound = nil + } + + guard let threadId = thread.uniqueId else { + owsFailDebug("threadId was unexpectedly nil") + return + } + + let userInfo = [ + AppNotificationUserInfoKey.threadId: threadId + ] + + DispatchQueue.main.async { + self.adaptee.notify(category: .errorMessage, body: notificationBody, userInfo: userInfo, sound: sound) + } + } + + public func notifyUser(for errorMessage: TSErrorMessage, thread: TSThread, transaction: YapDatabaseReadWriteTransaction) { + let messageText = errorMessage.previewText(with: transaction) + let authorName = thread.name() + + let notificationBody: String + switch self.previewType { + case .namePreview, .nameNoPreview: + // TODO better format string, seems weird to hardcode ":" + notificationBody = authorName.rtlSafeAppend(":").rtlSafeAppend(" ").rtlSafeAppend(messageText) + case .noNameNoPreview: + notificationBody = messageText + } + + let sound: OWSSound? + if shouldPlaySoundForNotification { + sound = OWSSounds.notificationSound(for: thread) + } else { + sound = nil + } + + guard let threadId = thread.uniqueId else { + owsFailDebug("threadId was unexpectedly nil") + return + } + + let userInfo = [ + AppNotificationUserInfoKey.threadId: threadId + ] + + transaction.addCompletionQueue(DispatchQueue.main) { + self.adaptee.notify(category: .errorMessage, body: notificationBody, userInfo: userInfo, sound: sound) + } + } + + public func notifyUser(forThreadlessErrorMessage errorMessage: TSErrorMessage, transaction: YapDatabaseReadWriteTransaction) { + let notificationBody = errorMessage.previewText(with: transaction) + + let sound: OWSSound? + if shouldPlaySoundForNotification { + sound = OWSSounds.globalNotificationSound() + } else { + sound = nil + } + + transaction.addCompletionQueue(DispatchQueue.main) { + self.adaptee.notify(category: .threadlessErrorMessage, body: notificationBody, userInfo: [:], sound: sound) + } + } + + public func cancelNotifications(threadId: String) { + self.adaptee.cancelNotifications(threadId: threadId) + } + + public func clearAllNotifications() { + adaptee.clearAllNotifications() + } + + // TODO rename to something like 'shouldThrottle' or 'requestAudioUsage' + var shouldPlaySoundForNotification: Bool { + return adaptee.shouldPlaySoundForNotification + } +} + +class NotificationActionHandler { + + static let shared: NotificationActionHandler = NotificationActionHandler() + + // MARK: - Dependencies + + var signalApp: SignalApp { + return SignalApp.shared() + } + + var messageSender: MessageSender { + return SSKEnvironment.shared.messageSender + } + + var callUIAdapter: CallUIAdapter { + return AppEnvironment.shared.callService.callUIAdapter + } + + var notificationPresenter: NotificationPresenter { + return AppEnvironment.shared.notificationPresenter + } + + var dbConnection: YapDatabaseConnection { + return OWSPrimaryStorage.shared().dbReadWriteConnection + } + + // MARK: - + + func answerCall(userInfo: [AnyHashable: Any]) throws -> Promise { + guard let localCallIdString = userInfo[AppNotificationUserInfoKey.localCallId] as? String else { + throw NotificationError.failDebug("localCallIdString was unexpectedly nil") + } + + guard let localCallId = UUID(uuidString: localCallIdString) else { + throw NotificationError.failDebug("unable to build localCallId. localCallIdString: \(localCallIdString)") + } + + callUIAdapter.answerCall(localId: localCallId) + return Promise.value(()) + } + + func callBack(userInfo: [AnyHashable: Any]) throws -> Promise { + guard let recipientId = userInfo[AppNotificationUserInfoKey.callBackNumber] as? String else { + throw NotificationError.failDebug("recipientId was unexpectedly nil") + } + + callUIAdapter.startAndShowOutgoingCall(recipientId: recipientId, hasLocalVideo: false) + return Promise.value(()) + } + + func declineCall(userInfo: [AnyHashable: Any]) throws -> Promise { + guard let localCallIdString = userInfo[AppNotificationUserInfoKey.localCallId] as? String else { + throw NotificationError.failDebug("localCallIdString was unexpectedly nil") + } + + guard let localCallId = UUID(uuidString: localCallIdString) else { + throw NotificationError.failDebug("unable to build localCallId. localCallIdString: \(localCallIdString)") + } + + callUIAdapter.declineCall(localId: localCallId) + return Promise.value(()) + } + + func markAsRead(userInfo: [AnyHashable: Any]) throws -> Promise { + guard let threadId = userInfo[AppNotificationUserInfoKey.threadId] as? String else { + throw NotificationError.failDebug("threadId was unexpectedly nil") + } + + guard let thread = TSThread.fetch(uniqueId: threadId) else { + throw NotificationError.failDebug("unable to find thread with id: \(threadId)") + } + + return Promise { resolver in + self.dbConnection.asyncReadWrite({ transaction in + thread.markAllAsRead(with: transaction) + }, + completionBlock: { + self.notificationPresenter.cancelNotifications(threadId: threadId) + resolver.fulfill(()) + }) + } + } + + func reply(userInfo: [AnyHashable: Any], replyText: String) throws -> Promise { + guard let threadId = userInfo[AppNotificationUserInfoKey.threadId] as? String else { + throw NotificationError.failDebug("threadId was unexpectedly nil") + } + + guard let thread = TSThread.fetch(uniqueId: threadId) else { + throw NotificationError.failDebug("unable to find thread with id: \(threadId)") + } + + return ThreadUtil.sendMessageNonDurably(text: replyText, + thread: thread, + quotedReplyModel: nil, + messageSender: messageSender).recover { error in + Logger.warn("Failed to send reply message from notification with error: \(error)") + self.notificationPresenter.notifyForFailedSend(inThread: thread) + } + } + + func showThread(userInfo: [AnyHashable: Any]) throws -> Promise { + guard let threadId = userInfo[AppNotificationUserInfoKey.threadId] as? String else { + throw NotificationError.failDebug("threadId was unexpectedly nil") + } + + // If this happens when the the app is not, visible we skip the animation so the thread + // can be visible to the user immediately upon opening the app, rather than having to watch + // it animate in from the homescreen. + let shouldAnimate = UIApplication.shared.applicationState == .active + signalApp.presentConversation(forThreadId: threadId, animated: shouldAnimate) + return Promise.value(()) + } +} + +extension ThreadUtil { + class func sendMessageNonDurably(text: String, thread: TSThread, quotedReplyModel: OWSQuotedReplyModel?, messageSender: MessageSender) -> Promise { + return Promise { resolver in + self.sendMessageNonDurably(withText: text, + in: thread, + quotedReplyModel: quotedReplyModel, + messageSender: messageSender, + success: resolver.fulfill, + failure: resolver.reject) + } + } +} + +extension OWSSound { + var filename: String? { + return OWSSounds.filename(for: self) + } +} + +enum NotificationError: Error { + case assertionError(description: String) +} + +extension NotificationError { + static func failDebug(_ description: String) -> NotificationError { + owsFailDebug(description) + return NotificationError.assertionError(description: description) + } +} diff --git a/Signal/src/UserInterface/Notifications/LegacyNotificationsAdaptee.swift b/Signal/src/UserInterface/Notifications/LegacyNotificationsAdaptee.swift new file mode 100644 index 000000000..295d2493d --- /dev/null +++ b/Signal/src/UserInterface/Notifications/LegacyNotificationsAdaptee.swift @@ -0,0 +1,290 @@ +// +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. +// + +import Foundation +import PromiseKit + +struct LegacyNotificationConfig { + + static var allNotificationCategories: Set { + let categories = AppNotificationCategory.allCases.map { notificationCategory($0) } + return Set(categories) + } + + static func notificationActions(for category: AppNotificationCategory) -> [UIUserNotificationAction] { + return category.actions.map { notificationAction($0) } + } + + static func notificationAction(_ action: AppNotificationAction) -> UIUserNotificationAction { + switch action { + case .answerCall: + let mutableAction = UIMutableUserNotificationAction() + mutableAction.identifier = action.identifier + mutableAction.title = CallStrings.answerCallButtonTitle + mutableAction.activationMode = .foreground + mutableAction.isDestructive = false + mutableAction.isAuthenticationRequired = false + return mutableAction + case .callBack: + let mutableAction = UIMutableUserNotificationAction() + mutableAction.identifier = action.identifier + mutableAction.title = CallStrings.callBackButtonTitle + mutableAction.activationMode = .foreground + mutableAction.isDestructive = false + mutableAction.isAuthenticationRequired = true + return mutableAction + case .declineCall: + let mutableAction = UIMutableUserNotificationAction() + mutableAction.identifier = action.identifier + mutableAction.title = CallStrings.declineCallButtonTitle + mutableAction.activationMode = .background + mutableAction.isDestructive = false + mutableAction.isAuthenticationRequired = false + return mutableAction + case .markAsRead: + let mutableAction = UIMutableUserNotificationAction() + mutableAction.identifier = action.identifier + mutableAction.title = MessageStrings.markAsReadNotificationAction + mutableAction.activationMode = .background + mutableAction.isDestructive = false + mutableAction.isAuthenticationRequired = false + return mutableAction + case .reply: + let mutableAction = UIMutableUserNotificationAction() + mutableAction.identifier = action.identifier + mutableAction.title = MessageStrings.replyNotificationAction + mutableAction.activationMode = .background + mutableAction.isDestructive = false + mutableAction.isAuthenticationRequired = false + mutableAction.behavior = .textInput + return mutableAction + case .showThread: + let mutableAction = UIMutableUserNotificationAction() + mutableAction.identifier = action.identifier + mutableAction.title = CallStrings.showThreadButtonTitle + mutableAction.activationMode = .foreground + mutableAction.isDestructive = false + mutableAction.isAuthenticationRequired = true + return mutableAction + } + } + + static func action(identifier: String) -> AppNotificationAction? { + return AppNotificationAction.allCases.first { notificationAction($0).identifier == identifier } + } + + static func notificationActions(category: AppNotificationCategory) -> [UIUserNotificationAction] { + return category.actions.map { notificationAction($0) } + } + + static func notificationCategory(_ category: AppNotificationCategory) -> UIUserNotificationCategory { + let notificationCategory = UIMutableUserNotificationCategory() + notificationCategory.identifier = category.identifier + + let actions = notificationActions(category: category) + notificationCategory.setActions(actions, for: .minimal) + notificationCategory.setActions(actions, for: .default) + + return notificationCategory + } +} + +class LegacyNotificationPresenterAdaptee { + + private var notifications: [String: UILocalNotification] = [:] + private var userNotificationSettingsPromise: Promise? + private var userNotificationSettingsResolver: Resolver? + + // Notification registration is confirmed via AppDelegate + // Before this occurs, it is not safe to assume push token requests will be acknowledged. + // + // e.g. in the case that Background Fetch is disabled, token requests will be ignored until + // we register user notification settings. + @objc + public func didRegisterUserNotificationSettings() { + AssertIsOnMainThread() + guard let userNotificationSettingsResolver = self.userNotificationSettingsResolver else { + owsFailDebug("promise completion in \(#function) unexpectedly nil") + return + } + + userNotificationSettingsResolver.fulfill(()) + } + +} + +extension LegacyNotificationPresenterAdaptee: NotificationPresenterAdaptee { + + func registerNotificationSettings() -> Promise { + AssertIsOnMainThread() + Logger.debug("") + + guard self.userNotificationSettingsPromise == nil else { + let promise = self.userNotificationSettingsPromise! + Logger.info("already registered user notification settings") + return promise + } + + let (promise, resolver) = Promise.pending() + self.userNotificationSettingsPromise = promise + self.userNotificationSettingsResolver = resolver + + let settings = UIUserNotificationSettings(types: [.alert, .sound, .badge], + categories: LegacyNotificationConfig.allNotificationCategories) + UIApplication.shared.registerUserNotificationSettings(settings) + + return promise + } + + func notify(category: AppNotificationCategory, body: String, userInfo: [AnyHashable: Any], sound: OWSSound?) { + AssertIsOnMainThread() + notify(category: category, body: body, userInfo: userInfo, sound: sound, replacingIdentifier: nil) + } + + func notify(category: AppNotificationCategory, body: String, userInfo: [AnyHashable: Any], sound: OWSSound?, replacingIdentifier: String?) { + AssertIsOnMainThread() + guard UIApplication.shared.applicationState != .active else { + Logger.info("skipping notification; app is in foreground") + return + } + + let notification = UILocalNotification() + notification.category = category.identifier + notification.alertBody = body + notification.userInfo = userInfo + notification.soundName = sound?.filename + + var notificationIdentifier: String = UUID().uuidString + if let replacingIdentifier = replacingIdentifier { + notificationIdentifier = replacingIdentifier + Logger.debug("replacing notification with identifier: \(notificationIdentifier)") + cancelNotification(identifier: notificationIdentifier) + } + + let checkForCancel = category == .incomingMessage + if checkForCancel && hasReceivedSyncMessageRecently { + assert(userInfo[AppNotificationUserInfoKey.threadId] != nil) + notification.fireDate = Date(timeIntervalSinceNow: kNotificationDelayForRemoteRead) + notification.timeZone = NSTimeZone.local + } + + Logger.debug("presenting notification with identifier: \(notificationIdentifier)") + UIApplication.shared.scheduleLocalNotification(notification) + notifications[notificationIdentifier] = notification + } + + func cancelNotification(_ notification: UILocalNotification) { + AssertIsOnMainThread() + UIApplication.shared.cancelLocalNotification(notification) + } + + func cancelNotification(identifier: String) { + AssertIsOnMainThread() + guard let notification = notifications.removeValue(forKey: identifier) else { + Logger.debug("no notification to cancel with identifier: \(identifier)") + return + } + + cancelNotification(notification) + } + + func cancelNotifications(threadId: String) { + AssertIsOnMainThread() + for notification in notifications.values { + guard let notificationThreadId = notification.userInfo?[AppNotificationUserInfoKey.threadId] as? String else { + continue + } + + guard notificationThreadId == threadId else { + continue + } + + cancelNotification(notification) + } + } + + func clearAllNotifications() { + AssertIsOnMainThread() + for (_, notification) in notifications { + cancelNotification(notification) + } + } + + // FIXME: Accomodate 'playSoundsInForeground' preference + // FIXME: debounce + var shouldPlaySoundForNotification: Bool { + return true + } +} + +@objc +public class LegacyNotificationActionHandler: NSObject { + + @objc + public static let kDefaultActionIdentifier = "LegacyNotificationActionHandler.kDefaultActionIdentifier" + + // TODO move this to environment? + @objc + static let shared: LegacyNotificationActionHandler = LegacyNotificationActionHandler() + + var actionHandler: NotificationActionHandler { + return NotificationActionHandler.shared + } + + @objc + func handleNotificationResponse(actionIdentifier: String, + notification: UILocalNotification, + responseInfo: [AnyHashable: Any], + completionHandler: @escaping () -> Void) { + firstly { + try handleNotificationResponse(actionIdentifier: actionIdentifier, notification: notification, responseInfo: responseInfo) + }.done { + completionHandler() + }.catch { error in + completionHandler() + owsFailDebug("error: \(error)") + Logger.error("error: \(error)") + }.retainUntilComplete() + } + + func handleNotificationResponse(actionIdentifier: String, + notification: UILocalNotification, + responseInfo: [AnyHashable: Any]) throws -> Promise { + assert(AppReadiness.isAppReady()) + + let userInfo = notification.userInfo ?? [:] + + switch actionIdentifier { + case type(of: self).kDefaultActionIdentifier: + Logger.debug("default action") + return try actionHandler.showThread(userInfo: userInfo) + default: + // proceed + break + } + + guard let action = LegacyNotificationConfig.action(identifier: actionIdentifier) else { + throw NotificationError.failDebug("unable to find action for actionIdentifier: \(actionIdentifier)") + } + + switch action { + case .answerCall: + return try actionHandler.answerCall(userInfo: userInfo) + case .callBack: + return try actionHandler.callBack(userInfo: userInfo) + case .declineCall: + return try actionHandler.declineCall(userInfo: userInfo) + case .markAsRead: + return try actionHandler.markAsRead(userInfo: userInfo) + case .reply: + guard let replyText = responseInfo[UIUserNotificationActionResponseTypedTextKey] as? String else { + throw NotificationError.failDebug("replyText was unexpectedly nil") + } + + return try actionHandler.reply(userInfo: userInfo, replyText: replyText) + case .showThread: + return try actionHandler.showThread(userInfo: userInfo) + } + } +} diff --git a/Signal/src/UserInterface/Notifications/NotificationsAdapter.swift b/Signal/src/UserInterface/Notifications/NotificationsAdapter.swift deleted file mode 100644 index 25ea8edee..000000000 --- a/Signal/src/UserInterface/Notifications/NotificationsAdapter.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// Copyright (c) 2019 Open Whisper Systems. All rights reserved. -// - -import Foundation - -@objc -public protocol NotificationsAdaptee: NotificationsProtocol, OWSCallNotificationsAdaptee { } - -extension NotificationsManager: NotificationsAdaptee { } - -/** - * Present call related notifications to the user. - */ -@objc(OWSNotificationsAdapter) -public class NotificationsAdapter: NSObject, NotificationsProtocol { - private let adaptee: NotificationsAdaptee - - @objc public override init() { - self.adaptee = NotificationsManager() - - super.init() - - SwiftSingletons.register(self) - } - - func presentIncomingCall(_ call: SignalCall, callerName: String) { - Logger.debug("") - adaptee.presentIncomingCall(call, callerName: callerName) - } - - func presentMissedCall(_ call: SignalCall, callerName: String) { - Logger.debug("") - adaptee.presentMissedCall(call, callerName: callerName) - } - - public func presentMissedCallBecauseOfNoLongerVerifiedIdentity(call: SignalCall, callerName: String) { - adaptee.presentMissedCallBecauseOfNoLongerVerifiedIdentity(call: call, callerName: callerName) - } - - public func presentMissedCallBecauseOfNewIdentity(call: SignalCall, callerName: String) { - adaptee.presentMissedCallBecauseOfNewIdentity(call: call, callerName: callerName) - } - - // MJK TODO DI contactsManager - public func notifyUser(for incomingMessage: TSIncomingMessage, in thread: TSThread, contactsManager: ContactsManagerProtocol, transaction: YapDatabaseReadTransaction) { - adaptee.notifyUser(for: incomingMessage, in: thread, contactsManager: contactsManager, transaction: transaction) - } - - public func notifyUser(for error: TSErrorMessage, thread: TSThread, transaction: YapDatabaseReadWriteTransaction) { - adaptee.notifyUser(for: error, thread: thread, transaction: transaction) - } - - public func notifyUser(forThreadlessErrorMessage error: TSErrorMessage, transaction: YapDatabaseReadWriteTransaction) { - adaptee.notifyUser(forThreadlessErrorMessage: error, transaction: transaction) - } - - public func clearAllNotifications() { - adaptee.clearAllNotifications() - } -} diff --git a/Signal/src/UserInterface/Notifications/UserNotificationsAdaptee.swift b/Signal/src/UserInterface/Notifications/UserNotificationsAdaptee.swift index ee6e28622..b9a8a7ffa 100644 --- a/Signal/src/UserInterface/Notifications/UserNotificationsAdaptee.swift +++ b/Signal/src/UserInterface/Notifications/UserNotificationsAdaptee.swift @@ -1,185 +1,288 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // -/** - * TODO This is currently unused code. I started implenting new notifications as UserNotifications rather than the deprecated - * LocalNotifications before I realized we can't mix and match. Registering notifications for one clobbers the other. - * So, for now iOS10 continues to use LocalNotifications until we can port all the NotificationsManager stuff here. - */ import Foundation import UserNotifications +import PromiseKit @available(iOS 10.0, *) -struct AppNotifications { - enum Category { - case missedCall, - missedCallFromNoLongerVerifiedIdentity +class UserNotificationConfig { - // Don't forget to update this! We use it to register categories. - static let allValues = [ missedCall, missedCallFromNoLongerVerifiedIdentity ] - } - - enum Action { - case callBack, - showThread - } - - static var allCategories: Set { - let categories = Category.allValues.map { category($0) } + class var allNotificationCategories: Set { + let categories = AppNotificationCategory.allCases.map { notificationCategory($0) } return Set(categories) } - static func category(_ type: Category) -> UNNotificationCategory { - switch type { - case .missedCall: - return UNNotificationCategory(identifier: "org.whispersystems.signal.AppNotifications.Category.missedCall", - actions: [ action(.callBack) ], - intentIdentifiers: [], - options: []) + class func notificationActions(for category: AppNotificationCategory) -> [UNNotificationAction] { + return category.actions.map { notificationAction($0) } + } - case .missedCallFromNoLongerVerifiedIdentity: - return UNNotificationCategory(identifier: "org.whispersystems.signal.AppNotifications.Category.missedCallFromNoLongerVerifiedIdentity", - actions: [ action(.showThread) ], - intentIdentifiers: [], - options: []) + class func notificationCategory(_ category: AppNotificationCategory) -> UNNotificationCategory { + return UNNotificationCategory(identifier: category.identifier, + actions: notificationActions(for: category), + intentIdentifiers: [], + options: []) + } + + class func notificationAction(_ action: AppNotificationAction) -> UNNotificationAction { + switch action { + case .answerCall: + return UNNotificationAction(identifier: action.identifier, + title: CallStrings.answerCallButtonTitle, + options: [.foreground]) + case .callBack: + return UNNotificationAction(identifier: action.identifier, + title: CallStrings.callBackButtonTitle, + options: [.foreground]) + case .declineCall: + return UNNotificationAction(identifier: action.identifier, + title: CallStrings.declineCallButtonTitle, + options: []) + case .markAsRead: + return UNNotificationAction(identifier: action.identifier, + title: MessageStrings.markAsReadNotificationAction, + options: []) + case .reply: + return UNTextInputNotificationAction(identifier: action.identifier, + title: MessageStrings.replyNotificationAction, + options: [], + textInputButtonTitle: MessageStrings.sendButton, + textInputPlaceholder: "") + case .showThread: + return UNNotificationAction(identifier: action.identifier, + title: CallStrings.showThreadButtonTitle, + options: [.foreground]) } } - static func action(_ type: Action) -> UNNotificationAction { - switch type { - case .callBack: - return UNNotificationAction(identifier: "org.whispersystems.signal.AppNotifications.Action.callBack", - title: CallStrings.callBackButtonTitle, - options: .authenticationRequired) - case .showThread: - return UNNotificationAction(identifier: "org.whispersystems.signal.AppNotifications.Action.showThread", - title: CallStrings.showThreadButtonTitle, - options: .authenticationRequired) - } + class func action(identifier: String) -> AppNotificationAction? { + return AppNotificationAction.allCases.first { notificationAction($0).identifier == identifier } + } + +} + +@available(iOS 10.0, *) +class UserNotificationPresenterAdaptee: NSObject, UNUserNotificationCenterDelegate { + + private let notificationCenter: UNUserNotificationCenter + private var notifications: [String: UNNotificationRequest] = [:] + + override init() { + self.notificationCenter = UNUserNotificationCenter.current() + super.init() + notificationCenter.delegate = self + SwiftSingletons.register(self) } } @available(iOS 10.0, *) -class UserNotificationsAdaptee: NSObject, OWSCallNotificationsAdaptee, UNUserNotificationCenterDelegate { +extension UserNotificationPresenterAdaptee: NotificationPresenterAdaptee { - private let center: UNUserNotificationCenter + func registerNotificationSettings() -> Promise { + return Promise { resolver in + notificationCenter.requestAuthorization(options: [.badge, .sound, .alert]) { (granted, error) in + self.notificationCenter.setNotificationCategories(UserNotificationConfig.allNotificationCategories) - var previewType: NotificationType { - return Environment.shared.preferences.notificationPreviewType() - } + if granted { + Logger.debug("succeeded.") + } else if error != nil { + Logger.error("failed with error: \(error!)") + } else { + owsFailDebug("error was unexpectedly nil") + Logger.error("failed without error.") + } - override init() { - self.center = UNUserNotificationCenter.current() - - super.init() - - SwiftSingletons.register(self) - - center.delegate = self - - // FIXME TODO only do this after user has registered. - // maybe the PushManager needs a reference to the NotificationsAdapter. - requestAuthorization() - - center.setNotificationCategories(AppNotifications.allCategories) - } - - func requestAuthorization() { - center.requestAuthorization(options: [.badge, .sound, .alert]) { (granted, error) in - if granted { - Logger.debug("succeeded.") - } else if error != nil { - Logger.error("failed with error: \(error!)") - } else { - Logger.error("failed without error.") + // Note that the promise is fulfilled regardless of if notification permssions were + // granted. This promise only indicates that the user has responded, so we can + // proceed with requesting push tokens and complete registration. + resolver.fulfill(()) } } } - // MARK: - OWSCallNotificationsAdaptee - - public func presentIncomingCall(_ call: SignalCall, callerName: String) { - Logger.debug("is no-op, because it's handled with callkit.") - // TODO since CallKit doesn't currently work on the simulator, - // we could implement UNNotifications for simulator testing, or if people have opted out of callkit. + func notify(category: AppNotificationCategory, body: String, userInfo: [AnyHashable: Any], sound: OWSSound?) { + AssertIsOnMainThread() + notify(category: category, body: body, userInfo: userInfo, sound: sound, replacingIdentifier: nil) } - public func presentMissedCall(_ call: SignalCall, callerName: String) { - Logger.debug("") + func notify(category: AppNotificationCategory, body: String, userInfo: [AnyHashable: Any], sound: OWSSound?, replacingIdentifier: String?) { + AssertIsOnMainThread() let content = UNMutableNotificationContent() - // TODO group by thread identifier - // content.threadIdentifier = threadId + content.categoryIdentifier = category.identifier + content.userInfo = userInfo + content.sound = sound?.notificationSound - let notificationBody = { () -> String in - switch previewType { - case .noNameNoPreview: - return CallStrings.missedCallNotificationBodyWithoutCallerName - case .nameNoPreview, .namePreview: - return (Environment.shared.preferences.isCallKitPrivacyEnabled() - ? CallStrings.missedCallNotificationBodyWithoutCallerName - : String(format: CallStrings.missedCallNotificationBodyWithCallerName, callerName)) - }}() + var notificationIdentifier: String = UUID().uuidString + if let replacingIdentifier = replacingIdentifier { + notificationIdentifier = replacingIdentifier + Logger.debug("replacing notification with identifier: \(notificationIdentifier)") + cancelNotification(identifier: notificationIdentifier) + } - content.body = notificationBody - content.sound = UNNotificationSound.default() - content.categoryIdentifier = AppNotifications.category(.missedCall).identifier + let trigger: UNNotificationTrigger? + let checkForCancel = category == .incomingMessage + if checkForCancel && hasReceivedSyncMessageRecently { + assert(userInfo[AppNotificationUserInfoKey.threadId] != nil) + trigger = UNTimeIntervalNotificationTrigger(timeInterval: kNotificationDelayForRemoteRead, repeats: false) + } else { + trigger = nil + } - let request = UNNotificationRequest.init(identifier: call.localId.uuidString, content: content, trigger: nil) + if shouldPresentNotification(category: category, userInfo: userInfo) { + content.body = body + } else { + // Play sound and vibrate, but without a `body` no banner will show. + Logger.debug("supressing notification body") + } - center.add(request) + let request = UNNotificationRequest(identifier: notificationIdentifier, content: content, trigger: trigger) + + Logger.debug("presenting notification with identifier: \(notificationIdentifier)") + notificationCenter.add(request) + notifications[notificationIdentifier] = request } - public func presentMissedCallBecauseOfNoLongerVerifiedIdentity(call: SignalCall, callerName: String) { - Logger.debug("") - - let content = UNMutableNotificationContent() - // TODO group by thread identifier - // content.threadIdentifier = threadId - - let notificationBody = { () -> String in - switch previewType { - case .noNameNoPreview: - return CallStrings.missedCallWithIdentityChangeNotificationBodyWithoutCallerName - case .nameNoPreview, .namePreview: - return (Environment.shared.preferences.isCallKitPrivacyEnabled() - ? CallStrings.missedCallWithIdentityChangeNotificationBodyWithoutCallerName - : String(format: CallStrings.missedCallWithIdentityChangeNotificationBodyWithCallerName, callerName)) - }}() - - content.body = notificationBody - content.sound = UNNotificationSound.default() - content.categoryIdentifier = AppNotifications.category(.missedCallFromNoLongerVerifiedIdentity).identifier - - let request = UNNotificationRequest.init(identifier: call.localId.uuidString, content: content, trigger: nil) - - center.add(request) + func cancelNotification(identifier: String) { + AssertIsOnMainThread() + notifications.removeValue(forKey: identifier) + notificationCenter.removeDeliveredNotifications(withIdentifiers: [identifier]) + notificationCenter.removePendingNotificationRequests(withIdentifiers: [identifier]) } - public func presentMissedCallBecauseOfNewIdentity(call: SignalCall, callerName: String) { - Logger.debug("") + func cancelNotification(_ notification: UNNotificationRequest) { + AssertIsOnMainThread() + cancelNotification(identifier: notification.identifier) + } - let content = UNMutableNotificationContent() - // TODO group by thread identifier - // content.threadIdentifier = threadId + func cancelNotifications(threadId: String) { + AssertIsOnMainThread() + for notification in notifications.values { + guard let notificationThreadId = notification.content.userInfo[AppNotificationUserInfoKey.threadId] as? String else { + continue + } - let notificationBody = { () -> String in - switch previewType { - case .noNameNoPreview: - return CallStrings.missedCallWithIdentityChangeNotificationBodyWithoutCallerName - case .nameNoPreview, .namePreview: - return (Environment.shared.preferences.isCallKitPrivacyEnabled() - ? CallStrings.missedCallWithIdentityChangeNotificationBodyWithoutCallerName - : String(format: CallStrings.missedCallWithIdentityChangeNotificationBodyWithCallerName, callerName)) - }}() + guard notificationThreadId == threadId else { + continue + } - content.body = notificationBody - content.sound = UNNotificationSound.default() - content.categoryIdentifier = AppNotifications.category(.missedCall).identifier + cancelNotification(notification) + } + } - let request = UNNotificationRequest.init(identifier: call.localId.uuidString, content: content, trigger: nil) + func clearAllNotifications() { + AssertIsOnMainThread() + notificationCenter.removeAllPendingNotificationRequests() + notificationCenter.removeAllDeliveredNotifications() + } - center.add(request) + // UNUserNotification framework does it's own audio throttling + var shouldPlaySoundForNotification: Bool { + return true + } + + func shouldPresentNotification(category: AppNotificationCategory, userInfo: [AnyHashable: Any]) -> Bool { + AssertIsOnMainThread() + guard UIApplication.shared.applicationState == .active else { + return true + } + + guard category == .incomingMessage || category == .errorMessage else { + return true + } + + guard let notificationThreadId = userInfo[AppNotificationUserInfoKey.threadId] as? String else { + owsFailDebug("threadId was unexpectedly nil") + return true + } + + guard let conversationViewController = UIApplication.shared.frontmostViewController as? ConversationViewController else { + return true + } + + // Show notifications for any *other* thread + return conversationViewController.thread.uniqueId != notificationThreadId + } +} + +@objc +@available(iOS 10.0, *) +public class UserNotificationActionHandler: NSObject { + + // TODO move this to environment? + @objc + static let shared: UserNotificationActionHandler = UserNotificationActionHandler() + + var actionHandler: NotificationActionHandler { + return NotificationActionHandler.shared + } + + @objc + func handleNotificationResponse( _ response: UNNotificationResponse, completionHandler: @escaping () -> Void) { + AssertIsOnMainThread() + firstly { + try handleNotificationResponse(response) + }.done { + completionHandler() + }.catch { error in + completionHandler() + owsFailDebug("error: \(error)") + Logger.error("error: \(error)") + }.retainUntilComplete() + } + + func handleNotificationResponse( _ response: UNNotificationResponse) throws -> Promise { + AssertIsOnMainThread() + assert(AppReadiness.isAppReady()) + + let userInfo = response.notification.request.content.userInfo + + switch response.actionIdentifier { + case UNNotificationDefaultActionIdentifier: + Logger.debug("default action") + return try actionHandler.showThread(userInfo: userInfo) + case UNNotificationDismissActionIdentifier: + // TODO - mark as read? + Logger.debug("dismissed notification") + return Promise.value(()) + default: + // proceed + break + } + + guard let action = UserNotificationConfig.action(identifier: response.actionIdentifier) else { + throw NotificationError.failDebug("unable to find action for actionIdentifier: \(response.actionIdentifier)") + } + + switch action { + case .answerCall: + return try actionHandler.answerCall(userInfo: userInfo) + case .callBack: + return try actionHandler.callBack(userInfo: userInfo) + case .declineCall: + return try actionHandler.declineCall(userInfo: userInfo) + case .markAsRead: + return try actionHandler.markAsRead(userInfo: userInfo) + case .reply: + guard let textInputResponse = response as? UNTextInputNotificationResponse else { + throw NotificationError.failDebug("response had unexpected type: \(response)") + } + + return try actionHandler.reply(userInfo: userInfo, replyText: textInputResponse.userText) + case .showThread: + return try actionHandler.showThread(userInfo: userInfo) + } + } +} + +extension OWSSound { + @available(iOS 10.0, *) + var notificationSound: UNNotificationSound { + guard let filename = OWSSounds.filename(for: self) else { + owsFailDebug("filename was unexpectedly nil") + return UNNotificationSound.default() + } + return UNNotificationSound(named: filename) } } diff --git a/Signal/src/ViewControllers/AppSettings/AdvancedSettingsTableViewController.m b/Signal/src/ViewControllers/AppSettings/AdvancedSettingsTableViewController.m index d198c2287..294ba1beb 100644 --- a/Signal/src/ViewControllers/AppSettings/AdvancedSettingsTableViewController.m +++ b/Signal/src/ViewControllers/AppSettings/AdvancedSettingsTableViewController.m @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // #import "AdvancedSettingsTableViewController.h" @@ -7,7 +7,6 @@ #import "DomainFrontingCountryViewController.h" #import "OWSCountryMetadata.h" #import "Pastelog.h" -#import "PushManager.h" #import "Signal-Swift.h" #import "TSAccountManager.h" #import diff --git a/Signal/src/ViewControllers/AppSettings/AppSettingsViewController.m b/Signal/src/ViewControllers/AppSettings/AppSettingsViewController.m index 2e5a152ab..82c7da228 100644 --- a/Signal/src/ViewControllers/AppSettings/AppSettingsViewController.m +++ b/Signal/src/ViewControllers/AppSettings/AppSettingsViewController.m @@ -13,7 +13,6 @@ #import "OWSNavigationController.h" #import "PrivacySettingsTableViewController.h" #import "ProfileViewController.h" -#import "PushManager.h" #import "RegistrationUtils.h" #import "Signal-Swift.h" #import diff --git a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m index 974b97746..18ce9b35e 100644 --- a/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m +++ b/Signal/src/ViewControllers/ConversationView/ConversationInputToolbar.m @@ -143,9 +143,7 @@ const CGFloat kMaxTextViewHeight = 98; [self.attachmentButton autoSetDimensionsToSize:CGSizeMake(40, kMinTextViewHeight)]; _sendButton = [UIButton buttonWithType:UIButtonTypeCustom]; - [self.sendButton - setTitle:NSLocalizedString(@"SEND_BUTTON_TITLE", @"Label for the send button in the conversation view.") - forState:UIControlStateNormal]; + [self.sendButton setTitle:MessageStrings.sendButton forState:UIControlStateNormal]; [self.sendButton setTitleColor:UIColor.ows_signalBlueColor forState:UIControlStateNormal]; self.sendButton.titleLabel.textAlignment = NSTextAlignmentCenter; self.sendButton.titleLabel.font = [UIFont ows_mediumFontWithSize:17.f]; diff --git a/Signal/src/ViewControllers/DebugUI/DebugUINotifications.swift b/Signal/src/ViewControllers/DebugUI/DebugUINotifications.swift index bdeae8f42..98cfa91e3 100644 --- a/Signal/src/ViewControllers/DebugUI/DebugUINotifications.swift +++ b/Signal/src/ViewControllers/DebugUI/DebugUINotifications.swift @@ -11,8 +11,8 @@ class DebugUINotifications: DebugUIPage { // MARK: Dependencies - var notificationsAdapter: NotificationsAdapter { - return AppEnvironment.shared.notificationsAdapter + var notificationPresenter: NotificationPresenter { + return AppEnvironment.shared.notificationPresenter } var messageSender: MessageSender { return SSKEnvironment.shared.messageSender @@ -135,28 +135,28 @@ class DebugUINotifications: DebugUIPage { func notifyForIncomingCall(thread: TSContactThread) -> Guarantee { Logger.info("⚠️ will present notification after delay") return delayedNotificationDispatchWithFakeCall(thread: thread) { call in - self.notificationsAdapter.presentIncomingCall(call, callerName: thread.name()) + self.notificationPresenter.presentIncomingCall(call, callerName: thread.name()) } } func notifyForMissedCall(thread: TSContactThread) -> Guarantee { Logger.info("⚠️ will present notification after delay") return delayedNotificationDispatchWithFakeCall(thread: thread) { call in - self.notificationsAdapter.presentMissedCall(call, callerName: thread.name()) + self.notificationPresenter.presentMissedCall(call, callerName: thread.name()) } } func notifyForMissedCallBecauseOfNewIdentity(thread: TSContactThread) -> Guarantee { Logger.info("⚠️ will present notification after delay") return delayedNotificationDispatchWithFakeCall(thread: thread) { call in - self.notificationsAdapter.presentMissedCallBecauseOfNewIdentity(call: call, callerName: thread.name()) + self.notificationPresenter.presentMissedCallBecauseOfNewIdentity(call: call, callerName: thread.name()) } } func notifyForMissedCallBecauseOfNoLongerVerifiedIdentity(thread: TSContactThread) -> Guarantee { Logger.info("⚠️ will present notification after delay") return delayedNotificationDispatchWithFakeCall(thread: thread) { call in - self.notificationsAdapter.presentMissedCallBecauseOfNoLongerVerifiedIdentity(call: call, callerName: thread.name()) + self.notificationPresenter.presentMissedCallBecauseOfNoLongerVerifiedIdentity(call: call, callerName: thread.name()) } } @@ -168,7 +168,7 @@ class DebugUINotifications: DebugUIPage { factory.threadCreator = { _ in return thread } let incomingMessage = factory.create(transaction: transaction) - self.notificationsAdapter.notifyUser(for: incomingMessage, + self.notificationPresenter.notifyUser(for: incomingMessage, in: thread, contactsManager: self.contactsManager, transaction: transaction) @@ -184,7 +184,7 @@ class DebugUINotifications: DebugUIPage { failedMessageType: TSErrorMessageType.invalidMessage) self.readWrite { transaction in - self.notificationsAdapter.notifyUser(for: errorMessage, thread: thread, transaction: transaction) + self.notificationPresenter.notifyUser(for: errorMessage, thread: thread, transaction: transaction) } } } @@ -195,7 +195,7 @@ class DebugUINotifications: DebugUIPage { self.readWrite { transaction in let errorMessage = TSErrorMessage.corruptedMessageInUnknownThread() - self.notificationsAdapter.notifyUser(forThreadlessErrorMessage: errorMessage, + self.notificationPresenter.notifyUser(forThreadlessErrorMessage: errorMessage, transaction: transaction) } } diff --git a/Signal/src/ViewControllers/HomeView/HomeViewController.m b/Signal/src/ViewControllers/HomeView/HomeViewController.m index 318fabf84..2e8ab8ba1 100644 --- a/Signal/src/ViewControllers/HomeView/HomeViewController.m +++ b/Signal/src/ViewControllers/HomeView/HomeViewController.m @@ -10,7 +10,6 @@ #import "OWSNavigationController.h" #import "OWSPrimaryStorage.h" #import "ProfileViewController.h" -#import "PushManager.h" #import "RegistrationUtils.h" #import "Signal-Swift.h" #import "SignalApp.h" @@ -943,10 +942,10 @@ NSString *const kArchivedConversationsReuseIdentifier = @"kArchivedConversations { OWSAssertIsOnMainThread(); OWSLogInfo(@"beggining refreshing."); - [AppEnvironment.shared.messageFetcherJob run].ensure(^{ + [[AppEnvironment.shared.messageFetcherJob run].ensure(^{ OWSLogInfo(@"ending refreshing."); [refreshControl endRefreshing]; - }); + }) retainUntilComplete]; } #pragma mark - Edit Actions diff --git a/Signal/src/call/CallService.swift b/Signal/src/call/CallService.swift index 47b0b5ea5..b79b3d4a5 100644 --- a/Signal/src/call/CallService.swift +++ b/Signal/src/call/CallService.swift @@ -401,8 +401,8 @@ private class SignalCallData: NSObject { return AppEnvironment.shared.accountManager } - private var notificationsAdapter: NotificationsAdapter { - return AppEnvironment.shared.notificationsAdapter + private var notificationPresenter: NotificationPresenter { + return AppEnvironment.shared.notificationPresenter } // MARK: - Notifications @@ -427,7 +427,7 @@ private class SignalCallData: NSObject { Logger.warn("ending current call in. Did user toggle callkit preference while in a call?") self.terminateCall() } - self.callUIAdapter = CallUIAdapter(callService: self, contactsManager: self.contactsManager, notificationsAdapter: self.notificationsAdapter) + self.callUIAdapter = CallUIAdapter(callService: self, contactsManager: self.contactsManager, notificationPresenter: self.notificationPresenter) } // MARK: - Service Actions @@ -691,11 +691,11 @@ private class SignalCallData: NSObject { switch untrustedIdentity!.verificationState { case .verified: owsFailDebug("shouldn't have missed a call due to untrusted identity if the identity is verified") - self.notificationsAdapter.presentMissedCall(newCall, callerName: callerName) + self.notificationPresenter.presentMissedCall(newCall, callerName: callerName) case .default: - self.notificationsAdapter.presentMissedCallBecauseOfNewIdentity(call: newCall, callerName: callerName) + self.notificationPresenter.presentMissedCallBecauseOfNewIdentity(call: newCall, callerName: callerName) case .noLongerVerified: - self.notificationsAdapter.presentMissedCallBecauseOfNoLongerVerifiedIdentity(call: newCall, callerName: callerName) + self.notificationPresenter.presentMissedCallBecauseOfNoLongerVerifiedIdentity(call: newCall, callerName: callerName) } // MJK TODO remove this timestamp param diff --git a/Signal/src/call/NonCallKitCallUIAdaptee.swift b/Signal/src/call/NonCallKitCallUIAdaptee.swift index 9f1b49fbd..9931d637d 100644 --- a/Signal/src/call/NonCallKitCallUIAdaptee.swift +++ b/Signal/src/call/NonCallKitCallUIAdaptee.swift @@ -11,17 +11,17 @@ import SignalMessaging */ class NonCallKitCallUIAdaptee: NSObject, CallUIAdaptee { - let notificationsAdapter: NotificationsAdapter + let notificationPresenter: NotificationPresenter let callService: CallService // Starting/Stopping incoming call ringing is our apps responsibility for the non CallKit interface. let hasManualRinger = true - required init(callService: CallService, notificationsAdapter: NotificationsAdapter) { + required init(callService: CallService, notificationPresenter: NotificationPresenter) { AssertIsOnMainThread() self.callService = callService - self.notificationsAdapter = notificationsAdapter + self.notificationPresenter = notificationPresenter super.init() } @@ -59,14 +59,14 @@ class NonCallKitCallUIAdaptee: NSObject, CallUIAdaptee { if UIApplication.shared.applicationState == .active { Logger.debug("skipping notification since app is already active.") } else { - notificationsAdapter.presentIncomingCall(call, callerName: callerName) + notificationPresenter.presentIncomingCall(call, callerName: callerName) } } func reportMissedCall(_ call: SignalCall, callerName: String) { AssertIsOnMainThread() - notificationsAdapter.presentMissedCall(call, callerName: callerName) + notificationPresenter.presentMissedCall(call, callerName: callerName) } func answerCall(localId: UUID) { diff --git a/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift b/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift index b6e8aab53..678288590 100644 --- a/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift +++ b/Signal/src/call/Speakerbox/CallKitCallUIAdaptee.swift @@ -20,7 +20,7 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { private let callManager: CallKitCallManager internal let callService: CallService - internal let notificationsAdapter: NotificationsAdapter + internal let notificationPresenter: NotificationPresenter internal let contactsManager: OWSContactsManager private let showNamesOnCallScreen: Bool private let provider: CXProvider @@ -76,7 +76,7 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { return providerConfiguration } - init(callService: CallService, contactsManager: OWSContactsManager, notificationsAdapter: NotificationsAdapter, showNamesOnCallScreen: Bool, useSystemCallLog: Bool) { + init(callService: CallService, contactsManager: OWSContactsManager, notificationPresenter: NotificationPresenter, showNamesOnCallScreen: Bool, useSystemCallLog: Bool) { AssertIsOnMainThread() Logger.debug("") @@ -84,7 +84,7 @@ final class CallKitCallUIAdaptee: NSObject, CallUIAdaptee, CXProviderDelegate { self.callManager = CallKitCallManager(showNamesOnCallScreen: showNamesOnCallScreen) self.callService = callService self.contactsManager = contactsManager - self.notificationsAdapter = notificationsAdapter + self.notificationPresenter = notificationPresenter self.provider = type(of: self).sharedProvider(useSystemCallLog: useSystemCallLog) diff --git a/Signal/src/call/UserInterface/CallUIAdapter.swift b/Signal/src/call/UserInterface/CallUIAdapter.swift index cfa6209d4..d2a2ba0b5 100644 --- a/Signal/src/call/UserInterface/CallUIAdapter.swift +++ b/Signal/src/call/UserInterface/CallUIAdapter.swift @@ -10,7 +10,7 @@ import SignalMessaging import WebRTC protocol CallUIAdaptee { - var notificationsAdapter: NotificationsAdapter { get } + var notificationPresenter: NotificationPresenter { get } var callService: CallService { get } var hasManualRinger: Bool { get } @@ -60,7 +60,7 @@ extension CallUIAdaptee { internal func reportMissedCall(_ call: SignalCall, callerName: String) { AssertIsOnMainThread() - notificationsAdapter.presentMissedCall(call, callerName: callerName) + notificationPresenter.presentMissedCall(call, callerName: callerName) } internal func startAndShowOutgoingCall(recipientId: String, hasLocalVideo: Bool) { @@ -88,7 +88,7 @@ extension CallUIAdaptee { internal let audioService: CallAudioService internal let callService: CallService - public required init(callService: CallService, contactsManager: OWSContactsManager, notificationsAdapter: NotificationsAdapter) { + public required init(callService: CallService, contactsManager: OWSContactsManager, notificationPresenter: NotificationPresenter) { AssertIsOnMainThread() self.contactsManager = contactsManager @@ -99,13 +99,13 @@ extension CallUIAdaptee { // e.g. you can't receive calls in the call screen. // So we use the non-CallKit call UI. Logger.info("choosing non-callkit adaptee for simulator.") - adaptee = NonCallKitCallUIAdaptee(callService: callService, notificationsAdapter: notificationsAdapter) + adaptee = NonCallKitCallUIAdaptee(callService: callService, notificationPresenter: notificationPresenter) } else if #available(iOS 11, *) { Logger.info("choosing callkit adaptee for iOS11+") let showNames = Environment.shared.preferences.notificationPreviewType() != .noNameNoPreview let useSystemCallLog = Environment.shared.preferences.isSystemCallLogEnabled() - adaptee = CallKitCallUIAdaptee(callService: callService, contactsManager: contactsManager, notificationsAdapter: notificationsAdapter, showNamesOnCallScreen: showNames, useSystemCallLog: useSystemCallLog) + adaptee = CallKitCallUIAdaptee(callService: callService, contactsManager: contactsManager, notificationPresenter: notificationPresenter, showNamesOnCallScreen: showNames, useSystemCallLog: useSystemCallLog) } else if #available(iOS 10.0, *), Environment.shared.preferences.isCallKitEnabled() { Logger.info("choosing callkit adaptee for iOS10") let hideNames = Environment.shared.preferences.isCallKitPrivacyEnabled() || Environment.shared.preferences.notificationPreviewType() == .noNameNoPreview @@ -114,10 +114,10 @@ extension CallUIAdaptee { // All CallKit calls use the system call log on iOS10 let useSystemCallLog = true - adaptee = CallKitCallUIAdaptee(callService: callService, contactsManager: contactsManager, notificationsAdapter: notificationsAdapter, showNamesOnCallScreen: showNames, useSystemCallLog: useSystemCallLog) + adaptee = CallKitCallUIAdaptee(callService: callService, contactsManager: contactsManager, notificationPresenter: notificationPresenter, showNamesOnCallScreen: showNames, useSystemCallLog: useSystemCallLog) } else { Logger.info("choosing non-callkit adaptee") - adaptee = NonCallKitCallUIAdaptee(callService: callService, notificationsAdapter: notificationsAdapter) + adaptee = NonCallKitCallUIAdaptee(callService: callService, notificationPresenter: notificationPresenter) } audioService = CallAudioService(handleRinging: adaptee.hasManualRinger) diff --git a/Signal/src/call/UserInterface/OWSCallNotificationsAdaptee.h b/Signal/src/call/UserInterface/OWSCallNotificationsAdaptee.h deleted file mode 100644 index 4e6c12c18..000000000 --- a/Signal/src/call/UserInterface/OWSCallNotificationsAdaptee.h +++ /dev/null @@ -1,26 +0,0 @@ -// -// Copyright (c) 2017 Open Whisper Systems. All rights reserved. -// - -NS_ASSUME_NONNULL_BEGIN - -@class OWSRecipientIdentity; -@class SignalCall; - -@protocol OWSCallNotificationsAdaptee - -- (void)presentIncomingCall:(SignalCall *)call callerName:(NSString *)callerName; - -- (void)presentMissedCall:(SignalCall *)call callerName:(NSString *)callerName; - -- (void)presentMissedCallBecauseOfNewIdentity:(SignalCall *)call - callerName:(NSString *)callerName - NS_SWIFT_NAME(presentMissedCallBecauseOfNewIdentity(call:callerName:)); - -- (void)presentMissedCallBecauseOfNoLongerVerifiedIdentity:(SignalCall *)call - callerName:(NSString *)callerName - NS_SWIFT_NAME(presentMissedCallBecauseOfNoLongerVerifiedIdentity(call:callerName:)); - -@end - -NS_ASSUME_NONNULL_END diff --git a/Signal/src/environment/AppEnvironment.swift b/Signal/src/environment/AppEnvironment.swift index 225d80660..b6acf63d1 100644 --- a/Signal/src/environment/AppEnvironment.swift +++ b/Signal/src/environment/AppEnvironment.swift @@ -41,14 +41,11 @@ import SignalMessaging public var accountManager: AccountManager @objc - public var notificationsAdapter: NotificationsAdapter + public var notificationPresenter: NotificationPresenter @objc public var pushRegistrationManager: PushRegistrationManager - @objc - public var pushManager: PushManager - @objc public var sessionResetJobQueue: SessionResetJobQueue @@ -64,9 +61,8 @@ import SignalMessaging self.outboundCallInitiator = OutboundCallInitiator() self.messageFetcherJob = MessageFetcherJob() self.accountManager = AccountManager() - self.notificationsAdapter = NotificationsAdapter() + self.notificationPresenter = NotificationPresenter() self.pushRegistrationManager = PushRegistrationManager() - self.pushManager = PushManager() self.sessionResetJobQueue = SessionResetJobQueue() self.backup = OWSBackup() self.backupLazyRestore = BackupLazyRestore() @@ -81,7 +77,7 @@ import SignalMessaging callService.createCallUIAdapter() // Hang certain singletons on SSKEnvironment too. - SSKEnvironment.shared.notificationsManager = notificationsAdapter + SSKEnvironment.shared.notificationsManager = notificationPresenter SSKEnvironment.shared.callMessageHandler = callMessageHandler } } diff --git a/Signal/src/environment/NotificationsManager.h b/Signal/src/environment/NotificationsManager.h deleted file mode 100644 index 0d2523ed6..000000000 --- a/Signal/src/environment/NotificationsManager.h +++ /dev/null @@ -1,26 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import "OWSCallNotificationsAdaptee.h" -#import - -NS_ASSUME_NONNULL_BEGIN - -@class OWSContactsManager; -@class OWSPreferences; -@class SignalCall; -@class TSCall; -@class TSContactThread; - -@interface NotificationsManager : NSObject - -#ifdef DEBUG - -+ (void)presentDebugNotification; - -#endif - -@end - -NS_ASSUME_NONNULL_END diff --git a/Signal/src/environment/NotificationsManager.m b/Signal/src/environment/NotificationsManager.m deleted file mode 100644 index 7a530bc94..000000000 --- a/Signal/src/environment/NotificationsManager.m +++ /dev/null @@ -1,505 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import "NotificationsManager.h" -#import "PushManager.h" -#import "Signal-Swift.h" -#import -#import -#import -#import -#import -#import -#import -#import -#import -#import -#import -#import -#import - -@interface NotificationsManager () - -@property (nonatomic, readonly) NSMutableDictionary *currentNotifications; -@property (nonatomic, readonly) NotificationType notificationPreviewType; - -@property (nonatomic, readonly) NSMutableArray *notificationHistory; -@property (nonatomic, nullable) OWSAudioPlayer *audioPlayer; - -@end - -#pragma mark - - -@implementation NotificationsManager - -- (instancetype)init -{ - self = [super init]; - if (!self) { - return self; - } - - _currentNotifications = [NSMutableDictionary new]; - - _notificationHistory = [NSMutableArray new]; - - OWSSingletonAssert(); - - return self; -} - -#pragma mark - Signal Calls - -/** - * Notify user for incoming WebRTC Call - */ -- (void)presentIncomingCall:(SignalCall *)call callerName:(NSString *)callerName -{ - OWSLogDebug(@"incoming call from: %@", call.remotePhoneNumber); - - UILocalNotification *notification = [UILocalNotification new]; - notification.category = PushManagerCategoriesIncomingCall; - // Rather than using notification sounds, we control the ringtone and repeat vibrations with the CallAudioManager. - notification.soundName = [OWSSounds filenameForSound:OWSSound_DefaultiOSIncomingRingtone]; - NSString *localCallId = call.localId.UUIDString; - notification.userInfo = @{ PushManagerUserInfoKeysLocalCallId : localCallId }; - - NSString *alertMessage; - switch (self.notificationPreviewType) { - case NotificationNoNameNoPreview: { - alertMessage = NSLocalizedString(@"INCOMING_CALL", @"notification body"); - break; - } - case NotificationNameNoPreview: - case NotificationNamePreview: { - alertMessage = - [NSString stringWithFormat:NSLocalizedString(@"INCOMING_CALL_FROM", @"notification body"), callerName]; - break; - } - } - notification.alertBody = [NSString stringWithFormat:@"☎️ %@", alertMessage]; - - [self presentNotification:notification identifier:localCallId]; -} - -/** - * Notify user for missed WebRTC Call - */ -- (void)presentMissedCall:(SignalCall *)call callerName:(NSString *)callerName -{ - TSContactThread *thread = [TSContactThread getOrCreateThreadWithContactId:call.remotePhoneNumber]; - OWSAssertDebug(thread != nil); - - UILocalNotification *notification = [UILocalNotification new]; - notification.category = PushManagerCategoriesMissedCall; - NSString *localCallId = call.localId.UUIDString; - notification.userInfo = @{ - PushManagerUserInfoKeysLocalCallId : localCallId, - PushManagerUserInfoKeysCallBackSignalRecipientId : call.remotePhoneNumber, - Signal_Thread_UserInfo_Key : thread.uniqueId - }; - - if ([self shouldPlaySoundForNotification]) { - OWSSound sound = [OWSSounds notificationSoundForThread:thread]; - notification.soundName = [OWSSounds filenameForSound:sound]; - } - - NSString *alertMessage; - switch (self.notificationPreviewType) { - case NotificationNoNameNoPreview: { - alertMessage = [CallStrings missedCallNotificationBodyWithoutCallerName]; - break; - } - case NotificationNameNoPreview: - case NotificationNamePreview: { - alertMessage = - [NSString stringWithFormat:[CallStrings missedCallNotificationBodyWithCallerName], callerName]; - break; - } - } - notification.alertBody = [NSString stringWithFormat:@"☎️ %@", alertMessage]; - - [self presentNotification:notification identifier:localCallId]; -} - - -- (void)presentMissedCallBecauseOfNewIdentity:(SignalCall *)call callerName:(NSString *)callerName -{ - TSContactThread *thread = [TSContactThread getOrCreateThreadWithContactId:call.remotePhoneNumber]; - OWSAssertDebug(thread != nil); - - UILocalNotification *notification = [UILocalNotification new]; - // Use category which allows call back - notification.category = PushManagerCategoriesMissedCall; - NSString *localCallId = call.localId.UUIDString; - notification.userInfo = @{ - PushManagerUserInfoKeysLocalCallId : localCallId, - PushManagerUserInfoKeysCallBackSignalRecipientId : call.remotePhoneNumber, - Signal_Thread_UserInfo_Key : thread.uniqueId - }; - if ([self shouldPlaySoundForNotification]) { - OWSSound sound = [OWSSounds notificationSoundForThread:thread]; - notification.soundName = [OWSSounds filenameForSound:sound]; - } - - NSString *alertMessage; - switch (self.notificationPreviewType) { - case NotificationNoNameNoPreview: { - alertMessage = [CallStrings missedCallWithIdentityChangeNotificationBodyWithoutCallerName]; - break; - } - case NotificationNameNoPreview: - case NotificationNamePreview: { - alertMessage = [NSString - stringWithFormat:[CallStrings missedCallWithIdentityChangeNotificationBodyWithCallerName], callerName]; - break; - } - } - notification.alertBody = [NSString stringWithFormat:@"☎️ %@", alertMessage]; - - [self presentNotification:notification identifier:localCallId]; -} - -- (void)presentMissedCallBecauseOfNoLongerVerifiedIdentity:(SignalCall *)call callerName:(NSString *)callerName -{ - TSContactThread *thread = [TSContactThread getOrCreateThreadWithContactId:call.remotePhoneNumber]; - OWSAssertDebug(thread != nil); - - UILocalNotification *notification = [UILocalNotification new]; - // Use category which does not allow call back - notification.category = PushManagerCategoriesMissedCallFromNoLongerVerifiedIdentity; - NSString *localCallId = call.localId.UUIDString; - notification.userInfo = @{ - PushManagerUserInfoKeysLocalCallId : localCallId, - PushManagerUserInfoKeysCallBackSignalRecipientId : call.remotePhoneNumber, - Signal_Thread_UserInfo_Key : thread.uniqueId - }; - if ([self shouldPlaySoundForNotification]) { - OWSSound sound = [OWSSounds notificationSoundForThread:thread]; - notification.soundName = [OWSSounds filenameForSound:sound]; - } - - NSString *alertMessage; - switch (self.notificationPreviewType) { - case NotificationNoNameNoPreview: { - alertMessage = [CallStrings missedCallWithIdentityChangeNotificationBodyWithoutCallerName]; - break; - } - case NotificationNameNoPreview: - case NotificationNamePreview: { - alertMessage = [NSString - stringWithFormat:[CallStrings missedCallWithIdentityChangeNotificationBodyWithCallerName], callerName]; - break; - } - } - notification.alertBody = [NSString stringWithFormat:@"☎️ %@", alertMessage]; - - [self presentNotification:notification identifier:localCallId]; -} - -#pragma mark - Signal Messages - -- (void)notifyUserForErrorMessage:(TSErrorMessage *)message - thread:(TSThread *)thread - transaction:(YapDatabaseReadWriteTransaction *)transaction -{ - OWSAssertDebug(message); - - if (!thread) { - OWSFailDebug(@"unexpected notification not associated with a thread: %@.", [message class]); - [self notifyUserForThreadlessErrorMessage:message transaction:transaction]; - return; - } - - NSString *messageText = [message previewTextWithTransaction:transaction]; - - [transaction - addCompletionQueue:nil - completionBlock:^() { - if (thread.isMuted) { - return; - } - - BOOL shouldPlaySound = [self shouldPlaySoundForNotification]; - - if (([UIApplication sharedApplication].applicationState != UIApplicationStateActive) && messageText) { - UILocalNotification *notification = [[UILocalNotification alloc] init]; - notification.userInfo = @{ Signal_Thread_UserInfo_Key : thread.uniqueId }; - if (shouldPlaySound) { - OWSSound sound = [OWSSounds notificationSoundForThread:thread]; - notification.soundName = [OWSSounds filenameForSound:sound]; - } - - NSString *alertBodyString = @""; - - NSString *authorName = [thread name]; - switch (self.notificationPreviewType) { - case NotificationNamePreview: - case NotificationNameNoPreview: - alertBodyString = [NSString stringWithFormat:@"%@: %@", authorName, messageText]; - break; - case NotificationNoNameNoPreview: - alertBodyString = messageText; - break; - } - notification.alertBody = alertBodyString; - - [[PushManager sharedManager] presentNotification:notification checkForCancel:NO]; - } else { - if (shouldPlaySound && [Environment.shared.preferences soundInForeground]) { - OWSSound sound = [OWSSounds notificationSoundForThread:thread]; - SystemSoundID soundId = [OWSSounds systemSoundIDForSound:sound quiet:YES]; - // Vibrate, respect silent switch, respect "Alert" volume, not media volume. - AudioServicesPlayAlertSound(soundId); - } - } - }]; -} - -- (void)notifyUserForThreadlessErrorMessage:(TSErrorMessage *)message - transaction:(YapDatabaseReadWriteTransaction *)transaction; -{ - OWSAssertDebug(message); - - NSString *messageText = [message previewTextWithTransaction:transaction]; - - [transaction - addCompletionQueue:nil - completionBlock:^() { - BOOL shouldPlaySound = [self shouldPlaySoundForNotification]; - - if (([UIApplication sharedApplication].applicationState != UIApplicationStateActive) && messageText) { - UILocalNotification *notification = [[UILocalNotification alloc] init]; - if (shouldPlaySound) { - OWSSound sound = [OWSSounds globalNotificationSound]; - notification.soundName = [OWSSounds filenameForSound:sound]; - } - - NSString *alertBodyString = messageText; - notification.alertBody = alertBodyString; - - [[PushManager sharedManager] presentNotification:notification checkForCancel:NO]; - } else { - if (shouldPlaySound && [Environment.shared.preferences soundInForeground]) { - OWSSound sound = [OWSSounds globalNotificationSound]; - SystemSoundID soundId = [OWSSounds systemSoundIDForSound:sound quiet:YES]; - // Vibrate, respect silent switch, respect "Alert" volume, not media volume. - AudioServicesPlayAlertSound(soundId); - } - } - }]; -} - -- (void)notifyUserForIncomingMessage:(TSIncomingMessage *)message - inThread:(TSThread *)thread - contactsManager:(id)contactsManager - transaction:(YapDatabaseReadTransaction *)transaction -{ - OWSAssertDebug(message); - OWSAssertDebug(thread); - OWSAssertDebug(contactsManager); - - // While batch processing, some of the necessary changes have not been commited. - NSString *rawMessageText = [message previewTextWithTransaction:transaction]; - - // iOS strips anything that looks like a printf formatting character from - // the notification body, so if we want to dispay a literal "%" in a notification - // it must be escaped. - // see https://developer.apple.com/documentation/uikit/uilocalnotification/1616646-alertbody - // for more details. - NSString *messageText = [DisplayableText filterNotificationText:rawMessageText]; - - dispatch_async(dispatch_get_main_queue(), ^{ - if (thread.isMuted) { - return; - } - - BOOL shouldPlaySound = [self shouldPlaySoundForNotification]; - - NSString *senderName = [contactsManager displayNameForPhoneIdentifier:message.authorId]; - NSString *groupName = [thread.name ows_stripped]; - if (groupName.length < 1) { - groupName = [MessageStrings newGroupDefaultTitle]; - } - - if ([UIApplication sharedApplication].applicationState != UIApplicationStateActive && messageText) { - UILocalNotification *notification = [[UILocalNotification alloc] init]; - if (shouldPlaySound) { - OWSSound sound = [OWSSounds notificationSoundForThread:thread]; - notification.soundName = [OWSSounds filenameForSound:sound]; - } - - switch (self.notificationPreviewType) { - case NotificationNamePreview: { - - // Don't reply from lockscreen if anyone in this conversation is - // "no longer verified". - BOOL isNoLongerVerified = NO; - for (NSString *recipientId in thread.recipientIdentifiers) { - if ([OWSIdentityManager.sharedManager verificationStateForRecipientId:recipientId] - == OWSVerificationStateNoLongerVerified) { - isNoLongerVerified = YES; - break; - } - } - - notification.category = (isNoLongerVerified ? Signal_Full_New_Message_Category_No_Longer_Verified - : Signal_Full_New_Message_Category); - notification.userInfo = @{ - Signal_Thread_UserInfo_Key : thread.uniqueId, - Signal_Message_UserInfo_Key : message.uniqueId - }; - - if ([thread isGroupThread]) { - NSString *threadName = [NSString stringWithFormat:@"\"%@\"", groupName]; - - // TODO: Format parameters might change order in l10n. We should use named parameters. - notification.alertBody = - [NSString stringWithFormat:NSLocalizedString(@"APN_MESSAGE_IN_GROUP_DETAILED", nil), - senderName, - threadName, - messageText]; - - } else { - notification.alertBody = [NSString stringWithFormat:@"%@: %@", senderName, messageText]; - } - break; - } - case NotificationNameNoPreview: { - notification.userInfo = @{ Signal_Thread_UserInfo_Key : thread.uniqueId }; - if ([thread isGroupThread]) { - notification.alertBody = [NSString - stringWithFormat:@"%@ \"%@\"", NSLocalizedString(@"APN_MESSAGE_IN_GROUP", nil), groupName]; - } else { - notification.alertBody = [NSString - stringWithFormat:@"%@ %@", NSLocalizedString(@"APN_MESSAGE_FROM", nil), senderName]; - } - break; - } - case NotificationNoNameNoPreview: - notification.alertBody = NSLocalizedString(@"APN_Message", nil); - break; - default: - OWSLogWarn(@"unknown notification preview type: %lu", (unsigned long)self.notificationPreviewType); - notification.alertBody = NSLocalizedString(@"APN_Message", nil); - break; - } - - [[PushManager sharedManager] presentNotification:notification checkForCancel:YES]; - } else { - if (shouldPlaySound && [Environment.shared.preferences soundInForeground]) { - OWSSound sound = [OWSSounds notificationSoundForThread:thread]; - SystemSoundID soundId = [OWSSounds systemSoundIDForSound:sound quiet:YES]; - // Vibrate, respect silent switch, respect "Alert" volume, not media volume. - AudioServicesPlayAlertSound(soundId); - } - } - }); -} - -- (BOOL)shouldPlaySoundForNotification -{ - @synchronized(self) - { - // Play no more than 2 notification sounds in a given - // five-second window. - const CGFloat kNotificationWindowSeconds = 5.f; - const NSUInteger kMaxNotificationRate = 2; - - // Cull obsolete notification timestamps from the thread's notification history. - while (self.notificationHistory.count > 0) { - NSDate *notificationTimestamp = self.notificationHistory[0]; - CGFloat notificationAgeSeconds = fabs(notificationTimestamp.timeIntervalSinceNow); - if (notificationAgeSeconds > kNotificationWindowSeconds) { - [self.notificationHistory removeObjectAtIndex:0]; - } else { - break; - } - } - - // Ignore notifications if necessary. - BOOL shouldPlaySound = self.notificationHistory.count < kMaxNotificationRate; - - if (shouldPlaySound) { - // Add new notification timestamp to the thread's notification history. - NSDate *newNotificationTimestamp = [NSDate new]; - [self.notificationHistory addObject:newNotificationTimestamp]; - - return YES; - } else { - OWSLogDebug(@"Skipping sound for notification"); - return NO; - } - } -} - -#pragma mark - Util - -- (NotificationType)notificationPreviewType -{ - OWSPreferences *prefs = Environment.shared.preferences; - return prefs.notificationPreviewType; -} - -- (void)presentNotification:(UILocalNotification *)notification identifier:(NSString *)identifier -{ - notification.alertBody = notification.alertBody.filterStringForDisplay; - - DispatchMainThreadSafe(^{ - if (UIApplication.sharedApplication.applicationState == UIApplicationStateActive) { - OWSLogWarn(@"skipping notification; app is in foreground and active."); - return; - } - - // Replace any existing notification - // e.g. when an "Incoming Call" notification gets replaced with a "Missed Call" notification. - if (self.currentNotifications[identifier]) { - [self cancelNotificationWithIdentifier:identifier]; - } - - [[UIApplication sharedApplication] scheduleLocalNotification:notification]; - OWSLogDebug(@"presenting notification with identifier: %@", identifier); - - self.currentNotifications[identifier] = notification; - }); -} - -- (void)cancelNotificationWithIdentifier:(NSString *)identifier -{ - DispatchMainThreadSafe(^{ - UILocalNotification *notification = self.currentNotifications[identifier]; - if (!notification) { - OWSLogWarn(@"Couldn't cancel notification because none was found with identifier: %@", identifier); - return; - } - [self.currentNotifications removeObjectForKey:identifier]; - - [[UIApplication sharedApplication] cancelLocalNotification:notification]; - }); -} - -#ifdef DEBUG - -+ (void)presentDebugNotification -{ - OWSAssertIsOnMainThread(); - - UILocalNotification *notification = [UILocalNotification new]; - notification.category = Signal_Full_New_Message_Category; - notification.soundName = [OWSSounds filenameForSound:OWSSound_DefaultiOSIncomingRingtone]; - notification.alertBody = @"test"; - - [[UIApplication sharedApplication] scheduleLocalNotification:notification]; -} - -#endif - -- (void)clearAllNotifications -{ - OWSAssertIsOnMainThread(); - - [self.currentNotifications removeAllObjects]; -} - -@end diff --git a/Signal/src/environment/PushRegistrationManager.swift b/Signal/src/environment/PushRegistrationManager.swift index c91ac321b..44ab9cb21 100644 --- a/Signal/src/environment/PushRegistrationManager.swift +++ b/Signal/src/environment/PushRegistrationManager.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // import Foundation @@ -21,8 +21,12 @@ public enum PushRegistrationError: Error { // MARK: - Dependencies - private var pushManager: PushManager { - return PushManager.shared() + private var messageFetcherJob: MessageFetcherJob { + return AppEnvironment.shared.messageFetcherJob + } + + private var notificationPresenter: NotificationPresenter { + return AppEnvironment.shared.notificationPresenter } // MARK: - Singleton class @@ -40,9 +44,6 @@ public enum PushRegistrationError: Error { SwiftSingletons.register(self) } - private var userNotificationSettingsPromise: Promise? - private var userNotificationSettingsResolver: Resolver? - private var vanillaTokenPromise: Promise? private var vanillaTokenResolver: Resolver? @@ -70,21 +71,6 @@ public enum PushRegistrationError: Error { } } - // Notification registration is confirmed via AppDelegate - // Before this occurs, it is not safe to assume push token requests will be acknowledged. - // - // e.g. in the case that Background Fetch is disabled, token requests will be ignored until - // we register user notification settings. - @objc - public func didRegisterUserNotificationSettings() { - guard let userNotificationSettingsResolver = self.userNotificationSettingsResolver else { - owsFailDebug("promise completion in \(#function) unexpectedly nil") - return - } - - userNotificationSettingsResolver.fulfill(()) - } - // MARK: Vanilla push token // Vanilla push token is obtained from the system via AppDelegate @@ -114,7 +100,9 @@ public enum PushRegistrationError: Error { public func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType) { Logger.info("") assert(type == .voIP) - self.pushManager.application(UIApplication.shared, didReceiveRemoteNotification: payload.dictionaryPayload) + AppReadiness.runNowOrWhenAppDidBecomeReady { + (self.messageFetcherJob.run() as Promise).retainUntilComplete() + } } public func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) { @@ -138,26 +126,11 @@ public enum PushRegistrationError: Error { // MARK: helpers // User notification settings must be registered *before* AppDelegate will - // return any requested push tokens. We don't consider the notifications settings registration - // *complete* until AppDelegate#didRegisterUserNotificationSettings is called. + // return any requested push tokens. private func registerUserNotificationSettings() -> Promise { AssertIsOnMainThread() - - guard self.userNotificationSettingsPromise == nil else { - let promise = self.userNotificationSettingsPromise! - Logger.info("already registered user notification settings") - return promise - } - - let (promise, resolver) = Promise.pending() - self.userNotificationSettingsPromise = promise - self.userNotificationSettingsResolver = resolver - Logger.info("registering user notification settings") - - UIApplication.shared.registerUserNotificationSettings(self.pushManager.userNotificationSettings) - - return promise + return notificationPresenter.registerNotificationSettings() } /** diff --git a/Signal/src/network/PushManager.h b/Signal/src/network/PushManager.h deleted file mode 100644 index d977c265f..000000000 --- a/Signal/src/network/PushManager.h +++ /dev/null @@ -1,75 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -NS_ASSUME_NONNULL_BEGIN - -@class UILocalNotification; - -extern NSString *const Signal_Thread_UserInfo_Key; -extern NSString *const Signal_Message_UserInfo_Key; - -extern NSString *const Signal_Full_New_Message_Category; -extern NSString *const Signal_Full_New_Message_Category_No_Longer_Verified; - -extern NSString *const Signal_Message_Reply_Identifier; -extern NSString *const Signal_Message_MarkAsRead_Identifier; - -#pragma mark Signal Calls constants - -extern NSString *const PushManagerCategoriesIncomingCall; -extern NSString *const PushManagerCategoriesMissedCall; -extern NSString *const PushManagerCategoriesMissedCallFromNoLongerVerifiedIdentity; - -extern NSString *const PushManagerActionsAcceptCall; -extern NSString *const PushManagerActionsDeclineCall; -extern NSString *const PushManagerActionsCallBack; -extern NSString *const PushManagerActionsShowThread; - -extern NSString *const PushManagerUserInfoKeysCallBackSignalRecipientId; -extern NSString *const PushManagerUserInfoKeysLocalCallId; - -typedef void (^failedPushRegistrationBlock)(NSError *error); -typedef void (^pushTokensSuccessBlock)(NSString *pushToken, NSString *voipToken); - -/** - * The Push Manager is responsible for handling received push notifications. - */ -@interface PushManager : NSObject - -@property (nonatomic) BOOL hasPresentedConversationSinceLastDeactivation; - -+ (PushManager *)sharedManager; - -/** - * Settings required for the notification categories we use. - */ -@property (nonatomic, readonly) UIUserNotificationSettings *userNotificationSettings; - -// If checkForCancel is set, the notification will be delayed for -// a moment. If a relevant cancel notification is received in that window, -// the notification will not be displayed. -- (void)presentNotification:(UILocalNotification *)notification checkForCancel:(BOOL)checkForCancel; -- (void)cancelNotificationsWithThreadId:(NSString *)threadId; - -#pragma mark Push Notifications Delegate Methods - -- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo; -- (void)application:(UIApplication *)application - didReceiveRemoteNotification:(NSDictionary *)userInfo - fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler; -- (void)application:(UIApplication *)application - handleActionWithIdentifier:(NSString *)identifier - forLocalNotification:(UILocalNotification *)notification - completionHandler:(void (^)(void))completionHandler; -- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification; -- (void)application:(UIApplication *)application - handleActionWithIdentifier:(NSString *)identifier - forLocalNotification:(UILocalNotification *)notification - withResponseInfo:(NSDictionary *)responseInfo - completionHandler:(void (^)(void))completionHandler; -- (void)applicationDidBecomeActive; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Signal/src/network/PushManager.m b/Signal/src/network/PushManager.m deleted file mode 100644 index d880fb95c..000000000 --- a/Signal/src/network/PushManager.m +++ /dev/null @@ -1,504 +0,0 @@ -// -// Copyright (c) 2019 Open Whisper Systems. All rights reserved. -// - -#import "PushManager.h" -#import "AppDelegate.h" -#import "Signal-Swift.h" -#import "SignalApp.h" -#import "ThreadUtil.h" -#import -#import -#import -#import -#import -#import -#import -#import -#import -#import -#import -#import - -NSString *const Signal_Thread_UserInfo_Key = @"Signal_Thread_Id"; -NSString *const Signal_Message_UserInfo_Key = @"Signal_Message_Id"; - -NSString *const Signal_Full_New_Message_Category = @"Signal_Full_New_Message"; -NSString *const Signal_Full_New_Message_Category_No_Longer_Verified = - @"Signal_Full_New_Message_Category_No_Longer_Verified"; - -NSString *const Signal_Message_Reply_Identifier = @"Signal_New_Message_Reply"; -NSString *const Signal_Message_MarkAsRead_Identifier = @"Signal_Message_MarkAsRead"; - -@interface PushManager () - -@property (nonatomic) NSMutableArray *currentNotifications; -@property (nonatomic) UIBackgroundTaskIdentifier callBackgroundTask; - -@end - -@implementation PushManager - -+ (instancetype)sharedManager { - OWSAssertDebug(AppEnvironment.shared.pushManager); - - return AppEnvironment.shared.pushManager; -} - -- (instancetype)init { - self = [super init]; - if (!self) { - return self; - } - - _callBackgroundTask = UIBackgroundTaskInvalid; - // TODO: consolidate notification tracking with NotificationsManager, which also maintains a list of notifications. - _currentNotifications = [NSMutableArray array]; - - OWSSingletonAssert(); - - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(handleMessageRead:) - name:kIncomingMessageMarkedAsReadNotification - object:nil]; - - return self; -} - -#pragma mark - Dependencies - -- (OWSMessageSender *)messageSender { - return SSKEnvironment.shared.messageSender; -} - -- (OWSMessageFetcherJob *)messageFetcherJob { - return AppEnvironment.shared.messageFetcherJob; -} - -- (id)notificationsManager { - return SSKEnvironment.shared.notificationsManager; -} - -#pragma mark - - -- (CallUIAdapter *)callUIAdapter -{ - return AppEnvironment.shared.callService.callUIAdapter; -} - -- (void)handleMessageRead:(NSNotification *)notification -{ - OWSAssertIsOnMainThread(); - - if ([notification.object isKindOfClass:[TSIncomingMessage class]]) { - TSIncomingMessage *message = (TSIncomingMessage *)notification.object; - - OWSLogDebug(@"canceled notification for message:%@", message); - [self cancelNotificationsWithThreadId:message.uniqueThreadId]; - } -} - -#pragma mark Manage Incoming Push - -- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo -{ - OWSLogInfo(@"received remote notification"); - - [AppReadiness runNowOrWhenAppDidBecomeReady:^{ - [self.messageFetcherJob run]; - }]; -} - -- (void)applicationDidBecomeActive { - [AppReadiness runNowOrWhenAppDidBecomeReady:^{ - [self.messageFetcherJob run]; - }]; -} - -/** - * This code should in principle never be called. The only cases where it would be called are with the old-style - * "content-available:1" pushes if there is no "voip" token registered - * - */ -- (void)application:(UIApplication *)application - didReceiveRemoteNotification:(NSDictionary *)userInfo - fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler -{ - OWSLogInfo(@"received content-available push"); - - // If we want to re-introduce silent pushes we can remove this assert. - OWSFailDebug(@"Unexpected content-available push."); - - [AppReadiness runNowOrWhenAppDidBecomeReady:^{ - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 20 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ - completionHandler(UIBackgroundFetchResultNewData); - }); - }]; -} - -- (void)presentOncePerActivationConversationWithThreadId:(NSString *)threadId -{ - if (self.hasPresentedConversationSinceLastDeactivation) { - OWSFailDebug(@"refusing to present conversation: %@ multiple times.", threadId); - return; - } - - self.hasPresentedConversationSinceLastDeactivation = YES; - - // This will happen before the app is visible. By making this animated:NO, the conversation screen - // will be visible to the user immediately upon opening the app, rather than having to watch it animate - // in from the homescreen. - [SignalApp.sharedApp presentConversationForThreadId:threadId animated:NO]; -} - -- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification -{ - OWSAssertIsOnMainThread(); - OWSLogInfo(@"launched from local notification"); - - NSString *_Nullable threadId = notification.userInfo[Signal_Thread_UserInfo_Key]; - - if (threadId) { - [self presentOncePerActivationConversationWithThreadId:threadId]; - } else { - OWSFailDebug(@"threadId was unexpectedly nil"); - } - - // We only want to receive a single local notification per launch. - [application cancelAllLocalNotifications]; - [self.currentNotifications removeAllObjects]; - [self.notificationsManager clearAllNotifications]; -} - -- (void)application:(UIApplication *)application - handleActionWithIdentifier:(NSString *)identifier - forLocalNotification:(UILocalNotification *)notification - completionHandler:(void (^)(void))completionHandler -{ - OWSLogInfo(@"in %s", __FUNCTION__); - - [self application:application - handleActionWithIdentifier:identifier - forLocalNotification:notification - withResponseInfo:@{} - completionHandler:completionHandler]; -} - -- (void)application:(UIApplication *)application - handleActionWithIdentifier:(NSString *)identifier - forLocalNotification:(UILocalNotification *)notification - withResponseInfo:(NSDictionary *)responseInfo - completionHandler:(void (^)(void))completionHandler -{ - OWSLogInfo(@"handling action with identifier: %@", identifier); - - if ([identifier isEqualToString:Signal_Message_Reply_Identifier]) { - NSString *threadId = notification.userInfo[Signal_Thread_UserInfo_Key]; - - if (threadId) { - TSThread *thread = [TSThread fetchObjectWithUniqueID:threadId]; - NSString *replyText = responseInfo[UIUserNotificationActionResponseTypedTextKey]; - - // In line with most apps, we send a normal outgoing messgae here - not a "quoted reply". - - // We use a non-durable send to delay calling the completion handler until sending completes - // in hopes our send will complete before the app gets suspended. - [ThreadUtil sendMessageNonDurablyWithText:replyText - inThread:thread - quotedReplyModel:nil - messageSender:self.messageSender - success:^{ - // TODO do we really want to mark them all as read? - [self markAllInThreadAsRead:notification.userInfo completionHandler:completionHandler]; - } - failure:^(NSError *_Nonnull error) { - // TODO Surface the specific error in the notification? - OWSLogError(@"Message send failed with error: %@", error); - - UILocalNotification *failedSendNotif = [[UILocalNotification alloc] init]; - failedSendNotif.alertBody = - [NSString stringWithFormat:NSLocalizedString(@"NOTIFICATION_SEND_FAILED", nil), [thread name]] - .filterStringForDisplay; - failedSendNotif.userInfo = @{ Signal_Thread_UserInfo_Key : thread.uniqueId }; - [self presentNotification:failedSendNotif checkForCancel:NO]; - completionHandler(); - }]; - } - } else if ([identifier isEqualToString:Signal_Message_MarkAsRead_Identifier]) { - // TODO mark all as read? Or just this one? - [self markAllInThreadAsRead:notification.userInfo completionHandler:completionHandler]; - } else if ([identifier isEqualToString:PushManagerActionsAcceptCall]) { - NSString *localIdString = notification.userInfo[PushManagerUserInfoKeysLocalCallId]; - if (!localIdString) { - OWSLogError(@"missing localIdString."); - return; - } - - NSUUID *localId = [[NSUUID alloc] initWithUUIDString:localIdString]; - if (!localId) { - OWSLogError(@"localIdString failed to parse as UUID."); - return; - } - - [self.callUIAdapter answerCallWithLocalId:localId]; - completionHandler(); - } else if ([identifier isEqualToString:PushManagerActionsDeclineCall]) { - NSString *localIdString = notification.userInfo[PushManagerUserInfoKeysLocalCallId]; - if (!localIdString) { - OWSLogError(@"missing localIdString."); - return; - } - - NSUUID *localId = [[NSUUID alloc] initWithUUIDString:localIdString]; - if (!localId) { - OWSLogError(@"localIdString failed to parse as UUID."); - return; - } - - [self.callUIAdapter declineCallWithLocalId:localId]; - completionHandler(); - } else if ([identifier isEqualToString:PushManagerActionsCallBack]) { - NSString *recipientId = notification.userInfo[PushManagerUserInfoKeysCallBackSignalRecipientId]; - if (!recipientId) { - OWSLogError(@"missing call back id"); - return; - } - - [self.callUIAdapter startAndShowOutgoingCallWithRecipientId:recipientId hasLocalVideo:NO]; - completionHandler(); - } else if ([identifier isEqualToString:PushManagerActionsShowThread]) { - NSString *threadId = notification.userInfo[Signal_Thread_UserInfo_Key]; - - if (threadId) { - [self presentOncePerActivationConversationWithThreadId:threadId]; - } else { - OWSFailDebug(@"threadId was unexpectedly nil in action with identifier: %@", identifier); - } - completionHandler(); - } else { - OWSFailDebug(@"Unhandled action with identifier: %@", identifier); - NSString *threadId = notification.userInfo[Signal_Thread_UserInfo_Key]; - if (threadId) { - [self presentOncePerActivationConversationWithThreadId:threadId]; - } else { - OWSFailDebug(@"threadId was unexpectedly nil in action with identifier: %@", identifier); - } - completionHandler(); - } -} - -- (void)markAllInThreadAsRead:(NSDictionary *)userInfo completionHandler:(void (^)(void))completionHandler -{ - NSString *threadId = userInfo[Signal_Thread_UserInfo_Key]; - if (!threadId) { - OWSFailDebug(@"missing thread id for notification."); - return; - } - - TSThread *thread = [TSThread fetchObjectWithUniqueID:threadId]; - [OWSPrimaryStorage.dbReadWriteConnection - asyncReadWriteWithBlock:^(YapDatabaseReadWriteTransaction *_Nonnull transaction) { - // TODO: I suspect we only want to mark the message in - // question as read. - [thread markAllAsReadWithTransaction:transaction]; - } - completionBlock:^{ - [self cancelNotificationsWithThreadId:threadId]; - - completionHandler(); - }]; -} - -- (UIUserNotificationCategory *)fullNewMessageNotificationCategory { - UIMutableUserNotificationAction *action_markRead = [self markAsReadAction]; - - UIMutableUserNotificationAction *action_reply = [UIMutableUserNotificationAction new]; - action_reply.identifier = Signal_Message_Reply_Identifier; - action_reply.title = NSLocalizedString(@"PUSH_MANAGER_REPLY", @""); - action_reply.destructive = NO; - action_reply.authenticationRequired = NO; - action_reply.behavior = UIUserNotificationActionBehaviorTextInput; - action_reply.activationMode = UIUserNotificationActivationModeBackground; - - UIMutableUserNotificationCategory *messageCategory = [UIMutableUserNotificationCategory new]; - messageCategory.identifier = Signal_Full_New_Message_Category; - [messageCategory setActions:@[ action_markRead, action_reply ] forContext:UIUserNotificationActionContextMinimal]; - [messageCategory setActions:@[ action_markRead, action_reply ] forContext:UIUserNotificationActionContextDefault]; - - return messageCategory; -} - -- (UIUserNotificationCategory *)fullNewMessageNoLongerVerifiedNotificationCategory -{ - UIMutableUserNotificationAction *action_markRead = [self markAsReadAction]; - - UIMutableUserNotificationCategory *messageCategory = [UIMutableUserNotificationCategory new]; - messageCategory.identifier = Signal_Full_New_Message_Category_No_Longer_Verified; - [messageCategory setActions:@[ action_markRead ] forContext:UIUserNotificationActionContextMinimal]; - [messageCategory setActions:@[ action_markRead ] forContext:UIUserNotificationActionContextDefault]; - - return messageCategory; -} - -- (UIMutableUserNotificationAction *)markAsReadAction -{ - UIMutableUserNotificationAction *action = [UIMutableUserNotificationAction new]; - action.identifier = Signal_Message_MarkAsRead_Identifier; - action.title = NSLocalizedString(@"PUSH_MANAGER_MARKREAD", nil); - action.destructive = NO; - action.authenticationRequired = NO; - action.activationMode = UIUserNotificationActivationModeBackground; - return action; -} - -#pragma mark - Signal Calls - -NSString *const PushManagerCategoriesIncomingCall = @"PushManagerCategoriesIncomingCall"; -NSString *const PushManagerCategoriesMissedCall = @"PushManagerCategoriesMissedCall"; -NSString *const PushManagerCategoriesMissedCallFromNoLongerVerifiedIdentity = - @"PushManagerCategoriesMissedCallFromNoLongerVerifiedIdentity"; - -NSString *const PushManagerActionsAcceptCall = @"PushManagerActionsAcceptCall"; -NSString *const PushManagerActionsDeclineCall = @"PushManagerActionsDeclineCall"; -NSString *const PushManagerActionsCallBack = @"PushManagerActionsCallBack"; -NSString *const PushManagerActionsIgnoreIdentityChangeAndCallBack = - @"PushManagerActionsIgnoreIdentityChangeAndCallBack"; -NSString *const PushManagerActionsShowThread = @"PushManagerActionsShowThread"; - -NSString *const PushManagerUserInfoKeysLocalCallId = @"PushManagerUserInfoKeysLocalCallId"; -NSString *const PushManagerUserInfoKeysCallBackSignalRecipientId = @"PushManagerUserInfoKeysCallBackSignalRecipientId"; - -- (UIUserNotificationCategory *)signalIncomingCallCategory -{ - UIMutableUserNotificationAction *acceptAction = [UIMutableUserNotificationAction new]; - acceptAction.identifier = PushManagerActionsAcceptCall; - acceptAction.title = NSLocalizedString(@"ANSWER_CALL_BUTTON_TITLE", @""); - acceptAction.activationMode = UIUserNotificationActivationModeForeground; - acceptAction.destructive = NO; - acceptAction.authenticationRequired = NO; - - UIMutableUserNotificationAction *declineAction = [UIMutableUserNotificationAction new]; - declineAction.identifier = PushManagerActionsDeclineCall; - declineAction.title = NSLocalizedString(@"REJECT_CALL_BUTTON_TITLE", @""); - declineAction.activationMode = UIUserNotificationActivationModeBackground; - declineAction.destructive = NO; - declineAction.authenticationRequired = NO; - - UIMutableUserNotificationCategory *callCategory = [UIMutableUserNotificationCategory new]; - callCategory.identifier = PushManagerCategoriesIncomingCall; - [callCategory setActions:@[ acceptAction, declineAction ] forContext:UIUserNotificationActionContextMinimal]; - [callCategory setActions:@[ acceptAction, declineAction ] forContext:UIUserNotificationActionContextDefault]; - - return callCategory; -} - -- (UIUserNotificationCategory *)signalMissedCallCategory -{ - UIMutableUserNotificationAction *callBackAction = [UIMutableUserNotificationAction new]; - callBackAction.identifier = PushManagerActionsCallBack; - callBackAction.title = [CallStrings callBackButtonTitle]; - callBackAction.activationMode = UIUserNotificationActivationModeForeground; - callBackAction.destructive = NO; - callBackAction.authenticationRequired = YES; - - UIMutableUserNotificationCategory *missedCallCategory = [UIMutableUserNotificationCategory new]; - missedCallCategory.identifier = PushManagerCategoriesMissedCall; - [missedCallCategory setActions:@[ callBackAction ] forContext:UIUserNotificationActionContextMinimal]; - [missedCallCategory setActions:@[ callBackAction ] forContext:UIUserNotificationActionContextDefault]; - - return missedCallCategory; -} - -- (UIUserNotificationCategory *)signalMissedCallWithNoLongerVerifiedIdentityChangeCategory -{ - - UIMutableUserNotificationAction *showThreadAction = [UIMutableUserNotificationAction new]; - showThreadAction.identifier = PushManagerActionsShowThread; - showThreadAction.title = [CallStrings showThreadButtonTitle]; - showThreadAction.activationMode = UIUserNotificationActivationModeForeground; - showThreadAction.destructive = NO; - showThreadAction.authenticationRequired = YES; - - UIMutableUserNotificationCategory *rejectedCallCategory = [UIMutableUserNotificationCategory new]; - rejectedCallCategory.identifier = PushManagerCategoriesMissedCallFromNoLongerVerifiedIdentity; - [rejectedCallCategory setActions:@[ showThreadAction ] forContext:UIUserNotificationActionContextMinimal]; - [rejectedCallCategory setActions:@[ showThreadAction ] forContext:UIUserNotificationActionContextDefault]; - - return rejectedCallCategory; -} - -#pragma mark Util - -- (int)allNotificationTypes { - return UIUserNotificationTypeAlert | UIUserNotificationTypeSound | UIUserNotificationTypeBadge; -} - -- (UIUserNotificationSettings *)userNotificationSettings -{ - OWSLogDebug(@"registering user notification settings"); - UIUserNotificationSettings *settings = [UIUserNotificationSettings - settingsForTypes:(UIUserNotificationType)[self allNotificationTypes] - categories:[NSSet setWithObjects:[self fullNewMessageNotificationCategory], - [self fullNewMessageNoLongerVerifiedNotificationCategory], - [self signalIncomingCallCategory], - [self signalMissedCallCategory], - [self signalMissedCallWithNoLongerVerifiedIdentityChangeCategory], - nil]]; - - return settings; -} - -- (BOOL)applicationIsActive { - UIApplication *app = [UIApplication sharedApplication]; - - if (app.applicationState == UIApplicationStateActive) { - return YES; - } - - return NO; -} - -// TODO: consolidate notification tracking with NotificationsManager, which also maintains a list of notifications. -- (void)presentNotification:(UILocalNotification *)notification checkForCancel:(BOOL)checkForCancel -{ - notification.alertBody = notification.alertBody.filterStringForDisplay; - - dispatch_async(dispatch_get_main_queue(), ^{ - NSString *threadId = notification.userInfo[Signal_Thread_UserInfo_Key]; - if (checkForCancel && threadId != nil) { - if ([[OWSDeviceManager sharedManager] hasReceivedSyncMessageInLastSeconds:60.f]) { - // "If you’ve heard from desktop in last minute, wait 5 seconds." - // - // This provides a window in which we can cancel notifications - // already viewed on desktop before they are presented here. - const CGFloat kDelaySeconds = 5.f; - notification.fireDate = [NSDate dateWithTimeIntervalSinceNow:kDelaySeconds]; - } else { - notification.fireDate = [NSDate new]; - } - - notification.timeZone = [NSTimeZone localTimeZone]; - } - - [[UIApplication sharedApplication] scheduleLocalNotification:notification]; - [self.currentNotifications addObject:notification]; - }); -} - -// TODO: consolidate notification tracking with NotificationsManager, which also maintains a list of notifications. -- (void)cancelNotificationsWithThreadId:(NSString *)threadId -{ - dispatch_async(dispatch_get_main_queue(), ^{ - NSMutableArray *toDelete = [NSMutableArray array]; - [self.currentNotifications - enumerateObjectsUsingBlock:^(UILocalNotification *notif, NSUInteger idx, BOOL *stop) { - if ([notif.userInfo[Signal_Thread_UserInfo_Key] isEqualToString:threadId]) { - [[UIApplication sharedApplication] cancelLocalNotification:notif]; - [toDelete addObject:notif]; - } - }]; - [self.currentNotifications removeObjectsInArray:toDelete]; - }); -} - -@end diff --git a/Signal/test/push/PushManagerTest.m b/Signal/test/push/PushManagerTest.m deleted file mode 100644 index 9c2f534ef..000000000 --- a/Signal/test/push/PushManagerTest.m +++ /dev/null @@ -1,33 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import "SignalBaseTest.h" - -@interface PushManagerTest : SignalBaseTest - -@end - -@implementation PushManagerTest - -- (void)setUp { - [super setUp]; - // Put setup code here. This method is called before the invocation of each test method in the class. -} - -- (void)tearDown { - // Put teardown code here. This method is called after the invocation of each test method in the class. - [super tearDown]; -} - -/** - * This test verifies that the enum containing the notifications types doesn't change for iOS7 support. - */ - -- (void)testNotificationTypesForiOS7 { - XCTAssert(UIRemoteNotificationTypeAlert == UIUserNotificationTypeAlert, @"iOS 7 <-> 8 compatibility"); - XCTAssert(UIRemoteNotificationTypeSound == UIUserNotificationTypeSound, @"iOS 7 <-> 8 compatibility"); - XCTAssert(UIRemoteNotificationTypeBadge == UIUserNotificationTypeBadge, @"iOS 7 <-> 8 compatibility"); -} - -@end diff --git a/SignalMessaging/Views/CommonStrings.swift b/SignalMessaging/Views/CommonStrings.swift index ac35634ad..8acba7c43 100644 --- a/SignalMessaging/Views/CommonStrings.swift +++ b/SignalMessaging/Views/CommonStrings.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // import Foundation @@ -26,6 +26,15 @@ import Foundation @objc public class MessageStrings: NSObject { @objc static public let newGroupDefaultTitle = NSLocalizedString("NEW_GROUP_DEFAULT_TITLE", comment: "Used in place of the group name when a group has not yet been named.") + + @objc + static public let replyNotificationAction = NSLocalizedString("PUSH_MANAGER_REPLY", comment: "Notification action button title") + + @objc + static public let markAsReadNotificationAction = NSLocalizedString("PUSH_MANAGER_MARKREAD", comment: "Notification action button title") + + @objc + static public let sendButton = NSLocalizedString("SEND_BUTTON_TITLE", comment: "Label for the button to send a message") } @objc public class CallStrings: NSObject { @@ -47,6 +56,10 @@ import Foundation static public let callBackButtonTitle = NSLocalizedString("CALLBACK_BUTTON_TITLE", comment: "notification action") @objc static public let showThreadButtonTitle = NSLocalizedString("SHOW_THREAD_BUTTON_TITLE", comment: "notification action") + @objc + static public let answerCallButtonTitle = NSLocalizedString("ANSWER_CALL_BUTTON_TITLE", comment: "notification action") + @objc + static public let declineCallButtonTitle = NSLocalizedString("REJECT_CALL_BUTTON_TITLE", comment: "") // MARK: Missed Call Notification @objc @@ -59,6 +72,12 @@ import Foundation static public let missedCallWithIdentityChangeNotificationBodyWithoutCallerName = NSLocalizedString("MISSED_CALL_WITH_CHANGED_IDENTITY_BODY_WITHOUT_CALLER_NAME", comment: "notification title") @objc static public let missedCallWithIdentityChangeNotificationBodyWithCallerName = NSLocalizedString("MISSED_CALL_WITH_CHANGED_IDENTITY_BODY_WITH_CALLER_NAME", comment: "notification title. Embeds {{caller's name or phone number}}") + + @objc + static public let incomingCallWithoutCallerNameNotification = NSLocalizedString("INCOMING_CALL", comment: "notification body, does not include the callers name") + + @objc + static public let incomingCallNotificationFormat = NSLocalizedString("INCOMING_CALL_FROM", comment: "notification body, embeds {{caller name or number}}") } @objc public class MediaStrings: NSObject { diff --git a/SignalServiceKit/src/SignalServiceKit.h b/SignalServiceKit/src/SignalServiceKit.h index 2ae48031f..eb630203b 100644 --- a/SignalServiceKit/src/SignalServiceKit.h +++ b/SignalServiceKit/src/SignalServiceKit.h @@ -1,10 +1,11 @@ // -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. +// Copyright (c) 2019 Open Whisper Systems. All rights reserved. // // Anything used by Swift outside of the framework must be imported. #import #import +#import #import #import #import diff --git a/SignalServiceKit/src/TestUtils/Factories.swift b/SignalServiceKit/src/TestUtils/Factories.swift index 34c597992..93d897be6 100644 --- a/SignalServiceKit/src/TestUtils/Factories.swift +++ b/SignalServiceKit/src/TestUtils/Factories.swift @@ -241,9 +241,12 @@ public class IncomingMessageFactory: NSObject, Factory { @objc public func create(transaction: YapDatabaseReadWriteTransaction) -> TSIncomingMessage { + + let thread = threadCreator(transaction) + let item = TSIncomingMessage(incomingMessageWithTimestamp: timestampBuilder(), - in: threadCreator(transaction), - authorId: authorIdBuilder(), + in: thread, + authorId: authorIdBuilder(thread), sourceDeviceId: sourceDeviceIdBuilder(), messageBody: messageBodyBuilder(), attachmentIds: attachmentIdsBuilder(), @@ -279,8 +282,16 @@ public class IncomingMessageFactory: NSObject, Factory { } @objc - public var authorIdBuilder: () -> String = { - return CommonGenerator.contactId + public var authorIdBuilder: (TSThread) -> String = { thread in + switch thread { + case let contactThread as TSContactThread: + return contactThread.contactIdentifier() + case let groupThread as TSGroupThread: + return groupThread.recipientIdentifiers.ows_randomElement() ?? CommonGenerator.contactId + default: + owsFailDebug("unexpected thread type") + return CommonGenerator.contactId + } } @objc @@ -344,7 +355,6 @@ class GroupThreadFactory: NSObject, Factory { (0.. - -@end - -#endif - -NS_ASSUME_NONNULL_END diff --git a/SignalServiceKit/src/TestUtils/OWSFakeNotificationsManager.m b/SignalServiceKit/src/TestUtils/OWSFakeNotificationsManager.m deleted file mode 100644 index 8d47849d2..000000000 --- a/SignalServiceKit/src/TestUtils/OWSFakeNotificationsManager.m +++ /dev/null @@ -1,39 +0,0 @@ -// -// Copyright (c) 2018 Open Whisper Systems. All rights reserved. -// - -#import "OWSFakeNotificationsManager.h" - -NS_ASSUME_NONNULL_BEGIN - -#ifdef DEBUG - -@implementation OWSFakeNotificationsManager - -- (void)notifyUserForIncomingMessage:(TSIncomingMessage *)incomingMessage - inThread:(TSThread *)thread - contactsManager:(id)contactsManager - transaction:(YapDatabaseReadTransaction *)transaction { - OWSLogInfo(@""); -} - -- (void)notifyUserForErrorMessage:(TSErrorMessage *)error - thread:(TSThread *)thread - transaction:(YapDatabaseReadWriteTransaction *)transaction { - OWSLogInfo(@""); -} - -- (void)notifyUserForThreadlessErrorMessage:(TSErrorMessage *)error - transaction:(YapDatabaseReadWriteTransaction *)transaction { - OWSLogInfo(@""); -} - -- (void)clearAllNotifications { - OWSLogInfo(@""); -} - -@end - -#endif - -NS_ASSUME_NONNULL_END