ConnectivityViewController.swift 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import UIKit
  2. import DcCore
  3. class ConnectivityViewController: WebViewViewController {
  4. private let dcContext: DcContext
  5. private var connectivityChangedObserver: NSObjectProtocol?
  6. init(dcContext: DcContext) {
  7. self.dcContext = dcContext
  8. super.init()
  9. }
  10. required init?(coder: NSCoder) {
  11. fatalError("init(coder:) has not been implemented")
  12. }
  13. // called only once after loading
  14. override func viewDidLoad() {
  15. super.viewDidLoad()
  16. self.title = String.localized("connectivity")
  17. self.webView.scrollView.bounces = false
  18. self.webView.isOpaque = false
  19. self.webView.backgroundColor = .clear
  20. view.backgroundColor = DcColors.defaultBackgroundColor
  21. }
  22. // called everytime the view will appear
  23. override func viewWillAppear(_ animated: Bool) {
  24. // set connectivity changed observer before we acutally init html,
  25. // otherwise, we may miss events and the html is not correct.
  26. connectivityChangedObserver = NotificationCenter.default.addObserver(forName: dcNotificationConnectivityChanged,
  27. object: nil,
  28. queue: nil) { [weak self] _ in
  29. self?.loadHtml()
  30. }
  31. loadHtml()
  32. }
  33. override func viewDidDisappear(_ animated: Bool) {
  34. if let connectivityChangedObserver = self.connectivityChangedObserver {
  35. NotificationCenter.default.removeObserver(connectivityChangedObserver)
  36. }
  37. }
  38. // this method needs to be run from a background thread
  39. private func getNotificationStatus(hasNotifyToken: Bool, backgroundRefreshStatus: UIBackgroundRefreshStatus) -> String {
  40. let connectiviy = self.dcContext.getConnectivity()
  41. let title = " <b>" + String.localized("pref_notifications") + ":</b> "
  42. let notificationsEnabledInDC = !UserDefaults.standard.bool(forKey: "notifications_disabled")
  43. var notificationsEnabledInSystem = false
  44. let semaphore = DispatchSemaphore(value: 0)
  45. DispatchQueue.global(qos: .userInitiated).async {
  46. NotificationManager.notificationEnabledInSystem { enabled in
  47. notificationsEnabledInSystem = enabled
  48. semaphore.signal()
  49. }
  50. }
  51. if semaphore.wait(timeout: .now() + 1) == .timedOut {
  52. return "<span class=\"red dot\"></span>"
  53. .appending(title)
  54. .appending("Timeout Error")
  55. }
  56. if !notificationsEnabledInDC {
  57. return "<span class=\"disabled dot\"></span>"
  58. .appending(title)
  59. .appending(String.localized("disabled_in_dc"))
  60. }
  61. if !notificationsEnabledInSystem {
  62. return "<span class=\"disabled dot\"></span>"
  63. .appending(title)
  64. .appending(String.localized("disabled_in_system_settings"))
  65. }
  66. if backgroundRefreshStatus != .available {
  67. return "<span class=\"disabled dot\"></span>"
  68. .appending(title)
  69. .appending(String.localized("bg_app_refresh_disabled"))
  70. }
  71. if !hasNotifyToken || connectiviy == DC_CONNECTIVITY_NOT_CONNECTED {
  72. return "<span class=\"red dot\"></span>"
  73. .appending(title)
  74. .appending(String.localized("connectivity_not_connected"))
  75. }
  76. let timestamps = UserDefaults.standard.array(forKey: Constants.Keys.notificationTimestamps) as? [Double]
  77. guard let timestamps = timestamps else {
  78. // in most cases, here the app was just installed and we do not have any data.
  79. // so, do not show something error-like here.
  80. // (in case of errors, it usually converts to an error sooner or later)
  81. return "<span class=\"green dot\"></span>"
  82. .appending(title)
  83. .appending(String.localized("connectivity_connected"))
  84. }
  85. var averageDelta: Double = 0
  86. if timestamps.isEmpty {
  87. // this should not happen:
  88. // the array should not be empty as old notifications are only removed if a new one is added
  89. return "<span class=\"red dot\"></span>"
  90. .appending(title)
  91. .appending("Bad Data")
  92. } else if timestamps.count == 1 {
  93. averageDelta = Double(Date().timeIntervalSince1970) - timestamps.first!
  94. } else {
  95. averageDelta = (timestamps.last! - timestamps.first!) / Double(timestamps.count-1)
  96. }
  97. var lastWakeups = ""
  98. var lastWakeupsCnt = 0
  99. for timestamp in timestamps.reversed() {
  100. lastWakeups += (lastWakeupsCnt > 0 ? ", " : "") + DateUtils.getExtendedAbsTimeSpanString(timeStamp: timestamp)
  101. lastWakeupsCnt += 1
  102. if lastWakeupsCnt >= 3 {
  103. break
  104. }
  105. }
  106. if Int(averageDelta / Double(60 * 60)) > 1 {
  107. // more than 1 hour in average
  108. return "<span class=\"red dot\"></span>"
  109. .appending(title)
  110. .appending(String.localized("delayed"))
  111. .appending(", ")
  112. .appending(String.localizedStringWithFormat(String.localized("last_check_at"), lastWakeups))
  113. .appending(", ")
  114. .appending(String.localized(stringID: "notifications_avg_hours", count: Int(averageDelta / Double(60 * 60))))
  115. }
  116. if averageDelta / Double(60 * 20) > 1 {
  117. // more than 20 minutes in average
  118. return "<span class=\"yellow dot\"></span>"
  119. .appending(title)
  120. .appending(String.localized("delayed"))
  121. .appending(", ")
  122. .appending(String.localizedStringWithFormat(String.localized("last_check_at"), lastWakeups))
  123. .appending(", ")
  124. .appending(String.localized(stringID: "notifications_avg_minutes", count: Int(averageDelta / 60)))
  125. }
  126. return "<span class=\"green dot\"></span>"
  127. .appending(title)
  128. .appending(String.localizedStringWithFormat(String.localized("last_check_at"), lastWakeups))
  129. .appending(", ")
  130. .appending(String.localized(stringID: "notifications_avg_minutes", count: Int(averageDelta / 60)))
  131. }
  132. private func loadHtml() {
  133. // `UIApplication.shared` needs to be called from main thread
  134. var hasNotifyToken = false
  135. if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
  136. hasNotifyToken = appDelegate.notifyToken != nil
  137. }
  138. let backgroundRefreshStatus = UIApplication.shared.backgroundRefreshStatus
  139. // do the remaining things in background thread
  140. DispatchQueue.global(qos: .userInitiated).async { [weak self] in
  141. guard let self = self else { return }
  142. var html = self.dcContext.getConnectivityHtml()
  143. .replacingOccurrences(of: "</style>", with:
  144. """
  145. body {
  146. font-size: 13pt;
  147. font-family: -apple-system, sans-serif;
  148. padding: 0 .5rem .5rem .5rem;
  149. -webkit-text-size-adjust: none;
  150. }
  151. .disabled {
  152. background-color: #aaaaaa;
  153. }
  154. @media (prefers-color-scheme: dark) {
  155. body {
  156. background-color: black !important;
  157. color: #eee;
  158. }
  159. }
  160. </style>
  161. """)
  162. let notificationStatus = self.getNotificationStatus(hasNotifyToken: hasNotifyToken, backgroundRefreshStatus: backgroundRefreshStatus)
  163. if let range = html.range(of: "</ul>") {
  164. html = html.replacingCharacters(in: range, with: "<li>" + notificationStatus + "</li></ul>")
  165. }
  166. DispatchQueue.main.async {
  167. self.webView.loadHTMLString(html, baseURL: nil)
  168. }
  169. }
  170. }
  171. }