First version with app notifications

This commit is contained in:
relikd
2020-07-26 22:32:11 +02:00
parent 88a52fb92c
commit a2b0f311d5
37 changed files with 2192 additions and 469 deletions

View File

@@ -0,0 +1,97 @@
import UIKit
import AudioToolbox
protocol NotificationSoundChangedDelegate {
/// Use `#mute` to disable sounds and `#default` to use default notification sound.
func notificationSoundCurrent() -> String
/// Called every time the user changes selection
func notificationSoundChanged(filename: String, title: String)
}
class TVCChooseAlertTone: UITableViewController {
var delegate: NotificationSoundChangedDelegate!
private lazy var selected: String = delegate.notificationSoundCurrent()
private func playTone(_ name: String) {
switch name {
case "#mute": return // No Sound
case "#default": AudioServicesPlayAlertSound(1315) // Default sound
default:
guard let url = Bundle.main.url(forResource: name, withExtension: "caf") else {
preconditionFailure("Something went wrong. Sound file \(name).caf does not exist.")
}
var soundId: SystemSoundID = 0
AudioServicesCreateSystemSoundID(url as CFURL, &soundId)
AudioServicesAddSystemSoundCompletion(soundId, nil, nil, { id, _ -> Void in
AudioServicesDisposeSystemSoundID(id)
}, nil)
AudioServicesPlayAlertSound(soundId)
}
}
// MARK: Table View Delegate
override func numberOfSections(in _: UITableView) -> Int {
AvailableSounds.count
}
override func tableView(_: UITableView, numberOfRowsInSection section: Int) -> Int {
AvailableSounds[section].count
}
override func tableView(_: UITableView, titleForHeaderInSection section: Int) -> String? {
section == 1 ? "AppCheck" : nil
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SettingsAlertToneCell")!
let src = AvailableSounds[indexPath.section][indexPath.row]
cell.textLabel?.text = src.title
cell.accessoryType = (src.file == selected) ? .checkmark : .none
return cell
}
override func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
let src = AvailableSounds[indexPath.section][indexPath.row]
selected = src.file
tableView.reloadData() // re-apply checkmarks
playTone(selected)
delegate.notificationSoundChanged(filename: src.file, title: src.title)
return nil
}
}
// MARK: - Sounds Data Source
// afconvert input.aiff output.caf -d ima4 -f caff -v
fileprivate let AvailableSounds: [[(title: String, file: String)]] = [
[ // System sounds
("None", "#mute"),
("Default", "#default")
], [ // AppCheck sounds
("Clock", "clock"),
("Drum 1", "drum1"),
("Drum 2", "drum2"),
("Plop 1", "plop1"),
("Plop 2", "plop2"),
("Snap 1", "snap1"),
("Snap 2", "snap2"),
("Typewriter 1", "typewriter1"),
("Typewriter 2", "typewriter2"),
("Wood 1", "wood1"),
("Wood 2", "wood2")
]
]
func AlertSoundTitle(for filename: String) -> String {
for section in AvailableSounds {
for row in section {
if row.file == filename {
return row.title
}
}
}
return ""
}

View File

