diff --git a/requirements/specifications/intents/user-interest.md b/requirements/specifications/intents/user-interest.md index bc5e5c65b..ae0c0c1c1 100644 --- a/requirements/specifications/intents/user-interest.md +++ b/requirements/specifications/intents/user-interest.md @@ -1,4 +1,4 @@ -# User Interest Intents +# User Interest Document Status: Candidate Specification @@ -51,22 +51,24 @@ track of which apps are using them separately. ## 2. Table of Contents - [1. Overview](#1-overview) - [2. Table of Contents](#2-table-of-contents) -- [3. User Interest from an in-app UX](#3-user-interest-from-an-in-app-ux) -- [4. User Interest from a platform UX](#4-user-interest-from-a-platform-ux) - - [4.1. Upstream User Interest Intent](#41-upstream-user-interest-intent) -- [5. User Interest Bulk Updates](#5-user-interest-bulk-updates) -- [6. Core SDK APIs](#6-core-sdk-apis) - - [6.1. InterestType](#61-interesttype) - - [6.2. Discovery.userInterest](#62-discoveryuserinterest) - - [6.3. Discovery Interest Provider](#63-discovery-interest-provider) - - [6.4. InterestIntent](#64-interestintent) -- [7. Discovery SDK APIs](#7-discovery-sdk-apis) - - [7.1. Content.requestUserInterest](#71-contentrequestuserinterest) - - [7.2. Content.onUserInterestedIn](#72-contentonuserinterestedin) - - [7.3. InterestedInIntent](#73-interestedinintent) - - -## 3. User Interest from an in-app UX +- [3. User Interest Flows](#3-user-interest-flows) + - [3.1. User Interest from an in-app UX](#31-user-interest-from-an-in-app-ux) + - [3.2. User Interest from a platform UX](#32-user-interest-from-a-platform-ux) + - [3.3. Upstream User Interest Intent](#33-upstream-user-interest-intent) + - [3.4. User Interest Bulk Updates](#34-user-interest-bulk-updates) +- [4. Core SDK APIs](#4-core-sdk-apis) + - [4.1. InterestType](#41-interesttype) + - [4.2. InterestReason](#42-interestreason) + - [4.3. Discovery.userInterest](#43-discoveryuserinterest) + - [4.4. Discovery Interest Provider](#44-discovery-interest-provider) + - [4.5. InterestIntent](#45-interestintent) +- [5. Discovery SDK APIs](#5-discovery-sdk-apis) + - [5.1. Content.requestUserInterest](#51-contentrequestuserinterest) + - [5.2. Content.onUserInterestedIn](#52-contentonuserinterestedin) + + +## 3. User Interest Flows +### 3.1. User Interest from an in-app UX Some Apps will have a built-in user interface for users to express interest in content from the App. This could be a "Favorite" button, @@ -82,33 +84,43 @@ meta-data, the device\'s Aggregated Experience will be notified of the user's interest in that entity: ```typescript -Discovery.userInterest(type:UserInterestType, entity:EntityInfo) +Discovery.userInterest(type:InterestType, reason: InterestReason, entity:EntityInfo) ``` -The type parameter denotes what sort of interest: +The `type` parameter denotes the directionality of the interest: - `interest` -- `disinterest` +- `disinterest` + +The `reason` parameter denotes why or how the user has expressed interest: + +| Reason | Description | +| ------ | ----------- | +| `playlist` | Interested in adding to a list | +| `reaction` | Interested in submitting a reaction, e.g. like or dislike | +| `recording` | Interest in scheduling a recording | +| `share` | Interest in sharing the content on social media | + +**NOTE**: We can remove some of these (not `playlist`) these are here for now to illustrate the purpose for the reason paramater. An app **MUST** `provide` the `xrn:firebolt:capability:discovery:interest` capability in order to call `Discovery.userInterest`. When this method is called with a valid `EntityInfo`, the platform -**MUST** dispatch a `Content.onUserInterestedIn` notification to all Apps +**MUST** dispatch a `Content.onUserInterest` notification to all Apps that have registered for it (typically Aggregated Experience Apps) with information about the app, interest type, and the entity. -The `Content.onUserInterestedIn` event has the `InterestedInIntent` as its -data. +The `Content.onUserInterest` event has a `UserInterestInfo` object with the `type`, `reason` and `entity` parameters as it's properties. -An Aggregated Experience can register for the `Content.onUserInterestedIn` +An Aggregated Experience can register for the `Content.onUserInterest` notification, and it will receive notifications when an `EntityInfo` is returned from the active App after a `Discovery.userInterest` call is fulfilled. An app **MUST** have permissions to `use` the `xrn:firebolt:capability:discovery:interest` capability in order to -listen to the `Content.onUserInterestedIn` notification. +listen to the `Content.onUserInterest` notification. If the result is `null` or is not a valid entity, i.e. does not match the [EntityInfo](../entities/) schema, then no `Content.onUserInterestedIn` @@ -123,7 +135,7 @@ The `Discovery.userInterest` method **SHOULD NOT** be called unless the user is activating a UI element in the app, or in a second screen experience that is communicating with the app, that implies interest of some kind. -## 4. User Interest from a platform UX +### 3.2. User Interest from a platform UX Firebolt platforms may provide a platform UX, e.g. voice or and RCU, to express user interest in content from an active App. To facilitate this @@ -165,31 +177,53 @@ exception. If the foreground App returns a valid `EntityInfo` before the timeout, then, the returned value **MUST** be used. -### 4.1. Upstream User Interest Intent +If there is no app registered the an error **MUST** be returned. + +### 3.3. Upstream User Interest Intent In some cases, e.g. a voice assistant, some upstream component will inform the platform that the user is interested in whatever is currently being presented. -To do this, the upstream system **MUST** send a `UserInterest` intent, which describes the type of interest. +To do this, the upstream system **MUST** send a `Interest` intent, which describes the type of and reason for the interest. + +```json +{ + "action": "interest", + "data": { + "type": "interest", + "reason": "playlist" + } +} +``` When a Firebolt platform receives this intent, it **SHOULD** initiate the platform's [user interest flow](#4-user-interest-from-a-platform-ux). -## 5. User Interest Bulk Updates +### 3.4. User Interest Bulk Updates Sending bulk interest updates, e.g. Entities the user expressed interest in on a different platform, is not supported. -## 6. Core SDK APIs +## 4. Core SDK APIs The following APIs are exposed by the Firebolt Core SDK as part of the `Discovery` module. -### 6.1. InterestType +### 4.1. InterestType This is an enum with the following values: - `"interest"` - `"disinterest"` -### 6.2. Discovery.userInterest +### 4.2. InterestReason +This is an enum with the following values: + +| Reason | Description | +| ------ | ----------- | +| `playlist` | Interested in adding to a list | +| `reaction` | Interested in submitting a reaction, e.g. like or dislike | +| `recording` | Interest in scheduling a recording | +| `share` | Interest in sharing the content on social media | + +### 4.3. Discovery.userInterest This is a push API that allows Apps to push entities that the user has expressed interest in to the platform. @@ -198,23 +232,23 @@ To push an entity that the user is interested in pass an `EntityInfo` object to the method: ```typescript -Discovery.userInterest(type: InterestType, entity: EntityInfo): Promise +Discovery.userInterest(type: InterestType, reason: InterestReason, entity: EntityInfo): Promise ``` -### 6.3. Discovery Interest Provider +### 4.4. Discovery Interest Provider To respond to requests for the current entity, because the user has expressed interest in some way that the platform manages, register a provider: ```typescript interface IDiscoveryInterestProvider { - function userInterest(type: InterestType): Promise + function userInterest(type: InterestType, reason: InterestReason): Promise } Discovery.provide("xrn:firbolt:capability:discovery:interest", IDiscoveryInterestProvider) ``` -### 6.4. InterestIntent +### 4.5. InterestIntent An `InterestIntent` denotes that the user has expressed interest in the currently displayed and/or selected content: @@ -223,7 +257,8 @@ currently displayed and/or selected content: type InterestIntent { action: "interest" data: { - type: "interest" + type: "interest" | "disinterest", + reason: "playlist" | "reaction" | "recording" }, context: { source: "rcu" | "voice" @@ -231,42 +266,26 @@ type InterestIntent { } ``` -## 7. Discovery SDK APIs +## 5. Discovery SDK APIs The following APIs are exposed by the Firebolt Discovery SDK as part of the `Content` module. -### 7.1. Content.requestUserInterest +### 5.1. Content.requestUserInterest This method triggers the corresponding Discovery provider API for the foreground app. ```typescript -Content.requestUserInterest(type: InterestType): Promise +Content.requestUserInterest(type: InterestType, reason: InterestReason): Promise ``` -### 7.2. Content.onUserInterestedIn +### 5.2. Content.onUserInterestedIn This notification allows Aggregated Experience Apps to be informed when a user expresses interest in some Content, and the content resolves to a valid Entity from some App. -`Content.listen('userInterestedIn', InterestedInIntent => void): Promise` - -The callback will be passed an `InterestedEventData` object with -information about the entity that the user expressed interest in. - -### 7.3. InterestedInIntent +`Content.listen('userInterest', UserInterestInfo => void): Promise` -`InterestedInIntent` provides information about what the user has -expressed interest in: - -```typescript -type InterestedInIntent { - action: "interestedIn", - data: { - appId: string, - type: InterestType, - entity: EntityInfo - } -} -``` +The callback will be passed an `UserInterestInfo` object with +the type, reason, and information about the entity that the user expressed interest in. diff --git a/requirements/specifications/openrpc-extensions/app-passthrough-apis.md b/requirements/specifications/openrpc-extensions/app-passthrough-apis.md index dc9d91d33..7e66951db 100644 --- a/requirements/specifications/openrpc-extensions/app-passthrough-apis.md +++ b/requirements/specifications/openrpc-extensions/app-passthrough-apis.md @@ -8,9 +8,12 @@ See [Firebolt Requirements Governance](../../governance.md) for more info. |-----------------|----------------| | Jeremy LaCivita | Comcast | | Kevin Pearson | Comcast | +| Yuri Pasquali | Sky | ## 1. Overview -This document describes the App Pass-through Firebolt OpenRPC extension. +This document describes how one Firebolt App can provide a capability that may be used by another Firebolt App leveraging the platform as a permission broker that passes the requests and respones to each app with out feature-specific logic. + +This document covers the App Pass-through Firebolt OpenRPC extension as well as how Firebolt implementations should detect and execute app provided pass-through APIs. Some APIs require an app to fulfill the request on behalf of another app, e.g. to provide a UX or cross-app data sharing. Generally the calling app doesn't care or have a say in which other app provides the API, that is up to the Firebolt distributor. @@ -23,113 +26,203 @@ The key words "**MUST**", "**MUST NOT**", "**REQUIRED**", "**SHALL**", "**SHALL ## 2. Table of Contents - [1. Overview](#1-overview) - [2. Table of Contents](#2-table-of-contents) -- [3. Provided By Extension](#3-provided-by-extension) -- [4. Selecting the best provider app](#4-selecting-the-best-provider-app) -- [5. Calculating the result](#5-calculating-the-result) - - [5.1. Selecting multiple provider apps](#51-selecting-multiple-provider-apps) - - [5.2. Composite Results](#52-composite-results) - - [5.3. Inserting the appId](#53-inserting-the-appid) -- [6. API Gateway](#6-api-gateway) -- [7. Example: User Interest](#7-example-user-interest) -- [8. Example: Keyboard](#8-example-keyboard) - -## 3. Provided By Extension -Firebolt OpenRPC **MUST** support a `string` `x-provided-by` extension property on the `capabilities` tag that denotes a method is provided by some app on the device registering for the specified provider API, e.g. `Module.onRequestMethod`. +- [3. Open RPC Extensions](#3-open-rpc-extensions) + - [3.1. Provided By Extension](#31-provided-by-extension) + - [3.2. Multiple Providers Extension](#32-multiple-providers-extension) +- [4. Routing App pass-through APIs](#4-routing-app-pass-through-apis) +- [5. Direct pass-through results](#5-direct-pass-through-results) +- [6. Aggregated pass-through results](#6-aggregated-pass-through-results) +- [7. Pass-through notifications](#7-pass-through-notifications) +- [8. Provider Candidates](#8-provider-candidates) +- [9. Best Candidate](#9-best-candidate) +- [10. Session Transformations](#10-session-transformations) +- [11. Provider Parameter Injection](#11-provider-parameter-injection) +- [12. API Gateway](#12-api-gateway) +- [13. Example: User Interest](#13-example-user-interest) +- [14. Example: Keyboard](#14-example-keyboard) + +## 3. Open RPC Extensions + +### 3.1. Provided By Extension +Firebolt OpenRPC **MUST** support a `string` `x-provided-by` extension property on the `capabilities` tag that denotes a method is provided by some app on the device registering for the specified provider API, e.g.: + +```json +{ + "methods": [ + { + "name": "Keyboard.standard", + "tags": [ + { + "name": "capabilities", + "x-provided-by": "Keyboard.onRequestStandard", + "x-uses": [ + "xrn:firebolt:capability:input:keyboard" + ] + } + ] + } + ] +} +``` The method denoted by `x-provided-by` is referred to as the "provider" or "provider method" for the remainder of this document. -The method with the `x-provided-by` extension is referred to as the "app provided method" for the remainder of this document. +The method with the `x-provided-by` extension is referred to as the "platform method" for the remainder of this document. -The `x-provided-by` extension **MUST NOT** be used on a method with any value in the `x-provides` extension. +To prevent unresolvable chaining of methods the `x-provided-by` extension **MUST NOT** be used on a method with any value in the `x-provides` extension. -An app provided method **MUST** `use` a single capability or `manage` a single capability, but not both. +To prevent compound methods an platform method **MUST** `use` a single capability or `manage` a single capability, but not both. -The provider method **MUST** provide the same capability that the app provided method either uses or manages. +The provider method **MUST** provide the same capability that the platform method either uses or manages. -If the app provided method has an `event` tag then the provider method **MUST** have a result schema with `"type"` set to the string `"null"`. +If an platform method has no provider method then it is not a valid Firebolt OpenRPC method schema, and a validation error **MUST** be generated. -If an app provided method has no provider method then it is not a valid Firebolt OpenRPC method schema, and a validation error **MUST** be generated. +### 3.2. Multiple Providers Extension +Firebolt OpenRPC **MUST** support a `string` `x-multiple-providers` extension property on the `capabilities` tag that denotes a single method request may be provided by multiple apps on the device registering for the specified provider API, e.g.: -## 4. Selecting the best provider app +```json +{ + "methods": [ + { + "name": "Content.search", + "tags": [ + { + "name": "capabilities", + "x-provided-by": "Discover.onRequestSearch", + "x-multiple-providers": true, + "x-uses": [ + "xrn:firebolt:capability:discovery:search" + ] + } + ] + } + ] +} +``` -**TODO**: This entire section should be moved into a per-device config for the capability, and not specified in the OpenRPC. +Setting `x-multiple-providers` to `true` means that all available apps that can provide the capability **MUST** be called and their results aggregated into an array for the final result. -A provider method's `capabilites` tag **MAY** have the `x-lifecycle` property which denotes which lifecycle states the providing app is allowed to be in during an app provided transaction. +A platform method with `x-multiple-providers` set to `true` **MUST** have an `array` result type. -If the `x-lifecycle` property is not present then it **MUST** be assumed to be `["foreground", "background", "inactive"]` for the remainder of this section. +## 4. Routing App pass-through APIs +When an app calls a platform method the platform **MUST** return an unavailable error if there is no candidate app to execute the provider method. -The app selected to provide a value **MUST** be in one of the lifecycle states listed in the `x-lifecycle` extension of the provider method. +## 5. Direct pass-through results +A direct pass-through is where a single app provides a single response to a single request by another app. -If the *app provided method's* `capabilities` tag has the `x-multiple-providers` property is set to `true` then all apps matching `x-lifecycle` at the time of the transaction **MUST** be used to provide the value, see [Selecting multiple provider apps](#32-selecting-multiple-provider-apps) for more info. +This section only applies to app provider methods that do not have an `event` tag and do not have the `x-multiple-providers` extension set to `true`. -If the *app provided method's* `capabilities` tag has the `x-multiple-providers` is set to `false` or not set then: +The platform method result schema **MUST** either: -> If more than one app is possible, then the candidate apps **MUST** be pruned by reevaluating the `x-lifecycle` array with the last value removed; This is repeated until there is only one app or only one lifecycle state remaining. -> -> If more than one app is still possible, then the app that was most recently in the `foreground` state **MUST** be selected; In the case of a tie, the platform **MUST** choose only one app using its own discretion. +> Match the `x-response` schema on the provider method so that the result can be passed through. > -> If there is no provider most recently in the `foreground` state then the app that was most recently in the inactive state **MUST** be selected. +> or +> +> Have a property that matches the `x-response` schema on the provider method so that the result can +> be composed and passed through. -If the app provided method does not have an `event` tag and no matching app provides the required capability then the calling app **MUST** receive an error that the capability is unavailable and not a result. +When an app calls a platform method the platform **MUST** return an unavailable error if there is no [candidate app](#7-provider-candidates) to execute the provider method. -If the app provided method has an `event` tag then event registration **MUST** not return an availability error due to a lack of providers, since one may be launched at a future point. +The platform **MUST** call the provider method from the [best candidate](#8-best-candidate) app and acquire the result. -**TODO**: ^^ do we want to scan the catalog and see if it's even possible to have an app that provides it? Seems heavy/overkill and dives into a spec we don't have yet. +If the platform method result schema matches the `x-response` schema on the provider method then the value **MUST** be used as-is. -## 5. Calculating the result -Each app provided method result **MUST** be calculated with the following potential transformations. +Otherwise if the platform method result schema has a property that matches the `x-response` schema on the provider method then the value **MUST** be composed into an object under the corresponding property name and the platform **MUST** apply any [session transformations](#9-session-transformations) to the composed result. -If an app provided method has `x-multiple-providers` set to `true` and the app provided method does not have an `event` tag, then the term "calculated result" refers to each item of the app provided method result array for the remainder of this section. +## 6. Aggregated pass-through results +An aggregated pass-through is where many apps provides responses to a single request by another app. The results are aggregated inside of an array. -Otherwise, the term "calculated result" refers to the app provided method result for the remainder of this section. +This section only applies to app provider methods that do not have an `event` tag and do have the `x-multiple-providers` extension set to `true`. -### 5.1. Selecting multiple provider apps -An app provided method's `capabilites` tag **MAY** have the `x-multiple-providers` property set to `true` which denotes that more than one app may provider this capability at the same time. +The platform method result schema **MUST** have a type of `array`. -If an app provided method has `x-multiple-providers` set to `true` and the app provided method does not have an `event` tag then: +The platform method result schema **MUST** have an `items` sub-schema that either: -> The method **MUST** have a result with the type set to `array`. +> Matches the `x-response` schema on the provider method so that the result can be added to the final array. > -> At least one of the following **MUST** be true: -> -> - The `items` schema of the array **MUST** match the `x-response` schema on the provider method. -> -> - The `items` schema of the array **MUST** have a property whose name is not `"appId"` and schema matches the `x-response` schema. +> or > -> The final result returned by the app provided method **MUST** be a flattened array with all of the values from all selected providers. +> Has a property that matches the `x-response` schema on the provider method so that the result can be composed +> and added to the final array. -If an app provided method has `x-multiple-providers` is set to `true` and the app provided method has an `event` tag then: +When an app calls a platform method the platform **MUST** return an unavailable error if there is no [candidate app](#7-provider-candidates) to execute the provider method. -> - The method result schema **MUST** match the `x-response` schema on the provider method. -> -> - The method result schema **MUST** have a property whose name is not `"appId"` and schema matches the `x-response` schema. -> -> The app provided method **MUST** dispatch each "calculated result" as a separate event to all listeners. +The platform **MUST** call the provider method from each [candidate app](#7-provider-candidates) and aggregated all of the results into an array. -### 5.2. Composite Results -An app provided method may be configured to use the provided value as the calculated result, or to compose it into an object along with other values. +If the platform method result `items` schema matches the `x-response` schema on the provider method then each provier value **MUST** be used as-is. -If the app provided method does not have an `event` tag: +Otherwise if the platform method result `items` schema has a property that matches the `x-response` schema on the provider method then each provider value **MUST** be composed into an object under the corresponding property name and the platform **MUST** apply any [session transformations](#9-session-transformations) to the composed result. -> If the calculated result schema matches the provider method result schema then the provider method result value **MUST** be passed through as-is, this is *not* considered a "composite result." -> -> Otherwise, if the calculated result schema is an object with a property whose name and schema matches the provider method result name and schema then the provider method result value **MUST** inserted into an object under the property name; this is refered to as a "composite result" for the rest of this document. +## 7. Pass-through notifications +Firebolt events have a synchronous subscriber registration method, e.g. `Lifecycle.onInactive(true)`, in addition to asynchronous notifications when the event actually happens. For events powered by an app pass-through, only the asynchronous notifications are passed in by the providing app. The initial event registration is handled by the platform, and the success response is not handled by the providing app. + +This section only applies to platform methods that have an `event` tag. + +App provided event registration **MUST** not return an availability error due to a lack of providers, since one may be launched at a future point. -If the app provided method has an `event` tag: +To ensure that event provider methods all behave the same the provider method **MUST** have a result schema with `"type"` set to the string `"null"`, since it will not expect any result from the platform after pushing the notification. -> If the calculated result schema matches the provider method's *last* parameter schema then the value of that parameter **MUST** be passed through as the calculated result value as-is. +The platform method result schema **MUST** either: + +> Match the *last* parameter schema on the provider method so that the result can be passed through. > -> Otherwise, if the calculated result schema is an object with a property whose name and schema matches the provider method's *last* parameter name and schema then the value of that parameter **MUST** inserted into an object under the property name; this is refered to as a "composite result" for the rest of this document. +> Have a property that matches the *last* parameter schema on the provider method so that the result can +> be passed through. + +The platform method event context parameters **MUST** each match the corresponding parameter schema on the provider method so that the result can be passed through. + +When a provider app calls a provider method mapped to an event the platform **MUST** ignore the notification if the app is not a [candidate app](#7-provider-candidates) for this capability. + +If the platform method result schema matches the *last* parameter schema on the provider method then the value **MUST** be used as-is. + +Otherwise if the platform method result schema has a property that matches the *last* parameter schema on the provider method then the value **MUST** be composed into an object under the corresponding property name and the platform **MUST** apply any [session transformations](#9-session-transformations) to the composed result. + +Finally the platform **MUST** dispatch the notification to the app that registered for the event via the original platform method, using all but the last parameter as context. + +## 8. Provider Candidates +The Firebolt Device Manfist **MUST** have a list of `ProviderPolicy` configurations that map capabilities to policies for determining candidate providers: + +```json +{ + "providerPolicies": [ + { + "lifecycle": [ + "foreground" + ], + "allowLaunch": true, + "capabilities": [ + "xrn:firebolt:capability:foo:bar" + ] + } + ] +} +``` + +The policy **MUST** have a list of valid lifecycle states for an app to provide the capability. + +The policy **MUST** have a boolean property `allowLaunch` to denote whether launching provider apps in order to fulfill a platform method is allowed. + +## 9. Best Candidate +If there is only one candidate then it **MUST** be the best candidate. + +If there is more than one candidate, then the app that was most recently in the foreground state **MUST** be the best candidate. + +If none of the candidates have been in the foreground state then the app that was most recently launched **MUST** be the best candidate. + +If none of the candidates have been launched and the `ProviderPolicy` has `allowLaunch` set to true then the platform **SHOULD** select a candidate app, launch it, and use it as the best candidate; how this selection occurs is out of scope for this document. + +## 10. Session Transformations +An platform method may be configured to insert the providing app id into composite values. This is not allowed in non-composite results to avoid collisions with the provder method sending an appId and Firebolt overriding it. -### 5.3. Inserting the appId -An app provided method may be configured to insert the providing app id into composite results. This is not allowed in non-composite results to avoid collisions with the provder method sending an appId and Firebolt overriding it. +If a "composite result" was used to wrap the provider method value and the platform method's schema has an `appId` `string` property at the top level then the property's value **MUST** be set to the the appId of the providing app for that calculated result. -If a "composite result" was used to wrap the provider method value and the app provided method's schema has an `appId` `string` property at the top level then the property's value **MUST** be set to the the appId of the providing app for that calculated result. +## 11. Provider Parameter Injection +If the provider method has a parameter named `appId` and the platform method *does not*, then the appId of the app calling the platform method **MUST** be sent to the provider in the `appId` parameter. -## 6. API Gateway +## 12. API Gateway The Firebolt API Gateway **MUST** detect app-passthrough APIs and map the `use`/`manage` APIs to the corresponding `provide` APIs by parsing the Firebolt OpenRPC Specification and following the logic outline in this document. -## 7. Example: User Interest +## 13. Example: User Interest User Interest does not use the `x-app-method` property because there is only one method and one event in the API, so they can be detected automatically via the capability string. @@ -335,7 +428,7 @@ Content.onUserInterest (push) } ``` -## 8. Example: Keyboard +## 14. Example: Keyboard Keyboard *requires* the* `x-app-method` property because there are three methods in the same capability, so the mapping cannot be detected automatically via the capability string.