Signal-iOS/Signal/src/ViewControllers/DebugUI/DebugContactsUtils.swift

226 lines
7.8 KiB
Swift

//
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Contacts
import SignalServiceKit
import SignalUI
#if USE_DEBUG_UI
class DebugContactsUtils: Dependencies {
// MARK: Random Contacts
static func randomPhoneNumber() -> String {
var result: String
var numberOfDigits: UInt
if Bool.random() {
// Generate a US phone number.
result = "+1"
numberOfDigits = 10
} else {
// Generate a UK phone number.
result = "+441"
numberOfDigits = 9
}
for _ in 0..<numberOfDigits {
result += String(Int.random(in: 0...9))
}
return result
}
static func createRandomContacts(
_ count: UInt,
contactHandler: ((CNContact, Int, inout Bool) async -> Void)? = nil
) async {
guard count > 0 else { return }
let authorizationStatus = CNContactStore.authorizationStatus(for: .contacts)
guard authorizationStatus != .denied && authorizationStatus != .restricted else {
OWSActionSheets.showErrorAlert(message: "No Contacts access.")
return
}
// Request access
guard authorizationStatus == .authorized else {
let store = CNContactStore()
let granted: Bool
do {
granted = try await store.requestAccess(for: .contacts)
} catch {
granted = false
}
guard granted else {
DispatchQueue.main.async {
OWSActionSheets.showErrorAlert(message: "No Contacts access.")
}
return
}
await createRandomContacts(count, contactHandler: contactHandler)
return
}
let maxBatchSize: UInt = 10
let batchSize = min(maxBatchSize, count)
let remainder = count - batchSize
await createRandomContactsBatch(batchSize, contactHandler: contactHandler)
guard remainder > 0 else { return }
Task { @MainActor in
await createRandomContacts(remainder, contactHandler: contactHandler)
}
}
private static func createRandomContactsBatch(
_ count: UInt,
contactHandler: ((CNContact, Int, inout Bool) async -> Void)?
) async {
Logger.debug("createRandomContactsBatch: \(count)")
var contacts = [CNContact]()
// 50% chance of fake contact having an avatar
let percentWithAvatar = 50
let percentWithLargeAvatar = 25
let minimumAvatarDiameter: UInt = 200
let maximimAvatarDiameter: UInt = 800
let saveRequest = CNSaveRequest()
for _ in 0..<count { autoreleasepool {
let contact = CNMutableContact()
contact.familyName = "Rando-" + CommonGenerator.lastName()
contact.givenName = CommonGenerator.firstName()
let homePhone = CNLabeledValue(label: CNLabelHome, value: CNPhoneNumber(stringValue: randomPhoneNumber()))
contact.phoneNumbers = [ homePhone ]
let avatarSeed = UInt.random(in: 0...99)
if avatarSeed < percentWithAvatar {
let shouldUseLargeAvatar = avatarSeed < percentWithLargeAvatar
let avatarDiameter: UInt = {
if shouldUseLargeAvatar {
return maximimAvatarDiameter
}
return UInt.random(in: minimumAvatarDiameter...maximimAvatarDiameter)
}()
let avatarImage: UIImage?
if shouldUseLargeAvatar {
avatarImage = AvatarBuilder.buildNoiseAvatar(diameterPoints: avatarDiameter)
} else {
avatarImage = AvatarBuilder.buildRandomAvatar(diameterPoints: avatarDiameter)
}
if let avatarImageData = avatarImage?.jpegData(compressionQuality: 0.9) {
contact.imageData = avatarImageData
Logger.debug("avatar size: \(avatarImageData.count) bytes")
}
}
contacts.append(contact)
saveRequest.add(contact, toContainerWithIdentifier: nil)
}}
Logger.info("Saving fake contacts: \(contacts.count)")
do {
try CNContactStore().execute(saveRequest)
if let contactHandler {
for (index, contact) in contacts.enumerated() {
var stop = false
await contactHandler(contact, index, &stop)
if stop {
break
}
}
}
} catch {
owsFailDebug("Error saving fake contacts: \(error)")
DispatchQueue.main.async {
OWSActionSheets.showErrorAlert(message: error.userErrorDescription)
}
}
}
// MARK: Contact Deletion
static func deleteAllContacts() {
let authorizationStatus = CNContactStore.authorizationStatus(for: .contacts)
guard authorizationStatus != .denied && authorizationStatus != .restricted else {
OWSActionSheets.showErrorAlert(message: "No Contacts access.")
return
}
// Request access
guard authorizationStatus == .authorized else {
let store = CNContactStore()
store.requestAccess(for: .contacts) { granted, error in
guard granted && error == nil else {
DispatchQueue.main.async {
OWSActionSheets.showErrorAlert(message: "No Contacts access.")
}
return
}
DispatchQueue.main.async {
deleteAllContacts()
}
}
return
}
deleteContactsWithFilter { _ in return true }
}
static func deleteRandomContacts() {
let authorizationStatus = CNContactStore.authorizationStatus(for: .contacts)
guard authorizationStatus != .denied && authorizationStatus != .restricted else {
OWSActionSheets.showErrorAlert(message: "No Contacts access.")
return
}
// Request access
guard authorizationStatus == .authorized else {
let store = CNContactStore()
store.requestAccess(for: .contacts) { granted, error in
guard granted && error == nil else {
DispatchQueue.main.async {
OWSActionSheets.showErrorAlert(message: "No Contacts access.")
}
return
}
DispatchQueue.main.async {
deleteRandomContacts()
}
}
return
}
deleteContactsWithFilter { contact in
return contact.familyName.hasPrefix("Rando-")
}
}
private static func deleteContactsWithFilter(_ filter: (CNContact) -> Bool) {
let keysToFetch: [CNKeyDescriptor] = [
CNContactIdentifierKey as CNKeyDescriptor,
CNContactGivenNameKey as CNKeyDescriptor,
CNContactFamilyNameKey as CNKeyDescriptor,
CNContactFormatter.descriptorForRequiredKeys(for: .fullName) as CNKeyDescriptor
]
let contactStore = CNContactStore()
let saveRequest = CNSaveRequest()
do {
try contactStore.enumerateContacts(with: CNContactFetchRequest(keysToFetch: keysToFetch)) { contact, _ in
if filter(contact) {
saveRequest.delete(contact.mutableCopy() as! CNMutableContact)
}
}
try contactStore.execute(saveRequest)
} catch {
Logger.error("\(error)")
OWSActionSheets.showErrorAlert(message: error.userErrorDescription)
}
}
}
#endif