Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalServiceKit/Calls/CallRecord/CallRecord+CallStatus.swift
1 views
//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

extension CallRecord {
    public enum CallStatus: Codable, Equatable {
        case individual(IndividualCallStatus)
        case group(GroupCallStatus)
        case callLink(CallLinkCallStatus)

        /// Represents the states that an individual (1:1) call may be in.
        ///
        /// - Important
        /// The raw values of the cases of this enum must not overlap with those
        /// for ``GroupCallStatus``, or en/decoding becomes ambiguous.
        public enum IndividualCallStatus: Int, CaseIterable {
            /// This is a call for which no action has yet been taken.
            ///
            /// For example, this call may have been accepted on a linked
            /// device, but we haven't yet received the corresponding sync
            /// message. Records with this status can be used to bridge between
            /// an incoming sync mesage and other state, such as the
            /// corresponding interaction.
            ///
            /// Records with this status should eventually be updated to another
            /// status. If they aren't, the call should be treated as missed.
            case pending = 0

            /// This call was accepted.
            ///
            /// For an incoming call, indicates we accepted the ring. For an
            /// outgoing call, indicates the receiver accepted the ring.
            case accepted = 1

            /// This call was not accepted.
            ///
            /// For an incoming call, indicates we actively declined the ring.
            /// For an outgoing call, indicates the receiver did not accept.
            case notAccepted = 2

            /// This was an incoming call that we missed.
            ///
            /// An incoming missed call is contrasted with an actively-declined
            /// one, which would fall under ``notAccepted`` above.
            ///
            /// - Note
            /// Calls declined as "busy" use this case.
            case incomingMissed = 3
        }

        /// Represents the states that a group call may be in.
        ///
        /// - Important
        /// The raw values of the cases of this enum must not overlap with those
        /// for ``IndividualCallStatus``, or en/decoding becomes ambiguous.
        public enum GroupCallStatus: Int, CaseIterable {
            /// This is a call that was started without ringing, which we have
            /// learned about but are not involved with.
            case generic = 4

            /// This is a call that was started without ringing, which we have
            /// joined.
            case joined = 5

            /// This call involves ringing which is actively occuring. No action
            /// has yet been taken on the ring, and it has not expired.
            ///
            /// - Note
            /// We do not track the state of outgoing group rings and instead
            /// record them as accepted when we start the ring. Consequently,
            /// only incoming rings should be in this state.
            case ringing = 9

            /// This call involved ringing, and the ring was accepted.
            ///
            /// - Note
            /// We do not track the state of outgoing group rings and instead
            /// record them as accepted when we start the ring. All outgoing
            /// group rings will therefore end up in this state.
            case ringingAccepted = 6

            /// This call involved ringing, and the ring was declined.
            ///
            /// For an incoming call, indicates we actively declined the ring.
            ///
            /// - Note
            /// We do not track the state of outgoing group rings and instead
            /// record them as accepted when we start the ring. Consequently,
            /// only incoming rings should be in this state.
            case ringingDeclined = 7

            /// This call involved ringing, and no action was taken on the ring
            /// before it expired.
            ///
            /// A missed call is contrasted with an actively-declined one, which
            /// would fall under ``ringingNotAccepted`` above.
            ///
            /// - Note
            /// Calls declined as "busy" use this case.
            ///
            /// - Note
            /// We do not track the state of outgoing group rings and instead
            /// record them as accepted when we start the ring. Consequently,
            /// only incoming rings should be in this state.
            case ringingMissed = 8

            /// This call involved ringing, but the ring was auto-declined due
            /// to the user's Do Not Disturb settings.
            ///
            /// A missed call is contrasted with an actively-declined one, which
            /// would fall under ``ringingNotAccepted`` above.
            ///
            /// - Note
            /// At the time of writing, the iOS app does not set this status for
            /// any group calls. However, we might encounter calls with this
            /// status when restoring from a Backup.
            ///
            /// - Note
            /// We do not track the state of outgoing group rings and instead
            /// record them as accepted when we start the ring. Consequently,
            /// only incoming rings should be in this state.
            case ringingMissedNotificationProfile = 10
        }

        /// Represents the states that a call link call may be in.
        ///
        /// - Important
        /// The raw values of the cases of this enum must not overlap with the enums
        /// for the other types of calls in this file.
        public enum CallLinkCallStatus: Int, CaseIterable {
            /// We've tapped the join button but haven't been let into the call yet.
            case generic = 11

            /// We've joined the call.
            case joined = 12

            func canTransition(to newValue: Self) -> Bool {
                return self != newValue && newValue == .joined
            }
        }

        // MARK: Codable

        var intValue: Int {
            switch self {
            case .individual(let individualCallStatus): return individualCallStatus.rawValue
            case .group(let groupCallStatus): return groupCallStatus.rawValue
            case .callLink(let callLinkCallStatus): return callLinkCallStatus.rawValue
            }
        }

        private init?(intValue: Int) {
            if let individualCallStatus = IndividualCallStatus(rawValue: intValue) {
                self = .individual(individualCallStatus)
            } else if let groupCallStatus = GroupCallStatus(rawValue: intValue) {
                self = .group(groupCallStatus)
            } else if let callLinkCallStatus = CallLinkCallStatus(rawValue: intValue) {
                self = .callLink(callLinkCallStatus)
            } else {
                owsFailDebug("Unexpected int value: \(intValue)")
                return nil
            }
        }

        public init(from decoder: Decoder) throws {
            let singleValueContainer = try decoder.singleValueContainer()
            let intValue = try singleValueContainer.decode(Int.self)

            guard let selfValue = CallStatus(intValue: intValue) else {
                throw DecodingError.dataCorruptedError(
                    in: singleValueContainer,
                    debugDescription: "\(type(of: self)) contained unexpected int value: \(intValue)",
                )
            }

            self = selfValue
        }

        public func encode(to encoder: Encoder) throws {
            var singleValueContainer = encoder.singleValueContainer()
            try singleValueContainer.encode(intValue)
        }
    }
}

// MARK: - All cases

public extension CallRecord.CallStatus {
    static var allCases: [CallRecord.CallStatus] {
        let allIndividualCases: [CallRecord.CallStatus] = IndividualCallStatus.allCases
            .map { .individual($0) }
        let allGroupCases: [CallRecord.CallStatus] = GroupCallStatus.allCases
            .map { .group($0) }
        let allCallLinkCases: [CallRecord.CallStatus] = CallLinkCallStatus.allCases
            .map { .callLink($0) }

        return allIndividualCases + allGroupCases + allCallLinkCases
    }
}

// MARK: - Missed calls

public extension CallRecord.CallStatus {
    static var missedCalls: [CallRecord.CallStatus] {
        return [
            .individual(.incomingMissed),
            .group(.ringingMissed),
            .group(.ringingMissedNotificationProfile),
        ]
    }

    var isMissedCall: Bool {
        return Self.missedCalls.contains(self)
    }
}