59 Commits

Author SHA1 Message Date
relikd
e54d69ef4b Version 1.0.0 (34) 2020-09-19 14:35:26 +02:00
relikd
be8269ad56 Include iOS version in json 2020-09-19 14:25:02 +02:00
relikd
7118ec3b02 Update to Xcode 12 2020-09-17 16:41:18 +02:00
relikd
71045bf0dd Ignore forceDisconnect on background recording 2020-09-17 13:46:18 +02:00
relikd
27abdd66f5 Display long domain names – two lines everywhere 2020-09-14 22:46:58 +02:00
relikd
162e18c912 Cleanup NEKit 2020-09-14 21:10:03 +02:00
relikd
d68e4ec869 Version 1.0.0 (33) 2020-09-14 12:56:30 +02:00
relikd
762263bfbd Default disconnect swdc + pre-check connect message 2020-09-14 12:56:06 +02:00
relikd
b1cddc796e Version 1.0.0 (32) 2020-09-14 11:52:21 +02:00
relikd
77e20f31f5 Persist recording logs in background 2020-09-14 11:51:44 +02:00
relikd
0175f5390e Fix crash trying to access userInfo 2020-09-13 12:13:14 +02:00
relikd
effc305b86 Version 1.0.0 (31) 2020-09-12 22:28:32 +02:00
relikd
c1fe258b0d Force disconnect to prevent domain spamming (optional in advanced settings) 2020-09-12 22:28:11 +02:00
relikd
36a8f0b97b Version 1.0.0 (30) 2020-09-12 11:35:11 +02:00
relikd
33b9cab8a8 Indicate background recording needs more time 2020-09-12 11:32:06 +02:00
relikd
b88874b38b Version 1.0.0 (29) 2020-09-12 10:57:20 +02:00
relikd
f55f3ea32d Disable copy menu on meta cells in 5 min context 2020-09-12 10:42:37 +02:00
relikd
c843bd76a2 Share notes opt-out, assuming notes are created for upload anyway 2020-09-12 10:31:13 +02:00
relikd
4dd2339ed8 Set recording segment color to indicate tap action 2020-09-12 10:27:04 +02:00
relikd
280526bef4 Hide filter button if new recording 2020-09-12 10:04:24 +02:00
relikd
34caffd4a7 Change tutorial text about app recording length 2020-09-12 09:57:53 +02:00
relikd
9e19b457e2 AppStore search: sort local apps case independent 2020-09-11 15:39:23 +02:00
relikd
e6846953b7 Copy upload key to clipboard 2020-09-08 18:35:30 +02:00
relikd
6d78aeac7b Fix header banner display issues 2020-09-08 18:16:21 +02:00
relikd
5d94fe3a0d Version 1.0.0 (28) 2020-09-08 11:56:02 +02:00
relikd
fb680d669b Fix crash on loading App Store search results 2020-09-08 11:52:07 +02:00
relikd
6409e5eaf3 Allow to contribute empty recordings 2020-09-08 04:28:07 +02:00
relikd
39ca9dbdb1 Persist recording logs before save operation (crash-safe) 2020-09-08 03:16:38 +02:00
relikd
27ab2a621a Set recording time as filter 2020-09-08 02:51:25 +02:00
relikd
3f572eeb15 Version 1.0.0 (27) 2020-09-06 10:13:01 +02:00
relikd
e83540d5de Fix empty json log 2020-09-05 23:32:57 +02:00
relikd
847556bec1 Add important notice to app recording 2020-09-05 22:26:04 +02:00
relikd
42b045fb85 Open co-occurrence from recording 2020-09-05 22:07:22 +02:00
relikd
35a211f87f Fix action target self-reference timing issues 2020-09-05 22:05:56 +02:00
relikd
d2fa67e0e3 Reduce redundant code, cell copy menu 2020-09-05 21:05:12 +02:00
relikd
b8660c9a35 Jump from Recordings to Requests tab 2020-09-05 20:08:37 +02:00
relikd
8cd3f7fb3a Fix iOS 10 layout issues 2020-09-04 09:14:35 +02:00
relikd
2ee0272a05 Improve recording contribution view. Replace TextView with interactive TableView. 2020-09-04 09:14:23 +02:00
relikd
4ae82fc763 Show recording how-to at least once after app install 2020-08-31 23:02:46 +02:00
relikd
aac42d7eff Fix duration 2020-08-31 22:42:55 +02:00
relikd
8bb77ef741 Tiny markdown parser, makes tutorial screens editing much simpler 2020-08-31 17:10:11 +02:00
relikd
ff4218981f Discard recording if time criteria not met 2020-08-31 12:18:36 +02:00
relikd
7b7c5f3d9a UI app recording vs. background recording 2020-08-30 00:03:15 +02:00
relikd
1c203e39c3 Fix iOS 10 Tutorial sheet top padding missing 2020-08-29 23:46:13 +02:00
relikd
7dbf21d564 Disable block & ignore filter during recording 2020-08-29 18:36:41 +02:00
relikd
8fcb5ad874 No VPN, no recording 2020-08-29 17:49:30 +02:00
relikd
b4bf705b7f Rename column uploadkey 2020-08-29 14:48:53 +02:00
relikd
69d8321180 check status 'ok' 2020-08-29 14:44:40 +02:00
relikd
b03daeca66 Store sharing key instead of just a bool 2020-08-28 23:41:08 +02:00
relikd
c502484bcf Indicate shared on recordings overview + move isShared check to sharing sheet 2020-08-28 23:02:10 +02:00
relikd
448d69c6d8 Show "no results" in recordings + mark recording as shared 2020-08-28 22:05:49 +02:00
relikd
42aa7cf926 Contribute recording 2020-08-28 18:36:52 +02:00
relikd
52fa2e460e Fix: Wait for busy lock instead of instantly dropping the operation 2020-08-24 00:58:50 +02:00
relikd
8855ae754a Fix: Auto-delete logs did not clear heap 2020-08-24 00:56:14 +02:00
relikd
908a909c87 Share results screen 2020-08-12 18:15:32 +02:00
relikd
41aee797a9 Split storyboard tabs 2020-08-11 20:10:13 +02:00
relikd
685f636d5b Recordings: Choose app instead of custom title 2020-08-11 19:21:07 +02:00
relikd
4af56b0cb1 Reverting to single step persist 2020-08-01 09:42:57 +02:00
relikd
a3973c7e9a Embed Recordings in navigation controller, not the other way around 2020-08-01 09:33:48 +02:00
103 changed files with 3403 additions and 3045 deletions

View File

@@ -70,6 +70,14 @@
545DDDD124436983003B6544 /* QuickUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDD024436983003B6544 /* QuickUI.swift */; }; 545DDDD124436983003B6544 /* QuickUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDD024436983003B6544 /* QuickUI.swift */; };
545DDDD424466D37003B6544 /* AutoLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDD324466D37003B6544 /* AutoLayout.swift */; }; 545DDDD424466D37003B6544 /* AutoLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDD324466D37003B6544 /* AutoLayout.swift */; };
546063E523FEFAFE008F505A /* DBCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B7562223D7B2DC008F0C41 /* DBCore.swift */; }; 546063E523FEFAFE008F505A /* DBCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B7562223D7B2DC008F0C41 /* DBCore.swift */; };
54686A7624F8062C0084934D /* NotificationBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54686A7524F8062C0084934D /* NotificationBanner.swift */; };
54686A8524FD0A3F0084934D /* tut-recording-howto.md in Resources */ = {isa = PBXBuildFile; fileRef = 54686A8424FD0A3F0084934D /* tut-recording-howto.md */; };
54686A8724FD27AA0084934D /* TinyMarkdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54686A8624FD26410084934D /* TinyMarkdown.swift */; };
54686A8D24FD428C0084934D /* tut-welcome-1.md in Resources */ = {isa = PBXBuildFile; fileRef = 54686A8824FD31580084934D /* tut-welcome-1.md */; };
54686A8E24FD42950084934D /* tut-welcome-2.md in Resources */ = {isa = PBXBuildFile; fileRef = 54686A8B24FD3F180084934D /* tut-welcome-2.md */; };
54686A8F24FD42950084934D /* tut-recording-1.md in Resources */ = {isa = PBXBuildFile; fileRef = 54686A8A24FD3F100084934D /* tut-recording-1.md */; };
54686A9024FD42950084934D /* tut-recording-2.md in Resources */ = {isa = PBXBuildFile; fileRef = 54686A8924FD31630084934D /* tut-recording-2.md */; };
54686A9124FD42950084934D /* tut-cooccurrence.md in Resources */ = {isa = PBXBuildFile; fileRef = 54686A8C24FD3F630084934D /* tut-cooccurrence.md */; };
54751E512423955100168273 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54751E502423955000168273 /* URL.swift */; }; 54751E512423955100168273 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54751E502423955000168273 /* URL.swift */; };
54751E522423955100168273 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54751E502423955000168273 /* URL.swift */; }; 54751E522423955100168273 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54751E502423955000168273 /* URL.swift */; };
54953E3323DC752E0054345C /* DBCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B7562223D7B2DC008F0C41 /* DBCore.swift */; }; 54953E3323DC752E0054345C /* DBCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B7562223D7B2DC008F0C41 /* DBCore.swift */; };
@@ -77,7 +85,12 @@
54953E6123E0D69A0054345C /* TVCHosts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54953E6023E0D69A0054345C /* TVCHosts.swift */; }; 54953E6123E0D69A0054345C /* TVCHosts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54953E6023E0D69A0054345C /* TVCHosts.swift */; };
54953E6F23E44CD00054345C /* TVCHostDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54953E6E23E44CD00054345C /* TVCHostDetails.swift */; }; 54953E6F23E44CD00054345C /* TVCHostDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54953E6E23E44CD00054345C /* TVCHostDetails.swift */; };
54953E7123E473F10054345C /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 54953E7023E473F10054345C /* Settings.bundle */; }; 54953E7123E473F10054345C /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 54953E7023E473F10054345C /* Settings.bundle */; };
549A96D62501198400C565FA /* VCEditText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549A96D52501198400C565FA /* VCEditText.swift */; };
549A96DA250419B200C565FA /* CoOccurrence.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 549A96D8250419B200C565FA /* CoOccurrence.storyboard */; };
549D6ED524D5BFDB0032E498 /* TVCAppSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549D6ED424D5BFDB0032E498 /* TVCAppSearch.swift */; };
549ECD9D24A7AD550097571C /* CustomAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549ECD9C24A7AD550097571C /* CustomAlert.swift */; }; 549ECD9D24A7AD550097571C /* CustomAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 549ECD9C24A7AD550097571C /* CustomAlert.swift */; };
54A0CC0924E30C56009B5EC1 /* Recordings.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54A0CC0724E30C56009B5EC1 /* Recordings.storyboard */; };
54A0CC0C24E30D6F009B5EC1 /* Requests.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 54A0CC0A24E30D6F009B5EC1 /* Requests.storyboard */; };
54B34594240E6343004C53CC /* TVCFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B34593240E6343004C53CC /* TVCFilter.swift */; }; 54B34594240E6343004C53CC /* TVCFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B34593240E6343004C53CC /* TVCFilter.swift */; };
54B34596240F0513004C53CC /* TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B34595240F0513004C53CC /* TableView.swift */; }; 54B34596240F0513004C53CC /* TableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B34595240F0513004C53CC /* TableView.swift */; };
54B345A6241BB982004C53CC /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345A5241BB982004C53CC /* Notifications.swift */; }; 54B345A6241BB982004C53CC /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345A5241BB982004C53CC /* Notifications.swift */; };
@@ -85,16 +98,14 @@
54B345AB241BBA5B004C53CC /* AlertSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345AA241BBA5B004C53CC /* AlertSheet.swift */; }; 54B345AB241BBA5B004C53CC /* AlertSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345AA241BBA5B004C53CC /* AlertSheet.swift */; };
54B345AD241BBB00004C53CC /* DBExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345AC241BBB00004C53CC /* DBExtensions.swift */; }; 54B345AD241BBB00004C53CC /* DBExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B345AC241BBB00004C53CC /* DBExtensions.swift */; };
54B345B0242264F8004C53CC /* third-level.txt in Resources */ = {isa = PBXBuildFile; fileRef = 54B345AF242264F8004C53CC /* third-level.txt */; }; 54B345B0242264F8004C53CC /* third-level.txt in Resources */ = {isa = PBXBuildFile; fileRef = 54B345AF242264F8004C53CC /* third-level.txt */; };
54C056DB23E9E36E00214A3F /* AppInfoType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54C056DA23E9E36E00214A3F /* AppInfoType.swift */; }; 54C056DB23E9E36E00214A3F /* AppStoreSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54C056DA23E9E36E00214A3F /* AppStoreSearch.swift */; };
54C056DD23E9EEF700214A3F /* BundleIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54C056DC23E9EEF700214A3F /* BundleIcon.swift */; }; 54C056DD23E9EEF700214A3F /* BundleIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54C056DC23E9EEF700214A3F /* BundleIcon.swift */; };
54CA01D32426B23D003A5E04 /* Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01D22426B23D003A5E04 /* Resolver.swift */; }; 54CA01D32426B23D003A5E04 /* Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01D22426B23D003A5E04 /* Resolver.swift */; };
54CA01D52426B252003A5E04 /* SafeDict.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01D42426B251003A5E04 /* SafeDict.swift */; }; 54CA01D52426B252003A5E04 /* SafeDict.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01D42426B251003A5E04 /* SafeDict.swift */; };
54CA025C2426B2FD003A5E04 /* ConnectSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01E22426B2FC003A5E04 /* ConnectSession.swift */; }; 54CA025C2426B2FD003A5E04 /* ConnectSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01E22426B2FC003A5E04 /* ConnectSession.swift */; };
54CA025D2426B2FD003A5E04 /* HTTPHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01E32426B2FC003A5E04 /* HTTPHeader.swift */; }; 54CA025D2426B2FD003A5E04 /* HTTPHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01E32426B2FC003A5E04 /* HTTPHeader.swift */; };
54CA025E2426B2FD003A5E04 /* ResponseGeneratorFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01E42426B2FC003A5E04 /* ResponseGeneratorFactory.swift */; };
54CA025F2426B2FD003A5E04 /* ProxyServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01E62426B2FC003A5E04 /* ProxyServer.swift */; }; 54CA025F2426B2FD003A5E04 /* ProxyServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01E62426B2FC003A5E04 /* ProxyServer.swift */; };
54CA02602426B2FD003A5E04 /* GCDProxyServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01E72426B2FC003A5E04 /* GCDProxyServer.swift */; }; 54CA02602426B2FD003A5E04 /* GCDProxyServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01E72426B2FC003A5E04 /* GCDProxyServer.swift */; };
54CA02612426B2FD003A5E04 /* GCDSOCKS5ProxyServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01E82426B2FC003A5E04 /* GCDSOCKS5ProxyServer.swift */; };
54CA02622426B2FD003A5E04 /* GCDHTTPProxyServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01E92426B2FC003A5E04 /* GCDHTTPProxyServer.swift */; }; 54CA02622426B2FD003A5E04 /* GCDHTTPProxyServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01E92426B2FC003A5E04 /* GCDHTTPProxyServer.swift */; };
54CA02662426B2FD003A5E04 /* NWUDPSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01EF2426B2FC003A5E04 /* NWUDPSocket.swift */; }; 54CA02662426B2FD003A5E04 /* NWUDPSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01EF2426B2FC003A5E04 /* NWUDPSocket.swift */; };
54CA02672426B2FD003A5E04 /* RawTCPSocketProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01F02426B2FC003A5E04 /* RawTCPSocketProtocol.swift */; }; 54CA02672426B2FD003A5E04 /* RawTCPSocketProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA01F02426B2FC003A5E04 /* RawTCPSocketProtocol.swift */; };
@@ -116,15 +127,8 @@
54CA027B2426B2FD003A5E04 /* HTTPAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02062426B2FC003A5E04 /* HTTPAuthentication.swift */; }; 54CA027B2426B2FD003A5E04 /* HTTPAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02062426B2FC003A5E04 /* HTTPAuthentication.swift */; };
54CA027C2426B2FD003A5E04 /* StreamScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02072426B2FC003A5E04 /* StreamScanner.swift */; }; 54CA027C2426B2FD003A5E04 /* StreamScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02072426B2FC003A5E04 /* StreamScanner.swift */; };
54CA027D2426B2FD003A5E04 /* GlobalIntializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02082426B2FC003A5E04 /* GlobalIntializer.swift */; }; 54CA027D2426B2FD003A5E04 /* GlobalIntializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02082426B2FC003A5E04 /* GlobalIntializer.swift */; };
54CA027E2426B2FD003A5E04 /* DomainListRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA020A2426B2FC003A5E04 /* DomainListRule.swift */; };
54CA02802426B2FD003A5E04 /* DNSSessionMatchType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA020C2426B2FC003A5E04 /* DNSSessionMatchType.swift */; }; 54CA02802426B2FD003A5E04 /* DNSSessionMatchType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA020C2426B2FC003A5E04 /* DNSSessionMatchType.swift */; };
54CA02812426B2FD003A5E04 /* DNSFailRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA020D2426B2FC003A5E04 /* DNSFailRule.swift */; };
54CA02822426B2FD003A5E04 /* AllRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA020E2426B2FC003A5E04 /* AllRule.swift */; };
54CA02832426B2FD003A5E04 /* DNSSessionMatchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA020F2426B2FC003A5E04 /* DNSSessionMatchResult.swift */; }; 54CA02832426B2FD003A5E04 /* DNSSessionMatchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA020F2426B2FC003A5E04 /* DNSSessionMatchResult.swift */; };
54CA02842426B2FD003A5E04 /* Rule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02102426B2FC003A5E04 /* Rule.swift */; };
54CA02852426B2FD003A5E04 /* DirectRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02112426B2FC003A5E04 /* DirectRule.swift */; };
54CA02862426B2FD003A5E04 /* RuleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02122426B2FC003A5E04 /* RuleManager.swift */; };
54CA02872426B2FD003A5E04 /* IPRangeListRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02132426B2FC003A5E04 /* IPRangeListRule.swift */; };
54CA02882426B2FD003A5E04 /* QueueFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02152426B2FC003A5E04 /* QueueFactory.swift */; }; 54CA02882426B2FD003A5E04 /* QueueFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02152426B2FC003A5E04 /* QueueFactory.swift */; };
54CA02892426B2FD003A5E04 /* Tunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02162426B2FC003A5E04 /* Tunnel.swift */; }; 54CA02892426B2FD003A5E04 /* Tunnel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02162426B2FC003A5E04 /* Tunnel.swift */; };
54CA028A2426B2FD003A5E04 /* ResponseGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02172426B2FC003A5E04 /* ResponseGenerator.swift */; }; 54CA028A2426B2FD003A5E04 /* ResponseGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02172426B2FC003A5E04 /* ResponseGenerator.swift */; };
@@ -143,32 +147,19 @@
54CA029E2426B2FD003A5E04 /* EventType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02302426B2FC003A5E04 /* EventType.swift */; }; 54CA029E2426B2FD003A5E04 /* EventType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02302426B2FC003A5E04 /* EventType.swift */; };
54CA029F2426B2FD003A5E04 /* ProxySocketEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02312426B2FC003A5E04 /* ProxySocketEvent.swift */; }; 54CA029F2426B2FD003A5E04 /* ProxySocketEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02312426B2FC003A5E04 /* ProxySocketEvent.swift */; };
54CA02A02426B2FD003A5E04 /* TunnelEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02322426B2FC003A5E04 /* TunnelEvent.swift */; }; 54CA02A02426B2FD003A5E04 /* TunnelEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02322426B2FC003A5E04 /* TunnelEvent.swift */; };
54CA02A12426B2FD003A5E04 /* RuleMatchEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02332426B2FC003A5E04 /* RuleMatchEvent.swift */; };
54CA02A22426B2FD003A5E04 /* ObserverFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02342426B2FC003A5E04 /* ObserverFactory.swift */; }; 54CA02A22426B2FD003A5E04 /* ObserverFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02342426B2FC003A5E04 /* ObserverFactory.swift */; };
54CA02A32426B2FD003A5E04 /* HTTPAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02372426B2FC003A5E04 /* HTTPAdapter.swift */; };
54CA02A42426B2FD003A5E04 /* SecureHTTPAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02382426B2FC003A5E04 /* SecureHTTPAdapter.swift */; };
54CA02A62426B2FD003A5E04 /* AdapterSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA023A2426B2FC003A5E04 /* AdapterSocket.swift */; }; 54CA02A62426B2FD003A5E04 /* AdapterSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA023A2426B2FC003A5E04 /* AdapterSocket.swift */; };
54CA02A72426B2FD003A5E04 /* DirectAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA023B2426B2FC003A5E04 /* DirectAdapter.swift */; }; 54CA02A72426B2FD003A5E04 /* DirectAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA023B2426B2FC003A5E04 /* DirectAdapter.swift */; };
54CA02A82426B2FD003A5E04 /* SOCKS5Adapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA023C2426B2FC003A5E04 /* SOCKS5Adapter.swift */; };
54CA02A92426B2FD003A5E04 /* RejectAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA023D2426B2FC003A5E04 /* RejectAdapter.swift */; };
54CA02AC2426B2FD003A5E04 /* AuthenticationServerAdapterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02412426B2FC003A5E04 /* AuthenticationServerAdapterFactory.swift */; };
54CA02AD2426B2FD003A5E04 /* RejectAdapterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02422426B2FC003A5E04 /* RejectAdapterFactory.swift */; };
54CA02AE2426B2FD003A5E04 /* AdapterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02432426B2FD003A5E04 /* AdapterFactory.swift */; }; 54CA02AE2426B2FD003A5E04 /* AdapterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02432426B2FD003A5E04 /* AdapterFactory.swift */; };
54CA02AF2426B2FD003A5E04 /* SOCKS5AdapterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02442426B2FD003A5E04 /* SOCKS5AdapterFactory.swift */; };
54CA02B02426B2FD003A5E04 /* SecureHTTPAdapterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02452426B2FD003A5E04 /* SecureHTTPAdapterFactory.swift */; };
54CA02B12426B2FD003A5E04 /* ServerAdapterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02462426B2FD003A5E04 /* ServerAdapterFactory.swift */; };
54CA02B22426B2FD003A5E04 /* AdapterFactoryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02472426B2FD003A5E04 /* AdapterFactoryManager.swift */; };
54CA02B32426B2FD003A5E04 /* HTTPAdapterFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02482426B2FD003A5E04 /* HTTPAdapterFactory.swift */; };
54CA02B82426B2FD003A5E04 /* HTTPProxySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA024F2426B2FD003A5E04 /* HTTPProxySocket.swift */; }; 54CA02B82426B2FD003A5E04 /* HTTPProxySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA024F2426B2FD003A5E04 /* HTTPProxySocket.swift */; };
54CA02B92426B2FD003A5E04 /* DirectProxySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02502426B2FD003A5E04 /* DirectProxySocket.swift */; };
54CA02BA2426B2FD003A5E04 /* ProxySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02512426B2FD003A5E04 /* ProxySocket.swift */; }; 54CA02BA2426B2FD003A5E04 /* ProxySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02512426B2FD003A5E04 /* ProxySocket.swift */; };
54CA02BB2426B2FD003A5E04 /* SOCKS5ProxySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02522426B2FD003A5E04 /* SOCKS5ProxySocket.swift */; };
54CA02BC2426B2FD003A5E04 /* SocketProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02532426B2FD003A5E04 /* SocketProtocol.swift */; }; 54CA02BC2426B2FD003A5E04 /* SocketProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02532426B2FD003A5E04 /* SocketProtocol.swift */; };
54CA02BE2426D4F3003A5E04 /* DDLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02BD2426D4F3003A5E04 /* DDLog.swift */; }; 54CA02BE2426D4F3003A5E04 /* DDLog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02BD2426D4F3003A5E04 /* DDLog.swift */; };
54CA02C32426DCCD003A5E04 /* GCDAsyncSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02BF2426DCCC003A5E04 /* GCDAsyncSocket.m */; }; 54CA02C32426DCCD003A5E04 /* GCDAsyncSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02BF2426DCCC003A5E04 /* GCDAsyncSocket.m */; };
54CA02C42426DCCD003A5E04 /* GCDAsyncUdpSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02C02426DCCD003A5E04 /* GCDAsyncUdpSocket.m */; }; 54CA02C42426DCCD003A5E04 /* GCDAsyncUdpSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 54CA02C02426DCCD003A5E04 /* GCDAsyncUdpSocket.m */; };
54CE8BC424B1ED2100CC1756 /* PushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CE8BC324B1ED2100CC1756 /* PushNotification.swift */; }; 54CE8BC424B1ED2100CC1756 /* PushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CE8BC324B1ED2100CC1756 /* PushNotification.swift */; };
54CE8BC524B1ED2100CC1756 /* PushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CE8BC324B1ED2100CC1756 /* PushNotification.swift */; }; 54CE8BC524B1ED2100CC1756 /* PushNotification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CE8BC324B1ED2100CC1756 /* PushNotification.swift */; };
54CFE86824E3F401001687DD /* TVCShareRecording.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54CFE86724E3F401001687DD /* TVCShareRecording.swift */; };
54D8B97A246C9F2000EB2414 /* FilterPipeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B979246C9F2000EB2414 /* FilterPipeline.swift */; }; 54D8B97A246C9F2000EB2414 /* FilterPipeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B979246C9F2000EB2414 /* FilterPipeline.swift */; };
54D8B97C2471A7E000EB2414 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B97B2471A7E000EB2414 /* String.swift */; }; 54D8B97C2471A7E000EB2414 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B97B2471A7E000EB2414 /* String.swift */; };
54D8B97E2471B88900EB2414 /* DBCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B97D2471B88900EB2414 /* DBCommon.swift */; }; 54D8B97E2471B88900EB2414 /* DBCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54D8B97D2471B88900EB2414 /* DBCommon.swift */; };
@@ -264,13 +255,26 @@
545DDDCE243E6267003B6544 /* TutorialSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialSheet.swift; sourceTree = "<group>"; }; 545DDDCE243E6267003B6544 /* TutorialSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialSheet.swift; sourceTree = "<group>"; };
545DDDD024436983003B6544 /* QuickUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickUI.swift; sourceTree = "<group>"; }; 545DDDD024436983003B6544 /* QuickUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickUI.swift; sourceTree = "<group>"; };
545DDDD324466D37003B6544 /* AutoLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoLayout.swift; sourceTree = "<group>"; }; 545DDDD324466D37003B6544 /* AutoLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoLayout.swift; sourceTree = "<group>"; };
54686A7524F8062C0084934D /* NotificationBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationBanner.swift; sourceTree = "<group>"; };
54686A8424FD0A3F0084934D /* tut-recording-howto.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = "tut-recording-howto.md"; sourceTree = "<group>"; };
54686A8624FD26410084934D /* TinyMarkdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TinyMarkdown.swift; sourceTree = "<group>"; };
54686A8824FD31580084934D /* tut-welcome-1.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "tut-welcome-1.md"; sourceTree = "<group>"; };
54686A8924FD31630084934D /* tut-recording-2.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "tut-recording-2.md"; sourceTree = "<group>"; };
54686A8A24FD3F100084934D /* tut-recording-1.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "tut-recording-1.md"; sourceTree = "<group>"; };
54686A8B24FD3F180084934D /* tut-welcome-2.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "tut-welcome-2.md"; sourceTree = "<group>"; };
54686A8C24FD3F630084934D /* tut-cooccurrence.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "tut-cooccurrence.md"; sourceTree = "<group>"; };
54751E502423955000168273 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; }; 54751E502423955000168273 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; };
548B1F9423D338EC005B047C /* main.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = main.entitlements; sourceTree = "<group>"; }; 548B1F9423D338EC005B047C /* main.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = main.entitlements; sourceTree = "<group>"; };
54953E5E23DEBE840054345C /* TVCDomains.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCDomains.swift; sourceTree = "<group>"; }; 54953E5E23DEBE840054345C /* TVCDomains.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCDomains.swift; sourceTree = "<group>"; };
54953E6023E0D69A0054345C /* TVCHosts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCHosts.swift; sourceTree = "<group>"; }; 54953E6023E0D69A0054345C /* TVCHosts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCHosts.swift; sourceTree = "<group>"; };
54953E6E23E44CD00054345C /* TVCHostDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCHostDetails.swift; sourceTree = "<group>"; }; 54953E6E23E44CD00054345C /* TVCHostDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCHostDetails.swift; sourceTree = "<group>"; };
54953E7023E473F10054345C /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; }; 54953E7023E473F10054345C /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
549A96D52501198400C565FA /* VCEditText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VCEditText.swift; sourceTree = "<group>"; };
549A96D9250419B200C565FA /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/CoOccurrence.storyboard; sourceTree = "<group>"; };
549D6ED424D5BFDB0032E498 /* TVCAppSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCAppSearch.swift; sourceTree = "<group>"; };
549ECD9C24A7AD550097571C /* CustomAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlert.swift; sourceTree = "<group>"; }; 549ECD9C24A7AD550097571C /* CustomAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomAlert.swift; sourceTree = "<group>"; };
54A0CC0824E30C56009B5EC1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Recordings.storyboard; sourceTree = "<group>"; };
54A0CC0B24E30D6F009B5EC1 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Requests.storyboard; sourceTree = "<group>"; };
54B34593240E6343004C53CC /* TVCFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCFilter.swift; sourceTree = "<group>"; }; 54B34593240E6343004C53CC /* TVCFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCFilter.swift; sourceTree = "<group>"; };
54B34595240F0513004C53CC /* TableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableView.swift; sourceTree = "<group>"; }; 54B34595240F0513004C53CC /* TableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableView.swift; sourceTree = "<group>"; };
54B345A5241BB982004C53CC /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; }; 54B345A5241BB982004C53CC /* Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
@@ -279,17 +283,15 @@
54B345AC241BBB00004C53CC /* DBExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBExtensions.swift; sourceTree = "<group>"; }; 54B345AC241BBB00004C53CC /* DBExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBExtensions.swift; sourceTree = "<group>"; };
54B345AF242264F8004C53CC /* third-level.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "third-level.txt"; sourceTree = "<group>"; }; 54B345AF242264F8004C53CC /* third-level.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = "third-level.txt"; sourceTree = "<group>"; };
54B7562223D7B2DC008F0C41 /* DBCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBCore.swift; sourceTree = "<group>"; }; 54B7562223D7B2DC008F0C41 /* DBCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBCore.swift; sourceTree = "<group>"; };
54C056DA23E9E36E00214A3F /* AppInfoType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoType.swift; sourceTree = "<group>"; }; 54C056DA23E9E36E00214A3F /* AppStoreSearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreSearch.swift; sourceTree = "<group>"; };
54C056DC23E9EEF700214A3F /* BundleIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIcon.swift; sourceTree = "<group>"; }; 54C056DC23E9EEF700214A3F /* BundleIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleIcon.swift; sourceTree = "<group>"; };
54CA00D62426A803003A5E04 /* CocoaAsyncSocket.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CocoaAsyncSocket.h; sourceTree = "<group>"; }; 54CA00D62426A803003A5E04 /* CocoaAsyncSocket.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CocoaAsyncSocket.h; sourceTree = "<group>"; };
54CA01D22426B23D003A5E04 /* Resolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Resolver.swift; sourceTree = "<group>"; }; 54CA01D22426B23D003A5E04 /* Resolver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Resolver.swift; sourceTree = "<group>"; };
54CA01D42426B251003A5E04 /* SafeDict.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SafeDict.swift; sourceTree = "<group>"; }; 54CA01D42426B251003A5E04 /* SafeDict.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SafeDict.swift; sourceTree = "<group>"; };
54CA01E22426B2FC003A5E04 /* ConnectSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectSession.swift; sourceTree = "<group>"; }; 54CA01E22426B2FC003A5E04 /* ConnectSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConnectSession.swift; sourceTree = "<group>"; };
54CA01E32426B2FC003A5E04 /* HTTPHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPHeader.swift; sourceTree = "<group>"; }; 54CA01E32426B2FC003A5E04 /* HTTPHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPHeader.swift; sourceTree = "<group>"; };
54CA01E42426B2FC003A5E04 /* ResponseGeneratorFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseGeneratorFactory.swift; sourceTree = "<group>"; };
54CA01E62426B2FC003A5E04 /* ProxyServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxyServer.swift; sourceTree = "<group>"; }; 54CA01E62426B2FC003A5E04 /* ProxyServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxyServer.swift; sourceTree = "<group>"; };
54CA01E72426B2FC003A5E04 /* GCDProxyServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GCDProxyServer.swift; sourceTree = "<group>"; }; 54CA01E72426B2FC003A5E04 /* GCDProxyServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GCDProxyServer.swift; sourceTree = "<group>"; };
54CA01E82426B2FC003A5E04 /* GCDSOCKS5ProxyServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GCDSOCKS5ProxyServer.swift; sourceTree = "<group>"; };
54CA01E92426B2FC003A5E04 /* GCDHTTPProxyServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GCDHTTPProxyServer.swift; sourceTree = "<group>"; }; 54CA01E92426B2FC003A5E04 /* GCDHTTPProxyServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GCDHTTPProxyServer.swift; sourceTree = "<group>"; };
54CA01EF2426B2FC003A5E04 /* NWUDPSocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NWUDPSocket.swift; sourceTree = "<group>"; }; 54CA01EF2426B2FC003A5E04 /* NWUDPSocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NWUDPSocket.swift; sourceTree = "<group>"; };
54CA01F02426B2FC003A5E04 /* RawTCPSocketProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RawTCPSocketProtocol.swift; sourceTree = "<group>"; }; 54CA01F02426B2FC003A5E04 /* RawTCPSocketProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RawTCPSocketProtocol.swift; sourceTree = "<group>"; };
@@ -311,15 +313,8 @@
54CA02062426B2FC003A5E04 /* HTTPAuthentication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPAuthentication.swift; sourceTree = "<group>"; }; 54CA02062426B2FC003A5E04 /* HTTPAuthentication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPAuthentication.swift; sourceTree = "<group>"; };
54CA02072426B2FC003A5E04 /* StreamScanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StreamScanner.swift; sourceTree = "<group>"; }; 54CA02072426B2FC003A5E04 /* StreamScanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StreamScanner.swift; sourceTree = "<group>"; };
54CA02082426B2FC003A5E04 /* GlobalIntializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalIntializer.swift; sourceTree = "<group>"; }; 54CA02082426B2FC003A5E04 /* GlobalIntializer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalIntializer.swift; sourceTree = "<group>"; };
54CA020A2426B2FC003A5E04 /* DomainListRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DomainListRule.swift; sourceTree = "<group>"; };
54CA020C2426B2FC003A5E04 /* DNSSessionMatchType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DNSSessionMatchType.swift; sourceTree = "<group>"; }; 54CA020C2426B2FC003A5E04 /* DNSSessionMatchType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DNSSessionMatchType.swift; sourceTree = "<group>"; };
54CA020D2426B2FC003A5E04 /* DNSFailRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DNSFailRule.swift; sourceTree = "<group>"; };
54CA020E2426B2FC003A5E04 /* AllRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AllRule.swift; sourceTree = "<group>"; };
54CA020F2426B2FC003A5E04 /* DNSSessionMatchResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DNSSessionMatchResult.swift; sourceTree = "<group>"; }; 54CA020F2426B2FC003A5E04 /* DNSSessionMatchResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DNSSessionMatchResult.swift; sourceTree = "<group>"; };
54CA02102426B2FC003A5E04 /* Rule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Rule.swift; sourceTree = "<group>"; };
54CA02112426B2FC003A5E04 /* DirectRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectRule.swift; sourceTree = "<group>"; };
54CA02122426B2FC003A5E04 /* RuleManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RuleManager.swift; sourceTree = "<group>"; };
54CA02132426B2FC003A5E04 /* IPRangeListRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IPRangeListRule.swift; sourceTree = "<group>"; };
54CA02152426B2FC003A5E04 /* QueueFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueueFactory.swift; sourceTree = "<group>"; }; 54CA02152426B2FC003A5E04 /* QueueFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueueFactory.swift; sourceTree = "<group>"; };
54CA02162426B2FC003A5E04 /* Tunnel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tunnel.swift; sourceTree = "<group>"; }; 54CA02162426B2FC003A5E04 /* Tunnel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Tunnel.swift; sourceTree = "<group>"; };
54CA02172426B2FC003A5E04 /* ResponseGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseGenerator.swift; sourceTree = "<group>"; }; 54CA02172426B2FC003A5E04 /* ResponseGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseGenerator.swift; sourceTree = "<group>"; };
@@ -338,26 +333,12 @@
54CA02302426B2FC003A5E04 /* EventType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventType.swift; sourceTree = "<group>"; }; 54CA02302426B2FC003A5E04 /* EventType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventType.swift; sourceTree = "<group>"; };
54CA02312426B2FC003A5E04 /* ProxySocketEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxySocketEvent.swift; sourceTree = "<group>"; }; 54CA02312426B2FC003A5E04 /* ProxySocketEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxySocketEvent.swift; sourceTree = "<group>"; };
54CA02322426B2FC003A5E04 /* TunnelEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelEvent.swift; sourceTree = "<group>"; }; 54CA02322426B2FC003A5E04 /* TunnelEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TunnelEvent.swift; sourceTree = "<group>"; };
54CA02332426B2FC003A5E04 /* RuleMatchEvent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RuleMatchEvent.swift; sourceTree = "<group>"; };
54CA02342426B2FC003A5E04 /* ObserverFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObserverFactory.swift; sourceTree = "<group>"; }; 54CA02342426B2FC003A5E04 /* ObserverFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObserverFactory.swift; sourceTree = "<group>"; };
54CA02372426B2FC003A5E04 /* HTTPAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPAdapter.swift; sourceTree = "<group>"; };
54CA02382426B2FC003A5E04 /* SecureHTTPAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureHTTPAdapter.swift; sourceTree = "<group>"; };
54CA023A2426B2FC003A5E04 /* AdapterSocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdapterSocket.swift; sourceTree = "<group>"; }; 54CA023A2426B2FC003A5E04 /* AdapterSocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdapterSocket.swift; sourceTree = "<group>"; };
54CA023B2426B2FC003A5E04 /* DirectAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectAdapter.swift; sourceTree = "<group>"; }; 54CA023B2426B2FC003A5E04 /* DirectAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectAdapter.swift; sourceTree = "<group>"; };
54CA023C2426B2FC003A5E04 /* SOCKS5Adapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SOCKS5Adapter.swift; sourceTree = "<group>"; };
54CA023D2426B2FC003A5E04 /* RejectAdapter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RejectAdapter.swift; sourceTree = "<group>"; };
54CA02412426B2FC003A5E04 /* AuthenticationServerAdapterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthenticationServerAdapterFactory.swift; sourceTree = "<group>"; };
54CA02422426B2FC003A5E04 /* RejectAdapterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RejectAdapterFactory.swift; sourceTree = "<group>"; };
54CA02432426B2FD003A5E04 /* AdapterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdapterFactory.swift; sourceTree = "<group>"; }; 54CA02432426B2FD003A5E04 /* AdapterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdapterFactory.swift; sourceTree = "<group>"; };
54CA02442426B2FD003A5E04 /* SOCKS5AdapterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SOCKS5AdapterFactory.swift; sourceTree = "<group>"; };
54CA02452426B2FD003A5E04 /* SecureHTTPAdapterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SecureHTTPAdapterFactory.swift; sourceTree = "<group>"; };
54CA02462426B2FD003A5E04 /* ServerAdapterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServerAdapterFactory.swift; sourceTree = "<group>"; };
54CA02472426B2FD003A5E04 /* AdapterFactoryManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdapterFactoryManager.swift; sourceTree = "<group>"; };
54CA02482426B2FD003A5E04 /* HTTPAdapterFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPAdapterFactory.swift; sourceTree = "<group>"; };
54CA024F2426B2FD003A5E04 /* HTTPProxySocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPProxySocket.swift; sourceTree = "<group>"; }; 54CA024F2426B2FD003A5E04 /* HTTPProxySocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPProxySocket.swift; sourceTree = "<group>"; };
54CA02502426B2FD003A5E04 /* DirectProxySocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectProxySocket.swift; sourceTree = "<group>"; };
54CA02512426B2FD003A5E04 /* ProxySocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxySocket.swift; sourceTree = "<group>"; }; 54CA02512426B2FD003A5E04 /* ProxySocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProxySocket.swift; sourceTree = "<group>"; };
54CA02522426B2FD003A5E04 /* SOCKS5ProxySocket.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SOCKS5ProxySocket.swift; sourceTree = "<group>"; };
54CA02532426B2FD003A5E04 /* SocketProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketProtocol.swift; sourceTree = "<group>"; }; 54CA02532426B2FD003A5E04 /* SocketProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SocketProtocol.swift; sourceTree = "<group>"; };
54CA02BD2426D4F3003A5E04 /* DDLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDLog.swift; sourceTree = "<group>"; }; 54CA02BD2426D4F3003A5E04 /* DDLog.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DDLog.swift; sourceTree = "<group>"; };
54CA02BF2426DCCC003A5E04 /* GCDAsyncSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDAsyncSocket.m; sourceTree = "<group>"; }; 54CA02BF2426DCCC003A5E04 /* GCDAsyncSocket.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GCDAsyncSocket.m; sourceTree = "<group>"; };
@@ -365,6 +346,7 @@
54CA02C12426DCCD003A5E04 /* GCDAsyncSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDAsyncSocket.h; sourceTree = "<group>"; }; 54CA02C12426DCCD003A5E04 /* GCDAsyncSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDAsyncSocket.h; sourceTree = "<group>"; };
54CA02C22426DCCD003A5E04 /* GCDAsyncUdpSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDAsyncUdpSocket.h; sourceTree = "<group>"; }; 54CA02C22426DCCD003A5E04 /* GCDAsyncUdpSocket.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GCDAsyncUdpSocket.h; sourceTree = "<group>"; };
54CE8BC324B1ED2100CC1756 /* PushNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotification.swift; sourceTree = "<group>"; }; 54CE8BC324B1ED2100CC1756 /* PushNotification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotification.swift; sourceTree = "<group>"; };
54CFE86724E3F401001687DD /* TVCShareRecording.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCShareRecording.swift; sourceTree = "<group>"; };
54D8B979246C9F2000EB2414 /* FilterPipeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterPipeline.swift; sourceTree = "<group>"; }; 54D8B979246C9F2000EB2414 /* FilterPipeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterPipeline.swift; sourceTree = "<group>"; };
54D8B97B2471A7E000EB2414 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; }; 54D8B97B2471A7E000EB2414 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
54D8B97D2471B88900EB2414 /* DBCommon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBCommon.swift; sourceTree = "<group>"; }; 54D8B97D2471B88900EB2414 /* DBCommon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBCommon.swift; sourceTree = "<group>"; };
@@ -430,8 +412,12 @@
children = ( children = (
540E677F242D2CF100871BBE /* VCRecordings.swift */, 540E677F242D2CF100871BBE /* VCRecordings.swift */,
540E67832433FAFE00871BBE /* TVCPreviousRecords.swift */, 540E67832433FAFE00871BBE /* TVCPreviousRecords.swift */,
540E67812433483D00871BBE /* VCEditRecording.swift */,
5458EBBF243A3F2200CFEB15 /* TVCRecordingDetails.swift */, 5458EBBF243A3F2200CFEB15 /* TVCRecordingDetails.swift */,
54CFE86724E3F401001687DD /* TVCShareRecording.swift */,
549A96D52501198400C565FA /* VCEditText.swift */,
540E67812433483D00871BBE /* VCEditRecording.swift */,
549D6ED424D5BFDB0032E498 /* TVCAppSearch.swift */,
54B345B12422E029004C53CC /* App Icons */,
); );
path = Recordings; path = Recordings;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -482,10 +468,7 @@
540C6454240D5BAE00E948F9 /* Requests */, 540C6454240D5BAE00E948F9 /* Requests */,
540E677E242D2CD200871BBE /* Recordings */, 540E677E242D2CD200871BBE /* Recordings */,
540C6455240D5BD200E948F9 /* Settings */, 540C6455240D5BD200E948F9 /* Settings */,
54B345B12422E029004C53CC /* unused */, 54A0CC0D24E314B6009B5EC1 /* GUI */,
541AC5E02399498B00A769D7 /* LaunchScreen.storyboard */,
541AC5DB2399498A00A769D7 /* Main.storyboard */,
543078C124B60F3B00278F2D /* Settings.storyboard */,
541AC5DE2399498B00A769D7 /* Assets.xcassets */, 541AC5DE2399498B00A769D7 /* Assets.xcassets */,
541AC5E32399498B00A769D7 /* Info.plist */, 541AC5E32399498B00A769D7 /* Info.plist */,
54953E7023E473F10054345C /* Settings.bundle */, 54953E7023E473F10054345C /* Settings.bundle */,
@@ -505,6 +488,7 @@
542E2A9B24051F79001462DC /* media */ = { 542E2A9B24051F79001462DC /* media */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
54686A8324FD0A3F0084934D /* tutorials */,
5430789E24B5E10E00278F2D /* sounds */, 5430789E24B5E10E00278F2D /* sounds */,
541A957523E602DF00C09C19 /* LaunchIcon.png */, 541A957523E602DF00C09C19 /* LaunchIcon.png */,
54B345AF242264F8004C53CC /* third-level.txt */, 54B345AF242264F8004C53CC /* third-level.txt */,
@@ -551,16 +535,44 @@
54E67E4524A8B0FE0025D261 /* PrefsShared.swift */, 54E67E4524A8B0FE0025D261 /* PrefsShared.swift */,
545DDDD024436983003B6544 /* QuickUI.swift */, 545DDDD024436983003B6544 /* QuickUI.swift */,
545DDDCE243E6267003B6544 /* TutorialSheet.swift */, 545DDDCE243E6267003B6544 /* TutorialSheet.swift */,
54686A8624FD26410084934D /* TinyMarkdown.swift */,
54D8B979246C9F2000EB2414 /* FilterPipeline.swift */, 54D8B979246C9F2000EB2414 /* FilterPipeline.swift */,
54448A3124899A4000771C96 /* SearchBarManager.swift */, 54448A3124899A4000771C96 /* SearchBarManager.swift */,
549ECD9C24A7AD550097571C /* CustomAlert.swift */, 549ECD9C24A7AD550097571C /* CustomAlert.swift */,
541FC47524A12D01009154D8 /* IBViews.swift */, 541FC47524A12D01009154D8 /* IBViews.swift */,
5404AEEC24A95F3F003B2F54 /* SlideInAnimation.swift */, 5404AEEC24A95F3F003B2F54 /* SlideInAnimation.swift */,
541075D024CDBA0000D6F1BF /* ThrottledBatchQueue.swift */, 541075D024CDBA0000D6F1BF /* ThrottledBatchQueue.swift */,
54686A7524F8062C0084934D /* NotificationBanner.swift */,
); );
path = "Common Classes"; path = "Common Classes";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
54686A8324FD0A3F0084934D /* tutorials */ = {
isa = PBXGroup;
children = (
54686A8824FD31580084934D /* tut-welcome-1.md */,
54686A8B24FD3F180084934D /* tut-welcome-2.md */,
54686A8A24FD3F100084934D /* tut-recording-1.md */,
54686A8924FD31630084934D /* tut-recording-2.md */,
54686A8424FD0A3F0084934D /* tut-recording-howto.md */,
54686A8C24FD3F630084934D /* tut-cooccurrence.md */,
);
path = tutorials;
sourceTree = "<group>";
};
54A0CC0D24E314B6009B5EC1 /* GUI */ = {
isa = PBXGroup;
children = (
541AC5E02399498B00A769D7 /* LaunchScreen.storyboard */,
541AC5DB2399498A00A769D7 /* Main.storyboard */,
54A0CC0A24E30D6F009B5EC1 /* Requests.storyboard */,
549A96D8250419B200C565FA /* CoOccurrence.storyboard */,
54A0CC0724E30C56009B5EC1 /* Recordings.storyboard */,
543078C124B60F3B00278F2D /* Settings.storyboard */,
);
path = GUI;
sourceTree = "<group>";
};
54B3459A2415651C004C53CC /* DB */ = { 54B3459A2415651C004C53CC /* DB */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -593,13 +605,13 @@
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
54B345B12422E029004C53CC /* unused */ = { 54B345B12422E029004C53CC /* App Icons */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
54C056DC23E9EEF700214A3F /* BundleIcon.swift */, 54C056DC23E9EEF700214A3F /* BundleIcon.swift */,
54C056DA23E9E36E00214A3F /* AppInfoType.swift */, 54C056DA23E9E36E00214A3F /* AppStoreSearch.swift */,
); );
path = unused; path = "App Icons";
sourceTree = "<group>"; sourceTree = "<group>";
}; };
54CA00D52426A7F2003A5E04 /* robbiehanson-CocoaAsyncSocket */ = { 54CA00D52426A7F2003A5E04 /* robbiehanson-CocoaAsyncSocket */ = {
@@ -627,7 +639,6 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
54CA01E12426B2FC003A5E04 /* Messages */, 54CA01E12426B2FC003A5E04 /* Messages */,
54CA01E42426B2FC003A5E04 /* ResponseGeneratorFactory.swift */,
54CA01E52426B2FC003A5E04 /* ProxyServer */, 54CA01E52426B2FC003A5E04 /* ProxyServer */,
54CA01EE2426B2FC003A5E04 /* RawSocket */, 54CA01EE2426B2FC003A5E04 /* RawSocket */,
54CA01F72426B2FC003A5E04 /* Opt.swift */, 54CA01F72426B2FC003A5E04 /* Opt.swift */,
@@ -658,7 +669,6 @@
children = ( children = (
54CA01E62426B2FC003A5E04 /* ProxyServer.swift */, 54CA01E62426B2FC003A5E04 /* ProxyServer.swift */,
54CA01E72426B2FC003A5E04 /* GCDProxyServer.swift */, 54CA01E72426B2FC003A5E04 /* GCDProxyServer.swift */,
54CA01E82426B2FC003A5E04 /* GCDSOCKS5ProxyServer.swift */,
54CA01E92426B2FC003A5E04 /* GCDHTTPProxyServer.swift */, 54CA01E92426B2FC003A5E04 /* GCDHTTPProxyServer.swift */,
); );
path = ProxyServer; path = ProxyServer;
@@ -699,15 +709,8 @@
54CA02092426B2FC003A5E04 /* Rule */ = { 54CA02092426B2FC003A5E04 /* Rule */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
54CA020A2426B2FC003A5E04 /* DomainListRule.swift */,
54CA020C2426B2FC003A5E04 /* DNSSessionMatchType.swift */, 54CA020C2426B2FC003A5E04 /* DNSSessionMatchType.swift */,
54CA020D2426B2FC003A5E04 /* DNSFailRule.swift */,
54CA020E2426B2FC003A5E04 /* AllRule.swift */,
54CA020F2426B2FC003A5E04 /* DNSSessionMatchResult.swift */, 54CA020F2426B2FC003A5E04 /* DNSSessionMatchResult.swift */,
54CA02102426B2FC003A5E04 /* Rule.swift */,
54CA02112426B2FC003A5E04 /* DirectRule.swift */,
54CA02122426B2FC003A5E04 /* RuleManager.swift */,
54CA02132426B2FC003A5E04 /* IPRangeListRule.swift */,
); );
path = Rule; path = Rule;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -770,7 +773,6 @@
54CA02302426B2FC003A5E04 /* EventType.swift */, 54CA02302426B2FC003A5E04 /* EventType.swift */,
54CA02312426B2FC003A5E04 /* ProxySocketEvent.swift */, 54CA02312426B2FC003A5E04 /* ProxySocketEvent.swift */,
54CA02322426B2FC003A5E04 /* TunnelEvent.swift */, 54CA02322426B2FC003A5E04 /* TunnelEvent.swift */,
54CA02332426B2FC003A5E04 /* RuleMatchEvent.swift */,
); );
path = Event; path = Event;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -788,12 +790,8 @@
54CA02362426B2FC003A5E04 /* AdapterSocket */ = { 54CA02362426B2FC003A5E04 /* AdapterSocket */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
54CA02372426B2FC003A5E04 /* HTTPAdapter.swift */,
54CA02382426B2FC003A5E04 /* SecureHTTPAdapter.swift */,
54CA023A2426B2FC003A5E04 /* AdapterSocket.swift */, 54CA023A2426B2FC003A5E04 /* AdapterSocket.swift */,
54CA023B2426B2FC003A5E04 /* DirectAdapter.swift */, 54CA023B2426B2FC003A5E04 /* DirectAdapter.swift */,
54CA023C2426B2FC003A5E04 /* SOCKS5Adapter.swift */,
54CA023D2426B2FC003A5E04 /* RejectAdapter.swift */,
54CA023E2426B2FC003A5E04 /* Factory */, 54CA023E2426B2FC003A5E04 /* Factory */,
); );
path = AdapterSocket; path = AdapterSocket;
@@ -802,14 +800,7 @@
54CA023E2426B2FC003A5E04 /* Factory */ = { 54CA023E2426B2FC003A5E04 /* Factory */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
54CA02412426B2FC003A5E04 /* AuthenticationServerAdapterFactory.swift */,
54CA02422426B2FC003A5E04 /* RejectAdapterFactory.swift */,
54CA02432426B2FD003A5E04 /* AdapterFactory.swift */, 54CA02432426B2FD003A5E04 /* AdapterFactory.swift */,
54CA02442426B2FD003A5E04 /* SOCKS5AdapterFactory.swift */,
54CA02452426B2FD003A5E04 /* SecureHTTPAdapterFactory.swift */,
54CA02462426B2FD003A5E04 /* ServerAdapterFactory.swift */,
54CA02472426B2FD003A5E04 /* AdapterFactoryManager.swift */,
54CA02482426B2FD003A5E04 /* HTTPAdapterFactory.swift */,
); );
path = Factory; path = Factory;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -818,9 +809,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
54CA024F2426B2FD003A5E04 /* HTTPProxySocket.swift */, 54CA024F2426B2FD003A5E04 /* HTTPProxySocket.swift */,
54CA02502426B2FD003A5E04 /* DirectProxySocket.swift */,
54CA02512426B2FD003A5E04 /* ProxySocket.swift */, 54CA02512426B2FD003A5E04 /* ProxySocket.swift */,
54CA02522426B2FD003A5E04 /* SOCKS5ProxySocket.swift */,
); );
path = ProxySocket; path = ProxySocket;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -887,7 +876,7 @@
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastSwiftUpdateCheck = 1130; LastSwiftUpdateCheck = 1130;
LastUpgradeCheck = 1010; LastUpgradeCheck = 1200;
ORGANIZATIONNAME = relikd; ORGANIZATIONNAME = relikd;
TargetAttributes = { TargetAttributes = {
541AC5D32399498A00A769D7 = { 541AC5D32399498A00A769D7 = {
@@ -933,21 +922,30 @@
files = ( files = (
543078AC24B5E12500278F2D /* typewriter2.caf in Resources */, 543078AC24B5E12500278F2D /* typewriter2.caf in Resources */,
54953E7123E473F10054345C /* Settings.bundle in Resources */, 54953E7123E473F10054345C /* Settings.bundle in Resources */,
54686A9024FD42950084934D /* tut-recording-2.md in Resources */,
543078B024B5E12500278F2D /* plop2.caf in Resources */, 543078B024B5E12500278F2D /* plop2.caf in Resources */,
541AC5E22399498B00A769D7 /* LaunchScreen.storyboard in Resources */, 541AC5E22399498B00A769D7 /* LaunchScreen.storyboard in Resources */,
54A0CC0924E30C56009B5EC1 /* Recordings.storyboard in Resources */,
54686A8D24FD428C0084934D /* tut-welcome-1.md in Resources */,
543078B824B5E12500278F2D /* wood2.caf in Resources */, 543078B824B5E12500278F2D /* wood2.caf in Resources */,
543078BE24B5E12500278F2D /* drum2.caf in Resources */, 543078BE24B5E12500278F2D /* drum2.caf in Resources */,
543078B424B5E12500278F2D /* snap1.caf in Resources */, 543078B424B5E12500278F2D /* snap1.caf in Resources */,
541AC5DF2399498B00A769D7 /* Assets.xcassets in Resources */, 541AC5DF2399498B00A769D7 /* Assets.xcassets in Resources */,
543078AE24B5E12500278F2D /* wood1.caf in Resources */, 543078AE24B5E12500278F2D /* wood1.caf in Resources */,
54686A8524FD0A3F0084934D /* tut-recording-howto.md in Resources */,
541AC5DD2399498A00A769D7 /* Main.storyboard in Resources */, 541AC5DD2399498A00A769D7 /* Main.storyboard in Resources */,
54B345B0242264F8004C53CC /* third-level.txt in Resources */, 54B345B0242264F8004C53CC /* third-level.txt in Resources */,
54686A8F24FD42950084934D /* tut-recording-1.md in Resources */,
543078BA24B5E12500278F2D /* typewriter1.caf in Resources */, 543078BA24B5E12500278F2D /* typewriter1.caf in Resources */,
543078B224B5E12500278F2D /* plop1.caf in Resources */, 543078B224B5E12500278F2D /* plop1.caf in Resources */,
543078B624B5E12500278F2D /* drum1.caf in Resources */, 543078B624B5E12500278F2D /* drum1.caf in Resources */,
543078BC24B5E12500278F2D /* clock.caf in Resources */, 543078BC24B5E12500278F2D /* clock.caf in Resources */,
543078C324B60F3B00278F2D /* Settings.storyboard in Resources */, 543078C324B60F3B00278F2D /* Settings.storyboard in Resources */,
543078AA24B5E12500278F2D /* snap2.caf in Resources */, 543078AA24B5E12500278F2D /* snap2.caf in Resources */,
54A0CC0C24E30D6F009B5EC1 /* Requests.storyboard in Resources */,
549A96DA250419B200C565FA /* CoOccurrence.storyboard in Resources */,
54686A9124FD42950084934D /* tut-cooccurrence.md in Resources */,
54686A8E24FD42950084934D /* tut-welcome-2.md in Resources */,
541A957623E602DF00C09C19 /* LaunchIcon.png in Resources */, 541A957623E602DF00C09C19 /* LaunchIcon.png in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -995,6 +993,7 @@
54B345A6241BB982004C53CC /* Notifications.swift in Sources */, 54B345A6241BB982004C53CC /* Notifications.swift in Sources */,
54448A2E2486464F00771C96 /* Array.swift in Sources */, 54448A2E2486464F00771C96 /* Array.swift in Sources */,
54E67E4B24A8C6370025D261 /* GlassVPN.swift in Sources */, 54E67E4B24A8C6370025D261 /* GlassVPN.swift in Sources */,
54686A7624F8062C0084934D /* NotificationBanner.swift in Sources */,
541FC47824A1453F009154D8 /* VCCoOccurrence.swift in Sources */, 541FC47824A1453F009154D8 /* VCCoOccurrence.swift in Sources */,
54B345AB241BBA5B004C53CC /* AlertSheet.swift in Sources */, 54B345AB241BBA5B004C53CC /* AlertSheet.swift in Sources */,
541DCA6124A6B0F6005F1A4B /* Color.swift in Sources */, 541DCA6124A6B0F6005F1A4B /* Color.swift in Sources */,
@@ -1009,9 +1008,11 @@
544F912024A67EC5001D4B00 /* TVCOccurrenceContext.swift in Sources */, 544F912024A67EC5001D4B00 /* TVCOccurrenceContext.swift in Sources */,
54448A30248647D900771C96 /* Time.swift in Sources */, 54448A30248647D900771C96 /* Time.swift in Sources */,
54953E6123E0D69A0054345C /* TVCHosts.swift in Sources */, 54953E6123E0D69A0054345C /* TVCHosts.swift in Sources */,
549D6ED524D5BFDB0032E498 /* TVCAppSearch.swift in Sources */,
54CFE86824E3F401001687DD /* TVCShareRecording.swift in Sources */,
54751E512423955100168273 /* URL.swift in Sources */, 54751E512423955100168273 /* URL.swift in Sources */,
54953E5F23DEBE840054345C /* TVCDomains.swift in Sources */, 54953E5F23DEBE840054345C /* TVCDomains.swift in Sources */,
54C056DB23E9E36E00214A3F /* AppInfoType.swift in Sources */, 54C056DB23E9E36E00214A3F /* AppStoreSearch.swift in Sources */,
54D8B98624796E9900EB2414 /* GroupedDomainDataSource.swift in Sources */, 54D8B98624796E9900EB2414 /* GroupedDomainDataSource.swift in Sources */,
541075CE24C9D43A00D6F1BF /* UNNotification.swift in Sources */, 541075CE24C9D43A00D6F1BF /* UNNotification.swift in Sources */,
54953E6F23E44CD00054345C /* TVCHostDetails.swift in Sources */, 54953E6F23E44CD00054345C /* TVCHostDetails.swift in Sources */,
@@ -1021,7 +1022,9 @@
542E2A982404973F001462DC /* TBCMain.swift in Sources */, 542E2A982404973F001462DC /* TBCMain.swift in Sources */,
54448A3224899A4000771C96 /* SearchBarManager.swift in Sources */, 54448A3224899A4000771C96 /* SearchBarManager.swift in Sources */,
5458EBC0243A3F2200CFEB15 /* TVCRecordingDetails.swift in Sources */, 5458EBC0243A3F2200CFEB15 /* TVCRecordingDetails.swift in Sources */,
549A96D62501198400C565FA /* VCEditText.swift in Sources */,
541FC9872497D81C00962623 /* TheGreatDestroyer.swift in Sources */, 541FC9872497D81C00962623 /* TheGreatDestroyer.swift in Sources */,
54686A8724FD27AA0084934D /* TinyMarkdown.swift in Sources */,
5412F8EE24571B8200A63D7A /* VCDateFilter.swift in Sources */, 5412F8EE24571B8200A63D7A /* VCDateFilter.swift in Sources */,
5412FCC424C628FA000DE429 /* TVCConnectionAlerts.swift in Sources */, 5412FCC424C628FA000DE429 /* TVCConnectionAlerts.swift in Sources */,
543078C924B75CEA00278F2D /* PushNotificationAppOnly.swift in Sources */, 543078C924B75CEA00278F2D /* PushNotificationAppOnly.swift in Sources */,
@@ -1046,7 +1049,6 @@
54CA027A2426B2FD003A5E04 /* HTTPURL.swift in Sources */, 54CA027A2426B2FD003A5E04 /* HTTPURL.swift in Sources */,
54CA025D2426B2FD003A5E04 /* HTTPHeader.swift in Sources */, 54CA025D2426B2FD003A5E04 /* HTTPHeader.swift in Sources */,
54CA02832426B2FD003A5E04 /* DNSSessionMatchResult.swift in Sources */, 54CA02832426B2FD003A5E04 /* DNSSessionMatchResult.swift in Sources */,
54CA02862426B2FD003A5E04 /* RuleManager.swift in Sources */,
54CA02B82426B2FD003A5E04 /* HTTPProxySocket.swift in Sources */, 54CA02B82426B2FD003A5E04 /* HTTPProxySocket.swift in Sources */,
54CA02C32426DCCD003A5E04 /* GCDAsyncSocket.m in Sources */, 54CA02C32426DCCD003A5E04 /* GCDAsyncSocket.m in Sources */,
54CA02752426B2FD003A5E04 /* IPRange.swift in Sources */, 54CA02752426B2FD003A5E04 /* IPRange.swift in Sources */,
@@ -1056,27 +1058,21 @@
5404AEEB24A90717003B2F54 /* PrefsShared.swift in Sources */, 5404AEEB24A90717003B2F54 /* PrefsShared.swift in Sources */,
54CA02C42426DCCD003A5E04 /* GCDAsyncUdpSocket.m in Sources */, 54CA02C42426DCCD003A5E04 /* GCDAsyncUdpSocket.m in Sources */,
54CA02BA2426B2FD003A5E04 /* ProxySocket.swift in Sources */, 54CA02BA2426B2FD003A5E04 /* ProxySocket.swift in Sources */,
54CA025E2426B2FD003A5E04 /* ResponseGeneratorFactory.swift in Sources */,
54CA02892426B2FD003A5E04 /* Tunnel.swift in Sources */, 54CA02892426B2FD003A5E04 /* Tunnel.swift in Sources */,
54CA029F2426B2FD003A5E04 /* ProxySocketEvent.swift in Sources */, 54CA029F2426B2FD003A5E04 /* ProxySocketEvent.swift in Sources */,
54CA027D2426B2FD003A5E04 /* GlobalIntializer.swift in Sources */, 54CA027D2426B2FD003A5E04 /* GlobalIntializer.swift in Sources */,
54CA026F2426B2FD003A5E04 /* Port.swift in Sources */, 54CA026F2426B2FD003A5E04 /* Port.swift in Sources */,
54CA028A2426B2FD003A5E04 /* ResponseGenerator.swift in Sources */, 54CA028A2426B2FD003A5E04 /* ResponseGenerator.swift in Sources */,
54CA027C2426B2FD003A5E04 /* StreamScanner.swift in Sources */, 54CA027C2426B2FD003A5E04 /* StreamScanner.swift in Sources */,
54CA02AF2426B2FD003A5E04 /* SOCKS5AdapterFactory.swift in Sources */,
54CA029E2426B2FD003A5E04 /* EventType.swift in Sources */, 54CA029E2426B2FD003A5E04 /* EventType.swift in Sources */,
54CA02912426B2FD003A5E04 /* DNSMessage.swift in Sources */, 54CA02912426B2FD003A5E04 /* DNSMessage.swift in Sources */,
54CA02712426B2FD003A5E04 /* UInt128.swift in Sources */, 54CA02712426B2FD003A5E04 /* UInt128.swift in Sources */,
54CA02882426B2FD003A5E04 /* QueueFactory.swift in Sources */, 54CA02882426B2FD003A5E04 /* QueueFactory.swift in Sources */,
54CA02A12426B2FD003A5E04 /* RuleMatchEvent.swift in Sources */,
54CA02BE2426D4F3003A5E04 /* DDLog.swift in Sources */, 54CA02BE2426D4F3003A5E04 /* DDLog.swift in Sources */,
54CA02962426B2FD003A5E04 /* PacketProtocolParser.swift in Sources */, 54CA02962426B2FD003A5E04 /* PacketProtocolParser.swift in Sources */,
54CA02932426B2FD003A5E04 /* DNSServer.swift in Sources */, 54CA02932426B2FD003A5E04 /* DNSServer.swift in Sources */,
54CA02B22426B2FD003A5E04 /* AdapterFactoryManager.swift in Sources */,
54CA02AE2426B2FD003A5E04 /* AdapterFactory.swift in Sources */, 54CA02AE2426B2FD003A5E04 /* AdapterFactory.swift in Sources */,
54CA02A82426B2FD003A5E04 /* SOCKS5Adapter.swift in Sources */,
54CA02792426B2FD003A5E04 /* Checksum.swift in Sources */, 54CA02792426B2FD003A5E04 /* Checksum.swift in Sources */,
54CA02AD2426B2FD003A5E04 /* RejectAdapterFactory.swift in Sources */,
541075D224CDBA0000D6F1BF /* ThrottledBatchQueue.swift in Sources */, 541075D224CDBA0000D6F1BF /* ThrottledBatchQueue.swift in Sources */,
54CA02672426B2FD003A5E04 /* RawTCPSocketProtocol.swift in Sources */, 54CA02672426B2FD003A5E04 /* RawTCPSocketProtocol.swift in Sources */,
54CA02602426B2FD003A5E04 /* GCDProxyServer.swift in Sources */, 54CA02602426B2FD003A5E04 /* GCDProxyServer.swift in Sources */,
@@ -1085,54 +1081,38 @@
54CA01D52426B252003A5E04 /* SafeDict.swift in Sources */, 54CA01D52426B252003A5E04 /* SafeDict.swift in Sources */,
54CA027B2426B2FD003A5E04 /* HTTPAuthentication.swift in Sources */, 54CA027B2426B2FD003A5E04 /* HTTPAuthentication.swift in Sources */,
54CA02762426B2FD003A5E04 /* IPAddress.swift in Sources */, 54CA02762426B2FD003A5E04 /* IPAddress.swift in Sources */,
54CA02B02426B2FD003A5E04 /* SecureHTTPAdapterFactory.swift in Sources */,
54CA02A62426B2FD003A5E04 /* AdapterSocket.swift in Sources */, 54CA02A62426B2FD003A5E04 /* AdapterSocket.swift in Sources */,
54CA02742426B2FD003A5E04 /* IPMask.swift in Sources */, 54CA02742426B2FD003A5E04 /* IPMask.swift in Sources */,
541075CF24C9D43A00D6F1BF /* UNNotification.swift in Sources */, 541075CF24C9D43A00D6F1BF /* UNNotification.swift in Sources */,
54CA02BB2426B2FD003A5E04 /* SOCKS5ProxySocket.swift in Sources */,
54D8B97F2471B89100EB2414 /* DBCommon.swift in Sources */, 54D8B97F2471B89100EB2414 /* DBCommon.swift in Sources */,
54CA02A42426B2FD003A5E04 /* SecureHTTPAdapter.swift in Sources */,
54CA02942426B2FD003A5E04 /* DNSResolver.swift in Sources */, 54CA02942426B2FD003A5E04 /* DNSResolver.swift in Sources */,
54CA025F2426B2FD003A5E04 /* ProxyServer.swift in Sources */, 54CA025F2426B2FD003A5E04 /* ProxyServer.swift in Sources */,
54CA02842426B2FD003A5E04 /* Rule.swift in Sources */,
54CE8BC524B1ED2100CC1756 /* PushNotification.swift in Sources */, 54CE8BC524B1ED2100CC1756 /* PushNotification.swift in Sources */,
54CA02B92426B2FD003A5E04 /* DirectProxySocket.swift in Sources */,
54751E522423955100168273 /* URL.swift in Sources */, 54751E522423955100168273 /* URL.swift in Sources */,
54CA02A92426B2FD003A5E04 /* RejectAdapter.swift in Sources */,
54CA02732426B2FD003A5E04 /* IPPool.swift in Sources */, 54CA02732426B2FD003A5E04 /* IPPool.swift in Sources */,
541075D624CE286200D6F1BF /* CachedConnectionAlert.swift in Sources */, 541075D624CE286200D6F1BF /* CachedConnectionAlert.swift in Sources */,
54CA027E2426B2FD003A5E04 /* DomainListRule.swift in Sources */,
54CA02782426B2FD003A5E04 /* BinaryDataScanner.swift in Sources */, 54CA02782426B2FD003A5E04 /* BinaryDataScanner.swift in Sources */,
54CA02B12426B2FD003A5E04 /* ServerAdapterFactory.swift in Sources */,
54CA02952426B2FD003A5E04 /* DNSEnums.swift in Sources */, 54CA02952426B2FD003A5E04 /* DNSEnums.swift in Sources */,
54CA02802426B2FD003A5E04 /* DNSSessionMatchType.swift in Sources */, 54CA02802426B2FD003A5E04 /* DNSSessionMatchType.swift in Sources */,
54CA02A22426B2FD003A5E04 /* ObserverFactory.swift in Sources */, 54CA02A22426B2FD003A5E04 /* ObserverFactory.swift in Sources */,
54CA02612426B2FD003A5E04 /* GCDSOCKS5ProxyServer.swift in Sources */,
54CA029D2426B2FD003A5E04 /* ProxyServerEvent.swift in Sources */, 54CA029D2426B2FD003A5E04 /* ProxyServerEvent.swift in Sources */,
54CA02BC2426B2FD003A5E04 /* SocketProtocol.swift in Sources */, 54CA02BC2426B2FD003A5E04 /* SocketProtocol.swift in Sources */,
54CA029C2426B2FD003A5E04 /* AdapterSocketEvent.swift in Sources */, 54CA029C2426B2FD003A5E04 /* AdapterSocketEvent.swift in Sources */,
54CA02A72426B2FD003A5E04 /* DirectAdapter.swift in Sources */, 54CA02A72426B2FD003A5E04 /* DirectAdapter.swift in Sources */,
54CA02A32426B2FD003A5E04 /* HTTPAdapter.swift in Sources */,
54CA02622426B2FD003A5E04 /* GCDHTTPProxyServer.swift in Sources */, 54CA02622426B2FD003A5E04 /* GCDHTTPProxyServer.swift in Sources */,
54CA02822426B2FD003A5E04 /* AllRule.swift in Sources */,
543CDB2023EEE61900B7F323 /* PacketTunnelProvider.swift in Sources */, 543CDB2023EEE61900B7F323 /* PacketTunnelProvider.swift in Sources */,
54CA02662426B2FD003A5E04 /* NWUDPSocket.swift in Sources */, 54CA02662426B2FD003A5E04 /* NWUDPSocket.swift in Sources */,
54CA02682426B2FD003A5E04 /* NWTCPSocket.swift in Sources */, 54CA02682426B2FD003A5E04 /* NWTCPSocket.swift in Sources */,
54CA02852426B2FD003A5E04 /* DirectRule.swift in Sources */,
54CA01D32426B23D003A5E04 /* Resolver.swift in Sources */, 54CA01D32426B23D003A5E04 /* Resolver.swift in Sources */,
54CA028B2426B2FD003A5E04 /* Utils.swift in Sources */, 54CA028B2426B2FD003A5E04 /* Utils.swift in Sources */,
54CA02972426B2FD003A5E04 /* IPPacket.swift in Sources */, 54CA02972426B2FD003A5E04 /* IPPacket.swift in Sources */,
54CA026A2426B2FD003A5E04 /* RawSocketFactory.swift in Sources */, 54CA026A2426B2FD003A5E04 /* RawSocketFactory.swift in Sources */,
54CA02A02426B2FD003A5E04 /* TunnelEvent.swift in Sources */, 54CA02A02426B2FD003A5E04 /* TunnelEvent.swift in Sources */,
546063E523FEFAFE008F505A /* DBCore.swift in Sources */, 546063E523FEFAFE008F505A /* DBCore.swift in Sources */,
54CA02872426B2FD003A5E04 /* IPRangeListRule.swift in Sources */,
54CA02922426B2FD003A5E04 /* DNSSession.swift in Sources */, 54CA02922426B2FD003A5E04 /* DNSSession.swift in Sources */,
54CA026D2426B2FD003A5E04 /* Opt.swift in Sources */, 54CA026D2426B2FD003A5E04 /* Opt.swift in Sources */,
54CA02B32426B2FD003A5E04 /* HTTPAdapterFactory.swift in Sources */,
54CA02702426B2FD003A5E04 /* HTTPStreamScanner.swift in Sources */, 54CA02702426B2FD003A5E04 /* HTTPStreamScanner.swift in Sources */,
54CA02812426B2FD003A5E04 /* DNSFailRule.swift in Sources */,
541075DA24CE2C7200D6F1BF /* GlassVPNHook.swift in Sources */, 541075DA24CE2C7200D6F1BF /* GlassVPNHook.swift in Sources */,
54CA02AC2426B2FD003A5E04 /* AuthenticationServerAdapterFactory.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -1171,6 +1151,30 @@
name = Settings.storyboard; name = Settings.storyboard;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
549A96D8250419B200C565FA /* CoOccurrence.storyboard */ = {
isa = PBXVariantGroup;
children = (
549A96D9250419B200C565FA /* Base */,
);
name = CoOccurrence.storyboard;
sourceTree = "<group>";
};
54A0CC0724E30C56009B5EC1 /* Recordings.storyboard */ = {
isa = PBXVariantGroup;
children = (
54A0CC0824E30C56009B5EC1 /* Base */,
);
name = Recordings.storyboard;
sourceTree = "<group>";
};
54A0CC0A24E30D6F009B5EC1 /* Requests.storyboard */ = {
isa = PBXVariantGroup;
children = (
54A0CC0B24E30D6F009B5EC1 /* Base */,
);
name = Requests.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */ /* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
@@ -1200,6 +1204,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -1264,6 +1269,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -1301,7 +1307,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = main/main.entitlements; CODE_SIGN_ENTITLEMENTS = main/main.entitlements;
CURRENT_PROJECT_VERSION = 26; CURRENT_PROJECT_VERSION = 34;
INFOPLIST_FILE = main/Info.plist; INFOPLIST_FILE = main/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@@ -1320,7 +1326,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = main/main.entitlements; CODE_SIGN_ENTITLEMENTS = main/main.entitlements;
CURRENT_PROJECT_VERSION = 26; CURRENT_PROJECT_VERSION = 34;
INFOPLIST_FILE = main/Info.plist; INFOPLIST_FILE = main/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@@ -1339,7 +1345,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = GlassVPN/GlassVPN.entitlements; CODE_SIGN_ENTITLEMENTS = GlassVPN/GlassVPN.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 26; CURRENT_PROJECT_VERSION = 34;
INFOPLIST_FILE = GlassVPN/Info.plist; INFOPLIST_FILE = GlassVPN/Info.plist;
MARKETING_VERSION = 1.0.0; MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = "de.uni-bamberg.psi.AppCheck.VPN"; PRODUCT_BUNDLE_IDENTIFIER = "de.uni-bamberg.psi.AppCheck.VPN";
@@ -1357,7 +1363,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = GlassVPN/GlassVPN.entitlements; CODE_SIGN_ENTITLEMENTS = GlassVPN/GlassVPN.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 26; CURRENT_PROJECT_VERSION = 34;
INFOPLIST_FILE = GlassVPN/Info.plist; INFOPLIST_FILE = GlassVPN/Info.plist;
MARKETING_VERSION = 1.0.0; MARKETING_VERSION = 1.0.0;
PRODUCT_BUNDLE_IDENTIFIER = "de.uni-bamberg.psi.AppCheck.VPN"; PRODUCT_BUNDLE_IDENTIFIER = "de.uni-bamberg.psi.AppCheck.VPN";

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1130" LastUpgradeVersion = "1200"
version = "1.3"> version = "1.3">
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<Scheme <Scheme
LastUpgradeVersion = "1130" LastUpgradeVersion = "1200"
wasCreatedForAppExtension = "YES" wasCreatedForAppExtension = "YES"
version = "2.0"> version = "2.0">
<BuildAction <BuildAction

View File

@@ -1,5 +1,7 @@
import NetworkExtension import NetworkExtension
let connectMessage: Data = "CONNECT".data(using: .ascii)!
let swcdUserAgent: Data = "User-Agent: swcd".data(using: .ascii)!
fileprivate var hook : GlassVPNHook! fileprivate var hook : GlassVPNHook!
// MARK: ObserverFactory // MARK: ObserverFactory
@@ -15,8 +17,18 @@ class LDObserverFactory: ObserverFactory {
override func signal(_ event: ProxySocketEvent) { override func signal(_ event: ProxySocketEvent) {
switch event { switch event {
case .receivedRequest(let session, let socket): case .receivedRequest(let session, let socket):
let kill = hook.processDNSRequest(session.host) var kill = !hook.isBackgroundRecording && hook.forceDisconnectUnresolvable && session.ipAddress.isEmpty
if kill || socket.isCancelled { // isCancelled is set by branch below
hook.silentlyPrevented(session.host)
} else {
kill = hook.processDNSRequest(session.host)
}
if kill { socket.forceDisconnect() } if kill { socket.forceDisconnect() }
case .readData(let data, on: let socket):
if !hook.isBackgroundRecording, hook.forceDisconnectSWCD,
data.starts(with: connectMessage), data.range(of: swcdUserAgent) != nil {
socket.disconnect() // sets isCancelled above
}
default: default:
break break
} }

BIN
GlassVPN/SwiftSocket/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -1,16 +0,0 @@
import Foundation
public enum RuleMatchEvent: EventType {
public var description: String {
switch self {
case let .ruleMatched(session, rule: rule):
return "Rule \(rule) matched session \(session)."
case let .ruleDidNotMatch(session, rule: rule):
return "Rule \(rule) did not match session \(session)."
case let .dnsRuleMatched(session, rule: rule, type: type, result: result):
return "Rule \(rule) matched DNS session \(session) of type \(type), the result is \(result)."
}
}
case ruleMatched(ConnectSession, rule: Rule), ruleDidNotMatch(ConnectSession, rule: Rule), dnsRuleMatched(DNSSession, rule: Rule, type: DNSSessionMatchType, result: DNSSessionMatchResult)
}

View File

@@ -20,8 +20,4 @@ open class ObserverFactory {
open func getObserverForProxyServer(_ server: ProxyServer) -> Observer<ProxyServerEvent>? { open func getObserverForProxyServer(_ server: ProxyServer) -> Observer<ProxyServerEvent>? {
return nil return nil
} }
open func getObserverForRuleManager(_ manager: RuleManager) -> Observer<RuleMatchEvent>? {
return nil
}
} }

View File

@@ -74,8 +74,6 @@ open class DNSServer: DNSResolverDelegate, IPStackProtocol {
return return
} }
RuleManager.currentManager.matchDNS(session, type: .domain)
switch session.matchResult! { switch session.matchResult! {
case .fake: case .fake:
guard setUpFakeIP(session) else { guard setUpFakeIP(session) else {
@@ -248,10 +246,6 @@ open class DNSServer: DNSResolverDelegate, IPStackProtocol {
session.realIP = message.resolvedIPv4Address session.realIP = message.resolvedIPv4Address
if session.matchResult != .fake && session.matchResult != .real {
RuleManager.currentManager.matchDNS(session, type: .ip)
}
switch session.matchResult! { switch session.matchResult! {
case .fake: case .fake:
if !self.setUpFakeIP(session) { if !self.setUpFakeIP(session) {

View File

@@ -7,7 +7,6 @@ open class DNSSession {
open var fakeIP: IPAddress? open var fakeIP: IPAddress?
open var realResponseMessage: DNSMessage? open var realResponseMessage: DNSMessage?
var realResponseIPPacket: IPPacket? var realResponseIPPacket: IPPacket?
open var matchedRule: Rule?
open var matchResult: DNSSessionMatchResult? open var matchResult: DNSSessionMatchResult?
var indexToMatch = 0 var indexToMatch = 0
var expireAt: Date? var expireAt: Date?

View File

@@ -21,9 +21,6 @@ public final class ConnectSession {
/// The requested port. /// The requested port.
public let port: Int public let port: Int
/// The rule to use to connect to remote.
public var matchedRule: Rule?
/// Whether If the `requestedHost` is an IP address. /// Whether If the `requestedHost` is an IP address.
public let fakeIPEnabled: Bool public let fakeIPEnabled: Bool
@@ -126,11 +123,6 @@ public final class ConnectSession {
host = session.requestMessage.queries[0].name host = session.requestMessage.queries[0].name
ipAddress = session.realIP?.presentation ?? "" ipAddress = session.realIP?.presentation ?? ""
matchedRule = session.matchedRule
// if session.countryCode != nil {
// country = session.countryCode!
// }
return true return true
} }

View File

@@ -17,7 +17,6 @@ open class HTTPHeader {
// Chunk is not supported yet. // Chunk is not supported yet.
open var contentLength: Int = 0 open var contentLength: Int = 0
open var headers: [(String, String)] = [] open var headers: [(String, String)] = []
open var rawHeader: Data?
public init(headerString: String) throws { public init(headerString: String) throws {
let lines = headerString.components(separatedBy: "\r\n") let lines = headerString.components(separatedBy: "\r\n")
@@ -127,7 +126,6 @@ open class HTTPHeader {
} }
try self.init(headerString: headerString) try self.init(headerString: headerString)
rawHeader = headerData
} }
open subscript(index: String) -> String? { open subscript(index: String) -> String? {

View File

@@ -1,24 +0,0 @@
import Foundation
/// The SOCKS5 proxy server.
public final class GCDSOCKS5ProxyServer: GCDProxyServer {
/**
Create an instance of SOCKS5 proxy server.
- parameter address: The address of proxy server.
- parameter port: The port of proxy server.
*/
override public init(address: IPAddress?, port: Port) {
super.init(address: address, port: port)
}
/**
Handle the new accepted socket as a SOCKS5 proxy connection.
- parameter socket: The accepted socket.
*/
override public func handleNewGCDSocket(_ socket: GCDTCPSocket) {
let proxySocket = SOCKS5ProxySocket(socket: socket)
didAcceptNewSocket(proxySocket)
}
}

View File

@@ -1,6 +0,0 @@
import Foundation
open class ResponseGeneratorFactory {
static var HTTPProxyResponseGenerator: ResponseGenerator.Type?
static var SOCKS5ProxyResponseGenerator: ResponseGenerator.Type?
}

View File

@@ -1,48 +0,0 @@
import Foundation
/// The rule matches all DNS and connect sessions.
open class AllRule: Rule {
fileprivate let adapterFactory: AdapterFactory
open override var description: String {
return "<AllRule>"
}
/**
Create a new `AllRule` instance.
- parameter adapterFactory: The factory which builds a corresponding adapter when needed.
*/
public init(adapterFactory: AdapterFactory) {
self.adapterFactory = adapterFactory
super.init()
}
/**
Match DNS session to this rule.
- parameter session: The DNS session to match.
- parameter type: What kind of information is available.
- returns: The result of match.
*/
override open func matchDNS(_ session: DNSSession, type: DNSSessionMatchType) -> DNSSessionMatchResult {
// only return real IP when we connect to remote directly
if let _ = adapterFactory as? DirectAdapterFactory {
return .real
} else {
return .fake
}
}
/**
Match connect session to this rule.
- parameter session: connect session to match.
- returns: The configured adapter.
*/
override open func match(_ session: ConnectSession) -> AdapterFactory? {
return adapterFactory
}
}

View File

@@ -1,60 +0,0 @@
import Foundation
/// The rule matches the request which failed to look up.
open class DNSFailRule: Rule {
fileprivate let adapterFactory: AdapterFactory
open override var description: String {
return "<DNSFailRule>"
}
/**
Create a new `DNSFailRule` instance.
- parameter adapterFactory: The factory which builds a corresponding adapter when needed.
*/
public init(adapterFactory: AdapterFactory) {
self.adapterFactory = adapterFactory
super.init()
}
/**
Match DNS request to this rule.
- parameter session: The DNS session to match.
- parameter type: What kind of information is available.
- returns: The result of match.
*/
override open func matchDNS(_ session: DNSSession, type: DNSSessionMatchType) -> DNSSessionMatchResult {
guard type == .ip else {
return .unknown
}
// only return real IP when we connect to remote directly
if session.realIP == nil {
if let _ = adapterFactory as? DirectAdapterFactory {
return .real
} else {
return .fake
}
} else {
return .pass
}
}
/**
Match connect session to this rule.
- parameter session: connect session to match.
- returns: The configured adapter.
*/
override open func match(_ session: ConnectSession) -> AdapterFactory? {
if session.ipAddress == "" {
return adapterFactory
} else {
return nil
}
}
}

View File

@@ -1,16 +0,0 @@
import Foundation
/// The rule matches every request and returns direct adapter.
///
/// This is equivalent to create an `AllRule` with a `DirectAdapterFactory`.
open class DirectRule: AllRule {
open override var description: String {
return "<DirectRule>"
}
/**
Create a new `DirectRule` instance.
*/
public init() {
super.init(adapterFactory: DirectAdapterFactory())
}
}

View File

@@ -1,84 +0,0 @@
import Foundation
/// The rule matches the host domain to a list of predefined criteria.
open class DomainListRule: Rule {
public enum MatchCriterion {
case regex(NSRegularExpression), prefix(String), suffix(String), keyword(String), complete(String)
func match(_ domain: String) -> Bool {
switch self {
case .regex(let regex):
return regex.firstMatch(in: domain, options: [], range: NSRange(location: 0, length: domain.utf8.count)) != nil
case .prefix(let prefix):
return domain.hasPrefix(prefix)
case .suffix(let suffix):
return domain.hasSuffix(suffix)
case .keyword(let keyword):
return domain.contains(keyword)
case .complete(let match):
return domain == match
}
}
}
fileprivate let adapterFactory: AdapterFactory
open override var description: String {
return "<DomainListRule>"
}
/// The list of criteria to match to.
open var matchCriteria: [MatchCriterion] = []
/**
Create a new `DomainListRule` instance.
- parameter adapterFactory: The factory which builds a corresponding adapter when needed.
- parameter criteria: The list of criteria to match.
*/
public init(adapterFactory: AdapterFactory, criteria: [MatchCriterion]) {
self.adapterFactory = adapterFactory
self.matchCriteria = criteria
}
/**
Match DNS request to this rule.
- parameter session: The DNS session to match.
- parameter type: What kind of information is available.
- returns: The result of match.
*/
override open func matchDNS(_ session: DNSSession, type: DNSSessionMatchType) -> DNSSessionMatchResult {
if matchDomain(session.requestMessage.queries.first!.name) {
if let _ = adapterFactory as? DirectAdapterFactory {
return .real
}
return .fake
}
return .pass
}
/**
Match connect session to this rule.
- parameter session: connect session to match.
- returns: The configured adapter if matched, return `nil` if not matched.
*/
override open func match(_ session: ConnectSession) -> AdapterFactory? {
if matchDomain(session.host) {
return adapterFactory
}
return nil
}
fileprivate func matchDomain(_ domain: String) -> Bool {
for criterion in matchCriteria {
if criterion.match(domain) {
return true
}
}
return false
}
}

View File

@@ -1,75 +0,0 @@
import Foundation
/// The rule matches the ip of the target hsot to a list of IP ranges.
open class IPRangeListRule: Rule {
fileprivate let adapterFactory: AdapterFactory
open override var description: String {
return "<IPRangeList>"
}
/// The list of regular expressions to match to.
open var ranges: [IPRange] = []
/**
Create a new `IPRangeListRule` instance.
- parameter adapterFactory: The factory which builds a corresponding adapter when needed.
- parameter ranges: The list of IP ranges to match. The IP ranges are expressed in CIDR form ("127.0.0.1/8") or range form ("127.0.0.1+16777216").
- throws: The error when parsing the IP range.
*/
public init(adapterFactory: AdapterFactory, ranges: [String]) throws {
self.adapterFactory = adapterFactory
self.ranges = try ranges.map {
let range = try IPRange(withString: $0)
return range
}
}
/**
Match DNS request to this rule.
- parameter session: The DNS session to match.
- parameter type: What kind of information is available.
- returns: The result of match.
*/
override open func matchDNS(_ session: DNSSession, type: DNSSessionMatchType) -> DNSSessionMatchResult {
guard type == .ip else {
return .unknown
}
// Probably we should match all answers?
guard let ip = session.realIP else {
return .pass
}
for range in ranges {
if range.contains(ip: ip) {
return .fake
}
}
return .pass
}
/**
Match connect session to this rule.
- parameter session: connect session to match.
- returns: The configured adapter if matched, return `nil` if not matched.
*/
override open func match(_ session: ConnectSession) -> AdapterFactory? {
guard let ip = IPAddress(fromString: session.ipAddress) else {
return nil
}
for range in ranges {
if range.contains(ip: ip) {
return adapterFactory
}
}
return nil
}
}

View File

@@ -1,37 +0,0 @@
import Foundation
/// The rule defines what to do for DNS requests and connect sessions.
open class Rule: CustomStringConvertible {
open var description: String {
return "<Rule>"
}
/**
Create a new rule.
*/
public init() {
}
/**
Match DNS request to this rule.
- parameter session: The DNS session to match.
- parameter type: What kind of information is available.
- returns: The result of match.
*/
open func matchDNS(_ session: DNSSession, type: DNSSessionMatchType) -> DNSSessionMatchResult {
return .real
}
/**
Match connect session to this rule.
- parameter session: connect session to match.
- returns: The configured adapter if matched, return `nil` if not matched.
*/
open func match(_ session: ConnectSession) -> AdapterFactory? {
return nil
}
}

View File

@@ -1,80 +0,0 @@
import Foundation
/// The class managing rules.
open class RuleManager {
/// The current used `RuleManager`, there is only one manager should be used at a time.
///
/// - note: This should be set before any DNS or connect sessions.
public static var currentManager: RuleManager = RuleManager(fromRules: [], appendDirect: true)
/// The rule list.
var rules: [Rule] = []
open var observer: Observer<RuleMatchEvent>?
/**
Create a new `RuleManager` from the given rules.
- parameter rules: The rules.
- parameter appendDirect: Whether to append a `DirectRule` at the end of the list so any request does not match with any rule go directly.
*/
public init(fromRules rules: [Rule], appendDirect: Bool = false) {
self.rules = []
if appendDirect || self.rules.count == 0 {
self.rules.append(DirectRule())
}
observer = ObserverFactory.currentFactory?.getObserverForRuleManager(self)
}
/**
Match DNS request to all rules.
- parameter session: The DNS session to match.
- parameter type: What kind of information is available.
*/
func matchDNS(_ session: DNSSession, type: DNSSessionMatchType) {
for (i, rule) in rules[session.indexToMatch..<rules.count].enumerated() {
let result = rule.matchDNS(session, type: type)
observer?.signal(.dnsRuleMatched(session, rule: rule, type: type, result: result))
switch result {
case .fake, .real, .unknown:
session.matchedRule = rule
session.matchResult = result
session.indexToMatch = i + session.indexToMatch // add the offset
return
case .pass:
break
}
}
}
/**
Match connect session to all rules.
- parameter session: connect session to match.
- returns: The matched configured adapter.
*/
func match(_ session: ConnectSession) -> AdapterFactory! {
if session.matchedRule != nil {
observer?.signal(.ruleMatched(session, rule: session.matchedRule!))
return session.matchedRule!.match(session)
}
for rule in rules {
if let adapterFactory = rule.match(session) {
observer?.signal(.ruleMatched(session, rule: rule))
session.matchedRule = rule
return adapterFactory
} else {
observer?.signal(.ruleDidNotMatch(session, rule: rule))
}
}
return nil // this should never happens
}
}

View File

@@ -1,27 +0,0 @@
import Foundation
/// This is a very simple wrapper of a dict of type `[String: AdapterFactory]`.
///
/// Use it as a normal dict.
public class AdapterFactoryManager {
private var factoryDict: [String: AdapterFactory]
public subscript(index: String) -> AdapterFactory? {
get {
if index == "direct" {
return DirectAdapterFactory()
}
return factoryDict[index]
}
set { factoryDict[index] = newValue }
}
/**
Initialize a new factory manager.
- parameter factoryDict: The factory dict.
*/
public init(factoryDict: [String: AdapterFactory]) {
self.factoryDict = factoryDict
}
}

View File

@@ -1,11 +0,0 @@
import Foundation
/// Factory building server adapter which requires authentication.
open class HTTPAuthenticationAdapterFactory: ServerAdapterFactory {
let auth: HTTPAuthentication?
required public init(serverHost: String, serverPort: Int, auth: HTTPAuthentication?) {
self.auth = auth
super.init(serverHost: serverHost, serverPort: serverPort)
}
}

View File

@@ -1,21 +0,0 @@
import Foundation
/// Factory building HTTP adapter.
open class HTTPAdapterFactory: HTTPAuthenticationAdapterFactory {
required public init(serverHost: String, serverPort: Int, auth: HTTPAuthentication?) {
super.init(serverHost: serverHost, serverPort: serverPort, auth: auth)
}
/**
Get a HTTP adapter.
- parameter session: The connect session.
- returns: The built adapter.
*/
override open func getAdapterFor(session: ConnectSession) -> AdapterSocket {
let adapter = HTTPAdapter(serverHost: serverHost, serverPort: serverPort, auth: auth)
adapter.socket = RawSocketFactory.getRawSocket()
return adapter
}
}

View File

@@ -1,13 +0,0 @@
import Foundation
open class RejectAdapterFactory: AdapterFactory {
public let delay: Int
public init(delay: Int = Opt.RejectAdapterDefaultDelay) {
self.delay = delay
}
override open func getAdapterFor(session: ConnectSession) -> AdapterSocket {
return RejectAdapter(delay: delay)
}
}

View File

@@ -1,21 +0,0 @@
import Foundation
/// Factory building SOCKS5 adapter.
open class SOCKS5AdapterFactory: ServerAdapterFactory {
override public init(serverHost: String, serverPort: Int) {
super.init(serverHost: serverHost, serverPort: serverPort)
}
/**
Get a SOCKS5 adapter.
- parameter session: The connect session.
- returns: The built adapter.
*/
override open func getAdapterFor(session: ConnectSession) -> AdapterSocket {
let adapter = SOCKS5Adapter(serverHost: serverHost, serverPort: serverPort)
adapter.socket = RawSocketFactory.getRawSocket()
return adapter
}
}

View File

@@ -1,21 +0,0 @@
import Foundation
/// Factory building secured HTTP (HTTP with SSL) adapter.
open class SecureHTTPAdapterFactory: HTTPAdapterFactory {
required public init(serverHost: String, serverPort: Int, auth: HTTPAuthentication?) {
super.init(serverHost: serverHost, serverPort: serverPort, auth: auth)
}
/**
Get a secured HTTP adapter.
- parameter session: The connect session.
- returns: The built adapter.
*/
override open func getAdapterFor(session: ConnectSession) -> AdapterSocket {
let adapter = SecureHTTPAdapter(serverHost: serverHost, serverPort: serverPort, auth: auth)
adapter.socket = RawSocketFactory.getRawSocket()
return adapter
}
}

View File

@@ -1,12 +0,0 @@
import Foundation
/// Factory building adapter with proxy server host and port.
open class ServerAdapterFactory: AdapterFactory {
let serverHost: String
let serverPort: Int
public init(serverHost: String, serverPort: Int) {
self.serverHost = serverHost
self.serverPort = serverPort
}
}

View File

@@ -1,110 +0,0 @@
import Foundation
public enum HTTPAdapterError: Error, CustomStringConvertible {
case invalidURL, serailizationFailure
public var description: String {
switch self {
case .invalidURL:
return "Invalid url when connecting through proxy"
case .serailizationFailure:
return "Failed to serialize HTTP CONNECT header"
}
}
}
/// This adapter connects to remote host through a HTTP proxy.
public class HTTPAdapter: AdapterSocket {
enum HTTPAdapterStatus {
case invalid,
connecting,
readingResponse,
forwarding,
stopped
}
/// The host domain of the HTTP proxy.
let serverHost: String
/// The port of the HTTP proxy.
let serverPort: Int
/// The authentication information for the HTTP proxy.
let auth: HTTPAuthentication?
/// Whether the connection to the proxy should be secured or not.
var secured: Bool
var internalStatus: HTTPAdapterStatus = .invalid
public init(serverHost: String, serverPort: Int, auth: HTTPAuthentication?) {
self.serverHost = serverHost
self.serverPort = serverPort
self.auth = auth
secured = false
super.init()
}
override public func openSocketWith(session: ConnectSession) {
super.openSocketWith(session: session)
guard !isCancelled else {
return
}
do {
internalStatus = .connecting
try socket.connectTo(host: serverHost, port: serverPort, enableTLS: secured, tlsSettings: nil)
} catch {}
}
override public func didConnectWith(socket: RawTCPSocketProtocol) {
super.didConnectWith(socket: socket)
guard let url = URL(string: "\(session.host):\(session.port)") else {
observer?.signal(.errorOccured(HTTPAdapterError.invalidURL, on: self))
disconnect()
return
}
let message = CFHTTPMessageCreateRequest(kCFAllocatorDefault, "CONNECT" as CFString, url as CFURL, kCFHTTPVersion1_1).takeRetainedValue()
if let authData = auth {
CFHTTPMessageSetHeaderFieldValue(message, "Proxy-Authorization" as CFString, authData.authString() as CFString?)
}
CFHTTPMessageSetHeaderFieldValue(message, "Host" as CFString, "\(session.host):\(session.port)" as CFString?)
CFHTTPMessageSetHeaderFieldValue(message, "Content-Length" as CFString, "0" as CFString?)
guard let requestData = CFHTTPMessageCopySerializedMessage(message)?.takeRetainedValue() else {
observer?.signal(.errorOccured(HTTPAdapterError.serailizationFailure, on: self))
disconnect()
return
}
internalStatus = .readingResponse
write(data: requestData as Data)
socket.readDataTo(data: Utils.HTTPData.DoubleCRLF)
}
override public func didRead(data: Data, from socket: RawTCPSocketProtocol) {
super.didRead(data: data, from: socket)
switch internalStatus {
case .readingResponse:
internalStatus = .forwarding
observer?.signal(.readyForForward(self))
delegate?.didBecomeReadyToForwardWith(socket: self)
case .forwarding:
observer?.signal(.readData(data, on: self))
delegate?.didRead(data: data, from: self)
default:
return
}
}
override public func didWrite(data: Data?, by socket: RawTCPSocketProtocol) {
super.didWrite(data: data, by: socket)
if internalStatus == .forwarding {
observer?.signal(.wroteData(data, on: self))
delegate?.didWrite(data: data, by: self)
}
}
}

View File

@@ -1,49 +0,0 @@
import Foundation
public class RejectAdapter: AdapterSocket {
public let delay: Int
public init(delay: Int) {
self.delay = delay
}
override public func openSocketWith(session: ConnectSession) {
super.openSocketWith(session: session)
QueueFactory.getQueue().asyncAfter(deadline: DispatchTime.now() + DispatchTimeInterval.milliseconds(delay)) {
[weak self] in
self?.disconnect()
}
}
/**
Disconnect the socket elegantly.
*/
public override func disconnect(becauseOf error: Error? = nil) {
guard !isCancelled else {
return
}
_cancelled = true
session.disconnected(becauseOf: error, by: .adapter)
observer?.signal(.disconnectCalled(self))
_status = .closed
delegate?.didDisconnectWith(socket: self)
}
/**
Disconnect the socket immediately.
*/
public override func forceDisconnect(becauseOf error: Error? = nil) {
guard !isCancelled else {
return
}
_cancelled = true
session.disconnected(becauseOf: error, by: .adapter)
observer?.signal(.forceDisconnectCalled(self))
_status = .closed
delegate?.didDisconnectWith(socket: self)
}
}

View File

@@ -1,112 +0,0 @@
import Foundation
public class SOCKS5Adapter: AdapterSocket {
enum SOCKS5AdapterStatus {
case invalid,
connecting,
readingMethodResponse,
readingResponseFirstPart,
readingResponseSecondPart,
forwarding
}
public let serverHost: String
public let serverPort: Int
var internalStatus: SOCKS5AdapterStatus = .invalid
let helloData = Data([0x05, 0x01, 0x00])
public enum ReadTag: Int {
case methodResponse = -20000, connectResponseFirstPart, connectResponseSecondPart
}
public enum WriteTag: Int {
case open = -21000, connectIPv4, connectIPv6, connectDomainLength, connectPort
}
public init(serverHost: String, serverPort: Int) {
self.serverHost = serverHost
self.serverPort = serverPort
super.init()
}
public override func openSocketWith(session: ConnectSession) {
super.openSocketWith(session: session)
guard !isCancelled else {
return
}
do {
internalStatus = .connecting
try socket.connectTo(host: serverHost, port: serverPort, enableTLS: false, tlsSettings: nil)
} catch {}
}
public override func didConnectWith(socket: RawTCPSocketProtocol) {
super.didConnectWith(socket: socket)
write(data: helloData)
internalStatus = .readingMethodResponse
socket.readDataTo(length: 2)
}
public override func didRead(data: Data, from socket: RawTCPSocketProtocol) {
super.didRead(data: data, from: socket)
switch internalStatus {
case .readingMethodResponse:
var response: [UInt8]
if session.isIPv4() {
response = [0x05, 0x01, 0x00, 0x01]
let address = IPAddress(fromString: session.host)!
response += [UInt8](address.dataInNetworkOrder)
} else if session.isIPv6() {
response = [0x05, 0x01, 0x00, 0x04]
let address = IPAddress(fromString: session.host)!
response += [UInt8](address.dataInNetworkOrder)
} else {
response = [0x05, 0x01, 0x00, 0x03]
response.append(UInt8(session.host.utf8.count))
response += [UInt8](session.host.utf8)
}
let portBytes: [UInt8] = Utils.toByteArray(UInt16(session.port)).reversed()
response.append(contentsOf: portBytes)
write(data: Data(response))
internalStatus = .readingResponseFirstPart
socket.readDataTo(length: 5)
case .readingResponseFirstPart:
var readLength = 0
switch data[3] {
case 1:
readLength = 3 + 2
case 3:
readLength = Int(data[4]) + 2
case 4:
readLength = 15 + 2
default:
break
}
internalStatus = .readingResponseSecondPart
socket.readDataTo(length: readLength)
case .readingResponseSecondPart:
internalStatus = .forwarding
observer?.signal(.readyForForward(self))
delegate?.didBecomeReadyToForwardWith(socket: self)
case .forwarding:
delegate?.didRead(data: data, from: self)
default:
return
}
}
override open func didWrite(data: Data?, by socket: RawTCPSocketProtocol) {
super.didWrite(data: data, by: socket)
if internalStatus == .forwarding {
delegate?.didWrite(data: data, by: self)
}
}
}

View File

@@ -1,9 +0,0 @@
import Foundation
/// This adapter connects to remote host through a HTTP proxy with SSL.
public class SecureHTTPAdapter: HTTPAdapter {
override public init(serverHost: String, serverPort: Int, auth: HTTPAuthentication?) {
super.init(serverHost: serverHost, serverPort: serverPort, auth: auth)
secured = true
}
}

View File

@@ -1,113 +0,0 @@
import Foundation
/// This class just forwards data directly.
/// - note: It is designed to work with tun2socks only.
public class DirectProxySocket: ProxySocket {
enum DirectProxyReadStatus: CustomStringConvertible {
case invalid,
forwarding,
stopped
var description: String {
switch self {
case .invalid:
return "invalid"
case .forwarding:
return "forwarding"
case .stopped:
return "stopped"
}
}
}
enum DirectProxyWriteStatus {
case invalid,
forwarding,
stopped
var description: String {
switch self {
case .invalid:
return "invalid"
case .forwarding:
return "forwarding"
case .stopped:
return "stopped"
}
}
}
private var readStatus: DirectProxyReadStatus = .invalid
private var writeStatus: DirectProxyWriteStatus = .invalid
public var readStatusDescription: String {
return readStatus.description
}
public var writeStatusDescription: String {
return writeStatus.description
}
/**
Begin reading and processing data from the socket.
- note: Since there is nothing to read and process before forwarding data, this just calls `delegate?.didReceiveRequest`.
*/
override public func openSocket() {
super.openSocket()
guard !isCancelled else {
return
}
if let address = socket.destinationIPAddress, let port = socket.destinationPort {
session = ConnectSession(host: address.presentation, port: Int(port.value))
observer?.signal(.receivedRequest(session!, on: self))
delegate?.didReceive(session: session!, from: self)
} else {
forceDisconnect()
}
}
/**
Response to the `AdapterSocket` on the other side of the `Tunnel` which has succefully connected to the remote server.
- parameter adapter: The `AdapterSocket`.
*/
override public func respondTo(adapter: AdapterSocket) {
super.respondTo(adapter: adapter)
guard !isCancelled else {
return
}
readStatus = .forwarding
writeStatus = .forwarding
observer?.signal(.readyForForward(self))
delegate?.didBecomeReadyToForwardWith(socket: self)
}
/**
The socket did read some data.
- parameter data: The data read from the socket.
- parameter from: The socket where the data is read from.
*/
override open func didRead(data: Data, from: RawTCPSocketProtocol) {
super.didRead(data: data, from: from)
delegate?.didRead(data: data, from: self)
}
/**
The socket did send some data.
- parameter data: The data which have been sent to remote (acknowledged). Note this may not be available since the data may be released to save memory.
- parameter by: The socket where the data is sent out.
*/
override open func didWrite(data: Data?, by: RawTCPSocketProtocol) {
super.didWrite(data: data, by: by)
delegate?.didWrite(data: data, by: self)
}
}

View File

@@ -1,244 +0,0 @@
import Foundation
public class SOCKS5ProxySocket: ProxySocket {
enum SOCKS5ProxyReadStatus: CustomStringConvertible {
case invalid,
readingVersionIdentifierAndNumberOfMethods,
readingMethods,
readingConnectHeader,
readingIPv4Address,
readingDomainLength,
readingDomain,
readingIPv6Address,
readingPort,
forwarding,
stopped
var description: String {
switch self {
case .invalid:
return "invalid"
case .readingVersionIdentifierAndNumberOfMethods:
return "reading version and methods"
case .readingMethods:
return "reading methods"
case .readingConnectHeader:
return "reading connect header"
case .readingIPv4Address:
return "IPv4 address"
case .readingDomainLength:
return "domain length"
case .readingDomain:
return "domain"
case .readingIPv6Address:
return "IPv6 address"
case .readingPort:
return "reading port"
case .forwarding:
return "forwarding"
case .stopped:
return "stopped"
}
}
}
enum SOCKS5ProxyWriteStatus: CustomStringConvertible {
case invalid,
sendingResponse,
forwarding,
stopped
var description: String {
switch self {
case .invalid:
return "invalid"
case .sendingResponse:
return "sending response"
case .forwarding:
return "forwarding"
case .stopped:
return "stopped"
}
}
}
/// The remote host to connect to.
public var destinationHost: String!
/// The remote port to connect to.
public var destinationPort: Int!
private var readStatus: SOCKS5ProxyReadStatus = .invalid
private var writeStatus: SOCKS5ProxyWriteStatus = .invalid
public var readStatusDescription: String {
return readStatus.description
}
public var writeStatusDescription: String {
return writeStatus.description
}
/**
Begin reading and processing data from the socket.
*/
override public func openSocket() {
super.openSocket()
guard !isCancelled else {
return
}
readStatus = .readingVersionIdentifierAndNumberOfMethods
socket.readDataTo(length: 2)
}
// swiftlint:disable function_body_length
// swiftlint:disable cyclomatic_complexity
/**
The socket did read some data.
- parameter data: The data read from the socket.
- parameter from: The socket where the data is read from.
*/
override public func didRead(data: Data, from: RawTCPSocketProtocol) {
super.didRead(data: data, from: from)
switch readStatus {
case .forwarding:
delegate?.didRead(data: data, from: self)
case .readingVersionIdentifierAndNumberOfMethods:
data.withUnsafeBytes { pointer in
let p = pointer.bindMemory(to: Int8.self)
guard p.baseAddress!.pointee == 5 else {
// TODO: notify observer
self.disconnect()
return
}
guard p.baseAddress!.successor().pointee > 0 else {
// TODO: notify observer
self.disconnect()
return
}
self.readStatus = .readingMethods
self.socket.readDataTo(length: Int(p.baseAddress!.successor().pointee))
}
case .readingMethods:
// TODO: check for 0x00 in read data
let response = Data([0x05, 0x00])
// we would not be able to read anything before the data is written out, so no need to handle the dataWrote event.
write(data: response)
readStatus = .readingConnectHeader
socket.readDataTo(length: 4)
case .readingConnectHeader:
data.withUnsafeBytes { pointer in
let p = pointer.bindMemory(to: Int8.self)
guard p.baseAddress!.pointee == 5 && p.baseAddress!.successor().pointee == 1 else {
// TODO: notify observer
self.disconnect()
return
}
switch p.baseAddress!.advanced(by: 3).pointee {
case 1:
readStatus = .readingIPv4Address
socket.readDataTo(length: 4)
case 3:
readStatus = .readingDomainLength
socket.readDataTo(length: 1)
case 4:
readStatus = .readingIPv6Address
socket.readDataTo(length: 16)
default:
break
}
}
case .readingIPv4Address:
var address = Data(count: Int(INET_ADDRSTRLEN))
_ = data.withUnsafeBytes { data_ptr in
address.withUnsafeMutableBytes { addr_ptr in
inet_ntop(AF_INET, data_ptr.baseAddress!, addr_ptr.bindMemory(to: Int8.self).baseAddress!, socklen_t(INET_ADDRSTRLEN))
}
}
destinationHost = String(data: address, encoding: .utf8)
readStatus = .readingPort
socket.readDataTo(length: 2)
case .readingIPv6Address:
var address = Data(count: Int(INET6_ADDRSTRLEN))
_ = data.withUnsafeBytes { data_ptr in
address.withUnsafeMutableBytes { addr_ptr in
inet_ntop(AF_INET6, data_ptr.baseAddress!, addr_ptr.bindMemory(to: Int8.self).baseAddress!, socklen_t(INET6_ADDRSTRLEN))
}
}
destinationHost = String(data: address, encoding: .utf8)
readStatus = .readingPort
socket.readDataTo(length: 2)
case .readingDomainLength:
readStatus = .readingDomain
socket.readDataTo(length: Int(data.first!))
case .readingDomain:
destinationHost = String(data: data, encoding: .utf8)
readStatus = .readingPort
socket.readDataTo(length: 2)
case .readingPort:
data.withUnsafeBytes {
destinationPort = Int($0.load(as: UInt16.self).bigEndian)
}
readStatus = .forwarding
session = ConnectSession(host: destinationHost, port: destinationPort)
observer?.signal(.receivedRequest(session!, on: self))
delegate?.didReceive(session: session!, from: self)
default:
return
}
}
/**
The socket did send some data.
- parameter data: The data which have been sent to remote (acknowledged). Note this may not be available since the data may be released to save memory.
- parameter from: The socket where the data is sent out.
*/
override public func didWrite(data: Data?, by: RawTCPSocketProtocol) {
super.didWrite(data: data, by: by)
switch writeStatus {
case .forwarding:
delegate?.didWrite(data: data, by: self)
case .sendingResponse:
writeStatus = .forwarding
observer?.signal(.readyForForward(self))
delegate?.didBecomeReadyToForwardWith(socket: self)
default:
return
}
}
/**
Response to the `AdapterSocket` on the other side of the `Tunnel` which has succefully connected to the remote server.
- parameter adapter: The `AdapterSocket`.
*/
override public func respondTo(adapter: AdapterSocket) {
super.respondTo(adapter: adapter)
guard !isCancelled else {
return
}
var responseBytes = [UInt8](repeating: 0, count: 10)
responseBytes[0...3] = [0x05, 0x00, 0x00, 0x01]
let responseData = Data(responseBytes)
writeStatus = .sendingResponse
write(data: responseData)
}
}

View File

@@ -170,9 +170,7 @@ public class Tunnel: NSObject, SocketDelegate {
return return
} }
let manager = RuleManager.currentManager adapterSocket = DirectAdapterFactory().getAdapterFor(session: session)
let factory = manager.match(session)!
adapterSocket = factory.getAdapterFor(session: session)
adapterSocket!.delegate = self adapterSocket!.delegate = self
adapterSocket!.openSocketWith(session: session) adapterSocket!.openSocketWith(session: session)
} }

View File

@@ -39,11 +39,11 @@ That means, AppCheck does not have to be active in the foreground all the time.
- What other domains occur often at the same time? - What other domains occur often at the same time?
- What happened immediately before or after the action? - What happened immediately before or after the action?
- Export results for custom analysis - Export results for custom analysis
**… and soon:**
- Alert Monitor & reminder - Alert Monitor & reminder
- Participate in privacy research - Participate in privacy research
- Contribute your results
- See what others have unveiled
- How much traffic does this app produce?
<sup>1</sup> Due to technical limitations, recordings can not be restricted to a single application. Remember to force-quit all other applications before starting a recording. <sup>1</sup> Due to technical limitations, recordings can not be restricted to a single application. Remember to force-quit all other applications before starting a recording.
@@ -53,3 +53,5 @@ That means, AppCheck does not have to be active in the foreground all the time.
*information will be added soon™* *information will be added soon™*
For now, go to the results page at [https://appchk.de/](https://appchk.de/).
Btw. we are searching for [help](https://appchk.de/help/) on our ongoing research project.

View File

@@ -33,3 +33,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
// This is a known issue and tolerated. // This is a known issue and tolerated.
} }
} }
extension URL {
@discardableResult func open() -> Bool { UIApplication.shared.openURL(self) }
}

Binary file not shown.

View File

@@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "img.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "img@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "img@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 B

View File

@@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "img.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "img@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "img@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 B

View File

@@ -0,0 +1,26 @@
{
"images" : [
{
"filename" : "img.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "img@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "img@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@ class CustomAlert<CustomView: UIView>: UIViewController {
private var callback: ((CustomView) -> Void)? private var callback: ((CustomView) -> Void)?
/// Default: `[Cancel, Save]` /// Default: `[Cancel, Save]`
let buttonsBar: UIStackView = { lazy var buttonsBar: UIStackView = {
let cancel = QuickUI.button("Cancel", target: self, action: #selector(didTapCancel)) let cancel = QuickUI.button("Cancel", target: self, action: #selector(didTapCancel))
let save = QuickUI.button("Save", target: self, action: #selector(didTapSave)) let save = QuickUI.button("Save", target: self, action: #selector(didTapSave))
save.titleLabel?.font = save.titleLabel?.font.bold() save.titleLabel?.font = save.titleLabel?.font.bold()

View File

@@ -0,0 +1,71 @@
import UIKit
struct NotificationBanner {
enum Style {
case fail, ok
}
let view: UIView
init(_ msg: String, style: Style) {
let bg, fg: UIColor
let imgName: String
switch style {
case .fail:
bg = .systemRed
fg = UIColor.black.withAlphaComponent(0.80)
imgName = "circle-x"
case .ok:
bg = .systemGreen
fg = UIColor.black.withAlphaComponent(0.65)
imgName = "circle-check"
}
view = UIView()
view.backgroundColor = bg
let lbl = QuickUI.label(msg, style: .callout)
lbl.textColor = fg
lbl.numberOfLines = 0
lbl.font = lbl.font.bold()
let img = QuickUI.image(UIImage(named: imgName))
img.tintColor = fg
view.addSubview(lbl)
view.addSubview(img)
img.anchor([.centerY], to: lbl)
lbl.anchor([.bottom, .trailing], to: view.layoutMarginsGuide)
img.widthAnchor =&= 25
img.heightAnchor =&= 25
if #available(iOS 11, *) {
img.leadingAnchor =&= view.layoutMarginsGuide.leadingAnchor
lbl.topAnchor =&= view.layoutMarginsGuide.topAnchor
} else {
img.leadingAnchor =&= view.leadingAnchor + 8
lbl.topAnchor =&= view.topAnchor + 8
}
lbl.leadingAnchor =&= img.trailingAnchor + 8
img.bottomAnchor =<= view.bottomAnchor - 8 | .init(rawValue: 999)
lbl.bottomAnchor =<= view.bottomAnchor - 8 | .init(rawValue: 999)
}
/// Animate header banner from the top of the view. Show for `delay` seconds and then hide again.
/// - Parameter onClose: Run after the close animation finishes.
func present(in vc: UIViewController, hideAfter delay: TimeInterval = 3, onClose: (() -> Void)? = nil) {
vc.view.addSubview(view)
view.anchor([.leading, .trailing], to: vc.view!)
view.widthAnchor =&= vc.view!.widthAnchor // Bug? left-right is not sufficient
vc.view.layoutIfNeeded() // sets the height
let h = view.frame.height
let constraint = view.topAnchor =&= vc.view.topAnchor - h
vc.view.layoutIfNeeded() // hide view
UIView.animate(withDuration: 0.3, animations: {
constraint.constant = 0
vc.view.layoutIfNeeded() // animate view
UIView.animate(withDuration: 0.3, delay: delay, options: .curveLinear, animations: {
constraint.constant = -h
vc.view.layoutIfNeeded() // hide again
}, completion: { _ in
self.view.removeFromSuperview()
onClose?()
})
})
}
}

View File

@@ -33,6 +33,10 @@ extension Prefs {
get { Prefs.Bool("didShowTutorialRecordings") } get { Prefs.Bool("didShowTutorialRecordings") }
set { Prefs.Bool("didShowTutorialRecordings", newValue) } set { Prefs.Bool("didShowTutorialRecordings", newValue) }
} }
static var RecordingHowTo: Bool {
get { Prefs.Bool("didShowTutorialRecordingHowTo") }
set { Prefs.Bool("didShowTutorialRecordingHowTo", newValue) }
}
} }
} }

View File

@@ -12,6 +12,7 @@ enum PrefsShared {
static func registerDefaults() { static func registerDefaults() {
suite.register(defaults: [ suite.register(defaults: [
"ForceDisconnectSWCD" : true,
"RestartReminderEnabled" : true, "RestartReminderEnabled" : true,
"RestartReminderWithText" : true, "RestartReminderWithText" : true,
"RestartReminderWithBadge" : true, "RestartReminderWithBadge" : true,
@@ -26,6 +27,28 @@ enum PrefsShared {
} }
// 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) }
}
static var ForceDisconnectUnresolvableDNS: Bool {
get { PrefsShared.Bool("ForceDisconnectUnresolvableDNS") }
set { PrefsShared.Bool("ForceDisconnectUnresolvableDNS", newValue) }
}
static var ForceDisconnectSWCD: Bool {
get { PrefsShared.Bool("ForceDisconnectSWCD") }
set { PrefsShared.Bool("ForceDisconnectSWCD", newValue) }
}
}
// MARK: - Notifications // MARK: - Notifications
extension PrefsShared { extension PrefsShared {

View File

@@ -0,0 +1,68 @@
import UIKit
struct TinyMarkdown {
/// Load markdown file and run through a (very) simple parser (see below).
/// - Parameters:
/// - filename: Will automatically append `.md` extension
/// - replacements: Replace a single occurrence of search string with an attributed replacement.
static func load(_ filename: String, replacements: [String : NSMutableAttributedString] = [:]) -> UITextView {
let url = Bundle.main.url(forResource: filename, withExtension: "md")!
let str = NSMutableAttributedString(withMarkdown: try! String(contentsOf: url))
for (key, val) in replacements {
guard let r = str.string.range(of: key) else {
QLog.Debug("WARN: markdown key '\(key)' does not exist in \(filename)")
continue
}
str.replaceCharacters(in: NSRange(r, in: str.string), with: val)
}
return QuickUI.text(attributed: str)
}
}
extension NSMutableAttributedString {
/// Supports only: `#h1`, `##h2`, `###h3`, `_italic_`, `__bold__`, `___boldItalic___`
convenience init(withMarkdown content: String) {
self.init()
let emph = try! NSRegularExpression(pattern: #"(?<=(^|\W))(_{1,3})(\S|\S.*?\S)\2"#, options: [])
beginEditing()
content.enumerateLines { (line, _) in
if line.starts(with: "#") {
var h = 0
for char in line {
if char == "#" { h += 1 }
else { break }
}
var line = line
line.removeFirst(h)
line = line.trimmingCharacters(in: CharacterSet(charactersIn: " "))
switch h {
case 1: self.h1(line + "\n")
case 2: self.h2(line + "\n")
default: self.h3(line + "\n")
}
} else {
let nsline = line as NSString
let range = NSRange(location: 0, length: nsline.length)
var i = 0
for x in emph.matches(in: line, options: [], range: range) {
let r = x.range
self.normal(nsline.substring(from: i, to: r.location))
i = r.upperBound
let before = nsline.substring(with: r)
let after = before.trimmingCharacters(in: CharacterSet(charactersIn: "_"))
switch (before.count - after.count) / 2 {
case 1: self.italic(after)
case 2: self.bold(after)
default: self.boldItalic(after)
}
}
if i < range.length {
self.normal(nsline.substring(from: i, to: range.length) + "\n")
} else {
self.normal("\n")
}
}
}
endEditing()
}
}

View File

@@ -59,7 +59,7 @@ class TutorialSheet: UIViewController, UIScrollViewDelegate {
return x return x
}() }()
private let button: UIButton = { private lazy var button: UIButton = {
let x = QuickUI.button("", target: self, action: #selector(buttonTapped)) let x = QuickUI.button("", target: self, action: #selector(buttonTapped))
x.contentEdgeInsets = UIEdgeInsets(all: 8) x.contentEdgeInsets = UIEdgeInsets(all: 8)
return x return x
@@ -132,9 +132,9 @@ class TutorialSheet: UIViewController, UIScrollViewDelegate {
sheetBg.addSubview(button) sheetBg.addSubview(button)
pager.anchor([.top, .left, .right], to: sheetBg) pager.anchor([.top, .left, .right], to: sheetBg)
pageScroll.topAnchor =&= pager.bottomAnchor pageScroll.topAnchor =&= pager.bottomAnchor | .defaultHigh
pageScroll.anchor([.left, .right, .top, .bottom], to: sheetBg, margin: sheetInset) | .defaultHigh pageScroll.anchor([.left, .right, .top, .bottom], to: sheetBg, margin: sheetInset) | .defaultHigh
button.topAnchor =&= pageScroll.bottomAnchor button.topAnchor =&= pageScroll.bottomAnchor | .defaultHigh
button.anchor([.bottom, .centerX], to: sheetBg) button.anchor([.bottom, .centerX], to: sheetBg)
// button.bottomAnchor =&= sheetBg.bottomAnchor - 30 // button.bottomAnchor =&= sheetBg.bottomAnchor - 30
// button.centerXAnchor =&= sheetBg.centerXAnchor // button.centerXAnchor =&= sheetBg.centerXAnchor

View File

@@ -20,25 +20,19 @@ extension SQLiteDatabase {
try ifStep(stmt, SQLITE_ROW) try ifStep(stmt, SQLITE_ROW)
return sqlite3_column_int(stmt, 0) return sqlite3_column_int(stmt, 0)
} }
if version != 1 { if version != 2 {
QLog.Info("migrate db \(version) -> 2")
// version 0 -> 1: req(domain) -> heap(fqdn, domain) // version 0 -> 1: req(domain) -> heap(fqdn, domain)
if version == 0 { // version 1 -> 2: rec(+subtitle, +opt)
try tempMigrate() if version == 1 {
transaction("""
ALTER TABLE rec ADD COLUMN subtitle TEXT;
ALTER TABLE rec ADD COLUMN uploadkey TEXT;
""")
} }
try run(sql: "PRAGMA user_version = 1;") try run(sql: "PRAGMA user_version = 2;")
} }
} }
private func tempMigrate() throws { // TODO: remove with next internal release
do {
try run(sql: "SELECT 1 FROM req LIMIT 1;") // fails if req doesnt exist
createFunction("domainof") { ($0.first as! String).extractDomain() }
transaction("""
INSERT INTO heap(ts,fqdn,domain,opt) SELECT ts,domain,domainof(domain),nullif(logOpt,0) FROM req;
DROP TABLE req;
""")
} catch { /* no need to migrate */ }
}
} }
private enum TableName: String { private enum TableName: String {
@@ -117,12 +111,11 @@ extension SQLiteDatabase {
/// - Returns: `nil` in case no entries were transmitted. /// - Returns: `nil` in case no entries were transmitted.
@discardableResult func dnsLogsPersist() -> SQLiteRowRange? { @discardableResult func dnsLogsPersist() -> SQLiteRowRange? {
guard lastRowId(.cache) > 0 else { return nil } guard lastRowId(.cache) > 0 else { return nil }
transaction("ALTER TABLE cache RENAME TO tmp_cache; \(CreateTable.cache)")
let before = lastRowId(.heap) + 1 let before = lastRowId(.heap) + 1
createFunction("domainof") { ($0.first as! String).extractDomain() } createFunction("domainof") { ($0.first as! String).extractDomain() }
transaction(""" transaction("""
INSERT INTO heap(ts,fqdn,domain,opt) SELECT ts,dns,domainof(dns),nullif(opt&1,0) FROM tmp_cache; INSERT INTO heap(ts,fqdn,domain,opt) SELECT ts,dns,domainof(dns),nullif(opt&1,0) FROM cache;
DROP TABLE tmp_cache; DELETE FROM cache;
""") """)
let after = lastRowId(.heap) let after = lastRowId(.heap)
return (before > after) ? nil : (before, after) return (before > after) ? nil : (before, after)
@@ -296,7 +289,7 @@ extension SQLiteDatabase {
// MARK: - Recordings // MARK: - Recordings
extension CreateTable { extension CreateTable {
/// `id`: Primary, `start`: Timestamp, `stop`: Timestamp, `appid`: String, `title`: String, `notes`: String /// `id`: Primary, `start`: Timestamp, `stop`: Timestamp, `appid`: String, `title`: String, `subtitle`: String, `notes`: String
static var rec: String {""" static var rec: String {"""
CREATE TABLE IF NOT EXISTS rec( CREATE TABLE IF NOT EXISTS rec(
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
@@ -304,19 +297,25 @@ extension CreateTable {
stop INTEGER, stop INTEGER,
appid TEXT, appid TEXT,
title TEXT, title TEXT,
notes TEXT subtitle TEXT,
notes TEXT,
uploadkey TEXT
); );
"""} """}
} }
let readRecordingSelect = "id, start, stop, appid, title, subtitle, notes, uploadkey"
struct Recording { struct Recording {
let id: sqlite3_int64 let id: sqlite3_int64
let start: Timestamp let start: Timestamp
let stop: Timestamp? let stop: Timestamp?
var appId: String? = nil var appId: String? = nil
var title: String? = nil var title: String? = nil
var subtitle: String? = nil
var notes: String? = nil var notes: String? = nil
var uploadkey: String? = nil
} }
typealias AppBundleInfo = (bundleId: String, name: String?, author: String?)
extension SQLiteDatabase { extension SQLiteDatabase {
@@ -343,8 +342,9 @@ extension SQLiteDatabase {
/// Update given recording by replacing `title`, `appid`, and `notes` with new values. /// Update given recording by replacing `title`, `appid`, and `notes` with new values.
func recordingUpdate(_ r: Recording) { func recordingUpdate(_ r: Recording) {
try? run(sql: "UPDATE rec SET title = ?, appid = ?, notes = ? WHERE id = ? LIMIT 1;", try? run(sql: "UPDATE rec SET appid = ?, title = ?, subtitle = ?, notes = ?, uploadkey = ? WHERE id = ? LIMIT 1;",
bind: [BindTextOrNil(r.title), BindTextOrNil(r.appId), BindTextOrNil(r.notes), BindInt64(r.id)]) { stmt -> Void in bind: [BindTextOrNil(r.appId), BindTextOrNil(r.title), BindTextOrNil(r.subtitle),
BindTextOrNil(r.notes), BindTextOrNil(r.uploadkey), BindInt64(r.id)]) { stmt -> Void in
sqlite3_step(stmt) sqlite3_step(stmt)
} }
} }
@@ -368,12 +368,14 @@ extension SQLiteDatabase {
stop: end == 0 ? nil : end, stop: end == 0 ? nil : end,
appId: col_text(stmt, 3), appId: col_text(stmt, 3),
title: col_text(stmt, 4), title: col_text(stmt, 4),
notes: col_text(stmt, 5)) subtitle: col_text(stmt, 5),
notes: col_text(stmt, 6),
uploadkey: col_text(stmt, 7))
} }
/// `WHERE stop IS NULL` /// `WHERE stop IS NULL`
func recordingGetOngoing() -> Recording? { func recordingGetOngoing() -> Recording? {
try? run(sql: "SELECT * FROM rec WHERE stop IS NULL LIMIT 1;") { try? run(sql: "SELECT \(readRecordingSelect) FROM rec WHERE stop IS NULL LIMIT 1;") {
try ifStep($0, SQLITE_ROW) try ifStep($0, SQLITE_ROW)
return readRecording($0) return readRecording($0)
} }
@@ -389,18 +391,26 @@ extension SQLiteDatabase {
/// `WHERE stop IS NOT NULL` /// `WHERE stop IS NOT NULL`
func recordingGetAll() -> [Recording]? { func recordingGetAll() -> [Recording]? {
try? run(sql: "SELECT * FROM rec WHERE stop IS NOT NULL;") { try? run(sql: "SELECT \(readRecordingSelect) FROM rec WHERE stop IS NOT NULL;") {
allRows($0) { readRecording($0) } allRows($0) { readRecording($0) }
} }
} }
/// `WHERE id = ?` /// `WHERE id = ?`
private func recordingGet(withID: sqlite3_int64) throws -> Recording { private func recordingGet(withID: sqlite3_int64) throws -> Recording {
try run(sql: "SELECT * FROM rec WHERE id = ? LIMIT 1;", bind: [BindInt64(withID)]) { try run(sql: "SELECT \(readRecordingSelect) FROM rec WHERE id = ? LIMIT 1;", bind: [BindInt64(withID)]) {
try ifStep($0, SQLITE_ROW) try ifStep($0, SQLITE_ROW)
return readRecording($0) return readRecording($0)
} }
} }
func appBundleList() -> [AppBundleInfo]? {
try? run(sql: "SELECT appid, title, subtitle FROM rec WHERE appid IS NOT NULL GROUP BY appid ORDER BY lower(title) ASC;") {
allRows($0) {
AppBundleInfo(col_text($0, 0)!, col_text($0, 1), col_text($0, 2))
}
}
}
} }

View File

@@ -48,11 +48,16 @@ extension SQLiteDatabase {
/// - Returns: `true` if at least one row was deleted. /// - Returns: `true` if at least one row was deleted.
@discardableResult func dnsLogsDeleteOlderThan(days: Int) throws -> Bool { @discardableResult func dnsLogsDeleteOlderThan(days: Int) throws -> Bool {
guard days > 0 else { return false } guard days > 0 else { return false }
return try self.run(sql: "DELETE FROM cache WHERE ts < strftime('%s', 'now', ?);", func delFrom(_ table: String) throws -> Bool {
bind: [BindText("-\(days) days")]) { return try self.run(sql: "DELETE FROM \(table) WHERE ts < strftime('%s', 'now', ?);",
try ifStep($0, SQLITE_DONE) bind: [BindText("-\(days) days")]) {
return numberOfChanges > 0 try ifStep($0, SQLITE_DONE)
return numberOfChanges > 0
}
} }
let didDelHeap = try delFrom("heap")
let didDelCache = try delFrom("cache")
return didDelHeap || didDelCache
} }
} }

View File

@@ -48,6 +48,7 @@ class SQLiteDatabase {
static func open(path: String = URL.internalDB().relativePath) throws -> SQLiteDatabase { static func open(path: String = URL.internalDB().relativePath) throws -> SQLiteDatabase {
var db: OpaquePointer? var db: OpaquePointer?
if sqlite3_open_v2(path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, nil) == SQLITE_OK { if sqlite3_open_v2(path, &db, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | SQLITE_OPEN_FULLMUTEX, nil) == SQLITE_OK {
sqlite3_busy_timeout(db, 800)
return SQLiteDatabase(dbPointer: db) return SQLiteDatabase(dbPointer: db)
} else { } else {
defer { sqlite3_close_v2(db) } defer { sqlite3_close_v2(db) }
@@ -168,6 +169,10 @@ protocol DBBinding {
func bind(_ stmt: OpaquePointer!, _ col: Int32) -> Int32 func bind(_ stmt: OpaquePointer!, _ col: Int32) -> Int32
} }
struct BindNull : DBBinding {
func bind(_ stmt: OpaquePointer!, _ col: Int32) -> Int32 { sqlite3_bind_null(stmt, col) }
}
struct BindInt32 : DBBinding { struct BindInt32 : DBBinding {
let raw: Int32 let raw: Int32
init(_ value: Int32) { raw = value } init(_ value: Int32) { raw = value }

View File

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

View File

@@ -34,10 +34,13 @@ struct TheGreatDestroyer {
DispatchQueue.global().async { DispatchQueue.global().async {
defer { sync.continue() } defer { sync.continue() }
QLog.Info("Auto-delete logs") QLog.Info("Auto-delete logs")
guard let success = try? AppDB?.dnsLogsDeleteOlderThan(days: days), success else { do {
return // nothing changed if try AppDB!.dnsLogsDeleteOlderThan(days: days) {
sync.needsReloadDB()
}
} catch {
QLog.Warning("Couldn't auto-delete logs, \(error)")
} }
sync.needsReloadDB()
} }
} }
} }

View File

@@ -28,6 +28,18 @@ enum RecordingsDB {
AppDB?.recordingLogsGet(r) ?? [] 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. /// Update `title`, `appid`, and `notes` and post `NotifyRecordingChanged` notification.
static func update(_ r: Recording) { static func update(_ r: Recording) {
AppDB?.recordingUpdate(r) AppDB?.recordingUpdate(r)
@@ -52,5 +64,10 @@ enum RecordingsDB {
static func deleteSingle(_ r: Recording, domain: String, ts: Timestamp) -> Bool { static func deleteSingle(_ r: Recording, domain: String, ts: Timestamp) -> Bool {
(try? AppDB?.recordingLogsDelete(r.id, singleEntry: ts, domain: domain)) ?? false (try? AppDB?.recordingLogsDelete(r.id, singleEntry: ts, domain: domain)) ?? false
} }
/// Return list of previously used apps found in all recordings.
static func appList() -> [AppBundleInfo] {
AppDB?.appBundleList() ?? []
}
} }

View File

@@ -46,7 +46,14 @@ class SimulatorVPN {
@objc static func insertRandom() { @objc static func insertRandom() {
//QLog.Debug("Inserting 1 periodic log entry") //QLog.Debug("Inserting 1 periodic log entry")
let domain = "\(arc4random() % 5).count.test.com" let rand = arc4random() % 8
let domain: String
switch rand {
case 6: domain = "tmp.b.test.com"
case 7: domain = "tmp.i.test.com"
case 8: domain = "tmp.bi.test.com"
default: domain = "\(rand).count.test.com"
}
let kill = hook.processDNSRequest(domain) let kill = hook.processDNSRequest(domain)
if kill { QLog.Info("Blocked: \(domain)") } if kill { QLog.Info("Blocked: \(domain)") }
} }

View File

@@ -31,17 +31,19 @@ func ErrorAlert(_ errorDescription: String, buttonText: String = "Dismiss") -> U
/// - Parameters: /// - Parameters:
/// - buttonText: Default: `"Continue"` /// - buttonText: Default: `"Continue"`
/// - buttonStyle: Default: `.default` /// - buttonStyle: Default: `.default`
func AskAlert(title: String?, text: String?, buttonText: String = "Continue", buttonStyle: UIAlertAction.Style = .default, action: @escaping (UIAlertController) -> Void) -> UIAlertController { func AskAlert(title: String?, text: String?, buttonText: String = "Continue", cancelButton: String = "Cancel", buttonStyle: UIAlertAction.Style = .default, action: @escaping (UIAlertController) -> Void) -> UIAlertController {
let alert = Alert(title: title, text: text, buttonText: "Cancel") let alert = Alert(title: title, text: text, buttonText: cancelButton)
alert.addAction(UIAlertAction(title: buttonText, style: buttonStyle) { _ in action(alert) }) alert.addAction(UIAlertAction(title: buttonText, style: buttonStyle) { _ in action(alert) })
return alert return alert
} }
/// Show alert hinting the user to go to system settings and re-enable notifications. /// Show alert hinting the user to go to system settings and re-enable notifications.
func NotificationsDisabledAlert(presentIn viewController: UIViewController) { func NotificationsDisabledAlert(presentIn viewController: UIViewController) {
Alert(title: "Notifications Disabled", AskAlert(title: "Notifications Disabled",
text: "Go to System Settings > Notifications > AppCheck to re-enable notifications." text: "Go to System Settings > Notifications > AppCheck to re-enable notifications.",
).presentIn(viewController) buttonText: "Open settings") { _ in
URL(string: UIApplication.openSettingsURLString)?.open()
}.presentIn(viewController)
} }
// MARK: Alert with multiple options // MARK: Alert with multiple options

View File

@@ -6,6 +6,7 @@ extension UIFont {
} }
func bold() -> UIFont { withTraits(traits: .traitBold) } func bold() -> UIFont { withTraits(traits: .traitBold) }
func italic() -> UIFont { withTraits(traits: .traitItalic) } func italic() -> UIFont { withTraits(traits: .traitItalic) }
func boldItalic() -> UIFont { withTraits(traits: [.traitBold, .traitItalic]) }
func monoSpace() -> UIFont { func monoSpace() -> UIFont {
let traits = fontDescriptor.object(forKey: .traits) as? [UIFontDescriptor.TraitKey: Any] ?? [:] let traits = fontDescriptor.object(forKey: .traits) as? [UIFontDescriptor.TraitKey: Any] ?? [:]
let weight = (traits[.weight] as? CGFloat) ?? UIFont.Weight.regular.rawValue let weight = (traits[.weight] as? CGFloat) ?? UIFont.Weight.regular.rawValue
@@ -13,24 +14,29 @@ extension UIFont {
} }
} }
extension NSAttributedString { extension NSMutableAttributedString {
static func image(_ img: UIImage) -> Self { convenience init(image: UIImage, centered: Bool = false) {
self.init()
let att = NSTextAttachment() let att = NSTextAttachment()
att.image = img att.image = image
return self.init(attachment: att) append(.init(attachment: att))
if centered {
let ps = NSMutableParagraphStyle()
ps.alignment = .center
addAttribute(.paragraphStyle, value: ps, range: .init(location: 0, length: length))
}
} }
} }
extension NSMutableAttributedString { extension NSMutableAttributedString {
static private var def: UIFont = .preferredFont(forTextStyle: .body) @discardableResult func normal(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: .preferredFont(forTextStyle: style)) }
@discardableResult func bold(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).bold()) }
@discardableResult func italic(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).italic()) }
@discardableResult func boldItalic(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).boldItalic()) }
func normal(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: .preferredFont(forTextStyle: style)) } @discardableResult func h1(_ str: String) -> Self { normal(str, .title1) }
func bold(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).bold()) } @discardableResult func h2(_ str: String) -> Self { normal(str, .title2) }
func italic(_ str: String, _ style: UIFont.TextStyle = .body) -> Self { append(str, withFont: UIFont.preferredFont(forTextStyle: style).italic()) } @discardableResult func h3(_ str: String) -> Self { normal(str, .title3) }
func h1(_ str: String) -> Self { normal(str, .title1) }
func h2(_ str: String) -> Self { normal(str, .title2) }
func h3(_ str: String) -> Self { normal(str, .title3) }
private func append(_ str: String, withFont: UIFont) -> Self { private func append(_ str: String, withFont: UIFont) -> Self {
append(NSAttributedString(string: str, attributes: [ append(NSAttributedString(string: str, attributes: [
@@ -39,13 +45,4 @@ extension NSMutableAttributedString {
])) ]))
return self 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

@@ -25,6 +25,13 @@ extension String {
let parts = components(separatedBy: ".") let parts = components(separatedBy: ".")
return parts.count == 2 && listOfSLDs[parts.last!]?[parts.first!] ?? false return parts.count == 2 && listOfSLDs[parts.last!]?[parts.first!] ?? false
} }
func isValidBundleId() -> Bool {
let regex = try! NSRegularExpression(pattern: #"^[A-Za-z0-9\.\-]{1,155}$"#, options: .anchorsMatchLines)
let range = NSRange(location: 0, length: self.utf16.count)
let matches = regex.matches(in: self, options: .anchored, range: range)
return matches.count == 1
}
} }
private var listOfSLDs: [String : [String : Bool]] = { private var listOfSLDs: [String : [String : Bool]] = {
@@ -40,3 +47,9 @@ private var listOfSLDs: [String : [String : Bool]] = {
} }
return res return res
}() }()
extension NSString {
func substring(from: Int, to: Int) -> String {
substring(with: NSRange(location: from, length: to - from))
}
}

View File

@@ -90,3 +90,34 @@ extension EditActionsRemove where Self: UITableViewController {
func editableRowActions(_: IndexPath) -> [(RowAction, String)] { [(.delete, "Remove")] } func editableRowActions(_: IndexPath) -> [(RowAction, String)] { [(.delete, "Remove")] }
func editableRowActionColor(_: IndexPath, _: RowAction) -> UIColor? { nil } func editableRowActionColor(_: IndexPath, _: RowAction) -> UIColor? { nil }
} }
// MARK: - Table Cell Tap Menu
struct TableCellTapMenu {
private var index: Int = Int.max
mutating func reset() { index = Int.max }
/// Create a new tap manu and shows it immediatelly. With optional buttons.
mutating func start(_ tableView: UITableView, _ indexPath: IndexPath, items: [UIMenuItem]? = nil) -> Bool {
let menu = UIMenuController.shared
if index == indexPath.row {
menu.setMenuVisible(false, animated: true)
reset()
return false
}
index = indexPath.row
let cell = tableView.cellForRow(at: indexPath)!
menu.setTargetRect(cell.bounds, in: cell)
menu.menuItems = items
menu.setMenuVisible(true, animated: true)
return true
}
/// Returns the item if the array index is in bounds.
func getSelected<T>(_ source: [T]) -> T? {
guard index < source.count else { return nil }
return source[index]
}
}

View File

@@ -86,17 +86,28 @@ 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) var t = Int(duration)
var min = t / 60
var 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 {
if t < Recording.minTimeLongTerm {
t = Int(Recording.minTimeLongTerm) - t
min = t / 60
sec = t % 60
return String(format: "-%02d:%02d:%02d", min / 60, min % 60, sec)
} else {
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

@@ -1,9 +1,9 @@
import Foundation import Foundation
fileprivate extension FileManager { fileprivate extension FileManager {
// func exportDir() -> URL { func documentDir() -> URL {
// try! url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) try! url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
// } }
func appGroupDir() -> URL { func appGroupDir() -> URL {
containerURL(forSecurityApplicationGroupIdentifier: "group.de.uni-bamberg.psi.AppCheck")! containerURL(forSecurityApplicationGroupIdentifier: "group.de.uni-bamberg.psi.AppCheck")!
} }
@@ -25,7 +25,34 @@ extension FileManager {
} }
extension URL { extension URL {
// static func exportDir() -> URL { FileManager.default.exportDir() } static func documentDir() -> URL { FileManager.default.documentDir() }
static func appGroupDir() -> URL { FileManager.default.appGroupDir() } static func appGroupDir() -> URL { FileManager.default.appGroupDir() }
static func internalDB() -> URL { FileManager.default.internalDB() } static func internalDB() -> URL { FileManager.default.internalDB() }
static func make(_ base: String, params: [String : String]) -> URL? {
guard var components = URLComponents(string: base) else {
return nil
}
components.queryItems = params.map {
URLQueryItem(name: $0, value: $1)
}
components.percentEncodedQuery = components.percentEncodedQuery?.replacingOccurrences(of: "+", with: "%2B")
return components.url
}
@discardableResult func download(to file: URL, onSuccess: @escaping () -> Void) -> URLSessionDownloadTask {
let task = URLSession.shared.downloadTask(with: self) { location, response, error in
if let loc = location {
try? FileManager.default.removeItem(at: file)
do {
try FileManager.default.moveItem(at: loc, to: file)
onSuccess()
} catch {
NSLog("[VPN.ERROR] \(error)")
}
}
}
task.resume()
return task
}
} }

View File

@@ -0,0 +1,207 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097.3" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="W5Q-oz-bFb">
<device id="retina4_0" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Co Occurrence-->
<scene sceneID="Gbm-AP-b72">
<objects>
<viewController storyboardIdentifier="IBCoOccurrence" id="W5Q-oz-bFb" customClass="VCCoOccurrence" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="f34-NO-d8f">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<navigationBar contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rvt-nC-2Zr">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<items>
<navigationItem title="Co-Occurrence" id="csY-x8-Rpe">
<barButtonItem key="leftBarButtonItem" systemItem="done" id="eg9-p3-Xas">
<connections>
<action selector="didClose:" destination="W5Q-oz-bFb" id="wyw-vo-6xL"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" image="detail-help" id="RTh-uI-ST6">
<connections>
<action selector="showInfoScreen" destination="W5Q-oz-bFb" id="xoS-z7-PHr"/>
</connections>
</barButtonItem>
</navigationItem>
</items>
</navigationBar>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" allowsSelection="NO" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="PGb-pB-cfO">
<rect key="frame" x="0.0" y="44" width="320" height="524"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<segmentedControl key="tableHeaderView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" id="7ye-tU-pdo">
<rect key="frame" x="0.0" y="0.0" width="320" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="10s"/>
<segment title="30s"/>
</segments>
<connections>
<action selector="didChangeTime:" destination="W5Q-oz-bFb" eventType="valueChanged" id="c5h-JG-S19"/>
</connections>
</segmentedControl>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="CoOccurrenceCell" rowHeight="72" id="2qH-Bh-644" customClass="CoOccurrenceCell" customModule="AppCheck" customModuleProvider="target">
<rect key="frame" x="0.0" y="60" width="320" height="72"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="2qH-Bh-644" id="Lwk-Uj-viQ">
<rect key="frame" x="0.0" y="0.0" width="320" height="72"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="99." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="qaw-ql-zIB" customClass="TagLabel" customModule="AppCheck" customModuleProvider="target">
<rect key="frame" x="15" y="39.5" width="32" height="21.5"/>
<color key="backgroundColor" systemColor="secondarySystemBackgroundColor" red="0.94901960780000005" green="0.94901960780000005" blue="0.96862745100000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="width" secondItem="qaw-ql-zIB" secondAttribute="height" multiplier="3:2" id="VOJ-f5-xhk"/>
</constraints>
<fontDescription key="fontDescription" style="UICTFontTextStyleFootnote"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="characterWrap" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" translatesAutoresizingMaskIntoConstraints="NO" id="zbU-wC-qJG">
<rect key="frame" x="15" y="11" width="290" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<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="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"/>
<nil key="highlightedColor"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="padRight">
<real key="value" value="1"/>
</userDefinedRuntimeAttribute>
</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="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"/>
<nil key="highlightedColor"/>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="padRight">
<real key="value" value="1"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</label>
<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="190" y="39.5" width="3" height="21.5"/>
<constraints>
<constraint firstAttribute="width" constant="3" id="wWb-VG-Kqa"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="percent">
<real key="value" value="1"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="color" keyPath="barColor">
<color key="value" systemColor="systemOrangeColor" red="1" green="0.58431372550000005" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</view>
<view opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="JwY-mq-rYZ" customClass="MeterBar" customModule="AppCheck" customModuleProvider="target">
<rect key="frame" x="302" y="39.5" width="3" height="21.5"/>
<constraints>
<constraint firstAttribute="width" constant="3" id="Tta-m5-vwa"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="percent">
<real key="value" value="1"/>
</userDefinedRuntimeAttribute>
<userDefinedRuntimeAttribute type="color" keyPath="barColor">
<color key="value" systemColor="systemOrangeColor" red="1" green="0.58431372550000005" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</view>
</subviews>
<constraints>
<constraint firstItem="qaw-ql-zIB" firstAttribute="height" secondItem="q5v-FM-iGo" secondAttribute="height" id="2Ug-qN-ido"/>
<constraint firstItem="zbU-wC-qJG" firstAttribute="leading" secondItem="Lwk-Uj-viQ" secondAttribute="leadingMargin" id="2Zo-jC-08y"/>
<constraint firstItem="JwY-mq-rYZ" firstAttribute="top" secondItem="zCg-I0-4Tz" secondAttribute="top" id="3MU-gk-eUU"/>
<constraint firstItem="T4X-cn-msT" firstAttribute="height" secondItem="zCg-I0-4Tz" secondAttribute="height" multiplier="0.75" id="AwE-JC-MFF"/>
<constraint firstAttribute="bottomMargin" secondItem="q5v-FM-iGo" secondAttribute="bottom" id="B2M-MQ-kAw"/>
<constraint firstItem="9Bb-e5-D3O" firstAttribute="bottom" secondItem="q5v-FM-iGo" secondAttribute="bottom" id="Efb-Ud-lxb"/>
<constraint firstItem="JWp-6l-HTJ" firstAttribute="height" secondItem="q5v-FM-iGo" secondAttribute="height" multiplier="0.75" id="Gfb-up-g1b"/>
<constraint firstItem="JwY-mq-rYZ" firstAttribute="trailing" secondItem="zCg-I0-4Tz" secondAttribute="trailing" id="RlS-DQ-pdh"/>
<constraint firstItem="zCg-I0-4Tz" firstAttribute="leading" secondItem="T4X-cn-msT" secondAttribute="trailing" constant="4" id="VpT-5w-aKh"/>
<constraint firstAttribute="trailingMargin" secondItem="zCg-I0-4Tz" secondAttribute="trailing" id="ai7-PW-ISq"/>
<constraint firstItem="qaw-ql-zIB" firstAttribute="leading" secondItem="Lwk-Uj-viQ" secondAttribute="leadingMargin" id="bGT-hc-lSG"/>
<constraint firstItem="JWp-6l-HTJ" firstAttribute="height" secondItem="T4X-cn-msT" secondAttribute="height" id="cKO-4d-ikl"/>
<constraint firstItem="JWp-6l-HTJ" firstAttribute="centerY" secondItem="q5v-FM-iGo" secondAttribute="centerY" id="dZr-0G-1sp"/>
<constraint firstAttribute="trailingMargin" secondItem="zbU-wC-qJG" secondAttribute="trailing" id="e7x-RS-YWo"/>
<constraint firstItem="JwY-mq-rYZ" firstAttribute="bottom" secondItem="zCg-I0-4Tz" secondAttribute="bottom" id="fAV-yh-H1r"/>
<constraint firstItem="9Bb-e5-D3O" firstAttribute="trailing" secondItem="q5v-FM-iGo" secondAttribute="trailing" id="fFF-y3-qOe"/>
<constraint firstItem="qaw-ql-zIB" firstAttribute="top" secondItem="zbU-wC-qJG" secondAttribute="bottom" constant="8" symbolic="YES" id="fgw-q8-YRD"/>
<constraint firstItem="9Bb-e5-D3O" firstAttribute="top" secondItem="q5v-FM-iGo" secondAttribute="top" id="idg-nm-vIj"/>
<constraint firstItem="T4X-cn-msT" firstAttribute="leading" secondItem="q5v-FM-iGo" secondAttribute="trailing" constant="12" id="kZj-Tn-BQ3"/>
<constraint firstItem="zbU-wC-qJG" firstAttribute="top" secondItem="Lwk-Uj-viQ" secondAttribute="top" constant="11" id="o7o-M0-sA2"/>
<constraint firstItem="q5v-FM-iGo" firstAttribute="top" secondItem="zbU-wC-qJG" secondAttribute="bottom" constant="8" symbolic="YES" id="peW-Pg-5WC"/>
<constraint firstItem="zCg-I0-4Tz" firstAttribute="top" secondItem="zbU-wC-qJG" secondAttribute="bottom" constant="8" symbolic="YES" id="ttp-yA-tsi"/>
<constraint firstItem="T4X-cn-msT" firstAttribute="centerY" secondItem="zCg-I0-4Tz" secondAttribute="centerY" id="tz9-Vr-fB6"/>
<constraint firstItem="JWp-6l-HTJ" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="qaw-ql-zIB" secondAttribute="trailing" constant="8" symbolic="YES" id="xFl-RU-Ynw"/>
<constraint firstItem="q5v-FM-iGo" firstAttribute="leading" secondItem="JWp-6l-HTJ" secondAttribute="trailing" constant="4" id="xHw-Pf-daH"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="avgdiff" destination="zCg-I0-4Tz" id="Jno-Yc-ngL"/>
<outlet property="avgdiffMeter" destination="JwY-mq-rYZ" id="QNx-rP-17Z"/>
<outlet property="count" destination="q5v-FM-iGo" id="AFk-93-mhs"/>
<outlet property="countMeter" destination="9Bb-e5-D3O" id="zqt-dT-ecT"/>
<outlet property="rank" destination="qaw-ql-zIB" id="q6Y-JS-NFU"/>
<outlet property="title" destination="zbU-wC-qJG" id="hgV-L0-blX"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="W5Q-oz-bFb" id="7lD-aQ-QhQ"/>
</connections>
</tableView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="PGb-pB-cfO" firstAttribute="top" secondItem="rvt-nC-2Zr" secondAttribute="bottom" id="Edp-lx-Xld"/>
<constraint firstItem="4eZ-5P-8sz" firstAttribute="bottom" secondItem="PGb-pB-cfO" secondAttribute="bottom" id="OAG-HL-4N4"/>
<constraint firstItem="PGb-pB-cfO" firstAttribute="leading" secondItem="4eZ-5P-8sz" secondAttribute="leading" id="V6d-HM-JzJ"/>
<constraint firstItem="4eZ-5P-8sz" firstAttribute="trailing" secondItem="rvt-nC-2Zr" secondAttribute="trailing" id="cmE-iH-06W"/>
<constraint firstItem="rvt-nC-2Zr" firstAttribute="top" secondItem="4eZ-5P-8sz" secondAttribute="top" id="epT-LW-CJV"/>
<constraint firstItem="4eZ-5P-8sz" firstAttribute="trailing" secondItem="PGb-pB-cfO" secondAttribute="trailing" id="j8i-8q-qGS"/>
<constraint firstItem="rvt-nC-2Zr" firstAttribute="leading" secondItem="4eZ-5P-8sz" secondAttribute="leading" id="skN-SN-Wu7"/>
</constraints>
<viewLayoutGuide key="safeArea" id="4eZ-5P-8sz"/>
</view>
<connections>
<outlet property="tableView" destination="PGb-pB-cfO" id="5gT-KC-ce5"/>
<outlet property="timeSegment" destination="7ye-tU-pdo" id="2ys-X4-Jff"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="yYY-5U-gct" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="0.0" y="0.0"/>
</scene>
</scenes>
<resources>
<image name="detail-help" width="22" height="22"/>
</resources>
</document>

View File

@@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097.2" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="sfA-EG-18J">
<device id="retina4_0" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Main-->
<scene sceneID="7Rl-BK-ry5">
<objects>
<tabBarController id="sfA-EG-18J" customClass="TBCMain" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<tabBar key="tabBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="qza-ey-Iaz">
<rect key="frame" x="0.0" y="0.0" width="414" height="49"/>
<autoresizingMask key="autoresizingMask"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</tabBar>
<connections>
<segue destination="RcB-4v-fd4" kind="relationship" relationship="viewControllers" id="cmC-pu-5n2"/>
<segue destination="xaF-Q9-CPX" kind="relationship" relationship="viewControllers" id="bi1-vo-w1d"/>
<segue destination="dIk-JY-9vE" kind="relationship" relationship="viewControllers" id="ude-bF-ump"/>
</connections>
</tabBarController>
<placeholder placeholderIdentifier="IBFirstResponder" id="RDz-8t-yhN" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="0.0" y="-150"/>
</scene>
<!--Requests-->
<scene sceneID="bDO-X1-bCe">
<objects>
<navigationController id="RcB-4v-fd4" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Requests" image="journal" id="Sj5-Kb-Li8"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="HWd-73-m8j">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="H0N-TG-Ck1" kind="relationship" relationship="rootViewController" id="jDh-HG-7ke"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="8j4-AX-JBN" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-700" y="650"/>
</scene>
<!--Requests-->
<scene sceneID="kwb-zk-i3S">
<objects>
<viewControllerPlaceholder storyboardName="Requests" id="H0N-TG-Ck1" sceneMemberID="viewController">
<navigationItem key="navigationItem" id="cLA-5B-TZx"/>
</viewControllerPlaceholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="rwq-DI-GUl" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-700" y="1150"/>
</scene>
<!--Recordings-->
<scene sceneID="V1I-GW-1gY">
<objects>
<placeholder placeholderIdentifier="IBFirstResponder" id="df6-nz-nUq" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
<navigationController id="xaF-Q9-CPX" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Recordings" image="tag" id="5ww-wt-MOH"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="ZFR-QK-Ofl">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="thF-Z0-CBq" kind="relationship" relationship="rootViewController" id="BuU-Uy-Zhv"/>
</connections>
</navigationController>
</objects>
<point key="canvasLocation" x="0.0" y="650"/>
</scene>
<!--Recordings-->
<scene sceneID="oLG-Zp-Qm7">
<objects>
<viewControllerPlaceholder storyboardName="Recordings" id="thF-Z0-CBq" sceneMemberID="viewController">
<navigationItem key="navigationItem" id="kMV-TC-eKI"/>
</viewControllerPlaceholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="B2d-DP-1Yp" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="0.0" y="1150"/>
</scene>
<!--Settings-->
<scene sceneID="OEQ-fb-haL">
<objects>
<navigationController id="dIk-JY-9vE" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Settings" image="settings" id="dQu-wE-a8u"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="yYW-rX-VnB">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="cV2-If-0fV" kind="relationship" relationship="rootViewController" id="vQq-KE-MOO"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="bg9-bR-vlx" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="700" y="650"/>
</scene>
<!--Settings-->
<scene sceneID="l0f-fL-3tG">
<objects>
<viewControllerPlaceholder storyboardName="Settings" id="cV2-If-0fV" sceneMemberID="viewController">
<navigationItem key="navigationItem" id="wlO-Ea-6he"/>
</viewControllerPlaceholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="i3W-Ff-rJL" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="700" y="1150"/>
</scene>
</scenes>
<resources>
<image name="journal" width="25" height="25"/>
<image name="settings" width="25" height="25"/>
<image name="tag" width="25" height="25"/>
</resources>
</document>

View File

@@ -0,0 +1,744 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097.3" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="hm5-7q-Zfi">
<device id="retina4_0" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Recordings-->
<scene sceneID="ODR-PD-nTU">
<objects>
<viewController id="hm5-7q-Zfi" customClass="VCRecordings" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="JYr-yE-eGS">
<rect key="frame" x="0.0" y="0.0" width="320" height="519"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="Wz5-zb-gwz">
<rect key="frame" x="0.0" y="0.0" width="320" height="519"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ppJ-js-Wwz">
<rect key="frame" x="0.0" y="0.0" width="320" height="40"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Start new recording" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="sYd-b2-Puz">
<rect key="frame" x="20" y="8" width="280" height="24"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="By4-Qr-Zg2">
<rect key="frame" x="278" y="9" width="22" height="22"/>
<state key="normal" image="detail-help"/>
<connections>
<action selector="showInfo:" destination="hm5-7q-Zfi" eventType="touchUpInside" id="AiT-rb-53G"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="sYd-b2-Puz" firstAttribute="leading" secondItem="ppJ-js-Wwz" secondAttribute="leading" constant="20" symbolic="YES" id="35n-12-YFH"/>
<constraint firstAttribute="bottom" secondItem="sYd-b2-Puz" secondAttribute="bottom" priority="999" constant="8" id="4We-J8-dH8"/>
<constraint firstAttribute="trailing" secondItem="sYd-b2-Puz" secondAttribute="trailing" constant="20" symbolic="YES" id="7eW-5L-F4Z"/>
<constraint firstItem="sYd-b2-Puz" firstAttribute="top" secondItem="ppJ-js-Wwz" secondAttribute="top" priority="999" constant="8" id="S3K-8P-agX"/>
<constraint firstItem="By4-Qr-Zg2" firstAttribute="centerY" secondItem="sYd-b2-Puz" secondAttribute="centerY" id="ScC-FQ-oU3"/>
<constraint firstAttribute="trailing" secondItem="By4-Qr-Zg2" secondAttribute="trailing" constant="20" symbolic="YES" id="yLs-7S-Uwz"/>
</constraints>
</view>
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="La3-9e-6TK">
<rect key="frame" x="0.0" y="40" width="320" height="55"/>
<subviews>
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" momentary="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2MI-6l-YQt">
<rect key="frame" x="20" y="8" width="280" height="32"/>
<segments>
<segment title="App"/>
<segment title="Background"/>
</segments>
<connections>
<action selector="startRecording:" destination="hm5-7q-Zfi" eventType="valueChanged" id="4p5-9Q-clW"/>
</connections>
</segmentedControl>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="2MI-6l-YQt" firstAttribute="top" secondItem="La3-9e-6TK" secondAttribute="top" priority="999" constant="8" id="TTg-gG-wEP"/>
<constraint firstAttribute="bottom" secondItem="2MI-6l-YQt" secondAttribute="bottom" priority="999" constant="16" id="WPY-VT-xfo"/>
<constraint firstAttribute="trailing" secondItem="2MI-6l-YQt" secondAttribute="trailing" constant="20" id="hhi-TT-2VS"/>
<constraint firstItem="2MI-6l-YQt" firstAttribute="leading" secondItem="La3-9e-6TK" secondAttribute="leading" constant="20" id="p5F-L6-whI"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="9Yj-FX-eFd">
<rect key="frame" x="0.0" y="95" width="320" height="54.5"/>
<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">
<rect key="frame" x="8" y="8" width="200" height="38.5"/>
<accessibility key="accessibilityConfiguration">
<accessibilityTraits key="traits" staticText="YES" updatesFrequently="YES"/>
</accessibility>
<fontDescription key="fontDescription" type="system" weight="ultraLight" pointSize="32"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<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.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
<state key="normal" title="Stop">
<color key="titleColor" systemColor="systemRedColor" red="1" green="0.23137254900000001" blue="0.18823529410000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</state>
<connections>
<action selector="stopRecording:" destination="hm5-7q-Zfi" eventType="touchUpInside" id="CWK-Tg-Tb8"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="rbR-np-cXD" firstAttribute="leading" secondItem="9Yj-FX-eFd" secondAttribute="leading" constant="8" id="610-aO-U6Y"/>
<constraint firstItem="vAq-EZ-Gmx" firstAttribute="top" secondItem="9Yj-FX-eFd" secondAttribute="top" priority="999" constant="8" id="CKZ-8m-kmt"/>
<constraint firstItem="vAq-EZ-Gmx" firstAttribute="leading" secondItem="rbR-np-cXD" secondAttribute="trailing" constant="4" id="Cya-vS-90h"/>
<constraint firstAttribute="trailing" secondItem="vAq-EZ-Gmx" secondAttribute="trailing" constant="8" id="DhM-H2-4I7"/>
<constraint firstItem="rbR-np-cXD" firstAttribute="width" secondItem="vAq-EZ-Gmx" secondAttribute="width" multiplier="2" id="REp-Ug-zuV"/>
<constraint firstAttribute="bottom" secondItem="rbR-np-cXD" secondAttribute="bottom" priority="999" constant="8" id="Ukk-3a-Vsi"/>
<constraint firstAttribute="bottom" secondItem="vAq-EZ-Gmx" secondAttribute="bottom" priority="999" constant="8" id="dV2-g0-7gH"/>
<constraint firstItem="rbR-np-cXD" firstAttribute="top" secondItem="9Yj-FX-eFd" secondAttribute="top" priority="999" constant="8" id="mrE-Ej-dqM"/>
</constraints>
</view>
<containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="v3Z-HR-abM">
<rect key="frame" x="0.0" y="149.5" width="320" height="369.5"/>
<connections>
<segue destination="Fln-DD-aId" kind="embed" id="eFH-Rl-aBL"/>
</connections>
</containerView>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="Wz5-zb-gwz" firstAttribute="leading" secondItem="lFq-fl-zah" secondAttribute="leading" id="Sjv-qq-h50"/>
<constraint firstItem="Wz5-zb-gwz" firstAttribute="trailing" secondItem="lFq-fl-zah" secondAttribute="trailing" id="hhA-jU-DJS"/>
<constraint firstItem="Wz5-zb-gwz" firstAttribute="bottom" secondItem="lFq-fl-zah" secondAttribute="bottom" id="m6I-NP-LhY"/>
<constraint firstItem="Wz5-zb-gwz" firstAttribute="top" secondItem="lFq-fl-zah" secondAttribute="top" id="pEc-Cz-v9f"/>
</constraints>
<viewLayoutGuide key="safeArea" id="lFq-fl-zah"/>
</view>
<navigationItem key="navigationItem" id="ceR-rC-rur"/>
<nil key="simulatedTopBarMetrics"/>
<simulatedTabBarMetrics key="simulatedBottomBarMetrics" translucent="NO"/>
<connections>
<outlet property="buttonView" destination="La3-9e-6TK" id="UMg-xx-6OV"/>
<outlet property="headerView" destination="ppJ-js-Wwz" id="68u-8M-R2Q"/>
<outlet property="runningView" destination="9Yj-FX-eFd" id="L2C-YR-2HN"/>
<outlet property="startSegment" destination="2MI-6l-YQt" id="Jun-ct-Xag"/>
<outlet property="stopButton" destination="vAq-EZ-Gmx" id="XiW-1H-I9y"/>
<outlet property="timeLabel" destination="rbR-np-cXD" id="EEe-8F-HT6"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Wfy-Tp-A9o" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="0.0" y="0.0"/>
</scene>
<!--Previous Recordings-->
<scene sceneID="RqA-Jc-FDE">
<objects>
<tableViewController id="Fln-DD-aId" customClass="TVCPreviousRecords" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="7cH-g6-H5z">
<rect key="frame" x="0.0" y="0.0" width="320" height="369.5"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="detailButton" indentationWidth="10" reuseIdentifier="PreviousRecordCell" textLabel="hr0-Xt-5gV" detailTextLabel="Xav-Ub-clj" style="IBUITableViewCellStyleSubtitle" id="3kW-3B-1bx">
<rect key="frame" x="0.0" y="28" width="320" height="57.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="3kW-3B-1bx" id="OKV-a6-jjd">
<rect key="frame" x="0.0" y="0.0" width="280" height="57.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" id="hr0-Xt-5gV">
<rect key="frame" x="16" y="9" width="33.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Xav-Ub-clj">
<rect key="frame" x="16" y="32.5" width="44" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<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>
</subviews>
</tableViewCellContentView>
<connections>
<segue destination="50g-BI-Q6S" kind="push" identifier="openRecordDetailsSegue" id="arP-jR-O9d"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="Fln-DD-aId" id="oHb-mU-M1Z"/>
<outlet property="delegate" destination="Fln-DD-aId" id="6PY-c0-Nfp"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Previous Recordings" id="ow1-cy-qXt"/>
<connections>
<segue destination="VRk-wv-rhk" kind="modal" identifier="editRecordSegue" id="8rY-sA-Iig"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Lta-uo-x4m" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="700" y="0.0"/>
</scene>
<!--Logs-->
<scene sceneID="DxJ-8o-gTM">
<objects>
<tableViewController id="50g-BI-Q6S" customClass="TVCRecordingDetails" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="cLV-Db-JxM">
<rect key="frame" x="0.0" y="0.0" width="320" height="369.5"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="RecordDetailCountedCell" textLabel="rN0-kA-Eln" detailTextLabel="xRp-XG-oKf" style="IBUITableViewCellStyleValue1" id="ceT-cF-lLF">
<rect key="frame" x="0.0" y="28" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ceT-cF-lLF" id="c5Y-xg-hSL">
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="characterWrap" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" id="rN0-kA-Eln">
<rect key="frame" x="16" y="12" width="33.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="xRp-XG-oKf">
<rect key="frame" x="260" y="12" width="44" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<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>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="RecordDetailShortCell" textLabel="rIc-r4-6pg" detailTextLabel="0pW-ZC-wmh" style="IBUITableViewCellStyleValue2" id="hzU-cx-nIs">
<rect key="frame" x="0.0" y="71.5" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="hzU-cx-nIs" id="scX-pQ-E7z">
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="right" lineBreakMode="headTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" id="rIc-r4-6pg">
<rect key="frame" x="16" y="12" width="91" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<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="Detail" textAlignment="natural" lineBreakMode="characterWrap" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" id="0pW-ZC-wmh">
<rect key="frame" x="113" y="12" width="44" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="RecordDetailLongCell" textLabel="xDy-8J-JFT" detailTextLabel="kgF-BN-FdV" style="IBUITableViewCellStyleSubtitle" id="Q4T-JJ-fqY">
<rect key="frame" x="0.0" y="115" width="320" height="57.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Q4T-JJ-fqY" id="8hy-Rg-b6Q">
<rect key="frame" x="0.0" y="0.0" width="320" height="57.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="characterWrap" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" id="xDy-8J-JFT">
<rect key="frame" x="16" y="9" width="33.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" adjustsFontSizeToFit="NO" id="kgF-BN-FdV">
<rect key="frame" x="16" y="32.5" width="44" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<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>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="RecordNoResultsCell" textLabel="bmQ-Cn-BOm" style="IBUITableViewCellStyleDefault" id="JZ4-vZ-MnG">
<rect key="frame" x="0.0" y="172.5" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="JZ4-vZ-MnG" id="TWb-p9-EMM">
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text=" no results " textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000001192092896" adjustsFontSizeToFit="NO" id="bmQ-Cn-BOm">
<rect key="frame" x="16" y="0.0" width="288" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<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>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="50g-BI-Q6S" id="SFM-IM-FRx"/>
<outlet property="delegate" destination="50g-BI-Q6S" id="LBY-sp-dg0"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Logs" id="AXT-fV-keV">
<rightBarButtonItems>
<barButtonItem systemItem="action" id="UkE-Wi-JjW">
<connections>
<segue destination="1e2-YP-lHV" kind="push" id="JL4-tN-vZg"/>
</connections>
</barButtonItem>
<barButtonItem image="line-expand" id="xLc-O7-KVB">
<connections>
<action selector="toggleDisplayStyle:" destination="50g-BI-Q6S" id="3wo-9O-7gV"/>
</connections>
</barButtonItem>
</rightBarButtonItems>
</navigationItem>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="lan-I9-b0a" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1400" y="0.0"/>
</scene>
<!--Contribute-->
<scene sceneID="np1-8y-nci">
<objects>
<tableViewController id="1e2-YP-lHV" customClass="TVCShareRecording" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="Q9f-Bw-9h3">
<rect key="frame" x="0.0" y="0.0" width="320" height="369.5"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="shareTextCell" textLabel="Jpk-wK-OOM" style="IBUITableViewCellStyleDefault" id="9z8-9E-JVQ">
<rect key="frame" x="0.0" y="55.5" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="9z8-9E-JVQ" id="TtO-r6-0fS">
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Description" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumScaleFactor="0.90000000000000002" id="Jpk-wK-OOM">
<rect key="frame" x="16" y="0.0" width="288" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="shareCheckboxCell" textLabel="OM7-b6-6E8" style="IBUITableViewCellStyleDefault" id="q9q-X3-TNl">
<rect key="frame" x="0.0" y="99" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="q9q-X3-TNl" id="Hf9-5j-89Q">
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="OM7-b6-6E8">
<rect key="frame" x="16" y="0.0" width="288" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="5am-ax-7O0">
<rect key="frame" x="257" y="6" width="49" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
<connections>
<action selector="didChangeNotesCheckbox:" destination="1e2-YP-lHV" eventType="valueChanged" id="xzg-3J-0IT"/>
</connections>
</switch>
</subviews>
</tableViewCellContentView>
<connections>
<outlet property="accessoryView" destination="5am-ax-7O0" id="fw9-Nb-leO"/>
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="shareOpenTextCell" textLabel="842-tZ-Dai" style="IBUITableViewCellStyleDefault" id="Fiz-tT-R1A">
<rect key="frame" x="0.0" y="142.5" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Fiz-tT-R1A" id="ZfU-g2-ohM">
<rect key="frame" x="0.0" y="0.0" width="293" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Open Text" lineBreakMode="tailTruncation" numberOfLines="6" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="842-tZ-Dai">
<rect key="frame" x="16" y="0.0" width="269" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
<connections>
<segue destination="5DP-MB-rOM" kind="push" id="sLA-GQ-Jxx"/>
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="shareKeyValueCell" textLabel="skk-x9-pZl" detailTextLabel="Z4C-mX-qNQ" style="IBUITableViewCellStyleValue2" id="3xN-XZ-3WD">
<rect key="frame" x="0.0" y="186" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="3xN-XZ-3WD" id="V5u-dB-PAq">
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="right" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="skk-x9-pZl">
<rect key="frame" x="16" y="13" width="91" height="18"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<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="Detail" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Z4C-mX-qNQ">
<rect key="frame" x="113" y="13" width="39.5" height="18"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="checkmark" indentationWidth="10" reuseIdentifier="shareLogCell" textLabel="c0B-OV-ujb" detailTextLabel="sAD-Ns-DV6" style="IBUITableViewCellStyleSubtitle" id="ilN-ct-Db4">
<rect key="frame" x="0.0" y="229.5" width="320" height="57.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ilN-ct-Db4" id="xMK-8l-diB">
<rect key="frame" x="0.0" y="0.0" width="280" height="57.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="characterWrap" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="c0B-OV-ujb">
<rect key="frame" x="16" y="9" width="33.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="4" baselineAdjustment="alignBaselines" adjustsLetterSpacingToFitWidth="YES" adjustsFontSizeToFit="NO" id="sAD-Ns-DV6">
<rect key="frame" x="16" y="32.5" width="33" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<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>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="1e2-YP-lHV" id="nSk-Aa-sk1"/>
<outlet property="delegate" destination="1e2-YP-lHV" id="bQg-k9-Jvt"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Contribute" id="rd1-ra-OBF">
<barButtonItem key="rightBarButtonItem" title="Send" id="N7P-2k-lkO">
<connections>
<action selector="shareRecording:" destination="1e2-YP-lHV" id="QrV-1X-bQ3"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="sendButton" destination="N7P-2k-lkO" id="POi-w1-U0C"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="cIS-61-X0s" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2100" y="0.0"/>
</scene>
<!--Edit Notes-->
<scene sceneID="1PP-Uc-VkW">
<objects>
<viewController hidesBottomBarWhenPushed="YES" id="5DP-MB-rOM" customClass="VCEditText" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="F5N-5G-rm9">
<rect key="frame" x="0.0" y="0.0" width="320" height="325.5"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" showsHorizontalScrollIndicator="NO" contentInsetAdjustmentBehavior="never" text="Lorem ipsum dolor sit er elit lamet" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="pCU-n1-q6J">
<rect key="frame" x="0.0" y="0.0" width="320" height="369.5"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="pCU-n1-q6J" firstAttribute="leading" secondItem="5JR-8n-qUg" secondAttribute="leading" id="7fM-0S-SUi"/>
<constraint firstItem="5JR-8n-qUg" firstAttribute="bottom" secondItem="pCU-n1-q6J" secondAttribute="bottom" id="AG3-zo-ek0"/>
<constraint firstItem="pCU-n1-q6J" firstAttribute="top" secondItem="5JR-8n-qUg" secondAttribute="top" id="IM4-ty-wxG"/>
<constraint firstItem="pCU-n1-q6J" firstAttribute="trailing" secondItem="5JR-8n-qUg" secondAttribute="trailing" id="dMB-pY-HEO"/>
</constraints>
<viewLayoutGuide key="safeArea" id="5JR-8n-qUg"/>
</view>
<extendedEdge key="edgesForExtendedLayout"/>
<navigationItem key="navigationItem" title="Edit Notes" id="Ex8-YV-Ebv"/>
<connections>
<outlet property="textBottom" destination="AG3-zo-ek0" id="EXG-Zb-UGb"/>
<outlet property="textView" destination="pCU-n1-q6J" id="K4D-t0-F9P"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="l5b-JD-MQb" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2800" y="0.0"/>
</scene>
<!--Edit Recording-->
<scene sceneID="pqx-CU-4AP">
<objects>
<viewController id="VRk-wv-rhk" customClass="VCEditRecording" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="rXz-Mk-wrK">
<rect key="frame" x="0.0" y="0.0" width="320" height="369.5"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<navigationBar contentMode="scaleToFill" translucent="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2yS-xK-Wac">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<gestureRecognizers/>
<items>
<navigationItem title="Edit" id="JSi-oz-VRx">
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="TGg-60-wZW">
<connections>
<action selector="didTapCancel" destination="VRk-wv-rhk" id="idg-Q7-qLu"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" enabled="NO" style="done" systemItem="save" id="rWg-hE-Ydl">
<connections>
<action selector="didTapSave" destination="VRk-wv-rhk" id="fPE-i2-I06"/>
</connections>
</barButtonItem>
</navigationItem>
</items>
<connections>
<outletCollection property="gestureRecognizers" destination="B0n-l6-MKc" appends="YES" id="57w-bJ-Vjh"/>
</connections>
</navigationBar>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="xdn-EU-IMx">
<rect key="frame" x="16" y="52" width="288" height="307.5"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Guy-Ra-fpS" userLabel="Title">
<rect key="frame" x="0.0" y="0.0" width="288" height="40"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="AppCheck" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Et0-8d-CId">
<rect key="frame" x="0.0" y="2" width="80" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="University Bamberg" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="l8O-Kw-uc8">
<rect key="frame" x="0.0" y="23.5" width="111" height="14.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<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>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="LaunchIcon.png" translatesAutoresizingMaskIntoConstraints="NO" id="rbW-pK-Kct">
<rect key="frame" x="248" y="0.0" width="40" height="40"/>
<constraints>
<constraint firstAttribute="width" secondItem="rbW-pK-Kct" secondAttribute="height" multiplier="1:1" id="dV9-kR-y39"/>
</constraints>
</imageView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<gestureRecognizers/>
<constraints>
<constraint firstAttribute="height" priority="999" constant="40" id="5ew-Cq-VKh"/>
<constraint firstItem="rbW-pK-Kct" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Et0-8d-CId" secondAttribute="trailing" constant="4" id="EzM-br-BIF"/>
<constraint firstItem="Et0-8d-CId" firstAttribute="leading" secondItem="Guy-Ra-fpS" secondAttribute="leading" id="F1b-aQ-6rA"/>
<constraint firstAttribute="bottom" secondItem="l8O-Kw-uc8" secondAttribute="bottom" priority="999" constant="2" id="Tpw-nU-HHb"/>
<constraint firstItem="l8O-Kw-uc8" firstAttribute="top" secondItem="Et0-8d-CId" secondAttribute="bottom" priority="999" constant="1" id="Wpc-8H-6b8"/>
<constraint firstItem="l8O-Kw-uc8" firstAttribute="leading" secondItem="Guy-Ra-fpS" secondAttribute="leading" id="Xmq-Pl-TrJ"/>
<constraint firstItem="rbW-pK-Kct" firstAttribute="centerY" secondItem="Guy-Ra-fpS" secondAttribute="centerY" id="bYB-Jd-Meb"/>
<constraint firstAttribute="trailing" secondItem="rbW-pK-Kct" secondAttribute="trailing" id="bsC-L7-fZn"/>
<constraint firstItem="rbW-pK-Kct" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="l8O-Kw-uc8" secondAttribute="trailing" constant="4" id="fJ8-TH-hwT"/>
<constraint firstItem="Et0-8d-CId" firstAttribute="top" secondItem="Guy-Ra-fpS" secondAttribute="top" priority="999" constant="2" id="nXu-FP-JVX"/>
<constraint firstItem="rbW-pK-Kct" firstAttribute="height" secondItem="Guy-Ra-fpS" secondAttribute="height" id="zLK-Gu-HcF"/>
</constraints>
<connections>
<outletCollection property="gestureRecognizers" destination="Jab-q2-U9X" appends="YES" id="V27-A7-AL5"/>
</connections>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ybL-UG-dwT" userLabel="Notes">
<rect key="frame" x="0.0" y="48" width="288" height="160.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Notes" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="QJp-6C-yoZ">
<rect key="frame" x="0.0" y="0.0" width="288" height="24"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" showsHorizontalScrollIndicator="NO" translatesAutoresizingMaskIntoConstraints="NO" id="NXU-yU-eST">
<rect key="frame" x="0.0" y="24" width="288" height="136.5"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<string key="text">1. Line
2. Line
3. Line
4. Line</string>
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
<connections>
<outlet property="delegate" destination="VRk-wv-rhk" id="vej-jI-13V"/>
</connections>
</textView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="NXU-yU-eST" firstAttribute="leading" secondItem="ybL-UG-dwT" secondAttribute="leading" id="D6U-8L-f9m"/>
<constraint firstAttribute="height" relation="greaterThanOrEqual" priority="750" constant="107" id="Pfy-uW-kRl"/>
<constraint firstItem="NXU-yU-eST" firstAttribute="trailing" secondItem="ybL-UG-dwT" secondAttribute="trailing" id="eBc-6g-nWr"/>
<constraint firstItem="NXU-yU-eST" firstAttribute="top" secondItem="QJp-6C-yoZ" secondAttribute="bottom" id="mnZ-WQ-LX8"/>
<constraint firstItem="NXU-yU-eST" firstAttribute="bottom" secondItem="ybL-UG-dwT" secondAttribute="bottom" id="vFS-tG-E43"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="QiY-Mm-Dej" userLabel="Details">
<rect key="frame" x="0.0" y="216.5" width="288" height="91"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Details" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="FR1-Nt-XuB">
<rect key="frame" x="0.0" y="0.0" width="288" height="24"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" contentMode="scaleToFill" bounces="NO" scrollEnabled="NO" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" bouncesZoom="NO" editable="NO" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pql-H5-k6U">
<rect key="frame" x="0.0" y="24" width="288" height="67"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<string key="text">Start: 1970-01-01 01:00
End: 1970-01-01 02:00
Duration: 60:00</string>
<color key="textColor" systemColor="labelColor" cocoaTouchSystemColor="darkTextColor"/>
<fontDescription key="fontDescription" type="system" weight="light" pointSize="14"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="right" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="LOr-e7-foG">
<rect key="frame" x="268" y="46.5" width="20" height="22"/>
<state key="normal" image="filter-clear"/>
<connections>
<action selector="didTapFilter" destination="VRk-wv-rhk" eventType="touchUpInside" id="5NH-qa-yMg"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="pql-H5-k6U" secondAttribute="trailing" id="44x-p3-qWK"/>
<constraint firstItem="LOr-e7-foG" firstAttribute="trailing" secondItem="pql-H5-k6U" secondAttribute="trailing" id="5sr-Cf-h0c"/>
<constraint firstItem="pql-H5-k6U" firstAttribute="top" secondItem="FR1-Nt-XuB" secondAttribute="bottom" id="As8-Px-t6G"/>
<constraint firstItem="pql-H5-k6U" firstAttribute="leading" secondItem="QiY-Mm-Dej" secondAttribute="leading" id="ItB-cO-reV"/>
<constraint firstItem="LOr-e7-foG" firstAttribute="centerY" secondItem="pql-H5-k6U" secondAttribute="centerY" id="OLu-MI-3sa"/>
<constraint firstAttribute="height" priority="250" constant="91" id="or7-9o-FZb"/>
<constraint firstAttribute="bottom" secondItem="pql-H5-k6U" secondAttribute="bottom" id="pbd-43-eN0"/>
</constraints>
</view>
</subviews>
<constraints>
<constraint firstItem="ybL-UG-dwT" firstAttribute="width" secondItem="xdn-EU-IMx" secondAttribute="width" id="PUH-xO-ZbD"/>
<constraint firstItem="QiY-Mm-Dej" firstAttribute="width" secondItem="xdn-EU-IMx" secondAttribute="width" id="U6e-10-j55"/>
<constraint firstItem="Guy-Ra-fpS" firstAttribute="width" secondItem="xdn-EU-IMx" secondAttribute="width" id="ZCJ-ol-1Jv"/>
</constraints>
</stackView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="2yS-xK-Wac" firstAttribute="trailing" secondItem="fMa-Lq-tGz" secondAttribute="trailing" id="1io-bA-4p9"/>
<constraint firstItem="2yS-xK-Wac" firstAttribute="leading" secondItem="fMa-Lq-tGz" secondAttribute="leading" id="Fv1-fO-22V"/>
<constraint firstItem="xdn-EU-IMx" firstAttribute="leading" secondItem="fMa-Lq-tGz" secondAttribute="leading" constant="16" id="JuR-Ro-IPi"/>
<constraint firstItem="xdn-EU-IMx" firstAttribute="top" secondItem="2yS-xK-Wac" secondAttribute="bottom" constant="8" id="Lec-83-aaD"/>
<constraint firstItem="fMa-Lq-tGz" firstAttribute="trailing" secondItem="xdn-EU-IMx" secondAttribute="trailing" constant="16" id="hhC-bL-G3S"/>
<constraint firstItem="fMa-Lq-tGz" firstAttribute="bottom" secondItem="xdn-EU-IMx" secondAttribute="bottom" constant="10" id="p7W-sr-Wch"/>
<constraint firstItem="2yS-xK-Wac" firstAttribute="top" secondItem="fMa-Lq-tGz" secondAttribute="top" id="yKh-gv-mgg"/>
</constraints>
<viewLayoutGuide key="safeArea" id="fMa-Lq-tGz"/>
</view>
<connections>
<outlet property="appDeveloper" destination="l8O-Kw-uc8" id="dfg-s6-Biz"/>
<outlet property="appIcon" destination="rbW-pK-Kct" id="VlO-fG-y1a"/>
<outlet property="appTitle" destination="Et0-8d-CId" id="HgD-oI-0J8"/>
<outlet property="buttonCancel" destination="TGg-60-wZW" id="5Ej-7t-jaD"/>
<outlet property="buttonFilter" destination="LOr-e7-foG" id="qUx-1k-xJK"/>
<outlet property="buttonSave" destination="rWg-hE-Ydl" id="zfM-kx-erX"/>
<outlet property="chooseAppTap" destination="Jab-q2-U9X" id="Tzv-lm-sUm"/>
<outlet property="inputDetails" destination="pql-H5-k6U" id="NXm-8f-5E6"/>
<outlet property="inputNotes" destination="NXU-yU-eST" id="c2n-cG-aLq"/>
<outlet property="noteBottom" destination="vFS-tG-E43" id="Bxh-Tl-E2U"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="KN7-F1-BOL" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
<tapGestureRecognizer id="Jab-q2-U9X">
<connections>
<segue destination="qNp-w1-7Md" kind="modal" id="22y-Dy-3xf"/>
</connections>
</tapGestureRecognizer>
<tapGestureRecognizer id="B0n-l6-MKc">
<connections>
<action selector="hideKeyboard" destination="VRk-wv-rhk" id="eMC-Nn-xoE"/>
</connections>
</tapGestureRecognizer>
</objects>
<point key="canvasLocation" x="700" y="700"/>
</scene>
<!--App Search-->
<scene sceneID="n6R-Wm-XxF">
<objects>
<tableViewController id="qNp-w1-7Md" customClass="TVCAppSearch" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="WYZ-wA-6Rh">
<rect key="frame" x="0.0" y="0.0" width="320" height="369.5"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<searchBar key="tableHeaderView" contentMode="redraw" preservesSuperviewLayoutMargins="YES" placeholder="Search AppStore" showsCancelButton="YES" id="9dl-ZI-85h">
<rect key="frame" x="0.0" y="0.0" width="320" height="56"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<textInputTraits key="textInputTraits" returnKeyType="search" enablesReturnKeyAutomatically="YES"/>
<connections>
<outlet property="delegate" destination="qNp-w1-7Md" id="vcp-BS-7xF"/>
</connections>
</searchBar>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="AppStoreSearchCell" textLabel="yIh-WB-0rK" detailTextLabel="PDe-1x-vle" style="IBUITableViewCellStyleSubtitle" id="Q8M-0Q-Mc7">
<rect key="frame" x="0.0" y="84" width="320" height="57.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Q8M-0Q-Mc7" id="BPc-V2-I7v">
<rect key="frame" x="0.0" y="0.0" width="320" height="57.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="yIh-WB-0rK">
<rect key="frame" x="16" y="9" width="33.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="PDe-1x-vle">
<rect key="frame" x="16" y="32.5" width="33" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<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>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="qNp-w1-7Md" id="Jdv-03-iVJ"/>
<outlet property="delegate" destination="qNp-w1-7Md" id="3vD-b3-Ake"/>
</connections>
</tableView>
<connections>
<outlet property="searchBar" destination="9dl-ZI-85h" id="8Zr-E0-mzs"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="PhN-mC-C3W" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1400" y="700"/>
</scene>
</scenes>
<resources>
<image name="LaunchIcon.png" width="128" height="128"/>
<image name="detail-help" width="22" height="22"/>
<image name="filter-clear" width="20" height="20"/>
<image name="line-expand" width="20" height="20"/>
</resources>
</document>

View File

@@ -0,0 +1,506 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097.3" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="pdd-aM-sKl">
<device id="retina4_0" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Domains-->
<scene sceneID="MN1-aZ-cZt">
<objects>
<tableViewController id="pdd-aM-sKl" customClass="TVCDomains" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="kj3-8X-TyT">
<rect key="frame" x="0.0" y="0.0" width="320" height="519"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="default" accessoryType="disclosureIndicator" hidesAccessoryWhenEditing="NO" indentationWidth="10" reuseIdentifier="DomainCell" textLabel="0HB-5f-eB1" detailTextLabel="MRe-Eq-gvc" style="IBUITableViewCellStyleSubtitle" id="F8D-aK-j1W">
<rect key="frame" x="0.0" y="28" width="320" height="57.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="F8D-aK-j1W" id="FY2-xr-hqh">
<rect key="frame" x="0.0" y="0.0" width="293" height="57.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="characterWrap" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" id="0HB-5f-eB1">
<rect key="frame" x="16" y="9" width="33.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="MRe-Eq-gvc">
<rect key="frame" x="16" y="32.5" width="44" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<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>
</subviews>
</tableViewCellContentView>
<connections>
<segue destination="WcC-nb-Vf5" kind="push" id="EVQ-hO-JE9"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="pdd-aM-sKl" id="4fX-iP-7Oa"/>
<outlet property="delegate" destination="pdd-aM-sKl" id="3RN-az-SYU"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Domains" id="nY5-jL-QT9">
<leftBarButtonItems>
<barButtonItem image="filter-clear" id="FZm-Ld-jJE">
<connections>
<action selector="filterButtonTapped:" destination="pdd-aM-sKl" id="Xyy-LF-eCF"/>
</connections>
</barButtonItem>
<barButtonItem enabled="NO" title="7 days" id="wxA-bC-1pN"/>
</leftBarButtonItems>
</navigationItem>
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
<simulatedTabBarMetrics key="simulatedBottomBarMetrics" translucent="NO"/>
<connections>
<outlet property="filterButton" destination="FZm-Ld-jJE" id="g96-Q2-cYX"/>
<outlet property="filterButtonDetail" destination="wxA-bC-1pN" id="CgP-oz-aXa"/>
</connections>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="jfx-iA-E0v" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="0.0" y="0.0"/>
</scene>
<!--Hosts-->
<scene sceneID="ZCV-Yx-jjW">
<objects>
<tableViewController storyboardIdentifier="requestsHosts" id="WcC-nb-Vf5" customClass="TVCHosts" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="nRF-dc-dC2">
<rect key="frame" x="0.0" y="0.0" width="320" height="519"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<containerView key="tableHeaderView" opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kh4-PQ-hy6">
<rect key="frame" x="0.0" y="0.0" width="320" height="49"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<connections>
<segue destination="1ba-SA-8sT" kind="embed" id="vf1-07-AS4"/>
</connections>
</containerView>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="HostCell" textLabel="Rnk-SP-UHm" detailTextLabel="ovQ-lJ-hWJ" style="IBUITableViewCellStyleSubtitle" id="uv0-9B-Zbb">
<rect key="frame" x="0.0" y="77" width="320" height="57.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="uv0-9B-Zbb" id="6vH-Du-gCg">
<rect key="frame" x="0.0" y="0.0" width="293" height="57.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="characterWrap" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Rnk-SP-UHm">
<rect key="frame" x="16" y="9" width="33.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="ovQ-lJ-hWJ">
<rect key="frame" x="16" y="32.5" width="44" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<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>
</subviews>
</tableViewCellContentView>
<connections>
<segue destination="h7Z-Qr-pJ5" kind="push" id="TPa-Zn-eOs"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="WcC-nb-Vf5" id="szM-iI-Jgi"/>
<outlet property="delegate" destination="WcC-nb-Vf5" id="sBd-BW-Wg6"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Hosts" prompt="com.app.Example" id="TvD-8U-F05"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Gdi-Xi-JUL" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="700" y="0.0"/>
</scene>
<!--Occurrences-->
<scene sceneID="ws3-sK-l8m">
<objects>
<tableViewController storyboardIdentifier="requestsOccurrences" id="h7Z-Qr-pJ5" customClass="TVCHostDetails" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="4ms-FO-Fge">
<rect key="frame" x="0.0" y="0.0" width="320" height="519"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<containerView key="tableHeaderView" opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="SxM-2c-aJb">
<rect key="frame" x="0.0" y="0.0" width="320" height="49"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<connections>
<segue destination="1ba-SA-8sT" kind="embed" id="ueN-6L-cP7"/>
</connections>
</containerView>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="HostDetailCell" textLabel="J2P-mU-Vad" detailTextLabel="eWb-mX-udN" style="IBUITableViewCellStyleValue1" id="ZCA-Dz-i92">
<rect key="frame" x="0.0" y="77" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="ZCA-Dz-i92" id="nxe-48-jAQ">
<rect key="frame" x="0.0" y="0.0" width="293" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" id="J2P-mU-Vad">
<rect key="frame" x="16" y="12" width="33.5" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="eWb-mX-udN">
<rect key="frame" x="241" y="12" width="44" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<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>
</subviews>
</tableViewCellContentView>
<connections>
<segue destination="rjy-Di-Cru" kind="push" id="SfC-iY-Ce0"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="h7Z-Qr-pJ5" id="fyW-Av-fWY"/>
<outlet property="delegate" destination="h7Z-Qr-pJ5" id="gBq-jA-u5V"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Occurrences" prompt="com.domain.network.cdn" id="bys-2u-rHs"/>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="UxH-PH-KQy" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1400" y="0.0"/>
</scene>
<!--Occurrence Context-->
<scene sceneID="A1T-7G-agr">
<objects>
<tableViewController id="rjy-Di-Cru" customClass="TVCOccurrenceContext" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="EfM-yv-85f">
<rect key="frame" x="0.0" y="0.0" width="320" height="519"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="OccurrenceContextCell" textLabel="xgq-hW-e3R" detailTextLabel="No8-Bf-ptL" style="IBUITableViewCellStyleValue2" id="KQh-Ei-If8">
<rect key="frame" x="0.0" y="28" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="KQh-Ei-If8" id="i32-u4-1Q8">
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="xgq-hW-e3R">
<rect key="frame" x="16" y="12" width="91" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<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="Subtitle" textAlignment="natural" lineBreakMode="characterWrap" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" id="No8-Bf-ptL">
<rect key="frame" x="113" y="12" width="59" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</tableViewCellContentView>
</tableViewCell>
</prototypes>
<connections>
<outlet property="dataSource" destination="rjy-Di-Cru" id="6CT-Vd-Ixn"/>
<outlet property="delegate" destination="rjy-Di-Cru" id="JtY-um-ZTF"/>
</connections>
</tableView>
<navigationItem key="navigationItem" title="Occurrence Context" id="2mj-It-uND">
<barButtonItem key="rightBarButtonItem" image="jump-to-target" id="TqX-qO-B3s">
<connections>
<action selector="jumpToTsZero" destination="rjy-Di-Cru" id="RS6-IO-hi4"/>
</connections>
</barButtonItem>
</navigationItem>
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="cYd-oX-akc" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2100" y="0.0"/>
</scene>
<!--Date Filter-->
<scene sceneID="GqC-c0-bWe">
<objects>
<viewController storyboardIdentifier="domainFilter" modalTransitionStyle="crossDissolve" id="r7v-PM-PrR" customClass="VCDateFilter" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="QBv-5g-BTH">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<navigationBar hidden="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="jAM-LN-evh">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<items>
<navigationItem title="Placeholder" id="s5o-aw-nIo">
<barButtonItem key="leftBarButtonItem" title="Item" image="filter-clear" id="oMW-R3-3Eh"/>
</navigationItem>
</items>
</navigationBar>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="pEc-vv-7Ts">
<rect key="frame" x="8" y="64" width="233.5" height="391.5"/>
<subviews>
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="UNT-qn-2cg">
<rect key="frame" x="8" y="8" width="217.5" height="32"/>
<segments>
<segment title="Most recent"/>
<segment title="Date Range"/>
</segments>
<connections>
<action selector="didChangeFilterBy:" destination="r7v-PM-PrR" eventType="valueChanged" id="kM6-QE-ZGV"/>
</connections>
</segmentedControl>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="gEf-Ra-RyA">
<rect key="frame" x="10" y="47" width="213.5" height="334.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Show entries no older than" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="UBq-oH-pKp">
<rect key="frame" x="0.0" y="0.0" width="213.5" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ucF-MH-iRP">
<rect key="frame" x="0.0" y="35.5" width="213.5" height="50"/>
<subviews>
<slider opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="0.5" minValue="0.0" maxValue="1" translatesAutoresizingMaskIntoConstraints="NO" id="qhe-6d-hGB">
<rect key="frame" x="-2" y="0.0" width="155.5" height="51"/>
<connections>
<action selector="durationSliderChanged:" destination="r7v-PM-PrR" eventType="valueChanged" id="nQB-w9-dY1"/>
</connections>
</slider>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="7 days" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ika-su-PZQ">
<rect key="frame" x="159.5" y="16" width="54" height="18"/>
<constraints>
<constraint firstAttribute="width" secondItem="ika-su-PZQ" secondAttribute="height" multiplier="3" id="o9m-cy-kkn"/>
</constraints>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="qhe-6d-hGB" firstAttribute="leading" secondItem="ucF-MH-iRP" secondAttribute="leading" id="EGU-ln-lJF"/>
<constraint firstItem="ika-su-PZQ" firstAttribute="trailing" secondItem="ucF-MH-iRP" secondAttribute="trailing" id="KLs-Ft-wSo"/>
<constraint firstItem="qhe-6d-hGB" firstAttribute="bottom" secondItem="ucF-MH-iRP" secondAttribute="bottom" id="PTR-Et-Klv"/>
<constraint firstItem="ika-su-PZQ" firstAttribute="leading" secondItem="qhe-6d-hGB" secondAttribute="trailing" constant="8" symbolic="YES" id="aHy-IX-X4B"/>
<constraint firstItem="ika-su-PZQ" firstAttribute="centerY" secondItem="qhe-6d-hGB" secondAttribute="centerY" id="cHs-5z-CHK"/>
<constraint firstItem="qhe-6d-hGB" firstAttribute="top" secondItem="ucF-MH-iRP" secondAttribute="top" id="eJC-d4-zg0"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Show entries within range" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="rtf-o1-gk6">
<rect key="frame" x="0.0" y="100.5" width="213.5" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="9As-hA-MKt">
<rect key="frame" x="0.0" y="136" width="213.5" height="74"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="From:" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wAd-o2-PHY">
<rect key="frame" x="0.0" y="6.5" width="44" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="FVD-kB-91w">
<rect key="frame" x="52" y="0.0" width="161.5" height="33"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<state key="normal" title="1970-01-01 01:00"/>
<connections>
<action selector="didTapRangeButton:" destination="r7v-PM-PrR" eventType="touchUpInside" id="g05-Sc-0P2"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="To:" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fzL-94-c0l">
<rect key="frame" x="0.0" y="47.5" width="44" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="IG3-Wc-UI4">
<rect key="frame" x="52" y="41" width="161.5" height="33"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<state key="normal" title="1970-01-01 01:00"/>
<connections>
<action selector="didTapRangeButton:" destination="r7v-PM-PrR" eventType="touchUpInside" id="63v-sy-Bpo"/>
</connections>
</button>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="FVD-kB-91w" firstAttribute="top" secondItem="9As-hA-MKt" secondAttribute="top" id="J2e-NO-NUD"/>
<constraint firstItem="IG3-Wc-UI4" firstAttribute="top" secondItem="FVD-kB-91w" secondAttribute="bottom" constant="8" symbolic="YES" id="Jyg-b6-5rH"/>
<constraint firstItem="fzL-94-c0l" firstAttribute="centerY" secondItem="IG3-Wc-UI4" secondAttribute="centerY" id="LAG-jg-nBi"/>
<constraint firstItem="wAd-o2-PHY" firstAttribute="trailing" secondItem="fzL-94-c0l" secondAttribute="trailing" id="Lav-7y-PUJ"/>
<constraint firstItem="IG3-Wc-UI4" firstAttribute="trailing" secondItem="9As-hA-MKt" secondAttribute="trailing" id="XqI-1p-FNv"/>
<constraint firstItem="FVD-kB-91w" firstAttribute="leading" secondItem="wAd-o2-PHY" secondAttribute="trailing" constant="8" symbolic="YES" id="YlE-w7-CZ5"/>
<constraint firstItem="FVD-kB-91w" firstAttribute="trailing" secondItem="9As-hA-MKt" secondAttribute="trailing" id="a6D-1D-HvF"/>
<constraint firstItem="wAd-o2-PHY" firstAttribute="leading" secondItem="fzL-94-c0l" secondAttribute="leading" id="bM0-gJ-IW5"/>
<constraint firstItem="wAd-o2-PHY" firstAttribute="centerY" secondItem="FVD-kB-91w" secondAttribute="centerY" id="g7F-LP-PQQ"/>
<constraint firstItem="IG3-Wc-UI4" firstAttribute="bottom" secondItem="9As-hA-MKt" secondAttribute="bottom" priority="750" id="jlK-69-8hl"/>
<constraint firstItem="IG3-Wc-UI4" firstAttribute="leading" secondItem="fzL-94-c0l" secondAttribute="trailing" constant="8" symbolic="YES" id="pcE-Gv-oj7"/>
<constraint firstItem="wAd-o2-PHY" firstAttribute="leading" secondItem="9As-hA-MKt" secondAttribute="leading" id="zgR-pJ-vFs"/>
</constraints>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Order by" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="9Fe-5F-TVt">
<rect key="frame" x="0.0" y="225" width="213.5" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cWy-un-IHC">
<rect key="frame" x="0.0" y="260.5" width="213.5" height="74"/>
<subviews>
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="UKE-MR-kRJ">
<rect key="frame" x="-2" y="0.0" width="217.5" height="36"/>
<segments>
<segment title="Date"/>
<segment title="Name"/>
<segment title="Count"/>
</segments>
</segmentedControl>
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="eG2-a4-zm5">
<rect key="frame" x="-2" y="43" width="217.5" height="32"/>
<segments>
<segment title="Ascending"/>
<segment title="Descending"/>
</segments>
</segmentedControl>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="eG2-a4-zm5" firstAttribute="top" secondItem="UKE-MR-kRJ" secondAttribute="bottom" constant="8" symbolic="YES" id="6oC-bZ-XdM"/>
<constraint firstItem="eG2-a4-zm5" firstAttribute="leading" secondItem="cWy-un-IHC" secondAttribute="leading" constant="-2" id="7R0-qB-J0u"/>
<constraint firstAttribute="bottom" secondItem="eG2-a4-zm5" secondAttribute="bottom" id="JbN-vA-Rd5"/>
<constraint firstItem="UKE-MR-kRJ" firstAttribute="top" secondItem="cWy-un-IHC" secondAttribute="top" id="L21-Kf-g2d"/>
<constraint firstAttribute="trailing" secondItem="eG2-a4-zm5" secondAttribute="trailing" constant="-2" id="cbD-H9-e1Q"/>
<constraint firstItem="UKE-MR-kRJ" firstAttribute="leading" secondItem="cWy-un-IHC" secondAttribute="leading" constant="-2" id="lKB-g4-asw"/>
<constraint firstAttribute="trailing" secondItem="UKE-MR-kRJ" secondAttribute="trailing" constant="-2" id="xIa-X2-0Lp"/>
</constraints>
</view>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="UNT-qn-2cg" firstAttribute="top" secondItem="pEc-vv-7Ts" secondAttribute="top" constant="8" id="Awu-uv-9wF"/>
<constraint firstItem="UNT-qn-2cg" firstAttribute="leading" secondItem="pEc-vv-7Ts" secondAttribute="leading" constant="8" id="Icx-YR-5bc"/>
<constraint firstItem="gEf-Ra-RyA" firstAttribute="top" secondItem="UNT-qn-2cg" secondAttribute="bottom" constant="8" symbolic="YES" id="QPi-aa-6ff"/>
<constraint firstItem="UNT-qn-2cg" firstAttribute="trailing" secondItem="pEc-vv-7Ts" secondAttribute="trailing" constant="-8" id="Sof-6L-T2D"/>
<constraint firstItem="gEf-Ra-RyA" firstAttribute="bottom" secondItem="pEc-vv-7Ts" secondAttribute="bottom" constant="-10" id="TMx-5J-z2P"/>
<constraint firstItem="gEf-Ra-RyA" firstAttribute="leading" secondItem="pEc-vv-7Ts" secondAttribute="leading" constant="10" id="U6l-7M-bm4"/>
<constraint firstItem="gEf-Ra-RyA" firstAttribute="trailing" secondItem="pEc-vv-7Ts" secondAttribute="trailing" constant="-10" id="YKE-TR-fTB"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
<integer key="value" value="12"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="sAi-8j-0n1" customClass="PopupTriangle" customModule="AppCheck" customModuleProvider="target">
<rect key="frame" x="14" y="46" width="28" height="22"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="22" id="MaD-aD-U8h"/>
<constraint firstAttribute="width" constant="28" id="ntP-rP-UMh"/>
</constraints>
<userDefinedRuntimeAttributes>
<userDefinedRuntimeAttribute type="color" keyPath="color">
<color key="value" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
</userDefinedRuntimeAttribute>
</userDefinedRuntimeAttributes>
</view>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.20000000000000001" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<gestureRecognizers/>
<constraints>
<constraint firstItem="sAi-8j-0n1" firstAttribute="bottom" secondItem="pEc-vv-7Ts" secondAttribute="top" constant="4" id="DCq-Ps-sQo"/>
<constraint firstItem="pEc-vv-7Ts" firstAttribute="top" secondItem="jAM-LN-evh" secondAttribute="bottom" constant="20" id="EdA-nv-DEa"/>
<constraint firstItem="pEc-vv-7Ts" firstAttribute="leading" secondItem="u0F-hK-vVD" secondAttribute="leading" constant="8" id="Iow-GV-Lxy"/>
<constraint firstItem="jAM-LN-evh" firstAttribute="trailing" secondItem="u0F-hK-vVD" secondAttribute="trailing" id="Lju-K6-G89"/>
<constraint firstItem="jAM-LN-evh" firstAttribute="top" secondItem="u0F-hK-vVD" secondAttribute="top" id="MqW-YU-POp"/>
<constraint firstItem="u0F-hK-vVD" firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="pEc-vv-7Ts" secondAttribute="trailing" constant="8" id="V9T-2Y-oNy"/>
<constraint firstItem="sAi-8j-0n1" firstAttribute="leading" secondItem="pEc-vv-7Ts" secondAttribute="leading" constant="6" id="cXH-3c-s6t"/>
<constraint firstItem="jAM-LN-evh" firstAttribute="leading" secondItem="u0F-hK-vVD" secondAttribute="leading" id="ula-eW-vAq"/>
</constraints>
<viewLayoutGuide key="safeArea" id="u0F-hK-vVD"/>
<connections>
<outletCollection property="gestureRecognizers" destination="oRi-17-cVM" appends="YES" id="2pk-5e-eJD"/>
</connections>
</view>
<extendedEdge key="edgesForExtendedLayout" bottom="YES"/>
<connections>
<outlet property="buttonRangeEnd" destination="IG3-Wc-UI4" id="wAd-ca-bVQ"/>
<outlet property="buttonRangeStart" destination="FVD-kB-91w" id="HbX-Vl-uBE"/>
<outlet property="durationLabel" destination="ika-su-PZQ" id="1Br-vu-xir"/>
<outlet property="durationSlider" destination="qhe-6d-hGB" id="wph-zX-WIz"/>
<outlet property="durationTitle" destination="UBq-oH-pKp" id="BEd-Lo-a2v"/>
<outlet property="durationView" destination="ucF-MH-iRP" id="TCI-Pp-drf"/>
<outlet property="filterBy" destination="UNT-qn-2cg" id="M1J-n8-LHq"/>
<outlet property="orderbyAsc" destination="eG2-a4-zm5" id="II1-hc-pyZ"/>
<outlet property="orderbyType" destination="UKE-MR-kRJ" id="fK7-dW-MLd"/>
<outlet property="rangeTitle" destination="rtf-o1-gk6" id="2DY-xP-VOg"/>
<outlet property="rangeView" destination="9As-hA-MKt" id="0Mq-Gi-nF6"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="xTS-RW-xLN" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
<tapGestureRecognizer id="oRi-17-cVM">
<connections>
<outlet property="delegate" destination="r7v-PM-PrR" id="JME-7W-w51"/>
</connections>
</tapGestureRecognizer>
</objects>
<point key="canvasLocation" x="0.0" y="-700"/>
</scene>
<!--Analysis Bar-->
<scene sceneID="1qq-WD-Lqq">
<objects>
<viewController id="1ba-SA-8sT" customClass="VCAnalysisBar" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="qp6-er-N6U">
<rect key="frame" x="0.0" y="0.0" width="320" height="49"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tabBar contentMode="scaleToFill" translucent="NO" id="1Jy-zg-CXR">
<rect key="frame" x="0.0" y="0.0" width="320" height="49"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<items>
<tabBarItem title="Co-Occurrence" image="intersection" id="KXh-kQ-rAF"/>
</items>
<connections>
<outlet property="delegate" destination="1ba-SA-8sT" id="bRS-kh-dOv"/>
</connections>
</tabBar>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="dtz-KG-P4C"/>
</view>
<connections>
<outlet property="tabBar" destination="1Jy-zg-CXR" id="VTV-xq-Aou"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="XnK-B9-RSJ" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="700" y="-700"/>
</scene>
</scenes>
<inferredMetricsTieBreakers>
<segue reference="vf1-07-AS4"/>
</inferredMetricsTieBreakers>
<resources>
<image name="filter-clear" width="20" height="20"/>
<image name="intersection" width="25" height="25"/>
<image name="jump-to-target" width="20" height="20"/>
</resources>
</document>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="qdB-ZO-LHY"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097.3" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="qdB-ZO-LHY">
<device id="retina4_0" orientation="portrait" appearance="light"/> <device id="retina4_0" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
@@ -243,25 +243,93 @@
</tableViewSection> </tableViewSection>
<tableViewSection headerTitle="Advanced Settings" id="Vlg-nm-VB3"> <tableViewSection headerTitle="Advanced Settings" id="Vlg-nm-VB3">
<cells> <cells>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="VnR-9B-1zl"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="rio-m6-pXN" detailTextLabel="rr4-sR-VxD" style="IBUITableViewCellStyleSubtitle" id="pQ5-lm-Rco">
<rect key="frame" x="0.0" y="687.5" width="320" height="44"/> <rect key="frame" x="0.0" y="687.5" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VnR-9B-1zl" id="ZTz-vZ-l5p"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="pQ5-lm-Rco" id="52Y-H3-jvJ">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/> <rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="twS-Ne-dU0"> <label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Force disconnect unresolvable" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="rio-m6-pXN">
<rect key="frame" x="15" y="5" width="234" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="in case DNS returns empty IP" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="rr4-sR-VxD">
<rect key="frame" x="15" y="25.5" width="166" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<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>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="TSu-IP-KFG">
<rect key="frame" x="257" y="6" width="49" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
<connections>
<action selector="togglePreventUnresolvable:" destination="qdB-ZO-LHY" eventType="valueChanged" id="xKv-Hp-Nyq"/>
</connections>
</switch>
</subviews>
</tableViewCellContentView>
<connections>
<outlet property="accessoryView" destination="TSu-IP-KFG" id="VGm-hN-DLK"/>
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" textLabel="x5u-5T-XTO" detailTextLabel="n7p-Ab-69G" style="IBUITableViewCellStyleSubtitle" id="lxs-NQ-Q7V">
<rect key="frame" x="0.0" y="731.5" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="lxs-NQ-Q7V" id="B1l-cb-yQg">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Force disconnect swcd" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="x5u-5T-XTO">
<rect key="frame" x="15" y="5" width="177" height="20.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="background deamon User-Agent: swcd" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="n7p-Ab-69G">
<rect key="frame" x="15" y="25.5" width="220.5" height="14.5"/>
<autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<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>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="c5N-PN-qWU">
<rect key="frame" x="257" y="6" width="49" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
<connections>
<action selector="togglePreventSWCD:" destination="qdB-ZO-LHY" eventType="valueChanged" id="MAa-5v-djq"/>
</connections>
</switch>
</subviews>
</tableViewCellContentView>
<connections>
<outlet property="accessoryView" destination="c5N-PN-qWU" id="SAN-Q9-VOn"/>
</connections>
</tableViewCell>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="Tgc-re-gI7">
<rect key="frame" x="0.0" y="775.5" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="Tgc-re-gI7" id="haV-RB-dEa">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="vZa-EO-FAZ">
<rect key="frame" x="125" y="7" width="70" height="30"/> <rect key="frame" x="125" y="7" width="70" height="30"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/> <fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<state key="normal" title="Export DB"/> <state key="normal" title="Export DB"/>
<connections> <connections>
<action selector="exportDB" destination="qdB-ZO-LHY" eventType="touchUpInside" id="FYN-Zz-UK4"/> <action selector="exportDB" destination="qdB-ZO-LHY" eventType="touchUpInside" id="NPx-9w-ua0"/>
</connections> </connections>
</button> </button>
</subviews> </subviews>
<constraints> <constraints>
<constraint firstItem="twS-Ne-dU0" firstAttribute="centerY" secondItem="ZTz-vZ-l5p" secondAttribute="centerY" id="LgK-8q-r6K"/> <constraint firstItem="vZa-EO-FAZ" firstAttribute="centerX" secondItem="haV-RB-dEa" secondAttribute="centerX" id="h0M-qz-pHV"/>
<constraint firstItem="twS-Ne-dU0" firstAttribute="centerX" secondItem="ZTz-vZ-l5p" secondAttribute="centerX" id="ltC-Ba-Bxr"/> <constraint firstItem="vZa-EO-FAZ" firstAttribute="centerY" secondItem="haV-RB-dEa" secondAttribute="centerY" id="xBJ-94-nJh"/>
</constraints> </constraints>
</tableViewCellContentView> </tableViewCellContentView>
</tableViewCell> </tableViewCell>
@@ -282,6 +350,8 @@
<outlet property="cellNotificationConnectionAlert" destination="OTC-Kt-LFT" id="XiG-CC-4lC"/> <outlet property="cellNotificationConnectionAlert" destination="OTC-Kt-LFT" id="XiG-CC-4lC"/>
<outlet property="cellNotificationReminder" destination="jZA-aP-aHG" id="sjo-2s-rqW"/> <outlet property="cellNotificationReminder" destination="jZA-aP-aHG" id="sjo-2s-rqW"/>
<outlet property="cellPrivacyAutoDelete" destination="Qyy-0U-yhd" id="PzN-iv-kFl"/> <outlet property="cellPrivacyAutoDelete" destination="Qyy-0U-yhd" id="PzN-iv-kFl"/>
<outlet property="swcdToggle" destination="c5N-PN-qWU" id="qDy-BX-O85"/>
<outlet property="unresolvableToggle" destination="TSu-IP-KFG" id="Vdb-cm-Uy2"/>
<outlet property="vpnToggle" destination="ZAz-WT-FDb" id="lGX-J8-WrU"/> <outlet property="vpnToggle" destination="ZAz-WT-FDb" id="lGX-J8-WrU"/>
</connections> </connections>
</tableViewController> </tableViewController>
@@ -293,19 +363,19 @@
<scene sceneID="218-uP-X7b"> <scene sceneID="218-uP-X7b">
<objects> <objects>
<tableViewController id="q3B-Yi-1bx" customClass="TVCFilter" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController"> <tableViewController id="q3B-Yi-1bx" customClass="TVCFilter" customModule="AppCheck" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" allowsSelection="NO" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="GSg-ZZ-F8J"> <tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="GSg-ZZ-F8J">
<rect key="frame" x="0.0" y="0.0" width="320" height="568"/> <rect key="frame" x="0.0" y="0.0" width="320" height="568"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/> <color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<prototypes> <prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="DomainFilterCell" textLabel="MrS-rb-RLB" style="IBUITableViewCellStyleDefault" id="EO2-ww-xuz"> <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="DomainFilterCell" textLabel="MrS-rb-RLB" style="IBUITableViewCellStyleDefault" id="EO2-ww-xuz">
<rect key="frame" x="0.0" y="28" width="320" height="43.5"/> <rect key="frame" x="0.0" y="28" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="EO2-ww-xuz" id="AtR-ce-uYs"> <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="EO2-ww-xuz" id="AtR-ce-uYs">
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/> <rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<subviews> <subviews>
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" id="MrS-rb-RLB"> <label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Title" textAlignment="natural" lineBreakMode="characterWrap" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.80000000000000004" id="MrS-rb-RLB">
<rect key="frame" x="16" y="0.0" width="288" height="43.5"/> <rect key="frame" x="16" y="0.0" width="288" height="43.5"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/> <fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
@@ -364,7 +434,7 @@ If App Badge is enabled, display the letter "1" on the homescreen app icon, as l
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" id="zaV-mh-eqb"> <switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" id="zaV-mh-eqb">
<rect key="frame" x="252" y="6" width="54" height="31"/> <rect key="frame" x="250" y="6" width="54" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
<connections> <connections>
<action selector="toggleAllowRestartReminder:" destination="JYM-cs-i4H" eventType="valueChanged" id="F4e-k2-bni"/> <action selector="toggleAllowRestartReminder:" destination="JYM-cs-i4H" eventType="valueChanged" id="F4e-k2-bni"/>
@@ -391,7 +461,7 @@ If App Badge is enabled, display the letter "1" on the homescreen app icon, as l
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" id="HaE-En-NH3"> <switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" id="HaE-En-NH3">
<rect key="frame" x="252" y="6" width="54" height="31"/> <rect key="frame" x="250" y="6" width="54" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
<connections> <connections>
<action selector="toggleAllowRestartNotify:" destination="JYM-cs-i4H" eventType="valueChanged" id="12C-h5-mrR"/> <action selector="toggleAllowRestartNotify:" destination="JYM-cs-i4H" eventType="valueChanged" id="12C-h5-mrR"/>
@@ -418,7 +488,7 @@ If App Badge is enabled, display the letter "1" on the homescreen app icon, as l
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" id="N2Q-cU-pkd"> <switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" id="N2Q-cU-pkd">
<rect key="frame" x="252" y="6" width="54" height="31"/> <rect key="frame" x="250" y="6" width="54" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
<connections> <connections>
<action selector="toggleAllowRestartBadge:" destination="JYM-cs-i4H" eventType="valueChanged" id="76l-6y-fOu"/> <action selector="toggleAllowRestartBadge:" destination="JYM-cs-i4H" eventType="valueChanged" id="76l-6y-fOu"/>
@@ -476,7 +546,7 @@ If App Badge is enabled, display the letter "1" on the homescreen app icon, as l
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="mTm-Rm-1RQ"> <switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="mTm-Rm-1RQ">
<rect key="frame" x="255" y="6" width="49" height="31"/> <rect key="frame" x="255" y="6" width="48" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
<connections> <connections>
<action selector="toggleAllowRecordingReminder:" destination="JYM-cs-i4H" eventType="valueChanged" id="unC-Ur-jPM"/> <action selector="toggleAllowRecordingReminder:" destination="JYM-cs-i4H" eventType="valueChanged" id="unC-Ur-jPM"/>
@@ -602,7 +672,7 @@ If App Badge is enabled, display the letter "1" on the homescreen app icon, as l
<nil key="highlightedColor"/> <nil key="highlightedColor"/>
</label> </label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="who-8G-voz"> <switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="who-8G-voz">
<rect key="frame" x="256" y="6" width="48" height="31"/> <rect key="frame" x="256" y="6" width="47" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
<connections> <connections>
<action selector="toggleShowNotifications:" destination="D2a-Po-vDU" eventType="valueChanged" id="Thg-6R-7wM"/> <action selector="toggleShowNotifications:" destination="D2a-Po-vDU" eventType="valueChanged" id="Thg-6R-7wM"/>
@@ -832,6 +902,6 @@ If App Badge is enabled, display the letter "1" on the homescreen app icon, as l
</scenes> </scenes>
<inferredMetricsTieBreakers> <inferredMetricsTieBreakers>
<segue reference="tUF-Kv-koO"/> <segue reference="tUF-Kv-koO"/>
<segue reference="6wc-d1-VYY"/> <segue reference="EzT-Xq-wka"/>
</inferredMetricsTieBreakers> </inferredMetricsTieBreakers>
</document> </document>

View File

@@ -157,4 +157,16 @@ struct VPNAppMessage {
static func notificationSettingsChanged() -> Self { static func notificationSettingsChanged() -> Self {
.init("notify-prefs-change:1") .init("notify-prefs-change:1")
} }
/// Triggered whenever user taps on the start/stop recording button
static func isRecording(_ state: CurrentRecordingState) -> Self {
.init("recording-now:\(state.rawValue)")
}
/// Triggered whenever user taps on the switch in settings
static func disconnectUnresolvable(_ state: Bool) -> Self {
.init("disconnect-unresolvable:\(state ? 1 : 0)")
}
/// Triggered whenever user taps on the switch in settings
static func disconnectSWCD(_ state: Bool) -> Self {
.init("disconnect-swcd:\(state ? 1 : 0)")
}
} }

View File

@@ -8,6 +8,12 @@ class GlassVPNHook {
private var filterOptions: [(block: Bool, ignore: Bool, customA: Bool, customB: Bool)]! private var filterOptions: [(block: Bool, ignore: Bool, customA: Bool, customB: Bool)]!
private var autoDeleteTimer: Timer? = nil private var autoDeleteTimer: Timer? = nil
private var cachedNotify: CachedConnectionAlert! private var cachedNotify: CachedConnectionAlert!
private var currentlyRecording: Bool = false
public var isBackgroundRecording: Bool = false
public var forceDisconnectUnresolvable: Bool = false
public var forceDisconnectSWCD: Bool = false
init() { reset() } init() { reset() }
@@ -16,6 +22,10 @@ class GlassVPNHook {
reloadDomainFilter() reloadDomainFilter()
setAutoDelete(PrefsShared.AutoDeleteLogsDays) setAutoDelete(PrefsShared.AutoDeleteLogsDays)
cachedNotify = CachedConnectionAlert() cachedNotify = CachedConnectionAlert()
currentlyRecording = PrefsShared.CurrentlyRecording != .Off
isBackgroundRecording = PrefsShared.CurrentlyRecording == .Background
forceDisconnectUnresolvable = PrefsShared.ForceDisconnectUnresolvableDNS
forceDisconnectSWCD = PrefsShared.ForceDisconnectSWCD
} }
/// 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.
@@ -25,6 +35,10 @@ class GlassVPNHook {
autoDeleteTimer?.fire() // one last time before we quit autoDeleteTimer?.fire() // one last time before we quit
autoDeleteTimer?.invalidate() autoDeleteTimer?.invalidate()
cachedNotify = nil cachedNotify = nil
currentlyRecording = false
isBackgroundRecording = false
forceDisconnectUnresolvable = false
forceDisconnectSWCD = false
} }
/// Call this method from `PacketTunnelProvider.handleAppMessage(_:completionHandler:)` /// Call this method from `PacketTunnelProvider.handleAppMessage(_:completionHandler:)`
@@ -43,6 +57,17 @@ class GlassVPNHook {
case "notify-prefs-change": case "notify-prefs-change":
cachedNotify = CachedConnectionAlert() cachedNotify = CachedConnectionAlert()
return return
case "recording-now":
let newState = CurrentRecordingState(rawValue: Int(value) ?? 0)
currentlyRecording = newState != .Off
isBackgroundRecording = newState == .Background
return
case "disconnect-unresolvable":
forceDisconnectUnresolvable = value == "1"
return
case "disconnect-swcd":
forceDisconnectSWCD = value == "1"
return
default: break default: break
} }
} }
@@ -57,18 +82,24 @@ class GlassVPNHook {
/// - Returns: `true` if the request shoud be blocked. /// - Returns: `true` if the request shoud be blocked.
func processDNSRequest(_ domain: String) -> Bool { func processDNSRequest(_ domain: String) -> Bool {
let i = filterIndex(for: domain) let i = filterIndex(for: domain)
// TODO: disable ignore & block during recordings
let (block, ignore, cA, cB) = (i<0) ? (false, false, false, false) : filterOptions[i] let (block, ignore, cA, cB) = (i<0) ? (false, false, false, false) : filterOptions[i]
if ignore { if ignore, !currentlyRecording {
return block return block
} }
let blockActive = block && !currentlyRecording
queue.async { queue.async {
do { try AppDB?.logWrite(domain, blocked: block) } do { try AppDB?.logWrite(domain, blocked: blockActive) }
catch { NSLog("[VPN.WARN] Couldn't write: \(error)") } catch { NSLog("[VPN.WARN] Couldn't write: \(error)") }
} }
// TODO: disable notifications during recording?
cachedNotify.postOrIgnore(domain, blck: block, custA: cA, custB: cB) cachedNotify.postOrIgnore(domain, blck: block, custA: cA, custB: cB)
// TODO: wait for notify response to block or allow connection // TODO: wait for notify response to block or allow connection
return block return blockActive
}
func silentlyPrevented(_ domain: String) {
// TODO: persist in a separate db/table?
NSLog("[VPN.INFO] preventing connection to \(domain)")
} }
/// Build binary tree for reverse DNS lookup /// Build binary tree for reverse DNS lookup
@@ -125,13 +156,12 @@ class GlassVPNHook {
@objc private func autoDeleteNow(_ sender: Timer) { @objc private func autoDeleteNow(_ sender: Timer) {
NSLog("[VPN.INFO] Auto-delete old logs") NSLog("[VPN.INFO] Auto-delete old logs")
queue.async { queue.async {
guard sender.isValid else { return }
do { do {
try AppDB?.dnsLogsDeleteOlderThan(days: sender.userInfo as! Int) try AppDB?.dnsLogsDeleteOlderThan(days: sender.userInfo as! Int)
} catch { } catch {
NSLog("[VPN.WARN] Couldn't delete logs, will retry in 5 minutes. \(error)") NSLog("[VPN.WARN] Couldn't delete logs, will retry in 5 minutes. \(error)")
if sender.isValid { sender.fireDate = Date().addingTimeInterval(300) // retry in 5 min
sender.fireDate = Date().addingTimeInterval(300) // retry in 5 min
}
} }
} }
} }

View File

@@ -9,6 +9,7 @@ enum NotificationRequestState {
case .authorized: self = .Authorized case .authorized: self = .Authorized
case .provisional: self = .Provisional case .provisional: self = .Provisional
case .notDetermined: fallthrough case .notDetermined: fallthrough
case .ephemeral: fallthrough
@unknown default: self = .NotDetermined @unknown default: self = .NotDetermined
} }
} }

View File

@@ -0,0 +1,52 @@
import Foundation
import UIKit
extension URL {
static func appStoreSearch(query: String) -> URL {
// https://itunes.apple.com/lookup?bundleId=...
URL.make("https://itunes.apple.com/search", params: [
"media" : "software",
"limit" : "25",
"country" : NSLocale.current.regionCode ?? "DE",
"version" : "2",
"term" : query,
])!
}
}
struct AppStoreSearch {
struct Result {
let bundleId, name: String
let developer, imageURL: String?
}
static func search(_ term: String, _ closure: @escaping ([Result]?, Error?) -> Void) {
URLSession.shared.dataTask(with: .init(url: .appStoreSearch(query: term))) { data, response, error in
guard let data = data, error == nil,
let response = response as? HTTPURLResponse,
(200 ..< 300) ~= response.statusCode else {
closure(nil, error ?? URLError(.badServerResponse))
return
}
closure(jsonSearchToList(data), nil)
}.resume()
}
private static func jsonSearchToList(_ data: Data) -> [Result]? {
guard let json = (try? JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed)) as? [String: Any],
let resAll = json["results"] as? [Any] else {
return nil
}
return resAll.compactMap {
guard let res = $0 as? [String: Any],
let bndl = res["bundleId"] as? String,
let name = res["trackName"] as? String // trackCensoredName
else {
return nil
}
let seller = res["sellerName"] as? String // artistName
let image = res["artworkUrl60"] as? String // artworkUrl100
return Result(bundleId: bndl, name: name, developer: seller, imageURL: image)
}
}
}

View File

@@ -0,0 +1,104 @@
import UIKit
extension CGContext {
func lineFromTo(x1: CGFloat, y1: CGFloat, x2: CGFloat, y2: CGFloat) {
self.move(to: CGPoint(x: x1, y: y1))
self.addLine(to: CGPoint(x: x2, y: y2))
}
}
struct BundleIcon {
static let unknown : UIImage? = {
let rect = CGRect(x: 0, y: 0, width: 30, height: 30)
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
let context = UIGraphicsGetCurrentContext()!
let lineWidth: CGFloat = 0.5
let corner: CGFloat = 6.75
let c = corner / CGFloat.pi + lineWidth/2
let sz: CGFloat = rect.height
let m = sz / 2
let r1 = 0.2 * sz, r2 = sqrt(2 * r1 * r1)
// diagonal
context.lineFromTo(x1: c, y1: c, x2: sz-c, y2: sz-c)
context.lineFromTo(x1: c, y1: sz-c, x2: sz-c, y2: c)
// horizontal
context.lineFromTo(x1: 0, y1: m, x2: sz, y2: m)
context.lineFromTo(x1: 0, y1: m + r1, x2: sz, y2: m + r1)
context.lineFromTo(x1: 0, y1: m - r1, x2: sz, y2: m - r1)
// vertical
context.lineFromTo(x1: m, y1: 0, x2: m, y2: sz)
context.lineFromTo(x1: m + r1, y1: 0, x2: m + r1, y2: sz)
context.lineFromTo(x1: m - r1, y1: 0, x2: m - r1, y2: sz)
// circles
context.addEllipse(in: CGRect(x: m - r1, y: m - r1, width: 2*r1, height: 2*r1))
context.addEllipse(in: CGRect(x: m - r2, y: m - r2, width: 2*r2, height: 2*r2))
let r3 = CGRect(x: c, y: c, width: sz - 2*c, height: sz - 2*c)
context.addEllipse(in: r3)
context.addRect(r3)
UIColor.clear.setFill()
UIColor.gray.setStroke()
let rounded = UIBezierPath(roundedRect: rect.insetBy(dx: lineWidth/2, dy: lineWidth/2), cornerRadius: corner)
rounded.lineWidth = lineWidth
rounded.stroke()
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img
}()
private static let apple : UIImage? = {
let rect = CGRect(x: 0, y: 0, width: 30, height: 30)
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
// #colorLiteral(red: 0.5843137503, green: 0.8235294223, blue: 0.4196078479, alpha: 1).setFill()
// UIBezierPath(roundedRect: rect, cornerRadius: 0).fill()
// print("drawing")
let fs = 36 as CGFloat
let hFont = UIFont.systemFont(ofSize: fs)
var attrib = [
NSAttributedString.Key.font: hFont,
NSAttributedString.Key.foregroundColor: UIColor.gray
]
let str = "" as NSString
let actualHeight = str.size(withAttributes: attrib).height
attrib[NSAttributedString.Key.font] = hFont.withSize(fs * fs / actualHeight)
let strW = str.size(withAttributes: attrib).width
str.draw(at: CGPoint(x: (rect.size.width - strW) / 2.0, y: -3), withAttributes: attrib)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img
}()
private static let cacheDir = URL.documentDir().appendingPathComponent("app-store-search-cache", isDirectory:true)
private static func local(_ bundleId: String) -> URL {
cacheDir.appendingPathComponent("\(bundleId).img")
}
static func initCache() {
try? FileManager.default.createDirectory(at: cacheDir, withIntermediateDirectories: true, attributes: nil)
}
static func image(_ bundleId: String?, ifNotStored: (() -> Void)? = nil) -> UIImage? {
guard let appId = bundleId else {
return unknown
}
guard let data = try? Data(contentsOf: local(appId)),
let img = UIImage(data: data, scale: 2.0) else {
ifNotStored?()
return appId.hasPrefix("com.apple.") ? apple : unknown
}
return img
}
static func download(_ bundleId: String, url: URL, whenDone: @escaping () -> Void) -> URLSessionDownloadTask {
return url.download(to: local(bundleId), onSuccess: whenDone)
}
}

View File

@@ -0,0 +1,190 @@
import UIKit
protocol TVCAppSearchDelegate {
func appSearch(didSelect bundleId: String, appName: String?, developer: String?)
}
class TVCAppSearch: UITableViewController, UISearchBarDelegate {
private var dataSource: [AppStoreSearch.Result] = []
private var dataSourceLocal: [AppBundleInfo] = []
private var isLoading: Bool = false
private var searchActive: Bool = false
var delegate: TVCAppSearchDelegate?
private var searchNo = 0
private var searchError: Bool = false
private var downloadQueue: [URLSessionDownloadTask] = []
@IBOutlet private var searchBar: UISearchBar!
override func viewDidLoad() {
super.viewDidLoad()
BundleIcon.initCache()
dataSourceLocal = AppDB?.appBundleList() ?? []
}
override var keyCommands: [UIKeyCommand]? {
[UIKeyCommand(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(closeThis))]
}
@objc private func closeThis() {
searchBar.endEditing(true)
dismiss(animated: true)
}
private func showManualEntryAlert() {
let alert = AskAlert(title: "App Name",
text: "Be as descriptive as possible. Preferably use app bundle id if available. Alternatively use app name or a link to a public repository.",
buttonText: "Set") {
self.delegate?.appSearch(didSelect: "_manually", appName: $0.textFields?.first?.text, developer: nil)
self.closeThis()
}
alert.addTextField { $0.placeholder = "com.apple.notes" }
alert.presentIn(self)
}
// MARK: - Table View Data Source
override func numberOfSections(in _: UITableView) -> Int {
dataSourceLocal.count > 0 ? 2 : 1
}
override func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0: return max(1, dataSource.count) + (searchActive ? 1 : 0)
case 1: return dataSourceLocal.count
default: preconditionFailure()
}
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch section {
case 0: return "AppStore"
case 1: return "Found in other recordings"
default: preconditionFailure()
}
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "AppStoreSearchCell")!
let bundleId: String
let altLoadUrl: String?
switch indexPath.section {
case 0:
guard dataSource.count > 0, indexPath.row < dataSource.count else {
if indexPath.row == 0 {
if searchError {
cell.textLabel?.text = "Error loading results"
} else if isLoading {
cell.textLabel?.text = "Loading …"
} else {
cell.textLabel?.text = "No results"
}
cell.isUserInteractionEnabled = false
} else {
cell.textLabel?.text = "Create manually …"
}
cell.detailTextLabel?.text = nil
cell.imageView?.image = nil
return cell
}
let src = dataSource[indexPath.row]
bundleId = src.bundleId
altLoadUrl = src.imageURL
cell.textLabel?.text = src.name
cell.detailTextLabel?.text = src.developer
case 1:
let src = dataSourceLocal[indexPath.row]
bundleId = src.bundleId
altLoadUrl = nil
cell.textLabel?.text = src.name
cell.detailTextLabel?.text = src.author
default:
preconditionFailure()
}
let sno = searchNo
cell.imageView?.image = BundleIcon.image(bundleId) {
guard let u = altLoadUrl, let url = URL(string: u) else { return }
self.downloadQueue.append(BundleIcon.download(bundleId, url: url) {
DispatchQueue.main.async {
// make sure its the same request
guard sno == self.searchNo else { return }
tableView.reloadRows(at: [indexPath], with: .automatic)
}
})
}
cell.isUserInteractionEnabled = true
cell.imageView?.layer.cornerRadius = 6.75
cell.imageView?.layer.masksToBounds = true
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
switch indexPath.section {
case 0:
guard indexPath.row < dataSource.count else {
showManualEntryAlert()
return
}
let src = dataSource[indexPath.row]
delegate?.appSearch(didSelect: src.bundleId, appName: src.name, developer: src.developer)
case 1:
let src = dataSourceLocal[indexPath.row]
delegate?.appSearch(didSelect: src.bundleId, appName: src.name, developer: src.author)
default: preconditionFailure()
}
closeThis()
}
// MARK: - Search Bar Delegate
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(performSearch), object: nil)
isLoading = true
tableView.reloadData()
for x in downloadQueue { x.cancel() }
downloadQueue = []
if searchText.count > 0 {
perform(#selector(performSearch), with: nil, afterDelay: 0.4)
} else {
performSearch()
}
}
/// Internal callback function for delayed text evaluation.
/// This way we can avoid unnecessary searches while user is typing.
@objc private func performSearch() {
func setSource(_ newSource: [AppStoreSearch.Result], _ err: Bool) {
searchNo += 1
searchError = err
dataSource = searchActive ? newSource : []
tableView.reloadData()
}
isLoading = false
let term = searchBar.text?.lowercased() ?? ""
searchActive = term.count > 0
guard searchActive else {
setSource([], false)
return
}
AppStoreSearch.search(term) { source, error in
DispatchQueue.main.async {
setSource(source ?? [], error != nil)
}
}
}
func searchBarSearchButtonClicked(_ searchBar: UISearchBar) {
searchBar.endEditing(true)
}
func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
closeThis()
}
}

View File

@@ -69,7 +69,14 @@ 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 = "at \(DateFormat.seconds(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?.layer.cornerRadius = 6.75
cell.imageView?.layer.masksToBounds = true
if #available(iOS 11, *) {} else {
cell.textLabel?.numberOfLines = 1
cell.detailTextLabel?.numberOfLines = 1
}
return cell return cell
} }

View File

@@ -2,11 +2,16 @@ import UIKit
class TVCRecordingDetails: UITableViewController, EditActionsRemove { class TVCRecordingDetails: UITableViewController, EditActionsRemove {
var record: Recording! var record: Recording!
private lazy var isLongRecording: Bool = (record.duration ?? 0) > Timestamp.hours(1) var noResults: Bool = false
private lazy var isLongRecording: Bool = record.isLongTerm
private var showRaw: Bool = false private var showRaw: Bool = false
/// Sorted by `ts` in ascending order (oldest first) /// Sorted by `ts` in ascending order (oldest first)
private lazy var dataSourceRaw: [DomainTsPair] = RecordingsDB.details(record) private lazy var dataSourceRaw: [DomainTsPair] = {
let list = RecordingsDB.details(record)
noResults = list.count == 0
return list
}()
/// Sorted by `count` (descending), then alphabetically /// Sorted by `count` (descending), then alphabetically
private lazy var dataSourceSum: [(domain: String, count: Int)] = { private lazy var dataSourceSum: [(domain: String, count: Int)] = {
var result: [String:Int] = [:] var result: [String:Int] = [:]
@@ -20,6 +25,14 @@ class TVCRecordingDetails: UITableViewController, EditActionsRemove {
override func viewDidLoad() { override func viewDidLoad() {
title = record.title ?? record.fallbackTitle title = record.title ?? record.fallbackTitle
NotifyRecordingChanged.observe(call: #selector(recordingDidChange(_:)), on: self)
}
@objc private func recordingDidChange(_ notification: Notification) {
let (rec, deleted) = notification.object as! (Recording, Bool)
if rec.id == record.id, !deleted {
record = rec // almost exclusively when 'shared' is set true
}
} }
@IBAction private func toggleDisplayStyle(_ sender: UIBarButtonItem) { @IBAction private func toggleDisplayStyle(_ sender: UIBarButtonItem) {
@@ -28,16 +41,25 @@ class TVCRecordingDetails: UITableViewController, EditActionsRemove {
tableView.reloadData() tableView.reloadData()
} }
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let tgt = segue.destination as? TVCShareRecording {
tgt.record = self.record
}
}
// MARK: - Table View Data Source // MARK: - Table View Data Source
override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int { override func tableView(_: UITableView, numberOfRowsInSection _: Int) -> Int {
showRaw ? dataSourceRaw.count : dataSourceSum.count max(1, showRaw ? dataSourceRaw.count : dataSourceSum.count)
} }
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: UITableViewCell let cell: UITableViewCell
if showRaw { if noResults {
cell = tableView.dequeueReusableCell(withIdentifier: "RecordNoResultsCell")!
cell.textLabel?.text = " empty recording "
} else if showRaw {
let x = dataSourceRaw[indexPath.row] let x = dataSourceRaw[indexPath.row]
if isLongRecording { if isLongRecording {
cell = tableView.dequeueReusableCell(withIdentifier: "RecordDetailLongCell")! cell = tableView.dequeueReusableCell(withIdentifier: "RecordDetailLongCell")!
@@ -61,11 +83,11 @@ class TVCRecordingDetails: UITableViewController, EditActionsRemove {
// MARK: - Editing // MARK: - Editing
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? {
getRowActionsIOS9(indexPath, tableView) noResults ? nil : getRowActionsIOS9(indexPath, tableView)
} }
@available(iOS 11.0, *) @available(iOS 11.0, *)
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
getRowActionsIOS11(indexPath) noResults ? nil : getRowActionsIOS11(indexPath)
} }
func editableRowCallback(_ index: IndexPath, _ action: RowAction, _ userInfo: Any?) -> Bool { func editableRowCallback(_ index: IndexPath, _ action: RowAction, _ userInfo: Any?) -> Bool {
@@ -89,6 +111,57 @@ class TVCRecordingDetails: UITableViewController, EditActionsRemove {
tableView.deleteRows(at: [index], with: .automatic) tableView.deleteRows(at: [index], with: .automatic)
} }
} }
noResults = dataSourceRaw.count == 0
return true return true
} }
// MARK: - Tap to Copy
private var cellMenu = TableCellTapMenu()
private var copyDomain: String? = nil
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
if noResults { return nil }
let buttons = [
UIMenuItem(title: "All requests", action: #selector(openInLogs)),
UIMenuItem(title: "Co-Occurrence", action: #selector(openCoOccurrence))
]
if cellMenu.start(tableView, indexPath, items: buttons) {
if showRaw {
copyDomain = cellMenu.getSelected(dataSourceRaw)?.domain
} else {
copyDomain = cellMenu.getSelected(dataSourceSum)?.domain
}
self.becomeFirstResponder()
}
return nil
}
override var canBecomeFirstResponder: Bool { true }
override func copy(_ sender: Any?) {
if let dom = copyDomain {
UIPasteboard.general.string = dom
}
cellMenu.reset()
copyDomain = nil
}
@objc private func openInLogs() {
if let dom = copyDomain, let req = (tabBarController as? TBCMain)?.openTab(0) as? TVCDomains {
VCDateFilter.disableFilter()
req.pushOpen(domain: dom)
}
cellMenu.reset()
copyDomain = nil
}
@objc private func openCoOccurrence() {
if let dom = copyDomain {
present(VCCoOccurrence.make(dom), animated: true)
}
cellMenu.reset()
copyDomain = nil
}
} }

View File

@@ -0,0 +1,279 @@
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)
}
}

View File

@@ -1,60 +1,97 @@
import UIKit import UIKit
class VCEditRecording: UIViewController, UITextFieldDelegate, UITextViewDelegate { class VCEditRecording: UIViewController, UITextFieldDelegate, UITextViewDelegate, TVCAppSearchDelegate {
var record: Recording! var record: Recording!
var deleteOnCancel: Bool = false var deleteOnCancel: Bool = false
var appId: String?
@IBOutlet private var buttonCancel: UIBarButtonItem! @IBOutlet private var buttonCancel: UIBarButtonItem!
@IBOutlet private var buttonSave: UIBarButtonItem! @IBOutlet private var buttonSave: UIBarButtonItem!
@IBOutlet private var inputTitle: UITextField! @IBOutlet private var appTitle: UILabel!
@IBOutlet private var appDeveloper: UILabel!
@IBOutlet private var appIcon: UIImageView!
@IBOutlet private var inputNotes: UITextView! @IBOutlet private var inputNotes: UITextView!
@IBOutlet private var inputDetails: UITextView! @IBOutlet private var inputDetails: UITextView!
@IBOutlet private var noteBottom: NSLayoutConstraint! @IBOutlet private var noteBottom: NSLayoutConstraint!
@IBOutlet private var chooseAppTap: UITapGestureRecognizer!
@IBOutlet private var buttonFilter: UIButton!
override func viewDidLoad() { override func viewDidLoad() {
inputTitle.placeholder = record.fallbackTitle if deleteOnCancel { // aka newly created
inputTitle.text = record.title let r = record!
inputNotes.text = record.notes DispatchQueue.global().async {
inputDetails.text = """ RecordingsDB.persist(r)
Start: \(DateFormat.seconds(record.start)) if Prefs.RecordingReminder.Enabled {
End: \(record.stop == nil ? "?" : DateFormat.seconds(record.stop!)) PushNotification.scheduleRecordingReminder(force: true)
Duration: \(TimeFormat.from(record.duration ?? 0)) }
""" }
validateSaveButton() buttonFilter.isHidden = true
if deleteOnCancel { // mark as destructive // mark as destructive
buttonCancel.tintColor = .systemRed buttonCancel.tintColor = .systemRed
if #available(iOS 13.0, *) { if #available(iOS 13.0, *) {
isModalInPresentation = true isModalInPresentation = true
} }
} }
if record.isLongTerm {
appId = nil
appIcon.image = nil
appTitle.text = record.fallbackTitle
appDeveloper.text = nil
chooseAppTap.isEnabled = false
} else {
appId = record.appId
appIcon.image = BundleIcon.image(record.appId)
appIcon.layer.cornerRadius = 6.75
appIcon.layer.masksToBounds = true
if record.appId == nil {
appTitle.text = "Tap here to choose app"
appDeveloper.text = record.title
} else {
appTitle.text = record.title ?? record.fallbackTitle
appDeveloper.text = record.subtitle
}
}
inputNotes.text = record.notes
inputDetails.text = """
Start: \(DateFormat.seconds(record.start))
End: \(record.stop == nil ? "?" : DateFormat.seconds(record.stop!))
Duration: \(TimeFormat.from(record.duration))
"""
validateSaveButton()
UIResponder.keyboardWillShowNotification.observe(call: #selector(keyboardWillShow), on: self) UIResponder.keyboardWillShowNotification.observe(call: #selector(keyboardWillShow), on: self)
UIResponder.keyboardWillHideNotification.observe(call: #selector(keyboardWillHide), on: self) UIResponder.keyboardWillHideNotification.observe(call: #selector(keyboardWillHide), on: self)
} }
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
// MARK: Save & Cancel Buttons if let tvc = segue.destination as? TVCAppSearch {
tvc.delegate = self
@IBAction func didTapSave(_ sender: UIBarButtonItem) {
let newlyCreated = deleteOnCancel
if newlyCreated {
// if remains true, `viewDidDisappear` will delete the record
deleteOnCancel = false
}
QLog.Debug("updating record #\(record.id)")
record.title = (inputTitle.text == "") ? nil : inputTitle.text
record.notes = (inputNotes.text == "") ? nil : inputNotes.text
dismiss(animated: true) {
RecordingsDB.update(self.record)
if newlyCreated {
RecordingsDB.persist(self.record)
if Prefs.RecordingReminder.Enabled {
PushNotification.scheduleRecordingReminder(force: true)
}
}
} }
} }
@IBAction func didTapCancel(_ sender: UIBarButtonItem) { // MARK: Save & Cancel Buttons
@IBAction func didTapSave() {
// if remains true, `viewDidDisappear` will delete the record
deleteOnCancel = false
QLog.Debug("updating record #\(record.id)")
if let id = appId, id != "" {
record.appId = id
record.title = (appTitle.text == "") ? nil : appTitle.text
record.subtitle = (appDeveloper.text == "") ? nil : appDeveloper.text
} else {
record.appId = nil
record.title = nil
record.subtitle = nil
}
record.notes = (inputNotes.text == "") ? nil : inputNotes.text
dismiss(animated: true) {
RecordingsDB.update(self.record)
}
}
@IBAction func didTapCancel() {
QLog.Debug("discard edit of record #\(record.id)") QLog.Debug("discard edit of record #\(record.id)")
dismiss(animated: true) dismiss(animated: true)
} }
@@ -67,6 +104,16 @@ class VCEditRecording: UIViewController, UITextFieldDelegate, UITextViewDelegate
} }
} }
@IBAction func didTapFilter() {
if buttonSave.isEnabled {
NotificationBanner("Filter set", style: .ok).present(in: self, hideAfter: 1)
} else {
(presentingViewController as? TBCMain)?.openTab(0)
didTapCancel()
}
VCDateFilter.setFilter(range: record.start, to: record.stop)
}
// MARK: Handle Keyboard & Notes Frame // MARK: Handle Keyboard & Notes Frame
@@ -121,11 +168,18 @@ class VCEditRecording: UIViewController, UITextFieldDelegate, UITextViewDelegate
func textViewDidChange(_ _: UITextView) { validateSaveButton() } func textViewDidChange(_ _: UITextView) { validateSaveButton() }
private func validateSaveButton() { private func validateSaveButton() {
let changed = (inputTitle.text != record.title ?? "" || inputNotes.text != record.notes ?? "") let changed = (appId != record.appId
|| (appTitle.text != record.title && appTitle.text != "Tap here to choose app" && appTitle.text != record.fallbackTitle)
|| appDeveloper.text != record.subtitle
|| inputNotes.text != record.notes ?? "")
buttonSave.isEnabled = changed || deleteOnCancel // always allow save for new recordings buttonSave.isEnabled = changed || deleteOnCancel // always allow save for new recordings
} }
func textFieldShouldReturn(_ textField: UITextField) -> Bool { func appSearch(didSelect bundleId: String, appName: String?, developer: String?) {
textField == inputTitle ? inputNotes.becomeFirstResponder() : true appId = bundleId
appTitle.text = appName
appDeveloper.text = developer
appIcon.image = BundleIcon.image(bundleId)
validateSaveButton()
} }
} }

View File

@@ -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
}
}

View File

@@ -3,138 +3,144 @@ 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 buttonView: UIView!
@IBOutlet private var runningView: UIView!
@IBOutlet private var timeLabel: UILabel! @IBOutlet private var timeLabel: UILabel!
@IBOutlet private var startButton: UIButton! @IBOutlet private var stopButton: UIButton!
@IBOutlet private var startNewRecView: UIView! @IBOutlet private var startSegment: UISegmentedControl!
private var prevRecController: UINavigationController!
override func viewDidLoad() { override func viewDidLoad() {
prevRecController = (children.first as! UINavigationController) startSegment.setTitleTextAttributes([NSAttributedString.Key.foregroundColor : UIColor.sysLink], for: .normal)
prevRecController.delegate = self
timeLabel.font = timeLabel.font.monoSpace() timeLabel.font = timeLabel.font.monoSpace()
// hide timer if not running if let ongoing = RecordingsDB.getCurrent() {
updateUI(setRecording: false, animated: false) currentRecording = ongoing
currentRecording = RecordingsDB.getCurrent() // 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
updateUI(setRecording: false, animated: false)
}
if !Prefs.DidShowTutorial.Recordings { if !Prefs.DidShowTutorial.Recordings {
self.perform(#selector(showTutorial), with: nil, afterDelay: 0.5) DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
let x = TutorialSheet()
x.addSheet().addArrangedSubview(TinyMarkdown.load("tut-recording-1"))
x.addSheet().addArrangedSubview(TinyMarkdown.load("tut-recording-2"))
x.buttonTitleDone = "Got it"
x.present(didClose: {
Prefs.DidShowTutorial.Recordings = true
})
}
} }
} }
override func viewDidAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
if currentRecording != nil { startTimer(animate: false) } super.viewWillAppear(animated)
recordingTimer?.fireDate = .distantPast
navigationController?.setNavigationBarHidden(true, animated: animated)
// set hidden in will appear causes UITableViewAlertForLayoutOutsideViewHierarchy
// but otherwise navBar is visible during transition
} }
override func viewWillDisappear(_ animated: Bool) { override func viewWillDisappear(_ animated: Bool) {
stopTimer(animate: false) super.viewWillDisappear(animated)
recordingTimer?.fireDate = .distantFuture
navigationController?.setNavigationBarHidden(false, animated: animated)
} }
func navigationController(_ nav: UINavigationController, willShow vc: UIViewController, animated: Bool) { @IBAction private func showInfo(_ sender: UIButton?) {
hideNewRecording(isRootVC: (vc == nav.viewControllers.first), didShow: false) let x = TutorialSheet()
} x.addSheet().addArrangedSubview(TinyMarkdown.load("tut-recording-howto"))
x.buttonTitleDone = "Close"
func navigationController(_ nav: UINavigationController, didShow vc: UIViewController, animated: Bool) { x.present(didClose: {
// TODO: use interactive animation handler to dynamically animate "new recording" view Prefs.DidShowTutorial.RecordingHowTo = true
hideNewRecording(isRootVC: (vc == nav.viewControllers.first), didShow: true) })
}
private func hideNewRecording(isRootVC: Bool, didShow: Bool) {
if isRootVC == didShow {
UIView.animate(withDuration: 0.3) {
self.startNewRecView.isHidden = !isRootVC // hide "new recording" if details open
}
}
} }
// MARK: Start New Recording // MARK: Start New Recording
@IBAction private func startRecordingButtonTapped(_ sender: UIButton) { @IBAction private func startRecording(_ sender: UISegmentedControl) {
if recordingTimer == nil { guard GlassVPN.state == .on else {
currentRecording = RecordingsDB.startNew() AskAlert(title: "VPN stopped",
startTimer(animate: true) text: "You need to start the VPN proxy before you can start a recording.",
} else { buttonText: "Start") { _ in
stopTimer(animate: true) GlassVPN.setEnabled(true)
RecordingsDB.stop(&currentRecording!) }.presentIn(self)
prevRecController.popToRootViewController(animated: true)
let editVC = (prevRecController.topViewController as! TVCPreviousRecords)
editVC.insertAndEditRecording(currentRecording!)
currentRecording = nil // otherwise it will restart
}
}
private func startTimer(animate: Bool) {
guard let r = currentRecording, r.stop == nil else {
return return
} }
recordingTimer = Timer.repeating(0.086, call: #selector(timerCallback(_:)), on: self, userInfo: Date(r.start)) guard Prefs.DidShowTutorial.RecordingHowTo else {
updateUI(setRecording: true, animated: animate) showInfo(nil) // show at least once. Later, user can click the help icon.
return
}
currentRecording = RecordingsDB.startNew()
QLog.Debug("start recording #\(currentRecording!.id)")
let longterm = sender.selectedSegmentIndex == 1
startTimer(animate: true, longterm: longterm)
notifyVPN(setRecording: longterm ? .Background : .App)
} }
@objc private func timerCallback(_ sender: Timer) { @IBAction private func stopRecording(_ sender: UIButton) {
timeLabel.text = TimeFormat.since(sender.userInfo as! Date, millis: true) let validRecording = (state == .Background) == currentRecording!.isLongTerm
notifyVPN(setRecording: .Off) // will change state = .Off
stopTimer()
QLog.Debug("stop recording #\(currentRecording!.id)")
RecordingsDB.stop(&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
} }
private func stopTimer(animate: Bool) { private func notifyVPN(setRecording state: CurrentRecordingState) {
recordingTimer?.invalidate() PrefsShared.CurrentlyRecording = state
recordingTimer = nil self.state = state
updateUI(setRecording: false, animated: animate) GlassVPN.send(.isRecording(state))
} }
private func updateUI(setRecording: Bool, animated: Bool) { private func updateUI(setRecording: Bool, animated: Bool) {
let title = setRecording ? "Stop Recording" : "Start New Recording" stopButton.tag = 99 // tag used in timerCallback()
let color = setRecording ? UIColor.systemRed : nil stopButton.setTitle("", for: .normal) // prevent flashing while animating in and out
let yT = setRecording ? 0 : -timeLabel.frame.height let block = {
let yB = (setRecording ? 1 : 0.5) * (startButton.superview!.frame.height - startButton.frame.height) self.headerView.isHidden = setRecording
if !animated { // else title will flash self.buttonView.isHidden = setRecording
startButton.titleLabel?.text = title self.runningView.isHidden = !setRecording
}
UIView.animate(withDuration: animated ? 0.3 : 0) {
self.timeLabel.frame.origin.y = yT
self.startButton.frame.origin.y = yB
self.startButton.setTitle(title, for: .normal)
self.startButton.setTitleColor(color, for: .normal)
} }
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
}
// MARK: Tutorial View Controller private func stopTimer() {
recordingTimer?.invalidate()
recordingTimer = nil
updateUI(setRecording: false, animated: true)
}
@objc private func showTutorial() { @objc private func timerCallback(_ sender: Timer) {
let x = TutorialSheet() let (slow, start) = sender.userInfo as! (Bool, Date)
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString() timeLabel.text = TimeFormat.since(start, millis: !slow, hours: slow)
.h1("What are Recordings?\n") let valid = slow == currentRecording!.isLongTerm
.normal("\nSimilar to the default logging, recordings will intercept every request and log it for later review. " + let validInt = (valid ? 1 : 0)
"Recordings are usually 3  5 minutes long and cover a single application. " + if stopButton.tag != validInt {
"You can utilize recordings for App analysis or to get a ground truth for background traffic." + stopButton.tag = validInt
"\n\n" + stopButton.setTitle(valid ? "Stop" : slow ? "Cancel" : "Discard", for: .normal)
"Optionally, you can help us by providing app specific recordings. " +
"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.")
))
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString()
.h1("How to record?\n")
.normal("\nBefore you begin a new recording make sure that you quit all running applications. " +
"Tap on the 'Start Recording' button and switch to the application you'd like to inspect. " +
"Use the App as you would normally. Try to get to all corners and functionality the App provides. " +
"When you feel that you have captured enough content, come back to ").italic("AppCheck").normal(" and stop the recording." +
"\n\n" +
"Upon completion you will find your recording in the 'Previous Recordings' section. " +
"You can review your results and remove user specific information if necessary.")
))
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString()
.h1("Share results\n")
.normal("\nThis step is completely ").bold("optional").normal(". " +
"You can choose to share your results with us. " +
"We can compare similar applications and suggest privacy friendly alternatives. " +
"Together with other likeminded individuals we can increase the awareness for privacy friendly design." +
"\n\n" +
"Thank you very much.")
))
x.buttonTitleDone = "Got it"
x.present {
Prefs.DidShowTutorial.Recordings = true
} }
} }
} }

View File

@@ -48,13 +48,10 @@ class VCAnalysisBar: UIViewController, UITabBarDelegate {
} }
private func openCoOccurrence() { private func openCoOccurrence() {
guard let delegate = parent as? AnalysisBarDelegate, guard let delegate = parent as? AnalysisBarDelegate else {
let vc: VCCoOccurrence = storyboard?.load("IBCoOccurrence") else {
return return
} }
let info = delegate.analysisBarWillOpenCoOccurrence() let x = delegate.analysisBarWillOpenCoOccurrence()
vc.domainName = info.domain present(VCCoOccurrence.make(x.domain, isFQDN: x.isFQDN), animated: true)
vc.isFQDN = info.isFQDN
present(vc, animated: true)
} }
} }

View File

@@ -14,6 +14,14 @@ class VCCoOccurrence: UIViewController, UITableViewDataSource {
private var logTimeDelta: CGFloat = 1 private var logTimeDelta: CGFloat = 1
private var logMaxCount: CGFloat = 1 private var logMaxCount: CGFloat = 1
static func make(_ domain: String, isFQDN: Bool = true) -> Self {
let story = UIStoryboard(name: "CoOccurrence", bundle: nil)
let vc = story.instantiateInitialViewController() as! Self
vc.domainName = domain
vc.isFQDN = isFQDN
return vc
}
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
selectedTime = Prefs.ContextAnalyis.CoOccurrenceTime // calls `didSet` and `logTimeDelta` selectedTime = Prefs.ContextAnalyis.CoOccurrenceTime // calls `didSet` and `logTimeDelta`
@@ -132,21 +140,9 @@ extension VCCoOccurrence {
}() }()
let x = TutorialSheet() let x = TutorialSheet()
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString() x.addSheet().addArrangedSubview(TinyMarkdown.load("tut-cooccurrence", replacements: [
.h3("Co-Occurrence") "<IMG>" : .init(image: sampleCell, centered: true)
.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) x.present(in: self)
} }
} }

