Signal-iOS/SignalServiceKit/src/Util/FeatureFlags.swift

465 lines
14 KiB
Swift

//
// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
//
import Foundation
enum FeatureBuild: Int {
case dev
case internalPreview
case qa
case beta
case production
}
extension FeatureBuild {
func includes(_ level: FeatureBuild) -> Bool {
return self.rawValue <= level.rawValue
}
}
let build: FeatureBuild = OWSIsDebugBuild() ? .dev : .qa
// MARK: -
@objc
public enum StorageMode: Int {
// Only use YDB. This should be used in production until we ship
// the YDB-to-GRDB migration.
case ydbForAll
// Use GRDB, migrating if possible on every launch.
// If no YDB database exists, a throwaway db is not used.
//
// Supercedes grdbMigratesFreshDBEveryLaunch.
//
// TODO: Remove.
case grdbThrowawayIfMigrating
// Use GRDB under certain conditions.
//
// TODO: Remove.
case grdbForAlreadyMigrated
case grdbForLegacyUsersOnly
case grdbForNewUsersOnly
// Use GRDB, migrating once if necessary.
case grdbForAll
// These modes can be used while running tests.
// They are more permissive than the release modes.
//
// The build shepherd should be running the test
// suites in .ydbTests and .grdbTests modes before each release.
case ydbTests
case grdbTests
}
// MARK: -
extension StorageMode: CustomStringConvertible {
public var description: String {
switch self {
case .ydbForAll:
return ".ydbForAll"
case .grdbThrowawayIfMigrating:
return ".grdbThrowawayIfMigrating"
case .grdbForAlreadyMigrated:
return ".grdbForAlreadyMigrated"
case .grdbForLegacyUsersOnly:
return ".grdbForLegacyUsersOnly"
case .grdbForNewUsersOnly:
return ".grdbForNewUsersOnly"
case .grdbForAll:
return ".grdbForAll"
case .ydbTests:
return ".ydbTests"
case .grdbTests:
return ".grdbTests"
}
}
}
// MARK: -
@objc
public enum StorageModeStrictness: Int {
// For DEBUG, QA and beta builds only.
case fail
// For production
case failDebug
// Temporary value to be used until existing issues are resolved.
case log
}
// MARK: -
/// By centralizing feature flags here and documenting their rollout plan, it's easier to review
/// which feature flags are in play.
@objc(SSKFeatureFlags)
public class FeatureFlags: BaseFlags {
@objc
public static var storageMode: StorageMode {
if CurrentAppContext().isRunningTests {
// We should be running the tests using both .ydbTests or .grdbTests.
return .grdbTests
} else {
return .grdbForAll
}
}
// Don't enable this flag in production.
// At least, not yet.
@objc
public static var storageModeStrictness: StorageModeStrictness {
return build.includes(.beta) ? .fail : .failDebug
}
@objc
public static var storageModeDescription: String {
return "\(storageMode)"
}
@objc
public static let stickerSearch = false
@objc
public static let stickerPackOrdering = false
// Don't enable this flag until the Desktop changes have been in production for a while.
@objc
public static let strictSyncTranscriptTimestamps = false
// Don't enable this flag in production.
@objc
public static let strictYDBExtensions = build.includes(.beta)
@objc
public static let phoneNumberPrivacy = false
@objc
public static let complainAboutSlowDBWrites = true
// Don't consult this flags; consult RemoteConfig.usernames.
static let usernamesSupported = build.includes(.dev)
@objc
public static var calling: Bool {
return answerCallsOnSecondaryDevice || TSAccountManager.sharedInstance().isRegisteredPrimaryDevice
}
// We can't enable this in production until sending calls to all devices has
// rolled out on iOS and Android.
//
// Note: As well as exposing incoming calls on iPads, this also exposes the outgoing call button
// on iPads, because we don't want to allow outgoing calls from an iPad until we're also ready
// to receive them on iPad.
@objc
public static let answerCallsOnSecondaryDevice: Bool = true
// Don't consult this flags; consult RemoteConfig.groupsV2...
static let groupsV2Supported = true
@objc
public static let groupsV2embedProtosInGroupUpdates = true
@objc
public static let groupsV2processProtosInGroupUpdates = true
@objc
public static let linkedPhones = build.includes(.internalPreview)
@objc
public static let isUsingProductionService = true
@objc
public static let useOrphanDataCleaner = true
@objc
public static let sendRecipientUpdates = false
@objc
public static let notificationServiceExtension = build.includes(.dev)
@objc
public static let pinsForNewUsers = true
@objc
public static let deviceTransferDestroyOldDevice = true
@objc
public static let deviceTransferThrowAway = false
// Don't consult this flags; consult RemoteConfig.mentions.
static let mentionsSupported = groupsV2Supported
@objc
public static let attachmentUploadV3 = build.includes(.qa)
@objc
public static let attachmentUploadV3ForV1GroupAvatars = false
private static let _ignoreCDSUnregisteredUsersInMessageSends = AtomicBool(true)
@objc
public static var ignoreCDSUnregisteredUsersInMessageSends: Bool {
get {
guard build.includes(.qa) else {
return true
}
return _ignoreCDSUnregisteredUsersInMessageSends.get()
}
set {
_ignoreCDSUnregisteredUsersInMessageSends.set(newValue)
}
}
public static func buildFlagMap() -> [String: Any] {
BaseFlags.buildFlagMap(for: FeatureFlags.self) { (key: String) -> Any? in
FeatureFlags.value(forKey: key)
}
}
@objc
public static func logFlags() {
let logFlag = { (prefix: String, key: String, value: Any?) in
if let value = value {
Logger.info("\(prefix): \(key) = \(value)", function: "")
} else {
Logger.info("\(prefix): \(key) = nil", function: "")
}
}
let flagMap = buildFlagMap()
for key in Array(flagMap.keys).sorted() {
let value = flagMap[key]
logFlag("FeatureFlag", key, value)
}
}
}
/// Flags that we'll leave in the code base indefinitely that are helpful for
/// development should go here, rather than cluttering up FeatureFlags.
@objc(SSKDebugFlags)
public class DebugFlags: BaseFlags {
@objc
public static let internalLogging = build.includes(.qa)
// DEBUG builds won't receive push notifications, which prevents receiving messages
// while the app is backgrounded or the system call screen is active.
//
// Set this flag to true to be able to download messages even when the app is in the background.
@objc
public static let keepWebSocketOpenInBackground = false
@objc
public static let audibleErrorLogging = build.includes(.qa)
@objc
public static let verboseAboutView = build.includes(.qa)
// This can be used to shut down various background operations.
@objc
public static let suppressBackgroundActivity = false
@objc
public static let logSQLQueries = build.includes(.dev)
@objc
public static let groupsV2IgnoreCapability = false
// We can use this to test recovery from "missed updates".
private static let _groupsV2dontSendUpdates = AtomicBool(false)
@objc
public static var groupsV2dontSendUpdates: Bool {
get {
guard build.includes(.qa) else {
return false
}
return _groupsV2dontSendUpdates.get()
}
set {
_groupsV2dontSendUpdates.set(newValue)
}
}
@objc
public static let groupsV2showV2Indicator = FeatureFlags.groupsV2Supported && build.includes(.qa)
// If set, v2 groups will be created and updated with invalid avatars
// so that we can test clients' robustness to this case.
private static let _groupsV2corruptAvatarUrlPaths = AtomicBool(false)
@objc
public static var groupsV2corruptAvatarUrlPaths: Bool {
get {
guard build.includes(.qa) else {
return false
}
return _groupsV2corruptAvatarUrlPaths.get()
}
set {
_groupsV2corruptAvatarUrlPaths.set(newValue)
}
}
// If set, v2 groups will be created and updated with
// corrupt avatars, group names, and/or dm state
// so that we can test clients' robustness to this case.
private static let _groupsV2corruptBlobEncryption = AtomicBool(false)
@objc
public static var groupsV2corruptBlobEncryption: Bool {
get {
guard build.includes(.qa) else {
return false
}
return _groupsV2corruptBlobEncryption.get()
}
set {
_groupsV2corruptBlobEncryption.set(newValue)
}
}
// This flag auto-enables the groupv2 flags in RemoteConfig.
static let groupsV2ForceEnable = FeatureFlags.groupsV2Supported && build.includes(.qa)
// If set, client will invite instead of adding other users.
private static let _groupsV2forceInvites = AtomicBool(false)
@objc
public static var groupsV2forceInvites: Bool {
get {
guard build.includes(.qa) else {
return false
}
return _groupsV2forceInvites.get()
}
set {
_groupsV2forceInvites.set(newValue)
}
}
// If set, client will always send corrupt invites.
private static let _groupsV2corruptInvites = AtomicBool(false)
@objc
public static var groupsV2corruptInvites: Bool {
get {
guard build.includes(.qa) else {
return false
}
return _groupsV2corruptInvites.get()
}
set {
_groupsV2corruptInvites.set(newValue)
}
}
private static let _groupsV2onlyCreateV1Groups = AtomicBool(false)
@objc
public static var groupsV2onlyCreateV1Groups: Bool {
get {
guard build.includes(.qa) else {
return false
}
return _groupsV2onlyCreateV1Groups.get()
}
set {
_groupsV2onlyCreateV1Groups.set(newValue)
}
}
@objc
public static let groupsV2ignoreCorruptInvites = false
@objc
public static let groupsV2memberStatusIndicators = FeatureFlags.groupsV2Supported && build.includes(.qa)
@objc
public static let groupsV2editMemberAccess = build.includes(.qa)
static let groupsV2forceModernCDS = build.includes(.qa)
@objc
public static let isMessageProcessingVerbose = false
// Currently this flag is only honored by TSNetworkManager,
// but we could eventually honor in other places as well:
//
// * The socket manager.
// * Places we make requests using tasks.
@objc
public static let logCurlOnSuccess = false
static let forceModernContactDiscovery = build.includes(.dev)
// Our "group update" info messages should be robust to
// various situations that shouldn't occur in production,
// bug we want to be able to test them using the debug UI.
@objc
public static let permissiveGroupUpdateInfoMessages = build.includes(.dev)
@objc
public static let showProfileKeyAndUuidsIndicator = build.includes(.qa)
@objc
public static let verboseNotificationLogging = build.includes(.qa)
@objc
public static let verboseSignalRecipientLogging = build.includes(.qa)
@objc
public static let shouldMergeUserProfiles = build.includes(.qa)
@objc
public static let deviceTransferVerboseProgressLogging = build.includes(.qa)
// We currently want to force-enable versioned profiles for
// all beta users, but not production.
static let forceVersionedProfiles = build.includes(.beta)
@objc
public static let reactWithThumbsUpFromLockscreen = build.includes(.qa)
static let forceMentions = build.includes(.dev)
public static func buildFlagMap() -> [String: Any] {
BaseFlags.buildFlagMap(for: DebugFlags.self) { (key: String) -> Any? in
DebugFlags.value(forKey: key)
}
}
@objc
public static func logFlags() {
let logFlag = { (prefix: String, key: String, value: Any?) in
if let value = value {
Logger.info("\(prefix): \(key) = \(value)", function: "")
} else {
Logger.info("\(prefix): \(key) = nil", function: "")
}
}
let flagMap = buildFlagMap()
for key in Array(flagMap.keys).sorted() {
let value = flagMap[key]
logFlag("DebugFlag", key, value)
}
}
}
// MARK: -
@objc
public class BaseFlags: NSObject {
static func buildFlagMap(for flagsClass: Any, flagFunc: (String) -> Any?) -> [String: Any] {
var result = [String: Any]()
var count: CUnsignedInt = 0
let methods = class_copyPropertyList(object_getClass(flagsClass), &count)!
for i in 0 ..< count {
let selector = property_getName(methods.advanced(by: Int(i)).pointee)
if let key = String(cString: selector, encoding: .utf8) {
guard !key.hasPrefix("_") else {
continue
}
if let value = flagFunc(key) {
result[key] = value
}
}
}
return result
}
}