From ef3de3e68619c6f666dd1d751b0acc25f6438122 Mon Sep 17 00:00:00 2001 From: Kevin <72702438+kevinshahfws@users.noreply.github.com> Date: Wed, 28 Feb 2024 11:51:02 -0500 Subject: [PATCH 01/21] fix: Removed sdk as required field from Device.version (#231) --- src/openrpc/device.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/openrpc/device.json b/src/openrpc/device.json index dc0031397..04cdced4d 100644 --- a/src/openrpc/device.json +++ b/src/openrpc/device.json @@ -316,7 +316,6 @@ } }, "required": [ - "sdk", "api", "firmware", "os" From 20c4564ab79580c0d3c1aebd54e8a7d8fb81ee1f Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 28 Feb 2024 16:52:50 +0000 Subject: [PATCH 02/21] chore(release): 1.1.1-next.1 [skip ci] ## [1.1.1-next.1](https://github.com/rdkcentral/firebolt-apis/compare/v1.1.0...v1.1.1-next.1) (2024-02-28) ### Bug Fixes * Removed sdk as required field from Device.version ([#231](https://github.com/rdkcentral/firebolt-apis/issues/231)) ([ef3de3e](https://github.com/rdkcentral/firebolt-apis/commit/ef3de3e68619c6f666dd1d751b0acc25f6438122)) --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- src/sdks/core/package.json | 2 +- src/sdks/manage/package.json | 2 +- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1f063944..05421e5ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [1.1.1-next.1](https://github.com/rdkcentral/firebolt-apis/compare/v1.1.0...v1.1.1-next.1) (2024-02-28) + + +### Bug Fixes + +* Removed sdk as required field from Device.version ([#231](https://github.com/rdkcentral/firebolt-apis/issues/231)) ([ef3de3e](https://github.com/rdkcentral/firebolt-apis/commit/ef3de3e68619c6f666dd1d751b0acc25f6438122)) + # [1.1.0](https://github.com/rdkcentral/firebolt-apis/compare/v1.0.0...v1.1.0) (2024-02-09) diff --git a/package-lock.json b/package-lock.json index 4994954ab..e5af29a6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@firebolt-js/sdks", - "version": "1.1.0", + "version": "1.1.1-next.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@firebolt-js/sdks", - "version": "1.1.0", + "version": "1.1.1-next.1", "license": "Apache-2.0", "workspaces": [ "src/sdks/core", diff --git a/package.json b/package.json index bab5f2042..3b4fb8fea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/sdks", - "version": "1.1.0", + "version": "1.1.1-next.1", "description": "The Firebolt JS SDK", "type": "module", "bin": { diff --git a/src/sdks/core/package.json b/src/sdks/core/package.json index e226ca444..5d5ab2e2a 100644 --- a/src/sdks/core/package.json +++ b/src/sdks/core/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/sdk", - "version": "1.1.0", + "version": "1.1.1-next.1", "description": "The Firebolt JS SDK", "main": "./dist/lib/firebolt.mjs", "types": "./dist/lib/firebolt.d.ts", diff --git a/src/sdks/manage/package.json b/src/sdks/manage/package.json index 361bf28bb..39a004fc5 100644 --- a/src/sdks/manage/package.json +++ b/src/sdks/manage/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/manage-sdk", - "version": "1.1.0", + "version": "1.1.1-next.1", "description": "The Firebolt Manage JS SDK", "main": "./dist/lib/firebolt-manage.mjs", "types": "./dist/lib/firebolt-manage.d.ts", From 9769d6d7d43f1d61dfcda16ba4806175c5cf7658 Mon Sep 17 00:00:00 2001 From: Jeremy LaCivita Date: Wed, 6 Mar 2024 09:51:47 -0500 Subject: [PATCH 03/21] fix: Add PlaylistEntity to EntityIntent --- src/schemas/intents.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/schemas/intents.json b/src/schemas/intents.json index 041018322..6fa2a5807 100644 --- a/src/schemas/intents.json +++ b/src/schemas/intents.json @@ -386,6 +386,9 @@ { "$ref": "#/definitions/MusicEntity" }, + { + "$ref": "#/definitions/PlaylistEntity" + }, { "$ref": "#/definitions/AdditionalEntity" }, From 869d9420aac4b91f41e10d0e1bb8752b72a258a2 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 6 Mar 2024 14:53:13 +0000 Subject: [PATCH 04/21] chore(release): 1.1.1-next.2 [skip ci] ## [1.1.1-next.2](https://github.com/rdkcentral/firebolt-apis/compare/v1.1.1-next.1...v1.1.1-next.2) (2024-03-06) ### Bug Fixes * Add PlaylistEntity to EntityIntent ([9769d6d](https://github.com/rdkcentral/firebolt-apis/commit/9769d6d7d43f1d61dfcda16ba4806175c5cf7658)) --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- src/sdks/core/package.json | 2 +- src/sdks/manage/package.json | 2 +- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05421e5ed..f1c3c0bcf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [1.1.1-next.2](https://github.com/rdkcentral/firebolt-apis/compare/v1.1.1-next.1...v1.1.1-next.2) (2024-03-06) + + +### Bug Fixes + +* Add PlaylistEntity to EntityIntent ([9769d6d](https://github.com/rdkcentral/firebolt-apis/commit/9769d6d7d43f1d61dfcda16ba4806175c5cf7658)) + ## [1.1.1-next.1](https://github.com/rdkcentral/firebolt-apis/compare/v1.1.0...v1.1.1-next.1) (2024-02-28) diff --git a/package-lock.json b/package-lock.json index e5af29a6b..69cadf7ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@firebolt-js/sdks", - "version": "1.1.1-next.1", + "version": "1.1.1-next.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@firebolt-js/sdks", - "version": "1.1.1-next.1", + "version": "1.1.1-next.2", "license": "Apache-2.0", "workspaces": [ "src/sdks/core", diff --git a/package.json b/package.json index 3b4fb8fea..59899531a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/sdks", - "version": "1.1.1-next.1", + "version": "1.1.1-next.2", "description": "The Firebolt JS SDK", "type": "module", "bin": { diff --git a/src/sdks/core/package.json b/src/sdks/core/package.json index 5d5ab2e2a..16a8bd6de 100644 --- a/src/sdks/core/package.json +++ b/src/sdks/core/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/sdk", - "version": "1.1.1-next.1", + "version": "1.1.1-next.2", "description": "The Firebolt JS SDK", "main": "./dist/lib/firebolt.mjs", "types": "./dist/lib/firebolt.d.ts", diff --git a/src/sdks/manage/package.json b/src/sdks/manage/package.json index 39a004fc5..4b354e5aa 100644 --- a/src/sdks/manage/package.json +++ b/src/sdks/manage/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/manage-sdk", - "version": "1.1.1-next.1", + "version": "1.1.1-next.2", "description": "The Firebolt Manage JS SDK", "main": "./dist/lib/firebolt-manage.mjs", "types": "./dist/lib/firebolt-manage.d.ts", From 7c6ca7e84935b5ee83091c4bb3e0e5a5460b0cdb Mon Sep 17 00:00:00 2001 From: bpvstaty366 <148532497+bpvstaty366@users.noreply.github.com> Date: Fri, 8 Mar 2024 17:50:51 +0200 Subject: [PATCH 05/21] CoreSDK suite on MFOS - FCA standalone actions (#234) Core sanity suite execution from Mock-Firebolt on the PR --- .github/workflows/pr-comment.yml | 52 ++++++ .../workflows/run-standalone-mfos-tests.yml | 34 ++++ .github/workflows/utils.cjs | 53 ++++++ .github/workflows/utils.sh | 162 ++++++++++++++++++ 4 files changed, 301 insertions(+) create mode 100644 .github/workflows/pr-comment.yml create mode 100644 .github/workflows/run-standalone-mfos-tests.yml create mode 100755 .github/workflows/utils.cjs create mode 100755 .github/workflows/utils.sh diff --git a/.github/workflows/pr-comment.yml b/.github/workflows/pr-comment.yml new file mode 100644 index 000000000..03dd88bc5 --- /dev/null +++ b/.github/workflows/pr-comment.yml @@ -0,0 +1,52 @@ +name: Comment Artifact URL on PR + +on: + workflow_run: + types: + - "completed" + workflows: + - "Core SDK - MFOS standalone sanity report" + +jobs: + comment-on-pr: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Get Artifact And Job Data + env: + GITHUB_TOKEN: ${{ github.token }} + OWNER: ${{ github.repository_owner }} + REPO: ${{ github.event.repository.name }} + SERVER_URL: ${{ github.server_url }} + GITHUB_REPO: ${{ github.repository }} + WORKFLOW_RUN_EVENT_OBJ: ${{ toJSON(github.event.workflow_run) }} + run: | + ./.github/workflows/utils.sh getArtifactData + + - name: Download an artifact + uses: actions/github-script@v6 + with: + script: | + const { downloadArtifact } = require('./.github/workflows/utils.cjs'); + downloadArtifact(process.env.ARTIFACT_ID,process.env.JOB_PATH,process.env.PR_NUMBER,context,github); + + - name: Unzip an artifact + run: | + ./.github/workflows/utils.sh unzipArtifact + + - name: Post a Comment + uses: actions/github-script@v6 + with: + script: | + const { createComment } = require('./.github/workflows/utils.cjs'); + createComment(github, context); + env: + issue_number: ${{ env.PR_NUMBER }} + Passes: ${{ env.Passes }} + Failures: ${{ env.Failures }} + Pending: ${{ env.Pending }} + Skipped: ${{ env.Skipped }} + ARTIFACT_URL: ${{ env.ARTIFACT_URL }} + JOB_PATH: ${{ env.JOB_PATH }} \ No newline at end of file diff --git a/.github/workflows/run-standalone-mfos-tests.yml b/.github/workflows/run-standalone-mfos-tests.yml new file mode 100644 index 000000000..675202521 --- /dev/null +++ b/.github/workflows/run-standalone-mfos-tests.yml @@ -0,0 +1,34 @@ +name: Core SDK - MFOS standalone sanity report + +on: + pull_request: + types: + - opened + - synchronize + branches: [ next ] + +jobs: + RunStandaloneTests: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Run tests and create assets + env: + EVENT_NAME: ${{ github.event_name }} + GITHUB_REF: ${{ github.ref }} + PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} + INTENT: ${{ secrets.INTENT }} + run: | + ./.github/workflows/utils.sh runTests + + - name: Upload report.json as an artifact + uses: actions/upload-artifact@v2 + with: + name: report + path: ${{ github.workspace }}/report + + - name: Get results from report.json + run: | + ./.github/workflows/utils.sh getResults \ No newline at end of file diff --git a/.github/workflows/utils.cjs b/.github/workflows/utils.cjs new file mode 100755 index 000000000..32e39a38b --- /dev/null +++ b/.github/workflows/utils.cjs @@ -0,0 +1,53 @@ + +async function downloadArtifact(ARTIFACT_ID, JOB_PATH, PR_NUMBER, context, github) { + if (!ARTIFACT_ID) { + console.log("Send the comment to PR and exit the job"); + // Create a comment + const comment = `Failed to create a report:\n Job logs: ${JOB_PATH}`; + + // Post the comment to the pull request + let prcomment = await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: PR_NUMBER, + body: comment, + }); + + process.exit(1); + } else { + // Download report.json + let download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: ARTIFACT_ID, + archive_format: 'zip', + }); + + let fs = require('fs'); + fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/report.zip`, Buffer.from(download.data)); + + console.log("Artifact downloaded successfully."); + } +} + +async function createComment(github, context) { + const { issue_number, Passes, Failures, Pending, Skipped, ARTIFACT_URL, JOB_PATH } = process.env; + + const body = ` + Core SDK - MFOS standalone sanity report: + Passes - ${Passes} Failures - ${Failures} Pending - ${Pending} Skipped - ${Skipped} + Report JSON/HTML Files: ${ARTIFACT_URL} + Job Logs: ${JOB_PATH} + `; + + const response = await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issue_number, + body: body.trim() + }); + + console.log('Comment updated successfully.'); +} + +module.exports = { downloadArtifact, createComment }; \ No newline at end of file diff --git a/.github/workflows/utils.sh b/.github/workflows/utils.sh new file mode 100755 index 000000000..13c8e7ff7 --- /dev/null +++ b/.github/workflows/utils.sh @@ -0,0 +1,162 @@ +#!/bin/bash +set -o pipefail + +function runTests(){ + echo "Clone firebolt-apis repo with pr branch" + PR_BRANCH=$(echo "$EVENT_NAME" | tr '[:upper:]' '[:lower:]') + if [ "${PR_BRANCH}" == "pull_request" ]; then + PR_BRANCH=$PR_HEAD_REF + elif [ "${PR_BRANCH}" == "push" ]; then + PR_BRANCH=$GITHUB_REF + PR_BRANCH="${PR_BRANCH#refs/heads/}" + else + echo "Unsupported event: $EVENT_NAME" + exit 1 + fi + + git clone --branch ${PR_BRANCH} https://github.com/rdkcentral/firebolt-apis.git + echo "cd to firebolt-apis repo and compile firebolt-open-rpc.json" + cd firebolt-apis + npm i + npm run compile + npm run dist + cd .. + + echo "clone mfos repo and start it in the background" + git clone https://github.com/rdkcentral/mock-firebolt.git + cd mock-firebolt/server + cp ../../firebolt-apis/dist/firebolt-open-rpc.json ../../mock-firebolt/server/src/firebolt-open-rpc.json + jq 'del(.supportedOpenRPCs[] | select(.name == "core"))' src/.mf.config.SAMPLE.json > src/.mf.config.SAMPLE.json.tmp && mv src/.mf.config.SAMPLE.json.tmp src/.mf.config.SAMPLE.json + jq '.supportedOpenRPCs += [{"name": "core","cliFlag": null,"cliShortFlag": null,"fileName": "firebolt-open-rpc.json","enabled": true}]' src/.mf.config.SAMPLE.json > src/.mf.config.SAMPLE.json.tmp && mv src/.mf.config.SAMPLE.json.tmp src/.mf.config.SAMPLE.json + cp src/.mf.config.SAMPLE.json src/.mf.config.json + npm install + npm start & + cd ..//.. + + echo "clone fca repo and start it in the background" + git clone --branch main https://github.com/rdkcentral/firebolt-certification-app.git + cd firebolt-certification-app + jq '.dependencies["@firebolt-js/sdk"] = "file:../firebolt-apis/src/sdks/core"' package.json > package.json.tmp && mv package.json.tmp package.json + npm install + npm start & + sleep 5s + cd .. + + echo "curl request with runTest install on initialization" + response=$(curl -X POST -H "Content-Type: application/json" -d "$INTENT" http://localhost:3333/api/v1/state/method/parameters.initialization/result) + + echo "run mfos tests in a headless browser" + npm install puppeteer + echo "Start xvfb" + Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & + export DISPLAY=:99 + + echo "Run headless browser script with puppeteer" + node -e ' + const puppeteer = require("puppeteer"); + const fs = require("fs"); + (async () => { + const browser = await puppeteer.launch({ headless: true, args: ["--no-sandbox", "--disable-gpu"] }); + const page = await browser.newPage(); + + // Enable console logging + page.on("console", (msg) => { + let logMessage=""; + if (msg.type().includes("log")) { + logMessage = `${msg.text()}`; + console.log(logMessage); + } + if (logMessage.includes("Response String:")) { + const jsonStringMatch = logMessage.match(/Response String:(.*)/); + if (jsonStringMatch && jsonStringMatch[1]) { + try { + const jsonString = jsonStringMatch[1].trim(); + const responseString = JSON.parse(jsonString); + console.log("Parsed JSON:", responseString); + const filePath="report.json" + fs.writeFileSync(filePath, JSON.stringify(responseString), "utf-8"); + console.log(`Parsed JSON written to ${filePath}`); + // Exit the Node.js script + process.exit(0); + + } catch (error) { + console.error("Error parsing JSON:", error); + } + } + } + }); + // Navigate to the URL + await page.goto("http://localhost:8081/?mf=ws://localhost:9998/12345&standalone=true"); + + // Sleep for 80 seconds (80,000 milliseconds) + await new Promise(resolve => setTimeout(resolve, 80000)); + + // Close the browser + await browser.close(); + })(); + ' + echo "create html and json assets" + npm i mochawesome-report-generator + mkdir report + mv report.json report/ + jq -r '.' report/report.json > tmp.json && mv tmp.json report/report.json + jq '.report' report/report.json > tmp.json && mv tmp.json report/report.json + node -e ' + const marge = require("mochawesome-report-generator/bin/cli-main"); + marge({ + _: ["report/report.json"], + reportFileName: "report.json", + reportTitle: "FireboltCertificationTestReport", + reportPageTitle: "FireboltCertificationTestReport", + reportDir: "./report", + }); + ' +} + +function getResults(){ + failures=$(cat report/report.json | jq -r '.stats.failures') + echo "If failures more than 0, fail the job" + echo "Failures=$failures" + if [ "$failures" -eq 0 ]; then + echo "No failures detected." + else + exit 1 + fi +} + +function getArtifactData(){ + PREVIOUS_JOB_ID=$(jq -r '.id' <<< "$WORKFLOW_RUN_EVENT_OBJ") && echo "PREVIOUS_JOB_ID=$PREVIOUS_JOB_ID" >> "$GITHUB_ENV" + SUITE_ID=$(jq -r '.check_suite_id' <<< "$WORKFLOW_RUN_EVENT_OBJ") && echo "SUITE_ID=$SUITE_ID" >> "$GITHUB_ENV" + ARTIFACT_ID=$(gh api "/repos/$OWNER/$REPO/actions/artifacts" --jq ".artifacts[] | select(.workflow_run.id==$PREVIOUS_JOB_ID and .expired==false) | .id") && echo "ARTIFACT_ID=$ARTIFACT_ID" >> "$GITHUB_ENV" + PR_NUMBER=$(jq -r '.pull_requests[0].number' <<< "$WORKFLOW_RUN_EVENT_OBJ") && echo "PR_NUMBER=$PR_NUMBER" >> "$GITHUB_ENV" + ARTIFACT_URL="$SERVER_URL/$GITHUB_REPO/suites/$SUITE_ID/artifacts/$ARTIFACT_ID" && echo "ARTIFACT_URL=$ARTIFACT_URL" >> "$GITHUB_ENV" + JOB_PATH="$SERVER_URL/$GITHUB_REPO/actions/runs/$PREVIOUS_JOB_ID" && echo "JOB_PATH=$JOB_PATH" >> "$GITHUB_ENV" +} + +function unzipArtifact(){ + unzip report.zip + # Extract values from report.json + report=$(cat report.json | jq -r '.') + passes=$(echo "$report" | jq -r '.stats.passes') + failures=$(echo "$report" | jq -r '.stats.failures') + pending=$(echo "$report" | jq -r '.stats.pending') + skipped=$(echo "$report" | jq -r '.stats.skipped') + echo "Skipped=$skipped" >> "$GITHUB_ENV" + echo "Pending=$pending" >> "$GITHUB_ENV" + echo "Passes=$passes" >> "$GITHUB_ENV" + echo "Failures=$failures" >> "$GITHUB_ENV" +} + +# Check argument and call corresponding function +if [ "$1" == "runTests" ]; then + runTests +elif [ "$1" == "getResults" ]; then + getResults +elif [ "$1" == "getArtifactData" ]; then + getArtifactData +elif [ "$1" == "unzipArtifact" ]; then + unzipArtifact +else + echo "Invalid function specified." + exit 1 +fi \ No newline at end of file From 5ea5d4e2f486a7bc3861955cd98323fc2c26cce1 Mon Sep 17 00:00:00 2001 From: HaseenaSainul <41037131+HaseenaSainul@users.noreply.github.com> Date: Wed, 27 Mar 2024 19:05:29 +0530 Subject: [PATCH 06/21] feat: Firebolt CPP SDKs * CPPSDK: manage test added (#189) * CPPSDK: standalone cpp test case added (#190) * CPPTest: CXX compilor target added (#191) * CPPSDK: test event sample test added (#192) * CPPSDK: test event sample test added * CPPSDK: more event test added * CPPSDK: test updates based on interface name changes + disable rpc-only based methods/events (#205) * CPPSDK: test case added for core (#209) * CPPSDK: test case added for core * Manage Test update based on provider integration * Manage Test update based on provider integration * Core test update to add more test case to link with provider sequence * Core/Manage test app updates * CPPSDK: templates added for metrics calls-metrics methods * CPPSDK: test updates * CPPSDK: Core test app updated with discovery.launch and watchNext * CPPSDK: Core test cases added for discovery and capabilities * CPPSDK: Core test cases updated based on openrpc generation support in features/cpp branch * CPPSDK: install command added (#218) * CoreTest: use has_value() check before accessing optional field (#222) * use cpp header for strtok (#228) * CPPSDK: use cpp header for strtok and cleanup (#230) * CPP: update based on merge with next branch (#237) * CPPSDK: updated Core test app to align with next branch merge changes (#238) * CPPSDK: Async test case updates (#249) * Pointed openrpc to cpp branch * Pointed openrpc to cpp branch * fix: Upgrade to latest firebolt-openrpc next build --------- Co-authored-by: Santhosh Ramani Co-authored-by: Shah, Kevin Co-authored-by: Jeremy LaCivita --- package-lock.json | 12 +- package.json | 2 +- src/sdks/core/package.json | 5 +- .../core/src/cpp/sdk/cpptest/CMakeLists.txt | 81 + .../core/src/cpp/sdk/cpptest/CoreSDKTest.cpp | 1415 +++++++++++++++++ .../core/src/cpp/sdk/cpptest/CoreSDKTest.h | 202 +++ src/sdks/core/src/cpp/sdk/cpptest/Main.cpp | 654 ++++++++ src/sdks/core/src/cpp/sdk/cpptest/build.sh | 41 + src/sdks/core/src/cpp/sdk/ctest/main.c | 470 ------ src/sdks/core/src/cpp/sdk/test/CMakeLists.txt | 8 +- .../cpp/sdk/test/CoreSDKTestGeneratedCode.c | 330 ---- .../cpp/sdk/test/CoreSDKTestGeneratedCode.cpp | 91 ++ .../cpp/sdk/test/CoreSDKTestGeneratedCode.h | 35 +- .../cpp/sdk/test/CoreSDKTestStaticCode.cpp | 46 + ...CoreSDKTests.h => CoreSDKTestStaticCode.h} | 9 +- .../core/src/cpp/sdk/test/CoreSDKTests.cpp | 86 - src/sdks/core/src/cpp/sdk/test/Main.c | 57 - src/sdks/core/src/cpp/sdk/test/Main.cpp | 38 + .../cpp/templates/Metrics/src/module_impl.cpp | 96 ++ .../cpp/templates/Metrics/src/module_impl.h | 52 + src/sdks/manage/package.json | 3 + .../src/cpp/sdk/cpptest}/CMakeLists.txt | 11 +- src/sdks/manage/src/cpp/sdk/cpptest/Main.cpp | 398 +++++ .../src/cpp/sdk/cpptest/ManageSDKTest.cpp | 979 ++++++++++++ .../src/cpp/sdk/cpptest/ManageSDKTest.h | 205 +++ .../src/cpp/sdk/cpptest}/build.sh | 0 .../manage/src/cpp/sdk/test/CMakeLists.txt | 87 + src/sdks/manage/src/cpp/sdk/test/Main.cpp | 42 + .../sdk/test/ManageSDKTestGeneratedCode.cpp | 103 ++ .../cpp/sdk/test/ManageSDKTestGeneratedCode.h | 40 + .../cpp/sdk/test/ManageSDKTestStaticCode.cpp | 46 + .../cpp/sdk/test/ManageSDKTestStaticCode.h | 61 + src/sdks/manage/src/cpp/sdk/test/Module.cpp | 21 + .../src/cpp/sdk/test/Module.h} | 19 +- 34 files changed, 4751 insertions(+), 994 deletions(-) create mode 100644 src/sdks/core/src/cpp/sdk/cpptest/CMakeLists.txt create mode 100644 src/sdks/core/src/cpp/sdk/cpptest/CoreSDKTest.cpp create mode 100644 src/sdks/core/src/cpp/sdk/cpptest/CoreSDKTest.h create mode 100644 src/sdks/core/src/cpp/sdk/cpptest/Main.cpp create mode 100755 src/sdks/core/src/cpp/sdk/cpptest/build.sh delete mode 100644 src/sdks/core/src/cpp/sdk/ctest/main.c delete mode 100644 src/sdks/core/src/cpp/sdk/test/CoreSDKTestGeneratedCode.c create mode 100644 src/sdks/core/src/cpp/sdk/test/CoreSDKTestGeneratedCode.cpp create mode 100644 src/sdks/core/src/cpp/sdk/test/CoreSDKTestStaticCode.cpp rename src/sdks/core/src/cpp/sdk/test/{CoreSDKTests.h => CoreSDKTestStaticCode.h} (91%) delete mode 100644 src/sdks/core/src/cpp/sdk/test/CoreSDKTests.cpp delete mode 100644 src/sdks/core/src/cpp/sdk/test/Main.c create mode 100644 src/sdks/core/src/cpp/sdk/test/Main.cpp create mode 100644 src/sdks/core/src/cpp/templates/Metrics/src/module_impl.cpp create mode 100644 src/sdks/core/src/cpp/templates/Metrics/src/module_impl.h rename src/sdks/{core/src/cpp/sdk/ctest => manage/src/cpp/sdk/cpptest}/CMakeLists.txt (91%) create mode 100644 src/sdks/manage/src/cpp/sdk/cpptest/Main.cpp create mode 100644 src/sdks/manage/src/cpp/sdk/cpptest/ManageSDKTest.cpp create mode 100644 src/sdks/manage/src/cpp/sdk/cpptest/ManageSDKTest.h rename src/sdks/{core/src/cpp/sdk/ctest => manage/src/cpp/sdk/cpptest}/build.sh (100%) create mode 100644 src/sdks/manage/src/cpp/sdk/test/CMakeLists.txt create mode 100644 src/sdks/manage/src/cpp/sdk/test/Main.cpp create mode 100644 src/sdks/manage/src/cpp/sdk/test/ManageSDKTestGeneratedCode.cpp create mode 100644 src/sdks/manage/src/cpp/sdk/test/ManageSDKTestGeneratedCode.h create mode 100644 src/sdks/manage/src/cpp/sdk/test/ManageSDKTestStaticCode.cpp create mode 100644 src/sdks/manage/src/cpp/sdk/test/ManageSDKTestStaticCode.h create mode 100644 src/sdks/manage/src/cpp/sdk/test/Module.cpp rename src/sdks/{core/src/cpp/sdk/test/CoreSDKCTests.h => manage/src/cpp/sdk/test/Module.h} (73%) diff --git a/package-lock.json b/package-lock.json index 69cadf7ef..90a6b24c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "devDependencies": { "@commitlint/cli": "^17.0.3", "@commitlint/config-conventional": "^17.0.3", - "@firebolt-js/openrpc": "2.3.0", + "@firebolt-js/openrpc": "3.0.0-next.1", "@firebolt-js/schemas": "2.0.0", "@saithodev/semantic-release-backmerge": "^3.2.0", "@semantic-release/changelog": "^6.0.1", @@ -1070,9 +1070,9 @@ "link": true }, "node_modules/@firebolt-js/openrpc": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@firebolt-js/openrpc/-/openrpc-2.3.0.tgz", - "integrity": "sha512-JlhFy4r1dkIUHESVi72LaYgbdjP3hG04OfaLEtqLx5sLYDOQ8Y+KHiD2h81u8fvRgLEc6HiX5HgCFdapoUg8Wg==", + "version": "3.0.0-next.1", + "resolved": "https://registry.npmjs.org/@firebolt-js/openrpc/-/openrpc-3.0.0-next.1.tgz", + "integrity": "sha512-bFNXTePgmYPqHqMiGX2aWuTBY0KrZtwd7TVd58tg8fKpIgCjaBLf/8MAXqTd6oWYKU9o86zi4De3ygmYQCuJYg==", "dev": true, "dependencies": { "ajv": "^8.3.0", @@ -16951,7 +16951,7 @@ }, "src/sdks/core": { "name": "@firebolt-js/sdk", - "version": "1.1.0-next.3", + "version": "1.1.1-next.1", "license": "Apache-2.0", "devDependencies": { "jest": "^28.1.0", @@ -16962,7 +16962,7 @@ }, "src/sdks/manage": { "name": "@firebolt-js/manage-sdk", - "version": "1.1.0-next.3", + "version": "1.1.1-next.1", "license": "Apache-2.0", "devDependencies": { "jest": "^28.1.0", diff --git a/package.json b/package.json index 59899531a..647bdb775 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "devDependencies": { "@commitlint/cli": "^17.0.3", "@commitlint/config-conventional": "^17.0.3", - "@firebolt-js/openrpc": "2.3.0", + "@firebolt-js/openrpc": "3.0.0-next.1", "@firebolt-js/schemas": "2.0.0", "@saithodev/semantic-release-backmerge": "^3.2.0", "@semantic-release/changelog": "^6.0.1", diff --git a/src/sdks/core/package.json b/src/sdks/core/package.json index 16a8bd6de..5dfe709fe 100644 --- a/src/sdks/core/package.json +++ b/src/sdks/core/package.json @@ -12,6 +12,9 @@ "validate": "npx firebolt-openrpc validate --input ./dist/firebolt-core-open-rpc.json", "sdk": "npx firebolt-openrpc sdk --input ./dist/firebolt-core-open-rpc.json --template ./src/js --output ./build/javascript/src --static-module Platform", "native": "npx firebolt-openrpc sdk --input ./dist/firebolt-core-open-rpc.json --template ./src/cpp --output ./build/c/src --static-module Platform --language ../../../node_modules/@firebolt-js/openrpc/languages/c", + "cpp": "npm run cpp:compile && npm run cpp:install", + "cpp:compile": "npx firebolt-openrpc sdk --input ./dist/firebolt-core-open-rpc.json --template ./src/cpp --output ./build/cpp/src --static-module Platform --language ../../../node_modules/@firebolt-js/openrpc/languages/cpp", + "cpp:install":"./build/cpp/src/scripts/install.sh -i ./build/cpp/src -s ./build/cpp/src/ -m core", "compile": "cd ../../.. && npm run compile", "slice": "npx firebolt-openrpc slice -i ../../../dist/firebolt-open-rpc.json --sdk ./sdk.config.json -o ./dist/firebolt-core-open-rpc.json", "docs": "npx firebolt-openrpc docs --input ./dist/firebolt-core-open-rpc.json --output build/docs/markdown --as-path", @@ -50,4 +53,4 @@ "sdk" ], "license": "Apache-2.0" -} \ No newline at end of file +} diff --git a/src/sdks/core/src/cpp/sdk/cpptest/CMakeLists.txt b/src/sdks/core/src/cpp/sdk/cpptest/CMakeLists.txt new file mode 100644 index 000000000..c542cf6cc --- /dev/null +++ b/src/sdks/core/src/cpp/sdk/cpptest/CMakeLists.txt @@ -0,0 +1,81 @@ +# Copyright 2023 Comcast Cable Communications Management, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.3) + +project(FireboltCoreSDKTests) + +if (CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${SYSROOT_PATH}/usr" CACHE INTERNAL "" FORCE) + set(CMAKE_PREFIX_PATH ${SYSROOT_PATH}/usr/lib/cmake CACHE INTERNAL "" FORCE) +endif() + +list(APPEND CMAKE_MODULE_PATH + "${SYSROOT_PATH}/usr/lib/cmake" + "${SYSROOT_PATH}/tools/cmake") +message("FIREBOLT_PATH inside cmake " ${FIREBOLT_PATH}) +if (FIREBOLT_PATH) + set(CMAKE_FIREBOLT_PATH + "${FIREBOLT_PATH}/usr/lib/cmake/Firebolt" + "${FIREBOLT_PATH}/usr/lib/cmake/FireboltSDK") + list(APPEND CMAKE_PREFIX_PATH ${CMAKE_FIREBOLT_PATH}) + list(APPEND CMAKE_MODULE_PATH ${CMAKE_FIREBOLT_PATH}) +else () + set(FIREBOLT_PATH "${SYSROOT_PATH}" CACHE INTERNAL "" FORCE) +endif () + +find_package(WPEFramework CONFIG REQUIRED) +find_package(${NAMESPACE}Core CONFIG REQUIRED) +find_package(Firebolt CONFIG REQUIRED) +find_package(${FIREBOLT_NAMESPACE}SDK CONFIG REQUIRED) + +set(TESTAPP TestFireboltCore) + +message("Setup ${TESTAPP}") + +add_executable(${TESTAPP} CoreSDKTest.cpp Main.cpp) + +target_link_libraries(${TESTAPP} + PRIVATE + ${NAMESPACE}Core::${NAMESPACE}Core + ${FIREBOLT_NAMESPACE}SDK::${FIREBOLT_NAMESPACE}SDK +) + +target_include_directories(${TESTAPP} + PRIVATE + $ + $ + $ +) + +if (POLYMORPHICS_REDUCER_METHODS) + target_compile_definitions(${TESTAPP} + PUBLIC + POLYMORPHICS_REDUCER_METHODS=1) +endif() + +set_target_properties(${TESTAPP} PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED YES +) + +add_custom_command( + TARGET ${TESTAPP} + POST_BUILD + COMMENT "=================== Installing TestApp ======================" + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/${FIREBOLT_NAMESPACE}/usr/bin + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/${TESTAPP} ${CMAKE_BINARY_DIR}/${FIREBOLT_NAMESPACE}/usr/bin +) diff --git a/src/sdks/core/src/cpp/sdk/cpptest/CoreSDKTest.cpp b/src/sdks/core/src/cpp/sdk/cpptest/CoreSDKTest.cpp new file mode 100644 index 000000000..065cfea6b --- /dev/null +++ b/src/sdks/core/src/cpp/sdk/cpptest/CoreSDKTest.cpp @@ -0,0 +1,1415 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "CoreSDKTest.h" + +using namespace std; +bool CoreSDKTest::_connected; +CoreSDKTest::OnPolicyChangedNotification CoreSDKTest::_policyChangedNotification; +CoreSDKTest::OnDeviceNameChangedNotification CoreSDKTest::_deviceNameChangedNotification; +CoreSDKTest::OnAudioChangedNotification CoreSDKTest::_audioChangedNotification; +CoreSDKTest::OnScreenResolutionChangedNotification CoreSDKTest::_screenResolutionChangedNotification; +CoreSDKTest::OnClosedCaptionsSettingsChangedNotification CoreSDKTest::_closedCaptionsSettingsChangedNotification; +CoreSDKTest::OnPreferredAudioLanguagesChangedNotification CoreSDKTest::_preferredAudioLanguagesChangedNotification; +CoreSDKTest::OnBackgroundNotification CoreSDKTest::_backgroundNotification; +CoreSDKTest::OnForegroundNotification CoreSDKTest::_foregroundNotification; +CoreSDKTest::OnFriendlyNameChangedNotification CoreSDKTest::_friendlyNameChangedNotification; +CoreSDKTest::OnAvailableNotification CoreSDKTest::_availableNotification; + +CoreSDKTest::OnNavigateToHomeIntentNotification CoreSDKTest::_navigateToHomeIntentNotification; +CoreSDKTest::OnNavigateToEntityIntentNotification CoreSDKTest::_navigateToEntityIntentNotification; +CoreSDKTest::OnNavigateToTuneIntentNotification CoreSDKTest::_navigateToTuneIntentNotification; + +CoreSDKTest::KeyboardEmailAsyncResponse CoreSDKTest::_keyboardEmailAsyncResponse; +CoreSDKTest::KeyboardPasswordAsyncResponse CoreSDKTest::_keyboardPasswordAsyncResponse; +CoreSDKTest::KeyboardStandardAsyncResponse CoreSDKTest::_keyboardStandardAsyncResponse; + +void CoreSDKTest::ConnectionChanged(const bool connected, const Firebolt::Error error) +{ + cout << "Change in connection: connected: " << connected << " error: " << static_cast(error) << endl; + _connected = connected; +} + +void CoreSDKTest::CreateFireboltInstance(const std::string& url) +{ + const std::string config = "{\ + \"waitTime\": 1000,\ + \"logLevel\": \"Info\",\ + \"workerPool\":{\ + \"queueSize\": 8,\ + \"threadCount\": 3\ + },\ + \"wsUrl\": " + url + "}"; + + _connected = false; + Firebolt::IFireboltAccessor::Instance().Initialize(config); + Firebolt::IFireboltAccessor::Instance().Connect(ConnectionChanged); +} + +void CoreSDKTest::DestroyFireboltInstance() +{ + Firebolt::IFireboltAccessor::Instance().Disconnect(); + Firebolt::IFireboltAccessor::Instance().Deinitialize(); + Firebolt::IFireboltAccessor::Instance().Dispose(); +} + +bool CoreSDKTest::WaitOnConnectionReady() +{ + uint32_t waiting = 10000; + static constexpr uint32_t SLEEPSLOT_TIME = 100; + + // Right, a wait till connection is closed is requested.. + while ((waiting > 0) && (_connected == false)) { + + uint32_t sleepSlot = (waiting > SLEEPSLOT_TIME ? SLEEPSLOT_TIME : waiting); + // Right, lets sleep in slices of 100 ms + usleep(sleepSlot); + waiting -= sleepSlot; + } + return _connected; +} + +void CoreSDKTest::GetAccountId() +{ + Firebolt::Error error = Firebolt::Error::None; + const std::string id = Firebolt::IFireboltAccessor::Instance().AccountInterface().id(&error); + if (error == Firebolt::Error::None) { + cout << "Get Account Id = " << id.c_str() << endl; + } else { + cout << "Get Account Id status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::GetAccountUid() +{ + Firebolt::Error error = Firebolt::Error::None; + const std::string uid = Firebolt::IFireboltAccessor::Instance().AccountInterface().uid(&error); + if (error == Firebolt::Error::None) { + cout << "Get Account Uid = " << uid.c_str() << endl; + } else { + cout << "Get Account Uid status = " << static_cast(error) << endl; + } +} + +template +using EnumMap = std::unordered_map; +template +inline const string& ConvertFromEnum(EnumMap enumMap, T type) +{ + return enumMap[type]; +} +template +inline const T ConvertToEnum(EnumMap enumMap, const string& str) +{ + T value; + for (auto element: enumMap) { + if (element.second == str) { + value = element.first; + break; + } + } + return value; +} + +EnumMap skipRestrictionMap = { + { Firebolt::Advertising::SkipRestriction::NONE, "none" }, + { Firebolt::Advertising::SkipRestriction::ADS_UNWATCHED, "adsUnwatched" }, + { Firebolt::Advertising::SkipRestriction::ADS_ALL, "adsAll" }, + { Firebolt::Advertising::SkipRestriction::ALL, "all" } +}; + +void PrintAdvertisingPolicy(const Firebolt::Advertising::AdPolicy& policy) +{ + if (policy.skipRestriction.has_value()) { + cout << "\tskipRestriction : " << ConvertFromEnum(skipRestrictionMap, policy.skipRestriction.value()) << endl; + } + if (policy.limitAdTracking.has_value()) { + cout << "\tlimitAdTracking : " << policy.limitAdTracking.value() << endl; + } +} +void CoreSDKTest::GetAdvertisingPolicy() +{ + Firebolt::Error error = Firebolt::Error::None; + const Firebolt::Advertising::AdPolicy policy = Firebolt::IFireboltAccessor::Instance().AdvertisingInterface().policy(&error); + if (error == Firebolt::Error::None) { + cout << "Get Advertising policy --> " << endl; + PrintAdvertisingPolicy(policy); + } else { + cout << "Get Advertising policy status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::OnPolicyChangedNotification::onPolicyChanged( const Firebolt::Advertising::AdPolicy& policy ) +{ + cout << "New policy --> " << endl; + PrintAdvertisingPolicy(policy); +} +void CoreSDKTest::SubscribeAdvertisingPolicyChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().AdvertisingInterface().subscribe(_policyChangedNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Subscribe Advertising PolicyChange is success" << endl; + } else { + cout << "Subscribe Advertising PolicyChange status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::UnsubscribeAdvertisingPolicyChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().AdvertisingInterface().unsubscribe(_policyChangedNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Unsubscribe Advertising PolicyChange is success" << endl; + } else { + cout << "Unsubscribe Advertising PolicyChange status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::BuildAdvertisingConfiguration() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::Advertising::AdConfigurationOptions options; + options.coppa = true; + options.environment = Firebolt::Advertising::AdConfigurationOptionsEnvironment::TEST; + options.authenticationEntity = "MVPD"; + + Firebolt::Advertising::AdFrameworkConfig adFrameworkConfig = Firebolt::IFireboltAccessor::Instance().AdvertisingInterface().config(options, &error); + if (error == Firebolt::Error::None) { + cout << "Build AdvertisingConfiguration is success, adFrameworkConfig : " << adFrameworkConfig << endl; + } else { + cout << "Build AdvertisingConfiguration status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::GetAdvertisingDeviceAttributes() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::Advertising::DeviceAttributes deviceAttributes = Firebolt::IFireboltAccessor::Instance().AdvertisingInterface().deviceAttributes(&error); + if (error == Firebolt::Error::None) { + cout << "Get Advertising DeviceAttributes is success, deviceAttributes : " << deviceAttributes << endl; + } else { + cout << "Get Advertising DeviceAttributes status = " << static_cast(error) << endl; + } +} + +void CoreSDKTest::GetDeviceName() +{ + Firebolt::Error error = Firebolt::Error::None; + const std::string name = Firebolt::IFireboltAccessor::Instance().DeviceInterface().name(&error); + + if (error == Firebolt::Error::None) { + cout << "Get Device Name = " << name.c_str() << endl; + } else { + cout << "Get Device Name status = " << static_cast(error) << endl; + } +} + +void CoreSDKTest::OnDeviceNameChangedNotification::onDeviceNameChanged( const std::string& name) +{ + cout << "Name changed, new name --> " << name << endl; +} + +void CoreSDKTest::SubscribeDeviceNameChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().DeviceInterface().subscribe(_deviceNameChangedNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Subscribe Device NameChange is success" << endl; + } else { + cout << "Subscribe Device NameChange status = " << static_cast(error) << endl; + } +} + +void CoreSDKTest::UnsubscribeDeviceNameChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().DeviceInterface().unsubscribe(_deviceNameChangedNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Unsubscribe Device NameChange is success" << endl; + } else { + cout << "Unsubscribe Device NameChange status = " << static_cast(error) << endl; + } +} + +void CoreSDKTest::GetDeviceModel() +{ + Firebolt::Error error = Firebolt::Error::None; + const std::string model = Firebolt::IFireboltAccessor::Instance().DeviceInterface().model(&error); + + if (error == Firebolt::Error::None) { + cout << "Get Device Model = " << model.c_str() << endl; + } else { + cout << "Get Device Model status = " << static_cast(error) << endl; + } +} + +void CoreSDKTest::GetDeviceSku() +{ + Firebolt::Error error = Firebolt::Error::None; + const std::string sku = Firebolt::IFireboltAccessor::Instance().DeviceInterface().sku(&error); + + if (error == Firebolt::Error::None) { + cout << "Get Device Sku = " << sku.c_str() << endl; + } else { + cout << "Get Device Sku status = " << static_cast(error) << endl; + } +} + +void PrintDeviceAudioProfiles( const Firebolt::Device::AudioProfiles& audioProfiles ) +{ + cout << "Get Device AudioProfiles :-> " << endl; + for (auto& item : audioProfiles) { + cout << "Profile: " << static_cast(item.first) << " status: " << item.second << endl; + } +} +void CoreSDKTest::GetDeviceAudio() +{ + Firebolt::Error error = Firebolt::Error::None; + const Firebolt::Device::AudioProfiles audioProfiles = Firebolt::IFireboltAccessor::Instance().DeviceInterface().audio(&error); + if (error == Firebolt::Error::None) { + PrintDeviceAudioProfiles(audioProfiles); + } else { + cout << "Get Device AudioProfiles status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::OnAudioChangedNotification::onAudioChanged( const Firebolt::Device::AudioProfiles& audioProfiles ) +{ + cout << "onAudioChanged event " << endl; + PrintDeviceAudioProfiles(audioProfiles); +} +void CoreSDKTest::SubscribeDeviceAudioChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().DeviceInterface().subscribe(_audioChangedNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Subscribe Device Audio Change is success" << endl; + } else { + cout << "Subscribe Device Audio Change status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::UnsubscribeDeviceAudioChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().DeviceInterface().unsubscribe(_audioChangedNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Unsubscribe Device Audio Change is success" << endl; + } else { + cout << "Unsubscribe Device Audio Change status = " << static_cast(error) << endl; + } +} + +void PrintDeviceScreenResolution( const Firebolt::Device::Resolution& resolution ) +{ + cout << "Get Device ScreenResolution :-> " << endl; + cout << resolution.first << " X " << resolution.second << endl; +} +void CoreSDKTest::GetDeviceScreenResolution() +{ + Firebolt::Error error = Firebolt::Error::None; + const Firebolt::Device::Resolution resolution = Firebolt::IFireboltAccessor::Instance().DeviceInterface().screenResolution(&error); + if (error == Firebolt::Error::None) { + PrintDeviceScreenResolution(resolution); + } else { + cout << "Get Device ScreenResolution status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::OnScreenResolutionChangedNotification::onScreenResolutionChanged( const Firebolt::Device::Resolution& resolution ) +{ + cout << "onScreenResolutionChanged event " << endl; + PrintDeviceScreenResolution(resolution); +} +void CoreSDKTest::SubscribeDeviceScreenResolutionChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().DeviceInterface().subscribe(_screenResolutionChangedNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Subscribe Device ScreenResolution Change is success" << endl; + } else { + cout << "Subscribe Device ScreenResolution Change status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::UnsubscribeDeviceScreenResolutionChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().DeviceInterface().unsubscribe(_screenResolutionChangedNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Unsubscribe Device ScreenResolution Change is success" << endl; + } else { + cout << "Unsubscribe Device ScreenResolution Change status = " << static_cast(error) << endl; + } +} + +void PrintClosedCaptionsSettings( const Firebolt::Accessibility::ClosedCaptionsSettings& closedCaptionsSettings) +{ + cout << "Get Accessibility ClosedCaptionsSettings :-> " << endl; + cout << "ClosedCaptionsSettings::Enabled : " << closedCaptionsSettings.enabled << endl; + if (closedCaptionsSettings.styles.fontFamily.has_value()) { + cout << "ClosedCaptionsSettings::Styles::FontFamily : " << static_cast(closedCaptionsSettings.styles.fontFamily.value()) << endl; + } + if (closedCaptionsSettings.styles.fontSize.has_value()) { + cout << "ClosedCaptionsSettings::Styles::FontSize : " << setprecision(3) << closedCaptionsSettings.styles.fontSize.value() << endl; + } + if (closedCaptionsSettings.styles.fontColor.has_value()) { + cout << "ClosedCaptionsSettings::Styles::FontColor : " << closedCaptionsSettings.styles.fontColor.value() << endl; + } + if (closedCaptionsSettings.styles.fontEdge.has_value()) { + cout << "ClosedCaptionsSettings::Styles::FontEdge : " << static_cast(closedCaptionsSettings.styles.fontEdge.value()) << endl; + } + if (closedCaptionsSettings.styles.fontEdgeColor.has_value()) { + cout << "ClosedCaptionsSettings::Styles::FontEdgeColor : " << closedCaptionsSettings.styles.fontEdgeColor.value() << endl; + } + if (closedCaptionsSettings.styles.fontOpacity.has_value()) { + cout << "ClosedCaptionsSettings::Styles::FontOpacity : " << closedCaptionsSettings.styles.fontOpacity.value() << endl; + } + if (closedCaptionsSettings.styles.backgroundColor.has_value()) { + cout << "ClosedCaptionsSettings::Styles::BackgroundColor : " << closedCaptionsSettings.styles.backgroundColor.value() << endl; + } + if (closedCaptionsSettings.styles.backgroundOpacity.has_value()) { + cout << "ClosedCaptionsSettings::Styles::BackgroundOpacity : " << closedCaptionsSettings.styles.backgroundOpacity.value() << endl; + } + if (closedCaptionsSettings.styles.textAlign.has_value()) { + cout << "ClosedCaptionsSettings::Styles::TextAlign : " << closedCaptionsSettings.styles.textAlign.value() << endl; + } + if (closedCaptionsSettings.styles.textAlignVertical.has_value()) { + cout << "ClosedCaptionsSettings::Styles::TextAlignVertical : " << closedCaptionsSettings.styles.textAlignVertical.value() << endl; + } + if (closedCaptionsSettings.styles.windowColor.has_value()) { + cout << "ClosedCaptionsSettings::Styles::WindowColor : " << closedCaptionsSettings.styles.windowColor.value() << endl; + } + if (closedCaptionsSettings.styles.windowOpacity.has_value()) { + cout << "ClosedCaptionsSettings::Styles::WindowOpacity : " << closedCaptionsSettings.styles.windowOpacity.value() << endl; + } + if (closedCaptionsSettings.preferredLanguages.has_value()) { + cout << "ClosedCaptionsSettings::PreferredLanguages :"; + for (auto index: closedCaptionsSettings.preferredLanguages.value()) { + cout << " " << index; + } + cout << endl; + } +} +void CoreSDKTest::GetAccessibilityClosedCaptionsSettings() +{ + Firebolt::Error error = Firebolt::Error::None; + const Firebolt::Accessibility::ClosedCaptionsSettings closedCaptionsSettings = Firebolt::IFireboltAccessor::Instance().AccessibilityInterface().closedCaptionsSettings(&error); + if (error == Firebolt::Error::None) { + PrintClosedCaptionsSettings(closedCaptionsSettings); + } else { + cout << "Get Accessibility ClosedCaptionsSettings status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::OnClosedCaptionsSettingsChangedNotification::onClosedCaptionsSettingsChanged( const Firebolt::Accessibility::ClosedCaptionsSettings& closedCaptionsSettings ) +{ + cout << "ClosedCaptionsSettingsChanged event " << endl; + PrintClosedCaptionsSettings(closedCaptionsSettings); +} +void CoreSDKTest::SubscribeAccessibilityClosedCaptionsSettingsChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().AccessibilityInterface().subscribe(_closedCaptionsSettingsChangedNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Subscribe Accessibilty ClosedCaptionSettings Change is success" << endl; + } else { + cout << "Subscribe Accessibilty ClosedCaptionSettings Change status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::UnsubscribeAccessibilityClosedCaptionsSettingsChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().AccessibilityInterface().unsubscribe(_closedCaptionsSettingsChangedNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Unsubscribe Accessibilty ClosedCaptionSettings Change is success" << endl; + } else { + cout << "Unsubscribe Accessibilty ClosedCaptionSettings Change status = " << static_cast(error) << endl; + } +} + +void CoreSDKTest::GetLocalizationAdditionalInfo() +{ + Firebolt::Error error = Firebolt::Error::None; + const Firebolt::Localization::Info info = Firebolt::IFireboltAccessor::Instance().LocalizationInterface().additionalInfo(&error); + + if (error == Firebolt::Error::None) { + cout << "Get Localization AdditionalInfo : " << endl; + for (auto element: info) { + cout << "key : " << element.first << ", value : " << element.second << endl; + } + } else { + cout << "Get Localization AdditionalInfo status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::GetLocalizationLatlon() +{ + Firebolt::Error error = Firebolt::Error::None; + const Firebolt::Localization::LatLon latlon = Firebolt::IFireboltAccessor::Instance().LocalizationInterface().latlon(&error); + + if (error == Firebolt::Error::None) { + cout << "Get Localization LatLon pair value : " << endl; + cout << "key : " << latlon.first << ", value : " << latlon.second << endl; + } else { + cout << "Get Localization LatLon status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::GetLocalizationPreferredAudioLanguages() +{ + Firebolt::Error error = Firebolt::Error::None; + const std::vector languages = Firebolt::IFireboltAccessor::Instance().LocalizationInterface().preferredAudioLanguages(&error); + + if (error == Firebolt::Error::None) { + cout << "Get Localization PreferredAudioLanguages : " << endl; + for (auto language: languages) { + cout << "----- > " < " << endl; + for (auto language : languages) { + cout << " -> " << language << endl; + } +} +void CoreSDKTest::SubscribeLocalizationPreferredAudioLanguagesChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().LocalizationInterface().subscribe(_preferredAudioLanguagesChangedNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Subscribe Localization PreferredAudioLanguagesChange is success" << endl; + } else { + cout << "Subscribe Localization PreferredAudioLanguagesChange status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::UnsubscribeLocalizationPreferredAudioLanguagesChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().LocalizationInterface().unsubscribe(_preferredAudioLanguagesChangedNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Unsubscribe Localization PreferredAudioLanguagesChange is success" << endl; + } else { + cout << "Unsubscribe Localization PreferredAudioLanguagesChange status = " << static_cast(error) << endl; + } +} + +void CoreSDKTest::KeyboardStandardAsyncResponse::response(const std::string& response, Firebolt::Error* error) +{ + if (error && (*error != Firebolt::Error::None)) { + cout << "Error with keyboard standard response, error = " << static_cast(*error) << endl; + } else { + cout << "Keyboard standard response: " << response << endl; + } +} +void CoreSDKTest::InvokeKeyboardStandard() +{ + Firebolt::Error error = Firebolt::Error::None; + std::string message = "Enter the name you'd like to associate with this device"; + Firebolt::IFireboltAccessor::Instance().KeyboardInterface().requestStandard(message, _keyboardStandardAsyncResponse, &error); + if (error != Firebolt::Error::None) { + cout << "Error with keyboard standard request method invoke, error = " << static_cast(error) << endl; + } +} +void CoreSDKTest::AbortKeyboardStandard() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().KeyboardInterface().abortStandard(_keyboardStandardAsyncResponse, &error); + if (error != Firebolt::Error::None) { + cout << "Error with keyboard standard abort method invoke, error = " << static_cast(error) << endl; + } +} +void CoreSDKTest::KeyboardPasswordAsyncResponse::response(const std::string& response, Firebolt::Error* error) +{ + if (error && (*error != Firebolt::Error::None)) { + cout << "Error with keyboard password response, error = " << static_cast(*error) << endl; + } else { + cout << "Keyboard password response: " << response << endl; + } +} +void CoreSDKTest::InvokeKeyboardPassword() +{ + Firebolt::Error error = Firebolt::Error::None; + std::string message = "Enter the password to associate with this device"; + Firebolt::IFireboltAccessor::Instance().KeyboardInterface().requestPassword(message, _keyboardPasswordAsyncResponse, &error); + if (error != Firebolt::Error::None) { + cout << "Error with keyboard password request method invoke, error = " << static_cast(error) << endl; + } +} +void CoreSDKTest::AbortKeyboardPassword() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().KeyboardInterface().abortPassword(_keyboardPasswordAsyncResponse, &error); + if (error != Firebolt::Error::None) { + cout << "Error with keyboard password abort method invoke, error = " << static_cast(error) << endl; + } +} +void CoreSDKTest::KeyboardEmailAsyncResponse::response(const std::string& response, Firebolt::Error* error) +{ + if (error && (*error != Firebolt::Error::None)) { + cout << "Error with email password response, error = " << static_cast(*error) << endl; + } else { + cout << "Keyboard email response: " << response << endl; + } +} +void CoreSDKTest::InvokeKeyboardEmail() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::Keyboard::EmailUsage type = Firebolt::Keyboard::EmailUsage::SIGN_IN; + std::string message = "Enter your email to sign into this app/device"; + Firebolt::IFireboltAccessor::Instance().KeyboardInterface().requestEmail(type, message, _keyboardEmailAsyncResponse, &error); + if (error != Firebolt::Error::None) { + cout << "Error with keyboard email requeset method invoke, error = " << static_cast(error) << endl; + } +} +void CoreSDKTest::AbortKeyboardEmail() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().KeyboardInterface().abortEmail(_keyboardEmailAsyncResponse, &error); + if (error != Firebolt::Error::None) { + cout << "Error with keyboard email abort method invoke, error = " << static_cast(error) << endl; + } +} + +void CoreSDKTest::VerifyProfileApproveContentRating() +{ + Firebolt::Error error = Firebolt::Error::None; + bool allow = Firebolt::IFireboltAccessor::Instance().ProfileInterface().approveContentRating(&error); + if (error == Firebolt::Error::None) { + cout << "Verify Profile ApproveContentRating is success value : " << allow << endl; + } else { + cout << "Verify Profile ApproveContentRating status : " << static_cast(error) << endl; + } +} +void CoreSDKTest::VerifyProfileApprovePurchase() +{ + Firebolt::Error error = Firebolt::Error::None; + bool allow = Firebolt::IFireboltAccessor::Instance().ProfileInterface().approvePurchase(&error); + if (error == Firebolt::Error::None) { + cout << "Verify Profile ApprovePurchase is success value : " << allow << endl; + } else { + cout << "Verify Profile ApprovePurchase status : " << static_cast(error) << endl; + } +} +void CoreSDKTest::GetProfileFlags() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::Types::FlatMap flatMap = Firebolt::IFireboltAccessor::Instance().ProfileInterface().flags(&error); + if (error == Firebolt::Error::None) { + cout << "Get Profile flags is success value : " << endl; + for (const auto& item : flatMap) { + cout << "\t" << item.first << " : " << item.second << endl; + } + } else { + cout << "Get Profile flags status : " << static_cast(error) << endl; + } +} + +void CoreSDKTest::LifecycleClose() +{ + Firebolt::Error error = Firebolt::Error::None; + cout << "Enter close reason remote button(0), user exit(1), done(2) or error(3)" << endl; + int32_t reason; + cin >> reason; + Firebolt::IFireboltAccessor::Instance().LifecycleInterface().close(static_cast(reason), &error); + if (error == Firebolt::Error::None) { + cout << "Lifecycle close is success" << endl; + } else { + cout << "Lifecycle close status = " << static_cast(error) << endl; + } +} +EnumMap lifecycleStateMap = { + { Firebolt::Lifecycle::LifecycleState::INITIALIZING, "initializing" }, + { Firebolt::Lifecycle::LifecycleState::INACTIVE, "inactive" }, + { Firebolt::Lifecycle::LifecycleState::FOREGROUND, "foreground" }, + { Firebolt::Lifecycle::LifecycleState::BACKGROUND, "background" }, + { Firebolt::Lifecycle::LifecycleState::UNLOADING, "unloading" }, + { Firebolt::Lifecycle::LifecycleState::SUSPENDED, "suspended" } +}; +EnumMap lifecycleEventSourceMap = { + { Firebolt::Lifecycle::LifecycleEventSource::VOICE, "voice" }, + { Firebolt::Lifecycle::LifecycleEventSource::REMOTE, "remote" } +}; +void CoreSDKTest::OnBackgroundNotification::onBackground( const Firebolt::Lifecycle::LifecycleEvent& lifecycleEvent) +{ + cout <<"onBackground event is triggered" << endl; + cout <<"\tstate: " << ConvertFromEnum(lifecycleStateMap, lifecycleEvent.state) << endl; + cout <<"\tprevious: " << ConvertFromEnum(lifecycleStateMap, lifecycleEvent.previous) << endl; + if (lifecycleEvent.source.has_value()) { + cout <<"\tsource: " << ConvertFromEnum(lifecycleEventSourceMap, lifecycleEvent.source.value()) << endl; + } +} +void CoreSDKTest::SubscribeLifecycleBackgroundNotification() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().LifecycleInterface().subscribe(_backgroundNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Subscribe Lifecycle BackgroundNotification is success" << endl; + } else { + cout << "Subscribe Lifecycle BackgroundNotification status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::UnsubscribeLifecycleBackgroundNotification() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().LifecycleInterface().unsubscribe(_backgroundNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Unsubscribe Lifecycle BackgroundNotification is success" << endl; + } else { + cout << "Unsubscribe Lifecycle BackgroundNotification status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::OnForegroundNotification::onForeground( const Firebolt::Lifecycle::LifecycleEvent& lifecycleEvent) +{ + cout <<"onForeground event is triggered" << endl; + cout <<"\tstate: " << ConvertFromEnum(lifecycleStateMap, lifecycleEvent.state) << endl; + cout <<"\tprevious: " << ConvertFromEnum(lifecycleStateMap, lifecycleEvent.previous) << endl; + if (lifecycleEvent.source.has_value()) { + cout <<"\tsource: " << ConvertFromEnum(lifecycleEventSourceMap, lifecycleEvent.source.value()) << endl; + } +} +void CoreSDKTest::SubscribeLifecycleForegroundNotification() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().LifecycleInterface().subscribe(_foregroundNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Subscribe Lifecycle ForegroundNotification is success" << endl; + } else { + cout << "Subscribe Lifecycle ForegroundNotification status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::UnsubscribeLifecycleForegroundNotification() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().LifecycleInterface().unsubscribe(_foregroundNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Unsubscribe Lifecycle ForegroundNotification is success" << endl; + } else { + cout << "Unsubscribe Lifecycle ForegroundNotification status = " << static_cast(error) << endl; + } +} + +void CoreSDKTest::GetAuthenticationDevice() +{ + Firebolt::Error error = Firebolt::Error::None; + const std::string device = Firebolt::IFireboltAccessor::Instance().AuthenticationInterface().device(&error); + + if (error == Firebolt::Error::None) { + cout << "Get Authentication of Device = " << device.c_str() << endl; + } else { + cout << "Get Authentication of Device status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::GetAuthenticationSession() +{ + Firebolt::Error error = Firebolt::Error::None; + const std::string session = Firebolt::IFireboltAccessor::Instance().AuthenticationInterface().device(&error); + + if (error == Firebolt::Error::None) { + cout << "Get Authentication of Session = " << session.c_str() << endl; + } else { + cout << "Get Authentication of Session status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::GetAuthenticationRoot() +{ + Firebolt::Error error = Firebolt::Error::None; + const std::string root = Firebolt::IFireboltAccessor::Instance().AuthenticationInterface().device(&error); + + if (error == Firebolt::Error::None) { + cout << "Get Authentication of Root = " << root.c_str() << endl; + } else { + cout << "Get Authentication of Root status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::GetAuthenticationToken() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::Authentication::TokenType type = Firebolt::Authentication::TokenType::DEVICE; + std::optional options; + + const Firebolt::Authentication::Token token = Firebolt::IFireboltAccessor::Instance().AuthenticationInterface().token(type, options, &error); + + if (error == Firebolt::Error::None) { + cout << "Get Authentication of token : " << endl; + cout << "\tvalue: " << token.value.c_str() << endl; + if (token.expires.has_value()) { + cout << "\texpires : " << token.expires.value().c_str() << endl; + } + if (token.type.has_value()) { + cout << "\ttype : " << token.type.value().c_str() << endl; + } + } else { + cout << "Get Authentication of Device status = " << static_cast(error) << endl; + } +} + +EnumMap denyReasonMap = { + { Firebolt::Capabilities::DenyReason::UNPERMITTED, "unpermitted" }, + { Firebolt::Capabilities::DenyReason::UNSUPPORTED, "unsupported" }, + { Firebolt::Capabilities::DenyReason::DISABLED, "disbaled" }, + { Firebolt::Capabilities::DenyReason::UNAVAILABLE, "unavailable" }, + { Firebolt::Capabilities::DenyReason::GRANT_DENIED, "grant_denied" }, + { Firebolt::Capabilities::DenyReason::UNGRANTED, "ungranted" } +}; + +static void PrintCapabilityInfo(const Firebolt::Capabilities::CapabilityInfo& info) +{ + if (info.capability.has_value()) { + cout << "\tcapability : " << info.capability.value() << endl; + } + cout << "\tsupported : " << (info.supported ? "true" : "false") << endl; + cout << "\tavailable : " << (info.available ? "true" : "false") << endl; + if (info.use.permitted.has_value() || info.use.granted.has_value()) { + if (info.use.permitted.has_value()) { + cout << "\tuse.permitted : " << (info.use.permitted.value() ? "true" : "false") << endl; + } + if (info.use.granted.has_value()) { + cout << "\tuse.granted : " << (info.use.granted.value() ? "true" : "false") << endl; + } + } + if (info.manage.permitted.has_value() || info.manage.granted.has_value()) { + if (info.manage.permitted.has_value()) { + cout << "\tmanage.permitted : " << (info.manage.permitted.value() ? "true" : "false") << endl; + } + if (info.manage.granted.has_value()) { + cout << "\tmanage.granted : " << (info.manage.granted.value() ? "true" : "false") << endl; + } + } + if (info.provide.permitted.has_value() || info.provide.granted.has_value()) { + if (info.provide.permitted.has_value()) { + cout << "\tprovide.permitted : " << (info.provide.permitted.value() ? "true" : "false") << endl; + } + if (info.provide.granted.has_value()) { + cout << "\tprovide.granted : " << (info.provide.granted.value() ? "true" : "false") << endl; + } + } + if (info.details.has_value()) { + cout << "\tdetails: " << endl; + for (auto item : info.details.value()) { + cout << "\t\t" << ConvertFromEnum(denyReasonMap, item) << endl; + } + } +} +void CoreSDKTest::GetCapabilitiesAvailable() +{ + Firebolt::Error error = Firebolt::Error::None; + std::string capability = "xrn:firebolt:capability:token:device"; + bool status = Firebolt::IFireboltAccessor::Instance().CapabilitiesInterface().available(capability, &error); + if (error == Firebolt::Error::None) { + cout << "Capabilities Available = " << (status ? "true" : "false") << endl; + } else { + cout << "Capabilities Available status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::SubscribeCapabilitiesAvailableChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + std::string capability = "xrn:firebolt:capability:usergrant:pinchallenge"; + Firebolt::IFireboltAccessor::Instance().CapabilitiesInterface().subscribe(capability, _availableNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Subscribe Capabilities AvailableNotification is success" << endl; + } else { + cout << "Subscribe Capabilities AvailableNotification status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::UnsubscribeCapabilitiesAvailableChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().CapabilitiesInterface().unsubscribe(_availableNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Unsubscribe Capabilities AvailableNotification is success" << endl; + } else { + cout << "Unsubscribe Capabilities AvailableNotification status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::OnAvailableNotification::onAvailable( const Firebolt::Capabilities::CapabilityInfo& info) +{ + cout << "OnAvailableNotification is invoked " << endl; + cout << "capabilityInfo: " << endl; + PrintCapabilityInfo(info); +} +void CoreSDKTest::GetCapabilitiesGranted() +{ + Firebolt::Error error = Firebolt::Error::None; + std::string capability = "xrn:firebolt:capability:localization:postal-code"; + std::optional options; + bool status = Firebolt::IFireboltAccessor::Instance().CapabilitiesInterface().granted(capability, options, &error); + if (error == Firebolt::Error::None) { + cout << "Capabilities Granted = " << (status ? "true" : "false") << endl; + } else { + cout << "Capabilities Granted status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::GetCapabilitiesPermitted() +{ + Firebolt::Error error = Firebolt::Error::None; + std::string capability = "xrn:firebolt:capability:input:keyboard"; + std::optional options = std::make_optional(); + options.value().role = std::make_optional(); + options.value().role.value() = Firebolt::Capabilities::Role::MANAGE; + bool status = Firebolt::IFireboltAccessor::Instance().CapabilitiesInterface().permitted(capability, options, &error); + if (error == Firebolt::Error::None) { + cout << "Capabilities Permitted = " << (status ? "true" : "false") << endl; + } else { + cout << "Capabilities Permitted status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::GetCapabilitiesSupported() +{ + Firebolt::Error error = Firebolt::Error::None; + std::string capability = "xrn:firebolt:capability:wifi:scan"; + bool status = Firebolt::IFireboltAccessor::Instance().CapabilitiesInterface().supported(capability, &error); + if (error == Firebolt::Error::None) { + cout << "Capabilities Supported = " << (status ? "true" : "false") << endl; + } else { + cout << "Capabilities Supported status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::GetCapabilitiesInfo() +{ + Firebolt::Error error = Firebolt::Error::None; + std::vector capabilities = { "xrn:firebolt:capability:device:model", "xrn:firebolt:capability:input:keyboard", "xrn:firebolt:capability:protocol:bluetoothle", "xrn:firebolt:capability:token:device", "xrn:firebolt:capability:token:platform", "xrn:firebolt:capability:protocol:moca", "xrn:firebolt:capability:wifi:scan", "xrn:firebolt:capability:localization:postal-code", "xrn:firebolt:capability:localization:locality"}; + std::vector info = Firebolt::IFireboltAccessor::Instance().CapabilitiesInterface().info(capabilities, &error); + if (error == Firebolt::Error::None) { + cout << "Capabilities Info : " << endl; + for (auto element : info) { + PrintCapabilityInfo(element); + cout << endl; + } + } else { + cout << "Capabilities Info status = " << static_cast(error) << endl; + } +} + +void CoreSDKTest::MetricsStartContent() +{ + Firebolt::Error error = Firebolt::Error::None; + std::optional entityId; + bool status = Firebolt::IFireboltAccessor::Instance().MetricsInterface().startContent(entityId, &error); + + if (error == Firebolt::Error::None) { + cout << "Metrics Start Content = " << (status ? "true" : "false") << endl; + } else { + cout << "Metrics Start Content status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::MetricsStopContent() +{ + Firebolt::Error error = Firebolt::Error::None; + std::optional entityId; + bool status = Firebolt::IFireboltAccessor::Instance().MetricsInterface().stopContent(entityId, &error); + + if (error == Firebolt::Error::None) { + cout << "Metrics Stop Content = " << (status ? "true" : "false") << endl; + } else { + cout << "Metrics Stop Content status = " << static_cast(error) << endl; + } +} + +void CoreSDKTest::GetSecondScreenDevice() +{ + Firebolt::Error error = Firebolt::Error::None; + std::optional type; + std::string deviceId = Firebolt::IFireboltAccessor::Instance().SecondScreenInterface().device(type, &error); + + if (error == Firebolt::Error::None) { + cout << "Get SecondScreen Device : " << deviceId.c_str() << endl; + } else { + cout << "Get SecondScreen Device status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::GetSecondScreenProtocols() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::Types::BooleanMap map = Firebolt::IFireboltAccessor::Instance().SecondScreenInterface().protocols(&error); + if (error == Firebolt::Error::None) { + cout << "Get SecondScreen Protocols : " << endl; + for (auto element : map) { + cout << element.first << " : " << (element.second ? "true" : "false") << endl; + } + } else { + cout << "Get SecondScreen Protocols status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::GetSecondScreenFriendlyName() +{ + Firebolt::Error error = Firebolt::Error::None; + std::string friendlyName = Firebolt::IFireboltAccessor::Instance().SecondScreenInterface().friendlyName(&error); + + if (error == Firebolt::Error::None) { + cout << "Get SecondScreen FriendlyName : " << friendlyName.c_str() << endl; + } else { + cout << "Get SecondScreen FriendlyName status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::OnFriendlyNameChangedNotification::onFriendlyNameChanged( const std::string& friendlyName) +{ + cout << "OnFriendlyNameChangedNotification friendlyName : " << friendlyName.c_str() << endl; +} +void CoreSDKTest::SubscribeSecondScreenFriendlyNameChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().SecondScreenInterface().subscribe(_friendlyNameChangedNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Subscribe SecondScreen FriendlyNameChangedNotification is success" << endl; + } else { + cout << "Subscribe SecondScreen FriendlyNameChangedNotification status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::UnsubscribeSecondScreenFriendlyNameChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().SecondScreenInterface().unsubscribe(_friendlyNameChangedNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Unsubscribe SecondScreen FriendlyNameChangedNotification is success" << endl; + } else { + cout << "Unsubscribe SecondScreen FriendlyNameChangedNotification status = " << static_cast(error) << endl; + } +} + +EnumMap secondScreenEventTypeMap = { + { Firebolt::SecondScreen::SecondScreenEventType::DIAL, "dial" } +}; +void CoreSDKTest::ParametersInitialization() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::Parameters::AppInitialization appInitialization = Firebolt::IFireboltAccessor::Instance().ParametersInterface().initialization(&error); + if (error == Firebolt::Error::None) { + cout << "Parameters Initialization is success" << endl; + if (appInitialization.us_privacy.has_value()) { + cout << "\tus_privacy : " << appInitialization.us_privacy.value().c_str() << endl; + } + if (appInitialization.lmt.has_value()) { + cout << "\tlmt : " << appInitialization.lmt.value() << endl; + } + if (appInitialization.discovery.has_value()) { + if (appInitialization.discovery.value().navigateTo.has_value()) { + cout << "\tdiscovery:navigateTo : " << appInitialization.discovery.value().navigateTo.value().c_str() << endl; + } + } + if (appInitialization.secondScreen.has_value()) { + if (appInitialization.secondScreen.value().launchRequest.has_value()) { + Firebolt::SecondScreen::SecondScreenEvent event = appInitialization.secondScreen.value().launchRequest.value(); + cout <<"\tsecondScreen:launchRequest:type : " << ConvertFromEnum(secondScreenEventTypeMap, event.type) << endl; + if (event.version.has_value()) { + cout <<"\tsecondScreen:launchRequest:version : " << event.version.value().c_str() << endl; + } + if (event.data.has_value()) { + cout <<"\tsecondScreen:launchRequest:data : " << event.data.value().c_str() << endl; + } + } + } + } else { + cout << "Parameters Initialization status = " << static_cast(error) << endl; + } +} + +void CoreSDKTest::DiscoverySignIn() +{ + Firebolt::Error error = Firebolt::Error::None; + std::optional> entitlements = std::make_optional>(); + Firebolt::Entertainment::Entitlement entitlement; + entitlement.startTime = "2025-01-01T00:00:00.000Z"; + entitlement.endTime = "2025-01-01T00:00:00.000Z"; + entitlements.value().push_back(entitlement); + + bool status = Firebolt::IFireboltAccessor::Instance().DiscoveryInterface().signIn(entitlements, &error); + if (error == Firebolt::Error::None) { + cout << "Discovery SignIn is " << (status ? "true" : "false") << endl; + } else { + cout << "Discovery SignIn status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::DiscoverySignOut() +{ + Firebolt::Error error = Firebolt::Error::None; + bool status = Firebolt::IFireboltAccessor::Instance().DiscoveryInterface().signOut(&error); + if (error == Firebolt::Error::None) { + cout << "Discovery SignOut is " << (status ? "true" : "false") << endl; + } else { + cout << "Discovery SignOut status = " << static_cast(error) << endl; + } +} + +void CoreSDKTest::DiscoveryContentAccess() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::Discovery::ContentAccessIdentifiers contentAccessIdentifiers; + contentAccessIdentifiers.availabilities = std::make_optional>(); + Firebolt::Discovery::Availability availability; + availability.type = Firebolt::Discovery::AvailabilityType::CHANNEL_LINEUP; + availability.id = "partner.com/availability/123"; + availability.startTime = "2021-04-23T18:25:43.511Z"; + availability.endTime = "2021-04-23T18:25:43.511Z"; + contentAccessIdentifiers.availabilities.value().push_back(availability); + + contentAccessIdentifiers.entitlements = std::make_optional>(); + Firebolt::Entertainment::Entitlement entitlement; + entitlement.startTime = "2025-01-01T00:00:00.000Z"; + entitlement.endTime = "2025-01-01T00:00:00.000Z"; + contentAccessIdentifiers.entitlements.value().push_back(entitlement); + + Firebolt::IFireboltAccessor::Instance().DiscoveryInterface().contentAccess(contentAccessIdentifiers, &error); + if (error == Firebolt::Error::None) { + cout << "Discovery ContentAccess is success" << endl; + } else { + cout << "Discovery ContentAccess status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::DiscoveryClearContentAccess() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().DiscoveryInterface().clearContentAccess(&error); + if (error == Firebolt::Error::None) { + cout << "Discovery ClearContentAccess is success" << endl; + } else { + cout << "Discovery ClearContentAccess status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::DiscoveryEntitlements() +{ + Firebolt::Error error = Firebolt::Error::None; + std::vector entitlements; + Firebolt::Entertainment::Entitlement entitlement; + entitlement.startTime = "2025-01-01T00:00:00.000Z"; + entitlement.endTime = "2025-01-01T00:00:00.000Z"; + entitlements.push_back(entitlement); + + bool status = Firebolt::IFireboltAccessor::Instance().DiscoveryInterface().entitlements(entitlements, &error); + if (error == Firebolt::Error::None) { + cout << "Discovery Entitlements is " << (status ? "true" : "false") << endl; + } else { + cout << "Discovery Entitlements status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::DiscoveryWatchNext() +{ + Firebolt::Error error = Firebolt::Error::None; + std::string title = "A Cool Show"; + Firebolt::Entertainment::ContentIdentifiers identifiers; + identifiers.entityId = "partner.com/entity/123"; + std::optional expires = "2021-04-23T18:25:43.511Z"; + std::optional images = std::make_optional(); + images = "{\"3x4\": {\"en-US\": \"https://i.ytimg.com/vi/4r7wHMg5Yjg/maxresdefault.jpg\", \"es\": \"https://i.ytimg.com/vi/4r7wHMg5Yjg/maxresdefault.jpg\"}, \"16x9\": {\"en\": \"https://i.ytimg.com/vi/4r7wHMg5Yjg/maxresdefault.jpg\"}}"; + + bool status = Firebolt::IFireboltAccessor::Instance().DiscoveryInterface().watchNext(title, identifiers, expires, images, &error); + if (error == Firebolt::Error::None) { + cout << "Discovery Watched Next is " << (status ? "true" : "false") << endl; + } else { + cout << "Discovery Watched Next status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::DiscoveryPolicy() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::Discovery::DiscoveryPolicy policy = Firebolt::IFireboltAccessor::Instance().DiscoveryInterface().policy(&error); + if (error == Firebolt::Error::None) { + cout << "Discovery Policy is " << endl; + cout << "\tenableRecommendations: " << policy.enableRecommendations << endl; + cout << "\tshareWatchHistory: " << policy.shareWatchHistory << endl; + cout << "\trememberWatchedPrograms: " << policy.rememberWatchedPrograms << endl; + } else { + cout << "Discovery Policy status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::DiscoveryEntityInfo() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::Discovery::EntityInfoResult result; + result.expires = "2025-01-01T00:00:00.000Z"; + { + result.entity.identifiers.entityId = "345"; + result.entity.title = "Cool Running"; + result.entity.entityType = Firebolt::Entertainment::EntityInfoEntityType::PROGRAM; + result.entity.programType = Firebolt::Entertainment::ProgramType::MOVIE; + result.entity.synopsis = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Pulvinar sapien et ligula ullamcorper malesuada proin libero nunc."; + result.entity.releaseDate = "1993-01-01T00:00:00.000Z"; + result.entity.contentRatings = std::make_optional>(); + { + Firebolt::Entertainment::ContentRating contentRating; + contentRating.scheme = Firebolt::Entertainment::ContentRatingScheme::US_MOVIE; + contentRating.rating = "PG"; + result.entity.contentRatings.value().push_back(contentRating); + } + { + Firebolt::Entertainment::ContentRating contentRating; + contentRating.scheme = Firebolt::Entertainment::ContentRatingScheme::CA_MOVIE; + contentRating.rating = "G"; + result.entity.contentRatings.value().push_back(contentRating); + } + + Firebolt::Entertainment::WayToWatch wayToWatch; + result.entity.waysToWatch = std::make_optional>(); + wayToWatch.identifiers.assetId = "123"; + wayToWatch.expires = "2025-01-01T00:00:00.000Z"; + wayToWatch.entitled = true; + wayToWatch.entitledExpires = "2025-01-01T00:00:00.000Z"; + wayToWatch.offeringType = Firebolt::Entertainment::OfferingType::BUY; + wayToWatch.price = 2.99; + wayToWatch.videoQuality = std::make_optional>(); + wayToWatch.videoQuality.value().push_back(Firebolt::Entertainment::WayToWatchVideoQuality::UHD); + wayToWatch.audioProfile.push_back(Firebolt::Types::AudioProfile::DOLBY_ATMOS); + wayToWatch.audioLanguages = std::make_optional>(); + wayToWatch.audioLanguages.value().push_back("en"); + wayToWatch.closedCaptions = std::make_optional>(); + wayToWatch.closedCaptions.value().push_back("en"); + wayToWatch.subtitles = std::make_optional>(); + wayToWatch.subtitles.value().push_back("es"); + wayToWatch.audioDescriptions = std::make_optional>(); + wayToWatch.audioDescriptions.value().push_back("en"); + result.entity.waysToWatch.value().push_back(wayToWatch); + } + { + result.related = std::make_optional>(); + Firebolt::Entertainment::EntityInfo entityInfo; + entityInfo.identifiers.entityId = "345"; + entityInfo.title = "Cool Runnings Trailer"; + entityInfo.entityType = Firebolt::Entertainment::EntityInfoEntityType::PROGRAM; + entityInfo.programType = Firebolt::Entertainment::ProgramType::PREVIEW; + entityInfo.releaseDate = "1993-01-01T00:00:00.000Z"; + entityInfo.waysToWatch = std::make_optional>(); + Firebolt::Entertainment::WayToWatch wayToWatch; + wayToWatch.identifiers.assetId = "123111"; + wayToWatch.identifiers.entityId = "345"; + wayToWatch.expires = "2026-01-01T00:00:00.000Z"; + wayToWatch.entitled = true; + wayToWatch.videoQuality = std::make_optional>(); + wayToWatch.videoQuality.value().push_back(Firebolt::Entertainment::WayToWatchVideoQuality::HD); + wayToWatch.audioProfile.push_back(Firebolt::Types::AudioProfile::DOLBY_ATMOS); + wayToWatch.audioLanguages = std::make_optional>(); + wayToWatch.audioLanguages.value().push_back("en"); + wayToWatch.closedCaptions = std::make_optional>(); + wayToWatch.closedCaptions.value().push_back("en"); + entityInfo.waysToWatch.value().push_back(wayToWatch); + result.related.value().push_back(entityInfo); + } + + bool status = Firebolt::IFireboltAccessor::Instance().DiscoveryInterface().entityInfo(result, &error); + if (error == Firebolt::Error::None) { + cout << "Discovery EntityInfo is " << (status ? "true" : "false") << endl; + } else { + cout << "Discovery EntityInfo status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::DiscoveryPurchasedContent() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::Discovery::PurchasedContentResult result; + result.expires = "2025-01-01T00:00:00.000Z"; + result.totalCount = 10; + Firebolt::Entertainment::EntityInfo entityInfo; + entityInfo.identifiers.entityId = "345"; + entityInfo.title = "Cool Runnings"; + entityInfo.entityType = Firebolt::Entertainment::EntityInfoEntityType::PROGRAM; + entityInfo.programType = Firebolt::Entertainment::ProgramType::MOVIE; + entityInfo.synopsis = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Pulvinar sapien et ligula ullamcorper malesuada proin libero nunc."; + entityInfo.releaseDate = "1993-01-01T00:00:00.000Z"; + entityInfo.contentRatings = std::make_optional>(); + { + Firebolt::Entertainment::ContentRating contentRating; + contentRating.scheme = Firebolt::Entertainment::ContentRatingScheme::US_MOVIE; + contentRating.rating = "PG"; + entityInfo.contentRatings.value().push_back(contentRating); + } + { + Firebolt::Entertainment::ContentRating contentRating; + contentRating.scheme = Firebolt::Entertainment::ContentRatingScheme::CA_TV; + contentRating.rating = "TV-PG"; + entityInfo.contentRatings.value().push_back(contentRating); + } + + Firebolt::Entertainment::WayToWatch wayToWatch; + entityInfo.waysToWatch = std::make_optional>(); + wayToWatch.identifiers.assetId = "123"; + wayToWatch.expires = "2025-01-01T00:00:00.000Z"; + wayToWatch.entitled = true; + wayToWatch.entitledExpires = "2025-01-01T00:00:00.000Z"; + wayToWatch.offeringType = Firebolt::Entertainment::OfferingType::BUY; + wayToWatch.price = 2.99; + wayToWatch.videoQuality = std::make_optional>(); + wayToWatch.videoQuality.value().push_back(Firebolt::Entertainment::WayToWatchVideoQuality::UHD); + wayToWatch.audioProfile.push_back(Firebolt::Types::AudioProfile::DOLBY_ATMOS); + wayToWatch.audioLanguages = std::make_optional>(); + wayToWatch.audioLanguages.value().push_back("en"); + wayToWatch.closedCaptions = std::make_optional>(); + wayToWatch.closedCaptions.value().push_back("en"); + wayToWatch.subtitles = std::make_optional>(); + wayToWatch.subtitles.value().push_back("es"); + wayToWatch.audioDescriptions = std::make_optional>(); + wayToWatch.audioDescriptions.value().push_back("en"); + entityInfo.waysToWatch.value().push_back(wayToWatch); + result.entries.push_back(entityInfo); + + bool status = Firebolt::IFireboltAccessor::Instance().DiscoveryInterface().purchasedContent(result, &error); + if (error == Firebolt::Error::None) { + cout << "Discovery PurchasedContent is " << (status ? "true" : "false") << endl; + } else { + cout << "Discovery PurchasedContent status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::DiscoveryLaunch() +{ + Firebolt::Error error = Firebolt::Error::None; + cout << "Enter appId :"; + getchar(); + std::string appId; + getline(cin, appId); + { + std::optional intent = std::make_optional(); + intent.value().action = "tune"; + intent.value().data.entity.entityType = "channel"; + intent.value().data.entity.channelType = Firebolt::Intents::ChannelEntityChannelType::STREAMING; + intent.value().data.entity.entityId = "an-ott-channel"; + std::string entityId; + std::optional appContentData; + intent.value().data.options = std::make_optional(); + intent.value().data.options.value().restartCurrentProgram = true; + intent.value().context.source = "voice"; + cout << "Calling Discovery Launch TuneIntent method " << endl; + bool status = Firebolt::IFireboltAccessor::Instance().DiscoveryInterface().launch(appId, intent, &error); + if (error == Firebolt::Error::None) { + cout << "Discovery Launch TuneIntent is " << (status ? "true" : "false") << endl; + } else { + cout << "Discovery Launch TuneIntent status = " << static_cast(error) << endl; + } + } + + { + std::optional intent = std::make_optional(); + intent.value().action = "entity"; + intent.value().context.source = "voice"; + intent.value().data = "{\"entityId\":\"example-movie-id\",\"entityType\":\"program\",\"programType\":\"movie\"}"; + intent.value().context.source = "voice"; + cout << "Calling Discovery Launch EntityIntent method " << endl; + bool status = Firebolt::IFireboltAccessor::Instance().DiscoveryInterface().launch(appId, intent, &error); + if (error == Firebolt::Error::None) { + cout << "Discovery Launch EntityIntent is " << (status ? "true" : "false") << endl; + } else { + cout << "Discovery Launch EntityIntent status = " << static_cast(error) << endl; + } + } + + { + std::optional intent = std::make_optional(); + intent.value().action = "home"; + intent.value().context.source = "voice"; + cout << "Calling Discovery Launch HomeIntent method " << endl; + bool status = Firebolt::IFireboltAccessor::Instance().DiscoveryInterface().launch(appId, intent, &error); + if (error == Firebolt::Error::None) { + cout << "Discovery Launch HomeIntent is " << (status ? "true" : "false") << endl; + } else { + cout << "Discovery Launch HomeIntent status = " << static_cast(error) << endl; + } + } + cin.putback('\n'); +} +#ifdef POLYMORPHICS_REDUCER_METHODS +void CoreSDKTest::DiscoveryWatched() +{ + Firebolt::Error error = Firebolt::Error::None; + std::string entityId = "partner.com/entity/123"; + std::optional progress = 0.95; + std::optional completed = true; + std::optional watchedOn = "2021-04-23T18:25:43.511Z"; + + bool status = Firebolt::IFireboltAccessor::Instance().DiscoveryInterface().watched(entityId, progress, completed, watchedOn, &error); + if (error == Firebolt::Error::None) { + cout << "Discovery Watched is " << (status ? "true" : "false") << endl; + } else { + cout << "Discovery Watched status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::DiscoveryWatchedReduced() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::Discovery::WatchedInfo watchedInfo; + watchedInfo.entityId = "partner.com/entity/123"; + watchedInfo.progress = 0.95; + watchedInfo.completed = true; + watchedInfo.watchedOn = "2021-04-23T18:25:43.511Z"; + std::vector watchedInfoList; + watchedInfoList.push_back(watchedInfo); + + bool status = Firebolt::IFireboltAccessor::Instance().DiscoveryInterface().watched(watchedInfoList, &error); + if (error == Firebolt::Error::None) { + cout << "Discovery Watched Reduced is " << (status ? "true" : "false") << endl; + } else { + cout << "Discovery Watched Reduced status = " << static_cast(error) << endl; + } +} +#endif +void CoreSDKTest::SubscribeDiscoveryOnNavigateToLaunchNotification() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().DiscoveryInterface().subscribe(_navigateToHomeIntentNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Subscribe Discovery OnNavigateToLaunch HomeIntent is success" << endl; + } else { + cout << "Subscribe Discovery OnNavigateToLaunch HomeIntent status = " << static_cast(error) << endl; + } + + error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().DiscoveryInterface().subscribe(_navigateToEntityIntentNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Subscribe Discovery OnNavigateToLaunch EntityIntent is success" << endl; + } else { + cout << "Subscribe Discovery OnNavigateToLaunch EntityIntent status = " << static_cast(error) << endl; + } + + error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().DiscoveryInterface().subscribe(_navigateToTuneIntentNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Subscribe Discovery OnNavigateToLaunch TuneIntent is success" << endl; + } else { + cout << "Subscribe Discovery OnNavigateToLaunch TuneIntent status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::UnsubscribeDiscoveryOnNavigateToLaunchNotification() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().DiscoveryInterface().unsubscribe(_navigateToHomeIntentNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Unsubscribe Discovery OnNavigateToLaunch HomeIntent is success" << endl; + } else { + cout << "Unsubscribe Discovery OnNavigateToLaunch HomeIntent status = " << static_cast(error) << endl; + } + + error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().DiscoveryInterface().unsubscribe(_navigateToEntityIntentNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Unsubscribe Discovery OnNavigateToLaunch EntityIntent is success" << endl; + } else { + cout << "Unsubscribe Discovery OnNavigateToLaunch EntityIntent status = " << static_cast(error) << endl; + } + + error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().DiscoveryInterface().unsubscribe(_navigateToTuneIntentNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Unsubscribe Discovery OnNavigateToLaunch TuneIntent is success" << endl; + } else { + cout << "Unsubscribe Discovery OnNavigateToLaunch TuneIntent status = " << static_cast(error) << endl; + } +} +void CoreSDKTest::OnNavigateToHomeIntentNotification::onNavigateTo( const Firebolt::Intents::HomeIntent& intent) +{ + cout << "onNavigateTo for action : " << intent.action << endl; +} +void CoreSDKTest::OnNavigateToEntityIntentNotification::onNavigateTo( const Firebolt::Intents::EntityIntent& intent) +{ + cout << "onNavigateTo for action : " << intent.action << endl; +} +void CoreSDKTest::OnNavigateToTuneIntentNotification::onNavigateTo( const Firebolt::Intents::TuneIntent& intent) +{ + cout << "onNavigateTo for action : " << intent.action << endl; +} diff --git a/src/sdks/core/src/cpp/sdk/cpptest/CoreSDKTest.h b/src/sdks/core/src/cpp/sdk/cpptest/CoreSDKTest.h new file mode 100644 index 000000000..9b8d056ea --- /dev/null +++ b/src/sdks/core/src/cpp/sdk/cpptest/CoreSDKTest.h @@ -0,0 +1,202 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include "firebolt.h" + +class CoreSDKTest { + class OnPolicyChangedNotification : public Firebolt::Advertising::IAdvertising::IOnPolicyChangedNotification { + public: + void onPolicyChanged( const Firebolt::Advertising::AdPolicy& ) override; + }; + class OnDeviceNameChangedNotification : public Firebolt::Device::IDevice::IOnDeviceNameChangedNotification { + public: + void onDeviceNameChanged( const std::string& ) override; + }; + class OnAudioChangedNotification : public Firebolt::Device::IDevice::IOnAudioChangedNotification { + public: + void onAudioChanged( const Firebolt::Device::AudioProfiles& ) override; + }; + class OnScreenResolutionChangedNotification : public Firebolt::Device::IDevice::IOnScreenResolutionChangedNotification { + public: + void onScreenResolutionChanged( const Firebolt::Device::Resolution& ) override; + }; + class OnPreferredAudioLanguagesChangedNotification : public Firebolt::Localization::ILocalization::IOnPreferredAudioLanguagesChangedNotification { + public: + void onPreferredAudioLanguagesChanged( const std::vector& ) override; + }; + struct OnClosedCaptionsSettingsChangedNotification : public Firebolt::Accessibility::IAccessibility::IOnClosedCaptionsSettingsChangedNotification { + void onClosedCaptionsSettingsChanged( const Firebolt::Accessibility::ClosedCaptionsSettings& ) override; + }; + + struct OnBackgroundNotification : public Firebolt::Lifecycle::ILifecycle::IOnBackgroundNotification { + void onBackground( const Firebolt::Lifecycle::LifecycleEvent& ) override; + }; + struct OnForegroundNotification : public Firebolt::Lifecycle::ILifecycle::IOnForegroundNotification { + void onForeground( const Firebolt::Lifecycle::LifecycleEvent& ) override; + }; + + struct OnFriendlyNameChangedNotification : public Firebolt::SecondScreen::ISecondScreen::IOnFriendlyNameChangedNotification { + void onFriendlyNameChanged( const std::string& ) override; + }; + + struct OnAvailableNotification : public Firebolt::Capabilities::ICapabilities::IOnAvailableNotification { + void onAvailable( const Firebolt::Capabilities::CapabilityInfo& ) override; + }; + + struct OnNavigateToHomeIntentNotification : public Firebolt::Discovery::IDiscovery::IOnNavigateToHomeIntentNotification { + void onNavigateTo( const Firebolt::Intents::HomeIntent& ) override; + }; + struct OnNavigateToEntityIntentNotification : public Firebolt::Discovery::IDiscovery::IOnNavigateToEntityIntentNotification { + void onNavigateTo( const Firebolt::Intents::EntityIntent& ) override; + }; + struct OnNavigateToTuneIntentNotification : public Firebolt::Discovery::IDiscovery::IOnNavigateToTuneIntentNotification { + void onNavigateTo( const Firebolt::Intents::TuneIntent& ) override; + }; + struct KeyboardEmailAsyncResponse : public Firebolt::Keyboard::IKeyboardAsyncResponse { + void response(const std::string&, Firebolt::Error*) override; + }; + struct KeyboardPasswordAsyncResponse : public Firebolt::Keyboard::IKeyboardAsyncResponse { + void response(const std::string&, Firebolt::Error*) override; + }; + struct KeyboardStandardAsyncResponse : public Firebolt::Keyboard::IKeyboardAsyncResponse { + void response(const std::string&, Firebolt::Error*) override; + }; + +public: + CoreSDKTest() = default; + virtual ~CoreSDKTest() = default; + + static void CreateFireboltInstance(const std::string& url); + static void DestroyFireboltInstance(); + static void TestCoreStaticSDK(); + + static void GetAccountId(); + static void GetAccountUid(); + + static void GetDeviceName(); + static void SubscribeDeviceNameChanged(); + static void UnsubscribeDeviceNameChanged(); + static void GetDeviceModel(); + static void GetDeviceSku(); + static void GetDeviceAudio(); + static void SubscribeDeviceAudioChanged(); + static void UnsubscribeDeviceAudioChanged(); + static void GetDeviceScreenResolution(); + static void SubscribeDeviceScreenResolutionChanged(); + static void UnsubscribeDeviceScreenResolutionChanged(); + + static void GetLocalizationAdditionalInfo(); + static void GetLocalizationLatlon(); + static void GetLocalizationPreferredAudioLanguages(); + static void SubscribeLocalizationPreferredAudioLanguagesChanged(); + static void UnsubscribeLocalizationPreferredAudioLanguagesChanged(); + + static void GetAccessibilityClosedCaptionsSettings(); + static void SubscribeAccessibilityClosedCaptionsSettingsChanged(); + static void UnsubscribeAccessibilityClosedCaptionsSettingsChanged(); + + static void GetAdvertisingPolicy(); + static void SubscribeAdvertisingPolicyChanged(); + static void UnsubscribeAdvertisingPolicyChanged(); + static void BuildAdvertisingConfiguration(); + static void GetAdvertisingDeviceAttributes(); + + static void InvokeKeyboardStandard(); + static void AbortKeyboardStandard(); + static void InvokeKeyboardPassword(); + static void AbortKeyboardPassword(); + static void InvokeKeyboardEmail(); + static void AbortKeyboardEmail(); + + static void VerifyProfileApproveContentRating(); + static void VerifyProfileApprovePurchase(); + static void GetProfileFlags(); + + static void LifecycleClose(); + static void SubscribeLifecycleBackgroundNotification(); + static void UnsubscribeLifecycleBackgroundNotification(); + static void SubscribeLifecycleForegroundNotification(); + static void UnsubscribeLifecycleForegroundNotification(); + + static void GetAuthenticationDevice(); + static void GetAuthenticationSession(); + static void GetAuthenticationToken(); + static void GetAuthenticationRoot(); + + static void GetCapabilitiesSupported(); + static void GetCapabilitiesAvailable(); + static void GetCapabilitiesGranted(); + static void GetCapabilitiesPermitted(); + static void GetCapabilitiesInfo(); + static void SubscribeCapabilitiesAvailableChanged(); + static void UnsubscribeCapabilitiesAvailableChanged(); + + static void MetricsStartContent(); + static void MetricsStopContent(); + + static void GetSecondScreenDevice(); + static void GetSecondScreenProtocols(); + static void GetSecondScreenFriendlyName(); + static void SubscribeSecondScreenFriendlyNameChanged(); + static void UnsubscribeSecondScreenFriendlyNameChanged(); + + static void DiscoverySignIn(); + static void DiscoverySignOut(); + static void DiscoveryContentAccess(); + static void DiscoveryClearContentAccess(); + static void DiscoveryEntitlements(); + static void DiscoveryEntityInfo(); + static void DiscoveryPolicy(); + static void DiscoveryPurchasedContent(); + static void DiscoveryLaunch(); +#ifdef POLYMORPHICS_REDUCER_METHODS + static void DiscoveryWatched(); + static void DiscoveryWatchedReduced(); +#endif + static void SubscribeDiscoveryOnNavigateToLaunchNotification(); + static void UnsubscribeDiscoveryOnNavigateToLaunchNotification(); + static void DiscoveryWatchNext(); + + static void ParametersInitialization(); + + static bool WaitOnConnectionReady(); + +private: + static void ConnectionChanged(const bool, const Firebolt::Error); + static bool _connected; + static OnPolicyChangedNotification _policyChangedNotification; + static OnDeviceNameChangedNotification _deviceNameChangedNotification; + static OnAudioChangedNotification _audioChangedNotification; + static OnScreenResolutionChangedNotification _screenResolutionChangedNotification; + static OnPreferredAudioLanguagesChangedNotification _preferredAudioLanguagesChangedNotification; + static OnClosedCaptionsSettingsChangedNotification _closedCaptionsSettingsChangedNotification; + static OnBackgroundNotification _backgroundNotification; + static OnForegroundNotification _foregroundNotification; + static OnFriendlyNameChangedNotification _friendlyNameChangedNotification; + static OnAvailableNotification _availableNotification; + static OnNavigateToHomeIntentNotification _navigateToHomeIntentNotification; + static OnNavigateToEntityIntentNotification _navigateToEntityIntentNotification; + static OnNavigateToTuneIntentNotification _navigateToTuneIntentNotification; + static KeyboardEmailAsyncResponse _keyboardEmailAsyncResponse; + static KeyboardPasswordAsyncResponse _keyboardPasswordAsyncResponse; + static KeyboardStandardAsyncResponse _keyboardStandardAsyncResponse; +}; + diff --git a/src/sdks/core/src/cpp/sdk/cpptest/Main.cpp b/src/sdks/core/src/cpp/sdk/cpptest/Main.cpp new file mode 100644 index 000000000..2e8bbba9b --- /dev/null +++ b/src/sdks/core/src/cpp/sdk/cpptest/Main.cpp @@ -0,0 +1,654 @@ +#include +#include "CoreSDKTest.h" + +void ShowMenu() +{ + printf("Enter\n" + "\tI : Get Account Id\n" + "\tU : Get Account Uid\n" + "\tM : Get Device Model\n" + "\tS : Get Device Sku\n" + "\tA : Handle Advertising methods\n" + "\tN : Get/Subscribe/Unsubscribe Device Name\n" + "\tP : Get/Subscribe/Unsubscribe Device Audio Profiles\n" + "\tR : Get/Subscribe/Unsubscribe Device Screen Resolution\n" + "\tL : Handle Localization methods\n" + "\tC : Get/Subscribe/Unsubscribe Closed Caption Settings\n" + "\tK : Invoke keyboard methods email/password/standard\n" + "\tV : Handle Profile methods\n" + "\tT : Handle Authentication methods\n" + "\tB : Handle Capabilities Method\n" + "\tE : Handle Metrics methods\n" + "\tF : Handle Lifecycle methods\n" + "\tD : Handle SecondScreen methods\n" + "\tY : Handle Discovery methods\n" + "\tZ : Parameters Initialization\n" + "\tQ : Quit\n\n" + ); +} + +void ShowPropertyMenu(std::string& module, std::string& property) +{ + printf("%s:%s property options \n" + "\tG : Get value\n" + "\tR : Show subscribe/unscribe event menu\n" + "\tQ : Quit\n", module.c_str(), property.c_str()); +} + +void ShowProfileMenu() +{ + printf("Options: \n" + "\tC : Verify ApproveContentRating\n" + "\tP : Verify ApprovePurchase\n" + "\tF : Get Flags\n" + "\tQ : Quit\n"); +} + +void ShowLocalizationMenu() +{ + printf("Options: \n" + "\tA: Get additionalInfo\n" + "\tL: Get LatLon\n" + "\tP: Get/Subscribe/Unsubscribe Preferred AudioLanguages\n" + "\tQ : Quit\n"); +} + +void ShowAuthenticationMenu() +{ + printf("Options: \n" + "\t Invoke lifecyclesession management sequence from postman before invoking methods\n" + "\tD : Get Authentication Device\n" + "\tS : Get Authentication Session\n" + "\tR : Get Authentication Root\n" + "\tT : Get Authentication Token\n" + "\tQ : Quit\n"); +} + +void ShowCapabilitiesMenu() +{ + printf("Options: \n" + "\tA : Get Capabilities Available\n" + "\tG : Get Capabilities Granted\n" + "\tP : Get Capabilities Permitted\n" + "\tS : Get Capabilities Supported\n" + "\tI : Get Capabilities Info\n" + "\tQ : Quit\n"); +} + +void ShowAdvertisingMenu() +{ + printf("Options: \n" + "\tP : Get/Subscribe/Unsubscribe Advertising Policy\n" + "\tC : Build configuration object for Ad Framework initialization\n" + "\tD : Get the device advertising device attributes\n" + "\tQ : Quit\n"); +} + +void ShowLifecycleMenu() +{ + printf("Options: \n" + "\tC : Close lifecycle of the app\n" + "\tB : Subscribe/Unsubscribe Background notification\n" + "\tF : Subscribe/Unsubscribe Foreground notification\n" + "\tQ : Quit\n"); +} + +void ShowMetricsMenu() +{ + printf("Options: \n" + "\tS : Start Content\n" + "\tT : Stop Content\n" + "\tQ : Quit\n"); +} + +void ShowSecondScreenMenu() +{ + printf("Options: \n" + "\tD : Get Device Id\n" + "\tF : Get/Subscribe/Unsubscribe FriendlyName\n" + "\tP : Get Protocols\n" + "\tQ : Quit\n"); +} + +void ShowDiscoveryMenu() +{ + printf("Options: \n" + "\tS : SignIn\n" + "\tO : SignOut\n" + "\tA : ContentAccess\n" + "\tC : ClearContentAccess\n" + "\tE : Entitlements\n" + "\tI : EntityInfo\n" + "\tP : Policy\n" + "\tU : PurchasedContent\n" + "\tN : WatchNext\n" + "\tL : Launch\n" +#ifdef POLYMORPHICS_REDUCER_METHODS + "\tW : Watched\n" + "\tR : WatchedReduced\n" +#endif + "\tB : Subscribe/Unsubscribe DiscoveryOnNavigateToLaunch\n" + "\tQ : Quit\n"); +} + +void ShowKeyboardMenu() +{ + printf("Enter\n" + "\tE : Invoke Email method\n" + "\tA : Abort Email method\n" + "\tP : Invoke Password method\n" + "\tC : Abort Password method\n" + "\tS : Invoke Standard method\n" + "\tK : Abort Standard method\n" + "\tQ : Quit\n"); +} + +void ShowEventMenu() +{ + printf("Enter\n" + "\tS : Subscribe Event\n" + "\tU : Unsubscribe Event\n" + "\tQ : Quit\n"); +} + +#define VALUE(string) #string +#define TO_STRING(string) VALUE(string) + +#define HandleEventListener(Module, eventFuncName) \ +{ \ + int opt; \ + do { \ + getchar(); \ + ShowEventMenu(); \ + printf("Enter option : "); \ + opt = toupper(getchar()); \ + switch (opt) { \ + case 'S': { \ + CoreSDKTest::Subscribe##Module##eventFuncName(); \ + break; \ + } \ + case 'U': { \ + CoreSDKTest::Unsubscribe##Module##eventFuncName(); \ + break; \ + } \ + default: \ + break; \ + } \ + } while (opt != 'Q'); \ +} + + +#define HandleProperty(Module, Property) \ +{ \ + int opt; \ + do { \ + getchar(); \ + std::string module = TO_STRING(Module); \ + std::string property = TO_STRING(Property); \ + ShowPropertyMenu(module, property); \ + printf("Enter option : "); \ + opt = toupper(getchar()); \ + switch (opt) { \ + case 'G': { \ + CoreSDKTest::Get##Module##Property(); \ + break; \ + } \ + case 'R': { \ + HandleEventListener(Module, Property##Changed) \ + break; \ + } \ + default: \ + break; \ + } \ + } while (opt != 'Q'); \ +} + +void HandleAuthenticationMethod() +{ + int opt; + do { + getchar(); + ShowAuthenticationMenu(); + printf("Enter option : "); + opt = toupper(getchar()); + switch (opt) { + case 'D': { + CoreSDKTest::GetAuthenticationDevice(); + break; + } + case 'S': { + CoreSDKTest::GetAuthenticationSession(); + break; + } + case 'R': { + CoreSDKTest::GetAuthenticationRoot(); + break; + } + case 'T': { + CoreSDKTest::GetAuthenticationToken(); + break; + } + default: + break; + } + } while (opt != 'Q'); +} + +void HandleCapabilitiesMethod() +{ + int opt; + do { + getchar(); + ShowCapabilitiesMenu(); + printf("Enter option : "); + opt = toupper(getchar()); + switch (opt) { + case 'A': { + HandleProperty(Capabilities, Available) + break; + } + case 'G': { + CoreSDKTest::GetCapabilitiesGranted(); + break; + } + case 'P': { + CoreSDKTest::GetCapabilitiesPermitted(); + break; + } + case 'S': { + CoreSDKTest::GetCapabilitiesSupported(); + break; + } + case 'I': { + CoreSDKTest::GetCapabilitiesInfo(); + break; + } + default: + break; + } + } while (opt != 'Q'); +} + +void HandleProfileMethod() +{ + int opt; + do { + getchar(); + ShowProfileMenu(); + printf("Enter option : "); + opt = toupper(getchar()); + switch (opt) { + case 'C': { + CoreSDKTest::VerifyProfileApproveContentRating(); // Error while testing, need to cross check + break; + } + case 'P': { + CoreSDKTest::VerifyProfileApprovePurchase(); // Error while testing, need to cross check + break; + } + case 'F': { + CoreSDKTest::GetProfileFlags(); + break; + } + default: + break; + } + } while (opt != 'Q'); +} + +void HandleLocalizationMethod() +{ + int opt; + do { + getchar(); + ShowLocalizationMenu(); + printf("Enter option : "); + opt = toupper(getchar()); + switch (opt) { + case 'A': { + CoreSDKTest::GetLocalizationAdditionalInfo(); + break; + } + case 'L': { + CoreSDKTest::GetLocalizationLatlon(); + break; + } + case 'P': { + HandleProperty(Localization, PreferredAudioLanguages) + break; + } + default: + break; + } + } while (opt != 'Q'); +} + +void HandleAdvertisingMethod() +{ + int opt; + do { + getchar(); + ShowAdvertisingMenu(); + printf("Enter option : "); + opt = toupper(getchar()); + switch (opt) { + case 'P': { + HandleProperty(Advertising, Policy) + break; + } + case 'C': { + CoreSDKTest::BuildAdvertisingConfiguration(); // Error while testing need to check + break; + } + case 'D': { + CoreSDKTest::GetAdvertisingDeviceAttributes(); // Error while testing need to check + break; + } + default: + break; + } + } while (opt != 'Q'); +} + +void HandleLifecycleMethod() +{ + int opt; + do { + getchar(); + ShowLifecycleMenu(); + printf("Enter option : "); + opt = toupper(getchar()); + switch (opt) { + case 'C': { + CoreSDKTest::LifecycleClose(); + break; + } + case 'B': { + HandleEventListener(Lifecycle, BackgroundNotification) + break; + } + case 'F': { + HandleEventListener(Lifecycle, ForegroundNotification) + break; + } + default: + break; + } + } while (opt != 'Q'); +} + +void HandleMetricsMethod() +{ + int opt; + do { + getchar(); + ShowMetricsMenu(); + printf("Enter option : "); + opt = toupper(getchar()); + switch (opt) { + case 'S': { + CoreSDKTest::MetricsStartContent(); + break; + } + case 'T': { + CoreSDKTest::MetricsStopContent(); + break; + } + default: + break; + } + } while (opt != 'Q'); +} + +void HandleSecondScreenMethod() +{ + int opt; + do { + getchar(); + ShowSecondScreenMenu(); + printf("Enter option : "); + opt = toupper(getchar()); + switch (opt) { + case 'D': { + CoreSDKTest::GetSecondScreenDevice(); + break; + } + case 'F': { + HandleProperty(SecondScreen, FriendlyName) + break; + } + case 'P': { + CoreSDKTest::GetSecondScreenProtocols(); + break; + } + default: + break; + } + } while (opt != 'Q'); +} + +void HandleDiscoveryMethod() +{ + int opt; + do { + getchar(); + ShowDiscoveryMenu(); + printf("Enter option : "); + opt = toupper(getchar()); + switch (opt) { + case 'S': { + CoreSDKTest::DiscoverySignIn(); + break; + } + case 'O': { + CoreSDKTest::DiscoverySignOut(); + break; + } + case 'A': { + CoreSDKTest::DiscoveryContentAccess(); + break; + } + case 'C': { + CoreSDKTest::DiscoveryClearContentAccess(); + break; + } + case 'E': { + CoreSDKTest::DiscoveryEntitlements(); + break; + } + case 'I': { + CoreSDKTest::DiscoveryEntityInfo(); + break; + } + case 'P': { + CoreSDKTest::DiscoveryPolicy(); + break; + } + case 'U': { + CoreSDKTest::DiscoveryPurchasedContent(); + break; + } + case 'N': { + CoreSDKTest::DiscoveryWatchNext(); + break; + } + case 'L': { + CoreSDKTest::DiscoveryLaunch(); + break; + } +#ifdef POLYMORPHICS_REDUCER_METHODS + case 'W': { + CoreSDKTest::DiscoveryWatched(); + break; + } + case 'R': { + CoreSDKTest::DiscoveryWatchedReduced(); + break; + } +#endif + case 'B': { + HandleEventListener(Discovery, OnNavigateToLaunchNotification) + } + default: + break; + } + } while (opt != 'Q'); +} + +void HandleKeyboardMethodsInvokation() +{ + int opt; + do { + getchar(); + ShowKeyboardMenu(); + printf("Enter option : "); + opt = toupper(getchar()); + switch (opt) { + case 'E': { + CoreSDKTest::InvokeKeyboardEmail(); + break; + } + case 'A': { + CoreSDKTest::AbortKeyboardEmail(); + break; + } + case 'P': { + CoreSDKTest::InvokeKeyboardPassword(); + break; + } + case 'C': { + CoreSDKTest::AbortKeyboardPassword(); + break; + } + case 'S': { + CoreSDKTest::InvokeKeyboardStandard(); + break; + } + case 'K': { + CoreSDKTest::AbortKeyboardStandard(); + break; + } + default: + break; + } + } while (opt != 'Q'); +} + +#define options ":hu:" +int main (int argc, char* argv[]) +{ + int c; + std::string url = "ws://127.0.0.1:9998"; + while ((c = getopt (argc, argv, options)) != -1) { + switch (c) + { + case 'u': + url = optarg; + break; + + case 'h': + printf("./TestFireboltManage -u ws://ip:port\n"); + exit(1); + } + } + + printf("Firebolt Core SDK Test\n"); + + CoreSDKTest::CreateFireboltInstance(url); + int option; + if (CoreSDKTest::WaitOnConnectionReady() == true) { + do { + ShowMenu(); + printf("Enter option : "); + option = toupper(getchar()); + switch (option) { + case 'I' : { + CoreSDKTest::GetAccountId(); // Error while running need to cross check + break; + } + case 'U' : { + CoreSDKTest::GetAccountUid(); // Error while running need to cross check + break; + } + case 'A' : { + HandleAdvertisingMethod(); + break; + } + case 'N': { + HandleProperty(Device, Name) + break; + } + case 'M': { + CoreSDKTest::GetDeviceModel(); + break; + } + case 'S': { + CoreSDKTest::GetDeviceSku(); + break; + } + case 'P': { + HandleProperty(Device, Audio) + break; + } + case 'R': { + HandleProperty(Device, ScreenResolution) + break; + } + case 'L': { + HandleLocalizationMethod(); + break; + } + case 'C': { + HandleProperty(Accessibility, ClosedCaptionsSettings) + break; + } + case 'K': { + HandleKeyboardMethodsInvokation(); + break; + } + case 'V': { + HandleProfileMethod(); + break; + } + case 'T': { + HandleAuthenticationMethod(); + break; + } + case 'B': { + HandleCapabilitiesMethod(); + break; + } + case 'F': { + HandleLifecycleMethod(); + break; + } + case 'E': { + HandleMetricsMethod(); + break; + } + case 'D': { + HandleSecondScreenMethod(); + break; + } + case 'Y': { + HandleDiscoveryMethod(); + break; + } + case 'Z': { + CoreSDKTest::ParametersInitialization(); + break; + } + default: + break; + } + getchar(); // Skip white space + } while (option != 'Q'); + + } else { + printf("Core Test not able to connect with server.... \n"); + } + + CoreSDKTest::DestroyFireboltInstance(); + + return 0; +} diff --git a/src/sdks/core/src/cpp/sdk/cpptest/build.sh b/src/sdks/core/src/cpp/sdk/cpptest/build.sh new file mode 100755 index 000000000..b404a78f3 --- /dev/null +++ b/src/sdks/core/src/cpp/sdk/cpptest/build.sh @@ -0,0 +1,41 @@ +#!/bin/bash +usage() +{ + echo "options:" + echo " -t test path" + echo " -s sysroot path" + echo " -f firebolt path" + echo " -c clear build" + echo " -h : help" + echo + echo "usage: " + echo " ./build.sh -t testpath -c -f fireboltpath -s sysrootpath" +} + +TestPath="." +FireboltPath=${FIREBOLT_PATH} +SysrootPath=${SYSROOT_PATH} +ClearBuild="N" +while getopts t:s:f:ch flag +do + case "${flag}" in + t) TestPath="${OPTARG}";; + s) SysrootPath="${OPTARG}";; + f) FireboltPath="${OPTARG}";; + c) ClearBuild="Y";; + h) usage && exit 1;; + esac +done + +if [ "${ClearBuild}" == "Y" ]; +then + rm -rf ${TestPath}/build +fi + +echo "TestPath" +echo "${TestPath}" +echo "FireboltPath" +echo ${FireboltPath} +cmake -B${TestPath}/build -S${TestPath} -DSYSROOT_PATH=${SysrootPath} -DFIREBOLT_PATH=${FireboltPath} +# -DPOLYMORPHICS_METHODS=ON // Enable this to test +cmake --build ${TestPath}/build diff --git a/src/sdks/core/src/cpp/sdk/ctest/main.c b/src/sdks/core/src/cpp/sdk/ctest/main.c deleted file mode 100644 index 6e6dbf020..000000000 --- a/src/sdks/core/src/cpp/sdk/ctest/main.c +++ /dev/null @@ -1,470 +0,0 @@ -#include -#include -#include - -#include "firebolt.h" -#include "accessibility.h" -#include "account.h" -#include "advertising.h" -#include "device.h" -#include "discovery.h" -#include "common/entertainment.h" - -void ShowMenu() -{ - printf("Enter\n" - "\tI : Get Device ID\n" - "\tC : Get Closed Caption Settings\n" - "\tG : Get Voice Guidance Settings\n" - "\tP : Get Advertising Policy\n" - "\tU : Get Account UID\n" - "\tE : Push EntityInfo\n" - "\tN : Register/Unregister for Device Name change\n" - "\tR : Register/Unregister for Screen Resolution change\n" - "\tA : Register/Unregister for Accessibilty Voice Guidance change\n" - "\tH : Register/Unregister for OnNavigateTo:HomeIntent\n" - "\tS : Register/Unregister for OnNavigateTo:SearchIntent\n" - "\tQ : Quit\n\n" - ); -} - -void ShowEventMenu() -{ - printf("Enter\n" - "\tR: Register Event\n" - "\tU: Unregister Event\n" - "\tQ : Quit\n"); -} - -#define HandleEventListener(Module, eventFuncName, Callback, eventTestStr, eventName) \ -{ \ - int opt; \ - do { \ - getchar(); \ - ShowEventMenu(); \ - printf("Enter option : "); \ - opt = toupper(getchar()); \ - switch (opt) { \ - case 'R': { \ - int32_t result = Module##_Register_On##eventFuncName((const void*)Callback, eventTestStr); \ - if (result != FireboltSDKErrorNone) { \ - printf("Register event %s is failed, status = %d \n", eventName, result); \ - } else { \ - printf("Event %s is registered successfully\n", eventName); \ - } \ - break; \ - } \ - case 'U': { \ - int32_t result = Module##_Unregister_On##eventFuncName((const void*)Callback); \ - if (result != FireboltSDKErrorNone) { \ - printf("Unregister event %s is failed, status = %d \n", eventName, result); \ - } else { \ - printf("Event %s is unregistered successfully\n", eventName); \ - } \ - break; \ - } \ - default: \ - break; \ - } \ - } while (opt != 'Q'); \ -} - -const char* get_skiprestriction_enum_string(Advertising_SkipRestriction skipRestriction) -{ - char* strSkipRestriction; - switch(skipRestriction) { - case ADVERTISING_SKIPRESTRICTION_NONE: - strSkipRestriction = "None"; - break; - case ADVERTISING_SKIPRESTRICTION_ADS_UNWATCHED: - strSkipRestriction = "AdsUnwatched"; - break; - case ADVERTISING_SKIPRESTRICTION_ADS_ALL: - strSkipRestriction = "AdsAll"; - break; - case ADVERTISING_SKIPRESTRICTION_ALL: - strSkipRestriction = "All"; - break; - default: - strSkipRestriction = "None"; - break; - } - return strSkipRestriction; -} - -static const char deviceNameTestStr[] = "DeviceNameTestStr"; -static void NotifyDeviceNameChange(const void* userData, Firebolt_String_t handle) -{ - if (handle) { - printf("Got new device.name :%s\n", Firebolt_String(handle)); - Firebolt_String_Release(handle); - } else { - printf("device.name event handle is not valid\n"); - } -} - -static const char deviceScreenResolutionTestStr[] = "deviceScreenResolutionTestStr"; -static void NotifyDeviceScreenResolutionChange(const void* userData, Device_ResolutionArray_t handle) -{ - if (Device_ResolutionArray_IsValid(handle) == true) { - uint32_t size = Device_ResolutionArray_Size(handle); - printf("Device ScreenResolutions changed for %d numbers\n", size); - for (uint32_t i = 0; i < size; ++i) { - printf("New reslution[%d] = %d\n", i, Device_ResolutionArray_Get(handle, i)); - } - Device_ResolutionArray_Release(handle); - } else { - printf("device.screenresolution event handle is not valid\n"); - } -} - -static const char accessibilityVoiceGuidanceTestStr[] = "AccessibilityVoiceGuidanceTestStr"; -static void NotifyAccessibilityVoiceGuidanceChange(const void* userData, Accessibility_VoiceGuidanceSettings_t handle) -{ - if (Accessibility_VoiceGuidanceSettings_IsValid(handle) == true) { - bool enabled = Accessibility_VoiceGuidanceSettings_Get_Enabled(handle); - uint32_t speed = Accessibility_VoiceGuidanceSettings_Get_Speed(handle); - printf("VoiceGuidanceSettings: Enabled : %d, Speed : %d\n", enabled, speed); - Accessibility_VoiceGuidanceSettings_Release(handle); - } else { - printf("accessibility.voiceguidance event handle is not valid\n"); - } -} - -static const char discoveryHomeIntentTestStr[] = "DiscoveryHomeIntentTestStr"; -static void NotifyOnNavigateToHomeIntent(const void* userData, Intents_HomeIntent_t handle) -{ - if (Intents_HomeIntent_IsValid(handle) == true) { - char* action = Intents_HomeIntent_Get_Action(handle); - printf("NavigateToHomeIntent: Action: %s\n", action); - Intents_HomeIntent_Release(handle); - } else { - printf("discovery.onNavigateToChanged HomeIntent event handle is not valid\n"); - } -} - -static const char discoverySearchIntentTestStr[] = "DiscoverySearchIntentTestStr"; -static void NotifyOnNavigateToSearchIntent(const void* userData, Intents_SearchIntent_t handle) -{ - if (Intents_SearchIntent_IsValid(handle) == true) { - char* action = Intents_SearchIntent_Get_Action(handle); - printf("NavigateToSearchIntent: Action: %s\n", action); - Intents_SearchIntent_Release(handle); - } else { - printf("discovery.onNavigateTo LauncIntent event handle is not valid\n"); - } -} - -int main (int argc, char* argv[]) -{ - char* config = "{\ - \"waitTime\": 1000,\ - \"logLevel\": \"Info\",\ - \"workerPool\":{\ - \"queueSize\": 8,\ - \"threadCount\": 3\ - },\ - \"wsUrl\": \"ws://127.0.0.1:9998\"\ -}"; - - printf("Firebolt Core SDK Test\n"); - - //Intitialize the SDK - FireboltSDK_Initialize(config); - int option; - - do { - ShowMenu(); - printf("Enter option : "); - option = toupper(getchar()); - switch (option) { - case 'I': { - //Lets get the Device ID - Firebolt_String_t handle; - int32_t result = Device_GetId(&handle); - if (result == FireboltSDKErrorNone) { - printf("Device: Id:%s\n\n", Firebolt_String(handle)); - Firebolt_String_Release(handle); - handle = NULL; - } else { - printf("Failed to get Device ID\n\n"); - } - break; - } - case 'C': { - Accessibility_ClosedCaptionsSettings_t handle; - int32_t result = Accessibility_GetClosedCaptionsSettings(&handle); - - if (result == FireboltSDKErrorNone) { - if (Accessibility_ClosedCaptionsSettings_IsValid(handle) == true) { - printf("ClosedCaption Settings ------------------\n"); - Accessibility_ClosedCaptionsStyles_t styleHandle = Accessibility_ClosedCaptionsSettings_Get_Styles(handle); - if (Accessibility_ClosedCaptionsStyles_IsValid(styleHandle)) { - printf("ClosedCaptionStyles:\n"); - char* fontFamily = Accessibility_ClosedCaptionsStyles_Get_FontFamily(styleHandle); - printf("\tFontFamily : %s\n", fontFamily); - uint32_t fontSize = Accessibility_ClosedCaptionsStyles_Get_FontSize(styleHandle); - printf("\tFontSize : %d\n", fontSize); - char* fontColor = Accessibility_ClosedCaptionsStyles_Get_FontColor(styleHandle); - printf("\tFontColor : %s\n", fontColor); - char* fontEdge = Accessibility_ClosedCaptionsStyles_Get_FontEdge(styleHandle); - printf("\tFontEdge : %s\n", fontEdge); - char* fontEdgeColor = Accessibility_ClosedCaptionsStyles_Get_FontEdgeColor(styleHandle); - printf("\tFontEdgeColor : %s\n", fontEdgeColor); - uint32_t fontOpacity = Accessibility_ClosedCaptionsStyles_Get_FontOpacity(styleHandle); - printf("\tFontOpacity : %d\n", fontOpacity); - char* bgColor = Accessibility_ClosedCaptionsStyles_Get_BackgroundColor(styleHandle); - printf("\tBackgroundColor : %s\n", bgColor); - uint32_t bgOpacity = Accessibility_ClosedCaptionsStyles_Get_BackgroundOpacity(styleHandle); - printf("\tBackgroundOpacity : %d\n", bgOpacity); - char* txAlign = Accessibility_ClosedCaptionsStyles_Get_TextAlign(styleHandle); - printf("\tTextAlign : %s\n", txAlign); - char* txAlignVertical = Accessibility_ClosedCaptionsStyles_Get_TextAlignVertical(styleHandle); - printf("\tTextAlignVertical : %s\n", txAlignVertical); - Accessibility_ClosedCaptionsStyles_Release(styleHandle); - } - bool enabled = Accessibility_ClosedCaptionsSettings_Get_Enabled(handle); - printf("Enabled: %d\n\n", enabled); - Accessibility_ClosedCaptionsSettings_Release(handle); - } else { - printf("Invalid ClosedCaptionsSettings_t\n\n"); - } - } else { - printf("Failed to get Closed Caption Settings\n\n"); - } - break; - } - case 'G': { - Accessibility_VoiceGuidanceSettings_t handle; - int32_t result = Accessibility_GetVoiceGuidanceSettings(&handle); - - if (result == FireboltSDKErrorNone) { - if (Accessibility_VoiceGuidanceSettings_IsValid(handle) == true) { - bool enabled = Accessibility_VoiceGuidanceSettings_Get_Enabled(handle); - uint32_t speed = Accessibility_VoiceGuidanceSettings_Get_Speed(handle); - printf("VoiceGuidanceSettings: Enabled : %d, Speed : %d\n", enabled, speed); - Accessibility_VoiceGuidanceSettings_Release(handle); - } else { - printf("Invalid VoiceGuidanceSettings_t\n\n"); - } - } else { - printf("Failed to get Voice Guidance Settings\n\n"); - } - break; - } - case 'P': { - Advertising_AdPolicy_t handle; - int32_t result = Advertising_GetPolicy(&handle); - if (result == FireboltSDKErrorNone) { - if (Advertising_AdPolicy_IsValid(handle) == true) { - printf("AdPolicy: "); - Advertising_SkipRestriction skipRestriction = Advertising_AdPolicy_Get_SkipRestriction(handle); - printf("SkipRestriction = %s ", get_skiprestriction_enum_string(skipRestriction)); - bool limitAdTracking = Advertising_AdPolicy_Get_LimitAdTracking(handle); - printf("LimitAdTracking = %s \n", limitAdTracking? "true" : "false"); - Advertising_AdPolicy_Release(handle); - } else { - printf("Invalid Advertising_AdPolicy_t\n\n"); - } - } else { - printf("Failed to get Advertising Policy\n\n"); - } - break; - } - case 'U': { - Firebolt_String_t handle; - int32_t result = Account_GetUid(&handle); - if (result == FireboltSDKErrorNone) { - printf("Account: Uid:%s\n\n", Firebolt_String(handle)); - Firebolt_String_Release(handle); - handle = NULL; - } else { - printf("Failed to get Advertising Policy\n\n"); - } - break; - } - case 'E': { - { - Discovery_EntityInfoResult_t entityInfoResult = Discovery_EntityInfoResult_Acquire(); - Entertainment_EntityInfo_t entityInfo = Entertainment_EntityInfo_Acquire(); - - //Populate the ContentIdentifiers Object - Entertainment_ContentIdentifiers_t ci = Entertainment_ContentIdentifiers_Acquire(); - Entertainment_ContentIdentifiers_Set_AssetId(ci, "12345678"); - Entertainment_ContentIdentifiers_Set_EntityId(ci, "ABCDEFGH"); - Entertainment_ContentIdentifiers_Set_SeasonId(ci, "1"); - Entertainment_ContentIdentifiers_Set_SeriesId(ci, "9ER34FR"); - Entertainment_ContentIdentifiers_Set_AppContentData(ci, "Sample App Content Data"); - - //Populate EntityInfo Object - //Set the ContentIdentifiers Object to EntityInfo Object - Entertainment_EntityInfo_Set_Identifiers(entityInfo, ci); - Entertainment_ContentIdentifiers_Release(ci); - Entertainment_EntityInfo_Set_Title(entityInfo, "Game Of Thrones"); - Entertainment_EntityInfo_Set_EntityType(entityInfo, "program"); - Entertainment_EntityInfo_Set_ProgramType(entityInfo, ENTERTAINMENT_PROGRAMTYPE_SEASON); - Entertainment_EntityInfo_Set_Synopsis(entityInfo, "The imaginary continent of Westeros was once inhabited by a magical people, the Children of the Forest. For centuries, other people came across the Narrow Sea from the eastern continent of Essos: up North, the First Men; in the Center, the Andals; down South, the Dornish."); - Entertainment_EntityInfo_Set_SeasonNumber(entityInfo, 1); - Entertainment_EntityInfo_Set_EpisodeNumber(entityInfo, 1); - Entertainment_EntityInfo_Set_ReleaseDate(entityInfo, "2012-04-23T18:25:43.511Z"); - - { - Entertainment_ContentRatingArray_t crArray = Entertainment_ContentRatingArray_Acquire(); - { - //Populate the ContentRatings Handle for US TV scheme - Entertainment_ContentRating_t cr = Entertainment_ContentRating_Acquire(); - Entertainment_ContentRating_Set_Scheme(cr, ENTERTAINMENT_CONTENTRATING_SCHEME_US_TV); - Entertainment_ContentRating_Set_Rating(cr, "TVMA"); - Entertainment_ContentRating_AdvisoriesArray_t crAdvisories = Entertainment_ContentRating_AdvisoriesArray_Acquire(); - Entertainment_ContentRating_AdvisoriesArray_Add(crAdvisories, "V"); - Entertainment_ContentRating_AdvisoriesArray_Add(crAdvisories, "S"); - Entertainment_ContentRating_AdvisoriesArray_Add(crAdvisories, "L"); - Entertainment_ContentRating_Set_Advisories(cr, crAdvisories); - Entertainment_ContentRating_AdvisoriesArray_Release(crAdvisories); - Entertainment_ContentRatingArray_Add(crArray, cr); - } - { - //Populate the ContentRatings Handle for US TV scheme - Entertainment_ContentRating_t cr = Entertainment_ContentRating_Acquire(); - Entertainment_ContentRating_Set_Scheme(cr, ENTERTAINMENT_CONTENTRATING_SCHEME_CA_TV); - Entertainment_ContentRating_Set_Rating(cr, "18+"); - Entertainment_ContentRating_AdvisoriesArray_t crAdvisories = Entertainment_ContentRating_AdvisoriesArray_Acquire(); - Entertainment_ContentRating_AdvisoriesArray_Add(crAdvisories, "18+"); - Entertainment_ContentRating_Set_Advisories(cr, crAdvisories); - Entertainment_ContentRating_AdvisoriesArray_Release(crAdvisories); - Entertainment_ContentRatingArray_Add(crArray, cr); - } - Entertainment_EntityInfo_Set_ContentRatings(entityInfo, crArray); - Entertainment_ContentRatingArray_Release(crArray); - } - - //Populate WayToWatch Array in EntityInfo - { - //Populate the WayToWatch Object - Entertainment_WayToWatch_t w2w = Entertainment_WayToWatch_Acquire(); - { - //Add the ContentIdentifiers Object in WayToWatch - Entertainment_ContentIdentifiers_t ciI = Entertainment_ContentIdentifiers_Acquire(); - Entertainment_ContentIdentifiers_Set_AssetId(ciI, "12345678"); - Entertainment_ContentIdentifiers_Set_EntityId(ciI, "ABCDEFGH"); - Entertainment_ContentIdentifiers_Set_SeasonId(ciI, "1"); - Entertainment_ContentIdentifiers_Set_SeriesId(ciI, "9ER34FR"); - Entertainment_ContentIdentifiers_Set_AppContentData(ciI, "Sample App Content Data"); - Entertainment_WayToWatch_Set_Identifiers(w2w, ciI); - Entertainment_ContentIdentifiers_Release(ciI); - } - Entertainment_WayToWatch_Set_Expires(w2w, "2014-04-23T18:25:43.511Z"); - Entertainment_WayToWatch_Set_Entitled(w2w, true); - Entertainment_WayToWatch_Set_EntitledExpires(w2w, "2014-04-23T18:25:43.511Z"); - Entertainment_WayToWatch_Set_OfferingType(w2w, ENTERTAINMENT_OFFERINGTYPE_FREE); - Entertainment_WayToWatch_Set_HasAds(w2w, true); - - //Populate VideoQuality Array - Entertainment_WayToWatch_VideoQualityArray_t vqArray = - Entertainment_WayToWatch_VideoQualityArray_Acquire(); - Entertainment_WayToWatch_VideoQualityArray_Add( - vqArray, ENTERTAINMENT_WAYTOWATCH_VIDEOQUALITY_HD); - Entertainment_WayToWatch_VideoQualityArray_Add( - vqArray, ENTERTAINMENT_WAYTOWATCH_VIDEOQUALITY_UHD); - Entertainment_WayToWatch_Set_VideoQuality(w2w, vqArray); - Entertainment_WayToWatch_VideoQualityArray_Release(vqArray); - - //Populate AudioProfile Array - Entertainment_AudioProfileArray_t apArray = - Entertainment_AudioProfileArray_Acquire(); - Entertainment_AudioProfileArray_Add( - apArray, TYPES_AUDIOPROFILE_STEREO); - Entertainment_AudioProfileArray_Add( - apArray, TYPES_AUDIOPROFILE_DOLBY_DIGITAL_5_1); - Entertainment_AudioProfileArray_Add( - apArray, TYPES_AUDIOPROFILE_DOLBY_DIGITAL_5_1_PLUS); - Entertainment_WayToWatch_Set_AudioProfile(w2w, apArray); - Entertainment_AudioProfileArray_Release(apArray); - - //Populate AudioLanguages Array - Entertainment_WayToWatch_AudioLanguagesArray_t alArray = - Entertainment_WayToWatch_AudioLanguagesArray_Acquire(); - Entertainment_WayToWatch_AudioLanguagesArray_Add(alArray, "en"); - Entertainment_WayToWatch_AudioLanguagesArray_Add(alArray, "fr"); - Entertainment_WayToWatch_Set_AudioLanguages(w2w, alArray); - Entertainment_WayToWatch_AudioLanguagesArray_Release(alArray); - - //Populate ClosedCaptions Array - Entertainment_WayToWatch_ClosedCaptionsArray_t ccArray = - Entertainment_WayToWatch_ClosedCaptionsArray_Acquire(); - Entertainment_WayToWatch_ClosedCaptionsArray_Add(ccArray, "en"); - Entertainment_WayToWatch_ClosedCaptionsArray_Add(ccArray, "fr"); - Entertainment_WayToWatch_Set_ClosedCaptions(w2w, ccArray); - Entertainment_WayToWatch_ClosedCaptionsArray_Release(ccArray); - - //Populate Subtitles Array - Entertainment_WayToWatch_SubtitlesArray_t sbArray = - Entertainment_WayToWatch_SubtitlesArray_Acquire(); - Entertainment_WayToWatch_SubtitlesArray_Add(sbArray, "en"); - Entertainment_WayToWatch_SubtitlesArray_Add(sbArray, "fr"); - Entertainment_WayToWatch_Set_Subtitles(w2w, sbArray); - Entertainment_WayToWatch_SubtitlesArray_Release(sbArray); - - //Populate AudioDescriptions Array - Entertainment_WayToWatch_AudioDescriptionsArray_t adArray = - Entertainment_WayToWatch_AudioDescriptionsArray_Acquire(); - Entertainment_WayToWatch_AudioDescriptionsArray_Add(adArray, "en"); - Entertainment_WayToWatch_Set_AudioDescriptions(w2w, adArray); - Entertainment_WayToWatch_AudioDescriptionsArray_Release(adArray); - - //Populate WayToWatch Array - Entertainment_WayToWatchArray_t w2wArray = - Entertainment_WayToWatchArray_Acquire(); - Entertainment_WayToWatchArray_Add(w2wArray, w2w); - Entertainment_EntityInfo_Set_WaysToWatch(entityInfo, w2wArray); - Entertainment_WayToWatchArray_Release(w2wArray); - Entertainment_WayToWatch_Release(w2w); - } - - //Populate EntityInfoResult Object - Discovery_EntityInfoResult_Set_Expires(entityInfoResult, "2012-06-23T18:25:43.511Z"); - Discovery_EntityInfoResult_Set_Entity(entityInfoResult, entityInfo); - Entertainment_EntityInfo_Release(entityInfo); - - //All Set, Call the Push - int32_t result = Discovery_PushEntityInfo(entityInfoResult); - if (result == FireboltSDKErrorNone) { - printf("\nSuccessfully Pushed entityInfo\n"); - - } else { - printf("\nFailed to Push entityInfo\n"); - } - Discovery_EntityInfoResult_Release(entityInfoResult); - break; - } - } - case 'R': { - HandleEventListener(Device, ScreenResolutionChanged, (const void*)NotifyDeviceScreenResolutionChange, deviceScreenResolutionTestStr, "device.screenresolution") - break; - } - case 'N': { - HandleEventListener(Device, NameChanged, (const void*)NotifyDeviceNameChange, deviceNameTestStr, "device.name") - break; - } - case 'A': { - HandleEventListener(Accessibility, VoiceGuidanceSettingsChanged, (const void*)NotifyAccessibilityVoiceGuidanceChange, accessibilityVoiceGuidanceTestStr, "accessibility.voiceguidance") - break; - } - case 'H': { - HandleEventListener(Discovery, NavigateToHomeIntent, (const void*)NotifyOnNavigateToHomeIntent, discoveryHomeIntentTestStr, "discovery.onNavigateTo") - break; - } - case 'S': { - HandleEventListener(Discovery, NavigateToSearchIntent, (const void*)NotifyOnNavigateToSearchIntent, discoverySearchIntentTestStr, "discovery.onNavigateTo") - break; - } - default: - break; - } - getchar(); // Skip white space - } while (option != 'Q'); - - FireboltSDK_Deinitialize(); - - return 0; -} diff --git a/src/sdks/core/src/cpp/sdk/test/CMakeLists.txt b/src/sdks/core/src/cpp/sdk/test/CMakeLists.txt index 9242ea10e..b196ac143 100644 --- a/src/sdks/core/src/cpp/sdk/test/CMakeLists.txt +++ b/src/sdks/core/src/cpp/sdk/test/CMakeLists.txt @@ -27,7 +27,9 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON) find_package(${NAMESPACE}Core CONFIG REQUIRED) file(GLOB CPP_SOURCES *.cpp) -add_library(${TESTLIB} STATIC ${CPP_SOURCES} CoreSDKTestGeneratedCode.c) +list(REMOVE_ITEM CPP_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/Main.cpp) +message("After CPP_SOURCES " ${CPP_SOURCES}) +add_library(${TESTLIB} STATIC ${CPP_SOURCES}) target_link_libraries(${TESTLIB} PRIVATE @@ -44,7 +46,7 @@ target_include_directories(${TESTLIB} ) set_target_properties(${TESTLIB} PROPERTIES - CXX_STANDARD 11 + CXX_STANDARD 17 CXX_STANDARD_REQUIRED YES LINK_WHAT_YOU_USE TRUE FRAMEWORK FALSE @@ -54,7 +56,7 @@ set(TESTAPP FireboltCoreSDKTestApp) message("Setup ${TESTAPP} v${PROJECT_VERSION}") -add_executable(${TESTAPP} Main.c) +add_executable(${TESTAPP} Main.cpp) target_link_libraries(${TESTAPP} PRIVATE diff --git a/src/sdks/core/src/cpp/sdk/test/CoreSDKTestGeneratedCode.c b/src/sdks/core/src/cpp/sdk/test/CoreSDKTestGeneratedCode.c deleted file mode 100644 index cbb0fa76c..000000000 --- a/src/sdks/core/src/cpp/sdk/test/CoreSDKTestGeneratedCode.c +++ /dev/null @@ -1,330 +0,0 @@ -/* - * Copyright 2023 Comcast Cable Communications Management, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#include - -#include "CoreSDKCTests.h" -#include "CoreSDKTestGeneratedCode.h" - -#include "accessibility.h" -#include "advertising.h" -#include "common/types.h" -#include "device.h" -#include "lifecycle.h" - -#ifdef __cplusplus -extern "C" { -#endif - -int32_t test_generated_properties_get_device_id() -{ - Firebolt_String_t handle; - int32_t result = Device_GetId(&handle); - - if (result == FireboltSDKErrorNone) { - printf("\nDevice: Id:%s\n", Firebolt_String(handle)); - Firebolt_String_Release(handle); - } - - EXPECT_EQ(result, FireboltSDKErrorNone); - return result; -} -#if 0 -int32_t test_generated_properties_get_device_version() -{ - Device_Versions_t handle; - int32_t result = Device_GetVersion(&handle); - - if (result == FireboltSDKErrorNone) { - if (Device_Versions_IsValid(handle)) { - Types_SemanticVersion_t sdkHandle = Device_Versions_Get_Sdk(handle); - if (Types_SemanticVersion_IsValid(sdkHandle)) { - uint32_t major = Types_SemanticVersion_Get_Major(sdkHandle); - uint32_t minor = Types_SemanticVersion_Get_Minor(sdkHandle); - uint32_t patch = Types_SemanticVersion_Get_Patch(sdkHandle); - char* readable = Types_SemanticVersion_Get_Readable(sdkHandle); - printf("\nDevice:SDK Version major:%d minor:%d patch:%d readable:%s\n", - major, minor, patch, readable); - Types_SemanticVersion_Release(sdkHandle); - result = FireboltSDKErrorNone; - } - Types_SemanticVersion_t osHandle = Device_Versions_Get_Os(handle); - if (Types_SemanticVersion_IsValid(osHandle)) { - uint32_t major = Types_SemanticVersion_Get_Major(osHandle); - uint32_t minor = Types_SemanticVersion_Get_Minor(osHandle); - uint32_t patch = Types_SemanticVersion_Get_Patch(osHandle); - const char* readable = Types_SemanticVersion_Get_Readable(osHandle); - printf("\nDevice:OS Version major:%d minor:%d patch:%d readable:%s\n", - major, minor, patch, readable); - Types_SemanticVersion_Release(osHandle); - result = FireboltSDKErrorNone; - } - Device_Versions_Release(handle); - } else { - result = FireboltSDKErrorUnavailable; - } - } - - EXPECT_EQ(result, FireboltSDKErrorNone); - return result; -} -#endif -int32_t test_generated_properties_get_accessibility_closedcaption() -{ - Accessibility_ClosedCaptionsSettings_t handle; - int32_t result = Accessibility_GetClosedCaptionsSettings(&handle); - - if (result == FireboltSDKErrorNone) { - if (Accessibility_ClosedCaptionsSettings_IsValid(handle) == true) { - printf("ClosedCaption Settings ------------------\n"); - Accessibility_ClosedCaptionsStyles_t styleHandle = Accessibility_ClosedCaptionsSettings_Get_Styles(handle); - if (Accessibility_ClosedCaptionsStyles_IsValid(styleHandle)) { - printf("ClosedCaptionStyles:\n"); - char* fontFamily = Accessibility_ClosedCaptionsStyles_Get_FontFamily(styleHandle); - printf("\tFontFamily : %s\n", fontFamily); - uint32_t fontSize = Accessibility_ClosedCaptionsStyles_Get_FontSize(styleHandle); - printf("\tFontSize : %d\n", fontSize); - char* fontColor = Accessibility_ClosedCaptionsStyles_Get_FontColor(styleHandle); - printf("\tFontColor : %s\n", fontColor); - char* fontEdge = Accessibility_ClosedCaptionsStyles_Get_FontEdge(styleHandle); - printf("\tFontEdge : %s\n", fontEdge); - char* fontEdgeColor = Accessibility_ClosedCaptionsStyles_Get_FontEdgeColor(styleHandle); - printf("\tFontEdgeColor : %s\n", fontEdgeColor); - uint32_t fontOpacity = Accessibility_ClosedCaptionsStyles_Get_FontOpacity(styleHandle); - printf("\tFontOpacity : %d\n", fontOpacity); - char* bgColor = Accessibility_ClosedCaptionsStyles_Get_BackgroundColor(styleHandle); - printf("\tBackgroundColor : %s\n", bgColor); - uint32_t bgOpacity = Accessibility_ClosedCaptionsStyles_Get_BackgroundOpacity(styleHandle); - printf("\tBackgroundOpacity : %d\n", bgOpacity); - char* txAlign = Accessibility_ClosedCaptionsStyles_Get_TextAlign(styleHandle); - printf("\tTextAlign : %s\n", txAlign); - char* txAlignVertical = Accessibility_ClosedCaptionsStyles_Get_TextAlignVertical(styleHandle); - printf("\tTextAlignVertical : %s\n", txAlignVertical); - Accessibility_ClosedCaptionsStyles_Release(styleHandle); - } - bool enabled = Accessibility_ClosedCaptionsSettings_Get_Enabled(handle); - printf("Enabled: %d\n", enabled); - Accessibility_ClosedCaptionsSettings_Release(handle); - } else { - result = FireboltSDKErrorUnavailable; - } - } - - EXPECT_EQ(result, FireboltSDKErrorNone); - return result; -} -int32_t test_generated_properties_get_accessibility_voiceguidancesettings() -{ - Accessibility_VoiceGuidanceSettings_t handle; - int32_t result = Accessibility_GetVoiceGuidanceSettings(&handle); - - if (result == FireboltSDKErrorNone) { - if (Accessibility_VoiceGuidanceSettings_IsValid(handle) == true) { - bool enabled = Accessibility_VoiceGuidanceSettings_Get_Enabled(handle); - uint32_t speed = Accessibility_VoiceGuidanceSettings_Get_Speed(handle); - printf("VoiceGuidanceSettings: Enabled : %d, Speed : %d\n", enabled, speed); - Accessibility_VoiceGuidanceSettings_Release(handle); - } else { - result = FireboltSDKErrorUnavailable; - } - } - EXPECT_EQ(result, FireboltSDKErrorNone); - return result; -} -const char* get_skiprestriction_enum_string(Advertising_SkipRestriction skipRestriction) -{ - const char* strSkipRestriction; - switch(skipRestriction) { - case ADVERTISING_SKIPRESTRICTION_NONE: - strSkipRestriction = "None"; - break; - case ADVERTISING_SKIPRESTRICTION_ADS_UNWATCHED: - strSkipRestriction = "AdsUnwatched"; - break; - case ADVERTISING_SKIPRESTRICTION_ADS_ALL: - strSkipRestriction = "AdsAll"; - break; - case ADVERTISING_SKIPRESTRICTION_ALL: - strSkipRestriction = "All"; - break; - default: - strSkipRestriction = "None"; - break; - } - return strSkipRestriction; -} - -int32_t test_generated_properties_get_advertising_policy() -{ - Advertising_AdPolicy_t handle; - int32_t result = Advertising_GetPolicy(&handle); - if (result == FireboltSDKErrorNone) { - if (Advertising_AdPolicy_IsValid(handle) == true) { - printf("AdPolicy: "); - Advertising_SkipRestriction skipRestriction = Advertising_AdPolicy_Get_SkipRestriction(handle); - printf("SkipRestriction = %s ", get_skiprestriction_enum_string(skipRestriction)); - bool limitAdTracking = Advertising_AdPolicy_Get_LimitAdTracking(handle); - printf("LimitAdTracking = %s \n", limitAdTracking? "true" : "false"); - Advertising_AdPolicy_Release(handle); - } else { - result = FireboltSDKErrorUnavailable; - } - } - EXPECT_EQ(result, FireboltSDKErrorNone); - return result; -} - -#include -#include -pthread_cond_t cond = PTHREAD_COND_INITIALIZER; -pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; - -static const char deviceNameTestStr[] = "DeviceNameTestStr"; -static void NotifyDeviceNameChange(const void* userData, Firebolt_String_t handle) -{ - EXPECT_NE(handle, NULL); - if (handle) { - printf("\nGot new device.name :%s\n", Firebolt_String(handle)); - Firebolt_String_Release(handle); - } - EXPECT_EQ(strncmp((const char*)userData, deviceNameTestStr, strlen(deviceNameTestStr)), 0); - pthread_cond_signal(&cond); -} -int32_t test_generated_event_device_name() -{ - int32_t result = Device_Register_OnNameChanged((const void*)&NotifyDeviceNameChange, deviceNameTestStr); - EXPECT_EQ(result, FireboltSDKErrorNone); - if (result != FireboltSDKErrorNone) { - printf("Set event device.name status = %d \n", result); - } else { - printf("Set event device.name registered successfully\n"); - - pthread_mutex_lock(&lock); - printf("Waiting for device.name event\n"); - pthread_cond_wait(&cond, &lock); - pthread_mutex_unlock(&lock); - } - - result = Device_Unregister_OnNameChanged((const void*)NotifyDeviceNameChange); - EXPECT_EQ(result, FireboltSDKErrorNone); - return result; -} - -int32_t test_generated_event_device_name_with_register_same_callback() -{ - int32_t result = Device_Register_OnNameChanged((const void*)NotifyDeviceNameChange, deviceNameTestStr); - EXPECT_EQ(result, FireboltSDKErrorNone); - if (result != FireboltSDKErrorNone) { - printf("Set event device.name status = %d \n", result); - } else { - printf("Set event device.name registered successfully\n"); - result = Device_Register_OnNameChanged((const void*)NotifyDeviceNameChange, deviceNameTestStr); - EXPECT_EQ(result, FireboltSDKErrorInUse); - if (result == FireboltSDKErrorInUse) { - printf("%s Yes this device.name event is already registered with same callback\n", __func__); - } - } - - result = Device_Unregister_OnNameChanged((const void*)NotifyDeviceNameChange); - EXPECT_EQ(result, FireboltSDKErrorNone); - return result; -} -static const char deviceScreenResolutionTestStr[] = "deviceScreenResolutionTestStr"; -static void NotifyDeviceScreenResolutionChange(const void* userData, Device_ResolutionArray_t handle) -{ - EXPECT_EQ(Device_ResolutionArray_IsValid(handle), true); - if (Device_ResolutionArray_IsValid(handle) == true) { - uint32_t size = Device_ResolutionArray_Size(handle); - printf("Device ScreenResolutions changed for %d numbers\n", size); - for (uint32_t i = 0; i < size; ++i) { - printf("New reslution[%d] = %d\n", i, Device_ResolutionArray_Get(handle, i)); - } - Device_ResolutionArray_Release(handle); - } - EXPECT_EQ(strncmp((const char*)userData, deviceScreenResolutionTestStr, strlen(deviceScreenResolutionTestStr)), 0); - pthread_cond_signal(&cond); -} -int32_t test_generated_event_device_screenresolution() -{ - int32_t result = Device_Register_OnScreenResolutionChanged((const void*)NotifyDeviceScreenResolutionChange, deviceScreenResolutionTestStr); - EXPECT_EQ(result, FireboltSDKErrorNone); - if (result != FireboltSDKErrorNone) { - printf("Set event device.screenresolution status = %d \n", result); - } else { - printf("Set event device.screenresolution registered successfully\n"); - - pthread_mutex_lock(&lock); - printf("Waiting for device.screenresolution event\n"); - pthread_cond_wait(&cond, &lock); - pthread_mutex_unlock(&lock); - } - - result = Device_Unregister_OnScreenResolutionChanged((const void*)NotifyDeviceScreenResolutionChange); - EXPECT_EQ(result, FireboltSDKErrorNone); - return result; -} - -static const char accessibilityVoiceGuidanceTestStr[] = "AccessibilityVoiceGuidanceTestStr"; -static void NotifyAccessibilityVoiceGuidanceChange(const void* userData, Accessibility_VoiceGuidanceSettings_t handle) -{ - EXPECT_EQ(Accessibility_VoiceGuidanceSettings_IsValid(handle), true); - if (Accessibility_VoiceGuidanceSettings_IsValid(handle) == true) { - bool enabled = Accessibility_VoiceGuidanceSettings_Get_Enabled(handle); - uint32_t speed = Accessibility_VoiceGuidanceSettings_Get_Speed(handle); - printf("VoiceGuidanceSettings: Enabled : %d, Speed : %d\n", enabled, speed); - Accessibility_VoiceGuidanceSettings_Release(handle); - } - EXPECT_EQ(strncmp((const char*)userData, accessibilityVoiceGuidanceTestStr, strlen(accessibilityVoiceGuidanceTestStr)), 0); - pthread_cond_signal(&cond); -} -int32_t test_generated_event_accessibility_voice_guidance_settings() -{ - int32_t result = Accessibility_Register_OnVoiceGuidanceSettingsChanged((const void*)NotifyAccessibilityVoiceGuidanceChange, accessibilityVoiceGuidanceTestStr); - EXPECT_EQ(result, FireboltSDKErrorNone); - if (result != FireboltSDKErrorNone) { - printf("Set event device.name status = %d \n", result); - } else { - printf("Set event accessibility.voiceguidance registered successfully\n"); - - pthread_mutex_lock(&lock); - printf("Waiting for accessibility.voiceguidance event\n"); - pthread_cond_wait(&cond, &lock); - pthread_mutex_unlock(&lock); - } - - result = Accessibility_Unregister_OnVoiceGuidanceSettingsChanged((const void*)NotifyAccessibilityVoiceGuidanceChange); - EXPECT_EQ(result, FireboltSDKErrorNone); - return result; -} -#if 0 -int32_t test_generated_calls_metrics_lifecycle_ready() -{ - int32_t result = Lifecycle_Ready(); - if (result != FireboltSDKErrorNone) { - printf("Lifecycle.ready call status = %d \n", result); - } else { - printf("Lifecycle.ready is called successfully\n"); - } - // Just add sleep() to keep the test process in active for a while, since the test process should be in-active to invoke worker pool dispatcher (for calls-metrics mehods). This sleep is not required for actual apps case. - sleep(1); - return result; -} -#endif -#ifdef __cplusplus -} -#endif diff --git a/src/sdks/core/src/cpp/sdk/test/CoreSDKTestGeneratedCode.cpp b/src/sdks/core/src/cpp/sdk/test/CoreSDKTestGeneratedCode.cpp new file mode 100644 index 000000000..3265daf68 --- /dev/null +++ b/src/sdks/core/src/cpp/sdk/test/CoreSDKTestGeneratedCode.cpp @@ -0,0 +1,91 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "Module.h" +#include "firebolt.h" +#include "CoreSDKTestStaticCode.h" +#include "CoreSDKTestGeneratedCode.h" + +using namespace std; +bool CoreSDKTestGeneratedCode::_connected; + +void CoreSDKTestGeneratedCode::ConnectionChanged(const bool connected, const Firebolt::Error error) +{ + cout << "Change in connection: connected: " << connected << " error: " << static_cast(error) << endl; + _connected = connected; +} + +void CoreSDKTestGeneratedCode::CreateFireboltInstance() +{ + const std::string config = _T("{\ + \"waitTime\": 1000,\ + \"logLevel\": \"Info\",\ + \"workerPool\":{\ + \"queueSize\": 8,\ + \"threadCount\": 3\ + },\ + \"wsUrl\": \"ws://127.0.0.1:9998\"\ + }"); + + _connected = false; + Firebolt::IFireboltAccessor::Instance().Initialize(config); + Firebolt::IFireboltAccessor::Instance().Connect(ConnectionChanged); +} + +void CoreSDKTestGeneratedCode::DestroyFireboltInstance() +{ + Firebolt::IFireboltAccessor::Instance().Disconnect(); + Firebolt::IFireboltAccessor::Instance().Deinitialize(); + Firebolt::IFireboltAccessor::Instance().Dispose(); +} + +bool CoreSDKTestGeneratedCode::WaitOnConnectionReady() +{ + uint32_t waiting = 10000; + static constexpr uint32_t SLEEPSLOT_TIME = 100; + + // Right, a wait till connection is closed is requested.. + while ((waiting > 0) && (_connected == false)) { + + uint32_t sleepSlot = (waiting > SLEEPSLOT_TIME ? SLEEPSLOT_TIME : waiting); + // Right, lets sleep in slices of 100 ms + SleepMs(sleepSlot); + waiting -= sleepSlot; + } + return _connected; +} + +void CoreSDKTestGeneratedCode::TestCoreStaticSDK() +{ + FireboltSDK::Tests::Main(); +} + +void CoreSDKTestGeneratedCode::GetDeviceName() +{ + Firebolt::Error error = Firebolt::Error::None; + const std::string name = Firebolt::IFireboltAccessor::Instance().DeviceInterface().Name(&error); + + if (error == Firebolt::Error::None) { + cout << "Get DeviceName = " << name.c_str() << endl; + } else { + cout << "Get DeviceName status = " << static_cast(error) << endl; + } +} + diff --git a/src/sdks/core/src/cpp/sdk/test/CoreSDKTestGeneratedCode.h b/src/sdks/core/src/cpp/sdk/test/CoreSDKTestGeneratedCode.h index 4b9d12441..cfa3c86ae 100644 --- a/src/sdks/core/src/cpp/sdk/test/CoreSDKTestGeneratedCode.h +++ b/src/sdks/core/src/cpp/sdk/test/CoreSDKTestGeneratedCode.h @@ -16,27 +16,24 @@ * SPDX-License-Identifier: Apache-2.0 */ -#ifndef CORESDK_TEST_GENERATED_CODE_H -#define CORESDK_TEST_GENERATED_CODE_H +#pragma once +#include "error.h" -#ifdef __cplusplus -extern "C" { -#endif +class CoreSDKTestGeneratedCode { -//int32_t test_generated_properties_get_device_version(); -int32_t test_generated_properties_get_device_id(); -int32_t test_generated_properties_get_accessibility_closedcaption(); -int32_t test_generated_properties_get_accessibility_voiceguidancesettings(); -int32_t test_generated_properties_get_advertising_policy(); -int32_t test_generated_event_device_name(); -int32_t test_generated_event_device_name_with_register_same_callback(); -int32_t test_generated_event_device_screenresolution(); -int32_t test_generated_event_accessibility_voice_guidance_settings(); -//int32_t test_generated_calls_metrics_lifecycle_ready(); +public: + CoreSDKTestGeneratedCode() = default; + virtual ~CoreSDKTestGeneratedCode() = default; -#ifdef __cplusplus -} -#endif + static void CreateFireboltInstance(); + static void DestroyFireboltInstance(); + static void TestCoreStaticSDK(); + static void GetDeviceName(); + static bool WaitOnConnectionReady(); + +private: + static void ConnectionChanged(const bool, const Firebolt::Error); + static bool _connected; +}; -#endif // CORESDK_TEST_GENERATED_CODE_H diff --git a/src/sdks/core/src/cpp/sdk/test/CoreSDKTestStaticCode.cpp b/src/sdks/core/src/cpp/sdk/test/CoreSDKTestStaticCode.cpp new file mode 100644 index 000000000..8ac533e31 --- /dev/null +++ b/src/sdks/core/src/cpp/sdk/test/CoreSDKTestStaticCode.cpp @@ -0,0 +1,46 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "Module.h" +#include "CoreSDKTestStaticCode.h" + +namespace FireboltSDK { + CoreTestStaticCode::CoreTestStaticCode() + : Tests() + { + _functionMap.emplace(std::piecewise_construct, std::forward_as_tuple("Get Country Code"), + std::forward_as_tuple(&GetCountryCode)); + } + + /* static */ Firebolt::Error CoreTestStaticCode::GetCountryCode() + { + const string method = _T("localization.countryCode"); + WPEFramework::Core::ProxyType response; + Firebolt::Error status = FireboltSDK::Properties::Get(method, response); + + EXPECT_EQ(status, Firebolt::Error::None); + if (status == Firebolt::Error::None) { + FIREBOLT_LOG_INFO(Logger::Category::Core, Logger::Module(), "CountryCode : %s", response->Value().c_str()); + } else { + FIREBOLT_LOG_ERROR(Logger::Category::Core, Logger::Module(), "Get %s status = %d\n", method.c_str(), status); + } + + return status; + } +} + diff --git a/src/sdks/core/src/cpp/sdk/test/CoreSDKTests.h b/src/sdks/core/src/cpp/sdk/test/CoreSDKTestStaticCode.h similarity index 91% rename from src/sdks/core/src/cpp/sdk/test/CoreSDKTests.h rename to src/sdks/core/src/cpp/sdk/test/CoreSDKTestStaticCode.h index 480129e6d..8966112f9 100644 --- a/src/sdks/core/src/cpp/sdk/test/CoreSDKTests.h +++ b/src/sdks/core/src/cpp/sdk/test/CoreSDKTestStaticCode.h @@ -17,7 +17,6 @@ */ #pragma once - #include namespace FireboltSDK { @@ -51,12 +50,12 @@ namespace FireboltSDK { WPEFramework::Core::JSON::Boolean RememberWatchedPrograms; }; - class CoreTests : public Tests { + class CoreTestStaticCode : public Tests { public: - CoreTests(); - ~CoreTests() override = default; + CoreTestStaticCode(); + ~CoreTestStaticCode() override = default; - static int32_t GetDiscoveryPolicy(); + static Firebolt::Error GetCountryCode(); }; } diff --git a/src/sdks/core/src/cpp/sdk/test/CoreSDKTests.cpp b/src/sdks/core/src/cpp/sdk/test/CoreSDKTests.cpp deleted file mode 100644 index cfcca0686..000000000 --- a/src/sdks/core/src/cpp/sdk/test/CoreSDKTests.cpp +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2023 Comcast Cable Communications Management, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#include "Module.h" -#include "CoreSDKTests.h" - -namespace FireboltSDK { - CoreTests::CoreTests() - : Tests() - { - _functionMap.emplace(std::piecewise_construct, std::forward_as_tuple("Get Discovery Policy"), - std::forward_as_tuple(&GetDiscoveryPolicy)); - } - - /* static */ int32_t CoreTests::GetDiscoveryPolicy() - { - const string method = _T("discovery.policy"); - WPEFramework::Core::ProxyType response; - int32_t status = FireboltSDK::Properties::Get(method, response); - - EXPECT_EQ(status, FireboltSDKErrorNone); - if (status == FireboltSDKErrorNone) { - FIREBOLT_LOG_INFO(FireboltSDK::Logger::Category::Core, Logger::Module(), - "EnableRecommendations : %d", response->EnableRecommendations.Value()); - FIREBOLT_LOG_INFO(FireboltSDK::Logger::Category::Core, Logger::Module(), - "ShareWatchHistory : %d", response->ShareWatchHistory.Value()); - FIREBOLT_LOG_INFO(FireboltSDK::Logger::Category::Core, Logger::Module(), - "RememberWatchedPrograms : %d", response->RememberWatchedPrograms.Value()); - } else { - FIREBOLT_LOG_ERROR(FireboltSDK::Logger::Category::Core, Logger::Module(), - "Get %s status = %d", method.c_str(), status); - } - - return status; - } -} - -#ifdef __cplusplus -extern "C" { -#endif - -int32_t test_firebolt_core_main() -{ - return FireboltSDK::Tests::Main(); -} - -int32_t test_properties_get_policy() -{ - const string method = _T("discovery.policy"); - WPEFramework::Core::ProxyType response; - int32_t status = FireboltSDK::Properties::Get(method, response); - - EXPECT_EQ(status, FireboltSDKErrorNone); - if (status == FireboltSDKErrorNone) { - FIREBOLT_LOG_INFO(FireboltSDK::Logger::Category::Core, "ctest", - "EnableRecommendations : %d", response->EnableRecommendations.Value()); - FIREBOLT_LOG_INFO(FireboltSDK::Logger::Category::Core, "ctest", - "ShareWatchHistory : %d", response->ShareWatchHistory.Value()); - FIREBOLT_LOG_INFO(FireboltSDK::Logger::Category::Core, "ctest", - "RememberWatchedPrograms : %d", response->RememberWatchedPrograms.Value()); - } else { - FIREBOLT_LOG_ERROR(FireboltSDK::Logger::Category::Core, "ctest", - "Get %s status = %d", method.c_str(), status); - } - - return status; -} - -#ifdef __cplusplus -} -#endif diff --git a/src/sdks/core/src/cpp/sdk/test/Main.c b/src/sdks/core/src/cpp/sdk/test/Main.c deleted file mode 100644 index 2490dbb95..000000000 --- a/src/sdks/core/src/cpp/sdk/test/Main.c +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2023 Comcast Cable Communications Management, LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#include -#include "CoreSDKCTests.h" -#include "CoreSDKTestGeneratedCode.h" - -int __cnt = 0; -int __pass = 0; - -int TotalTests = 0; -int TotalTestsPassed = 0; - -int main() -{ - test_firebolt_create_instance(); - test_firebolt_core_main(); - - // Calling C function sequences - printf("%s:%s:%d Calling C function tests\n", __FILE__, __func__, __LINE__); - EXECUTE("test_properties_get_device_id", test_properties_get_device_id); - EXECUTE("test_properties_get_policy", test_properties_get_policy); - EXECUTE("test_properties_set", test_properties_set); - EXECUTE("test_eventregister_by_providing_callback", test_eventregister_by_providing_callback); - EXECUTE("test_eventregister", test_eventregister); - EXECUTE("test_string_set_get_value", test_string_set_get_value); - EXECUTE("test_generated_properties_get_device_id", test_generated_properties_get_device_id); - //EXECUTE("test_generated_properties_get_device_version", test_generated_properties_get_device_version); - EXECUTE("test_generated_properties_get_accessibility_closedcaption", test_generated_properties_get_accessibility_closedcaption); - EXECUTE("test_generated_properties_get_accessibility_voiceguidancesettings", test_generated_properties_get_accessibility_voiceguidancesettings); - EXECUTE("test_generated_properties_get_advertising_policy", test_generated_properties_get_advertising_policy); - EXECUTE("test_generated_event_device_name", test_generated_event_device_name); - EXECUTE("test_generated_event_device_name_with_register_same_callback", test_generated_event_device_name_with_register_same_callback); - EXECUTE("test_generated_event_device_screenresolution", test_generated_event_device_screenresolution); - EXECUTE("test_generated_event_accessibility_voice_guidance_settings", test_generated_event_accessibility_voice_guidance_settings); - //EXECUTE("test_generated_calls_metrics_lifecycle_ready", test_generated_calls_metrics_lifecycle_ready); - - test_firebolt_dispose_instance(); - - printf("TOTAL: %i tests; %i PASSED, %i FAILED\n", TotalTests, TotalTestsPassed, (TotalTests - TotalTestsPassed)); -} - diff --git a/src/sdks/core/src/cpp/sdk/test/Main.cpp b/src/sdks/core/src/cpp/sdk/test/Main.cpp new file mode 100644 index 000000000..bd4329d42 --- /dev/null +++ b/src/sdks/core/src/cpp/sdk/test/Main.cpp @@ -0,0 +1,38 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "CoreSDKTestGeneratedCode.h" + +int __cnt = 0; +int __pass = 0; + +int TotalTests = 0; +int TotalTestsPassed = 0; + +int main() +{ + CoreSDKTestGeneratedCode::CreateFireboltInstance(); + + if (CoreSDKTestGeneratedCode::WaitOnConnectionReady() == true) { + CoreSDKTestGeneratedCode::GetDeviceName(); + } + CoreSDKTestGeneratedCode::DestroyFireboltInstance(); + printf("TOTAL: %i tests; %i PASSED, %i FAILED\n", TotalTests, TotalTestsPassed, (TotalTests - TotalTestsPassed)); +} + diff --git a/src/sdks/core/src/cpp/templates/Metrics/src/module_impl.cpp b/src/sdks/core/src/cpp/templates/Metrics/src/module_impl.cpp new file mode 100644 index 000000000..59a50162d --- /dev/null +++ b/src/sdks/core/src/cpp/templates/Metrics/src/module_impl.cpp @@ -0,0 +1,96 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "${info.title.lowercase}_impl.h" + +${if.implementations} +namespace Firebolt { +namespace ${info.Title} { +${if.providers} +/* ${PROVIDERS} */${end.if.providers} + void ${info.Title}Impl::signIn() + { + Firebolt::Error status = Firebolt::Error::NotConnected; + FireboltSDK::Transport* transport = FireboltSDK::Accessor::Instance().GetTransport(); + if (transport != nullptr) { + + JsonObject jsonParameters; + + WPEFramework::Core::JSON::Boolean jsonResult; + status = transport->Invoke("${info.title.lowercase}.signIn", jsonParameters, jsonResult); + if (status == Firebolt::Error::None) { + FIREBOLT_LOG_INFO(FireboltSDK::Logger::Category::OpenRPC, FireboltSDK::Logger::Module(), "${info.Title.signIn is successfully invoked, status : %s", (jsonResult.Value() ? "true" : "false")); + } + + } else { + FIREBOLT_LOG_ERROR(FireboltSDK::Logger::Category::OpenRPC, FireboltSDK::Logger::Module(), "Error in getting Transport err = %d", status); + } + } + + void ${info.Title}Impl::signOut() + { + Firebolt::Error status = Firebolt::Error::NotConnected; + FireboltSDK::Transport* transport = FireboltSDK::Accessor::Instance().GetTransport(); + if (transport != nullptr) { + + JsonObject jsonParameters; + + WPEFramework::Core::JSON::Boolean jsonResult; + status = transport->Invoke("${info.title.lowercase}.signOut", jsonParameters, jsonResult); + if (status == Firebolt::Error::None) { + FIREBOLT_LOG_INFO(FireboltSDK::Logger::Category::OpenRPC, FireboltSDK::Logger::Module(), "${info.Title}.signOut is successfully invoked, status : %s", (jsonResult.Value() ? "true" : "false")); + } + + } else { + FIREBOLT_LOG_ERROR(FireboltSDK::Logger::Category::OpenRPC, FireboltSDK::Logger::Module(), "Error in getting Transport err = %d", status); + } + } + + void ${info.Title}Impl::ready() + { + Firebolt::Error status = Firebolt::Error::NotConnected; + FireboltSDK::Transport* transport = FireboltSDK::Accessor::Instance().GetTransport(); + if (transport != nullptr) { + + JsonObject jsonParameters; + + WPEFramework::Core::JSON::Boolean jsonResult; + status = transport->Invoke("${info.title.lowercase}.ready", jsonParameters, jsonResult); + if (status == Firebolt::Error::None) { + FIREBOLT_LOG_INFO(FireboltSDK::Logger::Category::OpenRPC, FireboltSDK::Logger::Module(), "${info.Title}.ready is successfully invoked, status : %s", (jsonResult.Value() ? "true" : "false")); + } + + } else { + FIREBOLT_LOG_ERROR(FireboltSDK::Logger::Category::OpenRPC, FireboltSDK::Logger::Module(), "Error in getting Transport err = %d", status); + } + } + + // Methods + /* ${METHODS} */ + + // Events + /* ${EVENTS} */ + +}//namespace ${info.Title} +}${end.if.implementations} +${if.enums} + +namespace WPEFramework { + +/* ${ENUMS} */ +}${end.if.enums} diff --git a/src/sdks/core/src/cpp/templates/Metrics/src/module_impl.h b/src/sdks/core/src/cpp/templates/Metrics/src/module_impl.h new file mode 100644 index 000000000..d64b59695 --- /dev/null +++ b/src/sdks/core/src/cpp/templates/Metrics/src/module_impl.h @@ -0,0 +1,52 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "FireboltSDK.h" +#include "IModule.h" +/* ${IMPORTS} */ +#include "${info.title.lowercase}.h" + +${if.implementations} +namespace Firebolt { +namespace ${info.Title} { +${if.enums} + +/* ${ENUMS:json-types} */${end.if.enums} +${if.types} + // Types +/* ${TYPES:json-types} */${end.if.types} + ${if.methods}class ${info.Title}Impl : public I${info.Title}, public IModule { + + public: + ${info.Title}Impl() = default; + ${info.Title}Impl(const ${info.Title}Impl&) = delete; + ${info.Title}Impl& operator=(const ${info.Title}Impl&) = delete; + + ~${info.Title}Impl() override = default; + + static void signIn(); + static void signOut(); + static void ready(); + // Methods & Events + /* ${METHODS:declarations-override} */ + };${end.if.methods} + +}//namespace ${info.Title} +}${end.if.implementations} diff --git a/src/sdks/manage/package.json b/src/sdks/manage/package.json index 4b354e5aa..400779cbd 100644 --- a/src/sdks/manage/package.json +++ b/src/sdks/manage/package.json @@ -12,6 +12,9 @@ "validate": "npx firebolt-openrpc validate --input ./dist/firebolt-manage-open-rpc.json", "sdk": "npx firebolt-openrpc sdk --input ./dist/firebolt-manage-open-rpc.json --template ./src/js --output ./build/javascript/src", "native": "npx firebolt-openrpc sdk --input ./dist/firebolt-manage-open-rpc.json --template ./src/js --output ./build/c/src --language ../../../node_modules/@firebolt-js/openrpc/languages/c", + "cpp": "npm run cpp:compile && npm run cpp:install", + "cpp:compile": "npx firebolt-openrpc sdk --input ./dist/firebolt-manage-open-rpc.json --template ./src/cpp --output ./build/cpp/src --static-module Platform --language ../../../node_modules/@firebolt-js/openrpc/languages/cpp", + "cpp:install":"./build/cpp/src/scripts/install.sh -i ./build/cpp/src -s ./build/cpp/src/ -m manage", "compile": "cd ../../.. && npm run compile", "slice": "npx firebolt-openrpc slice -i ../../../dist/firebolt-open-rpc.json --sdk ./sdk.config.json -o ./dist/firebolt-manage-open-rpc.json", "docs": "npx firebolt-openrpc docs --input ./dist/firebolt-manage-open-rpc.json --output build/docs/markdown --as-path", diff --git a/src/sdks/core/src/cpp/sdk/ctest/CMakeLists.txt b/src/sdks/manage/src/cpp/sdk/cpptest/CMakeLists.txt similarity index 91% rename from src/sdks/core/src/cpp/sdk/ctest/CMakeLists.txt rename to src/sdks/manage/src/cpp/sdk/cpptest/CMakeLists.txt index c87f15662..b5053ba20 100644 --- a/src/sdks/core/src/cpp/sdk/ctest/CMakeLists.txt +++ b/src/sdks/manage/src/cpp/sdk/cpptest/CMakeLists.txt @@ -16,7 +16,7 @@ cmake_minimum_required(VERSION 3.3) -project(FireboltCoreSDKTests) +project(FireboltManageSDKTests) if (CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) set(CMAKE_INSTALL_PREFIX "${SYSROOT_PATH}/usr" CACHE INTERNAL "" FORCE) @@ -42,11 +42,11 @@ find_package(${NAMESPACE}Core CONFIG REQUIRED) find_package(Firebolt CONFIG REQUIRED) find_package(${FIREBOLT_NAMESPACE}SDK CONFIG REQUIRED) -set(TESTAPP TestFireboltCore) +set(TESTAPP TestFireboltManage) message("Setup ${TESTAPP}") -add_executable(${TESTAPP} main.c) +add_executable(${TESTAPP} ManageSDKTest.cpp Main.cpp) target_link_libraries(${TESTAPP} PRIVATE @@ -61,6 +61,11 @@ target_include_directories(${TESTAPP} $ ) +set_target_properties(${TESTAPP} PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED YES +) + add_custom_command( TARGET ${TESTAPP} POST_BUILD diff --git a/src/sdks/manage/src/cpp/sdk/cpptest/Main.cpp b/src/sdks/manage/src/cpp/sdk/cpptest/Main.cpp new file mode 100644 index 000000000..1a0b1df3b --- /dev/null +++ b/src/sdks/manage/src/cpp/sdk/cpptest/Main.cpp @@ -0,0 +1,398 @@ +#include +#include "ManageSDKTest.h" + +void ShowMenu() +{ + printf("Options ---- >\n" + "\tA : Get/Set Advertising skipRestriction\n" + "\tT : Set Account Session\n" + "\tE : Get/Set/Subscribe/Unsubscribe AudioDescriptions Enabled\n" + "\tN : Get/Set/Subscribe/Unsubscribe Device Name\n" + "\tB : Get/Set/Subscribe/Unsubscribe ClosedCaption Background Opacity\n" + "\tF : Get/Set/Subscribe/Unsubscribe ClosedCaption Font Family\n" + "\tL : Get/Set/Subscribe/Unsubscribe Localization Preferred AudioLanguages\n" + "\tC : Get/Set/Subscribe/Unsubscribe Privacy Allow ACR Collection\n" + "\tS : Get Privacy Settings\n" + "\tI : Subscribe/Unsubscribe SignIn notification\n" + "\tO : Subscribe/Unsubscribe SignOut notification\n" + "\tR : Reset Advertising Identifier\n" + "\tK : Register for Keyboard Provider and check sequence\n" + "\tG : Register for Acknowledge Challenge Provider and check sequence\n" + "\tP : Register for Pin Challenge Provider and check sequence\n" + "\tD : Operate on Localization Additional Info\n" + "\tU : Grant/Deny/Clear permission on App\n" + "\tW : Scan Wifi on device\n" + "\tQ : Quit\n\n" + ); +} + +void ShowWifiOperationsMenu() +{ + printf("Options \n" + "\tS : Scan access points\n" + "\tC : Connect to selected access point \n" + "\tD : Disconnect from access point \n" + "\tQ : Quit\n"); +} + +void ShowUserGrantsMenu() +{ + printf("Invoke lifecyclesession management sequence from postman \n" + "Once the permission granted/denies/cleared, please use provider + api test to validate it \n" + "Here capabilty used for the testing is hardcoded as device:model\n" + "\tG : Get Permission\n" + "\tR : Grant Permission\n" + "\tD : Deny Permission\n" + "\tC : Clear Permission\n" + "\tQ : Quit\n"); +} + +void ShowAdditionalInfoMenu() +{ + printf("Options \n" + "\tG : Get Additional Info\n" + "\tA : Add Additional Info\n" + "\tR : Remove Additional Info\n" + "\tQ : Quit\n"); +} + +void ShowPropertyMenu(std::string& module, std::string& property) +{ + printf("%s:%s property options \n" + "\tG : Get value\n" + "\tS : Set value\n" + "\tQ : Quit\n", module.c_str(), property.c_str()); +} + +void ShowPropertyWithEventMenu(std::string& module, std::string& property) +{ + printf("%s:%s property options \n" + "\tG : Get value\n" + "\tS : Set value\n" + "\tR : Show subscribe/unscribe event menu\n" + "\tQ : Quit\n", module.c_str(), property.c_str()); +} + +void ShowProviderMenu(std::string& module) +{ + printf("Invoke lifecyclesession management from postman and api sequence from other entity for %s and press\n" + "\tR : To Send Response\n" + "\tE : To Send Error\n" + "\tQ : Quit\n", module.c_str()); +} + +void ShowEventMenu() +{ + printf("Options \n" + "\tS : Subscribe Event\n" + "\tU : Unsubscribe Event\n" + "\tQ : Quit\n"); +} + +void HandleUserGrants() +{ + int opt; + do { + getchar(); + ShowUserGrantsMenu(); + printf("Enter option : "); + opt = toupper(getchar()); + switch (opt) { + case 'G': { + ManageSDKTest::GetUserGrantsPermission(); + break; + } + case 'R': { + ManageSDKTest::GrantUserGrantsPermission(); + break; + } + case 'D': { + ManageSDKTest::DenyUserGrantsPermission(); + break; + } + case 'C': { + ManageSDKTest::ClearUserGrantsPermission(); + break; + } + default: + break; + } + } while (opt != 'Q'); +} + +void HandleAdditionalInfo() +{ + int opt; + do { + getchar(); + ShowAdditionalInfoMenu(); + printf("Enter option : "); + opt = toupper(getchar()); + switch (opt) { + case 'G': { + ManageSDKTest::GetLocalizationAdditionalInfo(); + break; + } + case 'A': { + ManageSDKTest::AddLocalizationAdditionalInfo(); + break; + } + case 'R': { + ManageSDKTest::RemoveLocalizationAdditionalInfo(); + break; + } + default: + break; + } + } while (opt != 'Q'); +} + +#define VALUE(string) #string +#define TO_STRING(string) VALUE(string) + +#define HandleEventListener(Module, eventFuncName) \ +{ \ + int opt; \ + do { \ + getchar(); \ + ShowEventMenu(); \ + printf("Enter option : "); \ + opt = toupper(getchar()); \ + switch (opt) { \ + case 'S': { \ + ManageSDKTest::Subscribe##Module##eventFuncName(); \ + break; \ + } \ + case 'U': { \ + ManageSDKTest::Unsubscribe##Module##eventFuncName(); \ + break; \ + } \ + default: \ + break; \ + } \ + } while (opt != 'Q'); \ +} + +#define HandleProperty(Module, Property) \ +{ \ + int opt; \ + do { \ + getchar(); \ + std::string module = TO_STRING(Module); \ + std::string property = TO_STRING(Property); \ + ShowPropertyMenu(module, property); \ + printf("Enter option : "); \ + opt = toupper(getchar()); \ + switch (opt) { \ + case 'G': { \ + ManageSDKTest::Get##Module##Property(); \ + break; \ + } \ + case 'S': { \ + ManageSDKTest::Set##Module##Property(); \ + break; \ + } \ + default: \ + break; \ + } \ + } while (opt != 'Q'); \ +} + + +#define HandlePropertyWithEvent(Module, Property) \ +{ \ + int opt; \ + do { \ + getchar(); \ + std::string module = TO_STRING(Module); \ + std::string property = TO_STRING(Property); \ + ShowPropertyWithEventMenu(module, property); \ + printf("Enter option : "); \ + opt = toupper(getchar()); \ + switch (opt) { \ + case 'G': { \ + ManageSDKTest::Get##Module##Property(); \ + break; \ + } \ + case 'S': { \ + ManageSDKTest::Set##Module##Property(); \ + break; \ + } \ + case 'R': { \ + HandleEventListener(Module, Property##Changed) \ + break; \ + } \ + default: \ + break; \ + } \ + } while (opt != 'Q'); \ +} + +#define HandleProviderSequence(Module) \ +{ \ + int opt; \ + do { \ + getchar(); \ + std::string module = TO_STRING(Module); \ + ShowProviderMenu(module); \ + printf("Enter option : "); \ + opt = toupper(getchar()); \ + switch (opt) { \ + case 'R': { \ + ManageSDKTest::SendResponseMessageTo##Module##Provider(); \ + break; \ + } \ + case 'E': { \ + ManageSDKTest::SendErrorMessageTo##Module##Provider(); \ + break; \ + } \ + default: \ + break; \ + } \ + } while (opt != 'Q'); \ +} + +void HandleWifiOperations() +{ + int opt; + do { + getchar(); + ShowWifiOperationsMenu(); + printf("Enter option : "); + opt = toupper(getchar()); + switch (opt) { + case 'S': { + ManageSDKTest::WifiScan(); + break; + } + case 'C': { + ManageSDKTest::WifiConnect(); + break; + } + case 'D': { + ManageSDKTest::WifiDisconnect(); // This method is not avaialble from ripple now + break; + } + default: + break; + } + } while (opt != 'Q'); +} + +#define options ":hu:" +int main (int argc, char* argv[]) +{ + int c; + std::string url = "ws://127.0.0.1:9998"; + while ((c = getopt (argc, argv, options)) != -1) { + switch (c) + { + case 'u': + url = optarg; + break; + + case 'h': + printf("./TestFireboltManage -u ws://ip:port\n"); + exit(1); + } + } + + printf("Firebolt Manage SDK Test\n"); + ManageSDKTest::CreateFireboltInstance(url); + int option; + + if (ManageSDKTest::WaitOnConnectionReady() == true) { + do { + ShowMenu(); + printf("Enter option : "); + option = toupper(getchar()); + switch (option) { + case 'A': { + HandleProperty(Advertising, SkipRestriction) + break; + } + case 'T': { + ManageSDKTest::SetAccountSession(); + break; + } + case 'E': { + HandlePropertyWithEvent(AudioDescriptions, Enabled) + break; + } + case 'N': { + HandlePropertyWithEvent(Device, Name) + break; + } + case 'B': { + HandlePropertyWithEvent(ClosedCaptions, BackgroundOpacity) + break; + } + case 'F': { + HandlePropertyWithEvent(ClosedCaptions, FontFamily) + break; + } + case 'L': { + HandlePropertyWithEvent(Localization, PreferredAudioLanguages) + break; + } + case 'C': { + HandlePropertyWithEvent(Privacy, AllowACRCollection) + break; + } + case 'S': { + ManageSDKTest::GetPrivacySettings(); + break; + } + case 'I': { + HandleEventListener(Discovery, SignInNotification) + break; + } + case 'O': { + HandleEventListener(Discovery, SignOutNotification) + break; + } + case 'R': { + ManageSDKTest::ResetAdvertisingIdentifier(); + break; + } + case 'K': { + ManageSDKTest::RegisterKeyboardProvider(); + HandleProviderSequence(Keyboard) + break; + } + case 'G': { + ManageSDKTest::RegisterAcknowledgeChallengeProvider(); + HandleProviderSequence(AcknowledgeChallenge) + break; + } + case 'P': { + ManageSDKTest::RegisterPinChallengeProvider(); + HandleProviderSequence(PinChallenge) + break; + } + case 'D': { + HandleAdditionalInfo(); + break; + } + case 'U': { + HandleUserGrants(); + break; + } + case 'W': { + HandleWifiOperations(); + break; + } + default: + break; + } + getchar(); // Skip white space + } while (option != 'Q'); + + } else { + printf("Manage Test not able to connect with server.... \n"); + } + + ManageSDKTest::DestroyFireboltInstance(); + + return 0; +} diff --git a/src/sdks/manage/src/cpp/sdk/cpptest/ManageSDKTest.cpp b/src/sdks/manage/src/cpp/sdk/cpptest/ManageSDKTest.cpp new file mode 100644 index 000000000..47c128ab4 --- /dev/null +++ b/src/sdks/manage/src/cpp/sdk/cpptest/ManageSDKTest.cpp @@ -0,0 +1,979 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "ManageSDKTest.h" + +using namespace std; +bool ManageSDKTest::_connected; +ManageSDKTest::OnAudioDescriptionsEnabledChangedNotification ManageSDKTest::_audioDescriptionEnabledChangedNotification; +ManageSDKTest::OnDeviceNameChangedNotification ManageSDKTest::_deviceNameChangedNotification; +ManageSDKTest::OnFontFamilyChangedNotification ManageSDKTest::_fontFamilyChangedNotification; +ManageSDKTest::OnBackgroundOpacityChangedNotification ManageSDKTest::_backgroundOpacityChangedNotification; +ManageSDKTest::OnPreferredAudioLanguagesChangedNotification ManageSDKTest::_preferredAudioLanguagesChangedNotification; +ManageSDKTest::OnAllowACRCollectionChangedNotification ManageSDKTest::_allowACRCollectionChangedNotification; +ManageSDKTest::OnSignInNotification ManageSDKTest::_signInNotification; +ManageSDKTest::OnSignOutNotification ManageSDKTest::_signOutNotification; +ManageSDKTest::KeyboardProvider ManageSDKTest::_keyboardProvider; +ManageSDKTest::AcknowledgeChallengeProvider ManageSDKTest::_acknowledgeChallengeProvider; +ManageSDKTest::PinChallengeProvider ManageSDKTest::_pinChallengeProvider; +Firebolt::Wifi::AccessPointList ManageSDKTest::_apList; + +void ManageSDKTest::ConnectionChanged(const bool connected, const Firebolt::Error error) +{ + cout << "Change in connection: connected: " << connected << " error: " << static_cast(error) << endl; + _connected = connected; +} + +void ManageSDKTest::CreateFireboltInstance(const std::string& url) +{ + const std::string config = "{\ + \"waitTime\": 100000,\ + \"logLevel\": \"Info\",\ + \"workerPool\":{\ + \"queueSize\": 8,\ + \"threadCount\": 3\ + },\ + \"wsUrl\": " + url + "}"; + + _connected = false; + Firebolt::IFireboltAccessor::Instance().Initialize(config); + Firebolt::IFireboltAccessor::Instance().Connect(ConnectionChanged); +} + +void ManageSDKTest::DestroyFireboltInstance() +{ + Firebolt::IFireboltAccessor::Instance().Disconnect(); + Firebolt::IFireboltAccessor::Instance().Deinitialize(); + Firebolt::IFireboltAccessor::Instance().Dispose(); +} + +bool ManageSDKTest::WaitOnConnectionReady() +{ + uint32_t waiting = 10000; + static constexpr uint32_t SLEEPSLOT_TIME = 100; + + // Right, a wait till connection is closed is requested.. + while ((waiting > 0) && (_connected == false)) { + + uint32_t sleepSlot = (waiting > SLEEPSLOT_TIME ? SLEEPSLOT_TIME : waiting); + // Right, lets sleep in slices of 100 ms + usleep(sleepSlot); + waiting -= sleepSlot; + } + return _connected; +} + +template +using EnumMap = std::unordered_map; +template +inline const string& ConvertFromEnum(EnumMap enumMap, T type) +{ + return enumMap[type]; +} +template +inline const T ConvertToEnum(EnumMap enumMap, const string& str) +{ + T value; + for (auto element: enumMap) { + if (element.second == str) { + value = element.first; + break; + } + } + return value; +} + +EnumMap skipRestrictionMap = { + { Firebolt::Advertising::SkipRestriction::NONE, "none" }, + { Firebolt::Advertising::SkipRestriction::ADS_UNWATCHED, "adsUnwatched" }, + { Firebolt::Advertising::SkipRestriction::ADS_ALL, "adsAll" }, + { Firebolt::Advertising::SkipRestriction::ALL, "all" }}; +void ManageSDKTest::GetAdvertisingSkipRestriction() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::Advertising::SkipRestriction skipRestriction = Firebolt::IFireboltAccessor::Instance().AdvertisingInterface().skipRestriction(&error); + if (error == Firebolt::Error::None) { + cout << "Get Advertising SkipRestriction is success : " << ConvertFromEnum(skipRestrictionMap, skipRestriction) << endl; + } else { + cout << "Get Advertising SkipRestriction status: " << static_cast(error) << endl; + } +} +void ManageSDKTest::SetAdvertisingSkipRestriction() +{ + Firebolt::Error error = Firebolt::Error::None; + cout << "Support SkipRestriction -> " << endl; + for (auto skipRestriction : skipRestrictionMap) { + cout << skipRestriction.second << endl; + } + std::string skipRestriction; + cout << "Enter new skipRestriction : "; + cin >> skipRestriction; + + Firebolt::IFireboltAccessor::Instance().AdvertisingInterface().setSkipRestriction(ConvertToEnum(skipRestrictionMap, skipRestriction), &error); + if (error == Firebolt::Error::None) { + cout << "Set Advertising SkipRestriction is success " << endl; + } else { + cout << "Set Advertising SkipRestriction status: " << static_cast(error) << endl; + } +} +void ManageSDKTest::ResetAdvertisingIdentifier() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().AdvertisingInterface().resetIdentifier(&error); + if (error == Firebolt::Error::None) { + cout << "Set Advertising Reset Identifier is success " << endl; + } else { + cout << "Set Advertising Reset Identifier status: " << static_cast(error) << endl; + } +} + +void ManageSDKTest::SetAccountSession() +{ + Firebolt::Error error = Firebolt::Error::None; + std::string token = "Test"; + int32_t expiresIn = 60; + Firebolt::IFireboltAccessor::Instance().AccountInterface().session(token, expiresIn, &error); + if (error == Firebolt::Error::None) { + cout << "Set Account Session is success" << endl; + } else { + cout << "Set Account Session status : " << static_cast(error) << endl; + } +} + +void ManageSDKTest::GetAudioDescriptionsEnabled() +{ + Firebolt::Error error = Firebolt::Error::None; + bool enabled = Firebolt::IFireboltAccessor::Instance().AudioDescriptionsInterface().enabled(&error); + if (error == Firebolt::Error::None) { + cout << "Get AudioDescriptions Enabled: " << (enabled ? "true" : "false") << endl; + } else { + cout << "Get AudioDescriptions Enabled status: " << static_cast(error) << endl; + } +} +void ManageSDKTest::SetAudioDescriptionsEnabled() +{ + Firebolt::Error error = Firebolt::Error::None; + std::string strEnabled; + cout << "Enter enabled or not (true/false): "; + cin >> strEnabled; + bool enabled = strEnabled == "true" ? true : false; + Firebolt::IFireboltAccessor::Instance().AudioDescriptionsInterface().setEnabled(enabled, &error); + if (error == Firebolt::Error::None) { + cout << "Set AudioDescriptions Enabled: " << endl; + } else { + cout << "Set AudioDescriptions Enabled status: " << static_cast(error) << endl; + } +} +void ManageSDKTest::OnAudioDescriptionsEnabledChangedNotification::onEnabledChanged( const bool enabled ) +{ + cout << "AudioDescriptions Enabled changed, new value --> " << (enabled ? "true" : "false") << endl; +} +void ManageSDKTest::SubscribeAudioDescriptionsEnabledChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().AudioDescriptionsInterface().subscribe(_audioDescriptionEnabledChangedNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Subscribe AudioDescriptions EnabledChange is success" << endl; + } else { + cout << "Subscribe AudioDescriptions EnabledChange status = " << static_cast(error) << endl; + } +} +void ManageSDKTest::UnsubscribeAudioDescriptionsEnabledChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().AudioDescriptionsInterface().unsubscribe(_audioDescriptionEnabledChangedNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Unsubscribe AudioDescriptions EnabledChange is success" << endl; + } else { + cout << "Unsubscribe AudioDescriptions EnabledChange status = " << static_cast(error) << endl; + } +} + +void ManageSDKTest::GetDeviceName() +{ + Firebolt::Error error = Firebolt::Error::None; + const std::string name = Firebolt::IFireboltAccessor::Instance().DeviceInterface().name(&error); + + if (error == Firebolt::Error::None) { + cout << "Get Device Name = " << name.c_str() << endl; + } else { + cout << "Get Device Name status = " << static_cast(error) << endl; + } +} +void ManageSDKTest::SetDeviceName() +{ + Firebolt::Error error = Firebolt::Error::None; + std::string name; + cout << "Enter new device name :"; + getchar(); + getline(cin, name); + Firebolt::IFireboltAccessor::Instance().DeviceInterface().setName(name, &error); + + if (error == Firebolt::Error::None) { + cout << "Set Device Name is success" << endl; + } else { + cout << "Set Device Name status = " << static_cast(error) << endl; + } + cin.putback('\n'); +} +void ManageSDKTest::OnDeviceNameChangedNotification::onDeviceNameChanged( const std::string& name ) +{ + cout << "Name changed, new name --> " << name << endl; +} +void ManageSDKTest::SubscribeDeviceNameChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().DeviceInterface().subscribe(_deviceNameChangedNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Subscribe Device NameChange is success" << endl; + } else { + cout << "Subscribe Device NameChange status = " << static_cast(error) << endl; + } +} +void ManageSDKTest::UnsubscribeDeviceNameChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().DeviceInterface().unsubscribe(_deviceNameChangedNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Unsubscribe Device NameChange is success" << endl; + } else { + cout << "Unsubscribe Device NameChange status = " << static_cast(error) << endl; + } +} + +void ManageSDKTest::GetClosedCaptionsBackgroundOpacity() +{ + Firebolt::Error error = Firebolt::Error::None; + const float value = Firebolt::IFireboltAccessor::Instance().ClosedCaptionsInterface().backgroundOpacity(&error); + + if (error == Firebolt::Error::None) { + cout << "Get ClosedCaption BackgroundOpacity = " << value << endl; + } else { + cout << "Get ClosedCaption BackgroundOpacity status = " << static_cast(error) << endl; + } +} +void ManageSDKTest::SetClosedCaptionsBackgroundOpacity() +{ + Firebolt::Error error = Firebolt::Error::None; + uint32_t bgOpacity; + cout << "Enter new background opacity: "; + scanf("%d", &bgOpacity); + + Firebolt::IFireboltAccessor::Instance().ClosedCaptionsInterface().setBackgroundOpacity(bgOpacity, &error); + + if (error == Firebolt::Error::None) { + cout << "Set ClosedCaption BackgroundOpacity is success" << endl; + } else { + cout << "Set ClosedCaption BackgroundOpacity status = " << static_cast(error) << endl; + } +} +void ManageSDKTest::OnBackgroundOpacityChangedNotification::onBackgroundOpacityChanged( const float opacity ) +{ + cout << "BackgroundOpacity changed, new value --> " << opacity << endl; +} +void ManageSDKTest::SubscribeClosedCaptionsBackgroundOpacityChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().ClosedCaptionsInterface().subscribe(_backgroundOpacityChangedNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Subscribe ClosedCaptions BackgroundOpacityChange is success" << endl; + } else { + cout << "Subscribe ClosedCaptions BackgroundOpacityChange status = " << static_cast(error) << endl; + } +} +void ManageSDKTest::UnsubscribeClosedCaptionsBackgroundOpacityChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().ClosedCaptionsInterface().unsubscribe(_backgroundOpacityChangedNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Unsubscribe ClosedCaptions BackgroundOpacityChange is success" << endl; + } else { + cout << "Unsubscribe ClosedCaptions BackgroundOpacityChange status = " << static_cast(error) << endl; + } +} + +EnumMap fontFamilyMap = { + { Firebolt::Accessibility::FontFamily::MONOSPACED_SERIF, "MonospacedSerif" }, + { Firebolt::Accessibility::FontFamily::PROPORTIONAL_SERIF, "ProportionalSerif" }, + { Firebolt::Accessibility::FontFamily::MONOSPACED_SANSERIF, "MonospacedSanserif" }, + { Firebolt::Accessibility::FontFamily::PROPORTIONAL_SANSERIF, "ProportionalSanserif" }, + { Firebolt::Accessibility::FontFamily::SMALLCAPS, "SmallCaps" }, + { Firebolt::Accessibility::FontFamily::CURSIVE, "Cursive" }, + { Firebolt::Accessibility::FontFamily::CASUAL, "Casual" } +}; +void ManageSDKTest::GetClosedCaptionsFontFamily() +{ + Firebolt::Error error = Firebolt::Error::None; + const Firebolt::Accessibility::FontFamily value = Firebolt::IFireboltAccessor::Instance().ClosedCaptionsInterface().fontFamily(&error); + + if (error == Firebolt::Error::None) { + cout << "Get ClosedCaption FontFamily value = " << ConvertFromEnum(fontFamilyMap, value) << endl; + } else { + cout << "Get ClosedCaption FontFamily status = " << static_cast(error) << endl; + } +} +void ManageSDKTest::SetClosedCaptionsFontFamily() +{ + Firebolt::Error error = Firebolt::Error::None; + std::string fontFamily; + cout << "Supported font family -> " << endl; + for (auto family : fontFamilyMap) { + cout << family.second << endl; + } + cout << "Enter new font family : "; + cin >> fontFamily; + + Firebolt::IFireboltAccessor::Instance().ClosedCaptionsInterface().setFontFamily(ConvertToEnum(fontFamilyMap, fontFamily), &error); + if (error == Firebolt::Error::None) { + cout << "Set ClosedCaption FontFamily is success" << endl; + } else { + cout << "Set ClosedCaption FontFamily status = " << static_cast(error) << endl; + } +} +void ManageSDKTest::OnFontFamilyChangedNotification::onFontFamilyChanged( const Firebolt::Accessibility::FontFamily& family ) +{ + cout << "FontFamily changed, new code --> " << ConvertFromEnum(fontFamilyMap, family) << endl; +} +void ManageSDKTest::SubscribeClosedCaptionsFontFamilyChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().ClosedCaptionsInterface().subscribe(_fontFamilyChangedNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Subscribe ClosedCaptions FontFamilyChange is success" << endl; + } else { + cout << "Subscribe ClosedCaptions FontFamilyChange status = " << static_cast(error) << endl; + } +} +void ManageSDKTest::UnsubscribeClosedCaptionsFontFamilyChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().ClosedCaptionsInterface().unsubscribe(_fontFamilyChangedNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Unsubscribe ClosedCaptions FontFamilyChange is success" << endl; + } else { + cout << "Unsubscribe ClosedCaptions FontFamilyChange status = " << static_cast(error) << endl; + } +} + +void ManageSDKTest::GetLocalizationPreferredAudioLanguages() +{ + Firebolt::Error error = Firebolt::Error::None; + const std::vector languages = Firebolt::IFireboltAccessor::Instance().LocalizationInterface().preferredAudioLanguages(&error); + + if (error == Firebolt::Error::None) { + cout << "Get Localization PreferredAudioLanguages : " << endl; + for (auto language: languages) { + cout << "----- > " < " << endl; + for (auto language : languages) { + cout << language << " "; + } + std::string language; + cout << " enter new audio language code to set : "; + getchar(); + getline(cin, language); + + std::vector newLanguages; + string delimeter = " "; + char* token = strtok(const_cast(language.c_str()), delimeter.c_str()); + while (token != nullptr) + { + newLanguages.push_back(string(token)); + token = strtok(nullptr, delimeter.c_str()); + } + Firebolt::IFireboltAccessor::Instance().LocalizationInterface().setPreferredAudioLanguages(newLanguages, &error); + + if (error == Firebolt::Error::None) { + cout << "Set Localization PreferredAudioLanguages is success" << endl; + } else { + cout << "Get Localization PreferredAudioLanguages status = " << static_cast(error) << endl; + } + cin.putback('\n'); +} +void ManageSDKTest::OnPreferredAudioLanguagesChangedNotification::onPreferredAudioLanguagesChanged( const std::vector& languages) +{ + cout << "PreferredAudioLanguages Changed, new languages --> " << endl; + for (auto language : languages) { + cout << " -> " << language << endl; + } +} +void ManageSDKTest::SubscribeLocalizationPreferredAudioLanguagesChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().LocalizationInterface().subscribe(_preferredAudioLanguagesChangedNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Subscribe Localization PreferredAudioLanguagesChange is success" << endl; + } else { + cout << "Subscribe Localization PreferredAudioLanguagesChange status = " << static_cast(error) << endl; + } +} +void ManageSDKTest::UnsubscribeLocalizationPreferredAudioLanguagesChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().LocalizationInterface().unsubscribe(_preferredAudioLanguagesChangedNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Unsubscribe Localization PreferredAudioLanguagesChange is success" << endl; + } else { + cout << "Unsubscribe Localization PreferredAudioLanguagesChange status = " << static_cast(error) << endl; + } +} + +void ManageSDKTest::GetPrivacyAllowACRCollection() +{ + Firebolt::Error error = Firebolt::Error::None; + bool allowACRCollection = Firebolt::IFireboltAccessor::Instance().PrivacyInterface().allowACRCollection(&error); + + if (error == Firebolt::Error::None) { + cout << "Get Privacy AllowACRCollection : " << (allowACRCollection ? "true" : "false") << endl; + } else { + cout << "Get Privacy AllowACRCollection status = " << static_cast(error) << endl; + } +} +void ManageSDKTest::SetPrivacyAllowACRCollection() +{ + Firebolt::Error error = Firebolt::Error::None; + std::string strAllowACRCollection; + cout << "Enter new allowACRCollection (true/false): "; + cin >> strAllowACRCollection; + bool allowACRCollection = strAllowACRCollection == "true" ? true : false; + Firebolt::IFireboltAccessor::Instance().PrivacyInterface().setAllowACRCollection(allowACRCollection , &error); + + if (error == Firebolt::Error::None) { + cout << "Set Privacy AllowACRCollection is success" << endl; + } else { + cout << "Get Privacy AllowACRCollection status = " << static_cast(error) << endl; + } +} +void ManageSDKTest::OnAllowACRCollectionChangedNotification::onAllowACRCollectionChanged( const bool allowACRCollection ) +{ + cout << "AllowACRCollection Changed, new value of allowACRCollection : " << (allowACRCollection ? "true" : "false") << endl; +} +void ManageSDKTest::SubscribePrivacyAllowACRCollectionChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().PrivacyInterface().subscribe(_allowACRCollectionChangedNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Subscribe Privacy AllowACRCollectionChanged is success" << endl; + } else { + cout << "Subscribe Privacy AllowACRCollectionChanged status = " << static_cast(error) << endl; + } +} +void ManageSDKTest::UnsubscribePrivacyAllowACRCollectionChanged() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().PrivacyInterface().unsubscribe(_allowACRCollectionChangedNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Unsubscribe Privacy AllowACRCollectionChanged is success" << endl; + } else { + cout << "Unsubscribe Privacy AllowACRCollectionChanged status = " << static_cast(error) << endl; + } +} +void ManageSDKTest::GetPrivacySettings() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::Privacy::PrivacySettings privacySettings = Firebolt::IFireboltAccessor::Instance().PrivacyInterface().settings(&error); + + if (error == Firebolt::Error::None) { + cout << "Get Privacy Settings -> " << endl; + cout << "\tallowACRCollection : " << privacySettings.allowACRCollection << endl; + cout << "\tallowResumePoints : " << privacySettings.allowResumePoints << endl; + cout << "\tallowAppContentAdTargeting : " << privacySettings.allowAppContentAdTargeting << endl; + cout << "\tallowCameraAnalytics : " << privacySettings.allowCameraAnalytics << endl; + cout << "\tallowPersonalization : " << privacySettings.allowPersonalization << endl; + cout << "\tallowPrimaryBrowseAdTargeting : " << privacySettings.allowPrimaryBrowseAdTargeting << endl; + cout << "\tallowPrimaryContentAdTargeting : " << privacySettings.allowPrimaryContentAdTargeting << endl; + cout << "\tallowProductAnalytics : " << privacySettings.allowProductAnalytics << endl; + cout << "\tallowRemoteDiagnostics : " << privacySettings.allowRemoteDiagnostics << endl; + cout << "\tallowUnentitledPersonalization : " << privacySettings.allowUnentitledPersonalization << endl; + cout << "\tallowUnentitledResumePoints : " << privacySettings.allowUnentitledResumePoints << endl; + cout << "\tallowWatchHistory : " << privacySettings.allowWatchHistory << endl; + } else { + cout << "Get Privacy Settings status = " << static_cast(error) << endl; + } + +} + +void ManageSDKTest::OnSignInNotification::onSignIn( const Firebolt::Discovery::Event& event ) +{ + cout << "Discovery SignIn Event for appId --> " << event.appId << endl; +} +void ManageSDKTest::SubscribeDiscoverySignInNotification() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().DiscoveryInterface().subscribe(_signInNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Subscribe Discovery SignIn Notification is success" << endl; + } else { + cout << "Subscribe Discovery SignIn Notification status = " << static_cast(error) << endl; + } +} +void ManageSDKTest::UnsubscribeDiscoverySignInNotification() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().DiscoveryInterface().unsubscribe(_signInNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Unsubscribe Discovery SignIn Notification is success" << endl; + } else { + cout << "Unsubscribe Discovery SignIn Notification status = " << static_cast(error) << endl; + } +} + +void ManageSDKTest::OnSignOutNotification::onSignOut( const Firebolt::Discovery::Event& event ) +{ + cout << "Discovery SignOut Event for appId --> " << event.appId << endl; +} +void ManageSDKTest::SubscribeDiscoverySignOutNotification() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().DiscoveryInterface().subscribe(_signOutNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Subscribe Discovery SignOut Notification is success" << endl; + } else { + cout << "Subscribe Discovery SignOut Notification status = " << static_cast(error) << endl; + } +} +void ManageSDKTest::UnsubscribeDiscoverySignOutNotification() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().DiscoveryInterface().unsubscribe(_signOutNotification, &error); + if (error == Firebolt::Error::None) { + cout << "Unsubscribe Discovery SignOut Notification is success" << endl; + } else { + cout << "Unsubscribe Discovery SignOut Notification status = " << static_cast(error) << endl; + } +} + +ManageSDKTest::KeyboardProvider::KeyboardProvider() + : _session(nullptr) + , _parameters() + , _keyInput(false) +{ +} +void ManageSDKTest::KeyboardProvider::SendMessage(bool response) +{ + if (_keyInput) { + cout << " Invoking _session->focus " << endl; + _session->focus(); + getchar(); + + string key; + cout << _parameters.message << " : "; + getline(cin, key); + if (response) { + Firebolt::Keyboard::KeyboardResult keyboardResult; + keyboardResult.text = key; + keyboardResult.canceled = false; + cout << " Invoking _session->result " << endl; + _session->result(keyboardResult); + } else { + Firebolt::Keyboard::KeyboardError keyboardError; + keyboardError.code = 123; + keyboardError.message = key; + keyboardError.data = "nothing to send"; + cout << " Invoking _session->error " << endl; + _session->error(keyboardError); + } + _keyInput = false; + cin.putback('\n'); + } else { + cout << " there is no active keyboard input session " << endl; + } +} +void ManageSDKTest::KeyboardProvider::standard(const Firebolt::Keyboard::KeyboardParameters& parameters, std::unique_ptr session) +{ + cout << "KeyboardProvider Standard is invoked" << endl; + startKeyboardSession(parameters, std::move(session)); +} +void ManageSDKTest::KeyboardProvider::password(const Firebolt::Keyboard::KeyboardParameters& parameters, std::unique_ptr session) +{ + cout << "KeyboardProvider Password is invoked" << endl; + startKeyboardSession(parameters, std::move(session)); +} +void ManageSDKTest::KeyboardProvider::email(const Firebolt::Keyboard::KeyboardParameters& parameters, std::unique_ptr session) +{ + cout << "KeyboardProvider Email is invoked" << endl; + startKeyboardSession(parameters, std::move(session)); +} +void ManageSDKTest::KeyboardProvider::startKeyboardSession( const Firebolt::Keyboard::KeyboardParameters& parameters, std::unique_ptr session ) +{ + _session = std::move(session); + _parameters = parameters; + _keyInput = true; +} +void ManageSDKTest::RegisterKeyboardProvider() +{ + Firebolt::IFireboltAccessor::Instance().KeyboardInterface().provide(_keyboardProvider); +} +void ManageSDKTest::SendResponseMessageToKeyboardProvider() +{ + _keyboardProvider.SendMessage(true); +} +void ManageSDKTest::SendErrorMessageToKeyboardProvider() +{ + _keyboardProvider.SendMessage(false); +} + +ManageSDKTest::AcknowledgeChallengeProvider::AcknowledgeChallengeProvider() + : _session(nullptr) + , _parameters() + , _challengeInput(false) +{ +} +void ManageSDKTest::AcknowledgeChallengeProvider::SendMessage(bool response) +{ + if (_challengeInput) { + cout << " Invoking _session->focus " << endl; + _session->focus(); + cout << " capability : " << _parameters.capability << endl; + cout << " id : " << _parameters.requestor.id << endl; + cout << " name : " << _parameters.requestor.name << endl; + if (response) { + Firebolt::AcknowledgeChallenge::GrantResult challengeResult; + challengeResult.granted = true; + cout << " Invoking _session->result " << endl; + _session->result(challengeResult); + } else { + string key; + getline(cin, key); + + Firebolt::AcknowledgeChallenge::AcknowledgeChallengeError challengeError; + challengeError.code = 234; + challengeError.message = key; + cout << " Invoking _session->error " << endl; + _session->error(challengeError); + cin.putback('\n'); + } + _challengeInput = false; + } else { + cout << " there is no active acknowledge challenge input session " << endl; + } +} +void ManageSDKTest::AcknowledgeChallengeProvider::challenge(const Firebolt::AcknowledgeChallenge::Challenge& parameters, std::unique_ptr session) +{ + cout << "AcknowledgeChallengeProvider challenge is invoked" << endl; + startAcknowledgeChallengeSession(parameters, std::move(session)); +} +void ManageSDKTest::AcknowledgeChallengeProvider::startAcknowledgeChallengeSession( const Firebolt::AcknowledgeChallenge::Challenge& parameters, std::unique_ptr session ) +{ + _session = std::move(session); + _parameters = parameters; + _challengeInput = true; +} +void ManageSDKTest::RegisterAcknowledgeChallengeProvider() +{ + Firebolt::IFireboltAccessor::Instance().AcknowledgeChallengeInterface().provide(_acknowledgeChallengeProvider); +} +void ManageSDKTest::SendResponseMessageToAcknowledgeChallengeProvider() +{ + _acknowledgeChallengeProvider.SendMessage(true); +} +void ManageSDKTest::SendErrorMessageToAcknowledgeChallengeProvider() +{ + _acknowledgeChallengeProvider.SendMessage(false); +} + +ManageSDKTest::PinChallengeProvider::PinChallengeProvider() + : _session(nullptr) + , _parameters() + , _challengeInput(false) +{ +} +void ManageSDKTest::PinChallengeProvider::SendMessage(bool response) +{ + if (_challengeInput) { + cout << " Invoking _session->focus " << endl; + _session->focus(); + cout << " pinSpace : " << static_cast(_parameters.pinSpace) << endl; + if (_parameters.capability.has_value()) { + cout << " capability : " << _parameters.capability.value() << endl; + } + cout << " id : " << _parameters.requestor.id << endl; + cout << " name : " << _parameters.requestor.name << endl; + + if (response) { + Firebolt::PinChallenge::PinChallengeResult challengeResult; + challengeResult.granted = true; + challengeResult.reason = Firebolt::PinChallenge::ResultReason::CORRECT_PIN; + cout << " Invoking _session->result " << endl; + _session->result(challengeResult); + } else { + string key; + getline(cin, key); + + Firebolt::PinChallenge::PinChallengeError challengeError; + challengeError.code = 234; + challengeError.message = key; + cout << " Invoking _session->error " << endl; + _session->error(challengeError); + cin.putback('\n'); + } + _challengeInput = false; + } else { + cout << " there is no active pin challenge input session " << endl; + } +} +void ManageSDKTest::PinChallengeProvider::challenge(const Firebolt::PinChallenge::PinChallenge& parameters, std::unique_ptr session) +{ + cout << "PinChallengeProvider challenge is invoked" << endl; + startPinChallengeSession(parameters, std::move(session)); +} +void ManageSDKTest::PinChallengeProvider::startPinChallengeSession( const Firebolt::PinChallenge::PinChallenge& parameters, std::unique_ptr session ) +{ + _session = std::move(session); + _parameters = parameters; + _challengeInput = true; +} +void ManageSDKTest::RegisterPinChallengeProvider() +{ + Firebolt::IFireboltAccessor::Instance().PinChallengeInterface().provide(_pinChallengeProvider); +} +void ManageSDKTest::SendResponseMessageToPinChallengeProvider() +{ + _pinChallengeProvider.SendMessage(true); +} +void ManageSDKTest::SendErrorMessageToPinChallengeProvider() +{ + _pinChallengeProvider.SendMessage(false); +} + +void ManageSDKTest::GetLocalizationAdditionalInfo() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::Localization::Info info = Firebolt::IFireboltAccessor::Instance().LocalizationInterface().additionalInfo(&error); + if (error == Firebolt::Error::None) { + cout << "AdditionalInfo is success, Info : " << endl; + for (auto element : info) { + cout << " " << element.first << " : " << element.second << endl; + } + } else { + cout << "AdditionalInfo call status = " << static_cast(error) << endl; + } +} +void ManageSDKTest::AddLocalizationAdditionalInfo() +{ + Firebolt::Error error = Firebolt::Error::None; + std::string key; + cout << "Enter new key :"; + getchar(); + getline(cin, key); + std::string value; + cout << "Enter new value :"; + getline(cin, value); + + Firebolt::IFireboltAccessor::Instance().LocalizationInterface().addAdditionalInfo(key, value, &error); + if (error == Firebolt::Error::None) { + cout << "Add AdditionalInfo is success" << endl; + } else { + cout << "Add AdditionalInfo status = " << static_cast(error) << endl; + } + + cin.putback('\n'); +} +void ManageSDKTest::RemoveLocalizationAdditionalInfo() +{ + Firebolt::Error error = Firebolt::Error::None; + std::string key; + cout << "Enter key to be removed :"; + getchar(); + getline(cin, key); + + Firebolt::IFireboltAccessor::Instance().LocalizationInterface().removeAdditionalInfo(key, &error); + if (error == Firebolt::Error::None) { + cout << "Remove AdditionalInfo is success" << endl; + } else { + cout << "Remove AdditionalInfo status = " << static_cast(error) << endl; + } + + cin.putback('\n'); +} + +void ManageSDKTest::GetUserGrantsPermission() +{ + Firebolt::Error error = Firebolt::Error::None; + cout << "Enter appId :"; + getchar(); + std::string appId; + getline(cin, appId); + + std::vector grantInfo = Firebolt::IFireboltAccessor::Instance().UserGrantsInterface().app(appId, &error); + if (error == Firebolt::Error::None) { + cout << "Get UserGrants Permission is success" << endl; + if (grantInfo.size() > 0) { + cout << "Grant Permission list : " << endl; + for (auto info : grantInfo) { + cout << "GrantState : " << static_cast(info.state) << endl; + cout << "Capability : " << info.capability << endl; + cout << "Role : " << static_cast(info.role) << endl; + cout << "Lifespan : " << static_cast(info.lifespan) << endl; + } + } + } else { + cout << "Get UserGrants Permission status = " << static_cast(error) << endl; + } + + cin.putback('\n'); +} +void ManageSDKTest::GrantUserGrantsPermission() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::UserGrants::GrantModificationOptions grantModificationOptions; + cout << "Enter appId :"; + getchar(); + std::string appId; + getline(cin, appId); + grantModificationOptions.appId = std::make_optional(appId); + + Firebolt::IFireboltAccessor::Instance().UserGrantsInterface().grant(Firebolt::Capabilities::Role::USE, "xrn:firebolt:capability:device:model", grantModificationOptions, &error); + if (error == Firebolt::Error::None) { + cout << "Grant UserGrants Permission is success" << endl; + } else { + cout << "Grant UserGrants Permission status = " << static_cast(error) << endl; + } + + cin.putback('\n'); +} +void ManageSDKTest::DenyUserGrantsPermission() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::UserGrants::GrantModificationOptions grantModificationOptions; + cout << "Enter appId :"; + getchar(); + std::string appId; + getline(cin, appId); + grantModificationOptions.appId = std::make_optional(appId); + + Firebolt::IFireboltAccessor::Instance().UserGrantsInterface().deny(Firebolt::Capabilities::Role::USE, "xrn:firebolt:capability:device:model", grantModificationOptions, &error); + if (error == Firebolt::Error::None) { + cout << "Deny UserGrants Permission is success" << endl; + } else { + cout << "Deny UserGrants Permission status = " << static_cast(error) << endl; + } + + cin.putback('\n'); +} +void ManageSDKTest::ClearUserGrantsPermission() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::UserGrants::GrantModificationOptions grantModificationOptions; + cout << "Enter appId :"; + getchar(); + std::string appId; + getline(cin, appId); + grantModificationOptions.appId = std::make_optional(appId); + + Firebolt::IFireboltAccessor::Instance().UserGrantsInterface().clear(Firebolt::Capabilities::Role::USE, "xrn:firebolt:capability:device:model", grantModificationOptions, &error); + if (error == Firebolt::Error::None) { + cout << "Clear UserGrants Permission is success" << endl; + } else { + cout << "Clear UserGrants Permission status = " << static_cast(error) << endl; + } + + cin.putback('\n'); +} + +void PrintAP(const Firebolt::Wifi::AccessPoint ap) +{ + if (ap.ssid.has_value()) { + cout << " SSID : " << ap.ssid.value() << endl; + } + if (ap.securityMode.has_value()) { + cout << " SecurityMode : " << static_cast(ap.securityMode.value()) << endl; + } + if (ap.signalStrength.has_value()) { + cout << " SignalStrength : " << ap.signalStrength.value() << endl; + } + if (ap.frequency.has_value()) { + cout << " Frequency : " << ap.frequency.value() << endl; + } +} +void PrintAPList(const Firebolt::Wifi::AccessPointList& apList) +{ + if (apList.list.has_value()) { + cout<< "list ---->" << endl; + uint32_t index = 0; + for (const auto ap : apList.list.value()) { + cout << index << ". " << endl; + PrintAP(ap); + cout << endl; + index++; + } + } else { + cout << "empty list " << endl; + } +} +void ManageSDKTest::WifiScan() +{ + Firebolt::Error error = Firebolt::Error::None; + int32_t timeout = -1; + _apList = Firebolt::IFireboltAccessor::Instance().WifiInterface().scan(timeout, &error); + if (error == Firebolt::Error::None) { + cout << "Wifi Scan is success, "; + PrintAPList(_apList); + } else { + cout << "Wifi Scan status = " << static_cast(error) << endl; + } +} +void ManageSDKTest::WifiConnect() +{ + Firebolt::Error error = Firebolt::Error::None; + if (_apList.list.has_value()) { + cout << "Scanned access pointlist : " << endl; + PrintAPList(_apList); + cout << "Select access point from the list: enter index : "; + int32_t index; + cin >> index; + Firebolt::Wifi::AccessPoint ap = _apList.list.value()[index]; + std::string passphrase; + cout << "Enter passphrase to connect "; + cin >> passphrase; + + string ssid = ap.ssid.has_value() ? ap.ssid.value() : ""; + printf("%s:%s:%d _apList.list.value()[index] = %s\n", __FILE__, __func__, __LINE__, ssid.c_str()); + Firebolt::Wifi::WifiSecurityMode securityMode = ap.securityMode.has_value() ? ap.securityMode.value() : Firebolt::Wifi::WifiSecurityMode::NONE; + Firebolt::Wifi::AccessPoint connectedAP = Firebolt::IFireboltAccessor::Instance().WifiInterface().connect(ssid, passphrase, securityMode , &error); + if (error == Firebolt::Error::None) { + cout << "Wifi Connect is success, "; + PrintAP(connectedAP); + } else { + cout << "Wifi Connect status = " << static_cast(error) << endl; + } + } else { + cout << "Empty AP list, please initiate scan before trying connect " << endl; + } +} +void ManageSDKTest::WifiDisconnect() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().WifiInterface().disconnect(&error); + if (error == Firebolt::Error::None) { + cout << "Wifi Disconnect is success, "; + } else { + cout << "Wifi Disconnect status = " << static_cast(error) << endl; + } +} diff --git a/src/sdks/manage/src/cpp/sdk/cpptest/ManageSDKTest.h b/src/sdks/manage/src/cpp/sdk/cpptest/ManageSDKTest.h new file mode 100644 index 000000000..2f8be7145 --- /dev/null +++ b/src/sdks/manage/src/cpp/sdk/cpptest/ManageSDKTest.h @@ -0,0 +1,205 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include "firebolt.h" + +class ManageSDKTest { + class OnAudioDescriptionsEnabledChangedNotification : public Firebolt::AudioDescriptions::IAudioDescriptions::IOnEnabledChangedNotification { + public: + void onEnabledChanged( const bool ) override; + }; + + class OnDeviceNameChangedNotification : public Firebolt::Device::IDevice::IOnDeviceNameChangedNotification { + public: + void onDeviceNameChanged( const std::string& ) override; + }; + + class OnFontFamilyChangedNotification : public Firebolt::ClosedCaptions::IClosedCaptions::IOnFontFamilyChangedNotification { + void onFontFamilyChanged( const Firebolt::Accessibility::FontFamily& ) override; + }; + + class OnBackgroundOpacityChangedNotification : public Firebolt::ClosedCaptions::IClosedCaptions::IOnBackgroundOpacityChangedNotification { + void onBackgroundOpacityChanged( const float ) override; + }; + + class OnPreferredAudioLanguagesChangedNotification : public Firebolt::Localization::ILocalization::IOnPreferredAudioLanguagesChangedNotification { + public: + void onPreferredAudioLanguagesChanged( const std::vector& ) override; + }; + + class OnAllowACRCollectionChangedNotification : public Firebolt::Privacy::IPrivacy::IOnAllowACRCollectionChangedNotification { + public: + void onAllowACRCollectionChanged( const bool ) override; + }; + + class OnSignInNotification : public Firebolt::Discovery::IDiscovery::IOnSignInNotification { + public: + void onSignIn( const Firebolt::Discovery::Event& ) override; + }; + + class OnSignOutNotification : public Firebolt::Discovery::IDiscovery::IOnSignOutNotification { + public: + void onSignOut( const Firebolt::Discovery::Event& ) override; + }; + + class KeyboardProvider : public Firebolt::Keyboard::IKeyboardProvider { + public: + KeyboardProvider(); + ~KeyboardProvider() override = default; + void standard( const Firebolt::Keyboard::KeyboardParameters& parameters, std::unique_ptr session ) override; + void password( const Firebolt::Keyboard::KeyboardParameters& parameters, std::unique_ptr session ) override; + void email( const Firebolt::Keyboard::KeyboardParameters& parameters, std::unique_ptr session ) override; + void SendMessage(bool response); + + private: + void startKeyboardSession(const Firebolt::Keyboard::KeyboardParameters& parameters, std::unique_ptr session); + + private: + std::unique_ptr _session; + Firebolt::Keyboard::KeyboardParameters _parameters; + bool _keyInput; + }; + class AcknowledgeChallengeProvider : public Firebolt::AcknowledgeChallenge::IAcknowledgeChallengeProvider { + public: + AcknowledgeChallengeProvider(); + ~AcknowledgeChallengeProvider() override = default; + void challenge( const Firebolt::AcknowledgeChallenge::Challenge& parameters, std::unique_ptr session ) override; + void SendMessage(bool response); + + private: + void startAcknowledgeChallengeSession(const Firebolt::AcknowledgeChallenge::Challenge& parameters, std::unique_ptr session); + + private: + std::unique_ptr _session; + Firebolt::AcknowledgeChallenge::Challenge _parameters; + bool _challengeInput; + }; + class PinChallengeProvider : public Firebolt::PinChallenge::IPinChallengeProvider { + public: + PinChallengeProvider(); + ~PinChallengeProvider() override = default; + void challenge( const Firebolt::PinChallenge::PinChallenge& parameters, std::unique_ptr session ) override; + void SendMessage(bool response); + + private: + void startPinChallengeSession(const Firebolt::PinChallenge::PinChallenge& parameters, std::unique_ptr session); + + private: + std::unique_ptr _session; + Firebolt::PinChallenge::PinChallenge _parameters; + bool _challengeInput; + }; + +public: + ManageSDKTest() = default; + virtual ~ManageSDKTest() = default; + + static void CreateFireboltInstance(const std::string& url); + static void DestroyFireboltInstance(); + static void TestManageStaticSDK(); + + static void GetAdvertisingSkipRestriction(); + static void SetAdvertisingSkipRestriction(); + static void ResetAdvertisingIdentifier(); + + static void SetAccountSession(); + + static void GetAudioDescriptionsEnabled(); + static void SetAudioDescriptionsEnabled(); + static void SubscribeAudioDescriptionsEnabledChanged(); + static void UnsubscribeAudioDescriptionsEnabledChanged(); + + static void GetDeviceName(); + static void SetDeviceName(); + static void SubscribeDeviceNameChanged(); + static void UnsubscribeDeviceNameChanged(); + + static void GetClosedCaptionsBackgroundOpacity(); + static void SetClosedCaptionsBackgroundOpacity(); + static void SubscribeClosedCaptionsBackgroundOpacityChanged(); + static void UnsubscribeClosedCaptionsBackgroundOpacityChanged(); + + static void GetClosedCaptionsFontFamily(); + static void SetClosedCaptionsFontFamily(); + static void SubscribeClosedCaptionsFontFamilyChanged(); + static void UnsubscribeClosedCaptionsFontFamilyChanged(); + + static void GetLocalizationPreferredAudioLanguages(); + static void SetLocalizationPreferredAudioLanguages(); + static void SubscribeLocalizationPreferredAudioLanguagesChanged(); + static void UnsubscribeLocalizationPreferredAudioLanguagesChanged(); + + static void GetPrivacyAllowACRCollection(); + static void SetPrivacyAllowACRCollection(); + static void SubscribePrivacyAllowACRCollectionChanged(); + static void UnsubscribePrivacyAllowACRCollectionChanged(); + static void GetPrivacySettings(); + + static void SubscribeDiscoverySignInNotification(); + static void UnsubscribeDiscoverySignInNotification(); + static void SubscribeDiscoverySignOutNotification(); + static void UnsubscribeDiscoverySignOutNotification(); + + static void RegisterKeyboardProvider(); + static void SendResponseMessageToKeyboardProvider(); + static void SendErrorMessageToKeyboardProvider(); + + static void RegisterAcknowledgeChallengeProvider(); + static void SendResponseMessageToAcknowledgeChallengeProvider(); + static void SendErrorMessageToAcknowledgeChallengeProvider(); + + static void RegisterPinChallengeProvider(); + static void SendResponseMessageToPinChallengeProvider(); + static void SendErrorMessageToPinChallengeProvider(); + + static void GetLocalizationAdditionalInfo(); + static void AddLocalizationAdditionalInfo(); + static void RemoveLocalizationAdditionalInfo(); + + static void GetUserGrantsPermission(); + static void GrantUserGrantsPermission(); + static void DenyUserGrantsPermission(); + static void ClearUserGrantsPermission(); + + static void WifiScan(); + static void WifiConnect(); + static void WifiDisconnect(); + + static bool WaitOnConnectionReady(); + +private: + static void ConnectionChanged(const bool, const Firebolt::Error); + static bool _connected; + static OnAudioDescriptionsEnabledChangedNotification _audioDescriptionEnabledChangedNotification; + static OnDeviceNameChangedNotification _deviceNameChangedNotification; + static OnFontFamilyChangedNotification _fontFamilyChangedNotification; + static OnBackgroundOpacityChangedNotification _backgroundOpacityChangedNotification; + static OnPreferredAudioLanguagesChangedNotification _preferredAudioLanguagesChangedNotification; + static OnAllowACRCollectionChangedNotification _allowACRCollectionChangedNotification; + static KeyboardProvider _keyboardProvider; + static AcknowledgeChallengeProvider _acknowledgeChallengeProvider; + static PinChallengeProvider _pinChallengeProvider; + static OnSignInNotification _signInNotification; + static OnSignOutNotification _signOutNotification; + + static Firebolt::Wifi::AccessPointList _apList; +}; + diff --git a/src/sdks/core/src/cpp/sdk/ctest/build.sh b/src/sdks/manage/src/cpp/sdk/cpptest/build.sh similarity index 100% rename from src/sdks/core/src/cpp/sdk/ctest/build.sh rename to src/sdks/manage/src/cpp/sdk/cpptest/build.sh diff --git a/src/sdks/manage/src/cpp/sdk/test/CMakeLists.txt b/src/sdks/manage/src/cpp/sdk/test/CMakeLists.txt new file mode 100644 index 000000000..f92c73420 --- /dev/null +++ b/src/sdks/manage/src/cpp/sdk/test/CMakeLists.txt @@ -0,0 +1,87 @@ +# Copyright 2023 Comcast Cable Communications Management, LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.3) + +project(FireboltManageSDKTests) +project_version(1.0.0) + +set(TESTLIB ${PROJECT_NAME}) + +message("Setup ${TESTLIB} v${PROJECT_VERSION}") + +set(CMAKE_POSITION_INDEPENDENT_CODE ON) +find_package(${NAMESPACE}Core CONFIG REQUIRED) + +file(GLOB CPP_SOURCES *.cpp) +list(REMOVE_ITEM CPP_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/Main.cpp) +message("After CPP_SOURCES " ${CPP_SOURCES}) +add_library(${TESTLIB} STATIC ${CPP_SOURCES}) + +target_link_libraries(${TESTLIB} + PRIVATE + ${FIREBOLT_NAMESPACE}SDK::${FIREBOLT_NAMESPACE}SDK + ${NAMESPACE}Core::${NAMESPACE}Core +) + +target_include_directories(${TESTLIB} + PRIVATE + $ + $ + $ + $ +) + +set_target_properties(${TESTLIB} PROPERTIES + CXX_STANDARD 17 + CXX_STANDARD_REQUIRED YES + LINK_WHAT_YOU_USE TRUE + FRAMEWORK FALSE +) + +set(TESTAPP FireboltManageSDKTestApp) + +message("Setup ${TESTAPP} v${PROJECT_VERSION}") + +add_executable(${TESTAPP} Main.cpp) + +target_link_libraries(${TESTAPP} + PRIVATE + ${TESTLIB} +) + +target_include_directories(${TESTAPP} + PRIVATE + $ + $ + $ +) + +install( + TARGETS ${TESTLIB} EXPORT ${TESTLIB}Targets + ARCHIVE DESTINATION lib COMPONENT libs # static lib + LIBRARY DESTINATION lib COMPONENT libs # shared lib +) + +InstallCMakeConfig(TARGETS ${TESTLIB}) + +add_custom_command( + TARGET ${TESTAPP} + POST_BUILD + COMMENT "=================== Installing TestApp ======================" + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/${FIREBOLT_NAMESPACE}/usr/bin + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_BINARY_DIR}/${TESTAPP} ${CMAKE_BINARY_DIR}/${FIREBOLT_NAMESPACE}/usr/bin +) diff --git a/src/sdks/manage/src/cpp/sdk/test/Main.cpp b/src/sdks/manage/src/cpp/sdk/test/Main.cpp new file mode 100644 index 000000000..28fc4595f --- /dev/null +++ b/src/sdks/manage/src/cpp/sdk/test/Main.cpp @@ -0,0 +1,42 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include "ManageSDKTestGeneratedCode.h" + +int __cnt = 0; +int __pass = 0; + +int TotalTests = 0; +int TotalTestsPassed = 0; + +int main() +{ + printf("Calling CreateFireboltInstance ---\n"); + ManageSDKTestGeneratedCode::CreateFireboltInstance(); + + if (ManageSDKTestGeneratedCode::WaitOnConnectionReady() == true) { + printf("Calling GetDeviceName ---> \n"); + ManageSDKTestGeneratedCode::GetDeviceName(); + ManageSDKTestGeneratedCode::SetDeviceName(); + ManageSDKTestGeneratedCode::GetDeviceName(); + } + ManageSDKTestGeneratedCode::DestroyFireboltInstance(); + printf("TOTAL: %i tests; %i PASSED, %i FAILED\n", TotalTests, TotalTestsPassed, (TotalTests - TotalTestsPassed)); +} + diff --git a/src/sdks/manage/src/cpp/sdk/test/ManageSDKTestGeneratedCode.cpp b/src/sdks/manage/src/cpp/sdk/test/ManageSDKTestGeneratedCode.cpp new file mode 100644 index 000000000..c0d0123ab --- /dev/null +++ b/src/sdks/manage/src/cpp/sdk/test/ManageSDKTestGeneratedCode.cpp @@ -0,0 +1,103 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include "Module.h" +#include "firebolt.h" +#include "ManageSDKTestStaticCode.h" +#include "ManageSDKTestGeneratedCode.h" + +using namespace std; +bool ManageSDKTestGeneratedCode::_connected; + +void ManageSDKTestGeneratedCode::ConnectionChanged(const bool connected, const Firebolt::Error error) +{ + cout << "Change in connection: connected: " << connected << " error: " << static_cast(error) << endl; + _connected = connected; +} + +void ManageSDKTestGeneratedCode::CreateFireboltInstance() +{ + const std::string config = _T("{\ + \"waitTime\": 1000,\ + \"logLevel\": \"Info\",\ + \"workerPool\":{\ + \"queueSize\": 8,\ + \"threadCount\": 3\ + },\ + \"wsUrl\": \"ws://127.0.0.1:9998\"\ + }"); + + _connected = false; + Firebolt::IFireboltAccessor::Instance().Initialize(config); + Firebolt::IFireboltAccessor::Instance().Connect(ConnectionChanged); +} + +void ManageSDKTestGeneratedCode::DestroyFireboltInstance() +{ + Firebolt::IFireboltAccessor::Instance().Disconnect(); + Firebolt::IFireboltAccessor::Instance().Deinitialize(); + Firebolt::IFireboltAccessor::Instance().Dispose(); +} + +bool ManageSDKTestGeneratedCode::WaitOnConnectionReady() +{ + uint32_t waiting = 10000; + static constexpr uint32_t SLEEPSLOT_TIME = 100; + + // Right, a wait till connection is closed is requested.. + while ((waiting > 0) && (_connected == false)) { + + uint32_t sleepSlot = (waiting > SLEEPSLOT_TIME ? SLEEPSLOT_TIME : waiting); + // Right, lets sleep in slices of 100 ms + SleepMs(sleepSlot); + waiting -= sleepSlot; + } + return _connected; +} + +void ManageSDKTestGeneratedCode::TestManageStaticSDK() +{ + FireboltSDK::Tests::Main(); +} + +void ManageSDKTestGeneratedCode::GetDeviceName() +{ + Firebolt::Error error = Firebolt::Error::None; + const std::string name = Firebolt::IFireboltAccessor::Instance().DeviceInterface().Name(&error); + + if (error == Firebolt::Error::None) { + cout << "Get DeviceName = " << name.c_str() << endl; + } else { + cout << "Get DeviceName status = " << static_cast(error) << endl; + } +} + +void ManageSDKTestGeneratedCode::SetDeviceName() +{ + Firebolt::Error error = Firebolt::Error::None; + Firebolt::IFireboltAccessor::Instance().DeviceInterface().SetName("Hello", &error); + + if (error == Firebolt::Error::None) { + cout << "Set DeviceName is success" << endl; + } else { + cout << "Set DeviceName status = " << static_cast(error) << endl; + } +} + diff --git a/src/sdks/manage/src/cpp/sdk/test/ManageSDKTestGeneratedCode.h b/src/sdks/manage/src/cpp/sdk/test/ManageSDKTestGeneratedCode.h new file mode 100644 index 000000000..063c22bc1 --- /dev/null +++ b/src/sdks/manage/src/cpp/sdk/test/ManageSDKTestGeneratedCode.h @@ -0,0 +1,40 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "error.h" + +class ManageSDKTestGeneratedCode { + +public: + ManageSDKTestGeneratedCode() = default; + virtual ~ManageSDKTestGeneratedCode() = default; + + static void CreateFireboltInstance(); + static void DestroyFireboltInstance(); + static void TestManageStaticSDK(); + static void GetDeviceName(); + static void SetDeviceName(); + static bool WaitOnConnectionReady(); + +private: + static void ConnectionChanged(const bool, const Firebolt::Error); + static bool _connected; +}; + diff --git a/src/sdks/manage/src/cpp/sdk/test/ManageSDKTestStaticCode.cpp b/src/sdks/manage/src/cpp/sdk/test/ManageSDKTestStaticCode.cpp new file mode 100644 index 000000000..683054b37 --- /dev/null +++ b/src/sdks/manage/src/cpp/sdk/test/ManageSDKTestStaticCode.cpp @@ -0,0 +1,46 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "Module.h" +#include "ManageSDKTestStaticCode.h" + +namespace FireboltSDK { + ManageTestStaticCode::ManageTestStaticCode() + : Tests() + { + _functionMap.emplace(std::piecewise_construct, std::forward_as_tuple("Get Country Code"), + std::forward_as_tuple(&GetCountryCode)); + } + + /* static */ Firebolt::Error ManageTestStaticCode::GetCountryCode() + { + const string method = _T("localization.countryCode"); + WPEFramework::Core::ProxyType response; + Firebolt::Error status = FireboltSDK::Properties::Get(method, response); + + EXPECT_EQ(status, Firebolt::Error::None); + if (status == Firebolt::Error::None) { + FIREBOLT_LOG_INFO(Logger::Category::Manage, Logger::Module(), "CountryCode : %s", response->Value().c_str()); + } else { + FIREBOLT_LOG_ERROR(Logger::Category::Manage, Logger::Module(), "Get %s status = %d\n", method.c_str(), status); + } + + return status; + } +} + diff --git a/src/sdks/manage/src/cpp/sdk/test/ManageSDKTestStaticCode.h b/src/sdks/manage/src/cpp/sdk/test/ManageSDKTestStaticCode.h new file mode 100644 index 000000000..68daa106b --- /dev/null +++ b/src/sdks/manage/src/cpp/sdk/test/ManageSDKTestStaticCode.h @@ -0,0 +1,61 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once +#include + +namespace FireboltSDK { + class Policy : public WPEFramework::Core::JSON::Container { + public: + Policy(const Policy& copy) = delete; + Policy() + : WPEFramework::Core::JSON::Container() + , EnableRecommendations(false) + , ShareWatchHistory(false) + , RememberWatchedPrograms(false) + { + Add(_T("enableRecommendations"), &EnableRecommendations); + Add(_T("shareWatchHistory"), &ShareWatchHistory); + Add(_T("rememberWatchedPrograms"), &RememberWatchedPrograms); + } + Policy& operator=(const Policy& RHS) + { + EnableRecommendations = RHS.EnableRecommendations; + ShareWatchHistory = RHS.ShareWatchHistory; + RememberWatchedPrograms = RHS.RememberWatchedPrograms; + + return (*this); + } + + ~Policy() override = default; + + public: + WPEFramework::Core::JSON::Boolean EnableRecommendations; + WPEFramework::Core::JSON::Boolean ShareWatchHistory; + WPEFramework::Core::JSON::Boolean RememberWatchedPrograms; + }; + + class ManageTestStaticCode : public Tests { + + public: + ManageTestStaticCode(); + ~ManageTestStaticCode() override = default; + + static Firebolt::Error GetCountryCode(); + }; +} diff --git a/src/sdks/manage/src/cpp/sdk/test/Module.cpp b/src/sdks/manage/src/cpp/sdk/test/Module.cpp new file mode 100644 index 000000000..d63badc4b --- /dev/null +++ b/src/sdks/manage/src/cpp/sdk/test/Module.cpp @@ -0,0 +1,21 @@ +/* + * Copyright 2023 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "Module.h" + +MODULE_NAME_DECLARATION(BUILD_REFERENCE) diff --git a/src/sdks/core/src/cpp/sdk/test/CoreSDKCTests.h b/src/sdks/manage/src/cpp/sdk/test/Module.h similarity index 73% rename from src/sdks/core/src/cpp/sdk/test/CoreSDKCTests.h rename to src/sdks/manage/src/cpp/sdk/test/Module.h index a6592156c..bc4aee5e1 100644 --- a/src/sdks/core/src/cpp/sdk/test/CoreSDKCTests.h +++ b/src/sdks/manage/src/cpp/sdk/test/Module.h @@ -16,20 +16,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -#ifndef CORESDK_C_TESTS_H -#define CORESDK_C_TESTS_H +#pragma once -#include - -#ifdef __cplusplus -extern "C" { +#ifndef MODULE_NAME +#define MODULE_NAME OpenRPCManageTestApp #endif -int32_t test_firebolt_core_main(); -int32_t test_properties_get_policy(); - -#ifdef __cplusplus -} -#endif +#include -#endif //CORESDK_C_TESTS_H +#undef EXTERNAL +#define EXTERNAL From ef6e851c133c9ef0dce1f733d3ed81de1ca9190c Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 27 Mar 2024 13:38:18 +0000 Subject: [PATCH 07/21] chore(release): 1.2.0-next.1 [skip ci] # [1.2.0-next.1](https://github.com/rdkcentral/firebolt-apis/compare/v1.1.1-next.2...v1.2.0-next.1) (2024-03-27) ### Features * Firebolt CPP SDKs ([5ea5d4e](https://github.com/rdkcentral/firebolt-apis/commit/5ea5d4e2f486a7bc3861955cd98323fc2c26cce1)), closes [#189](https://github.com/rdkcentral/firebolt-apis/issues/189) [#190](https://github.com/rdkcentral/firebolt-apis/issues/190) [#191](https://github.com/rdkcentral/firebolt-apis/issues/191) [#192](https://github.com/rdkcentral/firebolt-apis/issues/192) [#205](https://github.com/rdkcentral/firebolt-apis/issues/205) [#209](https://github.com/rdkcentral/firebolt-apis/issues/209) [#218](https://github.com/rdkcentral/firebolt-apis/issues/218) [#222](https://github.com/rdkcentral/firebolt-apis/issues/222) [#228](https://github.com/rdkcentral/firebolt-apis/issues/228) [#230](https://github.com/rdkcentral/firebolt-apis/issues/230) [#237](https://github.com/rdkcentral/firebolt-apis/issues/237) [#238](https://github.com/rdkcentral/firebolt-apis/issues/238) [#249](https://github.com/rdkcentral/firebolt-apis/issues/249) --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- src/sdks/core/package.json | 6 +++--- src/sdks/manage/package.json | 4 ++-- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1c3c0bcf..2eafe40a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [1.2.0-next.1](https://github.com/rdkcentral/firebolt-apis/compare/v1.1.1-next.2...v1.2.0-next.1) (2024-03-27) + + +### Features + +* Firebolt CPP SDKs ([5ea5d4e](https://github.com/rdkcentral/firebolt-apis/commit/5ea5d4e2f486a7bc3861955cd98323fc2c26cce1)), closes [#189](https://github.com/rdkcentral/firebolt-apis/issues/189) [#190](https://github.com/rdkcentral/firebolt-apis/issues/190) [#191](https://github.com/rdkcentral/firebolt-apis/issues/191) [#192](https://github.com/rdkcentral/firebolt-apis/issues/192) [#205](https://github.com/rdkcentral/firebolt-apis/issues/205) [#209](https://github.com/rdkcentral/firebolt-apis/issues/209) [#218](https://github.com/rdkcentral/firebolt-apis/issues/218) [#222](https://github.com/rdkcentral/firebolt-apis/issues/222) [#228](https://github.com/rdkcentral/firebolt-apis/issues/228) [#230](https://github.com/rdkcentral/firebolt-apis/issues/230) [#237](https://github.com/rdkcentral/firebolt-apis/issues/237) [#238](https://github.com/rdkcentral/firebolt-apis/issues/238) [#249](https://github.com/rdkcentral/firebolt-apis/issues/249) + ## [1.1.1-next.2](https://github.com/rdkcentral/firebolt-apis/compare/v1.1.1-next.1...v1.1.1-next.2) (2024-03-06) diff --git a/package-lock.json b/package-lock.json index 90a6b24c8..2d88db841 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@firebolt-js/sdks", - "version": "1.1.1-next.2", + "version": "1.2.0-next.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@firebolt-js/sdks", - "version": "1.1.1-next.2", + "version": "1.2.0-next.1", "license": "Apache-2.0", "workspaces": [ "src/sdks/core", diff --git a/package.json b/package.json index 647bdb775..ac4fb15c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/sdks", - "version": "1.1.1-next.2", + "version": "1.2.0-next.1", "description": "The Firebolt JS SDK", "type": "module", "bin": { diff --git a/src/sdks/core/package.json b/src/sdks/core/package.json index 5dfe709fe..1c2cbb91e 100644 --- a/src/sdks/core/package.json +++ b/src/sdks/core/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/sdk", - "version": "1.1.1-next.2", + "version": "1.2.0-next.1", "description": "The Firebolt JS SDK", "main": "./dist/lib/firebolt.mjs", "types": "./dist/lib/firebolt.d.ts", @@ -14,7 +14,7 @@ "native": "npx firebolt-openrpc sdk --input ./dist/firebolt-core-open-rpc.json --template ./src/cpp --output ./build/c/src --static-module Platform --language ../../../node_modules/@firebolt-js/openrpc/languages/c", "cpp": "npm run cpp:compile && npm run cpp:install", "cpp:compile": "npx firebolt-openrpc sdk --input ./dist/firebolt-core-open-rpc.json --template ./src/cpp --output ./build/cpp/src --static-module Platform --language ../../../node_modules/@firebolt-js/openrpc/languages/cpp", - "cpp:install":"./build/cpp/src/scripts/install.sh -i ./build/cpp/src -s ./build/cpp/src/ -m core", + "cpp:install": "./build/cpp/src/scripts/install.sh -i ./build/cpp/src -s ./build/cpp/src/ -m core", "compile": "cd ../../.. && npm run compile", "slice": "npx firebolt-openrpc slice -i ../../../dist/firebolt-open-rpc.json --sdk ./sdk.config.json -o ./dist/firebolt-core-open-rpc.json", "docs": "npx firebolt-openrpc docs --input ./dist/firebolt-core-open-rpc.json --output build/docs/markdown --as-path", @@ -53,4 +53,4 @@ "sdk" ], "license": "Apache-2.0" -} +} \ No newline at end of file diff --git a/src/sdks/manage/package.json b/src/sdks/manage/package.json index 400779cbd..2bb97888d 100644 --- a/src/sdks/manage/package.json +++ b/src/sdks/manage/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/manage-sdk", - "version": "1.1.1-next.2", + "version": "1.2.0-next.1", "description": "The Firebolt Manage JS SDK", "main": "./dist/lib/firebolt-manage.mjs", "types": "./dist/lib/firebolt-manage.d.ts", @@ -14,7 +14,7 @@ "native": "npx firebolt-openrpc sdk --input ./dist/firebolt-manage-open-rpc.json --template ./src/js --output ./build/c/src --language ../../../node_modules/@firebolt-js/openrpc/languages/c", "cpp": "npm run cpp:compile && npm run cpp:install", "cpp:compile": "npx firebolt-openrpc sdk --input ./dist/firebolt-manage-open-rpc.json --template ./src/cpp --output ./build/cpp/src --static-module Platform --language ../../../node_modules/@firebolt-js/openrpc/languages/cpp", - "cpp:install":"./build/cpp/src/scripts/install.sh -i ./build/cpp/src -s ./build/cpp/src/ -m manage", + "cpp:install": "./build/cpp/src/scripts/install.sh -i ./build/cpp/src -s ./build/cpp/src/ -m manage", "compile": "cd ../../.. && npm run compile", "slice": "npx firebolt-openrpc slice -i ../../../dist/firebolt-open-rpc.json --sdk ./sdk.config.json -o ./dist/firebolt-manage-open-rpc.json", "docs": "npx firebolt-openrpc docs --input ./dist/firebolt-manage-open-rpc.json --output build/docs/markdown --as-path", From 49ab03db9986b27dafd279fcc6121abe447784dc Mon Sep 17 00:00:00 2001 From: kschrief Date: Mon, 8 Apr 2024 14:50:34 -0400 Subject: [PATCH 08/21] feat: CI/CD docs (next-major) and merges (next-major/main) (#245) * New workflow * Fix: Not really * Undo comments * Undo comments * Added merge workflows * fix: Reversed version override flow * Less logins on GH Actions --- .github/workflows/merge-to-main.yml | 43 ++++++++++++++ .github/workflows/merge-to-next-major.yml | 72 +++++++++++++++++++++++ .github/workflows/release-github.yml | 28 ++++++--- 3 files changed, 134 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/merge-to-main.yml create mode 100644 .github/workflows/merge-to-next-major.yml diff --git a/.github/workflows/merge-to-main.yml b/.github/workflows/merge-to-main.yml new file mode 100644 index 000000000..2c4c8f1df --- /dev/null +++ b/.github/workflows/merge-to-main.yml @@ -0,0 +1,43 @@ +name: Attempt to merge next to main +on: + workflow_dispatch: + +jobs: + # Check if next can merge into main + perform_merge: + name: Perform merge if "next" can merge into "main" + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + repo-token: ${{ secrets.SEMANTIC_RELEASE_BOT_PAT }} + persist-credentials: true + + # Set user identity + - name: Set-Identity + run: | + git config --global user.email "${{ secrets.GLOBAL_GITHUB_EMAIL }}" + git config --global user.name "${{ secrets.GLOBAL_GITHUB_USER }}" + + # Checkout "main" + - name: Checkout main + run: git checkout "main" + + - name: Perform the merge from next to main + run: | + git merge next + git push origin "main" + echo "Push to main succeeded" + + # If the merge cannot be performed, let stakeholders know + message_on_failure: + name: Merge failure + needs: perform_merge + runs-on: ubuntu-latest + if: ${{ failure() }} + + steps: + - name: Post error message (To-Do) + run: echo "Next cannot be merged into main cleanly" diff --git a/.github/workflows/merge-to-next-major.yml b/.github/workflows/merge-to-next-major.yml new file mode 100644 index 000000000..f0066b908 --- /dev/null +++ b/.github/workflows/merge-to-next-major.yml @@ -0,0 +1,72 @@ +name: Attempt to merge next to next-major +on: + workflow_dispatch: + push: + branches: + - 'next' + +jobs: + # Check if next can merge into next-major + perform_merge: + name: Perform merge if "next" can merge into "next-major" + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + repo-token: ${{ secrets.SEMANTIC_RELEASE_BOT_PAT }} + persist-credentials: true + + # Set user identity + - name: Set-Identity + run: | + git config --global user.email "${{ secrets.GLOBAL_GITHUB_EMAIL }}" + git config --global user.name "${{ secrets.GLOBAL_GITHUB_USER }}" + + # Checkout "next-major" + - name: Checkout next-major + run: git checkout "next-major" + + # Get the "next-major" version number + - name: Extract next-major version + id: extract_version + run: echo "::set-output name=version::$(node -e 'console.log(require("./package.json").version)')" + + # Checkout "next" + - name: Checkout next + run: git checkout "next" + + # Update "next" version to match "next-major" + - name: Update "next" version to match "next-major" + run: | + jq '.version = "${{ steps.extract_version.outputs.version }}"' package.json > temp.json + + if diff -q "package.json" "temp.json" >/dev/null; then + echo "Versions are identical. No change required." + rm temp.json + else + mv temp.json package.json + git add package.json && git commit -m "Sync version to ${{ steps.extract_version.outputs.version }}" + fi + + # Checkout "next-major" + - name: Checkout next-major + run: git checkout "next-major" + + - name: Perform the merge from next to next-major + run: | + git merge next + git push origin "next-major" + echo "Push to next-major succeeded" + + # If the merge cannot be performed, let stakeholders know + message_on_failure: + name: Merge failure + needs: perform_merge + runs-on: ubuntu-latest + if: ${{ failure() }} + + steps: + - name: Post error message (To-Do) + run: echo "Next cannot be merged into next-major cleanly" diff --git a/.github/workflows/release-github.yml b/.github/workflows/release-github.yml index 70fb05bd5..897303fae 100644 --- a/.github/workflows/release-github.yml +++ b/.github/workflows/release-github.yml @@ -9,7 +9,7 @@ on: - 'proposed' pull_request: types: [opened, synchronize] - branches: + branches: - 'next' - 'proposed' env: @@ -19,6 +19,7 @@ jobs: name: Release runs-on: ubuntu-latest steps: + # Checkout/install project - name: Checkout uses: actions/checkout@v2 with: @@ -35,6 +36,8 @@ jobs: run: 'echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc' - name: Install dependencies run: npm ci + + # Set NPM_DIST_TAGS and DOCS_TAG if applicable based on branch - name: Set main tags if: github.ref_name == 'main' && github.event_name != 'pull_request' run: | @@ -45,17 +48,24 @@ jobs: run: | echo "NPM_DIST_TAG=next" >> $GITHUB_ENV echo "DOCS_TAG=next" >> $GITHUB_ENV + - name: Set next-major tags + if: github.ref_name == 'next-major' && github.event_name != 'pull_request' + run: | + echo "NPM_DIST_TAG=next-major" >> $GITHUB_ENV + echo "DOCS_TAGE=next-major" >> $GITHUB_ENV - name: Set feature branch tags - if: github.ref_name != 'main' && github.ref_name != 'next' && github.event_name != 'pull_request' + if: github.ref_name != 'main' && github.ref_name != 'next' && github.ref_name != 'next-major' && github.event_name != 'pull_request' run: | echo "NPM_DIST_TAG=test" >> $GITHUB_ENV echo "FEATURE_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV - name: Set PR tags - if: github.ref_name != 'main' && github.ref_name != 'next' && github.event_name == 'pull_request' + if: github.ref_name != 'main' && github.ref_name != 'next' && github.ref_name != 'next-major' && github.event_name == 'pull_request' run: | echo "NPM_DIST_TAG=test" >> $GITHUB_ENV echo "DOCS_TAG=pr-$(node ./src/js/version.mjs branch-to-prerelease ${{ github.head_ref }})" >> $GITHUB_ENV echo "FEATURE_BRANCH=${{ github.head_ref }}" >> $GITHUB_ENV + + # Perform real (When not a PR) or dry-run (When PR) semantic-release - name: Release mono-artifact to GitHub if: (github.ref_name != 'main' && github.event_name != 'pull_request') || github.event_name == 'workflow_dispatch' env: @@ -67,7 +77,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_BOT_PAT }} # <-- Allows semantic-release-bot to push changes to protected branches run: npx semantic-release --dry-run - name: Build artifacts for feature branches - if: github.ref_name != 'main' && github.ref_name != 'next' + if: github.ref_name != 'main' && github.ref_name != 'next' && github.ref_name != 'next-major' run: | PRERELEASE=$(node ./src/js/version.mjs branch-to-prerelease $FEATURE_BRANCH) node ./src/js/version.mjs latest-prerelease $PRERELEASE @@ -78,15 +88,15 @@ jobs: id: check_build uses: andstor/file-existence-action@v2 with: - files: "./src/sdks/core/dist/lib/firebolt.mjs" + files: './src/sdks/core/dist/lib/firebolt.mjs' - name: Release Firebolt SDKs to NPM if: steps.check_build.outputs.files_exists == 'true' && github.event_name != 'pull_request' && (github.ref_name != 'main' || github.event_name == 'workflow_dispatch') env: NPM_TOKEN: ${{ secrets.SEMANTIC_RELEASE_NPM_TOKEN }} # <-- Allows semantic-release to publish to npm without 2 factor auth. run: | - npm --version - echo "NPM DIST TAG :: " $NPM_DIST_TAG - npm publish --tag $NPM_DIST_TAG --workspaces + npm --version + echo "NPM DIST TAG :: " $NPM_DIST_TAG + npm publish --tag $NPM_DIST_TAG --workspaces - name: Dry-run Firebolt SDKs to NPM if: steps.check_build.outputs.files_exists == 'true' && (github.ref_name == 'main' && github.event_name != 'workflow_dispatch') env: @@ -100,7 +110,7 @@ jobs: cd /tmp git clone https://rdkcentral:$GITHUB_TOKEN@github.com/rdkcentral/firebolt.git ./firebolt-docs - name: Release docs to GitHub.io - if: steps.check_build.outputs.files_exists == 'true' && (github.ref_name == 'main' || github.ref_name == 'next' || github.event_name == 'pull_request') + if: steps.check_build.outputs.files_exists == 'true' && (github.ref_name == 'main' || github.ref_name == 'next' || github.ref_name == 'next-major' || github.event_name == 'pull_request') env: GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_BOT_PAT }} # <-- Allows semantic-release-bot to push changes to protected branches run: | From 696f1f96cf87ad3b7241fabcc130f6dea222e080 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 8 Apr 2024 18:53:40 +0000 Subject: [PATCH 09/21] chore(release): 1.2.0-next.2 [skip ci] # [1.2.0-next.2](https://github.com/rdkcentral/firebolt-apis/compare/v1.2.0-next.1...v1.2.0-next.2) (2024-04-08) ### Features * CI/CD docs (next-major) and merges (next-major/main) ([#245](https://github.com/rdkcentral/firebolt-apis/issues/245)) ([49ab03d](https://github.com/rdkcentral/firebolt-apis/commit/49ab03db9986b27dafd279fcc6121abe447784dc)) --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- src/sdks/core/package.json | 2 +- src/sdks/manage/package.json | 2 +- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eafe40a6..ca266e76a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [1.2.0-next.2](https://github.com/rdkcentral/firebolt-apis/compare/v1.2.0-next.1...v1.2.0-next.2) (2024-04-08) + + +### Features + +* CI/CD docs (next-major) and merges (next-major/main) ([#245](https://github.com/rdkcentral/firebolt-apis/issues/245)) ([49ab03d](https://github.com/rdkcentral/firebolt-apis/commit/49ab03db9986b27dafd279fcc6121abe447784dc)) + # [1.2.0-next.1](https://github.com/rdkcentral/firebolt-apis/compare/v1.1.1-next.2...v1.2.0-next.1) (2024-03-27) diff --git a/package-lock.json b/package-lock.json index 2d88db841..c4faf171a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@firebolt-js/sdks", - "version": "1.2.0-next.1", + "version": "1.2.0-next.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@firebolt-js/sdks", - "version": "1.2.0-next.1", + "version": "1.2.0-next.2", "license": "Apache-2.0", "workspaces": [ "src/sdks/core", diff --git a/package.json b/package.json index ac4fb15c1..506674342 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/sdks", - "version": "1.2.0-next.1", + "version": "1.2.0-next.2", "description": "The Firebolt JS SDK", "type": "module", "bin": { diff --git a/src/sdks/core/package.json b/src/sdks/core/package.json index 1c2cbb91e..b97be4a3f 100644 --- a/src/sdks/core/package.json +++ b/src/sdks/core/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/sdk", - "version": "1.2.0-next.1", + "version": "1.2.0-next.2", "description": "The Firebolt JS SDK", "main": "./dist/lib/firebolt.mjs", "types": "./dist/lib/firebolt.d.ts", diff --git a/src/sdks/manage/package.json b/src/sdks/manage/package.json index 2bb97888d..c21e6138e 100644 --- a/src/sdks/manage/package.json +++ b/src/sdks/manage/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/manage-sdk", - "version": "1.2.0-next.1", + "version": "1.2.0-next.2", "description": "The Firebolt Manage JS SDK", "main": "./dist/lib/firebolt-manage.mjs", "types": "./dist/lib/firebolt-manage.d.ts", From 7b1ddb498b2b54de227c868d422fe97634ef5305 Mon Sep 17 00:00:00 2001 From: kschrief Date: Thu, 9 May 2024 11:11:36 -0400 Subject: [PATCH 10/21] chore: Update openRPC dependency (#262) --- package-lock.json | 12 ++++++------ package.json | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index c4faf171a..173940cab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "devDependencies": { "@commitlint/cli": "^17.0.3", "@commitlint/config-conventional": "^17.0.3", - "@firebolt-js/openrpc": "3.0.0-next.1", + "@firebolt-js/openrpc": "3.0.0-next.3", "@firebolt-js/schemas": "2.0.0", "@saithodev/semantic-release-backmerge": "^3.2.0", "@semantic-release/changelog": "^6.0.1", @@ -1070,9 +1070,9 @@ "link": true }, "node_modules/@firebolt-js/openrpc": { - "version": "3.0.0-next.1", - "resolved": "https://registry.npmjs.org/@firebolt-js/openrpc/-/openrpc-3.0.0-next.1.tgz", - "integrity": "sha512-bFNXTePgmYPqHqMiGX2aWuTBY0KrZtwd7TVd58tg8fKpIgCjaBLf/8MAXqTd6oWYKU9o86zi4De3ygmYQCuJYg==", + "version": "3.0.0-next.3", + "resolved": "https://registry.npmjs.org/@firebolt-js/openrpc/-/openrpc-3.0.0-next.3.tgz", + "integrity": "sha512-hhNtHIpNwwN99Zd4ZABnQ3MIEMPYDN5rSZPNad4ah4R5rmLypWv79vfHSPYeCrTTmoQCwwCFhpObjhJk0EVX/g==", "dev": true, "dependencies": { "ajv": "^8.3.0", @@ -16951,7 +16951,7 @@ }, "src/sdks/core": { "name": "@firebolt-js/sdk", - "version": "1.1.1-next.1", + "version": "1.2.0-next.2", "license": "Apache-2.0", "devDependencies": { "jest": "^28.1.0", @@ -16962,7 +16962,7 @@ }, "src/sdks/manage": { "name": "@firebolt-js/manage-sdk", - "version": "1.1.1-next.1", + "version": "1.2.0-next.2", "license": "Apache-2.0", "devDependencies": { "jest": "^28.1.0", diff --git a/package.json b/package.json index 506674342..f019e95da 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "devDependencies": { "@commitlint/cli": "^17.0.3", "@commitlint/config-conventional": "^17.0.3", - "@firebolt-js/openrpc": "3.0.0-next.1", + "@firebolt-js/openrpc": "3.0.0-next.3", "@firebolt-js/schemas": "2.0.0", "@saithodev/semantic-release-backmerge": "^3.2.0", "@semantic-release/changelog": "^6.0.1", From 61e152a6cc11094929344070b64ebe06f63d24d6 Mon Sep 17 00:00:00 2001 From: Jeremy LaCivita Date: Thu, 23 May 2024 15:03:38 -0400 Subject: [PATCH 11/21] Featuer/play intent (#272) Standardizing Intent Message requirements --- requirements/specifications/intents/index.md | 64 ++++++++++++++++++-- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/requirements/specifications/intents/index.md b/requirements/specifications/intents/index.md index 6925376e4..72d6138c8 100644 --- a/requirements/specifications/intents/index.md +++ b/requirements/specifications/intents/index.md @@ -7,6 +7,8 @@ See [Firebolt Requirements Governance](../../governance.md) for more info. | Contributor | Organization | | --------------- | ------------ | | Jeremy LaCivita | Comcast | +| Simon Grist | Sky | + ## 1. Overview Offen times an end-user has a specific intention that needs to be communicated @@ -27,18 +29,23 @@ additional `data` property. - [3. Intent Action](#3-intent-action) - [4. Intent Context](#4-intent-context) - [5. Intent Data](#5-intent-data) -- [Intent Types](#intent-types) +- [6. Intent Message](#6-intent-message) + - [6.1. App Intent Message](#61-app-intent-message) + - [6.2. Platform Intent Message](#62-platform-intent-message) + - [6.3. Intent Message Type](#63-intent-message-type) + - [6.4. Intent Message Metadata](#64-intent-message-metadata) +- [7. Intent Types](#7-intent-types) ## 3. Intent Action The intent `action` denotes what type of intent it is. -All intents **MUST** have a `string` attribute denoting the type of intent. +All intents **MUST** have an `action` `string` property denoting the type of intent. See the various [Intent Types](#intent-types) below for values. ## 4. Intent Context -The intent `context` provides information on where the intent orginated from. +The intent `context` provides information on where the intent orginated from. All intents **MUST** have a `context` property, which is an object. The `context` object **MUST** have a `source` string property with one of the @@ -56,7 +63,56 @@ any string value. This property denotes an editorial campaign. ## 5. Intent Data If an intent has any additional data, it **MUST** be in the `data` property. -## Intent Types +## 6. Intent Message +When an intent is sent to a Firebolt device from some other system, e.g. a cloud voice service, it **MUST** be wrapped in an `IntentMessage` object so that it can be properly routed after transport. + +An intent message **MUST** have an `intent` object property that is the intent being passed. + +An example intent message: + +```json +{ + "type": "xrn:firebolt:intent:app:launch", + "appId": "Netflix", + "intent": { + "action": "launch", + "context": { + "source": "voice" + } + }, + "metadata": { + "foo": "bar" + } +} +``` + +### 6.1. App Intent Message +If an intent is targeting a specific app, then the intent message **MUST** have an `appId` string property with the appId of the targeted app. + +### 6.2. Platform Intent Message +If an intent messagage does not have an `appId` property, then it **MUST** be targeting the device itself, e.g. a `power` intent to turn off the device. + +### 6.3. Intent Message Type +An intent message **MUST** have a `type` string property for categorizing the intent. + +The type property **MUST** match the regular expression: + +```regex +^xrn:firebolt:intent:(app|platform):[a-zA-Z]+$ +``` + +App Intent Messages **MUST** have the fourth section set to `app`. + +Platform Intent Messages **MUST** have the fourth section set to `platform` + +All Intent Messages **MUST** have the fifth section set to the same value as `intent.action`. + +Platforms may use this to route different types of intents to different subsystems without having to understand the internal structure of Firebolt intent objects. + +### 6.4. Intent Message Metadata +An intent message **MAY** have a `metadata` object property for adding distributor-specific metadata for logging or analytics. The values in `metadata` **MUST NOT** impact how the device interprets the intent. + +## 7. Intent Types - [Play](./play.md) - [Tune](./tune.md) From 48a1094aaab6418f09db662dbc81f090a34f32ed Mon Sep 17 00:00:00 2001 From: Jeremy LaCivita Date: Thu, 6 Jun 2024 16:12:48 -0400 Subject: [PATCH 12/21] feat: User Interest (#170) In additional to traditional discovery APIs such as Watch History and Watch Next, Firebolt provides a more abstract API that facilitates impromptu content discovery connections between first-party Aggregated Experiences and third-party Apps. The User Interest Capability enables Apps to provide meta-data on content that the user has expressed an interest in to Aggregated Experience Apps that have been given access to use this Capability. This allows for open ended design of Aggregated Experience App features that present App-specific content to re-engage the user with the content inside the originating App. While the functionality and UX is left to the Aggregated Experience App, typically designed by each Firebolt Distributor, the Firebolt API enables events to register user interest and pass entity meta-data. Which generally enables Aggregated Experiences to present that entity meta-data in some way that leads to re-launching the original App at a later point, using a `navigateTo` notification. This is just one example of what an Aggregated Experience App might do with the User Interest API. Note that this API **SHOULD NOT** be used to implement Watch History or Watch Next features. These concepts are much more fundamental to Firebolt and have explicit APIs so that Firebolt Distributors can keep track of which apps are using them separately. --- package-lock.json | 46 +- package.json | 11 +- .../intents/user-interest/media/image1.png | Bin 0 -> 14294 bytes .../intents/user-interest/media/image2.png | Bin 0 -> 14615 bytes .../intents/user-interest/media/image3.png | Bin 0 -> 16673 bytes .../intents/user-interest/media/image4.png | Bin 0 -> 15496 bytes .../specifications/discovery/user-interest.md | 361 +++++++++++++ .../general/context-parameters.md | 303 +++++++++++ .../specifications/intents/user-interest.md | 15 + .../app-passthrough-apis.md | 477 ++++++++++++++++++ src/openrpc/content.json | 195 +++++++ src/openrpc/discovery.json | 178 ++++++- src/openrpc/keyboard.json | 38 +- src/schemas/advertising.json | 23 + src/schemas/discovery.json | 79 +++ src/schemas/entertainment.json | 1 - src/schemas/entity.json | 456 +++++++++++++++++ src/schemas/intents.json | 458 +---------------- src/schemas/lifecycle.json | 38 ++ src/schemas/types.json | 194 +++++++ src/sdks/core/sdk.config.json | 3 +- src/sdks/core/test/suite/discovery.test.ts | 9 + src/sdks/discovery/.npmignore | 6 + src/sdks/discovery/CHANGELOG.md | 0 src/sdks/discovery/CONTRIBUTING.md | 1 + src/sdks/discovery/LICENSE | 1 + src/sdks/discovery/NOTICE | 1 + src/sdks/discovery/README.md | 26 + src/sdks/discovery/jest.config.json | 8 + src/sdks/discovery/package.json | 53 ++ src/sdks/discovery/sdk.config.json | 15 + src/sdks/discovery/src/js/sdk/index.mjs | 30 ++ src/sdks/discovery/test/suite/content.test.ts | 43 ++ src/sdks/discovery/tsconfig.json | 5 + src/sdks/manage/test/suite/keyboard.test.ts | 24 +- 35 files changed, 2595 insertions(+), 503 deletions(-) create mode 100644 requirements/images/specifications/intents/user-interest/media/image1.png create mode 100644 requirements/images/specifications/intents/user-interest/media/image2.png create mode 100644 requirements/images/specifications/intents/user-interest/media/image3.png create mode 100644 requirements/images/specifications/intents/user-interest/media/image4.png create mode 100644 requirements/specifications/discovery/user-interest.md create mode 100644 requirements/specifications/general/context-parameters.md create mode 100644 requirements/specifications/intents/user-interest.md create mode 100644 requirements/specifications/openrpc-extensions/app-passthrough-apis.md create mode 100644 src/openrpc/content.json create mode 100644 src/schemas/advertising.json create mode 100644 src/schemas/discovery.json create mode 100644 src/schemas/entity.json create mode 100644 src/schemas/lifecycle.json create mode 100644 src/schemas/types.json create mode 100644 src/sdks/discovery/.npmignore create mode 100644 src/sdks/discovery/CHANGELOG.md create mode 120000 src/sdks/discovery/CONTRIBUTING.md create mode 120000 src/sdks/discovery/LICENSE create mode 120000 src/sdks/discovery/NOTICE create mode 100644 src/sdks/discovery/README.md create mode 100644 src/sdks/discovery/jest.config.json create mode 100644 src/sdks/discovery/package.json create mode 100644 src/sdks/discovery/sdk.config.json create mode 100644 src/sdks/discovery/src/js/sdk/index.mjs create mode 100644 src/sdks/discovery/test/suite/content.test.ts create mode 100644 src/sdks/discovery/tsconfig.json diff --git a/package-lock.json b/package-lock.json index 173940cab..f9fe68d78 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "license": "Apache-2.0", "workspaces": [ "src/sdks/core", - "src/sdks/manage" + "src/sdks/manage", + "src/sdks/discovery" ], "bin": { "firebolt-version": "src/js/version.mjs" @@ -18,7 +19,7 @@ "devDependencies": { "@commitlint/cli": "^17.0.3", "@commitlint/config-conventional": "^17.0.3", - "@firebolt-js/openrpc": "3.0.0-next.3", + "@firebolt-js/openrpc": "3.0.0-next.4", "@firebolt-js/schemas": "2.0.0", "@saithodev/semantic-release-backmerge": "^3.2.0", "@semantic-release/changelog": "^6.0.1", @@ -1065,18 +1066,22 @@ "node": ">=8" } }, + "node_modules/@firebolt-js/discovery-sdk": { + "resolved": "src/sdks/discovery", + "link": true + }, "node_modules/@firebolt-js/manage-sdk": { "resolved": "src/sdks/manage", "link": true }, "node_modules/@firebolt-js/openrpc": { - "version": "3.0.0-next.3", - "resolved": "https://registry.npmjs.org/@firebolt-js/openrpc/-/openrpc-3.0.0-next.3.tgz", - "integrity": "sha512-hhNtHIpNwwN99Zd4ZABnQ3MIEMPYDN5rSZPNad4ah4R5rmLypWv79vfHSPYeCrTTmoQCwwCFhpObjhJk0EVX/g==", + "version": "3.0.0-next.4", + "resolved": "https://registry.npmjs.org/@firebolt-js/openrpc/-/openrpc-3.0.0-next.4.tgz", + "integrity": "sha512-Q3GZpaA2Gmhx5UOh7cOMpDjZZtbutVfTUUis0FrOKceVMVllRhHz/XeqZrpmKo4EX0hn4OPWOmVek34WO68jQg==", "dev": true, "dependencies": { - "ajv": "^8.3.0", - "ajv-formats": "^2.1.0", + "ajv": "^8.12.0", + "ajv-formats": "^2.1.1", "array.prototype.groupby": "^1.1.0", "crocks": "^0.12.4", "deepmerge": "^4.2.2", @@ -1098,15 +1103,15 @@ "dev": true }, "node_modules/@firebolt-js/openrpc/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.16.0.tgz", + "integrity": "sha512-F0twR8U1ZU67JIEtekUcLkXkoO5mMMmgGD8sK/xUFzJ805jxHQl92hImFAqqXMyMYjSPOyUPAwHYhB72g5sTXw==", "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "uri-js": "^4.4.1" }, "funding": { "type": "github", @@ -16669,9 +16674,9 @@ } }, "node_modules/web-streams-polyfill": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", - "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", "dev": true, "engines": { "node": ">= 8" @@ -16960,6 +16965,17 @@ "typescript": "^4.6.4" } }, + "src/sdks/discovery": { + "name": "@firebolt-js/discovery-sdk", + "version": "1.2.0-next.2", + "license": "Apache-2.0", + "devDependencies": { + "jest": "^28.1.0", + "jest-environment-jsdom": "^28.1.3", + "prettier": "^3.1.0", + "typescript": "^4.6.4" + } + }, "src/sdks/manage": { "name": "@firebolt-js/manage-sdk", "version": "1.2.0-next.2", diff --git a/package.json b/package.json index f019e95da..d27b2efd0 100644 --- a/package.json +++ b/package.json @@ -8,14 +8,15 @@ }, "workspaces": [ "src/sdks/core", - "src/sdks/manage" + "src/sdks/manage", + "src/sdks/discovery" ], "scripts": { "fs:setup": "npm run clean && mkdir -p dist", - "validate:each": "npx firebolt-openrpc validate --input src/openrpc --schemas node_modules/@firebolt-js/schemas/src/schemas --schemas src/schemas --transformations", - "validate:compiled": "npx firebolt-openrpc validate --input dist/firebolt-open-rpc.json && npm run validate --workspaces", + "validate:each": "npx firebolt-openrpc validate --input src/openrpc --schemas src/schemas --transformations", + "validate:compiled": "npx firebolt-openrpc validate --input dist/firebolt-open-rpc.json --pass-throughs && npm run validate --workspaces", "validate": "npm run validate:each && npm run validate:compiled ", - "compile": "npx firebolt-openrpc openrpc --input src --template src/template/openrpc/template.json --output ./dist/firebolt-open-rpc.json --schemas node_modules/@firebolt-js/schemas/src/schemas --schemas src/schemas", + "compile": "npx firebolt-openrpc openrpc --input src --template src/template/openrpc/template.json --output ./dist/firebolt-open-rpc.json --schemas src/schemas", "slice": "npm run slice --workspaces", "sdks": "npm run sdk --workspaces", "docs": "npm run docs --workspaces", @@ -44,7 +45,7 @@ "devDependencies": { "@commitlint/cli": "^17.0.3", "@commitlint/config-conventional": "^17.0.3", - "@firebolt-js/openrpc": "3.0.0-next.3", + "@firebolt-js/openrpc": "3.0.0-next.4", "@firebolt-js/schemas": "2.0.0", "@saithodev/semantic-release-backmerge": "^3.2.0", "@semantic-release/changelog": "^6.0.1", diff --git a/requirements/images/specifications/intents/user-interest/media/image1.png b/requirements/images/specifications/intents/user-interest/media/image1.png new file mode 100644 index 0000000000000000000000000000000000000000..b0171a9a4ca0e95d45314ce3ce84ff6669623e19 GIT binary patch literal 14294 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 literal 0 HcmV?d00001 diff --git a/requirements/images/specifications/intents/user-interest/media/image2.png b/requirements/images/specifications/intents/user-interest/media/image2.png new file mode 100644 index 0000000000000000000000000000000000000000..94102a60fa1d79eab4760418b07643dae07bd5b0 GIT binary patch literal 14615 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 literal 0 HcmV?d00001 diff --git a/requirements/images/specifications/intents/user-interest/media/image4.png b/requirements/images/specifications/intents/user-interest/media/image4.png new file mode 100644 index 0000000000000000000000000000000000000000..6a15fe0c612806675309696195d47143ed946315 GIT binary patch literal 15496 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 literal 0 HcmV?d00001 diff --git a/requirements/specifications/discovery/user-interest.md b/requirements/specifications/discovery/user-interest.md new file mode 100644 index 000000000..f53e239ba --- /dev/null +++ b/requirements/specifications/discovery/user-interest.md @@ -0,0 +1,361 @@ +# User Interest + +Document Status: Candidate Specification + +See [Firebolt Requirements Governance](../../governance.md) for more info. + +| Contributor | Organization | +| -------------- | -------------- | +| Eugene Chung | Comcast | +| Tim Dibben | Sky | +| Mike Horwitz | Comcast | +| Jeremy LaCivita | Comcast | + +## 1. Overview + +In additional to traditional discovery APIs such as Watch History and +Watch Next, Firebolt provides a more abstract API that facilitates +impromptu content discovery connections between first-party Aggregated +Experiences and third-party Apps. + +The User Interest Capability enables Apps to provide meta-data on +content that the user has expressed an interest in to Aggregated +Experience Apps that have been given access to use this Capability. + +This allows for open ended design of Aggregated Experience App features +that present App-specific content to re-engage the user with the content +inside the originating App. + +While the functionality and UX is left to the Aggregated Experience App, +typically designed by each Firebolt Distributor, the Firebolt API +enables events to register user interest and pass entity meta-data: + +![Diagram Description automatically +generated](../../../requirements/images/specifications/intents/user-interest/media/image1.png) + +Which generally enables Aggregated Experiences to present that entity +meta-data in some way that leads to re-launching the original App at a +later point, using a `navigateTo` notification: + +![Diagram Description automatically +generated](../../../requirements/images/specifications/intents/user-interest/media/image2.png) + +This is just one example of what an Aggregated Experience App might do +with the User Interest API. + +Note that this API **SHOULD NOT** be used to implement Watch History or +Watch Next features. These concepts are much more fundamental to +Firebolt and have explicit APIs so that Firebolt Distributors can keep +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 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) + - [User Interest Errors](#user-interest-errors) + - [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. Interest Types](#51-interest-types) + - [5.2. Content.requestUserInterest](#52-contentrequestuserinterest) + - [5.3. Content.onUserInterest](#53-contentonuserinterest) + + +## 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, +an in-app "My List" button, etc. + +If the App wants to leverage any additional exposure from the device's +Aggregated Experience, it can wire up its own UI to the Firebolt User +Interest API, in addition to any in-app features that it's already +invoking. + +By calling the `Discovery.userInterest` method with the relevant entity +meta-data, the device's Aggregated Experience will be notified of the +user's interest in that entity: + +```typescript +Discovery.userInterest(type: InterestType, reason: InterestReason, entity: EntityDetails) +``` + +The `type` parameter denotes the directionality of the interest: + +- `interest` +- `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 `EntityDetails`, the platform +**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.onUserInterest` event has a result type of `Interest`: + +| property | type | description | +|---------|------|-------------| +| appId | string | The id of the app that pushed the user interest. | +| type | `InterestType` | the type of interest. | +| reason | `InterestReason` | the reason for the interest | +| entity | `EntityDetails` | The entity the user expressed interest in. | + +An Aggregated Experience can register for the `Content.onUserInterest` +notification, and it will receive notifications when an `EntityDetails` 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.onUserInterest` notification. + +If the result is not a valid entity, i.e. does not match +the [EntityDetails](../entities/) schema, then no `Content.onUserInterest` +notification will be dispatched. + +The `Discovery.userInterest` method **SHOULD NOT** be used in place of more +specific Discovery methods, e.g. `Discovery.watchNext` or +`Discovery.watched`. These methods facilitate specific UX flows that may +have separate legal opt-outs for each user. + +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. + +### 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 +Apps will need to be told about the user's expressed interest in their +content. + +First, the Aggregated Experience (or some app with this +capability) detects that the user is interested in something. In this +picture the interest is triggered by an RCU button, but how this occurs +is outside the scope of this document. When this happens, the Aggregated +Experience app calls `Content.requestUserInterest()`, which will trigger the +platform to identify the best [Provider Candidate](../openrpc-extensions/app-passthrough-apis.md#5-provider-candidates) +and call that app's `userInterest` method via the Provider RPC method: +`Discovery.onRequestUserInterest`. + +![](../../../requirements/images/specifications/intents/user-interest/media/image3.png) + +Next, the provider app receives and responds to the request with an +EntityDetails, which is returned as the result to the pending +`Content.requestUserInterest` method: + +![](../../../requirements/images/specifications/intents/user-interest/media/image4.png) + +Once an App's callback is invoked, that app will have `interestTimeout` +milliseconds to return a value or throw an error. Values returned after +that time **MUST** be ignored. The timeout value is stored in the +device's configuration manifest. + +To be notified when a user expresses interest in the currently displayed +content, an App **MUST** provide the +`xrn:firebolt:capability:discovery:interest` capability by enabling the +`Discovery.onRequestUserInterest` notification. + +If there is a valid entity to return, then the method registered by the +App **MUST** return the currently displayed entity meta-data. + +If there is no valid entity to return, then the method **MUST** throw an +exception. + +If the provider app returns a valid `EntityDetails` before the timeout, +then, the returned value **MUST** be used. + +If there is no app registered the an error **MUST** be returned. + +#### User Interest Errors +An app is expected return either a valid result or an appriate error. + +If neither happens before `interestTimeout` expires then the platform **MUST** return a Provider timed-out error: + +```json +{ + "id": 1, + "error": { + "code": -50400, + "message": "Provider timed-out", + "data": { + "capability": "xrn:firebolt:capability:discovery:interest", + } + } +} +``` + +If an app recieves a request for user interest when there is nothing appropriate to return, e.g. nothing selected or presented that maps to an Entity, then the app **SHOULD** return an error with a valid JSON-RPC error response, e.g.: + +```json +{ + "id": 1, + "error": { + "code": -40400, + "message": "No entities currently presented." + } +} +``` + +The platform API Gateway **MUST** append, or overwrite, the `data.capability` value to any errors returned by the app, e.g.: + +```json +{ + "id": 1, + "error": { + "code": -40400, + "message": "No entities currently presented.", + "data": { + "capability": "xrn:firebolt:capability:discovery:interest", + } + } +} +``` + +### 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 `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). + +### 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. + +## 4. Core SDK APIs + +The following APIs are exposed by the Firebolt Core SDK as part of the +`Discovery` module. + +### 4.1. InterestType +This is an enum with the following values: + +- `"interest"` +- `"disinterest"` + +### 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. + +To push an entity that the user is interested in pass an `EntityDetails` +object to the method: + +```typescript +Discovery.userInterest(type: InterestType, reason: InterestReason, entity: EntityDetails): Promise +``` + +### 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, reason: InterestReason): Promise +} + +Discovery.provide("xrn:firbolt:capability:discovery:interest", IDiscoveryInterestProvider) +``` + +### 4.5. InterestIntent + +An `InterestIntent` denotes that the user has expressed interest in the +currently displayed and/or selected content: + +```typescript +type InterestIntent { + action: "interest" + data: { + type: "interest" | "disinterest", + reason: "playlist" | "reaction" | "recording" + }, + context: { + source: "rcu" | "voice" + } +} +``` + +## 5. Discovery SDK APIs + +The following APIs are exposed by the Firebolt Discovery SDK as part of the +`Content` module. + +### 5.1. Interest Types +This type stores the various attributes of an Interest response or event: + +```typescript +type InterestType = "interest" | "disinterest" +type InterestReason = "playlist" | "reaction" | "recording" | "share" + +type Interest { + appId: string + entity: EntityDetails + type?: InterestType + reason?: InterestReason +} +``` + +### 5.2. Content.requestUserInterest +This method triggers the corresponding Discovery provider API for the +provider app. + +```typescript +Content.requestUserInterest(type: InterestType, reason: InterestReason): Promise +``` + +### 5.3. Content.onUserInterest + +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('userInterest', Interest => void): Promise` + +The callback will be passed an `Interest` object with +the appId, type, reason, and information about the entity that the user expressed interest in. diff --git a/requirements/specifications/general/context-parameters.md b/requirements/specifications/general/context-parameters.md new file mode 100644 index 000000000..761a03fd7 --- /dev/null +++ b/requirements/specifications/general/context-parameters.md @@ -0,0 +1,303 @@ +# Context Parameters +Document Status: Working Draft + +See [Firebolt Requirements Governance](../../governance.md) for more info. + +| Contributor | Organization | +| -------------- | -------------- | +| Jeremy LaCivita | Comcast | +| Yuri Pasquali | Sky | + +## 1. Overview + +**TODO**: This doc is old and need to be refreshed + +Context Parameters are parameters on an RPC method that provide context +for the call via a set of primitive types (string, number, integer, +boolean). This allows for sharing the context parameters across property +getters, setters, and subscribers, as well as filtering which events to +listen for by context. + +An example of a property method with context could be: + +```javascript +// get a context-driven property (context: appId=hulu) +const huluShare = await Privacy.shareWatchHistory('hulu') +``` + +```javascript +// set a context-driven property (context: appId=hulu) +Privacy.shareWatchHistory('hulu', false) +``` + +In the example above, 'hulu' is the context parameter for both the +shareWatchHistory getter and setter. + +Context parameters can also be applied to property subscribers, as well +as other, non-property events: + +```javascript +// subscribe to a context-driven property +Privacy.shareWatchHistory('hulu', (value) => { + console.log('hulu value changed to: ' + value) +}) + +Privacy.listen('shareWatchHistoryChanged', 'hulu', (value) => { + console.log('hulu value changed to: ' + value) +}) +``` + +For subscribers and events, the context parameters may be omitted, in +which case, all events will be dispatched to the listener: + +```javascript +// subscribe to a context-driven property w/out any context (get all of them) + +Privacy.shareWatchHistory((appId, value) => { + console.log(`App '${appId}' value changed to ${value}`) +}) + +Privacy.listen('shareWatchHistoryChanged', (appId, value) => { + console.log(`App '${appId}' value changed to: ${value}`) +}) +``` + +Context Parameters **MUST** be of a primitive type, to avoid complex +object-tree filtering. + +This document describes an OpenRPC pattern and JavaScript code +generation for a Firebolt method template that uses Context Parameters. + +## 2. Table of Contesnts +- [1. Overview](#1-overview) +- [2. Table of Contesnts](#2-table-of-contesnts) +- [3. Context Parameters Use Cases](#3-context-parameters-use-cases) +- [4. Context Parameters API](#4-context-parameters-api) + - [4.1. JSON-RPC API](#41-json-rpc-api) + - [4.1.1. Setter RPC generation](#411-setter-rpc-generation) + - [4.1.2. onChanged RPC generation](#412-onchanged-rpc-generation) + - [4.1.3. Temporal Set onAvailable / Unavailable RPC generation](#413-temporal-set-onavailable--unavailable-rpc-generation) + - [4.1.4. Event RPC Decoration](#414-event-rpc-decoration) + - [4.2. JavaScript API](#42-javascript-api) + - [4.2.1. Event Listener Signatures](#421-event-listener-signatures) + +## 3. Context Parameters Use Cases + +How each parameter affects it's corresponding API is out of scope for +this document. See each API spec for details on what each context +parameter does. + +Setting a context parameter to null is still setting it to a value. If a +context parameter is passed to the SDK with either a value of null or +undefined, then it **MUST** be explicitly set in the RPC request to the +value null. This is to avoid additional method signature permutations +being required for the SDK. + +An effort should be made to sort the context parameters in order of most +usefulness, since not all languages support undefined. + +## 4. Context Parameters API + +The section describes the RPC and JavaScript APIs. + +### 4.1. JSON-RPC API + +Simple getters and event listeners don't need any parameters. + +If a method is tagged as either a property (any kind) or an event, then +**all** the parameters in the RPC definition **MUST** be context +parameters. + +To facilitate this, the listen parameter that all events currently have +will be removed from the source module and inserted into the generated +RPC by the firebolt-openrpc tooling. + +If any Context Parameters have a type other than: + +- `string` +- `boolean` +- `number` +- `integer` + +Then the RPC method **MUST NOT** pass validation. This is to ensure that +implementing context parameters is not overly complicated. + +#### 4.1.1. Setter RPC generation + +When generating the setter for a property method, all the Context +Parameters **MUST** be copied to the setter. The context parameters +**MUST** be before the value parameter, which itself **MUST** be last. + +#### 4.1.2. onChanged RPC generation + +When generating the onChanged notification for a property +method, all the Context Parameters **MUST** be copied to the event +parameters. The context parameters **MUST** be before the listen +parameter, which itself **MUST** be last. + +#### 4.1.3. Temporal Set onAvailable / Unavailable RPC generation + +Generated Temporal Set events will treat the entire parameter list from +the original temporal-set method as Context Parameters and copy them to +the event parameters. The context parameters **MUST** be before the +listen parameter, which itself **MUST** be last. + +#### 4.1.4. Event RPC Decoration + +All RPC methods tagged as event **MUST** have the listen parameter, of +type boolean, automatically added when generating the final RPC. This +parameter will be added at the end of the parameters list. + +All RPC methods tagged as event that have context parameters **MUST** +have the result schema wrapped in an object. The original result schema +**MUST** be moved to a property called data. Each of the context +parameters **MUST** be copied to a property called context. + +So the following RPC event: + +```json +{ + "name": "onContextualEvent", + "tags": [ + { + "name": "event" + } + ], + "params": [ + { + "name": "a", + "required": true, + "schema": { + "type": "boolean" + } + }, + { + "name": "b", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "c", + "required": true, + "schema": { + "type": "number" + } + } + ], + "result": { + "name": "result", + "schema": { + "type": "object", + "properties": { + "foo": { + "type": "boolean" + } + } + } + } +} +``` + +Would have its result transformed to: + +```json +{ + "name": "result", + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "foo": { + "type": "boolean" + } + } + }, + "context": { + "type": "object", + "properties": { + "a": { + "type": "boolean" + }, + "b": { + "type": "string" + }, + "c": { + "type": "number" + } + }, + "required": [ + "a", "b", "c" + ] + } + } + } +} +``` + +### 4.2. JavaScript API + +TBD + +#### 4.2.1. Event Listener Signatures + +If any of the context parameters are optional, then a callback signature +must be generated for each left-to-right combination of the parameters. + +For example, the method: + +```json +{ + "name": "onFoo", + "tags": [ + { + "name": "event" + } + ], + "params": [ + { + "name": "a", + "required": true, + "schema": { + "type": "boolean" + } + }, + { + "name": "b", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "c", + "required": false, + "schema": { + "type": "number" + } + } + ] +} +``` + +Would result in the following method signatures: + +```typescript +listen(event: 'foo', a: boolean, callback: (b: string, c: number, data: any) => {}) + +listen(event: 'foo', a: boolean, b: string, callback: (c: number, data: any) => {}) + +listen(event: 'foo', a: boolean, b: string, c: number, callback: (data: any) => {}) +``` + +Which allows parameters to be omitted, from right-to-left, and included +as part of the result, instead. + +When invoking the callback, the SDK **MUST** pass the data portion of +the result to the data parameter of the callback, and each context +property to the corresponding callback parameter. + +This pattern also applies to property subscribers. diff --git a/requirements/specifications/intents/user-interest.md b/requirements/specifications/intents/user-interest.md new file mode 100644 index 000000000..eae60ecf7 --- /dev/null +++ b/requirements/specifications/intents/user-interest.md @@ -0,0 +1,15 @@ +# User Interest + +Document Status: Candidate Specification + +See [Firebolt Requirements Governance](../../governance.md) for more info. + +| Contributor | Organization | +| -------------- | -------------- | +| Eugene Chung | Comcast | +| Tim Dibben | Sky | +| Mike Horwitz | Comcast | +| Jeremy LaCivita | Comcast | + +## 1. Overview +This document describes the intent to initiate a [User Interest](../discovery/user-interest.md) flow from an upstream system, e.g. a voice assistant. diff --git a/requirements/specifications/openrpc-extensions/app-passthrough-apis.md b/requirements/specifications/openrpc-extensions/app-passthrough-apis.md new file mode 100644 index 000000000..ec23922e6 --- /dev/null +++ b/requirements/specifications/openrpc-extensions/app-passthrough-apis.md @@ -0,0 +1,477 @@ +# App Pass-through APIs + +Document Status: Working Draft + +See [Firebolt Requirements Governance](../../governance.md) for more info. + +| Contributor | Organization | +|-----------------|----------------| +| Jeremy LaCivita | Comcast | +| Kevin Pearson | Comcast | +| Yuri Pasquali | Sky | + +## 1. Overview +This document describes how one Firebolt App can provide a capability that may be used by another Firebolt App, with the platform as a permission broker that passes the requests and responses to each app without 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. + +To facilitate these APIs, Firebolt denotes an OpenRPC tag with OpenRPC extensions to connect the `provide` API to the `use` API. + +This document is written using the [IETF Best Common Practice 14](https://www.rfc-editor.org/rfc/rfc2119.txt) and should include the following summary in the Overview section: + +The key words "**MUST**", "**MUST NOT**", "**REQUIRED**", "**SHALL**", "**SHALL NOT**", "**SHOULD**", "**SHOULD NOT**", "**RECOMMENDED**", "**NOT RECOMMENDED**", "**MAY**", and "**OPTIONAL**" in this document are to be interpreted as described in [BCP 14](https://www.rfc-editor.org/rfc/rfc2119.txt) [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here. + +## 2. Table of Contents +- [1. Overview](#1-overview) +- [2. Table of Contents](#2-table-of-contents) +- [3. Open RPC Extensions](#3-open-rpc-extensions) + - [3.1. Provided By Extension](#31-provided-by-extension) +- [4. Routing App pass-through APIs](#4-routing-app-pass-through-apis) + - [4.1. No available providers](#41-no-available-providers) + - [4.2. Direct pass-through](#42-direct-pass-through) + - [4.4. Pass-through notifications](#44-pass-through-notifications) +- [5. Provider Candidates](#5-provider-candidates) +- [6. Best Candidate](#6-best-candidate) +- [7. Result Transformations](#7-result-transformations) +- [8. Provider Parameter Injection](#8-provider-parameter-injection) +- [9. API Gateway](#9-api-gateway) +- [10. Example: User Interest](#10-example-user-interest) + - [10.1. User Interest Pull](#101-user-interest-pull) + - [10.2. User Interest Push](#102-user-interest-push) + +## 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 "*platform method*" for the remainder of this document. + +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. + +To prevent compound methods a 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 platform method either uses or manages. + +If a platform method has no provider method then it is not a valid Firebolt OpenRPC method schema, and a validation error **MUST** be generated. + +## 4. Routing App pass-through APIs +App pass-through APIs may be routed in one of several ways. + +When an app calls a platform method, i.e. one with an `x-provided-by` extension, the platform **MUST** use one of the routing methods defined in this section based on various properties of the method. + +### 4.1. No available providers +When an app calls a platform method with an `x-provided-by` extension, the platform **MUST** return an unavailable error if there is no [candidate app](#7-provider-candidates) to execute the provider method. + +```json +{ + "id": 1, + "error": { + "code": -50300, + "message": "Capability is unavailable." + } +} +``` + +Where `` is the capability XRN string, e.g. `xrn:firebolt:capabilities:example:foo`. + +### 4.2. Direct pass-through +A direct pass-through is where a single app provides a single response to a single request by another app. + +This section only applies to app provider methods that do not have an `event` tag. + +The platform method result schema **MUST** either: + +> Match the `x-response` schema on the provider method so that the result can be passed through. +> +> or +> +> Have a property that matches the `x-response-name` string and `x-response` schema on the +> provider method so that the result can be composed and passed through. + +The platform **MUST** call the provider method from the [best candidate](#8-best-candidate) app and acquire the result. + +If the platform method result schema matches the `x-response` 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 `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 [result transformations](#9-result-transformations) to the composed result. + +### 4.4. 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. + +To ensure that event provider methods all behave the same the provider method **MUST** have a `result` schema with `"type"` set to `"null"`, since it will not expect any data in the response from the platform after pushing the notification. + +The platform method result schema **MUST** either: + +> Match the *last* parameter schema on the provider method so that the result can be passed through. +> +> Have a property that matches the *last* parameter name and schema on the provider method so that the result can be passed through. + +Example platform method with context: +```json +{ + "name": "onFoo", + "tags": [ + { + "name": "capabilities", + "x-uses": [ + "xrn:firebolt:capabilities:example:foo" + ], + "x-provided-by": "foo" + }, + { + "name": "event" + } + ], + "params": [ + { + "name": "context1", + "schema":{ + "type": "string" + } + }, + { + "name": "context2", + "schema": { + "type": "number" + } + } + ], + "result": { + "name": "value", + "schema": { + "type": "boolean" + } + } +} +``` + +Matching provider method: + +```json +{ + "name": "foo", + "tags": [ + { + "name": "capabilities", + "x-provides": "xrn:firebolt:capabilities:example:foo" + } + ], + "params": [ + { + "name": "context1", + "schema":{ + "type": "string" + } + }, + { + "name": "context2", + "schema": { + "type": "number" + } + }, + { + "name": "value", + "schema": { + "type": "boolean" + } + } + ] +} +``` + +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 [result transformations](#9-result-transformations) to the composed result. + +If the value was composed into the platform method result under a matching property, then any context parameter values from the provider method that correspond to a property name and schema in the platform method result **MUST** also be composed into the platform method result under those properties. + +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. + +## 5. Provider Candidates +The Firebolt Device Manifist **MUST** have a list of `ProviderPolicy` configurations that map capabilities to policies for determining candidate providers: + +```json +{ + "providerPolicies": [ + { + "inFocus": true, + "capabilities": [ + "xrn:firebolt:capability:foo:bar" + ] + } + ] +} +``` +The policy **MUST** have a list of capabilities that it is applied to. + +A capability **MUST NOT** be included in more than one policy. + +The policy **MAY** have an `inFocus` boolean. + +If the policy has `inFocus` set to `true` then any app without RCU input focus when the capability is invoked **MUST NOT** be considered a candidate. + +## 6. Best Candidate +If there is only one candidate then it **MUST** be the best candidate. + +If there is more than one candidate, then the candidate app that most recently had RCU input focus **MUST** be the best candidate. + +If none of the candidates have had focus yet, then the candidate app that was most recently launched **MUST** be the best candidate. + +## 7. Result Transformations +A 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. + +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 result. + +## 8. 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. + +If the platform method is an `event` and the provider method has context parameters then each context parameter from the provider that has a matching context parameter in the event **MUST** have it's value passed to that parameter. + +If the platform method is an `event` with a "composite result" and the provider method has context parameters then each context parameter from the provider that has a matching property on the `result` object **MUST** have it's value copied into that property. + +## 9. 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. + +## 10. Example: User Interest + +The following schemas are referenced by these examples: + +```json +{ + "components": { + "schemas": { + "InterestType": { + "type": "string", + "enum": [ + "interest", + "disinterest" + ] + }, + "InterestReason": { + "type": "string", + "enum": [ + "playlist" + ] + }, + "EntityDetailsFromApp": { + "type": "object", + "properties": { + "appId": { + "type": "string" + }, + "entity": { + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/EntityDetails" + } + }, + "required": [ + "appId", + "entity" + ] + } + } + } +} +``` + +### 10.1. User Interest Pull + +Platform method: + +```json +{ + "methods": [ + { + "name": "requestUserInterest", + "tags": [ + { + "name": "capabilities", + "x-provided-by": "Discovery.onRequestUserInterest", + "x-uses": [ + "xrn:firebolt:capability:discovery:interest" + ] + } + ], + "params": [ + { + "name": "type", + "required": true, + "schema": { + "$ref": "#/components/schemas/InterestType" + } + }, + { + "name": "reason", + "required": true, + "schema": { + "$ref": "#/components/schemas/InterestReason" + } + } + ], + "result": { + "name": "interest", + "schema": { + "$ref": "#/components/schemas/EntityDetailsFromApp", + } + } + } + ] +} +``` + +Provider method: + +```json +{ + "methods": [ + { + "name": "onRequestUserInterest", + "tags": [ + { + "name": "capabilities", + "x-provides": "xrn:firebolt:capability:discovery:interest" + }, + { + "name": "event", + "x-response": { + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/EntityDetails" + } + } + ], + "result": { + "name": "request", + "schema": { + "type": "object", + "properties": { + "type": { + "$ref": "#/components/schemas/InterestType", + }, + "reason": { + "$ref": "#/components/schemas/InterestReason", + } + } + } + } + } + ] +} +``` + +### 10.2. User Interest Push + +Provider method: + +```json +{ + "methods": [ + { + "name": "userInterest", + "tags": [ + { + "name": "capabilities", + "x-provides": "xrn:firebolt:capability:discovery:interest" + } + ], + "params": [ + { + "name": "type", + "schema": { + "$ref": "#/components/schemas/InterestType", + } + }, + { + "name": "reason", + "schema": { + "$ref": "#/components/schemas/InterestReason", + } + }, + { + "name": "entity", + "schema": { + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/EntityDetails" + } + } + ], + "result": { + "name": "result", + "schema": { + "type": "null" + } + } + } + ] +} +``` + +Platform Method: + +```json +{ + "methods": [ + { + "name": "onUserInterest", + "tags": [ + { + "name": "capabilities", + "x-provided-by": "Discovery.userInterest", + "x-uses": [ + "xrn:firebolt:capability:discovery:interest" + ] + }, + { + "name": "event" + } + ], + "params": [], + "result": { + "name": "interest", + "schema": { + "type": "object", + "properties": { + "appId": { + "type": "string" + }, + "type": { + "$ref": "#/components/schemas/InterestType" + }, + "reason": { + "$ref": "#/components/schemas/InterestReason" + }, + "entity": { + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/EntityDetails" + } + } + + } + } + } + ] +} +``` diff --git a/src/openrpc/content.json b/src/openrpc/content.json new file mode 100644 index 000000000..0ffbeadbf --- /dev/null +++ b/src/openrpc/content.json @@ -0,0 +1,195 @@ +{ + "openrpc": "1.2.4", + "info": { + "title": "Content", + "version": "0.0.0", + "description": "" + }, + "methods": [ + { + "name": "requestUserInterest", + "tags": [ + { + "name": "capabilities", + "x-provided-by": "Discovery.onRequestUserInterest", + "x-uses": [ + "xrn:firebolt:capability:discovery:interest" + ] + } + ], + "summary": "Provide information about the entity currently displayed or selected on the screen.", + "description": "Provide information about the entity currently displayed or selected on the screen.", + "params": [ + { + "name": "type", + "required": true, + "schema": { + "$ref": "https://meta.comcast.com/firebolt/discovery#/definitions/InterestType" + } + }, + { + "name": "reason", + "required": true, + "schema": { + "$ref": "https://meta.comcast.com/firebolt/discovery#/definitions/InterestReason" + } + } + ], + "result": { + "name": "interest", + "schema": { + "$ref": "#/components/schemas/InterestResult" + }, + "summary": "The EntityDetails data." + }, + "examples": [ + { + "name": "Default Example", + "params": [ + { + "name": "type", + "value": "interest" + }, + { + "name": "reason", + "value": "playlist" + } + ], + "result": { + "name": "interest", + "value": { + "appId": "cool-app", + "entity": { + "identifiers": { + "entityId": "345", + "entityType": "program", + "programType": "movie" + }, + "info": { + "title": "Cool Runnings", + "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Pulvinar sapien et ligula ullamcorper malesuada proin libero nunc.", + "releaseDate": "1993-01-01T00:00:00.000Z", + "contentRatings": [ + { + "scheme": "US-Movie", + "rating": "PG" + }, + { + "scheme": "CA-Movie", + "rating": "G" + } + ] + } + } + } + } + } + ] + }, + { + "name": "onUserInterest", + "tags": [ + { + "name": "event" + }, + { + "name": "capabilities", + "x-provided-by": "Discovery.userInterest", + "x-uses": [ + "xrn:firebolt:capability:discovery:interest" + ] + } + ], + "summary": "Provide information about the entity currently displayed or selected on the screen.", + "description": "Provide information about the entity currently displayed or selected on the screen.", + "params": [], + "result": { + "name": "interest", + "schema": { + "$ref": "#/components/schemas/InterestEvent" + }, + "summary": "The EntityDetails data." + }, + "examples": [ + { + "name": "Default Example", + "params": [], + "result": { + "name": "interest", + "value": { + "appId": "cool-app", + "type": "interest", + "reason": "playlist", + "entity": { + "identifiers": { + "entityId": "345", + "entityType": "program", + "programType": "movie" + }, + "info": { + "title": "Cool Runnings", + "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Pulvinar sapien et ligula ullamcorper malesuada proin libero nunc.", + "releaseDate": "1993-01-01T00:00:00.000Z", + "contentRatings": [ + { + "scheme": "US-Movie", + "rating": "PG" + }, + { + "scheme": "CA-Movie", + "rating": "G" + } + ] + } + } + } + } + } + ] + } + ], + "components": { + "schemas": { + "InterestResult": { + "title": "InterestResult", + "type": "object", + "properties": { + "appId": { + "type": "string" + }, + "entity": { + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/EntityDetails" + } + }, + "required": [ + "appId", + "entity" + ] + }, + "InterestEvent": { + "title": "InterestEvent", + "type": "object", + "properties": { + "appId": { + "type": "string" + }, + "type": { + "$ref": "https://meta.comcast.com/firebolt/discovery#/definitions/InterestType" + }, + "reason": { + "$ref": "https://meta.comcast.com/firebolt/discovery#/definitions/InterestReason" + }, + "entity": { + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/EntityDetails" + } + }, + "required": [ + "appId", + "entity", + "type", + "reason" + ] + } + } + } +} \ No newline at end of file diff --git a/src/openrpc/discovery.json b/src/openrpc/discovery.json index eb7530a35..49b7e20aa 100644 --- a/src/openrpc/discovery.json +++ b/src/openrpc/discovery.json @@ -52,6 +52,10 @@ { "name": "capabilities", "x-provides": "xrn:firebolt:capability:discovery:entity-info" + }, + { + "name": "deprecated", + "x-alternative": "Discovery.details" } ], "summary": "Provide information about a program entity and its available watchable assets, such as entitlement status and price, via either a push or pull call flow.", @@ -409,7 +413,7 @@ } ], "result": { - "name": "success", + "name": "result", "value": true } } @@ -424,6 +428,10 @@ { "name": "capabilities", "x-provides": "xrn:firebolt:capability:discovery:purchased-content" + }, + { + "name": "deprecated", + "x-alternative": "Discovery.purchases" } ], "summary": "Provide a list of purchased content for the authenticated account, such as rentals and electronic sell through purchases.", @@ -636,7 +644,7 @@ "name": "identifiers", "summary": "A set of content identifiers for this call to action", "schema": { - "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/ContentIdentifiers" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/Entity" }, "required": true }, @@ -1467,6 +1475,156 @@ } } ] + }, + { + "name": "userInterest", + "summary": "Send an entity that the user has expressed interest in to the platform.", + "tags": [ + { + "name": "capabilities", + "x-provides": "xrn:firebolt:capability:discovery:interest" + } + ], + "params": [ + { + "name": "type", + "required": true, + "schema": { + "$ref": "https://meta.comcast.com/firebolt/discovery#/definitions/InterestType" + } + }, + { + "name": "reason", + "required": true, + "schema": { + "$ref": "https://meta.comcast.com/firebolt/discovery#/definitions/InterestReason" + } + }, + { + "name": "entity", + "required": true, + "schema": { + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/EntityDetails" + } + } + ], + "result": { + "name": "result", + "schema": { + "type": "null" + } + }, + "examples": [ + { + "name": "Default Example", + "params": [ + { + "name": "type", + "value": "interest" + }, + { + "name": "reason", + "value": "playlist" + }, + { + "name": "entity", + "value": { + "identifiers": { + "entityId": "345", + "entityType": "program", + "programType": "movie" + }, + "info": {} + } + } + ], + "result": { + "name": "result", + "value": null + } + } + ] + }, + { + "name": "Discovery.onRequestUserInterest", + "tags": [ + { + "name": "rpc-only" + }, + { + "name": "event", + "x-response-name": "entity", + "x-response": { + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/EntityDetails", + "examples": [ + { + "identifiers": { + "entityId": "345", + "entityType": "program", + "programType": "movie" + }, + "info": { + "title": "Cool Runnings", + "synopsis": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Pulvinar sapien et ligula ullamcorper malesuada proin libero nunc.", + "releaseDate": "1993-01-01T00:00:00.000Z", + "contentRatings": [ + { + "scheme": "US-Movie", + "rating": "PG" + }, + { + "scheme": "CA-Movie", + "rating": "G" + } + ] + } + } + ] + } + }, + { + "name": "capabilities", + "x-provides": "xrn:firebolt:capability:discovery:interest" + } + ], + "summary": "Provide information about the entity currently displayed or selected on the screen.", + "description": "Provide information about the entity currently displayed or selected on the screen.", + "params": [], + "result": { + "name": "request", + "schema": { + "type": "object", + "required": [ + "correlationId", + "parameters" + ], + "properties": { + "correlationId": { + "type": "string" + }, + "parameters": { + "$ref": "#/components/schemas/UserInterestProviderParameters" + } + }, + "additionalProperties": false + } + }, + "examples": [ + { + "name": "Default Example", + "params": [], + "result": { + "name": "request", + "value": { + "correlationId": "xyz", + "parameters": { + "type": "interest", + "reason": "playlist" + } + } + } + } + ] } ], "components": { @@ -1785,6 +1943,22 @@ "enum": [ "xrn:firebolt:channel:any" ] + }, + "UserInterestProviderParameters": { + "title": "UserInterestProviderParameters", + "type": "object", + "required": [ + "type", + "reason" + ], + "properties": { + "type": { + "$ref": "https://meta.comcast.com/firebolt/discovery#/definitions/InterestType" + }, + "reason": { + "$ref": "https://meta.comcast.com/firebolt/discovery#/definitions/InterestReason" + } + } } } } diff --git a/src/openrpc/keyboard.json b/src/openrpc/keyboard.json index 0ef1f725b..9d8e39412 100644 --- a/src/openrpc/keyboard.json +++ b/src/openrpc/keyboard.json @@ -11,6 +11,7 @@ "tags": [ { "name": "capabilities", + "x-provided-by": "Keyboard.onRequestEmail", "x-uses": [ "xrn:firebolt:capability:input:keyboard" ] @@ -84,6 +85,7 @@ "tags": [ { "name": "capabilities", + "x-provided-by": "Keyboard.onRequestPassword", "x-uses": [ "xrn:firebolt:capability:input:keyboard" ] @@ -128,6 +130,7 @@ "tags": [ { "name": "capabilities", + "x-provided-by": "Keyboard.onRequestStandard", "x-uses": [ "xrn:firebolt:capability:input:keyboard" ] @@ -175,11 +178,9 @@ { "name": "event", "x-response": { - "$ref": "#/components/schemas/KeyboardResult", + "type": "string", "examples": [ - { - "text": "username" - } + "username" ] } }, @@ -220,11 +221,9 @@ { "name": "event", "x-response": { - "$ref": "#/components/schemas/KeyboardResult", + "type": "string", "examples": [ - { - "text": "password" - } + "password" ] } }, @@ -265,11 +264,9 @@ { "name": "event", "x-response": { - "$ref": "#/components/schemas/KeyboardResult", + "type": "string", "examples": [ - { - "text": "email@address.com" - } + "email@address.com" ] } }, @@ -359,23 +356,6 @@ "$ref": "#/components/schemas/KeyboardParameters" } } - }, - "KeyboardResult": { - "title": "KeyboardResult", - "type": "object", - "required": [ - "text" - ], - "properties": { - "text": { - "type": "string", - "description": "The text the user entered into the keyboard" - }, - "canceled": { - "type": "boolean", - "description": "Whether the user canceled entering text before they were finished typing on the keyboard" - } - } } } } diff --git a/src/schemas/advertising.json b/src/schemas/advertising.json new file mode 100644 index 000000000..e3aca3ede --- /dev/null +++ b/src/schemas/advertising.json @@ -0,0 +1,23 @@ +{ + "$id": "https://meta.comcast.com/firebolt/advertising", + "title": "Advertising", + "oneOf": [ + { + "$ref": "#/definitions/SkipRestriction" + } + ], + "definitions": { + "SkipRestriction": { + "title": "SkipRestriction", + "$comment": "xrn:advertising:policy:skipRestriction:", + "type": "string", + "enum": [ + "none", + "adsUnwatched", + "adsAll", + "all" + ], + "description": "The advertisement skip restriction.\n\nApplies to fast-forward/rewind (e.g. trick mode), seeking over an entire opportunity (e.g. jump), seeking out of what's currently playing, and \"Skip this ad...\" features. Seeking over multiple ad opportunities only requires playback of the _last_ opportunity, not all opportunities, preceding the seek destination.\n\n| Value | Description |\n|--------------|--------------------------------------------------------------------------------|\n| none |No fast-forward, jump, or skip restrictions |\n| adsUnwatched | Restrict fast-forward, jump, and skip for unwatched ad opportunities only. |\n| adsAll | Restrict fast-forward, jump, and skip for all ad opportunities |\n| all | Restrict fast-forward, jump, and skip for all ad opportunities and all content |\n\nNamespace: `xrn:advertising:policy:skipRestriction:`\n\n" + } + } +} \ No newline at end of file diff --git a/src/schemas/discovery.json b/src/schemas/discovery.json new file mode 100644 index 000000000..1c13435a7 --- /dev/null +++ b/src/schemas/discovery.json @@ -0,0 +1,79 @@ +{ + "$id": "https://meta.comcast.com/firebolt/discovery", + "title": "Discovery", + "anyOf": [ + { + "$ref": "#/definitions/PurchasedContentResult" + } + ], + "definitions": { + "PurchasedContentResult": { + "title": "PurchasedContentResult", + "type": "object", + "properties": { + "expires": { + "type": "string", + "format": "date-time" + }, + "totalCount": { + "type": "integer", + "minimum": 0 + }, + "entries": { + "type": "array", + "items": { + "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/EntityInfo" + } + } + }, + "required": [ + "expires", + "totalCount", + "entries" + ], + "additionalProperties": false + }, + "EntityInfoResult": { + "title": "EntityInfoResult", + "description": "The result for an `entityInfo()` push or pull.", + "type": "object", + "properties": { + "expires": { + "type": "string", + "format": "date-time" + }, + "entity": { + "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/EntityInfo" + }, + "related": { + "type": "array", + "items": { + "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/EntityInfo" + } + } + }, + "required": [ + "expires", + "entity" + ], + "additionalProperties": false + }, + "InterestType": { + "title": "InterestType", + "type": "string", + "enum": [ + "interest", + "disinterest" + ] + }, + "InterestReason": { + "title": "InterestReason", + "type": "string", + "enum": [ + "playlist", + "reaction", + "recording" + ] + } + } + } diff --git a/src/schemas/entertainment.json b/src/schemas/entertainment.json index 8fd14ee38..8f94854e8 100644 --- a/src/schemas/entertainment.json +++ b/src/schemas/entertainment.json @@ -80,7 +80,6 @@ "required": [ "identifiers", "entityType", - "programType", "title" ], "properties": { diff --git a/src/schemas/entity.json b/src/schemas/entity.json new file mode 100644 index 000000000..2e486e865 --- /dev/null +++ b/src/schemas/entity.json @@ -0,0 +1,456 @@ +{ + "$id": "https://meta.comcast.com/firebolt/entity", + "title": "Entity", + "oneOf": [ + { + "$ref": "#/definitions/Entity" + }, + { + "$ref": "#/definitions/PlayableEntity" + }, + { + "$ref": "#/definitions/EntityDetails" + } + ], + "definitions": { + "Entity": { + "oneOf": [ + { + "$ref": "#/definitions/ProgramEntity" + }, + { + "$ref": "#/definitions/MusicEntity" + }, + { + "$ref": "#/definitions/ChannelEntity" + }, + { + "$ref": "#/definitions/UntypedEntity" + }, + { + "$ref": "#/definitions/PlaylistEntity" + } + ] + }, + "EntityDetails": { + "title": "EntityDetails", + "type": "object", + "required": [ + "identifiers" + ], + "properties": { + "identifiers": { + "$ref": "#/definitions/Entity" + }, + "info": { + "$ref": "#/definitions/Metadata" + }, + "waysToWatch": { + "type": "array", + "items": { + "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/WayToWatch" + }, + "description": "An array of ways a user is might watch this entity, regardless of entitlements." + } + } + }, + "ChannelEntity": { + "title": "ChannelEntity", + "type": "object", + "properties": { + "entityType": { + "const": "channel" + }, + "channelType": { + "type": "string", + "enum": [ + "streaming", + "overTheAir" + ] + }, + "entityId": { + "type": "string", + "description": "ID of the channel, in the target App's scope." + }, + "appContentData": { + "type": "string", + "maxLength": 256 + } + }, + "required": [ + "entityType", + "channelType", + "entityId" + ], + "additionalProperties": false + }, + "ProgramEntity": { + "title": "ProgramEntity", + "oneOf": [ + { + "$ref": "#/definitions/MovieEntity" + }, + { + "$ref": "#/definitions/TVEpisodeEntity" + }, + { + "$ref": "#/definitions/TVSeasonEntity" + }, + { + "$ref": "#/definitions/TVSeriesEntity" + }, + { + "$ref": "#/definitions/AdditionalEntity" + } + ] + }, + "MusicEntity": { + "title": "MusicEntity", + "type": "object", + "properties": { + "entityType": { + "const": "music" + }, + "musicType": { + "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/MusicType" + }, + "entityId": { + "type": "string" + } + }, + "required": [ + "entityType", + "musicType", + "entityId" + ] + }, + "MovieEntity": { + "title": "MovieEntity", + "description": "A Firebolt compliant representation of a Movie entity.", + "type": "object", + "required": [ + "entityType", + "programType", + "entityId" + ], + "properties": { + "entityType": { + "const": "program" + }, + "programType": { + "const": "movie" + }, + "entityId": { + "type": "string" + }, + "assetId": { + "type": "string" + }, + "appContentData": { + "type": "string", + "maxLength": 256 + } + }, + "additionalProperties": false, + "examples": [ + { + "entityType": "program", + "programType": "movie", + "entityId": "el-camino" + } + ] + }, + "TVEpisodeEntity": { + "title": "TVEpisodeEntity", + "description": "A Firebolt compliant representation of a TV Episode entity.", + "type": "object", + "required": [ + "entityType", + "programType", + "entityId", + "seriesId", + "seasonId" + ], + "properties": { + "entityType": { + "const": "program" + }, + "programType": { + "const": "episode" + }, + "entityId": { + "type": "string" + }, + "seriesId": { + "type": "string" + }, + "seasonId": { + "type": "string" + }, + "assetId": { + "type": "string" + }, + "appContentData": { + "type": "string", + "maxLength": 256 + } + }, + "additionalProperties": false, + "examples": [ + { + "entityType": "program", + "programType": "episode", + "entityId": "breaking-bad-pilot", + "seriesId": "breaking-bad", + "seasonId": "breaking-bad-season-1" + } + ] + }, + "TVSeasonEntity": { + "title": "TVSeasonEntity", + "description": "A Firebolt compliant representation of a TV Season entity.", + "type": "object", + "required": [ + "entityType", + "programType", + "entityId", + "seriesId" + ], + "properties": { + "entityType": { + "const": "program" + }, + "programType": { + "const": "season" + }, + "entityId": { + "type": "string" + }, + "seriesId": { + "type": "string" + }, + "assetId": { + "type": "string" + }, + "appContentData": { + "type": "string", + "maxLength": 256 + } + }, + "additionalProperties": false, + "examples": [ + { + "entityType": "program", + "programType": "season", + "entityId": "breaking-bad-season-1", + "seriesId": "breaking-bad" + } + ] + }, + "TVSeriesEntity": { + "title": "TVSeriesEntity", + "description": "A Firebolt compliant representation of a TV Series entity.", + "type": "object", + "required": [ + "entityType", + "programType", + "entityId" + ], + "properties": { + "entityType": { + "const": "program" + }, + "programType": { + "const": "series" + }, + "entityId": { + "type": "string" + }, + "assetId": { + "type": "string" + }, + "appContentData": { + "type": "string", + "maxLength": 256 + } + }, + "additionalProperties": false, + "examples": [ + { + "entityType": "program", + "programType": "series", + "entityId": "breaking-bad" + } + ] + }, + "PlaylistEntity": { + "title": "PlaylistEntity", + "description": "A Firebolt compliant representation of a Playlist entity.", + "type": "object", + "required": [ + "entityType", + "entityId" + ], + "properties": { + "entityType": { + "const": "playlist" + }, + "entityId": { + "type": "string" + }, + "assetId": { + "type": "string" + }, + "appContentData": { + "type": "string", + "maxLength": 256 + } + }, + "additionalProperties": false, + "examples": [ + { + "entityType": "playlist", + "entityId": "playlist/xyz" + } + ] + }, + "PlayableEntity": { + "title": "PlayableEntity", + "anyOf": [ + { + "$ref": "#/definitions/MovieEntity" + }, + { + "$ref": "#/definitions/TVEpisodeEntity" + }, + { + "$ref": "#/definitions/PlaylistEntity" + }, + { + "$ref": "#/definitions/MusicEntity" + }, + { + "$ref": "#/definitions/AdditionalEntity" + } + ] + }, + "AdditionalEntity": { + "title": "AdditionalEntity", + "description": "A Firebolt compliant representation of the remaining program entity types.", + "type": "object", + "required": [ + "entityType", + "programType", + "entityId" + ], + "properties": { + "entityType": { + "const": "program" + }, + "programType": { + "type": "string", + "enum": [ + "concert", + "sportingEvent", + "preview", + "other", + "advertisement", + "musicVideo", + "minisode", + "extra" + ] + }, + "entityId": { + "type": "string" + }, + "assetId": { + "type": "string" + }, + "appContentData": { + "type": "string", + "maxLength": 256 + } + }, + "additionalProperties": false, + "examples": [ + { + "entityType": "program", + "programType": "concert", + "entityId": "live-aid" + } + ] + }, + "UntypedEntity": { + "title": "UntypedEntity", + "allOf": [ + { + "description": "A Firebolt compliant representation of the remaining entity types.", + "type": "object", + "required": [ + "entityId" + ], + "properties": { + "entityId": { + "type": "string" + }, + "assetId": { + "type": "string" + }, + "appContentData": { + "type": "string", + "maxLength": 256 + } + }, + "additionalProperties": false + } + ], + "examples": [ + { + "entityId": "an-entity" + } + ] + }, + "Metadata": { + "title": "Metadata", + "type": "object", + "properties": { + "title": { + "type": "string", + "description": "Title of the entity." + }, + "synopsis": { + "type": "string", + "description": "Short description of the entity." + }, + "seasonNumber": { + "type": "number", + "description": "For TV seasons, the season number. For TV episodes, the season that the episode belongs to." + }, + "seasonCount": { + "type": "number", + "description": "For TV series, seasons, and episodes, the total number of seasons." + }, + "episodeNumber": { + "type": "number", + "description": "For TV episodes, the episode number." + }, + "episodeCount": { + "type": "number", + "description": "For TV seasons and episodes, the total number of episodes in the current season." + }, + "releaseDate": { + "type": "string", + "format": "date-time", + "description": "The date that the program or entity was released or first aired." + }, + "contentRatings": { + "type": "array", + "items": { + "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/ContentRating" + }, + "description": "A list of ContentRating objects, describing the entity's ratings in various rating schemes." + } + } + } + } +} \ No newline at end of file diff --git a/src/schemas/intents.json b/src/schemas/intents.json index 6fa2a5807..3ce3d1ca8 100644 --- a/src/schemas/intents.json +++ b/src/schemas/intents.json @@ -370,32 +370,7 @@ "const": "entity" }, "data": { - "anyOf": [ - { - "$ref": "#/definitions/MovieEntity" - }, - { - "$ref": "#/definitions/TVEpisodeEntity" - }, - { - "$ref": "#/definitions/TVSeriesEntity" - }, - { - "$ref": "#/definitions/TVSeasonEntity" - }, - { - "$ref": "#/definitions/MusicEntity" - }, - { - "$ref": "#/definitions/PlaylistEntity" - }, - { - "$ref": "#/definitions/AdditionalEntity" - }, - { - "$ref": "#/definitions/UntypedEntity" - } - ] + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/Entity" } } } @@ -414,398 +389,6 @@ } ] }, - "ChannelEntity": { - "title": "ChannelEntity", - "type": "object", - "properties": { - "entityType": { - "const": "channel" - }, - "channelType": { - "type": "string", - "enum": [ - "streaming", - "overTheAir" - ] - }, - "entityId": { - "type": "string", - "description": "ID of the channel, in the target App's scope." - }, - "appContentData": { - "type": "string", - "maxLength": 256 - } - }, - "required": [ - "entityType", - "channelType", - "entityId" - ], - "additionalProperties": false - }, - "ProgramEntity": { - "title": "ProgramEntity", - "type": "object", - "properties": { - "entityType": { - "const": "program" - }, - "programType": { - "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/ProgramType" - }, - "entityId": { - "type": "string" - } - }, - "required": [ - "entityType", - "programType", - "entityId" - ] - }, - "MusicEntity": { - "title": "MusicEntity", - "type": "object", - "properties": { - "entityType": { - "const": "music" - }, - "musicType": { - "$ref": "https://meta.comcast.com/firebolt/entertainment#/definitions/MusicType" - }, - "entityId": { - "type": "string" - } - }, - "required": [ - "entityType", - "musicType", - "entityId" - ] - }, - "MovieEntity": { - "title": "MovieEntity", - "allOf": [ - { - "$ref": "#/definitions/ProgramEntity" - }, - { - "description": "A Firebolt compliant representation of a Movie entity.", - "title": "MovieEntity", - "type": "object", - "required": [ - "entityType", - "programType", - "entityId" - ], - "properties": { - "entityType": { - "const": "program" - }, - "programType": { - "const": "movie" - }, - "entityId": { - "$ref": "#/definitions/Identifier" - }, - "assetId": { - "$ref": "#/definitions/Identifier" - }, - "appContentData": { - "type": "string", - "maxLength": 256 - } - }, - "additionalProperties": false - } - ], - "examples": [ - { - "entityType": "program", - "programType": "movie", - "entityId": "el-camino" - } - ] - }, - "TVEpisodeEntity": { - "title": "TVEpisodeEntity", - "allOf": [ - { - "$ref": "#/definitions/ProgramEntity" - }, - { - "description": "A Firebolt compliant representation of a TV Episode entity.", - "title": "TVEpisodeEntity", - "type": "object", - "required": [ - "entityType", - "programType", - "entityId", - "seriesId", - "seasonId" - ], - "properties": { - "entityType": { - "const": "program" - }, - "programType": { - "const": "episode" - }, - "entityId": { - "$ref": "#/definitions/Identifier" - }, - "seriesId": { - "$ref": "#/definitions/Identifier" - }, - "seasonId": { - "$ref": "#/definitions/Identifier" - }, - "assetId": { - "$ref": "#/definitions/Identifier" - }, - "appContentData": { - "type": "string", - "maxLength": 256 - } - }, - "additionalProperties": false - } - ], - "examples": [ - { - "entityType": "program", - "programType": "episode", - "entityId": "breaking-bad-pilot", - "seriesId": "breaking-bad", - "seasonId": "breaking-bad-season-1" - } - ] - }, - "TVSeasonEntity": { - "title": "TVSeasonEntity", - "description": "A Firebolt compliant representation of a TV Season entity.", - "allOf": [ - { - "$ref": "#/definitions/ProgramEntity" - }, - { - "type": "object", - "required": [ - "entityType", - "programType", - "entityId", - "seriesId" - ], - "properties": { - "entityType": { - "const": "program" - }, - "programType": { - "const": "season" - }, - "entityId": { - "$ref": "#/definitions/Identifier" - }, - "seriesId": { - "$ref": "#/definitions/Identifier" - }, - "assetId": { - "$ref": "#/definitions/Identifier" - }, - "appContentData": { - "type": "string", - "maxLength": 256 - } - }, - "additionalProperties": false - } - ], - "examples": [ - { - "entityType": "program", - "programType": "season", - "entityId": "breaking-bad-season-1", - "seriesId": "breaking-bad" - } - ] - }, - "TVSeriesEntity": { - "title": "TVSeriesEntity", - "allOf": [ - { - "$ref": "#/definitions/ProgramEntity" - }, - { - "description": "A Firebolt compliant representation of a TV Series entity.", - "type": "object", - "required": [ - "entityType", - "programType", - "entityId" - ], - "properties": { - "entityType": { - "const": "program" - }, - "programType": { - "const": "series" - }, - "entityId": { - "$ref": "#/definitions/Identifier" - }, - "assetId": { - "$ref": "#/definitions/Identifier" - }, - "appContentData": { - "type": "string", - "maxLength": 256 - } - }, - "additionalProperties": false - } - ], - "examples": [ - { - "entityType": "program", - "programType": "series", - "entityId": "breaking-bad" - } - ] - }, - "PlaylistEntity": { - "title": "PlaylistEntity", - "description": "A Firebolt compliant representation of a Playlist entity.", - "type": "object", - "required": [ - "entityType", - "entityId" - ], - "properties": { - "entityType": { - "const": "playlist" - }, - "entityId": { - "$ref": "#/definitions/Identifier" - }, - "assetId": { - "$ref": "#/definitions/Identifier" - }, - "appContentData": { - "type": "string", - "maxLength": 256 - } - }, - "additionalProperties": false, - "examples": [ - { - "entityType": "playlist", - "entityId": "playlist/xyz" - } - ] - }, - "PlayableEntity": { - "title": "PlayableEntity", - "anyOf": [ - { - "$ref": "#/definitions/MovieEntity" - }, - { - "$ref": "#/definitions/TVEpisodeEntity" - }, - { - "$ref": "#/definitions/PlaylistEntity" - }, - { - "$ref": "#/definitions/MusicEntity" - }, - { - "$ref": "#/definitions/AdditionalEntity" - } - ] - }, - "AdditionalEntity": { - "title": "AdditionalEntity", - "allOf": [ - { - "$ref": "#/definitions/ProgramEntity" - }, - { - "description": "A Firebolt compliant representation of the remaining entity types.", - "type": "object", - "required": [ - "entityType", - "entityId" - ], - "properties": { - "entityType": { - "const": "program" - }, - "programType": { - "type": "string", - "enum": [ - "concert", - "sportingEvent", - "preview", - "other", - "advertisement", - "musicVideo", - "minisode", - "extra" - ] - }, - "entityId": { - "$ref": "#/definitions/Identifier" - }, - "assetId": { - "$ref": "#/definitions/Identifier" - }, - "appContentData": { - "type": "string", - "maxLength": 256 - } - }, - "additionalProperties": false - } - ], - "examples": [ - { - "entityType": "program", - "programType": "concert", - "entityId": "live-aid" - } - ] - }, - "UntypedEntity": { - "title": "UntypedEntity", - "allOf": [ - { - "description": "A Firebolt compliant representation of the remaining entity types.", - "type": "object", - "required": [ - "entityId" - ], - "properties": { - "entityId": { - "$ref": "#/definitions/Identifier" - }, - "assetId": { - "$ref": "#/definitions/Identifier" - }, - "appContentData": { - "type": "string", - "maxLength": 256 - } - }, - "additionalProperties": false - } - ], - "examples": [ - { - "entityId": "an-entity" - } - ] - }, "TuneIntent": { "description": "A Firebolt compliant representation of a user intention to 'tune' to a traditional over-the-air broadcast, or an OTT Stream from an OTT or vMVPD App.", "title": "TuneIntent", @@ -833,7 +416,7 @@ "additionalProperties": false, "properties": { "entity": { - "$ref": "#/definitions/ChannelEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/ChannelEntity" }, "options": { "description": "The options property of the data property MUST have only one of the following fields.", @@ -902,7 +485,7 @@ "const": "playback" }, "data": { - "$ref": "#/definitions/PlayableEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/PlayableEntity" } } } @@ -1032,7 +615,7 @@ "type": "object", "properties": { "entity": { - "$ref": "#/definitions/PlayableEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/PlayableEntity" }, "options": { "type": "object", @@ -1272,7 +855,7 @@ "menus": { "type": "array", "items": { - "$ref": "#/definitions/Identifier" + "type": "string" }, "minItems": 1, "maxItems": 100 @@ -1283,7 +866,7 @@ "type": "object", "properties": { "appId": { - "$ref": "#/definitions/Identifier" + "type": "string" }, "exclude": { "type": "boolean" @@ -1369,19 +952,19 @@ "entity": { "anyOf": [ { - "$ref": "#/definitions/MovieEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/MovieEntity" }, { - "$ref": "#/definitions/TVEpisodeEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/TVEpisodeEntity" }, { - "$ref": "#/definitions/TVSeriesEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/TVSeriesEntity" }, { - "$ref": "#/definitions/TVSeasonEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/TVSeasonEntity" }, { - "$ref": "#/definitions/AdditionalEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/AdditionalEntity" } ] }, @@ -1391,24 +974,24 @@ "type": "object", "properties": { "appId": { - "$ref": "#/definitions/Identifier" + "type": "string" }, "entity": { "anyOf": [ { - "$ref": "#/definitions/MovieEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/MovieEntity" }, { - "$ref": "#/definitions/TVEpisodeEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/TVEpisodeEntity" }, { - "$ref": "#/definitions/TVSeriesEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/TVSeriesEntity" }, { - "$ref": "#/definitions/TVSeasonEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/TVSeasonEntity" }, { - "$ref": "#/definitions/AdditionalEntity" + "$ref": "https://meta.comcast.com/firebolt/entity#/definitions/AdditionalEntity" } ] } @@ -1419,7 +1002,7 @@ "menus": { "type": "array", "items": { - "$ref": "#/definitions/Identifier" + "type": "string" }, "minItems": 1, "maxItems": 100 @@ -1430,7 +1013,7 @@ "type": "object", "properties": { "appId": { - "$ref": "#/definitions/Identifier" + "type": "string" }, "exclude": { "type": "boolean" @@ -2205,6 +1788,7 @@ } ] }, + "Identifier": { "type": "string" }, @@ -2229,7 +1813,7 @@ "type": "string" }, "appId": { - "$ref": "#/definitions/Identifier" + "type": "string" } } }, diff --git a/src/schemas/lifecycle.json b/src/schemas/lifecycle.json new file mode 100644 index 000000000..906cf41ea --- /dev/null +++ b/src/schemas/lifecycle.json @@ -0,0 +1,38 @@ +{ + "$id": "https://meta.comcast.com/firebolt/lifecycle", + "title": "Lifecycle", + "oneOf": [ + { + "$ref": "#/definitions/LifecycleState" + }, + { + "$ref": "#/definitions/CloseReason" + } + ], + "definitions": { + "LifecycleState": { + "title": "LifecycleState", + "description": "The application lifecycle state", + "type": "string", + "enum": [ + "initializing", + "inactive", + "foreground", + "background", + "unloading", + "suspended" + ] + }, + "CloseReason": { + "title": "CloseReason", + "description": "The application close reason", + "type": "string", + "enum": [ + "remoteButton", + "userExit", + "done", + "error" + ] + } + } +} diff --git a/src/schemas/types.json b/src/schemas/types.json new file mode 100644 index 000000000..0caa49c1f --- /dev/null +++ b/src/schemas/types.json @@ -0,0 +1,194 @@ +{ + "$id": "https://meta.comcast.com/firebolt/types", + "title": "Types", + "definitions": { + "SemanticVersion": { + "title": "SemanticVersion", + "type": "object", + "properties": { + "major": { + "type": "integer", + "minimum": 0 + }, + "minor": { + "type": "integer", + "minimum": 0 + }, + "patch": { + "type": "integer", + "minimum": 0 + }, + "readable": { + "type": "string" + } + }, + "required": [ + "major", + "minor", + "patch", + "readable" + ], + "additionalProperties": false + }, + "AudioProfile": { + "title": "AudioProfile", + "type": "string", + "enum": [ + "stereo", + "dolbyDigital5.1", + "dolbyDigital7.1", + "dolbyDigital5.1+", + "dolbyDigital7.1+", + "dolbyAtmos" + ] + }, + "BooleanMap": { + "title": "BooleanMap", + "type": "object", + "additionalProperties": { + "type": "boolean" + } + }, + "FlatMap": { + "type": "object", + "additionalProperties": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + } + ] + } + }, + "LocalizedString": { + "title": "LocalizedString", + "description": "Localized string supports either a simple `string` or a Map of language codes to strings. When using a simple `string`, the current preferred langauge from `Localization.langauge()` is assumed.", + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + ], + "examples": [ + "A simple string, with no language code", + { + "en": "This is english", + "es": "esto es español" + } + ] + }, + "ListenResponse": { + "title": "ListenResponse", + "type": "object", + "required": [ + "event", + "listening" + ], + "properties": { + "event": { + "type": "string", + "pattern": "[a-zA-Z]+\\.on[A-Z][a-zA-Z]+" + }, + "listening": { + "type": "boolean" + } + }, + "additionalProperties": false + }, + "ProviderRequest": { + "title": "ProviderRequest", + "type": "object", + "required": [ + "correlationId" + ], + "additionalProperties": false, + "properties": { + "correlationId": { + "type": "string", + "description": "The id that was passed in to the event that triggered a provider method to be called" + }, + "parameters": { + "description": "The result of the provider response.", + "type": ["object", "null"] + } + } + }, + "ProviderResponse": { + "title": "ProviderResponse", + "type": "object", + "required": [ + "correlationId" + ], + "additionalProperties": false, + "properties": { + "correlationId": { + "type": "string", + "description": "The id that was passed in to the event that triggered a provider method to be called" + }, + "result": { + "description": "The result of the provider response." + } + } + }, + "Timeout": { + "title": "Timeout", + "description": "Defines the timeout in seconds. If the threshold for timeout is passed for any operation without a result it will throw an error.", + "type": "integer", + "default": 0, + "minimum": 0, + "maximum": 9999 + }, + "Dimensions": { + "type": "object", + "properties": { + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + } + }, + "required": [ "width", "height" ] + }, + "Image": { + "type": "object", + "properties": { + "uri": { + "description": "URI for the image. May be a relative path (e.g. ./foo/image.png) or absolute (e.g. https://foo.com/bar.png) depending on usage.", + "type": "string" + }, + "aspectRatio": { + "description": "The aspect ratio of the image", + "type": "string", + "pattern": "^\\d+x\\d+" + }, + "description": { + "description": "Description of the image.", + "type": "string" + }, + "type": { + "description": "The type of the image.", + "type": "string", + "enum": [ + "icon", "poster", "banner", "splash", "hero" + ] + } + }, + "required": [ + "uri", "aspectRatio", "type" + ] + } + } +} \ No newline at end of file diff --git a/src/sdks/core/sdk.config.json b/src/sdks/core/sdk.config.json index f406f8bea..14ba4d55c 100644 --- a/src/sdks/core/sdk.config.json +++ b/src/sdks/core/sdk.config.json @@ -69,7 +69,8 @@ ], "provide": [ "xrn:firebolt:capability:discovery:entity-info", - "xrn:firebolt:capability:discovery:purchased-content" + "xrn:firebolt:capability:discovery:purchased-content", + "xrn:firebolt:capability:discovery:interest" ] }, { diff --git a/src/sdks/core/test/suite/discovery.test.ts b/src/sdks/core/test/suite/discovery.test.ts index 638c57432..7402dce42 100644 --- a/src/sdks/core/test/suite/discovery.test.ts +++ b/src/sdks/core/test/suite/discovery.test.ts @@ -150,3 +150,12 @@ test("clear()", () => { const result: boolean = Discovery.clear(-1000); expect(result).toBeFalsy(); }); + +test("details() provider", () => { + + class myUserInterestProvider implements Discovery.UserInterestProvider { + userInterest(parameters?: object, session?: Discovery.ProviderSession): Promise { + return null + } + } +}) \ No newline at end of file diff --git a/src/sdks/discovery/.npmignore b/src/sdks/discovery/.npmignore new file mode 100644 index 000000000..0e2ba4d22 --- /dev/null +++ b/src/sdks/discovery/.npmignore @@ -0,0 +1,6 @@ +build/* +src/* +test/* +.DS_Store +jest.config.* +tsconfig.* \ No newline at end of file diff --git a/src/sdks/discovery/CHANGELOG.md b/src/sdks/discovery/CHANGELOG.md new file mode 100644 index 000000000..e69de29bb diff --git a/src/sdks/discovery/CONTRIBUTING.md b/src/sdks/discovery/CONTRIBUTING.md new file mode 120000 index 000000000..c97564d93 --- /dev/null +++ b/src/sdks/discovery/CONTRIBUTING.md @@ -0,0 +1 @@ +../../../CONTRIBUTING.md \ No newline at end of file diff --git a/src/sdks/discovery/LICENSE b/src/sdks/discovery/LICENSE new file mode 120000 index 000000000..5853aaea5 --- /dev/null +++ b/src/sdks/discovery/LICENSE @@ -0,0 +1 @@ +../../../LICENSE \ No newline at end of file diff --git a/src/sdks/discovery/NOTICE b/src/sdks/discovery/NOTICE new file mode 120000 index 000000000..295f6bdb3 --- /dev/null +++ b/src/sdks/discovery/NOTICE @@ -0,0 +1 @@ +../../../NOTICE \ No newline at end of file diff --git a/src/sdks/discovery/README.md b/src/sdks/discovery/README.md new file mode 100644 index 000000000..2ec46b95d --- /dev/null +++ b/src/sdks/discovery/README.md @@ -0,0 +1,26 @@ +--- +title: Firebolt Discovery SDK +--- + +[![semantic-release: conventional](https://img.shields.io/badge/semantic--release-conventional-e10079?logo=semantic-release)](https://github.com/semantic-release/semantic-release) + +# Firebolt Discovery SDK +For building Firebolt compliant apps for discovering first-party content on Firebolt devices. + +## Usage +To install, run: + +``` +npm install @firebolt-js/discovery-sdk +``` + +To use the package, import one of it's modules, e.g.: + +```js +import { Content } from '@firebolt-js/discovery-sdk' +``` + +## Contributing +The Firebolt SDKs are built using the Firebolt OpenRPC toolset: + +See [Firebolt OpenRPC](https://www.github.com/rdkcentral/firebolt-openrpc/), for more info. diff --git a/src/sdks/discovery/jest.config.json b/src/sdks/discovery/jest.config.json new file mode 100644 index 000000000..745370cfa --- /dev/null +++ b/src/sdks/discovery/jest.config.json @@ -0,0 +1,8 @@ +{ + "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|js?)$", + "transform": {}, + "testPathIgnorePatterns": ["/node_modules/", "/../../../test"], + "moduleFileExtensions": ["js", "jsx", "mjs"], + "verbose": true, + "testEnvironment": "jest-environment-jsdom" + } \ No newline at end of file diff --git a/src/sdks/discovery/package.json b/src/sdks/discovery/package.json new file mode 100644 index 000000000..40e4cd992 --- /dev/null +++ b/src/sdks/discovery/package.json @@ -0,0 +1,53 @@ +{ + "name": "@firebolt-js/discovery-sdk", + "version": "1.2.0-next.2", + "description": "The Firebolt Discovery JS SDK", + "main": "./dist/lib/firebolt-discovery.mjs", + "types": "./dist/lib/firebolt-discovery.d.ts", + "exports": { + ".": "./dist/lib/firebolt-discovery.mjs" + }, + "type": "module", + "scripts": { + "validate": "npx firebolt-openrpc validate --input ./dist/firebolt-discovery-open-rpc.json", + "sdk": "npx firebolt-openrpc sdk --input ./dist/firebolt-discovery-open-rpc.json --template ./src/js --output ./build/javascript/src", + "native": "npx firebolt-openrpc sdk --input ./dist/firebolt-discovery-open-rpc.json --template ./src/js --output ./build/c/src --language ../../../node_modules/@firebolt-js/openrpc/languages/c", + "compile": "cd ../../.. && npm run compile", + "slice": "npx firebolt-openrpc slice -i ../../../dist/firebolt-open-rpc.json --sdk ./sdk.config.json -o ./dist/firebolt-discovery-open-rpc.json", + "docs": "npx firebolt-openrpc docs --input ./dist/firebolt-discovery-open-rpc.json --output build/docs/markdown --as-path", + "wiki": "npx firebolt-openrpc docs --input ./dist/firebolt-discovery-open-rpc.json --output build/docs/markdown", + "dist:notest": "npm run clean && npm run slice && npm run validate && npm run sdk && npm run docs && npm run prettier && npm run dist:copy && echo 'Firebolt Discovery SDK /dist/ is ready.\n'", + "dist:copy": "npm run dist:copy:sdk && npm run dist:copy:docs", + "dist:copy:sdk": "mkdirp ./dist && cp -R build/javascript/src dist/lib && cp ./dist/firebolt-discovery-open-rpc.json ../../../dist/firebolt-discovery-open-rpc.json", + "dist:copy:docs": "mkdirp ./dist && cp -R build/docs/markdown dist/docs", + "dist": "npm run dist:notest && npm run test", + "clean": "rm -rf ./build && rm -rf ./dist", + "test:setup": "rm -rf test/transpiled-suite && npx tsc --target es6 --moduleResolution node --outDir test/transpiled-suite", + "test": "npm run test:setup && NODE_OPTIONS=--experimental-vm-modules npx --config=jest.config.json --detectOpenHandles jest", + "prepack": "node ../../js/version.mjs validate && npm run broilerplate", + "broilerplate": "rm ./CONTRIBUTING.md && cp ../../../CONTRIBUTING.md ./CONTRIBUTING.md && rm ./LICENSE && cp ../../../LICENSE ./LICENSE && rm ./NOTICE && cp ../../../NOTICE ./NOTICE", + "prettier": "prettier build/**/*.mjs --write --parser babel && prettier build/**/*.md --write --parser markdown" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rdkcentral/firebolt-core-sdk", + "directory": "src/sdks/discovery" + }, + "author": "", + "bugs": { + "url": "https://github.com/rdkcentral/firebolt-core-sdk/issues" + }, + "homepage": "https://github.com/rdkcentral/firebolt-core-sdk#readme", + "devDependencies": { + "jest": "^28.1.0", + "jest-environment-jsdom": "^28.1.3", + "prettier": "^3.1.0", + "typescript": "^4.6.4" + }, + "keywords": [ + "firebolt", + "apps", + "sdk" + ], + "license": "Apache-2.0" +} \ No newline at end of file diff --git a/src/sdks/discovery/sdk.config.json b/src/sdks/discovery/sdk.config.json new file mode 100644 index 000000000..535d3bb12 --- /dev/null +++ b/src/sdks/discovery/sdk.config.json @@ -0,0 +1,15 @@ +{ + "info": { + "title": "Firebolt Discovery SDK" + }, + "methods": [ + { + "module": "Content", + "use": [ + "xrn:firebolt:capability:discovery:purchased-content", + "xrn:firebolt:capability:discovery:entity-info", + "xrn:firebolt:capability:discovery:interest" + ] + } + ] +} diff --git a/src/sdks/discovery/src/js/sdk/index.mjs b/src/sdks/discovery/src/js/sdk/index.mjs new file mode 100644 index 000000000..a9f2f5ca6 --- /dev/null +++ b/src/sdks/discovery/src/js/sdk/index.mjs @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { setMockResponses } from './Transport/MockTransport.mjs' + +/* ${MOCK_IMPORTS} */ + +setMockResponses({ + /* ${MOCK_OBJECTS} */ +}) + +/* ${EXPORTS} */ +export { default as Log } from './Log/index.mjs' +export { default as Events } from './Events/index.mjs' +export { default as Settings } from './Settings/index.mjs' diff --git a/src/sdks/discovery/test/suite/content.test.ts b/src/sdks/discovery/test/suite/content.test.ts new file mode 100644 index 000000000..1ab4e98c8 --- /dev/null +++ b/src/sdks/discovery/test/suite/content.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright 2021 Comcast Cable Communications Management, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { test, expect } from "@jest/globals"; +import { Content } from "../../build/javascript/src/firebolt-discovery"; + +test("Content.requestUserInterest()", () => { + return Content.requestUserInterest(Content.InterestType.INTEREST, Content.InterestReason.PLAYLIST).then((interest: Content.InterestResult) => { + const entity = interest.entity + const appId = interest.appId + expect(appId).toBeDefined() + expect(entity).toBeDefined() + expect(entity.info.title).toBe("Cool Runnings") + }) +}); + +test("Content.onUserInterest()", () => { + return Content.listen('userInterest', (interest: Content.InterestEvent) => { + const entity = interest.entity + const appId = interest.appId + const reason = interest.reason + expect(interest['type']).toBeDefined() + expect(reason).toBeDefined() + expect(appId).toBeDefined() + expect(entity).toBeDefined() + expect(entity.info.title).toBe("Cool Runnings") + }) +}); diff --git a/src/sdks/discovery/tsconfig.json b/src/sdks/discovery/tsconfig.json new file mode 100644 index 000000000..56ff58bc8 --- /dev/null +++ b/src/sdks/discovery/tsconfig.json @@ -0,0 +1,5 @@ +{ + "include": [ + "test/suite/*" + ] +} \ No newline at end of file diff --git a/src/sdks/manage/test/suite/keyboard.test.ts b/src/sdks/manage/test/suite/keyboard.test.ts index c40b7d40e..6ee3a5aed 100644 --- a/src/sdks/manage/test/suite/keyboard.test.ts +++ b/src/sdks/manage/test/suite/keyboard.test.ts @@ -82,19 +82,19 @@ class DelegatingKBProvider implements Keyboard.KeyboardInputProvider { standard( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { + ): Promise { return this.delegate.standard(parameters, session) } password( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { + ): Promise { return this.delegate.password(parameters, session) } email( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { + ): Promise { return this.delegate.email(parameters, session) } } @@ -103,21 +103,19 @@ class KBProvider implements Keyboard.KeyboardInputProvider { standard( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { - return Promise.resolve({ - text: 'foo' - }); + ): Promise { + return Promise.resolve('foo'); } password( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { + ): Promise { return Promise.resolve(null); } email( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { + ): Promise { return Promise.resolve(null); } } @@ -126,19 +124,19 @@ class KBProviderWithError implements Keyboard.KeyboardInputProvider { async standard( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { + ): Promise { throw new Error('failed') } async password( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { + ): Promise { throw new Error('failed') } async email( parameters: Keyboard.KeyboardParameters, session: Keyboard.FocusableProviderSession - ): Promise { + ): Promise { throw new Error('failed') } } @@ -156,7 +154,7 @@ test("Keyboard.provide() declarations", async () => { let result = await promise console.log(result) expect(result.method).toStrictEqual('keyboard.standardResponse') - expect(result.params.result.text).toStrictEqual('foo') + expect(result.params.result).toStrictEqual('foo') }); test("Keyboard.provide() with blank object", () => { From e68c06121875987ffd7e5e1d22f17ffc79c4935f Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 6 Jun 2024 20:16:46 +0000 Subject: [PATCH 13/21] chore(release): 1.2.0-next.3 [skip ci] # [1.2.0-next.3](https://github.com/rdkcentral/firebolt-apis/compare/v1.2.0-next.2...v1.2.0-next.3) (2024-06-06) ### Features * User Interest ([#170](https://github.com/rdkcentral/firebolt-apis/issues/170)) ([48a1094](https://github.com/rdkcentral/firebolt-apis/commit/48a1094aaab6418f09db662dbc81f090a34f32ed)) --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- src/sdks/core/package.json | 2 +- src/sdks/manage/package.json | 2 +- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca266e76a..53aa56313 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [1.2.0-next.3](https://github.com/rdkcentral/firebolt-apis/compare/v1.2.0-next.2...v1.2.0-next.3) (2024-06-06) + + +### Features + +* User Interest ([#170](https://github.com/rdkcentral/firebolt-apis/issues/170)) ([48a1094](https://github.com/rdkcentral/firebolt-apis/commit/48a1094aaab6418f09db662dbc81f090a34f32ed)) + # [1.2.0-next.2](https://github.com/rdkcentral/firebolt-apis/compare/v1.2.0-next.1...v1.2.0-next.2) (2024-04-08) diff --git a/package-lock.json b/package-lock.json index f9fe68d78..5dc4afb85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@firebolt-js/sdks", - "version": "1.2.0-next.2", + "version": "1.2.0-next.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@firebolt-js/sdks", - "version": "1.2.0-next.2", + "version": "1.2.0-next.3", "license": "Apache-2.0", "workspaces": [ "src/sdks/core", diff --git a/package.json b/package.json index d27b2efd0..b47c37184 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/sdks", - "version": "1.2.0-next.2", + "version": "1.2.0-next.3", "description": "The Firebolt JS SDK", "type": "module", "bin": { diff --git a/src/sdks/core/package.json b/src/sdks/core/package.json index b97be4a3f..0ef48a7fb 100644 --- a/src/sdks/core/package.json +++ b/src/sdks/core/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/sdk", - "version": "1.2.0-next.2", + "version": "1.2.0-next.3", "description": "The Firebolt JS SDK", "main": "./dist/lib/firebolt.mjs", "types": "./dist/lib/firebolt.d.ts", diff --git a/src/sdks/manage/package.json b/src/sdks/manage/package.json index c21e6138e..37a6cd1a5 100644 --- a/src/sdks/manage/package.json +++ b/src/sdks/manage/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/manage-sdk", - "version": "1.2.0-next.2", + "version": "1.2.0-next.3", "description": "The Firebolt Manage JS SDK", "main": "./dist/lib/firebolt-manage.mjs", "types": "./dist/lib/firebolt-manage.d.ts", From c8f8dae5a9a0f14a3815c04df5a55763823d4898 Mon Sep 17 00:00:00 2001 From: Jeremy LaCivita Date: Thu, 6 Jun 2024 17:13:12 -0400 Subject: [PATCH 14/21] feat: Command and Control Intents (#251) * feat: Command and Control Intents * Addition of the intents to control Firebolt compliant devices. Control intents are for user intentions that will be needed regardless of whether there are any apps installed such as Power Intents, Volume Intents, Channel Intents, Media Control Intents --- package-lock.json | 4 +- .../intents/command-and-control.md | 984 +++++++++++++ src/schemas/intents.json | 1277 +++++++++++++---- 3 files changed, 1999 insertions(+), 266 deletions(-) create mode 100644 requirements/specifications/intents/command-and-control.md diff --git a/package-lock.json b/package-lock.json index 5dc4afb85..d8d00eb1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16956,7 +16956,7 @@ }, "src/sdks/core": { "name": "@firebolt-js/sdk", - "version": "1.2.0-next.2", + "version": "1.2.0-next.3", "license": "Apache-2.0", "devDependencies": { "jest": "^28.1.0", @@ -16978,7 +16978,7 @@ }, "src/sdks/manage": { "name": "@firebolt-js/manage-sdk", - "version": "1.2.0-next.2", + "version": "1.2.0-next.3", "license": "Apache-2.0", "devDependencies": { "jest": "^28.1.0", diff --git a/requirements/specifications/intents/command-and-control.md b/requirements/specifications/intents/command-and-control.md new file mode 100644 index 000000000..db799cde9 --- /dev/null +++ b/requirements/specifications/intents/command-and-control.md @@ -0,0 +1,984 @@ +# Command and Control Intents + +Document Status: Proposed Specification + +See [Firebolt Requirements Governance](../../governance.md) for more info. + +| Contributor | Organization | +| ---------------- | ------------ | +| Saras Arveti | Comcast | +| Eileen Bengston | Comcast | +| Michael Driscoll | Comcast | +| Simon Grist | Sky | +| Jeremy LaCivita | Comcast | + +## 1. Overview + +This document outlines several basic Intents for controlling a Firebolt +compliant device. + +### 1.1. Message.type + +Message.type should be a useful grouping to bucket related intents +together for easier forwarding to appropriate components. + +## 2. Table of Contents +- [1. Overview](#1-overview) + - [1.1. Message.type](#11-messagetype) +- [2. Table of Contents](#2-table-of-contents) +- [3. Control Intents](#3-control-intents) + - [3.1. Power Intent](#31-power-intent) + - [3.2. Volume Intents](#32-volume-intents) + - [3.2.1. Volume Intent](#321-volume-intent) + - [3.2.2. Mute Intent](#322-mute-intent) + - [3.3. Channel Intent](#33-channel-intent) + - [3.4. Media Control Intents](#34-media-control-intents) + - [3.4.1. Pause, Play, Replay, and Stop Intents](#341-pause-play-replay-and-stop-intents) + - [3.4.2. Seek Intent](#342-seek-intent) + - [3.4.3. Fast-forward and Rewind Intents](#343-fast-forward-and-rewind-intents) + - [3.5. Accessibility Intents](#35-accessibility-intents) + - [3.5.1. Closed Captions Intent](#351-closed-captions-intent) + - [3.5.2. Voice Guidance Intent](#352-voice-guidance-intent) + - [3.5.3. Audio Descritions Intent](#353-audio-descritions-intent) + - [3.5.4. High Contrast Intent](#354-high-contrast-intent) + - [3.5.5. Screen Magnification Intent](#355-screen-magnification-intent) + - [3.6. Interaction Intents](#36-interaction-intents) + - [3.6.1. Focus Intent](#361-focus-intent) + - [3.6.2. Select Intent](#362-select-intent) + - [3.6.3. Scroll Intent](#363-scroll-intent) + - [3.6.4. Back Intent](#364-back-intent) + - [3.6.5. Exit Intent](#365-exit-intent) +- [4. Launch Intents](#4-launch-intents) + - [4.1. Content Discovery Launch Intents](#41-content-discovery-launch-intents) + - [4.2. Device Settings Launch Intent](#42-device-settings-launch-intent) + +## 3. Control Intents + +Control intents are for user intentions that will be needed regardless +of whether there are any apps installed. + +For example, these intents are all useful even if only using your TV +with a single HDMI input, and not for apps. + +### 3.1. Power Intent + +This intent allows a user to turn the device on or off. + +```json +{ + "type": "xrn:firebolt:intent:platform:power", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "power", + "data": { + "value": true | false + }, + "context": { + "source": "voice" + } + } +} +``` + +Additionally, this intent may specify a toggle: + +```json +{ + "type": "xrn:firebolt:intent:platform:power", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "power", + "data": { + "toggle": true + }, + "context": { + "source": "voice" + } + } +} +``` + +Additionally, this intent allows a user to set a timer for turning off +the power, aka a "sleep timer." + +This is handled by the optional field delay, which is measured in whole +seconds: + +```json +{ + "type": "xrn:firebolt:intent:platform:power", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "power", + "data": { + "value": true | false, + "delay": 3600 + }, + "context": { + "source": "voice" + } + } +} +``` + +To cancel a sleep timer, send a new intent without a delay. + +While it may not be implemented by all platforms, this could also be +used to turn on the TV with a timer. + +### 3.2. Volume Intents + +Volume Intents control the audio level of the device. + +#### 3.2.1. Volume Intent + +This intent allows setting the volume to an absolute or relative value. + +```json +{ + "type": "xrn:firebolt:intent:platform:volume", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "volume", + "data": { + "value": 70 + }, + "context": { + "source": "VOICE" + } + } +} + +``` + +The value is an integer value from 0 to 100. + +This intent also supports relative volume changes, by providing the +optional relative field: + +```json +{ + "type": "xrn:firebolt:intent:platform:volume", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "volume", + "data": { + "value": -10, + "relative": true + }, + "context": { + "source": "VOICE" + } + } +} + +``` + +The value is a positive or negative integer that is relative to a scale +of 0-100. + +Firebolt will not support complicated relative changes, e.g. "Set the +volume to 50% *of what it currently is\...*" + +Firebolt uses a size of 0-100 for this intent. It\'s up to each voice +integration if it wants to convert "5" to "50%" before generating +the intent, but convenience transformations like this are recommended. + +Whether or not a TV uses logarithmic or linear scale is irrelevant to +the VolumeIntent schema. + +#### 3.2.2. Mute Intent + +This intent allows the user to mute or unmute the device. + +```json +{ + "type": "xrn:firebolt:intent:platform:volume", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "mute", + "data": { + "value": true | false + }, + "context": { + "source": "VOICE" + } + } +} +``` + +### 3.3. Channel Intent + +For tuning to a specific channel, either OTA or in-app, see [Tune +Intents](./tune.md). + +The intents in this section are for relative next/previous channel user +intentions and are a separate type of Intent. This allows each app to +decide what "channel" means. For example, an App might simply take you +to the next section/genre if it doesn\'t have linear streams in it\'s +catalog. + +The goal of the action property to is tell the client how to parse the +Intent, so overloading the tune intent with a different structure is not +desirable. + +Also, +"tune" inherently means to zero in on a specific part of a +scale, e.g. tuning a harp. + +For relative "channel surfing" we\'ll use the more content-centric +action "channel" which will also align with non-linear apps that want +to leverage the channel up/down intent. + +The Channel Intent allows a user to scan "channels" in an app (or +actual OTA channels if not in an app). + +Users can scan to the next or previous channel. For scanning to the most +recent, i.e. "Last" channel, see [Interaction +Intent +](#interaction-intents). + +```json +{ + "type": "xrn:firebolt:intent:platform:channel", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "channel", + "data": { + "value": "next" | "previous" + }, + "context": { + "source": "voice" + } + } +} + +``` + +The value property MUST always be "next" or "previous". These are +chosen over up/down since not all use cases will be numeric. + +Since this intent is always relative to the current app, there is no +need for an appId. + +If this Intent needs to be passed to the current app, it can be passed +as-is, via the Discovery. onNavigateTo API, or a simulated RCU press of +one of the channel up/down buttons. + +### 3.4. Media Control Intents + +#### 3.4.1. Pause, Play, Replay, and Stop Intents + +These intents allow the user to pause and resume playback of the current +Media: + +```json +{ + "type": "xrn:firebolt:intent:platform:media-control", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "pause" | "play" | "replay" | "stop", + "context": { + "source": "voice" + } + } +} +``` + +If the action is pause, then the currently playing media should be +paused, with the frames on-screen and the video decoder ready to resume. + +If the action is play, and the current media is paused, then the +currently paused media should resume. + +If the action is play, and there is something playbable selected, then +playback of the selected asset should be initiated. + +If the action is replay, then the currently paused or playing media should restart +from the beginning. This should work even if the decoder has finished, +and its resources have been released. + +If the action is stop, then the currently playing media should be +stopped, frames removed from the screen, and any decoder resources +should be released. + +#### 3.4.2. Seek Intent + +The seek intent allows users to jump to a relative or absolute position +in the currently playing media. + +```json +{ + "type": "xrn:firebolt:intent:platform:media-control", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "seek", + "data": { + "seconds": 3600 + }, + "context": { + "source": "voice" + } + } +} +``` + +The seconds value is a positive integer representing where to seek. + +This intent also supports relative seeking, by providing the optional +relative field: + +```json +{ + "type": "xrn:firebolt:intent:platform:media-control", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "seek", + "data": { + "seconds": -30, + "relative": true + }, + "context": { + "source": "voice" + } + } +} +``` + +For relative seeking, the seconds value may be a positive or negative value. + +If a relative seek intent with a seconds value of `0` is received, the platform **SHOULD** ignore it, rather than rebuffering at the current position. + +#### 3.4.3. Fast-forward and Rewind Intents + +These intents allow users to fast-forward or rewind: + +```json +{ + "type": "xrn:firebolt:intent:platform:media-control", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "fast-forward" | "rewind", + "data": { + "speed": 2.5 + }, + "context": { + "source": "voice" + } + } +} +``` + +Speed is a float in the range of 0 (non-includsive) to 10 (inclusive), +with values between 0 and 1 denoting slow motion. + +It is a device-level decision how to implement different speeds, however +actual fast playback (with audio) should be used where possible and +reasonable, e.g. a speed of 1.5 should actually be playing the video w/ +sync\'d audio, while a speed of 10 will likely be using iframes and not +have audio. For rewind it is not important, and likely undesirable, to +provide audio. + +If speed is not provided then the device should cycle through a range +of speeds defined by the device. This range of speeds **COULD** include +the value `1` so that users can get back to normal speed if desired. + +### 3.5. Accessibility Intents + +These intents manipulate accessibility features on the device. + +#### 3.5.1. Closed Captions Intent + +This intent allows a user to turn closed captions on or off. + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "closed-captions", + "data": { + "value": true | false + }, + "context": { + "source": "voice" + } + } +} +``` + +Additionally, this intent may specify a toggle: + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "closed-captions", + "data": { + "toggle": true + }, + "context": { + "source": "voice" + } + } +} +``` + +#### 3.5.2. Voice Guidance Intent + +This intent allows a user to turn voice guidance on or off. + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "voice-guidance", + "data": { + "value": true | false + }, + "context": { + "source": "voice" + } + } +} + +``` + +Additionally, this intent may specify a toggle: + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "voice-guidance", + "data": { + "toggle": true + }, + "context": { + "source": "voice" + } + } +} +``` + +The intent **MAY** specify `speed` `number` property that specifies a speed from 0 to 10: + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "voice-guidance", + "data": { + "speed": 2 + }, + "context": { + "source": "voice" + } + } +} +``` + +When providing a `speed` this intent **MAY** also set the `relative` property to `true` denoting an increase or decrease in speed. The speed value may be between -5 and 5 inclusive: + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "voice-guidance", + "data": { + "speed": -1, + "relative": true + }, + "context": { + "source": "voice" + } + } +} +``` + +Finally, the intent **MAY** specify a `verbosity` property, which **MUST** use one of the following values is provided: + +| Value | Description | +|--------|-------------| +| `low` | to select shorter response, less context, and less detail; can use abbreviations and can selectively skip words | +| `high` | to select longer response, more context, and more detail; full comprehensive readout and explicit reflection of what is seen on screen | + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "voice-guidance", + "data": { + "value": true, + "verbosity": "low" + }, + "context": { + "source": "voice" + } + } +} +``` + +#### 3.5.3. Audio Descritions Intent + +This intent allows a user to turn audio descriptions of content on or off. + + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "audio-descriptions", + "data": { + "value": true | false + }, + "context": { + "source": "voice" + } + } +} + +``` + +This intent may specify a language: + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "audio-descriptions", + "data": { + "value": true, + "language": "eng" + }, + "context": { + "source": "voice" + } + } +} +``` + +The `language` must be a three character ISO 639 1/2 code, e.g. `eng`. + +Additionally, this intent may specify a toggle: + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "audio-descriptions", + "data": { + "toggle": true + }, + "context": { + "source": "voice" + } + } +} +``` + +#### 3.5.4. High Contrast Intent + +This intent allows a user to turn high contrast mode on or off. + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "high-contrast", + "data": { + "value": true | false + }, + "context": { + "source": "voice" + } + } +} + +``` + +Additionally, this intent may specify a toggle: + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "high-contrast", + "data": { + "toggle": true + }, + "context": { + "source": "voice" + } + } +} +``` + +#### 3.5.5. Screen Magnification Intent + +This intent allows a user to turn screen magnification on or off. + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "screen-magnification", + "data": { + "value": true | false + }, + "context": { + "source": "voice" + } + } +} + +``` + +Additionally, this intent may specify a toggle: + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "screen-magnification", + "data": { + "toggle": true + }, + "context": { + "source": "voice" + } + } +} +``` + +Finally, this intent may specify a magnification scale as a number: + +```json +{ + "type": "xrn:firebolt:intent:platform:accessibility", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "screen-magnification", + "data": { + "scale": 2.5 + }, + "context": { + "source": "voice" + } + } +} +``` + +Setting the scale to `1` turns off magnification. Setting the scale to a value greater than 1 turns on magnification. + +Even if a Firebolt platform does not support specifying the numeric scale, it **MUST** turn magnifacation on and off based on them. + +If the intent has the `toggle` property, then it **MUST NOT** have the `scale` or `value` property. + +If the intent has the `value` property, then it **MUST NOT** have the `toggle`. + +### 3.6. Interaction Intents + +Interaction Intents allow for voice (or other upstream intent service) +to control an on-screen UI without need for a keyboard or remote. + +#### 3.6.1. Focus Intent + +The Focus Intent allows users to move the focus / cursor +up/down/left/right: + +```json +{ + "type": "xrn:firebolt:intent:platform:interaction", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "focus", + "data": { + "direction": "up" | "down" | "left" | "right" + }, + "context": { + "source": "voice" + } + } +} + +``` + +Note that this does not give focus to a particular app, which is handled +by the "launch" action. + +These Intents will generate appropriate HTML browser keyCode events to +facilitate up/down/left/right key presses. + +#### 3.6.2. Select Intent + +The select intent allows users to tell an app select, e.g., +"click" on +whatever is focused. This is a platform-level intent that effectively +sends the "Ok" or "Select" key to the current app. + +```json +{ + "type": "xrn:firebolt:intent:platform:interaction", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "select", + "context": { + "source": "voice" + } + } +} +``` + +#### 3.6.3. Scroll Intent + +The Scroll Intent allows users to move the current view port +up/down/left/right: + +```json +{ + "type": "xrn:firebolt:intent:platform:interaction", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "scroll", + "data": { + "direction": "up" | "down" | "left" | "right", + "unit": "page" | "line" | "percent" + }, + "context": { + "source": "voice" + } + } +} +``` + +Both `direction` and `unit` are required. + +These Intents will generate appropriate browser / DOM scrolling +operations that don\'t require custom APIs. + +#### 3.6.4. Back Intent + +The back intent allows users to tell an app go to "back" like a +browser. This is a platform-level intent and will initiate a browser +back flow for web apps. For native apps, this will be converted to an +app Navigation Intent by the client and surfaced through the navigateTo +API. + +```json +{ + "type": "xrn:firebolt:intent:platform:interaction", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "back", + "context": { + "source": "voice" + } + } +} +``` + +#### 3.6.5. Exit Intent + +The exit intent allows users to tell an app close. This is a +platform-level intent and will simply move the current app into the +inactive state. + +```json +{ + "type": "xrn:firebolt:intent:platform:interaction", + "target": "client", + "metadata": { + "assistant": "XFINITY", + "lang": "eng-USA", + "micType": "NEAR_FIELD" + }, + "intent": { + "action": "exit", + "context": { + "source": "voice" + } + } +} +``` + +## 4. Launch Intents + +If a Firebolt app wants to launch the main or settings experience of the device, it can use one of the following abstract appIds with the `launch` intent. + +### 4.1. Content Discovery Launch Intents + +The following section IDs will be used, with the Firebolt application +type as the target App ID: + +`xrn:firebolt:application-type:main` + +### 4.2. Device Settings Launch Intent + +To launch the settings UI, a Launch Intent will be used, with the +Firebolt application type: + +`xrn:firebolt:application-type:settings` diff --git a/src/schemas/intents.json b/src/schemas/intents.json index 3ce3d1ca8..8f5a86c38 100644 --- a/src/schemas/intents.json +++ b/src/schemas/intents.json @@ -243,12 +243,33 @@ { "$ref": "#/definitions/ButtonIntent" }, + { + "$ref": "#/definitions/FocusIntent" + }, + { + "$ref": "#/definitions/SelectIntent" + }, + { + "$ref": "#/definitions/BackIntent" + }, + { + "$ref": "#/definitions/ExitIntent" + }, + { + "$ref": "#/definitions/ChannelIntent" + }, + { + "$ref": "#/definitions/ScrollIntent" + }, { "$ref": "#/definitions/PowerIntent" }, { "$ref": "#/definitions/VolumeIntent" }, + { + "$ref": "#/definitions/MuteIntent" + }, { "$ref": "#/definitions/MicrophoneIntent" }, @@ -257,15 +278,36 @@ }, { "$ref": "#/definitions/TuneIntent" + }, + { + "$ref": "#/definitions/VoiceGuidanceIntent" + }, + { + "$ref": "#/definitions/HighContrastIntent" + }, + { + "$ref": "#/definitions/ScreenMagnificationIntent" } ] }, "PlaybackControlIntent": { "description": "A Firebolt compliant representation of a user intention to control some aspect of in-progress playback.", "anyOf": [ + { + "$ref": "#/definitions/PlayIntent" + }, { "$ref": "#/definitions/PauseIntent" }, + { + "$ref": "#/definitions/ReplayIntent" + }, + { + "$ref": "#/definitions/StopIntent" + }, + { + "$ref": "#/definitions/PlaybackSpeedIntent" + }, { "$ref": "#/definitions/SeekIntent" }, @@ -273,13 +315,16 @@ "$ref": "#/definitions/SkipIntent" }, { - "$ref": "#/definitions/TrickPlayIntent" + "$ref": "#/definitions/FastForwardIntent" + }, + { + "$ref": "#/definitions/RewindIntent" }, { "$ref": "#/definitions/ClosedCaptionsIntent" }, { - "$ref": "#/definitions/AudioDescriptionIntent" + "$ref": "#/definitions/AudioDescriptionsIntent" } ] }, @@ -307,6 +352,9 @@ "action": { "const": "launch" } + }, + "not": { + "required": [ "data" ] } } ], @@ -338,6 +386,9 @@ "action": { "const": "home" } + }, + "not": { + "required": [ "data" ] } } ], @@ -389,6 +440,34 @@ } ] }, + "ChannelIntent": { + "description": "A Firebolt compliant representation of a user intent to 'surf' to the next or previous channel.", + "title": "ChannelIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" + }, + { + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "required": [ "data" ], + "properties": { + "action": { + "const": "channel" + }, + "data": { + "type": "string", + "enum": [ + "next", + "previous" + ] + } + } + } + ] + }, "TuneIntent": { "description": "A Firebolt compliant representation of a user intention to 'tune' to a traditional over-the-air broadcast, or an OTT Stream from an OTT or vMVPD App.", "title": "TuneIntent", @@ -577,7 +656,8 @@ }, "additionalProperties": false } - } + }, + "required": [ "data" ] } ], "examples": [ @@ -659,6 +739,7 @@ "type": "object", "properties": { "options": { + "type": "object", "maxProperties": 1 } } @@ -667,6 +748,7 @@ "type": "object", "properties": { "options": { + "type": "object", "maxProperties": 0 } } @@ -1070,9 +1152,9 @@ } ] }, - "ButtonIntent": { - "description": "A Firebolt compliant representation of a user intention to interact with their device in a way analogous to pressing one of the remote buttons.", - "title": "ButtonIntent", + "FocusIntent": { + "description": "A Firebolt compliant representation of a user intention to interact with their device in a way analogous to pressing remote directional pad buttons.", + "title": "FocusIntent", "allOf": [ { "$ref": "#/definitions/Intent" @@ -1082,30 +1164,24 @@ }, { "type": "object", + "required": [ "data" ], "properties": { "action": { - "const": "button" + "const": "focus" }, "data": { "type": "object", "required": [ - "operation" + "direction" ], "properties": { - "operation": { + "direction": { "type": "string", "enum": [ - "down", "up", - "prev", - "next", - "enter", - "exit", - "info", - "menu", - "back", - "cancel", - "record" + "down", + "left", + "right" ] } }, @@ -1116,9 +1192,9 @@ ], "examples": [ { - "action": "button", + "action": "focus", "data": { - "operation": "menu" + "direction": "up" }, "context": { "source": "voice" @@ -1126,9 +1202,9 @@ } ] }, - "VolumeIntent": { - "description": "A Firebolt compliant representation of a user intention to change the device volume.", - "title": "VolumeIntent", + "SelectIntent": { + "description": "A Firebolt compliant representation of a user intention to interact with their device in a way analogous to pressing the remote 'select' button.", + "title": "SelectIntent", "allOf": [ { "$ref": "#/definitions/Intent" @@ -1140,79 +1216,26 @@ "type": "object", "properties": { "action": { - "const": "volume" - }, - "data": { - "anyOf": [ - { - "type": "object", - "properties": { - "value": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "toggle": { - "const": true - } - }, - "minProperties": 1, - "maxProperties": 1, - "additionalProperties": false - }, - { - "type": "object", - "properties": { - "value": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "relative": { - "const": true - } - }, - "additionalProperties": false - } - ] + "const": "select" } + }, + "not": { + "required": [ "data" ] } } ], "examples": [ { - "action": "volume", - "data": { - "toggle": true - }, - "context": { - "source": "voice" - } - }, - { - "action": "volume", - "data": { - "value": 0.7 - }, - "context": { - "source": "voice" - } - }, - { - "action": "volume", - "data": { - "value": 0.1, - "relative": true - }, + "action": "select", "context": { "source": "voice" } } ] }, - "PowerIntent": { - "description": "A Firebolt compliant representation of a user intention to turn their device on or off.", - "title": "PowerIntent", + "BackIntent": { + "description": "A Firebolt compliant representation of a user intention to interact with their device in a way analogous to pressing the remote 'back' button.", + "title": "BackIntent", "allOf": [ { "$ref": "#/definitions/Intent" @@ -1224,38 +1247,26 @@ "type": "object", "properties": { "action": { - "const": "power" - }, - "data": { - "$ref": "#/definitions/BooleanToggle" + "const": "back" } + }, + "not": { + "required": [ "data" ] } } ], "examples": [ { - "action": "power", - "data": { - "value": false - }, - "context": { - "source": "voice" - } - }, - { - "action": "power", - "data": { - "toggle": true - }, + "action": "back", "context": { "source": "voice" } } ] }, - "MicrophoneIntent": { - "description": "A Firebolt compliant representation of a user intention to turn their microphone on or off.", - "title": "MicrophoneIntent", + "ExitIntent": { + "description": "A Firebolt compliant representation of a user intention to interact with their device in a way analogous to pressing the remote 'back' button.", + "title": "ExitIntent", "allOf": [ { "$ref": "#/definitions/Intent" @@ -1267,38 +1278,26 @@ "type": "object", "properties": { "action": { - "const": "microphone" - }, - "data": { - "$ref": "#/definitions/BooleanToggle" + "const": "exit" } + }, + "not": { + "required": [ "data" ] } } ], "examples": [ { - "action": "microphone", - "data": { - "value": false - }, - "context": { - "source": "voice" - } - }, - { - "action": "microphone", - "data": { - "toggle": true - }, + "action": "exit", "context": { "source": "voice" } } ] - }, - "InputIntent": { - "description": "A Firebolt compliant representation of a user intention to change which video input is active.", - "title": "InputIntent", + }, + "ScrollIntent": { + "description": "A Firebolt compliant representation of a user intention to interact with their device in a way analogous to pressing remote directional pad buttons.", + "title": "ScrollIntent", "allOf": [ { "$ref": "#/definitions/Intent" @@ -1308,29 +1307,33 @@ }, { "type": "object", + "required": [ "data" ], "properties": { "action": { - "const": "input" + "const": "scroll" }, "data": { "type": "object", "required": [ - "interface" + "direction" ], "properties": { - "interface": { + "direction": { "type": "string", "enum": [ - "hdmi", - "rca", - "vga", - "etc..." + "up", + "down", + "left", + "right" ] }, - "number": { - "type": "integer", - "minimum": 1, - "maximum": 100 + "unit": { + "type": "string", + "enum": [ + "line", + "page", + "percent" + ] } }, "additionalProperties": false @@ -1340,19 +1343,9 @@ ], "examples": [ { - "action": "input", - "data": { - "interface": "hdmi" - }, - "context": { - "source": "voice" - } - }, - { - "action": "input", + "action": "scroll", "data": { - "interface": "hdmi", - "number": 1 + "direction": "up" }, "context": { "source": "voice" @@ -1360,9 +1353,9 @@ } ] }, - "PauseIntent": { - "description": "A Firebolt compliant representation of a user intention to pause/unpause in-progress playback.", - "title": "PauseIntent", + "ButtonIntent": { + "description": "A Firebolt compliant representation of a user intention to interact with their device in a way analogous to pressing one of the remote buttons.", + "title": "ButtonIntent", "allOf": [ { "$ref": "#/definitions/Intent" @@ -1374,28 +1367,41 @@ "type": "object", "properties": { "action": { - "const": "pause" + "const": "button" }, "data": { - "$ref": "#/definitions/BooleanToggle" + "type": "object", + "required": [ + "operation" + ], + "properties": { + "operation": { + "type": "string", + "enum": [ + "down", + "up", + "prev", + "next", + "enter", + "exit", + "info", + "menu", + "back", + "cancel", + "record" + ] + } + }, + "additionalProperties": false } } } ], "examples": [ { - "action": "pause", - "data": { - "value": false - }, - "context": { - "source": "voice" - } - }, - { - "action": "pause", + "action": "button", "data": { - "toggle": true + "operation": "menu" }, "context": { "source": "voice" @@ -1403,9 +1409,9 @@ } ] }, - "PlaybackSpeedIntent": { - "description": "A Firebolt compliant representation of a user intention to change the speed of in-progress playback.", - "title": "PlaybackSpeedIntent", + "VolumeIntent": { + "description": "A Firebolt compliant representation of a user intention to change the device volume.", + "title": "VolumeIntent", "allOf": [ { "$ref": "#/definitions/Intent" @@ -1417,22 +1423,40 @@ "type": "object", "properties": { "action": { - "const": "speed" + "const": "volume" }, "data": { "type": "object", "properties": { "value": { - "type": "number", - "exclusiveMinimum": 0, - "maximum": 4 + "type": "number" }, - "toggle": { - "type": "boolean" + "relative": { + "const": true + } + }, + "required": [ "value" ], + "if": { + "required": [ "relative" ] + }, + "then": { + "properties": { + "value": { + "type": "number", + "minimum": -50, + "maximum": 50 + } + } + }, + "else": { + "properties": { + "value": { + "type": "number", + "minimum": 0, + "maximum": 100 + } } }, - "minProperties": 1, - "maxProperties": 1, "additionalProperties": false } } @@ -1440,18 +1464,606 @@ ], "examples": [ { - "action": "speed", + "action": "volume", + "data": { + "value": 70 + }, + "context": { + "source": "voice" + } + }, + { + "action": "volume", + "data": { + "value": 10, + "relative": true + }, + "context": { + "source": "voice" + } + } + ] + }, + "MuteIntent": { + "description": "A Firebolt compliant representation of a user intention to mute or unmute the device.", + "title": "MuteIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" + }, + { + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "properties": { + "action": { + "const": "mute" + }, + "data": { + "$ref": "#/definitions/BooleanToggle" + } + } + } + ], + "examples": [ + { + "action": "mute", + "data": { + "value": false + }, + "context": { + "source": "voice" + } + }, + { + "action": "mute", + "data": { + "toggle": true + }, + "context": { + "source": "voice" + } + } + ] + }, + "PowerIntent": { + "description": "A Firebolt compliant representation of a user intention to turn their device on or off.", + "title": "PowerIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" + }, + { + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "properties": { + "action": { + "const": "power" + }, + "data": { + "type": "object", + "properties": { + "value": { + "type": "boolean" + }, + "toggle": { + "const": true + }, + "delay": { + "type": "integer", + "minimum": 0 + } + }, + "if": { + "required": [ "value" ] + }, + "then": { + "not": { + "required": [ "toggle" ] + } + }, + "else": { + "required": [ "toggle" ] + }, + "additionalProperties": false + } + } + } + ], + "examples": [ + { + "action": "power", + "data": { + "value": false + }, + "context": { + "source": "voice" + } + }, + { + "action": "power", + "data": { + "toggle": true + }, + "context": { + "source": "voice" + } + }, + { + "action": "power", + "data": { + "value": false, + "delay": 900 + }, + "context": { + "source": "voice" + } + } + ] + }, + "MicrophoneIntent": { + "description": "A Firebolt compliant representation of a user intention to turn their microphone on or off.", + "title": "MicrophoneIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" + }, + { + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "properties": { + "action": { + "const": "microphone" + }, + "data": { + "$ref": "#/definitions/BooleanToggle" + } + } + } + ], + "examples": [ + { + "action": "microphone", + "data": { + "value": false + }, + "context": { + "source": "voice" + } + }, + { + "action": "microphone", + "data": { + "toggle": true + }, + "context": { + "source": "voice" + } + } + ] + }, + "InputIntent": { + "description": "A Firebolt compliant representation of a user intention to change which video input is active.", + "title": "InputIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" + }, + { + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "properties": { + "action": { + "const": "input" + }, + "data": { + "type": "object", + "required": [ + "interface" + ], + "properties": { + "interface": { + "type": "string", + "enum": [ + "hdmi" + ] + }, + "number": { + "type": "integer", + "minimum": 1, + "maximum": 100 + } + }, + "additionalProperties": false + } + } + } + ], + "examples": [ + { + "action": "input", + "data": { + "interface": "hdmi" + }, + "context": { + "source": "voice" + } + }, + { + "action": "input", + "data": { + "interface": "hdmi", + "number": 1 + }, + "context": { + "source": "voice" + } + } + ] + }, + "PauseIntent": { + "description": "A Firebolt compliant representation of a user intention to pause in-progress playback.", + "title": "PauseIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" + }, + { + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "properties": { + "action": { + "const": "pause" + } + } + } + ], + "examples": [ + { + "action": "pause", + "context": { + "source": "voice" + } + } + ] + }, + "PlayIntent": { + "description": "A Firebolt compliant representation of a user intention to play/resume content.", + "title": "PlayIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" + }, + { + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "properties": { + "action": { + "const": "play" + } + } + } + ], + "examples": [ + { + "action": "play", + "context": { + "source": "voice" + } + } + ] + }, + "ReplayIntent": { + "description": "A Firebolt compliant representation of a user intention to replay content.", + "title": "ReplayIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" + }, + { + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "properties": { + "action": { + "const": "replay" + } + } + } + ], + "examples": [ + { + "action": "replay", + "context": { + "source": "voice" + } + } + ] + }, + "StopIntent": { + "description": "A Firebolt compliant representation of a user intention to stop content.", + "title": "StopIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" + }, + { + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "properties": { + "action": { + "const": "stop" + } + } + } + ], + "examples": [ + { + "action": "stop", + "context": { + "source": "voice" + } + } + ] + }, + "PlaybackSpeedIntent": { + "description": "A Firebolt compliant representation of a user intention to change the speed of in-progress playback.", + "title": "PlaybackSpeedIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" + }, + { + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "properties": { + "action": { + "const": "speed" + }, + "data": { + "type": "object", + "properties": { + "value": { + "type": "number", + "exclusiveMinimum": 0, + "maximum": 4 + }, + "toggle": { + "type": "boolean" + } + }, + "minProperties": 1, + "maxProperties": 1, + "additionalProperties": false + } + } + } + ], + "examples": [ + { + "action": "speed", + "data": { + "value": 2 + }, + "context": { + "source": "voice" + } + }, + { + "action": "speed", + "data": { + "toggle": true + }, + "context": { + "source": "voice" + } + } + ] + }, + "FastForwardIntent": { + "description": "A Firebolt compliant representation of a user intention to fast-forward in-progress playback.", + "title": "FastForwardIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" + }, + { + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "properties": { + "action": { + "const": "fast-forward" + }, + "data": { + "type": "object", + "properties": { + "speed": { + "type": "number", + "exclusiveMinimum": 0, + "maximum": 10 + } + } + } + } + } + ], + "examples": [ + { + "action": "fast-forward", + "data": { + "speed": 2 + }, + "context": { + "source": "voice" + } + }, + { + "action": "fast-forward", + "data": { + "speed": 0.5 + }, + "context": { + "source": "voice" + } + }, + { + "action": "fast-forward", + "context": { + "source": "voice" + } + } + ] + }, + "RewindIntent": { + "description": "A Firebolt compliant representation of a user intention to rewind in-progress playback.", + "title": "RewindIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" + }, + { + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "properties": { + "action": { + "const": "rewind" + }, + "data": { + "type": "object", + "properties": { + "speed": { + "type": "number", + "exclusiveMinimum": 0, + "maximum": 10 + } + } + } + } + } + ], + "examples": [ + { + "action": "rewind", + "data": { + "speed": 2 + }, + "context": { + "source": "voice" + } + }, + { + "action": "rewind", + "data": { + "speed": 0.5 + }, + "context": { + "source": "voice" + } + }, + { + "action": "rewind", + "context": { + "source": "voice" + } + } + ] + }, + "SeekIntent": { + "description": "A Firebolt compliant representation of a user intention to seek to a different time for in-progress playback.", + "title": "SeekIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" + }, + { + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "properties": { + "action": { + "const": "seek" + }, + "data": { + "allOf": [ + { + "$ref": "#/definitions/DirectionalOperation" + }, + { + "type": "object", + "properties": { + "seconds": { + "type": "number" + }, + "relative": { + "const": true + } + }, + "required": [ "seconds" ], + "if": { + "not": { + "required": [ "relative" ] + } + }, + "then": { + "properties": { + "seconds": { + "type": "number", + "minimum": 0, + "maximum": 86400 + } + } + }, + "else": { + "properties": { + "seconds": { + "type": "number", + "minimum": -43200, + "maximum": 43200 + } + } + } + } + ] + } + } + } + ], + "examples": [ + { + "action": "seek", "data": { - "value": 2 + "seconds": 300 }, "context": { "source": "voice" } }, { - "action": "speed", + "action": "seek", "data": { - "toggle": true + "relative": true, + "seconds": -30 }, "context": { "source": "voice" @@ -1459,9 +2071,9 @@ } ] }, - "TrickPlayIntent": { - "description": "A Firebolt compliant representation of a user intention to fast-forward or rewind in-progress playback.", - "title": "TrickPlayIntent", + "SkipIntent": { + "description": "A Firebolt compliant representation of a user intention to skip a scene/chapter/ad during in-progress playback.", + "title": "SkipIntent", "allOf": [ { "$ref": "#/definitions/Intent" @@ -1473,7 +2085,7 @@ "type": "object", "properties": { "action": { - "const": "trickplay" + "const": "skip" }, "data": { "allOf": [ @@ -1483,16 +2095,16 @@ { "type": "object", "properties": { - "speed": { + "count": { "type": "number", "exclusiveMinimum": 0, - "maximum": 10 + "maximum": 100 } }, "propertyNames": { "enum": [ "direction", - "speed" + "count" ] } } @@ -1503,20 +2115,20 @@ ], "examples": [ { - "action": "trickplay", + "action": "skip", "data": { "direction": "forward", - "speed": 2 + "count": 1 }, "context": { "source": "voice" } }, { - "action": "trickplay", + "action": "skip", "data": { "direction": "backward", - "speed": 2 + "count": 1 }, "context": { "source": "voice" @@ -1524,9 +2136,9 @@ } ] }, - "SeekIntent": { - "description": "A Firebolt compliant representation of a user intention to seek to a different time for in-progress playback.", - "title": "SeekIntent", + "ClosedCaptionsIntent": { + "description": "A Firebolt compliant representation of a user intention to enable/disable closed captions.", + "title": "ClosedCaptionsIntent", "allOf": [ { "$ref": "#/definitions/Intent" @@ -1538,59 +2150,143 @@ "type": "object", "properties": { "action": { - "const": "seek" + "const": "closed-captions" }, "data": { - "allOf": [ - { - "$ref": "#/definitions/DirectionalOperation" + "$ref": "#/definitions/BooleanToggle" + } + } + } + ], + "examples": [ + { + "action": "closed-captions", + "data": { + "value": false + }, + "context": { + "source": "voice" + } + }, + { + "action": "closed-captions", + "data": { + "toggle": true + }, + "context": { + "source": "voice" + } + } + ] + }, + "VoiceGuidanceIntent": { + "description": "A Firebolt compliant representation of a user intention to enable/disable voice guidance.", + "title": "VoiceGuidanceIntent", + "allOf": [ + { + "$ref": "#/definitions/Intent" + }, + { + "$ref": "#/definitions/IntentProperties" + }, + { + "type": "object", + "properties": { + "action": { + "const": "voice-guidance" + }, + "data": { + "type": "object", + "properties": { + "value": { + "type": "boolean" }, - { - "type": "object", + "toggle": { + "const": true + }, + "speed": { + "type": "integer" + }, + "relative": { + "const": true + }, + "verbosity": { + "type": "string", + "enum": [ + "low", + "hight" + ] + } + }, + "if": { + "required": [ "value" ] + }, + "then": { + "not": { + "required": [ "toggle" ] + }, + "if": { + "required": [ "relative" ] + }, + "then": { "properties": { - "seconds": { - "type": "number", - "minimum": 0, - "maximum": 1800 + "speed": { + "type": "integer", + "minimum": -10, + "maximum": 10 + } + } + }, + "else": { + "properties": { + "speed": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 10 } - }, - "propertyNames": { - "enum": [ - "direction", - "seconds" - ] } } - ] + }, + "else": { + "if": { + "required": [ "toggle" ] + }, + "then": { + "not": { + "required": [ "value" ] + } + } + }, + "additionalProperties": false } } } ], "examples": [ { - "action": "seek", + "action": "voice-guidance", "data": { - "seconds": 300 + "value": true, + "verbosity": "low" }, "context": { "source": "voice" } }, { - "action": "seek", + "action": "voice-guidance", "data": { - "direction": "forward", - "seconds": 30 + "speed": -1, + "relative": true }, "context": { "source": "voice" } - }, + }, { - "action": "seek", + "action": "voice-guidance", "data": { - "direction": "backward", - "seconds": 30 + "toggle": true }, "context": { "source": "voice" @@ -1598,9 +2294,9 @@ } ] }, - "SkipIntent": { - "description": "A Firebolt compliant representation of a user intention to skip a scene/chapter/ad during in-progress playback.", - "title": "SkipIntent", + "AudioDescriptionsIntent": { + "description": "A Firebolt compliant representation of a user intention to enable/disable audio descriptions.", + "title": "AudioDescriptionsIntent", "allOf": [ { "$ref": "#/definitions/Intent" @@ -1612,50 +2308,61 @@ "type": "object", "properties": { "action": { - "const": "skip" + "const": "audio-descriptions" }, "data": { - "allOf": [ - { - "$ref": "#/definitions/DirectionalOperation" + "type": "object", + "properties": { + "value": { + "type": "boolean" }, - { - "type": "object", - "properties": { - "count": { - "type": "number", - "exclusiveMinimum": 0, - "maximum": 100 - } - }, - "propertyNames": { - "enum": [ - "direction", - "count" - ] - } + "toggle": { + "const": true + }, + "language": { + "$ref": "https://meta.comcast.com/firebolt/localization#/definitions/ISO639_2Language" } - ] + }, + "if": { + "required": [ "value" ] + }, + "then": { + "not": { + "required": [ "toggle" ] + } + }, + "else": { + "required": [ "toggle" ] + }, + "additionalProperties": false } } } ], "examples": [ { - "action": "skip", + "action": "audio-descriptions", "data": { - "direction": "forward", - "count": 1 + "value": false }, "context": { "source": "voice" } }, { - "action": "skip", + "action": "audio-descriptions", "data": { - "direction": "backward", - "count": 1 + "toggle": true + }, + "context": { + "source": "voice" + } + }, + { + "action": "audio-descriptions", + "data": { + "value": true, + "language": "eng" }, "context": { "source": "voice" @@ -1663,9 +2370,9 @@ } ] }, - "ClosedCaptionsIntent": { - "description": "A Firebolt compliant representation of a user intention to enable/disable closed captions.", - "title": "ClosedCaptionsIntent", + "HighContrastIntent": { + "description": "A Firebolt compliant representation of a user intention to enable or disable high contrast mode.", + "title": "HighContrastIntent", "allOf": [ { "$ref": "#/definitions/Intent" @@ -1677,7 +2384,7 @@ "type": "object", "properties": { "action": { - "const": "closedcaptions" + "const": "high-contrast" }, "data": { "$ref": "#/definitions/BooleanToggle" @@ -1687,7 +2394,7 @@ ], "examples": [ { - "action": "closedcaptions", + "action": "high-contrast", "data": { "value": false }, @@ -1696,7 +2403,7 @@ } }, { - "action": "closedcaptions", + "action": "high-contrast", "data": { "toggle": true }, @@ -1705,10 +2412,10 @@ } } ] - }, - "AudioDescriptionIntent": { - "description": "A Firebolt compliant representation of a user intention to enable/disable audio descriptions.", - "title": "AudioDescriptionIntent", + }, + "ScreenMagnificationIntent": { + "description": "A Firebolt compliant representation of a user intention to turn screen magnification on or off.", + "title": "ScreenMagnificationIntent", "allOf": [ { "$ref": "#/definitions/Intent" @@ -1720,17 +2427,40 @@ "type": "object", "properties": { "action": { - "const": "audiodescriptions" + "const": "screen-magnification" }, "data": { - "$ref": "#/definitions/BooleanToggle" + "type": "object", + "properties": { + "value": { + "type": "boolean" + }, + "toggle": { + "const": true + }, + "scale": { + "type": "number", + "minimum": 1, + "maximum": 10 + } + }, + "if": { + "required": [ + "toggle" + ] + }, + "then": { + "type": "object", + "maxProperties": 1 + }, + "additionalProperties": false } } } ], "examples": [ { - "action": "audiodescriptions", + "action": "screen-magnification", "data": { "value": false }, @@ -1739,14 +2469,33 @@ } }, { - "action": "audiodescriptions", + "action": "screen-magnification", + "data": { + "value": true, + "scale": 2.5 + }, + "context": { + "source": "voice" + } + }, + { + "action": "screen-magnification", "data": { "toggle": true }, "context": { "source": "voice" } - } + }, + { + "action": "screen-magnification", + "data": { + "value": false + }, + "context": { + "source": "voice" + } + } ] }, "MessageIntent": { From 52134181a84e4bd3b1bd9a8ca98c4c99bd442960 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 6 Jun 2024 21:17:06 +0000 Subject: [PATCH 15/21] chore(release): 1.2.0-next.4 [skip ci] # [1.2.0-next.4](https://github.com/rdkcentral/firebolt-apis/compare/v1.2.0-next.3...v1.2.0-next.4) (2024-06-06) ### Features * Command and Control Intents ([#251](https://github.com/rdkcentral/firebolt-apis/issues/251)) ([c8f8dae](https://github.com/rdkcentral/firebolt-apis/commit/c8f8dae5a9a0f14a3815c04df5a55763823d4898)) --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- src/sdks/core/package.json | 2 +- src/sdks/manage/package.json | 2 +- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53aa56313..97cdddff2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [1.2.0-next.4](https://github.com/rdkcentral/firebolt-apis/compare/v1.2.0-next.3...v1.2.0-next.4) (2024-06-06) + + +### Features + +* Command and Control Intents ([#251](https://github.com/rdkcentral/firebolt-apis/issues/251)) ([c8f8dae](https://github.com/rdkcentral/firebolt-apis/commit/c8f8dae5a9a0f14a3815c04df5a55763823d4898)) + # [1.2.0-next.3](https://github.com/rdkcentral/firebolt-apis/compare/v1.2.0-next.2...v1.2.0-next.3) (2024-06-06) diff --git a/package-lock.json b/package-lock.json index d8d00eb1a..d749e0c1a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@firebolt-js/sdks", - "version": "1.2.0-next.3", + "version": "1.2.0-next.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@firebolt-js/sdks", - "version": "1.2.0-next.3", + "version": "1.2.0-next.4", "license": "Apache-2.0", "workspaces": [ "src/sdks/core", diff --git a/package.json b/package.json index b47c37184..b63abe049 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/sdks", - "version": "1.2.0-next.3", + "version": "1.2.0-next.4", "description": "The Firebolt JS SDK", "type": "module", "bin": { diff --git a/src/sdks/core/package.json b/src/sdks/core/package.json index 0ef48a7fb..13c9535b3 100644 --- a/src/sdks/core/package.json +++ b/src/sdks/core/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/sdk", - "version": "1.2.0-next.3", + "version": "1.2.0-next.4", "description": "The Firebolt JS SDK", "main": "./dist/lib/firebolt.mjs", "types": "./dist/lib/firebolt.d.ts", diff --git a/src/sdks/manage/package.json b/src/sdks/manage/package.json index 37a6cd1a5..9f3cbc6d3 100644 --- a/src/sdks/manage/package.json +++ b/src/sdks/manage/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/manage-sdk", - "version": "1.2.0-next.3", + "version": "1.2.0-next.4", "description": "The Firebolt Manage JS SDK", "main": "./dist/lib/firebolt-manage.mjs", "types": "./dist/lib/firebolt-manage.d.ts", From 12628e2d4ce72937cd2721062baad9c5eb0fc157 Mon Sep 17 00:00:00 2001 From: Jeremy LaCivita Date: Thu, 13 Jun 2024 08:50:38 -0400 Subject: [PATCH 16/21] chore: Adding Capabilities Specification (#273) * chore(capabilities): Adding missing specs * fix: Cleanup of Capabilities requirements * Remove dangling requirements or replaced them with "This concept is out of scope for this document" --- .../general/capabilities/image1.png | Bin 0 -> 14067 bytes .../general/capabilities/image2.png | Bin 0 -> 15714 bytes .../general/capabilities/image4.png | Bin 0 -> 22598 bytes .../capabilities/user-grants/image1.png | Bin 0 -> 16914 bytes .../capabilities/user-grants/image2.png | Bin 0 -> 23485 bytes .../capabilities/user-grants/image3.png | Bin 0 -> 29394 bytes .../capabilities/user-grants/image4.png | Bin 0 -> 18794 bytes .../capabilities/user-grants/image5.png | Bin 0 -> 25027 bytes .../capabilities/user-grants/image6.png | Bin 0 -> 32865 bytes .../capabilities/user-grants/image7.png | Bin 0 -> 45710 bytes .../general/capabilities/capabilities.md | 660 ++++++++++++++++++ .../general/capabilities/user-grants.md | 593 ++++++++++++++++ 12 files changed, 1253 insertions(+) create mode 100644 requirements/images/specifications/general/capabilities/image1.png create mode 100644 requirements/images/specifications/general/capabilities/image2.png create mode 100644 requirements/images/specifications/general/capabilities/image4.png create mode 100644 requirements/images/specifications/general/capabilities/user-grants/image1.png create mode 100644 requirements/images/specifications/general/capabilities/user-grants/image2.png create mode 100644 requirements/images/specifications/general/capabilities/user-grants/image3.png create mode 100644 requirements/images/specifications/general/capabilities/user-grants/image4.png create mode 100644 requirements/images/specifications/general/capabilities/user-grants/image5.png create mode 100644 requirements/images/specifications/general/capabilities/user-grants/image6.png create mode 100644 requirements/images/specifications/general/capabilities/user-grants/image7.png create mode 100644 requirements/specifications/general/capabilities/capabilities.md create mode 100644 requirements/specifications/general/capabilities/user-grants.md diff --git a/requirements/images/specifications/general/capabilities/image1.png b/requirements/images/specifications/general/capabilities/image1.png new file mode 100644 index 0000000000000000000000000000000000000000..049fc70bfc801a22ca4f9f290982cfaa7af30e49 GIT binary patch literal 14067 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 literal 0 HcmV?d00001 diff --git a/requirements/images/specifications/general/capabilities/image2.png b/requirements/images/specifications/general/capabilities/image2.png new file mode 100644 index 0000000000000000000000000000000000000000..7c0c10f7c9b0eeca22d2fc67a1f42f2ab1d1102a GIT binary patch literal 15714 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 literal 0 HcmV?d00001 diff --git a/requirements/images/specifications/general/capabilities/image4.png b/requirements/images/specifications/general/capabilities/image4.png new file mode 100644 index 0000000000000000000000000000000000000000..641c268472ba949afca8d0a585f0dd8bbd3632d6 GIT binary patch literal 22598 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 t)yP7{yrGO;fVFW{XPBA7Xg^5fM;>j z>zi*5#{@K}j3ph_6QiZSCq2e>)OoQw_%VRicNF+_T*oFRxAPAI&Z50$)9WzH_R36T2&=Xlmlndjp=& zA~8z$et_EOEL#W%shFS$W|6b5jhaoIa1Wv($pu=0_{<3Ah)?FPxmjbQwOpI?_#%p+ zVtmaVlhWU>pfuH|A47leOiV%KUB27r zlQ4Mu|N9U4EK}Yb+GWt@@^3yomY;FQopG1Fa6T5^p+4}Rf{~$-(Y^zQ`wUIaA27E# uaM0r5fjx$X7KVmj7OznMr$R9Ha)592|6L)oq+e-8TPKg7wXHhp8UG(p_rHSx literal 0 HcmV?d00001 diff --git a/requirements/images/specifications/general/capabilities/user-grants/image3.png b/requirements/images/specifications/general/capabilities/user-grants/image3.png new file mode 100644 index 0000000000000000000000000000000000000000..96d73b0ab490f6e8ea76136d11c964cd25493021 GIT binary patch literal 29394 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

+&C%v+v_F=#xuCtivh8T&v?8lsg$Nh{&+?sN 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 BU5fw! literal 0 HcmV?d00001 diff --git a/requirements/images/specifications/general/capabilities/user-grants/image5.png b/requirements/images/specifications/general/capabilities/user-grants/image5.png new file mode 100644 index 0000000000000000000000000000000000000000..26c583d2d3d7b8f2bec9c3dd9dfeafeb76af3c00 GIT binary patch literal 25027 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 literal 0 HcmV?d00001 diff --git a/requirements/images/specifications/general/capabilities/user-grants/image6.png b/requirements/images/specifications/general/capabilities/user-grants/image6.png new file mode 100644 index 0000000000000000000000000000000000000000..4761e5ea43f86552bea08db32369004b4e99a139 GIT binary patch literal 32865 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 Orx$chbjr1^-2N|1(hz(A literal 0 HcmV?d00001 diff --git a/requirements/specifications/general/capabilities/capabilities.md b/requirements/specifications/general/capabilities/capabilities.md new file mode 100644 index 000000000..30430ec06 --- /dev/null +++ b/requirements/specifications/general/capabilities/capabilities.md @@ -0,0 +1,660 @@ +# Capabilities + +Document Status: Candidate Specification + +See [Firebolt Requirements Governance](../../../governance.md) for more info. + +| Contributor | Organization | +| -------------- | -------------- | +| Andrew Bennett | Sky | +| Tim Dibben | Sky | +| Jeremy LaCivita | Comcast | +| Kevin Pearson | Comcast | +| Peter Yu | Comcast | + +## 1. Overview + +This document introduces Firebolt Capabilities, which enable discovery +of supported features, negotiation of App permissions, and end-user +grants for Apps to access sensitive features. + +Apps can detect if Capabilities are supported and available. If so, Apps +can **Use**, **Provide**, or **Manage** capabilities that they are given +access to. + +The most common case is for an App to `use` a Capability, the platform +to `provide` it, and the Distributor\'s Settings App to `manage` it: + +![Diagram Description automatically +generated](../../../images/specifications/general/capabilities/image1.png) + +However, Apps may also provide Capabilities back to the platform: + +![Diagram Description automatically +generated](../../../images/specifications/general/capabilities/image2.png) + +## 2. Table of Contents +- [1. Overview](#1-overview) +- [2. Table of Contents](#2-table-of-contents) +- [3. Introduction to Capabilities](#3-introduction-to-capabilities) + - [3.1. Capabilities](#31-capabilities) + - [3.2. Availability](#32-availability) + - [3.3. Permissions](#33-permissions) + - [3.4. Roles](#34-roles) + - [3.5. User Grants](#35-user-grants) +- [4. How Capabilities Work](#4-how-capabilities-work) + - [4.1. Supported Capabilities](#41-supported-capabilities) + - [4.2. Available Capabilities](#42-available-capabilities) + - [4.3. Permitted Capabilities](#43-permitted-capabilities) + - [4.4. User Granted Capabilities](#44-user-granted-capabilities) + - [4.5. Invoking Capabilities](#45-invoking-capabilities) +- [5. Configuring Device Capabilities](#5-configuring-device-capabilities) + - [5.1. Device Supported Capabilities](#51-device-supported-capabilities) + - [5.2. Device Grant Policy Overrides](#52-device-grant-policy-overrides) +- [6. Capability Schemas](#6-capability-schemas) + - [6.1. Firebolt Specification Manifest](#61-firebolt-specification-manifest) + - [6.2. Firebolt Device Manifest](#62-firebolt-device-manifest) +- [7. Capability to Feature Mapping](#7-capability-to-feature-mapping) + - [7.1. App installation](#71-app-installation) + - [7.2. Firebolt APIs](#72-firebolt-apis) + - [7.3. Extension APIs](#73-extension-apis) + - [7.4. Hidden Permissions](#74-hidden-permissions) +- [8. Firebolt Capability Catalog](#8-firebolt-capability-catalog) +- [9. APIs](#9-apis) + - [9.1. Core SDK APIs](#91-core-sdk-apis) + - [9.2. Manage SDK APIs](#92-manage-sdk-apis) + +## 3. Introduction to Capabilities + +This section is non-normative and defines the key concepts behind the +Firebolt Capabilities system. Each portion of this section provides a +background and conversational definitions of the terms used in the rest +of this document, and through all Firebolt Requirements Specifications. + +### 3.1. Capabilities + +A Capability is a discrete unit of functionality that a Firebolt device +might be able to perform. It is granular enough to enable appropriate +access controls across all Firebolt Apps, but useful enough to be a +meaningful functional unit that an App might care about. Bluetooth is a +good example of a Capability. + +The Firebolt Specification determines the range of possible +Capabilities. + +Firebolt platforms advertise which Capabilities are supported. The set +of capabilities supported by a device is an immutable set and will never +change at runtime. Supporting Wi-Fi and having an active Wi-Fi +connection are two different things. + +Firebolt platforms determine if an App is allowed to invoke the +different aspects of a Capability (see [Roles](#roles), below). + +Firebolt Apps list which Capabilities are required and optional for the +App to be installed. + +Firebolt Apps inspect which Capabilities are supported and active. + +Firebolt Distributors sign off on some, or all, of the App's listed +required and optional Capabilities. + +### 3.2. Availability + +A Capability may rely on intermittently available resources, for example +a network capability requires an Ethernet or Wi-Fi network connection. + +A supported capability is only considered available if those resources +could be used by the App right now (without taking into account any needed permissions or [User Grants](#user-grants)). For example, the needed +resources are not disabled, not tied up by the platform, and not experiencing +any intermittent network, hardware, or signal issues. + +A supported capability is always supported, but it may not always be +*available*. + +Availability of a capability is a global status, not a per-app status. + +If a Capability is available, then it is considered available globally. +Capabilities can not be available to one app, while simultaneously being +unavailable to another app. This is handled by permissions, or by +capability-specific error management, e.g. returning an error to an app that +attempts to use the video pipeline while in the Inactive Lifecycle state. + +### 3.3. Permissions + +A Permission is what allows an App to attempt to invoke an aspect of a +Capability. Permissions to a Capability are assigned to an App with a +[Role](#roles), by a Firebolt Distributor. + +Firebolt documents and APIs will use the term "Permission" +interchangeably with phrases like: + +- An App's Assigned Role for a Capability + +- App + Capability + Role + +- Etc. + +The Firebolt Specification determines which Capabilities require +Permissions. + +The Firebolt Specification determines which Permission requirements may +be overridden by Distributors. + +An App is considered to have Permission to a Capability if it is: + +- Listed with a Role in the App's manifest and signed by the App + provider + +- Listed and signed again by the Distributor on the App's behalf + +- Listed and signed by the App Publisher when running in self-signed developer + mode + +An App can invoke a Capability if: + +- It has permission, which includes a Role, for that capability + +- The capability is supported by the current device + +- The App is invoking aspects of the capability that it has the + appropriate Role for + +- The capability is [Available](#availability) at time of invocation + +See [Permitted Capabilities](#permitted-capabilities) for more info. + +### 3.4. Roles + +Every Firebolt Capability has exactly three roles: + +- Use + +- Manage + +- Provide + +While these roles will be defined on a case-by-case basis for each +capability, they should generally follow the guidelines set here. + +The `Use` role is for providing basic access so that an App can leverage +the standard use cases of the Capability. The `Use` role will not enable +an App to perform management tasks such as turning the Capability on and +off for the entire device. The `Use` role will not enable an App to +perform administrative tasks such as renaming HDMI inputs, etc. + +The `Manage` role is for providing access to a Capability's management +and administrative features. This role may be used to allow an App to +build a Settings UI for the Capability, for example. The `Manage` role +is not for creating tiers of permissions within a Capability, but for +separating out administrative APIs, for example turning a feature on and +off. + +Most Capabilities are provided by the platform, denoted by making the +`Provide` role `private` for that Capability. However, there are use +cases where Apps may provide Capabilities. The `Provide` role allows for +declaring that an App *implements* the Capability and can be registered +with the platform for fulfilling the Capability's features. This could +be used for functionality that needs to be customized from Distributor +to Distributor. The `Provide` role may also be used to allow Apps to +contribute to aggregated functionality, for example a federated search +feature. By providing a capability, an App is signing up to implement +any `Use` or `Manage` APIs required by that capability. + +Each role must be explicitly assigned. An App *never* inherits the +operations from one role by virtue of being granted another. Roles may +have overlapping functionality. + +A [Permission](#permissions) is the combination of a Capability w/ a +Role and determines which specific operations are permitted. + +### 3.5. User Grants + +A User Grant allows some Permissions to depend on the User of the +Firebolt Device to explicitly grant access for one or all Apps to use a +specific Capability. This an additional layer of access control on top +of Permissions. + +The Firebolt Specification determines which Permissions require User +Grants by default. + +It is not possible to require a User Grant for a Capability that is open +to all Apps. + +Distributors may override any User Grant details that are defined by the +Firebolt Specification if that User Grant Policy is set to +`overridable: true`. + +Consumers want to know that their Smart TV platform treats +security as a first-class design principal. As such, table-stakes User +Grant policies, e.g. App access to Bluetooth, may be denoted as +overridable: false in the Firebolt Specification, to remove any +possibility of a security flaw. + +An App is considered to have a User Grant to a Capability if it: + +- Has Permission to the Capability + +- The User Grant is for the same Role as the Permission above + +- The Capability is **securely** approved by the device user + +If an App invokes a Capability but does not have an active User Grant, +then the platform will block invocation, prompt the user for a grant, +and then continue with the original invocation, e.g. the App does not +have to call the originating API again (assuming the user did give their +approval). + +See [User Granted Capabilities](#user-granted-capabilities) for more +info. + +## 4. How Capabilities Work + +A given Capability may or may not be supported or available. +Additionally, a given Role may or may not be permitted or granted for a +given Capability. + +While support and availability of a Capability is static and global, the +permission and grant status may differ from App to App. + +Determining the status of a Role and Capability is fundamental to +both the Platform and the Apps that run on it. + +### 4.1. Supported Capabilities + +Once an App has been launched, it may need to check if a certain +capability is supported to present the appropriate user experience. For +example, an App may want to put Dolby Atmos badges next to its content, +but only if the platform supports Dolby Atmos. + +The Firebolt Specification determines which capabilities **MUST** be +supported by all Firebolt Devices, by listing those Capabilities in the +[Firebolt Specification Schema](#61-firebolt-specification-schema) as `level: "must"` in the Capabilities +array. + +For a capability to be supported, it **MUST** have *one* or *both* of +the following: + +> A supported capability **MUST** have the necessary hardware and +> software to invoke all required aspects of all Roles of the +> capability. +> +> **OR** +> +> A supported capability **MUST** have the necessary hardware and +> software to support a *certified*, after-market peripheral that +> provides the capability. Note that this includes peripherals that have +> not yet been connected to or installed on the device. + +However, a Firebolt device **MUST NOT** be considered to support a +capability if the capability is disabled by the static distributor +configuration. + +The platform **MUST** return accurate responses based on these +requirements to all [APIs](#apis) outlined below. + +### 4.2. Available Capabilities + +Once an App has been launched, it may need to check if a certain +capability is available, to start the appropriate user experience. For +example, an App that uses an external camera may need to check if the +user has set up their camera before proceeding to the main user +experience. + +A Capability is available if **all** the following are true: + +> An available Capability **MUST** be supported. +> +> An available Capability **MUST** have a provider, either an App, an +> Extension SDK, or the OS itself. +> +> The provider of the Capability **MUST** consider it to be available +> +> An available Capability **MUST NOT** be currently disabled by any +> user, account, or device setting. +> +> An available Capability **MUST** be considered available by at least +> one provider of the capability. This is specified by the requirements +> for each feature. + +An available Capability **MAY** be tied to a User Grant, regardless of +whether the user has granted it yet. Secure user grants gate permission, +not availability. + +The platform **MUST** return accurate responses based on these +requirements to all [APIs](#apis) outlined below. + +### 4.3. Permitted Capabilities + +Capabilities may be permitted to *all* Apps by the [Firebolt +Specification Schema](../../firebolt-specification.json), or to individual Apps by the Distributor-signed +App Manifest. + +An App may need to know if a Capability is permitted to it once it has +been launched. For example, it may need to check if a certain capability +is permitted, to enable the correct features for the current +distributor. For example, an App that has just been installed might not +show a Returns true for capability/role combinations that do not r +Sign-up option when running on a distributor that has not allowed it to +`use` the `commerce:subscribe` capability, and only allow existing users +to sign in. + +Determining if a Capability is permitted requires knowing which **Role** +is being leveraged. + +Permitted capabilities **MUST** be denoted as `public` in the Specification Schema](#61-firebolt-specification-schema). + +Additionally, **one** of the following **MUST** be true as well: + +> The Role **MUST** have `negotiable` set to `false` +> +> **OR** +> +> The Role **MUST** be approved for the capability by the distributor in +> the App Manifest + +A permitted Capability **MAY** be tied to a User Grant, regardless of +whether the user has granted it yet. + +The platform **MUST** return accurate responses based on these +requirements to all [APIs](#apis) outlined below. + +### 4.4. User Granted Capabilities + +Some Capabilities may require a User Grant. It is the App's choice +whether it requests these grants at launch or allows the platform to +interrupt the App's experience when User Grant-gated Capabilities are +invoked. + +Determining if a Capability has been granted by the user requires +knowing which Role is being used. + +For a Capability to be considered granted, it **MUST** be supported. + +Additionally, a Capability + Role is granted to an App if **one** of the +following is true: + +> A granted capability **MUST** have the Role securely granted to the +> App by the user and that grant must not be expired. +> +> **OR** +> +> A granted capability **MUST** have the Role permanently granted to the +> App by the App Manifest, e.g., if the user implicitly granted these by +> virtue of an end user license agreement. + +Firebolt platforms **MUST** support acquiring a user grant *at the +moment the Capability is invoked*, without requiring any reinvocation of +the Capability. + +See [User Grants](./user-grants.md), for more info. + +The platform **MUST** return accurate responses based on these +requirements to all [APIs](#apis) outlined below. + +### 4.5. Invoking Capabilities + +When invoking a Capability, there are several factors that determine +whether the calling App will be allowed. The primary factor is which +Role is being leveraged by the invocation. + +A given Capability & Role can be public or not, and it can be negotiable +or not. + +If a Role for a Capability is public and non-negotiable, then all Apps +are allowed to invoke it, without any explicit permission from the +platform distributor. This is subject to support, availability, and any +required User Grants. + +If a Role for a Capability is public and negotiable, then Apps **MUST** +be permitted by each distributor to use it on that distributor\'s +devices. + +If a Role for a Capability is private, then it **MUST NOT** be permitted +or granted to any Apps. + +A Role for a Capability **MUST NOT** be flagged as negotiable if it is +private. + +If a Capability is gated by a User Grant requirement, then any +invocation **MUST** check the grant status for the Role in question, and +potentially re-acquire a User Grant for the App to invoke it. + +The following flow diagrams show the order of operations for determining +if a Firebolt API, that depends on one or more capabilities, may be +invoked. The checks in this diagram **MUST** be executed in this order +to ensure that error codes are consistent and User Grant prompts are not +displayed to end users in situations where the API call will still not +be allowed for other reasons. + +![](../../../images/specifications/general/capabilities/image4.png) + +Note that determining user grants may involve presenting a UX to the end +user. During this time, a necessary capability may become unavailable. +Due to this, necessary Capabilities MUST be reevaluated for availability +after determining user grant status. For a more detailed flow diagram +for evaluating User Grants, see [User Grants](./user-grants.md). + +## 5. Configuring Device Capabilities + +The Firebolt Specification describes all the possible, non-Extension +capabilities that a particular version of Firebolt allows. Individual +Firebolt devices, however, may not support every Capability and may want +to override certain aspects of those Capabilities. + +Every Firebolt-compliant device **MUST** include an official, versioned +Firebolt JSON configuration that conforms to the Specification Schema](#61-firebolt-specification-schema), so that the implementation +knows how to configure each capability, and which aspects are +overridable. + +Additionally, every Firebolt-compliant device **MUST** have a versioned + +Firebolt Device JSON configuration that conforms to the Firebolt Device +Schema, so that the implementation knows how each capability has been +overridden by the Distributor for this device. + +See [Firebolt Device Schema](#firebolt-device-schema) for more info. + +### 5.1. Device Supported Capabilities + +Firebolt-compliant devices **MUST** list all the Capabilities they +support in the Device Capabilities configuration file\'s `supported` +array. + +The `supported` array **MUST** contain a `CapabilityConfig` for every +Firebolt Capability supported by this device. + +The `supported` array **MUST** contain an entry for every Capability +from the Specification Schema](#61-firebolt-specification-schema) that has a level of `must`. + +The `supported` array **MAY** contain entries for any `should` or +`could` Capabilities. + +The supported array **MUST NOT** contain any entries for Capabilities +not found in the Firebolt Specification Schema. + +### 5.2. Device Grant Policy Overrides + +Firebolt-compliant devices **MAY** override any `GrantPolicies` that +have `overridable` set to `true`. + +This can be used to permanently disable a User Grant + +Firebolt-compliant devices **MUST NOT** override any `GrantPolicies` +that have `overridable` set to `false`. + +`GrantOverrides` are included in the `grantOverrides` array of the +`CapabilityConfig` for the Capability in question. + +If both the Firebolt Specification Schema and the Device Schema have +different, valid overrides for the same capability + appId, then the +device config **MUST** be used by that device. + +## 6. Capability Schemas + +This section describes how Capabilities are represented in the static +Firebolt configuration for a device. + +### 6.1. Firebolt Specification Manifest + +Each version of Firebolt **MUST** have a single Firebolt Specification +Manifest that is the source-of-truth for and contains all possible +capabilities provided, used, or managed by Apps or Firebolt platforms. + ++The Firebolt Specification Manifest **MUST** list all capabilities defined by that version of Firebolt. + +The Firebolt Specification Manifest **MUST** specify whether each capability `must`, `should`, or `could` be implemented by Firebolt devices. + +The Firebolt Specification Manifest **MUST** specify whether each role, i.e. `use`, `manage`, and `provide`, is a `public` permission that apps may call. + +See [Invoking Capabilities](#46-invoking-capabilities), for more info on public and negotiable capabilities. + +The Firebolt Specification Manifest **MUST** include the entire Firebolt OpenRPC specification for all APIs in the 'apis` block. + +The Firebolt Specification Manifest **MUST** specify which major versions of the Firebolt RPC APIs are required for backwards compatibility. + +The [Firebolt Version Manifest JSON-Schema](https://github.com/rdkcentral/firebolt-configuration/blob/main/src/schemas/version-manifest/version-manifest.json) defines the JSON semantics for this file. + +The latest version of the firebolt-specification.json **MUST** be available at this URL: + +``` +http://rdkcentral.github.io/firebolt/requirements/latest/specifications/firebolt-specification.json +``` + +The version of the firebolt-specification.json associated with this document **SHOULD** be available at [../../firebolt-specification.json](../../firebolt-specification.json). + +### 6.2. Firebolt Device Manifest + +Each Firebolt device will have a static configuration for overriding +which capabilities are supported, as well as any negotiable Capability +overrides. + +The Device Manifest **MUST** specify which capabilities the device supports. + +The Device Manfiest **MUST** include every capability from the Firebolt Specification Manifest that has a `level` of `must` in its supported list. + +The Device Manifest **MUST** specify which capabilities have distributor define Grant Policy Overrides. + +The Device Manfiest **MUST NOT** have any Grant Policies that override Grant Policies from the Firebolt Specification Manifest that have `overridable` set to `false`. + +The [Firebolt Device Manifest JSON-Schema](https://github.com/rdkcentral/firebolt-configuration/blob/main/src/schemas/device-manifest/device-manifest.json) defines the JSON semantics for this file. + +## 7. Capability to Feature Mapping + +Capabilities denote functionality, and functionality can manifest in several ways. + +### 7.1. App installation + +Some Apps may not be useful, and may even prove counter to their goals, +if installed on a device without certain capabilities. For example, a +Dolby Vision demonstration App that requires it only be installed on +devices that support Dolby Vision, to avoid playing back lower quality +content that users might mistake for Dolby Vision content. + +If an App includes a capability in one of its App Manifest's `required` +Capability lists: + +- `app.capabilities.used.required` + +- `app.capabilities.managed.required` + +- `app.capabilities.provided.required` + +Then that App **MUST NOT** be installable on Firebolt devices that do +not support that capability. + +Apps that have an unsupported capability inside one of its `optional` +lists **SHOULD NOT** be prevented from installation, unless there is +some other reason outside the scope of this document. + +### 7.2. Firebolt APIs + +Some Firebolt APIs may require the use of one or more Capabilities. +These methods **MUST** have all required Capabilities listed in the method's OpenRPC schema. + +Capabilities are listed in one of three OpenRPC extensions attached to +the `'capabilities' `tag on the method: `x-uses`, `x-manages`, +`x-provides`. + +If a method lists more than one Capability for a role, then it may +specify that those capabilities are either all required, any combination +of them is required, or one and only one is required. The platform **MUST parse any `x-uses-operator` values which will have values of either `allOf`, `anyOf`, or `oneOf`. This value defaults to `allOf` if not specified. An example of +this is an API to find and pair remotes, regardless of which connection +protocol is needed. This API requires `anyOf` `bluetooth:scan`, +`rf4ce:scan`, `wifi:scan`. If one or more of these capabilities is +available (and permitted) then the API will execute using the available +and permitted protocols. The same pattern applies to `x-manages-operator`. The `x-provides` extension only supports a single capability, so this pattern does not extend to providers. + +If a method *requires* a capability, and that capability requires a user +grant that the App does not have, then the platform **MUST** block and +initiate a User Grant flow. Once granted, the platform **MUST** return +the expected value without the App having to reinvoke the API. If not +granted after the User Grant flow, the platform **MUST** return an +error. + +Capabilities that enhance an API, but are not fundamentally required, +for example a `play` API optionally uses the `'hdr:dolbyvision'` +capability, **MUST NOT** be listed in the OpenRPC schema. These are considered *optional* capabilities of the method implementation. + +If a method leverages an *optional* Capability that is unavailable or +unpermitted, it **MUST** leave out or defer the optional functionality. + +If a method leverages an *optional* Capability that requires a user +grant the App does not have, it **MUST** leave out the optional +functionality, and it **MUST NOT** request a user grant. + +### 7.3. Extension APIs + +Extension SDKs implement their methods in the cloud but rely on +Firebolt's Permissions and Capabilities model. + +Requirements for Extension SDKS are outside of the scope of this document. + +### 7.4. Hidden Permissions + +A hidden permission arises when an API requires permission to one +Capability which in turn requires another Capability gated by a +different permission. This is not supported by Firebolt, and all +permission dependencies should be validated to avoid this. + +Firebolt Capabilities **MUST NOT** have hidden permissions. + +For example: + +- API `Module.methodOne()` requires permission to `use` the + `'contrived:capability1'` Capability + +- API `Module.methodTwo()` requires permission to `use` the + `'contrived:capability2'` Capability + +- `methodOne`'s implementation calls `methodTwo` + +There is now a hidden permission: `methodOne` requires both +`capability1` and `capabilty2`. + +Note that it's fine for a method to leverage an ungated, but also +unavailable Capability, such as a DIAL API failing because the network +capability is unavailable. + +## 8. Firebolt Capability Catalog + +Firebolt Capabilities are enumeraged in the [Firebolt Specification Manifest](../../firebolt-specification.json). + +## 9. APIs +All of the APIs below have full OpenRPC schemas in the [Firebolt OpenRPC JSON document](../../../specifications/firebolt-open-rpc.json). + +### 9.1. Core SDK APIs + +Several APIs are exposed by the Firebolt Core SDK as part of the +`Capabilities` module. This module is intended for App +developers to have one place to check for all aspects of "can I do +this." Including supported, available, permitted, and granted +Capabilities. It also provides bulk operations for figuring out which +needed Capabilities are unavailable, in order to wait for them, and +which ones are ungranted, in order to request them. + +### 9.2. Manage SDK APIs + +Several APIs are exposed by the Firebolt Manage SDK as part of the +`UserGrants` module. This module is intended for +Management UIs that show a list of grants per App or Capability, and +allow users to revoke them. diff --git a/requirements/specifications/general/capabilities/user-grants.md b/requirements/specifications/general/capabilities/user-grants.md new file mode 100644 index 000000000..f87342a5c --- /dev/null +++ b/requirements/specifications/general/capabilities/user-grants.md @@ -0,0 +1,593 @@ +# User Grants + +Document status: Candidate Specification + +See [Firebolt Requirements Governance](../../../governance.md) for more info. + +| Contributor | Organization | +| -------------- | -------------- | +| Andrew Bennet | Sky | +| Tim Dibben | Sky | +| Jeremy LaCivita | Comcast | +| Kevin Pearson | Comcast | +| Peter Yu | Comcast | + +## 1. Overview + +This document describes the requirements for Firebolt User Grants. User +Grants enable end-user control over which Apps have access to +potentially sensitive Firebolt Capabilities. For example, a user might +want to explicitly approve of any App attempting to purchase new content +by entering an account-holder PIN on the RCU. + +User Grants are also common when two apps need to share data, for +example, Firebolt has a `Discovery.watched` method that allows apps to +inform Firebolt that a user has finished watching some content. This +method allows an app to **provide** the watch history capability and the +aggregated experience to **use** it. See Firebolt +[Capabilities](./capabilities.md) requirements for more info on the `use` and `provide` roles of a +capability. + +This sort of data exchange involves several parties: minimally the user, +the publisher of the OTT app, and the distributor of the Firebolt +device. Additionally, this exchange could also involve privacy laws of +the user\'s home geo-political state, local I.T. best practices around +PII, or even a particular product\'s public position on their privacy +offering to the market. Because of the importance and complexity of +personal data, Firebolt allows each distributor to configure which +Firebolt capabilities require User Grants and what policies should be +applied to acquiring those grants. + +![Diagram Description automatically +generated](../../../images/specifications/general/capabilities/user-grants/image1.png) + +Distributors can configure what kind of user grant is required for a +particular capability, e.g. a simple acknowledgement of an \"OK\" +button, an account-owner PIN entry, an account-owner three-factor +authentication challenge, etc. + +Distributors can also configure whether certain user grants are opt-in +or opt-out, and whether explicit prompting of the user is required. + +## 2. Table of Contents +- [1. Overview](#1-overview) +- [2. Table of Contents](#2-table-of-contents) +- [3. Introduction to User Grants](#3-introduction-to-user-grants) + - [3.1. Active Grant](#31-active-grant) + - [3.2. Denied Grant](#32-denied-grant) + - [3.3. Unset Grant](#33-unset-grant) + - [3.4. Granting Capability](#34-granting-capability) + - [3.5. Grant Policy](#35-grant-policy) + - [3.6. Privacy Setting](#36-privacy-setting) +- [4. User Grant Requirements](#4-user-grant-requirements) + - [4.1. Granting Capability](#41-granting-capability) + - [4.1.1. Acknowledgement](#411-acknowledgement) + - [4.1.2. Pin Challenge](#412-pin-challenge) + - [4.2. Grant Policy](#42-grant-policy) + - [4.2.1. Grant Requirements](#421-grant-requirements) + - [4.2.2. Grant Lifespan](#422-grant-lifespan) + - [4.2.3. Grant Privacy Setting](#423-grant-privacy-setting) + - [4.3. Distributor Overrides](#43-distributor-overrides) + - [4.4. Application Overrides](#44-application-overrides) +- [5. Grant Execution](#5-grant-execution) + - [5.1. Capability Check](#51-capability-check) + - [5.2. Grant Policy Resolution](#52-grant-policy-resolution) + - [5.3. Active Grant Check](#53-active-grant-check) + - [5.4. Grant Prompt and Resolution](#54-grant-prompt-and-resolution) +- [6. Schemas](#6-schemas) + - [6.1. Grant Policy](#61-grant-policy) + - [6.2. GrantRequirements](#62-grantrequirements) + - [6.3. GrantStep](#63-grantstep) + - [6.4. GrantKey](#64-grantkey) + - [6.4.1. GrantScope](#641-grantscope) + - [6.4.2. GrantLifespan](#642-grantlifespan) + - [6.4.3. PrivacySetting](#643-privacysetting) +- [7. APIs](#7-apis) + - [7.1. Manage SDK APIs](#71-manage-sdk-apis) + - [7.1.1. GrantInfo Object](#711-grantinfo-object) + - [7.1.2. App Method](#712-app-method) + - [7.1.3. Device Method](#713-device-method) + - [7.1.4. Capability Method](#714-capability-method) + - [7.1.5. Grant Method](#715-grant-method) + - [7.1.6. Deny Method](#716-deny-method) + - [7.1.7. Clear Method](#717-clear-method) + + +## 3. Introduction to User Grants + +This section defines the key concepts behind the Firebolt User Grant +system. User Grants build on top of [Firebolt +Capabilities](./capabilities.md). +Since Capabilities are fundamental to user grants, it is recommended to +read the Capabilities requirements first before reading this document. + +Each portion of this section provides a background and conversational +definitions of the terms used in the rest of this document, and through +other Firebolt Requirements Specifications. + +As described in the Overview, a User Grant allows an end user to +explicitly allow (or not allow) an app to use certain Firebolt +Capabilities. Firebolt Distributors need flexibility in configuring +which Capabilities require what types of user interaction, PIN vs +password, etc. + +The following concepts inform how the Firebolt User Grant APIs and +architecture are designed. + +### 3.1. Active Grant + +An **Active Grant** is a User Grant that has already been granted by the +user for a Capability to a specific app (if the Grant Policy is per-app) +or all apps. Active Grants do not need to be granted again (until they +expire) and may be revoked or expire. + +### 3.2. Denied Grant + +A **Denied Grant** is a User Grant that has already been explicitly +*denied* by the user for a Capability to a specific app (if the Grant +Policy is per-app) or all apps. Denied Grants do not need to be denied +again (until they expire) and may be revoked or expire. This serves to +allow the Grant Policy lifetime to be applied to explicit denials as +well. + +### 3.3. Unset Grant + +An **Unset Grant** is a User Grant that has no persisted state. This +could be because the user has never granted or denied it, or because it +was previously granted/denied, but has since expired. Additionally, the +user or platform may clear out an active or denied grant, which results +in this state. + +### 3.4. Granting Capability + +User grants typically require some sort of user experience. Firebolt +delegates display of these user experiences to (typically) first-party +apps, which must provide them as **Granting Capabilities**. Therefore, +it\'s quite possible that using a capability such as watch history +initiates a User Grant request which itself requires another capability, +e.g. the \"acknowledge\" capability, to fulfill the original API. + +In the following diagram, an OTT App is calling an API that would share +watch history data with the 1st party aggregated experience on the +device. This capability is configured with a [Grant +Policy](#grant-policy) (see below) that specifies use of an +Acknowledgement Challenge Capability. the \"Watch History\" capability +is what is being accessed, and the \"Acknowledgement Challenge\" +capability is how this device is configured to grant access to that +capability. Said another way, the Acknowledgement Challenge is the +*Granting Capability*. + +![Diagram Description automatically +generated](../../../images/specifications/general/capabilities/user-grants/image2.png) + +Note that the granting capability UI is provided by another app. The +Provider APIs for each granting capability type are out of scope for +this document. + +A granting capability is generally agnostic to the original capability +that it is helping to grant and can be reused by several capabilities +via a Grant Policy. + +### 3.5. Grant Policy + +A **Grant Policy** ties a set of user grant-related configuration values +to a capability. This allows a Firebolt distributor, or the Firebolt +specification itself, to define the business policy of a particular user +grant. + +Grant Policies specify details like how long the grant lasts and what is +the scope of the grant, e.g. a single app, or all apps. + +### 3.6. Privacy Setting + +A **Privacy Setting** is a link between a Grant Policy and any Firebolt API that is tagged with the Property pattern and returns a +`boolean`. This allows User Grant state to be linked to Privacy Settings +on the device or account. + +![Diagram Description automatically +generated](../../../images/specifications/general/capabilities/user-grants/image3.png) + +How Firebolt devices persist Privacy Settings is outside the scope of +this document. + +## 4. User Grant Requirements + +### 4.1. Granting Capability + +A granting capability **MUST** have the category portion of its +capability key set to `usergrant`. For example: + +`xrn:firebolt:capability:usergrant:acknowledge` + +In order for a granting capability to be available, it **MUST** be +provided by some app that is currently loaded. + +Each granting capability **MUST** have a schema, in the Firebolt +Specification Schema, to validate that any configuration associated with +it is valid. + +#### 4.1.1. Acknowledgement + +The Acknowledgement capability enables a user to acknowledge a user +grant by simply clicking a button. + +This capability has the following key: + +`xrn:firebolt:capability:usergrant:acknowledgechallenge` + +The configuration for this capability is always `null`, i.e., there is +no configuration. + +For more info on how an app can provide the acknowledge capability, see +Acknowledge Provider. **TODO**: add link. + +#### 4.1.2. Pin Challenge + +The Pin Challenge capability enables a user to confirm that they are the +account owner, or a delegate of, by responding to a numeric PIN +challenge. + +This capability has the following key: + +`xrn:firebolt:capability:usergrant:pinchallenge` + +For more info on how an app can provide the acknowledge capability, see +Pin Challenge Provider. **TODO**: add link. + +### 4.2. Grant Policy + +If a Capability is configured with a `GrantPolicy` then the User Grants +specified by the Grant Policy **MUST** be evaluated before a GrantPolicy +llowing access to that Capability. + +A Grant Policy **MUST** have a `scope` for the grant, which determines +whether the user will be granting the capability to the current app or +to all apps. + +A Grant Policy **MUST** have an `overridable` flag that determines if it +may be overridden downstream. For Grant Policies in the Firebolt Version +Manifest, the Device Manifest may override if `overridable` is `true`. +For Grant Policies in the Device Manifest, individual [App +Manifests](https://github.com/rdkcentral/firebolt-configuration/src/schemas/app-manifest/app/app.json) may override the grant if `overridable` is `true`. + +#### 4.2.1. Grant Requirements + +A Grant Policy **MUST** have at least one `GrantRequirements` object, +which itself has a list of zero or more [Granting +Capabilities](#granting-capability) and their configurations. If all the +granting capabilities are supported and available, then they all +**MUST** be invoked. This allows for multiple security checks, e.g. +allow watch history data to be collected if the user has opted into +usage data collection **and** they confirm via an acknowledgement +challenge UI. If one or more of the granting capabilities is not +supported or available, then the entire Grant Requirements block is not +valid for acquiring a user grant. + +A Grant Policy **MAY** have more than one `GrantRequirements` object, +which are ranked in order of preference. This allows for low-friction +but high-security methods to be preferred, e.g. allow in-app purchases +with a passive facial recognition scan, with higher-friction fallbacks +to ensure security on devices with less capabilities, e.g. allow in-app +purchases if the account owner performs a multi-factor authentication +flow. This list of `GrantRequirements` objects is stored in the Grant +Policy\'s `options` field. + +If a Grant Policy has more than one `GrantRequirements` option, then the +highest priority list with all its required capabilities supported and +available **MUST** be used. Note that the app which initiated the +original capability check **does not** need to have permission (or a +grant) to any of the granting capabilities that might be invoked. The +app is not using them, the Firebolt device is using them. + +#### 4.2.2. Grant Lifespan + +A Grant Policy **MUST** have a `lifespan`, which determines how long a +grant is valid for. + +> If the lifespan is set to `once`, then any grant resulting from this +> policy **MUST** be active for a single API transaction. +> +> If the lifespan is set to `forever`, then any grant resulting from +> this policy **MUST** be active until explicitly revoked. +> +> If the lifespan is set to `appActive`, then any grant resulting from +> this policy **MUST** be active until the app goes into one of the +> `inactive`, `suspended`, or `unloading` states. +> +> If the lifespan is set to `powerActive`, then any grant resulting from +> this policy **MUST** be active until the device power goes into any +> state other than `active`. +> +> If the lifespan is set to `seconds`, then any grant resulting from +> this policy **MUST** be active until `lifespanTtl` seconds have passed +> since the grant was issued. + +If a Grant Policy has the `lifespan` set to `seconds`, then the +`lifespanTtl` **MUST** be set to a non-zero, positive number of seconds. + +#### 4.2.3. Grant Privacy Setting + +A Grant Policy **MAY** have a `PrivacySetting` object, which +associates any Firebolt `boolean` property API with this Grant Policy. +The Privacy Setting points to the RPC name of the property\'s getter +method. + +The associated property API **MUST** have an `x-allow-value` extension +on the \"property\" tag set to true or false. + +If a Grant Policy has a Privacy Setting, then the `PrivacySetting` +**MUST** be evaluated **before** any `GrantRequirements`. + +> The Privacy Setting **MUST** have an `autoApplyPolicy` which **MUST** +> be one of the following values: +> +> `always` - Silently grant if the getter for the `property` returns the +> property\'s `x-allow-value`. Silently deny if the getter for the +> `property` returns `!x-allow-value`. +> +> `allowed` - Silently grant if the getter for the `property` returns +> the property\'s `x-allow-value`. +> +> `disallowed` - Silently deny if the getter for the `property` returns +> the property\'s `!x-allow-value`. +> +> `never` - Do not silently grant or deny based on this Grant Policy. +> +> If the Privacy Setting has `revokeGrantsOnDisallow` set to `true`, +> then **all active** user grants that resulted from this Grant Policy +> **MUST** be revoked if and when the property value specified by +> `property` ever changes to `!x-allow-value`. This enables +> distributors to decide whether changes to a Privacy Setting affects +> only new User Grants or existing, active User Grants as well. +> +> If the Privacy Setting has `updateProperty` set to `true`, then a +> successful user grant from this Grant Policy **MUST** also result in +> updating the property value specified by `property` to +> `x-allow-value`. +> +> If the Privacy Setting has `updateProperty` set to `true`, then a +> rejected **or expiring** user grant from this Grant Policy **MUST** +> also result in updating the property value specified by `property` to +> `!x-allow-value`. This is considered a \"disallow\" and +> **MUST** initiate evaluation of the `revokeGrantsOnDisallow` value. +> +> If the Grant Policy `scope` is set to `app` then the property +> specified by `property` **MUST** have a single context +> parameter of type `string` and the requesting appId **MUST** be passed +> to all get/set/subscribe calls fulfilling this section of requirements. + +### 4.3. Distributor Overrides + +Capability Grant Policies in the Firebolt Version Manifest are designed +to be overridden by Distributors to allow for different privacy use +cases across businesses and geographic regions. These overrides go in +the Device Manifest under the `grantPolicies` section of the +`capabilities` section. + +Distributor overrides **MUST** be for Grant Policies that are +`overridable`. + +See the [Firebolt Configuration Schemas +repository](https://github.com/rdkcentral/firebolt-configuration/) for +more information on the Firebolt Version Manifest and Device Manifest. + +### 4.4. Application Overrides + +Firebolt supports creating active grants via the App manifest, which +allows distributors and App publishers to decide that a particular app +will already be granted some capability. This is typically used for +first-party apps that come pre-installed but could be used for other +purposes. + +If an app has a role/capability listed in its App Manifest, then it is +considered to have an active grant for that capability if the manifest +has a valid signature. + +App Manifests overrides **MUST** be for Grant Policies that are +`overridable` and scoped to `app`. + +See the [Firebolt Configuration Schemas +repository](https://github.com/rdkcentral/firebolt-configuration/) for +more information on Firebolt App Manifests. + +## 5. Grant Execution + +When a Firebolt API is called the platform **MUST** first collate all +the device User Grant configurations that apply to every capability that +the API uses. This list **MUST** then be filtered to remove any User +Grants that have already been granted for the current capability for +this app and role. + +### 5.1. Capability Check + +First, all capabilities necessary to fulfill the API are collected. + +![](../../../images/specifications/general/capabilities/user-grants/image4.png) + +### 5.2. Grant Policy Resolution + +Next, each capability is checked to see if there is a Firebolt, +Distributor, or App Grant Policy that applies, and that none of them are +invalid, e.g. an override that is now allowed. + +![Diagram Description automatically +generated](../../../images/specifications/general/capabilities/user-grants/image5.png) + +### 5.3. Active Grant Check + +Then, each Grant Policy is evaluated to see if there is already an +active grant, or if it is linked to a Privacy Setting configured to skip +user-prompts. + +![Diagram Description automatically +generated](../../../images/specifications/general/capabilities/user-grants/image6.png) + +### 5.4. Grant Prompt and Resolution + +Finally, each needed Granting Capability is invoked to present a UX to +the end user, and any linked Privacy Settings are updated as +appropriate. If all succeeds, then the API is invoked. + +![Diagram Description automatically +generated](../../../images/specifications/general/capabilities/user-grants/image7.png) + +## 6. Schemas + +The following schemas give an overview of the schemas outlined by this +document. See the [Firebolt Configuration Schemas +repository](https://github.com/rdkcentral/firebolt-configuration/) for +the latest versions of these schemas. + +### 6.1. Grant Policy + +Grant Policies have the following properties. + +| Property | Type | Description | +|-------------|-----------------------|-------------------------------------| +| `options` | `GrantRequirements[]` | An array of GrantRequirements, any one of which could be used to Grant a requested capability/role to an App. The first one that is completely supported by the current device should be used. | +| `scope` | `string` | Whether the resulting user grant applies to all Apps or just the requesting App. | +| `lifespan` | `string` | How long the resulting user grant (or deny) will last. | +| `lifespanTtl` | `integer` | If lifespan is `'seconds'` this represents the number of seconds. | +| `privacySetting` | `PrivacySetting` | Links this Grant Policy to a privacy setting. | +| `overridable` | `boolean` | Whether a distributor may override this GrantPolicy with their own. | + +### 6.2. GrantRequirements + +A list of `GrantSteps` objects describing which capabilities and any +optional configuration needed to fulfill this grant policy. These +capabilities must be from the capability category `usergrant` which +denotes special capabilities that perform user grants, e.g. an +acknowledge challenge capability. + +| Property | Type | Description | +|-------------|-----------------|-------------------------------------------| +| `steps` | `GrantStep[]` | An array of GrantSteps, all of which need to be verified to fulfill this GrantPolicy. | + +### 6.3. GrantStep + +| Property | Type | Description | +|-------------|----------------|-------------------------------------------| +| `capability` | `string` | The Granting Capability to use for this step in the grant flow. | +| `configuration ` | `object` | The configuration for the Granting Capability, which is specific to each type. | + +### 6.4. GrantKey + +A CapabilityKey with the \ set to `usergrant`: + +`/\^xrn:firebolt:capability:usergrant:\[a-zA-Z0-9\\-\]{1,32}\$/` + +#### 6.4.1. GrantScope + +A `string` enumeration. + +| Value | Description | +|----------------|--------------------------------------------------------| +| `device` | The resulting user grant will apply to all Apps on the device. | +| `app` | The resulting user grant will apply to the requesting App only. | + +#### 6.4.2. GrantLifespan + +A `string` enumeration. + +The resulting user grant will only be valid: + +| Value | Description | +|----------------|--------------------------------------------------------| +| `once` | for a single usage, e.g. one API call. | +| `forever` | until the end of time. | +| `appActive` | until the app enters a non-Active Lifecycle state: inactive, suspended | +| `powerActive` | until the device leaves the Active power state. | +| `seconds` | until \ seconds have passed. | + +#### 6.4.3. PrivacySetting + +| Value | | Description | +|--------------------|----------|------------------------------------------| +| `property` | string | The fully qualified RPC method name of the associated property setter, e.g. `Privacy.limitAdTracking` | +| `autoApplyPolicy` | string | Set the auto grant policy to `always`, `allowed`, `disallowed`, `never` | +| `revokeGrantsOnDisallow` | boolean | Any grants made from this policy will be revoked if/when the property is ever set to the deny value | +| `updateProperty` | boolean | Whether to update the property value to match the grant. | + +## 7. APIs + +APIs for User Grants are covered in the [Capabilities Requirements +document](./capabilities.md). + +### 7.1. Manage SDK APIs + +These APIs are intended for trusted apps to expose a UI for end-users to +manage the grants given to the device and various apps. Permission for +these APIs should only be given to a trusted settings app performing +this function. + +#### 7.1.1. GrantInfo Object + +Object describing a persisted active or denied Grant. + +```typescript +type GrantInfo = { + appId: string, + state: "active" | "denied", + capability: string, + role: "use" | "manage" | "provide" + lifespan: "once" | "forever" | "appActive" | " powerActive" | "seconds" + expires: integer +} +``` + +#### 7.1.2. App Method + +Returns all active and denied user grants for the given App, **NOT** +including those granted to all apps via the device. + +```typescript +UserGrants.app(appId: string): GrantInfo[] +``` + +#### 7.1.3. Device Method + +Returns all active and denied user grants for the Device. + +```typescript +UserGrants.device(): GrantInfo[] +``` + +#### 7.1.4. Capability Method + +Returns all active and denied user grants for the given Capability. + +```typescript +UserGrants.capability(capability: string): GrantInfo [] +``` + +#### 7.1.5. Grant Method + +Grants a given Capability, to a specific app if appropriate. Calling +this results in a persisted Active Grant that lasts for the duration of +the Grant Policy lifespan. + + +```typescript +UserGrants.grant(role: string, capability: string, appId?:string): void +``` + +#### 7.1.6. Deny Method + +Denies a given Capability, to a specific app if appropriate. Calling +this results in a persisted Denied Grant that lasts for the duration of +the Grant Policy lifespan. + +```typescript +UserGrants.deny(role: string, capability: string, appId?: string):void +``` + +#### 7.1.7. Clear Method + +Clears any persisted active or denied grant for a Capability, for a +specific app if appropriate. This results in an Unset Grant. This method +may take a wildcard value of `'*'` for role, capability, or appId, in +order to clear grant state in bulk. + +```typescript +UserGrants.clear(role: string, capability: string, appId?:string): void +``` From fe8a895ecd0efb9f50e100edd400ea3a5b88a396 Mon Sep 17 00:00:00 2001 From: Jeremy LaCivita Date: Thu, 13 Jun 2024 09:30:00 -0400 Subject: [PATCH 17/21] chore: Adding spec for existing Language APIs (#167) --- .../specifications/localization/language.md | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 requirements/specifications/localization/language.md diff --git a/requirements/specifications/localization/language.md b/requirements/specifications/localization/language.md new file mode 100644 index 000000000..0b3df80ef --- /dev/null +++ b/requirements/specifications/localization/language.md @@ -0,0 +1,99 @@ +# Language Settings + +Document Status: Candidate Specification + +See [Firebolt Requirements Governance](../../governance.md) for more info. + +| Contributor | Organization | +| -------------- | -------------- | +| Jeremy LaCivita | Comcast | +| Kevin Pearson | Comcast | +| Tim Dibben | Sky | + +## 1. Overview + +This document describes the requirements that Firebolt platforms must +fulfill when surfacing user language preferences. + +Exposing these settings, e.g. the main device language, or a list of +preferred audio languages, allows Apps to respect the device's current +setting so that the user has a seamless experience when switching from +app to app. + +This document covers how Firebolt platforms manage language settings and +expose to Apps. It does not cover the use cases Apps might apply these +APIs to. + +The key words "**MUST**", "**MUST NOT**", "**REQUIRED**", "**SHALL**", "**SHALL NOT**", "**SHOULD**", "**SHOULD NOT**", "**RECOMMENDED**", "**NOT RECOMMENDED**", "**MAY**", and "**OPTIONAL**" in this document are to be interpreted as described in [BCP 14](https://www.rfc-editor.org/rfc/rfc2119.txt) [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here. + +- [1. Overview](#1-overview) +- [2. Language](#2-language) +- [3. Locale](#3-locale) +- [4. Preferred Audio Languages](#4-preferred-audio-languages) +- [5. Core SDK APIs](#5-core-sdk-apis) +- [6. Manage SDK APIs](#6-manage-sdk-apis) + +## 2. Language + +Language describes the ISO 639 1/2 code for the preferred language on +this device. All Apps **SHOULD** generally render their user experience +in this language. This is a guideline, not an absolute requirement, +since different Apps may have different target audiences with differing +languages prevalent within those audiences. + +This setting is represented by a mutable property which is a string +conforming to the ISO 639 1/2 standard, e.g. `'en'`. + +## 3. Locale + +Locale describes the *full* BCP 47 code, including script, region, +variant, etc., for the preferred language/locale on this device. Apps +**MAY** alter their user experience to match this locale to account for +local differences in the same language. + +This setting is represented by a mutable property which is a string +conforming to the full BCP 47 standard, e.g. `'en-US'`. + +## 4. Preferred Audio Languages + +The preferred audio languages setting provides a ranked list of +languages that the user prefers to be selected on this device. All +values are from the ISO 639 1/2 standard. Apps **MAY** use this list to +influence selection of an initial audio track when playing content with +multiple languages. For example, a bilingual user living in an English +speaking country may have the `language` set to `'en`\' and the +`preferredAudioLanguages` set to `['fr', 'en']`. This +enables them to typically consume content from apps whose catalogs are +mostly filmed in English, but automatically get the French language +track when watching a French movie that has both French and English +tracks. + +This setting is represented by a mutable property which is an array of +strings conforming to the ISO 639 1/2 standard, e.g. `'en'`. + +## 5. Core SDK APIs + +The following APIs are exposed by the Firebolt Core SDK as part of the +`core:localization` domain/module. + +Each of these APIs a read-only property. + +- `Localization.language():Promise` + +- `Localization.locale():Promise` + +- `Localization.preferredAudioLanguages():Promise` + +- `Localization.onLanguageChanged():Promise` + +- `Localization.onLocaleChanged():Promise` + +- `Localization.onPreferredAudioLanguagesChanged():Promise` + +## 6. Manage SDK APIs + +The following APIs are exposed by the Firebolt Manage SDK as part of the +`manage` domain. + +The Manage SDK APIs inclueare identical to the Core SDK, except that all +property APIs are mutable. From e38ad5a508fccb5c724cf0f03cbd67ad4a28378c Mon Sep 17 00:00:00 2001 From: Jeremy LaCivita Date: Thu, 13 Jun 2024 09:38:32 -0400 Subject: [PATCH 18/21] fix: Remove x-alternatives that don't exist (#278) * fix: Remove x-alternatives that don't exist * fix: Cut ProviderPolicy from App Pass-through --- .../app-passthrough-apis.md | 32 ++++++------------- src/openrpc/discovery.json | 6 ++-- 2 files changed, 11 insertions(+), 27 deletions(-) diff --git a/requirements/specifications/openrpc-extensions/app-passthrough-apis.md b/requirements/specifications/openrpc-extensions/app-passthrough-apis.md index ec23922e6..826f7bed3 100644 --- a/requirements/specifications/openrpc-extensions/app-passthrough-apis.md +++ b/requirements/specifications/openrpc-extensions/app-passthrough-apis.md @@ -216,32 +216,18 @@ If the value was composed into the platform method result under a matching prope 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. ## 5. Provider Candidates -The Firebolt Device Manifist **MUST** have a list of `ProviderPolicy` configurations that map capabilities to policies for determining candidate providers: - -```json -{ - "providerPolicies": [ - { - "inFocus": true, - "capabilities": [ - "xrn:firebolt:capability:foo:bar" - ] - } - ] -} -``` -The policy **MUST** have a list of capabilities that it is applied to. - -A capability **MUST NOT** be included in more than one policy. - -The policy **MAY** have an `inFocus` boolean. - -If the policy has `inFocus` set to `true` then any app without RCU input focus when the capability is invoked **MUST NOT** be considered a candidate. +When a platform method with an `x-provided-by` extension is called, then +all loaded apps that have permission to provide the capability **MUST** be +considered as candidates to fulfill the method. ## 6. Best Candidate -If there is only one candidate then it **MUST** be the best candidate. +Any provider candidates that have not registered to provide the method in +question **MUST NOT** be considered the best candidate and removed from +consideration. + +If there is only one candidate left then it **MUST** be the best candidate. -If there is more than one candidate, then the candidate app that most recently had RCU input focus **MUST** be the best candidate. +If there is more than one candidate left, then the candidate app that most recently had RCU input focus **MUST** be the best candidate. If none of the candidates have had focus yet, then the candidate app that was most recently launched **MUST** be the best candidate. diff --git a/src/openrpc/discovery.json b/src/openrpc/discovery.json index 49b7e20aa..c3706de03 100644 --- a/src/openrpc/discovery.json +++ b/src/openrpc/discovery.json @@ -54,8 +54,7 @@ "x-provides": "xrn:firebolt:capability:discovery:entity-info" }, { - "name": "deprecated", - "x-alternative": "Discovery.details" + "name": "deprecated" } ], "summary": "Provide information about a program entity and its available watchable assets, such as entitlement status and price, via either a push or pull call flow.", @@ -430,8 +429,7 @@ "x-provides": "xrn:firebolt:capability:discovery:purchased-content" }, { - "name": "deprecated", - "x-alternative": "Discovery.purchases" + "name": "deprecated" } ], "summary": "Provide a list of purchased content for the authenticated account, such as rentals and electronic sell through purchases.", From e00a1dd1cb1edfccd54ab72634a92506ea8be5cb Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Thu, 13 Jun 2024 13:42:25 +0000 Subject: [PATCH 19/21] chore(release): 1.2.0-next.5 [skip ci] # [1.2.0-next.5](https://github.com/rdkcentral/firebolt-apis/compare/v1.2.0-next.4...v1.2.0-next.5) (2024-06-13) ### Bug Fixes * Remove x-alternatives that don't exist ([#278](https://github.com/rdkcentral/firebolt-apis/issues/278)) ([e38ad5a](https://github.com/rdkcentral/firebolt-apis/commit/e38ad5a508fccb5c724cf0f03cbd67ad4a28378c)) --- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- src/sdks/core/package.json | 2 +- src/sdks/manage/package.json | 2 +- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97cdddff2..3c5423498 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +# [1.2.0-next.5](https://github.com/rdkcentral/firebolt-apis/compare/v1.2.0-next.4...v1.2.0-next.5) (2024-06-13) + + +### Bug Fixes + +* Remove x-alternatives that don't exist ([#278](https://github.com/rdkcentral/firebolt-apis/issues/278)) ([e38ad5a](https://github.com/rdkcentral/firebolt-apis/commit/e38ad5a508fccb5c724cf0f03cbd67ad4a28378c)) + # [1.2.0-next.4](https://github.com/rdkcentral/firebolt-apis/compare/v1.2.0-next.3...v1.2.0-next.4) (2024-06-06) diff --git a/package-lock.json b/package-lock.json index d749e0c1a..6a8170fcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@firebolt-js/sdks", - "version": "1.2.0-next.4", + "version": "1.2.0-next.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@firebolt-js/sdks", - "version": "1.2.0-next.4", + "version": "1.2.0-next.5", "license": "Apache-2.0", "workspaces": [ "src/sdks/core", diff --git a/package.json b/package.json index b63abe049..246731512 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/sdks", - "version": "1.2.0-next.4", + "version": "1.2.0-next.5", "description": "The Firebolt JS SDK", "type": "module", "bin": { diff --git a/src/sdks/core/package.json b/src/sdks/core/package.json index 13c9535b3..7df598348 100644 --- a/src/sdks/core/package.json +++ b/src/sdks/core/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/sdk", - "version": "1.2.0-next.4", + "version": "1.2.0-next.5", "description": "The Firebolt JS SDK", "main": "./dist/lib/firebolt.mjs", "types": "./dist/lib/firebolt.d.ts", diff --git a/src/sdks/manage/package.json b/src/sdks/manage/package.json index 9f3cbc6d3..86fb19a67 100644 --- a/src/sdks/manage/package.json +++ b/src/sdks/manage/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/manage-sdk", - "version": "1.2.0-next.4", + "version": "1.2.0-next.5", "description": "The Firebolt Manage JS SDK", "main": "./dist/lib/firebolt-manage.mjs", "types": "./dist/lib/firebolt-manage.d.ts", From c307f45ea0201f276fa61c0dc34207ee4b0a8a89 Mon Sep 17 00:00:00 2001 From: "Shah, Kevin" Date: Fri, 14 Jun 2024 11:11:59 -0400 Subject: [PATCH 20/21] chore: Updated OpenRPC version to the 3.0.0 --- package-lock.json | 16 ++++++++-------- package.json | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index d749e0c1a..80d33f7c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@firebolt-js/sdks", - "version": "1.2.0-next.4", + "version": "1.2.0-next.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@firebolt-js/sdks", - "version": "1.2.0-next.4", + "version": "1.2.0-next.3", "license": "Apache-2.0", "workspaces": [ "src/sdks/core", @@ -19,7 +19,7 @@ "devDependencies": { "@commitlint/cli": "^17.0.3", "@commitlint/config-conventional": "^17.0.3", - "@firebolt-js/openrpc": "3.0.0-next.4", + "@firebolt-js/openrpc": "3.0.0", "@firebolt-js/schemas": "2.0.0", "@saithodev/semantic-release-backmerge": "^3.2.0", "@semantic-release/changelog": "^6.0.1", @@ -1075,9 +1075,9 @@ "link": true }, "node_modules/@firebolt-js/openrpc": { - "version": "3.0.0-next.4", - "resolved": "https://registry.npmjs.org/@firebolt-js/openrpc/-/openrpc-3.0.0-next.4.tgz", - "integrity": "sha512-Q3GZpaA2Gmhx5UOh7cOMpDjZZtbutVfTUUis0FrOKceVMVllRhHz/XeqZrpmKo4EX0hn4OPWOmVek34WO68jQg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@firebolt-js/openrpc/-/openrpc-3.0.0.tgz", + "integrity": "sha512-oHRHsIGoVFYYwGJwlY5BMaI8pEi2cd96JHcPX3SaB7ehbyI//v3QwJTcNKmvuUzTnqJzP0pEbHjgCnG+AZIvbg==", "dev": true, "dependencies": { "ajv": "^8.12.0", @@ -16956,7 +16956,7 @@ }, "src/sdks/core": { "name": "@firebolt-js/sdk", - "version": "1.2.0-next.3", + "version": "1.2.0-next.4", "license": "Apache-2.0", "devDependencies": { "jest": "^28.1.0", @@ -16978,7 +16978,7 @@ }, "src/sdks/manage": { "name": "@firebolt-js/manage-sdk", - "version": "1.2.0-next.3", + "version": "1.2.0-next.4", "license": "Apache-2.0", "devDependencies": { "jest": "^28.1.0", diff --git a/package.json b/package.json index b63abe049..e6863c389 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@firebolt-js/sdks", - "version": "1.2.0-next.4", + "version": "1.2.0-next.3", "description": "The Firebolt JS SDK", "type": "module", "bin": { @@ -45,7 +45,7 @@ "devDependencies": { "@commitlint/cli": "^17.0.3", "@commitlint/config-conventional": "^17.0.3", - "@firebolt-js/openrpc": "3.0.0-next.4", + "@firebolt-js/openrpc": "3.0.0", "@firebolt-js/schemas": "2.0.0", "@saithodev/semantic-release-backmerge": "^3.2.0", "@semantic-release/changelog": "^6.0.1", From 0a3eb68be6c8de41c65cc03078488b06d16b686c Mon Sep 17 00:00:00 2001 From: "Shah, Kevin" Date: Fri, 14 Jun 2024 11:14:40 -0400 Subject: [PATCH 21/21] chore: Updated OpenRPC version to the 3.0.0 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0a275aa91..3921784f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16956,7 +16956,7 @@ }, "src/sdks/core": { "name": "@firebolt-js/sdk", - "version": "1.2.0-next.4", + "version": "1.2.0-next.5", "license": "Apache-2.0", "devDependencies": { "jest": "^28.1.0", @@ -16978,7 +16978,7 @@ }, "src/sdks/manage": { "name": "@firebolt-js/manage-sdk", - "version": "1.2.0-next.4", + "version": "1.2.0-next.5", "license": "Apache-2.0", "devDependencies": { "jest": "^28.1.0",