View File

@@ -66,32 +66,24 @@ class TVCOccurrenceContext: UITableViewController {
// MARK: - Tap to Copy // MARK: - Tap to Copy
private var rowToCopy: Int = Int.max private var cellMenu = TableCellTapMenu()
private var copyDomain: String? = nil
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
if firstOrLast(indexPath.row) { return nil } if !firstOrLast(indexPath.row), cellMenu.start(tableView, indexPath) {
if rowToCopy == indexPath.row { copyDomain = cellMenu.getSelected(dataSource)?.domain
UIMenuController.shared.setMenuVisible(false, animated: true) self.becomeFirstResponder()
rowToCopy = Int.max
return nil
} }
rowToCopy = indexPath.row
self.becomeFirstResponder()
let cell = tableView.cellForRow(at: indexPath)!
UIMenuController.shared.setTargetRect(cell.bounds, in: cell)
UIMenuController.shared.setMenuVisible(true, animated: true)
return nil return nil
} }
override var canBecomeFirstResponder: Bool { true } override var canBecomeFirstResponder: Bool { true }
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
action == #selector(UIResponderStandardEditActions.copy)
}
override func copy(_ sender: Any?) { override func copy(_ sender: Any?) {
guard rowToCopy < dataSource.count else { return } if let dom = copyDomain {
UIPasteboard.general.string = dataSource[rowToCopy].domain UIPasteboard.general.string = dom
rowToCopy = Int.max }
cellMenu.reset()
copyDomain = nil
} }
} }

