Co-Occurrence tutorial sheet + small bugfixes

This commit is contained in:
relikd
2020-06-26 20:26:30 +02:00
parent 96656438c6
commit 0b6dbfd888
6 changed files with 137 additions and 21 deletions

View File

@@ -461,6 +461,15 @@
<action selector="didClose:" destination="W5Q-oz-bFb" id="wyw-vo-6xL"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" id="bTi-7F-CFS">
<button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="infoLight" showsTouchWhenHighlighted="YES" lineBreakMode="middleTruncation" id="kqK-SL-CxZ">
<rect key="frame" x="279" y="16" width="25" height="24"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<connections>
<action selector="showInfoScreen" destination="W5Q-oz-bFb" eventType="touchUpInside" id="TuI-R9-PNr"/>
</connections>
</button>
</barButtonItem>
</navigationItem>
</items>
</navigationBar>
@@ -502,14 +511,14 @@
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" horizontalCompressionResistancePriority="300" insetsLayoutMarginsFromSafeArea="NO" text="Count" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JWp-6l-HTJ">
<rect key="frame" x="116.5" y="42.5" width="37" height="16"/>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" horizontalCompressionResistancePriority="500" insetsLayoutMarginsFromSafeArea="NO" text="Count" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JWp-6l-HTJ">
<rect key="frame" x="109.5" y="42.5" width="37" height="16"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="5900" textAlignment="natural" lineBreakMode="clip" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="q5v-FM-iGo" customClass="TagLabel" customModule="AppCheck" customModuleProvider="target">
<rect key="frame" x="157.5" y="39.5" width="32.5" height="21.5"/>
<rect key="frame" x="150.5" y="39.5" width="42.5" height="21.5"/>
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<nil key="textColor"/>
@@ -521,7 +530,7 @@
</userDefinedRuntimeAttributes>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="10.35s" textAlignment="natural" lineBreakMode="clip" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="zCg-I0-4Tz" customClass="TagLabel" customModule="AppCheck" customModuleProvider="target">
<rect key="frame" x="265" y="39.5" width="40" height="21.5"/>
<rect key="frame" x="255.5" y="39.5" width="49.5" height="21.5"/>
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<nil key="textColor"/>
@@ -532,14 +541,14 @@
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" horizontalHuggingPriority="750" horizontalCompressionResistancePriority="250" insetsLayoutMarginsFromSafeArea="NO" text="Divergent" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="T4X-cn-msT">
<rect key="frame" x="202" y="42.5" width="59" height="16"/>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" horizontalHuggingPriority="750" horizontalCompressionResistancePriority="400" insetsLayoutMarginsFromSafeArea="NO" text="Diverge" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="T4X-cn-msT">
<rect key="frame" x="205" y="42.5" width="46.5" height="16"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<color key="textColor" systemColor="secondaryLabelColor" red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<view opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="9Bb-e5-D3O" customClass="MeterBar" customModule="AppCheck" customModuleProvider="target">
<rect key="frame" x="187" y="39.5" width="3" height="21.5"/>
<rect key="frame" x="190" y="39.5" width="3" height="21.5"/>
<constraints>
<constraint firstAttribute="width" constant="3" id="wWb-VG-Kqa"/>
</constraints>

View File

@@ -36,6 +36,8 @@ struct QuickUI {
static func text(attributed: NSAttributedString, frame: CGRect = CGRect.zero) -> UITextView {
let txt = self.text("", frame: frame)
txt.attributedText = attributed
txt.textContainerInset = .zero
//txt.textContainer.lineFragmentPadding = 0 // remove left right padding
return txt
}
}

View File

