diff --git a/AppCheck.xcodeproj/project.pbxproj b/AppCheck.xcodeproj/project.pbxproj index 55bbda3..d714e60 100644 --- a/AppCheck.xcodeproj/project.pbxproj +++ b/AppCheck.xcodeproj/project.pbxproj @@ -141,6 +141,8 @@ 54E540F4247D3F2600F7C34A /* TestDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F3247D3F2600F7C34A /* TestDataSource.swift */; }; 54E540F8247DB90F00F7C34A /* RecordingsDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F7247DB90F00F7C34A /* RecordingsDB.swift */; }; 54E540FA2482414800F7C34A /* SyncUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E540F92482414800F7C34A /* SyncUpdate.swift */; }; + 54E67E4624A8B0FE0025D261 /* PrefsShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E67E4524A8B0FE0025D261 /* PrefsShared.swift */; }; + 54E67E4924A8B1280025D261 /* Prefs.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E67E4824A8B1280025D261 /* Prefs.swift */; }; 54EFA4E82491A16A0022D618 /* Font.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54EFA4E72491A16A0022D618 /* Font.swift */; }; /* End PBXBuildFile section */ @@ -308,6 +310,8 @@ 54E540F3247D3F2600F7C34A /* TestDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDataSource.swift; sourceTree = ""; }; 54E540F7247DB90F00F7C34A /* RecordingsDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordingsDB.swift; sourceTree = ""; }; 54E540F92482414800F7C34A /* SyncUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncUpdate.swift; sourceTree = ""; }; + 54E67E4524A8B0FE0025D261 /* PrefsShared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrefsShared.swift; sourceTree = ""; }; + 54E67E4824A8B1280025D261 /* Prefs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Prefs.swift; sourceTree = ""; }; 54EFA4E72491A16A0022D618 /* Font.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Font.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -438,6 +442,8 @@ 545DDDD224436A03003B6544 /* Common Classes */ = { isa = PBXGroup; children = ( + 54E67E4824A8B1280025D261 /* Prefs.swift */, + 54E67E4524A8B0FE0025D261 /* PrefsShared.swift */, 545DDDD024436983003B6544 /* QuickUI.swift */, 545DDDCE243E6267003B6544 /* TutorialSheet.swift */, 54D8B979246C9F2000EB2414 /* FilterPipeline.swift */, @@ -840,6 +846,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 54E67E4924A8B1280025D261 /* Prefs.swift in Sources */, 54E540F8247DB90F00F7C34A /* RecordingsDB.swift in Sources */, 54E540F4247D3F2600F7C34A /* TestDataSource.swift in Sources */, 545DDDD424466D37003B6544 /* AutoLayout.swift in Sources */, @@ -859,6 +866,7 @@ 54B34596240F0513004C53CC /* TableView.swift in Sources */, 540E6780242D2CF100871BBE /* VCRecordings.swift in Sources */, 54953E3323DC752E0054345C /* DBCore.swift in Sources */, + 54E67E4624A8B0FE0025D261 /* PrefsShared.swift in Sources */, 544F912024A67EC5001D4B00 /* TVCOccurrenceContext.swift in Sources */, 54448A30248647D900771C96 /* Time.swift in Sources */, 54953E6123E0D69A0054345C /* TVCHosts.swift in Sources */, diff --git a/main/Base.lproj/Main.storyboard b/main/Base.lproj/Main.storyboard index 90156e3..77574eb 100644 --- a/main/Base.lproj/Main.storyboard +++ b/main/Base.lproj/Main.storyboard @@ -1207,10 +1207,38 @@ Duration: 60:00 + + + + + + + + + + + + + + + + - + @@ -1232,7 +1260,7 @@ Duration: 60:00 - + @@ -1245,7 +1273,7 @@ Duration: 60:00 - + @@ -1257,27 +1285,27 @@ Duration: 60:00 - + - - + + - + - - - + + @@ -1293,6 +1321,7 @@ Duration: 60:00 + diff --git a/main/Common Classes/CustomAlert.swift b/main/Common Classes/CustomAlert.swift index 86694df..0491758 100644 --- a/main/Common Classes/CustomAlert.swift +++ b/main/Common Classes/CustomAlert.swift @@ -166,9 +166,9 @@ class DatePickerAlert : CustomAlert { datePicker.date = Date() } - func present(in viewController: UIViewController, onSuccess: @escaping (Date) -> Void) { + func present(in viewController: UIViewController, onSuccess: @escaping (UIDatePicker, Date) -> Void) { super.present(in: viewController) { - onSuccess($0.date) + onSuccess($0, $0.date) } } } @@ -219,9 +219,9 @@ class DurationPickerAlert: CustomAlert, UIPickerViewDataSource, UI compWidths[c] * pickerView.frame.width } - func present(in viewController: UIViewController, onSuccess: @escaping ([Int]) -> Void) { + func present(in viewController: UIViewController, onSuccess: @escaping (UIPickerView, [Int]) -> Void) { super.present(in: viewController) { - onSuccess($0.selection) + onSuccess($0, $0.selection) } } } diff --git a/main/Common Classes/Prefs.swift b/main/Common Classes/Prefs.swift new file mode 100644 index 0000000..d7e8a88 --- /dev/null +++ b/main/Common Classes/Prefs.swift @@ -0,0 +1,77 @@ +import Foundation + +enum Pref { + static func Int(_ key: String) -> Int { UserDefaults.standard.integer(forKey: key) } + static func Int(_ val: Int, _ key: String) { UserDefaults.standard.set(val, forKey: key) } + static func Bool(_ key: String) -> Bool { UserDefaults.standard.bool(forKey: key) } + static func Bool(_ val: Bool, _ key: String) { UserDefaults.standard.set(val, forKey: key) } + static func `Any`(_ key: String) -> Any? { UserDefaults.standard.object(forKey: key) } + static func `Any`(_ val: Any?, _ key: String) { UserDefaults.standard.set(val, forKey: key) } + + enum DidShowTutorial { + static var Welcome: Bool { + get { Pref.Bool("didShowTutorialAppWelcome") } + set { Pref.Bool(newValue, "didShowTutorialAppWelcome") } + } + static var Recordings: Bool { + get { Pref.Bool("didShowTutorialRecordings") } + set { Pref.Bool(newValue, "didShowTutorialRecordings") } + } + } + enum ContextAnalyis { + static var CoOccurrenceTime: Int? { + get { Pref.Any("contextAnalyisCoOccurrenceTime") as? Int } + set { Pref.Any(newValue, "contextAnalyisCoOccurrenceTime") } + } + } + enum DateFilter { + static var Kind: DateFilterKind { + get { DateFilterKind(rawValue: Pref.Int("dateFilterType"))! } + set { Pref.Int(newValue.rawValue, "dateFilterType") } + } + /// Default: `0` (disabled) + static var LastXMin: Int { + get { Pref.Int("dateFilterLastXMin") } + set { Pref.Int(newValue, "dateFilterLastXMin") } + } + /// Default: `nil` (disabled) + static var RangeA: Timestamp? { + get { Pref.Any("dateFilterRangeA") as? Timestamp } + set { Pref.Any(newValue, "dateFilterRangeA") } + } + /// Default: `nil` (disabled) + static var RangeB: Timestamp? { + get { Pref.Any("dateFilterRangeB") as? Timestamp } + set { Pref.Any(newValue, "dateFilterRangeB") } + } + /// default: `.Date` + static var OrderBy: DateFilterOrderBy { + get { DateFilterOrderBy(rawValue: Pref.Int("dateFilterOderType"))! } + set { Pref.Int(newValue.rawValue, "dateFilterOderType") } + } + /// default: `false` (Desc) + static var OrderAsc: Bool { + get { Pref.Bool("dateFilterOderAsc") } + set { Pref.Bool(newValue, "dateFilterOderAsc") } + } + + /// - Returns: Timestamp restriction depending on current selected date filter. + /// - `Off` : `(nil, nil)` + /// - `LastXMin` : `(now-LastXMin, nil)` + /// - `ABRange` : `(RangeA, RangeB)` + static func restrictions() -> (type: DateFilterKind, earliest: Timestamp?, latest: Timestamp?) { + let type = Kind + switch type { + case .Off: return (type, nil, nil) + case .LastXMin: return (type, Timestamp.past(minutes: Pref.DateFilter.LastXMin), nil) + case .ABRange: return (type, Pref.DateFilter.RangeA, Pref.DateFilter.RangeB) + } + } + } +} +enum DateFilterKind: Int { + case Off = 0, LastXMin = 1, ABRange = 2; +} +enum DateFilterOrderBy: Int { + case Date = 0, Name = 1, Count = 2; +} diff --git a/main/Common Classes/PrefsShared.swift b/main/Common Classes/PrefsShared.swift new file mode 100644 index 0000000..016fc98 --- /dev/null +++ b/main/Common Classes/PrefsShared.swift @@ -0,0 +1,15 @@ +import Foundation + +enum PrefsShared { + private static var suite: UserDefaults { UserDefaults(suiteName: "group.de.uni-bamberg.psi.AppCheck")! } + + private static func Int(_ key: String) -> Int { suite.integer(forKey: key) } + private static func Int(_ val: Int, _ key: String) { suite.set(val, forKey: key) } +// private static func Obj(_ key: String) -> Any? { suite.object(forKey: key) } +// private static func Obj(_ val: Any?, _ key: String) { suite.set(val, forKey: key) } + + static var AutoDeleteLogsDays: Int { + get { Int("AutoDeleteLogsDays") } + set { Int(newValue, "AutoDeleteLogsDays"); suite.synchronize() } + } +} diff --git a/main/Extensions/SharedState.swift b/main/Extensions/SharedState.swift index 81062ec..d1a3334 100644 --- a/main/Extensions/SharedState.swift +++ b/main/Extensions/SharedState.swift @@ -6,79 +6,3 @@ let sync = SyncUpdate(periodic: 7) public enum VPNState : Int { case on = 1, inbetween, off } - -enum Pref { - static func Int(_ key: String) -> Int { UserDefaults.standard.integer(forKey: key) } - static func Int(_ val: Int, _ key: String) { UserDefaults.standard.set(val, forKey: key) } - static func Bool(_ key: String) -> Bool { UserDefaults.standard.bool(forKey: key) } - static func Bool(_ val: Bool, _ key: String) { UserDefaults.standard.set(val, forKey: key) } - static func `Any`(_ key: String) -> Any? { UserDefaults.standard.object(forKey: key) } - static func `Any`(_ val: Any?, _ key: String) { UserDefaults.standard.set(val, forKey: key) } - - enum DidShowTutorial { - static var Welcome: Bool { - get { Pref.Bool("didShowTutorialAppWelcome") } - set { Pref.Bool(newValue, "didShowTutorialAppWelcome") } - } - static var Recordings: Bool { - get { Pref.Bool("didShowTutorialRecordings") } - set { Pref.Bool(newValue, "didShowTutorialRecordings") } - } - } - enum ContextAnalyis { - static var CoOccurrenceTime: Int? { - get { Pref.Any("contextAnalyisCoOccurrenceTime") as? Int } - set { Pref.Any(newValue, "contextAnalyisCoOccurrenceTime") } - } - } - enum DateFilter { - static var Kind: DateFilterKind { - get { DateFilterKind(rawValue: Pref.Int("dateFilterType"))! } - set { Pref.Int(newValue.rawValue, "dateFilterType") } - } - /// Default: `0` (disabled) - static var LastXMin: Int { - get { Pref.Int("dateFilterLastXMin") } - set { Pref.Int(newValue, "dateFilterLastXMin") } - } - /// Default: `nil` (disabled) - static var RangeA: Timestamp? { - get { Pref.Any("dateFilterRangeA") as? Timestamp } - set { Pref.Any(newValue, "dateFilterRangeA") } - } - /// Default: `nil` (disabled) - static var RangeB: Timestamp? { - get { Pref.Any("dateFilterRangeB") as? Timestamp } - set { Pref.Any(newValue, "dateFilterRangeB") } - } - /// default: `.Date` - static var OrderBy: DateFilterOrderBy { - get { DateFilterOrderBy(rawValue: Pref.Int("dateFilterOderType"))! } - set { Pref.Int(newValue.rawValue, "dateFilterOderType") } - } - /// default: `false` (Desc) - static var OrderAsc: Bool { - get { Pref.Bool("dateFilterOderAsc") } - set { Pref.Bool(newValue, "dateFilterOderAsc") } - } - - /// - Returns: Timestamp restriction depending on current selected date filter. - /// - `Off` : `(nil, nil)` - /// - `LastXMin` : `(now-LastXMin, nil)` - /// - `ABRange` : `(RangeA, RangeB)` - static func restrictions() -> (type: DateFilterKind, earliest: Timestamp?, latest: Timestamp?) { - let type = Kind - switch type { - case .Off: return (type, nil, nil) - case .LastXMin: return (type, Timestamp.past(minutes: Pref.DateFilter.LastXMin), nil) - case .ABRange: return (type, Pref.DateFilter.RangeA, Pref.DateFilter.RangeB) - } - } - } -} -enum DateFilterKind: Int { - case Off = 0, LastXMin = 1, ABRange = 2; -} -enum DateFilterOrderBy: Int { - case Date = 0, Name = 1, Count = 2; -} diff --git a/main/Requests/VCDateFilter.swift b/main/Requests/VCDateFilter.swift index 900d679..54cda65 100644 --- a/main/Requests/VCDateFilter.swift +++ b/main/Requests/VCDateFilter.swift @@ -64,8 +64,8 @@ class VCDateFilter: UIViewController, UIGestureRecognizerDelegate { @IBAction private func didTapRangeButton(_ sender: UIButton) { let flag = (sender == buttonRangeStart) let oldDate = flag ? Date(self.tsRangeA) : Date(self.tsRangeB) - DatePickerAlert(initial: oldDate).present(in: self) { (selected: Date) in - var ts = selected.timestamp + DatePickerAlert(initial: oldDate).present(in: self) { + var ts = $1.timestamp ts -= ts % 60 // remove seconds // if one of these is greater than the other, adjust the latter too. if flag || self.tsRangeA > ts { diff --git a/main/Settings/TVCSettings.swift b/main/Settings/TVCSettings.swift index 1f3f625..091aaf0 100644 --- a/main/Settings/TVCSettings.swift +++ b/main/Settings/TVCSettings.swift @@ -6,6 +6,7 @@ class TVCSettings: UITableViewController { @IBOutlet var vpnToggle: UISwitch! @IBOutlet var cellDomainsIgnored: UITableViewCell! @IBOutlet var cellDomainsBlocked: UITableViewCell! + @IBOutlet var cellPrivacyAutoDelete: UITableViewCell! override func viewDidLoad() { super.viewDidLoad() @@ -15,37 +16,13 @@ class TVCSettings: UITableViewController { reloadDataSource() } - @objc func reloadDataSource() { - let (blocked, ignored) = DomainFilter.counts() - cellDomainsIgnored.detailTextLabel?.text = "\(ignored) Domains" - cellDomainsBlocked.detailTextLabel?.text = "\(blocked) Domains" - } - @IBAction func toggleVPNProxy(_ sender: UISwitch) { + // MARK: - VPN Proxy Settings + + @IBAction private func toggleVPNProxy(_ sender: UISwitch) { appDelegate.setProxyEnabled(sender.isOn) } - @IBAction func exportDB(_ sender: Any) { - let sheet = UIActivityViewController(activityItems: [URL.internalDB()], applicationActivities: nil) - self.present(sheet, animated: true) - } - - @IBAction func resetTutorialAlerts(_ sender: UIButton) { - Pref.DidShowTutorial.Welcome = false - Pref.DidShowTutorial.Recordings = false - Alert(title: sender.titleLabel?.text, - text: "\nDone.\n\nYou may need to restart the application.").presentIn(self) - } - - @IBAction func clearDatabaseResults(_ sender: Any) { - AskAlert(title: "Clear results?", text: - "You are about to delete all results that have been logged in the past. " + - "Your preferences for blocked and ignored domains are preserved.\n" + - "Continue?", buttonText: "Delete", buttonStyle: .destructive) { _ in - TheGreatDestroyer.deleteAllLogs() - }.presentIn(self) - } - @objc func vpnStateChanged(_ notification: Notification) { changedState(notification.object as! VPNState) } @@ -55,6 +32,17 @@ class TVCSettings: UITableViewController { vpnToggle.onTintColor = (newState == .inbetween ? .systemYellow : nil) } + + // MARK: - Logging Filter + + @objc func reloadDataSource() { + let (blocked, ignored) = DomainFilter.counts() + cellDomainsIgnored.detailTextLabel?.text = "\(ignored) Domains" + cellDomainsBlocked.detailTextLabel?.text = "\(blocked) Domains" + let (one, two) = autoDeleteSelection([1, 7, 31]) + cellPrivacyAutoDelete.detailTextLabel?.text = autoDeleteString(one, unit: two) + } + override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) { let t:String, d: String switch tableView.cellForRow(at: indexPath)?.reuseIdentifier { @@ -82,4 +70,74 @@ class TVCSettings: UITableViewController { break } } + + + // MARK: - Privacy + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if let cell = tableView.cellForRow(at: indexPath), cell === cellPrivacyAutoDelete { + let multiplier = [1, 7, 31] + let (one, two) = autoDeleteSelection(multiplier) + + let picker = DurationPickerAlert( + title: "Auto-delete logs", + detail: "Logs will be deleted on app launch or periodically as long as the VPN is running.", + options: [(0...30).map{"\($0)"}, ["Days", "Weeks", "Months"]], + widths: [0.4, 0.6]) + picker.pickerView.setSelection([min(30, one), two]) + picker.present(in: self) { + PrefsShared.AutoDeleteLogsDays = $1[0] * multiplier[$1[1]] + cell.detailTextLabel?.text = autoDeleteString($1[0], unit: $1[1]) + // TODO: notify VPN and local delete timer + } + } + } + + + // MARK: - Reset Settings + + @IBAction private func resetTutorialAlerts(_ sender: UIButton) { + Pref.DidShowTutorial.Welcome = false + Pref.DidShowTutorial.Recordings = false + Alert(title: sender.titleLabel?.text, + text: "\nDone.\n\nYou may need to restart the application.").presentIn(self) + } + + @IBAction private func clearDatabaseResults() { + AskAlert(title: "Clear results?", text: + "You are about to delete all results that have been logged in the past. " + + "Your preferences for blocked and ignored domains are preserved.\n" + + "Continue?", buttonText: "Delete", buttonStyle: .destructive) { _ in + TheGreatDestroyer.deleteAllLogs() + }.presentIn(self) + } + + + // MARK: - Advanced + + @IBAction private func exportDB() { + let sheet = UIActivityViewController(activityItems: [URL.internalDB()], applicationActivities: nil) + self.present(sheet, animated: true) + } +} + + +// ------------------------------- +// | +// | MARK: - Helper methods +// | +// ------------------------------- + +private func autoDeleteSelection(_ multiplier: [Int]) -> (Int, Int) { + let current = PrefsShared.AutoDeleteLogsDays + let snd = multiplier.lastIndex { current % $0 == 0 }! // make sure 1 is in list + return (current / multiplier[snd], snd) +} + +private func autoDeleteString(_ num: Int, unit: Int) -> String { + switch num { + case 0: return "Never" + case 1: return "1 \(["Day", "Week", "Month"][unit])" + default: return "\(num) \(["Days", "Weeks", "Months"][unit])" + } }