@@ -0,0 +1,135 @@
import UIKit
class TVCConnectionAlerts: UITableViewController {
@IBOutlet var showNotifications: UISwitch!
@IBOutlet var cellSound: UITableViewCell!
@IBOutlet var listsCustomA: UITableViewCell!
@IBOutlet var listsCustomB: UITableViewCell!
override func viewDidLoad() {
super.viewDidLoad()
cascadeEnableConnAlert(PrefsShared.ConnectionAlerts.Enabled)
cellSound.detailTextLabel?.text = AlertSoundTitle(for: PrefsShared.ConnectionAlerts.Sound)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let (_, _, custA, custB) = DomainFilter.counts()
listsCustomA.detailTextLabel?.text = "\(custA) Domains"
listsCustomB.detailTextLabel?.text = "\(custB) Domains"
}
private func cascadeEnableConnAlert(_ flag: Bool) {
showNotifications.isOn = flag
// en/disable related controls
}
private func getListSelected(_ index: Int) -> Bool {
switch index {
case 0: return PrefsShared.ConnectionAlerts.Lists.Blocked
case 1: return PrefsShared.ConnectionAlerts.Lists.CustomA
case 2: return PrefsShared.ConnectionAlerts.Lists.CustomB
case 3: return PrefsShared.ConnectionAlerts.Lists.Else
default: preconditionFailure()
}
}
private func setListSelected(_ index: Int, _ value: Bool) {
switch index {
case 0: PrefsShared.ConnectionAlerts.Lists.Blocked = value
case 1: PrefsShared.ConnectionAlerts.Lists.CustomA = value
case 2: PrefsShared.ConnectionAlerts.Lists.CustomB = value
case 3: PrefsShared.ConnectionAlerts.Lists.Else = value
default: preconditionFailure()
}
}
// MARK: - Toggles
@IBAction private func toggleShowNotifications(_ sender: UISwitch) {
PrefsShared.ConnectionAlerts.Enabled = sender.isOn
cascadeEnableConnAlert(sender.isOn)
GlassVPN.send(.notificationSettingsChanged())
if sender.isOn {
PushNotification.requestAuthorization { granted in
if !granted {
NotificationsDisabledAlert(presentIn: self)
self.cascadeEnableConnAlert(false)
}
}
} else {
PushNotification.cancel(.AllConnectionAlertNotifications)
}
}
// MARK: - Table View Delegate
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = super.tableView(tableView, cellForRowAt: indexPath)
let checked: Bool
switch indexPath.section {
case 1: // mode selection
checked = (indexPath.row == (PrefsShared.ConnectionAlerts.ExcludeMode ? 1 : 0))
case 2: // include & exclude lists
checked = getListSelected(indexPath.row)
default: return cell // process only checkmarked cells
}
cell.accessoryType = checked ? .checkmark : .none
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch indexPath.section {
case 1: // mode selection
PrefsShared.ConnectionAlerts.ExcludeMode = indexPath.row == 1
tableView.reloadSections(.init(integer: 2), with: .none)
case 2: // include & exclude lists
let prev = tableView.cellForRow(at: indexPath)?.accessoryType == .checkmark
setListSelected(indexPath.row, !prev)
default: return // process only checkmarked cells
}
tableView.deselectRow(at: indexPath, animated: true)
tableView.reloadSections(.init(integer: indexPath.section), with: .none)
GlassVPN.send(.notificationSettingsChanged())
}
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if section == 2 {
return PrefsShared.ConnectionAlerts.ExcludeMode ? "Exclude All" : "Include All"
}
return super.tableView(tableView, titleForHeaderInSection: section)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let dest = segue.destination as? TVCFilter {
switch segue.identifier {
case "segueFilterListCustomA":
dest.navigationItem.title = "Custom List A"
dest.currentFilter = .customA
case "segueFilterListCustomB":
dest.navigationItem.title = "Custom List B"
dest.currentFilter = .customB
default:
break
}
} else if let tvc = segue.destination as? TVCChooseAlertTone {
tvc.delegate = self
}
}
}
// MARK: - Sound Selection
extension TVCConnectionAlerts: NotificationSoundChangedDelegate {
func notificationSoundCurrent() -> String {
PrefsShared.ConnectionAlerts.Sound
}
func notificationSoundChanged(filename: String, title: String) {
cellSound.detailTextLabel?.text = title
PrefsShared.ConnectionAlerts.Sound = filename
GlassVPN.send(.notificationSettingsChanged())
}
}

View File

