Skip to content

Commit b74aad8

Browse files
Add permissions plugin
1 parent 54e3f99 commit b74aad8

6 files changed

Lines changed: 564 additions & 0 deletions

File tree

.gitignore

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
### macOS
2+
# Finder metadata
3+
.DS_Store
4+
5+
# Thumbnails
6+
._*
7+
8+
# Custom folder icons
9+
Icon
10+
11+
# Volume root files
12+
.DocumentRevisions-V100
13+
.fseventsd
14+
.Spotlight-V100
15+
.TemporaryItems
16+
.Trashes
17+
.VolumeIcon.icns
18+
.com.apple.timemachine.donotpresent
19+
20+
### VS Code
21+
# VSCode settings (keep shared configuration)
22+
.vscode/*
23+
!.vscode/settings.json
24+
!.vscode/tasks.json
25+
!.vscode/launch.json
26+
!.vscode/extensions.json
27+
28+
# Local History for Visual Studio Code
29+
.history/
30+
31+
# Built Visual Studio Code Extensions
32+
*.vsix
33+
34+
### Xcode
35+
# Xcode user settings
36+
xcuserdata/
37+
38+
# Xcode build artifacts
39+
build/
40+
DerivedData/
41+
42+
# Obj-C/Swift specific
43+
*.hmap
44+
*.ipa
45+
*.dSYM.zip
46+
*.dSYM
47+
48+
# Playgrounds
49+
timeline.xctimeline
50+
playground.xcworkspace
51+
52+
# Xcode state files
53+
*.xccheckout
54+
*.moved-aside
55+
*.xcuserstate
56+
*.xcscmblueprint
57+
58+
59+
### Objective-C
60+
# Xcode user settings
61+
xcuserdata/
62+
63+
# Xcode build data
64+
DerivedData/
65+
66+
# Obj-C/Swift specific
67+
*.hmap
68+
69+
# App packaging
70+
*.ipa
71+
*.dSYM.zip
72+
*.dSYM
73+
74+
# Playgrounds
75+
timeline.xctimeline
76+
playground.xcworkspace
77+
78+
### Swift
79+
# Xcode user settings
80+
xcuserdata/
81+
82+
# Xcode build data
83+
DerivedData/
84+
85+
# Swift/Obj-C specific
86+
*.hmap
87+
88+
# App packaging
89+
*.ipa
90+
*.dSYM.zip
91+
*.dSYM
92+
93+
# Playgrounds
94+
timeline.xctimeline
95+
playground.xcworkspace
96+
97+
# Swift Package Manager
98+
# Package.resolved

Package.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ let package = Package(
1616
name: "StreamLayerSDKPluginsGooglePAL",
1717
type: .static,
1818
targets: ["StreamLayerSDKPluginsGooglePAL"]
19+
),
20+
.library(
21+
name: "StreamLayerSDKPluginsPermissions",
22+
type: .static,
23+
targets: ["StreamLayerSDKPluginsPermissions"]
1924
)
2025
],
2126
dependencies: [
@@ -39,6 +44,13 @@ let package = Package(
3944
.product(name: "StreamLayerGooglePAL", package: "sdk-ios", condition: .when(platforms: [.iOS, .tvOS]))
4045
],
4146
path: "Sources/GooglePAL/"
47+
),
48+
.target(
49+
name: "StreamLayerSDKPluginsPermissions",
50+
dependencies: [
51+
.product(name: "StreamLayer", package: "sdk-ios", condition: .when(platforms: [.iOS]))
52+
],
53+
path: "Sources/Permissions/"
4254
)
4355
]
4456
)
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//
2+
// SLRCameraCaptureSessionImpl.swift
3+
// StreamLayerPlugins
4+
//
5+
// Copyright © 2024 StreamLayer, Inc. All rights reserved.
6+
//
7+
8+
#if os(iOS)
9+
import UIKit
10+
import AVFoundation
11+
import StreamLayerSDK
12+
13+
final class SLRCameraCaptureSessionImpl: SLRCameraCaptureSession {
14+
15+
private(set) var previewLayer: CALayer?
16+
private(set) var isInitialized: Bool = false
17+
private(set) var isCapturing: Bool = false
18+
19+
private var captureSession: AVCaptureSession?
20+
private let operationQueue: OperationQueue = {
21+
let queue = OperationQueue()
22+
queue.qualityOfService = .userInitiated
23+
queue.maxConcurrentOperationCount = 1
24+
return queue
25+
}()
26+
27+
func startCapturing(_ completion: @escaping () -> Void) {
28+
let operation = BlockOperation()
29+
operation.addExecutionBlock { [weak operation, weak self] in
30+
guard let self = self, let strongOperation = operation, !strongOperation.isCancelled else {
31+
DispatchQueue.main.async(execute: completion)
32+
return
33+
}
34+
35+
if !self.isInitialized {
36+
#if !(arch(i386) || arch(x86_64))
37+
let session = AVCaptureSession()
38+
let layer = AVCaptureVideoPreviewLayer(session: session)
39+
layer.videoGravity = .resizeAspectFill
40+
41+
if let device = AVCaptureDevice.default(.builtInWideAngleCamera,
42+
for: .video,
43+
position: .unspecified) {
44+
do {
45+
let input = try AVCaptureDeviceInput(device: device)
46+
if session.canAddInput(input) {
47+
session.addInput(input)
48+
}
49+
} catch {
50+
// Failed to create input device
51+
}
52+
}
53+
54+
self.captureSession = session
55+
self.previewLayer = layer
56+
#endif
57+
self.isInitialized = true
58+
}
59+
60+
self.captureSession?.startRunning()
61+
self.isCapturing = true
62+
DispatchQueue.main.async(execute: completion)
63+
}
64+
self.operationQueue.cancelAllOperations()
65+
self.operationQueue.addOperation(operation)
66+
}
67+
68+
func stopCapturing(_ completion: @escaping () -> Void) {
69+
let operation = BlockOperation()
70+
operation.addExecutionBlock { [weak operation, weak self] in
71+
guard let self = self, let strongOperation = operation, !strongOperation.isCancelled else {
72+
DispatchQueue.main.async(execute: completion)
73+
return
74+
}
75+
76+
self.captureSession?.stopRunning()
77+
self.captureSession?.inputs.forEach { self.captureSession?.removeInput($0) }
78+
self.isCapturing = false
79+
DispatchQueue.main.async(execute: completion)
80+
}
81+
self.operationQueue.cancelAllOperations()
82+
self.operationQueue.addOperation(operation)
83+
}
84+
85+
deinit {
86+
var layer = self.previewLayer
87+
layer?.removeFromSuperlayer()
88+
var session: AVCaptureSession? = self.isInitialized ? self.captureSession : nil
89+
DispatchQueue.global(qos: .default).async {
90+
if layer != nil { layer = nil }
91+
if session != nil { session = nil }
92+
}
93+
}
94+
}
95+
#endif
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
//
2+
// SLRPermissionDelegateImpl.swift
3+
// StreamLayerPlugins
4+
//
5+
// Copyright © 2024 StreamLayer, Inc. All rights reserved.
6+
//
7+
8+
#if os(iOS)
9+
import UIKit
10+
import AVFoundation
11+
import Contacts
12+
import Photos
13+
import StreamLayerSDK
14+
15+
final class SLRPermissionDelegateImpl: NSObject, SLRPermissionDelegate {
16+
17+
// MARK: - Permission Status
18+
19+
func permissionStatus(for type: SLRPermissionType) -> SLRPermissionStatus {
20+
switch type {
21+
case .camera:
22+
switch AVCaptureDevice.authorizationStatus(for: .video) {
23+
case .authorized: return .authorized
24+
case .denied: return .denied
25+
case .notDetermined: return .notDetermined
26+
case .restricted: return .restricted
27+
@unknown default: return .denied
28+
}
29+
case .microphone:
30+
switch AVAudioSession.sharedInstance().recordPermission {
31+
case .granted: return .authorized
32+
case .denied: return .denied
33+
case .undetermined: return .notDetermined
34+
@unknown default: return .denied
35+
}
36+
case .contacts:
37+
switch CNContactStore.authorizationStatus(for: .contacts) {
38+
case .authorized: return .authorized
39+
case .denied: return .denied
40+
case .notDetermined: return .notDetermined
41+
case .restricted: return .restricted
42+
@unknown default: return .denied
43+
}
44+
case .photoLibrary:
45+
switch PHPhotoLibrary.authorizationStatus() {
46+
case .authorized, .limited: return .authorized
47+
case .denied: return .denied
48+
case .notDetermined: return .notDetermined
49+
case .restricted: return .restricted
50+
@unknown default: return .denied
51+
}
52+
}
53+
}
54+
55+
// MARK: - Permission Requests
56+
57+
func requestPermission(for type: SLRPermissionType, completion: @escaping (Bool) -> Void) {
58+
switch type {
59+
case .camera:
60+
AVCaptureDevice.requestAccess(for: .video) { granted in
61+
DispatchQueue.main.async { completion(granted) }
62+
}
63+
case .microphone:
64+
AVAudioSession.sharedInstance().requestRecordPermission { granted in
65+
DispatchQueue.main.async { completion(granted) }
66+
}
67+
case .contacts:
68+
CNContactStore().requestAccess(for: .contacts) { granted, _ in
69+
DispatchQueue.main.async { completion(granted) }
70+
}
71+
case .photoLibrary:
72+
PHPhotoLibrary.requestAuthorization { status in
73+
DispatchQueue.main.async { completion(status == .authorized || status == .limited) }
74+
}
75+
}
76+
}
77+
78+
// MARK: - Contacts Data Access
79+
80+
func fetchContacts(completion: @escaping ([SLRContactInfo]) -> Void) {
81+
DispatchQueue.global(qos: .userInitiated).async {
82+
let store = CNContactStore()
83+
let keysToFetch: [CNKeyDescriptor] = [
84+
CNContactIdentifierKey as CNKeyDescriptor,
85+
CNContactGivenNameKey as CNKeyDescriptor,
86+
CNContactFamilyNameKey as CNKeyDescriptor,
87+
CNContactPhoneNumbersKey as CNKeyDescriptor,
88+
CNContactThumbnailImageDataKey as CNKeyDescriptor
89+
]
90+
let request = CNContactFetchRequest(keysToFetch: keysToFetch)
91+
var contacts: [SLRContactInfo] = []
92+
93+
do {
94+
try store.enumerateContacts(with: request) { cnContact, _ in
95+
let phoneNumbers = cnContact.phoneNumbers.map { $0.value.stringValue }
96+
contacts.append(SLRContactInfo(
97+
identifier: cnContact.identifier,
98+
givenName: cnContact.givenName,
99+
familyName: cnContact.familyName,
100+
phoneNumbers: phoneNumbers,
101+
thumbnailImageData: cnContact.thumbnailImageData
102+
))
103+
}
104+
} catch {
105+
print("Failed to fetch contacts: \(error)")
106+
}
107+
108+
DispatchQueue.main.async { completion(contacts) }
109+
}
110+
}
111+
112+
func fetchContact(identifier: String, completion: @escaping (SLRContactInfo?) -> Void) {
113+
DispatchQueue.global(qos: .userInitiated).async {
114+
let store = CNContactStore()
115+
let keysToFetch: [CNKeyDescriptor] = [
116+
CNContactIdentifierKey as CNKeyDescriptor,
117+
CNContactGivenNameKey as CNKeyDescriptor,
118+
CNContactFamilyNameKey as CNKeyDescriptor,
119+
CNContactPhoneNumbersKey as CNKeyDescriptor,
120+
CNContactThumbnailImageDataKey as CNKeyDescriptor
121+
]
122+
do {
123+
let cnContact = try store.unifiedContact(withIdentifier: identifier,
124+
keysToFetch: keysToFetch)
125+
let info = SLRContactInfo(
126+
identifier: cnContact.identifier,
127+
givenName: cnContact.givenName,
128+
familyName: cnContact.familyName,
129+
phoneNumbers: cnContact.phoneNumbers.map { $0.value.stringValue },
130+
thumbnailImageData: cnContact.thumbnailImageData
131+
)
132+
DispatchQueue.main.async { completion(info) }
133+
} catch {
134+
DispatchQueue.main.async { completion(nil) }
135+
}
136+
}
137+
}
138+
139+
// MARK: - Photo Library Data Source
140+
141+
func createPhotoLibraryDataSource() -> SLRPhotoLibraryDataSource? {
142+
return SLRPhotoLibraryDataSourceImpl()
143+
}
144+
145+
// MARK: - Camera Capture Session
146+
147+
func createCameraCaptureSession() -> SLRCameraCaptureSession? {
148+
return SLRCameraCaptureSessionImpl()
149+
}
150+
}
151+
#endif

0 commit comments

Comments
 (0)