Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/Signal/src/ViewControllers/AppSettings/Privacy/PrivacySettingsViewController.swift
1 views
//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import SignalServiceKit
import SignalUI

class PrivacySettingsViewController: OWSTableViewController2 {
    override func viewDidLoad() {
        super.viewDidLoad()

        title = OWSLocalizedString("SETTINGS_PRIVACY_TITLE", comment: "")

        updateTableContents()

        NotificationCenter.default.addObserver(
            self,
            selector: #selector(updateTableContents),
            name: .syncManagerConfigurationSyncDidComplete,
            object: nil,
        )
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        updateTableContents()
    }

    @objc
    private func updateTableContents() {
        let contents = OWSTableContents()

        let whoCanSection = OWSTableSection()

        whoCanSection.add(.disclosureItem(
            withText: OWSLocalizedString(
                "SETTINGS_PHONE_NUMBER_PRIVACY_TITLE",
                comment: "The title for phone number privacy settings.",
            ),
            actionBlock: { [weak self] in
                let vc = PhoneNumberPrivacySettingsViewController()
                self?.navigationController?.pushViewController(vc, animated: true)
            },
        ))
        whoCanSection.footerTitle = OWSLocalizedString(
            "SETTINGS_PHONE_NUMBER_PRIVACY_DESCRIPTION_LABEL",
            comment: "Description label for Phone Number Privacy",
        )

        if !whoCanSection.items.isEmpty {
            contents.add(whoCanSection)
        }

        let blockedSection = OWSTableSection()
        blockedSection.add(.disclosureItem(
            withText: OWSLocalizedString(
                "SETTINGS_BLOCK_LIST_TITLE",
                comment: "Label for the block list section of the settings view",
            ),
            actionBlock: { [weak self] in
                let vc = BlockListViewController()
                self?.navigationController?.pushViewController(vc, animated: true)
            },
        ))
        contents.add(blockedSection)

        let messagingSection = OWSTableSection()
        messagingSection.footerTitle = OWSLocalizedString(
            "SETTINGS_MESSAGING_FOOTER",
            comment: "Explanation for the 'messaging' privacy settings.",
        )
        messagingSection.add(.switch(
            withText: OWSLocalizedString(
                "SETTINGS_READ_RECEIPT",
                comment: "Label for the 'read receipts' setting.",
            ),
            isOn: {
                let databaseStorage = SSKEnvironment.shared.databaseStorageRef
                return databaseStorage.read(block: OWSReceiptManager.areReadReceiptsEnabled(transaction:))
            },
            target: self,
            selector: #selector(didToggleReadReceiptsSwitch),
        ))
        messagingSection.add(.switch(
            withText: OWSLocalizedString(
                "SETTINGS_TYPING_INDICATORS",
                comment: "Label for the 'typing indicators' setting.",
            ),
            isOn: { SSKEnvironment.shared.typingIndicatorsRef.areTypingIndicatorsEnabled() },
            target: self,
            selector: #selector(didToggleTypingIndicatorsSwitch),
        ))
        contents.add(messagingSection)

        let disappearingMessagesSection = OWSTableSection()
        disappearingMessagesSection.footerTitle = OWSLocalizedString(
            "SETTINGS_DISAPPEARING_MESSAGES_FOOTER",
            comment: "Explanation for the 'disappearing messages' privacy settings.",
        )
        let disappearingMessagesConfiguration = SSKEnvironment.shared.databaseStorageRef.read { tx in
            let dmConfigurationStore = DependenciesBridge.shared.disappearingMessagesConfigurationStore
            return dmConfigurationStore.fetchOrBuildDefault(for: .universal, tx: tx)
        }
        disappearingMessagesSection.add(.init(
            customCellBlock: { [weak self] in
                guard let self else { return UITableViewCell() }
                let cell = OWSTableItem.buildCell(
                    itemName: OWSLocalizedString(
                        "SETTINGS_DISAPPEARING_MESSAGES",
                        comment: "Label for the 'disappearing messages' privacy settings.",
                    ),
                    accessoryText: disappearingMessagesConfiguration.isEnabled
                        ? DateUtil.formatDuration(seconds: disappearingMessagesConfiguration.durationSeconds, useShortFormat: true)
                        : CommonStrings.switchOff,
                    accessoryType: .disclosureIndicator,
                    accessibilityIdentifier: UIView.accessibilityIdentifier(in: self, name: "disappearing_messages"),
                )
                return cell
            },
            actionBlock: { [weak self] in
                let vc = DisappearingMessagesTimerSettingsViewController(
                    initialConfiguration: disappearingMessagesConfiguration,
                    settingsMode: .universal,
                ) { configuration in
                    if self != nil {
                        SSKEnvironment.shared.databaseStorageRef.write { transaction in
                            var configuration = configuration
                            if configuration.id == nil {
                                failIfThrows {
                                    try configuration.insert(transaction.database)
                                }
                            } else {
                                failIfThrows {
                                    try configuration.update(transaction.database)
                                }
                            }
                        }
                        SSKEnvironment.shared.storageServiceManagerRef.recordPendingLocalAccountUpdates()
                    }
                    self?.updateTableContents()
                }
                self?.presentFormSheet(OWSNavigationController(rootViewController: vc), animated: true)
            },
        ))
        contents.add(disappearingMessagesSection)

        let appSecuritySection = OWSTableSection()
        appSecuritySection.headerTitle = OWSLocalizedString("SETTINGS_SECURITY_TITLE", comment: "Section header")

        switch DeviceOwnerAuthenticationType.current {
        case .unknown:
            appSecuritySection.footerTitle = OWSLocalizedString("SETTINGS_SECURITY_DETAIL", comment: "Section footer")
        case .passcode:
            appSecuritySection.footerTitle = OWSLocalizedString("SETTINGS_SECURITY_DETAIL_PASSCODE", comment: "Section footer")
        case .faceId:
            appSecuritySection.footerTitle = OWSLocalizedString("SETTINGS_SECURITY_DETAIL_FACEID", comment: "Section footer")
        case .touchId:
            appSecuritySection.footerTitle = OWSLocalizedString("SETTINGS_SECURITY_DETAIL_TOUCHID", comment: "Section footer")
        case .opticId:
            appSecuritySection.footerTitle = OWSLocalizedString("SETTINGS_SECURITY_DETAIL_OPTICID", comment: "Section footer")
        }

        appSecuritySection.add(.switch(
            withText: OWSLocalizedString("SETTINGS_SCREEN_SECURITY", comment: ""),
            isOn: { SSKEnvironment.shared.preferencesRef.isScreenSecurityEnabled },
            target: self,
            selector: #selector(didToggleScreenSecuritySwitch),
        ))
        appSecuritySection.add(.switch(
            withText: OWSLocalizedString(
                "SETTINGS_SCREEN_LOCK_SWITCH_LABEL",
                comment: "Label for the 'enable screen lock' switch of the privacy settings.",
            ),
            isOn: { ScreenLock.shared.isScreenLockEnabled() },
            target: self,
            selector: #selector(didToggleScreenLockSwitch),
        ))
        if ScreenLock.shared.isScreenLockEnabled() {
            appSecuritySection.add(.disclosureItem(
                withText: OWSLocalizedString(
                    "SETTINGS_SCREEN_LOCK_ACTIVITY_TIMEOUT",
                    comment: "Label for the 'screen lock activity timeout' setting of the privacy settings.",
                ),
                accessoryText: formatScreenLockTimeout(ScreenLock.shared.screenLockTimeout()),
                actionBlock: { [weak self] in
                    self?.showScreenLockTimeoutPicker()
                },
            ))
        }
        contents.add(appSecuritySection)

        // Payments
        let paymentsSection = OWSTableSection()
        paymentsSection.headerTitle = OWSLocalizedString("SETTINGS_PAYMENTS_SECURITY_TITLE", comment: "Title for the payments section in the app’s privacy settings tableview")

        switch DeviceOwnerAuthenticationType.current {
        case .unknown:
            paymentsSection.footerTitle = OWSLocalizedString("SETTINGS_PAYMENTS_SECURITY_DETAIL", comment: "Caption for footer label beneath the payments lock privacy toggle for a biometry type that is unknown.")
        case .passcode:
            paymentsSection.footerTitle = OWSLocalizedString("SETTINGS_PAYMENTS_SECURITY_DETAIL_PASSCODE", comment: "Caption for footer label beneath the payments lock privacy toggle for a biometry type that is a passcode.")
        case .faceId:
            paymentsSection.footerTitle = OWSLocalizedString("SETTINGS_PAYMENTS_SECURITY_DETAIL_FACEID", comment: "Caption for footer label beneath the payments lock privacy toggle for faceid biometry.")
        case .touchId:
            paymentsSection.footerTitle = OWSLocalizedString("SETTINGS_PAYMENTS_SECURITY_DETAIL_TOUCHID", comment: "Caption for footer label beneath the payments lock privacy toggle for touchid biometry")
        case .opticId:
            paymentsSection.footerTitle = OWSLocalizedString("SETTINGS_PAYMENTS_SECURITY_DETAIL_OPTICID", comment: "Caption for footer label beneath the payments lock privacy toggle for opticid biometry")
        }

        paymentsSection.add(.switch(
            withText: OWSLocalizedString(
                "SETTINGS_PAYMENTS_LOCK_SWITCH_LABEL",
                comment: "Label for UISwitch based payments-lock setting that when enabled requires biometric-authentication (or passcode) to transfer funds or view the recovery phrase.",
            ),
            isOn: { SSKEnvironment.shared.owsPaymentsLockRef.isPaymentsLockEnabled() },
            target: self,
            selector: #selector(didTogglePaymentsLockSwitch),
        ))
        contents.add(paymentsSection)

        let callsSection = OWSTableSection()
        callsSection.headerTitle = OWSLocalizedString(
            "SETTINGS_SECTION_TITLE_CALLING",
            comment: "settings topic header for table section",
        )
        callsSection.footerTitle = OWSLocalizedString(
            "SETTINGS_SECTION_FOOTER_CALLING",
            comment: "Footer for table section",
        )
        callsSection.add(.switch(
            withText: OWSLocalizedString(
                "SETTINGS_PRIVACY_CALLKIT_SYSTEM_CALL_LOG_PREFERENCE_TITLE",
                comment: "Short table cell label",
            ),
            isOn: {
                return SSKEnvironment.shared.databaseStorageRef.read { tx in
                    SSKEnvironment.shared.preferencesRef.isSystemCallLogEnabledOrDefault(tx: tx)
                }
            },
            target: self,
            selector: #selector(didToggleEnableSystemCallLogSwitch),
        ))
        contents.add(callsSection)

        let advancedSection = OWSTableSection()
        advancedSection.footerTitle = OWSLocalizedString(
            "SETTINGS_PRIVACY_ADVANCED_FOOTER",
            comment: "Footer for table section",
        )
        advancedSection.add(.disclosureItem(
            withText: OWSLocalizedString(
                "SETTINGS_PRIVACY_ADVANCED_TITLE",
                comment: "Title for the advanced privacy settings",
            ),
            actionBlock: { [weak self] in
                let vc = AdvancedPrivacySettingsViewController()
                self?.navigationController?.pushViewController(vc, animated: true)
            },
        ))
        contents.add(advancedSection)

        self.contents = contents
    }

