diff --git a/Linphone/Contacts/ContactsManager.swift b/Linphone/Contacts/ContactsManager.swift index 43e1fd5ed..e252d20f7 100644 --- a/Linphone/Contacts/ContactsManager.swift +++ b/Linphone/Contacts/ContactsManager.swift @@ -394,118 +394,122 @@ final class ContactsManager: ObservableObject { } func addFriendListDelegate() { - CoreContext.shared.mCore.friendListSubscriptionEnabled = true - - CoreContext.shared.mCore.friendsLists.forEach { friendList in - friendList.updateSubscriptions() - } - - let friendListDelegateTmp = FriendListDelegateStub( - onContactCreated: { (friendList: FriendList, linphoneFriend: Friend) in - Log.info("\(ContactsManager.TAG) FriendListDelegateStub onContactCreated") - }, - onContactDeleted: { (friendList: FriendList, linphoneFriend: Friend) in - Log.info("\(ContactsManager.TAG) FriendListDelegateStub onContactDeleted") - }, - onContactUpdated: { (friendList: FriendList, newFriend: Friend, oldFriend: Friend) in - Log.info("\(ContactsManager.TAG) FriendListDelegateStub onContactUpdated") - }, - onSyncStatusChanged: { (friendList: FriendList, status: FriendList.SyncStatus?, message: String?) in - Log.info("\(ContactsManager.TAG) FriendListDelegateStub onSyncStatusChanged") - if status == .Successful { - friendList.friends.forEach { friend in - let addressTmp = friend.address?.clone()?.asStringUriOnly() ?? "" - - let newContact = Contact( - identifier: UUID().uuidString, - firstName: friend.name ?? addressTmp, - lastName: "", - organizationName: "", - jobTitle: "", - displayName: friend.address?.displayName ?? "", - sipAddresses: friend.addresses.map { $0.asStringUriOnly() }, - phoneNumbers: [], - imageData: "" - ) - - self.textToImageInMainThread(firstName: friend.name ?? addressTmp, lastName: "") { image in - self.saveImage( - image: image, - name: friend.name ?? addressTmp, - prefix: "-default", - contact: newContact, linphoneFriend: false, existingFriend: friend) { - - } - } - } - } - - MagicSearchSingleton.shared.searchForContactsWithoutCoreThread(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) - }, - onPresenceReceived: { (friendList: FriendList, friends: [Friend?]) in - Log.info("\(ContactsManager.TAG) FriendListDelegateStub onPresenceReceived \(friends.count)") - }, - onNewSipAddressDiscovered: { (friendList: FriendList, linphoneFriend: Friend, sipUri: String) in - Log.info("\(ContactsManager.TAG) FriendListDelegateStub onNewSipAddressDiscovered \(linphoneFriend.name ?? "")") - var addedAvatarListModel: [ContactAvatarModel] = [] - if !self.avatarListModel.contains(where: {$0.friend?.name == linphoneFriend.name}) { - if let address = try? Factory.Instance.createAddress(addr: sipUri) { - linphoneFriend.edit() - linphoneFriend.addAddress(address: address) - linphoneFriend.done() - - let addressTmp = linphoneFriend.address?.clone()?.asStringUriOnly() ?? "" - addedAvatarListModel.append( - ContactAvatarModel( - friend: linphoneFriend, - name: linphoneFriend.name ?? "", - address: addressTmp, - withPresence: true - ) - ) - - addedAvatarListModel += self.avatarListModel - addedAvatarListModel = addedAvatarListModel.sorted { $0.name < $1.name } - - DispatchQueue.main.async { - self.avatarListModel = addedAvatarListModel - - NotificationCenter.default.post( - name: NSNotification.Name("ContactAdded"), - object: nil, - userInfo: ["address": addressTmp] - ) - } - } - } + self.coreContext.doOnCoreQueue { _ in + CoreContext.shared.mCore.friendListSubscriptionEnabled = true + + CoreContext.shared.mCore.friendsLists.forEach { friendList in + friendList.updateSubscriptions() + } + + let friendListDelegateTmp = FriendListDelegateStub( + onContactCreated: { (friendList: FriendList, linphoneFriend: Friend) in + Log.info("\(ContactsManager.TAG) FriendListDelegateStub onContactCreated") + }, + onContactDeleted: { (friendList: FriendList, linphoneFriend: Friend) in + Log.info("\(ContactsManager.TAG) FriendListDelegateStub onContactDeleted") + }, + onContactUpdated: { (friendList: FriendList, newFriend: Friend, oldFriend: Friend) in + Log.info("\(ContactsManager.TAG) FriendListDelegateStub onContactUpdated") + }, + onSyncStatusChanged: { (friendList: FriendList, status: FriendList.SyncStatus?, message: String?) in + Log.info("\(ContactsManager.TAG) FriendListDelegateStub onSyncStatusChanged") + if status == .Successful { + friendList.friends.forEach { friend in + let addressTmp = friend.address?.clone()?.asStringUriOnly() ?? "" + + let newContact = Contact( + identifier: UUID().uuidString, + firstName: friend.name ?? addressTmp, + lastName: "", + organizationName: "", + jobTitle: "", + displayName: friend.address?.displayName ?? "", + sipAddresses: friend.addresses.map { $0.asStringUriOnly() }, + phoneNumbers: [], + imageData: "" + ) + + self.textToImageInMainThread(firstName: friend.name ?? addressTmp, lastName: "") { image in + self.saveImage( + image: image, + name: friend.name ?? addressTmp, + prefix: "-default", + contact: newContact, linphoneFriend: false, existingFriend: friend) { + + } + } + } + } + + MagicSearchSingleton.shared.searchForContactsWithoutCoreThread(sourceFlags: MagicSearch.Source.Friends.rawValue | MagicSearch.Source.LdapServers.rawValue) + }, + onPresenceReceived: { (friendList: FriendList, friends: [Friend?]) in + Log.info("\(ContactsManager.TAG) FriendListDelegateStub onPresenceReceived \(friends.count)") + }, + onNewSipAddressDiscovered: { (friendList: FriendList, linphoneFriend: Friend, sipUri: String) in + Log.info("\(ContactsManager.TAG) FriendListDelegateStub onNewSipAddressDiscovered \(linphoneFriend.name ?? "")") + var addedAvatarListModel: [ContactAvatarModel] = [] + if !self.avatarListModel.contains(where: {$0.friend?.name == linphoneFriend.name}) { + if let address = try? Factory.Instance.createAddress(addr: sipUri) { + linphoneFriend.edit() + linphoneFriend.addAddress(address: address) + linphoneFriend.done() + + let addressTmp = linphoneFriend.address?.clone()?.asStringUriOnly() ?? "" + addedAvatarListModel.append( + ContactAvatarModel( + friend: linphoneFriend, + name: linphoneFriend.name ?? "", + address: addressTmp, + withPresence: true + ) + ) + + addedAvatarListModel += self.avatarListModel + addedAvatarListModel = addedAvatarListModel.sorted { $0.name < $1.name } + + DispatchQueue.main.async { + self.avatarListModel = addedAvatarListModel + + NotificationCenter.default.post( + name: NSNotification.Name("ContactAdded"), + object: nil, + userInfo: ["address": addressTmp] + ) + } + } + } + } + ) + + CoreContext.shared.mCore.friendsLists.forEach { friendList in + friendList.addDelegate(delegate: friendListDelegateTmp) } - ) - - CoreContext.shared.mCore.friendsLists.forEach { friendList in - friendList.addDelegate(delegate: friendListDelegateTmp) } } func addCoreDelegate(core: Core) { - self.coreDelegate = CoreDelegateStub( - onFriendListCreated: { (_: Core, friendList: FriendList) in - Log.info("\(ContactsManager.TAG) Friend list \(friendList.displayName) created") - if self.friendListDelegate != nil { - friendList.addDelegate(delegate: self.friendListDelegate!) + self.coreContext.doOnCoreQueue { _ in + self.coreDelegate = CoreDelegateStub( + onFriendListCreated: { (_: Core, friendList: FriendList) in + Log.info("\(ContactsManager.TAG) Friend list \(friendList.displayName) created") + if self.friendListDelegate != nil { + friendList.addDelegate(delegate: self.friendListDelegate!) + } + }, onFriendListRemoved: { (_: Core, friendList: FriendList) in + Log.info("\(ContactsManager.TAG) Friend list \(friendList.displayName) removed") + if self.friendListDelegate != nil { + friendList.removeDelegate(delegate: self.friendListDelegate!) + } + }, onDefaultAccountChanged: { (_: Core, _: Account?) in + Log.info("\(ContactsManager.TAG) Default account changed, update all contacts' model showTrust value") + //updateContactsModelDependingOnDefaultAccountMode() } - }, onFriendListRemoved: { (_: Core, friendList: FriendList) in - Log.info("\(ContactsManager.TAG) Friend list \(friendList.displayName) removed") - if self.friendListDelegate != nil { - friendList.removeDelegate(delegate: self.friendListDelegate!) - } - }, onDefaultAccountChanged: { (_: Core, _: Account?) in - Log.info("\(ContactsManager.TAG) Default account changed, update all contacts' model showTrust value") - //updateContactsModelDependingOnDefaultAccountMode() + ) + + if self.coreDelegate != nil { + core.addDelegate(delegate: self.coreDelegate!) } - ) - - if self.coreDelegate != nil { - core.addDelegate(delegate: self.coreDelegate!) } } diff --git a/Linphone/Info.plist b/Linphone/Info.plist index 2912e9348..d546482b6 100644 --- a/Linphone/Info.plist +++ b/Linphone/Info.plist @@ -4,6 +4,16 @@ CFBundleURLTypes + + CFBundleTypeRole + Editor + CFBundleURLName + org.linphone.phone + CFBundleURLSchemes + + linphone-message + + CFBundleTypeRole Editor diff --git a/Linphone/LinphoneApp.swift b/Linphone/LinphoneApp.swift index 3b475c3cd..61d96ccdb 100644 --- a/Linphone/LinphoneApp.swift +++ b/Linphone/LinphoneApp.swift @@ -190,19 +190,34 @@ struct RootView: View { ToastView().zIndex(3) } } else { - MainViewSwitcher( - coreContext: coreContext, - navigationManager: navigationManager, - sharedMainViewModel: sharedMainViewModel, - pendingURL: $pendingURL, - appDelegate: appDelegate - ) + ZStack { + MainViewSwitcher( + coreContext: coreContext, + navigationManager: navigationManager, + sharedMainViewModel: sharedMainViewModel, + pendingURL: $pendingURL, + appDelegate: appDelegate + ) + + if coreContext.coreIsStarted { + VStack {} // Force trigger .onAppear + .onAppear { + if let url = pendingURL { + URIHandler.handleURL(url: url) + pendingURL = nil + } + } + } + } } } else { SplashScreen() } } .onOpenURL { url in + if SharedMainViewModel.shared.displayedConversation != nil && url.absoluteString.contains("linphone-message://") { + SharedMainViewModel.shared.displayedConversation = nil + } if coreContext.coreIsStarted { URIHandler.handleURL(url: url) } else { @@ -230,19 +245,7 @@ struct MainViewSwitcher: View { let appDelegate: AppDelegate var body: some View { - ZStack { - if coreContext.coreIsStarted { - selectedMainView() - - VStack {} // Force trigger .onAppear - .onAppear { - if let url = pendingURL { - URIHandler.handleURL(url: url) - pendingURL = nil - } - } - } - } + selectedMainView() } @ViewBuilder diff --git a/Linphone/Localizable/en.lproj/Localizable.strings b/Linphone/Localizable/en.lproj/Localizable.strings index 17ef6fd28..765b398e7 100644 --- a/Linphone/Localizable/en.lproj/Localizable.strings +++ b/Linphone/Localizable/en.lproj/Localizable.strings @@ -218,6 +218,8 @@ "conversation_event_participant_removed" = "%@ has left"; "conversation_event_subject_changed" = "New subject: %@"; "conversation_failed_to_create_toast" = "Failed to create conversation!"; +"conversations_files_waiting_to_be_shared_single" = "1 file waiting to be shared"; +"conversations_files_waiting_to_be_shared_multiple" = "%@ files waiting to be shared"; "conversation_forward_message_title" = "Forward message to…"; "conversation_info_add_participants_label" = "Add participants"; "conversation_info_admin_menu_remove_participant" = "Remove from the group"; diff --git a/Linphone/Localizable/fr.lproj/Localizable.strings b/Linphone/Localizable/fr.lproj/Localizable.strings index 48cab1d2a..0dade4119 100644 --- a/Linphone/Localizable/fr.lproj/Localizable.strings +++ b/Linphone/Localizable/fr.lproj/Localizable.strings @@ -218,6 +218,8 @@ "conversation_event_participant_removed" = "%@ a quitté la conversation"; "conversation_event_subject_changed" = "La conversation a été renommée : %@"; "conversation_failed_to_create_toast" = "Échec de création de la conversation !"; +"conversations_files_waiting_to_be_shared_single" = "1 fichier en attente de partage"; +"conversations_files_waiting_to_be_shared_multiple" = "%@ fichiers en attente de partage"; "conversation_forward_message_title" = "Transférer à…"; "conversation_info_add_participants_label" = "Ajouter des participants"; "conversation_info_admin_menu_remove_participant" = "Retirer participant"; diff --git a/Linphone/UI/Main/ContentView.swift b/Linphone/UI/Main/ContentView.swift index b733e0718..b07e20acf 100644 --- a/Linphone/UI/Main/ContentView.swift +++ b/Linphone/UI/Main/ContentView.swift @@ -85,6 +85,47 @@ struct ContentView: View { var body: some View { GeometryReader { geometry in VStack(spacing: 0) { + if !sharedMainViewModel.fileUrlsToShare.isEmpty && !telecomManager.callInProgress || (telecomManager.callInProgress && !telecomManager.callDisplayed) { + HStack { + Image("share-network") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 26, height: 26) + .padding(.leading, 10) + + if sharedMainViewModel.fileUrlsToShare.count > 1 { + Text(String(format: String(localized: "conversations_files_waiting_to_be_shared_multiple"), sharedMainViewModel.fileUrlsToShare.count.description)) + .default_text_style_white(styleSize: 16) + } else { + Text(String(localized: "conversations_files_waiting_to_be_shared_single")) + .default_text_style_white(styleSize: 16) + } + + Spacer() + + Button( + action: { + withAnimation { + sharedMainViewModel.fileUrlsToShare = [] + } + }, label: { + Image("x") + .renderingMode(.template) + .resizable() + .foregroundStyle(.white) + .frame(width: 26, height: 26) + .padding(.trailing, 10) + } + ) + + } + .frame(maxWidth: .infinity) + .frame(height: 40) + .padding(.horizontal, 10) + .background(Color.gray) + } + if (telecomManager.callInProgress && !fullscreenVideo && ((!telecomManager.callDisplayed && callViewModel.callsCounter == 1) || callViewModel.callsCounter > 1)) || isShowConversationFragment { HStack { Image("phone") @@ -111,7 +152,8 @@ struct ContentView: View { } } .frame(maxWidth: .infinity) - .frame(height: 30) + .frame(height: 40) + .padding(.horizontal, 10) .background(Color.greenSuccess500) .onTapGesture { withAnimation { diff --git a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift index 26dc09826..a12d44fc0 100644 --- a/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift +++ b/Linphone/UI/Main/Conversations/Fragments/ConversationFragment.swift @@ -199,11 +199,30 @@ struct ConversationFragment: View { } } .navigationViewStyle(.stack) - .onAppear { - if let conv = SharedMainViewModel.shared.displayedConversation { - cachedConversation = conv - } - } + .onAppear { + if let conv = SharedMainViewModel.shared.displayedConversation { + cachedConversation = conv + } + + if !SharedMainViewModel.shared.fileUrlsToShare.isEmpty { + var urlList: [URL] = [] + SharedMainViewModel.shared.fileUrlsToShare.forEach { urlFile in + urlList.append(URL(fileURLWithPath: urlFile)) + } + + FilePicker.convertToAttachmentArray(fromResults: urlList) { mediasOrNil, errorOrNil in + if let error = errorOrNil { + print(error) + } + + if let medias = mediasOrNil { + conversationViewModel.mediasToSend.append(contentsOf: medias) + } + } + + SharedMainViewModel.shared.fileUrlsToShare.removeAll() + } + } } // swiftlint:disable cyclomatic_complexity diff --git a/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift b/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift index b9e20049f..4c2c41b80 100644 --- a/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift +++ b/Linphone/UI/Main/Viewmodel/SharedMainViewModel.swift @@ -39,6 +39,8 @@ class SharedMainViewModel: ObservableObject { @Published var dialPlansLabelList: [String] = [] @Published var dialPlansShortLabelList: [String] = [] + @Published var fileUrlsToShare: [String] = [] + @Published var operationInProgress = false let welcomeViewKey = "welcome_view" diff --git a/Linphone/Utils/URIHandler.swift b/Linphone/Utils/URIHandler.swift index d478f896b..8710d3b21 100644 --- a/Linphone/Utils/URIHandler.swift +++ b/Linphone/Utils/URIHandler.swift @@ -27,6 +27,7 @@ class URIHandler { private static let callSchemes = ["sip", "sip-linphone", "linphone-sip", "tel", "callto"] private static let secureCallSchemes = ["sips", "sips-linphone", "linphone-sips"] private static let configurationSchemes = ["linphone-config"] + private static let sharedExtensionSchemes = ["linphone-message"] private static var uriHandlerCoreDelegate: CoreDelegateStub? @@ -63,6 +64,8 @@ class URIHandler { initiateCall(url: url, withScheme: "sip") } else if configurationSchemes.contains(scheme) { initiateConfiguration(url: url) + } else if sharedExtensionSchemes.contains(scheme) { + processReceivedFiles(url: url) } else if scheme == SingleSignOnManager.shared.ssoRedirectUri.scheme { continueSSO(url: url) } else { @@ -113,6 +116,21 @@ class URIHandler { } } + private static func processReceivedFiles(url: URL) { + Log.info("[URIHandler] processing received files from URL: \(url.path)") + + var urlString = url.path + if urlString.starts(with: "//") { + urlString = String(urlString.dropFirst(2)) + } + + for urlFile in urlString.components(separatedBy: ",") { + SharedMainViewModel.shared.fileUrlsToShare.append(urlFile) + } + + SharedMainViewModel.shared.changeIndexView(indexViewInt: 2) + } + private static func continueSSO(url: URL) { if let authorizationFlow = SingleSignOnManager.shared.currentAuthorizationFlow, authorizationFlow.resumeExternalUserAgentFlow(with: url) { diff --git a/LinphoneApp.xcodeproj/project.pbxproj b/LinphoneApp.xcodeproj/project.pbxproj index c4d0af8e3..ca44cc4da 100644 --- a/LinphoneApp.xcodeproj/project.pbxproj +++ b/LinphoneApp.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 56; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ @@ -108,6 +108,7 @@ D73449992BC6932A00778C56 /* MeetingWaitingRoomFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D73449982BC6932A00778C56 /* MeetingWaitingRoomFragment.swift */; }; D734499B2BC694C900778C56 /* MeetingWaitingRoomViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D734499A2BC694C900778C56 /* MeetingWaitingRoomViewModel.swift */; }; D737AEEF2DA011F2005C1280 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D737AEED2DA011F2005C1280 /* Localizable.strings */; }; + D7458F392E0BDCF4000C957A /* linphoneExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = D7458F2F2E0BDCF4000C957A /* linphoneExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; D748BF2C2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D748BF2B2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift */; }; D748BF2E2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D748BF2D2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift */; }; D74C9CF82ACACECE0021626A /* WelcomePage1Fragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D74C9CF72ACACECE0021626A /* WelcomePage1Fragment.swift */; }; @@ -204,6 +205,13 @@ remoteGlobalIDString = 660AAF7A2B839271004C0FA6; remoteInfo = msgNotificationService; }; + D7458F372E0BDCF4000C957A /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D719ABAB2ABC67BF00B41C10 /* Project object */; + proxyType = 1; + remoteGlobalIDString = D7458F2E2E0BDCF4000C957A; + remoteInfo = linphoneExtension; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -214,6 +222,7 @@ dstSubfolderSpec = 13; files = ( 660AAF7F2B839272004C0FA6 /* msgNotificationService.appex in Embed Foundation Extensions */, + D7458F392E0BDCF4000C957A /* linphoneExtension.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -321,6 +330,7 @@ D734499A2BC694C900778C56 /* MeetingWaitingRoomViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeetingWaitingRoomViewModel.swift; sourceTree = ""; }; D737AEEE2DA011F2005C1280 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = Localizable/en.lproj/Localizable.strings; sourceTree = ""; }; D737AEF02DA01203005C1280 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = Localizable/fr.lproj/Localizable.strings; sourceTree = ""; }; + D7458F2F2E0BDCF4000C957A /* linphoneExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = linphoneExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; D748BF2B2ACD82D2004844EB /* ThirdPartySipAccountLoginFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartySipAccountLoginFragment.swift; sourceTree = ""; }; D748BF2D2ACD82E7004844EB /* ThirdPartySipAccountWarningFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThirdPartySipAccountWarningFragment.swift; sourceTree = ""; }; D74C9CF72ACACECE0021626A /* WelcomePage1Fragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomePage1Fragment.swift; sourceTree = ""; }; @@ -403,6 +413,20 @@ D7FB55102AD447FD00A5AB15 /* RegisterFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterFragment.swift; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + D7458F3C2E0BDCF4000C957A /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = D7458F2E2E0BDCF4000C957A /* linphoneExtension */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + D7458F302E0BDCF4000C957A /* linphoneExtension */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (D7458F3C2E0BDCF4000C957A /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = linphoneExtension; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 660AAF782B839271004C0FA6 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -425,6 +449,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D7458F2C2E0BDCF4000C957A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -568,6 +599,7 @@ 660D8A702B517D260092694D /* GoogleService-Info.plist */, D719ABB52ABC67BF00B41C10 /* Linphone */, 660AAF7C2B839272004C0FA6 /* msgNotificationService */, + D7458F302E0BDCF4000C957A /* linphoneExtension */, D719ABB42ABC67BF00B41C10 /* Products */, ); sourceTree = ""; @@ -577,6 +609,7 @@ children = ( D719ABB32ABC67BF00B41C10 /* LinphoneApp.app */, 660AAF7B2B839271004C0FA6 /* msgNotificationService.appex */, + D7458F2F2E0BDCF4000C957A /* linphoneExtension.appex */, ); name = Products; sourceTree = ""; @@ -1037,12 +1070,35 @@ ); dependencies = ( 660AAF7E2B839272004C0FA6 /* PBXTargetDependency */, + D7458F382E0BDCF4000C957A /* PBXTargetDependency */, ); name = LinphoneApp; productName = Linphone; productReference = D719ABB32ABC67BF00B41C10 /* LinphoneApp.app */; productType = "com.apple.product-type.application"; }; + D7458F2E2E0BDCF4000C957A /* linphoneExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = D7458F3D2E0BDCF4000C957A /* Build configuration list for PBXNativeTarget "linphoneExtension" */; + buildPhases = ( + D7458F2B2E0BDCF4000C957A /* Sources */, + D7458F2C2E0BDCF4000C957A /* Frameworks */, + D7458F2D2E0BDCF4000C957A /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + D7458F302E0BDCF4000C957A /* linphoneExtension */, + ); + name = linphoneExtension; + packageProductDependencies = ( + ); + productName = linphoneExtension; + productReference = D7458F2F2E0BDCF4000C957A /* linphoneExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -1050,7 +1106,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1430; + LastSwiftUpdateCheck = 1640; LastUpgradeCheck = 1430; TargetAttributes = { 660AAF7A2B839271004C0FA6 = { @@ -1060,6 +1116,9 @@ D719ABB22ABC67BF00B41C10 = { CreatedOnToolsVersion = 14.3.1; }; + D7458F2E2E0BDCF4000C957A = { + CreatedOnToolsVersion = 16.4; + }; }; }; buildConfigurationList = D719ABAE2ABC67BF00B41C10 /* Build configuration list for PBXProject "LinphoneApp" */; @@ -1085,6 +1144,7 @@ targets = ( D719ABB22ABC67BF00B41C10 /* LinphoneApp */, 660AAF7A2B839271004C0FA6 /* msgNotificationService */, + D7458F2E2E0BDCF4000C957A /* linphoneExtension */, ); }; /* End PBXProject section */ @@ -1121,6 +1181,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D7458F2D2E0BDCF4000C957A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -1320,6 +1387,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + D7458F2B2E0BDCF4000C957A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -1328,6 +1402,11 @@ target = 660AAF7A2B839271004C0FA6 /* msgNotificationService */; targetProxy = 660AAF7D2B839272004C0FA6 /* PBXContainerItemProxy */; }; + D7458F382E0BDCF4000C957A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D7458F2E2E0BDCF4000C957A /* linphoneExtension */; + targetProxy = D7458F372E0BDCF4000C957A /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -1664,6 +1743,74 @@ }; name = Release; }; + D7458F3A2E0BDCF4000C957A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CODE_SIGN_ENTITLEMENTS = linphoneExtension/linphoneExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = Z2V957B3D6; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = linphoneExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = linphoneExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone.linphoneExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + D7458F3B2E0BDCF4000C957A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CODE_SIGN_ENTITLEMENTS = linphoneExtension/linphoneExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = Z2V957B3D6; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = linphoneExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = linphoneExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 18.5; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.linphone.phone.linphoneExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1694,6 +1841,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + D7458F3D2E0BDCF4000C957A /* Build configuration list for PBXNativeTarget "linphoneExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D7458F3A2E0BDCF4000C957A /* Debug */, + D7458F3B2E0BDCF4000C957A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ diff --git a/LinphoneApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/LinphoneApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 8d86d14dd..f107b5f05 100644 --- a/LinphoneApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/LinphoneApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -115,7 +115,7 @@ "location" : "https://gitlab.linphone.org/BC/public/linphone-sdk-swift-ios.git", "state" : { "branch" : "stable", - "revision" : "ffedf77da711ea0f90ddd99a1785a84268413122" + "revision" : "2d65f089792cb91c50f51dc26417c7d4c0ebb988" } }, { diff --git a/linphoneExtension/Base.lproj/MainInterface.storyboard b/linphoneExtension/Base.lproj/MainInterface.storyboard new file mode 100644 index 000000000..286a50894 --- /dev/null +++ b/linphoneExtension/Base.lproj/MainInterface.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/linphoneExtension/Info.plist b/linphoneExtension/Info.plist new file mode 100644 index 000000000..4b1f7e705 --- /dev/null +++ b/linphoneExtension/Info.plist @@ -0,0 +1,18 @@ + + + + + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + TRUEPREDICATE + + NSExtensionMainStoryboard + MainInterface + NSExtensionPointIdentifier + com.apple.share-services + + + diff --git a/linphoneExtension/ShareViewController.swift b/linphoneExtension/ShareViewController.swift new file mode 100644 index 000000000..396bb3273 --- /dev/null +++ b/linphoneExtension/ShareViewController.swift @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2010-2024 Belledonne Communications SARL. + * + * This file is part of linphone-iphone + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +import UIKit +import Social + +class ShareViewController: SLComposeServiceViewController { + + var remainingSlots = 12 + + override func isContentValid() -> Bool { + return true + } + + override func viewDidAppear(_ animated: Bool) { + handleSharedFiles() + } + + override func configurationItems() -> [Any]! { + return [] + } + + private func handleSharedFiles() { + guard let extensionItems = self.extensionContext?.inputItems as? [NSExtensionItem] else { return } + + var fileURLs: [URL] = [] + let dispatchGroup = DispatchGroup() + + for item in extensionItems { + if let attachments = item.attachments { + for provider in attachments { + guard remainingSlots > 0 else { break } + if provider.hasItemConformingToTypeIdentifier("public.item") { + remainingSlots -= 1 + dispatchGroup.enter() + provider.loadFileRepresentation(forTypeIdentifier: "public.item") { urlFile, error in + if let url = urlFile { + if let urlSaved = self.copyFileToSharedContainer(from: url) { + fileURLs.append(urlSaved) + } + } + dispatchGroup.leave() + } + } + } + } + } + + dispatchGroup.notify(queue: .main) { + if !fileURLs.isEmpty { + self.openParentApp(with: fileURLs) + } else { + self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil) + } + } + } + + func openParentApp(with fileURLs: [URL]) { + let urlStrings = fileURLs.map { $0.path } + let joinedURLs = urlStrings.joined(separator: ",") + + let urlScheme = "linphone-message://\(joinedURLs)" + if let url = URL(string: urlScheme) { + var responder: UIResponder? = self + while responder != nil { + if let application = responder as? UIApplication { + application.open(url) + break + } + responder = responder?.next + } + } + + self.extensionContext?.completeRequest(returningItems: [], completionHandler: nil) + } + + func copyFileToSharedContainer(from url: URL) -> URL? { + let fileManager = FileManager.default + guard let sharedContainerURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: "group.org.linphone.phone.linphoneExtension") else { + return nil + } + + let destinationURL = sharedContainerURL.appendingPathComponent(url.lastPathComponent) + + do { + if fileManager.fileExists(atPath: destinationURL.path) { + try fileManager.removeItem(at: destinationURL) + } + + try fileManager.copyItem(at: url, to: destinationURL) + return destinationURL + } catch { + return nil + } + } +} + diff --git a/linphoneExtension/linphoneExtension.entitlements b/linphoneExtension/linphoneExtension.entitlements new file mode 100644 index 000000000..36778db04 --- /dev/null +++ b/linphoneExtension/linphoneExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.org.linphone.phone.linphoneExtension + + +