首页 > 代码库 > [转]Hacking the iOS Spotlight

[转]Hacking the iOS Spotlight

原文:http://theiostream.tumblr.com/post/36905860826/hacking-the-ios-spotlight

原文:http://theiostream.tumblr.com/post/54299348185/searchloader

原文:http://theiostream.tumblr.com/post/54339870761/hacking-the-ios-spotlight-v2

[Update] A lot of new findings have been uncovered! I will take the contents of this blog post into a decent iPhone Dev Wiki article as soon as I have the time.

It shall be noted that I might be wrong in how I approach some of these subjects. I have not experimented all possible cases for this topic, and I’ll edit the post as new findings come.

Back at the iOS 3.x betas,?KennyTM?createdCalculator.searchBundle, a way to show Calculator results on Spotlight at the iOS, just like in OSX!

Since then, many tweaks have attempted to improve Spotlight. Fromthis list:

  • SpotEnhancer, from abart997. It checked exact search string matches for something and performed some action. Turned Spotlight into a non-extensible command station. I have myself done an extension of this concept which allows simple plugins namedSearchCommands.

  • QuickMath?from moeseth, which would evaluate the Search Bar text into a math expression and then turn that text into the result. It started off with a simple parsing system but the currently released version (as opposed to the opensourced one) uses the same thing KennyTM used for his Search Bundle:?CalculatePerformExpression(after intervention by myself).

  • SLURLs: Open URLs directly from Spotlight.

  • ListLauncher?and?RunningList?did fact add some results to Spotlight, and yet not what it was meant to be: It does not add search results to user queries, just information in the form of search results in there (in their case, respectively, all apps and running apps).

I wanted to be able to add custom Spotlight results for user queries, just like Apple’s way of doing it, and after a week of disassembling, this is what I can tell:

The Hack

Spotlight gets its search results from?SPSearchQuery?which contain data such as the search string sent to?search bundles, which get a result from that query.

  • Search Bundles: Located at?/System/Library/SearchBundles, are the iOS search built-in bundles. The bundle’s principal class conforms to the?SPSearchDatastore?protocol.

  • Spotlight Bundles: Located at/System/Library/Spotlight/SearchBundles, are search bundles which conform to the?SPSpotlightDatastore?protocol.

In both bundles, the?datastore?is the principal class, which will in some way give data for Spotlight to show. That data will be shown by SpringBoard.

Yet there are in fact many fundamental differences between those two:?Search Bundles?get the user query (serialized in theSPSearchQuery?class) through?searchd, and through the same mean they reply with a?SPSearchResult, directly parsed bySBSearchModel?in SpringBoard and displayed as UI.

Meanwhile,?Spotlight Bundles?have a complete different mean of functioning:?Extended.searchBundle?(a search bundle) uses theSPContentIndexer?class to look up a certain identified record store (some sort of special?AppSupport.framework?database) for data. From there they compose their?SPSearchResult?and send it over to Spotlight. Spotlight bundles are there for composing those record stores with data.

Search bundles also have to return a?-displayIdentifierForDomain:?method to associate the app for the search result with the search result (each result has a?domainfor it, which identifies it). The?-performQuery:withResultsPipe:?is used to create the search results from the passed-in query and send them over to searchd through the results pipe (SDActor?object).

Now, why don’t we create a search bundle to add results? Because every search query has an array of hardcoded domains (the existing Apple search bundles). And every search bundle’s array of domains (defined in?-searchDomains) must have one of its members as part of the search query’s domains.

OK, why not use one of the following existing domains?

  • 0: Top Hits
  • 2: Address Book
  • 3: Mail
  • 5: Notes
  • [6, 7, 8, 9]: iPod
  • 10: Calendar
  • 12: Voice Memos
  • 13: Reminders
  • 15: Extended

Because then an exception will be thrown for two bundles adding results with the same domain!

I tried hooking?-[SPSearchQuery searchDomains]?and add a couple of different domains to it, but there was no success, since the same exception was thrown – even with my custom domain for both result and bundle.

With some more time, I might try reversing this system. Meanwhile, we shall move on.

