Discard recording if time criteria not met

This commit is contained in:
relikd
2020-08-31 12:18:36 +02:00
parent 7b7c5f3d9a
commit ff4218981f
10 changed files with 106 additions and 58 deletions

View File

@@ -23,10 +23,19 @@ enum PrefsShared {
get { Int("AutoDeleteLogsDays") } get { Int("AutoDeleteLogsDays") }
set { Int("AutoDeleteLogsDays", newValue) } set { Int("AutoDeleteLogsDays", newValue) }
} }
}
static var CurrentlyRecording: Bool {
get { Bool("CurrentlyRecording") }
set { Bool("CurrentlyRecording", newValue) } // MARK: - Recording State
enum CurrentRecordingState : Int {
case Off = 0, App = 1, Background = 2
}
extension PrefsShared {
static var CurrentlyRecording: CurrentRecordingState {
get { CurrentRecordingState(rawValue: Int("CurrentlyRecording")) ?? .Off }
set { Int("CurrentlyRecording", newValue.rawValue) }
} }
} }

View File

@@ -36,8 +36,8 @@ extension Recording {
var fallbackTitle: String { get { var fallbackTitle: String { get {
isLongTerm ? "Background Recording" : "Unnamed Recording #\(id)" isLongTerm ? "Background Recording" : "Unnamed Recording #\(id)"
} } } }
var duration: Timestamp? { get { stop == nil ? nil : stop! - start } } var duration: Timestamp { get { stop ?? .now() - start } }
var isLongTerm: Bool { (duration ?? 0) > Timestamp.hours(1) } var isLongTerm: Bool { duration > Timestamp.hours(1) }
var isShared: Bool { uploadkey?.count ?? 0 > 0} var isShared: Bool { uploadkey?.count ?? 0 > 0}
} }

View File

@@ -86,17 +86,21 @@ struct TimeFormat {
} }
/// Duration string with format `mm:ss` or `mm:ss.SSS` /// Duration string with format `mm:ss` or `mm:ss.SSS`
static func from(_ duration: TimeInterval, millis: Bool = false) -> String { static func from(_ duration: TimeInterval, millis: Bool = false, hours: Bool = false) -> String {
let t = Int(duration) let t = Int(duration)
let min = t / 60
let sec = t % 60
if millis { if millis {
let mil = Int(duration * 1000) % 1000 let mil = Int(duration * 1000) % 1000
return String(format: "%02d:%02d.%03d", t / 60, t % 60, mil) return String(format: "%02d:%02d.%03d", min, sec, mil)
} else if hours {
return String(format: "%02d:%02d:%02d", min / 60, min % 60, sec)
} }
return String(format: "%02d:%02d", t / 60, t % 60) return String(format: "%02d:%02d", min, sec)
} }
/// Duration string with format `mm:ss` or `mm:ss.SSS` since reference date /// Duration string with format `mm:ss` or `mm:ss.SSS` or `HH:mm:ss` since reference date
static func since(_ date: Date, millis: Bool = false) -> String { static func since(_ date: Date, millis: Bool = false, hours: Bool = false) -> String {
from(Date().timeIntervalSince(date), millis: millis) from(Date().timeIntervalSince(date), millis: millis, hours: hours)
} }
} }

View File

@@ -72,7 +72,7 @@
<rect key="frame" x="0.0" y="95" width="320" height="54.5"/> <rect key="frame" x="0.0" y="95" width="320" height="54.5"/>
<subviews> <subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="00:00.000" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rbR-np-cXD"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="00:00.000" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rbR-np-cXD">
<rect key="frame" x="8" y="8" width="200" height="38"/> <rect key="frame" x="8" y="8" width="200" height="38.5"/>
<accessibility key="accessibilityConfiguration"> <accessibility key="accessibilityConfiguration">
<accessibilityTraits key="traits" staticText="YES" updatesFrequently="YES"/> <accessibilityTraits key="traits" staticText="YES" updatesFrequently="YES"/>
</accessibility> </accessibility>
@@ -81,7 +81,7 @@
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="vAq-EZ-Gmx"> <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="vAq-EZ-Gmx">
<rect key="frame" x="212" y="8" width="100" height="38"/> <rect key="frame" x="212" y="8" width="100" height="38.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/> <fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
<state key="normal" title="Stop"> <state key="normal" title="Stop">
<color key="titleColor" systemColor="systemRedColor" red="1" green="0.23137254900000001" blue="0.18823529410000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="titleColor" systemColor="systemRedColor" red="1" green="0.23137254900000001" blue="0.18823529410000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@@ -128,6 +128,7 @@
<outlet property="buttonView" destination="La3-9e-6TK" id="UMg-xx-6OV"/> <outlet property="buttonView" destination="La3-9e-6TK" id="UMg-xx-6OV"/>
<outlet property="headerView" destination="ppJ-js-Wwz" id="68u-8M-R2Q"/> <outlet property="headerView" destination="ppJ-js-Wwz" id="68u-8M-R2Q"/>
<outlet property="runningView" destination="9Yj-FX-eFd" id="L2C-YR-2HN"/> <outlet property="runningView" destination="9Yj-FX-eFd" id="L2C-YR-2HN"/>
<outlet property="stopButton" destination="vAq-EZ-Gmx" id="XiW-1H-I9y"/>
<outlet property="timeLabel" destination="rbR-np-cXD" id="EEe-8F-HT6"/> <outlet property="timeLabel" destination="rbR-np-cXD" id="EEe-8F-HT6"/>
</connections> </connections>
</viewController> </viewController>

View File

@@ -158,7 +158,7 @@ struct VPNAppMessage {
.init("notify-prefs-change:1") .init("notify-prefs-change:1")
} }
/// Triggered whenever user taps on the start/stop recording button /// Triggered whenever user taps on the start/stop recording button
static func isRecording(_ state: Bool) -> Self { static func isRecording(_ state: CurrentRecordingState) -> Self {
.init("recording-now:\(state ? 1 : 0)") .init("recording-now:\(state.rawValue)")
} }
} }

View File

@@ -17,7 +17,7 @@ class GlassVPNHook {
reloadDomainFilter() reloadDomainFilter()
setAutoDelete(PrefsShared.AutoDeleteLogsDays) setAutoDelete(PrefsShared.AutoDeleteLogsDays)
cachedNotify = CachedConnectionAlert() cachedNotify = CachedConnectionAlert()
currentlyRecording = PrefsShared.CurrentlyRecording currentlyRecording = PrefsShared.CurrentlyRecording != .Off
} }
/// Invalidate auto-delete timer and release stored properties. You should nullify this instance afterwards. /// Invalidate auto-delete timer and release stored properties. You should nullify this instance afterwards.
@@ -47,7 +47,8 @@ class GlassVPNHook {
cachedNotify = CachedConnectionAlert() cachedNotify = CachedConnectionAlert()
return return
case "recording-now": case "recording-now":
currentlyRecording = value == "1" let newState = CurrentRecordingState(rawValue: Int(value) ?? 0)
currentlyRecording = newState != .Off
return return
default: break default: break
} }

View File

