iOS/Sources/App/Settings/DebugSettingsViewController...

460 lines
17 KiB
Swift

import Eureka
import MBProgressHUD
import PromiseKit
import RealmSwift
import Shared
import SwiftUI
import WebKit
import XCGLogger
class DebugSettingsViewController: HAFormViewController {
private var shakeCount = 0
private var maxShakeCount = 3
override init() {
super.init()
title = L10n.Settings.Debugging.title
}
override func viewDidLoad() {
super.viewDidLoad()
becomeFirstResponder()
if #available(iOS 17, *), !Current.isCatalyst {
form.append(threadCredentials())
}
form.append(contentsOf: [
logs(),
reset(),
developerOptions(),
])
}
override var canBecomeFirstResponder: Bool {
true
}
override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
if motion == .motionShake {
Current.Log.verbose("shake!")
if shakeCount >= maxShakeCount {
if let section = form.sectionBy(tag: "developerOptions") {
section.hidden = false
section.evaluateHidden()
tableView.reloadData()
let alert = UIAlertController(
title: "You did it!",
message: "Developer functions unlocked",
preferredStyle: UIAlertController.Style.alert
)
alert.addAction(UIAlertAction(
title: L10n.okLabel,
style: UIAlertAction.Style.default,
handler: nil
))
present(alert, animated: true, completion: nil)
alert.popoverPresentationController?.barButtonItem = navigationItem.rightBarButtonItem
}
return
}
shakeCount += 1
}
}
static func showMapContentExtension() {
let content = UNMutableNotificationContent()
content.body = L10n.Settings.Developer.MapNotification.Notification.body
content.sound = .default
var firstPinLatitude = "40.785091"
var firstPinLongitude = "-73.968285"
if Current.appConfiguration == .fastlaneSnapshot,
let lat = prefs.string(forKey: "mapPin1Latitude"),
let lon = prefs.string(forKey: "mapPin1Longitude") {
firstPinLatitude = lat
firstPinLongitude = lon
}
var secondPinLatitude = "40.758896"
var secondPinLongitude = "-73.985130"
if Current.appConfiguration == .fastlaneSnapshot,
let lat = prefs.string(forKey: "mapPin2Latitude"),
let lon = prefs.string(forKey: "mapPin2Longitude") {
secondPinLatitude = lat
secondPinLongitude = lon
}
content.userInfo = [
"homeassistant": [
"latitude": firstPinLatitude,
"longitude": firstPinLongitude,
"second_latitude": secondPinLatitude,
"second_longitude": secondPinLongitude,
],
]
content.categoryIdentifier = "map"
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
let notificationRequest = UNNotificationRequest(
identifier: "mapContentExtension",
content: content,
trigger: trigger
)
UNUserNotificationCenter.current().add(notificationRequest)
}
static func showCameraContentExtension() {
let content = UNMutableNotificationContent()
content.body = L10n.Settings.Developer.CameraNotification.Notification.body
content.sound = .default
var entityID = "camera.amcrest_camera"
if Current.appConfiguration == .fastlaneSnapshot,
let snapshotEntityID = prefs.string(forKey: "cameraEntityID") {
entityID = snapshotEntityID
}
content.userInfo = ["entity_id": entityID]
content.categoryIdentifier = "camera"
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)
let notificationRequest = UNNotificationRequest(
identifier: "cameraContentExtension",
content: content,
trigger: trigger
)
UNUserNotificationCenter.current().add(notificationRequest)
}
private func threadCredentials() -> Eureka.Section {
Section() <<< SettingsRootDataSource.Row.thread.row
}
private func logs() -> Eureka.Section {
let section = Section()
section <<< SettingsButtonRow {
$0.title = L10n.Settings.EventLog.title
let scene = StoryboardScene.ClientEvents.self
$0.presentationMode = .show(controllerProvider: .storyBoard(
storyboardId: scene.clientEventsList.identifier,
storyboardName: scene.storyboardName,
bundle: Bundle.main
), onDismiss: { vc in
_ = vc.navigationController?.popViewController(animated: true)
})
}
section <<< SettingsButtonRow {
$0.title = L10n.Settings.LocationHistory.title
$0.presentationMode = .show(controllerProvider: .callback(builder: {
LocationHistoryListViewController()
}), onDismiss: nil)
}
return section
}
private func reset() -> Eureka.Section {
let section = Section()
section <<< ButtonRow {
if Current.isCatalyst {
$0.title = L10n.Settings.Developer.ShowLogFiles.title
} else {
$0.title = L10n.Settings.Developer.ExportLogFiles.title
}
$0.cellUpdate { cell, _ in
cell.textLabel?.textAlignment = .natural
}
}.onCellSelection { [weak self] cell, _ in
guard let self else { return }
Current.Log.export(from: self, sender: cell, openURLHandler: { url in
UIApplication.shared.open(url, options: [:], completionHandler: nil)
})
}
section <<< SettingsButtonRow {
$0.isDestructive = true
$0.title = L10n.Debug.Reset.EntitiesDatabase.title
$0.onCellSelection { [weak self] _, _ in
guard let self else { return }
let hud = MBProgressHUD.showAdded(to: view.window ?? view, animated: true)
hud.backgroundView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
let (promise, seal) = Guarantee<Void>.pending()
do {
_ = try Current.database.write { db in
try HAAppEntity.deleteAll(db)
seal(())
}
} catch {
Current.Log.error("Failed to reset app entities, error: \(error)")
seal(())
}
when(promise, after(seconds: 2.0)).done { _ in
hud.hide(animated: true)
}
}
}
section <<< SettingsButtonRow {
$0.isDestructive = true
$0.title = L10n.Settings.ResetSection.ResetWebCache.title
$0.onCellSelection { [weak self] _, _ in
guard let self else { return }
let hud = MBProgressHUD.showAdded(to: view.window ?? view, animated: true)
hud.backgroundView.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
let (promise, seal) = Guarantee<Void>.pending()
WKWebsiteDataStore.default().removeData(
ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(),
modifiedSince: Date(timeIntervalSince1970: 0),
completionHandler: {
Current.Log.verbose("Reset browser caches!")
seal(())
}
)
when(promise, after(seconds: 2.0)).done { _ in
hud.hide(animated: true)
}
}
}
section <<< SettingsButtonRow {
$0.isDestructive = true
$0.title = L10n.Settings.ResetSection.ResetRow.title
$0.onCellSelection { [weak self] cell, row in
let alert = UIAlertController(
title: L10n.Settings.ResetSection.ResetAlert.title,
message: L10n.Settings.ResetSection.ResetAlert.message,
preferredStyle: .actionSheet
)
alert.addAction(UIAlertAction(title: L10n.cancelLabel, style: .cancel, handler: nil))
alert.addAction(UIAlertAction(
title: L10n.Settings.ResetSection.ResetAlert.title,
style: .destructive,
handler: { _ in
row.hidden = true
row.evaluateHidden()
self?.ResetApp()
}
))
with(alert.popoverPresentationController) {
$0?.sourceView = cell
$0?.sourceRect = cell.bounds
}
self?.present(alert, animated: true, completion: nil)
}
}
return section
}
private func developerOptions() -> Eureka.Section {
let section = Section(header: L10n.Settings.Developer.header, footer: L10n.Settings.Developer.footer) {
$0.hidden = Condition(booleanLiteral: Current.appConfiguration.rawValue > 1)
$0.tag = "developerOptions"
}
section <<< ButtonRow("onboardTest") {
$0.title = "Onboard (Initial)"
$0.presentationMode = .presentModally(controllerProvider: .callback(builder: {
OnboardingNavigationViewController(onboardingStyle: .initial)
}), onDismiss: nil)
}.cellUpdate { cell, _ in
cell.textLabel?.textAlignment = .center
cell.accessoryType = .none
cell.editingAccessoryType = cell.accessoryType
cell.textLabel?.textColor = cell.tintColor.withAlphaComponent(1.0)
}
section <<< ButtonRow {
$0.title = L10n.Settings.Developer.SyncWatchContext.title
}.onCellSelection { [weak self] cell, _ in
if let syncError = HomeAssistantAPI.SyncWatchContext() {
let alert = UIAlertController(
title: L10n.errorLabel,
message: syncError.localizedDescription,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: L10n.okLabel, style: .default, handler: nil))
self?.present(alert, animated: true, completion: nil)
alert.popoverPresentationController?.sourceView = cell.formViewController()?.view
}
}
section <<< ButtonRow {
$0.title = L10n.Settings.Developer.CopyRealm.title
}.onCellSelection { [weak self] cell, _ in
guard let backupURL = Realm.backup() else {
fatalError("Unable to get Realm backup")
}
let containerRealmPath = Realm.Configuration.defaultConfiguration.fileURL!
Current.Log.verbose("Would copy from \(backupURL) to \(containerRealmPath)")
if FileManager.default.fileExists(atPath: containerRealmPath.path) {
do {
_ = try FileManager.default.removeItem(at: containerRealmPath)
} catch {
Current.Log.error("Error occurred, here are the details:\n \(error)")
}
}
do {
_ = try FileManager.default.copyItem(at: backupURL, to: containerRealmPath)
} catch let error as NSError {
// Catch fires here, with an NSError being thrown
Current.Log.error("Error occurred, here are the details:\n \(error)")
}
let msg = L10n.Settings.Developer.CopyRealm.Alert.message(
backupURL.path,
containerRealmPath.path
)
let alert = UIAlertController(
title: L10n.Settings.Developer.CopyRealm.Alert.title,
message: msg,
preferredStyle: UIAlertController.Style.alert
)
alert.addAction(UIAlertAction(title: L10n.okLabel, style: .default, handler: nil))
self?.present(alert, animated: true, completion: nil)
alert.popoverPresentationController?.sourceView = cell.formViewController()?.view
}
section <<< ButtonRow {
$0.title = L10n.Settings.Developer.DebugStrings.title
}.onCellSelection { [weak self] cell, _ in
prefs.set(!prefs.bool(forKey: "showTranslationKeys"), forKey: "showTranslationKeys")
let alert = UIAlertController(title: L10n.okLabel, message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: L10n.okLabel, style: .default, handler: nil))
self?.present(alert, animated: true, completion: nil)
alert.popoverPresentationController?.sourceView = cell.formViewController()?.view
}
section <<< ButtonRow("camera_notification_test") {
$0.title = L10n.Settings.Developer.CameraNotification.title
}.onCellSelection { _, _ in
Self.showCameraContentExtension()
}
section <<< ButtonRow("map_notification_test") {
$0.title = L10n.Settings.Developer.MapNotification.title
}.onCellSelection { _, _ in
Self.showMapContentExtension()
}
section <<< ButtonRow {
$0.title = L10n.Settings.Developer.CrashlyticsTest.NonFatal.title
}.onCellSelection { [weak self] cell, _ in
let alert = UIAlertController(
title: L10n.Settings.Developer.CrashlyticsTest.NonFatal.Notification.title,
message: L10n.Settings.Developer.CrashlyticsTest.NonFatal.Notification.body,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: L10n.okLabel, style: .default, handler: { _ in
let userInfo = [
NSLocalizedDescriptionKey: NSLocalizedString("The request failed.", comment: ""),
NSLocalizedFailureReasonErrorKey: NSLocalizedString(
"The response returned a 404.",
comment: ""
),
NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString("Does this page exist?", comment: ""),
"ProductID": "123456",
"View": "MainView",
]
let error = NSError(domain: NSCocoaErrorDomain, code: -1001, userInfo: userInfo)
Current.crashReporter.logError(error)
}))
self?.present(alert, animated: true, completion: nil)
alert.popoverPresentationController?.sourceView = cell.formViewController()?.view
}
section <<< ButtonRow {
$0.title = L10n.Settings.Developer.CrashlyticsTest.Fatal.title
}.onCellSelection { [weak self] cell, _ in
let alert = UIAlertController(
title: L10n.Settings.Developer.CrashlyticsTest.Fatal.Notification.title,
message: L10n.Settings.Developer.CrashlyticsTest.Fatal.Notification.body,
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: L10n.okLabel, style: .default, handler: { _ in
// SentrySDK.crash()
}))
self?.present(alert, animated: true, completion: nil)
alert.popoverPresentationController?.sourceView = cell.formViewController()?.view
}
section <<< SwitchRow {
$0.title = L10n.Settings.Developer.AnnoyingBackgroundNotifications.title
$0.value = prefs.bool(forKey: XCGLogger.shouldNotifyUserDefaultsKey)
$0.onChange { row in
prefs.set(row.value ?? false, forKey: XCGLogger.shouldNotifyUserDefaultsKey)
}
}
return section
}
func ResetApp() {
Current.Log.verbose("Resetting app!")
let hud = MBProgressHUD.showAdded(to: view, animated: true)
hud.label.text = L10n.Settings.ResetSection.ResetAlert.progressMessage
hud.show(animated: true)
let waitAtLeast = after(seconds: 3.0)
firstly {
race(
when(resolved: Current.apis.map { $0.tokenManager.revokeToken() }).asVoid(),
after(seconds: 10.0)
)
}.then {
waitAtLeast
}.get {
Current.apis.compactMap(\.connection).forEach { $0.disconnect() }
Current.servers.removeAll()
resetStores()
setDefaults()
}.then {
Current.notificationManager.resetPushID().asVoid().recover { _ in }
}.ensure {
hud.hide(animated: true)
Current.onboardingObservation.needed(.logout)
}.cauterize()
}
}