Intents¶
Overview¶
A typical Cozy Cloud runs multiple applications, but most of these applications are focused on one task and interact with one particular type of data.
However, Cozy Cloud especially shines when data is combined across apps to provide an integrated experience. This is made difficult by the fact that apps have no dedicated back-end and that they have restricted access to documents.
This document outlines a proposal for apps to rely on each other to accomplish certain tasks and gain access to new documents, in a way that hopefully is neither painful for the users nor the developers.
Glossary¶
- Intent: Intents, sometimes also called Activities, is a pattern used in environments where multiple apps with different purposes coexist. The idea is that any app can express the need to do something that it can’t do itself, and an app that can do it will take over from there.
- Stack: refers to cozy-stack, the server-side part of the Cozy infrastructure.
- Client: the client is the application that starts an intent.
- Service: the service is the application that handles an intent started by a client.
- Available apps: Uninstalled applications that handles the intent.
Proposal¶
In this proposal, services declare themselves through their manifest. When a clients starts an intent, the stack finds the appropriate service and help the two apps to create a communication channel between them. After the channel is ready, the intent is processed. These steps are detailed below.
1. Manifest¶
Every app can register itself as a potential handler for one or more intents. To do so, it must provide the following information for each intent it wishes to handle:
action
: A verb that describes what the service should do. The most common actions areCREATE
,EDIT
,OPEN
,PICK
, andSHARE
. While we recommend using one of these verbs, the list is not exhaustive and may be extended by any app.type
: One or more types of data on which the service knows how to operate theaction
. Atype
can be expressed as a MIME type or a Cozy Document Type. The application must have permissions for any Cozy Document Type listed here. You can also think of thetype
as the intent’s subject.href
: the relative URL of the route designed to handle this intent. A query-string with the intent id will be added to this URL.
These informations must be provided in the manifest of the application, inside
the intents
key.
Here is a very simple example:
"intents": [ { "action": "PICK", "type": ["io.cozy.files"], "href": "/pick" } ]
When this service is called, it will load the page
https://service.domain.example.com/pick?intent=123abc
.
Here is an example of an app that supports multiple data types:
"intents": [ { "action": "PICK", "type": ["io.cozy.files", "image/*"], "href": "/pick" } ]
Finally, here is an example of an app that supports several intent types:
"intents": [ { "action": "PICK", "type": ["io.cozy.files", "image/*"], "href": "/pick" }, { "action": "VIEW", "type": ["image/gif"], "href": "/viewer" } ]
This information is stored by the stack when the application is installed.
2. Intent Start¶
Any app can start a new intent whenever it wants. When it does, the app becomes the client.
To start an intent, it must specify the following information:
action
: an action verb, which will be matched against the actions declared in services manifest files.type
: a single data type, which will be matched against the types declared in services manifest files.
There are also two optional fields that can be defined at the start of the intent:
data
: Any data that the client wants to make available to the service.data
must be a JSON object but its structure is left to the discretion of the client. The only exception is whentype
is a MIME type and the client wishes to send a file to the service. In that case, the file should be represented as a base-64 encoded Data URL and must be namedcontent
. This convention is also recomended when dealing with other intenttype
s. See the examples below for an example of this.permissions
: Whentype
is a Cozy Document Type and the client expects to receive one or more documents as part of the reply from the service, thepermissions
field allows the client to request permissions for these documents.permissions
is a list of HTTP Verbs. Refer to this section of the permission documentation for more information.
Note: if the intent’s subject is a Cozy Doctype that holds references to other Cozy Documents (such as an album referencing photos or a playlist referencing music files), the permissions should be granted for the referenced documents too, whenever possible.
Here is an example of what the API could look like:
// "Let the user pick a file" cozy.intents.start('PICK', 'io.cozy.files') .then(document => { // document is a JSON representation of the picked file }); // "Create a contact, with some information already filled out" cozy.intents.start('CREATE', 'io.cozy.contacts', { name: 'John Johnsons', tel: '+12345678' email: 'john@johnsons.com' }) .then(document => { // document is a JSON representation of the contact that was created }); // "Save this file somewhere" cozy.intents.start('CREATE', 'io.cozy.files', { content: 'data:application/zip;base64,UEsDB...', name: 'photos.zip' }); // "Create a new note, and give me read-only access to it" cozy.intents.start('CREATE', 'io.cozy.notes', {}, ['GET']) .then(document => { // document is a JSON representation of the note that was created. // Additionally, this note can now be retrieved through the API since we have read access on it. }); // "Create an event based on the provided data, and give me full access to it" cozy.intents.start('CREATE', 'io.cozy.events', { date: '2017/06/24', title: 'Beach day' }, ['ALL']) .then(document => { // document is a JSON representation of the note that was created. // Additionally, this note can now be retrieved through the API since we have read access on it. }); // "Crop this picture" cozy.intents.start('EDIT', 'image/png', { content: 'data:image/png;base64,iVBORw...', width: 50, height: 50 }) .then(image => { //image is the edited version of the image provided above. })
3. Service Resolution¶
The service resolution is the phase where a service is chosen to handle an intent. This phase is handled by the stack.
After the client has started it’s intent, it sends the action
, the type
and
the permissions
to the stack. The stack will then traverse the list of
installed apps and find all the apps that can handle that specific combination.
Note that if the intent request GET
permissions on a certain Cozy Document
Type, but the service does not have this permission itself, it can not handle
the intent.
The stack then stores the information for that intent:
- A unique id for that intent
- Client URL
- Service URL
action
type
permissions
Finally, the service URL is suffixed with ?intent=
followed by the intent’s
id, and then sent to the client.
4. Available apps¶
In addition to the services that manage intents, a list of available (but not
installed) applications of the instance that handles the intent is returned.
This object list is called availableApps
and contains:
- The application slug
- The application name
Service choice¶
If more than one service match the intent’s criteria, the stack returns the list of all matching service URLs to the client (and stores it in the service URL version of the intent it keeps in memory). The client is then free to pick one arbitrarily.
The client may also decide to let the user choose one of the services. To do
this, it should start another intent with a PICK
action and a io.cozy.apps
type
. This intent should be resolved by the stack to a special page, in order
to avoid having multiple services trying to handle it and ending up in a loop.
This special page is a service like any other; it expects the list of services to pick from as input data, and will return the one that has been picked to the client. The client can then proceed with the first intent.
The user may decide to abort the intent before picking a service. If that is the case, the choice page will need to inform the client that the intent was aborted.
No available service¶
If no service is available to handle an intent, the stack returns an error to
the client. However, some non-installed applications may handle the intent, and
are listed in the availableApps
field.
4. Handshake¶
The next phase consist of the client and the service establishing a communication channel between them. The communication will be done using the window.postMessage API.
Service Initialization¶
When the client receives the service URL from the stack, it starts to listen for messages coming from that URL. Once the listener is started, it opens an iframe that points to the service’s URL.
Service to Client¶
At this point, the service app is opened on the route that it provided in the
href
part of it’s manifest. This route now also contains the intent’s id.
The service queries the stack to find out information about the intent, passing
along the intent id. In response, the stack sends the client’s URL, the
action
, and the type
. If the intent includes permissions
, the stack sends
them too, as well as the client’s permission id.
It then starts to listen for messages coming from the client’s URL. Eventually, it sends a message to the client, as a mean to inform it that the service is now ready.
Client to Service¶
After the client receives the “ready” message from the service, it sends a message to the service acknowledging the “ready” state.
Along with this message, it should send the data
that was provided at the
start of the intent, if any.
5. Processing & Terminating¶
After this handshake, there is a confirmed communication channel between the client and the service, and the service knows what it has to do. This is the phase where the user interacts with the service.
If the service is going to grant extra permissions to the client app, it is strongly recommended to make this clear to the user.
When the service has finished his task, it sends a “completed” message to the
client. Permissions extensions should have been done before that. Along with the
completed message, the service should send any data it deems relevant. This data
should be a JSON object. Again, the structure of that object is left to the
discretion of the service, except when type
is a MIME type. In that case, the
file should be represented as a base-64 encoded
Data URL and must be named content
. This
convention is also recomended when dealing with other intent type
s.
After the client receives a “completed” message, it can close the service’s iframe and resume operations as usual.
If, for whatever reason, the service can not fulfill the intent, it can send an “error” message to the client. When the client receives an “error” message, the intent is aborted and the iframe can be closed.
Routes¶
POST /intents¶
The client app can ask to start an intent via this route.
Any client-side app can call this route, no permission is needed.
Request¶
POST /intents HTTP/1.1 Host: cozy.example.net Authorization: Bearer eyJhbG... Content-Type: application/vnd.api+json Accept: application/vnd.api+json
{ "data": { "type": "io.cozy.intents", "attributes": { "action": "PICK", "type": "io.cozy.files", "permissions": ["GET"] } } }
Response¶
HTTP/1.1 201 Created Content-Type: application/vnd.api+json
{ "data": { "id": "77bcc42c-0fd8-11e7-ac95-8f605f6e8338", "type": "io.cozy.intents", "attributes": { "action": "PICK", "type": "io.cozy.files", "permissions": ["GET"], "client": "https://contacts.cozy.example.net", "services": [ { "slug": "files", "href": "https://files.cozy.example.net/pick?intent=77bcc42c-0fd8-11e7-ac95-8f605f6e8338" } ], "availableApps": [ { "slug": "myapp", "name": "My App" } ] }, "links": { "self": "/intents/77bcc42c-0fd8-11e7-ac95-8f605f6e8338", "permissions": "/permissions/a340d5e0-d647-11e6-b66c-5fc9ce1e17c6" } } }
GET /intents/:id¶
Get all the informations about the intent
Note: only the service can access this route (no permission involved).
Request¶
GET /intents/77bcc42c-0fd8-11e7-ac95-8f605f6e8338 HTTP/1.1 Host: cozy.example.net Authorization: Bearer J9l-ZhwP... Content-Type: application/vnd.api+json Accept: application/vnd.api+json
Response¶
HTTP/1.1 200 OK Content-Type: application/vnd.api+json
{ "data": { "id": "77bcc42c-0fd8-11e7-ac95-8f605f6e8338", "type": "io.cozy.intents", "attributes": { "action": "PICK", "type": "io.cozy.files", "permissions": ["GET"], "client": "https://contacts.cozy.example.net", "services": [ { "slug": "files", "href": "https://files.cozy.example.net/pick?intent=77bcc42c-0fd8-11e7-ac95-8f605f6e8338" } ], "availableApps": [ { "slug": "myapp", "name": "My App" } ] }, "links": { "self": "/intents/77bcc42c-0fd8-11e7-ac95-8f605f6e8338", "permissions": "/permissions/a340d5e0-d647-11e6-b66c-5fc9ce1e17c6" } } }
Annexes¶
Use Cases¶
Here is a non exhaustive list of situations that may use intents:
- Configure a new connector account
- Share a photo album via email
- Add an attachment to an email
- Attach a note to a contact
- Create a contact based on an email
- Save an attachment received in an email
- Create a birthday event in a calendar, based on a contact
- Create an event based on an email’s content
- Create an event based on an ICS file
- Chose an avatar for a contact
- Open a file from the file browser (music, PDF, image, …)
- Attach a receipt to an expense
- Provide a tip for another application
Bibliography & Prior Art¶
- Prior art
- Web intents
- WebActivities and WebActivities on FirefoxOS (archive)
- Siri Intents
- Android Intents
- iOS extensions
Discarded Ideas¶
Disposition¶
Some specifications include a disposition
field in the manifests, that gives a
hint to the client about how to display the service (inlined or in a new
window). Since we were unable to find a use case where a new window is required,
we decided not to include the disposition
in this specification. It might be
added later if the need arises.
Client / Server architecture¶
Instead of letting the two applications communicate with each other, they could be made to talk through the stack. This would be useful as the stack could act as a middleware, transforming data on the fly or granting permissions where appropriate.
However, this approach has also severe drawbacks, notably the fact that the stack must hold a copy of all the data that the apps want to transfer (which can include large files). It also makes it significantly harder for the client to know when the intent has been processed.
Data representation¶
The client could explicitly request a data format to be used in the
communication. However, this idea was abandoned because the format is always
json, except when then intent’s type
is a MIME type, in which case the data
format also uses this type.