mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Merge remote-tracking branch 'upstream/dev' into dev
This commit is contained in:
commit
7256cc6871
67 changed files with 85 additions and 6698 deletions
|
@ -14,7 +14,6 @@
|
|||
340FC8AA204DAC8D007AEB0F /* NotificationSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC87C204DAC8C007AEB0F /* NotificationSettingsViewController.m */; };
|
||||
340FC8AC204DAC8D007AEB0F /* PrivacySettingsTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC87E204DAC8C007AEB0F /* PrivacySettingsTableViewController.m */; };
|
||||
340FC8AE204DAC8D007AEB0F /* OWSSoundSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC883204DAC8C007AEB0F /* OWSSoundSettingsViewController.m */; };
|
||||
340FC8B4204DAC8D007AEB0F /* OWSBackupSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC88E204DAC8C007AEB0F /* OWSBackupSettingsViewController.m */; };
|
||||
340FC8B6204DAC8D007AEB0F /* OWSQRCodeScanningViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC896204DAC8C007AEB0F /* OWSQRCodeScanningViewController.m */; };
|
||||
340FC8B7204DAC8D007AEB0F /* OWSConversationSettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 340FC89A204DAC8D007AEB0F /* OWSConversationSettingsViewController.m */; };
|
||||
341341EF2187467A00192D59 /* ConversationViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 341341EE2187467900192D59 /* ConversationViewModel.m */; };
|
||||
|
@ -22,8 +21,6 @@
|
|||
3430FE181F7751D4000EC51B /* GiphyAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3430FE171F7751D4000EC51B /* GiphyAPI.swift */; };
|
||||
34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34330AA21E79686200DF2FB9 /* OWSProgressView.m */; };
|
||||
34386A54207D271D009F5D9C /* NeverClearView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34386A53207D271C009F5D9C /* NeverClearView.swift */; };
|
||||
3441FD9F21A3604F00BB9542 /* BackupRestoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3441FD9E21A3604F00BB9542 /* BackupRestoreViewController.swift */; };
|
||||
344825C6211390C800DB4BD8 /* OWSOrphanDataCleaner.m in Sources */ = {isa = PBXBuildFile; fileRef = 344825C5211390C800DB4BD8 /* OWSOrphanDataCleaner.m */; };
|
||||
346129991FD1E4DA00532771 /* SignalApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 346129971FD1E4D900532771 /* SignalApp.m */; };
|
||||
34661FB820C1C0D60056EDD6 /* message_sent.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 34661FB720C1C0D60056EDD6 /* message_sent.aiff */; };
|
||||
346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */; };
|
||||
|
@ -35,13 +32,6 @@
|
|||
3496955D219B605E00DCFE74 /* PhotoCollectionPickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3496955A219B605E00DCFE74 /* PhotoCollectionPickerController.swift */; };
|
||||
3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3496955B219B605E00DCFE74 /* PhotoLibrary.swift */; };
|
||||
3496956021A2FC8100DCFE74 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3496955F21A2FC8100DCFE74 /* CloudKit.framework */; };
|
||||
3496956E21A301A100DCFE74 /* OWSBackupExportJob.m in Sources */ = {isa = PBXBuildFile; fileRef = 3496956221A301A100DCFE74 /* OWSBackupExportJob.m */; };
|
||||
3496956F21A301A100DCFE74 /* OWSBackupLazyRestore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3496956321A301A100DCFE74 /* OWSBackupLazyRestore.swift */; };
|
||||
3496957021A301A100DCFE74 /* OWSBackupIO.m in Sources */ = {isa = PBXBuildFile; fileRef = 3496956521A301A100DCFE74 /* OWSBackupIO.m */; };
|
||||
3496957121A301A100DCFE74 /* OWSBackupImportJob.m in Sources */ = {isa = PBXBuildFile; fileRef = 3496956621A301A100DCFE74 /* OWSBackupImportJob.m */; };
|
||||
3496957221A301A100DCFE74 /* OWSBackup.m in Sources */ = {isa = PBXBuildFile; fileRef = 3496956921A301A100DCFE74 /* OWSBackup.m */; };
|
||||
3496957321A301A100DCFE74 /* OWSBackupJob.m in Sources */ = {isa = PBXBuildFile; fileRef = 3496956A21A301A100DCFE74 /* OWSBackupJob.m */; };
|
||||
3496957421A301A100DCFE74 /* OWSBackupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3496956B21A301A100DCFE74 /* OWSBackupAPI.swift */; };
|
||||
34A6C28021E503E700B5B12E /* OWSImagePickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A6C27F21E503E600B5B12E /* OWSImagePickerController.swift */; };
|
||||
34A8B3512190A40E00218A25 /* MediaAlbumView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34A8B3502190A40E00218A25 /* MediaAlbumView.swift */; };
|
||||
34ABC0E421DD20C500ED9469 /* ConversationMessageMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34ABC0E321DD20C500ED9469 /* ConversationMessageMapping.swift */; };
|
||||
|
@ -459,24 +449,20 @@
|
|||
C33FDCFA255A582000E217F9 /* SignalIOSProto.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB40255A580C00E217F9 /* SignalIOSProto.swift */; };
|
||||
C33FDD03255A582000E217F9 /* WeakTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB49255A580C00E217F9 /* WeakTimer.swift */; };
|
||||
C33FDD06255A582000E217F9 /* AppVersion.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB4C255A580D00E217F9 /* AppVersion.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C33FDD12255A582000E217F9 /* OWSPrimaryStorage+Loki.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB58255A580E00E217F9 /* OWSPrimaryStorage+Loki.m */; };
|
||||
C33FDD13255A582000E217F9 /* OWSFailedAttachmentDownloadsJob.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB59255A580E00E217F9 /* OWSFailedAttachmentDownloadsJob.m */; };
|
||||
C33FDD23255A582000E217F9 /* FeatureFlags.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB69255A580F00E217F9 /* FeatureFlags.swift */; };
|
||||
C33FDD32255A582000E217F9 /* OWSOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB78255A581000E217F9 /* OWSOperation.m */; };
|
||||
C33FDD3A255A582000E217F9 /* Notification+Loki.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB80255A581100E217F9 /* Notification+Loki.swift */; };
|
||||
C33FDD49255A582000E217F9 /* ParamParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB8F255A581200E217F9 /* ParamParser.swift */; };
|
||||
C33FDD53255A582000E217F9 /* OWSPrimaryStorage+keyFromIntLong.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB99255A581300E217F9 /* OWSPrimaryStorage+keyFromIntLong.m */; };
|
||||
C33FDD5A255A582000E217F9 /* TSStorageHeaders.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBA0255A581400E217F9 /* TSStorageHeaders.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C33FDD5B255A582000E217F9 /* OWSOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBA1255A581400E217F9 /* OWSOperation.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C33FDD68255A582000E217F9 /* SignalAccount.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBAE255A581500E217F9 /* SignalAccount.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C33FDD6E255A582000E217F9 /* NSURLSessionDataTask+StatusCode.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBB4255A581600E217F9 /* NSURLSessionDataTask+StatusCode.m */; };
|
||||
C33FDD74255A582000E217F9 /* OWSPrimaryStorage+keyFromIntLong.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBBA255A581600E217F9 /* OWSPrimaryStorage+keyFromIntLong.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C33FDD75255A582000E217F9 /* OWSPrimaryStorage+Loki.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBBB255A581600E217F9 /* OWSPrimaryStorage+Loki.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C33FDD7C255A582000E217F9 /* SSKAsserts.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBC2255A581700E217F9 /* SSKAsserts.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C33FDD8D255A582000E217F9 /* OWSSignalAddress.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBD3255A581800E217F9 /* OWSSignalAddress.swift */; };
|
||||
C33FDD91255A582000E217F9 /* OWSMessageUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBD7255A581900E217F9 /* OWSMessageUtils.m */; };
|
||||
C33FDD92255A582000E217F9 /* SignalIOS.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBD8255A581900E217F9 /* SignalIOS.pb.swift */; };
|
||||
C33FDDA9255A582000E217F9 /* TSStorageKeys.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBEF255A581B00E217F9 /* TSStorageKeys.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C33FDDB0255A582000E217F9 /* NSURLSessionDataTask+StatusCode.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBF6255A581C00E217F9 /* NSURLSessionDataTask+StatusCode.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C33FDDB2255A582000E217F9 /* NSArray+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBF8255A581C00E217F9 /* NSArray+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C33FDDB3255A582000E217F9 /* OWSError.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDBF9255A581C00E217F9 /* OWSError.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
|
@ -522,8 +508,6 @@
|
|||
C37F54DC255BB84A002AEA92 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; };
|
||||
C38D5E8D2575011E00B6A65C /* MessageSender+ClosedGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38D5E8C2575011E00B6A65C /* MessageSender+ClosedGroups.swift */; };
|
||||
C38EF00C255B61CC007E1867 /* SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; };
|
||||
C38EF216255B6D3B007E1867 /* Theme.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF212255B6D3A007E1867 /* Theme.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C38EF218255B6D3B007E1867 /* Theme.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF214255B6D3A007E1867 /* Theme.m */; };
|
||||
C38EF228255B6D5D007E1867 /* AttachmentSharing.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF223255B6D5D007E1867 /* AttachmentSharing.m */; };
|
||||
C38EF22A255B6D5D007E1867 /* AttachmentSharing.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF225255B6D5D007E1867 /* AttachmentSharing.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C38EF22B255B6D5D007E1867 /* ShareViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF226255B6D5D007E1867 /* ShareViewDelegate.swift */; };
|
||||
|
@ -558,7 +542,6 @@
|
|||
C38EF30C255B6DBF007E1867 /* OWSScreenLock.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2E2255B6DB9007E1867 /* OWSScreenLock.swift */; };
|
||||
C38EF30D255B6DBF007E1867 /* OWSUnreadIndicator.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2E3255B6DB9007E1867 /* OWSUnreadIndicator.m */; };
|
||||
C38EF30E255B6DBF007E1867 /* FullTextSearcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2E4255B6DB9007E1867 /* FullTextSearcher.swift */; };
|
||||
C38EF30F255B6DBF007E1867 /* AppPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2E5255B6DB9007E1867 /* AppPreferences.swift */; };
|
||||
C38EF313255B6DBF007E1867 /* OWSUnreadIndicator.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF2E9255B6DBA007E1867 /* OWSUnreadIndicator.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C38EF317255B6DBF007E1867 /* DisplayableText.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2ED255B6DBB007E1867 /* DisplayableText.swift */; };
|
||||
C38EF31A255B6DBF007E1867 /* OWSAnyTouchGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF2F0255B6DBB007E1867 /* OWSAnyTouchGestureRecognizer.m */; };
|
||||
|
@ -612,7 +595,6 @@
|
|||
C38EF3C7255B6DE7007E1867 /* ImageEditorCanvasView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3B7255B6DE6007E1867 /* ImageEditorCanvasView.swift */; };
|
||||
C38EF3EF255B6DF7007E1867 /* ThreadViewHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3D1255B6DEE007E1867 /* ThreadViewHelper.m */; };
|
||||
C38EF3F0255B6DF7007E1867 /* ThreadViewHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF3D2255B6DEE007E1867 /* ThreadViewHelper.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C38EF3F1255B6DF7007E1867 /* OWSSearchBar.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF3D3255B6DEE007E1867 /* OWSSearchBar.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C38EF3F2255B6DF7007E1867 /* DisappearingTimerConfigurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3D4255B6DEE007E1867 /* DisappearingTimerConfigurationView.swift */; };
|
||||
C38EF3F4255B6DF7007E1867 /* ContactCellView.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3D6255B6DEF007E1867 /* ContactCellView.m */; };
|
||||
C38EF3F5255B6DF7007E1867 /* OWSTextField.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF3D7255B6DF0007E1867 /* OWSTextField.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
|
@ -631,7 +613,6 @@
|
|||
C38EF404255B6DF7007E1867 /* ContactTableViewCell.h in Headers */ = {isa = PBXBuildFile; fileRef = C38EF3E6255B6DF4007E1867 /* ContactTableViewCell.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C38EF405255B6DF7007E1867 /* OWSButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3E7255B6DF5007E1867 /* OWSButton.swift */; };
|
||||
C38EF407255B6DF7007E1867 /* Toast.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3E9255B6DF6007E1867 /* Toast.swift */; };
|
||||
C38EF408255B6DF7007E1867 /* OWSSearchBar.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3EA255B6DF6007E1867 /* OWSSearchBar.m */; };
|
||||
C38EF409255B6DF7007E1867 /* ContactTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3EB255B6DF6007E1867 /* ContactTableViewCell.m */; };
|
||||
C38EF40A255B6DF7007E1867 /* OWSFlatButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3EC255B6DF6007E1867 /* OWSFlatButton.swift */; };
|
||||
C38EF40B255B6DF7007E1867 /* TappableStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF3ED255B6DF6007E1867 /* TappableStackView.swift */; };
|
||||
|
@ -725,7 +706,6 @@
|
|||
C3D9E3C025676AD70040E4F3 /* TSAttachment.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAC2255A580200E217F9 /* TSAttachment.m */; };
|
||||
C3D9E3C925676AF30040E4F3 /* TSYapDatabaseObject.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA90255A57FD00E217F9 /* TSYapDatabaseObject.m */; };
|
||||
C3D9E3FA25676BCE0040E4F3 /* TSYapDatabaseObject.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDAA1255A57FF00E217F9 /* TSYapDatabaseObject.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C3D9E40C25676C100040E4F3 /* Storage+Conformances.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3D9E40B25676C100040E4F3 /* Storage+Conformances.swift */; };
|
||||
C3D9E41525676C320040E4F3 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB36255A580B00E217F9 /* Storage.swift */; };
|
||||
C3D9E41F25676C870040E4F3 /* OWSPrimaryStorageProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3D9E41E25676C870040E4F3 /* OWSPrimaryStorageProtocol.swift */; };
|
||||
C3D9E43125676D3D0040E4F3 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3D9E43025676D3D0040E4F3 /* Configuration.swift */; };
|
||||
|
@ -950,12 +930,10 @@
|
|||
340FC87B204DAC8C007AEB0F /* NotificationSettingsOptionsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NotificationSettingsOptionsViewController.m; sourceTree = "<group>"; };
|
||||
340FC87C204DAC8C007AEB0F /* NotificationSettingsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NotificationSettingsViewController.m; sourceTree = "<group>"; };
|
||||
340FC87E204DAC8C007AEB0F /* PrivacySettingsTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PrivacySettingsTableViewController.m; sourceTree = "<group>"; };
|
||||
340FC87F204DAC8C007AEB0F /* OWSBackupSettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupSettingsViewController.h; sourceTree = "<group>"; };
|
||||
340FC883204DAC8C007AEB0F /* OWSSoundSettingsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSSoundSettingsViewController.m; sourceTree = "<group>"; };
|
||||
340FC888204DAC8C007AEB0F /* OWSQRCodeScanningViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSQRCodeScanningViewController.h; sourceTree = "<group>"; };
|
||||
340FC88A204DAC8C007AEB0F /* NotificationSettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NotificationSettingsViewController.h; sourceTree = "<group>"; };
|
||||
340FC88B204DAC8C007AEB0F /* NotificationSettingsOptionsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NotificationSettingsOptionsViewController.h; sourceTree = "<group>"; };
|
||||
340FC88E204DAC8C007AEB0F /* OWSBackupSettingsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupSettingsViewController.m; sourceTree = "<group>"; };
|
||||
340FC88F204DAC8C007AEB0F /* PrivacySettingsTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PrivacySettingsTableViewController.h; sourceTree = "<group>"; };
|
||||
340FC894204DAC8C007AEB0F /* OWSSoundSettingsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSSoundSettingsViewController.h; sourceTree = "<group>"; };
|
||||
340FC896204DAC8C007AEB0F /* OWSQRCodeScanningViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSQRCodeScanningViewController.m; sourceTree = "<group>"; };
|
||||
|
@ -970,11 +948,8 @@
|
|||
34330AA11E79686200DF2FB9 /* OWSProgressView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSProgressView.h; sourceTree = "<group>"; };
|
||||
34330AA21E79686200DF2FB9 /* OWSProgressView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSProgressView.m; sourceTree = "<group>"; };
|
||||
34386A53207D271C009F5D9C /* NeverClearView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NeverClearView.swift; sourceTree = "<group>"; };
|
||||
3441FD9E21A3604F00BB9542 /* BackupRestoreViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackupRestoreViewController.swift; sourceTree = "<group>"; };
|
||||
34480B371FD092A900BC14EF /* SignalShareExtension-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SignalShareExtension-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
34480B381FD092E300BC14EF /* SessionShareExtension-Prefix.pch */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SessionShareExtension-Prefix.pch"; sourceTree = "<group>"; };
|
||||
344825C4211390C700DB4BD8 /* OWSOrphanDataCleaner.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSOrphanDataCleaner.h; sourceTree = "<group>"; };
|
||||
344825C5211390C800DB4BD8 /* OWSOrphanDataCleaner.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSOrphanDataCleaner.m; sourceTree = "<group>"; };
|
||||
346129971FD1E4D900532771 /* SignalApp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SignalApp.m; sourceTree = "<group>"; };
|
||||
346129981FD1E4DA00532771 /* SignalApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SignalApp.h; sourceTree = "<group>"; };
|
||||
34661FB720C1C0D60056EDD6 /* message_sent.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; name = message_sent.aiff; path = Session/Meta/AudioFiles/message_sent.aiff; sourceTree = SOURCE_ROOT; };
|
||||
|
@ -985,18 +960,6 @@
|
|||
3496955A219B605E00DCFE74 /* PhotoCollectionPickerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCollectionPickerController.swift; sourceTree = "<group>"; };
|
||||
3496955B219B605E00DCFE74 /* PhotoLibrary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoLibrary.swift; sourceTree = "<group>"; };
|
||||
3496955F21A2FC8100DCFE74 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
|
||||
3496956221A301A100DCFE74 /* OWSBackupExportJob.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupExportJob.m; sourceTree = "<group>"; };
|
||||
3496956321A301A100DCFE74 /* OWSBackupLazyRestore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSBackupLazyRestore.swift; sourceTree = "<group>"; };
|
||||
3496956421A301A100DCFE74 /* OWSBackup.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackup.h; sourceTree = "<group>"; };
|
||||
3496956521A301A100DCFE74 /* OWSBackupIO.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupIO.m; sourceTree = "<group>"; };
|
||||
3496956621A301A100DCFE74 /* OWSBackupImportJob.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupImportJob.m; sourceTree = "<group>"; };
|
||||
3496956721A301A100DCFE74 /* OWSBackupJob.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupJob.h; sourceTree = "<group>"; };
|
||||
3496956821A301A100DCFE74 /* OWSBackupExportJob.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupExportJob.h; sourceTree = "<group>"; };
|
||||
3496956921A301A100DCFE74 /* OWSBackup.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackup.m; sourceTree = "<group>"; };
|
||||
3496956A21A301A100DCFE74 /* OWSBackupJob.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSBackupJob.m; sourceTree = "<group>"; };
|
||||
3496956B21A301A100DCFE74 /* OWSBackupAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSBackupAPI.swift; sourceTree = "<group>"; };
|
||||
3496956C21A301A100DCFE74 /* OWSBackupImportJob.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupImportJob.h; sourceTree = "<group>"; };
|
||||
3496956D21A301A100DCFE74 /* OWSBackupIO.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSBackupIO.h; sourceTree = "<group>"; };
|
||||
34A6C27F21E503E600B5B12E /* OWSImagePickerController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSImagePickerController.swift; sourceTree = "<group>"; };
|
||||
34A8B3502190A40E00218A25 /* MediaAlbumView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaAlbumView.swift; sourceTree = "<group>"; };
|
||||
34ABC0E321DD20C500ED9469 /* ConversationMessageMapping.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConversationMessageMapping.swift; sourceTree = "<group>"; };
|
||||
|
@ -1430,7 +1393,6 @@
|
|||
C33FDB51255A580D00E217F9 /* NSUserDefaults+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSUserDefaults+OWS.h"; sourceTree = "<group>"; };
|
||||
C33FDB54255A580D00E217F9 /* DataSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DataSource.h; sourceTree = "<group>"; };
|
||||
C33FDB56255A580D00E217F9 /* TSOutgoingMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSOutgoingMessage.m; sourceTree = "<group>"; };
|
||||
C33FDB58255A580E00E217F9 /* OWSPrimaryStorage+Loki.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "OWSPrimaryStorage+Loki.m"; sourceTree = "<group>"; };
|
||||
C33FDB59255A580E00E217F9 /* OWSFailedAttachmentDownloadsJob.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSFailedAttachmentDownloadsJob.m; sourceTree = "<group>"; };
|
||||
C33FDB5B255A580E00E217F9 /* YapDatabaseTransaction+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "YapDatabaseTransaction+OWS.m"; sourceTree = "<group>"; };
|
||||
C33FDB5C255A580E00E217F9 /* NSArray+Functional.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSArray+Functional.h"; sourceTree = "<group>"; };
|
||||
|
@ -1461,7 +1423,6 @@
|
|||
C33FDB99255A581300E217F9 /* OWSPrimaryStorage+keyFromIntLong.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "OWSPrimaryStorage+keyFromIntLong.m"; sourceTree = "<group>"; };
|
||||
C33FDB9C255A581300E217F9 /* TSIncomingMessage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSIncomingMessage.h; sourceTree = "<group>"; };
|
||||
C33FDB9E255A581400E217F9 /* TSAttachmentPointer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSAttachmentPointer.m; sourceTree = "<group>"; };
|
||||
C33FDBA0255A581400E217F9 /* TSStorageHeaders.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSStorageHeaders.h; sourceTree = "<group>"; };
|
||||
C33FDBA1255A581400E217F9 /* OWSOperation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSOperation.h; sourceTree = "<group>"; };
|
||||
C33FDBA4255A581400E217F9 /* OWSDisappearingMessagesConfiguration.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSDisappearingMessagesConfiguration.m; sourceTree = "<group>"; };
|
||||
C33FDBA8255A581500E217F9 /* OWSLinkPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSLinkPreview.swift; sourceTree = "<group>"; };
|
||||
|
@ -1474,7 +1435,6 @@
|
|||
C33FDBB8255A581600E217F9 /* TSThread.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSThread.m; sourceTree = "<group>"; };
|
||||
C33FDBB9255A581600E217F9 /* ProfileManagerProtocol.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ProfileManagerProtocol.h; sourceTree = "<group>"; };
|
||||
C33FDBBA255A581600E217F9 /* OWSPrimaryStorage+keyFromIntLong.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OWSPrimaryStorage+keyFromIntLong.h"; sourceTree = "<group>"; };
|
||||
C33FDBBB255A581600E217F9 /* OWSPrimaryStorage+Loki.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "OWSPrimaryStorage+Loki.h"; sourceTree = "<group>"; };
|
||||
C33FDBBC255A581600E217F9 /* SSKKeychainStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SSKKeychainStorage.swift; sourceTree = "<group>"; };
|
||||
C33FDBC1255A581700E217F9 /* General.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = General.swift; sourceTree = "<group>"; };
|
||||
C33FDBC2255A581700E217F9 /* SSKAsserts.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SSKAsserts.h; sourceTree = "<group>"; };
|
||||
|
@ -1487,7 +1447,6 @@
|
|||
C33FDBE1255A581A00E217F9 /* LKGroupUtilities.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LKGroupUtilities.m; sourceTree = "<group>"; };
|
||||
C33FDBE9255A581A00E217F9 /* TSInteraction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSInteraction.m; sourceTree = "<group>"; };
|
||||
C33FDBEC255A581B00E217F9 /* OWSRecipientIdentity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSRecipientIdentity.m; sourceTree = "<group>"; };
|
||||
C33FDBEF255A581B00E217F9 /* TSStorageKeys.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TSStorageKeys.h; sourceTree = "<group>"; };
|
||||
C33FDBF1255A581B00E217F9 /* OWSIdentityManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSIdentityManager.h; sourceTree = "<group>"; };
|
||||
C33FDBF6255A581C00E217F9 /* NSURLSessionDataTask+StatusCode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSURLSessionDataTask+StatusCode.h"; sourceTree = "<group>"; };
|
||||
C33FDBF8255A581C00E217F9 /* NSArray+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSArray+OWS.h"; sourceTree = "<group>"; };
|
||||
|
@ -1540,8 +1499,6 @@
|
|||
C37F5402255BA9ED002AEA92 /* Environment.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Environment.m; sourceTree = "<group>"; };
|
||||
C38D5E8C2575011E00B6A65C /* MessageSender+ClosedGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageSender+ClosedGroups.swift"; sourceTree = "<group>"; };
|
||||
C38EEF09255B49A8007E1867 /* SNProtoEnvelope+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SNProtoEnvelope+Conversion.swift"; sourceTree = "<group>"; };
|
||||
C38EF212255B6D3A007E1867 /* Theme.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = Theme.h; path = SignalUtilitiesKit/Utilities/Theme.h; sourceTree = SOURCE_ROOT; };
|
||||
C38EF214255B6D3A007E1867 /* Theme.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = Theme.m; path = SignalUtilitiesKit/Utilities/Theme.m; sourceTree = SOURCE_ROOT; };
|
||||
C38EF223255B6D5D007E1867 /* AttachmentSharing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AttachmentSharing.m; path = "SignalUtilitiesKit/Media Viewing & Editing/AttachmentSharing.m"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF224255B6D5D007E1867 /* SignalAttachment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SignalAttachment.swift; path = "SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF225255B6D5D007E1867 /* AttachmentSharing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AttachmentSharing.h; path = "SignalUtilitiesKit/Media Viewing & Editing/AttachmentSharing.h"; sourceTree = SOURCE_ROOT; };
|
||||
|
@ -1585,7 +1542,6 @@
|
|||
C38EF2E2255B6DB9007E1867 /* OWSScreenLock.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OWSScreenLock.swift; path = "SignalUtilitiesKit/Screen Lock/OWSScreenLock.swift"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF2E3255B6DB9007E1867 /* OWSUnreadIndicator.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSUnreadIndicator.m; path = SignalUtilitiesKit/Messaging/OWSUnreadIndicator.m; sourceTree = SOURCE_ROOT; };
|
||||
C38EF2E4255B6DB9007E1867 /* FullTextSearcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = FullTextSearcher.swift; path = SignalUtilitiesKit/Messaging/FullTextSearcher.swift; sourceTree = SOURCE_ROOT; };
|
||||
C38EF2E5255B6DB9007E1867 /* AppPreferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AppPreferences.swift; path = SignalUtilitiesKit/Database/AppPreferences.swift; sourceTree = SOURCE_ROOT; };
|
||||
C38EF2E9255B6DBA007E1867 /* OWSUnreadIndicator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSUnreadIndicator.h; path = SignalUtilitiesKit/Messaging/OWSUnreadIndicator.h; sourceTree = SOURCE_ROOT; };
|
||||
C38EF2EC255B6DBA007E1867 /* ProximityMonitoringManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ProximityMonitoringManager.swift; path = SessionMessagingKit/Utilities/ProximityMonitoringManager.swift; sourceTree = SOURCE_ROOT; };
|
||||
C38EF2ED255B6DBB007E1867 /* DisplayableText.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DisplayableText.swift; path = SignalUtilitiesKit/Utilities/DisplayableText.swift; sourceTree = SOURCE_ROOT; };
|
||||
|
@ -1651,7 +1607,6 @@
|
|||
C38EF3B7255B6DE6007E1867 /* ImageEditorCanvasView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ImageEditorCanvasView.swift; path = "SignalUtilitiesKit/Media Viewing & Editing/Image Editing/ImageEditorCanvasView.swift"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF3D1255B6DEE007E1867 /* ThreadViewHelper.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ThreadViewHelper.m; path = SignalUtilitiesKit/Database/ThreadViewHelper.m; sourceTree = SOURCE_ROOT; };
|
||||
C38EF3D2255B6DEE007E1867 /* ThreadViewHelper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ThreadViewHelper.h; path = SignalUtilitiesKit/Database/ThreadViewHelper.h; sourceTree = SOURCE_ROOT; };
|
||||
C38EF3D3255B6DEE007E1867 /* OWSSearchBar.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSSearchBar.h; path = "SignalUtilitiesKit/Shared Views/OWSSearchBar.h"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF3D4255B6DEE007E1867 /* DisappearingTimerConfigurationView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DisappearingTimerConfigurationView.swift; path = SignalUtilitiesKit/Messaging/DisappearingTimerConfigurationView.swift; sourceTree = SOURCE_ROOT; };
|
||||
C38EF3D6255B6DEF007E1867 /* ContactCellView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ContactCellView.m; path = "SignalUtilitiesKit/To Do/ContactCellView.m"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF3D7255B6DF0007E1867 /* OWSTextField.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = OWSTextField.h; path = "SignalUtilitiesKit/Shared Views/OWSTextField.h"; sourceTree = SOURCE_ROOT; };
|
||||
|
@ -1670,7 +1625,6 @@
|
|||
C38EF3E6255B6DF4007E1867 /* ContactTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ContactTableViewCell.h; path = "SignalUtilitiesKit/To Do/ContactTableViewCell.h"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF3E7255B6DF5007E1867 /* OWSButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OWSButton.swift; path = "SignalUtilitiesKit/Shared Views/OWSButton.swift"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF3E9255B6DF6007E1867 /* Toast.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Toast.swift; path = "SignalUtilitiesKit/Shared Views/Toast.swift"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF3EA255B6DF6007E1867 /* OWSSearchBar.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = OWSSearchBar.m; path = "SignalUtilitiesKit/Shared Views/OWSSearchBar.m"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF3EB255B6DF6007E1867 /* ContactTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ContactTableViewCell.m; path = "SignalUtilitiesKit/To Do/ContactTableViewCell.m"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF3EC255B6DF6007E1867 /* OWSFlatButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OWSFlatButton.swift; path = "SignalUtilitiesKit/Shared Views/OWSFlatButton.swift"; sourceTree = SOURCE_ROOT; };
|
||||
C38EF3ED255B6DF6007E1867 /* TappableStackView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TappableStackView.swift; path = "SignalUtilitiesKit/Shared Views/TappableStackView.swift"; sourceTree = SOURCE_ROOT; };
|
||||
|
@ -1751,7 +1705,6 @@
|
|||
C3CA3ABD255CDB0D00F4C6D4 /* portuguese.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = portuguese.txt; sourceTree = "<group>"; };
|
||||
C3CA3AC7255CDB2900F4C6D4 /* spanish.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = spanish.txt; sourceTree = "<group>"; };
|
||||
C3D0972A2510499C00F6E3E4 /* BackgroundPoller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundPoller.swift; sourceTree = "<group>"; };
|
||||
C3D9E40B25676C100040E4F3 /* Storage+Conformances.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+Conformances.swift"; sourceTree = "<group>"; };
|
||||
C3D9E41E25676C870040E4F3 /* OWSPrimaryStorageProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OWSPrimaryStorageProtocol.swift; sourceTree = "<group>"; };
|
||||
C3D9E43025676D3D0040E4F3 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
|
||||
C3DA9C0625AE7396008F7C7E /* ConfigurationMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationMessage.swift; sourceTree = "<group>"; };
|
||||
|
@ -2029,8 +1982,6 @@
|
|||
4CC1ECFA211A553000CC13BE /* AppUpdateNag.swift */,
|
||||
B90418E4183E9DD40038554A /* DateUtil.h */,
|
||||
B90418E5183E9DD40038554A /* DateUtil.m */,
|
||||
344825C4211390C700DB4BD8 /* OWSOrphanDataCleaner.h */,
|
||||
344825C5211390C800DB4BD8 /* OWSOrphanDataCleaner.m */,
|
||||
4C21D5D5223A9DC500EF8A77 /* UIAlerts+iOS9.m */,
|
||||
45C0DC1A1E68FE9000E04C47 /* UIApplication+OWS.swift */,
|
||||
45C0DC1D1E69011F00E04C47 /* UIStoryboard+OWS.swift */,
|
||||
|
@ -2940,28 +2891,6 @@
|
|||
path = Notifications;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C36096BC25AD1C3E008B62B2 /* Backups */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
340FC87F204DAC8C007AEB0F /* OWSBackupSettingsViewController.h */,
|
||||
340FC88E204DAC8C007AEB0F /* OWSBackupSettingsViewController.m */,
|
||||
3441FD9E21A3604F00BB9542 /* BackupRestoreViewController.swift */,
|
||||
3496956421A301A100DCFE74 /* OWSBackup.h */,
|
||||
3496956921A301A100DCFE74 /* OWSBackup.m */,
|
||||
3496956B21A301A100DCFE74 /* OWSBackupAPI.swift */,
|
||||
3496956821A301A100DCFE74 /* OWSBackupExportJob.h */,
|
||||
3496956221A301A100DCFE74 /* OWSBackupExportJob.m */,
|
||||
3496956C21A301A100DCFE74 /* OWSBackupImportJob.h */,
|
||||
3496956621A301A100DCFE74 /* OWSBackupImportJob.m */,
|
||||
3496956D21A301A100DCFE74 /* OWSBackupIO.h */,
|
||||
3496956521A301A100DCFE74 /* OWSBackupIO.m */,
|
||||
3496956721A301A100DCFE74 /* OWSBackupJob.h */,
|
||||
3496956A21A301A100DCFE74 /* OWSBackupJob.m */,
|
||||
3496956321A301A100DCFE74 /* OWSBackupLazyRestore.swift */,
|
||||
);
|
||||
path = Backups;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C36096ED25AD20FD008B62B2 /* Media Viewing & Editing */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -3084,8 +3013,6 @@
|
|||
C38EF3EC255B6DF6007E1867 /* OWSFlatButton.swift */,
|
||||
C38EF3DB255B6DF1007E1867 /* OWSLayerView.swift */,
|
||||
C38EF3D9255B6DF1007E1867 /* OWSNavigationBar.swift */,
|
||||
C38EF3D3255B6DEE007E1867 /* OWSSearchBar.h */,
|
||||
C38EF3EA255B6DF6007E1867 /* OWSSearchBar.m */,
|
||||
C38EF3D7255B6DF0007E1867 /* OWSTextField.h */,
|
||||
C38EF3E0255B6DF3007E1867 /* OWSTextField.m */,
|
||||
C38EF3D8255B6DF0007E1867 /* OWSTextView.h */,
|
||||
|
@ -3109,8 +3036,6 @@
|
|||
C38EF3EB255B6DF6007E1867 /* ContactTableViewCell.m */,
|
||||
C38EF2D2255B6DAF007E1867 /* OWSProfileManager.h */,
|
||||
C38EF2CF255B6DAE007E1867 /* OWSProfileManager.m */,
|
||||
C33FDBBB255A581600E217F9 /* OWSPrimaryStorage+Loki.h */,
|
||||
C33FDB58255A580E00E217F9 /* OWSPrimaryStorage+Loki.m */,
|
||||
);
|
||||
path = "To Do";
|
||||
sourceTree = "<group>";
|
||||
|
@ -3139,14 +3064,10 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
C379DCE82567330E0002D4EB /* Migrations */,
|
||||
C38EF2E5255B6DB9007E1867 /* AppPreferences.swift */,
|
||||
C33FDBBA255A581600E217F9 /* OWSPrimaryStorage+keyFromIntLong.h */,
|
||||
C33FDB99255A581300E217F9 /* OWSPrimaryStorage+keyFromIntLong.m */,
|
||||
C3D9E40B25676C100040E4F3 /* Storage+Conformances.swift */,
|
||||
C38EF3D2255B6DEE007E1867 /* ThreadViewHelper.h */,
|
||||
C38EF3D1255B6DEE007E1867 /* ThreadViewHelper.m */,
|
||||
C33FDBA0255A581400E217F9 /* TSStorageHeaders.h */,
|
||||
C33FDBEF255A581B00E217F9 /* TSStorageKeys.h */,
|
||||
C33FDA6D255A57FA00E217F9 /* YapDatabase+Promise.swift */,
|
||||
);
|
||||
path = Database;
|
||||
|
@ -3359,8 +3280,6 @@
|
|||
C38EF302255B6DBE007E1867 /* OWSAnyTouchGestureRecognizer.h */,
|
||||
C38EF2F0255B6DBB007E1867 /* OWSAnyTouchGestureRecognizer.m */,
|
||||
C38EF2ED255B6DBB007E1867 /* DisplayableText.swift */,
|
||||
C38EF212255B6D3A007E1867 /* Theme.h */,
|
||||
C38EF214255B6D3A007E1867 /* Theme.m */,
|
||||
C38EF3DC255B6DF1007E1867 /* DirectionalPanGestureRecognizer.swift */,
|
||||
C38EF240255B6D67007E1867 /* UIView+OWS.swift */,
|
||||
C38EF236255B6D65007E1867 /* UIViewController+OWS.h */,
|
||||
|
@ -3573,7 +3492,6 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
C3F0A58F255C8E3D007BE2A3 /* Meta */,
|
||||
C36096BC25AD1C3E008B62B2 /* Backups */,
|
||||
C360969C25AD18BA008B62B2 /* Closed Groups */,
|
||||
B835246C25C38AA20089A44F /* Conversations */,
|
||||
C32B405424A961E1001117B5 /* Dependencies */,
|
||||
|
@ -3631,7 +3549,6 @@
|
|||
C33FDD5B255A582000E217F9 /* OWSOperation.h in Headers */,
|
||||
C38EF313255B6DBF007E1867 /* OWSUnreadIndicator.h in Headers */,
|
||||
C33FDD7C255A582000E217F9 /* SSKAsserts.h in Headers */,
|
||||
C33FDDA9255A582000E217F9 /* TSStorageKeys.h in Headers */,
|
||||
C38EF3F6255B6DF7007E1867 /* OWSTextView.h in Headers */,
|
||||
C38EF24C255B6D67007E1867 /* NSAttributedString+OWS.h in Headers */,
|
||||
C38EF32B255B6DBF007E1867 /* OWSFormat.h in Headers */,
|
||||
|
@ -3643,12 +3560,9 @@
|
|||
C38EF243255B6D67007E1867 /* UIViewController+OWS.h in Headers */,
|
||||
C38EF35D255B6DCC007E1867 /* OWSNavigationController.h in Headers */,
|
||||
C38EF249255B6D67007E1867 /* UIColor+OWS.h in Headers */,
|
||||
C38EF216255B6D3B007E1867 /* Theme.h in Headers */,
|
||||
C38EF3F0255B6DF7007E1867 /* ThreadViewHelper.h in Headers */,
|
||||
C38EF274255B6D7A007E1867 /* OWSResaveCollectionDBMigration.h in Headers */,
|
||||
C33FDC95255A582000E217F9 /* OWSFailedMessagesJob.h in Headers */,
|
||||
C38EF3F1255B6DF7007E1867 /* OWSSearchBar.h in Headers */,
|
||||
C33FDD75255A582000E217F9 /* OWSPrimaryStorage+Loki.h in Headers */,
|
||||
C38EF277255B6D7A007E1867 /* OWSDatabaseMigration.h in Headers */,
|
||||
C38EF3F5255B6DF7007E1867 /* OWSTextField.h in Headers */,
|
||||
C38EF275255B6D7A007E1867 /* OWSDatabaseMigrationRunner.h in Headers */,
|
||||
|
@ -3669,7 +3583,6 @@
|
|||
C33FD9AF255A548A00E217F9 /* SignalUtilitiesKit.h in Headers */,
|
||||
C33FDC50255A582000E217F9 /* OWSDispatch.h in Headers */,
|
||||
C33FDD06255A582000E217F9 /* AppVersion.h in Headers */,
|
||||
C33FDD5A255A582000E217F9 /* TSStorageHeaders.h in Headers */,
|
||||
C33FDCA2255A582000E217F9 /* OWSMessageUtils.h in Headers */,
|
||||
C38EF28F255B6D86007E1867 /* VersionMigrations.h in Headers */,
|
||||
);
|
||||
|
@ -4445,7 +4358,6 @@
|
|||
C38EF3FD255B6DF7007E1867 /* OWSTextView.m in Sources */,
|
||||
C38EF3C6255B6DE7007E1867 /* ImageEditorModel.swift in Sources */,
|
||||
C38EF317255B6DBF007E1867 /* DisplayableText.swift in Sources */,
|
||||
C38EF30F255B6DBF007E1867 /* AppPreferences.swift in Sources */,
|
||||
C38EF3C3255B6DE7007E1867 /* ImageEditorTextItem.swift in Sources */,
|
||||
FD28A4F227E990E800FF65E7 /* BlockingManagerRemovalMigration.swift in Sources */,
|
||||
C33FDC7D255A582000E217F9 /* OWSDispatch.m in Sources */,
|
||||
|
@ -4455,8 +4367,6 @@
|
|||
C38EF39B255B6DDA007E1867 /* ThreadViewModel.swift in Sources */,
|
||||
C38EF2A5255B6D93007E1867 /* Identicon+ObjC.swift in Sources */,
|
||||
C38EF273255B6D7A007E1867 /* OWSDatabaseMigrationRunner.m in Sources */,
|
||||
C33FDD12255A582000E217F9 /* OWSPrimaryStorage+Loki.m in Sources */,
|
||||
C3D9E40C25676C100040E4F3 /* Storage+Conformances.swift in Sources */,
|
||||
C38EF31A255B6DBF007E1867 /* OWSAnyTouchGestureRecognizer.m in Sources */,
|
||||
C38EF385255B6DD2007E1867 /* AttachmentTextToolbar.swift in Sources */,
|
||||
C33FDD23255A582000E217F9 /* FeatureFlags.swift in Sources */,
|
||||
|
@ -4501,7 +4411,6 @@
|
|||
C38EF400255B6DF7007E1867 /* GalleryRailView.swift in Sources */,
|
||||
C38EF32E255B6DBF007E1867 /* ImageCache.swift in Sources */,
|
||||
C38EF32F255B6DBF007E1867 /* OWSFormat.m in Sources */,
|
||||
C38EF218255B6D3B007E1867 /* Theme.m in Sources */,
|
||||
C38EF3BA255B6DE7007E1867 /* ImageEditorItem.swift in Sources */,
|
||||
C38EF3F7255B6DF7007E1867 /* OWSNavigationBar.swift in Sources */,
|
||||
C38EF2D4255B6DAF007E1867 /* OWSProfileManager.m in Sources */,
|
||||
|
@ -4515,7 +4424,6 @@
|
|||
C33FDCFA255A582000E217F9 /* SignalIOSProto.swift in Sources */,
|
||||
C33FDD13255A582000E217F9 /* OWSFailedAttachmentDownloadsJob.m in Sources */,
|
||||
C38EF24D255B6D67007E1867 /* UIView+OWS.swift in Sources */,
|
||||
C38EF408255B6DF7007E1867 /* OWSSearchBar.m in Sources */,
|
||||
C38EF38B255B6DD2007E1867 /* AttachmentPrepViewController.swift in Sources */,
|
||||
C33FDC7B255A582000E217F9 /* NSSet+Functional.m in Sources */,
|
||||
C38EF405255B6DF7007E1867 /* OWSButton.swift in Sources */,
|
||||
|
@ -4814,16 +4722,13 @@
|
|||
B8041AA725C90927003C2166 /* TypingIndicatorCell.swift in Sources */,
|
||||
B8CCF63723961D6D0091D419 /* NewDMVC.swift in Sources */,
|
||||
452EC6DF205E9E30000E787C /* MediaGalleryViewController.swift in Sources */,
|
||||
3496956E21A301A100DCFE74 /* OWSBackupExportJob.m in Sources */,
|
||||
4C1885D2218F8E1C00B67051 /* PhotoGridViewCell.swift in Sources */,
|
||||
34D1F0501F7D45A60066283D /* GifPickerCell.swift in Sources */,
|
||||
3496957421A301A100DCFE74 /* OWSBackupAPI.swift in Sources */,
|
||||
C3E5C2FA251DBABB0040DFFC /* EditClosedGroupVC.swift in Sources */,
|
||||
B8783E9E23EB948D00404FB8 /* UILabel+Interaction.swift in Sources */,
|
||||
B893063F2383961A005EAA8E /* ScanQRCodeWrapperVC.swift in Sources */,
|
||||
B879D449247E1BE300DB3608 /* PathVC.swift in Sources */,
|
||||
454A84042059C787008B8C75 /* MediaTileViewController.swift in Sources */,
|
||||
340FC8B4204DAC8D007AEB0F /* OWSBackupSettingsViewController.m in Sources */,
|
||||
34D1F0871F8678AA0066283D /* ConversationViewItem.m in Sources */,
|
||||
451A13B11E13DED2000A50FD /* AppNotifications.swift in Sources */,
|
||||
34D99CE4217509C2000AFB39 /* AppEnvironment.swift in Sources */,
|
||||
|
@ -4859,7 +4764,6 @@
|
|||
C3548F0624456447009433A8 /* PNModeVC.swift in Sources */,
|
||||
B80A579F23DFF1F300876683 /* NewClosedGroupVC.swift in Sources */,
|
||||
D221A09A169C9E5E00537ABF /* main.m in Sources */,
|
||||
3496957221A301A100DCFE74 /* OWSBackup.m in Sources */,
|
||||
B835247925C38D880089A44F /* MessageCell.swift in Sources */,
|
||||
B86BD08623399CEF000F5AE3 /* SeedModal.swift in Sources */,
|
||||
34E3E5681EC4B19400495BAC /* AudioProgressView.swift in Sources */,
|
||||
|
@ -4869,7 +4773,6 @@
|
|||
B82149B825D60393009C0F2A /* BlockedModal.swift in Sources */,
|
||||
B82B408C239A068800A248E7 /* RegisterVC.swift in Sources */,
|
||||
346129991FD1E4DA00532771 /* SignalApp.m in Sources */,
|
||||
3496957121A301A100DCFE74 /* OWSBackupImportJob.m in Sources */,
|
||||
34BECE301F7ABCF800D7438D /* GifPickerLayout.swift in Sources */,
|
||||
C331FFFE2558FF3B00070591 /* ConversationCell.swift in Sources */,
|
||||
FD659AC027A7649600F12C02 /* MessageRequestsViewController.swift in Sources */,
|
||||
|
@ -4890,7 +4793,6 @@
|
|||
7BA9057E27911C5800998B3C /* GlobalSearchViewController.swift in Sources */,
|
||||
4CA485BB2232339F004B9E7D /* PhotoCaptureViewController.swift in Sources */,
|
||||
34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */,
|
||||
344825C6211390C800DB4BD8 /* OWSOrphanDataCleaner.m in Sources */,
|
||||
C328254925CA60E60062D0A7 /* ContextMenuVC+Action.swift in Sources */,
|
||||
4542DF54208D40AC007B4E76 /* LoadingViewController.swift in Sources */,
|
||||
34D5CCA91EAE3D30005515DB /* AvatarViewHelper.m in Sources */,
|
||||
|
@ -4917,7 +4819,6 @@
|
|||
B875885A264503A6000E60D0 /* JoinOpenGroupModal.swift in Sources */,
|
||||
B8CCF6432397711F0091D419 /* SettingsVC.swift in Sources */,
|
||||
C354E75A23FE2A7600CE22E3 /* BaseVC.swift in Sources */,
|
||||
3441FD9F21A3604F00BB9542 /* BackupRestoreViewController.swift in Sources */,
|
||||
45C0DC1B1E68FE9000E04C47 /* UIApplication+OWS.swift in Sources */,
|
||||
4539B5861F79348F007141FF /* PushRegistrationManager.swift in Sources */,
|
||||
B8041A9525C8FA1D003C2166 /* MediaLoaderView.swift in Sources */,
|
||||
|
@ -4939,7 +4840,6 @@
|
|||
34A6C28021E503E700B5B12E /* OWSImagePickerController.swift in Sources */,
|
||||
C31A6C5C247F2CF3001123EF /* CGRect+Utilities.swift in Sources */,
|
||||
4C21D5D6223A9DC500EF8A77 /* UIAlerts+iOS9.m in Sources */,
|
||||
3496957021A301A100DCFE74 /* OWSBackupIO.m in Sources */,
|
||||
B8269D3325C7A8C600488AB4 /* InputViewButton.swift in Sources */,
|
||||
B8269D3D25C7B34D00488AB4 /* InputTextView.swift in Sources */,
|
||||
76EB054018170B33006006FC /* AppDelegate.m in Sources */,
|
||||
|
@ -4959,7 +4859,6 @@
|
|||
B835249B25C3AB650089A44F /* VisibleMessageCell.swift in Sources */,
|
||||
340FC8AE204DAC8D007AEB0F /* OWSSoundSettingsViewController.m in Sources */,
|
||||
B8D0A25025E3678700C1835E /* LinkDeviceVC.swift in Sources */,
|
||||
3496957321A301A100DCFE74 /* OWSBackupJob.m in Sources */,
|
||||
B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */,
|
||||
7B4C75CD26BB92060000AC89 /* DeletedMessageView.swift in Sources */,
|
||||
B897621C25D201F7004F83B2 /* ScrollToBottomButton.swift in Sources */,
|
||||
|
@ -4976,7 +4875,6 @@
|
|||
7BA7F4BD27A216B600B3A466 /* Storage+RecentSearchResults.swift in Sources */,
|
||||
B8269D2925C7A4B400488AB4 /* InputView.swift in Sources */,
|
||||
C374EEE225DA26740073A857 /* LinkPreviewModal.swift in Sources */,
|
||||
3496956F21A301A100DCFE74 /* OWSBackupLazyRestore.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
@ -1,175 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@objc
|
||||
public class BackupRestoreViewController: OWSTableViewController {
|
||||
|
||||
private var hasBegunImport = false
|
||||
|
||||
// MARK: - Dependencies
|
||||
|
||||
private var backup: OWSBackup {
|
||||
return AppEnvironment.shared.backup
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
override public func loadView() {
|
||||
super.loadView()
|
||||
|
||||
navigationItem.title = NSLocalizedString("SETTINGS_BACKUP", comment: "Label for the backup view in app settings.")
|
||||
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(didPressCancelButton))
|
||||
}
|
||||
|
||||
override public func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(backupStateDidChange),
|
||||
name: NSNotification.Name(NSNotificationNameBackupStateDidChange),
|
||||
object: nil)
|
||||
|
||||
updateTableContents()
|
||||
}
|
||||
|
||||
private func updateTableContents() {
|
||||
if hasBegunImport {
|
||||
updateProgressContents()
|
||||
} else {
|
||||
updateDecisionContents()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateDecisionContents() {
|
||||
let contents = OWSTableContents()
|
||||
|
||||
let section = OWSTableSection()
|
||||
|
||||
section.headerTitle = NSLocalizedString("BACKUP_RESTORE_DECISION_TITLE", comment: "Label for the backup restore decision section.")
|
||||
|
||||
section.add(OWSTableItem.actionItem(withText: NSLocalizedString("CHECK_FOR_BACKUP_DO_NOT_RESTORE",
|
||||
comment: "The label for the 'do not restore backup' button."), actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.cancelAndDismiss()
|
||||
}))
|
||||
section.add(OWSTableItem.actionItem(withText: NSLocalizedString("CHECK_FOR_BACKUP_RESTORE",
|
||||
comment: "The label for the 'restore backup' button."), actionBlock: { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.startImport()
|
||||
}))
|
||||
|
||||
contents.addSection(section)
|
||||
self.contents = contents
|
||||
}
|
||||
|
||||
private var progressFormatter: NumberFormatter = {
|
||||
let numberFormatter = NumberFormatter()
|
||||
numberFormatter.numberStyle = .percent
|
||||
numberFormatter.maximumFractionDigits = 0
|
||||
numberFormatter.multiplier = 1
|
||||
return numberFormatter
|
||||
}()
|
||||
|
||||
private func updateProgressContents() {
|
||||
let contents = OWSTableContents()
|
||||
|
||||
let section = OWSTableSection()
|
||||
|
||||
section.add(OWSTableItem.label(withText: NSLocalizedString("BACKUP_RESTORE_STATUS", comment: "Label for the backup restore status."), accessoryText: NSStringForBackupImportState(backup.backupImportState)))
|
||||
|
||||
if backup.backupImportState == .inProgress {
|
||||
if let backupImportDescription = backup.backupImportDescription {
|
||||
section.add(OWSTableItem.label(withText: NSLocalizedString("BACKUP_RESTORE_DESCRIPTION", comment: "Label for the backup restore description."), accessoryText: backupImportDescription))
|
||||
}
|
||||
|
||||
if let backupImportProgress = backup.backupImportProgress {
|
||||
let progressInt = backupImportProgress.floatValue * 100
|
||||
if let progressString = progressFormatter.string(from: NSNumber(value: progressInt)) {
|
||||
section.add(OWSTableItem.label(withText: NSLocalizedString("BACKUP_RESTORE_PROGRESS", comment: "Label for the backup restore progress."), accessoryText: progressString))
|
||||
} else {
|
||||
owsFailDebug("Could not format progress: \(progressInt)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
contents.addSection(section)
|
||||
self.contents = contents
|
||||
|
||||
// TODO: Add cancel button.
|
||||
}
|
||||
|
||||
// MARK: Helpers
|
||||
|
||||
@objc
|
||||
private func didPressCancelButton(sender: UIButton) {
|
||||
Logger.info("")
|
||||
|
||||
// TODO: Cancel import.
|
||||
|
||||
cancelAndDismiss()
|
||||
}
|
||||
|
||||
@objc
|
||||
private func cancelAndDismiss() {
|
||||
Logger.info("")
|
||||
|
||||
backup.setHasPendingRestoreDecision(false)
|
||||
|
||||
showHomeView()
|
||||
}
|
||||
|
||||
@objc
|
||||
private func startImport() {
|
||||
Logger.info("")
|
||||
|
||||
hasBegunImport = true
|
||||
|
||||
backup.tryToImport()
|
||||
}
|
||||
|
||||
private func showHomeView() {
|
||||
// In production, this view will never be presented in a modal.
|
||||
// During testing (debug UI, etc.), it may be a modal.
|
||||
let isModal = navigationController?.presentingViewController != nil
|
||||
if isModal {
|
||||
dismiss(animated: true, completion: {
|
||||
SignalApp.shared().showHomeView()
|
||||
})
|
||||
} else {
|
||||
SignalApp.shared().showHomeView()
|
||||
}
|
||||
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
@objc func backupStateDidChange() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
Logger.verbose("backup.backupImportState: \(NSStringForBackupImportState(backup.backupImportState))")
|
||||
Logger.flush()
|
||||
|
||||
if backup.backupImportState == .succeeded {
|
||||
backup.setHasPendingRestoreDecision(false)
|
||||
|
||||
showHomeView()
|
||||
} else {
|
||||
updateTableContents()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Orientation
|
||||
|
||||
public override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
||||
return .portrait
|
||||
}
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSString *const NSNotificationNameBackupStateDidChange;
|
||||
|
||||
typedef void (^OWSBackupBoolBlock)(BOOL value);
|
||||
typedef void (^OWSBackupStringListBlock)(NSArray<NSString *> *value);
|
||||
typedef void (^OWSBackupErrorBlock)(NSError *error);
|
||||
|
||||
typedef NS_ENUM(NSUInteger, OWSBackupState) {
|
||||
// Has never backed up, not trying to backup yet.
|
||||
OWSBackupState_Idle = 0,
|
||||
// Backing up.
|
||||
OWSBackupState_InProgress,
|
||||
// Last backup failed.
|
||||
OWSBackupState_Failed,
|
||||
// Last backup succeeded.
|
||||
OWSBackupState_Succeeded,
|
||||
};
|
||||
|
||||
NSString *NSStringForBackupExportState(OWSBackupState state);
|
||||
NSString *NSStringForBackupImportState(OWSBackupState state);
|
||||
|
||||
NSArray<NSString *> *MiscCollectionsToBackup(void);
|
||||
|
||||
NSError *OWSBackupErrorWithDescription(NSString *description);
|
||||
|
||||
@class AnyPromise;
|
||||
@class OWSBackupIO;
|
||||
@class TSAttachmentPointer;
|
||||
@class TSThread;
|
||||
@class YapDatabaseConnection;
|
||||
|
||||
@interface OWSBackup : NSObject
|
||||
|
||||
- (instancetype)init NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
+ (instancetype)sharedManager NS_SWIFT_NAME(shared());
|
||||
|
||||
#pragma mark - Backup Export
|
||||
|
||||
@property (atomic, readonly) OWSBackupState backupExportState;
|
||||
|
||||
// If a "backup export" is in progress (see backupExportState),
|
||||
// backupExportDescription _might_ contain a string that describes
|
||||
// the current phase and backupExportProgress _might_ contain a
|
||||
// 0.0<=x<=1.0 progress value that indicates progress within the
|
||||
// current phase.
|
||||
@property (nonatomic, readonly, nullable) NSString *backupExportDescription;
|
||||
@property (nonatomic, readonly, nullable) NSNumber *backupExportProgress;
|
||||
|
||||
+ (BOOL)isFeatureEnabled;
|
||||
|
||||
- (BOOL)isBackupEnabled;
|
||||
- (void)setIsBackupEnabled:(BOOL)value;
|
||||
|
||||
- (BOOL)hasPendingRestoreDecision;
|
||||
- (void)setHasPendingRestoreDecision:(BOOL)value;
|
||||
|
||||
- (void)tryToExportBackup;
|
||||
- (void)cancelExportBackup;
|
||||
|
||||
#pragma mark - Backup Import
|
||||
|
||||
@property (atomic, readonly) OWSBackupState backupImportState;
|
||||
|
||||
// If a "backup import" is in progress (see backupImportState),
|
||||
// backupImportDescription _might_ contain a string that describes
|
||||
// the current phase and backupImportProgress _might_ contain a
|
||||
// 0.0<=x<=1.0 progress value that indicates progress within the
|
||||
// current phase.
|
||||
@property (nonatomic, readonly, nullable) NSString *backupImportDescription;
|
||||
@property (nonatomic, readonly, nullable) NSNumber *backupImportProgress;
|
||||
|
||||
- (void)allRecipientIdsWithManifestsInCloud:(OWSBackupStringListBlock)success failure:(OWSBackupErrorBlock)failure;
|
||||
|
||||
- (AnyPromise *)ensureCloudKitAccess __attribute__((warn_unused_result));
|
||||
|
||||
- (void)checkCanImportBackup:(OWSBackupBoolBlock)success failure:(OWSBackupErrorBlock)failure;
|
||||
|
||||
// TODO: After a successful import, we should enable backup and
|
||||
// preserve our PIN and/or private key so that restored users
|
||||
// continues to backup.
|
||||
- (void)tryToImportBackup;
|
||||
- (void)cancelImportBackup;
|
||||
|
||||
- (void)logBackupRecords;
|
||||
- (void)clearAllCloudKitRecords;
|
||||
|
||||
- (void)logBackupMetadataCache:(YapDatabaseConnection *)dbConnection;
|
||||
|
||||
#pragma mark - Lazy Restore
|
||||
|
||||
- (NSArray<NSString *> *)attachmentRecordNamesForLazyRestore;
|
||||
|
||||
- (NSArray<NSString *> *)attachmentIdsForLazyRestore;
|
||||
|
||||
- (AnyPromise *)lazyRestoreAttachment:(TSAttachmentPointer *)attachment backupIO:(OWSBackupIO *)backupIO __attribute__((warn_unused_result));
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,911 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSBackup.h"
|
||||
#import "OWSBackupExportJob.h"
|
||||
#import "OWSBackupIO.h"
|
||||
#import "OWSBackupImportJob.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <PromiseKit/AnyPromise.h>
|
||||
#import <SignalCoreKit/Randomness.h>
|
||||
#import <SessionMessagingKit/OWSIdentityManager.h>
|
||||
#import <SessionMessagingKit/YapDatabaseConnection+OWS.h>
|
||||
|
||||
@import CloudKit;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
NSString *const NSNotificationNameBackupStateDidChange = @"NSNotificationNameBackupStateDidChange";
|
||||
|
||||
NSString *const OWSPrimaryStorage_OWSBackupCollection = @"OWSPrimaryStorage_OWSBackupCollection";
|
||||
NSString *const OWSBackup_IsBackupEnabledKey = @"OWSBackup_IsBackupEnabledKey";
|
||||
NSString *const OWSBackup_LastExportSuccessDateKey = @"OWSBackup_LastExportSuccessDateKey";
|
||||
NSString *const OWSBackup_LastExportFailureDateKey = @"OWSBackup_LastExportFailureDateKey";
|
||||
NSString *const OWSBackupErrorDomain = @"OWSBackupErrorDomain";
|
||||
|
||||
NSString *NSStringForBackupExportState(OWSBackupState state)
|
||||
{
|
||||
switch (state) {
|
||||
case OWSBackupState_Idle:
|
||||
return NSLocalizedString(@"SETTINGS_BACKUP_STATUS_IDLE", @"Indicates that app is not backing up.");
|
||||
case OWSBackupState_InProgress:
|
||||
return NSLocalizedString(@"SETTINGS_BACKUP_STATUS_IN_PROGRESS", @"Indicates that app is backing up.");
|
||||
case OWSBackupState_Failed:
|
||||
return NSLocalizedString(@"SETTINGS_BACKUP_STATUS_FAILED", @"Indicates that the last backup failed.");
|
||||
case OWSBackupState_Succeeded:
|
||||
return NSLocalizedString(@"SETTINGS_BACKUP_STATUS_SUCCEEDED", @"Indicates that the last backup succeeded.");
|
||||
}
|
||||
}
|
||||
|
||||
NSString *NSStringForBackupImportState(OWSBackupState state)
|
||||
{
|
||||
switch (state) {
|
||||
case OWSBackupState_Idle:
|
||||
return NSLocalizedString(@"SETTINGS_BACKUP_IMPORT_STATUS_IDLE", @"Indicates that app is not restoring up.");
|
||||
case OWSBackupState_InProgress:
|
||||
return NSLocalizedString(
|
||||
@"SETTINGS_BACKUP_IMPORT_STATUS_IN_PROGRESS", @"Indicates that app is restoring up.");
|
||||
case OWSBackupState_Failed:
|
||||
return NSLocalizedString(
|
||||
@"SETTINGS_BACKUP_IMPORT_STATUS_FAILED", @"Indicates that the last backup restore failed.");
|
||||
case OWSBackupState_Succeeded:
|
||||
return NSLocalizedString(
|
||||
@"SETTINGS_BACKUP_IMPORT_STATUS_SUCCEEDED", @"Indicates that the last backup restore succeeded.");
|
||||
}
|
||||
}
|
||||
|
||||
NSArray<NSString *> *MiscCollectionsToBackup(void)
|
||||
{
|
||||
return @[
|
||||
OWSUserProfile.collection,
|
||||
SSKIncrementingIdFinder.collectionName,
|
||||
OWSPreferencesSignalDatabaseCollection,
|
||||
];
|
||||
}
|
||||
|
||||
typedef NS_ENUM(NSInteger, OWSBackupErrorCode) {
|
||||
OWSBackupErrorCodeAssertionFailure = 0,
|
||||
};
|
||||
|
||||
NSError *OWSBackupErrorWithDescription(NSString *description)
|
||||
{
|
||||
return [NSError errorWithDomain:@"OWSBackupErrorDomain"
|
||||
code:OWSBackupErrorCodeAssertionFailure
|
||||
userInfo:@{ NSLocalizedDescriptionKey : description }];
|
||||
}
|
||||
|
||||
// TODO: Observe Reachability.
|
||||
@interface OWSBackup () <OWSBackupJobDelegate>
|
||||
|
||||
@property (nonatomic, readonly) YapDatabaseConnection *dbConnection;
|
||||
|
||||
// This property should only be accessed on the main thread.
|
||||
@property (nonatomic, nullable) OWSBackupExportJob *backupExportJob;
|
||||
|
||||
// This property should only be accessed on the main thread.
|
||||
@property (nonatomic, nullable) OWSBackupImportJob *backupImportJob;
|
||||
|
||||
@property (nonatomic, nullable) NSString *backupExportDescription;
|
||||
@property (nonatomic, nullable) NSNumber *backupExportProgress;
|
||||
|
||||
@property (nonatomic, nullable) NSString *backupImportDescription;
|
||||
@property (nonatomic, nullable) NSNumber *backupImportProgress;
|
||||
|
||||
@property (atomic) OWSBackupState backupExportState;
|
||||
@property (atomic) OWSBackupState backupImportState;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSBackup
|
||||
|
||||
@synthesize dbConnection = _dbConnection;
|
||||
|
||||
+ (instancetype)sharedManager
|
||||
{
|
||||
OWSAssertDebug(AppEnvironment.shared.backup);
|
||||
|
||||
return AppEnvironment.shared.backup;
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
self.backupExportState = OWSBackupState_Idle;
|
||||
self.backupImportState = OWSBackupState_Idle;
|
||||
|
||||
OWSSingletonAssert();
|
||||
|
||||
[AppReadiness runNowOrWhenAppDidBecomeReady:^{
|
||||
[self setup];
|
||||
}];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)setup
|
||||
{
|
||||
if (!OWSBackup.isFeatureEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
[OWSBackupAPI setup];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(applicationDidBecomeActive:)
|
||||
name:OWSApplicationDidBecomeActiveNotification
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(registrationStateDidChange)
|
||||
name:RegistrationStateDidChangeNotification
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(ckAccountChanged)
|
||||
name:CKAccountChangedNotification
|
||||
object:nil];
|
||||
|
||||
// We want to start a backup if necessary on app launch, but app launch is a
|
||||
// busy time and it's important to remain responsive, so wait a few seconds before
|
||||
// starting the backup.
|
||||
//
|
||||
// TODO: Make this period longer.
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
|
||||
[self ensureBackupExportState];
|
||||
});
|
||||
}
|
||||
|
||||
- (YapDatabaseConnection *)dbConnection
|
||||
{
|
||||
@synchronized(self) {
|
||||
if (!_dbConnection) {
|
||||
_dbConnection = self.primaryStorage.newDatabaseConnection;
|
||||
}
|
||||
return _dbConnection;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Dependencies
|
||||
|
||||
- (OWSPrimaryStorage *)primaryStorage
|
||||
{
|
||||
OWSAssertDebug(SSKEnvironment.shared.primaryStorage);
|
||||
|
||||
return SSKEnvironment.shared.primaryStorage;
|
||||
}
|
||||
|
||||
- (TSAccountManager *)tsAccountManager
|
||||
{
|
||||
OWSAssertDebug(SSKEnvironment.shared.tsAccountManager);
|
||||
|
||||
return SSKEnvironment.shared.tsAccountManager;
|
||||
}
|
||||
|
||||
+ (BOOL)isFeatureEnabled
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
#pragma mark - Backup Export
|
||||
|
||||
- (void)tryToExportBackup
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(!self.backupExportJob);
|
||||
|
||||
if (!self.canBackupExport) {
|
||||
// TODO: Offer a reason in the UI.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!self.tsAccountManager.isRegisteredAndReady) {
|
||||
OWSFailDebug(@"Can't backup; not registered and ready.");
|
||||
return;
|
||||
}
|
||||
NSString *_Nullable recipientId = self.tsAccountManager.localNumber;
|
||||
if (recipientId.length < 1) {
|
||||
OWSFailDebug(@"Can't backup; missing recipientId.");
|
||||
return;
|
||||
}
|
||||
|
||||
// In development, make sure there's no export or import in progress.
|
||||
[self.backupExportJob cancel];
|
||||
self.backupExportJob = nil;
|
||||
[self.backupImportJob cancel];
|
||||
self.backupImportJob = nil;
|
||||
|
||||
self.backupExportState = OWSBackupState_InProgress;
|
||||
|
||||
self.backupExportJob = [[OWSBackupExportJob alloc] initWithDelegate:self recipientId:recipientId];
|
||||
[self.backupExportJob start];
|
||||
|
||||
[self postDidChangeNotification];
|
||||
}
|
||||
|
||||
- (void)cancelExportBackup
|
||||
{
|
||||
[self.backupExportJob cancel];
|
||||
self.backupExportJob = nil;
|
||||
|
||||
[self ensureBackupExportState];
|
||||
}
|
||||
|
||||
- (void)setLastExportSuccessDate:(NSDate *)value
|
||||
{
|
||||
OWSAssertDebug(value);
|
||||
|
||||
[self.dbConnection setDate:value
|
||||
forKey:OWSBackup_LastExportSuccessDateKey
|
||||
inCollection:OWSPrimaryStorage_OWSBackupCollection];
|
||||
}
|
||||
|
||||
- (nullable NSDate *)lastExportSuccessDate
|
||||
{
|
||||
return [self.dbConnection dateForKey:OWSBackup_LastExportSuccessDateKey
|
||||
inCollection:OWSPrimaryStorage_OWSBackupCollection];
|
||||
}
|
||||
|
||||
- (void)setLastExportFailureDate:(NSDate *)value
|
||||
{
|
||||
OWSAssertDebug(value);
|
||||
|
||||
[self.dbConnection setDate:value
|
||||
forKey:OWSBackup_LastExportFailureDateKey
|
||||
inCollection:OWSPrimaryStorage_OWSBackupCollection];
|
||||
}
|
||||
|
||||
|
||||
- (nullable NSDate *)lastExportFailureDate
|
||||
{
|
||||
return [self.dbConnection dateForKey:OWSBackup_LastExportFailureDateKey
|
||||
inCollection:OWSPrimaryStorage_OWSBackupCollection];
|
||||
}
|
||||
|
||||
- (BOOL)isBackupEnabled
|
||||
{
|
||||
return [self.dbConnection boolForKey:OWSBackup_IsBackupEnabledKey
|
||||
inCollection:OWSPrimaryStorage_OWSBackupCollection
|
||||
defaultValue:NO];
|
||||
}
|
||||
|
||||
- (void)setIsBackupEnabled:(BOOL)value
|
||||
{
|
||||
[self.dbConnection setBool:value
|
||||
forKey:OWSBackup_IsBackupEnabledKey
|
||||
inCollection:OWSPrimaryStorage_OWSBackupCollection];
|
||||
|
||||
if (!value) {
|
||||
[self.dbConnection removeObjectForKey:OWSBackup_LastExportSuccessDateKey
|
||||
inCollection:OWSPrimaryStorage_OWSBackupCollection];
|
||||
[self.dbConnection removeObjectForKey:OWSBackup_LastExportFailureDateKey
|
||||
inCollection:OWSPrimaryStorage_OWSBackupCollection];
|
||||
}
|
||||
|
||||
[self postDidChangeNotification];
|
||||
|
||||
[self ensureBackupExportState];
|
||||
}
|
||||
|
||||
- (BOOL)hasPendingRestoreDecision
|
||||
{
|
||||
return [self.tsAccountManager hasPendingBackupRestoreDecision];
|
||||
}
|
||||
|
||||
- (void)setHasPendingRestoreDecision:(BOOL)value
|
||||
{
|
||||
[self.tsAccountManager setHasPendingBackupRestoreDecision:value];
|
||||
}
|
||||
|
||||
- (BOOL)canBackupExport
|
||||
{
|
||||
if (!self.isBackupEnabled) {
|
||||
return NO;
|
||||
}
|
||||
if (UIApplication.sharedApplication.applicationState != UIApplicationStateActive) {
|
||||
// Don't start backups when app is in the background.
|
||||
return NO;
|
||||
}
|
||||
if (![self.tsAccountManager isRegisteredAndReady]) {
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)shouldHaveBackupExport
|
||||
{
|
||||
if (!self.canBackupExport) {
|
||||
return NO;
|
||||
}
|
||||
if (self.backupExportJob) {
|
||||
// If there's already a job in progress, let it complete.
|
||||
return YES;
|
||||
}
|
||||
NSDate *_Nullable lastExportSuccessDate = self.lastExportSuccessDate;
|
||||
NSDate *_Nullable lastExportFailureDate = self.lastExportFailureDate;
|
||||
// Wait N hours before retrying after a success.
|
||||
const NSTimeInterval kRetryAfterSuccess = 24 * kHourInterval;
|
||||
if (lastExportSuccessDate && fabs(lastExportSuccessDate.timeIntervalSinceNow) < kRetryAfterSuccess) {
|
||||
return NO;
|
||||
}
|
||||
// Wait N hours before retrying after a failure.
|
||||
const NSTimeInterval kRetryAfterFailure = 6 * kHourInterval;
|
||||
if (lastExportFailureDate && fabs(lastExportFailureDate.timeIntervalSinceNow) < kRetryAfterFailure) {
|
||||
return NO;
|
||||
}
|
||||
// Don't export backup if there's an import in progress.
|
||||
//
|
||||
// This conflict shouldn't occur in production since we won't enable backup
|
||||
// export until an import is complete, but this could happen in development.
|
||||
if (self.backupImportJob) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
// TODO: There's other conditions that affect this decision,
|
||||
// e.g. Reachability, wifi v. cellular, etc.
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)ensureBackupExportState
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
if (!OWSBackup.isFeatureEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CurrentAppContext().isMainApp) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!self.tsAccountManager.isRegisteredAndReady) {
|
||||
OWSLogError(@"Can't backup; not registered and ready.");
|
||||
return;
|
||||
}
|
||||
NSString *_Nullable recipientId = self.tsAccountManager.localNumber;
|
||||
if (recipientId.length < 1) {
|
||||
OWSFailDebug(@"Can't backup; missing recipientId.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Start or abort a backup export if neccessary.
|
||||
if (!self.shouldHaveBackupExport && self.backupExportJob) {
|
||||
[self.backupExportJob cancel];
|
||||
self.backupExportJob = nil;
|
||||
} else if (self.shouldHaveBackupExport && !self.backupExportJob) {
|
||||
self.backupExportJob = [[OWSBackupExportJob alloc] initWithDelegate:self recipientId:recipientId];
|
||||
[self.backupExportJob start];
|
||||
}
|
||||
|
||||
// Update the state flag.
|
||||
OWSBackupState backupExportState = OWSBackupState_Idle;
|
||||
if (self.backupExportJob) {
|
||||
backupExportState = OWSBackupState_InProgress;
|
||||
} else {
|
||||
NSDate *_Nullable lastExportSuccessDate = self.lastExportSuccessDate;
|
||||
NSDate *_Nullable lastExportFailureDate = self.lastExportFailureDate;
|
||||
if (!lastExportSuccessDate && !lastExportFailureDate) {
|
||||
backupExportState = OWSBackupState_Idle;
|
||||
} else if (lastExportSuccessDate && lastExportFailureDate) {
|
||||
backupExportState = ([lastExportSuccessDate isAfterDate:lastExportFailureDate] ? OWSBackupState_Succeeded
|
||||
: OWSBackupState_Failed);
|
||||
} else if (lastExportSuccessDate) {
|
||||
backupExportState = OWSBackupState_Succeeded;
|
||||
} else if (lastExportFailureDate) {
|
||||
backupExportState = OWSBackupState_Failed;
|
||||
} else {
|
||||
OWSFailDebug(@"unexpected condition.");
|
||||
}
|
||||
}
|
||||
|
||||
BOOL stateDidChange = self.backupExportState != backupExportState;
|
||||
self.backupExportState = backupExportState;
|
||||
if (stateDidChange) {
|
||||
[self postDidChangeNotification];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Backup Import
|
||||
|
||||
- (void)allRecipientIdsWithManifestsInCloud:(OWSBackupStringListBlock)success failure:(OWSBackupErrorBlock)failure
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSLogInfo(@"");
|
||||
|
||||
[OWSBackupAPI
|
||||
allRecipientIdsWithManifestsInCloudWithSuccess:^(NSArray<NSString *> *recipientIds) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
success(recipientIds);
|
||||
});
|
||||
}
|
||||
failure:^(NSError *error) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
failure(error);
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
- (AnyPromise *)ensureCloudKitAccess
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSLogInfo(@"");
|
||||
|
||||
AnyPromise * (^failWithUnexpectedError)(void) = ^{
|
||||
NSError *error = [NSError errorWithDomain:OWSBackupErrorDomain
|
||||
code:1
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey : NSLocalizedString(@"BACKUP_UNEXPECTED_ERROR",
|
||||
@"Error shown when backup fails due to an unexpected error.")
|
||||
}];
|
||||
return [AnyPromise promiseWithValue:error];
|
||||
};
|
||||
|
||||
if (!self.tsAccountManager.isRegisteredAndReady) {
|
||||
OWSLogError(@"Can't backup; not registered and ready.");
|
||||
return failWithUnexpectedError();
|
||||
}
|
||||
NSString *_Nullable recipientId = self.tsAccountManager.localNumber;
|
||||
if (recipientId.length < 1) {
|
||||
OWSFailDebug(@"Can't backup; missing recipientId.");
|
||||
return failWithUnexpectedError();
|
||||
}
|
||||
|
||||
return [OWSBackupAPI ensureCloudKitAccessObjc];
|
||||
}
|
||||
|
||||
- (void)checkCanImportBackup:(OWSBackupBoolBlock)success failure:(OWSBackupErrorBlock)failure
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSLogInfo(@"");
|
||||
|
||||
if (!OWSBackup.isFeatureEnabled) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
success(NO);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
void (^failWithUnexpectedError)(void) = ^{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSError *error =
|
||||
[NSError errorWithDomain:OWSBackupErrorDomain
|
||||
code:1
|
||||
userInfo:@{
|
||||
NSLocalizedDescriptionKey : NSLocalizedString(@"BACKUP_UNEXPECTED_ERROR",
|
||||
@"Error shown when backup fails due to an unexpected error.")
|
||||
}];
|
||||
failure(error);
|
||||
});
|
||||
};
|
||||
|
||||
if (!self.tsAccountManager.isRegisteredAndReady) {
|
||||
OWSLogError(@"Can't backup; not registered and ready.");
|
||||
return failWithUnexpectedError();
|
||||
}
|
||||
NSString *_Nullable recipientId = self.tsAccountManager.localNumber;
|
||||
if (recipientId.length < 1) {
|
||||
OWSFailDebug(@"Can't backup; missing recipientId.");
|
||||
return failWithUnexpectedError();
|
||||
}
|
||||
|
||||
[[OWSBackupAPI ensureCloudKitAccessObjc]
|
||||
.thenInBackground(^{
|
||||
return [OWSBackupAPI checkForManifestInCloudObjcWithRecipientId:recipientId];
|
||||
})
|
||||
.then(^(NSNumber *value) {
|
||||
success(value.boolValue);
|
||||
})
|
||||
.catch(^(NSError *error) {
|
||||
failure(error);
|
||||
}) retainUntilComplete];
|
||||
}
|
||||
|
||||
- (void)tryToImportBackup
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(!self.backupImportJob);
|
||||
|
||||
if (!self.tsAccountManager.isRegisteredAndReady) {
|
||||
OWSLogError(@"Can't restore backup; not registered and ready.");
|
||||
return;
|
||||
}
|
||||
NSString *_Nullable recipientId = self.tsAccountManager.localNumber;
|
||||
if (recipientId.length < 1) {
|
||||
OWSLogError(@"Can't restore backup; missing recipientId.");
|
||||
return;
|
||||
}
|
||||
|
||||
// In development, make sure there's no export or import in progress.
|
||||
[self.backupExportJob cancel];
|
||||
self.backupExportJob = nil;
|
||||
[self.backupImportJob cancel];
|
||||
self.backupImportJob = nil;
|
||||
|
||||
self.backupImportState = OWSBackupState_InProgress;
|
||||
|
||||
self.backupImportJob = [[OWSBackupImportJob alloc] initWithDelegate:self recipientId:recipientId];
|
||||
[self.backupImportJob start];
|
||||
|
||||
[self postDidChangeNotification];
|
||||
}
|
||||
|
||||
- (void)cancelImportBackup
|
||||
{
|
||||
[self.backupImportJob cancel];
|
||||
self.backupImportJob = nil;
|
||||
|
||||
self.backupImportState = OWSBackupState_Idle;
|
||||
|
||||
[self postDidChangeNotification];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (void)applicationDidBecomeActive:(NSNotification *)notification
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self ensureBackupExportState];
|
||||
}
|
||||
|
||||
- (void)registrationStateDidChange
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self ensureBackupExportState];
|
||||
|
||||
[self postDidChangeNotification];
|
||||
}
|
||||
|
||||
- (void)ckAccountChanged
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self ensureBackupExportState];
|
||||
|
||||
[self postDidChangeNotification];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - OWSBackupJobDelegate
|
||||
|
||||
// We use a delegate method to avoid storing this key in memory.
|
||||
- (nullable NSData *)backupEncryptionKey
|
||||
{
|
||||
// TODO: Use actual encryption key.
|
||||
return [@"temp" dataUsingEncoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
- (void)backupJobDidSucceed:(OWSBackupJob *)backupJob
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSLogInfo(@".");
|
||||
|
||||
if (self.backupImportJob == backupJob) {
|
||||
self.backupImportJob = nil;
|
||||
|
||||
self.backupImportState = OWSBackupState_Succeeded;
|
||||
} else if (self.backupExportJob == backupJob) {
|
||||
self.backupExportJob = nil;
|
||||
|
||||
[self setLastExportSuccessDate:[NSDate new]];
|
||||
|
||||
[self ensureBackupExportState];
|
||||
} else {
|
||||
OWSLogWarn(@"obsolete job succeeded: %@", [backupJob class]);
|
||||
return;
|
||||
}
|
||||
|
||||
[self postDidChangeNotification];
|
||||
}
|
||||
|
||||
- (void)backupJobDidFail:(OWSBackupJob *)backupJob error:(NSError *)error
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSLogInfo(@": %@", error);
|
||||
|
||||
if (self.backupImportJob == backupJob) {
|
||||
self.backupImportJob = nil;
|
||||
|
||||
self.backupImportState = OWSBackupState_Failed;
|
||||
} else if (self.backupExportJob == backupJob) {
|
||||
self.backupExportJob = nil;
|
||||
|
||||
[self setLastExportFailureDate:[NSDate new]];
|
||||
|
||||
[self ensureBackupExportState];
|
||||
} else {
|
||||
OWSLogInfo(@"obsolete backup job failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
[self postDidChangeNotification];
|
||||
}
|
||||
|
||||
- (void)backupJobDidUpdate:(OWSBackupJob *)backupJob
|
||||
description:(nullable NSString *)description
|
||||
progress:(nullable NSNumber *)progress
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSLogInfo(@"");
|
||||
|
||||
// TODO: Should we consolidate this state?
|
||||
BOOL didChange;
|
||||
if (self.backupImportJob == backupJob) {
|
||||
didChange = !([NSObject isNullableObject:self.backupImportDescription equalTo:description] &&
|
||||
[NSObject isNullableObject:self.backupImportProgress equalTo:progress]);
|
||||
|
||||
self.backupImportDescription = description;
|
||||
self.backupImportProgress = progress;
|
||||
} else if (self.backupExportJob == backupJob) {
|
||||
didChange = !([NSObject isNullableObject:self.backupExportDescription equalTo:description] &&
|
||||
[NSObject isNullableObject:self.backupExportProgress equalTo:progress]);
|
||||
|
||||
self.backupExportDescription = description;
|
||||
self.backupExportProgress = progress;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (didChange) {
|
||||
[self postDidChangeNotification];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)logBackupRecords
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSLogInfo(@"");
|
||||
|
||||
if (!self.tsAccountManager.isRegisteredAndReady) {
|
||||
OWSLogError(@"Can't interact with backup; not registered and ready.");
|
||||
return;
|
||||
}
|
||||
NSString *_Nullable recipientId = self.tsAccountManager.localNumber;
|
||||
if (recipientId.length < 1) {
|
||||
OWSLogError(@"Can't interact with backup; missing recipientId.");
|
||||
return;
|
||||
}
|
||||
|
||||
[OWSBackupAPI fetchAllRecordNamesWithRecipientId:recipientId
|
||||
success:^(NSArray<NSString *> *recordNames) {
|
||||
for (NSString *recordName in [recordNames sortedArrayUsingSelector:@selector(compare:)]) {
|
||||
OWSLogInfo(@"\t %@", recordName);
|
||||
}
|
||||
OWSLogInfo(@"record count: %zd", recordNames.count);
|
||||
}
|
||||
failure:^(NSError *error) {
|
||||
OWSLogError(@"Failed to retrieve backup records: %@", error);
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)clearAllCloudKitRecords
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSLogInfo(@"");
|
||||
|
||||
if (!self.tsAccountManager.isRegisteredAndReady) {
|
||||
OWSLogError(@"Can't interact with backup; not registered and ready.");
|
||||
return;
|
||||
}
|
||||
NSString *_Nullable recipientId = self.tsAccountManager.localNumber;
|
||||
if (recipientId.length < 1) {
|
||||
OWSLogError(@"Can't interact with backup; missing recipientId.");
|
||||
return;
|
||||
}
|
||||
|
||||
[OWSBackupAPI fetchAllRecordNamesWithRecipientId:recipientId
|
||||
success:^(NSArray<NSString *> *recordNames) {
|
||||
if (recordNames.count < 1) {
|
||||
OWSLogInfo(@"No CloudKit records found to clear.");
|
||||
return;
|
||||
}
|
||||
[OWSBackupAPI deleteRecordsFromCloudWithRecordNames:recordNames
|
||||
success:^{
|
||||
OWSLogInfo(@"Clear all CloudKit records succeeded.");
|
||||
}
|
||||
failure:^(NSError *error) {
|
||||
OWSLogError(@"Clear all CloudKit records failed: %@.", error);
|
||||
}];
|
||||
}
|
||||
failure:^(NSError *error) {
|
||||
OWSLogError(@"Failed to retrieve CloudKit records: %@", error);
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Lazy Restore
|
||||
|
||||
- (NSArray<NSString *> *)attachmentRecordNamesForLazyRestore
|
||||
{
|
||||
NSMutableArray<NSString *> *recordNames = [NSMutableArray new];
|
||||
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
id ext = [transaction ext:TSLazyRestoreAttachmentsDatabaseViewExtensionName];
|
||||
if (!ext) {
|
||||
OWSFailDebug(@"Could not load database view.");
|
||||
return;
|
||||
}
|
||||
|
||||
[ext enumerateKeysAndObjectsInGroup:TSLazyRestoreAttachmentsGroup
|
||||
usingBlock:^(
|
||||
NSString *collection, NSString *key, id object, NSUInteger index, BOOL *stop) {
|
||||
if (![object isKindOfClass:[TSAttachmentPointer class]]) {
|
||||
OWSFailDebug(
|
||||
@"Unexpected object: %@ in collection:%@", [object class], collection);
|
||||
return;
|
||||
}
|
||||
TSAttachmentPointer *attachmentPointer = object;
|
||||
if (!attachmentPointer.lazyRestoreFragment) {
|
||||
OWSFailDebug(
|
||||
@"Invalid object: %@ in collection:%@", [object class], collection);
|
||||
return;
|
||||
}
|
||||
[recordNames addObject:attachmentPointer.lazyRestoreFragment.recordName];
|
||||
}];
|
||||
}];
|
||||
return recordNames;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)attachmentIdsForLazyRestore
|
||||
{
|
||||
NSMutableArray<NSString *> *attachmentIds = [NSMutableArray new];
|
||||
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
id ext = [transaction ext:TSLazyRestoreAttachmentsDatabaseViewExtensionName];
|
||||
if (!ext) {
|
||||
OWSFailDebug(@"Could not load database view.");
|
||||
return;
|
||||
}
|
||||
|
||||
[ext enumerateKeysInGroup:TSLazyRestoreAttachmentsGroup
|
||||
usingBlock:^(NSString *collection, NSString *key, NSUInteger index, BOOL *stop) {
|
||||
[attachmentIds addObject:key];
|
||||
}];
|
||||
}];
|
||||
return attachmentIds;
|
||||
}
|
||||
|
||||
- (AnyPromise *)lazyRestoreAttachment:(TSAttachmentPointer *)attachment backupIO:(OWSBackupIO *)backupIO
|
||||
{
|
||||
OWSAssertDebug(attachment);
|
||||
OWSAssertDebug(backupIO);
|
||||
|
||||
OWSBackupFragment *_Nullable lazyRestoreFragment = attachment.lazyRestoreFragment;
|
||||
if (!lazyRestoreFragment) {
|
||||
OWSLogError(@"Attachment missing lazy restore metadata.");
|
||||
return
|
||||
[AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Attachment missing lazy restore metadata.")];
|
||||
}
|
||||
if (lazyRestoreFragment.recordName.length < 1 || lazyRestoreFragment.encryptionKey.length < 1) {
|
||||
OWSLogError(@"Incomplete lazy restore metadata.");
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Incomplete lazy restore metadata.")];
|
||||
}
|
||||
|
||||
// Use a predictable file path so that multiple "import backup" attempts
|
||||
// will leverage successful file downloads from previous attempts.
|
||||
//
|
||||
// TODO: This will also require imports using a predictable jobTempDirPath.
|
||||
NSString *tempFilePath = [backupIO generateTempFilePath];
|
||||
|
||||
return [OWSBackupAPI downloadFileFromCloudObjcWithRecordName:lazyRestoreFragment.recordName
|
||||
toFileUrl:[NSURL fileURLWithPath:tempFilePath]]
|
||||
.thenInBackground(^{
|
||||
return [self lazyRestoreAttachment:attachment
|
||||
backupIO:backupIO
|
||||
encryptedFilePath:tempFilePath
|
||||
encryptionKey:lazyRestoreFragment.encryptionKey];
|
||||
});
|
||||
}
|
||||
|
||||
- (AnyPromise *)lazyRestoreAttachment:(TSAttachmentPointer *)attachmentPointer
|
||||
backupIO:(OWSBackupIO *)backupIO
|
||||
encryptedFilePath:(NSString *)encryptedFilePath
|
||||
encryptionKey:(NSData *)encryptionKey
|
||||
{
|
||||
OWSAssertDebug(attachmentPointer);
|
||||
OWSAssertDebug(backupIO);
|
||||
OWSAssertDebug(encryptedFilePath.length > 0);
|
||||
OWSAssertDebug(encryptionKey.length > 0);
|
||||
|
||||
NSData *_Nullable data = [NSData dataWithContentsOfFile:encryptedFilePath];
|
||||
if (!data) {
|
||||
OWSLogError(@"Could not load encrypted file.");
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not load encrypted file.")];
|
||||
}
|
||||
|
||||
NSString *decryptedFilePath = [backupIO generateTempFilePath];
|
||||
|
||||
@autoreleasepool {
|
||||
if (![backupIO decryptFileAsFile:encryptedFilePath dstFilePath:decryptedFilePath encryptionKey:encryptionKey]) {
|
||||
OWSLogError(@"Could not load decrypt file.");
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not load decrypt file.")];
|
||||
}
|
||||
}
|
||||
|
||||
TSAttachmentStream *stream = [[TSAttachmentStream alloc] initWithPointer:attachmentPointer];
|
||||
|
||||
NSString *attachmentFilePath = stream.originalFilePath;
|
||||
if (attachmentFilePath.length < 1) {
|
||||
OWSLogError(@"Attachment has invalid file path.");
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Attachment has invalid file path.")];
|
||||
}
|
||||
|
||||
NSString *attachmentDirPath = [attachmentFilePath stringByDeletingLastPathComponent];
|
||||
if (![OWSFileSystem ensureDirectoryExists:attachmentDirPath]) {
|
||||
OWSLogError(@"Couldn't create directory for attachment file.");
|
||||
return [AnyPromise
|
||||
promiseWithValue:OWSBackupErrorWithDescription(@"Couldn't create directory for attachment file.")];
|
||||
}
|
||||
|
||||
if (![OWSFileSystem deleteFileIfExists:attachmentFilePath]) {
|
||||
OWSFailDebug(@"Couldn't delete existing file at attachment path.");
|
||||
return [AnyPromise
|
||||
promiseWithValue:OWSBackupErrorWithDescription(@"Couldn't delete existing file at attachment path.")];
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
BOOL success =
|
||||
[NSFileManager.defaultManager moveItemAtPath:decryptedFilePath toPath:attachmentFilePath error:&error];
|
||||
if (!success || error) {
|
||||
OWSLogError(@"Attachment file could not be restored: %@.", error);
|
||||
return [AnyPromise promiseWithValue:error];
|
||||
}
|
||||
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
// This should overwrite the attachment pointer with an attachment stream.
|
||||
[stream saveWithTransaction:transaction];
|
||||
}];
|
||||
|
||||
return [AnyPromise promiseWithValue:@(1)];
|
||||
}
|
||||
|
||||
- (void)logBackupMetadataCache:(YapDatabaseConnection *)dbConnection
|
||||
{
|
||||
OWSLogInfo(@"");
|
||||
|
||||
[dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
[transaction enumerateKeysAndObjectsInCollection:[OWSBackupFragment collection]
|
||||
usingBlock:^(NSString *key, OWSBackupFragment *fragment, BOOL *stop) {
|
||||
OWSLogVerbose(@"fragment: %@, %@, %lu, %@, %@, %@, %@",
|
||||
key,
|
||||
fragment.recordName,
|
||||
(unsigned long)fragment.encryptionKey.length,
|
||||
fragment.relativeFilePath,
|
||||
fragment.attachmentId,
|
||||
fragment.downloadFilePath,
|
||||
fragment.uncompressedDataLength);
|
||||
}];
|
||||
OWSLogVerbose(@"Number of fragments: %lu",
|
||||
(unsigned long)[transaction numberOfKeysInCollection:[OWSBackupFragment collection]]);
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Notifications
|
||||
|
||||
- (void)postDidChangeNotification
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[[NSNotificationCenter defaultCenter] postNotificationNameAsync:NSNotificationNameBackupStateDidChange
|
||||
object:nil
|
||||
userInfo:nil];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,740 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SignalUtilitiesKit
|
||||
import CloudKit
|
||||
import PromiseKit
|
||||
|
||||
// We don't worry about atomic writes. Each backup export
|
||||
// will diff against last successful backup.
|
||||
//
|
||||
// Note that all of our CloudKit records are immutable.
|
||||
// "Persistent" records are only uploaded once.
|
||||
// "Ephemeral" records are always uploaded to a new record name.
|
||||
@objc public class OWSBackupAPI: NSObject {
|
||||
|
||||
// If we change the record types, we need to ensure indices
|
||||
// are configured properly in the CloudKit dashboard.
|
||||
//
|
||||
// TODO: Change the record types when we ship to production.
|
||||
static let signalBackupRecordType = "signalBackup"
|
||||
static let manifestRecordNameSuffix = "manifest"
|
||||
static let payloadKey = "payload"
|
||||
static let maxRetries = 5
|
||||
|
||||
private class func database() -> CKDatabase {
|
||||
let myContainer = CKContainer.default()
|
||||
let privateDatabase = myContainer.privateCloudDatabase
|
||||
return privateDatabase
|
||||
}
|
||||
|
||||
private class func invalidServiceResponseError() -> Error {
|
||||
return OWSErrorWithCodeDescription(.backupFailure,
|
||||
NSLocalizedString("BACKUP_EXPORT_ERROR_INVALID_CLOUDKIT_RESPONSE",
|
||||
comment: "Error indicating that the app received an invalid response from CloudKit."))
|
||||
}
|
||||
|
||||
// MARK: - Upload
|
||||
|
||||
@objc
|
||||
public class func recordNameForTestFile(recipientId: String) -> String {
|
||||
return "\(recordNamePrefix(forRecipientId: recipientId))test-\(NSUUID().uuidString)"
|
||||
}
|
||||
|
||||
// "Ephemeral" files are specific to this backup export and will always need to
|
||||
// be saved. For example, a complete image of the database is exported each time.
|
||||
// We wouldn't want to overwrite previous images until the entire backup export is
|
||||
// complete.
|
||||
@objc
|
||||
public class func recordNameForEphemeralFile(recipientId: String,
|
||||
label: String) -> String {
|
||||
return "\(recordNamePrefix(forRecipientId: recipientId))ephemeral-\(label)-\(NSUUID().uuidString)"
|
||||
}
|
||||
|
||||
// "Persistent" files may be shared between backup export; they should only be saved
|
||||
// once. For example, attachment files should only be uploaded once. Subsequent
|
||||
// backups can reuse the same record.
|
||||
@objc
|
||||
public class func recordNameForPersistentFile(recipientId: String,
|
||||
fileId: String) -> String {
|
||||
return "\(recordNamePrefix(forRecipientId: recipientId))persistentFile-\(fileId)"
|
||||
}
|
||||
|
||||
// "Persistent" files may be shared between backup export; they should only be saved
|
||||
// once. For example, attachment files should only be uploaded once. Subsequent
|
||||
// backups can reuse the same record.
|
||||
@objc
|
||||
public class func recordNameForManifest(recipientId: String) -> String {
|
||||
return "\(recordNamePrefix(forRecipientId: recipientId))\(manifestRecordNameSuffix)"
|
||||
}
|
||||
|
||||
private class func isManifest(recordName: String) -> Bool {
|
||||
return recordName.hasSuffix(manifestRecordNameSuffix)
|
||||
}
|
||||
|
||||
private class func recordNamePrefix(forRecipientId recipientId: String) -> String {
|
||||
return "\(recipientId)-"
|
||||
}
|
||||
|
||||
private class func recipientId(forRecordName recordName: String) -> String? {
|
||||
let recipientIds = self.recipientIds(forRecordNames: [recordName])
|
||||
guard let recipientId = recipientIds.first else {
|
||||
return nil
|
||||
}
|
||||
return recipientId
|
||||
}
|
||||
|
||||
private static var recordNamePrefixRegex = {
|
||||
return try! NSRegularExpression(pattern: "^(\\+[0-9]+)\\-")
|
||||
}()
|
||||
|
||||
private class func recipientIds(forRecordNames recordNames: [String]) -> [String] {
|
||||
var recipientIds = [String]()
|
||||
for recordName in recordNames {
|
||||
let regex = recordNamePrefixRegex
|
||||
guard let match: NSTextCheckingResult = regex.firstMatch(in: recordName, options: [], range: NSRange(location: 0, length: recordName.utf16.count)) else {
|
||||
Logger.warn("no match: \(recordName)")
|
||||
continue
|
||||
}
|
||||
guard match.numberOfRanges > 0 else {
|
||||
// Match must include first group.
|
||||
Logger.warn("invalid match: \(recordName)")
|
||||
continue
|
||||
}
|
||||
let firstRange = match.range(at: 1)
|
||||
guard firstRange.location == 0,
|
||||
firstRange.length > 0 else {
|
||||
// Match must be at start of string and non-empty.
|
||||
Logger.warn("invalid match: \(recordName) \(firstRange)")
|
||||
continue
|
||||
}
|
||||
let recipientId = (recordName as NSString).substring(with: firstRange) as String
|
||||
recipientIds.append(recipientId)
|
||||
}
|
||||
return recipientIds
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func record(forFileUrl fileUrl: URL,
|
||||
recordName: String) -> CKRecord {
|
||||
let recordType = signalBackupRecordType
|
||||
let recordID = CKRecord.ID(recordName: recordName)
|
||||
let record = CKRecord(recordType: recordType, recordID: recordID)
|
||||
let asset = CKAsset(fileURL: fileUrl)
|
||||
record[payloadKey] = asset
|
||||
|
||||
return record
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func saveRecordsToCloudObjc(records: [CKRecord]) -> AnyPromise {
|
||||
return AnyPromise(saveRecordsToCloud(records: records))
|
||||
}
|
||||
|
||||
public class func saveRecordsToCloud(records: [CKRecord]) -> Promise<Void> {
|
||||
|
||||
// CloudKit's internal limit is 400, but I haven't found a constant for this.
|
||||
let kMaxBatchSize = 100
|
||||
return records.chunked(by: kMaxBatchSize).reduce(Promise.value(())) { (promise, batch) -> Promise<Void> in
|
||||
return promise.then(on: .global()) {
|
||||
saveRecordsToCloud(records: batch, remainingRetries: maxRetries)
|
||||
}.done {
|
||||
Logger.verbose("Saved batch: \(batch.count)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class func saveRecordsToCloud(records: [CKRecord],
|
||||
remainingRetries: Int) -> Promise<Void> {
|
||||
|
||||
let recordNames = records.map { (record) in
|
||||
return record.recordID.recordName
|
||||
}
|
||||
Logger.verbose("recordNames[\(recordNames.count)] \(recordNames[0..<10])...")
|
||||
|
||||
return Promise { resolver in
|
||||
let saveOperation = CKModifyRecordsOperation(recordsToSave: records, recordIDsToDelete: nil)
|
||||
saveOperation.modifyRecordsCompletionBlock = { (savedRecords: [CKRecord]?, _, error) in
|
||||
|
||||
let retry = {
|
||||
// Only retry records which didn't already succeed.
|
||||
var savedRecordNames = [String]()
|
||||
if let savedRecords = savedRecords {
|
||||
savedRecordNames = savedRecords.map { (record) in
|
||||
return record.recordID.recordName
|
||||
}
|
||||
}
|
||||
let retryRecords = records.filter({ (record) in
|
||||
return !savedRecordNames.contains(record.recordID.recordName)
|
||||
})
|
||||
|
||||
saveRecordsToCloud(records: retryRecords,
|
||||
remainingRetries: remainingRetries - 1)
|
||||
.done { _ in
|
||||
resolver.fulfill(())
|
||||
}.catch { (error) in
|
||||
resolver.reject(error)
|
||||
}.retainUntilComplete()
|
||||
}
|
||||
|
||||
let outcome = outcomeForCloudKitError(error: error,
|
||||
remainingRetries: remainingRetries,
|
||||
label: "Save Records[\(recordNames.count)]")
|
||||
switch outcome {
|
||||
case .success:
|
||||
resolver.fulfill(())
|
||||
case .failureDoNotRetry(let outcomeError):
|
||||
resolver.reject(outcomeError)
|
||||
case .failureRetryAfterDelay(let retryDelay):
|
||||
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + retryDelay, execute: {
|
||||
retry()
|
||||
})
|
||||
case .failureRetryWithoutDelay:
|
||||
DispatchQueue.global().async {
|
||||
retry()
|
||||
}
|
||||
case .unknownItem:
|
||||
owsFailDebug("unexpected CloudKit response.")
|
||||
resolver.reject(invalidServiceResponseError())
|
||||
}
|
||||
}
|
||||
saveOperation.isAtomic = false
|
||||
saveOperation.savePolicy = .allKeys
|
||||
|
||||
// TODO: use perRecordProgressBlock and perRecordCompletionBlock.
|
||||
// open var perRecordProgressBlock: ((CKRecord, Double) -> Void)?
|
||||
// open var perRecordCompletionBlock: ((CKRecord, Error?) -> Void)?
|
||||
|
||||
// These APIs are only available in iOS 9.3 and later.
|
||||
if #available(iOS 9.3, *) {
|
||||
saveOperation.isLongLived = true
|
||||
saveOperation.qualityOfService = .background
|
||||
}
|
||||
|
||||
database().add(saveOperation)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Delete
|
||||
|
||||
@objc
|
||||
public class func deleteRecordsFromCloud(recordNames: [String],
|
||||
success: @escaping () -> Void,
|
||||
failure: @escaping (Error) -> Void) {
|
||||
deleteRecordsFromCloud(recordNames: recordNames,
|
||||
remainingRetries: maxRetries,
|
||||
success: success,
|
||||
failure: failure)
|
||||
}
|
||||
|
||||
private class func deleteRecordsFromCloud(recordNames: [String],
|
||||
remainingRetries: Int,
|
||||
success: @escaping () -> Void,
|
||||
failure: @escaping (Error) -> Void) {
|
||||
|
||||
let recordIDs = recordNames.map { CKRecord.ID(recordName: $0) }
|
||||
let deleteOperation = CKModifyRecordsOperation(recordsToSave: nil, recordIDsToDelete: recordIDs)
|
||||
deleteOperation.modifyRecordsCompletionBlock = { (records, recordIds, error) in
|
||||
|
||||
let outcome = outcomeForCloudKitError(error: error,
|
||||
remainingRetries: remainingRetries,
|
||||
label: "Delete Records")
|
||||
switch outcome {
|
||||
case .success:
|
||||
success()
|
||||
case .failureDoNotRetry(let outcomeError):
|
||||
failure(outcomeError)
|
||||
case .failureRetryAfterDelay(let retryDelay):
|
||||
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + retryDelay, execute: {
|
||||
deleteRecordsFromCloud(recordNames: recordNames,
|
||||
remainingRetries: remainingRetries - 1,
|
||||
success: success,
|
||||
failure: failure)
|
||||
})
|
||||
case .failureRetryWithoutDelay:
|
||||
DispatchQueue.global().async {
|
||||
deleteRecordsFromCloud(recordNames: recordNames,
|
||||
remainingRetries: remainingRetries - 1,
|
||||
success: success,
|
||||
failure: failure)
|
||||
}
|
||||
case .unknownItem:
|
||||
owsFailDebug("unexpected CloudKit response.")
|
||||
failure(invalidServiceResponseError())
|
||||
}
|
||||
}
|
||||
database().add(deleteOperation)
|
||||
}
|
||||
|
||||
// MARK: - Exists?
|
||||
|
||||
private class func checkForFileInCloud(recordName: String,
|
||||
remainingRetries: Int) -> Promise<CKRecord?> {
|
||||
|
||||
Logger.verbose("checkForFileInCloud \(recordName)")
|
||||
|
||||
let (promise, resolver) = Promise<CKRecord?>.pending()
|
||||
|
||||
let recordId = CKRecord.ID(recordName: recordName)
|
||||
let fetchOperation = CKFetchRecordsOperation(recordIDs: [recordId ])
|
||||
// Don't download the file; we're just using the fetch to check whether or
|
||||
// not this record already exists.
|
||||
fetchOperation.desiredKeys = []
|
||||
fetchOperation.perRecordCompletionBlock = { (record, recordId, error) in
|
||||
|
||||
let outcome = outcomeForCloudKitError(error: error,
|
||||
remainingRetries: remainingRetries,
|
||||
label: "Check for Record")
|
||||
switch outcome {
|
||||
case .success:
|
||||
guard let record = record else {
|
||||
owsFailDebug("missing fetching record.")
|
||||
resolver.reject(invalidServiceResponseError())
|
||||
return
|
||||
}
|
||||
// Record found.
|
||||
resolver.fulfill(record)
|
||||
case .failureDoNotRetry(let outcomeError):
|
||||
resolver.reject(outcomeError)
|
||||
case .failureRetryAfterDelay(let retryDelay):
|
||||
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + retryDelay, execute: {
|
||||
checkForFileInCloud(recordName: recordName,
|
||||
remainingRetries: remainingRetries - 1)
|
||||
.done { (record) in
|
||||
resolver.fulfill(record)
|
||||
}.catch { (error) in
|
||||
resolver.reject(error)
|
||||
}.retainUntilComplete()
|
||||
})
|
||||
case .failureRetryWithoutDelay:
|
||||
DispatchQueue.global().async {
|
||||
checkForFileInCloud(recordName: recordName,
|
||||
remainingRetries: remainingRetries - 1)
|
||||
.done { (record) in
|
||||
resolver.fulfill(record)
|
||||
}.catch { (error) in
|
||||
resolver.reject(error)
|
||||
}.retainUntilComplete()
|
||||
}
|
||||
case .unknownItem:
|
||||
// Record not found.
|
||||
resolver.fulfill(nil)
|
||||
}
|
||||
}
|
||||
database().add(fetchOperation)
|
||||
return promise
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func checkForManifestInCloudObjc(recipientId: String) -> AnyPromise {
|
||||
return AnyPromise(checkForManifestInCloud(recipientId: recipientId))
|
||||
}
|
||||
|
||||
public class func checkForManifestInCloud(recipientId: String) -> Promise<Bool> {
|
||||
|
||||
let recordName = recordNameForManifest(recipientId: recipientId)
|
||||
return checkForFileInCloud(recordName: recordName,
|
||||
remainingRetries: maxRetries)
|
||||
.map { (record) in
|
||||
return record != nil
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func allRecipientIdsWithManifestsInCloud(success: @escaping ([String]) -> Void,
|
||||
failure: @escaping (Error) -> Void) {
|
||||
|
||||
let processResults = { (recordNames: [String]) in
|
||||
DispatchQueue.global().async {
|
||||
let manifestRecordNames = recordNames.filter({ (recordName) -> Bool in
|
||||
self.isManifest(recordName: recordName)
|
||||
})
|
||||
let recipientIds = self.recipientIds(forRecordNames: manifestRecordNames)
|
||||
success(recipientIds)
|
||||
}
|
||||
}
|
||||
|
||||
let query = CKQuery(recordType: signalBackupRecordType, predicate: NSPredicate(value: true))
|
||||
// Fetch the first page of results for this query.
|
||||
fetchAllRecordNamesStep(recipientId: nil,
|
||||
query: query,
|
||||
previousRecordNames: [String](),
|
||||
cursor: nil,
|
||||
remainingRetries: maxRetries,
|
||||
success: processResults,
|
||||
failure: failure)
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func fetchAllRecordNames(recipientId: String,
|
||||
success: @escaping ([String]) -> Void,
|
||||
failure: @escaping (Error) -> Void) {
|
||||
|
||||
let query = CKQuery(recordType: signalBackupRecordType, predicate: NSPredicate(value: true))
|
||||
// Fetch the first page of results for this query.
|
||||
fetchAllRecordNamesStep(recipientId: recipientId,
|
||||
query: query,
|
||||
previousRecordNames: [String](),
|
||||
cursor: nil,
|
||||
remainingRetries: maxRetries,
|
||||
success: success,
|
||||
failure: failure)
|
||||
}
|
||||
|
||||
private class func fetchAllRecordNamesStep(recipientId: String?,
|
||||
query: CKQuery,
|
||||
previousRecordNames: [String],
|
||||
cursor: CKQueryOperation.Cursor?,
|
||||
remainingRetries: Int,
|
||||
success: @escaping ([String]) -> Void,
|
||||
failure: @escaping (Error) -> Void) {
|
||||
|
||||
var allRecordNames = previousRecordNames
|
||||
|
||||
let queryOperation = CKQueryOperation(query: query)
|
||||
// If this isn't the first page of results for this query, resume
|
||||
// where we left off.
|
||||
queryOperation.cursor = cursor
|
||||
// Don't download the file; we're just using the query to get a list of record names.
|
||||
queryOperation.desiredKeys = []
|
||||
queryOperation.recordFetchedBlock = { (record) in
|
||||
assert(record.recordID.recordName.count > 0)
|
||||
|
||||
let recordName = record.recordID.recordName
|
||||
|
||||
if let recipientId = recipientId {
|
||||
let prefix = recordNamePrefix(forRecipientId: recipientId)
|
||||
guard recordName.hasPrefix(prefix) else {
|
||||
Logger.info("Ignoring record: \(recordName)")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
allRecordNames.append(recordName)
|
||||
}
|
||||
queryOperation.queryCompletionBlock = { (cursor, error) in
|
||||
|
||||
let outcome = outcomeForCloudKitError(error: error,
|
||||
remainingRetries: remainingRetries,
|
||||
label: "Fetch All Records")
|
||||
switch outcome {
|
||||
case .success:
|
||||
if let cursor = cursor {
|
||||
Logger.verbose("fetching more record names \(allRecordNames.count).")
|
||||
// There are more pages of results, continue fetching.
|
||||
fetchAllRecordNamesStep(recipientId: recipientId,
|
||||
query: query,
|
||||
previousRecordNames: allRecordNames,
|
||||
cursor: cursor,
|
||||
remainingRetries: maxRetries,
|
||||
success: success,
|
||||
failure: failure)
|
||||
return
|
||||
}
|
||||
Logger.info("fetched \(allRecordNames.count) record names.")
|
||||
success(allRecordNames)
|
||||
case .failureDoNotRetry(let outcomeError):
|
||||
failure(outcomeError)
|
||||
case .failureRetryAfterDelay(let retryDelay):
|
||||
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + retryDelay, execute: {
|
||||
fetchAllRecordNamesStep(recipientId: recipientId,
|
||||
query: query,
|
||||
previousRecordNames: allRecordNames,
|
||||
cursor: cursor,
|
||||
remainingRetries: remainingRetries - 1,
|
||||
success: success,
|
||||
failure: failure)
|
||||
})
|
||||
case .failureRetryWithoutDelay:
|
||||
DispatchQueue.global().async {
|
||||
fetchAllRecordNamesStep(recipientId: recipientId,
|
||||
query: query,
|
||||
previousRecordNames: allRecordNames,
|
||||
cursor: cursor,
|
||||
remainingRetries: remainingRetries - 1,
|
||||
success: success,
|
||||
failure: failure)
|
||||
}
|
||||
case .unknownItem:
|
||||
owsFailDebug("unexpected CloudKit response.")
|
||||
failure(invalidServiceResponseError())
|
||||
}
|
||||
}
|
||||
database().add(queryOperation)
|
||||
}
|
||||
|
||||
// MARK: - Download
|
||||
|
||||
@objc
|
||||
public class func downloadManifestFromCloudObjc(recipientId: String) -> AnyPromise {
|
||||
return AnyPromise(downloadManifestFromCloud(recipientId: recipientId))
|
||||
}
|
||||
|
||||
public class func downloadManifestFromCloud(recipientId: String) -> Promise<Data> {
|
||||
|
||||
let recordName = recordNameForManifest(recipientId: recipientId)
|
||||
return downloadDataFromCloud(recordName: recordName)
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func downloadDataFromCloudObjc(recordName: String) -> AnyPromise {
|
||||
return AnyPromise(downloadDataFromCloud(recordName: recordName))
|
||||
}
|
||||
|
||||
public class func downloadDataFromCloud(recordName: String) -> Promise<Data> {
|
||||
return downloadFromCloud(recordName: recordName,
|
||||
remainingRetries: maxRetries)
|
||||
.map { (asset) -> Data in
|
||||
guard let fileURL = asset.fileURL else {
|
||||
throw invalidServiceResponseError()
|
||||
}
|
||||
return try Data(contentsOf: fileURL)
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func downloadFileFromCloudObjc(recordName: String,
|
||||
toFileUrl: URL) -> AnyPromise {
|
||||
return AnyPromise(downloadFileFromCloud(recordName: recordName,
|
||||
toFileUrl: toFileUrl))
|
||||
}
|
||||
|
||||
public class func downloadFileFromCloud(recordName: String,
|
||||
toFileUrl: URL) -> Promise<Void> {
|
||||
|
||||
return downloadFromCloud(recordName: recordName,
|
||||
remainingRetries: maxRetries)
|
||||
.done { asset in
|
||||
guard let fileURL = asset.fileURL else {
|
||||
throw invalidServiceResponseError()
|
||||
}
|
||||
try FileManager.default.copyItem(at: fileURL, to: toFileUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// We return the CKAsset and not its fileUrl because
|
||||
// CloudKit offers no guarantees around how long it'll
|
||||
// keep around the underlying file. Presumably we can
|
||||
// defer cleanup by maintaining a strong reference to
|
||||
// the asset.
|
||||
private class func downloadFromCloud(recordName: String,
|
||||
remainingRetries: Int) -> Promise<CKAsset> {
|
||||
|
||||
Logger.verbose("downloadFromCloud \(recordName)")
|
||||
|
||||
let (promise, resolver) = Promise<CKAsset>.pending()
|
||||
|
||||
let recordId = CKRecord.ID(recordName: recordName)
|
||||
let fetchOperation = CKFetchRecordsOperation(recordIDs: [recordId ])
|
||||
// Download all keys for this record.
|
||||
fetchOperation.perRecordCompletionBlock = { (record, recordId, error) in
|
||||
|
||||
let outcome = outcomeForCloudKitError(error: error,
|
||||
remainingRetries: remainingRetries,
|
||||
label: "Download Record")
|
||||
switch outcome {
|
||||
case .success:
|
||||
guard let record = record else {
|
||||
Logger.error("missing fetching record.")
|
||||
resolver.reject(invalidServiceResponseError())
|
||||
return
|
||||
}
|
||||
guard let asset = record[payloadKey] as? CKAsset else {
|
||||
Logger.error("record missing payload.")
|
||||
resolver.reject(invalidServiceResponseError())
|
||||
return
|
||||
}
|
||||
resolver.fulfill(asset)
|
||||
case .failureDoNotRetry(let outcomeError):
|
||||
resolver.reject(outcomeError)
|
||||
case .failureRetryAfterDelay(let retryDelay):
|
||||
DispatchQueue.global().asyncAfter(deadline: DispatchTime.now() + retryDelay, execute: {
|
||||
downloadFromCloud(recordName: recordName,
|
||||
remainingRetries: remainingRetries - 1)
|
||||
.done { (asset) in
|
||||
resolver.fulfill(asset)
|
||||
}.catch { (error) in
|
||||
resolver.reject(error)
|
||||
}.retainUntilComplete()
|
||||
})
|
||||
case .failureRetryWithoutDelay:
|
||||
DispatchQueue.global().async {
|
||||
downloadFromCloud(recordName: recordName,
|
||||
remainingRetries: remainingRetries - 1)
|
||||
.done { (asset) in
|
||||
resolver.fulfill(asset)
|
||||
}.catch { (error) in
|
||||
resolver.reject(error)
|
||||
}.retainUntilComplete()
|
||||
}
|
||||
case .unknownItem:
|
||||
Logger.error("missing fetching record.")
|
||||
resolver.reject(invalidServiceResponseError())
|
||||
}
|
||||
}
|
||||
database().add(fetchOperation)
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
// MARK: - Access
|
||||
|
||||
@objc public enum BackupError: Int, Error {
|
||||
case couldNotDetermineAccountStatus
|
||||
case noAccount
|
||||
case restrictedAccountStatus
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func ensureCloudKitAccessObjc() -> AnyPromise {
|
||||
return AnyPromise(ensureCloudKitAccess())
|
||||
}
|
||||
|
||||
public class func ensureCloudKitAccess() -> Promise<Void> {
|
||||
let (promise, resolver) = Promise<Void>.pending()
|
||||
CKContainer.default().accountStatus { (accountStatus, error) in
|
||||
if let error = error {
|
||||
Logger.error("Unknown error: \(String(describing: error)).")
|
||||
resolver.reject(error)
|
||||
return
|
||||
}
|
||||
switch accountStatus {
|
||||
case .couldNotDetermine:
|
||||
Logger.error("could not determine CloudKit account status: \(String(describing: error)).")
|
||||
resolver.reject(BackupError.couldNotDetermineAccountStatus)
|
||||
case .noAccount:
|
||||
Logger.error("no CloudKit account.")
|
||||
resolver.reject(BackupError.noAccount)
|
||||
case .restricted:
|
||||
Logger.error("restricted CloudKit account.")
|
||||
resolver.reject(BackupError.restrictedAccountStatus)
|
||||
case .available:
|
||||
Logger.verbose("CloudKit access okay.")
|
||||
resolver.fulfill(())
|
||||
default: resolver.fulfill(())
|
||||
}
|
||||
}
|
||||
return promise
|
||||
}
|
||||
|
||||
@objc
|
||||
public class func errorMessage(forCloudKitAccessError error: Error) -> String {
|
||||
if let backupError = error as? BackupError {
|
||||
Logger.error("Backup error: \(String(describing: backupError)).")
|
||||
switch backupError {
|
||||
case .couldNotDetermineAccountStatus:
|
||||
return NSLocalizedString("CLOUDKIT_STATUS_COULD_NOT_DETERMINE", comment: "Error indicating that the app could not determine that user's iCloud account status")
|
||||
case .noAccount:
|
||||
return NSLocalizedString("CLOUDKIT_STATUS_NO_ACCOUNT", comment: "Error indicating that user does not have an iCloud account.")
|
||||
case .restrictedAccountStatus:
|
||||
return NSLocalizedString("CLOUDKIT_STATUS_RESTRICTED", comment: "Error indicating that the app was prevented from accessing the user's iCloud account.")
|
||||
}
|
||||
} else {
|
||||
Logger.error("Unknown error: \(String(describing: error)).")
|
||||
return NSLocalizedString("CLOUDKIT_STATUS_COULD_NOT_DETERMINE", comment: "Error indicating that the app could not determine that user's iCloud account status")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Retry
|
||||
|
||||
private enum APIOutcome {
|
||||
case success
|
||||
case failureDoNotRetry(error:Error)
|
||||
case failureRetryAfterDelay(retryDelay: TimeInterval)
|
||||
case failureRetryWithoutDelay
|
||||
// This only applies to fetches.
|
||||
case unknownItem
|
||||
}
|
||||
|
||||
private class func outcomeForCloudKitError(error: Error?,
|
||||
remainingRetries: Int,
|
||||
label: String) -> APIOutcome {
|
||||
if let error = error as? CKError {
|
||||
if error.code == CKError.unknownItem {
|
||||
// This is not always an error for our purposes.
|
||||
Logger.verbose("\(label) unknown item.")
|
||||
return .unknownItem
|
||||
}
|
||||
|
||||
Logger.error("\(label) failed: \(error)")
|
||||
|
||||
if remainingRetries < 1 {
|
||||
Logger.verbose("\(label) no more retries.")
|
||||
return .failureDoNotRetry(error:error)
|
||||
}
|
||||
|
||||
if #available(iOS 11, *) {
|
||||
if error.code == CKError.serverResponseLost {
|
||||
Logger.verbose("\(label) retry without delay.")
|
||||
return .failureRetryWithoutDelay
|
||||
}
|
||||
}
|
||||
|
||||
switch error {
|
||||
case CKError.requestRateLimited, CKError.serviceUnavailable, CKError.zoneBusy:
|
||||
let retryDelay = error.retryAfterSeconds ?? 3.0
|
||||
Logger.verbose("\(label) retry with delay: \(retryDelay).")
|
||||
return .failureRetryAfterDelay(retryDelay:retryDelay)
|
||||
case CKError.networkFailure:
|
||||
Logger.verbose("\(label) retry without delay.")
|
||||
return .failureRetryWithoutDelay
|
||||
default:
|
||||
Logger.verbose("\(label) unknown CKError.")
|
||||
return .failureDoNotRetry(error:error)
|
||||
}
|
||||
} else if let error = error {
|
||||
Logger.error("\(label) failed: \(error)")
|
||||
if remainingRetries < 1 {
|
||||
Logger.verbose("\(label) no more retries.")
|
||||
return .failureDoNotRetry(error:error)
|
||||
}
|
||||
Logger.verbose("\(label) unknown error.")
|
||||
return .failureDoNotRetry(error:error)
|
||||
} else {
|
||||
Logger.info("\(label) succeeded.")
|
||||
return .success
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
@objc
|
||||
public class func setup() {
|
||||
cancelAllLongLivedOperations()
|
||||
}
|
||||
|
||||
private class func cancelAllLongLivedOperations() {
|
||||
// These APIs are only available in iOS 9.3 and later.
|
||||
guard #available(iOS 9.3, *) else {
|
||||
return
|
||||
}
|
||||
|
||||
let container = CKContainer.default()
|
||||
container.fetchAllLongLivedOperationIDs { (operationIds, error) in
|
||||
if let error = error {
|
||||
Logger.error("Could not get all long lived operations: \(error)")
|
||||
return
|
||||
}
|
||||
guard let operationIds = operationIds else {
|
||||
Logger.error("No operation ids.")
|
||||
return
|
||||
}
|
||||
|
||||
for operationId in operationIds {
|
||||
container.fetchLongLivedOperation(withID: operationId, completionHandler: { (operation, error) in
|
||||
if let error = error {
|
||||
Logger.error("Could not get long lived operation [\(operationId)]: \(error)")
|
||||
return
|
||||
}
|
||||
guard let operation = operation else {
|
||||
Logger.error("No operation.")
|
||||
return
|
||||
}
|
||||
operation.cancel()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSBackupJob.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSBackupExportJob : OWSBackupJob
|
||||
|
||||
- (void)start;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
File diff suppressed because it is too large
Load diff
|
@ -1,61 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSBackupEncryptedItem : NSObject
|
||||
|
||||
@property (nonatomic) NSString *filePath;
|
||||
|
||||
@property (nonatomic) NSData *encryptionKey;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSBackupIO : NSObject
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
- (instancetype)initWithJobTempDirPath:(NSString *)jobTempDirPath;
|
||||
|
||||
- (NSString *)generateTempFilePath;
|
||||
|
||||
- (nullable NSString *)createTempFile;
|
||||
|
||||
#pragma mark - Encrypt
|
||||
|
||||
- (nullable OWSBackupEncryptedItem *)encryptFileAsTempFile:(NSString *)srcFilePath;
|
||||
|
||||
- (nullable OWSBackupEncryptedItem *)encryptFileAsTempFile:(NSString *)srcFilePath
|
||||
encryptionKey:(NSData *)encryptionKey;
|
||||
|
||||
- (nullable OWSBackupEncryptedItem *)encryptDataAsTempFile:(NSData *)srcData;
|
||||
|
||||
- (nullable OWSBackupEncryptedItem *)encryptDataAsTempFile:(NSData *)srcData encryptionKey:(NSData *)encryptionKey;
|
||||
|
||||
#pragma mark - Decrypt
|
||||
|
||||
- (BOOL)decryptFileAsFile:(NSString *)srcFilePath
|
||||
dstFilePath:(NSString *)dstFilePath
|
||||
encryptionKey:(NSData *)encryptionKey;
|
||||
|
||||
- (nullable NSData *)decryptFileAsData:(NSString *)srcFilePath encryptionKey:(NSData *)encryptionKey;
|
||||
|
||||
- (nullable NSData *)decryptDataAsData:(NSData *)srcData encryptionKey:(NSData *)encryptionKey;
|
||||
|
||||
#pragma mark - Compression
|
||||
|
||||
- (nullable NSData *)compressData:(NSData *)srcData;
|
||||
|
||||
// I'm using the (new in iOS 9) compressionlib. One of its weaknesses is that it
|
||||
// requires you to pre-allocate output buffers during compression and decompression.
|
||||
// During decompression this is particularly tricky since there's no way to safely
|
||||
// predict how large the output will be based on the input. So, we store the
|
||||
// uncompressed size for compressed backup items.
|
||||
- (nullable NSData *)decompressData:(NSData *)srcData uncompressedDataLength:(NSUInteger)uncompressedDataLength;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,273 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSBackupIO.h"
|
||||
#import <SignalCoreKit/Randomness.h>
|
||||
#import <SessionUtilitiesKit/OWSFileSystem.h>
|
||||
|
||||
@import Compression;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// TODO:
|
||||
static const NSUInteger kOWSBackupKeyLength = 32;
|
||||
|
||||
// LZMA algorithm significantly outperforms the other compressionlib options
|
||||
// for our database snapshots and is a widely adopted standard.
|
||||
static const compression_algorithm SignalCompressionAlgorithm = COMPRESSION_LZMA;
|
||||
|
||||
@implementation OWSBackupEncryptedItem
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSBackupIO ()
|
||||
|
||||
@property (nonatomic) NSString *jobTempDirPath;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSBackupIO
|
||||
|
||||
- (instancetype)initWithJobTempDirPath:(NSString *)jobTempDirPath
|
||||
{
|
||||
if (!(self = [super init])) {
|
||||
return self;
|
||||
}
|
||||
|
||||
self.jobTempDirPath = jobTempDirPath;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)generateTempFilePath
|
||||
{
|
||||
return [self.jobTempDirPath stringByAppendingPathComponent:[NSUUID UUID].UUIDString];
|
||||
}
|
||||
|
||||
- (nullable NSString *)createTempFile
|
||||
{
|
||||
NSString *filePath = [self generateTempFilePath];
|
||||
if (![OWSFileSystem ensureFileExists:filePath]) {
|
||||
OWSFailDebug(@"could not create temp file.");
|
||||
return nil;
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
|
||||
#pragma mark - Encrypt
|
||||
|
||||
- (nullable OWSBackupEncryptedItem *)encryptFileAsTempFile:(NSString *)srcFilePath
|
||||
{
|
||||
OWSAssertDebug(srcFilePath.length > 0);
|
||||
|
||||
NSData *encryptionKey = [Randomness generateRandomBytes:(int)kOWSBackupKeyLength];
|
||||
|
||||
return [self encryptFileAsTempFile:srcFilePath encryptionKey:encryptionKey];
|
||||
}
|
||||
|
||||
- (nullable OWSBackupEncryptedItem *)encryptFileAsTempFile:(NSString *)srcFilePath encryptionKey:(NSData *)encryptionKey
|
||||
{
|
||||
OWSAssertDebug(srcFilePath.length > 0);
|
||||
OWSAssertDebug(encryptionKey.length > 0);
|
||||
|
||||
@autoreleasepool {
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:srcFilePath]) {
|
||||
OWSFailDebug(@"Missing source file.");
|
||||
return nil;
|
||||
}
|
||||
|
||||
// TODO: Encrypt the file without loading it into memory.
|
||||
NSData *_Nullable srcData = [NSData dataWithContentsOfFile:srcFilePath];
|
||||
if (srcData.length < 1) {
|
||||
OWSFailDebug(@"could not load file into memory for encryption.");
|
||||
return nil;
|
||||
}
|
||||
return [self encryptDataAsTempFile:srcData encryptionKey:encryptionKey];
|
||||
}
|
||||
}
|
||||
|
||||
- (nullable OWSBackupEncryptedItem *)encryptDataAsTempFile:(NSData *)srcData
|
||||
{
|
||||
OWSAssertDebug(srcData);
|
||||
|
||||
NSData *encryptionKey = [Randomness generateRandomBytes:(int)kOWSBackupKeyLength];
|
||||
|
||||
return [self encryptDataAsTempFile:srcData encryptionKey:encryptionKey];
|
||||
}
|
||||
|
||||
- (nullable OWSBackupEncryptedItem *)encryptDataAsTempFile:(NSData *)unencryptedData
|
||||
encryptionKey:(NSData *)encryptionKey
|
||||
{
|
||||
OWSAssertDebug(unencryptedData);
|
||||
OWSAssertDebug(encryptionKey.length > 0);
|
||||
|
||||
@autoreleasepool {
|
||||
|
||||
// TODO: Encrypt the data using key;
|
||||
NSData *encryptedData = unencryptedData;
|
||||
|
||||
NSString *_Nullable dstFilePath = [self createTempFile];
|
||||
if (!dstFilePath) {
|
||||
return nil;
|
||||
}
|
||||
NSError *error;
|
||||
BOOL success = [encryptedData writeToFile:dstFilePath options:NSDataWritingAtomic error:&error];
|
||||
if (!success || error) {
|
||||
OWSFailDebug(@"error writing encrypted data: %@", error);
|
||||
return nil;
|
||||
}
|
||||
[OWSFileSystem protectFileOrFolderAtPath:dstFilePath];
|
||||
OWSBackupEncryptedItem *item = [OWSBackupEncryptedItem new];
|
||||
item.filePath = dstFilePath;
|
||||
item.encryptionKey = encryptionKey;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Decrypt
|
||||
|
||||
- (BOOL)decryptFileAsFile:(NSString *)srcFilePath
|
||||
dstFilePath:(NSString *)dstFilePath
|
||||
encryptionKey:(NSData *)encryptionKey
|
||||
{
|
||||
OWSAssertDebug(srcFilePath.length > 0);
|
||||
OWSAssertDebug(encryptionKey.length > 0);
|
||||
|
||||
@autoreleasepool {
|
||||
|
||||
// TODO: Decrypt the file without loading it into memory.
|
||||
NSData *data = [self decryptFileAsData:srcFilePath encryptionKey:encryptionKey];
|
||||
|
||||
if (data.length < 1) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
BOOL success = [data writeToFile:dstFilePath options:NSDataWritingAtomic error:&error];
|
||||
if (!success || error) {
|
||||
OWSFailDebug(@"error writing decrypted data: %@", error);
|
||||
return NO;
|
||||
}
|
||||
[OWSFileSystem protectFileOrFolderAtPath:dstFilePath];
|
||||
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (nullable NSData *)decryptFileAsData:(NSString *)srcFilePath encryptionKey:(NSData *)encryptionKey
|
||||
{
|
||||
OWSAssertDebug(srcFilePath.length > 0);
|
||||
OWSAssertDebug(encryptionKey.length > 0);
|
||||
|
||||
@autoreleasepool {
|
||||
|
||||
if (![NSFileManager.defaultManager fileExistsAtPath:srcFilePath]) {
|
||||
OWSLogError(@"missing downloaded file.");
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSData *_Nullable srcData = [NSData dataWithContentsOfFile:srcFilePath];
|
||||
if (srcData.length < 1) {
|
||||
OWSFailDebug(@"could not load file into memory for decryption.");
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSData *_Nullable dstData = [self decryptDataAsData:srcData encryptionKey:encryptionKey];
|
||||
return dstData;
|
||||
}
|
||||
}
|
||||
|
||||
- (nullable NSData *)decryptDataAsData:(NSData *)encryptedData encryptionKey:(NSData *)encryptionKey
|
||||
{
|
||||
OWSAssertDebug(encryptedData);
|
||||
OWSAssertDebug(encryptionKey.length > 0);
|
||||
|
||||
@autoreleasepool {
|
||||
|
||||
// TODO: Decrypt the data using key;
|
||||
NSData *unencryptedData = encryptedData;
|
||||
|
||||
return unencryptedData;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Compression
|
||||
|
||||
- (nullable NSData *)compressData:(NSData *)srcData
|
||||
{
|
||||
OWSAssertDebug(srcData);
|
||||
|
||||
@autoreleasepool {
|
||||
|
||||
if (!srcData) {
|
||||
OWSFailDebug(@"missing unencrypted data.");
|
||||
return nil;
|
||||
}
|
||||
|
||||
size_t srcLength = [srcData length];
|
||||
|
||||
// This assumes that dst will always be smaller than src.
|
||||
//
|
||||
// We slightly pad the buffer size to account for the worst case.
|
||||
size_t dstBufferLength = srcLength + 64 * 1024;
|
||||
NSMutableData *dstBufferData = [NSMutableData dataWithLength:dstBufferLength];
|
||||
if (!dstBufferData) {
|
||||
OWSFailDebug(@"Failed to allocate buffer.");
|
||||
return nil;
|
||||
}
|
||||
|
||||
size_t dstLength = compression_encode_buffer(
|
||||
dstBufferData.mutableBytes, dstBufferLength, srcData.bytes, srcLength, NULL, SignalCompressionAlgorithm);
|
||||
NSData *compressedData = [dstBufferData subdataWithRange:NSMakeRange(0, dstLength)];
|
||||
|
||||
OWSLogVerbose(@"compressed %zd -> %zd = %0.2f",
|
||||
srcLength,
|
||||
dstLength,
|
||||
(srcLength > 0 ? (dstLength / (CGFloat)srcLength) : 0));
|
||||
|
||||
return compressedData;
|
||||
}
|
||||
}
|
||||
|
||||
- (nullable NSData *)decompressData:(NSData *)srcData uncompressedDataLength:(NSUInteger)uncompressedDataLength
|
||||
{
|
||||
OWSAssertDebug(srcData);
|
||||
|
||||
@autoreleasepool {
|
||||
|
||||
if (!srcData) {
|
||||
OWSFailDebug(@"missing unencrypted data.");
|
||||
return nil;
|
||||
}
|
||||
|
||||
size_t srcLength = [srcData length];
|
||||
|
||||
// We pad the buffer to be defensive.
|
||||
size_t dstBufferLength = uncompressedDataLength + 1024;
|
||||
NSMutableData *dstBufferData = [NSMutableData dataWithLength:dstBufferLength];
|
||||
if (!dstBufferData) {
|
||||
OWSFailDebug(@"Failed to allocate buffer.");
|
||||
return nil;
|
||||
}
|
||||
|
||||
size_t dstLength = compression_decode_buffer(
|
||||
dstBufferData.mutableBytes, dstBufferLength, srcData.bytes, srcLength, NULL, SignalCompressionAlgorithm);
|
||||
NSData *decompressedData = [dstBufferData subdataWithRange:NSMakeRange(0, dstLength)];
|
||||
OWSAssertDebug(decompressedData.length == uncompressedDataLength);
|
||||
OWSLogVerbose(@"decompressed %zd -> %zd = %0.2f",
|
||||
srcLength,
|
||||
dstLength,
|
||||
(dstLength > 0 ? (srcLength / (CGFloat)dstLength) : 0));
|
||||
|
||||
return decompressedData;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,15 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSBackupJob.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSBackupImportJob : OWSBackupJob
|
||||
|
||||
- (void)start;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,635 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSBackupImportJob.h"
|
||||
#import "OWSBackupIO.h"
|
||||
#import "OWSDatabaseMigration.h"
|
||||
#import "OWSDatabaseMigrationRunner.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <PromiseKit/AnyPromise.h>
|
||||
#import <SignalCoreKit/NSData+OWS.h>
|
||||
#import <SessionMessagingKit/OWSBackgroundTask.h>
|
||||
#import <SessionUtilitiesKit/OWSFileSystem.h>
|
||||
#import <SessionMessagingKit/TSAttachment.h>
|
||||
#import <SessionMessagingKit/TSMessage.h>
|
||||
#import <SessionMessagingKit/TSThread.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
NSString *const kOWSBackup_ImportDatabaseKeySpec = @"kOWSBackup_ImportDatabaseKeySpec";
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSBackupImportJob ()
|
||||
|
||||
@property (nonatomic, nullable) OWSBackgroundTask *backgroundTask;
|
||||
|
||||
@property (nonatomic) OWSBackupIO *backupIO;
|
||||
|
||||
@property (nonatomic) OWSBackupManifestContents *manifest;
|
||||
|
||||
@property (nonatomic, nullable) YapDatabaseConnection *dbConnection;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSBackupImportJob
|
||||
|
||||
#pragma mark - Dependencies
|
||||
|
||||
- (OWSPrimaryStorage *)primaryStorage
|
||||
{
|
||||
OWSAssertDebug(SSKEnvironment.shared.primaryStorage);
|
||||
|
||||
return SSKEnvironment.shared.primaryStorage;
|
||||
}
|
||||
|
||||
- (OWSProfileManager *)profileManager
|
||||
{
|
||||
return [OWSProfileManager sharedManager];
|
||||
}
|
||||
|
||||
- (TSAccountManager *)tsAccountManager
|
||||
{
|
||||
OWSAssertDebug(SSKEnvironment.shared.tsAccountManager);
|
||||
|
||||
return SSKEnvironment.shared.tsAccountManager;
|
||||
}
|
||||
|
||||
- (OWSBackup *)backup
|
||||
{
|
||||
OWSAssertDebug(AppEnvironment.shared.backup);
|
||||
|
||||
return AppEnvironment.shared.backup;
|
||||
}
|
||||
|
||||
- (OWSBackupLazyRestore *)backupLazyRestore
|
||||
{
|
||||
return AppEnvironment.shared.backupLazyRestore;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (NSArray<OWSBackupFragment *> *)databaseItems
|
||||
{
|
||||
OWSAssertDebug(self.manifest);
|
||||
|
||||
return self.manifest.databaseItems;
|
||||
}
|
||||
|
||||
- (NSArray<OWSBackupFragment *> *)attachmentsItems
|
||||
{
|
||||
OWSAssertDebug(self.manifest);
|
||||
|
||||
return self.manifest.attachmentsItems;
|
||||
}
|
||||
|
||||
- (void)start
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSLogInfo(@"");
|
||||
|
||||
self.backgroundTask = [OWSBackgroundTask backgroundTaskWithLabelStr:__PRETTY_FUNCTION__];
|
||||
|
||||
self.dbConnection = self.primaryStorage.newDatabaseConnection;
|
||||
|
||||
[self updateProgressWithDescription:nil progress:nil];
|
||||
|
||||
[[self.backup ensureCloudKitAccess]
|
||||
.thenInBackground(^{
|
||||
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_IMPORT_PHASE_CONFIGURATION",
|
||||
@"Indicates that the backup import is being configured.")
|
||||
progress:nil];
|
||||
|
||||
return [self configureImport];
|
||||
})
|
||||
.thenInBackground(^{
|
||||
if (self.isComplete) {
|
||||
return
|
||||
[AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")];
|
||||
}
|
||||
|
||||
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_IMPORT_PHASE_IMPORT",
|
||||
@"Indicates that the backup import data is being imported.")
|
||||
progress:nil];
|
||||
|
||||
return [self downloadAndProcessManifestWithBackupIO:self.backupIO];
|
||||
})
|
||||
.thenInBackground(^(OWSBackupManifestContents *manifest) {
|
||||
OWSCAssertDebug(manifest.databaseItems.count > 0);
|
||||
OWSCAssertDebug(manifest.attachmentsItems);
|
||||
|
||||
self.manifest = manifest;
|
||||
|
||||
return [self downloadAndProcessImport];
|
||||
})
|
||||
.catch(^(NSError *error) {
|
||||
[self failWithErrorDescription:
|
||||
NSLocalizedString(@"BACKUP_IMPORT_ERROR_COULD_NOT_IMPORT",
|
||||
@"Error indicating the backup import could not import the user's data.")];
|
||||
}) retainUntilComplete];
|
||||
}
|
||||
|
||||
- (AnyPromise *)downloadAndProcessImport
|
||||
{
|
||||
OWSAssertDebug(self.databaseItems);
|
||||
OWSAssertDebug(self.attachmentsItems);
|
||||
|
||||
// These items should be downloaded immediately.
|
||||
NSMutableArray<OWSBackupFragment *> *allItems = [NSMutableArray new];
|
||||
[allItems addObjectsFromArray:self.databaseItems];
|
||||
|
||||
// Make a copy of the blockingItems before we add
|
||||
// the optional items.
|
||||
NSArray<OWSBackupFragment *> *blockingItems = [allItems copy];
|
||||
|
||||
// Local profile avatars are optional in the sense that if their
|
||||
// download fails, we want to proceed with the import.
|
||||
if (self.manifest.localProfileAvatarItem) {
|
||||
[allItems addObject:self.manifest.localProfileAvatarItem];
|
||||
}
|
||||
|
||||
// Attachment items can be downloaded later;
|
||||
// they will can be lazy-restored.
|
||||
[allItems addObjectsFromArray:self.attachmentsItems];
|
||||
|
||||
// Record metadata for all items, so that we can re-use them in incremental backups after the restore.
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
for (OWSBackupFragment *item in allItems) {
|
||||
[item saveWithTransaction:transaction];
|
||||
}
|
||||
}];
|
||||
|
||||
return [self downloadFilesFromCloud:blockingItems]
|
||||
.thenInBackground(^{
|
||||
return [self restoreDatabase];
|
||||
})
|
||||
.thenInBackground(^{
|
||||
return [self ensureMigrations];
|
||||
})
|
||||
.thenInBackground(^{
|
||||
return [self restoreLocalProfile];
|
||||
})
|
||||
.thenInBackground(^{
|
||||
return [self restoreAttachmentFiles];
|
||||
})
|
||||
.then(^{
|
||||
// Kick off lazy restore on main thread.
|
||||
[self.backupLazyRestore clearCompleteAndRunIfNecessary];
|
||||
|
||||
// Make sure backup is enabled once we complete
|
||||
// a backup restore.
|
||||
[OWSBackup.sharedManager setIsBackupEnabled:YES];
|
||||
})
|
||||
.thenInBackground(^{
|
||||
return [self.tsAccountManager updateAccountAttributes];
|
||||
})
|
||||
.thenInBackground(^{
|
||||
[self succeed];
|
||||
});
|
||||
}
|
||||
|
||||
- (AnyPromise *)configureImport
|
||||
{
|
||||
OWSLogVerbose(@"");
|
||||
|
||||
if (![self ensureJobTempDir]) {
|
||||
OWSFailDebug(@"Could not create jobTempDirPath.");
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not create jobTempDirPath.")];
|
||||
}
|
||||
|
||||
self.backupIO = [[OWSBackupIO alloc] initWithJobTempDirPath:self.jobTempDirPath];
|
||||
|
||||
return [AnyPromise promiseWithValue:@(1)];
|
||||
}
|
||||
|
||||
- (AnyPromise *)downloadFilesFromCloud:(NSArray<OWSBackupFragment *> *)items
|
||||
{
|
||||
OWSAssertDebug(items.count > 0);
|
||||
|
||||
OWSLogVerbose(@"");
|
||||
|
||||
NSUInteger recordCount = items.count;
|
||||
|
||||
if (self.isComplete) {
|
||||
// Job was aborted.
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")];
|
||||
}
|
||||
|
||||
if (items.count < 1) {
|
||||
// All downloads are complete; exit.
|
||||
return [AnyPromise promiseWithValue:@(1)];
|
||||
}
|
||||
|
||||
AnyPromise *promise = [AnyPromise promiseWithValue:@(1)];
|
||||
for (OWSBackupFragment *item in items) {
|
||||
promise = promise
|
||||
.thenInBackground(^{
|
||||
CGFloat progress
|
||||
= (recordCount > 0 ? ((recordCount - items.count) / (CGFloat)recordCount) : 0.f);
|
||||
[self updateProgressWithDescription:
|
||||
NSLocalizedString(@"BACKUP_IMPORT_PHASE_DOWNLOAD",
|
||||
@"Indicates that the backup import data is being downloaded.")
|
||||
progress:@(progress)];
|
||||
})
|
||||
.thenInBackground(^{
|
||||
return [self downloadFileFromCloud:item];
|
||||
});
|
||||
}
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
- (AnyPromise *)downloadFileFromCloud:(OWSBackupFragment *)item
|
||||
{
|
||||
OWSAssertDebug(item);
|
||||
|
||||
OWSLogVerbose(@"");
|
||||
|
||||
if (self.isComplete) {
|
||||
// Job was aborted.
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")];
|
||||
}
|
||||
|
||||
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
|
||||
// TODO: Use a predictable file path so that multiple "import backup" attempts
|
||||
// will leverage successful file downloads from previous attempts.
|
||||
//
|
||||
// TODO: This will also require imports using a predictable jobTempDirPath.
|
||||
NSString *tempFilePath = [self.jobTempDirPath stringByAppendingPathComponent:item.recordName];
|
||||
|
||||
// Skip redundant file download.
|
||||
if ([NSFileManager.defaultManager fileExistsAtPath:tempFilePath]) {
|
||||
[OWSFileSystem protectFileOrFolderAtPath:tempFilePath];
|
||||
|
||||
item.downloadFilePath = tempFilePath;
|
||||
|
||||
return resolve(@(1));
|
||||
}
|
||||
|
||||
[OWSBackupAPI downloadFileFromCloudObjcWithRecordName:item.recordName
|
||||
toFileUrl:[NSURL fileURLWithPath:tempFilePath]]
|
||||
.thenInBackground(^{
|
||||
[OWSFileSystem protectFileOrFolderAtPath:tempFilePath];
|
||||
item.downloadFilePath = tempFilePath;
|
||||
|
||||
resolve(@(1));
|
||||
})
|
||||
.catchInBackground(^(NSError *error) {
|
||||
resolve(error);
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
- (AnyPromise *)restoreLocalProfile
|
||||
{
|
||||
OWSLogVerbose(@"");
|
||||
|
||||
if (self.isComplete) {
|
||||
// Job was aborted.
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")];
|
||||
}
|
||||
|
||||
AnyPromise *promise = [AnyPromise promiseWithValue:@(1)];
|
||||
|
||||
if (self.manifest.localProfileAvatarItem) {
|
||||
promise = promise.thenInBackground(^{
|
||||
return
|
||||
[self downloadFileFromCloud:self.manifest.localProfileAvatarItem].catchInBackground(^(NSError *error) {
|
||||
OWSLogInfo(@"Ignoring error; profiles are optional: %@", error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
promise = promise.thenInBackground(^{
|
||||
return [self applyLocalProfile];
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
- (AnyPromise *)applyLocalProfile
|
||||
{
|
||||
OWSLogVerbose(@"");
|
||||
|
||||
if (self.isComplete) {
|
||||
// Job was aborted.
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")];
|
||||
}
|
||||
|
||||
NSString *_Nullable localProfileName = self.manifest.localProfileName;
|
||||
UIImage *_Nullable localProfileAvatar = [self tryToLoadLocalProfileAvatar];
|
||||
|
||||
OWSLogVerbose(@"local profile name: %@, avatar: %d", localProfileName, localProfileAvatar != nil);
|
||||
|
||||
if (localProfileName.length < 1 && !localProfileAvatar) {
|
||||
return [AnyPromise promiseWithValue:@(1)];
|
||||
}
|
||||
|
||||
return [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
|
||||
[self.profileManager updateLocalProfileName:localProfileName
|
||||
avatarImage:localProfileAvatar
|
||||
success:^{
|
||||
resolve(@(1));
|
||||
}
|
||||
failure:^(NSError *error) {
|
||||
// Ignore errors related to local profile.
|
||||
resolve(@(1));
|
||||
}
|
||||
requiresSync:YES];
|
||||
}];
|
||||
}
|
||||
|
||||
- (nullable UIImage *)tryToLoadLocalProfileAvatar
|
||||
{
|
||||
if (!self.manifest.localProfileAvatarItem) {
|
||||
return nil;
|
||||
}
|
||||
if (!self.manifest.localProfileAvatarItem.downloadFilePath) {
|
||||
// Download of the avatar failed.
|
||||
// We can safely ignore errors related to local profile.
|
||||
OWSLogError(@"local profile avatar was not downloaded.");
|
||||
return nil;
|
||||
}
|
||||
OWSBackupFragment *item = self.manifest.localProfileAvatarItem;
|
||||
if (item.recordName.length < 1) {
|
||||
OWSFailDebug(@"item missing record name.");
|
||||
return nil;
|
||||
}
|
||||
|
||||
@autoreleasepool {
|
||||
NSData *_Nullable data =
|
||||
[self.backupIO decryptFileAsData:item.downloadFilePath encryptionKey:item.encryptionKey];
|
||||
if (!data) {
|
||||
OWSLogError(@"could not decrypt local profile avatar.");
|
||||
// Ignore errors related to local profile.
|
||||
return nil;
|
||||
}
|
||||
// TODO: Verify that we're not compressing the profile avatar data.
|
||||
UIImage *_Nullable image = [UIImage imageWithData:data];
|
||||
if (!image) {
|
||||
OWSLogError(@"could not decrypt local profile avatar.");
|
||||
// Ignore errors related to local profile.
|
||||
return nil;
|
||||
}
|
||||
return image;
|
||||
}
|
||||
}
|
||||
|
||||
- (AnyPromise *)restoreAttachmentFiles
|
||||
{
|
||||
OWSLogVerbose(@": %zd", self.attachmentsItems.count);
|
||||
|
||||
if (self.isComplete) {
|
||||
// Job was aborted.
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")];
|
||||
}
|
||||
|
||||
__block NSUInteger count = 0;
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
for (OWSBackupFragment *item in self.attachmentsItems) {
|
||||
if (self.isComplete) {
|
||||
return;
|
||||
}
|
||||
if (item.recordName.length < 1) {
|
||||
OWSLogError(@"attachment was not downloaded.");
|
||||
// Attachment-related errors are recoverable and can be ignored.
|
||||
continue;
|
||||
}
|
||||
if (item.attachmentId.length < 1) {
|
||||
OWSLogError(@"attachment missing attachment id.");
|
||||
// Attachment-related errors are recoverable and can be ignored.
|
||||
continue;
|
||||
}
|
||||
if (item.relativeFilePath.length < 1) {
|
||||
OWSLogError(@"attachment missing relative file path.");
|
||||
// Attachment-related errors are recoverable and can be ignored.
|
||||
continue;
|
||||
}
|
||||
TSAttachmentPointer *_Nullable attachment =
|
||||
[TSAttachmentPointer fetchObjectWithUniqueID:item.attachmentId transaction:transaction];
|
||||
if (!attachment) {
|
||||
OWSLogError(@"attachment to restore could not be found.");
|
||||
// Attachment-related errors are recoverable and can be ignored.
|
||||
continue;
|
||||
}
|
||||
if (![attachment isKindOfClass:[TSAttachmentPointer class]]) {
|
||||
OWSFailDebug(@"attachment has unexpected type: %@.", attachment.class);
|
||||
// Attachment-related errors are recoverable and can be ignored.
|
||||
continue;
|
||||
}
|
||||
[attachment markForLazyRestoreWithFragment:item transaction:transaction];
|
||||
count++;
|
||||
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_IMPORT_PHASE_RESTORING_FILES",
|
||||
@"Indicates that the backup import data is being restored.")
|
||||
progress:@(count / (CGFloat)self.attachmentsItems.count)];
|
||||
}
|
||||
}];
|
||||
|
||||
OWSLogError(@"enqueued lazy restore of %zd files.", count);
|
||||
|
||||
return [AnyPromise promiseWithValue:@(1)];
|
||||
}
|
||||
|
||||
- (AnyPromise *)restoreDatabase
|
||||
{
|
||||
OWSLogVerbose(@"");
|
||||
|
||||
if (self.isComplete) {
|
||||
// Job was aborted.
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")];
|
||||
}
|
||||
|
||||
// Order matters here.
|
||||
NSArray<NSString *> *collectionsToRestore = @[
|
||||
[TSThread collection],
|
||||
[TSAttachment collection],
|
||||
// Interactions refer to threads and attachments,
|
||||
// so copy them afterward.
|
||||
[TSInteraction collection],
|
||||
[OWSDatabaseMigration collection],
|
||||
];
|
||||
NSMutableDictionary<NSString *, NSNumber *> *restoredEntityCounts = [NSMutableDictionary new];
|
||||
__block unsigned long long copiedEntities = 0;
|
||||
__block BOOL aborted = NO;
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
for (NSString *collection in collectionsToRestore) {
|
||||
if ([collection isEqualToString:[OWSDatabaseMigration collection]]) {
|
||||
// It's okay if there are existing migrations; we'll clear those
|
||||
// before restoring.
|
||||
continue;
|
||||
}
|
||||
if ([transaction numberOfKeysInCollection:collection] > 0) {
|
||||
OWSLogError(@"unexpected contents in database (%@).", collection);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear existing database contents.
|
||||
//
|
||||
// This should be safe since we only ever import into an empty database.
|
||||
//
|
||||
// Note that if the app receives a message after registering and before restoring
|
||||
// backup, it will be lost.
|
||||
//
|
||||
// Note that this will clear all migrations.
|
||||
for (NSString *collection in collectionsToRestore) {
|
||||
[transaction removeAllObjectsInCollection:collection];
|
||||
}
|
||||
|
||||
NSUInteger count = 0;
|
||||
for (OWSBackupFragment *item in self.databaseItems) {
|
||||
if (self.isComplete) {
|
||||
return;
|
||||
}
|
||||
if (item.recordName.length < 1) {
|
||||
OWSLogError(@"database snapshot was not downloaded.");
|
||||
// Attachment-related errors are recoverable and can be ignored.
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return;
|
||||
}
|
||||
if (!item.uncompressedDataLength || item.uncompressedDataLength.unsignedIntValue < 1) {
|
||||
OWSLogError(@"database snapshot missing size.");
|
||||
// Attachment-related errors are recoverable and can be ignored.
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return;
|
||||
}
|
||||
|
||||
count++;
|
||||
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_IMPORT_PHASE_RESTORING_DATABASE",
|
||||
@"Indicates that the backup database is being restored.")
|
||||
progress:@(count / (CGFloat)self.databaseItems.count)];
|
||||
|
||||
@autoreleasepool {
|
||||
NSData *_Nullable compressedData =
|
||||
[self.backupIO decryptFileAsData:item.downloadFilePath encryptionKey:item.encryptionKey];
|
||||
if (!compressedData) {
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return;
|
||||
}
|
||||
NSData *_Nullable uncompressedData =
|
||||
[self.backupIO decompressData:compressedData
|
||||
uncompressedDataLength:item.uncompressedDataLength.unsignedIntValue];
|
||||
if (!uncompressedData) {
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return;
|
||||
}
|
||||
NSError *error;
|
||||
SignalIOSProtoBackupSnapshot *_Nullable entities =
|
||||
[SignalIOSProtoBackupSnapshot parseData:uncompressedData error:&error];
|
||||
if (!entities || error) {
|
||||
OWSLogError(@"could not parse proto: %@.", error);
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return;
|
||||
}
|
||||
if (!entities || entities.entity.count < 1) {
|
||||
OWSLogError(@"missing entities.");
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return;
|
||||
}
|
||||
for (SignalIOSProtoBackupSnapshotBackupEntity *entity in entities.entity) {
|
||||
NSData *_Nullable entityData = entity.entityData;
|
||||
if (entityData.length < 1) {
|
||||
OWSLogError(@"missing entity data.");
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *_Nullable collection = entity.collection;
|
||||
if (collection.length < 1) {
|
||||
OWSLogError(@"missing collection.");
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *_Nullable key = entity.key;
|
||||
if (key.length < 1) {
|
||||
OWSLogError(@"missing key.");
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return;
|
||||
}
|
||||
|
||||
__block NSObject *object = nil;
|
||||
@try {
|
||||
NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:entityData];
|
||||
object = [unarchiver decodeObjectForKey:@"root"];
|
||||
if (![object isKindOfClass:[object class]]) {
|
||||
OWSLogError(@"invalid decoded entity: %@.", [object class]);
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return;
|
||||
}
|
||||
} @catch (NSException *exception) {
|
||||
OWSLogError(@"could not decode entity.");
|
||||
// Database-related errors are unrecoverable.
|
||||
aborted = YES;
|
||||
return;
|
||||
}
|
||||
|
||||
[transaction setObject:object forKey:key inCollection:collection];
|
||||
copiedEntities++;
|
||||
NSUInteger restoredEntityCount = restoredEntityCounts[collection].unsignedIntValue;
|
||||
restoredEntityCounts[collection] = @(restoredEntityCount + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
if (aborted) {
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import failed.")];
|
||||
}
|
||||
if (self.isComplete) {
|
||||
// Job was aborted.
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")];
|
||||
}
|
||||
|
||||
for (NSString *collection in restoredEntityCounts) {
|
||||
OWSLogInfo(@"copied %@: %@", collection, restoredEntityCounts[collection]);
|
||||
}
|
||||
OWSLogInfo(@"copiedEntities: %llu", copiedEntities);
|
||||
|
||||
[self.primaryStorage logFileSizes];
|
||||
|
||||
return [AnyPromise promiseWithValue:@(1)];
|
||||
}
|
||||
|
||||
- (AnyPromise *)ensureMigrations
|
||||
{
|
||||
OWSLogVerbose(@"");
|
||||
|
||||
if (self.isComplete) {
|
||||
// Job was aborted.
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup import no longer active.")];
|
||||
}
|
||||
|
||||
[self updateProgressWithDescription:NSLocalizedString(@"BACKUP_IMPORT_PHASE_FINALIZING",
|
||||
@"Indicates that the backup import data is being finalized.")
|
||||
progress:nil];
|
||||
|
||||
|
||||
// It's okay that we do this in a separate transaction from the
|
||||
// restoration of backup contents. If some of migrations don't
|
||||
// complete, they'll be run the next time the app launches.
|
||||
AnyPromise *promise = [AnyPromise promiseWithResolverBlock:^(PMKResolver resolve) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[[[OWSDatabaseMigrationRunner alloc] init] runAllOutstandingWithCompletion:^(BOOL successful, BOOL needsConfigSync){
|
||||
resolve(@(1));
|
||||
}];
|
||||
});
|
||||
}];
|
||||
return promise;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,92 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "TSYapDatabaseObject.h"
|
||||
#import <SessionMessagingKit/OWSBackupFragment.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSString *const kOWSBackup_ManifestKey_DatabaseFiles;
|
||||
extern NSString *const kOWSBackup_ManifestKey_AttachmentFiles;
|
||||
extern NSString *const kOWSBackup_ManifestKey_RecordName;
|
||||
extern NSString *const kOWSBackup_ManifestKey_EncryptionKey;
|
||||
extern NSString *const kOWSBackup_ManifestKey_RelativeFilePath;
|
||||
extern NSString *const kOWSBackup_ManifestKey_AttachmentId;
|
||||
extern NSString *const kOWSBackup_ManifestKey_DataSize;
|
||||
extern NSString *const kOWSBackup_ManifestKey_LocalProfileAvatar;
|
||||
extern NSString *const kOWSBackup_ManifestKey_LocalProfileName;
|
||||
|
||||
@class AnyPromise;
|
||||
@class OWSBackupIO;
|
||||
@class OWSBackupJob;
|
||||
@class OWSBackupManifestContents;
|
||||
|
||||
typedef void (^OWSBackupJobBoolCompletion)(BOOL success);
|
||||
typedef void (^OWSBackupJobCompletion)(NSError *_Nullable error);
|
||||
typedef void (^OWSBackupJobManifestSuccess)(OWSBackupManifestContents *manifest);
|
||||
typedef void (^OWSBackupJobManifestFailure)(NSError *error);
|
||||
|
||||
@interface OWSBackupManifestContents : NSObject
|
||||
|
||||
@property (nonatomic) NSArray<OWSBackupFragment *> *databaseItems;
|
||||
@property (nonatomic) NSArray<OWSBackupFragment *> *attachmentsItems;
|
||||
@property (nonatomic, nullable) OWSBackupFragment *localProfileAvatarItem;
|
||||
@property (nonatomic, nullable) NSString *localProfileName;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@protocol OWSBackupJobDelegate <NSObject>
|
||||
|
||||
- (nullable NSData *)backupEncryptionKey;
|
||||
|
||||
// Either backupJobDidSucceed:... or backupJobDidFail:... will
|
||||
// be called exactly once on the main thread UNLESS:
|
||||
//
|
||||
// * The job was never started.
|
||||
// * The job was cancelled.
|
||||
- (void)backupJobDidSucceed:(OWSBackupJob *)backupJob;
|
||||
- (void)backupJobDidFail:(OWSBackupJob *)backupJob error:(NSError *)error;
|
||||
|
||||
- (void)backupJobDidUpdate:(OWSBackupJob *)backupJob
|
||||
description:(nullable NSString *)description
|
||||
progress:(nullable NSNumber *)progress;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSBackupJob : NSObject
|
||||
|
||||
@property (nonatomic, weak, readonly) id<OWSBackupJobDelegate> delegate;
|
||||
|
||||
@property (nonatomic, readonly) NSString *recipientId;
|
||||
|
||||
// Indicates that the backup succeeded, failed or was cancelled.
|
||||
@property (atomic, readonly) BOOL isComplete;
|
||||
|
||||
@property (nonatomic, readonly) NSString *jobTempDirPath;
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
- (instancetype)initWithDelegate:(id<OWSBackupJobDelegate>)delegate recipientId:(NSString *)recipientId;
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (BOOL)ensureJobTempDir;
|
||||
|
||||
- (void)cancel;
|
||||
- (void)succeed;
|
||||
- (void)failWithErrorDescription:(NSString *)description;
|
||||
- (void)failWithError:(NSError *)error;
|
||||
- (void)updateProgressWithDescription:(nullable NSString *)description progress:(nullable NSNumber *)progress;
|
||||
|
||||
#pragma mark - Manifest
|
||||
|
||||
- (AnyPromise *)downloadAndProcessManifestWithBackupIO:(OWSBackupIO *)backupIO __attribute__((warn_unused_result));
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,316 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSBackupJob.h"
|
||||
#import "OWSBackupIO.h"
|
||||
#import "Session-Swift.h"
|
||||
#import <PromiseKit/AnyPromise.h>
|
||||
#import <SignalCoreKit/Randomness.h>
|
||||
#import <YapDatabase/YapDatabaseCryptoUtils.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
NSString *const kOWSBackup_ManifestKey_DatabaseFiles = @"database_files";
|
||||
NSString *const kOWSBackup_ManifestKey_AttachmentFiles = @"attachment_files";
|
||||
NSString *const kOWSBackup_ManifestKey_RecordName = @"record_name";
|
||||
NSString *const kOWSBackup_ManifestKey_EncryptionKey = @"encryption_key";
|
||||
NSString *const kOWSBackup_ManifestKey_RelativeFilePath = @"relative_file_path";
|
||||
NSString *const kOWSBackup_ManifestKey_AttachmentId = @"attachment_id";
|
||||
NSString *const kOWSBackup_ManifestKey_DataSize = @"data_size";
|
||||
NSString *const kOWSBackup_ManifestKey_LocalProfileAvatar = @"local_profile_avatar";
|
||||
NSString *const kOWSBackup_ManifestKey_LocalProfileName = @"local_profile_name";
|
||||
|
||||
NSString *const kOWSBackup_KeychainService = @"kOWSBackup_KeychainService";
|
||||
|
||||
@implementation OWSBackupManifestContents
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@interface OWSBackupJob ()
|
||||
|
||||
@property (nonatomic, weak) id<OWSBackupJobDelegate> delegate;
|
||||
|
||||
@property (nonatomic) NSString *recipientId;
|
||||
|
||||
@property (atomic) BOOL isComplete;
|
||||
@property (atomic) BOOL hasSucceeded;
|
||||
|
||||
@property (nonatomic) NSString *jobTempDirPath;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSBackupJob
|
||||
|
||||
- (instancetype)initWithDelegate:(id<OWSBackupJobDelegate>)delegate recipientId:(NSString *)recipientId
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (!self) {
|
||||
return self;
|
||||
}
|
||||
|
||||
OWSAssertDebug(recipientId.length > 0);
|
||||
OWSAssertDebug([OWSStorage isStorageReady]);
|
||||
|
||||
self.delegate = delegate;
|
||||
self.recipientId = recipientId;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
// Surface memory leaks by logging the deallocation.
|
||||
OWSLogVerbose(@"Dealloc: %@", self.class);
|
||||
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
|
||||
if (self.jobTempDirPath) {
|
||||
[OWSFileSystem deleteFileIfExists:self.jobTempDirPath];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)ensureJobTempDir
|
||||
{
|
||||
OWSLogVerbose(@"");
|
||||
|
||||
// TODO: Exports should use a new directory each time, but imports
|
||||
// might want to use a predictable directory so that repeated
|
||||
// import attempts can reuse downloads from previous attempts.
|
||||
NSString *temporaryDirectory = OWSTemporaryDirectory();
|
||||
self.jobTempDirPath = [temporaryDirectory stringByAppendingPathComponent:[NSUUID UUID].UUIDString];
|
||||
|
||||
if (![OWSFileSystem ensureDirectoryExists:self.jobTempDirPath]) {
|
||||
OWSFailDebug(@"Could not create jobTempDirPath.");
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (void)cancel
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
self.isComplete = YES;
|
||||
}
|
||||
|
||||
- (void)succeed
|
||||
{
|
||||
OWSLogInfo(@"");
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self.isComplete) {
|
||||
OWSAssertDebug(!self.hasSucceeded);
|
||||
return;
|
||||
}
|
||||
self.isComplete = YES;
|
||||
|
||||
// There's a lot of asynchrony in these backup jobs;
|
||||
// ensure we only end up finishing these jobs once.
|
||||
OWSAssertDebug(!self.hasSucceeded);
|
||||
self.hasSucceeded = YES;
|
||||
|
||||
[self.delegate backupJobDidSucceed:self];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)failWithErrorDescription:(NSString *)description
|
||||
{
|
||||
[self failWithError:OWSErrorWithCodeDescription(OWSErrorCodeImportBackupFailed, description)];
|
||||
}
|
||||
|
||||
- (void)failWithError:(NSError *)error
|
||||
{
|
||||
OWSFailDebug(@"%@", error);
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
OWSAssertDebug(!self.hasSucceeded);
|
||||
if (self.isComplete) {
|
||||
return;
|
||||
}
|
||||
self.isComplete = YES;
|
||||
[self.delegate backupJobDidFail:self error:error];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)updateProgressWithDescription:(nullable NSString *)description progress:(nullable NSNumber *)progress
|
||||
{
|
||||
OWSLogInfo(@"");
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self.isComplete) {
|
||||
return;
|
||||
}
|
||||
[self.delegate backupJobDidUpdate:self description:description progress:progress];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Manifest
|
||||
|
||||
- (AnyPromise *)downloadAndProcessManifestWithBackupIO:(OWSBackupIO *)backupIO
|
||||
{
|
||||
OWSAssertDebug(backupIO);
|
||||
|
||||
OWSLogVerbose(@"");
|
||||
|
||||
if (self.isComplete) {
|
||||
// Job was aborted.
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup job no longer active.")];
|
||||
}
|
||||
|
||||
return
|
||||
[OWSBackupAPI downloadManifestFromCloudObjcWithRecipientId:self.recipientId].thenInBackground(^(NSData *data) {
|
||||
return [self processManifest:data backupIO:backupIO];
|
||||
});
|
||||
}
|
||||
|
||||
- (AnyPromise *)processManifest:(NSData *)manifestDataEncrypted backupIO:(OWSBackupIO *)backupIO
|
||||
{
|
||||
OWSAssertDebug(manifestDataEncrypted.length > 0);
|
||||
OWSAssertDebug(backupIO);
|
||||
|
||||
if (self.isComplete) {
|
||||
// Job was aborted.
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Backup job no longer active.")];
|
||||
}
|
||||
|
||||
OWSLogVerbose(@"");
|
||||
|
||||
NSData *_Nullable manifestDataDecrypted =
|
||||
[backupIO decryptDataAsData:manifestDataEncrypted encryptionKey:self.delegate.backupEncryptionKey];
|
||||
if (!manifestDataDecrypted) {
|
||||
OWSFailDebug(@"Could not decrypt manifest.");
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not decrypt manifest.")];
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
NSDictionary<NSString *, id> *_Nullable json =
|
||||
[NSJSONSerialization JSONObjectWithData:manifestDataDecrypted options:0 error:&error];
|
||||
if (![json isKindOfClass:[NSDictionary class]]) {
|
||||
OWSFailDebug(@"Could not download manifest.");
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"Could not download manifest.")];
|
||||
}
|
||||
|
||||
OWSLogVerbose(@"json: %@", json);
|
||||
|
||||
NSArray<OWSBackupFragment *> *_Nullable databaseItems =
|
||||
[self parseManifestItems:json key:kOWSBackup_ManifestKey_DatabaseFiles];
|
||||
if (!databaseItems) {
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"No database items in manifest.")];
|
||||
}
|
||||
NSArray<OWSBackupFragment *> *_Nullable attachmentsItems =
|
||||
[self parseManifestItems:json key:kOWSBackup_ManifestKey_AttachmentFiles];
|
||||
if (!attachmentsItems) {
|
||||
return [AnyPromise promiseWithValue:OWSBackupErrorWithDescription(@"No attachment items in manifest.")];
|
||||
}
|
||||
|
||||
NSArray<OWSBackupFragment *> *_Nullable localProfileAvatarItems;
|
||||
if ([self parseManifestItem:json key:kOWSBackup_ManifestKey_LocalProfileAvatar]) {
|
||||
localProfileAvatarItems = [self parseManifestItems:json key:kOWSBackup_ManifestKey_LocalProfileAvatar];
|
||||
}
|
||||
|
||||
NSString *_Nullable localProfileName = [self parseManifestItem:json key:kOWSBackup_ManifestKey_LocalProfileName];
|
||||
|
||||
OWSBackupManifestContents *contents = [OWSBackupManifestContents new];
|
||||
contents.databaseItems = databaseItems;
|
||||
contents.attachmentsItems = attachmentsItems;
|
||||
contents.localProfileAvatarItem = localProfileAvatarItems.firstObject;
|
||||
if ([localProfileName isKindOfClass:[NSString class]]) {
|
||||
contents.localProfileName = localProfileName;
|
||||
} else if (localProfileName) {
|
||||
OWSFailDebug(@"Invalid localProfileName: %@", [localProfileName class]);
|
||||
}
|
||||
|
||||
return [AnyPromise promiseWithValue:contents];
|
||||
}
|
||||
|
||||
- (nullable id)parseManifestItem:(id)json key:(NSString *)key
|
||||
{
|
||||
OWSAssertDebug(json);
|
||||
OWSAssertDebug(key.length);
|
||||
|
||||
if (![json isKindOfClass:[NSDictionary class]]) {
|
||||
OWSFailDebug(@"manifest has invalid data.");
|
||||
return nil;
|
||||
}
|
||||
id _Nullable value = json[key];
|
||||
return value;
|
||||
}
|
||||
|
||||
- (nullable NSArray<OWSBackupFragment *> *)parseManifestItems:(id)json key:(NSString *)key
|
||||
{
|
||||
OWSAssertDebug(json);
|
||||
OWSAssertDebug(key.length);
|
||||
|
||||
if (![json isKindOfClass:[NSDictionary class]]) {
|
||||
OWSFailDebug(@"manifest has invalid data.");
|
||||
return nil;
|
||||
}
|
||||
NSArray *itemMaps = json[key];
|
||||
if (![itemMaps isKindOfClass:[NSArray class]]) {
|
||||
OWSFailDebug(@"manifest has invalid data.");
|
||||
return nil;
|
||||
}
|
||||
NSMutableArray<OWSBackupFragment *> *items = [NSMutableArray new];
|
||||
for (NSDictionary *itemMap in itemMaps) {
|
||||
if (![itemMap isKindOfClass:[NSDictionary class]]) {
|
||||
OWSFailDebug(@"manifest has invalid item.");
|
||||
return nil;
|
||||
}
|
||||
NSString *_Nullable recordName = itemMap[kOWSBackup_ManifestKey_RecordName];
|
||||
NSString *_Nullable encryptionKeyString = itemMap[kOWSBackup_ManifestKey_EncryptionKey];
|
||||
NSString *_Nullable relativeFilePath = itemMap[kOWSBackup_ManifestKey_RelativeFilePath];
|
||||
NSString *_Nullable attachmentId = itemMap[kOWSBackup_ManifestKey_AttachmentId];
|
||||
NSNumber *_Nullable uncompressedDataLength = itemMap[kOWSBackup_ManifestKey_DataSize];
|
||||
if (![recordName isKindOfClass:[NSString class]]) {
|
||||
OWSFailDebug(@"manifest has invalid recordName: %@.", recordName);
|
||||
return nil;
|
||||
}
|
||||
if (![encryptionKeyString isKindOfClass:[NSString class]]) {
|
||||
OWSFailDebug(@"manifest has invalid encryptionKey.");
|
||||
return nil;
|
||||
}
|
||||
// relativeFilePath is an optional field.
|
||||
if (relativeFilePath && ![relativeFilePath isKindOfClass:[NSString class]]) {
|
||||
OWSLogDebug(@"manifest has invalid relativeFilePath: %@.", relativeFilePath);
|
||||
OWSFailDebug(@"manifest has invalid relativeFilePath");
|
||||
return nil;
|
||||
}
|
||||
// attachmentId is an optional field.
|
||||
if (attachmentId && ![attachmentId isKindOfClass:[NSString class]]) {
|
||||
OWSLogDebug(@"manifest has invalid attachmentId: %@.", attachmentId);
|
||||
OWSFailDebug(@"manifest has invalid attachmentId");
|
||||
return nil;
|
||||
}
|
||||
NSData *_Nullable encryptionKey = [NSData dataFromBase64String:encryptionKeyString];
|
||||
if (!encryptionKey) {
|
||||
OWSFailDebug(@"manifest has corrupt encryptionKey");
|
||||
return nil;
|
||||
}
|
||||
// uncompressedDataLength is an optional field.
|
||||
if (uncompressedDataLength && ![uncompressedDataLength isKindOfClass:[NSNumber class]]) {
|
||||
OWSFailDebug(@"manifest has invalid uncompressedDataLength: %@.", uncompressedDataLength);
|
||||
return nil;
|
||||
}
|
||||
|
||||
OWSBackupFragment *item = [[OWSBackupFragment alloc] initWithUniqueId:recordName];
|
||||
item.recordName = recordName;
|
||||
item.encryptionKey = encryptionKey;
|
||||
item.relativeFilePath = relativeFilePath;
|
||||
item.attachmentId = attachmentId;
|
||||
item.uncompressedDataLength = uncompressedDataLength;
|
||||
[items addObject:item];
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,176 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
import SignalUtilitiesKit
|
||||
|
||||
@objc(OWSBackupLazyRestore)
|
||||
public class BackupLazyRestore: NSObject {
|
||||
|
||||
// MARK: - Dependencies
|
||||
|
||||
private var backup: OWSBackup {
|
||||
return AppEnvironment.shared.backup
|
||||
}
|
||||
|
||||
private var primaryStorage: OWSPrimaryStorage {
|
||||
return SSKEnvironment.shared.primaryStorage
|
||||
}
|
||||
|
||||
private var tsAccountManager: TSAccountManager {
|
||||
return TSAccountManager.sharedInstance()
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private var isRunning = false
|
||||
private var isComplete = false
|
||||
|
||||
@objc
|
||||
public required override init() {
|
||||
super.init()
|
||||
|
||||
SwiftSingletons.register(self)
|
||||
|
||||
AppReadiness.runNowOrWhenAppDidBecomeReady {
|
||||
self.runIfNecessary()
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(forName: .OWSApplicationDidBecomeActive, object: nil, queue: nil) { _ in
|
||||
self.runIfNecessary()
|
||||
}
|
||||
NotificationCenter.default.addObserver(forName: .RegistrationStateDidChange, object: nil, queue: nil) { _ in
|
||||
self.runIfNecessary()
|
||||
}
|
||||
NotificationCenter.default.addObserver(forName: .reachabilityChanged, object: nil, queue: nil) { _ in
|
||||
self.runIfNecessary()
|
||||
}
|
||||
NotificationCenter.default.addObserver(forName: .reachabilityChanged, object: nil, queue: nil) { _ in
|
||||
self.runIfNecessary()
|
||||
}
|
||||
NotificationCenter.default.addObserver(forName: NSNotification.Name(NSNotificationNameBackupStateDidChange), object: nil, queue: nil) { _ in
|
||||
self.runIfNecessary()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private let backgroundQueue = DispatchQueue.global(qos: .background)
|
||||
|
||||
@objc
|
||||
public func clearCompleteAndRunIfNecessary() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
isComplete = false
|
||||
|
||||
runIfNecessary()
|
||||
}
|
||||
|
||||
@objc
|
||||
public func isBackupImportInProgress() -> Bool {
|
||||
return backup.backupImportState == .inProgress
|
||||
}
|
||||
|
||||
@objc
|
||||
public func runIfNecessary() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
guard !CurrentAppContext().isRunningTests else {
|
||||
return
|
||||
}
|
||||
guard AppReadiness.isAppReady() else {
|
||||
return
|
||||
}
|
||||
guard CurrentAppContext().isMainAppAndActive else {
|
||||
return
|
||||
}
|
||||
guard tsAccountManager.isRegisteredAndReady() else {
|
||||
return
|
||||
}
|
||||
guard !isBackupImportInProgress() else {
|
||||
return
|
||||
}
|
||||
guard !isRunning, !isComplete else {
|
||||
return
|
||||
}
|
||||
|
||||
isRunning = true
|
||||
|
||||
backgroundQueue.async {
|
||||
self.restoreAttachments()
|
||||
}
|
||||
}
|
||||
|
||||
private func restoreAttachments() {
|
||||
let temporaryDirectory = OWSTemporaryDirectory()
|
||||
let jobTempDirPath = (temporaryDirectory as NSString).appendingPathComponent(NSUUID().uuidString)
|
||||
|
||||
guard OWSFileSystem.ensureDirectoryExists(jobTempDirPath) else {
|
||||
Logger.error("could not create temp directory.")
|
||||
complete(errorCount: 1)
|
||||
return
|
||||
}
|
||||
|
||||
let backupIO = OWSBackupIO(jobTempDirPath: jobTempDirPath)
|
||||
|
||||
let attachmentIds = backup.attachmentIdsForLazyRestore()
|
||||
guard attachmentIds.count > 0 else {
|
||||
Logger.info("No attachments need lazy restore.")
|
||||
complete(errorCount: 0)
|
||||
return
|
||||
}
|
||||
Logger.info("Lazy restoring \(attachmentIds.count) attachments.")
|
||||
tryToRestoreNextAttachment(attachmentIds: attachmentIds, errorCount: 0, backupIO: backupIO)
|
||||
}
|
||||
|
||||
private func tryToRestoreNextAttachment(attachmentIds: [String], errorCount: UInt, backupIO: OWSBackupIO) {
|
||||
guard !isBackupImportInProgress() else {
|
||||
Logger.verbose("A backup import is in progress; abort.")
|
||||
complete(errorCount: errorCount + 1)
|
||||
return
|
||||
}
|
||||
|
||||
var attachmentIdsCopy = attachmentIds
|
||||
guard let attachmentId = attachmentIdsCopy.popLast() else {
|
||||
// This job is done.
|
||||
Logger.verbose("job is done.")
|
||||
complete(errorCount: errorCount)
|
||||
return
|
||||
}
|
||||
guard let attachmentPointer = TSAttachment.fetch(uniqueId: attachmentId) as? TSAttachmentPointer else {
|
||||
Logger.warn("could not load attachment.")
|
||||
// Not necessarily an error.
|
||||
// The attachment might have been deleted since the job began.
|
||||
// Continue trying to restore the other attachments.
|
||||
tryToRestoreNextAttachment(attachmentIds: attachmentIds, errorCount: errorCount + 1, backupIO: backupIO)
|
||||
return
|
||||
}
|
||||
backup.lazyRestoreAttachment(attachmentPointer,
|
||||
backupIO: backupIO)
|
||||
.done(on: self.backgroundQueue) { _ in
|
||||
Logger.info("Restored attachment.")
|
||||
|
||||
// Continue trying to restore the other attachments.
|
||||
self.tryToRestoreNextAttachment(attachmentIds: attachmentIdsCopy, errorCount: errorCount, backupIO: backupIO)
|
||||
}.catch(on: self.backgroundQueue) { (error) in
|
||||
Logger.error("Could not restore attachment: \(error)")
|
||||
|
||||
// Continue trying to restore the other attachments.
|
||||
self.tryToRestoreNextAttachment(attachmentIds: attachmentIdsCopy, errorCount: errorCount + 1, backupIO: backupIO)
|
||||
}.retainUntilComplete()
|
||||
}
|
||||
|
||||
private func complete(errorCount: UInt) {
|
||||
Logger.verbose("")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.isRunning = false
|
||||
|
||||
if errorCount == 0 {
|
||||
self.isComplete = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/OWSTableViewController.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSBackupSettingsViewController : OWSTableViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,214 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSBackupSettingsViewController.h"
|
||||
#import "OWSBackup.h"
|
||||
#import "Session-Swift.h"
|
||||
|
||||
#import <PromiseKit/AnyPromise.h>
|
||||
#import <SignalUtilitiesKit/AttachmentSharing.h>
|
||||
#import <SessionMessagingKit/Environment.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIFont+OWS.h>
|
||||
#import <SessionUtilitiesKit/UIView+OWS.h>
|
||||
#import <SessionUtilitiesKit/MIMETypeUtil.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSBackupSettingsViewController ()
|
||||
|
||||
@property (nonatomic, nullable) NSError *iCloudError;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSBackupSettingsViewController
|
||||
|
||||
#pragma mark - Dependencies
|
||||
|
||||
- (OWSBackup *)backup
|
||||
{
|
||||
OWSAssertDebug(AppEnvironment.shared.backup);
|
||||
|
||||
return AppEnvironment.shared.backup;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
self.title = NSLocalizedString(@"SETTINGS_BACKUP", @"Label for the backup view in app settings.");
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(backupStateDidChange:)
|
||||
name:NSNotificationNameBackupStateDidChange
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(applicationDidBecomeActive:)
|
||||
name:OWSApplicationDidBecomeActiveNotification
|
||||
object:nil];
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
[self updateTableContents];
|
||||
[self updateICloudStatus];
|
||||
}
|
||||
|
||||
- (void)updateICloudStatus
|
||||
{
|
||||
__weak OWSBackupSettingsViewController *weakSelf = self;
|
||||
[[self.backup ensureCloudKitAccess]
|
||||
.then(^{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
weakSelf.iCloudError = nil;
|
||||
[weakSelf updateTableContents];
|
||||
})
|
||||
.catch(^(NSError *error) {
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
weakSelf.iCloudError = error;
|
||||
[weakSelf updateTableContents];
|
||||
}) retainUntilComplete];
|
||||
}
|
||||
|
||||
#pragma mark - Table Contents
|
||||
|
||||
- (void)updateTableContents
|
||||
{
|
||||
OWSTableContents *contents = [OWSTableContents new];
|
||||
|
||||
BOOL isBackupEnabled = [OWSBackup.sharedManager isBackupEnabled];
|
||||
|
||||
if (self.iCloudError) {
|
||||
OWSTableSection *iCloudSection = [OWSTableSection new];
|
||||
iCloudSection.headerTitle = NSLocalizedString(
|
||||
@"SETTINGS_BACKUP_ICLOUD_STATUS", @"Label for iCloud status row in the in the backup settings view.");
|
||||
[iCloudSection
|
||||
addItem:[OWSTableItem
|
||||
longDisclosureItemWithText:[OWSBackupAPI errorMessageForCloudKitAccessError:self.iCloudError]
|
||||
actionBlock:^{
|
||||
[[UIApplication sharedApplication]
|
||||
openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]];
|
||||
}]];
|
||||
[contents addSection:iCloudSection];
|
||||
}
|
||||
|
||||
// TODO: This UI is temporary.
|
||||
// Enabling backup will involve entering and registering a PIN.
|
||||
OWSTableSection *enableSection = [OWSTableSection new];
|
||||
enableSection.headerTitle = NSLocalizedString(@"SETTINGS_BACKUP", @"Label for the backup view in app settings.");
|
||||
[enableSection
|
||||
addItem:[OWSTableItem switchItemWithText:
|
||||
NSLocalizedString(@"SETTINGS_BACKUP_ENABLING_SWITCH",
|
||||
@"Label for switch in settings that controls whether or not backup is enabled.")
|
||||
isOnBlock:^{
|
||||
return [OWSBackup.sharedManager isBackupEnabled];
|
||||
}
|
||||
target:self
|
||||
selector:@selector(isBackupEnabledDidChange:)]];
|
||||
[contents addSection:enableSection];
|
||||
|
||||
if (isBackupEnabled) {
|
||||
// TODO: This UI is temporary.
|
||||
// Enabling backup will involve entering and registering a PIN.
|
||||
OWSTableSection *progressSection = [OWSTableSection new];
|
||||
[progressSection
|
||||
addItem:[OWSTableItem
|
||||
labelItemWithText:NSLocalizedString(@"SETTINGS_BACKUP_STATUS",
|
||||
@"Label for backup status row in the in the backup settings view.")
|
||||
accessoryText:NSStringForBackupExportState(OWSBackup.sharedManager.backupExportState)]];
|
||||
if (OWSBackup.sharedManager.backupExportState == OWSBackupState_InProgress) {
|
||||
if (OWSBackup.sharedManager.backupExportDescription) {
|
||||
[progressSection
|
||||
addItem:[OWSTableItem
|
||||
labelItemWithText:NSLocalizedString(@"SETTINGS_BACKUP_PHASE",
|
||||
@"Label for phase row in the in the backup settings view.")
|
||||
accessoryText:OWSBackup.sharedManager.backupExportDescription]];
|
||||
if (OWSBackup.sharedManager.backupExportProgress) {
|
||||
NSUInteger progressPercent
|
||||
= (NSUInteger)round(OWSBackup.sharedManager.backupExportProgress.floatValue * 100);
|
||||
NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
|
||||
[numberFormatter setNumberStyle:NSNumberFormatterPercentStyle];
|
||||
[numberFormatter setMaximumFractionDigits:0];
|
||||
[numberFormatter setMultiplier:@1];
|
||||
NSString *progressString = [numberFormatter stringFromNumber:@(progressPercent)];
|
||||
[progressSection
|
||||
addItem:[OWSTableItem
|
||||
labelItemWithText:NSLocalizedString(@"SETTINGS_BACKUP_PROGRESS",
|
||||
@"Label for phase row in the in the backup settings view.")
|
||||
accessoryText:progressString]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (OWSBackup.sharedManager.backupExportState) {
|
||||
case OWSBackupState_Idle:
|
||||
case OWSBackupState_Failed:
|
||||
case OWSBackupState_Succeeded:
|
||||
[progressSection
|
||||
addItem:[OWSTableItem disclosureItemWithText:
|
||||
NSLocalizedString(@"SETTINGS_BACKUP_BACKUP_NOW",
|
||||
@"Label for 'backup now' button in the backup settings view.")
|
||||
actionBlock:^{
|
||||
[OWSBackup.sharedManager tryToExportBackup];
|
||||
}]];
|
||||
break;
|
||||
case OWSBackupState_InProgress:
|
||||
[progressSection
|
||||
addItem:[OWSTableItem disclosureItemWithText:
|
||||
NSLocalizedString(@"SETTINGS_BACKUP_CANCEL_BACKUP",
|
||||
@"Label for 'cancel backup' button in the backup settings view.")
|
||||
actionBlock:^{
|
||||
[OWSBackup.sharedManager cancelExportBackup];
|
||||
}]];
|
||||
break;
|
||||
}
|
||||
|
||||
[contents addSection:progressSection];
|
||||
}
|
||||
|
||||
self.contents = contents;
|
||||
}
|
||||
|
||||
- (void)isBackupEnabledDidChange:(UISwitch *)sender
|
||||
{
|
||||
[OWSBackup.sharedManager setIsBackupEnabled:sender.isOn];
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
#pragma mark - Events
|
||||
|
||||
- (void)backupStateDidChange:(NSNotification *)notification
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self updateTableContents];
|
||||
}
|
||||
|
||||
- (void)applicationDidBecomeActive:(NSNotification *)notification
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self updateICloudStatus];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -275,10 +275,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
selector:@selector(typingIndicatorStateDidChange:)
|
||||
name:[OWSTypingIndicatorsImpl typingIndicatorStateDidChange]
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(profileWhitelistDidChange:)
|
||||
name:kNSNotificationName_ProfileWhitelistDidChange
|
||||
object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(blockListDidChange:)
|
||||
name:NSNotification.contactBlockedStateChanged
|
||||
|
@ -289,14 +285,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
object:nil];
|
||||
}
|
||||
|
||||
- (void)profileWhitelistDidChange:(NSNotification *)notification
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
self.conversationProfileState = nil;
|
||||
[self updateForTransientItems];
|
||||
}
|
||||
|
||||
- (void)localProfileDidChange:(NSNotification *)notification
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
|
|
@ -113,7 +113,7 @@ public class LongTextViewController: OWSViewController {
|
|||
// MARK: - Create Views
|
||||
|
||||
private func createViews() {
|
||||
view.backgroundColor = Theme.backgroundColor
|
||||
view.backgroundColor = Colors.navigationBarBackground
|
||||
|
||||
let messageTextView = OWSTextView()
|
||||
self.messageTextView = messageTextView
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import SessionUIKit
|
||||
|
||||
@objc(OWSMediaView)
|
||||
public class MediaView: UIView {
|
||||
|
@ -149,7 +150,7 @@ public class MediaView: UIView {
|
|||
configure(forError: .missing)
|
||||
return
|
||||
}
|
||||
backgroundColor = (Theme.isDarkThemeEnabled ? .ows_gray90 : .ows_gray05)
|
||||
backgroundColor = (isDarkMode ? .ows_gray90 : .ows_gray05)
|
||||
let loader = MediaLoaderView()
|
||||
addSubview(loader)
|
||||
loader.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.bottom, UIView.HorizontalEdge.right ], to: self)
|
||||
|
@ -352,7 +353,7 @@ public class MediaView: UIView {
|
|||
}
|
||||
|
||||
private func configure(forError error: MediaError) {
|
||||
backgroundColor = (Theme.isDarkThemeEnabled ? .ows_gray90 : .ows_gray05)
|
||||
backgroundColor = (isDarkMode ? .ows_gray90 : .ows_gray05)
|
||||
let icon: UIImage
|
||||
switch error {
|
||||
case .failed:
|
||||
|
|
|
@ -270,7 +270,7 @@ class GifPickerCell: UICollectionViewCell {
|
|||
|
||||
private func clearViewState() {
|
||||
imageView?.image = nil
|
||||
self.backgroundColor = (Theme.isDarkThemeEnabled
|
||||
self.backgroundColor = (isDarkMode
|
||||
? UIColor(white: 0.25, alpha: 1.0)
|
||||
: UIColor(white: 0.95, alpha: 1.0))
|
||||
}
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import SignalUtilitiesKit
|
||||
import Reachability
|
||||
import SignalUtilitiesKit
|
||||
import PromiseKit
|
||||
import SessionUIKit
|
||||
|
||||
@objc
|
||||
protocol GifPickerViewControllerDelegate: class {
|
||||
|
@ -234,7 +234,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect
|
|||
private func createErrorLabel(text: String) -> UILabel {
|
||||
let label = UILabel()
|
||||
label.text = text
|
||||
label.textColor = Theme.primaryColor
|
||||
label.textColor = Colors.text
|
||||
label.font = UIFont.ows_mediumFont(withSize: 20)
|
||||
label.textAlignment = .center
|
||||
label.numberOfLines = 0
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
#import <MediaPlayer/MediaPlayer.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SessionUtilitiesKit/NSData+Image.h>
|
||||
#import <SessionUIKit/SessionUIKit.h>
|
||||
#import <YYImage/YYImage.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
@ -201,18 +202,18 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
self.mediaView = animatedView;
|
||||
} else {
|
||||
self.mediaView = [UIView new];
|
||||
self.mediaView.backgroundColor = Theme.offBackgroundColor;
|
||||
self.mediaView.backgroundColor = LKColors.unimportant;
|
||||
}
|
||||
} else if (!self.image) {
|
||||
// Still loading thumbnail.
|
||||
self.mediaView = [UIView new];
|
||||
self.mediaView.backgroundColor = Theme.offBackgroundColor;
|
||||
self.mediaView.backgroundColor = LKColors.unimportant;
|
||||
} else if (self.isVideo) {
|
||||
if (self.attachmentStream.isValidVideo) {
|
||||
self.mediaView = [self buildVideoPlayerView];
|
||||
} else {
|
||||
self.mediaView = [UIView new];
|
||||
self.mediaView.backgroundColor = Theme.offBackgroundColor;
|
||||
self.mediaView.backgroundColor = LKColors.unimportant;
|
||||
}
|
||||
} else {
|
||||
// Present the static image using standard UIImageView
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import UIKit
|
||||
import PromiseKit
|
||||
import SessionUIKit
|
||||
|
||||
// Objc wrapper for the MediaGalleryItem struct
|
||||
@objc
|
||||
|
@ -280,7 +281,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
|
||||
lazy var shareBarButton: UIBarButtonItem = {
|
||||
let shareBarButton = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(didPressShare))
|
||||
shareBarButton.tintColor = Theme.darkThemePrimaryColor
|
||||
shareBarButton.tintColor = Colors.text
|
||||
return shareBarButton
|
||||
}()
|
||||
|
||||
|
@ -288,7 +289,7 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
let deleteBarButton = UIBarButtonItem(barButtonSystemItem: .trash,
|
||||
target: self,
|
||||
action: #selector(didPressDelete))
|
||||
deleteBarButton.tintColor = Theme.darkThemePrimaryColor
|
||||
deleteBarButton.tintColor = Colors.text
|
||||
return deleteBarButton
|
||||
}()
|
||||
|
||||
|
@ -298,14 +299,14 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
|
|||
|
||||
lazy var videoPlayBarButton: UIBarButtonItem = {
|
||||
let videoPlayBarButton = UIBarButtonItem(barButtonSystemItem: .play, target: self, action: #selector(didPressPlayBarButton))
|
||||
videoPlayBarButton.tintColor = Theme.darkThemePrimaryColor
|
||||
videoPlayBarButton.tintColor = Colors.text
|
||||
return videoPlayBarButton
|
||||
}()
|
||||
|
||||
lazy var videoPauseBarButton: UIBarButtonItem = {
|
||||
let videoPauseBarButton = UIBarButtonItem(barButtonSystemItem: .pause, target: self, action:
|
||||
#selector(didPressPauseBarButton))
|
||||
videoPauseBarButton.tintColor = Theme.darkThemePrimaryColor
|
||||
videoPauseBarButton.tintColor = Colors.text
|
||||
return videoPauseBarButton
|
||||
}()
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import SessionUIKit
|
||||
import UIKit
|
||||
|
||||
public protocol MediaTileViewControllerDelegate: class {
|
||||
func mediaTileViewController(_ viewController: MediaTileViewController, didTapView tappedView: UIView, mediaGalleryItem: MediaGalleryItem)
|
||||
|
@ -75,7 +77,7 @@ public class MediaTileViewController: UICollectionViewController, MediaGalleryDa
|
|||
let deleteButton = UIBarButtonItem(barButtonSystemItem: .trash,
|
||||
target: self,
|
||||
action: #selector(didPressDelete))
|
||||
deleteButton.tintColor = Theme.darkThemeNavbarIconColor
|
||||
deleteButton.tintColor = Colors.text
|
||||
|
||||
return deleteButton
|
||||
}()
|
||||
|
@ -823,16 +825,16 @@ private class MediaGallerySectionHeader: UICollectionReusableView {
|
|||
|
||||
override init(frame: CGRect) {
|
||||
label = UILabel()
|
||||
label.textColor = Theme.darkThemePrimaryColor
|
||||
label.textColor = Colors.text
|
||||
|
||||
let blurEffect = Theme.darkThemeBarBlurEffect
|
||||
let blurEffect = UIBlurEffect(style: .dark)
|
||||
let blurEffectView = UIVisualEffectView(effect: blurEffect)
|
||||
|
||||
blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.backgroundColor = isLightMode ? Colors.cellBackground : Theme.darkThemeNavbarBackgroundColor.withAlphaComponent(OWSNavigationBar.backgroundBlurMutingFactor)
|
||||
self.backgroundColor = isLightMode ? Colors.cellBackground : UIColor.ows_black.withAlphaComponent(OWSNavigationBar.backgroundBlurMutingFactor)
|
||||
|
||||
self.addSubview(blurEffectView)
|
||||
self.addSubview(label)
|
||||
|
@ -871,7 +873,7 @@ private class MediaGalleryStaticHeader: UICollectionViewCell {
|
|||
|
||||
addSubview(label)
|
||||
|
||||
label.textColor = Theme.darkThemePrimaryColor
|
||||
label.textColor = Colors.text
|
||||
label.textAlignment = .center
|
||||
label.numberOfLines = 0
|
||||
label.autoPinEdgesToSuperviewMargins(with: UIEdgeInsets(top: 0, leading: Values.largeSpacing, bottom: 0, trailing: Values.largeSpacing))
|
||||
|
|
|
@ -2,6 +2,9 @@
|
|||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SessionUIKit
|
||||
|
||||
public enum PhotoGridItemType {
|
||||
case photo, animated, video
|
||||
}
|
||||
|
@ -29,7 +32,7 @@ public class PhotoGridViewCell: UICollectionViewCell {
|
|||
private static let animatedBadgeImage = #imageLiteral(resourceName: "ic_gallery_badge_gif")
|
||||
private static let selectedBadgeImage = #imageLiteral(resourceName: "selected_blue_circle")
|
||||
|
||||
public var loadingColor = Theme.offBackgroundColor
|
||||
public var loadingColor = Colors.unimportant
|
||||
|
||||
override public var isSelected: Bool {
|
||||
didSet {
|
||||
|
|
|
@ -4,8 +4,6 @@
|
|||
|
||||
#import "AppDelegate.h"
|
||||
#import "MainAppContext.h"
|
||||
#import "OWSBackup.h"
|
||||
#import "OWSOrphanDataCleaner.h"
|
||||
#import "OWSScreenLockUI.h"
|
||||
#import "Session-Swift.h"
|
||||
#import "SignalApp.h"
|
||||
|
@ -101,11 +99,6 @@ static NSTimeInterval launchStartedAt;
|
|||
return Environment.shared.windowManager;
|
||||
}
|
||||
|
||||
- (OWSBackup *)backup
|
||||
{
|
||||
return AppEnvironment.shared.backup;
|
||||
}
|
||||
|
||||
- (OWSNotificationPresenter *)notificationPresenter
|
||||
{
|
||||
return AppEnvironment.shared.notificationPresenter;
|
||||
|
@ -557,11 +550,7 @@ static NSTimeInterval launchStartedAt;
|
|||
UIViewController *rootViewController;
|
||||
BOOL navigationBarHidden = NO;
|
||||
if ([self.tsAccountManager isRegistered]) {
|
||||
if (self.backup.hasPendingRestoreDecision) {
|
||||
rootViewController = [BackupRestoreViewController new];
|
||||
} else {
|
||||
rootViewController = [HomeVC new];
|
||||
}
|
||||
} else {
|
||||
rootViewController = [LandingVC new];
|
||||
navigationBarHidden = NO;
|
||||
|
|
|
@ -34,9 +34,6 @@ import SignalUtilitiesKit
|
|||
@objc
|
||||
public var pushRegistrationManager: PushRegistrationManager
|
||||
|
||||
@objc
|
||||
public var backup: OWSBackup
|
||||
|
||||
@objc
|
||||
public var fileLogger: DDFileLogger
|
||||
|
||||
|
@ -49,15 +46,10 @@ import SignalUtilitiesKit
|
|||
return _userNotificationActionHandler as! UserNotificationActionHandler
|
||||
}
|
||||
|
||||
@objc
|
||||
public var backupLazyRestore: BackupLazyRestore
|
||||
|
||||
private override init() {
|
||||
self.accountManager = AccountManager()
|
||||
self.notificationPresenter = NotificationPresenter()
|
||||
self.pushRegistrationManager = PushRegistrationManager()
|
||||
self.backup = OWSBackup()
|
||||
self.backupLazyRestore = BackupLazyRestore()
|
||||
self._userNotificationActionHandler = UserNotificationActionHandler()
|
||||
self.fileLogger = DDFileLogger()
|
||||
|
||||
|
|
|
@ -19,8 +19,6 @@
|
|||
#import "NotificationSettingsViewController.h"
|
||||
#import "OWSAnyTouchGestureRecognizer.h"
|
||||
#import "OWSAudioPlayer.h"
|
||||
#import "OWSBackup.h"
|
||||
#import "OWSBackupIO.h"
|
||||
#import "OWSBezierPathView.h"
|
||||
#import "OWSConversationSettingsViewController.h"
|
||||
#import "OWSDatabaseMigration.h"
|
||||
|
|
|
@ -23,7 +23,7 @@ class DismissInputBar: UIToolbar {
|
|||
self.items = [spacer, dismissButton]
|
||||
self.isTranslucent = false
|
||||
self.isOpaque = true
|
||||
self.barTintColor = Theme.toolbarBackgroundColor
|
||||
self.barTintColor = UIColor.lokiDarkestGray()
|
||||
|
||||
self.autoresizingMask = .flexibleHeight
|
||||
self.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import SessionUIKit
|
||||
|
||||
class ReminderView: UIView {
|
||||
|
||||
|
@ -70,9 +71,9 @@ class ReminderView: UIView {
|
|||
iconColor = UIColor.ows_gray60
|
||||
case .explanation:
|
||||
// TODO: Theme, review with design.
|
||||
self.backgroundColor = Theme.offBackgroundColor
|
||||
textColor = Theme.primaryColor
|
||||
iconColor = Theme.secondaryColor
|
||||
self.backgroundColor = Colors.unimportant
|
||||
textColor = Colors.text
|
||||
iconColor = Colors.separator
|
||||
}
|
||||
self.clipsToBounds = true
|
||||
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// Notes:
|
||||
//
|
||||
// * On disk, we only bother cleaning up files, not directories.
|
||||
@interface OWSOrphanDataCleaner : NSObject
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
// This is exposed for the debug UI.
|
||||
+ (void)auditAndCleanup:(BOOL)shouldCleanup;
|
||||
// This is exposed for the tests.
|
||||
+ (void)auditAndCleanup:(BOOL)shouldCleanup completion:(dispatch_block_t)completion;
|
||||
|
||||
+ (void)auditOnLaunchIfNecessary;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,737 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSOrphanDataCleaner.h"
|
||||
#import "DateUtil.h"
|
||||
#import <SignalCoreKit/NSDate+OWS.h>
|
||||
#import <SignalUtilitiesKit/OWSProfileManager.h>
|
||||
#import <SessionMessagingKit/OWSUserProfile.h>
|
||||
#import <SessionMessagingKit/AppReadiness.h>
|
||||
#import <SignalUtilitiesKit/AppVersion.h>
|
||||
#import <SessionUtilitiesKit/SessionUtilitiesKit.h>
|
||||
#import <SessionUtilitiesKit/OWSFileSystem.h>
|
||||
#import <SessionMessagingKit/OWSPrimaryStorage.h>
|
||||
#import <SessionMessagingKit/TSAttachmentStream.h>
|
||||
#import <SessionMessagingKit/TSInteraction.h>
|
||||
#import <SessionMessagingKit/TSMessage.h>
|
||||
#import <SessionMessagingKit/TSQuotedMessage.h>
|
||||
#import <SessionMessagingKit/TSThread.h>
|
||||
#import <SessionMessagingKit/YapDatabaseTransaction+OWS.h>
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <YapDatabase/YapDatabase.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// LOG_ALL_FILE_PATHS can be used to determine if there are other kinds of files
|
||||
// that we're not cleaning up.
|
||||
//#define LOG_ALL_FILE_PATHS
|
||||
|
||||
#define ENABLE_ORPHAN_DATA_CLEANER
|
||||
|
||||
NSString *const OWSOrphanDataCleaner_Collection = @"OWSOrphanDataCleaner_Collection";
|
||||
NSString *const OWSOrphanDataCleaner_LastCleaningVersionKey = @"OWSOrphanDataCleaner_LastCleaningVersionKey";
|
||||
NSString *const OWSOrphanDataCleaner_LastCleaningDateKey = @"OWSOrphanDataCleaner_LastCleaningDateKey";
|
||||
|
||||
@interface OWSOrphanData : NSObject
|
||||
|
||||
@property (nonatomic) NSSet<NSString *> *interactionIds;
|
||||
@property (nonatomic) NSSet<NSString *> *attachmentIds;
|
||||
@property (nonatomic) NSSet<NSString *> *filePaths;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation OWSOrphanData
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
typedef void (^OrphanDataBlock)(OWSOrphanData *);
|
||||
|
||||
@implementation OWSOrphanDataCleaner
|
||||
|
||||
// Unlike CurrentAppContext().isMainAppAndActive, this method can be safely
|
||||
// invoked off the main thread.
|
||||
+ (BOOL)isMainAppAndActive
|
||||
{
|
||||
return CurrentAppContext().reportedApplicationState == UIApplicationStateActive;
|
||||
}
|
||||
|
||||
+ (void)printPaths:(NSArray<NSString *> *)paths label:(NSString *)label
|
||||
{
|
||||
for (NSString *path in [paths sortedArrayUsingSelector:@selector(compare:)]) {
|
||||
OWSLogDebug(@"%@: %@", label, path);
|
||||
}
|
||||
}
|
||||
|
||||
+ (long long)fileSizeOfFilePath:(NSString *)filePath
|
||||
{
|
||||
NSError *error;
|
||||
NSNumber *fileSize = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error][NSFileSize];
|
||||
if (error) {
|
||||
if ([error.domain isEqualToString:NSCocoaErrorDomain] && error.code == 260) {
|
||||
OWSLogWarn(@"can't find size of missing file.");
|
||||
OWSLogDebug(@"can't find size of missing file: %@", filePath);
|
||||
} else {
|
||||
OWSFailDebug(@"attributesOfItemAtPath: %@ error: %@", filePath, error);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return fileSize.longLongValue;
|
||||
}
|
||||
|
||||
+ (nullable NSNumber *)fileSizeOfFilePathsSafe:(NSArray<NSString *> *)filePaths
|
||||
{
|
||||
long long result = 0;
|
||||
for (NSString *filePath in filePaths) {
|
||||
if (!self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
result += [self fileSizeOfFilePath:filePath];
|
||||
}
|
||||
return @(result);
|
||||
}
|
||||
|
||||
+ (nullable NSSet<NSString *> *)filePathsInDirectorySafe:(NSString *)dirPath
|
||||
{
|
||||
NSMutableSet *filePaths = [NSMutableSet new];
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:dirPath]) {
|
||||
return filePaths;
|
||||
}
|
||||
NSError *error;
|
||||
NSArray<NSString *> *fileNames = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:dirPath error:&error];
|
||||
if (error) {
|
||||
OWSFailDebug(@"contentsOfDirectoryAtPath error: %@", error);
|
||||
return [NSSet new];
|
||||
}
|
||||
for (NSString *fileName in fileNames) {
|
||||
if (!self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
NSString *filePath = [dirPath stringByAppendingPathComponent:fileName];
|
||||
BOOL isDirectory;
|
||||
[[NSFileManager defaultManager] fileExistsAtPath:filePath isDirectory:&isDirectory];
|
||||
if (isDirectory) {
|
||||
NSSet<NSString *> *_Nullable dirPaths = [self filePathsInDirectorySafe:filePath];
|
||||
if (!dirPaths) {
|
||||
return nil;
|
||||
}
|
||||
[filePaths unionSet:dirPaths];
|
||||
} else {
|
||||
[filePaths addObject:filePath];
|
||||
}
|
||||
}
|
||||
return filePaths;
|
||||
}
|
||||
|
||||
// This method finds (but does not delete):
|
||||
//
|
||||
// * Orphan TSInteractions (with no thread).
|
||||
// * Orphan TSAttachments (with no message).
|
||||
// * Orphan attachment files (with no corresponding TSAttachment).
|
||||
// * Orphan profile avatars.
|
||||
// * Temporary files (all).
|
||||
//
|
||||
// It also finds (we don't clean these up).
|
||||
//
|
||||
// * Missing attachment files (cannot be cleaned up).
|
||||
// These are attachments which have no file on disk. They should be extremely rare -
|
||||
// the only cases I have seen are probably due to debugging.
|
||||
// They can't be cleaned up - we don't want to delete the TSAttachmentStream or
|
||||
// its corresponding message. Better that the broken message shows up in the
|
||||
// conversation view.
|
||||
+ (void)findOrphanDataWithRetries:(NSInteger)remainingRetries
|
||||
databaseConnection:(YapDatabaseConnection *)databaseConnection
|
||||
success:(OrphanDataBlock)success
|
||||
failure:(dispatch_block_t)failure
|
||||
{
|
||||
OWSAssertDebug(databaseConnection);
|
||||
|
||||
if (remainingRetries < 1) {
|
||||
OWSLogInfo(@"Aborting orphan data search.");
|
||||
dispatch_async(self.workQueue, ^{
|
||||
failure();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait until the app is active...
|
||||
[CurrentAppContext() runNowOrWhenMainAppIsActive:^{
|
||||
// ...but perform the work off the main thread.
|
||||
dispatch_async(self.workQueue, ^{
|
||||
OWSOrphanData *_Nullable orphanData = [self findOrphanDataSync:databaseConnection];
|
||||
if (orphanData) {
|
||||
success(orphanData);
|
||||
} else {
|
||||
[self findOrphanDataWithRetries:remainingRetries - 1
|
||||
databaseConnection:databaseConnection
|
||||
success:success
|
||||
failure:failure];
|
||||
}
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
// Returns nil on failure, usually indicating that the search
|
||||
// aborted due to the app resigning active. This method is extremely careful to
|
||||
// abort if the app resigns active, in order to avoid 0xdead10cc crashes.
|
||||
+ (nullable OWSOrphanData *)findOrphanDataSync:(YapDatabaseConnection *)databaseConnection
|
||||
{
|
||||
OWSAssertDebug(databaseConnection);
|
||||
|
||||
__block BOOL shouldAbort = NO;
|
||||
|
||||
#ifdef LOG_ALL_FILE_PATHS
|
||||
{
|
||||
NSString *documentDirPath = [OWSFileSystem appDocumentDirectoryPath];
|
||||
NSArray<NSString *> *_Nullable allDocumentFilePaths =
|
||||
[self filePathsInDirectorySafe:documentDirPath].allObjects;
|
||||
allDocumentFilePaths = [allDocumentFilePaths sortedArrayUsingSelector:@selector(compare:)];
|
||||
NSString *attachmentsFolder = [TSAttachmentStream attachmentsFolder];
|
||||
for (NSString *filePath in allDocumentFilePaths) {
|
||||
if ([filePath hasPrefix:attachmentsFolder]) {
|
||||
continue;
|
||||
}
|
||||
OWSLogVerbose(@"non-attachment file: %@", filePath);
|
||||
}
|
||||
}
|
||||
{
|
||||
NSString *documentDirPath = [OWSFileSystem appSharedDataDirectoryPath];
|
||||
NSArray<NSString *> *_Nullable allDocumentFilePaths =
|
||||
[self filePathsInDirectorySafe:documentDirPath].allObjects;
|
||||
allDocumentFilePaths = [allDocumentFilePaths sortedArrayUsingSelector:@selector(compare:)];
|
||||
NSString *attachmentsFolder = [TSAttachmentStream attachmentsFolder];
|
||||
for (NSString *filePath in allDocumentFilePaths) {
|
||||
if ([filePath hasPrefix:attachmentsFolder]) {
|
||||
continue;
|
||||
}
|
||||
OWSLogVerbose(@"non-attachment file: %@", filePath);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
// We treat _all_ temp files as orphan files. This is safe
|
||||
// because temp files only need to be retained for the
|
||||
// a single launch of the app. Since our "date threshold"
|
||||
// for deletion is relative to the current launch time,
|
||||
// all temp files currently in use should be safe.
|
||||
NSArray<NSString *> *_Nullable tempFilePaths = [self getTempFilePaths];
|
||||
if (!tempFilePaths || !self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
#ifdef LOG_ALL_FILE_PATHS
|
||||
{
|
||||
NSDateFormatter *dateFormatter = [NSDateFormatter new];
|
||||
[dateFormatter setDateStyle:NSDateFormatterLongStyle];
|
||||
[dateFormatter setTimeStyle:NSDateFormatterLongStyle];
|
||||
|
||||
tempFilePaths = [tempFilePaths sortedArrayUsingSelector:@selector(compare:)];
|
||||
for (NSString *filePath in tempFilePaths) {
|
||||
NSError *error;
|
||||
NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error];
|
||||
if (!attributes || error) {
|
||||
OWSLogDebug(@"Could not get attributes of file at: %@", filePath);
|
||||
OWSFailDebug(@"Could not get attributes of file");
|
||||
continue;
|
||||
}
|
||||
OWSLogVerbose(
|
||||
@"temp file: %@, %@", filePath, [dateFormatter stringFromDate:attributes.fileModificationDate]);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
NSString *legacyAttachmentsDirPath = TSAttachmentStream.legacyAttachmentsDirPath;
|
||||
NSString *sharedDataAttachmentsDirPath = TSAttachmentStream.sharedDataAttachmentsDirPath;
|
||||
NSSet<NSString *> *_Nullable legacyAttachmentFilePaths = [self filePathsInDirectorySafe:legacyAttachmentsDirPath];
|
||||
if (!legacyAttachmentFilePaths || !self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
NSSet<NSString *> *_Nullable sharedDataAttachmentFilePaths =
|
||||
[self filePathsInDirectorySafe:sharedDataAttachmentsDirPath];
|
||||
if (!sharedDataAttachmentFilePaths || !self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *legacyProfileAvatarsDirPath = OWSUserProfile.legacyProfileAvatarsDirPath;
|
||||
NSString *sharedDataProfileAvatarsDirPath = OWSUserProfile.sharedDataProfileAvatarsDirPath;
|
||||
NSSet<NSString *> *_Nullable legacyProfileAvatarsFilePaths =
|
||||
[self filePathsInDirectorySafe:legacyProfileAvatarsDirPath];
|
||||
if (!legacyProfileAvatarsFilePaths || !self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
NSSet<NSString *> *_Nullable sharedDataProfileAvatarFilePaths =
|
||||
[self filePathsInDirectorySafe:sharedDataProfileAvatarsDirPath];
|
||||
if (!sharedDataProfileAvatarFilePaths || !self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableSet<NSString *> *allOnDiskFilePaths = [NSMutableSet new];
|
||||
[allOnDiskFilePaths unionSet:legacyAttachmentFilePaths];
|
||||
[allOnDiskFilePaths unionSet:sharedDataAttachmentFilePaths];
|
||||
[allOnDiskFilePaths unionSet:legacyProfileAvatarsFilePaths];
|
||||
[allOnDiskFilePaths unionSet:sharedDataProfileAvatarFilePaths];
|
||||
[allOnDiskFilePaths addObjectsFromArray:tempFilePaths];
|
||||
|
||||
NSSet<NSString *> *profileAvatarFilePaths = [OWSUserProfile allProfileAvatarFilePaths];
|
||||
|
||||
if (!self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSNumber *_Nullable totalFileSize = [self fileSizeOfFilePathsSafe:allOnDiskFilePaths.allObjects];
|
||||
|
||||
if (!totalFileSize || !self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSUInteger fileCount = allOnDiskFilePaths.count;
|
||||
|
||||
// Attachments
|
||||
__block int attachmentStreamCount = 0;
|
||||
NSMutableSet<NSString *> *allAttachmentFilePaths = [NSMutableSet new];
|
||||
NSMutableSet<NSString *> *allAttachmentIds = [NSMutableSet new];
|
||||
// Threads
|
||||
__block NSSet *threadIds;
|
||||
// Messages
|
||||
NSMutableSet<NSString *> *orphanInteractionIds = [NSMutableSet new];
|
||||
NSMutableSet<NSString *> *allMessageAttachmentIds = [NSMutableSet new];
|
||||
[databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
[transaction
|
||||
enumerateKeysAndObjectsInCollection:TSAttachmentStream.collection
|
||||
usingBlock:^(NSString *key, TSAttachment *attachment, BOOL *stop) {
|
||||
if (!self.isMainAppAndActive) {
|
||||
shouldAbort = YES;
|
||||
*stop = YES;
|
||||
return;
|
||||
}
|
||||
if (![attachment isKindOfClass:[TSAttachmentStream class]]) {
|
||||
return;
|
||||
}
|
||||
[allAttachmentIds addObject:attachment.uniqueId];
|
||||
|
||||
TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment;
|
||||
attachmentStreamCount++;
|
||||
NSString *_Nullable filePath = [attachmentStream originalFilePath];
|
||||
if (filePath) {
|
||||
[allAttachmentFilePaths addObject:filePath];
|
||||
} else {
|
||||
OWSFailDebug(@"attachment has no file path.");
|
||||
}
|
||||
|
||||
[allAttachmentFilePaths
|
||||
addObjectsFromArray:attachmentStream.allThumbnailPaths];
|
||||
}];
|
||||
|
||||
if (shouldAbort) {
|
||||
return;
|
||||
}
|
||||
|
||||
threadIds = [NSSet setWithArray:[transaction allKeysInCollection:TSThread.collection]];
|
||||
|
||||
[transaction
|
||||
enumerateKeysAndObjectsInCollection:TSMessage.collection
|
||||
usingBlock:^(NSString *key, TSInteraction *interaction, BOOL *stop) {
|
||||
if (!self.isMainAppAndActive) {
|
||||
shouldAbort = YES;
|
||||
*stop = YES;
|
||||
return;
|
||||
}
|
||||
if (interaction.uniqueThreadId.length < 1
|
||||
|| ![threadIds containsObject:interaction.uniqueThreadId]) {
|
||||
[orphanInteractionIds addObject:interaction.uniqueId];
|
||||
}
|
||||
|
||||
if (![interaction isKindOfClass:[TSMessage class]]) {
|
||||
return;
|
||||
}
|
||||
|
||||
TSMessage *message = (TSMessage *)interaction;
|
||||
[allMessageAttachmentIds addObjectsFromArray:message.allAttachmentIds];
|
||||
}];
|
||||
}];
|
||||
if (shouldAbort) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
OWSLogDebug(@"fileCount: %zu", fileCount);
|
||||
OWSLogDebug(@"totalFileSize: %lld", totalFileSize.longLongValue);
|
||||
OWSLogDebug(@"attachmentStreams: %d", attachmentStreamCount);
|
||||
OWSLogDebug(@"attachmentStreams with file paths: %zu", allAttachmentFilePaths.count);
|
||||
|
||||
NSMutableSet<NSString *> *orphanFilePaths = [allOnDiskFilePaths mutableCopy];
|
||||
[orphanFilePaths minusSet:allAttachmentFilePaths];
|
||||
[orphanFilePaths minusSet:profileAvatarFilePaths];
|
||||
NSMutableSet<NSString *> *missingAttachmentFilePaths = [allAttachmentFilePaths mutableCopy];
|
||||
[missingAttachmentFilePaths minusSet:allOnDiskFilePaths];
|
||||
|
||||
OWSLogDebug(@"orphan file paths: %zu", orphanFilePaths.count);
|
||||
OWSLogDebug(@"missing attachment file paths: %zu", missingAttachmentFilePaths.count);
|
||||
|
||||
[self printPaths:orphanFilePaths.allObjects label:@"orphan file paths"];
|
||||
[self printPaths:missingAttachmentFilePaths.allObjects label:@"missing attachment file paths"];
|
||||
|
||||
OWSLogDebug(@"attachmentIds: %zu", allAttachmentIds.count);
|
||||
OWSLogDebug(@"allMessageAttachmentIds: %zu", allMessageAttachmentIds.count);
|
||||
|
||||
NSMutableSet<NSString *> *orphanAttachmentIds = [allAttachmentIds mutableCopy];
|
||||
[orphanAttachmentIds minusSet:allMessageAttachmentIds];
|
||||
NSMutableSet<NSString *> *missingAttachmentIds = [allMessageAttachmentIds mutableCopy];
|
||||
[missingAttachmentIds minusSet:allAttachmentIds];
|
||||
|
||||
OWSLogDebug(@"orphan attachmentIds: %zu", orphanAttachmentIds.count);
|
||||
OWSLogDebug(@"missing attachmentIds: %zu", missingAttachmentIds.count);
|
||||
OWSLogDebug(@"orphan interactions: %zu", orphanInteractionIds.count);
|
||||
|
||||
OWSOrphanData *result = [OWSOrphanData new];
|
||||
result.interactionIds = [orphanInteractionIds copy];
|
||||
result.attachmentIds = [orphanAttachmentIds copy];
|
||||
result.filePaths = [orphanFilePaths copy];
|
||||
return result;
|
||||
}
|
||||
|
||||
+ (BOOL)shouldAuditOnLaunch:(YapDatabaseConnection *)databaseConnection {
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
#ifndef ENABLE_ORPHAN_DATA_CLEANER
|
||||
return NO;
|
||||
#endif
|
||||
|
||||
__block NSString *_Nullable lastCleaningVersion;
|
||||
__block NSDate *_Nullable lastCleaningDate;
|
||||
[databaseConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
lastCleaningVersion = [transaction stringForKey:OWSOrphanDataCleaner_LastCleaningVersionKey
|
||||
inCollection:OWSOrphanDataCleaner_Collection];
|
||||
lastCleaningDate = [transaction dateForKey:OWSOrphanDataCleaner_LastCleaningDateKey
|
||||
inCollection:OWSOrphanDataCleaner_Collection];
|
||||
}];
|
||||
|
||||
// Clean up once per app version.
|
||||
NSString *currentAppVersion = AppVersion.sharedInstance.currentAppVersion;
|
||||
if (!lastCleaningVersion || ![lastCleaningVersion isEqualToString:currentAppVersion]) {
|
||||
OWSLogVerbose(@"Performing orphan data cleanup; new version: %@.", currentAppVersion);
|
||||
return YES;
|
||||
}
|
||||
|
||||
// Clean up once per N days.
|
||||
if (lastCleaningDate) {
|
||||
#ifdef DEBUG
|
||||
BOOL shouldAudit = [DateUtil dateIsOlderThanToday:lastCleaningDate];
|
||||
#else
|
||||
BOOL shouldAudit = [DateUtil dateIsOlderThanOneWeek:lastCleaningDate];
|
||||
#endif
|
||||
|
||||
if (shouldAudit) {
|
||||
OWSLogVerbose(@"Performing orphan data cleanup; time has passed.");
|
||||
}
|
||||
return shouldAudit;
|
||||
}
|
||||
|
||||
// Has never audited before.
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (void)auditOnLaunchIfNecessary {
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
|
||||
YapDatabaseConnection *databaseConnection = [primaryStorage newDatabaseConnection];
|
||||
|
||||
if (![self shouldAuditOnLaunch:databaseConnection]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we want to be cautious, we can disable orphan deletion using
|
||||
// flag - the cleanup will just be a dry run with logging.
|
||||
BOOL shouldRemoveOrphans = YES;
|
||||
[self auditAndCleanup:shouldRemoveOrphans databaseConnection:databaseConnection completion:nil];
|
||||
}
|
||||
|
||||
+ (void)auditAndCleanup:(BOOL)shouldRemoveOrphans
|
||||
{
|
||||
[self auditAndCleanup:shouldRemoveOrphans
|
||||
completion:^ {
|
||||
}];
|
||||
}
|
||||
|
||||
+ (void)auditAndCleanup:(BOOL)shouldRemoveOrphans completion:(dispatch_block_t)completion
|
||||
{
|
||||
OWSPrimaryStorage *primaryStorage = [OWSPrimaryStorage sharedManager];
|
||||
YapDatabaseConnection *databaseConnection = [primaryStorage newDatabaseConnection];
|
||||
|
||||
[self auditAndCleanup:shouldRemoveOrphans databaseConnection:databaseConnection completion:completion];
|
||||
}
|
||||
|
||||
// We use the lowest priority possible.
|
||||
+ (dispatch_queue_t)workQueue
|
||||
{
|
||||
return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
|
||||
}
|
||||
|
||||
+ (void)auditAndCleanup:(BOOL)shouldRemoveOrphans
|
||||
databaseConnection:(YapDatabaseConnection *)databaseConnection
|
||||
completion:(nullable dispatch_block_t)completion
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
OWSAssertDebug(databaseConnection);
|
||||
|
||||
if (!AppReadiness.isAppReady) {
|
||||
OWSFailDebug(@"can't audit orphan data until app is ready.");
|
||||
return;
|
||||
}
|
||||
if (!CurrentAppContext().isMainApp) {
|
||||
OWSFailDebug(@"can't audit orphan data in app extensions.");
|
||||
return;
|
||||
}
|
||||
if (CurrentAppContext().isRunningTests) {
|
||||
OWSLogVerbose(@"Ignoring audit orphan data in tests.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Orphan cleanup has two risks:
|
||||
//
|
||||
// * As a long-running process that involves access to the
|
||||
// shared data container, it could cause 0xdead10cc.
|
||||
// * It could accidentally delete data still in use,
|
||||
// e.g. a profile avatar which has been saved to disk
|
||||
// but whose OWSUserProfile hasn't been saved yet.
|
||||
//
|
||||
// To prevent 0xdead10cc, the cleaner continually checks
|
||||
// whether the app has resigned active. If so, it aborts.
|
||||
// Each phase (search, re-search, processing) retries N times,
|
||||
// then gives up until the next app launch.
|
||||
//
|
||||
// To prevent accidental data deletion, we take the following
|
||||
// measures:
|
||||
//
|
||||
// * Only cleanup data of the following types (which should
|
||||
// include all relevant app data): profile avatar,
|
||||
// attachment, temporary files (including temporary
|
||||
// attachments).
|
||||
// * We don't delete any data created more recently than N seconds
|
||||
// _before_ when the app launched. This prevents any stray data
|
||||
// currently in use by the app from being accidentally cleaned
|
||||
// up.
|
||||
const NSInteger kMaxRetries = 3;
|
||||
[self findOrphanDataWithRetries:kMaxRetries
|
||||
databaseConnection:databaseConnection
|
||||
success:^(OWSOrphanData *orphanData) {
|
||||
[self processOrphans:orphanData
|
||||
remainingRetries:kMaxRetries
|
||||
databaseConnection:databaseConnection
|
||||
shouldRemoveOrphans:shouldRemoveOrphans
|
||||
success:^{
|
||||
OWSLogInfo(@"Completed orphan data cleanup.");
|
||||
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
[transaction setObject:AppVersion.sharedInstance.currentAppVersion
|
||||
forKey:OWSOrphanDataCleaner_LastCleaningVersionKey
|
||||
inCollection:OWSOrphanDataCleaner_Collection];
|
||||
[transaction setDate:[NSDate new]
|
||||
forKey:OWSOrphanDataCleaner_LastCleaningDateKey
|
||||
inCollection:OWSOrphanDataCleaner_Collection];
|
||||
}];
|
||||
|
||||
if (completion) {
|
||||
completion();
|
||||
}
|
||||
}
|
||||
failure:^{
|
||||
OWSLogInfo(@"Aborting orphan data cleanup.");
|
||||
if (completion) {
|
||||
completion();
|
||||
}
|
||||
}];
|
||||
}
|
||||
failure:^{
|
||||
OWSLogInfo(@"Aborting orphan data cleanup.");
|
||||
if (completion) {
|
||||
completion();
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
// Returns NO on failure, usually indicating that orphan processing
|
||||
// aborted due to the app resigning active. This method is extremely careful to
|
||||
// abort if the app resigns active, in order to avoid 0xdead10cc crashes.
|
||||
+ (void)processOrphans:(OWSOrphanData *)orphanData
|
||||
remainingRetries:(NSInteger)remainingRetries
|
||||
databaseConnection:(YapDatabaseConnection *)databaseConnection
|
||||
shouldRemoveOrphans:(BOOL)shouldRemoveOrphans
|
||||
success:(dispatch_block_t)success
|
||||
failure:(dispatch_block_t)failure
|
||||
{
|
||||
OWSAssertDebug(databaseConnection);
|
||||
OWSAssertDebug(orphanData);
|
||||
|
||||
if (remainingRetries < 1) {
|
||||
OWSLogInfo(@"Aborting orphan data audit.");
|
||||
dispatch_async(self.workQueue, ^{
|
||||
failure();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait until the app is active...
|
||||
[CurrentAppContext() runNowOrWhenMainAppIsActive:^{
|
||||
// ...but perform the work off the main thread.
|
||||
dispatch_async(self.workQueue, ^{
|
||||
if ([self processOrphansSync:orphanData
|
||||
databaseConnection:databaseConnection
|
||||
shouldRemoveOrphans:shouldRemoveOrphans]) {
|
||||
success();
|
||||
return;
|
||||
} else {
|
||||
[self processOrphans:orphanData
|
||||
remainingRetries:remainingRetries - 1
|
||||
databaseConnection:databaseConnection
|
||||
shouldRemoveOrphans:shouldRemoveOrphans
|
||||
success:success
|
||||
failure:failure];
|
||||
}
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
// Returns NO on failure, usually indicating that orphan processing
|
||||
// aborted due to the app resigning active. This method is extremely careful to
|
||||
// abort if the app resigns active, in order to avoid 0xdead10cc crashes.
|
||||
+ (BOOL)processOrphansSync:(OWSOrphanData *)orphanData
|
||||
databaseConnection:(YapDatabaseConnection *)databaseConnection
|
||||
shouldRemoveOrphans:(BOOL)shouldRemoveOrphans
|
||||
{
|
||||
OWSAssertDebug(databaseConnection);
|
||||
OWSAssertDebug(orphanData);
|
||||
|
||||
__block BOOL shouldAbort = NO;
|
||||
|
||||
// We need to avoid cleaning up new files that are still in the process of
|
||||
// being created/written, so we don't clean up anything recent.
|
||||
const NSTimeInterval kMinimumOrphanAgeSeconds = CurrentAppContext().isRunningTests ? 0.f : 15 * kMinuteInterval;
|
||||
NSDate *appLaunchTime = CurrentAppContext().appLaunchTime;
|
||||
NSTimeInterval thresholdTimestamp = appLaunchTime.timeIntervalSince1970 - kMinimumOrphanAgeSeconds;
|
||||
NSDate *thresholdDate = [NSDate dateWithTimeIntervalSince1970:thresholdTimestamp];
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
NSUInteger interactionsRemoved = 0;
|
||||
for (NSString *interactionId in orphanData.interactionIds) {
|
||||
if (!self.isMainAppAndActive) {
|
||||
shouldAbort = YES;
|
||||
return;
|
||||
}
|
||||
TSInteraction *_Nullable interaction =
|
||||
[TSInteraction fetchObjectWithUniqueID:interactionId transaction:transaction];
|
||||
if (!interaction) {
|
||||
// This could just be a race condition, but it should be very unlikely.
|
||||
OWSLogWarn(@"Could not load interaction: %@", interactionId);
|
||||
continue;
|
||||
}
|
||||
// Don't delete interactions which were created in the last N minutes.
|
||||
NSDate *creationDate = [NSDate ows_dateWithMillisecondsSince1970:interaction.timestamp];
|
||||
if ([creationDate isAfterDate:thresholdDate]) {
|
||||
OWSLogInfo(@"Skipping orphan interaction due to age: %f", fabs(creationDate.timeIntervalSinceNow));
|
||||
continue;
|
||||
}
|
||||
OWSLogInfo(@"Removing orphan message: %@", interaction.uniqueId);
|
||||
interactionsRemoved++;
|
||||
if (!shouldRemoveOrphans) {
|
||||
continue;
|
||||
}
|
||||
[interaction removeWithTransaction:transaction];
|
||||
}
|
||||
OWSLogInfo(@"Deleted orphan interactions: %zu", interactionsRemoved);
|
||||
|
||||
NSUInteger attachmentsRemoved = 0;
|
||||
for (NSString *attachmentId in orphanData.attachmentIds) {
|
||||
if (!self.isMainAppAndActive) {
|
||||
shouldAbort = YES;
|
||||
return;
|
||||
}
|
||||
TSAttachment *_Nullable attachment =
|
||||
[TSAttachment fetchObjectWithUniqueID:attachmentId transaction:transaction];
|
||||
if (!attachment) {
|
||||
// This can happen on launch since we sync contacts/groups, especially if you have a lot of attachments
|
||||
// to churn through, it's likely it's been deleted since starting this job.
|
||||
OWSLogWarn(@"Could not load attachment: %@", attachmentId);
|
||||
continue;
|
||||
}
|
||||
if (![attachment isKindOfClass:[TSAttachmentStream class]]) {
|
||||
continue;
|
||||
}
|
||||
TSAttachmentStream *attachmentStream = (TSAttachmentStream *)attachment;
|
||||
// Don't delete attachments which were created in the last N minutes.
|
||||
NSDate *creationDate = attachmentStream.creationTimestamp;
|
||||
if ([creationDate isAfterDate:thresholdDate]) {
|
||||
OWSLogInfo(@"Skipping orphan attachment due to age: %f", fabs(creationDate.timeIntervalSinceNow));
|
||||
continue;
|
||||
}
|
||||
OWSLogInfo(@"Removing orphan attachmentStream: %@", attachmentStream.uniqueId);
|
||||
attachmentsRemoved++;
|
||||
if (!shouldRemoveOrphans) {
|
||||
continue;
|
||||
}
|
||||
[attachmentStream removeWithTransaction:transaction];
|
||||
}
|
||||
OWSLogInfo(@"Deleted orphan attachments: %zu", attachmentsRemoved);
|
||||
}];
|
||||
|
||||
if (shouldAbort) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSUInteger filesRemoved = 0;
|
||||
NSArray<NSString *> *filePaths = [orphanData.filePaths.allObjects sortedArrayUsingSelector:@selector(compare:)];
|
||||
for (NSString *filePath in filePaths) {
|
||||
if (!self.isMainAppAndActive) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:filePath error:&error];
|
||||
if (!attributes || error) {
|
||||
// This is fine; the file may have been deleted since we found it.
|
||||
OWSLogWarn(@"Could not get attributes of file at: %@", filePath);
|
||||
continue;
|
||||
}
|
||||
// Don't delete files which were created in the last N minutes.
|
||||
NSDate *creationDate = attributes.fileModificationDate;
|
||||
if ([creationDate isAfterDate:thresholdDate]) {
|
||||
OWSLogInfo(@"Skipping file due to age: %f", fabs([creationDate timeIntervalSinceNow]));
|
||||
continue;
|
||||
}
|
||||
OWSLogInfo(@"Deleting file: %@", filePath);
|
||||
filesRemoved++;
|
||||
if (!shouldRemoveOrphans) {
|
||||
continue;
|
||||
}
|
||||
[[NSFileManager defaultManager] removeItemAtPath:filePath error:&error];
|
||||
if (error) {
|
||||
OWSLogDebug(@"Could not remove orphan file at: %@", filePath);
|
||||
OWSFailDebug(@"Could not remove orphan file");
|
||||
}
|
||||
}
|
||||
OWSLogInfo(@"Deleted orphan files: %zu", filesRemoved);
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (nullable NSArray<NSString *> *)getTempFilePaths
|
||||
{
|
||||
NSString *dir1 = OWSTemporaryDirectory();
|
||||
NSArray<NSString *> *_Nullable paths1 = [[self filePathsInDirectorySafe:dir1].allObjects mutableCopy];
|
||||
|
||||
NSString *dir2 = OWSTemporaryDirectoryAccessibleAfterFirstAuth();
|
||||
NSArray<NSString *> *_Nullable paths2 = [[self filePathsInDirectorySafe:dir2].allObjects mutableCopy];
|
||||
|
||||
if (paths1 && paths2) {
|
||||
return [paths1 arrayByAddingObjectsFromArray:paths2];
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -150,7 +150,6 @@ public final class OpenGroupManagerV2 : NSObject {
|
|||
messageIDs.insert(interaction.uniqueId!)
|
||||
messageTimestamps.insert(interaction.timestamp)
|
||||
}
|
||||
storage.updateMessageIDCollectionByPruningMessagesWithIDs(messageIDs, using: transaction)
|
||||
Storage.shared.removeReceivedMessageTimestamps(messageTimestamps, using: transaction)
|
||||
Storage.shared.removeLastMessageServerID(for: openGroup.room, on: openGroup.server, using: transaction)
|
||||
Storage.shared.removeLastDeletionServerID(for: openGroup.room, on: openGroup.server, using: transaction)
|
||||
|
|
|
@ -16,12 +16,6 @@
|
|||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
typedef NS_ENUM(NSUInteger, OWSReceiptType) {
|
||||
OWSReceiptType_Delivery,
|
||||
OWSReceiptType_Read,
|
||||
};
|
||||
|
||||
NSString *const kOutgoingDeliveryReceiptManagerCollection = @"kOutgoingDeliveryReceiptManagerCollection";
|
||||
NSString *const kOutgoingReadReceiptManagerCollection = @"kOutgoingReadReceiptManagerCollection";
|
||||
|
||||
@interface OWSOutgoingReceiptManager ()
|
||||
|
@ -103,7 +97,7 @@ NSString *const kOutgoingReadReceiptManagerCollection = @"kOutgoingReadReceiptMa
|
|||
}
|
||||
|
||||
NSMutableArray<AnyPromise *> *sendPromises = [NSMutableArray array];
|
||||
[sendPromises addObjectsFromArray:[self sendReceiptsForReceiptType:OWSReceiptType_Read]];
|
||||
[sendPromises addObjectsFromArray:[self sendReceipts]];
|
||||
|
||||
if (sendPromises.count < 1) {
|
||||
// No work to do; abort.
|
||||
|
@ -132,10 +126,8 @@ NSString *const kOutgoingReadReceiptManagerCollection = @"kOutgoingReadReceiptMa
|
|||
});
|
||||
}
|
||||
|
||||
- (NSArray<AnyPromise *> *)sendReceiptsForReceiptType:(OWSReceiptType)receiptType {
|
||||
if (receiptType == OWSReceiptType_Delivery) { return @[]; } // Don't send delivery receipts
|
||||
|
||||
NSString *collection = [self collectionForReceiptType:receiptType];
|
||||
- (NSArray<AnyPromise *> *)sendReceipts {
|
||||
NSString *collection = kOutgoingReadReceiptManagerCollection;
|
||||
|
||||
NSMutableDictionary<NSString *, NSSet<NSNumber *> *> *queuedReceiptMap = [NSMutableDictionary new];
|
||||
[self.dbConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
|
@ -170,7 +162,7 @@ NSString *const kOutgoingReadReceiptManagerCollection = @"kOutgoingReadReceiptMa
|
|||
[LKStorage writeWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
AnyPromise *promise = [SNMessageSender sendNonDurably:readReceipt inThread:thread usingTransaction:transaction]
|
||||
.thenOn(self.serialQueue, ^(id object) {
|
||||
[self dequeueReceiptsWithRecipientId:recipientId timestamps:timestampsAsSet receiptType:OWSReceiptType_Read];
|
||||
[self dequeueReceiptsWithRecipientId:recipientId timestamps:timestampsAsSet];
|
||||
});
|
||||
[sendPromises addObject:promise];
|
||||
}];
|
||||
|
@ -179,22 +171,11 @@ NSString *const kOutgoingReadReceiptManagerCollection = @"kOutgoingReadReceiptMa
|
|||
return [sendPromises copy];
|
||||
}
|
||||
|
||||
- (void)enqueueDeliveryReceiptForEnvelope:(SNProtoEnvelope *)envelope
|
||||
{
|
||||
[self enqueueReceiptWithRecipientId:envelope.source
|
||||
timestamp:envelope.timestamp
|
||||
receiptType:OWSReceiptType_Delivery];
|
||||
}
|
||||
|
||||
- (void)enqueueReadReceiptForEnvelope:(NSString *)messageAuthorId timestamp:(uint64_t)timestamp {
|
||||
[self enqueueReceiptWithRecipientId:messageAuthorId timestamp:timestamp receiptType:OWSReceiptType_Read];
|
||||
[self enqueueReceiptWithRecipientId:messageAuthorId timestamp:timestamp];
|
||||
}
|
||||
|
||||
- (void)enqueueReceiptWithRecipientId:(NSString *)recipientId
|
||||
timestamp:(uint64_t)timestamp
|
||||
receiptType:(OWSReceiptType)receiptType {
|
||||
NSString *collection = [self collectionForReceiptType:receiptType];
|
||||
|
||||
- (void)enqueueReceiptWithRecipientId:(NSString *)recipientId timestamp:(uint64_t)timestamp {
|
||||
if (recipientId.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
@ -203,23 +184,19 @@ NSString *const kOutgoingReadReceiptManagerCollection = @"kOutgoingReadReceiptMa
|
|||
}
|
||||
dispatch_async(self.serialQueue, ^{
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
NSSet<NSNumber *> *_Nullable oldTimestamps = [transaction objectForKey:recipientId inCollection:collection];
|
||||
NSSet<NSNumber *> *_Nullable oldTimestamps = [transaction objectForKey:recipientId inCollection:kOutgoingReadReceiptManagerCollection];
|
||||
NSMutableSet<NSNumber *> *newTimestamps
|
||||
= (oldTimestamps ? [oldTimestamps mutableCopy] : [NSMutableSet new]);
|
||||
[newTimestamps addObject:@(timestamp)];
|
||||
|
||||
[transaction setObject:newTimestamps forKey:recipientId inCollection:collection];
|
||||
[transaction setObject:newTimestamps forKey:recipientId inCollection:kOutgoingReadReceiptManagerCollection];
|
||||
}];
|
||||
|
||||
[self process];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)dequeueReceiptsWithRecipientId:(NSString *)recipientId
|
||||
timestamps:(NSSet<NSNumber *> *)timestamps
|
||||
receiptType:(OWSReceiptType)receiptType {
|
||||
NSString *collection = [self collectionForReceiptType:receiptType];
|
||||
|
||||
- (void)dequeueReceiptsWithRecipientId:(NSString *)recipientId timestamps:(NSSet<NSNumber *> *)timestamps {
|
||||
if (recipientId.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
@ -228,15 +205,15 @@ NSString *const kOutgoingReadReceiptManagerCollection = @"kOutgoingReadReceiptMa
|
|||
}
|
||||
dispatch_async(self.serialQueue, ^{
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
NSSet<NSNumber *> *_Nullable oldTimestamps = [transaction objectForKey:recipientId inCollection:collection];
|
||||
NSSet<NSNumber *> *_Nullable oldTimestamps = [transaction objectForKey:recipientId inCollection:kOutgoingReadReceiptManagerCollection];
|
||||
NSMutableSet<NSNumber *> *newTimestamps
|
||||
= (oldTimestamps ? [oldTimestamps mutableCopy] : [NSMutableSet new]);
|
||||
[newTimestamps minusSet:timestamps];
|
||||
|
||||
if (newTimestamps.count > 0) {
|
||||
[transaction setObject:newTimestamps forKey:recipientId inCollection:collection];
|
||||
[transaction setObject:newTimestamps forKey:recipientId inCollection:kOutgoingReadReceiptManagerCollection];
|
||||
} else {
|
||||
[transaction removeObjectForKey:recipientId inCollection:collection];
|
||||
[transaction removeObjectForKey:recipientId inCollection:kOutgoingReadReceiptManagerCollection];
|
||||
}
|
||||
}];
|
||||
});
|
||||
|
@ -247,15 +224,6 @@ NSString *const kOutgoingReadReceiptManagerCollection = @"kOutgoingReadReceiptMa
|
|||
[self process];
|
||||
}
|
||||
|
||||
- (NSString *)collectionForReceiptType:(OWSReceiptType)receiptType {
|
||||
switch (receiptType) {
|
||||
case OWSReceiptType_Delivery:
|
||||
return kOutgoingDeliveryReceiptManagerCollection;
|
||||
case OWSReceiptType_Read:
|
||||
return kOutgoingReadReceiptManagerCollection;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import PromiseKit
|
||||
import Sodium
|
||||
import SessionSnodeKit
|
||||
|
||||
public protocol SessionMessagingKitStorageProtocol {
|
||||
|
||||
|
@ -52,7 +53,6 @@ public protocol SessionMessagingKitStorageProtocol {
|
|||
func getAllV2OpenGroups() -> [String:OpenGroupV2]
|
||||
func getV2OpenGroup(for threadID: String) -> OpenGroupV2?
|
||||
func v2GetThreadID(for v2OpenGroupID: String) -> String?
|
||||
func updateMessageIDCollectionByPruningMessagesWithIDs(_ messageIDs: Set<String>, using transaction: Any)
|
||||
|
||||
// MARK: - Open Group Public Keys
|
||||
|
||||
|
@ -97,3 +97,5 @@ public protocol SessionMessagingKitStorageProtocol {
|
|||
/// Also touches the associated message.
|
||||
func persist(_ stream: TSAttachmentStream, associatedWith tsIncomingMessageID: String, using transaction: Any)
|
||||
}
|
||||
|
||||
extension Storage: SessionMessagingKitStorageProtocol, SessionSnodeKitStorageProtocol {}
|
||||
|
|
|
@ -140,9 +140,6 @@ typedef NS_ENUM(NSUInteger, OWSRegistrationState) {
|
|||
- (BOOL)isDeregistered;
|
||||
- (void)setIsDeregistered:(BOOL)isDeregistered;
|
||||
|
||||
- (BOOL)hasPendingBackupRestoreDecision;
|
||||
- (void)setHasPendingBackupRestoreDecision:(BOOL)value;
|
||||
|
||||
#pragma mark - Re-registration
|
||||
|
||||
// Re-registration is the process of re-registering _with the same phone number_.
|
||||
|
|
|
@ -418,22 +418,6 @@ NSString *const TSAccountManager_NeedsAccountAttributesUpdateKey = @"TSAccountMa
|
|||
inCollection:TSAccountManager_UserAccountCollection];
|
||||
}
|
||||
|
||||
- (BOOL)hasPendingBackupRestoreDecision
|
||||
{
|
||||
return [self.dbConnection boolForKey:TSAccountManager_HasPendingRestoreDecisionKey
|
||||
inCollection:TSAccountManager_UserAccountCollection
|
||||
defaultValue:NO];
|
||||
}
|
||||
|
||||
- (void)setHasPendingBackupRestoreDecision:(BOOL)value
|
||||
{
|
||||
[self.dbConnection setBool:value
|
||||
forKey:TSAccountManager_HasPendingRestoreDecisionKey
|
||||
inCollection:TSAccountManager_UserAccountCollection];
|
||||
|
||||
[self postRegistrationStateDidChangeNotification];
|
||||
}
|
||||
|
||||
- (BOOL)isManualMessageFetchEnabled
|
||||
{
|
||||
return [self.dbConnection boolForKey:TSAccountManager_ManualMessageFetchKey
|
||||
|
|
|
@ -15,8 +15,6 @@ extern NSString *const LKED25519SecretKey;
|
|||
extern NSString *const LKED25519PublicKey;
|
||||
extern NSString *const OWSPrimaryStorageIdentityKeyStoreCollection;
|
||||
|
||||
extern NSString *const OWSPrimaryStorageTrustedKeysCollection;
|
||||
|
||||
// This notification will be fired whenever identities are created
|
||||
// or their verification state changes.
|
||||
extern NSString *const kNSNotificationName_IdentityStateDidChange;
|
||||
|
@ -55,20 +53,8 @@ extern const NSUInteger kStoredIdentityKeyLength;
|
|||
*/
|
||||
- (nullable OWSRecipientIdentity *)untrustedIdentityForSendingToRecipientId:(NSString *)recipientId;
|
||||
|
||||
- (BOOL)saveRemoteIdentity:(NSData *)identityKey recipientId:(NSString *)recipientId;
|
||||
|
||||
- (nullable ECKeyPair *)identityKeyPair;
|
||||
|
||||
#pragma mark - Debug
|
||||
|
||||
#if DEBUG
|
||||
// Clears everything except the local identity key.
|
||||
- (void)clearIdentityState:(YapDatabaseReadWriteTransaction *)transaction;
|
||||
|
||||
- (void)snapshotIdentityState:(YapDatabaseReadWriteTransaction *)transaction;
|
||||
- (void)restoreIdentityState:(YapDatabaseReadWriteTransaction *)transaction;
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -33,12 +33,6 @@ NSString *const LKED25519SecretKey = @"LKED25519SecretKey";
|
|||
NSString *const LKED25519PublicKey = @"LKED25519PublicKey";
|
||||
NSString *const OWSPrimaryStorageIdentityKeyStoreCollection = @"TSStorageManagerIdentityKeyStoreCollection";
|
||||
|
||||
// Storing recipients identity keys
|
||||
NSString *const OWSPrimaryStorageTrustedKeysCollection = @"TSStorageManagerTrustedKeysCollection";
|
||||
|
||||
NSString *const OWSIdentityManager_QueuedVerificationStateSyncMessages =
|
||||
@"OWSIdentityManager_QueuedVerificationStateSyncMessages";
|
||||
|
||||
// Don't trust an identity for sending to unless they've been around for at least this long
|
||||
const NSTimeInterval kIdentityKeyStoreNonBlockingSecondsThreshold = 5.0;
|
||||
|
||||
|
@ -79,8 +73,6 @@ NSString *const kNSNotificationName_IdentityStateDidChange = @"kNSNotificationNa
|
|||
_dbConnection = primaryStorage.newDatabaseConnection;
|
||||
self.dbConnection.objectCacheEnabled = NO;
|
||||
|
||||
[self observeNotifications];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
@ -91,14 +83,6 @@ NSString *const kNSNotificationName_IdentityStateDidChange = @"kNSNotificationNa
|
|||
|
||||
#pragma mark -
|
||||
|
||||
- (void)observeNotifications
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(applicationDidBecomeActive:)
|
||||
name:UIApplicationDidBecomeActiveNotification
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)generateNewIdentityKeyPair
|
||||
{
|
||||
ECKeyPair *keyPair = [Curve25519 generateKeyPair];
|
||||
|
@ -165,70 +149,6 @@ NSString *const kNSNotificationName_IdentityStateDidChange = @"kNSNotificationNa
|
|||
return (int)[TSAccountManager getOrGenerateRegistrationId:transaction];
|
||||
}
|
||||
|
||||
- (BOOL)saveRemoteIdentity:(NSData *)identityKey recipientId:(NSString *)recipientId
|
||||
{
|
||||
__block BOOL result;
|
||||
[LKStorage writeSyncWithBlock:^(YapDatabaseReadWriteTransaction *transaction) {
|
||||
result = [self saveRemoteIdentity:identityKey recipientId:recipientId protocolContext:transaction];
|
||||
}];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (BOOL)saveRemoteIdentity:(NSData *)identityKey
|
||||
recipientId:(NSString *)recipientId
|
||||
protocolContext:(nullable id)protocolContext
|
||||
{
|
||||
YapDatabaseReadWriteTransaction *transaction = protocolContext;
|
||||
|
||||
// Deprecated. We actually no longer use the OWSPrimaryStorageTrustedKeysCollection for trust
|
||||
// decisions, but it's desirable to try to keep it up to date with our trusted identitys
|
||||
// while we're switching between versions, e.g. so we don't get into a state where we have a
|
||||
// session for an identity not in our key store.
|
||||
[transaction setObject:identityKey forKey:recipientId inCollection:OWSPrimaryStorageTrustedKeysCollection];
|
||||
|
||||
OWSRecipientIdentity *existingIdentity =
|
||||
[OWSRecipientIdentity fetchObjectWithUniqueID:recipientId transaction:transaction];
|
||||
|
||||
if (existingIdentity == nil) {
|
||||
[[[OWSRecipientIdentity alloc] initWithRecipientId:recipientId
|
||||
identityKey:identityKey
|
||||
isFirstKnownKey:YES
|
||||
createdAt:[NSDate new]
|
||||
verificationState:OWSVerificationStateDefault]
|
||||
saveWithTransaction:transaction];
|
||||
|
||||
[self fireIdentityStateChangeNotification];
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (![existingIdentity.identityKey isEqual:identityKey]) {
|
||||
OWSVerificationState verificationState;
|
||||
switch (existingIdentity.verificationState) {
|
||||
case OWSVerificationStateDefault:
|
||||
verificationState = OWSVerificationStateDefault;
|
||||
break;
|
||||
case OWSVerificationStateVerified:
|
||||
case OWSVerificationStateNoLongerVerified:
|
||||
verificationState = OWSVerificationStateNoLongerVerified;
|
||||
break;
|
||||
}
|
||||
|
||||
[[[OWSRecipientIdentity alloc] initWithRecipientId:recipientId
|
||||
identityKey:identityKey
|
||||
isFirstKnownKey:NO
|
||||
createdAt:[NSDate new]
|
||||
verificationState:verificationState] saveWithTransaction:transaction];
|
||||
|
||||
[self fireIdentityStateChangeNotification];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (nullable OWSRecipientIdentity *)recipientIdentityForRecipientId:(NSString *)recipientId
|
||||
{
|
||||
__block OWSRecipientIdentity *_Nullable result;
|
||||
|
@ -341,65 +261,6 @@ NSString *const kNSNotificationName_IdentityStateDidChange = @"kNSNotificationNa
|
|||
}
|
||||
}
|
||||
|
||||
#pragma mark - Debug
|
||||
|
||||
#if DEBUG
|
||||
- (void)clearIdentityState:(YapDatabaseReadWriteTransaction *)transaction
|
||||
{
|
||||
NSMutableArray<NSString *> *identityKeysToRemove = [NSMutableArray new];
|
||||
[transaction enumerateKeysInCollection:OWSPrimaryStorageIdentityKeyStoreCollection
|
||||
usingBlock:^(NSString *_Nonnull key, BOOL *_Nonnull stop) {
|
||||
if ([key isEqualToString:OWSPrimaryStorageIdentityKeyStoreIdentityKey]) {
|
||||
// Don't delete our own key.
|
||||
return;
|
||||
}
|
||||
[identityKeysToRemove addObject:key];
|
||||
}];
|
||||
for (NSString *key in identityKeysToRemove) {
|
||||
[transaction removeObjectForKey:key inCollection:OWSPrimaryStorageIdentityKeyStoreCollection];
|
||||
}
|
||||
[transaction removeAllObjectsInCollection:OWSPrimaryStorageTrustedKeysCollection];
|
||||
}
|
||||
|
||||
- (NSString *)identityKeySnapshotFilePath
|
||||
{
|
||||
// Prefix name with period "." so that backups will ignore these snapshots.
|
||||
NSString *dirPath = [OWSFileSystem appDocumentDirectoryPath];
|
||||
return [dirPath stringByAppendingPathComponent:@".identity-key-snapshot"];
|
||||
}
|
||||
|
||||
- (NSString *)trustedKeySnapshotFilePath
|
||||
{
|
||||
// Prefix name with period "." so that backups will ignore these snapshots.
|
||||
NSString *dirPath = [OWSFileSystem appDocumentDirectoryPath];
|
||||
return [dirPath stringByAppendingPathComponent:@".trusted-key-snapshot"];
|
||||
}
|
||||
|
||||
- (void)snapshotIdentityState:(YapDatabaseReadWriteTransaction *)transaction
|
||||
{
|
||||
[transaction snapshotCollection:OWSPrimaryStorageIdentityKeyStoreCollection
|
||||
snapshotFilePath:self.identityKeySnapshotFilePath];
|
||||
[transaction snapshotCollection:OWSPrimaryStorageTrustedKeysCollection
|
||||
snapshotFilePath:self.trustedKeySnapshotFilePath];
|
||||
}
|
||||
|
||||
- (void)restoreIdentityState:(YapDatabaseReadWriteTransaction *)transaction
|
||||
{
|
||||
[transaction restoreSnapshotOfCollection:OWSPrimaryStorageIdentityKeyStoreCollection
|
||||
snapshotFilePath:self.identityKeySnapshotFilePath];
|
||||
[transaction restoreSnapshotOfCollection:OWSPrimaryStorageTrustedKeysCollection
|
||||
snapshotFilePath:self.trustedKeySnapshotFilePath];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#pragma mark - Notifications
|
||||
|
||||
- (void)applicationDidBecomeActive:(NSNotification *)notification
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -126,7 +126,6 @@ final class ShareVC : UINavigationController, ShareViewDelegate, AppModeManagerD
|
|||
|
||||
// We don't need to use OWSMessageReceiver in the SAE.
|
||||
// We don't need to use OWSBatchMessageProcessor in the SAE.
|
||||
// We don't need to use OWSOrphanDataCleaner in the SAE.
|
||||
// We don't need to fetch the local profile in the SAE
|
||||
|
||||
OWSReadReceiptManager.shared().prepareCachedValues()
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc
|
||||
public class AppPreferences: NSObject {
|
||||
// Never instantiate this class.
|
||||
private override init() {}
|
||||
|
||||
private static let collection = "AppPreferences"
|
||||
|
||||
// MARK: -
|
||||
|
||||
private static let hasDimissedFirstConversationCueKey = "hasDimissedFirstConversationCue"
|
||||
|
||||
@objc
|
||||
public static var hasDimissedFirstConversationCue: Bool {
|
||||
get {
|
||||
return getBool(key: hasDimissedFirstConversationCueKey)
|
||||
}
|
||||
set {
|
||||
setBool(newValue, key: hasDimissedFirstConversationCueKey)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private class func getBool(key: String, defaultValue: Bool = false) -> Bool {
|
||||
return OWSPrimaryStorage.dbReadConnection().bool(forKey: key, inCollection: collection, defaultValue: defaultValue)
|
||||
}
|
||||
|
||||
private class func setBool(_ value: Bool, key: String) {
|
||||
OWSPrimaryStorage.dbReadWriteConnection().setBool(value, forKey: key, inCollection: collection)
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
|
||||
extension Storage : SessionMessagingKitStorageProtocol, SessionSnodeKitStorageProtocol {
|
||||
|
||||
public func updateMessageIDCollectionByPruningMessagesWithIDs(_ messageIDs: Set<String>, using transaction: Any) {
|
||||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
||||
OWSPrimaryStorage.shared().updateMessageIDCollectionByPruningMessagesWithIDs(messageIDs, in: transaction)
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef Signal_TSStorageHeaders_h
|
||||
#define Signal_TSStorageHeaders_h
|
||||
#import <SessionMessagingKit/OWSIdentityManager.h>
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage+keyFromIntLong.h>
|
||||
#import <SessionMessagingKit/OWSPrimaryStorage.h>
|
||||
|
||||
#endif
|
|
@ -1,32 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#ifndef TextSecureKit_TSStorageKeys_h
|
||||
#define TextSecureKit_TSStorageKeys_h
|
||||
|
||||
/**
|
||||
* Preferences exposed to the user
|
||||
*/
|
||||
|
||||
#pragma mark User Preferences
|
||||
|
||||
#define TSStorageUserPreferencesCollection @"TSStorageUserPreferencesCollection"
|
||||
|
||||
|
||||
/**
|
||||
* Internal settings of the application, not exposed to the user.
|
||||
*/
|
||||
|
||||
#pragma mark Internal Settings
|
||||
|
||||
#define TSStorageInternalSettingsCollection @"TSStorageInternalSettingsCollection"
|
||||
#define TSStorageInternalSettingsVersion @"TSLastLaunchedVersion"
|
||||
|
||||
#endif
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -3,6 +3,7 @@
|
|||
//
|
||||
|
||||
import UIKit
|
||||
import SessionUIKit
|
||||
|
||||
protocol AttachmentCaptionDelegate: class {
|
||||
func captionView(_ captionView: AttachmentCaptionViewController, didChangeCaptionText captionText: String?, attachmentItem: SignalAttachmentItem)
|
||||
|
@ -217,8 +218,8 @@ class AttachmentCaptionViewController: OWSViewController {
|
|||
placeholderTextView.backgroundColor = .clear
|
||||
placeholderTextView.font = UIFont.ows_dynamicTypeBody
|
||||
|
||||
placeholderTextView.textColor = Theme.darkThemePrimaryColor
|
||||
placeholderTextView.tintColor = Theme.darkThemePrimaryColor
|
||||
placeholderTextView.textColor = Colors.text
|
||||
placeholderTextView.tintColor = Colors.text
|
||||
placeholderTextView.returnKeyType = .done
|
||||
|
||||
return placeholderTextView
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import SessionUIKit
|
||||
|
||||
@objc
|
||||
public protocol MessageApprovalViewControllerDelegate: class {
|
||||
|
@ -70,7 +71,7 @@ public class MessageApprovalViewController: OWSViewController, UITextViewDelegat
|
|||
public override func loadView() {
|
||||
|
||||
self.view = UIView.container()
|
||||
self.view.backgroundColor = Theme.backgroundColor
|
||||
self.view.backgroundColor = Colors.navigationBarBackground
|
||||
|
||||
// Recipient Row
|
||||
let recipientRow = createRecipientRow()
|
||||
|
@ -82,8 +83,8 @@ public class MessageApprovalViewController: OWSViewController, UITextViewDelegat
|
|||
// Text View
|
||||
textView = OWSTextView()
|
||||
textView.delegate = self
|
||||
textView.backgroundColor = Theme.backgroundColor
|
||||
textView.textColor = Theme.primaryColor
|
||||
textView.backgroundColor = Colors.navigationBarBackground
|
||||
textView.textColor = Colors.text
|
||||
textView.font = UIFont.ows_dynamicTypeBody
|
||||
textView.text = self.initialMessageText
|
||||
textView.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
|
||||
|
@ -97,11 +98,11 @@ public class MessageApprovalViewController: OWSViewController, UITextViewDelegat
|
|||
|
||||
private func createRecipientRow() -> UIView {
|
||||
let recipientRow = UIView.container()
|
||||
recipientRow.backgroundColor = Theme.toolbarBackgroundColor
|
||||
recipientRow.backgroundColor = UIColor.lokiDarkestGray()
|
||||
|
||||
// Hairline borders should be 1 pixel, not 1 point.
|
||||
let borderThickness = 1.0 / UIScreen.main.scale
|
||||
let borderColor = Theme.middleGrayColor
|
||||
let borderColor = UIColor(white: 0.5, alpha: 1)
|
||||
|
||||
let topBorder = UIView.container()
|
||||
topBorder.backgroundColor = borderColor
|
||||
|
@ -126,12 +127,12 @@ public class MessageApprovalViewController: OWSViewController, UITextViewDelegat
|
|||
let toLabel = UILabel()
|
||||
toLabel.text = NSLocalizedString("MESSAGE_APPROVAL_RECIPIENT_LABEL",
|
||||
comment: "Label for the recipient name in the 'message approval' dialog.")
|
||||
toLabel.textColor = Theme.secondaryColor
|
||||
toLabel.textColor = Colors.separator
|
||||
toLabel.font = font
|
||||
recipientRow.addSubview(toLabel)
|
||||
|
||||
let nameLabel = UILabel()
|
||||
nameLabel.textColor = Theme.primaryColor
|
||||
nameLabel.textColor = Colors.text
|
||||
nameLabel.font = font
|
||||
nameLabel.lineBreakMode = .byTruncatingTail
|
||||
recipientRow.addSubview(nameLabel)
|
||||
|
@ -164,12 +165,12 @@ public class MessageApprovalViewController: OWSViewController, UITextViewDelegat
|
|||
|
||||
let publicKey = contactThread.contactSessionID()
|
||||
nameLabel.text = Storage.shared.getContact(with: publicKey)?.displayName(for: .regular) ?? publicKey
|
||||
nameLabel.textColor = Theme.primaryColor
|
||||
nameLabel.textColor = Colors.text
|
||||
|
||||
if let profileName = self.profileName(contactThread: contactThread) {
|
||||
// If there's a profile name worth showing, add it as a second line below the name.
|
||||
let profileNameLabel = UILabel()
|
||||
profileNameLabel.textColor = Theme.secondaryColor
|
||||
profileNameLabel.textColor = Colors.separator
|
||||
profileNameLabel.font = font
|
||||
profileNameLabel.text = profileName
|
||||
profileNameLabel.lineBreakMode = .byTruncatingTail
|
||||
|
|
|
@ -31,11 +31,9 @@ FOUNDATION_EXPORT const unsigned char SignalUtilitiesKitVersionString[];
|
|||
#import <SignalUtilitiesKit/OWSNavigationController.h>
|
||||
#import <SignalUtilitiesKit/OWSOperation.h>
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage+keyFromIntLong.h>
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage+Loki.h>
|
||||
#import <SignalUtilitiesKit/OWSProfileManager.h>
|
||||
#import <SignalUtilitiesKit/OWSQueues.h>
|
||||
#import <SignalUtilitiesKit/OWSResaveCollectionDBMigration.h>
|
||||
#import <SignalUtilitiesKit/OWSSearchBar.h>
|
||||
#import <SignalUtilitiesKit/OWSTableViewController.h>
|
||||
#import <SignalUtilitiesKit/OWSTextField.h>
|
||||
#import <SignalUtilitiesKit/OWSTextView.h>
|
||||
|
@ -44,12 +42,9 @@ FOUNDATION_EXPORT const unsigned char SignalUtilitiesKitVersionString[];
|
|||
#import <SignalUtilitiesKit/ScreenLockViewController.h>
|
||||
#import <SignalUtilitiesKit/SignalAccount.h>
|
||||
#import <SignalUtilitiesKit/SSKAsserts.h>
|
||||
#import <SignalUtilitiesKit/Theme.h>
|
||||
#import <SignalUtilitiesKit/ThreadUtil.h>
|
||||
#import <SignalUtilitiesKit/ThreadViewHelper.h>
|
||||
#import <SignalUtilitiesKit/TSConstants.h>
|
||||
#import <SignalUtilitiesKit/TSStorageHeaders.h>
|
||||
#import <SignalUtilitiesKit/TSStorageKeys.h>
|
||||
#import <SignalUtilitiesKit/UIFont+OWS.h>
|
||||
#import <SignalUtilitiesKit/UIUtil.h>
|
||||
#import <SignalUtilitiesKit/UIViewController+OWS.h>
|
||||
|
|
|
@ -55,15 +55,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
|
||||
#pragma mark -
|
||||
|
||||
- (void)themeDidChange:(NSNotification *)notification
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
self.navigationBar.barTintColor = [UINavigationBar appearance].barTintColor;
|
||||
self.navigationBar.tintColor = [UINavigationBar appearance].tintColor;
|
||||
self.navigationBar.titleTextAttributes = [UINavigationBar appearance].titleTextAttributes;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
@ -90,11 +81,6 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
OWSNavigationBar *navbar = (OWSNavigationBar *)self.navigationBar;
|
||||
navbar.navBarLayoutDelegate = self;
|
||||
[self updateLayoutForNavbar:navbar];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(themeDidChange:)
|
||||
name:ThemeDidChangeNotification
|
||||
object:nil];
|
||||
}
|
||||
|
||||
// All OWSNavigationController serve as the UINavigationBarDelegate for their navbar.
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
#import "OWSTableViewController.h"
|
||||
#import "OWSNavigationController.h"
|
||||
#import "Theme.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIFont+OWS.h"
|
||||
#import "UIView+OWS.h"
|
||||
|
@ -594,11 +593,6 @@ NSString *const kOWSTableCellIdentifier = @"kOWSTableCellIdentifier";
|
|||
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:kOWSTableCellIdentifier];
|
||||
|
||||
[self applyTheme];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(themeDidChange:)
|
||||
name:ThemeDidChangeNotification
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
|
@ -816,23 +810,13 @@ NSString *const kOWSTableCellIdentifier = @"kOWSTableCellIdentifier";
|
|||
[self.delegate tableViewWillBeginDragging];
|
||||
}
|
||||
|
||||
#pragma mark - Theme
|
||||
|
||||
- (void)themeDidChange:(NSNotification *)notification
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self applyTheme];
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
- (void)applyTheme
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
self.view.backgroundColor = Theme.backgroundColor;
|
||||
self.tableView.backgroundColor = Theme.backgroundColor;
|
||||
self.tableView.separatorColor = Theme.cellSeparatorColor;
|
||||
self.view.backgroundColor = LKColors.navigationBarBackground;
|
||||
self.tableView.backgroundColor = LKColors.navigationBarBackground;
|
||||
self.tableView.separatorColor = LKColors.separator;
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
#import "OWSViewController.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalUtilitiesKit/Theme.h>
|
||||
#import <SessionUIKit/SessionUIKit.h>
|
||||
#import "AppContext.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
@ -87,7 +87,7 @@ UIInterfaceOrientationMask DefaultUIInterfaceOrientationMask(void)
|
|||
[super viewDidLoad];
|
||||
|
||||
if (self.shouldUseTheme) {
|
||||
self.view.backgroundColor = Theme.backgroundColor;
|
||||
self.view.backgroundColor = [LKColors navigationBarBackground];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import SessionUIKit
|
||||
|
||||
@objc(OWSSheetViewControllerDelegate)
|
||||
public protocol SheetViewControllerDelegate: class {
|
||||
|
@ -55,7 +56,7 @@ public class SheetViewController: UIViewController {
|
|||
sheetView.setCompressionResistanceHigh()
|
||||
self.sheetViewVerticalConstraint = sheetView.autoPinEdge(.top, to: .bottom, of: self.view)
|
||||
|
||||
handleView.backgroundColor = Theme.isDarkThemeEnabled ? UIColor.ows_white : UIColor.ows_gray05
|
||||
handleView.backgroundColor = isDarkMode ? UIColor.ows_white : UIColor.ows_gray05
|
||||
let kHandleViewHeight: CGFloat = 5
|
||||
handleView.autoSetDimensions(to: CGSize(width: 40, height: kHandleViewHeight))
|
||||
handleView.layer.cornerRadius = kHandleViewHeight / 2
|
||||
|
@ -82,7 +83,7 @@ public class SheetViewController: UIViewController {
|
|||
|
||||
let backgroundDuration: TimeInterval = 0.1
|
||||
UIView.animate(withDuration: backgroundDuration) {
|
||||
let alpha: CGFloat = Theme.isDarkThemeEnabled ? 0.7 : 0.6
|
||||
let alpha: CGFloat = isDarkMode ? 0.7 : 0.6
|
||||
self.view.backgroundColor = UIColor.black.withAlphaComponent(alpha)
|
||||
}
|
||||
|
||||
|
@ -199,8 +200,7 @@ private class SheetView: UIView {
|
|||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
self.backgroundColor = Theme.isDarkThemeEnabled ? UIColor.ows_gray90
|
||||
: UIColor.ows_gray05
|
||||
self.backgroundColor = isDarkMode ? UIColor.ows_gray90 : UIColor.ows_gray05
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
|
|
|
@ -50,10 +50,6 @@ public class OWSNavigationBar: UINavigationBar {
|
|||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(callDidChange), name: .OWSWindowManagerCallDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(didChangeStatusBarFrame), name: UIApplication.didChangeStatusBarFrameNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(themeDidChange),
|
||||
name: .ThemeDidChange,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
// MARK: FirstResponder Stubbing
|
||||
|
@ -82,13 +78,13 @@ public class OWSNavigationBar: UINavigationBar {
|
|||
|
||||
if UIAccessibility.isReduceTransparencyEnabled {
|
||||
blurEffectView?.isHidden = true
|
||||
let color = Theme.navbarBackgroundColor
|
||||
let color = UIColor.lokiDarkestGray()
|
||||
let backgroundImage = UIImage(color: color)
|
||||
self.setBackgroundImage(backgroundImage, for: .default)
|
||||
} else {
|
||||
// Make navbar more translucent than default. Navbars remove alpha from any assigned backgroundColor, so
|
||||
// to achieve transparency, we have to assign a transparent image.
|
||||
let color = Theme.navbarBackgroundColor
|
||||
let color = UIColor.lokiDarkestGray()
|
||||
let backgroundImage = UIImage(color: color)
|
||||
self.setBackgroundImage(backgroundImage, for: .default)
|
||||
|
||||
|
@ -97,16 +93,10 @@ public class OWSNavigationBar: UINavigationBar {
|
|||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
public func themeDidChange() {
|
||||
Logger.debug("")
|
||||
applyTheme()
|
||||
}
|
||||
|
||||
@objc
|
||||
public var respectsTheme: Bool = true {
|
||||
didSet {
|
||||
themeDidChange()
|
||||
applyTheme()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -191,9 +181,9 @@ public class OWSNavigationBar: UINavigationBar {
|
|||
respectsTheme = false
|
||||
|
||||
barStyle = .black
|
||||
titleTextAttributes = [NSAttributedString.Key.foregroundColor: Theme.darkThemePrimaryColor]
|
||||
barTintColor = Theme.darkThemeBackgroundColor.withAlphaComponent(0.6)
|
||||
tintColor = Theme.darkThemePrimaryColor
|
||||
titleTextAttributes = [NSAttributedString.Key.foregroundColor: Colors.text]
|
||||
barTintColor = Colors.navigationBarBackground.withAlphaComponent(0.6)
|
||||
tintColor = Colors.text
|
||||
|
||||
switch type {
|
||||
case .clear:
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSSearchBar : UISearchBar
|
||||
|
||||
+ (void)applyThemeToSearchBar:(UISearchBar *)searchBar;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,118 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "OWSSearchBar.h"
|
||||
#import "Theme.h"
|
||||
#import "UIView+OWS.h"
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
#import <SessionUIKit/SessionUIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@implementation OWSSearchBar
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
if (self = [super init]) {
|
||||
[self ows_configure];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
if (self = [super initWithFrame:frame]) {
|
||||
[self ows_configure];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
|
||||
{
|
||||
if (self = [super initWithCoder:aDecoder]) {
|
||||
[self ows_configure];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)ows_configure
|
||||
{
|
||||
[self ows_applyTheme];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(themeDidChange:)
|
||||
name:ThemeDidChangeNotification
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)ows_applyTheme
|
||||
{
|
||||
[self.class applyThemeToSearchBar:self];
|
||||
}
|
||||
|
||||
+ (void)applyThemeToSearchBar:(UISearchBar *)searchBar
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
UIColor *foregroundColor = UIColor.lokiLightestGray;
|
||||
searchBar.barTintColor = Theme.backgroundColor;
|
||||
searchBar.barStyle = Theme.barStyle;
|
||||
searchBar.tintColor = UIColor.lokiGreen;
|
||||
|
||||
// Hide searchBar border.
|
||||
// Alternatively we could hide the border by using `UISearchBarStyleMinimal`, but that causes an issue when toggling
|
||||
// from light -> dark -> light theme wherein the textField background color appears darker than it should
|
||||
// (regardless of our re-setting textfield.backgroundColor below).
|
||||
searchBar.backgroundImage = [UIImage new];
|
||||
|
||||
if (Theme.isDarkThemeEnabled) {
|
||||
UIImage *clearImage = [UIImage imageNamed:@"searchbar_clear"];
|
||||
[searchBar setImage:[clearImage asTintedImageWithColor:foregroundColor]
|
||||
forSearchBarIcon:UISearchBarIconClear
|
||||
state:UIControlStateNormal];
|
||||
|
||||
UIImage *searchImage = [UIImage imageNamed:@"searchbar_search"];
|
||||
[searchBar setImage:[searchImage asTintedImageWithColor:foregroundColor]
|
||||
forSearchBarIcon:UISearchBarIconSearch
|
||||
state:UIControlStateNormal];
|
||||
} else {
|
||||
[searchBar setImage:nil forSearchBarIcon:UISearchBarIconClear state:UIControlStateNormal];
|
||||
|
||||
[searchBar setImage:nil forSearchBarIcon:UISearchBarIconSearch state:UIControlStateNormal];
|
||||
}
|
||||
|
||||
[searchBar traverseViewHierarchyWithVisitor:^(UIView *view) {
|
||||
if ([view isKindOfClass:[UITextField class]]) {
|
||||
UITextField *textField = (UITextField *)view;
|
||||
textField.backgroundColor = Theme.searchFieldBackgroundColor;
|
||||
textField.textColor = Theme.primaryColor;
|
||||
NSString *placeholder = textField.placeholder;
|
||||
if (placeholder != nil) {
|
||||
NSMutableAttributedString *attributedPlaceholder = [[NSMutableAttributedString alloc] initWithString:placeholder];
|
||||
[attributedPlaceholder addAttribute:NSForegroundColorAttributeName value:foregroundColor range:NSMakeRange(0, placeholder.length)];
|
||||
textField.attributedPlaceholder = attributedPlaceholder;
|
||||
}
|
||||
textField.keyboardAppearance = LKAppModeUtilities.isLightMode ? UIKeyboardAppearanceDefault : UIKeyboardAppearanceDark;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)themeDidChange:(NSNotification *)notification
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
[self ows_applyTheme];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -3,7 +3,6 @@
|
|||
//
|
||||
|
||||
#import "OWSTextField.h"
|
||||
#import "Theme.h"
|
||||
|
||||
#import <SessionUIKit/SessionUIKit.h>
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
//
|
||||
|
||||
#import "OWSTextView.h"
|
||||
#import "Theme.h"
|
||||
|
||||
#import <SessionUIKit/SessionUIKit.h>
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import SessionUIKit
|
||||
|
||||
@objc
|
||||
public class ToastController: NSObject, ToastViewDelegate {
|
||||
|
@ -116,11 +117,11 @@ class ToastView: UIView {
|
|||
super.init(frame: frame)
|
||||
|
||||
self.layer.cornerRadius = 4
|
||||
self.backgroundColor = Theme.toastBackgroundColor
|
||||
self.backgroundColor = (isDarkMode ? UIColor.ows_gray75 : UIColor.ows_gray60)
|
||||
self.layoutMargins = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
|
||||
|
||||
label.textAlignment = .center
|
||||
label.textColor = Theme.toastForegroundColor
|
||||
label.textColor = (isDarkMode ? UIColor.ows_white : UIColor.ows_white)
|
||||
label.font = UIFont.ows_dynamicTypeBody
|
||||
label.numberOfLines = 0
|
||||
self.addSubview(label)
|
||||
|
|
|
@ -114,10 +114,10 @@ const CGFloat kContactCellAvatarTextMargin = 12;
|
|||
self.subtitleLabel.font = [UIFont ows_regularFontWithSize:11.f];
|
||||
self.accessoryLabel.font = [UIFont ows_mediumFontWithSize:13.f];
|
||||
|
||||
self.nameLabel.textColor = [Theme primaryColor];
|
||||
self.profileNameLabel.textColor = [Theme secondaryColor];
|
||||
self.subtitleLabel.textColor = [Theme secondaryColor];
|
||||
self.accessoryLabel.textColor = Theme.middleGrayColor;
|
||||
self.nameLabel.textColor = LKColors.text;
|
||||
self.profileNameLabel.textColor = LKColors.separator;
|
||||
self.subtitleLabel.textColor = LKColors.separator;
|
||||
self.accessoryLabel.textColor = [UIColor colorWithWhite:0.5f alpha:1.f];
|
||||
}
|
||||
|
||||
- (void)configureWithRecipientId:(NSString *)recipientId
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
#import <SessionMessagingKit/OWSPrimaryStorage.h>
|
||||
#import <Curve25519Kit/Ed25519.h>
|
||||
#import <YapDatabase/YapDatabase.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OWSPrimaryStorage (Loki)
|
||||
|
||||
- (void)updateMessageIDCollectionByPruningMessagesWithIDs:(NSSet<NSString *> *)targetMessageIDs in:(YapDatabaseReadWriteTransaction *)transaction NS_SWIFT_NAME(updateMessageIDCollectionByPruningMessagesWithIDs(_:in:));
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,26 +0,0 @@
|
|||
#import "OWSPrimaryStorage+Loki.h"
|
||||
#import "OWSPrimaryStorage+keyFromIntLong.h"
|
||||
#import "OWSIdentityManager.h"
|
||||
#import "NSDate+OWS.h"
|
||||
#import "TSAccountManager.h"
|
||||
#import "YapDatabaseConnection+OWS.h"
|
||||
#import "YapDatabaseTransaction+OWS.h"
|
||||
#import "NSObject+Casting.h"
|
||||
#import <SignalUtilitiesKit/SignalUtilitiesKit-Swift.h>
|
||||
|
||||
#define LKMessageIDCollection @"LKMessageIDCollection"
|
||||
|
||||
@implementation OWSPrimaryStorage (Loki)
|
||||
|
||||
- (void)updateMessageIDCollectionByPruningMessagesWithIDs:(NSSet<NSString *> *)targetMessageIDs in:(YapDatabaseReadWriteTransaction *)transaction {
|
||||
NSMutableArray<NSString *> *serverIDs = [NSMutableArray new];
|
||||
[transaction enumerateRowsInCollection:LKMessageIDCollection usingBlock:^(NSString *key, id object, id metadata, BOOL *stop) {
|
||||
if (![object isKindOfClass:NSString.class]) { return; }
|
||||
NSString *messageID = (NSString *)object;
|
||||
if (![targetMessageIDs containsObject:messageID]) { return; }
|
||||
[serverIDs addObject:key];
|
||||
}];
|
||||
[transaction removeObjectsForKeys:serverIDs inCollection:LKMessageIDCollection];
|
||||
}
|
||||
|
||||
@end
|
|
@ -6,9 +6,6 @@
|
|||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSString *const kNSNotificationName_ProfileWhitelistDidChange;
|
||||
extern NSString *const kNSNotificationName_ProfileKeyDidChange;
|
||||
|
||||
extern const NSUInteger kOWSProfileManager_NameDataLength;
|
||||
extern const NSUInteger kOWSProfileManager_MaxAvatarDiameter;
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
#import <SessionUtilitiesKit/NSNotificationCenter+OWS.h>
|
||||
#import <SessionUtilitiesKit/NSString+SSK.h>
|
||||
#import <SessionUtilitiesKit/OWSFileSystem.h>
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage+Loki.h>
|
||||
#import <SessionMessagingKit/SSKEnvironment.h>
|
||||
#import <SessionMessagingKit/TSAccountManager.h>
|
||||
#import <SessionMessagingKit/TSGroupThread.h>
|
||||
|
@ -26,13 +25,6 @@
|
|||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
NSString *const kNSNotificationName_ProfileWhitelistDidChange = @"kNSNotificationName_ProfileWhitelistDidChange";
|
||||
|
||||
NSString *const kOWSProfileManager_UserWhitelistCollection = @"kOWSProfileManager_UserWhitelistCollection";
|
||||
NSString *const kOWSProfileManager_GroupWhitelistCollection = @"kOWSProfileManager_GroupWhitelistCollection";
|
||||
|
||||
NSString *const kNSNotificationName_ProfileKeyDidChange = @"kNSNotificationName_ProfileKeyDidChange";
|
||||
|
||||
// The max bytes for a user's profile name, encoded in UTF8.
|
||||
// Before encrypting and submitting we NULL pad the name data to this length.
|
||||
const NSUInteger kOWSProfileManager_NameDataLength = 26;
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import <SignalUtilitiesKit/UIColor+OWS.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSString *const ThemeDidChangeNotification;
|
||||
|
||||
@interface Theme : NSObject
|
||||
|
||||
- (instancetype)init NS_UNAVAILABLE;
|
||||
|
||||
@property (class, readonly, atomic) BOOL isDarkThemeEnabled;
|
||||
|
||||
+ (void)setIsDarkThemeEnabled:(BOOL)value;
|
||||
|
||||
@property (class, readonly, nonatomic) UIColor *backgroundColor;
|
||||
@property (class, readonly, nonatomic) UIColor *primaryColor;
|
||||
@property (class, readonly, nonatomic) UIColor *secondaryColor;
|
||||
@property (class, readonly, nonatomic) UIColor *boldColor;
|
||||
@property (class, readonly, nonatomic) UIColor *offBackgroundColor;
|
||||
@property (class, readonly, nonatomic) UIColor *middleGrayColor;
|
||||
@property (class, readonly, nonatomic) UIColor *placeholderColor;
|
||||
@property (class, readonly, nonatomic) UIColor *hairlineColor;
|
||||
|
||||
#pragma mark - Global App Colors
|
||||
|
||||
@property (class, readonly, nonatomic) UIColor *navbarBackgroundColor;
|
||||
@property (class, readonly, nonatomic) UIColor *navbarIconColor;
|
||||
@property (class, readonly, nonatomic) UIColor *navbarTitleColor;
|
||||
|
||||
@property (class, readonly, nonatomic) UIColor *toolbarBackgroundColor;
|
||||
|
||||
@property (class, readonly, nonatomic) UIColor *conversationButtonBackgroundColor;
|
||||
|
||||
@property (class, readonly, nonatomic) UIColor *cellSelectedColor;
|
||||
@property (class, readonly, nonatomic) UIColor *cellSeparatorColor;
|
||||
|
||||
// In some contexts, e.g. media viewing/sending, we always use "dark theme" UI regardless of the
|
||||
// users chosen theme.
|
||||
@property (class, readonly, nonatomic) UIColor *darkThemeNavbarIconColor;
|
||||
@property (class, readonly, nonatomic) UIColor *darkThemeNavbarBackgroundColor;
|
||||
@property (class, readonly, nonatomic) UIColor *darkThemeBackgroundColor;
|
||||
@property (class, readonly, nonatomic) UIColor *darkThemePrimaryColor;
|
||||
@property (class, readonly, nonatomic) UIBlurEffect *darkThemeBarBlurEffect;
|
||||
@property (class, readonly, nonatomic) UIColor *galleryHighlightColor;
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@property (class, readonly, nonatomic) UIBarStyle barStyle;
|
||||
@property (class, readonly, nonatomic) UIColor *searchFieldBackgroundColor;
|
||||
@property (class, readonly, nonatomic) UIBlurEffect *barBlurEffect;
|
||||
@property (class, readonly, nonatomic) UIKeyboardAppearance keyboardAppearance;
|
||||
@property (class, readonly, nonatomic) UIKeyboardAppearance darkThemeKeyboardAppearance;
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@property (class, readonly, nonatomic) UIColor *toastForegroundColor;
|
||||
@property (class, readonly, nonatomic) UIColor *toastBackgroundColor;
|
||||
|
||||
@property (class, readonly, nonatomic) UIColor *scrollButtonBackgroundColor;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -1,217 +0,0 @@
|
|||
//
|
||||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
#import "Theme.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIUtil.h"
|
||||
#import <SessionUtilitiesKit/NSNotificationCenter+OWS.h>
|
||||
#import <SessionMessagingKit/OWSPrimaryStorage.h>
|
||||
#import <SessionMessagingKit/YapDatabaseConnection+OWS.h>
|
||||
|
||||
#import <SessionUIKit/SessionUIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
NSString *const ThemeDidChangeNotification = @"ThemeDidChangeNotification";
|
||||
|
||||
NSString *const ThemeCollection = @"ThemeCollection";
|
||||
NSString *const ThemeKeyThemeEnabled = @"ThemeKeyThemeEnabled";
|
||||
|
||||
|
||||
@interface Theme ()
|
||||
|
||||
@property (nonatomic) NSNumber *isDarkThemeEnabledNumber;
|
||||
|
||||
@end
|
||||
|
||||
@implementation Theme
|
||||
|
||||
+ (instancetype)sharedInstance
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
static Theme *instance;
|
||||
dispatch_once(&onceToken, ^{
|
||||
instance = [Theme new];
|
||||
});
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
+ (BOOL)isDarkThemeEnabled
|
||||
{
|
||||
return LKAppModeUtilities.isDarkMode;
|
||||
}
|
||||
|
||||
- (BOOL)isDarkThemeEnabled
|
||||
{
|
||||
OWSAssertIsOnMainThread();
|
||||
|
||||
return LKAppModeUtilities.isDarkMode;
|
||||
}
|
||||
|
||||
+ (void)setIsDarkThemeEnabled:(BOOL)value
|
||||
{
|
||||
return [self.sharedInstance setIsDarkThemeEnabled:value];
|
||||
}
|
||||
|
||||
- (void)setIsDarkThemeEnabled:(BOOL)value
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
+ (UIColor *)backgroundColor
|
||||
{
|
||||
return LKColors.navigationBarBackground;
|
||||
}
|
||||
|
||||
+ (UIColor *)offBackgroundColor
|
||||
{
|
||||
return LKColors.unimportant;
|
||||
}
|
||||
|
||||
+ (UIColor *)primaryColor
|
||||
{
|
||||
return LKColors.text;
|
||||
}
|
||||
|
||||
+ (UIColor *)secondaryColor
|
||||
{
|
||||
return LKColors.separator;
|
||||
}
|
||||
|
||||
+ (UIColor *)boldColor
|
||||
{
|
||||
return (Theme.isDarkThemeEnabled ? UIColor.ows_whiteColor : UIColor.blackColor);
|
||||
}
|
||||
|
||||
+ (UIColor *)middleGrayColor
|
||||
{
|
||||
return [UIColor colorWithWhite:0.5f alpha:1.f];
|
||||
}
|
||||
|
||||
+ (UIColor *)placeholderColor
|
||||
{
|
||||
return LKColors.navigationBarBackground;
|
||||
}
|
||||
|
||||
+ (UIColor *)hairlineColor
|
||||
{
|
||||
return LKColors.separator;
|
||||
}
|
||||
|
||||
#pragma mark - Global App Colors
|
||||
|
||||
+ (UIColor *)navbarBackgroundColor
|
||||
{
|
||||
return UIColor.lokiDarkestGray;
|
||||
}
|
||||
|
||||
+ (UIColor *)darkThemeNavbarBackgroundColor
|
||||
{
|
||||
return UIColor.ows_blackColor;
|
||||
}
|
||||
|
||||
+ (UIColor *)navbarIconColor
|
||||
{
|
||||
return UIColor.lokiGreen;
|
||||
}
|
||||
|
||||
+ (UIColor *)darkThemeNavbarIconColor;
|
||||
{
|
||||
return LKColors.text;
|
||||
}
|
||||
|
||||
+ (UIColor *)navbarTitleColor
|
||||
{
|
||||
return Theme.primaryColor;
|
||||
}
|
||||
|
||||
+ (UIColor *)toolbarBackgroundColor
|
||||
{
|
||||
return self.navbarBackgroundColor;
|
||||
}
|
||||
|
||||
+ (UIColor *)cellSelectedColor
|
||||
{
|
||||
return UIColor.lokiDarkGray;
|
||||
}
|
||||
|
||||
+ (UIColor *)cellSeparatorColor
|
||||
{
|
||||
return Theme.hairlineColor;
|
||||
}
|
||||
|
||||
+ (UIColor *)darkThemeBackgroundColor
|
||||
{
|
||||
return LKColors.navigationBarBackground;
|
||||
}
|
||||
|
||||
+ (UIColor *)darkThemePrimaryColor
|
||||
{
|
||||
return LKColors.text;
|
||||
}
|
||||
|
||||
+ (UIColor *)galleryHighlightColor
|
||||
{
|
||||
return UIColor.lokiGreen;
|
||||
}
|
||||
|
||||
+ (UIColor *)conversationButtonBackgroundColor
|
||||
{
|
||||
return (Theme.isDarkThemeEnabled ? [UIColor colorWithWhite:0.35f alpha:1.f] : UIColor.ows_gray02Color);
|
||||
}
|
||||
|
||||
+ (UIBlurEffect *)barBlurEffect
|
||||
{
|
||||
return Theme.isDarkThemeEnabled ? self.darkThemeBarBlurEffect
|
||||
: [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
|
||||
}
|
||||
|
||||
+ (UIBlurEffect *)darkThemeBarBlurEffect
|
||||
{
|
||||
return [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark];
|
||||
}
|
||||
|
||||
+ (UIKeyboardAppearance)keyboardAppearance
|
||||
{
|
||||
return LKAppModeUtilities.isLightMode ? UIKeyboardAppearanceDefault : UIKeyboardAppearanceDark;
|
||||
}
|
||||
|
||||
+ (UIKeyboardAppearance)darkThemeKeyboardAppearance;
|
||||
{
|
||||
return UIKeyboardAppearanceDark;
|
||||
}
|
||||
|
||||
#pragma mark - Search Bar
|
||||
|
||||
+ (UIBarStyle)barStyle
|
||||
{
|
||||
return Theme.isDarkThemeEnabled ? UIBarStyleBlack : UIBarStyleDefault;
|
||||
}
|
||||
|
||||
+ (UIColor *)searchFieldBackgroundColor
|
||||
{
|
||||
return Theme.isDarkThemeEnabled ? Theme.offBackgroundColor : UIColor.ows_gray05Color;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
+ (UIColor *)toastForegroundColor
|
||||
{
|
||||
return (Theme.isDarkThemeEnabled ? UIColor.ows_whiteColor : UIColor.ows_whiteColor);
|
||||
}
|
||||
|
||||
+ (UIColor *)toastBackgroundColor
|
||||
{
|
||||
return (Theme.isDarkThemeEnabled ? UIColor.ows_gray75Color : UIColor.ows_gray60Color);
|
||||
}
|
||||
|
||||
+ (UIColor *)scrollButtonBackgroundColor
|
||||
{
|
||||
return UIColor.lokiDarkerGray;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -3,7 +3,6 @@
|
|||
//
|
||||
|
||||
#import "UIUtil.h"
|
||||
#import "Theme.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import <SessionUtilitiesKit/AppContext.h>
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "Theme.h"
|
||||
#import "UIColor+OWS.h"
|
||||
#import "UIUtil.h"
|
||||
#import "UIView+OWS.h"
|
||||
|
@ -96,7 +95,7 @@ NS_ASSUME_NONNULL_BEGIN
|
|||
imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
OWSAssertDebug(backImage);
|
||||
[backButton setImage:backImage forState:UIControlStateNormal];
|
||||
backButton.tintColor = Theme.navbarIconColor;
|
||||
backButton.tintColor = UIColor.lokiGreen;
|
||||
|
||||
backButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
#import <SessionUtilitiesKit/AppContext.h>
|
||||
#import <SignalUtilitiesKit/AppVersion.h>
|
||||
#import <SessionUtilitiesKit/NSUserDefaults+OWS.h>
|
||||
#import <SignalUtilitiesKit/OWSPrimaryStorage+Loki.h>
|
||||
#import <SessionMessagingKit/TSAccountManager.h>
|
||||
#import <SessionMessagingKit/TSThread.h>
|
||||
#import <SessionMessagingKit/TSGroupThread.h>
|
||||
|
|
Loading…
Reference in a new issue