Signal-iOS/Signal/src/ViewControllers/AppSettings/Internal/InternalSettingsViewController.swift
2026-05-22 15:46:13 -07:00

280 lines
12 KiB
Swift

//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import AVFoundation
import SignalServiceKit
import SignalUI
class InternalSettingsViewController: OWSTableViewController2 {
enum Mode: Equatable {
case registration
case standard
}
private let mode: Mode
init(
mode: Mode = .standard,
) {
self.mode = mode
super.init()
}
override func viewDidLoad() {
super.viewDidLoad()
title = "Internal"
updateTableContents()
}
func updateTableContents() {
let contents = OWSTableContents()
let debugSection = OWSTableSection()
#if USE_DEBUG_UI
debugSection.add(.disclosureItem(
withText: "Debug UI",
actionBlock: { [weak self] in
guard let self else { return }
DebugUITableViewController.presentDebugUI(
fromViewController: self,
thread: nil,
)
},
))
#endif
debugSection.add(.disclosureItem(
withText: "Remote Configs",
actionBlock: { [weak self] in
let vc = FlagsViewController()
self?.navigationController?.pushViewController(vc, animated: true)
},
))
debugSection.add(.disclosureItem(
withText: "Testing",
actionBlock: { [weak self] in
let vc = TestingViewController()
self?.navigationController?.pushViewController(vc, animated: true)
},
))
debugSection.add(.actionItem(
withText: "Export Database",
actionBlock: { [weak self] in
guard let self else {
return
}
SignalApp.shared.showExportDatabaseUI(from: self)
},
))
debugSection.add(.actionItem(
withText: "Query Database",
actionBlock: { [weak self] in
let vc = InternalSQLClientViewController()
self?.navigationController?.pushViewController(vc, animated: true)
},
))
debugSection.add(.disclosureItem(
withText: "Backups",
actionBlock: { [weak self] in
let vc = InternalBackupSettingsViewController()
self?.navigationController?.pushViewController(vc, animated: true)
},
))
if mode == .registration {
debugSection.add(.actionItem(withText: "Submit debug logs") {
DebugLogs.submitLogs(supportTag: "Registration", dumper: .fromGlobals())
})
}
contents.add(debugSection)
let (
contactThreadCount,
groupThreadCount,
messageCount,
attachmentCount,
donationSubscriberID,
storageServiceManifestVersion,
aciRegistrationId,
pniRegistrationId,
) = SSKEnvironment.shared.databaseStorageRef.read { tx in
return (
TSThread.anyFetchAll(transaction: tx).filter { !$0.isGroupThread }.count,
TSThread.anyFetchAll(transaction: tx).filter { $0.isGroupThread }.count,
TSInteraction.anyCount(transaction: tx),
try? Attachment.Record.fetchCount(tx.database),
DependenciesBridge.shared.donationSubscriptionManager.getSubscriberID(tx: tx),
SSKEnvironment.shared.storageServiceManagerRef.currentManifestVersion(tx: tx),
DependenciesBridge.shared.tsAccountManager.getRegistrationId(for: .aci, tx: tx),
DependenciesBridge.shared.tsAccountManager.getRegistrationId(for: .pni, tx: tx),
)
}
let regSection = OWSTableSection(title: "Account")
let localIdentifiers = DependenciesBridge.shared.tsAccountManager.localIdentifiersWithMaybeSneakyTransaction
regSection.add(.copyableItem(label: "Phone Number", value: localIdentifiers?.phoneNumber))
regSection.add(.copyableItem(label: "ACI", value: localIdentifiers?.aci.serviceIdString))
regSection.add(.copyableItem(label: "PNI", value: localIdentifiers?.pni?.serviceIdString))
regSection.add(.copyableItem(label: "Device ID", value: "\(DependenciesBridge.shared.tsAccountManager.storedDeviceIdWithMaybeTransaction)"))
regSection.add(.copyableItem(label: "ACI Registration ID", value: aciRegistrationId.map({ "\($0)" }) ?? "<missing>"))
regSection.add(.copyableItem(label: "PNI Registration ID", value: pniRegistrationId.map({ "\($0)" }) ?? "<missing>"))
regSection.add(.copyableItem(label: "Push Token", value: SSKEnvironment.shared.preferencesRef.pushToken))
regSection.add(.copyableItem(label: "Profile Key", value: SSKEnvironment.shared.databaseStorageRef.read(block: SSKEnvironment.shared.profileManagerRef.localUserProfile(tx:))?.profileKey?.keyData.hexadecimalString ?? "none"))
if let donationSubscriberID {
regSection.add(.copyableItem(label: "Donation Subscriber ID", value: donationSubscriberID.asBase64Url))
}
contents.add(regSection)
let buildSection = OWSTableSection(title: "Build")
buildSection.add(.copyableItem(label: "Environment", value: TSConstants.isUsingProductionService ? "Production" : "Staging"))
buildSection.add(.copyableItem(label: "Variant", value: BuildFlags.buildVariantString))
buildSection.add(.copyableItem(label: "Current Version", value: AppVersionImpl.shared.currentAppVersion))
buildSection.add(.copyableItem(label: "First Version", value: AppVersionImpl.shared.firstAppVersion))
if let buildDetails = Bundle.main.object(forInfoDictionaryKey: "BuildDetails") as? [String: AnyObject] {
if let signalCommit = (buildDetails["SignalCommit"] as? String)?.strippedOrNil?.prefix(12) {
buildSection.add(.copyableItem(label: "Git Commit", value: String(signalCommit)))
}
}
contents.add(buildSection)
// format counts with thousands separator
let numberFormatter = NumberFormatter()
numberFormatter.formatterBehavior = .behavior10_4
numberFormatter.numberStyle = .decimal
let dbSection = OWSTableSection(title: "Database")
dbSection.add(.copyableItem(label: "Contact Threads", value: numberFormatter.string(for: contactThreadCount)))
dbSection.add(.copyableItem(label: "Group Threads", value: numberFormatter.string(for: groupThreadCount)))
dbSection.add(.copyableItem(label: "Messages", value: numberFormatter.string(for: messageCount)))
dbSection.add(.copyableItem(label: "Attachments", value: numberFormatter.string(for: attachmentCount)))
dbSection.add(.actionItem(
withText: "Disk Usage",
actionBlock: { [weak self] in
ModalActivityIndicatorViewController.present(
fromViewController: self!,
asyncBlock: { [weak self] modal in
let vc = await InternalDiskUsageViewController.build()
self?.navigationController?.pushViewController(vc, animated: true)
modal.dismiss(animated: true)
},
)
},
))
contents.add(dbSection)
let deviceSection = OWSTableSection(title: "Device")
deviceSection.add(.copyableItem(label: "Model", value: AppVersionImpl.shared.hardwareInfoString))
deviceSection.add(.copyableItem(label: "iOS Version", value: AppVersionImpl.shared.iosVersionString))
let memoryUsage = LocalDevice.currentMemoryStatus(forceUpdate: true)?.footprint
let memoryUsageString = memoryUsage.map { ByteCountFormatter.string(fromByteCount: Int64($0), countStyle: .memory) }
deviceSection.add(.copyableItem(label: "Memory Usage", value: memoryUsageString))
deviceSection.add(.copyableItem(label: "Locale Identifier", value: Locale.current.identifier.nilIfEmpty))
deviceSection.add(.copyableItem(label: "Language Code", value: Locale.current.languageCode?.nilIfEmpty))
deviceSection.add(.copyableItem(label: "Region Code", value: Locale.current.regionCode?.nilIfEmpty))
deviceSection.add(.copyableItem(label: "Currency Code", value: Locale.current.currencyCode?.nilIfEmpty))
contents.add(deviceSection)
let otherSection = OWSTableSection(title: "Other")
otherSection.add(.copyableItem(label: "Storage Service Manifest Version", value: "\(storageServiceManifestVersion)"))
otherSection.add(.copyableItem(label: "CC?", value: SSKEnvironment.shared.signalServiceRef.isCensorshipCircumventionActive ? "Yes" : "No"))
otherSection.add(.copyableItem(label: "Audio Category", value: AVAudioSession.sharedInstance().category.rawValue.replacingOccurrences(of: "AVAudioSessionCategory", with: "")))
otherSection.add(.switch(
withText: "Spinning checkmarks",
isOn: { InMemorySettings.spinningCheckmarks },
target: self,
selector: #selector(spinCheckmarks(_:)),
))
otherSection.add(.switch(
withText: "Spinning conversation title",
isOn: { InMemorySettings.spinningConversationTitle },
actionBlock: { _ in
InMemorySettings.spinningConversationTitle.toggle()
},
))
otherSection.add(.switch(
withText: "Force call quality survey",
isOn: { InMemorySettings.forceCallQualitySurvey },
actionBlock: { _ in
InMemorySettings.forceCallQualitySurvey.toggle()
},
))
if #available(iOS 26, *) {
otherSection.add(.switch(
withText: "Disable Content Tracking in Chat Header",
isOn: { self.isChatHeaderContentTrackingDisabled },
target: self,
selector: #selector(toggleChatHeaderContentTrackingDisabled(sender:)),
))
}
contents.add(otherSection)
if mode != .registration {
let paymentsSection = OWSTableSection(title: "Payments")
paymentsSection.add(.copyableItem(label: "MobileCoin Environment", value: MobileCoinAPI.Environment.current.description))
paymentsSection.add(.copyableItem(label: "Enabled?", value: SSKEnvironment.shared.paymentsHelperRef.arePaymentsEnabled ? "Yes" : "No"))
if SSKEnvironment.shared.paymentsHelperRef.arePaymentsEnabled, let paymentsEntropy = SUIEnvironment.shared.paymentsSwiftRef.paymentsEntropy {
paymentsSection.add(.copyableItem(label: "Entropy", value: paymentsEntropy.hexadecimalString))
if let passphrase = SUIEnvironment.shared.paymentsSwiftRef.passphrase {
paymentsSection.add(.copyableItem(label: "Mnemonic", value: passphrase.asPassphrase))
}
if let walletAddressBase58 = SUIEnvironment.shared.paymentsSwiftRef.walletAddressBase58() {
paymentsSection.add(.copyableItem(label: "B58", value: walletAddressBase58))
}
}
contents.add(paymentsSection)
}
self.contents = contents
}
}
// MARK: -
public enum InMemorySettings {
static var spinningCheckmarks = false
static var spinningConversationTitle = false
static var forceCallQualitySurvey = false
}
private extension InternalSettingsViewController {
var isChatHeaderContentTrackingDisabled: Bool {
CurrentAppContext().appUserDefaults().bool(forKey: "DisableChatHeaderContentTracking")
}
@objc
func toggleChatHeaderContentTrackingDisabled(sender: Any) {
guard let toggleSwitch = sender as? UISwitch else { return }
let isDisabled = toggleSwitch.isOn
CurrentAppContext().appUserDefaults().set(isDisabled, forKey: "DisableChatHeaderContentTracking")
}
}
private extension InternalSettingsViewController {
@objc
func spinCheckmarks(_ sender: Any) {
let wasSpinning = InMemorySettings.spinningCheckmarks
if let view = sender as? UIView {
if wasSpinning {
view.layer.removeAnimation(forKey: "spin")
} else {
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
animation.toValue = NSNumber(value: Double.pi * 2)
animation.duration = TimeInterval.second
animation.isCumulative = true
animation.repeatCount = .greatestFiniteMagnitude
view.layer.add(animation, forKey: "spin")
}
}
InMemorySettings.spinningCheckmarks = !wasSpinning
}
}