diff --git a/AppCheck.xcodeproj/project.pbxproj b/AppCheck.xcodeproj/project.pbxproj index 1e00d91..db39d2c 100644 --- a/AppCheck.xcodeproj/project.pbxproj +++ b/AppCheck.xcodeproj/project.pbxproj @@ -85,6 +85,7 @@ 54953E6123E0D69A0054345C /* TVCHosts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54953E6023E0D69A0054345C /* TVCHosts.swift */; }; 54953E6F23E44CD00054345C /* TVCHostDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54953E6E23E44CD00054345C /* TVCHostDetails.swift */; }; 54953E7123E473F10054345C /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 54953E7023E473F10054345C /* Settings.bundle */; }; + 549A96D62501198400C565FA /* VCEditText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549A96D52501198400C565FA /* VCEditText.swift */; }; 549D6ED524D5BFDB0032E498 /* TVCAppSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549D6ED424D5BFDB0032E498 /* TVCAppSearch.swift */; }; 549ECD9D24A7AD550097571C /* CustomAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549ECD9C24A7AD550097571C /* CustomAlert.swift */; }; 54A0CC0924E30C56009B5EC1 /* Recordings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54A0CC0724E30C56009B5EC1 /* Recordings.storyboard */; }; @@ -180,7 +181,7 @@ 54CA02C42426DCCD003A5E04 /* GCDAsyncUdpSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02C02426DCCD003A5E04 /* GCDAsyncUdpSocket.m */; }; 54CE8BC424B1ED2100CC1756 /* PushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CE8BC324B1ED2100CC1756 /* PushNotification.swift */; }; 54CE8BC524B1ED2100CC1756 /* PushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CE8BC324B1ED2100CC1756 /* PushNotification.swift */; }; - 54CFE86824E3F401001687DD /* VCShareRecording.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFE86724E3F401001687DD /* VCShareRecording.swift */; }; + 54CFE86824E3F401001687DD /* TVCShareRecording.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFE86724E3F401001687DD /* TVCShareRecording.swift */; }; 54D8B97A246C9F2000EB2414 /* FilterPipeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B979246C9F2000EB2414 /* FilterPipeline.swift */; }; 54D8B97C2471A7E000EB2414 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B97B2471A7E000EB2414 /* String.swift */; }; 54D8B97E2471B88900EB2414 /* DBCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B97D2471B88900EB2414 /* DBCommon.swift */; }; @@ -290,6 +291,7 @@ 54953E6023E0D69A0054345C /* TVCHosts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCHosts.swift; sourceTree = ""; }; 54953E6E23E44CD00054345C /* TVCHostDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCHostDetails.swift; sourceTree = ""; }; 54953E7023E473F10054345C /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; + 549A96D52501198400C565FA /* VCEditText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCEditText.swift; sourceTree = ""; }; 549D6ED424D5BFDB0032E498 /* TVCAppSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCAppSearch.swift; sourceTree = ""; }; 549ECD9C24A7AD550097571C /* CustomAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlert.swift; sourceTree = ""; }; 54A0CC0824E30C56009B5EC1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Recordings.storyboard; sourceTree = ""; }; @@ -388,7 +390,7 @@ 54CA02C12426DCCD003A5E04 /* GCDAsyncSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDAsyncSocket.h; sourceTree = ""; }; 54CA02C22426DCCD003A5E04 /* GCDAsyncUdpSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDAsyncUdpSocket.h; sourceTree = ""; }; 54CE8BC324B1ED2100CC1756 /* PushNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotification.swift; sourceTree = ""; }; - 54CFE86724E3F401001687DD /* VCShareRecording.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCShareRecording.swift; sourceTree = ""; }; + 54CFE86724E3F401001687DD /* TVCShareRecording.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCShareRecording.swift; sourceTree = ""; }; 54D8B979246C9F2000EB2414 /* FilterPipeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterPipeline.swift; sourceTree = ""; }; 54D8B97B2471A7E000EB2414 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 54D8B97D2471B88900EB2414 /* DBCommon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBCommon.swift; sourceTree = ""; }; @@ -455,7 +457,8 @@ 540E677F242D2CF100871BBE /* VCRecordings.swift */, 540E67832433FAFE00871BBE /* TVCPreviousRecords.swift */, 5458EBBF243A3F2200CFEB15 /* TVCRecordingDetails.swift */, - 54CFE86724E3F401001687DD /* VCShareRecording.swift */, + 54CFE86724E3F401001687DD /* TVCShareRecording.swift */, + 549A96D52501198400C565FA /* VCEditText.swift */, 540E67812433483D00871BBE /* VCEditRecording.swift */, 549D6ED424D5BFDB0032E498 /* TVCAppSearch.swift */, 54B345B12422E029004C53CC /* App Icons */, @@ -1071,7 +1074,7 @@ 54448A30248647D900771C96 /* Time.swift in Sources */, 54953E6123E0D69A0054345C /* TVCHosts.swift in Sources */, 549D6ED524D5BFDB0032E498 /* TVCAppSearch.swift in Sources */, - 54CFE86824E3F401001687DD /* VCShareRecording.swift in Sources */, + 54CFE86824E3F401001687DD /* TVCShareRecording.swift in Sources */, 54751E512423955100168273 /* URL.swift in Sources */, 54953E5F23DEBE840054345C /* TVCDomains.swift in Sources */, 54C056DB23E9E36E00214A3F /* AppStoreSearch.swift in Sources */, @@ -1084,6 +1087,7 @@ 542E2A982404973F001462DC /* TBCMain.swift in Sources */, 54448A3224899A4000771C96 /* SearchBarManager.swift in Sources */, 5458EBC0243A3F2200CFEB15 /* TVCRecordingDetails.swift in Sources */, + 549A96D62501198400C565FA /* VCEditText.swift in Sources */, 541FC9872497D81C00962623 /* TheGreatDestroyer.swift in Sources */, 54686A8724FD27AA0084934D /* TinyMarkdown.swift in Sources */, 5412F8EE24571B8200A63D7A /* VCDateFilter.swift in Sources */, diff --git a/main/Data Source/RecordingsDB.swift b/main/Data Source/RecordingsDB.swift index 605b7ea..04698a0 100644 --- a/main/Data Source/RecordingsDB.swift +++ b/main/Data Source/RecordingsDB.swift @@ -28,6 +28,18 @@ enum RecordingsDB { AppDB?.recordingLogsGet(r) ?? [] } + /// Get dictionary of domains with `ts` in ascending order. + static func detailCluster(_ r: Recording) -> [String : [Timestamp]] { + var cluster: [String : [Timestamp]] = [:] + for (dom, ts) in details(r) { + if cluster[dom] == nil { + cluster[dom] = [] + } + cluster[dom]!.append(ts - r.start) + } + return cluster + } + /// Update `title`, `appid`, and `notes` and post `NotifyRecordingChanged` notification. static func update(_ r: Recording) { AppDB?.recordingUpdate(r) diff --git a/main/GUI/Base.lproj/Recordings.storyboard b/main/GUI/Base.lproj/Recordings.storyboard index f7f7935..01277dd 100644 --- a/main/GUI/Base.lproj/Recordings.storyboard +++ b/main/GUI/Base.lproj/Recordings.storyboard @@ -295,7 +295,7 @@ - + @@ -314,66 +314,179 @@ - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + - Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. - + - - - - - - - - - - + + + + - + + + - - - + + - + - + @@ -432,16 +545,16 @@ - + - - + + - + @@ -466,7 +579,7 @@ 3. Line 4. Line - + diff --git a/main/Recordings/TVCPreviousRecords.swift b/main/Recordings/TVCPreviousRecords.swift index f274a05..e548358 100644 --- a/main/Recordings/TVCPreviousRecords.swift +++ b/main/Recordings/TVCPreviousRecords.swift @@ -64,13 +64,6 @@ class TVCPreviousRecords: UITableViewController, EditActionsRemove { dataSource.count } -// override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { -// let lbl = QuickUI.label("Previous Recordings", align: .center) -// lbl.font = lbl.font.bold() -// lbl.backgroundColor = .sysBackground -// return lbl -// } - override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "PreviousRecordCell")! let x = dataSource[indexPath.row] @@ -80,6 +73,10 @@ class TVCPreviousRecords: UITableViewController, EditActionsRemove { cell.imageView?.image = x.isLongTerm ? nil : BundleIcon.image(x.appId) cell.imageView?.layer.cornerRadius = 6.75 cell.imageView?.layer.masksToBounds = true + if #available(iOS 11, *) {} else { + cell.textLabel?.numberOfLines = 1 + cell.detailTextLabel?.numberOfLines = 1 + } return cell } diff --git a/main/Recordings/TVCRecordingDetails.swift b/main/Recordings/TVCRecordingDetails.swift index 31a8e86..c863331 100644 --- a/main/Recordings/TVCRecordingDetails.swift +++ b/main/Recordings/TVCRecordingDetails.swift @@ -45,7 +45,7 @@ class TVCRecordingDetails: UITableViewController, EditActionsRemove { } override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - if let tgt = segue.destination as? VCShareRecording { + if let tgt = segue.destination as? TVCShareRecording { tgt.record = self.record } } diff --git a/main/Recordings/TVCShareRecording.swift b/main/Recordings/TVCShareRecording.swift new file mode 100644 index 0000000..1b7b5eb --- /dev/null +++ b/main/Recordings/TVCShareRecording.swift @@ -0,0 +1,269 @@ +import UIKit + +class TVCShareRecording : UITableViewController, UITextViewDelegate, VCEditTextDelegate { + + @IBOutlet private var sendButton: UIBarButtonItem! + + // vars + var record: Recording! + private var shareNotes: Bool = false // opt-in + private lazy var hasNotes: Bool = (self.record.notes != nil) + private lazy var editedNotes: String = self.record.notes ?? "" + private lazy var weekInYear: String = { + let comp = Calendar.current.dateComponents( + [.weekOfYear, .yearForWeekOfYear], from: Date(self.record.start)) + return "\(comp.yearForWeekOfYear ?? 0).\(comp.weekOfYear ?? 0)" + }() + + // Data source + private lazy var dataSource: [String : [Timestamp]] = RecordingsDB.detailCluster(self.record) + + private lazy var dataSourceKeyValue: [(key: String, value: String)] = [ + ("Date", self.weekInYear), + ("Rec-Length", "\(self.record.duration) sec"), + ("App-Bundle", self.record.appId ?? " – "), + ("App-Name", self.record.title ?? " – "), + ("Notes", " – ") // see delegate below + ] + + private lazy var dataSourceLogs: [(domain: String, occurrences: String, enabled: Bool)] = self.dataSource.map { + ($0.key, $0.value.map{"\($0)"}.joined(separator: ", "), true) + }.sorted(by: { $0.domain < $1.domain }) + + + override func viewDidLoad() { + super.viewDidLoad() + + if record.isShared { + sendButton.tintColor = .gray + } + } + + private func reloadNotes() { + tableView.reloadRows(at: [ + IndexPath(row: 0, section: 1), // edit field + IndexPath(row: 4, section: 2) // display field + ], with: .automatic) + } + + + // MARK: - User Interaction + + @IBAction private func didChangeNotesCheckbox(_ sender: UISwitch) { + shareNotes = sender.isOn + reloadNotes() + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + if let dest = segue.destination as? VCEditText { + dest.text = editedNotes + dest.delegate = self + } + } + + func editText(didFinish text: String) { + editedNotes = text + reloadNotes() + } + + @IBAction private func shareRecording(_ sender: UIBarButtonItem) { + guard !record.isShared else { + showAlertAlreadyShared() + return + } + navigationItem.rightBarButtonItem = { + let v = UIView() + let activity = UIActivityIndicatorView() + v.addSubview(activity) + activity.anchor([.centerX, .centerY], to: v) + activity.startAnimating() + v.widthAnchor =&= 2 * activity.widthAnchor + return UIBarButtonItem(customView: v) + }() + + postToServer() { [weak self] in + self?.navigationItem.rightBarButtonItem = self?.sendButton + } + } + + + // MARK: - Table Data Source + + override func numberOfSections(in _: UITableView) -> Int { 4 } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch section { + case 0: return 1 // description + case 1: return hasNotes ? 2 : 0 // notes + checkbox + case 2: return dataSourceKeyValue.count + case 3: return dataSourceLogs.count + default: preconditionFailure() + } + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + switch section { + case 0: return "Review before sending" + case 1: return hasNotes ? "Notes" : nil + case 2: return "Send to server" + case 3: return "Logs" + default: return nil + } + } + + override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { + switch section { + case 0: return "You can tap on a domain cell to exclude it from the upload." + case 2: return "Below you see the domain names, followed by a list of relative time offsets (in seconds)." + default: return nil + } + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell: UITableViewCell + switch indexPath.section { + case 0: + cell = tableView.dequeueReusableCell(withIdentifier: "shareTextCell")! + cell.textLabel?.text = """ + You are about to upload the following information to our servers. + The data is anonymized in regards to device identifiers and time of recording. However, it is not anonymous to the domains requested during the recording. + """ + case 1: + switch indexPath.row { + case 0: + cell = tableView.dequeueReusableCell(withIdentifier: "shareOpenTextCell")! + cell.textLabel?.text = editedNotes + cell.textLabel?.textColor = shareNotes ? nil : .gray + case 1: + cell = tableView.dequeueReusableCell(withIdentifier: "shareCheckboxCell")! + cell.textLabel?.text = "Upload your notes?" + let accessory = cell.accessoryView as! UISwitch + accessory.isOn = shareNotes + default: preconditionFailure() + } + case 2: + cell = tableView.dequeueReusableCell(withIdentifier: "shareKeyValueCell")! + let src = dataSourceKeyValue[indexPath.row] + cell.textLabel?.text = src.key + let flag = shareNotes && indexPath.row == 4 + cell.detailTextLabel?.text = flag ? editedNotes : src.value + case 3: + cell = tableView.dequeueReusableCell(withIdentifier: "shareLogCell")! + let src = dataSourceLogs[indexPath.row] + let sent = src.enabled + cell.textLabel?.text = src.domain + cell.detailTextLabel?.text = sent ? src.occurrences : "don't upload" + cell.accessoryType = sent ? .checkmark : .none + cell.textLabel?.isEnabled = sent + cell.detailTextLabel?.isEnabled = sent + default: + preconditionFailure() + } + if #available(iOS 11, *) {} else { + cell.detailTextLabel?.numberOfLines = 1 + } + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard indexPath.section == 3 else { return } + dataSourceLogs[indexPath.row].enabled = !dataSourceLogs[indexPath.row].enabled + tableView.deselectRow(at: indexPath, animated: true) + tableView.reloadRows(at: [indexPath], with: .automatic) + } + + + // MARK: - Upload + + private func postToServer(_ onceLoaded: @escaping () -> Void) { + // prepare json + let json = try? JSONSerialization.data(withJSONObject: [ + "v" : 1, + "date" : weekInYear, + "duration" : record.duration, + "app-bundle" : record.appId ?? "", + "app-name" : record.title ?? "", + "notes" : shareNotes ? editedNotes : "", + "logs" : [] + ]) + + // prepare post request + let url = URL(string: "https://appchk.de/api/v1/contribute/")! + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.httpBody = json + var rec = record! // store temporarily so self can be released + + // send to server + URLSession.shared.dataTask(with: request) { data, response, error in + DispatchQueue.main.async { [weak self] in + onceLoaded() + guard error == nil, let data = data, + let response = response as? HTTPURLResponse else { + self?.banner(.fail, "\(error?.localizedDescription ?? "Unkown error occurred")") + return + } + guard let json = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any], + let v = json["v"] as? Int, v > 0 else { + QLog.Warning("Couldn't contribute: Not JSON or no version key") + self?.banner(.fail, "Server couldn't parse request.\nTry again later.") + return + } + let status = json["status"] as? String ?? "unkown reason" + guard status == "ok", (200 ... 299) ~= response.statusCode else { + QLog.Warning("Couldn't contribute: \(status)") + self?.banner(.fail, "Error: \(status)") + return + } + // update db, mark record as shared + rec.uploadkey = json["key"] as? String ?? "_" + self?.record = rec // in case view is still open + RecordingsDB.update(rec) // rec cause self may not be available + self?.sendButton.tintColor = .gray + // notify user about results + if v == 1, let urlStr = json["url"] as? String { + let nextUpdateIn = json["when"] as? Int + self?.showAlertAvailableSoon(urlStr, when: nextUpdateIn) + } + self?.banner(.ok, "Thank you for your contribution.") + } + }.resume() + } + + + // MARK: - Alerts & Banner + + private func banner(_ style: NotificationBanner.Style, _ msg: String) { + NotificationBanner(msg, style: style).present(in: self) + } + + private func showAlertAvailableSoon(_ urlStr: String, when: Int?) { + var msg = "Your contribution is being processed and will be available " + if let when = when { + if when < 61 { + msg += "in approx. \(when) sec. " + } else { + let fmt = TimeFormat.from(Timestamp(when)) + msg += "in \(fmt) min. " + } + } else { + msg += "shortly. " + } + msg += "Open results webpage now?" + AskAlert(title: "Thank you", text: msg, buttonText: "Show results", cancelButton: "Not now") { _ in + if let url = URL(string: urlStr) { + UIApplication.shared.openURL(url) + } + }.presentIn(self) + } + + private func showAlertAlreadyShared() { + let alert = Alert(title: nil, text: "You already shared this recording.") + if let bid = record.appId, bid.isValidBundleId() { + alert.addAction(UIAlertAction.init(title: "Open results", style: .default, handler: { _ in + URL(string: "https://appchk.de/redirect.html?id=\(bid)")?.open() + })) + } + alert.presentIn(self) + } +} diff --git a/main/Recordings/VCEditText.swift b/main/Recordings/VCEditText.swift new file mode 100644 index 0000000..d68758a --- /dev/null +++ b/main/Recordings/VCEditText.swift @@ -0,0 +1,39 @@ +import UIKit + +protocol VCEditTextDelegate { + func editText(didFinish text: String) +} + +class VCEditText: UIViewController, UITextViewDelegate { + + var text: String! + var delegate: VCEditTextDelegate! + + @IBOutlet private var textView: UITextView! + @IBOutlet private var textBottom: NSLayoutConstraint! + + override func viewDidLoad() { + super.viewDidLoad() + textView.text = text + textView.becomeFirstResponder() + + UIResponder.keyboardWillShowNotification.observe(call: #selector(keyboardWillShow), on: self) + UIResponder.keyboardWillHideNotification.observe(call: #selector(keyboardWillHide), on: self) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + delegate.editText(didFinish: textView.text) + } + + + // MARK: - Adapt to Keyboard + + @objc func keyboardWillShow(_ notification: NSNotification) { + textBottom.constant = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue.height ?? 0 + } + + @objc func keyboardWillHide(_ notification: NSNotification) { + textBottom.constant = 0 + } +} diff --git a/main/Recordings/VCShareRecording.swift b/main/Recordings/VCShareRecording.swift deleted file mode 100644 index 2c3bfd5..0000000 --- a/main/Recordings/VCShareRecording.swift +++ /dev/null @@ -1,159 +0,0 @@ -import UIKit - -class VCShareRecording : UIViewController { - - var record: Recording! - private var jsonData: Data? - - @IBOutlet private var text : UITextView! - @IBOutlet private var sendButton: UIBarButtonItem! - @IBOutlet private var sendActivity : UIActivityIndicatorView! - - override func viewDidLoad() { - super.viewDidLoad() - - if record.isShared { - sendButton.tintColor = .gray - } - - let start = record.start - let comp = Calendar.current.dateComponents([.weekOfYear, .yearForWeekOfYear], from: Date(start)) - let wkYear = "\(comp.yearForWeekOfYear ?? 0).\(comp.weekOfYear ?? 0)" - let lenSec = record.duration - - let res = RecordingsDB.details(record) - var cluster: [String : [Timestamp]] = [:] - for (dom, ts) in res { - if cluster[dom] == nil { - cluster[dom] = [] - } - cluster[dom]?.append(ts - start) - } - let domList = cluster.reduce("") { - $0 + "\($1.key) : \($1.value.map{"\($0)"}.joined(separator: ", "))\n" - } - text.attributedText = NSMutableAttributedString() - .h2("Review before sending\n") - .normal("\nRead carefully. " + - "You are about to upload the following information to our servers. " + - "The data is anonymized in regards to device identifiers and time of recording. " + - "It is however not anonymous to the domains requested during the recording." + - "\n\n" + - "If necessary, you can cancel this dialog and return to the recording overview. " + - "Use swipe to delete individual domains." + - "\n\n") - .bold("Send to server:\n") - .italic("\nDate: ", .callout).bold(wkYear, .callout) - .italic("\nRec-Length: ", .callout).bold("\(lenSec) sec", .callout) - .italic("\nApp-Bundle: ", .callout).bold(record.appId ?? "–", .callout) - .italic("\nApp-Name: ", .callout).bold(record.title ?? "–", .callout) - .italic("\n\n[domain name] : [relative time offsets]\n", .callout) - .bold(domList, .callout) - - let json: [String : Any] = [ - "v" : 1, - "date" : wkYear, - "duration" : lenSec, - "app-bundle" : record.appId ?? "", - "app-name" : record.title ?? "", - "logs" : cluster - ] - jsonData = try? JSONSerialization.data(withJSONObject: json) - } - - @IBAction private func closeView() { - dismiss(animated: true) - } - - @IBAction private func shareRecording(_ sender: UIBarButtonItem) { - guard !record.isShared else { - showAlertAlreadyShared() - return - } - sender.isEnabled = false - sendActivity.startAnimating() - postToServer() { [weak self, weak sender] in - self?.sendActivity.stopAnimating() - sender?.isEnabled = true - } - } - - private func postToServer(_ onceLoaded: @escaping () -> Void) { - let url = URL(string: "http://127.0.0.1/api/v1/contribute/")! - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.httpBody = jsonData - var rec = record! // store temporarily so self can be released - - URLSession.shared.dataTask(with: request) { data, response, error in - DispatchQueue.main.async { [weak self] in - onceLoaded() - guard error == nil, let data = data, - let response = response as? HTTPURLResponse else { - self?.banner(.fail, "\(error?.localizedDescription ?? "Unkown error occurred")") - return - } - guard let json = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any], - let v = json["v"] as? Int, v > 0 else { - QLog.Warning("Couldn't contribute: Not JSON or no version key") - self?.banner(.fail, "Server couldn't parse request.\nTry again later.") - return - } - let status = json["status"] as? String ?? "unkown reason" - guard status == "ok", (200 ... 299) ~= response.statusCode else { - QLog.Warning("Couldn't contribute: \(status)") - self?.banner(.fail, "Error: \(status)") - return - } - // update db, mark record as shared - rec.uploadkey = json["key"] as? String ?? "_" - self?.record = rec // in case view is still open - RecordingsDB.update(rec) // rec cause self may not be available - self?.sendButton.tintColor = .gray - // notify user about results - var autoHide = true - if v == 1, let urlStr = json["url"] as? String { - let nextUpdateIn = json["when"] as? Int - self?.showAlertAvailableSoon(urlStr, when: nextUpdateIn) - autoHide = false - } - self?.banner(.ok, "Thank you for your contribution.", - autoHide ? { [weak self] in self?.closeView() } : nil) - } - }.resume() - } - - private func banner(_ style: NotificationBanner.Style, _ msg: String, _ closure: (() -> Void)? = nil) { - NotificationBanner(msg, style: style).present(in: self, onClose: closure) - } - - private func showAlertAvailableSoon(_ urlStr: String, when: Int?) { - var msg = "Your contribution is being processed and will be available " - if let when = when { - if when < 61 { - msg += "in approx. \(when) sec. " - } else { - let fmt = TimeFormat.from(Timestamp(when)) - msg += "in \(fmt) min. " - } - } else { - msg += "shortly. " - } - msg += "Open results webpage now?" - AskAlert(title: "Thank you", text: msg, buttonText: "Show results", cancelButton: "Not now") { _ in - if let url = URL(string: urlStr) { - UIApplication.shared.openURL(url) - } - }.presentIn(self) - } - - private func showAlertAlreadyShared() { - let alert = Alert(title: nil, text: "You already shared this recording.") - if let bid = record.appId, bid.isValidBundleId() { - alert.addAction(UIAlertAction.init(title: "Open results", style: .default, handler: { _ in - URL(string: "http://127.0.0.1/redirect.html?id=\(bid)")?.open() - })) - } - alert.presentIn(self) - } -}