Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature]Introduce CallKit availability policies #611

Merged
merged 3 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading