First version with app notifications
This commit is contained in:
@@ -1,58 +1,81 @@
|
||||
import Foundation
|
||||
|
||||
enum Prefs {
|
||||
private static func Int(_ key: String) -> Int { UserDefaults.standard.integer(forKey: key) }
|
||||
private static func Int(_ val: Int, _ key: String) { UserDefaults.standard.set(val, forKey: key) }
|
||||
private static func Bool(_ key: String) -> Bool { UserDefaults.standard.bool(forKey: key) }
|
||||
private static func Bool(_ val: Bool, _ key: String) { UserDefaults.standard.set(val, forKey: key) }
|
||||
private static func `Any`(_ key: String) -> Any? { UserDefaults.standard.object(forKey: key) }
|
||||
private static func `Any`(_ val: Any?, _ key: String) { UserDefaults.standard.set(val, forKey: key) }
|
||||
private static var suite: UserDefaults { UserDefaults.standard }
|
||||
|
||||
private static func Int(_ key: String) -> Int { suite.integer(forKey: key) }
|
||||
private static func Int(_ key: String, _ val: Int) { suite.set(val, forKey: key) }
|
||||
private static func Bool(_ key: String) -> Bool { suite.bool(forKey: key) }
|
||||
private static func Bool(_ key: String, _ val: Bool) { suite.set(val, forKey: key) }
|
||||
private static func Str(_ key: String) -> String? { suite.string(forKey: key) }
|
||||
private static func Str(_ key: String, _ val: String?) { suite.set(val, forKey: key) }
|
||||
private static func Obj(_ key: String) -> Any? { suite.object(forKey: key) }
|
||||
private static func Obj(_ key: String, _ val: Any?) { suite.set(val, forKey: key) }
|
||||
|
||||
static func registerDefaults() {
|
||||
suite.register(defaults: [
|
||||
"RecordingReminderEnabled" : true,
|
||||
"contextAnalyisCoOccurrenceTime" : 5,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Tutorial
|
||||
|
||||
extension Prefs {
|
||||
enum DidShowTutorial {
|
||||
static var Welcome: Bool {
|
||||
get { Prefs.Bool("didShowTutorialAppWelcome") }
|
||||
set { Prefs.Bool(newValue, "didShowTutorialAppWelcome") }
|
||||
set { Prefs.Bool("didShowTutorialAppWelcome", newValue) }
|
||||
}
|
||||
static var Recordings: Bool {
|
||||
get { Prefs.Bool("didShowTutorialRecordings") }
|
||||
set { Prefs.Bool(newValue, "didShowTutorialRecordings") }
|
||||
}
|
||||
}
|
||||
enum ContextAnalyis {
|
||||
static var CoOccurrenceTime: Int? {
|
||||
get { Prefs.Any("contextAnalyisCoOccurrenceTime") as? Int }
|
||||
set { Prefs.Any(newValue, "contextAnalyisCoOccurrenceTime") }
|
||||
set { Prefs.Bool("didShowTutorialRecordings", newValue) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Date Filter
|
||||
|
||||
enum DateFilterKind: Int {
|
||||
case Off = 0, LastXMin = 1, ABRange = 2;
|
||||
}
|
||||
enum DateFilterOrderBy: Int {
|
||||
case Date = 0, Name = 1, Count = 2;
|
||||
}
|
||||
|
||||
extension Prefs {
|
||||
enum DateFilter {
|
||||
static var Kind: DateFilterKind {
|
||||
get { DateFilterKind(rawValue: Prefs.Int("dateFilterType"))! }
|
||||
set { Prefs.Int(newValue.rawValue, "dateFilterType") }
|
||||
set { Prefs.Int("dateFilterType", newValue.rawValue) }
|
||||
}
|
||||
/// Default: `0` (disabled)
|
||||
static var LastXMin: Int {
|
||||
get { Prefs.Int("dateFilterLastXMin") }
|
||||
set { Prefs.Int(newValue, "dateFilterLastXMin") }
|
||||
set { Prefs.Int("dateFilterLastXMin", newValue) }
|
||||
}
|
||||
/// Default: `nil` (disabled)
|
||||
static var RangeA: Timestamp? {
|
||||
get { Prefs.Any("dateFilterRangeA") as? Timestamp }
|
||||
set { Prefs.Any(newValue, "dateFilterRangeA") }
|
||||
get { Prefs.Obj("dateFilterRangeA") as? Timestamp }
|
||||
set { Prefs.Obj("dateFilterRangeA", newValue) }
|
||||
}
|
||||
/// Default: `nil` (disabled)
|
||||
static var RangeB: Timestamp? {
|
||||
get { Prefs.Any("dateFilterRangeB") as? Timestamp }
|
||||
set { Prefs.Any(newValue, "dateFilterRangeB") }
|
||||
get { Prefs.Obj("dateFilterRangeB") as? Timestamp }
|
||||
set { Prefs.Obj("dateFilterRangeB", newValue) }
|
||||
}
|
||||
/// default: `.Date`
|
||||
static var OrderBy: DateFilterOrderBy {
|
||||
get { DateFilterOrderBy(rawValue: Prefs.Int("dateFilterOderType"))! }
|
||||
set { Prefs.Int(newValue.rawValue, "dateFilterOderType") }
|
||||
set { Prefs.Int("dateFilterOderType", newValue.rawValue) }
|
||||
}
|
||||
/// default: `false` (Desc)
|
||||
static var OrderAsc: Bool {
|
||||
get { Prefs.Bool("dateFilterOderAsc") }
|
||||
set { Prefs.Bool(newValue, "dateFilterOderAsc") }
|
||||
set { Prefs.Bool("dateFilterOderAsc", newValue) }
|
||||
}
|
||||
|
||||
/// - Returns: Timestamp restriction depending on current selected date filter.
|
||||
@@ -69,9 +92,31 @@ enum Prefs {
|
||||
}
|
||||
}
|
||||
}
|
||||
enum DateFilterKind: Int {
|
||||
case Off = 0, LastXMin = 1, ABRange = 2;
|
||||
|
||||
|
||||
// MARK: - ContextAnalyis
|
||||
|
||||
extension Prefs {
|
||||
enum ContextAnalyis {
|
||||
static var CoOccurrenceTime: Int {
|
||||
get { Prefs.Int("contextAnalyisCoOccurrenceTime") }
|
||||
set { Prefs.Int("contextAnalyisCoOccurrenceTime", newValue) }
|
||||
}
|
||||
}
|
||||
}
|
||||
enum DateFilterOrderBy: Int {
|
||||
case Date = 0, Name = 1, Count = 2;
|
||||
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
extension Prefs {
|
||||
enum RecordingReminder {
|
||||
static var Enabled: Bool {
|
||||
get { Prefs.Bool("RecordingReminderEnabled") }
|
||||
set { Prefs.Bool("RecordingReminderEnabled", newValue) }
|
||||
}
|
||||
static var Sound: String {
|
||||
get { Prefs.Str("RecordingReminderSound") ?? "#default" }
|
||||
set { Prefs.Str("RecordingReminderSound", newValue) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,79 @@ enum PrefsShared {
|
||||
private static var suite: UserDefaults { UserDefaults(suiteName: "group.de.uni-bamberg.psi.AppCheck")! }
|
||||
|
||||
private static func Int(_ key: String) -> Int { suite.integer(forKey: key) }
|
||||
private static func Int(_ val: Int, _ key: String) { suite.set(val, forKey: key) }
|
||||
// private static func Obj(_ key: String) -> Any? { suite.object(forKey: key) }
|
||||
// private static func Obj(_ val: Any?, _ key: String) { suite.set(val, forKey: key) }
|
||||
private static func Int(_ key: String, _ val: Int) { suite.set(val, forKey: key); suite.synchronize() }
|
||||
private static func Bool(_ key: String) -> Bool { suite.bool(forKey: key) }
|
||||
private static func Bool(_ key: String, _ val: Bool) { suite.set(val, forKey: key); suite.synchronize() }
|
||||
private static func Str(_ key: String) -> String? { suite.string(forKey: key) }
|
||||
private static func Str(_ key: String, _ val: String?) { suite.set(val, forKey: key); suite.synchronize() }
|
||||
|
||||
static func registerDefaults() {
|
||||
suite.register(defaults: [
|
||||
"RestartReminderEnabled" : true,
|
||||
"RestartReminderWithText" : true,
|
||||
"RestartReminderWithBadge" : true,
|
||||
"ConnectionAlertsListsElse" : true,
|
||||
])
|
||||
}
|
||||
|
||||
static var AutoDeleteLogsDays: Int {
|
||||
get { Int("AutoDeleteLogsDays") }
|
||||
set { Int(newValue, "AutoDeleteLogsDays"); suite.synchronize() }
|
||||
set { Int("AutoDeleteLogsDays", newValue) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Notifications
|
||||
|
||||
extension PrefsShared {
|
||||
enum RestartReminder {
|
||||
static var Enabled: Bool {
|
||||
get { PrefsShared.Bool("RestartReminderEnabled") }
|
||||
set { PrefsShared.Bool("RestartReminderEnabled", newValue) }
|
||||
}
|
||||
static var WithText: Bool {
|
||||
get { PrefsShared.Bool("RestartReminderWithText") }
|
||||
set { PrefsShared.Bool("RestartReminderWithText", newValue) }
|
||||
}
|
||||
static var WithBadge: Bool {
|
||||
get { PrefsShared.Bool("RestartReminderWithBadge") }
|
||||
set { PrefsShared.Bool("RestartReminderWithBadge", newValue) }
|
||||
}
|
||||
static var Sound: String {
|
||||
get { PrefsShared.Str("RestartReminderSound") ?? "#default" }
|
||||
set { PrefsShared.Str("RestartReminderSound", newValue) }
|
||||
}
|
||||
}
|
||||
enum ConnectionAlerts {
|
||||
static var Enabled: Bool {
|
||||
get { PrefsShared.Bool("ConnectionAlertsEnabled") }
|
||||
set { PrefsShared.Bool("ConnectionAlertsEnabled", newValue) }
|
||||
}
|
||||
static var Sound: String {
|
||||
get { PrefsShared.Str("ConnectionAlertsSound") ?? "#default" }
|
||||
set { PrefsShared.Str("ConnectionAlertsSound", newValue) }
|
||||
}
|
||||
static var ExcludeMode: Bool {
|
||||
get { PrefsShared.Bool("ConnectionAlertsExcludeMode") }
|
||||
set { PrefsShared.Bool("ConnectionAlertsExcludeMode", newValue) }
|
||||
}
|
||||
enum Lists {
|
||||
static var CustomA: Bool {
|
||||
get { PrefsShared.Bool("ConnectionAlertsListsCustomA") }
|
||||
set { PrefsShared.Bool("ConnectionAlertsListsCustomA", newValue) }
|
||||
}
|
||||
static var CustomB: Bool {
|
||||
get { PrefsShared.Bool("ConnectionAlertsListsCustomB") }
|
||||
set { PrefsShared.Bool("ConnectionAlertsListsCustomB", newValue) }
|
||||
}
|
||||
static var Blocked: Bool {
|
||||
get { PrefsShared.Bool("ConnectionAlertsListsBlocked") }
|
||||
set { PrefsShared.Bool("ConnectionAlertsListsBlocked", newValue) }
|
||||
}
|
||||
static var Else: Bool {
|
||||
get { PrefsShared.Bool("ConnectionAlertsListsElse") }
|
||||
set { PrefsShared.Bool("ConnectionAlertsListsElse", newValue) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
153
main/Common Classes/PushNotification.swift
Normal file
153
main/Common Classes/PushNotification.swift
Normal file
@@ -0,0 +1,153 @@
|
||||
import UserNotifications
|
||||
|
||||
struct PushNotification {
|
||||
|
||||
enum Identifier: String {
|
||||
case YouShallRecordMoreReminder
|
||||
case CantStopMeNowReminder
|
||||
case RestInPeaceTombstone
|
||||
case AllConnectionAlertNotifications
|
||||
}
|
||||
|
||||
static func allowed(_ closure: @escaping (NotificationRequestState) -> Void) {
|
||||
guard #available(iOS 10, *) else { return }
|
||||
|
||||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||
let state = NotificationRequestState(settings.authorizationStatus)
|
||||
DispatchQueue.main.async {
|
||||
closure(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Available in iOS 12+
|
||||
static func requestProvisionalOrDoNothing(_ closure: @escaping (Bool) -> Void) {
|
||||
guard #available(iOS 12, *) else { return closure(false) }
|
||||
|
||||
let opt: UNAuthorizationOptions = [.alert, .sound, .badge, .provisional, .providesAppNotificationSettings]
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: opt) { granted, _ in
|
||||
DispatchQueue.main.async {
|
||||
closure(granted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func requestAuthorization(_ closure: @escaping (Bool) -> Void) {
|
||||
guard #available(iOS 10, *) else { return }
|
||||
|
||||
var opt: UNAuthorizationOptions = [.alert, .sound, .badge]
|
||||
if #available(iOS 12, *) {
|
||||
opt.formUnion(.providesAppNotificationSettings)
|
||||
}
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: opt) { granted, _ in
|
||||
DispatchQueue.main.async {
|
||||
closure(granted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func hasPending(_ ident: Identifier, _ closure: @escaping (Bool) -> Void) {
|
||||
guard #available(iOS 10, *) else { return }
|
||||
|
||||
UNUserNotificationCenter.current().getPendingNotificationRequests {
|
||||
let hasIt = $0.contains { $0.identifier == ident.rawValue }
|
||||
DispatchQueue.main.async {
|
||||
closure(hasIt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func cancel(_ ident: Identifier, keepDelivered: Bool = false) {
|
||||
guard #available(iOS 10, *) else { return }
|
||||
|
||||
let center = UNUserNotificationCenter.current()
|
||||
guard ident != .AllConnectionAlertNotifications else {
|
||||
// remove all connection alert notifications while
|
||||
// keeping general purpose reminder notifications
|
||||
center.getDeliveredNotifications {
|
||||
var list = $0.map { $0.request.identifier }
|
||||
list.removeAll { !$0.contains(".") } // each domain (or IP) has a dot
|
||||
center.removeDeliveredNotifications(withIdentifiers: list)
|
||||
// no need to do the same for pending since con-alerts are always immediate
|
||||
}
|
||||
return
|
||||
}
|
||||
center.removePendingNotificationRequests(withIdentifiers: [ident.rawValue])
|
||||
if !keepDelivered {
|
||||
center.removeDeliveredNotifications(withIdentifiers: [ident.rawValue])
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 10.0, *)
|
||||
static func schedule(_ ident: Identifier, content: UNNotificationContent, trigger: UNNotificationTrigger? = nil, waitUntilDone: Bool = false) {
|
||||
schedule(ident.rawValue, content: content, trigger: trigger, waitUntilDone: waitUntilDone)
|
||||
}
|
||||
|
||||
@available(iOS 10.0, *)
|
||||
static func schedule(_ ident: String, content: UNNotificationContent, trigger: UNNotificationTrigger? = nil, waitUntilDone: Bool = false) {
|
||||
let req = UNNotificationRequest(identifier: ident, content: content, trigger: trigger)
|
||||
waitUntilDone ? req.pushAndWait() : req.push()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Reminder Alerts
|
||||
|
||||
extension PushNotification {
|
||||
/// Auto-check preferences whether `withText` is set, then schedule notification to 5 min in the future.
|
||||
static func scheduleRestartReminderBanner() {
|
||||
guard #available(iOS 10, *), PrefsShared.RestartReminder.WithText else { return }
|
||||
|
||||
schedule(.CantStopMeNowReminder,
|
||||
content: .make("AppCheck disabled",
|
||||
body: "AppCheck can't monitor network traffic because VPN has stopped.",
|
||||
sound: .from(string: PrefsShared.RestartReminder.Sound)),
|
||||
trigger: .make(Date(timeIntervalSinceNow: 5 * 60)),
|
||||
waitUntilDone: true)
|
||||
}
|
||||
|
||||
/// Auto-check preferences whether `withBadge` is set, then post badge immediatelly.
|
||||
/// - Parameter on: If `true`, set `1` on app icon. If `false`, remove badge on app icon.
|
||||
static func scheduleRestartReminderBadge(on: Bool) {
|
||||
guard #available(iOS 10, *), PrefsShared.RestartReminder.WithBadge else { return }
|
||||
|
||||
schedule(.RestInPeaceTombstone, content: .makeBadge(on ? 1 : 0), waitUntilDone: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Connection Alerts
|
||||
|
||||
extension PushNotification {
|
||||
static private let queue = ThrottledBatchQueue<String>(0.5, using: .init(label: "PSINotificationQueue", qos: .default, target: .global()))
|
||||
|
||||
/// Post new notification with given domain name. If notification already exists, increase occurrence count.
|
||||
/// - Parameter domain: Used in the description and as notification identifier.
|
||||
@available(iOS 10.0, *)
|
||||
static func scheduleConnectionAlert(_ domain: String, sound: UNNotificationSound?) {
|
||||
queue.addDelayed(domain) { batch in
|
||||
let groupSum = batch.reduce(into: [:]) { $0[$1] = ($0[$1] ?? 0) + 1 }
|
||||
scheduleConnectionAlertMulti(groupSum, sound: sound)
|
||||
}
|
||||
}
|
||||
|
||||
/// Internal method to post a batch of counted domains.
|
||||
@available(iOS 10.0, *)
|
||||
static private func scheduleConnectionAlertMulti(_ group: [String: Int], sound: UNNotificationSound?) {
|
||||
UNUserNotificationCenter.current().getDeliveredNotifications { delivered in
|
||||
for (dom, count) in group {
|
||||
let num: Int
|
||||
if let prev = delivered.first(where: { $0.request.identifier == dom })?.request.content {
|
||||
if let p = prev.body.split(separator: "×").first, let i = Int(p) {
|
||||
num = count + i
|
||||
} else {
|
||||
num = count + 1
|
||||
}
|
||||
} else {
|
||||
num = count
|
||||
}
|
||||
schedule(dom, content: .make("DNS connection", body: num > 1 ? "\(num)× \(dom)" : dom, sound: sound))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
main/Common Classes/PushNotificationAppOnly.swift
Normal file
28
main/Common Classes/PushNotificationAppOnly.swift
Normal file
@@ -0,0 +1,28 @@
|
||||
import UserNotifications
|
||||
|
||||
extension PushNotification {
|
||||
static func scheduleRecordingReminder(force: Bool) {
|
||||
if force {
|
||||
scheduleRecordingReminder()
|
||||
} else {
|
||||
hasPending(.YouShallRecordMoreReminder) {
|
||||
if !$0 { scheduleRecordingReminder() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func scheduleRecordingReminder() {
|
||||
guard #available(iOS 10, *) else { return }
|
||||
|
||||
let now = Timestamp.now()
|
||||
var next = RecordingsDB.lastTimestamp() ?? (now - 1)
|
||||
while next < now {
|
||||
next += .days(14)
|
||||
}
|
||||
schedule(.YouShallRecordMoreReminder,
|
||||
content: .make("Start new recording",
|
||||
body: "It's been a while since your last recording …",
|
||||
sound: .from(string: Prefs.RecordingReminder.Sound)),
|
||||
trigger: .make(Date(next)))
|
||||
}
|
||||
}
|
||||
31
main/Common Classes/ThrottledBatchQueue.swift
Normal file
31
main/Common Classes/ThrottledBatchQueue.swift
Normal file
@@ -0,0 +1,31 @@
|
||||
import Foundation
|
||||
|
||||
class ThrottledBatchQueue<T> {
|
||||
private var cache: [T] = []
|
||||
private var scheduled: Bool = false
|
||||
private let queue: DispatchQueue
|
||||
private let delay: Double
|
||||
|
||||
init(_ delay: Double, using queue: DispatchQueue) {
|
||||
self.queue = queue
|
||||
self.delay = delay
|
||||
}
|
||||
|
||||
func addDelayed(_ elem: T, afterDelay closure: @escaping ([T]) -> Void) {
|
||||
queue.sync {
|
||||
cache.append(elem)
|
||||
guard !scheduled else {
|
||||
return
|
||||
}
|
||||
scheduled = true
|
||||
queue.asyncAfter(deadline: .now() + delay) {
|
||||
let aCopy = self.cache
|
||||
self.cache.removeAll(keepingCapacity: true)
|
||||
self.scheduled = false
|
||||
DispatchQueue.main.async {
|
||||
closure(aCopy)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user