// /* * Copyright (c) 2021 BWI GmbH * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import Foundation import UIKit import MatrixSDK fileprivate let BWI_ServerDowntimes = "BWI_ServerDowntimes" fileprivate let BWI_ValidAppVersions = "BWI_ValidAppVersions" fileprivate let exampleFile = "exampleDowntime" fileprivate let maintenanceURL = "/_matrix/cmaintenance" @objcMembers class ServerDowntimeDefaultService : NSObject { /// Find the next relevant downtime /// - Returns: ServerDowntime or nil, if there is no downtime in the future or the warning date is still in the future func nextDowntime() -> ServerDowntime? { guard let encodedDowntimes = UserDefaults.standard.object(forKey: BWI_ServerDowntimes) as? [Data] else { return nil } let serverDowntimes = encodedDowntimes .compactMap { try? JSONDecoder().decode(ServerDowntime.self, from: $0) } .filter { // remove downtimes that are already done guard let downtimeEndDate = $0.endTime.iso8601LocalDate else { return false } return downtimeEndDate.timeIntervalSinceNow >= 0 } .filter { // remove downtimes where the warning time is still in the future guard let warningStartDate = $0.warningStartTime.iso8601LocalDate else { return false } return warningStartDate.timeIntervalSinceNow <= 0 } .filter { // remove downtimes with invalid formed start or end times return $0.startTime.iso8601LocalDate != nil && $0.endTime.iso8601LocalDate != nil } .sorted { // sort downtimes be startTime guard let downtimeStartDate1 = $0.startTime.iso8601LocalDate, let downtimeStartDate2 = $1.startTime.iso8601LocalDate else { return false } return downtimeStartDate1 < downtimeStartDate2 } return serverDowntimes.first } /// Check the state of the next downtime /// - Returns: ongoing, warning or none func nextDowntimeType() -> ServerDowntimeType { guard let downTime = self.nextDowntime() else { return .none } if let startDate = downTime.startTime.iso8601LocalDate { if startDate.timeIntervalSinceNow < 0 { return .ongoing } } return .warning } private func loadTestMaintanceJSON(forName name: String) -> Data? { if let path = Bundle.main.path(forResource: name, ofType: "json") { do { let jsonString = try String(contentsOfFile: path) return jsonString.data(using: .utf8) } catch { print(error.localizedDescription) } } return nil } // MARK: - Handle Server Response func copyFetchResultToUserDefaults(fetchResult: [String : Any]) throws { if let downtimes = fetchResult["downtime"] as? [[String : Any]] { try copyServerDowntimesToUserDefaults(downtimes: downtimes) } if let versions = fetchResult["versions"] as? [String : Any] { try copyValidAppVersionsToUserDefaults(versions: versions) } } private func copyServerDowntimesToUserDefaults(downtimes: [[String : Any]]) throws { var encodedDowntimes: [Data] = [] for downtimeDict in downtimes { if let serverDowntime = try? ServerDowntime(dict: downtimeDict) { let data = try JSONEncoder().encode(serverDowntime) encodedDowntimes.append(data) } } UserDefaults.standard.set(encodedDowntimes, forKey: BWI_ServerDowntimes) } private func copyValidAppVersionsToUserDefaults(versions: [String : Any]) throws { if let iosVersionsDict = versions["ios"] as? [String : Any] { if let validAppVersions = try? ValidAppVersions(dict: iosVersionsDict) { let data = try JSONEncoder().encode(validAppVersions) UserDefaults.standard.set(data, forKey: BWI_ValidAppVersions) } } } // MARK: - Mockup Fetch func fetchDowntimes(completion: @escaping () -> Void) { if let exampleFetch = loadTestMaintanceJSON(forName: exampleFile) { do { if let fetchResult = try JSONSerialization.jsonObject(with: exampleFetch, options: []) as? [String: Any] { try copyFetchResultToUserDefaults(fetchResult: fetchResult) } } catch { print(error.localizedDescription) } } completion() } } // MARK: - extension ServerDowntimeDefaultService : ServerDowntimeService { func fetchDowntimes(session: MXSession, completion: @escaping () -> Void) { session.matrixRestClient.getDowntime(completion: { (jsonResponse, error) in do { if let fetchResult = jsonResponse as? [String : Any] { try self.copyFetchResultToUserDefaults(fetchResult: fetchResult) } } catch { print(error.localizedDescription) } completion() }) } func fetchDowntimesWithDirectRequest(completion: @escaping (Bool) -> Void) { guard let url = URL(string: AppConfigService.shared.serverUrl() + maintenanceURL) else { completion(false) return } let config = URLSessionConfiguration.default config.httpAdditionalHeaders = ["User-Agent": UserAgentService().bwiUserAgent] let session : URLSession = URLSession(configuration: config) let task = session.dataTask(with: url) { data, response, error in guard let response = response as? HTTPURLResponse, response.statusCode == 200 else { completion(false) return } do { if let data = data, let fetchResult = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { try self.copyFetchResultToUserDefaults(fetchResult: fetchResult) } } catch { print(error.localizedDescription) } completion(true) } task.resume() } // MARK: - @objc func isDowntimePresentable() -> Bool { return nextDowntimeType() != .none } @objc func downtimeText() -> String { guard let downTime = self.nextDowntime(), let startDate = downTime.startTime.iso8601LocalDate, let endDate = downTime.endTime.iso8601LocalDate else { return "" } let dayOfWeekFormatter = DateFormatter() dayOfWeekFormatter.dateFormat = "EEEE" dayOfWeekFormatter.timeZone = TimeZone.current dayOfWeekFormatter.calendar = Calendar.current let dateFormatter = DateFormatter() dateFormatter.dateFormat = "dd.MM.yyyy" dateFormatter.timeZone = TimeZone.current dateFormatter.calendar = Calendar.current let dateTimeFormatter = DateFormatter() dateTimeFormatter.dateFormat = "dd.MM.yyyy HH:mm" dateTimeFormatter.timeZone = TimeZone.current dateTimeFormatter.calendar = Calendar.current let timeFormatter = DateFormatter() timeFormatter.dateFormat = "HH:mm" timeFormatter.timeZone = TimeZone.current timeFormatter.calendar = Calendar.current let utcFormatter = DateFormatter() utcFormatter.dateFormat = "ZZZZZ" utcFormatter.timeZone = TimeZone.current utcFormatter.calendar = Calendar.current if self.isSameDay() { return BWIL10n.settingsDowntimeMessageSameDay(AppInfo.current.displayName, dayOfWeekFormatter.string(from: startDate), dateFormatter.string(from: startDate), timeFormatter.string(from: startDate), timeFormatter.string(from: endDate), utcFormatter.string(from: startDate)) } else { return BWIL10n.settingsDowntimeMessageDifferentDays(AppInfo.current.displayName, dayOfWeekFormatter.string(from: startDate), dateTimeFormatter.string(from: startDate), utcFormatter.string(from: startDate), dayOfWeekFormatter.string(from: endDate), dateTimeFormatter.string(from: endDate), utcFormatter.string(from: endDate)) } } @objc func isDowntimeNow() -> Bool { guard let downTime = self.nextDowntime(), let startDate = downTime.startTime.iso8601LocalDate, let endDate = downTime.endTime.iso8601LocalDate else { return false } return startDate >= Date() && Date() <= endDate } @objc func downtimeColor() -> UIColor { switch nextDowntimeType() { case .warning: return .yellow case .ongoing: return .red default: return .clear } } @objc func downtimeTextColor() -> UIColor { switch nextDowntimeType() { case .warning: return .black case .ongoing: return .white default: return .clear } } @objc func isSameDay() -> Bool { guard let downTime = self.nextDowntime(), let startDate = downTime.startTime.iso8601LocalDate, let endDate = downTime.endTime.iso8601LocalDate else { return false } return Calendar.current.dateComponents([.day], from: startDate, to: endDate).day == 0 } }