Adopt StateStoreViewModelV2 in the remaining settings screens. (#4158)

* Move the AuthenticationStartScreen into the Authentication directory.

* Commit the updated Sentry license.

No idea why they dropped the 2024 🤷‍♂️

* Use StateStoreViewModelV2 in BugReportScreen.

* Use StateStoreViewModelV2 in UserDetailsEditScreen.

* Use StateStoreViewModelV2 in NotificationSettingsScreen.

* Use StateStoreViewModelV2 in NotificationSettingsEditScreen.

* Use StateStoreViewModelV2 in LegalInformationScreen.

* Use StateStoreViewModelV2 in LogViewerScreen.

* Use StateStoreViewModelV2 in AnalyticsSettingsScreen.

* Rename AdvancedSettingsScreen directory.

* Use StateStoreViewModelV2 in EncryptionResetScreen.

* Use StateStoreViewModelV2 in EncryptionResetPasswordScreen.

* Use StateStoreViewModelV2 in SecureBackup…Screens.

* Use StateStoreViewModelV2 in LoginScreen.

Seems this one was ignored waiting on the fulfillment transitionValues implementation.

* Use StateStoreViewModelV2 in DeactivateAccountScreen.

* Move DeactivateAccountScreen into the Settings directory.
This commit is contained in:
Doug
2025-05-30 12:24:56 +01:00
committed by GitHub
parent 565ca3ab0b
commit 458c6aaa57
54 changed files with 238 additions and 294 deletions
@@ -944,11 +944,10 @@ class AppCoordinator: AppCoordinatorProtocol, AuthenticationFlowCoordinatorDeleg
ServiceLocator.shared.bugReportService.lastCrashEventID = event.eventId.sentryIdString
}
SentrySDK.start(options: options)
SentrySDK.start(options: options) // Swift
enableSentryLogging(enabled: options.enabled) // Rust
enableSentryLogging(enabled: appSettings.analyticsConsentState == .optedIn)
MXLog.info("SentrySDK started")
MXLog.info("Sentry configured (enabled: \(options.enabled))")
}
private func teardownSentry() {
@@ -54,7 +54,7 @@ extension XCTestCase {
/// - timeout: A timeout after which we give up.
/// - message: An optional custom expectation message
/// - until: callback that evaluates outputs until some condition is reached
/// - Returns: The deferred fulfilment to be executed after some actions and that returns the result of the publisher.
/// - Returns: The deferred fulfilment to be executed after some actions and that returns the result of the stream.
func deferFulfillment<Value>(_ asyncStream: AsyncStream<Value>,
timeout: TimeInterval = 10,
message: String? = nil,
@@ -107,6 +107,29 @@ extension XCTestCase {
return deferred
}
/// XCTest utility that assists in subscribing to an async stream and deferring the fulfilment and results until some other actions have been performed.
/// - Parameters:
/// - asyncStream: The stream to wait on.
/// - transitionValues: the values through which the stream needs to transition through
/// - timeout: A timeout after which we give up.
/// - message: An optional custom expectation message
/// - Returns: The deferred fulfilment to be executed after some actions and that returns the result of the stream.
func deferFulfillment<Value: Equatable>(_ asyncStream: AsyncStream<Value>,
transitionValues: [Value],
timeout: TimeInterval = 10,
message: String? = nil) -> DeferredFulfillment<Value> {
var expectedOrder = transitionValues
let deferred = deferFulfillment(asyncStream, timeout: timeout, message: message) { value in
if let index = expectedOrder.firstIndex(where: { $0 == value }), index == 0 {
expectedOrder.remove(at: index)
}
return expectedOrder.isEmpty
}
return deferred
}
/// XCTest utility that assists in subscribing to a publisher and deferring the failure for a particular value until some other actions have been performed.
/// - Parameters:
/// - publisher: The publisher to wait on.
@@ -8,7 +8,7 @@
import Combine
import SwiftUI
typealias LoginScreenViewModelType = StateStoreViewModel<LoginScreenViewState, LoginScreenViewAction>
typealias LoginScreenViewModelType = StateStoreViewModelV2<LoginScreenViewState, LoginScreenViewAction>
class LoginScreenViewModel: LoginScreenViewModelType, LoginScreenViewModelProtocol {
private let authenticationService: AuthenticationServiceProtocol
@@ -13,7 +13,7 @@ struct LoginScreen: View {
/// The focus state of the password text field.
@FocusState private var isPasswordFocused: Bool
@ObservedObject var context: LoginScreenViewModel.Context
@Bindable var context: LoginScreenViewModel.Context
var body: some View {
ScrollView {
@@ -136,17 +136,13 @@ struct LoginScreen_Previews: PreviewProvider, TestablePreview {
NavigationStack {
LoginScreen(context: viewModel.context)
}
.snapshotPreferences(expect: viewModel.context.$viewState.map { state in
state.homeserver.loginMode == .password
})
.snapshotPreferences(expect: viewModel.context.observe(\.viewState.homeserver.loginMode).map { $0 == .password }.eraseToStream())
.previewDisplayName("Initial State")
NavigationStack {
LoginScreen(context: credentialsViewModel.context)
}
.snapshotPreferences(expect: credentialsViewModel.context.$viewState.map { state in
state.homeserver.loginMode == .password
})
.snapshotPreferences(expect: credentialsViewModel.context.observe(\.viewState.homeserver.loginMode).map { $0 == .password }.eraseToStream())
.previewDisplayName("Credentials Entered")
NavigationStack {
@@ -8,7 +8,7 @@
import Combine
import SwiftUI
typealias BugReportScreenViewModelType = StateStoreViewModel<BugReportScreenViewState, BugReportScreenViewAction>
typealias BugReportScreenViewModelType = StateStoreViewModelV2<BugReportScreenViewState, BugReportScreenViewAction>
class BugReportScreenViewModel: BugReportScreenViewModelType, BugReportScreenViewModelProtocol {
private let bugReportService: BugReportServiceProtocol
@@ -12,7 +12,7 @@ import SwiftUI
struct BugReportScreen: View {
@State private var selectedScreenshot: PhotosPickerItem?
@ObservedObject var context: BugReportScreenViewModel.Context
@Bindable var context: BugReportScreenViewModel.Context
var photosPickerTitle: String { context.viewState.screenshot == nil ? L10n.screenBugReportAttachScreenshot : L10n.screenBugReportEditScreenshot }
@@ -8,7 +8,7 @@
import Combine
import SwiftUI
typealias EncryptionResetPasswordScreenViewModelType = StateStoreViewModel<EncryptionResetPasswordScreenViewState, EncryptionResetPasswordScreenViewAction>
typealias EncryptionResetPasswordScreenViewModelType = StateStoreViewModelV2<EncryptionResetPasswordScreenViewState, EncryptionResetPasswordScreenViewAction>
class EncryptionResetPasswordScreenViewModel: EncryptionResetPasswordScreenViewModelType, EncryptionResetPasswordScreenViewModelProtocol {
private let passwordPublisher: PassthroughSubject<String, Never>
@@ -10,7 +10,7 @@ import Compound
import SwiftUI
struct EncryptionResetPasswordScreen: View {
@ObservedObject var context: EncryptionResetPasswordScreenViewModel.Context
@Bindable var context: EncryptionResetPasswordScreenViewModel.Context
@FocusState private var textFieldFocus
var body: some View {
@@ -9,7 +9,7 @@ import Combine
import MatrixRustSDK
import SwiftUI
typealias EncryptionResetScreenViewModelType = StateStoreViewModel<EncryptionResetScreenViewState, EncryptionResetScreenViewAction>
typealias EncryptionResetScreenViewModelType = StateStoreViewModelV2<EncryptionResetScreenViewState, EncryptionResetScreenViewAction>
class EncryptionResetScreenViewModel: EncryptionResetScreenViewModelType, EncryptionResetScreenViewModelProtocol {
private let clientProxy: ClientProxyProtocol
@@ -9,7 +9,7 @@ import Compound
import SwiftUI
struct EncryptionResetScreen: View {
@ObservedObject var context: EncryptionResetScreenViewModel.Context
@Bindable var context: EncryptionResetScreenViewModel.Context
var body: some View {
FullscreenDialog {
@@ -8,7 +8,7 @@
import Combine
import SwiftUI
typealias LogViewerScreenViewModelType = StateStoreViewModel<LogViewerScreenViewState, LogViewerScreenViewAction>
typealias LogViewerScreenViewModelType = StateStoreViewModelV2<LogViewerScreenViewState, LogViewerScreenViewAction>
class LogViewerScreenViewModel: LogViewerScreenViewModelType, LogViewerScreenViewModelProtocol {
private var actionsSubject: PassthroughSubject<LogViewerScreenViewModelAction, Never> = .init()
@@ -10,7 +10,7 @@ import QuickLook
import SwiftUI
struct LogViewerScreen: View {
@ObservedObject var context: LogViewerScreenViewModel.Context
let context: LogViewerScreenViewModel.Context
var body: some View {
PreviewView(urls: context.viewState.urls)
@@ -8,7 +8,7 @@
import Combine
import SwiftUI
typealias SecureBackupKeyBackupScreenViewModelType = StateStoreViewModel<SecureBackupKeyBackupScreenViewState, SecureBackupKeyBackupScreenViewAction>
typealias SecureBackupKeyBackupScreenViewModelType = StateStoreViewModelV2<SecureBackupKeyBackupScreenViewState, SecureBackupKeyBackupScreenViewAction>
class SecureBackupKeyBackupScreenViewModel: SecureBackupKeyBackupScreenViewModelType, SecureBackupKeyBackupScreenViewModelProtocol {
private let secureBackupController: SecureBackupControllerProtocol
@@ -10,7 +10,7 @@ import Compound
import SwiftUI
struct SecureBackupKeyBackupScreen: View {
@ObservedObject var context: SecureBackupKeyBackupScreenViewModel.Context
@Bindable var context: SecureBackupKeyBackupScreenViewModel.Context
var body: some View {
FullscreenDialog {
@@ -8,7 +8,7 @@
import Combine
import SwiftUI
typealias SecureBackupLogoutConfirmationScreenViewModelType = StateStoreViewModel<SecureBackupLogoutConfirmationScreenViewState, SecureBackupLogoutConfirmationScreenViewAction>
typealias SecureBackupLogoutConfirmationScreenViewModelType = StateStoreViewModelV2<SecureBackupLogoutConfirmationScreenViewState, SecureBackupLogoutConfirmationScreenViewAction>
class SecureBackupLogoutConfirmationScreenViewModel: SecureBackupLogoutConfirmationScreenViewModelType, SecureBackupLogoutConfirmationScreenViewModelProtocol {
private let secureBackupController: SecureBackupControllerProtocol
@@ -10,7 +10,7 @@ import Compound
import SwiftUI
struct SecureBackupLogoutConfirmationScreen: View {
@ObservedObject var context: SecureBackupLogoutConfirmationScreenViewModel.Context
@Bindable var context: SecureBackupLogoutConfirmationScreenViewModel.Context
var body: some View {
FullscreenDialog {
@@ -126,26 +126,26 @@ struct SecureBackupLogoutConfirmationScreen_Previews: PreviewProvider, TestableP
SecureBackupLogoutConfirmationScreen(context: waitingViewModel.context)
}
.previewDisplayName("Waiting")
.snapshotPreferences(expect: waitingViewModel.context.$viewState.map { $0.mode == .waitingToStart(hasStalled: false) })
.snapshotPreferences(expect: waitingViewModel.context.observe(\.viewState.mode).map { $0 == .waitingToStart(hasStalled: false) }.eraseToStream())
NavigationStack {
SecureBackupLogoutConfirmationScreen(context: ongoingViewModel.context)
}
.previewDisplayName("Ongoing")
.snapshotPreferences(expect: ongoingViewModel.context.$viewState.map { $0.mode == .backupOngoing(progress: 0.5) })
.snapshotPreferences(expect: ongoingViewModel.context.observe(\.viewState.mode).map { $0 == .backupOngoing(progress: 0.5) }.eraseToStream())
// Uses the same view model as Waiting but with a different expectation.
NavigationStack {
SecureBackupLogoutConfirmationScreen(context: waitingViewModel.context)
}
.previewDisplayName("Stalled")
.snapshotPreferences(expect: waitingViewModel.context.$viewState.map { $0.mode == .waitingToStart(hasStalled: true) })
.snapshotPreferences(expect: waitingViewModel.context.observe(\.viewState.mode).map { $0 == .waitingToStart(hasStalled: true) }.eraseToStream())
NavigationStack {
SecureBackupLogoutConfirmationScreen(context: offlineViewModel.context)
}
.previewDisplayName("Offline")
.snapshotPreferences(expect: offlineViewModel.context.$viewState.map { $0.mode == .offline })
.snapshotPreferences(expect: offlineViewModel.context.observe(\.viewState.mode).map { $0 == .offline }.eraseToStream())
}
static func makeViewModel(mode: SecureBackupLogoutConfirmationScreenViewMode) -> SecureBackupLogoutConfirmationScreenViewModel {
@@ -8,7 +8,7 @@
import Combine
import SwiftUI
typealias SecureBackupRecoveryKeyScreenViewModelType = StateStoreViewModel<SecureBackupRecoveryKeyScreenViewState, SecureBackupRecoveryKeyScreenViewAction>
typealias SecureBackupRecoveryKeyScreenViewModelType = StateStoreViewModelV2<SecureBackupRecoveryKeyScreenViewState, SecureBackupRecoveryKeyScreenViewAction>
class SecureBackupRecoveryKeyScreenViewModel: SecureBackupRecoveryKeyScreenViewModelType, SecureBackupRecoveryKeyScreenViewModelProtocol {
private let secureBackupController: SecureBackupControllerProtocol
@@ -62,7 +62,7 @@ class SecureBackupRecoveryKeyScreenViewModel: SecureBackupRecoveryKeyScreenViewM
switch await secureBackupController.confirmRecoveryKey(state.bindings.confirmationRecoveryKey) {
case .success:
actionsSubject.send(.done(mode: context.viewState.mode))
actionsSubject.send(.done(mode: state.mode))
case .failure(let error):
MXLog.error("Failed confirming recovery key with error: \(error)")
state.bindings.alertInfo = .init(id: .init(),
@@ -80,7 +80,7 @@ class SecureBackupRecoveryKeyScreenViewModel: SecureBackupRecoveryKeyScreenViewM
message: L10n.screenRecoveryKeySetupConfirmationDescription,
primaryButton: .init(title: L10n.actionContinue) { [weak self] in
guard let self else { return }
actionsSubject.send(.done(mode: context.viewState.mode))
actionsSubject.send(.done(mode: state.mode))
},
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
}
@@ -10,7 +10,7 @@ import Compound
import SwiftUI
struct SecureBackupRecoveryKeyScreen: View {
@ObservedObject var context: SecureBackupRecoveryKeyScreenViewModel.Context
@Bindable var context: SecureBackupRecoveryKeyScreenViewModel.Context
@FocusState private var focused
private let textFieldIdentifier = "textFieldIdentifier"
@@ -260,9 +260,7 @@ struct SecureBackupRecoveryKeyScreen_Previews: PreviewProvider, TestablePreview
NavigationStack {
SecureBackupRecoveryKeyScreen(context: setupViewModel.context)
}
.snapshotPreferences(expect: setupViewModel.context.$viewState.map { state in
state.recoveryKey != nil
})
.snapshotPreferences(expect: setupViewModel.context.observe(\.viewState.recoveryKey).map { $0 != nil }.eraseToStream())
.previewDisplayName("Set up")
NavigationStack {
@@ -8,7 +8,7 @@
import Combine
import SwiftUI
typealias SecureBackupScreenViewModelType = StateStoreViewModel<SecureBackupScreenViewState, SecureBackupScreenViewAction>
typealias SecureBackupScreenViewModelType = StateStoreViewModelV2<SecureBackupScreenViewState, SecureBackupScreenViewAction>
class SecureBackupScreenViewModel: SecureBackupScreenViewModelType, SecureBackupScreenViewModelProtocol {
private let secureBackupController: SecureBackupControllerProtocol
@@ -10,7 +10,7 @@ import Compound
import SwiftUI
struct SecureBackupScreen: View {
@ObservedObject var context: SecureBackupScreenViewModel.Context
@Bindable var context: SecureBackupScreenViewModel.Context
var body: some View {
Form {
@@ -128,33 +128,25 @@ struct SecureBackupScreen_Previews: PreviewProvider, TestablePreview {
NavigationStack {
SecureBackupScreen(context: bothSetupViewModel.context)
}
.snapshotPreferences(expect: bothSetupViewModel.context.$viewState.map { state in
state.keyBackupState == .enabled
})
.snapshotPreferences(expect: bothSetupViewModel.context.observe(\.viewState.keyBackupState).map { $0 == .enabled }.eraseToStream())
.previewDisplayName("Both setup")
NavigationStack {
SecureBackupScreen(context: onlyKeyBackupSetUpViewModel.context)
}
.snapshotPreferences(expect: onlyKeyBackupSetUpViewModel.context.$viewState.map { state in
state.keyBackupState == .enabled
})
.snapshotPreferences(expect: onlyKeyBackupSetUpViewModel.context.observe(\.viewState.keyBackupState).map { $0 == .enabled }.eraseToStream())
.previewDisplayName("Only key backup setup")
NavigationStack {
SecureBackupScreen(context: keyBackupDisabledViewModel.context)
}
.snapshotPreferences(expect: keyBackupDisabledViewModel.context.$viewState.map { state in
state.keyBackupState == .unknown
})
.snapshotPreferences(expect: keyBackupDisabledViewModel.context.observe(\.viewState.keyBackupState).map { $0 == .unknown }.eraseToStream())
.previewDisplayName("Key backup disabled")
NavigationStack {
SecureBackupScreen(context: recoveryIncompleteViewModel.context)
}
.snapshotPreferences(expect: recoveryIncompleteViewModel.context.$viewState.map { state in
state.recoveryState == .incomplete
})
.snapshotPreferences(expect: recoveryIncompleteViewModel.context.observe(\.viewState.recoveryState).map { $0 == .incomplete }.eraseToStream())
.previewDisplayName("Recovery incomplete")
}
@@ -8,7 +8,7 @@
import Combine
import SwiftUI
typealias AnalyticsSettingsScreenViewModelType = StateStoreViewModel<AnalyticsSettingsScreenViewState, AnalyticsSettingsScreenViewAction>
typealias AnalyticsSettingsScreenViewModelType = StateStoreViewModelV2<AnalyticsSettingsScreenViewState, AnalyticsSettingsScreenViewAction>
class AnalyticsSettingsScreenViewModel: AnalyticsSettingsScreenViewModelType, AnalyticsSettingsScreenViewModelProtocol {
private let analytics: AnalyticsService
@@ -9,7 +9,7 @@ import Compound
import SwiftUI
struct AnalyticsSettingsScreen: View {
@ObservedObject var context: AnalyticsSettingsScreenViewModel.Context
@Bindable var context: AnalyticsSettingsScreenViewModel.Context
var body: some View {
Form {
@@ -8,7 +8,7 @@
import Combine
import SwiftUI
typealias DeactivateAccountScreenViewModelType = StateStoreViewModel<DeactivateAccountScreenViewState, DeactivateAccountScreenViewAction>
typealias DeactivateAccountScreenViewModelType = StateStoreViewModelV2<DeactivateAccountScreenViewState, DeactivateAccountScreenViewAction>
class DeactivateAccountScreenViewModel: DeactivateAccountScreenViewModelType, DeactivateAccountScreenViewModelProtocol {
private let clientProxy: ClientProxyProtocol
@@ -9,7 +9,7 @@ import Compound
import SwiftUI
struct DeactivateAccountScreen: View {
@ObservedObject var context: DeactivateAccountScreenViewModel.Context
@Bindable var context: DeactivateAccountScreenViewModel.Context
var body: some View {
Form {
@@ -8,7 +8,7 @@
import Combine
import SwiftUI
typealias LegalInformationScreenViewModelType = StateStoreViewModel<LegalInformationScreenViewState, LegalInformationScreenViewAction>
typealias LegalInformationScreenViewModelType = StateStoreViewModelV2<LegalInformationScreenViewState, LegalInformationScreenViewAction>
class LegalInformationScreenViewModel: LegalInformationScreenViewModelType, LegalInformationScreenViewModelProtocol {
init(appSettings: AppSettings) {
@@ -9,7 +9,7 @@ import Compound
import SwiftUI
struct LegalInformationScreen: View {
@ObservedObject var context: LegalInformationScreenViewModel.Context
let context: LegalInformationScreenViewModel.Context
@Environment(\.openURL) private var openURL
var body: some View {
@@ -8,7 +8,7 @@
import Combine
import SwiftUI
typealias NotificationSettingsEditScreenViewModelType = StateStoreViewModel<NotificationSettingsEditScreenViewState, NotificationSettingsEditScreenViewAction>
typealias NotificationSettingsEditScreenViewModelType = StateStoreViewModelV2<NotificationSettingsEditScreenViewState, NotificationSettingsEditScreenViewAction>
class NotificationSettingsEditScreenViewModel: NotificationSettingsEditScreenViewModelType, NotificationSettingsEditScreenViewModelProtocol {
private var actionsSubject: PassthroughSubject<NotificationSettingsEditScreenViewModelAction, Never> = .init()
@@ -9,7 +9,7 @@ import Compound
import SwiftUI
struct NotificationSettingsEditScreen: View {
@ObservedObject var context: NotificationSettingsEditScreenViewModel.Context
@Bindable var context: NotificationSettingsEditScreenViewModel.Context
var body: some View {
Form {
@@ -8,7 +8,7 @@
import Combine
import SwiftUI
typealias NotificationSettingsScreenViewModelType = StateStoreViewModel<NotificationSettingsScreenViewState, NotificationSettingsScreenViewAction>
typealias NotificationSettingsScreenViewModelType = StateStoreViewModelV2<NotificationSettingsScreenViewState, NotificationSettingsScreenViewAction>
class NotificationSettingsScreenViewModel: NotificationSettingsScreenViewModelType, NotificationSettingsScreenViewModelProtocol {
private var actionsSubject: PassthroughSubject<NotificationSettingsScreenViewModelAction, Never> = .init()
@@ -9,7 +9,7 @@ import Compound
import SwiftUI
struct NotificationSettingsScreen: View {
@ObservedObject var context: NotificationSettingsScreenViewModel.Context
@Bindable var context: NotificationSettingsScreenViewModel.Context
var body: some View {
Form {
@@ -254,14 +254,10 @@ struct NotificationSettingsScreen_Previews: PreviewProvider, TestablePreview {
static var previews: some View {
NotificationSettingsScreen(context: viewModel.context)
.snapshotPreferences(expect: viewModel.context.$viewState.map { state in
state.settings != nil
})
.snapshotPreferences(expect: viewModel.context.observe(\.viewState.settings).map { $0 != nil }.eraseToStream())
NotificationSettingsScreen(context: viewModelConfigurationMismatch.context)
.snapshotPreferences(expect: viewModelConfigurationMismatch.context.$viewState.map { state in
state.settings != nil
})
.snapshotPreferences(expect: viewModelConfigurationMismatch.context.observe(\.viewState.settings).map { $0 != nil }.eraseToStream())
.previewDisplayName("Configuration mismatch")
}
}
@@ -8,7 +8,7 @@
import Combine
import SwiftUI
typealias UserDetailsEditScreenViewModelType = StateStoreViewModel<UserDetailsEditScreenViewState, UserDetailsEditScreenViewAction>
typealias UserDetailsEditScreenViewModelType = StateStoreViewModelV2<UserDetailsEditScreenViewState, UserDetailsEditScreenViewAction>
class UserDetailsEditScreenViewModel: UserDetailsEditScreenViewModelType, UserDetailsEditScreenViewModelProtocol {
private let actionsSubject: PassthroughSubject<UserDetailsEditScreenViewModelAction, Never> = .init()
@@ -9,7 +9,7 @@ import Compound
import SwiftUI
struct UserDetailsEditScreen: View {
@ObservedObject var context: UserDetailsEditScreenViewModel.Context
@Bindable var context: UserDetailsEditScreenViewModel.Context
@FocusState private var focus: Bool
var body: some View {
@@ -8,7 +8,7 @@
<key>FooterText</key>
<string>The MIT License (MIT)
Copyright (c) 2015-2024 Sentry
Copyright (c) 2015 Sentry
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -27,7 +27,6 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
</string>
<key>Type</key>
<string>PSGroupSpecifier</string>