From 7843814429b4ccd1f60c57a6c9921122c4548bde Mon Sep 17 00:00:00 2001 From: Matte23 Date: Mon, 21 Dec 2020 16:38:22 +0100 Subject: [PATCH] Add Spotlight support for iOS --- .../viewmodels/CircularLetterViewModel.kt | 2 +- .../net/underdesk/circolapp/works/PollWork.kt | 2 +- .../circolapp.xcodeproj/project.pbxproj | 4 ++ ios/circolapp/circolapp/AppDelegate.swift | 18 ++---- .../circolapp/Model/iOSRepository.swift | 62 +++++++++++++++++++ .../circolapp/Model/iOSServerApi.swift | 12 ++-- ios/circolapp/circolapp/SceneDelegate.swift | 26 +++++++- ios/circolapp/circolapp/URLUtils.swift | 27 ++++++++ .../circolapp/View/AttachmentView.swift | 4 +- .../circolapp/View/CircularView.swift | 4 +- .../ViewModel/CircularViewModel.swift | 13 +--- .../shared/data/CircularRepository.kt | 8 ++- 12 files changed, 139 insertions(+), 43 deletions(-) create mode 100644 ios/circolapp/circolapp/URLUtils.swift diff --git a/app/src/main/java/net/underdesk/circolapp/viewmodels/CircularLetterViewModel.kt b/app/src/main/java/net/underdesk/circolapp/viewmodels/CircularLetterViewModel.kt index cde31c8..4694044 100644 --- a/app/src/main/java/net/underdesk/circolapp/viewmodels/CircularLetterViewModel.kt +++ b/app/src/main/java/net/underdesk/circolapp/viewmodels/CircularLetterViewModel.kt @@ -65,7 +65,7 @@ class CircularLetterViewModel internal constructor( viewModelScope.launch { isNotUpdating = false - if (!circularRepository.updateCirculars(false).second) { + if (circularRepository.updateCirculars(false).second == -1) { showMessage.postValue(true) } diff --git a/app/src/main/java/net/underdesk/circolapp/works/PollWork.kt b/app/src/main/java/net/underdesk/circolapp/works/PollWork.kt index b927143..0caf332 100644 --- a/app/src/main/java/net/underdesk/circolapp/works/PollWork.kt +++ b/app/src/main/java/net/underdesk/circolapp/works/PollWork.kt @@ -94,7 +94,7 @@ class PollWork(appContext: Context, workerParams: WorkerParameters) : val circularRepository = AndroidCircularRepository.getInstance(applicationContext) val result = circularRepository.updateCirculars() - if (!result.second) + if (result.second == -1) return@coroutineScope Result.retry() val newCirculars = result.first diff --git a/ios/circolapp/circolapp.xcodeproj/project.pbxproj b/ios/circolapp/circolapp.xcodeproj/project.pbxproj index c25c07e..b1f7ea5 100644 --- a/ios/circolapp/circolapp.xcodeproj/project.pbxproj +++ b/ios/circolapp/circolapp.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ 95CA31B7255C1EE000AC095B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 95CA31B6255C1EE000AC095B /* Preview Assets.xcassets */; }; 95CA31C0255C28C300AC095B /* CircularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CA31BF255C28C300AC095B /* CircularView.swift */; }; 95DB71AB258A1C1500A78033 /* CarteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95DB71AA258A1C1500A78033 /* CarteView.swift */; }; + 95F6666E2590D712006DE74F /* URLUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95F6666D2590D712006DE74F /* URLUtils.swift */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -86,6 +87,7 @@ 95CA31B8255C1EE000AC095B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 95CA31BF255C28C300AC095B /* CircularView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularView.swift; sourceTree = ""; }; 95DB71AA258A1C1500A78033 /* CarteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarteView.swift; sourceTree = ""; }; + 95F6666D2590D712006DE74F /* URLUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLUtils.swift; sourceTree = ""; }; 976621FBDDCA894FD23FBA8B /* Pods-circolapp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-circolapp.release.xcconfig"; path = "Target Support Files/Pods-circolapp/Pods-circolapp.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -193,6 +195,7 @@ 95906F4C257510370060F5D0 /* GoogleService-Info.plist */, 95906F47257506360060F5D0 /* Assets.xcassets */, 95CA31B5255C1EE000AC095B /* Preview Content */, + 95F6666D2590D712006DE74F /* URLUtils.swift */, ); path = circolapp; sourceTree = ""; @@ -382,6 +385,7 @@ 955B7A0E257D098C0091B1F9 /* SearchBar.swift in Sources */, 9554BDB8257E498F00D8925B /* OnboardingView.swift in Sources */, 950C1788258E5BD300B2DBFE /* PhoneView.swift in Sources */, + 95F6666E2590D712006DE74F /* URLUtils.swift in Sources */, 95C46A51255D3A34007A75E5 /* CircularViewModel.swift in Sources */, 95B4CE142588BC890090D5E8 /* SettingsView.swift in Sources */, ); diff --git a/ios/circolapp/circolapp/AppDelegate.swift b/ios/circolapp/circolapp/AppDelegate.swift index 87ee286..743deea 100644 --- a/ios/circolapp/circolapp/AppDelegate.swift +++ b/ios/circolapp/circolapp/AppDelegate.swift @@ -71,14 +71,8 @@ extension AppDelegate : UNUserNotificationCenterDelegate { } // Handle new circular notification - iOSRepository.getCircularRepository().updateCirculars(returnNewCirculars: false, completionHandler: - { result, error in - if let errorReal = error { - print(errorReal.localizedDescription) - } - }) - - + let repository = iOSRepository.getCircularRepository() + iOSRepository.updateCirculars(circularRepository: repository) // UI is updated automatically completionHandler([]) @@ -96,12 +90,8 @@ extension AppDelegate : UNUserNotificationCenterDelegate { return } - iOSRepository.getCircularRepository().updateCirculars(returnNewCirculars: false, completionHandler: - { result, error in - if let errorReal = error { - print(errorReal.localizedDescription) - } - }) + let repository = iOSRepository.getCircularRepository() + iOSRepository.updateCirculars(circularRepository: repository) completionHandler() } diff --git a/ios/circolapp/circolapp/Model/iOSRepository.swift b/ios/circolapp/circolapp/Model/iOSRepository.swift index 4260a87..ecb14a4 100644 --- a/ios/circolapp/circolapp/Model/iOSRepository.swift +++ b/ios/circolapp/circolapp/Model/iOSRepository.swift @@ -17,6 +17,8 @@ */ import Foundation +import CoreSpotlight +import MobileCoreServices import Shared class iOSRepository { @@ -30,4 +32,64 @@ class iOSRepository { let serverAPI = iOSServerApi.instance.serverAPI return CircularRepository(circularDao: getCircularDao(), serverAPI: serverAPI) } + + public static func updateCirculars(circularRepository: CircularRepository) { + circularRepository.updateCirculars(returnNewCirculars: true, completionHandler: + { result, error in + if let errorReal = error { + print(errorReal.localizedDescription) + return + } + + // Database was resetted, remove all circulars from spotlight + if result?.second == 1 { + deleteAllFromSpotlight(reindex: false, serverID: -1) + } + + // Index circulars + for circular in result!.first as! Array { + indexToSpotlight(circular: circular) + } + }) + } + + public static func indexAllToSpotlight(serverID: Int) { + let circulars = getCircularDao().getCirculars(school: Int32(serverID)) + + for circular in circulars { + indexToSpotlight(circular: circular) + } + } + + public static func indexToSpotlight(circular: Circular) { + let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeText as String) + attributeSet.title = "Circular number \(circular.id)" + attributeSet.contentDescription = circular.name + attributeSet.identifier = "\(circular.id)" + + let item = CSSearchableItem(uniqueIdentifier: "\(circular.id)", domainIdentifier: "net.underdesk.circolapp", attributeSet: attributeSet) + item.expirationDate = Date.distantFuture + + CSSearchableIndex.default().indexSearchableItems([item]) { error in + if let error = error { + print("Indexing error: \(error.localizedDescription)") + } + } + } + + public static func deleteAllFromSpotlight(reindex: Bool, serverID: Int) { + CSSearchableIndex.default().deleteAllSearchableItems(completionHandler: { + error in + if let errorReal = error { + print(errorReal.localizedDescription) + return + } + + if reindex { + DispatchQueue.main.async { + indexAllToSpotlight(serverID: serverID) + } + } + }) + } } diff --git a/ios/circolapp/circolapp/Model/iOSServerApi.swift b/ios/circolapp/circolapp/Model/iOSServerApi.swift index 0596411..d6e90d4 100644 --- a/ios/circolapp/circolapp/Model/iOSServerApi.swift +++ b/ios/circolapp/circolapp/Model/iOSServerApi.swift @@ -45,10 +45,15 @@ class iOSServerApi { } func changeServer(serverID: Int) { + // Change server provider let serverID = UserDefaults.standard.integer(forKey: schoolKey) let server = serverCompanion.getServer(serverID: Int32(serverID)) serverAPI = ServerAPI(serverName: server) + // Reset spotlight indexed items + iOSRepository.deleteAllFromSpotlight(reindex: true, serverID: serverID) + + // Change FCM topic let nullableOldTopic = UserDefaults.standard.string(forKey: topicKey) let newTopic = serverCompanion.getServerTopic(serverID: Int32(serverID)) if (nullableOldTopic == nil || nullableOldTopic != newTopic) { @@ -63,11 +68,6 @@ class iOSServerApi { UserDefaults.standard.set(newTopic, forKey: topicKey) } - CircularRepository(circularDao: iOSRepository.getCircularDao(), serverAPI: serverAPI).updateCirculars(returnNewCirculars: false, completionHandler: - { result, error in - if let errorReal = error { - print(errorReal.localizedDescription) - } - }) + iOSRepository.updateCirculars(circularRepository: CircularRepository(circularDao: iOSRepository.getCircularDao(), serverAPI: serverAPI)) } } diff --git a/ios/circolapp/circolapp/SceneDelegate.swift b/ios/circolapp/circolapp/SceneDelegate.swift index 31aed86..1468bf0 100644 --- a/ios/circolapp/circolapp/SceneDelegate.swift +++ b/ios/circolapp/circolapp/SceneDelegate.swift @@ -6,6 +6,7 @@ // import UIKit +import CoreSpotlight import SwiftUI class SceneDelegate: UIResponder, UIWindowSceneDelegate { @@ -27,6 +28,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { self.window = window window.makeKeyAndVisible() } + + if let userActivity = connectionOptions.userActivities.first { + handleUserActivity(userActivity: userActivity) + } + } + + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + // Called when the user open an entry from Spotlight + handleUserActivity(userActivity: userActivity) } func sceneDidDisconnect(_ scene: UIScene) { @@ -52,7 +62,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let schoolID = UserDefaults.standard.integer(forKey: "school") let reminders = iOSRepository.getCircularDao().getReminders(school: Int32(schoolID)) - + // Remove reminder flag if notification was delivered UNUserNotificationCenter.current().getPendingNotificationRequests { notifications in // This function has to be called from the main thread because it is where database is accessible @@ -76,4 +86,18 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Use this method to save data, release shared resources, and store enough scene-specific state information // to restore the scene back to its current state. } + + func handleUserActivity(userActivity: NSUserActivity) { + if userActivity.activityType == CSSearchableItemActionType { + if let uniqueIdentifier = userActivity.userInfo?[CSSearchableItemActivityIdentifier] as? String { + guard let circularID = Int64(uniqueIdentifier) else { return } + let schoolID = UserDefaults.standard.integer(forKey: "school") + + let circularDao = iOSRepository.getCircularDao() + let circular = circularDao.getCircular(id: circularID, school: Int32(schoolID)) + + URLUtils.openUrl(url: circular.url) + } + } + } } diff --git a/ios/circolapp/circolapp/URLUtils.swift b/ios/circolapp/circolapp/URLUtils.swift new file mode 100644 index 0000000..16d2d59 --- /dev/null +++ b/ios/circolapp/circolapp/URLUtils.swift @@ -0,0 +1,27 @@ +/* + * Circolapp + * Copyright (C) 2019-2020 Matteo Schiff + * + * 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 Foundation +import UIKit + +class URLUtils { + static func openUrl(url: String) { + guard let safeUrl = URL(string: url.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed)!) else { return } + UIApplication.shared.open(safeUrl) + } +} diff --git a/ios/circolapp/circolapp/View/AttachmentView.swift b/ios/circolapp/circolapp/View/AttachmentView.swift index 50922de..f6f937a 100644 --- a/ios/circolapp/circolapp/View/AttachmentView.swift +++ b/ios/circolapp/circolapp/View/AttachmentView.swift @@ -17,7 +17,6 @@ */ import SwiftUI -import UIKit import Shared struct AttachmentView: View { @@ -33,8 +32,7 @@ struct AttachmentView: View { Spacer() Button(action: { - guard let url = URL(string: attachmentUrl.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed)!) else { return } - UIApplication.shared.open(url) + URLUtils.openUrl(url: attachmentUrl) }) { Image(systemName: "envelope.open.fill") .resizable() diff --git a/ios/circolapp/circolapp/View/CircularView.swift b/ios/circolapp/circolapp/View/CircularView.swift index bf74dd4..b319aba 100644 --- a/ios/circolapp/circolapp/View/CircularView.swift +++ b/ios/circolapp/circolapp/View/CircularView.swift @@ -17,7 +17,6 @@ */ import SwiftUI -import UIKit import Shared struct CircularView: View { @@ -49,8 +48,7 @@ struct CircularView: View { if showDetail { HStack { Button(action: { - guard let url = URL(string: circular.url.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed)!) else { return } - UIApplication.shared.open(url) + URLUtils.openUrl(url: circular.url) }) { Image(systemName: "envelope.open.fill") .resizable() diff --git a/ios/circolapp/circolapp/ViewModel/CircularViewModel.swift b/ios/circolapp/circolapp/ViewModel/CircularViewModel.swift index 0cffa19..1e166ca 100644 --- a/ios/circolapp/circolapp/ViewModel/CircularViewModel.swift +++ b/ios/circolapp/circolapp/ViewModel/CircularViewModel.swift @@ -33,8 +33,8 @@ class CircularViewModel: ObservableObject { self.repository = repository schoolID = UserDefaults.standard.integer(forKey: key) - updateCirculars() - + iOSRepository.updateCirculars(circularRepository: repository) + userDefaultsObserver = UserDefaults.standard.observe(\.school, options: [.initial, .new], changeHandler: { (defaults, change) in self.schoolID = change.newValue ?? 0 @@ -48,15 +48,6 @@ class CircularViewModel: ObservableObject { userDefaultsObserver?.invalidate() } - func updateCirculars() { - self.repository.updateCirculars(returnNewCirculars: false, completionHandler: - { result, error in - if let errorReal = error { - print(errorReal.localizedDescription) - } - }) - } - func startObservingCirculars() { stopObserving() circularWatcher = repository.circularDao.getCFlowCirculars(school: Int32(schoolID)).watch { circulars in diff --git a/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/data/CircularRepository.kt b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/data/CircularRepository.kt index 8d485cb..b4d3b23 100644 --- a/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/data/CircularRepository.kt +++ b/shared/src/commonMain/kotlin/net/underdesk/circolapp/shared/data/CircularRepository.kt @@ -6,12 +6,13 @@ class CircularRepository( val circularDao: CircularDao, private val serverAPI: ServerAPI ) { - suspend fun updateCirculars(returnNewCirculars: Boolean = true): Pair, Boolean> { + suspend fun updateCirculars(returnNewCirculars: Boolean = true): Pair, Int> { var onlyNewCirculars = listOf() + var errorCode = 0 val result = serverAPI.getCircularsFromServer() if (result.second == ServerAPI.Companion.Result.ERROR) - return Pair(emptyList(), false) + return Pair(emptyList(), -1) val oldCirculars = circularDao.getCirculars(serverAPI.serverID()) val newCirculars = result.first @@ -19,6 +20,7 @@ class CircularRepository( if (newCirculars.size != oldCirculars.size) { if (newCirculars.size < oldCirculars.size) { circularDao.deleteAll() + errorCode = 1 } if (returnNewCirculars) { @@ -31,6 +33,6 @@ class CircularRepository( circularDao.insertAll(newCirculars) } - return Pair(onlyNewCirculars, true) + return Pair(onlyNewCirculars, errorCode) } }