Skip to content

Commit

Permalink
[Feature]Introduce CallKit availability policies (#611)
Browse files Browse the repository at this point in the history
  • Loading branch information
ipavlidakis authored Dec 2, 2024
1 parent d4531cf commit 2ee1c89
Show file tree
Hide file tree
Showing 11 changed files with 354 additions and 0 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

# Upcoming

### ✅ Added
- CallKit availability policies allows you to control wether `Callkit` should be enabled/disabled based on different rules [#611](https://github.com/GetStream/stream-video-swift/pull/611)

### 🐞 Fixed
- By observing the `CallKitPushNotificationAdapter.deviceToken` you will be notified with an empty `deviceToken` value, once the object unregister push notifications. [#608](https://github.com/GetStream/stream-video-swift/pull/608)
- When a call you receive a ringing while the app isn't running (and the screen is locked), websocket connection wasn't recovered. [#600](https://github.com/GetStream/stream-video-swift/pull/600)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,30 @@ import Intents

@MainActor
fileprivate func content() {
container {
@Injected(\.callKitAdapter) var callKitAdapter

callKitAdapter.availabilityPolicy = .always
}

container {
@Injected(\.callKitAdapter) var callKitAdapter

callKitAdapter.availabilityPolicy = .regionBased
}

container {
struct MyCustomAvailabilityPolicy: CallKitAvailabilityPolicyProtocol {
var isAvailable: Bool {
// Example: Enable CallKit only for premium users
return UserManager.currentUser?.isPremium == true
}
}

@Injected(\.callKitAdapter) var callKitAdapter
callKitAdapter.availabilityPolicy = .custom(MyCustomAvailabilityPolicy())
}

container {
@Injected(\.callKitAdapter) var callKitAdapter

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -422,3 +422,10 @@ var otherParticipant = CallParticipant(
audioLevels: [],
pin: nil
)

final class UserManager {
struct AppUser {
var isPremium: Bool
}
static var currentUser: AppUser?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// Copyright © 2024 Stream.io Inc. All rights reserved.
//

import Foundation

/// A policy implementation where CallKit is always available.
///
/// This policy ignores regional or other constraints.
struct CallKitAlwaysAvailabilityPolicy: CallKitAvailabilityPolicyProtocol {
/// CallKit is always available with this policy.
var isAvailable: Bool { true }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// Copyright © 2024 Stream.io Inc. All rights reserved.
//

import Foundation

/// A policy that defines when CallKit is available.
/// It can be configured to always enable CallKit, enable it based on the user's
/// region, or use a custom implementation.
public enum CallKitAvailabilityPolicy: CustomStringConvertible {

/// CallKit is always available, regardless of conditions.
case always

/// CallKit availability is determined based on the user's region.
case regionBased

/// CallKit availability is determined by a custom policy.
/// - Parameter policy: A custom policy implementing `CallKitAvailabilityPolicyProtocol`.
case custom(CallKitAvailabilityPolicyProtocol)

/// A textual description of the availability policy.
///
/// - Returns: A string representation of the policy.
public var description: String {
switch self {
case .always:
return ".always"
case .regionBased:
return ".regionBased"
case let .custom(policy):
return ".custom(\(policy))"
}
}

/// The underlying policy implementation based on the selected availability.
///
/// - Returns: An instance conforming to `CallKitAvailabilityPolicyProtocol`.
var policy: CallKitAvailabilityPolicyProtocol {
switch self {
case .always:
return CallKitAlwaysAvailabilityPolicy()
case .regionBased:
return CallKitRegionBasedAvailabilityPolicy()
case let .custom(policy):
return policy
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//
// Copyright © 2024 Stream.io Inc. All rights reserved.
//

import Foundation

/// A protocol defining the requirements for CallKit availability policies.
public protocol CallKitAvailabilityPolicyProtocol {
/// Indicates whether CallKit is available under the policy.
var isAvailable: Bool { get }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//
// Copyright © 2024 Stream.io Inc. All rights reserved.
//

import Foundation

/// A policy implementation where CallKit availability depends on the region.
///
/// This policy disables CallKit in specific regions, identified by their region
/// codes, to comply with regional regulations or restrictions. It utilizes the
/// injected `StreamLocaleProvider` to retrieve the current locale information.
struct CallKitRegionBasedAvailabilityPolicy: CallKitAvailabilityPolicyProtocol {

/// A provider for locale information.
@Injected(\.localeProvider) private var localeProvider

/// A set of region identifiers where CallKit is unavailable.
///
/// This includes both two-letter and three-letter region codes.
private var unavailableRegions: Set<String> = [
"CN", // China (two-letter code)
"CHN" // China (three-letter code)
]

/// Determines if CallKit is available based on the current region.
///
/// - Returns: `true` if CallKit is available; otherwise, `false`.
/// - Note: If the region cannot be determined, CallKit is considered unavailable.
var isAvailable: Bool {
// Retrieve the current region identifier from the locale provider.
guard let identifier = localeProvider.identifier else {
return false
}

// CallKit is unavailable if the region is part of the restricted set.
return !unavailableRegions.contains(identifier)
}
}
13 changes: 13 additions & 0 deletions Sources/StreamVideo/CallKit/CallKitAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ open class CallKitAdapter {
didSet { callKitService.callSettings = callSettings }
}

/// The policy defining the availability of CallKit services.
///
/// - Default: `.regionBased`
public var availabilityPolicy: CallKitAvailabilityPolicy = .regionBased

/// The currently active StreamVideo client.
/// - Important: We need to update it whenever a user logins.
public var streamVideo: StreamVideo? {
Expand All @@ -46,6 +51,14 @@ open class CallKitAdapter {
}

private func didUpdate(_ streamVideo: StreamVideo?) {
guard availabilityPolicy.policy.isAvailable else {
log
.warning(
"CallKitAdapter cannot be activated because the current availability policy (\(availabilityPolicy.policy)) doesn't allow it."
)
return
}

callKitService.streamVideo = streamVideo

guard streamVideo != nil else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// Copyright © 2024 Stream.io Inc. All rights reserved.
//

import Foundation

/// A protocol that defines the requirements for a locale provider.
///
/// This protocol abstracts the retrieval of a region identifier, allowing
/// flexibility and testability in components that depend on locale information.
protocol LocaleProviding {

/// The region identifier of the current locale.
///
/// - Returns: A string representing the region identifier (e.g., "US" or "GB"),
/// or `nil` if the region cannot be determined.
var identifier: String? { get }
}

/// A provider for accessing the current locale's region identifier.
///
/// This class abstracts locale information, offering compatibility for different
/// iOS versions.
final class StreamLocaleProvider: LocaleProviding {

/// Retrieves the region identifier for the current locale.
///
/// - For iOS 16 and later, it uses the `region` property.
/// - For earlier versions, it falls back to `regionCode`.
///
/// - Returns: A string representing the region identifier, or `nil` if unavailable.
var identifier: String? {
if #available(iOS 16, *) {
// Retrieve the region identifier for iOS 16 and later.
return NSLocale.current.region?.identifier
} else {
// Retrieve the region code for earlier iOS versions.
return NSLocale.current.regionCode
}
}
}

enum LocaleProvidingKey: InjectionKey {
/// The current value of the `StreamLocaleProvider` used for dependency injection.
static var currentValue: LocaleProviding = StreamLocaleProvider()
}

/// Extension of `InjectedValues` to provide access to the `StreamLocaleProvider`.
extension InjectedValues {

/// The locale provider, used to access region information within the app.
///
/// This value can be overridden for testing or specific use cases.
var localeProvider: LocaleProviding {
get { Self[LocaleProvidingKey.self] }
set { Self[LocaleProvidingKey.self] = newValue }
}
}
48 changes: 48 additions & 0 deletions StreamVideo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@

/* Begin PBXBuildFile section */
40013DDC2B87AA2300915453 /* SerialActor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40013DDB2B87AA2300915453 /* SerialActor.swift */; };
40034C262CFE155C00A318B1 /* CallKitAvailabilityPolicyProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40034C252CFE155C00A318B1 /* CallKitAvailabilityPolicyProtocol.swift */; };
40034C282CFE156800A318B1 /* CallKitAvailabilityPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40034C272CFE156800A318B1 /* CallKitAvailabilityPolicy.swift */; };
40034C2A2CFE156F00A318B1 /* CallKitRegionBasedAvailabilityPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40034C292CFE156F00A318B1 /* CallKitRegionBasedAvailabilityPolicy.swift */; };
40034C2C2CFE157300A318B1 /* CallKitAlwaysAvailabilityPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40034C2B2CFE157300A318B1 /* CallKitAlwaysAvailabilityPolicy.swift */; };
40034C2E2CFE15AC00A318B1 /* CallKitRegionBasedAvailabilityPolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40034C2D2CFE15AC00A318B1 /* CallKitRegionBasedAvailabilityPolicy.swift */; };
40034C312CFE168D00A318B1 /* StreamLocaleProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40034C302CFE168D00A318B1 /* StreamLocaleProvider.swift */; };
40073B6F2C456CB4006A2867 /* StreamPictureInPictureVideoRendererTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40073B6E2C456CB4006A2867 /* StreamPictureInPictureVideoRendererTests.swift */; };
40073B752C456E06006A2867 /* StreamPictureInPictureAdaptiveWindowSizePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40073B732C456DFC006A2867 /* StreamPictureInPictureAdaptiveWindowSizePolicy.swift */; };
40073B762C456E0E006A2867 /* StreamPictureInPictureWindowSizePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40073B682C456250006A2867 /* StreamPictureInPictureWindowSizePolicy.swift */; };
Expand Down Expand Up @@ -1431,6 +1437,12 @@

/* Begin PBXFileReference section */
40013DDB2B87AA2300915453 /* SerialActor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SerialActor.swift; sourceTree = "<group>"; };
40034C252CFE155C00A318B1 /* CallKitAvailabilityPolicyProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallKitAvailabilityPolicyProtocol.swift; sourceTree = "<group>"; };
40034C272CFE156800A318B1 /* CallKitAvailabilityPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallKitAvailabilityPolicy.swift; sourceTree = "<group>"; };
40034C292CFE156F00A318B1 /* CallKitRegionBasedAvailabilityPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallKitRegionBasedAvailabilityPolicy.swift; sourceTree = "<group>"; };
40034C2B2CFE157300A318B1 /* CallKitAlwaysAvailabilityPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallKitAlwaysAvailabilityPolicy.swift; sourceTree = "<group>"; };
40034C2D2CFE15AC00A318B1 /* CallKitRegionBasedAvailabilityPolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallKitRegionBasedAvailabilityPolicy.swift; sourceTree = "<group>"; };
40034C302CFE168D00A318B1 /* StreamLocaleProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamLocaleProvider.swift; sourceTree = "<group>"; };
40073B682C456250006A2867 /* StreamPictureInPictureWindowSizePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamPictureInPictureWindowSizePolicy.swift; sourceTree = "<group>"; };
40073B6E2C456CB4006A2867 /* StreamPictureInPictureVideoRendererTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamPictureInPictureVideoRendererTests.swift; sourceTree = "<group>"; };
40073B712C456DF6006A2867 /* StreamPictureInPictureFixedWindowSizePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamPictureInPictureFixedWindowSizePolicy.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2601,6 +2613,33 @@
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
40034C212CFE116200A318B1 /* AvailabilityPolicy */ = {
isa = PBXGroup;
children = (
40034C2B2CFE157300A318B1 /* CallKitAlwaysAvailabilityPolicy.swift */,
40034C292CFE156F00A318B1 /* CallKitRegionBasedAvailabilityPolicy.swift */,
40034C272CFE156800A318B1 /* CallKitAvailabilityPolicy.swift */,
40034C252CFE155C00A318B1 /* CallKitAvailabilityPolicyProtocol.swift */,
);
path = AvailabilityPolicy;
sourceTree = "<group>";
};
40034C242CFE154F00A318B1 /* AvailabilityPolicy */ = {
isa = PBXGroup;
children = (
40034C2D2CFE15AC00A318B1 /* CallKitRegionBasedAvailabilityPolicy.swift */,
);
path = AvailabilityPolicy;
sourceTree = "<group>";
};
40034C2F2CFE168900A318B1 /* LocaleProvider */ = {
isa = PBXGroup;
children = (
40034C302CFE168D00A318B1 /* StreamLocaleProvider.swift */,
);
path = LocaleProvider;
sourceTree = "<group>";
};
40073B702C456DE0006A2867 /* WindowSizePolicy */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -3898,6 +3937,7 @@
40DE867A2BBEAA6900E88D8A /* CallKit */ = {
isa = PBXGroup;
children = (
40034C242CFE154F00A318B1 /* AvailabilityPolicy */,
40DE867C2BBEAA8600E88D8A /* CallKitPushNotificationAdapterTests.swift */,
40F017412BBEC81C00E89FD1 /* CallKitServiceTests.swift */,
40F0173A2BBEB1A900E89FD1 /* CallKitAdapterTests.swift */,
Expand Down Expand Up @@ -4147,6 +4187,7 @@
40FB01FF2BAC8A4000A1C206 /* CallKit */ = {
isa = PBXGroup;
children = (
40034C212CFE116200A318B1 /* AvailabilityPolicy */,
40FB02022BAC93A800A1C206 /* CallKitAdapter.swift */,
40FB02042BAC94FB00A1C206 /* CallKitPushNotificationAdapter.swift */,
40FB02002BAC8A4A00A1C206 /* CallKitService.swift */,
Expand Down Expand Up @@ -4919,6 +4960,7 @@
84AF64D3287C79220012A503 /* Utils */ = {
isa = PBXGroup;
children = (
40034C2F2CFE168900A318B1 /* LocaleProvider */,
4067F3062CDA32F0002E28BD /* AudioSession */,
408CF9C42CAEC24500F56833 /* ScreenPropertiesAdapter */,
40C9E44F2C9880D300802B28 /* Unwrap */,
Expand Down Expand Up @@ -6285,12 +6327,14 @@
8490DD21298D4ADF007E53D2 /* StreamJsonDecoder.swift in Sources */,
40382F2E2C88B87D00C2D00F /* ReflectiveStringConvertible.swift in Sources */,
40BBC48F2C623C6E002AEF92 /* StreamRTCPeerConnection+Events.swift in Sources */,
40034C2C2CFE157300A318B1 /* CallKitAlwaysAvailabilityPolicy.swift in Sources */,
84C4004229E3F446007B69C2 /* ConnectedEvent.swift in Sources */,
84DC389C29ADFCFD00946713 /* GetOrCreateCallResponse.swift in Sources */,
406B3BD92C8F337000FC93A1 /* MediaAdapting.swift in Sources */,
4065839B2B877ADA00B4F979 /* CIImage+Sendable.swift in Sources */,
84DCA2242A3A0F0D000C3411 /* HTTPClient.swift in Sources */,
84A737CE28F4716E001A6769 /* signal.pb.swift in Sources */,
40034C2A2CFE156F00A318B1 /* CallKitRegionBasedAvailabilityPolicy.swift in Sources */,
84D6494029E94C14002CA428 /* CallsQuery.swift in Sources */,
8490032329D308A000AD9BB4 /* BackstageSettingsRequest.swift in Sources */,
40C6891C2C657F280054528A /* Publisher+AsyncStream.swift in Sources */,
Expand Down Expand Up @@ -6328,6 +6372,7 @@
84BAD77E2A6BFFB200733156 /* BroadcastSampleHandler.swift in Sources */,
40C2B5BB2C2C41DA00EC2C2D /* RejectCallRequest+Reason.swift in Sources */,
40C9E4482C94743800802B28 /* Stream_Video_Sfu_Signal_TrackSubscriptionDetails+Convenience.swift in Sources */,
40034C282CFE156800A318B1 /* CallKitAvailabilityPolicy.swift in Sources */,
840042C92A6FF9A200917B30 /* BroadcastConstants.swift in Sources */,
84F73854287C1A2D00A363F4 /* InjectedValuesExtensions.swift in Sources */,
40C9E44A2C94744E00802B28 /* Stream_Video_Sfu_Models_VideoDimension+Convenience.swift in Sources */,
Expand All @@ -6354,6 +6399,7 @@
40FB15192BF77EE700D5E580 /* StreamCallStateMachine+IdleStage.swift in Sources */,
40382F2B2C88B84800C2D00F /* Stream_Video_Sfu_Event_SfuEvent.OneOf_EventPayload+Payload.swift in Sources */,
84BAD7842A6C01AF00733156 /* BroadcastBufferReader.swift in Sources */,
40034C312CFE168D00A318B1 /* StreamLocaleProvider.swift in Sources */,
84D91E9C2C7CB0AA00B163A0 /* CallSessionParticipantCountsUpdatedEvent.swift in Sources */,
846E4AF529CDEA66003733AB /* ConnectUserDetailsRequest.swift in Sources */,
846D16262A52CE8C0036CE4C /* SpeakerManager.swift in Sources */,
Expand Down Expand Up @@ -6677,6 +6723,7 @@
40429D612C779B7000AC7FFF /* SFUSignalService.swift in Sources */,
435F01B32A501148009CD0BD /* OwnCapability+Identifiable.swift in Sources */,
40BBC4B32C6276C4002AEF92 /* LocalNoOpMediaAdapter.swift in Sources */,
40034C262CFE155C00A318B1 /* CallKitAvailabilityPolicyProtocol.swift in Sources */,
40FB150F2BF77CEC00D5E580 /* StreamStateMachine.swift in Sources */,
40CB9FA42B7F8EA4006BED93 /* AVCaptureSession+ActiveCaptureDevice.swift in Sources */,
4159F1762C86FA41002B94D3 /* RTMPSettingsResponse.swift in Sources */,
Expand Down Expand Up @@ -6764,6 +6811,7 @@
40C9E4572C98B06E00802B28 /* WebRTCConfiguration_Tests.swift in Sources */,
40C9E4592C98B1A900802B28 /* WebRTCStateAdapter_Tests.swift in Sources */,
40F017612BBEF15E00E89FD1 /* CallParticipantResponse+Dummy.swift in Sources */,
40034C2E2CFE15AC00A318B1 /* CallKitRegionBasedAvailabilityPolicy.swift in Sources */,
406B3C552C92031000FC93A1 /* WebRTCCoordinatorStateMachine_JoiningStageTests.swift in Sources */,
40C9E4642C99886900802B28 /* WebRTCCoorindator_Tests.swift in Sources */,
40F017772BBEF43B00E89FD1 /* CallSessionParticipantLeftEvent+Dummy.swift in Sources */,
Expand Down
Loading

0 comments on commit 2ee1c89

Please sign in to comment.