From b74771416a4902ed882704b3c2493249b7da7e57 Mon Sep 17 00:00:00 2001 From: Adam Stolcenburg Date: Fri, 16 Jan 2026 13:19:47 +0100 Subject: [PATCH] Basic integration with the RDK Reference DAC 2.0 App Store Ref: #RDKEAPPRT-538 --- accelerator-home-ui/src/api/AppManagerApi.js | 2 +- accelerator-home-ui/src/api/DACApi.js | 932 ++++++------------ .../src/api/DownloadManagerApi.js | 188 ++++ .../src/api/PackageManagerApi.js | 242 +++-- accelerator-home-ui/src/api/ThunderError.js | 29 + .../src/items/AppCatalogItem.js | 67 +- accelerator-home-ui/src/items/AppStoreItem.js | 18 +- .../src/items/ManageAppItem.js | 30 +- 8 files changed, 700 insertions(+), 808 deletions(-) create mode 100644 accelerator-home-ui/src/api/DownloadManagerApi.js create mode 100644 accelerator-home-ui/src/api/ThunderError.js diff --git a/accelerator-home-ui/src/api/AppManagerApi.js b/accelerator-home-ui/src/api/AppManagerApi.js index beef3c7..c7d8fd9 100644 --- a/accelerator-home-ui/src/api/AppManagerApi.js +++ b/accelerator-home-ui/src/api/AppManagerApi.js @@ -107,7 +107,7 @@ export default class AppManager { }) }) } - Isinstalled(appId){ + isInstalled(appId){ return new Promise((resolve, reject) => { this.thunder.call(this.callsign, 'isInstalled', {appId:appId}) .then(response => { diff --git a/accelerator-home-ui/src/api/DACApi.js b/accelerator-home-ui/src/api/DACApi.js index 0d9f40c..1f8f9ce 100644 --- a/accelerator-home-ui/src/api/DACApi.js +++ b/accelerator-home-ui/src/api/DACApi.js @@ -17,716 +17,398 @@ * limitations under the License. **/ -import ThunderJS from 'ThunderJS' -import HomeApi from './HomeApi'; -import { Storage } from "@lightningjs/sdk"; -import { CONFIG, GLOBALS } from '../Config/Config'; -import LISAApi from './LISAApi'; -import {Metrics} from '@firebolt-js/sdk' - -let platform = null -let thunder = null - -function thunderJS() { - if (thunder) - return thunder - - thunder = ThunderJS(CONFIG.thunderConfig) - return thunder +import DownloadManager from './DownloadManagerApi'; +import PackageManager from './PackageManagerApi'; +import AppManager from './AppManagerApi'; +import { ThunderError } from './ThunderError'; +import { Metrics } from '@firebolt-js/sdk' + +// the size that is assumed if it is not possible to retrieve package size +// from the server, according to server API this should never happen +const DEFAULT_SIZE = 1000000; + +// how many applications to request in one call +const APPS_REQUEST_LIMIT = 200; +// the maximum number of requests to retrieve apps +const APPS_REQUESTS_MAX = 5; + +const APP_DETAILS_KEY = "refui.details"; + +const APP_DEFAULT_ARCH = "arm"; + +function makeLogMessage(call, err) { + return err.toString() + " <=> " + call; } -async function registerListener(plugin, eventname, cb) { - return await LISAApi.get().thunder.on(plugin, eventname, (notification) => { - console.log("DACApi Received event " + plugin + ":" + eventname + " " + JSON.stringify(notification)) - if (cb != null) { - cb(notification, eventname, plugin) - } - }) +function logWarning(call, err) { + console.warn(makeLogMessage(call, err)); } -async function addEventHandler(eventHandlers, pluginname, eventname, cb) { - eventHandlers.push(await registerListener(pluginname, eventname, cb)) +function logError(call, err) { + let errMessage = makeLogMessage(call, err); + console.error(errMessage); + Metrics.error(Metrics.ErrorType.OTHER, "DACApiError", errMessage, false, null); } -function translateLisaProgressEvent(evtname) { - if (evtname === "DOWNLOADING") { - return "Downloading" - } else if (evtname === "UNTARING") { - return "Extracting" - } else if (evtname === "UPDATING_DATABASE") { - return "Installing" - } else if (evtname === "FINISHED") { - return "Finished" - } else { - return evtname +class OperationLock { + constructor() { + this.prev = Promise.resolve(); } -} -async function registerLISAEvents(id, progress) { - let eventHandlers = [] - if (progress === undefined) { - console.log("DACApi progress undefined, return") - return + lock() { + let unlock; + const next = new Promise(resolve => { + unlock = resolve; + }); + const result = this.prev.then(() => unlock); + this.prev = next; + return result; } - progress.reset() +}; - let handleProgress = (notification, eventname, plugin) => { - console.log('DACApi handleProgress: ' + plugin + ' ' + eventname) - if (plugin !== 'LISA') { - return +let packageLock = new OperationLock(); +let storeConfig = null; + +async function getStoreConfig() { + if (!storeConfig) { + const config = await PackageManager.get().configuration(); + if (typeof config?.configUrl !== "string") { + throw new Error("Invalid config: " + JSON.stringify(config)); } - if (notification.status === 'Progress') { - let parts = notification.details.split(" ") - if (parts.length >= 2) { - let pc = parseFloat(parts[1]) / 100.0 - progress.setProgress(pc, translateLisaProgressEvent(parts[0])) - } - } else if (notification.status === 'Success') { - progress.fireAncestors('$fireDACOperationFinished', true) - eventHandlers.map(h => { h.dispose() }) - eventHandlers = [] - } else if (notification.status === 'Failed') { - progress.fireAncestors('$fireDACOperationFinished', false, 'Failed') - eventHandlers.map(h => { h.dispose() }) - eventHandlers = [] + const fetchResponse = await fetch(config.configUrl); + if (!fetchResponse.ok) { + throw new Error(`Unexpected response: ${fetchResponse.status}: ${fetchResponse.statusText}`); } + const responseObject = await fetchResponse.json(); + if (typeof responseObject?.["appstore-catalog"]?.url !== "string") { + throw new Error("Invalid response object: " + JSON.stringify(responseObject)); + } + storeConfig = responseObject["appstore-catalog"]; } - addEventHandler(eventHandlers, 'LISA', 'operationStatus', handleProgress); + + return storeConfig; } -export const installDACApp = async (app, progress) => { - let platName = await getPlatformNameForDAC() - let url = app.url - if (!Storage.get("CloudAppStore")) { - url = app.url.replace(/ah212/g, platName) +async function fetchStoreObject(request) { + let config = await getStoreConfig(); + let headers = new Headers(); + + if (typeof config?.authentication?.user === "string" && + typeof config?.authentication?.password === "string") { + headers.append( + "Authorization", + "Basic " + btoa(config.authentication.user + ':' + config.authentication.password) + ); } - registerLISAEvents(app.id, progress) + let requestOptions = { + method: 'GET', + headers, + redirect: 'follow', + }; - let param = { - id: app.id, - type: 'application/dac.native', - appName: app.name, - category: app.category, - versionAsParameter: app.version, - url: url + const fetchResponse = await fetch(config.url + request, requestOptions); + if (!fetchResponse.ok) { + throw new Error(`Unexpected response: ${fetchResponse.status}: ${fetchResponse.statusText}`); } - try { - console.info("installDACApp LISA.install with param:"+ JSON.stringify(param)) - app.handle = await LISAApi.get().install(param) - } catch (error) { - console.error('DACApi Error on installDACApp: ' + error.code + ' ' + error.message) - Metrics.error(Metrics.ErrorType.OTHER, "DACApiError", 'DACApi Error on installDACApp: '+JSON.stringify(error), true, null) - app.errorCode = error.code; - return false - } - return true + return fetchResponse.json(); } -export const uninstallDACApp = async (app, progress) => { - // Could be same app is running; lets end it if so. - await thunderJS()['org.rdk.RDKShell'].getClients().then(response => { - if (Array.isArray(response.clients) && response.clients.includes(app.id.toLowerCase())) { - console.log("DACApi killing " + app.id + " as we got a match in getClients response."); - thunderJS()['org.rdk.RDKShell'].kill({ client: app.id }) +export async function getAppCatalogInfo() { + let result = []; + let offset = 0; + + for (let i = 0; i < APPS_REQUESTS_MAX; ++i) { + const request = "/apps?arch=" + APP_DEFAULT_ARCH + "&offset=" + offset + "&limit=" + APPS_REQUEST_LIMIT; + console.log(`Requesting: ${request}`); + try { + const appsResponse = await fetchStoreObject(request); + if (!Array.isArray(appsResponse?.applications)) { + break; + } + result = result.concat(appsResponse.applications); + if (result.length >= appsResponse?.meta?.resultSet?.total ?? 0) { + break; + } + offset = result.length; + } catch (err) { + console.error(`fetch(${request}) ${err}`); + Metrics.error(Metrics.ErrorType.OTHER, "DACApiError", err.toString(), false, null); + break; } - }) - - registerLISAEvents(app.id, progress) - - let param = { - id: app.id, - type: 'application/dac.native', - versionAsParameter: app.version, - uninstallType: 'full' } - try { - console.info("uninstallDACApp LISA.uninstall with params:"+ JSON.stringify(param)) - if (Object.prototype.hasOwnProperty.call(app, "errorCode")) delete app.errorCode; - await LISAApi.get().uninstall(param) - } catch (error) { - console.error('DACApi Error on LISA uninstall: ' + error.code + ' ' + error.message) - Metrics.error(Metrics.ErrorType.OTHER, "DACApiError", 'DACApi Error on LISA uninstall: '+JSON.stringify(error), true, null) - app.errorCode = error.code; - return false - } - return true + return result; } -export const isInstalledDACApp = async (app) => { - let result = null - try { - result = await LISAApi.get().getStorageDetails({ - id: app.id, - type: 'application/dac.native', - versionAsParameter: app.version, - }) - } catch (error) { - console.error('DACApi Error on LISA getStorageDetails: ' + JSON.stringify(error)) - Metrics.error(Metrics.ErrorType.OTHER, "DACApiError", 'DACApi Error on LISA getStorageDetails: '+JSON.stringify(error), false, null) - return false - } - - return result == null ? false : result.apps.path !== '' +function getAppDetails(id, version) { + return fetchStoreObject("/apps/" + id + ":" + version + "?arch=" + APP_DEFAULT_ARCH); } -export const getInstalledDACApps = async () => { - let result = null - try { - result = await LISAApi.get().getList() - } catch (error) { - console.error('DACApi Error on LISA getList: ' + JSON.stringify(error)) - Metrics.error(Metrics.ErrorType.OTHER, "DACApiError", 'DACApi Error on LISA getList: '+JSON.stringify(error), false, null) - } +function retrieveURLAndSize(details) { + if (typeof details?.header?.url === "string") { + const url = details.header.url; + let size = DEFAULT_SIZE; - return result == null ? [] : (result.apps ? result.apps : []) -} + if (typeof details.header.size === "number") { + size = details.header.size; + } -export const getPlatformNameForDAC = async () => { - //code temporarily added based on new api implementation - platform = await getPlatform() - if (platform == null || platform === "") { - platform = await getDeviceName() - platform = platform.split('-')[0] - console.info("getPlatformNameForDAC platform after split: " + JSON.stringify(platform)) - } - else { - return platform + return { url, size }; } - if (platform.startsWith('raspberrypi4')) { - return 'rpi4' - } else if (platform.startsWith('raspberrypi')) { - return 'rpi3' - } else if (platform === 'brcm972180hbc') { - return '7218c' - } else if (platform === 'brcm972127ott') { - return '72127ott' - } else if (platform === 'vip7802') { - return '7218c' - } else if (platform === 'm393') { - return '7218c' - } else if (platform.toLowerCase().includes('hp44h')) { - return 'ah212' - } else if (platform.toLowerCase().includes('amlogic')) { - return 'ah212' - } else if (platform.toLowerCase().includes('mediaclient')) { - return 'rtd1319' - } else if (platform.toLowerCase().includes('blade')) { - return 'rtd1319' - } else { - // default - return 'rpi3' - } + throw new Error(`No URL in details: ${JSON.stringify(details)}`); } -export const getDeviceName = async () => { - let result = null +async function isPackageInstalled(id, version) { + let result = false; + try { - result = await thunderJS().DeviceInfo.systeminfo() - } catch (error) { - console.error('DAC Api Error on systeminfo: '+ error) - Metrics.error(Metrics.ErrorType.OTHER, "DACApiError", 'DAC Api Error on systeminfo: '+JSON.stringify(error), false, null) + const packageState = await PackageManager.get().packageState(id, version); + result = (packageState === "INSTALLED"); + } catch (err) { + logWarning(`isPackageInstalled()`, err); } - return result == null ? "unknown" : result.devicename + + return result; } -export const startDACApp = async (app) => { - console.log('DACApi startDACApp invoked with data:' + app) - let result = null - try { - if (app.type === 'application/dac.native') { - result = await thunderJS()['org.rdk.RDKShell'].launchApplication({ - client: app.id, - mimeType: app.type, - uri: app.id + ';' + app.version + ';' + app.type, - topmost: true, - focus: true - }) - } else if (app.type === 'application/html') { - result = await thunderJS()['org.rdk.RDKShell'].launch({ callsign: app.id, uri: app.url, type: 'HtmlApp' }) - } else if (app.type === 'application/lightning') { - result = await thunderJS()['org.rdk.RDKShell'].launch({ callsign: app.id, uri: app.url, type: 'LightningApp' }) - } else { - console.warn('DACApi Unsupported app type: ' + app.type) - return false +async function downloadAndInstall(pkg, downloadedSize, totalSize, progress) { + console.log(`downloadAndInstall(${pkg.url}, ${pkg.size})`); + + const downloadId = await new Promise(async (resolve, reject) => { + try { + await DownloadManager.get().download(pkg.url, (downloadId, percent, failReason) => { + if (!failReason) { + if (percent !== 100) { + progress((downloadedSize + pkg.size * percent / 100) / totalSize, "Downloading"); + } else { + resolve(downloadId); + } + } else { + reject(failReason); + } + }); + } catch (err) { + reject(err); } - } catch (error) { - console.error('DACApi Error on launchApplication: ' + JSON.stringify(error)) - Metrics.error(Metrics.ErrorType.OTHER, "DACApiError", 'DACApi Error on launchApplication: '+JSON.stringify(error), false, null) - return false + }); + + let errResult = null; + + const fileLocator = DownloadManager.get().getFileLocator(downloadId); + if (!fileLocator) { + throw new Error(`Missing file locator for downloadId ${downloadId}`); } - if (result == null) { - console.error('DACApi launch error returned result: ' + JSON.stringify(result)) - return false - } else if (!result.success) { - // Could be same app is in suspended mode. - await thunderJS()['org.rdk.RDKShell'].getClients().then(response => { - if (Array.isArray(response.clients) && response.clients.includes(app.id.toLowerCase())) { - console.log("DACApi " + app.id + " got a match in getClients response; could be in suspended mode, resume it."); - thunderJS()['org.rdk.RDKShell'].resumeApplication({ client: app.id }).then(result => { - if (!result.success) { - return false; - } else if (result.success) { - if (GLOBALS.topmostApp === GLOBALS.selfClientName) { - thunder.call('org.rdk.RDKShell', 'setVisibility', { "client": GLOBALS.selfClientName, "visible": false }) - } - } - }) - } - }) - } else if (result.success) { - if (GLOBALS.topmostApp === GLOBALS.selfClientName) { - thunder.call('org.rdk.RDKShell', 'setVisibility', { "client": GLOBALS.selfClientName, "visible": false }) + try { + let installResult = await PackageManager.get().install( + pkg.id, pkg.version, fileLocator, + ); + + if (installResult !== "NONE") { + errResult = new Error(installResult); } - } else { - // Nothing to do here. + } catch (err) { + logError(`install(${pkg.id}, ${pkg.version})`, err); + errResult = err; } try { - result = await thunderJS()['org.rdk.RDKShell'].moveToFront({ client: app.id }) - } catch (error) { - console.log('DACApi Error on moveToFront: ' + JSON.stringify(error)) - Metrics.error(Metrics.ErrorType.OTHER, "DACApiError", "Error in Thunder RDKShell moveToFront DACApiError"+JSON.stringify(error), false, null) + await DownloadManager.get().delete(downloadId); + } catch (err) { + logWarning(`delete(${downloadId})`, err); } try { - result = await thunderJS()['org.rdk.RDKShell'].setFocus({ client: app.id }) - GLOBALS.topmostApp = (app.id + ';' + app.version + ';' + app.type); - } catch (error) { - console.log('DACApi Error on setFocus: ' + JSON.stringify(error)) - Metrics.error(Metrics.ErrorType.OTHER, "DACApiError", "Error in Thunder DACApi setFocus"+JSON.stringify(error), false, null) - return false + await AppManager.get().setAppProperty(pkg.id, APP_DETAILS_KEY, JSON.stringify(pkg.details)); + } catch (err) { + logWarning(`downloadAndInstall(${pkg.id})`, new ThunderError("setAppProperty()", err)); } - return result == null ? false : result.success + + if (errResult) { + throw errResult; + } + + return true; } -/* WorkAround until proper cloud based App Catalog support */ -export const getAppCatalogInfo = async () => { - Storage.set("CloudAppStore", true); - let appListArray = [] +export async function installDACApp(app, progressElement) { + let result = false; + + console.log(`installDACApp ${JSON.stringify(app)}`); + + const unlock = await packageLock.lock(); + + function progress(percent, state) { + progressElement.setProgress(percent, state); + } + + function success() { + progressElement.fireAncestors('$fireDACOperationFinished', true); + } + try { - let data = new HomeApi().getPartnerAppsInfo(); - if (data) { - data = await JSON.parse(data); - if (data != null && Object.prototype.hasOwnProperty.call(data, "app-catalog-path")) { - Storage.set("CloudAppStore", false); - console.log("Fetching apps from local server") - let url = data["app-catalog-path"] - await fetch(url, { method: 'GET', cache: "no-store" }) - .then(response => response.text()) - .then(result => { - result = JSON.parse(result) - console.log("DACApi fetch result: " + JSON.stringify(result)) - if (Object.prototype.hasOwnProperty.call(result, "applications")) { - appListArray = result["applications"]; - } else { - console.error("DACApi result does not have applications") - Metrics.error(Metrics.ErrorType.OTHER, "DACApiError","DACApi result does not have applications", false, null) - Storage.set("CloudAppStore", true); - } - }) - .catch(error => { - console.error("DACApi fetch error from local server", error) - Metrics.error(Metrics.ErrorType.OTHER, "DACApiError", JSON.stringify(error), false, null) - Storage.set("CloudAppStore", true); - }); - } - else if (Storage.get("CloudAppStore") && (data != null) && Object.prototype.hasOwnProperty.call(data, "app-catalog-cloud")) { - console.log("Fetching apps from cloud server") - let cloud_data = data["app-catalog-cloud"] - if (cloud_data && Object.prototype.hasOwnProperty.call(cloud_data, "url")) { - let url = cloud_data["url"] + "?platform=arm:v7:linux&category=application" - await fetch(url, { method: 'GET', cache: "no-store" }) - .then(response => response.text()) - .then(result => { - result = JSON.parse(result) - console.log("DACApi fetch result: " + JSON.stringify(result)) - if (Object.prototype.hasOwnProperty.call(result, "applications")) { - appListArray = result["applications"]; - } else { - console.error("DACApi result does not have applications") - Metrics.error(Metrics.ErrorType.OTHER, "DACApiError", "DACApi result does not have applications", false, null) - } - }) - .catch(error => { - console.log("DACApi fetch error from cloud: " + JSON.stringify(error)) - Metrics.error(Metrics.ErrorType.OTHER, "DACApiError", JSON.stringify(error), false, null) - }); + const appDetails = await getAppDetails(app.id, app.version); + const packages = []; + let totalSize = 0; + + if (typeof appDetails?.dependencies === "object") { + for (const id in appDetails.dependencies) { + const version = appDetails.dependencies[id]; + if (!await isPackageInstalled(id, version)) { + const depDetails = await getAppDetails(id, version); + packages.push(Object.assign( + retrieveURLAndSize(depDetails), + { id, version, details: depDetails } + )); + totalSize += packages.at(-1).size; } else { - console.error("DACApi app-catalog-cloud does not have URL property.") - Metrics.error(Metrics.ErrorType.OTHER, "DACApiError", "DACApi app-catalog-cloud does not have URL property.", false, null) + console.log(`Package ${id}+${version} is already installed`); } } - } else { - let hasmore=true - let offset =0 - let asms = await getAsmsUrlObj() - let myHeaders = new Headers(); - if ((asms.password !== null) && (asms.username !== null)) { - myHeaders.append("Authorization", "Basic " + btoa(asms.username + ':' + asms.password)); - } - let requestOptions = { - method: 'GET', - headers: myHeaders, - redirect: 'follow' - }; - while(hasmore){ - let url = asms.url +"?offset="+offset+"&platformName=" + await getPlatformNameForDAC() + "&firmwareVer=" + await getFirmwareVersion() - await fetch(url, requestOptions) - .then(response => response.json()) - .then(result => { - if (Object.prototype.hasOwnProperty.call(result, "applications")) { - const apps = result["applications"]; - appListArray = appListArray.concat(apps); - const count = result?.meta?.resultSet?.count || 0; - - if (count < 10) { - hasmore= false; - } else { - offset += 10; - } - } else { - console.error("DACApi result does not have applications") - Metrics.error(Metrics.ErrorType.OTHER,"DACApiError", "DACApi result does not have applications", false, null) - hasmore = false; - } - }) - .catch(error => { - console.log("DACApi fetch error from cloud: " + JSON.stringify(error)) - Metrics.error(Metrics.ErrorType.OTHER,"DACApiError", error, false, null) - hasmore = false; - }); - } } - } catch (error) { - console.log("DACApi Using new getMetadata API.") - Metrics.error(Metrics.ErrorType.OTHER,"DACApiError", "DACApi Using new getMetadata API.", false, null) - let hasmore=true - let offset =0 - let asms = await getAsmsUrlObj() - let myHeaders = new Headers(); - if ((asms.password !== null) && (asms.username !== null)) { - myHeaders.append("Authorization", "Basic " + btoa(asms.username + ':' + asms.password)); + + packages.push(Object.assign( + retrieveURLAndSize(appDetails), + { id: app.id, version: app.version, details: appDetails } + )); + totalSize += packages.at(-1).size; + + let downloadedSize = 0; + for (let pkg of packages) { + await downloadAndInstall(pkg, downloadedSize, totalSize, progress); + downloadedSize += pkg.size; } - let requestOptions = { - method: 'GET', - headers: myHeaders, - redirect: 'follow' - }; - while(hasmore){ - let url = asms.url +"?offset="+offset+"&platformName=" + await getPlatformNameForDAC() + "&firmwareVer=" + await getFirmwareVersion() - await fetch(url, requestOptions) - .then(response => response.json()) - .then(result => { - if (Object.prototype.hasOwnProperty.call(result, "applications")) { - appListArray = appListArray.concat(result["applications"]); - const count = result?.meta?.resultSet?.count || 0; - - if (count < 10) { - hasmore= false; - } else { - offset += 10; - } - } else { - console.error("DACApi result does not have applications") - Metrics.error(Metrics.ErrorType.OTHER,"DACApiError", "DACApi result does not have applications", false, null) - hasmore = false; - } - }) - .catch(error => { - console.log("DACApi fetch error from cloud: " + JSON.stringify(error)) - Metrics.error(Metrics.ErrorType.OTHER,"DACApiError", error, false, null) - hasmore = false; - }); + success(); + result = true; + } catch (err) { + app.errorCode = err?.cause?.code ?? -2; + logError(`installDACApp(${app.id})`, err); + } finally { + unlock(); } - } - return appListArray == [] ? undefined : appListArray; + + return result; } -export const getFirmwareVersion = async () => { - let firmwareVerList = null, firmwareVer = null +export async function isDACAppInstalled(app) { + let result = false; try { - let data = new HomeApi().getPartnerAppsInfo(); - if (data) { - data = await JSON.parse(data); - if (data != null && Object.prototype.hasOwnProperty.call(data, "app-catalog-cloud")) { - let cloud_data = data["app-catalog-cloud"] - if (Object.prototype.hasOwnProperty.call(cloud_data, "firmwareVersions")) { - firmwareVerList = cloud_data['firmwareVersions'] - let i = 0 - while (i < firmwareVerList.length) { - if (await getPlatformNameForDAC() === firmwareVerList[i].platform) { - firmwareVer = firmwareVerList[i].ver - break - } - i += 1 - } - if (firmwareVer === null) { - console.error("Platform not supported") - Metrics.error(Metrics.ErrorType.OTHER,"DACApiError", "Platform not supported", false, null) - } - } - else { - console.error("Firmware version not available") - Metrics.error(Metrics.ErrorType.OTHER,"DACApiError", "Firmware version not available", false, null) - } - } - } else { - //code temporarily added based on new api implementation - firmwareVer = await getFirmareVer() - } - } catch (error) { - console.log("DACApi getFirmwareVersion Error: " + JSON.stringify(error)) - //code temporarily added based on new api implementation - Metrics.error(Metrics.ErrorType.OTHER,"DACApiError", error, false, null) - firmwareVer = await getFirmareVer() + result = await AppManager.get().isInstalled(app.id); + } catch (err) { + logWarning(`isDACAppInstalled(${app.id})`, new ThunderError("isInstalled()", err)); } - return firmwareVer; + return result; } -export const fetchAppIcon = async (id, version) => { - let appIcon = null - try { - let data = new HomeApi().getPartnerAppsInfo(); - if (data) { - data = await JSON.parse(data); - if (data != null && Object.prototype.hasOwnProperty.call(data, "app-catalog-cloud")) { - let cloud_data = data["app-catalog-cloud"] - let url = cloud_data["url"] + "/" + id + ":" + version + "?platformName=" + await getPlatformNameForDAC() + "&firmwareVer=" + await getFirmwareVersion() - await fetch(url, { method: 'GET', cache: "no-store" }) - .then(response => response.text()) - .then(result => { - result = JSON.parse(result) - console.log("fetchAppIcon fetch result: " + JSON.stringify(result)) - if (Object.prototype.hasOwnProperty.call(result, "header")) { - appIcon = result.header.icon; - } else { - console.error("fetchAppIcon App does not have URL") - Metrics.error(Metrics.ErrorType.OTHER,"DACApiError", "fetchAppIcon App does not have URL", false, null) - } - }) - .catch(error => { - console.log("App Icon fetch error: " + JSON.stringify(error)) - Metrics.error(Metrics.ErrorType.OTHER,"DACApiError", error, false, null) - }); - } - } else { - //code temporarily added based on new api implementation - let asms = await getAsmsUrlObj() - let myHeaders = new Headers(); - if ((asms.password !== null) && (asms.username !== null)) { - myHeaders.append("Authorization", "Basic " + btoa(asms.username + ':' + asms.password)); - } +export async function uninstallDACApp(app, progressElement) { + let result = false; - let requestOptions = { - method: 'GET', - headers: myHeaders, - redirect: 'follow' - }; - let url = asms.url + "/" + id + ":" + version + "?platformName=" + await getPlatformNameForDAC() + "&firmwareVer=" + await getFirmwareVersion() - - await fetch(url, requestOptions) - .then(response => response.text()) - .then(result => { - result = JSON.parse(result) - if (Object.prototype.hasOwnProperty.call(result, "header")) { - appIcon = result.header.icon; - } else { - console.error("fetchAppIcon App does not have URL") - Metrics.error(Metrics.ErrorType.OTHER,"DACApiError", "fetchAppIcon App does not have URL", false, null) - } - }) - .catch(error => { - console.log("fetchAppIcon App Icon fetch error: " + JSON.stringify(error)) - Metrics.error(Metrics.ErrorType.OTHER,"DACApiError", error, false, null) - }); - } - } catch (error) { - console.log("DACApi fetchAppIcon try block Error: " + JSON.stringify(error)) - //code temporarily added based on new api implementation - let asms = await getAsmsUrlObj() - let myHeaders = new Headers(); - if ((asms.password !== null) && (asms.username !== null)) { - myHeaders.append("Authorization", "Basic " + btoa(asms.username + ':' + asms.password)); - } + console.log(`uninstallDACApp ${JSON.stringify(app)}`); - let requestOptions = { - method: 'GET', - headers: myHeaders, - redirect: 'follow' - }; - let url = asms.url + "/" + id + ":" + version + "?platformName=" + await getPlatformNameForDAC() + "&firmwareVer=" + await getFirmwareVersion() - await fetch(url, requestOptions) - .then(response => response.json()) - .then(result => { - if (Object.prototype.hasOwnProperty.call(result, "header")) { - appIcon = result.header.icon; - } else { - console.error("fetchAppIcon App does not have URL") - Metrics.error(Metrics.ErrorType.OTHER,"DACApiError", "fetchAppIcon App does not have URL", false, null) - } - }) - .catch(error => { - console.log("fetchAppIcon App Icon fetch error: " + JSON.stringify(error)) - Metrics.error(Metrics.ErrorType.OTHER,"DACApiError", error, false, null) - }); + const unlock = await packageLock.lock(); + + function success() { + progressElement.fireAncestors('$fireDACOperationFinished', true); } - return appIcon == null ? undefined : appIcon; -} -export const fetchLocalAppIcon = async (id) => { - let appIcon = null try { - let data = new HomeApi().getPartnerAppsInfo(); - if (data) { - data = await JSON.parse(data); - if (data != null && Object.prototype.hasOwnProperty.call(data, "app-catalog-path")) { - let url = data["app-catalog-path"] - await fetch(url, { method: 'GET', cache: "no-store" }) - .then(response => response.text()) - .then(result => { - result = JSON.parse(result) - if (Object.prototype.hasOwnProperty.call(result, "applications")) { - let appListArray = result["applications"]; - for (let i = 0; i < appListArray.length; i++) { - if (appListArray[i].id === id) { - appIcon = appListArray[i]["icon"] - break; - } - } - } else { - console.error("fetchLocalAppIcon DACApi result does not have applications") - Metrics.error(Metrics.ErrorType.OTHER,"DACApiError", "fetchLocalAppIcon DACApi result does not have applications", false, null) - Storage.set("CloudAppStore", true); - } - }) - .catch(error => { - console.log("fetchLocalAppIcon App Icon fetch error: " + JSON.stringify(error)) - Metrics.error(Metrics.ErrorType.OTHER,"DACApiError", error, false, null) - }); - } - } else { - console.error("fetchLocalAppIcon Appstore info not available; DAC features won't work.") - Metrics.error(Metrics.ErrorType.OTHER,"DACApiError", "etchLocalAppIcon Appstore info not available; DAC features won't work", false, null) - } - } catch (error) { - console.log("fetchLocalAppIcon Appstore info Error: " + JSON.stringify(error)) - Metrics.error(Metrics.ErrorType.OTHER,"DACApiError", error, false, null) + await AppManager.get().terminateApp(app.id); + } catch (err) { + logWarning(`uninstallDACApp(${app.id})`, new ThunderError("terminateApp()", err)); } - return appIcon == null ? undefined : appIcon; + + try { + await PackageManager.get().uninstall(app.id); + success(); + result = true; + } catch (err) { + app.errorCode = err?.cause?.code ?? -2; + logError(`uninstall(${app.id})`, err); + } finally { + unlock(); + } + + return result; } -export const fetchAppUrl = async (id, version) => { - let appUrl = null +const storedAppInfoCache = new Map(); + +export async function getInstalledDACApps() { + const result = []; + console.log("getInstalledDACApps()"); + try { - let data = new HomeApi().getPartnerAppsInfo(); - if (data) { - data = await JSON.parse(data); - if (data != null && Object.prototype.hasOwnProperty.call(data, "app-catalog-cloud")) { - let cloud_data = data["app-catalog-cloud"] - let url = cloud_data["url"] + "/" + id + ":" + version + "?platformName=" + await getPlatformNameForDAC() + "&firmwareVer=" + await getFirmwareVersion() - await fetch(url, { method: 'GET', cache: "no-store" }) - .then(response => response.text()) - .then(result => { - result = JSON.parse(result) - console.log("fetchAppUrl App fetch result: " + JSON.stringify(result)) - if (Object.prototype.hasOwnProperty.call(result, "header")) { - appUrl = result.header.url; - } else { - console.error("fetchAppUrl App does not have URL") - Metrics.error(Metrics.ErrorType.OTHER,"DACApiError", "fetchAppUrl App does not have URL", false, null) + const packages = await PackageManager.get().listPackages(); + + for (const pkg of packages) { + const key = pkg.packageId + ":" + pkg.version; + let storedAppInfo = storedAppInfoCache.get(key); + + if (!storedAppInfo) { + try { + let cachedAppDetails = null; + try { + cachedAppDetails = JSON.parse(await AppManager.get().getAppProperty(pkg.packageId, APP_DETAILS_KEY)); + if (!cachedAppDetails?.header?.name) { + storedAppInfoCache.set(key, {}); + continue; } - }) - .catch(error => { - console.log("fetchAppUrl App URL fetch error: " + JSON.stringify(error)) - Metrics.error(Metrics.ErrorType.OTHER,"DACApiError", error, false, null) - }); - } - } else { - //code temporarily added based on new api implementation - let asms = await getAsmsUrlObj() - let myHeaders = new Headers(); - if ((asms.password !== null) && (asms.username !== null)) { - myHeaders.append("Authorization", "Basic " + btoa(asms.username + ":" + asms.password)); - } - let requestOptions = { - method: 'GET', - headers: myHeaders, - redirect: 'follow' - }; - let platformName = await getPlatformNameForDAC(); - let firmwareVer = await getFirmwareVersion(); - let url = asms.url + "/" + id + ":" + version + "?platformName=" + platformName + "&firmwareVer=" + firmwareVer; - await fetch(url, requestOptions) - .then(response => response.json()) - .then(result => { - if (Object.prototype.hasOwnProperty.call(result, "header")) { - appUrl = result.header.url; - } else { - console.error("fetchAppUrl App does not have URL") - Metrics.error(Metrics.ErrorType.OTHER,"DACApiError", "fetchAppUrl App does not have URL", false, null) + } catch (err) { + logWarning("getInstalledDACApps()", new ThunderError(`getAppProperty(${pkg.packageId})`, err)); + } + + let appDetails = null; + try { + appDetails = await getAppDetails(pkg.packageId, pkg.version); + } catch (err) { + logWarning("getInstalledDACApps()", err); + } + + storedAppInfo = { + name: appDetails?.header?.name ?? cachedAppDetails?.header?.name, + icon: appDetails?.header?.icon, } - }) - .catch(error => { - console.log("fetchAppUrl App URL fetch error: " + JSON.stringify(error)) - Metrics.error(Metrics.ErrorType.OTHER,"DACApiError", error, false, null) + storedAppInfoCache.set(key, storedAppInfo); + } catch (err) { + logError(`getAppDetails(${pkg.packageId}, ${pkg.version})`, err); + } + } + + if (storedAppInfo?.name) { + result.push({ + id: pkg.packageId, + version: pkg.version, + name: storedAppInfo.name, + installed: (pkg.state === "INSTALLED") ? [{ + appName: storedAppInfo.name, + version: pkg.version, + }] : [], }); + if (storedAppInfo.icon) { + result.at(-1).icon = storedAppInfo.icon; + } + } } - } catch (error) { - console.log("fetchAppUrl DACApi Appstore info Error: " + JSON.stringify(error)) - Metrics.error(Metrics.ErrorType.OTHER,"DACApiError", error, false, null) + } catch (err) { + logError("getInstalledDACApps()", err); } - return appUrl == null ? undefined : appUrl; -} - -//New api implementation -export const getMetadata = async () => { - const result = await LISAApi.get().getMetadata(); return result; } -export const getAsmsUrlObj = async () => { +export async function startDACApp(app) { + let result = false; + console.log(`startDACApp ${JSON.stringify(app)}`); + try { - let metadata = await getMetadata(); - let response = await fetch(metadata.configUrl); - let result = await response.json(); - - if (Object.prototype.hasOwnProperty.call(result, "appstore-catalog")) { - const appstoreCatalog = result['appstore-catalog']; - const asmsUrl = appstoreCatalog.url + "/apps" || null; - const authentication = appstoreCatalog.authentication || {}; - const username = authentication.user || null; - const password = authentication.password || null; - return { url: asmsUrl, username: username, password: password }; - } else { - console.error("getAsmsUrlObj: Don't have ASMS URL"); - Metrics.error(Metrics.ErrorType.OTHER,"DACApiError", "getAsmsUrlObj: Don't have ASMS URL", false, null) - throw new Error('getAsmsUrlObj: Failed to parse data'); - } + await AppManager.get().launchApp(app.id); + result = true; } catch (err) { - console.error("getAsmsUrlObj FETCH error: " + JSON.stringify(err)); - Metrics.error(Metrics.ErrorType.OTHER,"DACApiError", err, false, null) - throw new Error('getAsmsUrlObj: Failed to parse data: ' + err); + logError(`startDACApp(${app.id})`, new ThunderError("launchApplication()", err)); } -} - -export const getFirmareVer = async () => { - const firmwareVer = await getMetadata().then(metadata => { - let key = metadata.dacBundleFirmwareCompatibilityKey; - return key - }); - return firmwareVer -} -export const getPlatform = async () => { - const platformName = await getMetadata().then(metadata => { - let platName = metadata.dacBundlePlatformNameOverride; - return platName - }); - return platformName; + return result; } diff --git a/accelerator-home-ui/src/api/DownloadManagerApi.js b/accelerator-home-ui/src/api/DownloadManagerApi.js new file mode 100644 index 0000000..5083475 --- /dev/null +++ b/accelerator-home-ui/src/api/DownloadManagerApi.js @@ -0,0 +1,188 @@ +/** + * If not stated otherwise in this file or this component's LICENSE + * file the following copyright and licenses apply: + * + * Copyright 2026 RDK Management + * + * 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. + **/ + +import { ThunderError } from './ThunderError'; +import ThunderJS from 'ThunderJS'; +import { CONFIG } from '../Config/Config' +import { Metrics } from "@firebolt-js/sdk" + +const REFRESH_INTERVAL_MS = 500; + +let instance = null; + +export default class DownloadManager { + static get() { + if (instance === null) { + instance = new DownloadManager() + } + return instance; + } + + constructor() { + this.thunder = ThunderJS(CONFIG.thunderConfig); + this.callsign = 'org.rdk.DownloadManager'; + this.downloadIdToStatus = new Map(); + this.listeners = new Map(); + this.tickTimeout = null; + this.INFO = console.info; + this.LOG = console.log; + this.ERR = console.error; + + this.thunder.on(this.callsign, "onAppDownloadStatus", event => { + if (typeof event.downloadStatus === "string") { + try { + const downloadStatusArray = JSON.parse(event.downloadStatus); + for (const downloadStatus of downloadStatusArray) { + this.LOG(`DownloadManager download status: ${JSON.stringify(downloadStatus)}`); + if (typeof downloadStatus.downloadId === "string" && + typeof downloadStatus.fileLocator === "string") { + const downloadId = downloadStatus.downloadId; + this.downloadIdToStatus.set(downloadId, downloadStatus); + const listener = this.listeners.get(downloadId); + if (listener) { + this.listeners.delete(downloadId); + if (this.listeners.size === 0) { + clearTimeout(this.tickTimeout); + this.tickTimeout = null; + } + listener(downloadId, 100, downloadStatus.failReason); + } + } else { + throw new Error(`Required properties are missing ${JSON.stringify(downloadStatus)}`); + } + } + } catch (err) { + this.ERR(`onAppDownloadStatus() error: ${err} for event: ${JSON.stringify(event)}`); + } + } + }); + } + + handleThunderError(thunderCall, thunderErr) { + const err = new ThunderError(thunderCall, thunderErr); + const errString = err.toString(); + this.ERR(errString); + Metrics.error(Metrics.ErrorType.OTHER, "DownloadManager", errString, false, null); + throw err; + } + + activate() { + return this.thunder.Controller.activate( + { callsign: this.callsign } + ).then(() => { + this.INFO("DownloadManager activated"); + return true; + }).catch(err => { + this.handleThunderError(`activate(${this.callsign})`, err); + }); + } + + deactivate() { + return this.thunder.Controller.deactivate( + { callsign: this.callsign } + ).then(() => { + this.INFO("DownloadManager deactivated"); + return true; + }).catch(err => { + this.handleThunderError(`deactivate(${this.callsign})`, err); + }); + } + + async tick() { + for (const [downloadId] of this.listeners) { + try { + const progress = await this.progress(downloadId); + if (typeof progress?.percent === "number") { + const listener = this.listeners.get(downloadId); + if (listener) { + listener(downloadId, progress.percent); + } + } + } catch (err) { + this.ERR(`Error: tick() ${downloadId} ${err}`); + } + } + if (this.listeners.size) { + this.tickTimeout = setTimeout(this.tick.bind(this), REFRESH_INTERVAL_MS); + } else { + this.tickTimeout = null; + } + } + + download(url, listener) { + return this.thunder.call(this.callsign, 'download', { + url, + }).then(result => { + this.LOG(`download(${url}) result: ${JSON.stringify(result)}`); + const downloadId = result.downloadId ?? result; + const downloadStatus = this.downloadIdToStatus.get(downloadId); + + if (!downloadStatus) { + this.listeners.set(downloadId, listener); + if (!this.tickTimeout) { + this.tickTimeout = setTimeout(this.tick.bind(this), 0); + } + } else { + listener(downloadId, 100, downloadStatus.failReason); + } + + return downloadId; + }).catch(err => { + this.handleThunderError(`download(${url})`, err); + }); + } + + progress(downloadId) { + return this.thunder.call(this.callsign, 'progress', { + downloadId, + }).then(result => { + this.LOG(`progress(${downloadId}) result: ${JSON.stringify(result)}`); + if (typeof result === "number") { + return { + percent: result, + }; + } + return result; + }).catch(err => { + this.handleThunderError(`progress(${downloadId})`, err); + }); + } + + getFileLocator(downloadId) { + return this.downloadIdToStatus.get(downloadId)?.fileLocator; + } + + delete(downloadId) { + const fileLocator = this.getFileLocator(downloadId); + + if (fileLocator) { + return this.thunder.call(this.callsign, 'delete', { + fileLocator, + }).then(result => { + this.downloadIdToStatus.delete(downloadId); + this.LOG(`delete(${fileLocator}) result: ${JSON.stringify(result)}`); + return result; + }).catch(err => { + this.handleThunderError(`delete(${fileLocator})`, err); + }); + } else { + throw new Error(`Unknown fileLocator of ${downloadId}`); + } + } +} diff --git a/accelerator-home-ui/src/api/PackageManagerApi.js b/accelerator-home-ui/src/api/PackageManagerApi.js index a447b14..4af42c1 100644 --- a/accelerator-home-ui/src/api/PackageManagerApi.js +++ b/accelerator-home-ui/src/api/PackageManagerApi.js @@ -16,12 +16,23 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ + import ThunderJS from 'ThunderJS'; import { CONFIG } from '../Config/Config' import { Metrics } from "@firebolt-js/sdk" +import { ThunderError } from './ThunderError'; + +let instance = null; -let instance = null export default class PackageManager { + static get() { + if (instance === null) { + instance = new PackageManager() + } + + return instance; + } + constructor() { this.thunder = ThunderJS(CONFIG.thunderConfig); this.callsign = 'org.rdk.PackageManagerRDKEMS'; @@ -29,111 +40,138 @@ export default class PackageManager { this.LOG = console.log; this.ERR = console.error; } - static get() { - if (instance === null) { - instance = new PackageManager() - } - return instance; + + handleThunderError(thunderCall, thunderErr) { + const err = new ThunderError(thunderCall, thunderErr); + const errString = err.toString(); + this.ERR(errString); + Metrics.error(Metrics.ErrorType.OTHER, "PackageManagerRDKEMS", errString, false, null) + + throw err; } - activate() { - return new Promise((resolve, reject) => { - this.thunder.Controller.activate({ callsign: callsign }) - .then(() => { - resolve(true) - this.INFO("PackageManagerRDKEMS activated successfully"); - }) - .catch(err => { - this.ERR("Error Activation PackageManagerRDKEMS" + JSON.stringify(err)) - Metrics.error(Metrics.ErrorType.OTHER, "PackageManagerRDKEMS", `Error while Thunder Controller ${callsign} activate ${JSON.stringify(err)}`, false, null) - reject(err) - }) - }) - } - - deactivate() { - return new Promise((resolve, reject) => { - this.thunder.Controller.deactivate({ callsign: callsign }) - .then(() => { - resolve(true) - this.INFO("PackageManagerRDKEMS deactivated successfully"); - }) - .catch(err => { - this.ERR("Error Deactivation PackageManagerRDKEMS" + JSON.stringify(err)) - Metrics.error(Metrics.ErrorType.OTHER, "PackageManagerRDKEMS", `Error while Thunder Controller ${callsign} deactivate ${JSON.stringify(err)}`, false, null) - reject(err) - }) - }) - } + activate() { + return this.thunder.Controller.activate( + { callsign: this.callsign } + ).then(() => { + this.INFO("PackageManagerRDKEMS activated"); + return true; + }).catch(err => { + this.handleThunderError(`activate(${this.callsign})`, err); + }); + } - listPackages() { - return new Promise((resolve, reject) => { - this.thunder.call(this.callsign, 'listPackages', {}).then(result => { - this.LOG(" listPackages result:", JSON.stringify(result)) - resolve(result.packages) - }).catch(err => { - this.ERR(" listPackages error:", JSON.stringify(err)) - reject(err) - }) - }) - } + deactivate() { + return this.thunder.Controller.deactivate( + { callsign: this.callsign } + ).then(() => { + this.INFO("PackageManagerRDKEMS deactivated"); + return true; + }).catch(err => { + this.handleThunderError(`deactivate(${this.callsign})`, err); + }); + } - getStorageDetails(packageId,version) { - return new Promise((resolve, reject) => { - this.thunder.call(this.callsign, 'getStorageDetails', { "packageId":packageId, "version":version }).then(result => { - this.LOG(" getStorageDetails result:", JSON.stringify(result)) - resolve(result) - }).catch(err => { - this.ERR(" getStorageDetails error:", JSON.stringify(err)) - reject(err) - }) - }) - } - - config(packageId,version) { - return new Promise((resolve, reject) => { - this.thunder.call(this.callsign, 'config', { "packageId":packageId, "version":version }).then(result => { - this.LOG(" config result:", JSON.stringify(result)) - resolve(result) - }).catch(err => { - this.ERR(" config error:", JSON.stringify(err)) - reject(err) - }) - }) - } + configuration() { + const thunderCall = `configuration@${this.callsign}`; - uninstall(packageId,version,fileLocator,additionalMetadata) { - return new Promise((resolve, reject) => { - this.thunder.call(this.callsign, 'uninstall', { "packageId":packageId, "version":version, "fileLocator":fileLocator, "additionalMetadata":additionalMetadata }).then(result => { - this.LOG(" uninstall result:", JSON.stringify(result)) - resolve(result) - }).catch(err => { - this.ERR(" uninstall error:", JSON.stringify(err)) - reject(err) - }) - }) - } + return this.thunder.Controller[thunderCall]( + ).then(result => { + this.LOG(thunderCall, " result:", JSON.stringify(result)); + return result; + }).catch(err => { + this.handleThunderError(thunderCall, err); + }); + } - install(packageId,version,fileLocator,additionalMetadata) { - return new Promise((resolve, reject) => { - this.thunder.call(this.callsign, 'install', { "packageId":packageId, "version":version, "fileLocator":fileLocator, "additionalMetadata":additionalMetadata }).then(result => { - this.LOG(" install result:", JSON.stringify(result)) - resolve(result) - }).catch(err => { - this.ERR(" install error:", JSON.stringify(err)) - reject(err) - }) - }) - } - packageState(packageId,version) { - return new Promise((resolve, reject) => { - this.thunder.call(this.callsign, 'packageState', { "packageId":packageId, "version":version }).then(result => { - this.LOG(" packageState result:", JSON.stringify(result)) - resolve(result) - } ).catch(err => { - this.ERR(" packageState error:", JSON.stringify(err)) - reject(err) - }) - }) - } + listPackages() { + const thunderCall = "listPackages"; + + return this.thunder.call( + this.callsign, thunderCall + ).then(result => { + this.LOG(thunderCall, " result:", JSON.stringify(result)); + return result.packages ?? result; + }).catch(err => { + this.handleThunderError(thunderCall, err); + }); + } + + getStorageDetails(packageId, version) { + const thunderCall = "getStorageDetails"; + + return this.thunder.call( + this.callsign, thunderCall, + /* ThunderJS treats "version" as its own parameter. + To forward a version to the remote function, use "versionAsParameter". */ + { packageId, "versionAsParameter": version } + ).then(result => { + this.LOG(thunderCall, " result:", JSON.stringify(result)); + return result; + }).catch(err => { + this.handleThunderError(thunderCall, err); + }); + } + + config(packageId, version) { + const thunderCall = "config"; + + return this.thunder.call( + this.callsign, thunderCall, + /* ThunderJS treats "version" as its own parameter. + To forward a version to the remote function, use "versionAsParameter". */ + { packageId, "versionAsParameter": version } + ).then(result => { + this.LOG(thunderCall, " result:", JSON.stringify(result)); + return result; + }).catch(err => { + this.handleThunderError(thunderCall, err); + }); + } + + uninstall(packageId) { + const thunderCall = "uninstall"; + + return this.thunder.call( + this.callsign, thunderCall, + { packageId } + ).then(result => { + this.LOG(thunderCall, " result:", JSON.stringify(result)); + return result; + }).catch(err => { + this.handleThunderError(thunderCall, err); + }); + } + + install(packageId, version, fileLocator) { + const thunderCall = "install"; + + return this.thunder.call( + this.callsign, thunderCall, + /* ThunderJS treats "version" as its own parameter. + To forward a version to the remote function, use "versionAsParameter". */ + { packageId, "versionAsParameter": version, fileLocator } + ).then(result => { + this.LOG(thunderCall, " result:", JSON.stringify(result)); + return result; + }).catch(err => { + this.handleThunderError(thunderCall, err); + }); + } + + packageState(packageId, version) { + const thunderCall = "packageState"; + + return this.thunder.call( + this.callsign, thunderCall, + /* ThunderJS treats "version" as its own parameter. + To forward a version to the remote function, use "versionAsParameter". */ + { packageId, "versionAsParameter": version } + ).then(result => { + this.LOG(thunderCall, " result:", JSON.stringify(result)); + return result; + }).catch(err => { + this.handleThunderError(thunderCall, err); + }); + } } diff --git a/accelerator-home-ui/src/api/ThunderError.js b/accelerator-home-ui/src/api/ThunderError.js new file mode 100644 index 0000000..e64ed55 --- /dev/null +++ b/accelerator-home-ui/src/api/ThunderError.js @@ -0,0 +1,29 @@ +/** + * If not stated otherwise in this file or this component's LICENSE + * file the following copyright and licenses apply: + * + * Copyright 2026 RDK Management + * + * 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. + **/ + +export class ThunderError extends Error { + constructor(thunderCall, thunderErr) { + super( + thunderCall + ": " + (thunderErr?.message ?? thunderErr?.code ?? "Unknown"), + { cause: thunderErr } + ); + + this.name = this.constructor.name; + } +} diff --git a/accelerator-home-ui/src/items/AppCatalogItem.js b/accelerator-home-ui/src/items/AppCatalogItem.js index 269941e..fa022cc 100644 --- a/accelerator-home-ui/src/items/AppCatalogItem.js +++ b/accelerator-home-ui/src/items/AppCatalogItem.js @@ -20,8 +20,7 @@ import { Lightning, Utils, Language, Storage } from "@lightningjs/sdk"; import { CONFIG } from "../Config/Config"; import StatusProgress from '../overlays/StatusProgress' -import { installDACApp, getInstalledDACApps, fetchAppUrl } from '../api/DACApi' -import LISAApi from '../api/LISAApi'; +import { installDACApp, isDACAppInstalled } from '../api/DACApi' export default class AppCatalogItem extends Lightning.Component { constructor(...args) { @@ -107,7 +106,6 @@ export default class AppCatalogItem extends Lightning.Component { if (this._app.isInstalling) { this._app.isInstalled = success this._app.isInstalling = false - if (Object.prototype.hasOwnProperty.call(this._app, "handle")) delete this._app.handle; if (Object.prototype.hasOwnProperty.call(this._app, "errorCode")) delete this._app.errorCode; this.updateStatus() if (!success) { @@ -143,24 +141,6 @@ export default class AppCatalogItem extends Lightning.Component { } } async myfireINSTALL() { - if ((Object.prototype.hasOwnProperty.call(this._app, "handle") && (this._app.handle.length)) - || (Object.prototype.hasOwnProperty.call(this._app, "errorCode") && (this._app.errorCode))) { - let result = null - if (this._app.handle) { - result = await LISAApi.get().getProgress(this._app.handle) - this.tag("OverlayText").text.text = Language.translate("Please wait"); - this.tag("Overlay").alpha = 0.7 - this.tag("OverlayText").alpha = 1 - this.tag("Overlay").setSmooth('alpha', 0, { duration: 5 }) - } - if ((result && result.code) || this._app.errorCode) { - this.tag("OverlayText").text.text = Language.translate("Status") + ':' + ((result && result.code) ? result.code : this._app.errorCode); - this.tag("Overlay").alpha = 0.7 - this.tag("OverlayText").alpha = 1 - this.tag("Overlay").setSmooth('alpha', 0, { duration: 5 }) - } - return - } if (this._app.isInstalled) { this.LOG("App is already installed") this.tag("Overlay").alpha = 0.7 @@ -168,9 +148,24 @@ export default class AppCatalogItem extends Lightning.Component { this.tag("OverlayText").text.text = Language.translate('Already installed') + "!"; this.tag("Overlay").setSmooth('alpha', 0, { duration: 5 }) return + } else if (this._app.isInstalling) { + console.log(`App installation is in progress`); + return; + } + + this.tag("OverlayText").text.text = Language.translate("Please wait"); + this.tag("Overlay").alpha = 0.7; + this.tag("OverlayText").alpha = 1; + this.tag("Overlay").setSmooth('alpha', 0, { duration: 5 }); + + this._app.isInstalling = true; + if (!await installDACApp(this._app, this.tag('StatusProgress'))) { + this._app.isInstalling = false; + this.tag("OverlayText").text.text = Language.translate("Status") + ':' + (this._app.errorCode ?? -1); + this.tag("Overlay").alpha = 0.7 + this.tag("OverlayText").alpha = 1 + this.tag("Overlay").setSmooth('alpha', 0, { duration: 5 }) } - this._app.isInstalling = await installDACApp(this._app, this.tag('StatusProgress')) - this.updateStatus() } _init() { @@ -199,26 +194,10 @@ export default class AppCatalogItem extends Lightning.Component { this._app.name = this.data.name this._app.version = this.data.version this._app.type = this.data.type - if (Storage.get("CloudAppStore")) { - this._app.description = this.data.description - this._app.size = this.data.size - this._app.category = this.data.category - this._app.url = await fetchAppUrl(this._app.id, this._app.version) - this.LOG("fetchAppUrl: " + JSON.stringify(this._app.url)) - } - else { - this._app.url = this.data.uri - } - let installedApps = await getInstalledDACApps() - this._app.isInstalled = installedApps.find((a) => { - return a.id === this._app.id && a.installed && a.installed.length > 0 - }) - if (this._app.isInstalled === undefined) - this._app.isInstalled = false - if (this._app.url !== undefined) { - this.myfireINSTALL() - } - else - this.ERR("App url undefined") + this._app.description = this.data.description; + this._app.size = this.data.size; + this._app.category = this.data.category; + this._app.isInstalled = await isDACAppInstalled(this._app); + this.myfireINSTALL(); } } diff --git a/accelerator-home-ui/src/items/AppStoreItem.js b/accelerator-home-ui/src/items/AppStoreItem.js index 43d6d8b..d1b40b6 100644 --- a/accelerator-home-ui/src/items/AppStoreItem.js +++ b/accelerator-home-ui/src/items/AppStoreItem.js @@ -1,7 +1,7 @@ import { Lightning, Utils, Language, Storage } from "@lightningjs/sdk"; import { CONFIG } from "../Config/Config"; import { ProgressBar } from '@lightningjs/ui-components' -import { startDACApp, fetchAppIcon, fetchLocalAppIcon } from '../api/DACApi' +import { startDACApp } from '../api/DACApi' export default class AppStoreItem extends Lightning.Component { static _template() { @@ -91,22 +91,6 @@ export default class AppStoreItem extends Lightning.Component { this._app.isInstalling = false this._app.isUnInstalling = false this._buttonIndex = 0; - if (Storage.get("CloudAppStore")) { - if (this.data.installed && this.data.installed.length > 0 && this.data.installed[0].version) { - let icon = await fetchAppIcon(this.data.id, this.data.installed[0].version) - this.tag('Image').patch({ - src: icon, - }); - } - } - else { - let icon = await fetchLocalAppIcon(this.data.id) - if (icon !== undefined) { - this.tag('Image').patch({ - src: Utils.asset(icon), - }); - } - } } _focus() { diff --git a/accelerator-home-ui/src/items/ManageAppItem.js b/accelerator-home-ui/src/items/ManageAppItem.js index 7d34252..78484fd 100644 --- a/accelerator-home-ui/src/items/ManageAppItem.js +++ b/accelerator-home-ui/src/items/ManageAppItem.js @@ -20,7 +20,7 @@ import { Lightning, Utils, Storage, Language } from "@lightningjs/sdk"; import { CONFIG } from "../Config/Config"; import StatusProgress from '../overlays/StatusProgress' -import { uninstallDACApp, fetchAppIcon, fetchLocalAppIcon } from '../api/DACApi' +import { uninstallDACApp } from '../api/DACApi' export default class ManageAppItem extends Lightning.Component { static _template() { @@ -122,9 +122,14 @@ export default class ManageAppItem extends Lightning.Component { } async myfireUNINSTALL() { - this._app.isUnInstalling = await uninstallDACApp(this._app, this.tag('StatusProgress')) - if (!this._app.isUnInstalling && ("errorCode" in this._app)) { - this.tag("OverlayText").text.text = Language.translate("Status") + ':' + this._app.errorCode; + if (this._app.isUnInstalling || !this._app.isInstalled) { + console.log(`App uninstallation is in progress`); + return; + } + this._app.isUnInstalling = true; + if (!await uninstallDACApp(this._app, this.tag('StatusProgress'))) { + this._app.isUnInstalling = false; + this.tag("OverlayText").text.text = Language.translate("Status") + ':' + (this._app.errorCode ?? -1); this.tag("Overlay").alpha = 0.7 this.tag("OverlayText").alpha = 1 this.tag("Overlay").setSmooth('alpha', 0, { duration: 5 }) @@ -134,24 +139,11 @@ export default class ManageAppItem extends Lightning.Component { async _init() { this._app = {} this._app.isRunning = false - this._app.isInstalled = false + // this component is used on the "Manage Apps" screen, which only lists installed apps + this._app.isInstalled = true; this._app.isInstalling = false this._app.isUnInstalling = false this._buttonIndex = 0; - if (Storage.get("CloudAppStore")) { - let icon = await fetchAppIcon(this.data.id, this.data.installed[0].version) - this.tag('Image').patch({ - src: icon, - }); - } - else { - let icon = await fetchLocalAppIcon(this.data.id) - if (icon !== undefined) { - this.tag('Image').patch({ - src: Utils.asset(icon), - }); - } - } } _focus() {