From bf5e3137dc97fb82bb8eefc02d578bf2ac9a71c0 Mon Sep 17 00:00:00 2001 From: fanyu Date: Tue, 14 Mar 2023 11:18:21 +0800 Subject: [PATCH 1/5] Add strings and icons --- .../ic_repair_database.imageset/Contents.json | 22 ++++++++++++++++++ .../ic_repair_database@2x.png | Bin 0 -> 4987 bytes .../ic_repair_database@3x.png | Bin 0 -> 9183 bytes Mixin/Resources/en.lproj/Localizable.strings | 5 ++++ Mixin/Resources/ja.lproj/Localizable.strings | 5 ++++ Mixin/Resources/ru.lproj/Localizable.strings | 5 ++++ .../zh-Hans.lproj/Localizable.strings | 5 ++++ .../zh-Hant.lproj/Localizable.strings | 5 ++++ 8 files changed, 47 insertions(+) create mode 100644 Mixin/Assets.xcassets/ic_repair_database.imageset/Contents.json create mode 100644 Mixin/Assets.xcassets/ic_repair_database.imageset/ic_repair_database@2x.png create mode 100644 Mixin/Assets.xcassets/ic_repair_database.imageset/ic_repair_database@3x.png diff --git a/Mixin/Assets.xcassets/ic_repair_database.imageset/Contents.json b/Mixin/Assets.xcassets/ic_repair_database.imageset/Contents.json new file mode 100644 index 0000000000..4a0113883a --- /dev/null +++ b/Mixin/Assets.xcassets/ic_repair_database.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ic_repair_database@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_repair_database@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Mixin/Assets.xcassets/ic_repair_database.imageset/ic_repair_database@2x.png b/Mixin/Assets.xcassets/ic_repair_database.imageset/ic_repair_database@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..1cbfba295de34d73371754c3f3c23cc1e39b74a4 GIT binary patch literal 4987 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 J)+k!R{}1aNef9tV literal 0 HcmV?d00001 diff --git a/Mixin/Resources/en.lproj/Localizable.strings b/Mixin/Resources/en.lproj/Localizable.strings index 0d50370ab8..e2ace46793 100644 --- a/Mixin/Resources/en.lproj/Localizable.strings +++ b/Mixin/Resources/en.lproj/Localizable.strings @@ -680,6 +680,11 @@ "remove_emergency_contact_for_sure" = "Remove emergency contact for sure?"; "remove_from_group" = "Remove from Group"; "remove_stickers" = "Remove Stickers"; +"repair" = "Repair"; +"repair_chat_history" = "Repair Chat History"; +"repair_chat_history_hint" = "The database file is found to be damaged, some messages may be lost, please try to repair it."; +"repair_chat_history_success" = "The chat history has been repaired successfully! Chat records before %@ have been restored."; +"repairing" = "Repairing"; "reply" = "Reply"; "report" = "Report"; "report_and_block" = "Report and block?"; diff --git a/Mixin/Resources/ja.lproj/Localizable.strings b/Mixin/Resources/ja.lproj/Localizable.strings index ef37cd4ccb..ee0de9dba9 100644 --- a/Mixin/Resources/ja.lproj/Localizable.strings +++ b/Mixin/Resources/ja.lproj/Localizable.strings @@ -680,6 +680,11 @@ "remove_emergency_contact_for_sure" = "本当に緊急連絡先を削除しますか?"; "remove_from_group" = "グループから退会させる"; "remove_stickers" = "スタンプの削除"; +"repair" = "Repair"; +"repair_chat_history" = "Repair Chat History"; +"repair_chat_history_hint" = "The database file is found to be damaged, some messages may be lost, please try to repair it."; +"repair_chat_history_success" = "The chat history has been repaired successfully! Chat records before %@ have been restored."; +"repairing" = "Repairing"; "reply" = "返信"; "report" = "報告"; "report_and_block" = "報告してブロックしますか?"; diff --git a/Mixin/Resources/ru.lproj/Localizable.strings b/Mixin/Resources/ru.lproj/Localizable.strings index d8de88cbeb..7eb08f3e96 100644 --- a/Mixin/Resources/ru.lproj/Localizable.strings +++ b/Mixin/Resources/ru.lproj/Localizable.strings @@ -680,6 +680,11 @@ "remove_emergency_contact_for_sure" = "Точно удалить экстренный контакт?"; "remove_from_group" = "Удалить из группы"; "remove_stickers" = "Удалить наклейки"; +"repair" = "Repair"; +"repair_chat_history" = "Repair Chat History"; +"repair_chat_history_hint" = "The database file is found to be damaged, some messages may be lost, please try to repair it."; +"repair_chat_history_success" = "The chat history has been repaired successfully! Chat records before %@ have been restored."; +"repairing" = "Repairing"; "reply" = "Ответить"; "report" = "Отчет"; "report_and_block" = "Пожаловаться и заблокировать?"; diff --git a/Mixin/Resources/zh-Hans.lproj/Localizable.strings b/Mixin/Resources/zh-Hans.lproj/Localizable.strings index dc254f9fbd..929705b756 100644 --- a/Mixin/Resources/zh-Hans.lproj/Localizable.strings +++ b/Mixin/Resources/zh-Hans.lproj/Localizable.strings @@ -680,6 +680,11 @@ "remove_emergency_contact_for_sure" = "确定删除紧急联系人?"; "remove_from_group" = "从群组中移除"; "remove_stickers" = "移除所有表情"; +"repair" = "修复"; +"repair_chat_history" = "修复聊天记录"; +"repair_chat_history_hint" = "数据库文件被发现损坏,可能会丢失一些信息,请尝试修复它。"; +"repair_chat_history_success" = "聊天记录修复成功!%@ 之前的聊天记录已经恢复。"; +"repairing" = "修复中"; "reply" = "回复"; "report" = "举报"; "report_and_block" = "举报并屏蔽?"; diff --git a/Mixin/Resources/zh-Hant.lproj/Localizable.strings b/Mixin/Resources/zh-Hant.lproj/Localizable.strings index f38f39490b..b635001b6d 100644 --- a/Mixin/Resources/zh-Hant.lproj/Localizable.strings +++ b/Mixin/Resources/zh-Hant.lproj/Localizable.strings @@ -680,6 +680,11 @@ "remove_emergency_contact_for_sure" = "確定刪除緊急聯絡人?"; "remove_from_group" = "從群組中移除"; "remove_stickers" = "移除所有表情"; +"repair" = "修復"; +"repair_chat_history" = "修復聊天記錄"; +"repair_chat_history_hint" = "資料庫檔案被發現損壞,可能會丟失一些資訊,請嘗試修復它。"; +"repair_chat_history_success" = "聊天記錄修復成功!%@ 之前的聊天記錄已經恢復。"; +"repairing" = "修复中"; "reply" = "回覆"; "report" = "舉報"; "report_and_block" = "舉報並封鎖?"; From 8e644993b3767987d6cf4fabf0a555f396ad7dac Mon Sep 17 00:00:00 2001 From: fanyu Date: Wed, 15 Mar 2023 10:53:59 +0800 Subject: [PATCH 2/5] Back up and repair database --- Mixin.xcodeproj/project.pbxproj | 16 ++ Mixin/AppDelegate.swift | 6 +- Mixin/Service/Job/DatabaseBackupJob.swift | 31 ++++ .../Common/InitialViewControllerFactory.swift | 4 +- .../Home/DatabaseRepairViewController.swift | 36 ++++ .../Controllers/Home/HomeViewController.swift | 1 + .../Home/Model/DatabaseBackupManager.swift | 23 +++ .../Controllers/Home/Model/DatabaseFile.swift | 59 +++++++ .../UserInterface/Storyboard/Home.storyboard | 156 ++++++++++++++---- .../MixinServices/Database/Database.swift | 9 + .../Database/User/UserDatabase.swift | 7 +- .../AppGroupUserDefaults+User.swift | 14 ++ 12 files changed, 330 insertions(+), 32 deletions(-) create mode 100644 Mixin/Service/Job/DatabaseBackupJob.swift create mode 100644 Mixin/UserInterface/Controllers/Home/DatabaseRepairViewController.swift create mode 100644 Mixin/UserInterface/Controllers/Home/Model/DatabaseBackupManager.swift create mode 100644 Mixin/UserInterface/Controllers/Home/Model/DatabaseFile.swift diff --git a/Mixin.xcodeproj/project.pbxproj b/Mixin.xcodeproj/project.pbxproj index 2ae9acf1a3..47b614eb80 100644 --- a/Mixin.xcodeproj/project.pbxproj +++ b/Mixin.xcodeproj/project.pbxproj @@ -595,6 +595,7 @@ 7C5DFE34284F3EA3008733FC /* UserCenterTableHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C5DFE33284F3EA3008733FC /* UserCenterTableHeaderView.swift */; }; 7C6132B627953B15002777EE /* DeleteAccountAbortWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C6132B527953B15002777EE /* DeleteAccountAbortWindow.swift */; }; 7C6132B827953B4F002777EE /* DeleteAccountAbortWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7C6132B727953B4F002777EE /* DeleteAccountAbortWindow.xib */; }; + 7C62922229B9699000B3596C /* DatabaseBackupJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C62922129B9699000B3596C /* DatabaseBackupJob.swift */; }; 7C66E7B82743988500FF24C1 /* ProfileDescriptionLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C66E7B72743988500FF24C1 /* ProfileDescriptionLabel.swift */; }; 7C66F0272689D1FE006D8462 /* HomeAppsDragInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C66F0262689D1FE006D8462 /* HomeAppsDragInteraction.swift */; }; 7C66F029268A0384006D8462 /* AppPageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C66F028268A0384006D8462 /* AppPageCell.swift */; }; @@ -636,6 +637,9 @@ 7CCC801C292DC68E000B4200 /* ImageCropViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CCC801A292DC68E000B4200 /* ImageCropViewController.swift */; }; 7CCE65A828D69D1D00FE944A /* TransferActionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CCE65A628D69D1D00FE944A /* TransferActionView.swift */; }; 7CCE65A928D69D1D00FE944A /* TransferActionView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7CCE65A728D69D1D00FE944A /* TransferActionView.xib */; }; + 7CD9C17229B9BE94008F8D85 /* DatabaseBackupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CD9C17129B9BE94008F8D85 /* DatabaseBackupManager.swift */; }; + 7CD9C17429B9DDE0008F8D85 /* DatabaseRepairViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CD9C17329B9DDE0008F8D85 /* DatabaseRepairViewController.swift */; }; + 7CD9C17829BB306F008F8D85 /* DatabaseFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CD9C17729BB306F008F8D85 /* DatabaseFile.swift */; }; 7CDBA58A28F64E5000AC3777 /* WalletTransferSearchResultsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CDBA58928F64E5000AC3777 /* WalletTransferSearchResultsViewController.swift */; }; 7CDBA58E28F7B6CB00AC3777 /* TransferSearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CDBA58D28F7B6CB00AC3777 /* TransferSearchViewController.swift */; }; 7CDF316C29890FB200421808 /* ConversationFontSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7CDF316B29890FB200421808 /* ConversationFontSet.swift */; }; @@ -1622,6 +1626,7 @@ 7C5DFE33284F3EA3008733FC /* UserCenterTableHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserCenterTableHeaderView.swift; sourceTree = ""; }; 7C6132B527953B15002777EE /* DeleteAccountAbortWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAccountAbortWindow.swift; sourceTree = ""; }; 7C6132B727953B4F002777EE /* DeleteAccountAbortWindow.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = DeleteAccountAbortWindow.xib; sourceTree = ""; }; + 7C62922129B9699000B3596C /* DatabaseBackupJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseBackupJob.swift; sourceTree = ""; }; 7C66E7B72743988500FF24C1 /* ProfileDescriptionLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDescriptionLabel.swift; sourceTree = ""; }; 7C66F0262689D1FE006D8462 /* HomeAppsDragInteraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeAppsDragInteraction.swift; sourceTree = ""; }; 7C66F028268A0384006D8462 /* AppPageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPageCell.swift; sourceTree = ""; }; @@ -1663,6 +1668,9 @@ 7CCC801A292DC68E000B4200 /* ImageCropViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCropViewController.swift; sourceTree = ""; }; 7CCE65A628D69D1D00FE944A /* TransferActionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransferActionView.swift; sourceTree = ""; }; 7CCE65A728D69D1D00FE944A /* TransferActionView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TransferActionView.xib; sourceTree = ""; }; + 7CD9C17129B9BE94008F8D85 /* DatabaseBackupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseBackupManager.swift; sourceTree = ""; }; + 7CD9C17329B9DDE0008F8D85 /* DatabaseRepairViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseRepairViewController.swift; sourceTree = ""; }; + 7CD9C17729BB306F008F8D85 /* DatabaseFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseFile.swift; sourceTree = ""; }; 7CDBA58928F64E5000AC3777 /* WalletTransferSearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletTransferSearchResultsViewController.swift; sourceTree = ""; }; 7CDBA58D28F7B6CB00AC3777 /* TransferSearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferSearchViewController.swift; sourceTree = ""; }; 7CDF316B29890FB200421808 /* ConversationFontSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationFontSet.swift; sourceTree = ""; }; @@ -2368,6 +2376,8 @@ 7BA24D7225342575004906AD /* HomeOverlaysCoordinator.swift */, 7BA24D76253438B9004906AD /* ViewPanningController.swift */, 94D9DF6025F89D6E00FC2F28 /* BulletinContent.swift */, + 7CD9C17729BB306F008F8D85 /* DatabaseFile.swift */, + 7CD9C17129B9BE94008F8D85 /* DatabaseBackupManager.swift */, ); path = Model; sourceTree = ""; @@ -3288,6 +3298,7 @@ DF75FF4324FE61E1008A7CF3 /* UpdateViewController.swift */, 7BDB4724215A666B008B21F9 /* SignalLoadingViewController.swift */, DFD1FBC62302CB7A00C570D4 /* DatabaseUpgradeViewController.swift */, + 7CD9C17329B9DDE0008F8D85 /* DatabaseRepairViewController.swift */, DFB2062721ABC088006E4341 /* RestoreViewController.swift */, 7B63A8622431C9EE00D0F7C7 /* CirclesViewController.swift */, 7BB0F90F2434821000BEDA97 /* CircleEditorViewController.swift */, @@ -3723,6 +3734,7 @@ 947F4AD525866D6C00B0A5F9 /* InitializeFTSJob.swift */, 842347ED2695BA6400009A39 /* InitializeBotJob.swift */, 94FCB83A264683D900CCC8FD /* TranscriptAttachmentUploadJob.swift */, + 7C62922129B9699000B3596C /* DatabaseBackupJob.swift */, ); path = Job; sourceTree = ""; @@ -4442,6 +4454,7 @@ files = ( 7BF49DD320C3DBAC00A8510E /* CaptchaManager.swift in Sources */, DF8CECE11FC3054700E40064 /* TransferTypeCell.swift in Sources */, + 7CD9C17829BB306F008F8D85 /* DatabaseFile.swift in Sources */, 7BCB8C8422BB56B8002A13CC /* DataAndStorageSettingsViewController.swift in Sources */, 9BB351671FB19ECB00EDDD2C /* ConversationDateHeaderView.swift in Sources */, DF2819752014669E001EE5FA /* RefreshAccountJob.swift in Sources */, @@ -4674,6 +4687,7 @@ E041064B23C5C3BC00A6F08E /* CoreTextLabelDelegate.swift in Sources */, 7B915F74215FB0C100A562C6 /* GiphySearchViewController.swift in Sources */, 7B7DACA623505793006AA2AC /* AudioCell.swift in Sources */, + 7CD9C17429B9DDE0008F8D85 /* DatabaseRepairViewController.swift in Sources */, 5E5CA86D2674B09100C1E113 /* ScreenLockSettingViewController.swift in Sources */, DF5D9F251F9C79E10036D5FD /* LocalizedExtension.swift in Sources */, 7B4FCCE02440A66600360F65 /* SolidBackgroundColorImageView.swift in Sources */, @@ -4792,6 +4806,7 @@ 7BFDB73920DA41E3005673CC /* Quote.swift in Sources */, 7B51DDB5223A489F008ACDBB /* LoginContext.swift in Sources */, DF5D9F281F9C79E10036D5FD /* UIApplicationExtension.swift in Sources */, + 7C62922229B9699000B3596C /* DatabaseBackupJob.swift in Sources */, DF7A4B4A1FCE6EE200F21BCB /* UIViewExtension.swift in Sources */, 7C5823D5268966A1003AA142 /* HomeAppsFolderViewController.swift in Sources */, 7BD00F1E2559711A004D8814 /* WalletSearchResultsViewController.swift in Sources */, @@ -4991,6 +5006,7 @@ 7BEE5351222D0E5C008D3911 /* ConversationExtensionCell.swift in Sources */, DF8CECF21FC4256D00E40064 /* BlockUserCell.swift in Sources */, 7CC730502745F95D002780F5 /* StickerStore.swift in Sources */, + 7CD9C17229B9BE94008F8D85 /* DatabaseBackupManager.swift in Sources */, 7B2E56B0244EA6BB0073102C /* SettingsFooterView.swift in Sources */, 7BD7534E2182CDCE00BAC172 /* IconPrefixedTextMessageCell.swift in Sources */, 7B369206233A3314007321A7 /* SharedMediaViewController.swift in Sources */, diff --git a/Mixin/AppDelegate.swift b/Mixin/AppDelegate.swift index c21f5bd335..d21cc02b7e 100644 --- a/Mixin/AppDelegate.swift +++ b/Mixin/AppDelegate.swift @@ -33,12 +33,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate { if #available(iOS 15.0, *), !ProcessInfo.processInfo.isiOSAppOnMac { UITableView.appearance().sectionHeaderTopPadding = 0 } + addObservers() checkLogin() ScreenLockManager.shared.lockScreenIfNeeded() checkJailbreak() configAnalytics() pendingShortcutItem = launchOptions?[UIApplication.LaunchOptionsKey.shortcutItem] as? UIApplicationShortcutItem - addObservers() Logger.general.info(category: "AppDelegate", message: "App \(Bundle.main.shortVersion)(\(Bundle.main.bundleVersion)) did finish launching with state: \(UIApplication.shared.applicationStateString), device: \(Device.current.machineName) \(ProcessInfo.processInfo.operatingSystemVersionString), id: \(Device.current.id)") if UIApplication.shared.applicationState == .background { MixinService.isStopProcessMessages = false @@ -197,6 +197,7 @@ extension AppDelegate { NotificationCenter.default.addObserver(self, selector: #selector(handleClockSkew), name: MixinService.clockSkewDetectedNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(webSocketDidConnect), name: WebSocketService.didConnectNotification, object: nil) NotificationCenter.default.addObserver(JobService.shared, selector: #selector(JobService.restoreJobs), name: WebSocketService.didSendListPendingMessageNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(databaseCorrupted), name: AppGroupUserDefaults.User.databaseCorruptedNotification, object: nil) } @objc func webSocketDidConnect() { @@ -257,6 +258,9 @@ extension AppDelegate { } } + @objc func databaseCorrupted() { + mainWindow.rootViewController = makeInitialViewController() + } } extension AppDelegate { diff --git a/Mixin/Service/Job/DatabaseBackupJob.swift b/Mixin/Service/Job/DatabaseBackupJob.swift new file mode 100644 index 0000000000..b87a4922e7 --- /dev/null +++ b/Mixin/Service/Job/DatabaseBackupJob.swift @@ -0,0 +1,31 @@ +import Foundation +import MixinServices +import GRDB + +class DatabaseBackupJob: AsynchronousJob { + + override func getJobId() -> String { + "database-backup" + } + + override func execute() -> Bool { + do { + try UserDatabase.current.writeWithoutTransaction { _ in + try DatabaseFile.copy(at: .original, to: .temp) + } + let dbQueue = try DatabaseQueue(path: DatabaseFile.temp.db.path) + try dbQueue.write { db in + try db.execute(sql: "PRAGMA integrity_check") + } + try DatabaseFile.copy(at: .temp, to: .backup) + try DatabaseFile.removeIfExists(.temp) + AppGroupUserDefaults.User.lastDatabaseBackupDate = Date() + finishJob() + } catch { + Logger.general.error(category: "BackupDatabaseJob", message: "Backup database failed: \(error)") + reporter.report(error: error) + } + return true + } + +} diff --git a/Mixin/UserInterface/Controllers/Common/InitialViewControllerFactory.swift b/Mixin/UserInterface/Controllers/Common/InitialViewControllerFactory.swift index 1abccc7397..87d363ffa5 100644 --- a/Mixin/UserInterface/Controllers/Common/InitialViewControllerFactory.swift +++ b/Mixin/UserInterface/Controllers/Common/InitialViewControllerFactory.swift @@ -2,7 +2,9 @@ import UIKit import MixinServices func makeInitialViewController(isUsernameJustInitialized: Bool = false) -> UIViewController { - if AppGroupUserDefaults.Account.isClockSkewed { + if AppGroupUserDefaults.User.isDatabaseCorrupted { + return DatabaseRepairViewController.instance() + } else if AppGroupUserDefaults.Account.isClockSkewed { if let viewController = AppDelegate.current.mainWindow.rootViewController as? ClockSkewViewController { viewController.checkFailed() return viewController diff --git a/Mixin/UserInterface/Controllers/Home/DatabaseRepairViewController.swift b/Mixin/UserInterface/Controllers/Home/DatabaseRepairViewController.swift new file mode 100644 index 0000000000..d713025e58 --- /dev/null +++ b/Mixin/UserInterface/Controllers/Home/DatabaseRepairViewController.swift @@ -0,0 +1,36 @@ +import UIKit +import MixinServices + +class DatabaseRepairViewController: UIViewController { + + @IBOutlet weak var repairButton: RoundedButton! + @IBOutlet weak var stackView: UIStackView! + @IBOutlet weak var activityIndicator: ActivityIndicatorView! + + class func instance() -> DatabaseRepairViewController { + R.storyboard.home.repair_database()! + } + + @IBAction func repairAction(_ sender: Any) { + repairButton.isHidden = true + stackView.isHidden = false + activityIndicator.startAnimating() + if DatabaseFile.exists(.backup) { + do { + try DatabaseFile.removeIfExists(.original) + try DatabaseFile.copy(at: .backup, to: .original) + let lastBackupDate = AppGroupUserDefaults.User.lastDatabaseBackupDate ?? Date() + let formattedDate = DateFormatter.dateFull.string(from: lastBackupDate) + stackView.isHidden = true + alert(nil, message: R.string.localizable.repair_chat_history_success(formattedDate)) { _ in + AppGroupUserDefaults.User.isDatabaseCorrupted = false + } + } catch { + LoginManager.shared.logout(reason: "Failed to repair database") + } + } else { + LoginManager.shared.logout(reason: "Failed to repair database") + } + } + +} diff --git a/Mixin/UserInterface/Controllers/Home/HomeViewController.swift b/Mixin/UserInterface/Controllers/Home/HomeViewController.swift index 278894e032..86559d1838 100644 --- a/Mixin/UserInterface/Controllers/Home/HomeViewController.swift +++ b/Mixin/UserInterface/Controllers/Home/HomeViewController.swift @@ -151,6 +151,7 @@ class HomeViewController: UIViewController { if SpotlightManager.isAvailable { SpotlightManager.shared.indexIfNeeded() } + DatabaseBackupManager.shared.backupIfNeeded() } UIApplication.homeContainerViewController?.clipSwitcher.loadClipsFromPreviousSession() } diff --git a/Mixin/UserInterface/Controllers/Home/Model/DatabaseBackupManager.swift b/Mixin/UserInterface/Controllers/Home/Model/DatabaseBackupManager.swift new file mode 100644 index 0000000000..486e07c2fe --- /dev/null +++ b/Mixin/UserInterface/Controllers/Home/Model/DatabaseBackupManager.swift @@ -0,0 +1,23 @@ +import Foundation +import MixinServices + +class DatabaseBackupManager: NSObject { + + static let shared = DatabaseBackupManager() + + override init() { + super.init() + NotificationCenter.default.addObserver(self, selector: #selector(backupIfNeeded), name: UIApplication.didBecomeActiveNotification, object: nil) + } + + @objc func backupIfNeeded() { + guard LoginManager.shared.isLoggedIn else { + return + } + let lastDatabaseBackupDate = AppGroupUserDefaults.User.lastDatabaseBackupDate + if lastDatabaseBackupDate == nil || -lastDatabaseBackupDate!.timeIntervalSinceNow > TimeInterval.hour * 2 { + ConcurrentJobQueue.shared.addJob(job: DatabaseBackupJob()) + } + } + +} diff --git a/Mixin/UserInterface/Controllers/Home/Model/DatabaseFile.swift b/Mixin/UserInterface/Controllers/Home/Model/DatabaseFile.swift new file mode 100644 index 0000000000..b79b87a68e --- /dev/null +++ b/Mixin/UserInterface/Controllers/Home/Model/DatabaseFile.swift @@ -0,0 +1,59 @@ +import Foundation +import MixinServices + +enum DatabaseFile { + + case original + case backup + case temp + + private var name: String { + switch self { + case .original: + return "mixin.db" + case .backup: + return "mixin-backup.db" + case .temp: + return "mixin-backup-temp.db" + } + } + + var db: URL { + AppGroupContainer.accountUrl.appendingPathComponent("\(name)", isDirectory: false) + } + + var shm: URL { + AppGroupContainer.accountUrl.appendingPathComponent("\(name)-shm", isDirectory: false) + } + + var wal: URL { + AppGroupContainer.accountUrl.appendingPathComponent("\(name)-wal", isDirectory: false) + } + + static func removeIfExists(_ file: DatabaseFile) throws { + if FileManager.default.fileExists(atPath: file.db.path) { + try FileManager.default.removeItem(at: file.db) + } + if FileManager.default.fileExists(atPath: file.wal.path) { + try FileManager.default.removeItem(at: file.wal) + } + if FileManager.default.fileExists(atPath: file.shm.path) { + try FileManager.default.removeItem(at: file.shm) + } + } + + static func copy(at srcFile: DatabaseFile, to dstFIle: DatabaseFile) throws { + try removeIfExists(dstFIle) + try FileManager.default.copyItem(at: srcFile.db, to: dstFIle.db) + try FileManager.default.copyItem(at: srcFile.wal, to: dstFIle.wal) + try FileManager.default.copyItem(at: srcFile.shm, to: dstFIle.shm) + } + + static func exists(_ file: DatabaseFile) -> Bool { + FileManager.default.fileExists(atPath: file.db.path) && + FileManager.default.fileExists(atPath: file.wal.path) && + FileManager.default.fileExists(atPath: file.shm.path) + } + +} + diff --git a/Mixin/UserInterface/Storyboard/Home.storyboard b/Mixin/UserInterface/Storyboard/Home.storyboard index fe8c8960aa..40dc2c1096 100644 --- a/Mixin/UserInterface/Storyboard/Home.storyboard +++ b/Mixin/UserInterface/Storyboard/Home.storyboard @@ -1,9 +1,9 @@ - + - + @@ -38,7 +38,7 @@ - + @@ -56,22 +56,22 @@ - + - + - + - + @@ -785,7 +785,7 @@ - + @@ -1556,7 +1556,7 @@