Unity Scopes API
|
One of Unity’s core features on the desktop is the Dash. The Dash allows users to search for and discover virtually anything from local files and applications, to web content and other online data. The Dash achieves this by interfacing with one or more search plug-ins called “scopes” (e.g. “Apps”, “Music”, “Videos”, or “Amazon”, “Wikipedia”, “Youtube”).
On the phone and tablet, scopes make up the central user interface, as they provide everything a user needs from an operating system. Scopes enable users to locate and launch applications, access local files, play music and videos, search the web, manage their favourite social network, keep up with the latest news, and much more.
Each scope is a dedicated search engine for the category / data source it represents. The data source could be a local database, a web service, or even an aggregation of other scopes (e.g. the “Music” scope aggregates “Local Music” and “Online Music” scopes). A scope is primarily responsible for performing the actual search logic and returning the best possible results for each query it receives.
This document describes how to implement, test and package your own scope using the Unity Scopes C++ API (unity-scopes-api).
Local scopes are scopes that are located and run on the user’s device, while remote scopes (or “Smart Scopes”) are scopes that are located and run remotely on the Ubuntu Smart Scopes Server (or “SSS”). (Note: Although local scopes execute as local processes, they may still query online services in order to retrieve search results). A local scope usually requires local data, and therefore, can only be run locally, while a remote scope can effectively be run both locally and remotely.
When deciding on whether to write a local or remote scope, keep the user’s privacy in mind. For security reasons, a scope should not access the user’s personal data unless absolutely necessary (i.e. The scope requires account information or local data to perform searches). It is only in these situations that a scope should be written to run locally. By default, a scope should be written with the intention of running remotely on the Smart Scopes Server.
(For more information on how to deploy your scope to the Smart Scopes Server, or how to implement a native remote scope using the SSS REST API see: link_not_yet_available)
A simple C++ scope template with cmake build system is currently available on Launchpad, to use it install the packages required for scope development:
sudo apt-get install libunity-scopes-dev
Clone the bazaar branch with the scope template and build the scope:
bzr branch lp:~jpakkane/+junk/scopetemplate cd scopetemplate mkdir build cd build cmake .. make
Now you're ready to explore and modify the sample code in the src/ directory.
This short tutorial covers the basic steps and building blocks needed for implementing your own scope with unity-scopes-api, using C++. For complete examples of various scopes see demo/scopes subdirectory of the unity-scopes-api source project.
A typical scope implementation needs to implement interfaces of the following classes from the Scopes API:
The following sections show them in more detail.
This is the typical case: a scope that connects to a remote or local backend, database etc. and provides results in response to search queries coming from a client (i.e. Unity Dash or another scope).
There are a few pure virtual methods that need to be implemented; at the very minimum you need to provide a non-empty implementation of start and unity::scopes::ScopeBase::search() and unity::scopes::ScopeBase::preview() methods.
The start method must, at the very least return unity::scopes::ScopeBase::VERSION, e.g.
The stop method should release any resources, such as network connections where applicable. See the documentation of ScopeBase for an explanation of when ScopeBase::run; is useful; for typical and simple cases the implementation of run can be an empty function.
The unity::scopes::ScopeBase::search() method of scope implementation is the entry point of every search - it receives search queries from the Dash or other scopes. This method must return an instance of an object that implements unity::scopes::SearchQueryBase interface, e.g:
The search() method receives two arguments: a unity::scopes::CannedQuery query object that carries actual query string (among other information) and additional parameters of the search request, stored in unity::scopes::SearchMetadata - such as locale string, form factor string and cardinality. Cardinality is the maximum number of results expected from the scope (the value of 0 should be treated as if no limit was set). For optimal performance scopes should provide no more results than requested; if they however fail to handle cardinality constraint, any excessive results will be ignored by scopes API.
The central and most important method that needs to be implemented in this interface is unity::scopes::SearchQueryBase::run(). This is where actual processing of current search query takes place, and this is the spot where you may want to query local or remote data source for results matching the query.
The unity::scopes::SearchQueryBase::run() method gets passed an instance of SearchReplyProxy, which represents a receiver of query results. Please note that SearchReplyProxy is just a shared pointer for SearchReply object. The two most important methods of SearchReply object that every scope have to use are register_category and push.
The register_category method is a factory method for creating new categories (see unity::scopes::Category). Categories can be created at any point during query processing inside run method, but it's recommended to create them as soon as possible (ideally as soon as they are known to the scope).
When creating a category, one of its parameters is a unity::scopes::CategoryRenderer instance, which specifies how will a particular category be rendered. See the unity::scopes::CategoryRenderer documentation for more on that subject.
The actual search results have to be wrapped inside CategorisedResult objects and passed to push.
A typical implementation of run may look like this:
Scopes are responsible for handling preview requests for results they created; this needs to be implemented by overriding unity::scopes::ScopeBase::preview() method:
This method must return an instance derived from unity::scopes::PreviewQueryBase. The implementation of unity::scopes::PreviewQueryBase interface is similar to unity::scopes::SearchQueryBase in that its central method is unity::scopes::PreviewQueryBase::run(). This method is responsible for gathering preview data (from local or remote sources) and passing it along with the definition of preview look to unity::scopes::PreviewReplyProxy (this is a pointer to unity::scopes::PreviewReplyBasel; the run() method receives a pointer to an instance of unity::scopes::PreviewReply).
A preview consists of one or more preview widgets - these are the basic building blocks for previews, such as a header with a title and subtitle, an image, a gallery with multiple images, a list of audio tracks etc.; see unity::scopes::PreviewWidget for a detailed documentation and a list of supported widget types. So, the implementation of unity::scopes::PreviewQueryBase::run() needs to create and populate one or more instances of unity::scopes::PreviewWidget and push them to the client with unity::scopes::PreviewReply::push().
Every unity::scopes::PreviewWidget has a unique identifier, a type name and a set of attributes determined by its type. For example, a widget of "image" type expects two attributes: "source", which should point to an image (an uri) and "zoomable" boolean flag, which determines if the image should be zoomable. Values of such attributes can either be specified directly, or they can reference values present already in the unity::scopes::Result instance, or pushed spearately during the execution of unity::scopes::PreviewQueryBase::run().
Attributes can be specified directly with unity::scopes::PreviewWidget::add_attribute_value() method, e.g:
To reference values from results or arbitrary values pushed separately, use unity::scopes::PreviewWidget::add_attribute_mapping() method:
To push preview widgets to the client, use unity::scopes::PreviewReply::push():
Previews can have actions (i.e. buttons) that user can activate - they are supported by unity::scopes::PreviewWidget of "actions" type. This type of widget takes one or more action button definitions, where every button is constituted by an unique identifier, a label and an optional icon. For example, a widget with two buttons: "Open" and "Download" can be defined as follows (using unity::scopes::VariantBuilder helper class):
To handle activation of preview actions, scope needs to implement the following method of unity::scopes::ScopeBase:
This method receives a widget identifier and action identifier that was activated. This method needs to return an instance derived from unity::scopes::ActivationQueryBase. The derived class needs to reimplement unity::scopes::ActivationQueryBase::activate() method and put any activation logic in there. This method needs to respond with an instance of unity::scopes::ActivationResponse, which informs the shell about status of activation and the expected behaviour of the UI. For example, activate() may request a new search query to be executed as follows:
In many cases search results can be activated (i.e. when user taps or clicks them) directly by the shell - as long as a desktop schema (such as "http://") of result's uri has a handler in the system. If this is the case, then there is nothing to do in terms of activation handling in the scope code. If however a scope relies on a schema handler that's not present in the system, the offending result will be ignored by Unity shell and nothing will happen on activation.
In cases where scope wants to intercept and handle activation request (e.g. when no handler for specifc type of uri exists, or to do some extra work on activation), it has to reimplement unity::scopes::ScopeBase::activate() method:
and also call Result::set_intercept_activation() for all results that should trigger unity::scopes::ScopeBase::activate() on activation. The implementation of unity::scopes::ScopeBase::activate() should follow the same guidelines as unity::scopes::ScopeBase::perform_action(), the only difference with result activation being the lack of widget or action identifiers, as those are specific to preview widgets.
The scope needs to be compiled into a .so shared library and to be succesfully loaded at runtime it must provide two C functions to create and destroy it - a typical code snippet to do this looks as follows:
Aggregator scope is not much different from regular scopes, except for its data sources can include any other scope(s). The main difference is in the implementation of run method of unity::scopes::SearchQueryBase and in the new class that has to implement SearchListenerBase interface, which receives result from other scope(s).
To send search query to another scope, use one of the subsearch()
overloads of unity::scopes::SearchQueryBase inside your implementation of unity::scopes::SearchQueryBase. This method requires - among search query string - an instance of ScopeProxy that points to the target scope and an instance of class that implements SearchListenerBase interface. ScopeProxy can be obtained from unity::scopes::RegistryProxy and the right place to do this is in the implementation of start() method of ScopeBase interface.
The SearchListenerBase is an abstract class to receive the results of a query sent to a scope. Its virtual push methods let the implementation receive result items and categories returned by that query. A simple implementation of an aggregator scope may just register all categories it receives and push all received results upstream to the query originator, e.g.
A more sophisticated aggregator scope can rearrange results it receives into a different set of categories, alter or enrich the results before pushing them upstream etc.
If an aggregator scope just forwards results it receives from other scopes, possibly only changing their category assignment, then there is nothing to do in terms of handling previews, preview actions and result activation: preview and perform_action requests will trigger respective methods of unity::scopes::ScopeBase for the scope that created results. Result activation will trigger unity::scopes::ScopeBase::activate() method for the scope that produced the result as long as it set interception flag for it. In other words, when aggreagor scope just forwards results (and makes only minor adjustements to them, such as category assignment), it is not involved in preview or activation handling at all.
If, however, aggregator scope changes attributes of results (or creates completely new results that "replace" received results), then some extra care needs to be taken:
if original scope should still handle preview (and activation) requests, then aggregator has to store a copy of original result in the modified (or brand new) result. This can be done with unity::scopes::Result::store method. Preview request for such result will automatically trigger a scope that created the most inner stored result, and that scope will receive the stored result. It will also do the same for activation as long as the original scope set interception flag on that result.
Consider the following example of implementation of unity::scopes::SearchListenerBase interface that modifies results and stores their copies, so that original scope can handle previews and activation for them:
Unity Scopes API provides testing helpers based on well-known and established testing frameworks: googletest and googlemock. Please see respective documentation of those projects for general information about how to use Google C++ Testing Framework.
All the helper classes provided by Scopes API are located in unity::scopes::testing namespace. The most important ones are:
With the above classes a test case that checks if MyScope calls appropriate methods of unity::scopes::SearchReply may look like this (note that it just checks if proper methods get called and uses _ matchers that match any values; put actual values in there for stricts checks):
Installing a scope is as simple as running make install
when using the scope template. You might need to restart the global scope registry when a new scope is installed by running:
restart scope-registry
The scope will be installed under one of the "scopes directories" scanned by the scope registry. Currently these default to:
/custom/lib/${arch}/unity-scopes
Individual scopes are installed into a subdirectory matching the scope's name. At a minimum, the directory structure should contain the following:
-+- ${scopesdir} `-+- scopename |--- scopename.ini `--- libscopename.so
That is, a scope metadata file and a shared library containing the scope code. The scope author is free to ship additional data in this directory (e.g. icons and screenshots).
The scope metadata file uses the standard ini file format, with the following keys:
[ScopeConfig] DisplayName = human readable name of scope Description = description of scope Author = Author Icon = path to icon representing the scope Art = path to screenshot of the scope SearchHint = hint text displayed to user when viewing scope HotKey =
In addition to allowing the registry to make the scope available, this information controls how the scope appears in the "Scopes" scope.
To help with the development of a scope and to be able to see how will the dash render the dynamically-specified categories (see unity::scopes::CategoryRenderer), a specialized tool to preview a scope is provided - the "Unity Scope Tool".
You can install it from the Ubuntu archive using:
sudo apt-get install unity-scope-tool
After installation, you can run the scope-tool with a parameter specifying path to your scope configuration file (for example unity-scope-tool ~/dev/myscope/build/myscope.ini
). If a binary for your scope can be found in the same directory (ie there's ~/dev/myscope/build/libmyscope.so
), the scope-tool will display surfacing and search results provided by your scope, and allow you to perform searches, invoke previews and actions within previews.
Note that the scope-tool is using the same rendering mechanism as Unity itself, and therefore what you see in the scope-tool is what you get in Unity. It can also be used to fine-tune the category definitions, as it allows you to manipulate the definitions on the fly, and once you're happy with the result you can just copy the JSON definition back into your scope (see unity::scopes::CategoryRenderer::CategoryRenderer()).
The scope-tool supports a few command line arguments:
--include-system-scopes
/ --include-server-scopes
option to allow development of aggregating scopes.