import UIKit class TVCShareRecording : UITableViewController, UITextViewDelegate, VCEditTextDelegate { @IBOutlet private var sendButton: UIBarButtonItem! // vars var record: Recording! private var shareNotes: Bool = true // green switch is more present 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)] = [ ("Notes", " – "), // see delegate below, update reloadNotes() and cellForRowAt ("Date", self.weekInYear), ("Rec-Length", "\(self.record.duration) sec"), ("App-Bundle", self.record.appId ?? " – "), ("App-Name", self.record.title ?? " – "), ("iOS", UIDevice.current.systemVersion), ] 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: 0, 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 } } override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { if motion == .motionShake, let key = record.uploadkey { UIPasteboard.general.string = key banner(.ok, "Copied to clipboard", timeout: 1) } } // 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 = indexPath.row == 0 && shareNotes && hasNotes 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 allowed = dataSourceLogs.filter{ $0.enabled }.map{ $0.domain } let json = try? JSONSerialization.data(withJSONObject: [ "v" : 1, "ios" : UIDevice.current.systemVersion, "date" : weekInYear, "duration" : record.duration, "app-bundle" : record.appId ?? "", "app-name" : record.title ?? "", "notes" : shareNotes ? editedNotes : "", "logs" : dataSource.filter{ allowed.contains($0.key) } ]) // 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, timeout: TimeInterval = 3) { NotificationBanner(msg, style: style).present(in: navigationController!, hideAfter: timeout) } 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) } }