Add Spotlight support for iOS

This commit is contained in:
2020-12-21 16:38:22 +01:00
parent 01b62f667a
commit 7843814429
12 changed files with 139 additions and 43 deletions

View File

@@ -65,7 +65,7 @@ class CircularLetterViewModel internal constructor(
viewModelScope.launch { viewModelScope.launch {
isNotUpdating = false isNotUpdating = false
if (!circularRepository.updateCirculars(false).second) { if (circularRepository.updateCirculars(false).second == -1) {
showMessage.postValue(true) showMessage.postValue(true)
} }

View File

@@ -94,7 +94,7 @@ class PollWork(appContext: Context, workerParams: WorkerParameters) :
val circularRepository = AndroidCircularRepository.getInstance(applicationContext) val circularRepository = AndroidCircularRepository.getInstance(applicationContext)
val result = circularRepository.updateCirculars() val result = circularRepository.updateCirculars()
if (!result.second) if (result.second == -1)
return@coroutineScope Result.retry() return@coroutineScope Result.retry()
val newCirculars = result.first val newCirculars = result.first

View File

@@ -36,6 +36,7 @@
95CA31B7255C1EE000AC095B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 95CA31B6255C1EE000AC095B /* Preview Assets.xcassets */; }; 95CA31B7255C1EE000AC095B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 95CA31B6255C1EE000AC095B /* Preview Assets.xcassets */; };
95CA31C0255C28C300AC095B /* CircularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CA31BF255C28C300AC095B /* CircularView.swift */; }; 95CA31C0255C28C300AC095B /* CircularView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CA31BF255C28C300AC095B /* CircularView.swift */; };
95DB71AB258A1C1500A78033 /* CarteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95DB71AA258A1C1500A78033 /* CarteView.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 */ /* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */ /* Begin PBXCopyFilesBuildPhase section */
@@ -86,6 +87,7 @@
95CA31B8255C1EE000AC095B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 95CA31B8255C1EE000AC095B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
95CA31BF255C28C300AC095B /* CircularView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularView.swift; sourceTree = "<group>"; }; 95CA31BF255C28C300AC095B /* CircularView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularView.swift; sourceTree = "<group>"; };
95DB71AA258A1C1500A78033 /* CarteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarteView.swift; sourceTree = "<group>"; }; 95DB71AA258A1C1500A78033 /* CarteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarteView.swift; sourceTree = "<group>"; };
95F6666D2590D712006DE74F /* URLUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLUtils.swift; sourceTree = "<group>"; };
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 = "<group>"; }; 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 = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@@ -193,6 +195,7 @@
95906F4C257510370060F5D0 /* GoogleService-Info.plist */, 95906F4C257510370060F5D0 /* GoogleService-Info.plist */,
95906F47257506360060F5D0 /* Assets.xcassets */, 95906F47257506360060F5D0 /* Assets.xcassets */,
95CA31B5255C1EE000AC095B /* Preview Content */, 95CA31B5255C1EE000AC095B /* Preview Content */,
95F6666D2590D712006DE74F /* URLUtils.swift */,
); );
path = circolapp; path = circolapp;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -382,6 +385,7 @@
955B7A0E257D098C0091B1F9 /* SearchBar.swift in Sources */, 955B7A0E257D098C0091B1F9 /* SearchBar.swift in Sources */,
9554BDB8257E498F00D8925B /* OnboardingView.swift in Sources */, 9554BDB8257E498F00D8925B /* OnboardingView.swift in Sources */,
950C1788258E5BD300B2DBFE /* PhoneView.swift in Sources */, 950C1788258E5BD300B2DBFE /* PhoneView.swift in Sources */,
95F6666E2590D712006DE74F /* URLUtils.swift in Sources */,
95C46A51255D3A34007A75E5 /* CircularViewModel.swift in Sources */, 95C46A51255D3A34007A75E5 /* CircularViewModel.swift in Sources */,
95B4CE142588BC890090D5E8 /* SettingsView.swift in Sources */, 95B4CE142588BC890090D5E8 /* SettingsView.swift in Sources */,
); );

View File

@@ -71,14 +71,8 @@ extension AppDelegate : UNUserNotificationCenterDelegate {
} }
// Handle new circular notification // Handle new circular notification
iOSRepository.getCircularRepository().updateCirculars(returnNewCirculars: false, completionHandler: let repository = iOSRepository.getCircularRepository()
{ result, error in iOSRepository.updateCirculars(circularRepository: repository)
if let errorReal = error {
print(errorReal.localizedDescription)
}
})
// UI is updated automatically // UI is updated automatically
completionHandler([]) completionHandler([])
@@ -96,12 +90,8 @@ extension AppDelegate : UNUserNotificationCenterDelegate {
return return
} }
iOSRepository.getCircularRepository().updateCirculars(returnNewCirculars: false, completionHandler: let repository = iOSRepository.getCircularRepository()
{ result, error in iOSRepository.updateCirculars(circularRepository: repository)
if let errorReal = error {
print(errorReal.localizedDescription)
}
})
completionHandler() completionHandler()
} }

View File

@@ -17,6 +17,8 @@
*/ */
import Foundation import Foundation
import CoreSpotlight
import MobileCoreServices
import Shared import Shared
class iOSRepository { class iOSRepository {
@@ -30,4 +32,64 @@ class iOSRepository {
let serverAPI = iOSServerApi.instance.serverAPI let serverAPI = iOSServerApi.instance.serverAPI
return CircularRepository(circularDao: getCircularDao(), serverAPI: 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<Circular> {
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)
}
}
})
}
} }

View File

@@ -45,10 +45,15 @@ class iOSServerApi {
} }
func changeServer(serverID: Int) { func changeServer(serverID: Int) {
// Change server provider
let serverID = UserDefaults.standard.integer(forKey: schoolKey) let serverID = UserDefaults.standard.integer(forKey: schoolKey)
let server = serverCompanion.getServer(serverID: Int32(serverID)) let server = serverCompanion.getServer(serverID: Int32(serverID))
serverAPI = ServerAPI(serverName: server) 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 nullableOldTopic = UserDefaults.standard.string(forKey: topicKey)
let newTopic = serverCompanion.getServerTopic(serverID: Int32(serverID)) let newTopic = serverCompanion.getServerTopic(serverID: Int32(serverID))
if (nullableOldTopic == nil || nullableOldTopic != newTopic) { if (nullableOldTopic == nil || nullableOldTopic != newTopic) {
@@ -63,11 +68,6 @@ class iOSServerApi {
UserDefaults.standard.set(newTopic, forKey: topicKey) UserDefaults.standard.set(newTopic, forKey: topicKey)
} }
CircularRepository(circularDao: iOSRepository.getCircularDao(), serverAPI: serverAPI).updateCirculars(returnNewCirculars: false, completionHandler: iOSRepository.updateCirculars(circularRepository: CircularRepository(circularDao: iOSRepository.getCircularDao(), serverAPI: serverAPI))
{ result, error in
if let errorReal = error {
print(errorReal.localizedDescription)
}
})
} }
} }

View File

@@ -6,6 +6,7 @@
// //
import UIKit import UIKit
import CoreSpotlight
import SwiftUI import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate { class SceneDelegate: UIResponder, UIWindowSceneDelegate {
@@ -27,6 +28,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
self.window = window self.window = window
window.makeKeyAndVisible() 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) { func sceneDidDisconnect(_ scene: UIScene) {
@@ -52,7 +62,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
let schoolID = UserDefaults.standard.integer(forKey: "school") let schoolID = UserDefaults.standard.integer(forKey: "school")
let reminders = iOSRepository.getCircularDao().getReminders(school: Int32(schoolID)) let reminders = iOSRepository.getCircularDao().getReminders(school: Int32(schoolID))
// Remove reminder flag if notification was delivered // Remove reminder flag if notification was delivered
UNUserNotificationCenter.current().getPendingNotificationRequests { notifications in UNUserNotificationCenter.current().getPendingNotificationRequests { notifications in
// This function has to be called from the main thread because it is where database is accessible // 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 // 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. // 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)
}
}
}
} }

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
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)
}
}

View File

@@ -17,7 +17,6 @@
*/ */
import SwiftUI import SwiftUI
import UIKit
import Shared import Shared
struct AttachmentView: View { struct AttachmentView: View {
@@ -33,8 +32,7 @@ struct AttachmentView: View {
Spacer() Spacer()
Button(action: { Button(action: {
guard let url = URL(string: attachmentUrl.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed)!) else { return } URLUtils.openUrl(url: attachmentUrl)
UIApplication.shared.open(url)
}) { }) {
Image(systemName: "envelope.open.fill") Image(systemName: "envelope.open.fill")
.resizable() .resizable()

View File

@@ -17,7 +17,6 @@
*/ */
import SwiftUI import SwiftUI
import UIKit
import Shared import Shared
struct CircularView: View { struct CircularView: View {
@@ -49,8 +48,7 @@ struct CircularView: View {
if showDetail { if showDetail {
HStack { HStack {
Button(action: { Button(action: {
guard let url = URL(string: circular.url.addingPercentEncoding(withAllowedCharacters: .urlFragmentAllowed)!) else { return } URLUtils.openUrl(url: circular.url)
UIApplication.shared.open(url)
}) { }) {
Image(systemName: "envelope.open.fill") Image(systemName: "envelope.open.fill")
.resizable() .resizable()

View File

@@ -33,8 +33,8 @@ class CircularViewModel: ObservableObject {
self.repository = repository self.repository = repository
schoolID = UserDefaults.standard.integer(forKey: key) schoolID = UserDefaults.standard.integer(forKey: key)
updateCirculars() iOSRepository.updateCirculars(circularRepository: repository)
userDefaultsObserver = UserDefaults.standard.observe(\.school, options: [.initial, .new], changeHandler: { (defaults, change) in userDefaultsObserver = UserDefaults.standard.observe(\.school, options: [.initial, .new], changeHandler: { (defaults, change) in
self.schoolID = change.newValue ?? 0 self.schoolID = change.newValue ?? 0
@@ -48,15 +48,6 @@ class CircularViewModel: ObservableObject {
userDefaultsObserver?.invalidate() userDefaultsObserver?.invalidate()
} }
func updateCirculars() {
self.repository.updateCirculars(returnNewCirculars: false, completionHandler:
{ result, error in
if let errorReal = error {
print(errorReal.localizedDescription)
}
})
}
func startObservingCirculars() { func startObservingCirculars() {
stopObserving() stopObserving()
circularWatcher = repository.circularDao.getCFlowCirculars(school: Int32(schoolID)).watch { circulars in circularWatcher = repository.circularDao.getCFlowCirculars(school: Int32(schoolID)).watch { circulars in

View File

@@ -6,12 +6,13 @@ class CircularRepository(
val circularDao: CircularDao, val circularDao: CircularDao,
private val serverAPI: ServerAPI private val serverAPI: ServerAPI
) { ) {
suspend fun updateCirculars(returnNewCirculars: Boolean = true): Pair<List<Circular>, Boolean> { suspend fun updateCirculars(returnNewCirculars: Boolean = true): Pair<List<Circular>, Int> {
var onlyNewCirculars = listOf<Circular>() var onlyNewCirculars = listOf<Circular>()
var errorCode = 0
val result = serverAPI.getCircularsFromServer() val result = serverAPI.getCircularsFromServer()
if (result.second == ServerAPI.Companion.Result.ERROR) if (result.second == ServerAPI.Companion.Result.ERROR)
return Pair(emptyList(), false) return Pair(emptyList(), -1)
val oldCirculars = circularDao.getCirculars(serverAPI.serverID()) val oldCirculars = circularDao.getCirculars(serverAPI.serverID())
val newCirculars = result.first val newCirculars = result.first
@@ -19,6 +20,7 @@ class CircularRepository(
if (newCirculars.size != oldCirculars.size) { if (newCirculars.size != oldCirculars.size) {
if (newCirculars.size < oldCirculars.size) { if (newCirculars.size < oldCirculars.size) {
circularDao.deleteAll() circularDao.deleteAll()
errorCode = 1
} }
if (returnNewCirculars) { if (returnNewCirculars) {
@@ -31,6 +33,6 @@ class CircularRepository(
circularDao.insertAll(newCirculars) circularDao.insertAll(newCirculars)
} }
return Pair(onlyNewCirculars, true) return Pair(onlyNewCirculars, errorCode)
} }
} }