@@ -1,11 +1,18 @@
import UIKit
fileprivate let margin: CGFloat = 20
fileprivate let cornerRadius: CGFloat = 15
fileprivate let uniRect = CGRect(x: 0, y: 0, width: 500, height: 500)
fileprivate var margin: CGFloat { 20 }
fileprivate var sheetInset: CGFloat { cornerRadius/2 }
fileprivate var cornerRadius: CGFloat { 15 }
fileprivate var uniRect: CGRect { CGRect(x: 0, y: 0, width: 500, height: 500) }
class TutorialSheet: UIViewController, UIScrollViewDelegate {
/// Maximum displayable width of a Tutorial Sheet in portrait mode.
public static var verticalWidth: CGFloat {
let s = UIScreen.main.bounds.size
return min(s.width, s.height) - 2 * (margin + sheetInset)
}
public var buttonTitleNext: String = "Next"
public var buttonTitleDone: String = "Close"
@@ -105,7 +112,8 @@ class TutorialSheet: UIViewController, UIScrollViewDelegate {
}
let prev = content.subviews.last
content.addSubview(x)
x.anchor([.top, .width, .height], to: pageScroll)
x.anchor([.top, .height], to: pageScroll)
x.widthAnchor =&= sheetBg.widthAnchor - 2 * sheetInset
x.leadingAnchor =&= (prev==nil ? content.leadingAnchor : prev!.trailingAnchor)
lastAnchor?.isActive = false
lastAnchor = (x.trailingAnchor =&= pageScroll.trailingAnchor)
@@ -125,7 +133,7 @@ class TutorialSheet: UIViewController, UIScrollViewDelegate {
pager.anchor([.top, .left, .right], to: sheetBg)
pageScroll.topAnchor =&= pager.bottomAnchor
pageScroll.anchor([.left, .right, .top, .bottom], to: sheetBg, margin: cornerRadius/2) | .defaultHigh
pageScroll.anchor([.left, .right, .top, .bottom], to: sheetBg, margin: sheetInset) | .defaultHigh
button.topAnchor =&= pageScroll.bottomAnchor
button.anchor([.bottom, .centerX], to: sheetBg)
// button.bottomAnchor =&= sheetBg.bottomAnchor - 30

View File

@@ -159,7 +159,6 @@ class SyncUpdate {
notify(insert: r, .Latest)
}
} else if range != nil {
// FIXME: removing latest entries will invalidate "last changed" label
if let r = rows(from(new!), to(old), scope: range!) {
notify(remove: r, .Latest)
}

View File

@@ -13,6 +13,14 @@ extension UIFont {
}
}
extension NSAttributedString {
static func image(_ img: UIImage) -> Self {
let att = NSTextAttachment()
att.image = img
return self.init(attachment: att)
}
}
extension NSMutableAttributedString {
static private var def: UIFont = .preferredFont(forTextStyle: .body)
@@ -31,4 +39,13 @@ extension NSMutableAttributedString {
]))
return self
}
func centered(_ content: NSAttributedString) -> Self {
let before = length
append(content)
let ps = NSMutableParagraphStyle()
ps.alignment = .center
addAttribute(.paragraphStyle, value: ps, range: .init(location: before, length: content.length))
return self
}
}

View File

