1 // Copyright 2023-present 650 Industries. All rights reserved. 2 3 import ExpoModulesCore 4 import CoreSpotlight 5 import MobileCoreServices 6 7 struct MetadataOptions: Record { 8 @Field 9 // swiftlint:disable:next implicitly_unwrapped_optional 10 var activityType: String! 11 @Field 12 // swiftlint:disable:next implicitly_unwrapped_optional 13 var id: String! 14 @Field 15 var isEligibleForHandoff: Bool = true 16 @Field 17 var isEligibleForPrediction: Bool = true 18 @Field 19 var isEligibleForPublicIndexing: Bool = false 20 @Field 21 var isEligibleForSearch: Bool = true 22 @Field 23 var title: String? 24 @Field 25 var webpageURL: URL? 26 @Field 27 var imageUrl: URL? 28 @Field 29 var keywords: [String]? 30 @Field 31 var dateModified: Date? 32 @Field 33 var userInfo: [String: AnyHashable]? 34 @Field 35 var description: String? 36 } 37 38 // swiftlint:disable:next force_unwrapping 39 let indexRouteTag = Bundle.main.bundleIdentifier! + ".expo.index_route" 40 41 var launchedActivity: NSUserActivity? 42 43 internal class InvalidSchemeException: Exception { 44 override var reason: String { 45 "Scheme file:// is not allowed for location origin (webpageUrl in NSUserActivity)" 46 } 47 } 48 49 public class ExpoHeadModule: Module { 50 private var activities = Set<NSUserActivity>() 51 52 public required init(appContext: AppContext) { 53 super.init(appContext: appContext) 54 } 55 56 // Each module class must implement the definition function. The definition consists of components 57 // that describes the module's functionality and behavior. 58 // See https://docs.expo.dev/modules/module-api for more details about available components. definitionnull59 public func definition() -> ModuleDefinition { 60 // Sets the name of the module that JavaScript code will use to refer to the module. 61 // Takes a string as an argument. Can be inferred from module's class name, but it's 62 // recommended to set it explicitly for clarity. 63 // The module will be accessible from `requireNativeModule('ExpoHead')` in JavaScript. 64 Name("ExpoHead") 65 66 Constants([ 67 "activities": [ 68 "INDEXED_ROUTE": indexRouteTag 69 ] 70 ]) 71 72 Function("getLaunchActivity") { () -> [String: Any]? in 73 if let activity = launchedActivity { 74 return [ 75 "activityType": activity.activityType, 76 "description": activity.contentAttributeSet?.contentDescription, 77 "id": activity.persistentIdentifier, 78 "isEligibleForHandoff": activity.isEligibleForHandoff, 79 "isEligibleForPrediction": activity.isEligibleForPrediction, 80 "isEligibleForPublicIndexing": activity.isEligibleForPublicIndexing, 81 "isEligibleForSearch": activity.isEligibleForSearch, 82 "title": activity.title, 83 "webpageURL": activity.webpageURL, 84 "imageUrl": activity.contentAttributeSet?.thumbnailURL, 85 "keywords": activity.keywords, 86 "dateModified": activity.contentAttributeSet?.metadataModificationDate, 87 "userInfo": activity.userInfo 88 ] 89 } 90 return nil 91 } 92 93 Function("createActivity") { (value: MetadataOptions) in 94 if let webpageUrl = value.webpageURL { 95 if webpageUrl.absoluteString.starts(with: "file://") == true { 96 throw Exception(name: "Invalid webpageUrl", description: "Scheme file:// is not allowed for location origin (webpageUrl in NSUserActivity). URL: \(webpageUrl.absoluteString)") 97 } 98 } 99 100 let activity = createOrUpdateActivity(value: value) 101 activity.becomeCurrent() 102 } 103 104 AsyncFunction("clearActivitiesAsync") { (ids: [String], promise: Promise) in 105 ids.forEach { id in 106 self.revokeActivity(id: id) 107 } 108 109 CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: ids, completionHandler: { error in 110 if error != nil { 111 // swiftlint:disable:next force_cast 112 promise.reject(error as! Exception) 113 } else { 114 promise.resolve() 115 } 116 }) 117 } 118 119 Function("suspendActivity") { (id: String) in 120 let activity = self.activities.first(where: { $0.persistentIdentifier == id }) 121 activity?.resignCurrent() 122 } 123 124 Function("revokeActivity") { (id: String) in 125 self.revokeActivity(id: id) 126 } 127 } 128 createOrUpdateActivitynull129 func createOrUpdateActivity(value: MetadataOptions) -> NSUserActivity { 130 let att = CSSearchableItemAttributeSet(itemContentType: kUTTypeText as String) 131 let existing = self.activities.first(where: { $0.persistentIdentifier == value.id }) 132 let activity = existing ?? NSUserActivity(activityType: value.activityType) 133 134 if existing == nil { 135 self.activities.insert(activity) 136 } 137 138 activity.targetContentIdentifier = value.id 139 activity.persistentIdentifier = value.id 140 activity.isEligibleForHandoff = value.isEligibleForHandoff 141 activity.isEligibleForPrediction = value.isEligibleForPrediction 142 activity.isEligibleForPublicIndexing = value.isEligibleForPublicIndexing 143 activity.isEligibleForSearch = value.isEligibleForSearch 144 activity.title = value.title 145 146 if let keywords = value.keywords { 147 activity.keywords = Set(keywords) 148 } 149 150 activity.userInfo = value.userInfo 151 152 if value.webpageURL != nil { 153 // If you’re using all three APIs, it works well to use the URL of the relevant webpage as the value 154 // for uniqueIdentifier, relatedUniqueIdentifier, and webpageURL. 155 // https://developer.apple.com/library/archive/documentation/General/Conceptual/AppSearch/CombiningAPIs.html 156 activity.webpageURL = value.webpageURL 157 att.relatedUniqueIdentifier = value.webpageURL?.absoluteString 158 } 159 160 att.title = value.title 161 // Make all indexed routes deletable 162 att.domainIdentifier = indexRouteTag 163 164 if let localUrl = value.imageUrl?.path { 165 att.thumbnailURL = value.imageUrl 166 } 167 168 if let description = value.description { 169 att.contentDescription = description 170 } 171 172 activity.contentAttributeSet = att 173 174 return activity 175 } 176 177 @discardableResult revokeActivitynull178 func revokeActivity(id: String) -> NSUserActivity? { 179 let activity = self.activities.first(where: { $0.persistentIdentifier == id }) 180 activity?.invalidate() 181 if let activity = activity { 182 self.activities.remove(activity) 183 } 184 return activity 185 } 186 } 187