View File

@@ -116,4 +116,20 @@ class VCDateFilter: UIViewController, UIGestureRecognizerDelegate {
NotifyDateFilterChanged.post() NotifyDateFilterChanged.post()
} }
} }
static func disableFilter() {
if Prefs.DateFilter.Kind <-? .Off {
Prefs.DateFilter.LastXMin = 0
Prefs.DateFilter.RangeA = nil
Prefs.DateFilter.RangeB = nil
NotifyDateFilterChanged.post()
}
}
static func setFilter(range from: Timestamp?, to: Timestamp?) {
Prefs.DateFilter.Kind = .ABRange
Prefs.DateFilter.RangeA = from
Prefs.DateFilter.RangeB = to
NotifyDateFilterChanged.post()
}
} }

View File

@@ -51,9 +51,6 @@ class TVCFilter: UITableViewController, EditActionsRemove {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "DomainFilterCell")! let cell = tableView.dequeueReusableCell(withIdentifier: "DomainFilterCell")!
cell.textLabel?.text = dataSource[indexPath.row] cell.textLabel?.text = dataSource[indexPath.row]
if cell.gestureRecognizers?.isEmpty ?? true {
cell.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(didLongTap)))
}
return cell return cell
} }
@@ -75,29 +72,24 @@ class TVCFilter: UITableViewController, EditActionsRemove {
// MARK: - Long Press Gesture // MARK: - Long Press Gesture
private var cellTitleCopy: String? private var cellMenu = TableCellTapMenu()
private var copyDomain: String? = nil
@objc private func didLongTap(_ sender: UILongPressGestureRecognizer) { override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
guard let cell = sender.view as? UITableViewCell else { if cellMenu.start(tableView, indexPath) {
return copyDomain = cellMenu.getSelected(dataSource)
}
if sender.state == .began {
cellTitleCopy = cell.textLabel?.text
self.becomeFirstResponder() self.becomeFirstResponder()
let menu = UIMenuController.shared
// menu.setTargetRect(CGRect(origin: sender.location(in: cell), size: CGSize.zero), in: cell)
menu.setTargetRect(cell.bounds, in: cell)
menu.setMenuVisible(true, animated: true)
} }
} return nil
override var canBecomeFirstResponder: Bool { get { true } }
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
action == #selector(UIResponderStandardEditActions.copy)
} }
override var canBecomeFirstResponder: Bool { true }
override func copy(_ sender: Any?) { override func copy(_ sender: Any?) {
UIPasteboard.general.string = cellTitleCopy if let dom = copyDomain {
cellTitleCopy = nil UIPasteboard.general.string = dom
}
cellMenu.reset()
copyDomain = nil
} }
} }