@@ -25,14 +25,10 @@ class TVCFilter: UITableViewController, EditActionsRemove {
}
@IBAction private func addNewFilter() {
let desc: String
switch currentFilter {
case .blocked: desc = "Enter the domain name you wish to block."
case .ignored: desc = "Enter the domain name you wish to ignore."
default: return
}
let alert = AskAlert(title: "Create new filter", text: desc, buttonText: "Add") {
guard let dom = $0.textFields?.first?.text else {
let alert = AskAlert(title: "Create new filter",
text: "Enter the domain name you wish to add.",
buttonText: "Add") {
guard let dom = $0.textFields?.first?.text?.lowercased() else {
return
}
guard dom.contains("."), !dom.isKnownSLD() else {

View File

@@ -0,0 +1,154 @@
import UIKit
class TVCReminderAlerts: UITableViewController {
@IBOutlet var restartAllow: UISwitch!
@IBOutlet var restartAllowNotify: UISwitch!
@IBOutlet var restartAllowBadge: UISwitch!
@IBOutlet var restartSound: UITableViewCell!
@IBOutlet var recordingAllow: UISwitch!
@IBOutlet var recordingSound: UITableViewCell!
private enum ReminderCellType { case Restart, Recording }
private var selectedSound: ReminderCellType = .Restart
override func viewDidLoad() {
super.viewDidLoad()
restartAllowNotify.isOn = PrefsShared.RestartReminder.WithText
restartAllowBadge.isOn = PrefsShared.RestartReminder.WithBadge
restartSound.detailTextLabel?.text = AlertSoundTitle(for: PrefsShared.RestartReminder.Sound)
recordingSound.detailTextLabel?.text = AlertSoundTitle(for: Prefs.RecordingReminder.Sound)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
readNotificationState { (allowStart, allowRecord, isProvisional) in
self.cascadeEnableRestart(allowStart && !isProvisional)
self.recordingAllow.isOn = (allowRecord && !isProvisional)
self.setIndicateProvisional(isProvisional)
}
}
private func readNotificationState(_ closure: @escaping (Bool, Bool, Bool) -> Void) {
let en1 = PrefsShared.RestartReminder.Enabled
let en2 = Prefs.RecordingReminder.Enabled
closure(en1, en2, false)
guard en1 || en2 else { return }
PushNotification.allowed { state in
switch state {
case .NotDetermined, .Denied: closure(false, false, false)
case .Authorized, .Provisional: closure(en1, en2, state == .Provisional)
}
}
}
private func cascadeEnableRestart(_ flag: Bool) {
restartAllow.isOn = flag
restartAllowNotify.isEnabled = flag
restartAllowBadge.isEnabled = flag
}
private func setIndicateProvisional(_ flag: Bool) {
if flag {
restartAllow.thumbTintColor = .systemGreen
recordingAllow.thumbTintColor = .systemGreen
} else {
// thumb tint is only set in provisional mode
if restartAllow.thumbTintColor <-? nil { restartAllow.isOn = true }
if recordingAllow.thumbTintColor <-? nil { recordingAllow.isOn = true }
}
}
private func updateBadge() {
let flag = (restartAllow.isOn && restartAllowBadge.isOn && GlassVPN.state != .on)
UIApplication.shared.applicationIconBadgeNumber = flag ? 1 : 0
}
}
// MARK: - Toggles
extension TVCReminderAlerts {
@IBAction private func toggleAllowRestartReminder(_ sender: UISwitch) {
PrefsShared.RestartReminder.Enabled = sender.isOn
cascadeEnableRestart(sender.isOn)
updateBadge()
if sender.isOn {
askAuthorization {}
} else {
PushNotification.cancel(.CantStopMeNowReminder)
}
}
@IBAction private func toggleAllowRestartNotify(_ sender: UISwitch) {
PrefsShared.RestartReminder.WithText = sender.isOn
if !sender.isOn {
PushNotification.cancel(.CantStopMeNowReminder)
}
}
@IBAction private func toggleAllowRestartBadge(_ sender: UISwitch) {
PrefsShared.RestartReminder.WithBadge = sender.isOn
updateBadge()
}
@IBAction private func toggleAllowRecordingReminder(_ sender: UISwitch) {
Prefs.RecordingReminder.Enabled = sender.isOn
if sender.isOn {
askAuthorization { PushNotification.scheduleRecordingReminder(force: false) }
} else {
PushNotification.cancel(.YouShallRecordMoreReminder)
}
}
private func askAuthorization(_ closure: @escaping () -> Void) {
setIndicateProvisional(false)
PushNotification.requestAuthorization { granted in
if granted {
closure()
} else {
NotificationsDisabledAlert(presentIn: self)
self.cascadeEnableRestart(false)
self.recordingAllow.isOn = false
}
}
}
}
// MARK: - Sound Selection
extension TVCReminderAlerts: NotificationSoundChangedDelegate {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let tvc = segue.destination as? TVCChooseAlertTone {
switch segue.identifier {
case "segueSoundRestartReminder": selectedSound = .Restart
case "segueSoundRecordingReminder": selectedSound = .Recording
default: preconditionFailure()
}
tvc.delegate = self
}
}
func notificationSoundCurrent() -> String {
switch selectedSound {
case .Restart: return PrefsShared.RestartReminder.Sound
case .Recording: return Prefs.RecordingReminder.Sound
}
}
func notificationSoundChanged(filename: String, title: String) {
switch selectedSound {
case .Restart:
restartSound.detailTextLabel?.text = title
PrefsShared.RestartReminder.Sound = filename
case .Recording:
recordingSound.detailTextLabel?.text = title
Prefs.RecordingReminder.Sound = filename
if Prefs.RecordingReminder.Enabled {
PushNotification.scheduleRecordingReminder(force: true)
}
}
}
}

View File

@@ -6,36 +6,63 @@ class TVCSettings: UITableViewController {
@IBOutlet var cellDomainsIgnored: UITableViewCell!
@IBOutlet var cellDomainsBlocked: UITableViewCell!
@IBOutlet var cellPrivacyAutoDelete: UITableViewCell!
@IBOutlet var cellNotificationReminder: UITableViewCell!
@IBOutlet var cellNotificationConnectionAlert: UITableViewCell!
override func viewDidLoad() {
super.viewDidLoad()
reloadToggleState()
reloadDataSource()
NotifyVPNStateChanged.observe(call: #selector(reloadToggleState), on: self)
NotifyDNSFilterChanged.observe(call: #selector(reloadDataSource), on: self)
reloadVPNState()
reloadLoggingFilterUI()
reloadPrivacyUI()
NotifyVPNStateChanged.observe(call: #selector(reloadVPNState), on: self)
NotifyDNSFilterChanged.observe(call: #selector(reloadLoggingFilterUI), on: self)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
reloadNotificationState()
}
// MARK: - VPN Proxy Settings
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// FIXME: there is a lag between tap and open when run on device
if let cell = tableView.cellForRow(at: indexPath), cell === cellPrivacyAutoDelete {
openAutoDeletePicker()
}
}
func openRestartVPNSettings() { scrollToSection(0, animated: false) }
func openNotificationSettings() { scrollToSection(2, animated: false) }
private func scrollToSection(_ section: Int, animated: Bool) {
tableView.scrollToRow(at: .init(row: 0, section: section), at: .top, animated: animated)
}
}
// MARK: - VPN Proxy Settings
extension TVCSettings {
@objc private func reloadVPNState() {
vpnToggle.isOn = (GlassVPN.state != .off)
vpnToggle.onTintColor = (GlassVPN.state == .inbetween ? .systemYellow : nil)
UIApplication.shared.applicationIconBadgeNumber =
!vpnToggle.isOn &&
PrefsShared.RestartReminder.Enabled &&
PrefsShared.RestartReminder.WithBadge ? 1 : 0
}
@IBAction private func toggleVPNProxy(_ sender: UISwitch) {
GlassVPN.setEnabled(sender.isOn)
}
@objc private func reloadToggleState() {
vpnToggle.isOn = (GlassVPN.state != .off)
vpnToggle.onTintColor = (GlassVPN.state == .inbetween ? .systemYellow : nil)
}
// MARK: - Logging Filter
@objc private func reloadDataSource() {
let (blocked, ignored) = DomainFilter.counts()
}
// MARK: - Logging Filter
extension TVCSettings {
@objc private func reloadLoggingFilterUI() {
let (blocked, ignored, _, _) = DomainFilter.counts()
cellDomainsIgnored.detailTextLabel?.text = "\(ignored) Domains"
cellDomainsBlocked.detailTextLabel?.text = "\(blocked) Domains"
let (one, two) = autoDeleteSelection([1, 7, 31])
cellPrivacyAutoDelete.detailTextLabel?.text = autoDeleteString(one, unit: two)
}
override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) {
@@ -65,38 +92,84 @@ class TVCSettings: UITableViewController {
break
}
}
}
// MARK: - Privacy
extension TVCSettings {
private func reloadPrivacyUI() {
let (num, unit) = getAutoDeleteSelection([1, 7, 31])
let str: String
switch num {
case 0: str = "Never"
case 1: str = "1 \(["Day", "Week", "Month"][unit])"
default: str = "\(num) \(["Days", "Weeks", "Months"][unit])"
}
cellPrivacyAutoDelete.detailTextLabel?.text = str
}
private func getAutoDeleteSelection(_ multiplier: [Int]) -> (Int, Int) {
let current = PrefsShared.AutoDeleteLogsDays
let snd = multiplier.lastIndex { current % $0 == 0 }! // make sure 1 is in list
return (current / multiplier[snd], snd)
}
// MARK: - Privacy
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
// FIXME: there is a lag between tap and open when run on device
if let cell = tableView.cellForRow(at: indexPath), cell === cellPrivacyAutoDelete {
let multiplier = [1, 7, 31]
let (one, two) = autoDeleteSelection(multiplier)
let picker = DurationPickerAlert(
title: "Auto-delete logs",
detail: "Warning: Logs older than the selected interval are deleted immediately! " +
"Logs are also deleted on each app launch, and periodically in the background as long as the VPN is running.",
options: [(0...30).map{"\($0)"}, ["Days", "Weeks", "Months"]],
widths: [0.4, 0.6])
picker.pickerView.setSelection([min(30, one), two])
picker.present(in: self) { _, idx in
cell.detailTextLabel?.text = autoDeleteString(idx[0], unit: idx[1])
let asDays = idx[0] * multiplier[idx[1]]
PrefsShared.AutoDeleteLogsDays = asDays
if !GlassVPN.send(.autoDelete(after: asDays)) {
// if VPN isn't active, fallback to immediate local delete
TheGreatDestroyer.deleteLogs(olderThan: asDays)
}
private func openAutoDeletePicker() {
let multiplier = [1, 7, 31]
let (one, two) = getAutoDeleteSelection(multiplier)
let picker = DurationPickerAlert(
title: "Auto-delete logs",
detail: "Warning: Logs older than the selected interval are deleted immediately! " +
"Logs are also deleted on each app launch, and periodically in the background as long as the VPN is running.",
options: [(0...30).map{"\($0)"}, ["Days", "Weeks", "Months"]],
widths: [0.4, 0.6])
picker.pickerView.setSelection([min(30, one), two])
picker.present(in: self) { _, idx in
let asDays = idx[0] * multiplier[idx[1]]
PrefsShared.AutoDeleteLogsDays = asDays
self.reloadPrivacyUI()
if !GlassVPN.send(.autoDelete(after: asDays)) {
// if VPN isn't active, fallback to immediate local delete
TheGreatDestroyer.deleteLogs(olderThan: asDays)
}
}
}
}
// MARK: - Notification Settings
extension TVCSettings {
private func reloadNotificationState() {
let lbl1 = cellNotificationReminder.detailTextLabel
let lbl2 = cellNotificationConnectionAlert.detailTextLabel
readNotificationState { (realAllowed, provisional) in
lbl1?.text = provisional ? "Enabled" : "Disabled"
lbl2?.text = realAllowed ? "Enabled" : "Disabled"
}
}
// MARK: - Reset Settings
private func readNotificationState(_ closure: @escaping (_ all: Bool, _ prov: Bool) -> Void) {
let en1 = PrefsShared.ConnectionAlerts.Enabled
let en2 = Prefs.RecordingReminder.Enabled || PrefsShared.RestartReminder.Enabled
closure(en1, en2)
guard en1 || en2 else { return }
PushNotification.allowed { state in
switch state {
case .NotDetermined, .Denied: closure(false, false)
case .Authorized: closure(en1, en2)
case .Provisional: closure(false, en2)
}
}
}
}
// MARK: - Reset Settings
extension TVCSettings {
@IBAction private func resetTutorialAlerts(_ sender: UIButton) {
Prefs.DidShowTutorial.Welcome = false
Prefs.DidShowTutorial.Recordings = false
@@ -112,10 +185,12 @@ class TVCSettings: UITableViewController {
TheGreatDestroyer.deleteAllLogs()
}.presentIn(self)
}
// MARK: - Advanced
}
// MARK: - Advanced
extension TVCSettings {
@IBAction private func exportDB() {
AppDB?.vacuum()
let sheet = UIActivityViewController(activityItems: [URL.internalDB()], applicationActivities: nil)
@@ -130,24 +205,3 @@ class TVCSettings: UITableViewController {
return nil
}
}
// -------------------------------
// |
// | MARK: - Helper methods
// |
// -------------------------------
private func autoDeleteSelection(_ multiplier: [Int]) -> (Int, Int) {
let current = PrefsShared.AutoDeleteLogsDays
let snd = multiplier.lastIndex { current % $0 == 0 }! // make sure 1 is in list
return (current / multiplier[snd], snd)
}
private func autoDeleteString(_ num: Int, unit: Int) -> String {
switch num {
case 0: return "Never"
case 1: return "1 \(["Day", "Week", "Month"][unit])"
default: return "\(num) \(["Days", "Weeks", "Months"][unit])"
}
}