[转]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.

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.

posted @ 2014-06-27 10:40  Proteas  阅读(723)  评论(0编辑  收藏  举报