470 lines
20 KiB
Swift
470 lines
20 KiB
Swift
//
|
|
// Copyright 2021 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import AVFoundation
|
|
import GRDB
|
|
import LibSignalClient
|
|
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
|
|
|
|
if DebugFlags.audibleErrorLogging {
|
|
debugSection.add(.disclosureItem(
|
|
withText: OWSLocalizedString("SETTINGS_ADVANCED_VIEW_ERROR_LOG", comment: ""),
|
|
actionBlock: { [weak self] in
|
|
Logger.flush()
|
|
let vc = LogPickerViewController(logDirUrl: DebugLogger.errorLogsDir)
|
|
self?.navigationController?.pushViewController(vc, animated: true)
|
|
},
|
|
))
|
|
}
|
|
|
|
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(.actionItem(
|
|
withText: "Run Database Integrity Checks",
|
|
actionBlock: { [weak self] in
|
|
guard let self else { return }
|
|
|
|
ModalActivityIndicatorViewController.present(
|
|
fromViewController: self,
|
|
) { modal in
|
|
let databaseStorage = SSKEnvironment.shared.databaseStorageRef
|
|
let integrityCheckResult = GRDBDatabaseStorageAdapter.checkIntegrity(
|
|
databaseStorage: databaseStorage,
|
|
)
|
|
|
|
modal.dismiss {
|
|
switch integrityCheckResult {
|
|
case .ok:
|
|
self.presentToast(text: "Integrity check: ok! More detail in logs.")
|
|
case .notOk:
|
|
self.presentToast(text: "Integrity check: not ok! More detail in logs.")
|
|
}
|
|
}
|
|
}
|
|
},
|
|
))
|
|
debugSection.add(.actionItem(
|
|
withText: "Clean Orphaned Data",
|
|
actionBlock: { [weak self] in
|
|
guard let self else { return }
|
|
ModalActivityIndicatorViewController.present(
|
|
fromViewController: self,
|
|
canCancel: false,
|
|
asyncBlock: { modalActivityIndicator in
|
|
try? await OWSOrphanDataCleaner.cleanUp(shouldRemoveOrphanedData: true)
|
|
modalActivityIndicator.dismiss()
|
|
},
|
|
)
|
|
},
|
|
))
|
|
|
|
if mode == .registration {
|
|
debugSection.add(.actionItem(withText: "Submit debug logs") {
|
|
DebugLogs.submitLogs(supportTag: "Registration", dumper: .fromGlobals())
|
|
})
|
|
}
|
|
|
|
contents.add(debugSection)
|
|
|
|
let backupsSection = OWSTableSection(title: "Backups")
|
|
|
|
let backupSettingsStore = BackupSettingsStore()
|
|
let db = DependenciesBridge.shared.db
|
|
let lastBackupDetails = db.read { tx in
|
|
return backupSettingsStore.lastBackupDetails(tx: tx)
|
|
}
|
|
|
|
backupsSection.add(.actionItem(withText: "Export + Validate Message Backup proto") { [self] in
|
|
Task {
|
|
await exportMessageBackupProto()
|
|
}
|
|
})
|
|
if BuildFlags.Backups.showOptimizeMedia {
|
|
backupsSection.add(.switch(
|
|
withText: "Offload all attachments",
|
|
subtitle: "If on and \"Optimize Storage\" enabled, offload all attachments instead of only those >30d old",
|
|
isOn: { Attachment.offloadingThresholdOverride },
|
|
actionBlock: { _ in
|
|
Attachment.offloadingThresholdOverride = !Attachment.offloadingThresholdOverride
|
|
},
|
|
))
|
|
}
|
|
backupsSection.add(.actionItem(withText: "Enable Backups onboarding flow") { [weak self] in
|
|
let backupSettingsStore = BackupSettingsStore()
|
|
let db = DependenciesBridge.shared.db
|
|
|
|
db.write { tx in
|
|
backupSettingsStore.setShouldOverrideShowBackupsOnboarding(true, tx: tx)
|
|
}
|
|
|
|
self?.presentToast(text: "Backups onboarding enabled!")
|
|
})
|
|
backupsSection.add(.copyableItem(
|
|
label: "Last Backup chats/messages file size",
|
|
value: lastBackupDetails.flatMap { ByteCountFormatter().string(for: $0.backupFileSizeBytes) },
|
|
))
|
|
backupsSection.add(.actionItem(withText: #"Show "Backup Key Reminder" flow"#) { [weak self] in
|
|
guard let self else { return }
|
|
|
|
let accountKeyStore = DependenciesBridge.shared.accountKeyStore
|
|
let db = DependenciesBridge.shared.db
|
|
|
|
guard let aep = db.read(block: { accountKeyStore.getAccountEntropyPool(tx: $0) }) else {
|
|
presentToast(text: "Missing AEP?!")
|
|
return
|
|
}
|
|
|
|
BackupRecoveryKeyReminderCoordinator(
|
|
aep: aep,
|
|
fromViewController: self,
|
|
onSuccess: {
|
|
self.presentToast(text: "Success!")
|
|
},
|
|
).presentVerifyFlow()
|
|
})
|
|
backupsSection.add(.actionItem(withText: "Backup media integrity check") { [weak self] in
|
|
let vc = InternalListMediaViewController()
|
|
self?.navigationController?.pushViewController(vc, animated: true)
|
|
})
|
|
contents.add(backupsSection)
|
|
|
|
do {
|
|
func makeFileBrowsingActionItem(_ title: String, _ fileUrl: URL) -> OWSTableItem {
|
|
return .actionItem(
|
|
withText: title,
|
|
actionBlock: { [weak self] in
|
|
guard let self else { return }
|
|
navigationController?.pushViewController(
|
|
InternalFileBrowserViewController(fileURL: fileUrl),
|
|
animated: true,
|
|
)
|
|
},
|
|
)
|
|
}
|
|
|
|
let fileBrowsingSection = OWSTableSection(title: "Browse App Files")
|
|
fileBrowsingSection.add(makeFileBrowsingActionItem(
|
|
"App Container: Library",
|
|
URL(string: OWSFileSystem.appLibraryDirectoryPath())!.deletingLastPathComponent(),
|
|
))
|
|
fileBrowsingSection.add(makeFileBrowsingActionItem(
|
|
"App Container: Documents",
|
|
URL(string: OWSFileSystem.appDocumentDirectoryPath())!.deletingLastPathComponent(),
|
|
))
|
|
fileBrowsingSection.add(makeFileBrowsingActionItem(
|
|
"Shared App Container",
|
|
URL(string: OWSFileSystem.appSharedDataDirectoryPath())!.deletingLastPathComponent(),
|
|
))
|
|
contents.add(fileBrowsingSection)
|
|
}
|
|
|
|
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: "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 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
|
|
}
|
|
|
|
func exportMessageBackupProto() async {
|
|
let accountKeyStore = DependenciesBridge.shared.accountKeyStore
|
|
let backupArchiveManager = DependenciesBridge.shared.backupArchiveManager
|
|
let db = DependenciesBridge.shared.db
|
|
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
|
|
|
|
let backupKey: MessageBackupKey
|
|
let exportMetadata: Upload.EncryptedBackupUploadMetadata
|
|
do {
|
|
(backupKey, exportMetadata) = try await ModalActivityIndicatorViewController.presentAndPropagateResult(
|
|
from: self,
|
|
title: "Exporting...",
|
|
) {
|
|
let (messageBackupKey, localIdentifiers) = try db.read { tx in
|
|
let localIdentifiers = try tsAccountManager.registeredState(tx: tx).localIdentifiers
|
|
return (
|
|
try accountKeyStore.getMessageRootBackupKey(aci: localIdentifiers.aci, tx: tx)!,
|
|
localIdentifiers,
|
|
)
|
|
}
|
|
|
|
let backupKey = try MessageBackupKey(
|
|
backupKey: messageBackupKey.backupKey,
|
|
backupId: messageBackupKey.backupId,
|
|
)
|
|
|
|
let exportMetadata = try await backupArchiveManager.exportEncryptedBackup(
|
|
localIdentifiers: localIdentifiers,
|
|
backupPurpose: .remoteExport(key: messageBackupKey, chatAuth: .implicit()),
|
|
progress: nil,
|
|
logger: PrefixedLogger(prefix: "[Backups]"),
|
|
)
|
|
|
|
return (backupKey, exportMetadata)
|
|
}
|
|
} catch {
|
|
let message = "Failed to export Backup proto! \(error)"
|
|
Logger.error(message)
|
|
presentToast(text: message)
|
|
return
|
|
}
|
|
|
|
let keyString = "AES key: \(backupKey.aesKey.base64EncodedString())"
|
|
+ "\nHMAC key: \(backupKey.hmacKey.base64EncodedString())"
|
|
|
|
let activityVC = UIActivityViewController(
|
|
activityItems: [exportMetadata.fileUrl],
|
|
applicationActivities: nil,
|
|
)
|
|
activityVC.popoverPresentationController?.sourceView = view
|
|
activityVC.completionWithItemsHandler = { [self] _, _, _, _ in
|
|
UIPasteboard.general.setItems(
|
|
[[UIPasteboard.typeAutomatic: keyString]],
|
|
options: [.expirationDate: Date().addingTimeInterval(120)],
|
|
)
|
|
|
|
presentToast(text: "Success! Encryption key copied.")
|
|
}
|
|
|
|
present(activityVC, animated: true)
|
|
}
|
|
}
|