@@ -76,7 +76,7 @@ class TVCPreviousRecords: UITableViewController, EditActionsRemove {
let x = dataSource[indexPath.row] let x = dataSource[indexPath.row]
cell.textLabel?.text = x.title ?? x.fallbackTitle cell.textLabel?.text = x.title ?? x.fallbackTitle
cell.textLabel?.textColor = (x.title == nil) ? .systemGray : nil cell.textLabel?.textColor = (x.title == nil) ? .systemGray : nil
cell.detailTextLabel?.text = "\(x.isShared ? "" : "")at \(DateFormat.minutes(x.start)), duration: \(TimeFormat.from(x.duration ?? 0))" cell.detailTextLabel?.text = "\(x.isShared ? "" : "")at \(DateFormat.minutes(x.start)), duration: \(TimeFormat.from(x.duration))"
cell.imageView?.image = x.isLongTerm ? nil : BundleIcon.image(x.appId) cell.imageView?.image = x.isLongTerm ? nil : BundleIcon.image(x.appId)
cell.imageView?.layer.cornerRadius = 6.75 cell.imageView?.layer.cornerRadius = 6.75
cell.imageView?.layer.masksToBounds = true cell.imageView?.layer.masksToBounds = true

View File

@@ -41,7 +41,7 @@ class VCEditRecording: UIViewController, UITextFieldDelegate, UITextViewDelegate
inputDetails.text = """ inputDetails.text = """
Start: \(DateFormat.seconds(record.start)) Start: \(DateFormat.seconds(record.start))
End: \(record.stop == nil ? "?" : DateFormat.seconds(record.stop!)) End: \(record.stop == nil ? "?" : DateFormat.seconds(record.stop!))
Duration: \(TimeFormat.from(record.duration ?? 0)) Duration: \(TimeFormat.from(record.duration))
""" """
validateSaveButton() validateSaveButton()
if deleteOnCancel { // mark as destructive if deleteOnCancel { // mark as destructive

View File

@@ -3,17 +3,22 @@ import UIKit
class VCRecordings: UIViewController, UINavigationControllerDelegate { class VCRecordings: UIViewController, UINavigationControllerDelegate {
private var currentRecording: Recording? private var currentRecording: Recording?
private var recordingTimer: Timer? private var recordingTimer: Timer?
private var state: CurrentRecordingState = .Off
@IBOutlet private var headerView: UIView! @IBOutlet private var headerView: UIView!
@IBOutlet private var buttonView: UIView! @IBOutlet private var buttonView: UIView!
@IBOutlet private var runningView: UIView! @IBOutlet private var runningView: UIView!
@IBOutlet private var timeLabel: UILabel! @IBOutlet private var timeLabel: UILabel!
@IBOutlet private var stopButton: UIButton!
override func viewDidLoad() { override func viewDidLoad() {
timeLabel.font = timeLabel.font.monoSpace() timeLabel.font = timeLabel.font.monoSpace()
if let ongoing = RecordingsDB.getCurrent() { if let ongoing = RecordingsDB.getCurrent() {
currentRecording = ongoing currentRecording = ongoing
startTimer(animate: false) // Currently this class is the only one that changes the state,
// if that ever changes, make sure to update local state as well
state = PrefsShared.CurrentlyRecording
startTimer(animate: false, longterm: state == .Background)
} else { // hide timer if not running } else { // hide timer if not running
updateUI(setRecording: false, animated: false) updateUI(setRecording: false, animated: false)
} }
@@ -50,44 +55,36 @@ class VCRecordings: UIViewController, UINavigationControllerDelegate {
} }
currentRecording = RecordingsDB.startNew() currentRecording = RecordingsDB.startNew()
QLog.Debug("start recording #\(currentRecording!.id)") QLog.Debug("start recording #\(currentRecording!.id)")
startTimer(animate: true) let longterm = sender.selectedSegmentIndex == 1
notifyVPN(setRecording: true) startTimer(animate: true, longterm: longterm)
notifyVPN(setRecording: longterm ? .Background : .App)
} }
@IBAction private func stopRecording(_ sender: UIButton) { @IBAction private func stopRecording(_ sender: UIButton) {
notifyVPN(setRecording: false) let validRecording = (state == .Background) == currentRecording!.isLongTerm
stopTimer(animate: true) notifyVPN(setRecording: .Off) // will change state = .Off
RecordingsDB.stop(&currentRecording!) stopTimer()
QLog.Debug("stop recording #\(currentRecording!.id)") QLog.Debug("stop recording #\(currentRecording!.id)")
let editVC = (children.first as! TVCPreviousRecords) RecordingsDB.stop(&currentRecording!)
editVC.insertAndEditRecording(currentRecording!) if validRecording {
let editVC = (children.first as! TVCPreviousRecords)
editVC.insertAndEditRecording(currentRecording!)
} else {
QLog.Debug("Discard illegal recording #\(currentRecording!.id)")
RecordingsDB.delete(currentRecording!)
}
currentRecording = nil // otherwise it will restart currentRecording = nil // otherwise it will restart
} }
private func notifyVPN(setRecording state: Bool) { private func notifyVPN(setRecording state: CurrentRecordingState) {
PrefsShared.CurrentlyRecording = state PrefsShared.CurrentlyRecording = state
self.state = state
GlassVPN.send(.isRecording(state)) GlassVPN.send(.isRecording(state))
} }
private func startTimer(animate: Bool) {
guard let r = currentRecording, r.stop == nil else {
return
}
recordingTimer = Timer.repeating(0.086, call: #selector(timerCallback(_:)), on: self, userInfo: Date(r.start))
updateUI(setRecording: true, animated: animate)
}
@objc private func timerCallback(_ sender: Timer) {
timeLabel.text = TimeFormat.since(sender.userInfo as! Date, millis: true)
}
private func stopTimer(animate: Bool) {
recordingTimer?.invalidate()
recordingTimer = nil
updateUI(setRecording: false, animated: animate)
}
private func updateUI(setRecording: Bool, animated: Bool) { private func updateUI(setRecording: Bool, animated: Bool) {
stopButton.tag = 99 // tag used in timerCallback()
stopButton.setTitle("", for: .normal) // prevent flashing while animating in and out
let block = { let block = {
self.headerView.isHidden = setRecording self.headerView.isHidden = setRecording
self.buttonView.isHidden = setRecording self.buttonView.isHidden = setRecording
@@ -96,6 +93,34 @@ class VCRecordings: UIViewController, UINavigationControllerDelegate {
animated ? UIView.animate(withDuration: 0.3, animations: block) : block() animated ? UIView.animate(withDuration: 0.3, animations: block) : block()
} }
private func startTimer(animate: Bool, longterm: Bool) {
guard let r = currentRecording, r.stop == nil else {
return
}
updateUI(setRecording: true, animated: animate)
let freq = longterm ? 1 : 0.086
let obj = (longterm, Date(r.start))
recordingTimer = Timer.repeating(freq, call: #selector(timerCallback(_:)), on: self, userInfo: obj)
recordingTimer!.fire() // update label immediately
}
private func stopTimer() {
recordingTimer?.invalidate()
recordingTimer = nil
updateUI(setRecording: false, animated: true)
}
@objc private func timerCallback(_ sender: Timer) {
let (slow, start) = sender.userInfo as! (Bool, Date)
timeLabel.text = TimeFormat.since(start, millis: !slow, hours: slow)
let valid = slow == currentRecording!.isLongTerm
let validInt = (valid ? 1 : 0)
if stopButton.tag != validInt {
stopButton.tag = validInt
stopButton.setTitle(valid ? "Stop" : slow ? "Cancel" : "Discard", for: .normal)
}
}
// MARK: Tutorial View Controller // MARK: Tutorial View Controller
@@ -103,13 +128,21 @@ class VCRecordings: UIViewController, UINavigationControllerDelegate {
let x = TutorialSheet() let x = TutorialSheet()
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString() x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString()
.h1("How to record?\n") .h1("How to record?\n")
.normal("\nBefore you begin a new recording make sure that you quit all running applications. " + .normal("\nThere are two types: specific app recordings and general background activity. " +
"Tap on the 'Start Recording' button and switch to the application you'd like to inspect. " + "The former are usually 3  5 minutes long, the latter need to be at least an hour long.")
"Use the App as you would normally. Try to get to all corners and functionality the App provides. " + .h2("\n\nApp recording\n")
"When you feel that you have captured enough content, come back to ").italic("AppCheck").normal(" and stop the recording." + .normal("Before you begin make sure that you quit all running applications and wait a few seconds. " +
"\n\n" + "Tap on the 'App' recording button and switch to the application you'd like to inspect. " +
"Upon completion you will find your recording in the section below. " + "Use the App as you would normally. Try to get to all corners and functionality the App provides. " +
"You can review your results and remove user specific information if necessary.") "When you feel that you have captured enough content, come back to ").italic("AppCheck").normal(" and stop the recording.")
.h2("\n\nBackground recording\n")
.normal("Will answer one simple question: What communications happen while you aren't using your device. " +
"You should solely start a background recording when you know you aren't going to use your device in the near future. " +
"For example, before you go to bed.\n" +
"As soon as you start using your device, you should stop the recording to avoid distorting the results.")
.h2("\n\nFinish\n")
.normal("Upon completion you will find your recording in the section below. " +
"You can review your results and remove any user specific information if necessary.\n")
)) ))
x.buttonTitleDone = "Close" x.buttonTitleDone = "Close"
x.present() x.present()
@@ -120,10 +153,10 @@ class VCRecordings: UIViewController, UINavigationControllerDelegate {
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString() x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString()
.h1("What are Recordings?\n") .h1("What are Recordings?\n")
.normal("\nSimilar to the default logging, recordings will intercept every request and log it for later review. " + .normal("\nSimilar to the default logging, recordings will intercept every request and log it for later review. " +
"Recordings are usually 3  5 minutes long and cover a single application. " + "App recordings are usually 3  5 minutes long and cover a single application. " +
"You can utilize recordings for App analysis or to get a ground truth for background traffic." + "You can utilize recordings for App analysis or to get a ground truth on background traffic." +
"\n\n" + "\n\n" +
"Optionally, you can help us by providing app specific recordings. " + "Optionally, you can help us by providing your app specific recordings. " +
"Together with your findings we can create a community driven privacy monitor. " + "Together with your findings we can create a community driven privacy monitor. " +
"The research results will help you and others avoid Apps that unnecessarily share data with third-party providers.") "The research results will help you and others avoid Apps that unnecessarily share data with third-party providers.")
)) ))

View File

@@ -19,7 +19,7 @@ class VCShareRecording : UIViewController {
let start = record.start let start = record.start
let comp = Calendar.current.dateComponents([.weekOfYear, .yearForWeekOfYear], from: Date(start)) let comp = Calendar.current.dateComponents([.weekOfYear, .yearForWeekOfYear], from: Date(start))
let wkYear = "\(comp.yearForWeekOfYear ?? 0).\(comp.weekOfYear ?? 0)" let wkYear = "\(comp.yearForWeekOfYear ?? 0).\(comp.weekOfYear ?? 0)"
let lenSec = record.duration ?? 0 let lenSec = record.duration
let res = RecordingsDB.details(record) let res = RecordingsDB.details(record)
var cluster: [String : [Timestamp]] = [:] var cluster: [String : [Timestamp]] = [:]