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).
A simple C++ scope template with cmake build system is currently available as part of the Ubuntu SDK IDE. To use it install the packages required for scope development:
sudo apt-get install libunity-scopes-dev
In the Ubuntu SDK you will have to decide whether your scope will either access the network, or access the local filesystem.
Now you're ready to explore and modify the sample code in the src/ directory.
To register your scope, you must use the "scope" click hook, and point it to a directory containing your .ini file and .so file. In the template, a manifest like the follow is used:
Scopes that are packaged using click are inherently untrusted and must be confined. At present, there is only a single type of scope that can be defined:
The security manifest for this type of scope should be as follows:
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 gets passed a RegistryProxy instance, which can be used by aggregating scopes to locate their children. The stop method is usually used to release any resources, such as network connections where applicable. Non-aggregating scopes are not required to override the start nor stop methods.
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 we recommend 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 simple 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:
A scope can provide for simple customizations, such as allowing the user to configure an email address or select a distance unit as metric or imperial.
You can define such settings in a configuration file. The file must be placed into the same directory as the scope's normal configuration file, with the name <scope-name>-settings.ini
. For example, for a scope with ID com.acme.myscope
, the normal configuration file is com.acme.myscope.ini
, and the settings definition file is com.acme.myscope-settings.ini
. Both files must be installed in the same directory (together with the scope's .so
file).
The shell constructs a user interface from the settings definitions. The user can change settings via that UI. The scope can retrieve the actual setting values at run time (see Accessing settings values).
The following types are supported for settings:
string
- a string value number
- an integer value boolean
- true
or false
list
- a list of alternatives to choose from (single-choice) It is possible to optionally define a default value for each setting.
Here are the contents of an example definition file:
The file must contain a group for each setting. The order of the groups determines the display order for the user interface that is constructed by the shell. The group name is the ID of the corresponding setting.
Each setting definition must contain at least the following mandatory definitions:
type
- Defines the type of the setting (string
, number
, boolean
, or list
). displayName
- Defines a display name that is shown for this setting by the shell. The defaultValue field is optional. If present, it defines a default value that is provided to the scope if the user has not changed anything (or has never used the settings UI before using the scope). It is possible to test for settings that do not have a default value and were never set by the user (see Accessing settings values).
For settings of type list
, the displayValues
field is mandatory. It must contain an array that lists the available choices. If you provide a default value, it must be in the range 0..max-1
(where max
is the number of choices).
The displayName
and displayValues
fields can be localized by appending a locale identifier in square brackets. If no entry can be found that matches the current locale, the non-localized value is used.
The settings that are currently in effect are available to a scope via the unity::scopes::ScopeBase::settings() and unity::scopes::QueryBase::settings() methods. These methods return a unity::scopes::VariantMap with one entry per setting. The map contains an entry for each setting (using the group name as the key). The lookup value is a unity::scopes::Variant that holds the current value of the setting.
If a setting has a value, the corresponding entry in the map contains a string (for settings of type string
, a boolean (for settings of type boolean
), or an integer (for settings of type number
and list
). (If the user did not provide a particular value, but the settings definition provided a default value, the Variant
contains the default value.
If a setting does not have a default value, and the user did not establish a value for the setting, the corresponding entry is absent from the map.
When you use settings in your scope implementation, do not cache the values and re-use them for a different query. If you do, any setting changes made by the user will not take effect until your scope is re-started by the run time. (Because the user cannot know when that happens, this can be highly confusing.) Instead, call settings()
each time you need to use the value of a setting. That way, your scope will react to any change made by the user as soon as it receives another query.
Here is an example of how to read the current settings values for the definition in Settings Definition :
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
Scopes are installed under one of the "scopes directories" scanned by the scope registry. Currently these default to:
The /usr/lib
directory is for scopes that are pre-installed by Canonical. The /custom/lib
directory is for scopes that pre-installed by OEMs. The $HOME/.local
directory is for scopes that are installed from click packages.
Individual scopes are installed into subdirectories of these installation directories. The name of the subdirectory containing a scope's .ini
and .so
files can be anything but, to avoid name clashes, we strongly suggest something that is unique, such as com.canonical.scopes.scopename
. At a minimum, the directory structure must contain the following:
-+- ${scopesdir} `-+- subdirectory |--- scopename.ini `--- <library>.so
That is, each subdirectory must contain a scope .ini
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 name of the scope's .ini
file must be a unique ID for the scope. We strongly suggest to use a unique identifier, such as com.canonical.scopes.scopename
, to avoid clashes with scopes created by other developers.
The name of of the scope's .so
file can be libscopename.so
, scopename.so
, or simply scope.so
. For example, for a scope named Fred
, the names libFred.so
, Fred.so
, and scope.so
are acceptable. (No other library names are valid.)
The scope .ini
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 = ResultsTtlType = None, Small, Medium, or Large [Appearance] ForegroundColor = default text color (defaults to theme-provided foreground color) BackgroundColor = color of scope background (default is transparent) ShapeImages = whether to use Ubuntu-shape for all cards and artwork (defaults to true) CategoryHeaderBackground = background scheme of the results categories PreviewButtonColor = color of preview buttons (defaults to theme-provided color) LogoOverlayColor = color for the overlay in scopes overview (defaults to semi-transparent black) PageHeader.Logo = image containing scope's logo PageHeader.ForegroundColor = default header text color (defaults to the overall foreground color) PageHeader.Background = background scheme of the header PageHeader.DividerColor = color of the header divider PageHeader.NavigationBackground = background scheme of the navigation bar
The ScopeConfig
group is mandatory and must contain settings for at least DisplayName
, Description
, and Author
. DisplayName
and Description
can (and should) be localized. For example:
Description[de_DE] = Fußballergebnisse
In addition to allowing the registry to make the scope available, this information controls how the scope appears in the "Scopes" scope.
The group Appearance
and all keys within are optional and can be used to customize the look of the scope. Some of the Appearance
keys (like PageHeader.Background
) require background scheme uris, valid uris for these keys include:
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 the 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, 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 uses 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. Once you are 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.