KennyTM’s Calculator bundle was a search bundle which worked before Apple had this exception handling system, which allowed his bundle to run smoothly with an already hardcoded (but not fulfilled by any search bundle) domain.

That left me to hack into where no one had ever hacked before:Spotlight Bundles, and was successful. Now, I shall explain how does its system work in detail.

Spotlight directly iterates through all existing search bundles, reaching?Extended.searchBundle. Extended uses theSPContentIndexer?class to look over saved possible search results. Spotlight bundles fill this database with data.

Each spotlight bundle is registered inside/System/Library/Spotlight/domains.plist. The plist is an array of dictionaries which consist of the following main keys:

  • SPDisplayIdentifier: The display identifier for the application this bundle provides search results for.
  • SPCategory: Similar to domains for Search Bundles, but for Spotlight Bundles. It identifies the bundle.

To get the Search Bundle, it’ll get the bundle for theSPDisplayIdentifier?and from its Info.plist get the following key values:

  • SPDatastoreVersion: Version for the Datastore system. As of iOS 5, should always be?1.
  • SPSearchBundle: The name of the Search Bundle located inside/System/Library/Spotlight/SearchBundles, without the.searchBundle?extension.

It is possible to have multiple categories for a same display identifier, generating double Spotlight results for that bundle.

With these registered, the following three files are created inside~/Library/Spotlight/<display identifier>:

  • <category>db.sqlitedb: Holds data for?SPContentIndexer.
  • <category>idx.spotlight: Holds data for?SPContentIndexer.
  • updates.<category>.spotlight: Updates file. Throttles a new result to be brought into the database/index.

To create a database entry, the following shall be done:

[[SPDaemonConnection sharedConnection] startRecordUpdatesForApplication:@"<display identifier>" andCategory:@"<category>];
[[SPDaemonConnection sharedConnection] requestRecordUpdatesForApplication:@"<display identifier>" category:@"<category>" andIDs:[NSArray arrayWithObject:@"<some external ID>"]];
[[SPDaemonConnection sharedConnection] endRecordUpdatesForApplication:@"<display identifier>" andCategory:@"<category>"];

With these updates to the?Updates file, the?AppIndexer?process is invoked and it deals with opening bundles and transferring their results to the record store.

[Update]?AppIndexer?will also be invoked using the Application Display ID as the ID passed into the?andIDs:?argument of?-requestRecordUpdatesForApplication:category:andIDs:if the record store is empty for every respring.

An example of sent ID is Messages.app’s: The Group ID for aCKConversation.

The?SPSpotlightDatastore?protocol is defined as follows:

  • -allIdentifiersInCategory:: NSArray of all IDs to be sent into-contentToIndexForID:inCategory:?when there is no database.
  • -contentToIndexForID:inCategory:: NSDictionary with special key-value pairs. Receives an?NSString *?as first parameter (there is one call for this method at each?ID?passed into?-requestRecordUpdatesForApplication:category:andIDs:. The category is the one passed into the previously mentioned method.

A?NSDictionary *?shall be returned, parsing the ID into a full-fledged data holder for making up Search Results. In Messages.app’s, it’s a dictionary containing a whole conversation’s text as content, conversation recipients as title, etc.

Accepted keys are constants from Search.framework:

  • kSPContentTitleKey?(“title”): Title for Spotlight Search result.
  • kSPContentSummaryKey?(“summary”): Subtitle for Spotlight Search result.
  • kSPContentContentKey?(“content”): What we search for for the result to show up.
  • kSPContentCategoryKey?(“category”): Category name.
  • kSPContentExternalIDKey?(“external_id”): External ID for dictionary.
  • kSPContentSubtitleKey: Unknown.
  • kSPContentAuxiliaryTitleKey: Unknown.
  • kSPContentAuxiliarySubtitleKey: Unknown.

This returned?NSDictionary *?will complete the spotlight bundle cycle: Updates will be commited to the record store andExtended.searchBundle?will take care of showing all of those into Spotlight.

Now, clicking that result will launch the app. What then? How to show something specific inside it? I am completely clueless, yet I shall update this soon. I suspect?SPDaemonQueryDelegate?is the key for this problem (as present in?CKConversationSearcher?in ChatKit).

I guess that’s it. I’m working on a pretty cool tweak which will contain a bunch of these plugins, but will also release a MobileSubstrate extension to deal with the possible limitations of the Spotlight Bundle approach:

  • What if we were making a Calculator bundle? We wouldn’t be able to simply update every calculation we do at the app and expect it to be always the same one at Search, and we also can’t index every possible calculation. We’d have to have a “watcher” for every search request placed in?searchd?and if it matches a calculation, index it in our datastore.

  • MobileSubstrate is there to?avoid file patching. We can’t have every search plugin to edit an app’s?Info.plist?for specifying Search-specific keys. Though since Spotlight traditionally searches things related to apps, I can’t see the point in being able to add app-less search results.

  • Have an easy wrapper around those?Search.framework?calls.

  • Transfer content to?/Library?and make it have to be loaded by MobileSubstrate. It shall avoid issues?WeeLoader?avoids for/System/Library-placed wee apps.

Credits on the finding also go to:

  • IDA, ac3xx’s lovely wife: The best disassembler out there.
  • MobileSubstrate: Used for a lot of guessing work.

— Daniel Ferreira.

It’s been 7 months since I published my first article on this subject: Spotlight. Yes. That long. I will soon (that is, in a matter of hours) be writing a new technical writeup on the subject, named?Hacking the iOS Spotlight, v2.

Let me start by telling you a bit about how research was prior, and after the publishing of the article.

The whole thing started with a project I had with Ariel Aouizerate and Eliran Manzeli nicknamed Spotlight+, which aimed to extend Spotlight by displaying selected search results in a different way. This has not yet been implemented, even though it was the start of it all, and since the future of this project is yet unknown, I won’t disclose much else about it.

We soon realized that Spotlight lacked the categories of results we all wanted. Ariel thought we should display whatever we displayed through a?SearchCommands-like thing, but I figured, since we were doing this, we were going to do it right.

So the project changed its focus to a whole new direction. It was no longer about making Spotlight more practical, but about adding a whole new set of functionalities to it. Adding custom search results.

Then research started. I spent a week studying the SpringBoard, searchd and AppIndexer layers of it, and in a hurry released?Hacking the iOS Spotlight?in this blog.

The article covered an overview of existing software which extended Spotlight, a bit about the two kinds of search?plugins?–?Search Bundles?and the nicknamed?Spotlight Bundles, and how both functioned, and a small na?ve description of what domains were.

That writeup lacked two vital pieces of information, and had one error at its first release (which was then corrected with further research). It lacked:

  • Information about loading Search.framework;
  • Information about?SPSpotlightManager, which should be used instead of?SPDaemonConnection?directly for AppIndexer-related purposes;
  • The error: The actual purpose of?-allIdentifiersInCategory:in a Spotlight bundle.

Ever since, research has progressed into a complete piece of software, named?SearchLoader, which, as the name says, loads Search bundles and tricks Extended.searchBundle into loading our Spotlight bundles.

It was no simple task, but it will soon, as one would expect, be opensourced, with a through API documentation (in the upcoming technical article).

Loader started simply as a buggy loading thing, but it soon evolved into a stable Loader + Library set, with a lot of stuff one might need when developing their own search plugins. And when I see the first SearchLoader’s working code (which I had copied in my Mac) and compare it with the current code, I can barely believe it evolved so much.

With some short breaks from this project, I worked on some unfinished tweaks, did TweetAmplius, fixed stuff for iOS 6, wrote some of the Theos Docs, did some work on Oligos, re-beat the Mass Effect saga, played some Minecraft and Civilization, but now, it’s finished! I honestly can’t believe this!

To end this “say everything yet nothing” article, this wouldn’t have been possible without the tools of the trade:?class-dump, IDA,dyld_decache, and so forth.

And a huge thanks goes to my friends at the jailbreak community. Without them I would have most likely given up on this ;P.

So, expect cool stuff I’ve coded to come alongside Loader, and I’ll hopefully expect from you some cool plugins for SearchLoader. :)

Introduction

This article is purely technical. If you want to know a bit about the history of SearchLoader, go to?this blog post.

All of the data in this article reflects iOS 6. There has been an intermediate number of changes from iOS 5.

History:?The?TL?prefix for everything around SearchLoader and my projects related to Spotlight stands for?theiostream spotlight. It looked better than?SL, I could certainly not take?SP, and?TL?also looked nicer than?TP.

This article limits itself to the basic process of a search, and the creation of Search Bundles and Extended Domains. It does not go into detail on how each component of the Search framework work internally.

There are a couple of things which are irrelevant for this article, but some supposedly interesting areas of this subject might still make new articles. More precisely:

  • SPContentIndexer: how does it interact with the Content Index; how can we perform our own searches? (SMS app does that!)

  • IPC: How do the search-related process intercommunicate? This should be an easy question to answer, but I was never interested enough to look it up! :o

  • Domain registration: How are domains registered out of the Extended domain scope? Regarding the search process itself’s internals, this is the only key point I’ve never closely looked into, and which I suspect isn’t much simple.


?

An overview of the search process

Spotlight is divided between two layers: SpringBoard (UI) and searchd (search).

SpringBoard has four?SBSearch...?classes:

  • SBSearchTableViewCell: The?UITableViewCell?subclass for the Spotlight table view. It has nothing special regarding the search process. Whole articles could be written on cheating the Search Table View, though.

  • SBSearchView: The main view for Spotlight. It has as subviews anUISearchBar, an?UITableView?and, on the iPad, aSBNoResultsView?for stylish purposes. This view also handles some layouting code for the table view.

  • SBSearchController: It serves as a bridge between the table view and?SBSearchModel. It is the table view and search bar delegate, and transfers information from searched content to your nice-looking results.

  • SBSearchModel: A subclass of?SPSearchAgent. As a subclass, it handles the timer for search results to vanish after a while, obtaining images for display identifiers and the final launching URL for the result, from data it holds.

SPSearchAgent, in SpringBoard, is incorporated bySBSearchModel‘s shared instance.

SBSearchController?asks for it to do what it’s meant to do: Take a query string, turn it into a?SPSearchQuery?and send it tosearchd.

The searchd layer uses?SPBundleManager?to load all existent search bundles (placed at?/System/Library/SearchBundlesor, with SearchLoader,/Library/SearchLoader/SearchBundles, and then it gets out of them a set of?datastore?objects.

Datastores on Search Bundles areNSObject<SPSearchDatastore> *?objects. These objects, through an API specified by the?SPSearchDatastore?protocol, perform a search through the query they receive and produce aSPSearchResult.

Search Results are sorted by an integer named a?search domain. Each search bundle provides a set of domains it “owns”.

When creating?SPSearchResults, internally or externally they will get placed inside a?SPSearchResultSection?object, which will be assigned to a domain.

Multiple sections can be added under the same domain, as done byApplication.searchBundle. The only search bundles to use multiple domains are?iPod.searchBundle?(due to unknown purposes) and?Extended.searchBundle?(each index gets one domain).

Back to SpringBoard, it gets sections for each domain and places them in the table view.

Yet, there is some special attention that should be paid toExtended.searchBundle?and Spotlight Bundles.

Extended.searchBundle?reads from?database or database-like entries?in some files to generate its results. The following content describes the generation of these entries. But as you can notice, they are?database entries. Therefore, this method of displaying search results should be used when results are not generated dynamically, but when they can be indexed.

SearchLoader edits?com.apple.search.appindexer.plist(the AppIndexer daemon’s?launchd.plist) so it’ll load MobileSubstrate. With this, it manages to control Spotlight Bundle loading.

Every time?searchd?is initialized (when Spotlight is brought up), it invokes the?AppIndexer?daemon.

On launch (usually), this daemon finds existing?Extended Domainsthrough?SPGetExtendedDomains(). This function reads from/System/Library/Spotlight/domains.plist?and returns an array of dictionaries. These dictionaries contain this domain’s display identifier (which reflects the generated search results refer to), a category (a string which usually has the format?<Name>Search) for it (a way to differentiate different search bundles/extended domains with the same display identifier due to referring to the same app) and required capabilities. This sort of dictionary is, therefore, namedextended domain.

Before going further, it’s important to introduce the file hierarchy for Spotlight Bundle databases, etc. Files related to extended domain with display identifier?com.yourcompany.test?and categoryTestSearch?will be placed at/var/mobile/Library/Spotlight/com.yourcompany.test. The files are:

  • TestSearchindex.spotlight:?Content index?to index search results. Managed by?CPRecordStore?in AppSupport and theContentIndex?framework;
  • TestSearchindex.sqlite: SQLite database managed byCPRecordStore?and?ContentIndex.framework?to index search results;
  • updates.TestSearch.spotlight:?Content index?to track desired updates to the database/content index.

From these extended domains, it usesSPDomainHasUpdatesFile()?to determine whether the updates file for an extended domain is empty. In case it is non-existent or contains updates, an?AppIndexer?instance is initialized with information about this extended domain.

Here, Spotlight Bundles (finally) get in the scene. They are loaded from?/System/Library/Spotlight/SearchBundles. Through a principal class of?NSObject<SPSpotlightDatastore> *?type, it generates a dictionary with specific keys which tells?AppIndexerhow it should index results into the content index/database, from anidentifier. If the updates file is empty, a list of?identifiers is created by the spotlight bundle itself. Else, the contents of the?updates file?are used. Therefore, it can be said that the updates file tracks identifiers that require indexing from the Spotlight bundle.

After getting this data,?AppIndexer?asks?searchd?to update the actual database/content index. Meanwhile, one might ask?Where do the identifiers for the update file come from?. The only native extended domain,?SMSSearch, uses a whole direct?ContentIndexwrapper to write to its update file (the?IMDSpotlight?function family from?IMCore). But happily, we don’t have to either useContentIndex, nor link to?IMCore. Apple provides some APIs inSPDaemonConnection, or even a whole framework just about that:Spotlight.framework?with?SPSpotlightManager.

And then we go back to the top.?Extended.searchBundle?will useSPContentIndexer?to look up contents of existent content indexes/databases and from them build Search Bundle-like results which will go to SpringBoard.

This finishes my Spotlight overview. The further sections will describe, respectively, the structure of a search bundle, an extended domain Spotlight Bundle (documenting?libspotlight?– a part of SearchLoader –’s APIs), how to make your bundle loadable by SearchLoader, some details about the SearchLoader tweak itself, SearchLoaderPreferences, and then will conclude.

Search Bundles

Search Bundles are composed of a principal class, which is theSearch Bundle datastore. It conforms to the?SPSearchDatastoreprotocol.

SearchLoader plugins actually should conform to theTLSearchDatastore?protocol, which conforms toSPSearchDatastore?and adds one method.

TLSearchDatastore

  • - (void)performQuery:(SPSearchQuery *)query withResultsPipe:(SDSearchQuery *)pipe;

In this method, the search bundle should send its generatedSPSearchResult?or?SPSearchResultSection?objects back tosearchd?so it can be shown in the SpringBoard layer.

This method takes as arguments?query?and?pipe. They are the same (as of iOS 6:?SDSearchQuery?is a subclass ofSPSearchQuery), yet theoretically?query?should be used to obtain information regarding the search query (essentially, its query string, obtainable through the?- (NSString *)searchString;method), and?pipe?to send results back to?searchd, through the following methods:

  • - (void)appendSection:(SPSearchResultSection *)section toSerializerDomain:(NSUInteger)domain;
  • - (void)appendResults:(NSArray *)results toSerializerDomain:(NSUInteger)domain;

In these methods, the?domain?argument should always be the search domain taken by the search bundle, the?section?parameter should be an initialized?SPSearchResultSection?to contain the desired search results, and?results?should be an array ofSPSearchResult?objects.

In case there is usage of the below-described?-(BOOL)blockDatastoreComplete?method and at some point asynchronous behavior happens, you should call?-[SDSearchQuery storeCompletedSearch:], passing?self?as a parameter, and?pipe?as an object.

  • - (NSArray *)searchDomains;

It should return a?NSArray?object with?NSInteger?objects as its contents. Each?NSInteger?should hold an integer to serve as its taken search domain.

Due to a Loader limitation, in SearchLoader-loaded plugins only one search domain should be taken, else unknown results may be yielded.

  • - (NSString *)displayIdentifierForDomain:(int)domain;

This method should return a?NSString?object to represent the display identifier for a given domain. This display identifier is usually the application which search results reflect.

  • - (BOOL)blockDatastoreComplete;

To perform some asynchronous-only tasks inside your search bundle or delegate-calling requesters, you can return?YES?on this method to block the?-[SDSearchQuery storeCompletedSearch:]method, therefore not progressing further in the search process and then rendering result committing from the datastore impossible.

Later, a call to?-[SDSearchQuery storeCompletedSearch:]should be placed as described above for the result to be actually committed, and obviously,?NO?should be returned here then for your call not to be subsequently blocked.

libspotlight APIs

The following?libspotlight?functions can be used for convenience or are required during the development ofSearchLoader-loaded search bundles:

  • NSUInteger TLDomain(NSString *displayID, NSString *category);

(The internals of this function will be discussed further, and with it the need for a?category?parameter, which is characteristic of extended domains.)

This function gets the domain for a given display identifier (usually of the application which search results reflect) and a category string (defined above).

This?must?be the way to obtain the domain for a SearchLoader plugin, to avoid issues with other plugins.

  • void TLRequireInternet(BOOL require);

This enables or disables the status bar activity indicator in SpringBoard. This should be used if you are loading content from the Internet.

This function is?completely unrelated?to the?-blockDatastoreComplete?method from?TLSearchDatastore.

Miscellaneous

  • If you, for some reason, cannot use?-blockDatastoreCompleteto order?searchd?to wait for asynchronous tasks, you can useCFRunLoopRunInMode()?(so you can set timeouts, rather thanCFRunLoopRun()?where you can’t) to stop it from progressing without results being properly committed. It can later beCFRunLoopStop()ped.

A convention (made by me) states that you should?unless extremely required?never take over?3 seconds?with Internet requests.

Spotlight Bundles

SPSpotlightDatastore

  • - (NSDictionary *)contentToIndexForID:(NSString *)anId inCategory:(NSString *)category;

From parameter?anId, a string which serves as an *identifier, this method should return a?NSDictionary?object with specific keys to represent a result. The?category?parameter is the extended domain’s category.

The following keys can have values assigned for in the returned dictionary. They are all constants defined in the SearchLoader headers, and part of?Spotlight.framework:

  • kSPContentContentKey: The content of the search result. The query string matching this one is what defines whether this result should or not be displayed.
  • kSPContentTitleKey: The title for the search result.
  • kSPContentSummaryKey: The summary label of the search result.
  • kSPContentSubtitleKey: The subtitle label of the search result.
  • kSPContentAuxiliaryTitleKey: (iPad only) The auxiliary title for the search result.
  • kSPContentAuxiliarySubtitleKey: (iPad only) The auxiliary subtitle for the search result.
  • kSPContentCategoryKey: The category. Use is unknown.
  • kSPContentExternalIDKey: The external identifier of the result. Use is unknown.

I should provide an image specifying which labels are which graphically soon. Meanwhile, you’ll have to experiment with it ;)

  • - (NSArray *)allIdentifiersInCategory:(NSString *)category;

This should return an array of?NSString?objects to be passed into?-contentToIndexForID:inCategory:?as the?anId?parameter.

This method is called when the content index/database for given category is empty, and therefore it needs all existing data related to it put into identifiers, which will initially populate them.

An identifier has no proper definition nor standard, except the one that if it does not conform to URL standards it will not be put into the default-generated URL for it. The domain will be used instead. It can be as it best fits your parsing needs on?-contentToIndexForID:inCategory:. More details regarding default URL generation can be found below on the URL correction InfoBundle plist keys’ documentation.

To set a custom URL for a Spotlight bundle, the?TLCorrectURL...InfoBundle keys should be used. More details can be found below. This API is quite limited at the moment, but it can be expanded if a specific request regarding it is placed.

Updates File Manipulation

The following?SPSpotlightManager?method fromSpotlight.framework?can be used to modify the Updates file:

  • + (id)sharedManager;

Obtains the shared instance for the?SPSpotlightManager?class.

  • - (void)application:(NSString *)displayID modifiedRecordIDs:(NSArray *)identifiers forCategory:(NSString *)category;

This method adds the identifiers, described as?NSString?objects inside the?identifiers?parameter, to the updates file of extended domain of display identifier?displayID?and category?category.

Content Index/Database Manipulation

  • SPSpotlightManager: - (void)eraseIndexForApplication:(NSString *)displayID category:(NSString *)category;

This method deletes the?/var/mobile/Library/Spotlightfiles for certain category of certain application for given display identifier.

  • SPDomainManager: - (void)notifyIndexer;

This method triggers?AppIndexer, which will perform its on-launch tasks (update extended domains which require updating).

InfoBundles

InfoBundles are?document packages?(bundles without executables) placed inside?/Library/SearchLoader/Applications/. They tell SearchLoader which search/spotlight bundles placed at their respective directories should be loaded.

Required Keys

  • LSTypeIsPackage: Should always be set to true.
  • SPDisplayIdentifier: String representing the display identifier of the app which search results refer to.
  • SPCategory: Category for the plugin.
  • TLDisplayName: Display name for the plugin.

Required Keys for Search Bundles

  • TLIsSearchBundle: If set to true, defines that this plugin is a search bundle.

Required Keys for Extended Domains

  • SPSearchBundle: The name of the Spotlight bundle related to the extended domain.
  • SPDatastoreVersion: This value is mostly unused. Should be set to integer 1.

Optional Keys

  • TLCorrectURL: Boolean which defines whether SearchLoader should attempt to correct the URL generated by a search bundle/extended domain. Since you can generate your own URLs with search bundles, this is only intended to be used with extended domains.

The below keys show the process of creating your corrected URL with InfoBundle keys. It should be noted that the generated string should be a valid URL, else it will have no effect.

Correction works based on the manipulation of the original URL string. On search bundles, they are custom, and on Spotlight bundles, they take the following format:search://displayID/category/identifier.

It shall be noted that if?identifier?does not conform to URL standards, the original output URL after processing this string will have the?search://domain/record-entry-ID?format.

  • TLCorrectURLFormat: String which identifies a format string for the new URL. It should contain:

  • Substring?<$ID$>?expands to the display ID.

  • Substring?<$C$>?expands to the category.
  • Substring?<$D$>?expands to the domain.
  • Substring?%@?will be replaced by the selection of text from the original result URL defined by the keys below.

If there is no defined format and yet a delimiter, the default formatsearch://<$ID$>/<$C$>/%@?will be used.

  • TLCorrectURLStartZero: Boolean which defines whether the selection from the original URL’s text starts at the beginning. Takes precedence over?TLCorrectURLStartDelimiter.

  • TLCorrectURLEndLength: Boolean which defined whether the selection from the original URL’s text ends at the string’s end. Takes precedence over?TLCorrectURLEndDelimiter.

  • TLCorrectURLStartDelimiter: Required if?TLCorrectURLStartZerois absent. Defines the delimiter string for the start of the selection.

  • TLCorrectURLEndDelimiter: Required if?TLCorrectURLEndLengthis absent. Defines delimiter string for the end of the selection.

Optional Keys for Extended Domains

  • TLQueryLengthMinimum: Integer which represents the minimum character count for the content index/database for this extended domain to be searched.

SearchLoader

In the SpringBoard layer, SearchLoader changes:

  • Emptying the?_prefixWithNoResults?instance variable every time?-[SPSearchAgent setQueryString:]?is called, therefore making every query valid, as opposed to queries with only valid prefixes. This logic works with cases such as “if there’s no?Nolcontact, there’ll be no?Nolan, but doesn’t work with, for instance, Calculator, in which?1-?is valid and?1-1?is not.

  • Hooking?-[SBSearchModel _imageForDomain:andDisplayID:], allowing search results for non-existent apps to exist and still have icons in the table view. For instance, YouTube Search doesn’t require the YouTube app, yet should have an icon.

  • It hooks?-[SBSearchModel launchingURLForResult:withDisplayIdentifier: andSection:], to apply the changes asked for by theTLCorrectURL...?InfoBundle keys.

In the searchd layer, the core hooks are:

  • SPGetExtendedDomains(): Every SearchLoader plugin is faked as an existing extended domain, even being a search bundle. This provides a healthy domain for our search bundles and lets our spotlight bundles to be loaded.

  • -[SPExtendedDatastore searchDomains]: This prevents our search bundles’ domains to be registered byExtended.searchBundle. This is essential, else an exception will be thrown due to two search bundles having the same domain – our plugin and?Extended. With this put aside, our bundle is loaded without any major complication by?SPBundleManager.

These are the other hooks:

  • SPDisplayNameForExtendedDomain(int): This applies the chosen display name on the InfoBundle.

-[SPContentIndexer beginSearch:]: This is used to apply the restriction from the?TLQueryLengthMinimum?InfoBundle key.

  • NSBundle/NSFileManager?Path Hooks: Hooked to allow bundles at custom locations (in this case,/Library/SearchLoader/SearchBundles) to be loaded.

-[SDSearchQuery storeCompletedSearch:]: Hooked to allow?TLSearchDatastore‘s?-blockDatastoreCompletemethod to be implemented.

  • -[SDClient removeActiveQuery:]: Avoids a crash previously caused by a cached value which states SearchLoader was loading from?Extended.searchBundle?after finishing a query.

On?AppIndexer, domain hooks are placed and the following:

  • SBSCopyBundlePathForDisplayIdentifier(NSString *): This tricks?AppIndexer?into getting?SPSearchBundle?andSPDatastoreVersion?keys from our InfoBundle, not the actual app bundle. This is required to avoid file patching and allow extended domains for apps which are not installed.

Lastly, for the?TLRequireInternet(BOOL)?function fromlibspotlight?to work, a small Darwin notification system is placed inside the SpringBoard layer of the tweak, and when it receives a notification, it accordingly changes whether the status bar activity indicator is or not activated.

SearchLoaderPreferences

SearchLoader also hooks into Preferences to allow the native Spotlight preferences to know about our own plugins. It only applies the core hooks when?SearchSettings.bundle?is loaded.

Yet, it also adds a preference bundle of its own which thanks to rpetrich and DHowett’s?libprefs, can load – just like PreferenceLoader – preference entry plists! So you are allowed to – much like PreferenceLoader – place your plists exactly as you would with PL on?/Library/SearchLoader/Preferences. Neat, huh? :)

SearchLoader Limitations/Bugs

  • It does not allow multiple domains for search bundles, and unknown consequences may happen if a search bundle attempts to do so. There is, though, no known reason for this to be allowed.

  • While search bundles can be placed in/Library/SearchLoader/SearchBundles, Spotlight bundles require to be placed at/System/Library/Spotlight/SearchBundles. The reason for this is purely laziness.

  • SearchLoader creates empty Content Indexes for?every?search bundle, generating a number of empty-and-unused indexes at~/Library/Spotlight.


?

Conclusion

After 8 months of work in this area and some hours in this blog post (naturally not?only?on Loader, I’ve made my own share of Loader plugins to be released alongside it), I present this research. I hope it turns out to be useful. Seeing cool things being done with this is the best thing I could ever hope to achieve by making this.

I’d like to thank AAouiz and cydevlop for coordinating the Spotlight+ and SearchResults projects, which drove Loader to be created, and in no particular order Maximus, Cykey, DHowett, rms, fr0st, Nolan, ac3xx, his delightful wife, cj, Optimo, saurik and so many others who helped (directly or indirectly) to make this possible.

Finally, Loader has some fails, as the above section states, but I gladly take feature requests or bug reports.