View File

@@ -3,6 +3,8 @@ import UIKit
class TVCSettings: UITableViewController { class TVCSettings: UITableViewController {
@IBOutlet var vpnToggle: UISwitch! @IBOutlet var vpnToggle: UISwitch!
@IBOutlet var unresolvableToggle: UISwitch!
@IBOutlet var swcdToggle: UISwitch!
@IBOutlet var cellDomainsIgnored: UITableViewCell! @IBOutlet var cellDomainsIgnored: UITableViewCell!
@IBOutlet var cellDomainsBlocked: UITableViewCell! @IBOutlet var cellDomainsBlocked: UITableViewCell!
@IBOutlet var cellPrivacyAutoDelete: UITableViewCell! @IBOutlet var cellPrivacyAutoDelete: UITableViewCell!
@@ -14,6 +16,7 @@ class TVCSettings: UITableViewController {
reloadVPNState() reloadVPNState()
reloadLoggingFilterUI() reloadLoggingFilterUI()
reloadPrivacyUI() reloadPrivacyUI()
reloadAdvancedUI()
NotifyVPNStateChanged.observe(call: #selector(reloadVPNState), on: self) NotifyVPNStateChanged.observe(call: #selector(reloadVPNState), on: self)
NotifyDNSFilterChanged.observe(call: #selector(reloadLoggingFilterUI), on: self) NotifyDNSFilterChanged.observe(call: #selector(reloadLoggingFilterUI), on: self)
} }
@@ -191,6 +194,21 @@ extension TVCSettings {
// MARK: - Advanced // MARK: - Advanced
extension TVCSettings { extension TVCSettings {
private func reloadAdvancedUI() {
unresolvableToggle.isOn = PrefsShared.ForceDisconnectUnresolvableDNS
swcdToggle.isOn = PrefsShared.ForceDisconnectSWCD
}
@IBAction private func togglePreventUnresolvable(_ sender: UISwitch) {
PrefsShared.ForceDisconnectUnresolvableDNS = sender.isOn
GlassVPN.send(.disconnectUnresolvable(sender.isOn))
}
@IBAction private func togglePreventSWCD(_ sender: UISwitch) {
PrefsShared.ForceDisconnectSWCD = sender.isOn
GlassVPN.send(.disconnectSWCD(sender.isOn))
}
@IBAction private func exportDB() { @IBAction private func exportDB() {
AppDB?.vacuum() AppDB?.vacuum()
let sheet = UIActivityViewController(activityItems: [URL.internalDB()], applicationActivities: nil) let sheet = UIActivityViewController(activityItems: [URL.internalDB()], applicationActivities: nil)

View File

@@ -34,34 +34,16 @@ class TBCMain: UITabBarController {
@objc private func showWelcomeMessage() { @objc private func showWelcomeMessage() {
let x = TutorialSheet() let x = TutorialSheet()
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString() x.addSheet().addArrangedSubview(TinyMarkdown.load("tut-welcome-1"))
.h1("Welcome\n") x.addSheet().addArrangedSubview(TinyMarkdown.load("tut-welcome-2"))
.normal("\nAppCheck helps you identify which applications communicate with third parties. " + x.present(didClose: {
"It does so by logging network requests. " +
"AppCheck learns only the destination addresses, not the actual data that is exchanged." +
"\n\n" +
"Your data belongs to you. " +
"Therefore, monitoring and analysis take place on your device only. " +
"The app does not share any data with us or any other third-party. " +
"Unless you choose to.")
))
x.addSheet().addArrangedSubview(QuickUI.text(attributed: NSMutableAttributedString()
.h1("How it works\n")
.normal("\nAppCheck creates a local VPN tunnel to intercept all network connections. " +
"For each connection AppCheck looks into the DNS headers only, namely the domain names. " +
"\n" +
"These domain names are logged in the background while the VPN is active. " +
"That means, AppCheck does not have to be active in the foreground. " +
"You can close the app and come back later to see the results."
)
))
x.present {
Prefs.DidShowTutorial.Welcome = true Prefs.DidShowTutorial.Welcome = true
} })
} }
} }
extension TBCMain { extension TBCMain {
/// Open tab and pop to root view controller.
@discardableResult func openTab(_ index: Int) -> UIViewController? { @discardableResult func openTab(_ index: Int) -> UIViewController? {
selectedIndex = index selectedIndex = index
guard let nav = selectedViewController as? UINavigationController else { guard let nav = selectedViewController as? UINavigationController else {

View File

@@ -1,129 +0,0 @@
import Foundation
import UIKit
private let fm = FileManager.default
private let documentsDir = try! fm.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
private let bundleInfoDir = documentsDir.appendingPathComponent("bundleInfo", isDirectory:true)
struct AppInfoType : Decodable {
var id: String
var name: String?
var seller: String?
var imageURL: URL?
private var remoteImgURL: String?
private var cache: Bool?
private let localJSON: URL
private let localImgURL: URL
static func initWorkingDir() {
try? fm.createDirectory(at: bundleInfoDir, withIntermediateDirectories: true, attributes: nil)
// print("init dir: \(bundleInfoDir)")
}
init(id: String) {
self.id = id
if id == "" {
name = "?"
cache = true
localJSON = URL(fileURLWithPath: "")
localImgURL = localJSON
} else {
localJSON = bundleInfoDir.appendingPathComponent("\(id).json")
localImgURL = bundleInfoDir.appendingPathComponent("\(id).img")
reload()
}
}
mutating func reload() {
if fm.fileExists(atPath: localImgURL.path) {
imageURL = localImgURL
}
guard name == nil, seller == nil,
fm.fileExists(atPath: localJSON.path),
let attr = try? fm.attributesOfItem(atPath: localJSON.path),
attr[FileAttributeKey.size] as! UInt64 > 0 else
{
// process json only if attributes not set yet,
// OR json doesn't exist, OR json is empty
return
}
(name, seller, remoteImgURL) = parseJSON(localJSON)
if remoteImgURL == nil || imageURL != nil {
cache = true
}
}
func getImage() -> UIImage? {
if let img = imageURL, let data = try? Data(contentsOf: img) {
return UIImage(data: data, scale: 2.0)
} else if id.hasPrefix("com.apple.") {
return appIconApple
} else {
return appIconUnknown
}
}
private func parseJSON(_ location: URL) -> (name: String?, seller: String?, image: String?) {
do {
let data = try Data.init(contentsOf: location)
if
let json = try JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed) as? [String: Any],
let resAll = json["results"] as? [Any],
let res = resAll.first as? [String: Any]
{
let name = res["trackName"] as? String // trackCensoredName
let seller = res["sellerName"] as? String // artistName
let image = res["artworkUrl60"] as? String // artworkUrl100
return (name, seller, image)
} else if id.hasPrefix("com.apple.") {
return (String(id.dropFirst(10)), "Apple Inc.", nil)
}
} catch {}
return (nil, nil, nil)
}
mutating func updateIfNeeded(_ updateClosure: () -> Void) {
guard cache == nil,
let safeId = id.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
return
}
cache = false // meaning: hasn't downloaded yet, but is about to do
// print("downloading \(id)")
_ = downloadURL("https://itunes.apple.com/lookup?bundleId=\(safeId)", toFile: localJSON).flatMap{
// print("downloading \(id) done.")
reload()
updateClosure()
return downloadURL(remoteImgURL, toFile: localImgURL)
}.map{
// print("downloading \(id) image done.")
reload()
updateClosure()
}
}
enum NetworkError: Error {
case url
}
private func downloadURL(_ urlStr: String?, toFile: URL) -> Result<Void, Error> {
guard let urlStr = urlStr, let url = URL(string: urlStr) else {
return .failure(NetworkError.url)
}
var result: Result<Void, Error>!
let semaphore = DispatchSemaphore(value: 0)
URLSession.shared.downloadTask(with: url) { location, response, error in
if let loc = location {
try? fm.removeItem(at: toFile)
try? fm.moveItem(at: loc, to: toFile)
result = .success(())
} else {
result = .failure(error!)
}
semaphore.signal()
}.resume()
_ = semaphore.wait(wallTimeout: .distantFuture)
return result
}
}

View File

@@ -1,78 +0,0 @@
import UIKit
let appIconApple = generateAppleIcon()
let appIconUnknown = generateUnknownIcon()
func generateAppleIcon() -> UIImage? {
let rect = CGRect(x: 0, y: 0, width: 30, height: 30)
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
// #colorLiteral(red: 0.5843137503, green: 0.8235294223, blue: 0.4196078479, alpha: 1).setFill()
// UIBezierPath(roundedRect: rect, cornerRadius: 0).fill()
// print("drawing")
let fs = 36 as CGFloat
let hFont = UIFont.systemFont(ofSize: fs)
var attrib = [
NSAttributedString.Key.font: hFont,
NSAttributedString.Key.foregroundColor: UIColor.gray
]
let str = "" as NSString
let actualHeight = str.size(withAttributes: attrib).height
attrib[NSAttributedString.Key.font] = hFont.withSize(fs * fs / actualHeight)
let strW = str.size(withAttributes: attrib).width
str.draw(at: CGPoint(x: (rect.size.width - strW) / 2.0, y: -3), withAttributes: attrib)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img
}
func generateUnknownIcon() -> UIImage? {
let rect = CGRect(x: 0, y: 0, width: 30, height: 30)
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
let context = UIGraphicsGetCurrentContext()!
let lineWidth: CGFloat = 0.5
let corner: CGFloat = 6.75
let c = corner / CGFloat.pi + lineWidth/2
let sz: CGFloat = rect.height
let m = sz / 2
let r1 = 0.2 * sz, r2 = sqrt(2 * r1 * r1)
// diagonal
context.lineFromTo(x1: c, y1: c, x2: sz-c, y2: sz-c)
context.lineFromTo(x1: c, y1: sz-c, x2: sz-c, y2: c)
// horizontal
context.lineFromTo(x1: 0, y1: m, x2: sz, y2: m)
context.lineFromTo(x1: 0, y1: m + r1, x2: sz, y2: m + r1)
context.lineFromTo(x1: 0, y1: m - r1, x2: sz, y2: m - r1)
// vertical
context.lineFromTo(x1: m, y1: 0, x2: m, y2: sz)
context.lineFromTo(x1: m + r1, y1: 0, x2: m + r1, y2: sz)
context.lineFromTo(x1: m - r1, y1: 0, x2: m - r1, y2: sz)
// circles
context.addEllipse(in: CGRect(x: m - r1, y: m - r1, width: 2*r1, height: 2*r1))
context.addEllipse(in: CGRect(x: m - r2, y: m - r2, width: 2*r2, height: 2*r2))
let r3 = CGRect(x: c, y: c, width: sz - 2*c, height: sz - 2*c)
context.addEllipse(in: r3)
context.addRect(r3)
UIColor.clear.setFill()
UIColor.gray.setStroke()
let rounded = UIBezierPath(roundedRect: rect.insetBy(dx: lineWidth/2, dy: lineWidth/2), cornerRadius: corner)
rounded.lineWidth = lineWidth
rounded.stroke()
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img
}
extension CGContext {
func lineFromTo(x1: CGFloat, y1: CGFloat, x2: CGFloat, y2: CGFloat) {
self.move(to: CGPoint(x: x1, y: y1))
self.addLine(to: CGPoint(x: x2, y: y2))
}
}

View File

@@ -84,6 +84,31 @@ br.vet
br.vlog br.vlog
br.wiki br.wiki
br.zlg br.zlg
co.a
co.b
co.com
co.edu
co.g
co.gov
co.inf
co.m
co.mil
co.net
co.ngo
co.nom
co.o
co.org
co.s
co.t
co.x
co.y
er.com
er.edu
er.gov
er.mil
er.net
er.org
er.ind
es.com es.com
es.edu es.edu
es.gob es.gob

View File

@@ -0,0 +1,11 @@
___Co-Occurrence___ 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.
How do you interpret these results? Lets look at an example:
<IMG>
The domain __example.org__ had __14__ requests with an _average time divergence_ of __0.71 seconds__. That is, these 14 domain calls happend, on average, less then a second before or after the original request of the selected domain.
Close temporal proximity and high occurrence counts are both indicators for domain correlation. Results are sorted by a ranking index (__9.__) which strikes a balance between the two. Preferring entries with higher counts as well as low time divergence.
_Tip:_ As a visual guide you can look for the colored bar beside each value. The larger the bar, the greater the correlation.

View File

@@ -0,0 +1,5 @@
# What are Recordings?
Similar to the default logging, recordings will intercept every request and log it for later review. App recordings are usually 1  4 minutes long and cover a single application. You can utilize recordings for App analysis or to get a ground truth on background traffic.
Optionally, you can help us by providing your app specific recordings. 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.

View File

@@ -0,0 +1,5 @@
# Contribute
This step is completely __optional__. You can choose to share your results with us. We can compare similar applications and suggest privacy friendly alternatives. Together with other likeminded individuals we can increase the awareness for privacy friendly design.
Thank you very much.

Some files were not shown because too many files have changed in this diff Show More