479 lines
17 KiB
Swift
479 lines
17 KiB
Swift
import PromiseKit
|
|
import RealmSwift
|
|
import SFSafeSymbols
|
|
import Shared
|
|
import SwiftUI
|
|
import WebKit
|
|
import XCGLogger
|
|
|
|
struct DebugView: View {
|
|
@State private var showShareSheet = false
|
|
@State private var logsURL: URL?
|
|
@State private var tapsOnCasitaLogo = 0
|
|
|
|
private let feedbackGenerator = UINotificationFeedbackGenerator()
|
|
|
|
// Progress views
|
|
@State private var loadingLogs = false
|
|
@State private var loadingCleaningWebCache = false
|
|
@State private var loadingResetApp = false
|
|
|
|
// Alerts
|
|
@State private var showDeleteEntitiesAlert = false
|
|
@State private var showResetAppAlert = false
|
|
@State private var watchSyncErrorMessage: String?
|
|
@State private var showWatchSyncError = false
|
|
|
|
var body: some View {
|
|
List {
|
|
AppleLikeListTopRowHeader(
|
|
image: .init(uiImage: MaterialDesignIcons.bugIcon.image(
|
|
ofSize: .init(width: 120, height: 120),
|
|
color: Asset.Colors.haPrimary.color
|
|
)),
|
|
title: L10n.Settings.Debugging.Header.title,
|
|
subtitle: L10n.Settings.Debugging.Header.subtitle
|
|
)
|
|
|
|
Section {
|
|
Button(action: {
|
|
if let url = Current.Log.archiveURL() {
|
|
logsURL = url
|
|
if Current.isCatalyst {
|
|
if let url = Current.Log.archiveURL() {
|
|
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
|
}
|
|
} else {
|
|
loadingLogs = true
|
|
showShareSheet = true
|
|
}
|
|
} else {
|
|
Current.Log.error("Logs archive URL not available")
|
|
}
|
|
}, label: {
|
|
linkContent(
|
|
image: .init(systemSymbol: .filemenuAndSelection),
|
|
title: Current.isCatalyst ? L10n.Settings.Developer.ShowLogFiles.title : L10n.Settings.Developer
|
|
.ExportLogFiles.title,
|
|
showProgressView: loadingLogs
|
|
)
|
|
})
|
|
}
|
|
.sheet(isPresented: .init(get: { showShareSheet && logsURL != nil }, set: { showShareSheet = $0 })) {
|
|
if let logsURL {
|
|
ActivityViewController(shareWrapper: .init(url: logsURL))
|
|
.onAppear {
|
|
loadingLogs = false
|
|
}
|
|
.onDisappear {
|
|
do {
|
|
try FileManager.default.removeItem(at: logsURL)
|
|
} catch {
|
|
Current.Log.error("Error deleting logs file: \(error)")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if #available(iOS 17, *) {
|
|
Section {
|
|
NavigationLink {
|
|
ThreadCredentialsManagementView()
|
|
} label: {
|
|
linkContent(
|
|
image: Image(
|
|
uiImage: Asset.SharedAssets.thread.image.withRenderingMode(
|
|
.alwaysTemplate
|
|
)
|
|
),
|
|
title: L10n.SettingsDetails.Thread.title,
|
|
imageSize: 22
|
|
)
|
|
}
|
|
} footer: {
|
|
Text(
|
|
L10n.Settings.Debugging.Thread.footer
|
|
)
|
|
}
|
|
}
|
|
|
|
Section {
|
|
NavigationLink {
|
|
ClientEventsLogView()
|
|
} label: {
|
|
linkContent(image: .init(systemSymbol: .listDash), title: L10n.Settings.EventLog.title)
|
|
}
|
|
|
|
NavigationLink {
|
|
LocationHistoryListViewControllerWrapper()
|
|
} label: {
|
|
linkContent(
|
|
image: .init(systemSymbol: .map),
|
|
title: L10n.Settings.LocationHistory.title
|
|
)
|
|
}
|
|
}
|
|
|
|
criticalSection
|
|
|
|
if tapsOnCasitaLogo < 10 {
|
|
Button(action: {
|
|
feedbackGenerator.notificationOccurred(.success)
|
|
tapsOnCasitaLogo += 1
|
|
}, label: {
|
|
Image(uiImage: Asset.SharedAssets.casita.image)
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fit)
|
|
.frame(width: 100, height: 100, alignment: .center)
|
|
})
|
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
.listRowBackground(Color.clear)
|
|
} else {
|
|
developerSection
|
|
}
|
|
}
|
|
}
|
|
|
|
private func linkContent(
|
|
image: Image,
|
|
title: String,
|
|
imageSize: CGFloat = 18,
|
|
iconColor: Color = Color.asset(Asset.Colors.haPrimary),
|
|
textColor: Color = Color(uiColor: .label),
|
|
showProgressView: Bool? = nil
|
|
) -> some View {
|
|
HStack(spacing: Spaces.two) {
|
|
image
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fit)
|
|
.frame(width: imageSize, height: imageSize, alignment: .center)
|
|
.foregroundStyle(iconColor)
|
|
Text(title)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.foregroundStyle(textColor)
|
|
if let showProgressView {
|
|
ProgressView()
|
|
.progressViewStyle(.circular)
|
|
.opacity(showProgressView ? 1 : 0)
|
|
.animation(.easeOut(duration: 2), value: showProgressView)
|
|
}
|
|
}
|
|
}
|
|
|
|
private var criticalSection: some View {
|
|
Section {
|
|
Button {
|
|
showDeleteEntitiesAlert = true
|
|
} label: {
|
|
linkContent(
|
|
image: .init(systemSymbol: .deleteBackwardFill),
|
|
title: L10n.Debug.Reset.EntitiesDatabase.title,
|
|
iconColor: .red,
|
|
textColor: .red
|
|
)
|
|
}
|
|
.alert(L10n.Alert.Confirmation.Generic.title, isPresented: $showDeleteEntitiesAlert) {
|
|
Button(role: .cancel, action: { /* no-op */ }) {
|
|
Text(L10n.cancelLabel)
|
|
}
|
|
Button(role: .destructive, action: {
|
|
do {
|
|
_ = try Current.database.write { db in
|
|
try HAAppEntity.deleteAll(db)
|
|
Current.Log.verbose("Deleted all app entities")
|
|
}
|
|
} catch {
|
|
Current.Log.error("Failed to reset app entities, error: \(error)")
|
|
}
|
|
}) {
|
|
Text(L10n.yesLabel)
|
|
}
|
|
} message: {
|
|
Text(L10n.Alert.Confirmation.DeleteEntities.message)
|
|
}
|
|
Button {
|
|
loadingCleaningWebCache = true
|
|
WKWebsiteDataStore.default().removeData(
|
|
ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(),
|
|
modifiedSince: Date(timeIntervalSince1970: 0),
|
|
completionHandler: {
|
|
Current.Log.verbose("Reset browser caches!")
|
|
loadingCleaningWebCache = false
|
|
}
|
|
)
|
|
} label: {
|
|
linkContent(
|
|
image: .init(systemSymbol: .deleteBackwardFill),
|
|
title: L10n.Settings.ResetSection.ResetWebCache.title,
|
|
iconColor: .red,
|
|
textColor: .red,
|
|
showProgressView: loadingCleaningWebCache
|
|
)
|
|
}
|
|
|
|
Button {
|
|
showResetAppAlert = true
|
|
} label: {
|
|
linkContent(
|
|
image: .init(systemSymbol: .deleteBackwardFill),
|
|
title: L10n.Settings.ResetSection.ResetApp.title,
|
|
iconColor: .red,
|
|
textColor: .red,
|
|
showProgressView: loadingResetApp
|
|
)
|
|
}
|
|
.alert(L10n.Alert.Confirmation.Generic.title, isPresented: $showResetAppAlert) {
|
|
Button(role: .cancel, action: { /* no-op */ }) {
|
|
Text(L10n.cancelLabel)
|
|
}
|
|
Button(role: .destructive, action: {
|
|
Task {
|
|
await resetApp()
|
|
}
|
|
}) {
|
|
Text(L10n.yesLabel)
|
|
}
|
|
} message: {
|
|
Text(L10n.Settings.ResetSection.ResetAlert.title)
|
|
}
|
|
|
|
} footer: {
|
|
Text(L10n.Settings.Debugging.CriticalSection.footer)
|
|
}
|
|
}
|
|
|
|
private var developerSection: some View {
|
|
Section {
|
|
Button {
|
|
if let syncError = HomeAssistantAPI.SyncWatchContext() {
|
|
watchSyncErrorMessage = syncError.localizedDescription
|
|
showWatchSyncError = true
|
|
}
|
|
} label: {
|
|
linkContent(
|
|
image: .init(systemSymbol: .applewatchWatchface),
|
|
title: L10n.Settings.Developer.SyncWatchContext.title
|
|
)
|
|
}
|
|
.alert(L10n.errorLabel, isPresented: $showWatchSyncError) {
|
|
Button(role: .cancel, action: { /* no-op */ }) {
|
|
Text(L10n.okLabel)
|
|
}
|
|
} message: {
|
|
Text(watchSyncErrorMessage ?? "Unknown")
|
|
}
|
|
|
|
Button {
|
|
copyRealm()
|
|
} label: {
|
|
linkContent(
|
|
image: .init(systemSymbol: .docOnDoc),
|
|
title: L10n.Settings.Developer.CopyRealm.title
|
|
)
|
|
}
|
|
|
|
Button {
|
|
prefs.set(!prefs.bool(forKey: "showTranslationKeys"), forKey: "showTranslationKeys")
|
|
} label: {
|
|
linkContent(
|
|
image: .init(systemSymbol: .textBubble),
|
|
title: L10n.Settings.Developer.DebugStrings.title
|
|
)
|
|
}
|
|
|
|
Button {
|
|
sendCameraNotification()
|
|
} label: {
|
|
linkContent(
|
|
image: .init(systemSymbol: .camera),
|
|
title: L10n.Settings.Developer.CameraNotification.title
|
|
)
|
|
}
|
|
|
|
Button {
|
|
sendMapNotification()
|
|
} label: {
|
|
linkContent(
|
|
image: .init(systemSymbol: .map),
|
|
title: L10n.Settings.Developer.MapNotification.title
|
|
)
|
|
}
|
|
|
|
Toggle(isOn: .init(get: {
|
|
prefs.bool(forKey: XCGLogger.shouldNotifyUserDefaultsKey)
|
|
}, set: { newValue in
|
|
prefs.set(newValue, forKey: XCGLogger.shouldNotifyUserDefaultsKey)
|
|
|
|
})) {
|
|
linkContent(
|
|
image: .init(systemSymbol: .info),
|
|
title: L10n.Settings.Developer.AnnoyingBackgroundNotifications.title
|
|
)
|
|
}
|
|
|
|
} header: {
|
|
Text(L10n.Settings.Developer.header)
|
|
} footer: {
|
|
Text(L10n.Settings.Developer.footer)
|
|
}
|
|
}
|
|
|
|
private func copyRealm() {
|
|
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
|
|
)
|
|
Current.Log.verbose(msg)
|
|
}
|
|
|
|
private func sendMapNotification() {
|
|
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)
|
|
}
|
|
|
|
private func sendCameraNotification() {
|
|
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 resetApp() async {
|
|
loadingResetApp = true
|
|
Current.Log.verbose("Resetting app!")
|
|
|
|
for api in Current.apis {
|
|
await revokeToken(api: api)
|
|
await wait(seconds: 13)
|
|
api.connection.disconnect()
|
|
}
|
|
for server in Current.servers.all {
|
|
Current.servers.remove(identifier: server.identifier)
|
|
}
|
|
resetStores()
|
|
setDefaults()
|
|
await resetPushID()
|
|
loadingResetApp = false
|
|
Current.onboardingObservation.needed(.logout)
|
|
}
|
|
|
|
private func wait(seconds: Int) async {
|
|
await Task.sleep(UInt64(seconds * 1_000_000_000))
|
|
}
|
|
|
|
private func revokeToken(api: HomeAssistantAPI) async {
|
|
await withCheckedContinuation { continuation in
|
|
api.tokenManager.revokeToken().pipe { result in
|
|
switch result {
|
|
case .fulfilled:
|
|
break
|
|
case let .rejected(error):
|
|
Current.Log
|
|
.error(
|
|
"Failed to revoke token for api \(api.server.info.name) \(api.server.info.connection.activeURL()?.absoluteString ?? "Uknown active URL"), error: \(error.localizedDescription)"
|
|
)
|
|
}
|
|
continuation.resume()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func resetPushID() async {
|
|
await withCheckedContinuation { continuation in
|
|
Current.notificationManager.resetPushID().pipe { result in
|
|
switch result {
|
|
case .fulfilled:
|
|
break
|
|
case let .rejected(error):
|
|
Current.Log.error("Failed to reset push ID, error: \(error.localizedDescription)")
|
|
}
|
|
continuation.resume()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
DebugView()
|
|
}
|