@@ -33,15 +33,24 @@ class VCCoOccurrence: UIViewController, UITableViewDataSource {
let domain = fqdn!
let time = Timestamp(selectedTime)
DispatchQueue.global().async { [weak self] in
guard let db = AppDB, let times = db.dnsLogsUniqTs(domain), times.count > 0 else {
return // should never happen, or what did you tap then?
let temp: [ContextAnalysisResult]
let total: Int32
if let db = AppDB,
let times = db.dnsLogsUniqTs(domain), times.count > 0,
let result = db.contextAnalysis(coOccurrence: times, plusMinus: time, exclude: domain),
result.count > 0
{
temp = result
var sum: Int32 = 0
for x in result { sum += x.count }
total = sum // if statement guarantees >= 1
} else {
temp = []
total = 1
}
guard let result = db.contextAnalysis(coOccurrence: times, plusMinus: time, exclude: domain) else {
return
}
self?.dataSource = result
self?.logMaxCount = log(CGFloat(result.reduce(0) { max($0, $1.count) }))
DispatchQueue.main.sync { [weak self] in
self?.dataSource = temp
self?.logMaxCount = log(CGFloat(total + 1))
self?.tableView.reloadData()
}
}
@@ -72,7 +81,9 @@ class VCCoOccurrence: UIViewController, UITableViewDataSource {
cell.count.text = "\(src.count)"
cell.avgdiff.text = String(format: "%.2fs", src.avg)
cell.countMeter.percent = (log(CGFloat(src.count)) / logMaxCount)
// log percentage of total co-occurrence count + 1 (min: log(2))
cell.countMeter.percent = (log(CGFloat(src.count + 1)) / logMaxCount)
// log percentage of selected time window (0s/5s/15s/30s) + 1 (min: log(2))
cell.avgdiffMeter.percent = 1 - (log(CGFloat(src.avg + 1)) / logTimeDelta)
return cell
}
@@ -86,3 +97,73 @@ class CoOccurrenceCell: UITableViewCell {
@IBOutlet var countMeter: MeterBar!
@IBOutlet var avgdiffMeter: MeterBar!
}
// MARK: - Tutorial Screen
extension VCCoOccurrence {
@IBAction func showInfoScreen() {
let sampleCell: UIImage = {
let cell = tableView.dequeueReusableCell(withIdentifier: "CoOccurrenceCell") as! CoOccurrenceCell
cell.title.text = "example.org"
cell.rank.text = "9."
cell.count.text = "14"
cell.avgdiff.text = String(format: "%.2fs", 0.71)
cell.countMeter.percent = 0.35
cell.avgdiffMeter.percent = 0.95
// Bug: Sometimes dequeue will return a "broken" hidden cell.
// It can't be set visible and thus can't render an image.
// Funnily `cell.contentView` can rendered.
let theView = cell.isHidden ? cell.contentView : cell
// resize view to fit into tutorial sheet
let minWidth = TutorialSheet.verticalWidth - 10 //-> 2 * textContainer.lineFragmentPadding
theView.frame.size = theView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
theView.frame.size.width = min(theView.frame.size.width, minWidth)
// set width in two steps because first call may change layoutMargins
theView.frame.size.width += theView.layoutMargins.left + theView.layoutMargins.right
// FIXME: In case `hidden == false`, backgroundColor will be black in Dark mode.
theView.backgroundColor = tableView.backgroundColor
return theView.asImage(insets: theView.layoutMargins)
}()
let x = TutorialSheet()
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString()
.h3("Co-Occurrence")
.normal(" allows you to find requests that happen often at the same time as the selected domain. " +
"Hence it will give you a hint what Apps might be involved in the activity." +
"\n\nHow do you interpret these results? Lets look at an example:\n\n")
.centered(.image(sampleCell))
.normal("\n\nThe domain ").bold("example.org").normal(" had ").bold("14").normal(" requests with an ").italic("average time divergence").normal(" of ").bold("0.71 seconds").normal(". " +
"That is, these 14 domain calls happend, on average, less then a second before or after the original request of the selected domain." +
"\n\nClose temporal proximity and high occurrence counts are both indicators for domain correlation. " +
"Results are sorted by a ranking index (").bold("9.").normal(") which strikes a balance between the two. " +
"Preferring entries with higher counts as well as low time divergence.")
.italic("\n\nTip: ").normal("As a visual guide you can look for the colored bar beside each value. " +
"The larger the bar, the greater the correlation.")
))
x.present(in: self)
}
}
fileprivate extension UIView {
func asImage(insets: UIEdgeInsets = .zero) -> UIImage {
if #available(iOS 10.0, *) {
let renderer = UIGraphicsImageRenderer(bounds: bounds.inset(by: insets))
return renderer.image { rendererContext in
layer.render(in: rendererContext.cgContext)
}
} else {
UIGraphicsBeginImageContext(bounds.inset(by: insets).size)
let ctx = UIGraphicsGetCurrentContext()!
ctx.translateBy(x: -insets.left, y: -insets.top)
layer.render(in:ctx)
let image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return UIImage(cgImage: image!.cgImage!)
}
}
}