    @objc
    private func didToggleReadReceiptsSwitch(_ sender: UISwitch) {
        SSKEnvironment.shared.receiptManagerRef.setAreReadReceiptsEnabledWithSneakyTransactionAndSyncConfiguration(sender.isOn)
    }

    @objc
    private func didToggleTypingIndicatorsSwitch(_ sender: UISwitch) {
        SSKEnvironment.shared.typingIndicatorsRef.setTypingIndicatorsEnabledAndSendSyncMessage(value: sender.isOn)
    }

    @objc
    private func didToggleScreenSecuritySwitch(_ sender: UISwitch) {
        SSKEnvironment.shared.preferencesRef.setIsScreenSecurityEnabled(sender.isOn)
    }

    @objc
    private func didToggleScreenLockSwitch(_ sender: UISwitch) {
        ScreenLock.shared.setIsScreenLockEnabled(sender.isOn)
        updateTableContents()
    }

    @objc
    private func didTogglePaymentsLockSwitch(_ sender: UISwitch) {
        // Require unlock to disable payments lock
        if SSKEnvironment.shared.owsPaymentsLockRef.isPaymentsLockEnabled() {
            SSKEnvironment.shared.owsPaymentsLockRef.tryToUnlock { [weak self] outcome in
                guard let self else { return }
                guard case .success = outcome else {
                    self.updateTableContents()
                    PaymentActionSheets.showBiometryAuthFailedActionSheet()
                    return
                }
                SSKEnvironment.shared.databaseStorageRef.write { transaction in
                    SSKEnvironment.shared.owsPaymentsLockRef.setIsPaymentsLockEnabled(false, transaction: transaction)
                }
                self.updateTableContents()
            }
        } else {
            SSKEnvironment.shared.databaseStorageRef.write { transaction in
                SSKEnvironment.shared.owsPaymentsLockRef.setIsPaymentsLockEnabled(true, transaction: transaction)
            }
            self.updateTableContents()
        }
    }

    private func showScreenLockTimeoutPicker() {
        let actionSheet = ActionSheetController(title: OWSLocalizedString(
            "SETTINGS_SCREEN_LOCK_ACTIVITY_TIMEOUT",
            comment: "Label for the 'screen lock activity timeout' setting of the privacy settings.",
        ))

        for timeout in ScreenLock.shared.screenLockTimeouts {
            actionSheet.addAction(.init(
                title: formatScreenLockTimeout(timeout, useShortFormat: false),
                handler: { [weak self] _ in
                    ScreenLock.shared.setScreenLockTimeout(timeout)
                    self?.updateTableContents()
                },
            ))
        }

        actionSheet.addAction(OWSActionSheets.cancelAction)

        presentActionSheet(actionSheet)
    }

    private func formatScreenLockTimeout(_ value: TimeInterval, useShortFormat: Bool = true) -> String {
        guard value > 0 else {
            return OWSLocalizedString(
                "SCREEN_LOCK_ACTIVITY_TIMEOUT_NONE",
                comment: "Indicates a delay of zero seconds, and that 'screen lock activity' will timeout immediately.",
            )
        }
        return DateUtil.formatDuration(seconds: UInt32(value), useShortFormat: useShortFormat)
    }

    @objc
    private func didToggleEnableSystemCallLogSwitch(_ sender: UISwitch) {
        let callService = AppEnvironment.shared.callService!
        let db = DependenciesBridge.shared.db
        let preferences = SSKEnvironment.shared.preferencesRef

        db.write { tx in
            preferences.setIsSystemCallLogEnabled(sender.isOn, tx: tx)
        }

        // rebuild callUIAdapter since CallKit configuration changed.
        callService.rebuildCallUIAdapter()
    }
}