-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Expand file tree
/
Copy pathGeneral.swift
More file actions
137 lines (125 loc) · 5.54 KB
/
General.swift
File metadata and controls
137 lines (125 loc) · 5.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import Client
import ComposableArchitecture
import Foundation
import LaunchAgentManager
import Status
import SwiftUI
import XPCShared
import Logger
@Reducer
public struct General {
@ObservableState
public struct State: Equatable {
var xpcServiceVersion: String?
var isAccessibilityPermissionGranted: ObservedAXStatus = .unknown
var isExtensionPermissionGranted: ExtensionPermissionStatus = .unknown
var isReloading = false
}
public enum Action: Equatable {
case appear
case setupLaunchAgentIfNeeded
case openExtensionManager
case reloadStatus
case finishReloading(
xpcServiceVersion: String,
axStatus: ObservedAXStatus,
extensionStatus: ExtensionPermissionStatus
)
case failedReloading
case retryReloading
}
@Dependency(\.toast) var toast
struct ReloadStatusCancellableId: Hashable {}
public var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .appear:
return .run { send in
await send(.setupLaunchAgentIfNeeded)
for await _ in DistributedNotificationCenter.default().notifications(named: .serviceStatusDidChange) {
await send(.reloadStatus)
}
}
case .setupLaunchAgentIfNeeded:
return .run { send in
#if DEBUG
// do not auto install on debug build
await send(.reloadStatus)
#else
Task {
do {
try await LaunchAgentManager()
.setupLaunchAgentForTheFirstTimeIfNeeded()
} catch {
Logger.ui.error("Failed to setup launch agent. \(error.localizedDescription)")
toast("Operation failed: permission denied. This may be due to missing background permissions.", .error)
}
await send(.reloadStatus)
}
#endif
}
case .openExtensionManager:
return .run { send in
let service = try getService()
do {
_ = try await service
.send(requestBody: ExtensionServiceRequests.OpenExtensionManager())
} catch {
Logger.ui.error("Failed to open extension manager. \(error.localizedDescription)")
toast(error.localizedDescription, .error)
await send(.failedReloading)
}
}
case .reloadStatus:
guard !state.isReloading else { return .none }
state.isReloading = true
return .run { send in
let service = try getService()
do {
let isCommunicationReady = try await service.launchIfNeeded()
if isCommunicationReady {
let xpcServiceVersion = try await service.getXPCServiceVersion().version
let isAccessibilityPermissionGranted = try await service
.getXPCServiceAccessibilityPermission()
let isExtensionPermissionGranted = try await service.getXPCServiceExtensionPermission()
await send(.finishReloading(
xpcServiceVersion: xpcServiceVersion,
axStatus: isAccessibilityPermissionGranted,
extensionStatus: isExtensionPermissionGranted
))
} else {
toast("Launching service app.", .info)
try await Task.sleep(nanoseconds: 5_000_000_000)
await send(.retryReloading)
}
} catch let error as XPCCommunicationBridgeError {
Logger.ui.error("Failed to reach communication bridge. \(error.localizedDescription)")
toast(
"Unable to connect to the communication bridge. The helper application didn't respond. This may be due to missing background permissions.",
.error
)
await send(.failedReloading)
} catch {
Logger.ui.error("Failed to reload status. \(error.localizedDescription)")
toast(error.localizedDescription, .error)
await send(.failedReloading)
}
}.cancellable(id: ReloadStatusCancellableId(), cancelInFlight: true)
case let .finishReloading(version, axStatus, extensionStatus):
state.xpcServiceVersion = version
state.isAccessibilityPermissionGranted = axStatus
state.isExtensionPermissionGranted = extensionStatus
state.isReloading = false
return .none
case .failedReloading:
state.isReloading = false
return .none
case .retryReloading:
state.isReloading = false
return .run { send in
await send(.reloadStatus)
}
}
}
}
}