diff --git a/CHANGELOG.md b/CHANGELOG.md index de3f7df..6b35ff0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,58 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). +#### [6.0.3](https://github.com/rdkcentral/rdke-refui/compare/6.0.2...6.0.3) + +- Updated the version for refui boltpackage [`#175`](https://github.com/rdkcentral/rdke-refui/pull/175) +- Merge tag '6.0.2' into develop [`7d4582c`](https://github.com/rdkcentral/rdke-refui/commit/7d4582c752991e63442df51fecbaf152e355e4c5) + +#### [6.0.2](https://github.com/rdkcentral/rdke-refui/compare/6.0.1...6.0.2) + +> 7 April 2026 + +- RDKEAPPRT-683: RemoteControl plugin API align with MW APIv3.4.2 [`#171`](https://github.com/rdkcentral/rdke-refui/pull/171) +- RDKEAPPRT-683: Fix the RCU pairing trigger timers [`47df636`](https://github.com/rdkcentral/rdke-refui/commit/47df6367819c67c9bcab0a5ed99fa3007e976d99) +- RDKEAPPRT-683: fix the RCU re scanning logic leak. [`2610642`](https://github.com/rdkcentral/rdke-refui/commit/2610642a37e0d975cfb59ac9d5d4aa498ae4a115) +- /RDKEAPPRT-683: stop the scan triggers after RCU pairing [`34ee6be`](https://github.com/rdkcentral/rdke-refui/commit/34ee6bea00a1aba71dcbc2e969ef5a6d22f2e94c) + +#### [6.0.1](https://github.com/rdkcentral/rdke-refui/compare/6.0.0...6.0.1) + +> 31 March 2026 + +- Feature/rdkeapprt 680 :Update to new RDK logos [`#168`](https://github.com/rdkcentral/rdke-refui/pull/168) +- RDKEAPPRT-682 Changelog and package config version updates for 6.0.1 [`32bbba6`](https://github.com/rdkcentral/rdke-refui/commit/32bbba6f7cde97607cac613f80240a174073c6bf) +- RDKEAPPRT-680 :Update new logo [`83513d8`](https://github.com/rdkcentral/rdke-refui/commit/83513d8d0566d994927583ed7891c655660af97f) +- RDKEAPPRT-680:Update the new logo files [`c2b261f`](https://github.com/rdkcentral/rdke-refui/commit/c2b261ff1ea9e4e72a8750f77652d6f4261775ce) + +### [6.0.0](https://github.com/rdkcentral/rdke-refui/compare/5.0.24...6.0.0) + +> 31 March 2026 + +- To refresh the App store and main view when the authentication done [`#166`](https://github.com/rdkcentral/rdke-refui/pull/166) +- RDKEAPPRT-661 Add support for new authorization in App Catalog [`#165`](https://github.com/rdkcentral/rdke-refui/pull/165) +- RDKEAPPRT-621,606,648,644 fix for Language screen and powerstate issue [`#164`](https://github.com/rdkcentral/rdke-refui/pull/164) +- RDKEAPPRT-646 Create the Username, Password page and add it inside the settings route [`#163`](https://github.com/rdkcentral/rdke-refui/pull/163) +- RDKEAPPRT-645 Add default error overlay when user tries to enter VOD content [`#162`](https://github.com/rdkcentral/rdke-refui/pull/162) +- RDKEAPPRT-612 :Add dac store details via RFC [`#160`](https://github.com/rdkcentral/rdke-refui/pull/160) +- RDKEAPPRT-626,627,628,605,608,622 Fix error and uninstall overlay screen with bug fixes [`#161`](https://github.com/rdkcentral/rdke-refui/pull/161) +- RDKEAPPRT-601,602 App side loading and App side loading and closeApp() function [`#158`](https://github.com/rdkcentral/rdke-refui/pull/158) +- To merge the changes from Feature/app managers to develop [`#157`](https://github.com/rdkcentral/rdke-refui/pull/157) +- RDKEAPPRT-576 Improve focus management [`#155`](https://github.com/rdkcentral/rdke-refui/pull/155) +- RDKEAPPRT-341 App managers intake: abstractions, API migration and YouTube Launch Support [`#146`](https://github.com/rdkcentral/rdke-refui/pull/146) +- RDKEAPPRT-571 Create UI prototype with My Apps, Recommended Apps, VOD Rows and Appinfo (#156) [`eff1a86`](https://github.com/rdkcentral/rdke-refui/commit/eff1a864e55c28ed1c542e26f818d20f082a6dca) +- RDKEAPPRT-538 Add basic integration with the RDK Reference DAC 2.0 App Store (#152) [`82eda3d`](https://github.com/rdkcentral/rdke-refui/commit/82eda3d7b34b80be63bdf3d0cb39d6c3f7f1f681) +- Changes for the error and uninstall overlay screen then some bug fixes also part of this [`749f89d`](https://github.com/rdkcentral/rdke-refui/commit/749f89d0af61e7aab7d96cb70a392e4efb8f8020) + #### [5.0.24](https://github.com/rdkcentral/rdke-refui/compare/5.0.20...5.0.24) +> 12 February 2026 + +- RDKEAPPRT-575 [RDKUI] 5.0.24 Merge latest changes from develop to main, create release tag, and publish release. [`#154`](https://github.com/rdkcentral/rdke-refui/pull/154) - RDKEAPPRT-268 [RDK UI] Replace deprecated APIs for USB Media Device [`#153`](https://github.com/rdkcentral/rdke-refui/pull/153) - RDKEAPPRT-394 Adopt to New APIs for deprecated ones if not yet [`#151`](https://github.com/rdkcentral/rdke-refui/pull/151) - RDKEAPPRT-533-[RDK UI] Device is not discovered via DIAL after re-enabling Local Device Discovery option [`#150`](https://github.com/rdkcentral/rdke-refui/pull/150) - RDKEAPPRT-518 All SSIDs are not visible in FTI SSID selection screen [`#149`](https://github.com/rdkcentral/rdke-refui/pull/149) +- RDKEAPPRT-575 Changelog updates for 5.0.24 [`59d8252`](https://github.com/rdkcentral/rdke-refui/commit/59d8252ac9401f7882716fc5c055346653004996) - Merge tag '5.0.20' into develop [`4413c1c`](https://github.com/rdkcentral/rdke-refui/commit/4413c1cf7d14b54c8952299b291a44f311d7033e) #### [5.0.20](https://github.com/rdkcentral/rdke-refui/compare/5.0.17...5.0.20) diff --git a/accelerator-home-ui/settings.json b/accelerator-home-ui/settings.json index b73ffb3..90202c2 100644 --- a/accelerator-home-ui/settings.json +++ b/accelerator-home-ui/settings.json @@ -13,6 +13,6 @@ "log": true, "enableAppSuspended": true, "showVersion": false, - "version": "5.0.24" + "version": "6.0.4" } } diff --git a/accelerator-home-ui/src/App.js b/accelerator-home-ui/src/App.js index c86832c..9058ae4 100644 --- a/accelerator-home-ui/src/App.js +++ b/accelerator-home-ui/src/App.js @@ -35,6 +35,7 @@ import { import Keymap from './Config/Keymap'; import Menu from './views/Menu' import Failscreen from './screens/FailScreen'; +import FailAndOkScreen from './screens/FailAndOkScreen'; import { keyIntercept } from './keyIntercept/keyIntercept'; @@ -193,6 +194,9 @@ export default class App extends Router.App { Fail: { type: Failscreen, }, + FailOk: { + type: FailAndOkScreen, + }, Volume: { type: Volume }, @@ -251,8 +255,8 @@ export default class App extends Router.App { _captureKey(key) { this.LOG("Got keycode : " + JSON.stringify(key.keyCode)) this.LOG("powerState ===>" + JSON.stringify(GLOBALS.powerState)) - if (GLOBALS.powerState !== "ON") { - appApi.setPowerState("ON").then(res => { + if (GLOBALS.powerState !== PowerState.POWER_STATE_ON) { + appApi.setPowerState(PowerState.POWER_STATE_ON).then(res => { res ? this.LOG("successfully set the power state to ON from " + JSON.stringify(GLOBALS.powerState)) : this.LOG("Failure while turning ON the device") GLOBALS.powerState = PowerState.POWER_STATE_ON; this.LOG("powerState after ===>" + JSON.stringify(GLOBALS.powerState)) @@ -272,6 +276,12 @@ export default class App extends Router.App { if(GLOBALS.MiracastNotificationstatus && key.keyCode !== Keymap.Power && key.keyCode !== Keymap.Home ){ return false } else if ((key.keyCode == Keymap.Home || key.keyCode == Keymap.Escape) && !Router.isNavigating()) { + if (Router.getActiveHash().startsWith("splash")) { + if (Router.getActiveHash() !== "splash/language") { + Router.navigate("splash/language"); + } + return true; + } if (GLOBALS.topmostApp.includes("dac.native")) { this.jumpToRoute("apps"); } else if (GLOBALS.Miracastclientdevicedetails.state === "INITIATED" || GLOBALS.Miracastclientdevicedetails.state === "INPROGRESS ") { @@ -385,60 +395,24 @@ export default class App extends Router.App { // Remote power key and keyboard F1 key used for STANDBY and POWER_ON return this._powerKeyPressed() } else if (key.keyCode === Keymap.AudioVolumeMute && !Router.isNavigating()) { - if (GLOBALS.topmostApp === GLOBALS.selfClientName) { + if (GLOBALS.topmostApp === GLOBALS.selfclientAppName) { this.tag("Volume").onVolumeMute(); } else { this.LOG("muting on some app") - if (Router.getActiveHash() === "applauncher") { - this.LOG("muting on some app while route is app launcher") - RDKShellApis.moveToFront(GLOBALS.selfClientName) - RDKShellApis.setVisibility(GLOBALS.selfClientName, true) - this.tag("Volume").onVolumeMute(); - } else { - this.LOG("muting on some app while route is NOT app launcher") - RDKShellApis.moveToFront(GLOBALS.selfClientName) - RDKShellApis.setVisibility(GLOBALS.selfClientName, true) - Router.navigate("applauncher"); - this.tag("Volume").onVolumeMute(); - } } return true } else if (key.keyCode == Keymap.AudioVolumeUp && !Router.isNavigating()) { - if (GLOBALS.topmostApp === GLOBALS.selfClientName) { + if (GLOBALS.topmostApp === GLOBALS.selfclientAppName) { this.tag("Volume").onVolumeKeyUp(); } else { this.LOG("muting on some app") - if (Router.getActiveHash() === "applauncher") { - this.LOG("muting on some app while route is app launcher") - RDKShellApis.moveToFront(GLOBALS.selfClientName) - RDKShellApis.setVisibility(GLOBALS.selfClientName, true) - this.tag("Volume").onVolumeKeyUp(); - } else { - this.LOG("muting on some app while route is NOT app launcher") - RDKShellApis.moveToFront(GLOBALS.selfClientName) - RDKShellApis.setVisibility(GLOBALS.selfClientName, true) - Router.navigate("applauncher"); - this.tag("Volume").onVolumeKeyUp(); - } } return true } else if (key.keyCode == Keymap.AudioVolumeDown && !Router.isNavigating()) { - if (GLOBALS.topmostApp === GLOBALS.selfClientName) { + if (GLOBALS.topmostApp === GLOBALS.selfclientAppName) { this.tag("Volume").onVolumeKeyDown(); } else { this.LOG("muting on some app") - if (Router.getActiveHash() === "applauncher") { - this.LOG("muting on some app while route is app launcher") - RDKShellApis.moveToFront(GLOBALS.selfClientName) - RDKShellApis.setVisibility(GLOBALS.selfClientName, true) - this.tag("Volume").onVolumeKeyDown(); - } else { - this.LOG("muting on some app while route is NOT app launcher") - RDKShellApis.moveToFront(GLOBALS.selfClientName) - RDKShellApis.setVisibility(GLOBALS.selfClientName, true) - Router.navigate("applauncher"); - this.tag("Volume").onVolumeKeyDown(); - } } return true } else { @@ -514,7 +488,7 @@ export default class App extends Router.App { "Prime": "n:2" } this._getPowerStatebeforeReboot(); - this._registerFireboltListeners() + // this._registerFireboltListeners() Keyboard.provide('xrn:firebolt:capability:input:keyboard', new KeyboardUIProvider(this)) this.LOG("Keyboard provider registered") @@ -1672,6 +1646,10 @@ export default class App extends Router.App { } _PowerStateHandlingWhileReboot() { + if (this._oldPowerStateWhileReboot === PowerState.POWER_STATE_STANDBY) { + this.LOG("_PowerStateHandlingWhileReboot: oldPowerStateWhileReboot is STANDBY, setting it to ON"); + this._oldPowerStateWhileReboot = PowerState.POWER_STATE_ON; + } this.LOG("_PowerStateHandlingWhileReboot: this._oldPowerStateWhileReboot , " + JSON.stringify(this._oldPowerStateWhileReboot) + " this._powerStateWhileReboot, " + JSON.stringify(this._powerStateWhileReboot) + " "); if (this._oldPowerStateWhileReboot != this._powerStateWhileReboot) { this.LOG("_PowerStateHandlingWhileReboot: old power state is not equal to powerstate while reboot " + JSON.stringify(this._oldPowerStateWhileReboot) + " " + JSON.stringify(this._powerStateWhileReboot)); @@ -1704,32 +1682,32 @@ export default class App extends Router.App { this._getPowerStateWhileReboot(); }); } - _registerFireboltListeners() { - FireBoltApi.get().deviceinfo.gettype() - FireBoltApi.get().lifecycle.ready() - - FireBoltApi.get().lifecycle.registerEvent('foreground', value => { - this.LOG("FireBoltApi[foreground] value:" + JSON.stringify(value) + ", launchResidentApp with:" + JSON.stringify(GLOBALS.selfClientName)); - // Ripple launches refui with this rdkshell client name. - GLOBALS.topmostApp = GLOBALS.selfClientName; - FireBoltApi.get().discovery.launch("refui", { - "action": "home", - "context": { - "source": "device" - } - }).then(() => { - AlexaApi.get().reportApplicationState("menu", true); - }) - }) - FireBoltApi.get().lifecycle.registerEvent('background', value => { - // Ripple changed app states; it will be a 'FireboltApp' - GLOBALS.topmostApp = "FireboltApp"; - this.LOG("FireBoltApi[foreground] value:" + JSON.stringify(value) + ", Updating top app as:" + JSON.stringify(GLOBALS.topmostApp)); - }) - FireBoltApi.get().lifecycle.state().then(res => { - this.LOG("Lifecycle.state result:" + JSON.stringify(res)) - }); - } + // _registerFireboltListeners() { + // FireBoltApi.get().deviceinfo.gettype() + // FireBoltApi.get().lifecycle.ready() + + // FireBoltApi.get().lifecycle.registerEvent('foreground', value => { + // this.LOG("FireBoltApi[foreground] value:" + JSON.stringify(value) + ", launchResidentApp with:" + JSON.stringify(GLOBALS.selfClientName)); + // // Ripple launches refui with this rdkshell client name. + // GLOBALS.topmostApp = GLOBALS.selfClientName; + // FireBoltApi.get().discovery.launch("refui", { + // "action": "home", + // "context": { + // "source": "device" + // } + // }).then(() => { + // AlexaApi.get().reportApplicationState("menu", true); + // }) + // }) + // FireBoltApi.get().lifecycle.registerEvent('background', value => { + // // Ripple changed app states; it will be a 'FireboltApp' + // GLOBALS.topmostApp = "FireboltApp"; + // this.LOG("FireBoltApi[foreground] value:" + JSON.stringify(value) + ", Updating top app as:" + JSON.stringify(GLOBALS.topmostApp)); + // }) + // FireBoltApi.get().lifecycle.state().then(res => { + // this.LOG("Lifecycle.state result:" + JSON.stringify(res)) + // }); + // } _firstEnable() { this.LOG("App Calling listenToVoiceControl method to activate VoiceControl Plugin") @@ -2124,7 +2102,7 @@ export default class App extends Router.App { appApi.getPowerState().then(res => { GLOBALS.powerState = res ? res.currentState : notification.newState }).catch(e => GLOBALS.powerState = notification.newState) - if (notification.newState !== "ON" && notification.currentState === "ON") { + if (notification.newState !== PowerState.POWER_STATE_ON && notification.currentState === PowerState.POWER_STATE_ON) { this.LOG("onPowerModeChanged Notification: power state was changed from ON to " + JSON.stringify(notification.newState)) //TURNING OFF THE DEVICE @@ -2134,7 +2112,15 @@ export default class App extends Router.App { appApi.exitApp(currentApp); //will suspend/destroy the app depending on the setting. } Router.navigate('menu'); - } else if (notification.newState === "ON" && notification.currentState !== "ON") { + } + else if(notification.newState === PowerState.POWER_STATE_LIGHT_SLEEP && notification.currentState === PowerState.POWER_STATE_DEEP_SLEEP){ + appApi.setPowerState(PowerState.POWER_STATE_ON).then(res => { + this.LOG("Device woke up from DEEP_SLEEP to LIGHT_SLEEP . setPowerState result: " + JSON.stringify(res)) + }).catch(err => { + this.ERR("Failed to set power state to ON when device woke up from DEEP_SLEEP to LIGHT_SLEEP. Error: " + JSON.stringify(err)) + }) + } + else if (notification.newState === PowerState.POWER_STATE_ON && notification.currentState !== PowerState.POWER_STATE_ON) { //TURNING ON THE DEVICE Storage.remove(SLEEP_STATE) } @@ -2852,4 +2838,4 @@ export default class App extends Router.App { } } } -} \ No newline at end of file +} diff --git a/accelerator-home-ui/src/AppController.js b/accelerator-home-ui/src/AppController.js index 4ae3d35..5a4a564 100644 --- a/accelerator-home-ui/src/AppController.js +++ b/accelerator-home-ui/src/AppController.js @@ -175,25 +175,15 @@ export default class AppController { async addKeyIntercepts(appId, clientId) { if (appId === "com.rdkcentral.youtube") { try { + const intercepts = [ + { "keyCode": Keymap.AudioVolumeMute, "modifiers": [] }, + { "keyCode": Keymap.AudioVolumeDown, "modifiers": [] }, + { "keyCode": Keymap.AudioVolumeUp, "modifiers": [] }, + { "keyCode": Keymap.Youtube, "modifiers": [] } + ]; await RDKWindowManager.get().addKeyIntercepts({ - "intercepts": { - "intercepts": [{ - "keys": [{ - "keyCode": Keymap.AudioVolumeMute, - "modifiers": [] - }, { - "keyCode": Keymap.AudioVolumeDown, - "modifiers": [] - }, { - "keyCode": Keymap.AudioVolumeUp, - "modifiers": [] - }, { - "keyCode": Keymap.Youtube, - "modifiers": [] - }], - "client": clientId - }] - } + "clientId": clientId, + "intercepts": JSON.stringify(intercepts) }); } catch (err) { throw new ThunderError("RDKWindowManager.addKeyIntercepts()", err); diff --git a/accelerator-home-ui/src/api/AppApi.js b/accelerator-home-ui/src/api/AppApi.js index 4b2bb9b..c1d5f45 100644 --- a/accelerator-home-ui/src/api/AppApi.js +++ b/accelerator-home-ui/src/api/AppApi.js @@ -1607,7 +1607,7 @@ export default class AppApi { getRFCConfig(rfcParamsList) { return new Promise((resolve, reject) => { - thunder.call('org.rdk.System', 'getRFCConfig', rfcParamsList).then(result => { + thunder.call('org.rdk.System', 'getRFCConfig',{"rfcList":[rfcParamsList]}).then(result => { if (result.success) { resolve(result) } else { diff --git a/accelerator-home-ui/src/api/AppCatalog.js b/accelerator-home-ui/src/api/AppCatalog.js new file mode 100644 index 0000000..0a65a5d --- /dev/null +++ b/accelerator-home-ui/src/api/AppCatalog.js @@ -0,0 +1,449 @@ +/** + * 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 PackageManager from './PackageManagerApi'; +import AppApi from './AppApi'; + +const APP_DEFAULT_ARCH = "arm"; +const APP_STORE_RFC_KEY = "Device.DeviceInfo.X_RDKCENTRAL-COM_RFC.DAC.ConfigURL"; + +const debug = false; + +let appCatalogHandler = null; + +export const eventTarget = new EventTarget(); + +export class AuthNeeded extends Event { + static eventName = 'authNeeded'; + + constructor() { + super(AuthNeeded.eventName); + } +} + +export class RefreshNeeded extends Event { + static eventName = 'refreshNeeded'; + + constructor() { + super(RefreshNeeded.eventName); + } +} + +class AuthExpiredError extends Error { + constructor(url) { + super(`Authentication expired (${url})`); + this.name = 'AuthExpiredError'; + } +} + +class PromiseQueue { + constructor() { + this.next = Promise.resolve(); + } + + enqueue(fn) { + let unlock; + const next = new Promise(resolve => { unlock = resolve; }); + const result = this.next.then(fn); + result.then(unlock, unlock); + this.next = next; + return result; + } +} + +async function getConfigUrlFromRFC() { + try { + const appApi = new AppApi(); + console.log("Resolving config URL from RFC "); + const result = await appApi.getRFCConfig(APP_STORE_RFC_KEY); + const rfcUrl = result?.RFCConfig?.[APP_STORE_RFC_KEY]; + if (typeof rfcUrl === "string" && rfcUrl.trim().length > 0) { + const resolvedConfigUrl = rfcUrl.trim(); + console.log("Resolved config URL from RFC "); + return resolvedConfigUrl; + } + console.warn("Config RFC URL empty or invalid"); + return null; + } catch (err) { + console.error("Failed to get config URL from RFC", err); + return null; + } +} + +async function getConfigUrlFromPackageManager() { + try { + console.log("Resolving config URL from PackageManager..."); + const config = await PackageManager.get().configuration(); + console.log("Resolved server config URL from PackageManager: "); + if (typeof config?.configUrl !== "string" || config.configUrl.trim().length === 0) { + throw new Error("Invalid config: " + JSON.stringify(config)); + } + + return config.configUrl.trim(); + } catch (err) { + console.error("Failed to resolve config URL from PackageManager", err); + throw err; + } +} + +let serverURL = null; +let serverURLPromise = null; + +async function getServerURL() { + if (!serverURL) { + if (!serverURLPromise) { + async function resolve() { + let url = await getConfigUrlFromRFC(); + + if (!url) { + url = await getConfigUrlFromPackageManager(); + } + console.log(`Server URL: ${url}`); + return url; + } + + serverURLPromise = resolve(); + } + + try { + serverURL = await serverURLPromise; + } catch (err) { + serverURLPromise = null; + throw err; + } + } + + return serverURL; +} + +class StubAppCatalogHandler { + getApps(offset, limit) { + return { applications: [] }; + } + + getAppDetails(id, version) { + throw new Error('getAppDetails() is not supported'); + } + + makeDownloadURL(url) { + throw new Error('makeDownloadURL() is not supported'); + } +} + +class LegacyAppCatalogHandler { + constructor(configURL) { + this.configURL = configURL; + this.storeConfig = null; + } + + async getStoreConfig() { + if (!this.storeConfig) { + let resolvedConfigUrl = this.configURL; + + const fetchResponse = await fetch(resolvedConfigUrl); + 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)); + } + const catalog = responseObject["appstore-catalog"]; + this.storeConfig = { url: catalog.url }; + + if (typeof catalog?.authentication?.user === "string" && + typeof catalog?.authentication?.password === "string") { + this.storeConfig.authHeader = + "Basic " + btoa(catalog.authentication.user + ':' + catalog.authentication.password); + } + } + + return this.storeConfig; + } + + async fetchStoreObject(request) { + let config = await this.getStoreConfig(); + let headers = new Headers(); + + if (config.authHeader) { + headers.append("Authorization", config.authHeader); + } + + let requestOptions = { + method: 'GET', + headers, + redirect: 'follow', + }; + + const fetchResponse = await fetch(config.url + request, requestOptions); + if (!fetchResponse.ok) { + throw new Error(`Unexpected response: ${fetchResponse.status}: ${fetchResponse.statusText}`); + } + + return fetchResponse.json(); + } + + getAppDetails(id, version) { + return this.fetchStoreObject("/apps/" + id + ":" + version + "?arch=" + APP_DEFAULT_ARCH); + } + + async getApps(offset, limit) { + const request = "/apps?arch=" + APP_DEFAULT_ARCH + "&offset=" + offset + "&limit=" + limit; + + try { + return await this.fetchStoreObject(request); + } catch (err) { + console.error(`fetchStoreObject(${request}) ${err}`); + throw err; + } + } + + makeDownloadURL(url) { + return url; + } +} + +class AppCatalogHandler { + constructor(serverURL) { + this.serverURL = serverURL; + this.authURL = this.serverURL + '/auth'; + this.appCatalogURL = this.serverURL + '/appcatalog'; + this.timerId = null; + this.queue = new PromiseQueue(); + } + + async fetch(url, options) { + const response = await this.queue.enqueue(() => fetch(url, { credentials: 'include', ...options })); + if (response.status === 401 || response.status === 403) { + this.cancelRefresh(); + throw new AuthExpiredError(url); + } + if (!response.ok) { + throw new Error(`Unexpected response: ${response.status}: ${response.statusText} (${url})`); + } + return response; + } + + async postAuthObject(request, obj) { + let options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + redirect: 'follow', + body: JSON.stringify(obj), + }; + + const url = this.authURL + request; + const response = await this.fetch(url, options); + const responseObj = await response.json(); + if (debug) { + console.log(`Auth response: ${JSON.stringify(responseObj, null, 2)}`); + } + return responseObj; + } + + calculateTimeout(expiresIn) { + const MIN = 60; + const MAX = 4 * 60 * 60; // 4 hours + + let timeout = Math.min(Math.max(expiresIn / 2, MIN), MAX); + + if (debug) { + console.log(`Calculated timeout: ${timeout}`); + } + + return timeout * 1000; + } + + scheduleRefresh(response, onAuthExpired) { + if (typeof response.expiresIn !== "number") { + return; + } + const expiresIn = response.expiresIn; + + this.cancelRefresh(); + + this.timerId = setTimeout(async () => { + this.timerId = null; + try { + const refreshResponse = await this.refresh(); + this.scheduleRefresh(refreshResponse, onAuthExpired); + } catch (err) { + console.warn(`Refresh failed: ${err}`); + onAuthExpired(); + } + }, this.calculateTimeout(expiresIn)); + } + + cancelRefresh() { + if (this.timerId) { + clearTimeout(this.timerId); + this.timerId = null; + } + } + + async refresh() { + return this.postAuthObject('/refresh', {}); + } + + async login(user, pass) { + this.cancelRefresh(); + return this.postAuthObject('/login', { username: user, password: pass }); + } + + async fetchAppCatalogObject(request) { + const url = this.appCatalogURL + request; + const fetchResponse = await this.fetch(url, { method: 'GET', redirect: 'follow' }); + const result = await fetchResponse.json(); + if (debug) { + console.log(`${url} : ${JSON.stringify(result, null, 2)}`); + } + return result; + } + + async getAppDetails(id, version) { + return this.fetchAppCatalogObject("/apps/" + id + ":" + version + "?arch=" + APP_DEFAULT_ARCH); + } + + async getApps(offset, limit) { + const request = "/apps?arch=" + APP_DEFAULT_ARCH + "&offset=" + offset + "&limit=" + limit; + + try { + return await this.fetchAppCatalogObject(request); + } catch (err) { + console.error(`fetchAppCatalogObject(${request}) ${err}`); + throw err; + } + } + + async getServiceToken(token) { + const url = this.authURL + "/servicetokens/" + token; + const response = await this.fetch(url, { method: 'GET', redirect: 'follow' }); + const result = await response.json(); + if (debug) { + console.log(`${url}: ${JSON.stringify(result, null, 2)}`); + } + return result; + } + + async makeDownloadURL(url) { + const token = await this.getServiceToken('download-manager'); + const downloadURL = new URL(url); + downloadURL.searchParams.set('token', token.token); + return downloadURL.toString(); + } +} + +let initPromise = null; + +function handleAuthExpired(handler) { + if (appCatalogHandler === handler) { + appCatalogHandler = new StubAppCatalogHandler(); + eventTarget.dispatchEvent(new AuthNeeded()); + } +} + +async function callAndHandleAuthExpired(handler, fn) { + try { + return await fn(); + } catch (err) { + if (err instanceof AuthExpiredError) { + handleAuthExpired(handler); + } + throw err; + } +} + +async function initAppCatalogHandler() { + if (!initPromise) { + async function init() { + appCatalogHandler = new StubAppCatalogHandler(); + + try { + const url = await getServerURL(); + + if (url.endsWith('/cpe.json')) { + appCatalogHandler = new LegacyAppCatalogHandler(url); + return; + } + + const handler = new AppCatalogHandler(url); + const refreshResponse = await handler.refresh(); + handler.scheduleRefresh(refreshResponse, () => handleAuthExpired(handler)); + appCatalogHandler = handler; + } catch (err) { + console.log(`initAppCatalogHandler() ${err}`); + if (err instanceof AuthExpiredError) { + eventTarget.dispatchEvent(new AuthNeeded()); + } else { + throw err; + } + } + } + + initPromise = init().catch(() => { + initPromise = null; + }); + } + return initPromise; +} + +export async function isLoggedIn() { + await initAppCatalogHandler(); + return appCatalogHandler instanceof AppCatalogHandler; +} + +export async function login(user, pass) { + await initAppCatalogHandler(); + + const handler = appCatalogHandler instanceof AppCatalogHandler + ? appCatalogHandler + : new AppCatalogHandler(await getServerURL()); + + try { + const loginResponse = await handler.login(user, pass); + appCatalogHandler = handler; + handler.scheduleRefresh(loginResponse, () => handleAuthExpired(handler)); + eventTarget.dispatchEvent(new RefreshNeeded()); + return true; + } catch (err) { + console.warn(`Login failed: ${err}`); + return false; + } +} + +export async function getApps(offset, limit) { + await initAppCatalogHandler(); + const handler = appCatalogHandler; + return callAndHandleAuthExpired(handler, () => handler.getApps(offset, limit)); +} + +export async function getAppDetails(id, version) { + await initAppCatalogHandler(); + const handler = appCatalogHandler; + return callAndHandleAuthExpired(handler, () => handler.getAppDetails(id, version)); +} + +export async function makeDownloadURL(url) { + await initAppCatalogHandler(); + const handler = appCatalogHandler; + return callAndHandleAuthExpired(handler, () => handler.makeDownloadURL(url)); +} diff --git a/accelerator-home-ui/src/api/DACApi.js b/accelerator-home-ui/src/api/DACApi.js index d03afbf..79766e0 100644 --- a/accelerator-home-ui/src/api/DACApi.js +++ b/accelerator-home-ui/src/api/DACApi.js @@ -24,6 +24,7 @@ import AppController from '../AppController'; import { ThunderError } from './ThunderError'; import { Metrics } from '@firebolt-js/sdk' import { SIDELOADED_APP_DEFAULT_ICON, deriveNameFromPackageId } from '../helpers/DACAppPresentation' +import { getApps, getAppDetails, makeDownloadURL } from './AppCatalog'; // 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 @@ -36,8 +37,6 @@ const APPS_REQUESTS_MAX = 5; const APP_DETAILS_KEY = "refui.details"; -const APP_DEFAULT_ARCH = "arm"; - function makeLogMessage(call, err) { return err.toString() + " <=> " + call; } @@ -69,73 +68,23 @@ class OperationLock { }; 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)); - } - 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"]; - } - - return storeConfig; -} - -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) - ); - } - - let requestOptions = { - method: 'GET', - headers, - redirect: 'follow', - }; - - const fetchResponse = await fetch(config.url + request, requestOptions); - if (!fetchResponse.ok) { - throw new Error(`Unexpected response: ${fetchResponse.status}: ${fetchResponse.statusText}`); - } - - return fetchResponse.json(); -} 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); + const appsResponse = await getApps(offset, APPS_REQUEST_LIMIT); if (!Array.isArray(appsResponse?.applications)) { break; } result = result.concat(appsResponse.applications); - if (result.length >= appsResponse?.meta?.resultSet?.total ?? 0) { + 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; } @@ -144,10 +93,6 @@ export async function getAppCatalogInfo() { return result; } -function getAppDetails(id, version) { - return fetchStoreObject("/apps/" + id + ":" + version + "?arch=" + APP_DEFAULT_ARCH); -} - function retrieveURLAndSize(details) { if (typeof details?.header?.url === "string") { const url = details.header.url; @@ -181,7 +126,8 @@ async function downloadAndInstall(pkg, downloadedSize, totalSize, progress) { const downloadId = await new Promise(async (resolve, reject) => { try { - await DownloadManager.get().download(pkg.url, (downloadId, percent, failReason) => { + const downloadURL = await makeDownloadURL(pkg.url); + await DownloadManager.get().download(downloadURL, (downloadId, percent, failReason) => { if (!failReason) { if (percent !== 100) { progress((downloadedSize + pkg.size * percent / 100) / totalSize, "Downloading"); diff --git a/accelerator-home-ui/src/api/RemoteControl.js b/accelerator-home-ui/src/api/RemoteControl.js index 1caf9f1..0cb6978 100644 --- a/accelerator-home-ui/src/api/RemoteControl.js +++ b/accelerator-home-ui/src/api/RemoteControl.js @@ -92,21 +92,33 @@ export default class RCApi { }) } - startPairing(timeout = 30, netType ) { + startPairing(timeout = 30) { return new Promise((resolve, reject) => { - //this.INFO("RCApi: startPairing netType " + netType + " timeout " + timeout); - this.thunder.call('org.rdk.RemoteControl', 'startPairing', { netType: netType, timeout: timeout }).then(result => { - //this.INFO("RCApi: startPairing result: ", JSON.stringify(result)) + this.thunder.call('org.rdk.RemoteControl', 'startPairing', { timeout: timeout, screenBindEnable: false }).then(result => { + this.INFO("RCApi: startPairing result: " + JSON.stringify(result)) resolve(result.success); }).catch(err => { this.ERR("RCApi: startPairing error: " + JSON.stringify(err)); Metrics.error(Metrics.ErrorType.OTHER,"RemoteControlApiError", "Error in Thunder RemoteControl startPairing "+JSON.stringify(err), false, null) reject(err); }); - resolve(true); }) } + stopPairing() { + return new Promise((resolve, reject) => { + this.INFO("RCApi: stopPairing"); + this.thunder.call('org.rdk.RemoteControl', 'stopPairing', {scanDisable: true}).then(result => { + this.INFO("RCApi: stopPairing result: " + JSON.stringify(result)) + resolve(result.success); + }).catch(err => { + this.ERR("RCApi: stopPairing error: " + JSON.stringify(err)); + Metrics.error(Metrics.ErrorType.OTHER, "RemoteControlApiError", "Error in Thunder RemoteControl stopPairing " + JSON.stringify(err), false, null) + reject(err); + }); + }); + } + initializeIRDB() { return new Promise((resolve, reject) => { /*TODO: implement when requirement comes.*/ @@ -186,10 +198,10 @@ export default class RCApi { }) } - findMyRemote(netType = 1, level = "mid") { + findMyRemote(level = "mid") { return new Promise((resolve, reject) => { - this.INFO("RCApi: findMyRemote netType:" + JSON.stringify(netType) + " level:" + JSON.stringify(level)); - this.thunder.call('org.rdk.RemoteControl', 'findMyRemote', { netType: netType, level: level }).then(result => { + this.INFO("RCApi: findMyRemote level:" + JSON.stringify(level)); + this.thunder.call('org.rdk.RemoteControl', 'findMyRemote', { level: level }).then(result => { this.INFO("RCApi: findMyRemote result: " + JSON.stringify(result)) resolve(result.success); }).catch(err => { @@ -200,6 +212,8 @@ export default class RCApi { }) } + // This is to reset the remote control firmware; not to be confused with factory reset of the device. + // This will not erase user data or settings on the device. factoryReset() { return new Promise((resolve, reject) => { this.INFO("RCApi: factoryReset"); @@ -213,4 +227,18 @@ export default class RCApi { }); }) } + + unpair(macAddressList) { + return new Promise((resolve, reject) => { + this.INFO("RCApi: unpair macAddressList:" + JSON.stringify(macAddressList)); + this.thunder.call('org.rdk.RemoteControl', 'unpair', { macAddressList: macAddressList }).then(result => { + this.INFO("RCApi: unpair result: " + JSON.stringify(result)) + resolve(result.success); + }).catch(err => { + this.ERR("RCApi: unpair error: " + JSON.stringify(err)); + Metrics.error(Metrics.ErrorType.OTHER, "RemoteControlApiError", "Error in Thunder RemoteControl unpair " + JSON.stringify(err), false, null) + reject(err); + }); + }); + } } diff --git a/accelerator-home-ui/src/items/AppCard.js b/accelerator-home-ui/src/items/AppCard.js index 52d5582..a5badc6 100644 --- a/accelerator-home-ui/src/items/AppCard.js +++ b/accelerator-home-ui/src/items/AppCard.js @@ -64,17 +64,6 @@ class ActionButton extends Lightning.Component { this.tag('Label').text.text = text; } - set primary(isPrimary) { - this._isPrimary = isPrimary; - if (isPrimary) { - this.patch({ color: CONFIG.theme.hex }); - } - } - - get primary() { - return this._isPrimary || false; - } - set action(actionType) { this._action = actionType; } @@ -97,7 +86,7 @@ class ActionButton extends Lightning.Component { _unfocus() { this.tag('FocusIndicator').alpha = 0; this.patch({ - color: this._isPrimary ? CONFIG.theme.hex : 0xFF3D3D3D, + color: 0xFF3D3D3D, smooth: { scale: 1 } }); } @@ -193,7 +182,6 @@ export default class AppCard extends Lightning.Component { x: 0, type: ActionButton, label: Language.translate('Launch'), - primary: true, action: 'launch' }, UpdateButton: { diff --git a/accelerator-home-ui/src/items/AppCatalogItem.js b/accelerator-home-ui/src/items/AppCatalogItem.js index 5922135..af4fd2c 100644 --- a/accelerator-home-ui/src/items/AppCatalogItem.js +++ b/accelerator-home-ui/src/items/AppCatalogItem.js @@ -46,10 +46,12 @@ export const DACAppMixin = (Base) => class extends Base { if (this._app.isInstalling) { this._app.isInstalled = success this._app.isInstalling = false + const errorCode = this._app.errorCode ?? -1; if (Object.prototype.hasOwnProperty.call(this._app, "errorCode")) delete this._app.errorCode; this.updateDACStatus(statusProgressTag, overlayTag) if (!success) { this.tag(statusProgressTag).setProgress(1.0, 'Error: ' + msg) + this.fireAncestors('$showInstallError', { name: this._app.name, errorCode: errorCode }) } return true; // Installation operation completed } else if (this._app.isUnInstalling) { @@ -58,6 +60,7 @@ export const DACAppMixin = (Base) => class extends Base { this.updateDACStatus(statusProgressTag, overlayTag) if (!success) { this.tag(statusProgressTag).setProgress(1.0, 'Error: ' + msg) + this.fireAncestors('$showUninstallError', { name: this._app.name, error: msg }) } return true; // Uninstall operation completed } @@ -97,10 +100,12 @@ export const DACAppMixin = (Base) => class extends Base { } else { this.ERR("Failed to launch app: " + this._app.name) this.tag(overlayTag + '.OverlayText').text.text = Language.translate('Launch failed'); + this.fireAncestors('$showLaunchError', { name: this._app.name }); } } catch (err) { this.ERR("Error launching app: " + JSON.stringify(err)) this.tag(overlayTag + '.OverlayText').text.text = Language.translate('Launch failed'); + this.fireAncestors('$showLaunchError', { name: this._app.name, error: err.message || err }); } this.tag(overlayTag).setSmooth('alpha', 0, { duration: 5 }) return true; // Already installed @@ -114,13 +119,18 @@ export const DACAppMixin = (Base) => class extends Base { this.tag(overlayTag + '.OverlayText').alpha = 1; this.tag(overlayTag).setSmooth('alpha', 0, { duration: 5 }); + // Reset progress bar to clear any stale state from a previous install cycle + this.tag(statusProgressTag).reset(); + this._app.isInstalling = true; if (!await installDACApp(this._app, this.tag(statusProgressTag))) { this._app.isInstalling = false; - this.tag(overlayTag + '.OverlayText').text.text = Language.translate("Status") + ':' + (this._app.errorCode ?? -1); + const errorCode = this._app.errorCode ?? -1; + this.tag(overlayTag + '.OverlayText').text.text = Language.translate("Status") + ':' + errorCode; this.tag(overlayTag).alpha = 0.7 this.tag(overlayTag + '.OverlayText').alpha = 1 this.tag(overlayTag).setSmooth('alpha', 0, { duration: 5 }) + this.fireAncestors('$showInstallError', { name: this._app.name, errorCode: errorCode }); return false; } return true; diff --git a/accelerator-home-ui/src/items/DacAppItem.js b/accelerator-home-ui/src/items/DacAppItem.js index 006c604..28319c1 100644 --- a/accelerator-home-ui/src/items/DacAppItem.js +++ b/accelerator-home-ui/src/items/DacAppItem.js @@ -189,7 +189,14 @@ export default class DacAppItem extends DACAppMixin(Lightning.Component) { if (this._app.isInstalled) { this.LOG("App is already installed, launching...") // Launch the installed app - this._app.isRunning = await startDACApp(this._app); + try { + this._app.isRunning = await startDACApp(this._app); + if (!this._app.isRunning) { + this.fireAncestors('$showLaunchError', { name: this._app.name }); + } + } catch (err) { + this.fireAncestors('$showLaunchError', { name: this._app.name, error: err.message || err }); + } return } await this.performDACInstall('ImageWrapper.StatusProgress', 'ImageWrapper.Overlay'); @@ -244,6 +251,10 @@ export default class DacAppItem extends DACAppMixin(Lightning.Component) { async _handleEnter() { // Handle "More Apps" item - navigate to apps route if (this.data.applicationType === 'MoreApps') { + if (!GLOBALS.IsConnectedToInternet) { + this.fireAncestors('$showNetworkError') + return + } Router.navigate('apps'); return; } diff --git a/accelerator-home-ui/src/keyIntercept/keyIntercept.js b/accelerator-home-ui/src/keyIntercept/keyIntercept.js index d810cfb..f7ba8ba 100644 --- a/accelerator-home-ui/src/keyIntercept/keyIntercept.js +++ b/accelerator-home-ui/src/keyIntercept/keyIntercept.js @@ -26,29 +26,26 @@ const thunder = ThunderJS(CONFIG.thunderConfig); export function keyIntercept(clientName = GLOBALS.selfClientId) { return new Promise((resolve, reject) => { + const intercepts = [ + { "keyCode": Keymap.Home, "modifiers": [] }, + { "keyCode": Keymap.AudioVolumeDown, "modifiers": [] }, + { "keyCode": Keymap.AudioVolumeUp, "modifiers": [] }, + { "keyCode": Keymap.AudioVolumeMute, "modifiers": [] }, + { "keyCode": Keymap.Inputs_Shortcut, "modifiers": [] }, + { "keyCode": Keymap.Picture_Setting_Shortcut, "modifiers": [] }, + { "keyCode": Keymap.Youtube, "modifiers": [] }, + { "keyCode": Keymap.Power, "modifiers": [] }, + { "keyCode": Keymap.Amazon, "modifiers": [] }, + { "keyCode": Keymap.Netflix, "modifiers": [] }, + { "keyCode": Keymap.Settings_Shortcut, "modifiers": [] }, + { "keyCode": Keymap.Guide_Shortcut, "modifiers": [] }, + { "keyCode": Keymap.AppCarousel, "modifiers": [] }, + { "keyCode": Keymap.Escape, "modifiers": [] } + ]; RDKWindowManager.get().addKeyIntercepts( { - "intercepts":{ - "intercepts": [{ - "keys": [ - { "keyCode": Keymap.Home, "modifiers": [] }, - { "keyCode": Keymap.AudioVolumeDown, "modifiers": [] }, - { "keyCode": Keymap.AudioVolumeUp, "modifiers": [] }, - { "keyCode": Keymap.AudioVolumeMute, "modifiers": [] }, - { "keyCode": Keymap.Inputs_Shortcut, "modifiers": [] }, - { "keyCode": Keymap.Picture_Setting_Shortcut, "modifiers": [] }, - { "keyCode": Keymap.Youtube, "modifiers": [] }, - { "keyCode": Keymap.Power, "modifiers": [] }, - { "keyCode": Keymap.Amazon, "modifiers": [] }, - { "keyCode": Keymap.Netflix, "modifiers": [] }, - { "keyCode": Keymap.Settings_Shortcut, "modifiers": [] }, - { "keyCode": Keymap.Guide_Shortcut, "modifiers": [] }, - { "keyCode": Keymap.AppCarousel, "modifiers": [] }, - { "keyCode": Keymap.Escape, "modifiers": [] } - ], - "client": clientName, - }] - }, + "clientId": clientName, + "intercepts": JSON.stringify(intercepts) } ).then(result => { if (result.success) { diff --git a/accelerator-home-ui/src/overlays/OtherSettings/LanguageScreenOverlay.js b/accelerator-home-ui/src/overlays/OtherSettings/LanguageScreenOverlay.js index 19bcf30..b44ebf0 100644 --- a/accelerator-home-ui/src/overlays/OtherSettings/LanguageScreenOverlay.js +++ b/accelerator-home-ui/src/overlays/OtherSettings/LanguageScreenOverlay.js @@ -20,15 +20,10 @@ import { Language, Lightning } from '@lightningjs/sdk' import LanguageItem from '../../items/LanguageItem' import { availableLanguages, availableLanguageCodes } from '../../Config/Config' import AppApi from '../../api/AppApi'; -import RDKShellApis from '../../api/RDKShellApis'; -import thunderJS from 'ThunderJS'; -import { CONFIG, GLOBALS } from '../../Config/Config' -import { Metrics } from '@firebolt-js/sdk'; -import FireBoltApi from '../../api/firebolt/FireBoltApi'; + + const appApi = new AppApi() -const thunder = thunderJS(CONFIG.thunderConfig) -const loader = 'Loader' export default class LanguageScreen extends Lightning.Component { constructor(...args) { @@ -74,17 +69,6 @@ export default class LanguageScreen extends Lightning.Component { item: item, } }) - RDKShellApis.destroy(loader).catch(err => { - this.ERR("LanguageScreenOverlay: Error destroy loader: " + JSON.stringify(err)) - }); - RDKShellApis.setVisibility(GLOBALS.selfClientName, true); - RDKShellApis.moveToFront(GLOBALS.selfClientName); - RDKShellApis.setFocus(GLOBALS.selfClientName).then(() => { - this.LOG('LanguageScreenOverlay: ResidentApp moveToFront Success'); - }).catch(err => { - this.ERR('LanguageScreenOverlay: Error: ' + JSON.stringify(err)); - Metrics.error(Metrics.ErrorType.OTHER, "PluginError", "Thunder RDKShell Failed to moveToFront " + JSON.stringify(err), false, null) - }); } _focus() { @@ -109,20 +93,8 @@ export default class LanguageScreen extends Lightning.Component { //need to verify if (Language.get() !== availableLanguages[this._Languages.tag('List').index]) { let updatedLanguage = availableLanguageCodes[availableLanguages[this._Languages.tag('List').index]] - if ("ResidentApp" !== GLOBALS.selfClientName) { - FireBoltApi.get().localization.setlanguage(availableLanguages[this._Languages.tag('List').index]).then(res => this.LOG("language set successfully")) - } else { - appApi.setUILanguage(updatedLanguage) - } + appApi.setUILanguage(updatedLanguage) localStorage.setItem('Language',availableLanguages[this._Languages.tag('List').index]) - let path = location.pathname.split('index.html')[0] - let url = path.slice(-1) === '/' ? "static/loaderApp/index.html" : "/static/loaderApp/index.html" - let notification_url = location.origin + path + url - this.LOG(notification_url) - appApi.launchResident(notification_url, loader).catch(err => { - this.ERR("Error launchResident: " + JSON.stringify(err)) - }) - RDKShellApis.setVisibility(GLOBALS.selfClientName, false) location.reload(); } } diff --git a/accelerator-home-ui/src/overlays/StatusProgress.js b/accelerator-home-ui/src/overlays/StatusProgress.js index 532c27f..168e3f5 100644 --- a/accelerator-home-ui/src/overlays/StatusProgress.js +++ b/accelerator-home-ui/src/overlays/StatusProgress.js @@ -66,6 +66,7 @@ export default class Progress extends lng.Component { var ww = (this.w - 4) * pc if(pc != 1.0) { this.tag("BackgroundOverlay").alpha=0.6 + this.tag("Label").alpha = 1.0 this.tag("ProgressBar").setSmooth('alpha', 0.7, {duration: .1}) this.tag("Progress").setSmooth('w', ww, { duration: 1 }) } diff --git a/accelerator-home-ui/src/overlays/UninstallConfirmation.js b/accelerator-home-ui/src/overlays/UninstallConfirmation.js new file mode 100644 index 0000000..cc146d6 --- /dev/null +++ b/accelerator-home-ui/src/overlays/UninstallConfirmation.js @@ -0,0 +1,314 @@ +/** + * If not stated otherwise in this file or this component's LICENSE + * file the following copyright and licenses apply: + * + * Copyright 2020 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 { Lightning, Utils, Language } from "@lightningjs/sdk"; +import { CONFIG } from "../Config/Config"; + +/** + * Class for Uninstall Confirmation Overlay Component. + */ +export default class UninstallConfirmation extends Lightning.Component { + static _template() { + return { + rect: true, + w: 1920, + h: 1080, + color: 0xCC000000, // Semi-transparent black background + zIndex: 10, + UninstallDialog: { + x: 960, + y: 540, + mount: 0.5, + rect: true, + w: 620, + h: 320, + color: 0xFF1A1A1A, + shader: { + type: Lightning.shaders.RoundedRectangle, + radius: 16, + }, + DialogBorder: { + x: -2, + y: -2, + w: 624, + h: 324, + rect: true, + color: 0x00000000, + shader: { + type: Lightning.shaders.RoundedRectangle, + radius: 18, + stroke: 2, + strokeColor: 0xFF3D3D3D, + }, + }, + Title: { + x: 310, + y: 40, + mountX: 0.5, + text: { + text: Language.translate("Uninstall"), + fontFace: CONFIG.language.font, + fontSize: 36, + textColor: CONFIG.theme.hex, + fontStyle: "bold", + }, + }, + BorderTop: { + x: 30, + y: 90, + w: 560, + h: 2, + rect: true, + color: 0xFF3D3D3D, + }, + Info: { + x: 310, + y: 130, + mountX: 0.5, + text: { + text: Language.translate("Are you sure you want to uninstall this app?"), + fontFace: CONFIG.language.font, + fontSize: 24, + textColor: 0xFFCCCCCC, + textAlign: "center", + wordWrapWidth: 520, + }, + }, + AppName: { + x: 310, + y: 170, + mountX: 0.5, + text: { + text: "", + fontFace: CONFIG.language.font, + fontSize: 22, + textColor: 0xFFAAAAAA, + textAlign: "center", + wordWrapWidth: 520, + }, + }, + Buttons: { + x: 310, + y: 230, + mountX: 0.5, + w: 440, + h: 50, + Confirm: { + x: 0, + w: 200, + h: 50, + rect: true, + color: 0xFFFFFFFF, + shader: { + type: Lightning.shaders.RoundedRectangle, + radius: 8, + }, + Title: { + x: 100, + y: 25, + mount: 0.5, + text: { + text: Language.translate("Confirm"), + fontFace: CONFIG.language.font, + fontSize: 22, + textColor: 0xFF000000, + }, + }, + }, + Cancel: { + x: 220, + w: 200, + h: 50, + rect: true, + color: 0xFF7D7D7D, + shader: { + type: Lightning.shaders.RoundedRectangle, + radius: 8, + }, + Title: { + x: 100, + y: 25, + mount: 0.5, + text: { + text: Language.translate("Cancel"), + fontFace: CONFIG.language.font, + fontSize: 22, + textColor: 0xFF000000, + }, + }, + }, + }, + Loader: { + x: 310, + y: 160, + mountX: 0.5, + w: 90, + h: 90, + zIndex: 2, + src: Utils.asset("images/settings/Loading.png"), + visible: false, + }, + }, + }; + } + + /** + * Set the app info for the uninstall confirmation + * @param {Object} appInfo - App data (id, name, version, etc.) + */ + set appInfo(data) { + this._appInfo = data; + const appName = data.name || data.appName || "Unknown App"; + this.tag("UninstallDialog.AppName").text.text = `"${appName}"`; + } + + get appInfo() { + return this._appInfo; + } + + _focus() { + this._setState("Confirm"); + + this.loadingAnimation = this.tag("UninstallDialog.Loader").animation({ + duration: 3, + repeat: -1, + stopMethod: "immediate", + stopDelay: 0.2, + actions: [{ p: "rotation", v: { sm: 0, 0: 0, 1: 2 * Math.PI } }], + }); + } + + /** + * Safety-net cleanup when the overlay loses focus (e.g. parent hides or + * navigates away). Ensures the loader animation is stopped and the dialog + * UI is restored to its default state even if no explicit state transition + * (Uninstalling → Confirm) was triggered before dismissal. + */ + _unfocus() { + if (this.loadingAnimation && this.loadingAnimation.isActive()) { + this.loadingAnimation.stop(); + } + this.tag("UninstallDialog.Loader").visible = false; + this.tag("UninstallDialog.Title").text.text = Language.translate("Uninstall"); + this.tag("UninstallDialog.Buttons").visible = true; + this.tag("UninstallDialog.Info").visible = true; + this.tag("UninstallDialog.AppName").visible = true; + } + + static _states() { + return [ + class Confirm extends this { + $enter() { + this._focus(); + } + _handleEnter() { + this.fireAncestors("$confirmUninstall", this._appInfo); + } + _handleRight() { + this._setState("Cancel"); + } + _handleBack() { + this.fireAncestors("$cancelUninstall"); + } + _focus() { + this.tag("Buttons.Confirm").patch({ + color: CONFIG.theme.hex, + }); + this.tag("Buttons.Confirm.Title").patch({ + text: { textColor: 0xFFFFFFFF }, + }); + } + _unfocus() { + this.tag("Buttons.Confirm").patch({ + color: 0xFFFFFFFF, + }); + this.tag("Buttons.Confirm.Title").patch({ + text: { textColor: 0xFF000000 }, + }); + } + $exit() { + this._unfocus(); + } + }, + class Cancel extends this { + $enter() { + this._focus(); + } + _handleEnter() { + this.fireAncestors("$cancelUninstall"); + } + _handleLeft() { + this._setState("Confirm"); + } + _handleBack() { + this.fireAncestors("$cancelUninstall"); + } + _focus() { + this.tag("Buttons.Cancel").patch({ + color: CONFIG.theme.hex, + }); + this.tag("Buttons.Cancel.Title").patch({ + text: { textColor: 0xFFFFFFFF }, + }); + } + _unfocus() { + this.tag("Buttons.Cancel").patch({ + color: 0xFF7D7D7D, + }); + this.tag("Buttons.Cancel.Title").patch({ + text: { textColor: 0xFF000000 }, + }); + } + $exit() { + this._unfocus(); + } + }, + class Uninstalling extends this { + $enter() { + this.loadingAnimation.start(); + this.tag("UninstallDialog.Loader").visible = true; + this.tag("UninstallDialog.Title").text.text = Language.translate("Uninstalling") + "..."; + this.tag("UninstallDialog.Buttons").visible = false; + this.tag("UninstallDialog.Info").visible = false; + this.tag("UninstallDialog.AppName").visible = false; + } + $exit() { + this.loadingAnimation.stop(); + this.tag("UninstallDialog.Loader").visible = false; + this.tag("UninstallDialog.Title").text.text = Language.translate("Uninstall"); + this.tag("UninstallDialog.Buttons").visible = true; + this.tag("UninstallDialog.Info").visible = true; + this.tag("UninstallDialog.AppName").visible = true; + } + _handleEnter() { /* do nothing */ } + _handleLeft() { /* do nothing */ } + _handleRight() { /* do nothing */ } + _handleBack() { /* do nothing */ } + _handleUp() { /* do nothing */ } + _handleDown() { /* do nothing */ } + }, + ]; + } + + /** + * Show the uninstalling state with loader + */ + showUninstalling() { + this._setState("Uninstalling"); + } +} diff --git a/accelerator-home-ui/src/routes/networkRoutes.js b/accelerator-home-ui/src/routes/networkRoutes.js index 1f8e091..a262b47 100644 --- a/accelerator-home-ui/src/routes/networkRoutes.js +++ b/accelerator-home-ui/src/routes/networkRoutes.js @@ -26,6 +26,7 @@ import NetworkInterfaceScreen from "../screens/OtherSettingsScreens/NetworkInter import WifiPairingScreen from "../screens/WiFiPairingScreen" import WiFiScreen from "../screens/WifiScreen" import RCVolumeInfoScreen from '../screens/RcInformationScreen' +import AppCatalogLoginComponent from '../screens/AppCatalogLoginComponent' const networkRoutes = [ { @@ -72,6 +73,11 @@ const networkRoutes = [ path: 'settings/bluetooth/RCVolumeInfoScreen', component: RCVolumeInfoScreen, widgets: ["Menu",'Volume', "Fail","AppCarousel"] + }, + { + path: 'settings/appcataloglogin', + component: AppCatalogLoginComponent, + widgets: ['Volume', 'AppCarousel'] } ] diff --git a/accelerator-home-ui/src/routes/routes.js b/accelerator-home-ui/src/routes/routes.js index bafef6c..dd67e76 100644 --- a/accelerator-home-ui/src/routes/routes.js +++ b/accelerator-home-ui/src/routes/routes.js @@ -101,12 +101,12 @@ export default { { path: 'apps', component: AppStore, - widgets: ['Menu', 'Volume', "AppCarousel"] + widgets: ['Menu', 'FailOk', 'Volume', "AppCarousel"] }, { path: 'appinfo', component: AppInfoPage, - widgets: ['Menu', 'Volume', "AppCarousel"] + widgets: ['Menu', 'FailOk', 'Volume', "AppCarousel"] }, { path: 'usb/player', @@ -140,7 +140,7 @@ export default { } return Promise.resolve() }, - widgets: ['Menu', 'Fail', 'Volume','MiracastNotification', "AppCarousel", "VideoInfoChange"], + widgets: ['Menu', 'Fail', 'FailOk', 'Volume','MiracastNotification', "AppCarousel", "VideoInfoChange"], }, { path: 'tv-overlay/:type', @@ -207,7 +207,7 @@ export default { if ("ResidentApp" !== GLOBALS.selfClientName) { Metrics.page(request.hash) .then(success => { - console.log("successfully routed to page ==>", request.hash) + console.log("successfully routed to page ==>" + JSON.stringify(request.hash)) }) .catch(err => console.log("error in metrics.page", err)) } diff --git a/accelerator-home-ui/src/screens/AppCatalogLoginComponent.js b/accelerator-home-ui/src/screens/AppCatalogLoginComponent.js new file mode 100644 index 0000000..a6e1fa0 --- /dev/null +++ b/accelerator-home-ui/src/screens/AppCatalogLoginComponent.js @@ -0,0 +1,382 @@ +/** + * If not stated otherwise in this file or this component's LICENSE + * file the following copyright and licenses apply: + * + * Copyright 2020 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 { Language, Lightning, Router } from '@lightningjs/sdk' +import { CONFIG } from '../Config/Config'; +import { Keyboard } from '../ui-components/index' +import { KEYBOARD_FORMATS } from '../ui-components/components/Keyboard' +import PasswordSwitch from './PasswordSwitch'; +import { login } from '../api/AppCatalog'; + +export default class AppCatalogLoginComponent extends Lightning.Component { + + constructor(...args) { + super(...args); + this.INFO = console.info; + this.LOG = console.log; + this.ERR = console.error; + this.WARN = console.warn; + } + + pageTransition() { + return 'left' + } + + _active() { + this.hidePasswd = true + this.star = "" + this.tag("Keyboard").visible = false + } + + handleDone() { + this.tag("Keyboard").visible = false + if (!this.textCollection['EnterUsername']) { + this._setState("EnterUsername"); + } + else if (!this.textCollection['EnterPassword']) { + this._setState("EnterPassword"); + } + else { + this.LOG('App Catalog Login - credentials submitted') + login(this.textCollection['EnterUsername'], this.textCollection['EnterPassword']) + .then(result => { + if (result) { + this.LOG('Login successful - navigating back') + if (!Router.isNavigating()) { + Router.back() + } + } else { + this.ERR('Login failed') + } + }) + .catch(err => this.ERR('Login error: ' + err)) + } + } + + static _template() { + return { + Background: { + w: 1920, + h: 1080, + rect: true, + color: 0xCC000000, + }, + Text: { + x: 758, + y: 70, + text: { + text: Language.translate("Connect to the Application Catalog"), + fontFace: CONFIG.language.font, + fontSize: 35, + textColor: CONFIG.theme.hex, + }, + }, + BorderTop: { + x: 190, y: 130, w: 1488, h: 2, rect: true, + }, + Username: { + x: 190, + y: 176, + text: { + text: Language.translate("Username") + ": ", + fontFace: CONFIG.language.font, + fontSize: 25, + }, + }, + UsernameBox: { + x: 400, + y: 160, + texture: Lightning.Tools.getRoundRect(1273, 58, 0, 3, 0xffffffff, false) + }, + UsernameText: { + x: 420, + y: 170, + zIndex: 2, + text: { + text: '', + fontSize: 25, + fontFace: CONFIG.language.font, + textColor: 0xffffffff, + wordWrapWidth: 1300, + wordWrap: false, + textOverflow: 'ellipsis', + }, + }, + Password: { + x: 190, + y: 246, + text: { + text: Language.translate("Password") + ":", + fontFace: CONFIG.language.font, + fontSize: 25, + }, + }, + PasswordBox: { + x: 400, + y: 230, + texture: Lightning.Tools.getRoundRect(1273, 58, 0, 3, 0xffffffff, false) + }, + Pwd: { + x: 420, + y: 240, + zIndex: 2, + text: { + text: '', + fontSize: 25, + fontFace: CONFIG.language.font, + textColor: 0xffffffff, + wordWrapWidth: 1300, + wordWrap: false, + textOverflow: 'ellipsis', + }, + }, + BorderBottom: { + x: 190, y: 326, w: 1488, h: 2, rect: true, + }, + ExitButton: { + x: 960, + y: 350, + mountX: 0.5, + w: 200, + h: 50, + rect: true, + color: 0xff444444, + shader: { type: Lightning.shaders.RoundedRectangle, radius: 10 }, + ExitLabel: { + x: 100, + y: 25, + mount: 0.5, + text: { + text: Language.translate('Exit'), + fontFace: CONFIG.language.font, + fontSize: 22, + textColor: 0xffffffff, + }, + }, + }, + Keyboard: { + y: 420, + x: 400, + type: Keyboard, + visible: false, + zIndex: 2, + formats: KEYBOARD_FORMATS.qwerty + }, + PasswrdSwitch: { + h: 45, + w: 66.9, + x: 1642, + y: 260, + zIndex: 2, + type: PasswordSwitch, + mount: 0.5, + visible: true + }, + ShowPassword: { + x: 1365, + y: 242, + w: 300, + h: 75, + zIndex: 2, + text: { text: Language.translate('Show Password'), fontSize: 25, fontFace: CONFIG.language.font, textColor: 0xffffffff, textAlign: 'left' }, + visible: true + } + } + } + + _focus() { + this._setState('EnterUsername'); + this.textCollection = { 'EnterUsername': '', 'EnterPassword': '' } + this.tag('Pwd').text.text = Language.translate("Press OK to enter Password"); + this.tag("UsernameText").text.text = Language.translate("Press OK to enter Username"); + this.tag('UsernameText').text.textColor = 0xff808080 + this.tag('Pwd').text.textColor = 0xff808080 + } + + encrypt() { + if (this.prevState === "EnterPassword" && this.hidePasswd) + return true + else + return false + } + + _updateText(txt) { + this.tag("Pwd").text.text = txt; + } + + _handleBack() { + if (!Router.isNavigating()) { + Router.back() + } + } + + static _states() { + return [ + class EnterUsername extends this { + $enter() { + this.tag('UsernameBox').texture = Lightning.Tools.getRoundRect(1273, 58, 0, 3, CONFIG.theme.hex, false) + } + _handleDown() { + this._setState("EnterPassword"); + } + _handleEnter() { + this._setState('Keyboard') + this.tag('UsernameText').text.text = this.textCollection['EnterUsername'] + this.tag('UsernameText').text.textColor = 0xffffffff + this.tag("Keyboard").visible = true + } + $exit() { + this.tag('UsernameBox').texture = Lightning.Tools.getRoundRect(1273, 58, 0, 3, 0xffffffff, false) + } + }, + class EnterPassword extends this { + $enter() { + this.tag('PasswordBox').texture = Lightning.Tools.getRoundRect(1273, 58, 0, 3, CONFIG.theme.hex, false) + } + _handleUp() { + this._setState("EnterUsername"); + } + _handleDown() { + this._setState("ExitButton"); + } + _handleRight() { + this._setState("PasswordSwitchState") + } + _handleEnter() { + this.tag("Keyboard").visible = true + this._setState('Keyboard') + this.tag('Pwd').text.text = this.hidePasswd ? this.star : this.textCollection['EnterPassword'] + this.tag('Pwd').text.textColor = 0xffffffff + } + $exit() { + this.tag('PasswordBox').texture = Lightning.Tools.getRoundRect(1273, 58, 0, 3, 0xffffffff, false); + } + }, + class PasswordSwitchState extends this { + $enter() { + this.tag("PasswordBox").texture = Lightning.Tools.getRoundRect(1273, 58, 0, 3, CONFIG.theme.hex, false) + this.tag('ShowPassword').text.textColor = CONFIG.theme.hex + } + _handleDown() { + this._setState("ExitButton"); + } + _handleUp() { + this._setState("EnterUsername"); + } + _handleLeft() { + this._setState("EnterPassword"); + } + _getFocused() { + return this.tag('PasswrdSwitch'); + } + + $handleEnter(bool) { + if (bool) { + this._updateText(this.textCollection['EnterPassword']) + this.hidePasswd = false; + } + else { + this._updateText(this.star); + this.hidePasswd = true; + } + this.isOn = bool; + } + + $exit() { + this.tag("PasswordBox").texture = Lightning.Tools.getRoundRect(1273, 58, 0, 3, 0xffffffff, false) + this.tag('ShowPassword').text.textColor = 0xffffffff + } + }, + class ExitButton extends this { + $enter() { + this.tag('ExitButton').color = CONFIG.theme.hex + } + $exit() { + this.tag('ExitButton').color = 0xff444444 + } + _handleUp() { + this._setState('EnterPassword') + } + _handleDown() { + this._setState('EnterUsername') + } + _handleEnter() { + if (!Router.isNavigating()) { + Router.back() + } + } + }, + class Keyboard extends this { + $enter(state) { + this.prevState = state.prevState + if (this.prevState === 'EnterUsername') { + this.element = 'UsernameText' + } + if (this.prevState === 'EnterPassword') { + this.element = 'Pwd' + } + } + _getFocused() { + return this.tag('Keyboard') + } + + $onSoftKey({ key }) { + if (this.prevState === 'PasswordSwitchState') { + this.prevState = "EnterPassword" + } + this.LOG("Prev state: " + JSON.stringify(this.prevState)) + if (key === 'Done') { + this.handleDone(); + } else if (key === 'Clear') { + this.textCollection[this.prevState] = this.textCollection[this.prevState].substring(0, this.textCollection[this.prevState].length - 1); + this.star = (this.prevState === "EnterPassword") ? this.star.substring(0, this.star.length - 1) : this.star + this.tag(this.element).text.text = this.encrypt() ? this.star : this.textCollection[this.prevState]; + } else if (key === '#@!' || key === 'abc' || key === 'áöû' || key === 'shift') { + this.LOG('no saving') + } else if (key === 'Space') { + this.textCollection[this.prevState] += ' ' + this.star += (this.prevState === "EnterPassword") ? '\u25CF' : '' + this.tag(this.element).text.text = this.encrypt() ? this.star : this.textCollection[this.prevState]; + } else if (key === 'Delete') { + this.textCollection[this.prevState] = '' + this.star = (this.prevState === "EnterPassword") ? '' : this.star + this.tag(this.element).text.text = this.encrypt() ? this.star : this.textCollection[this.prevState]; + } else { + this.textCollection[this.prevState] += key + this.star += (this.prevState === "EnterPassword") ? '\u25CF' : '' + this.tag(this.element).text.text = this.encrypt() ? this.star : this.textCollection[this.prevState]; + } + } + _handleUp() { + this._setState(this.prevState) + } + + _handleBack() { + this._setState(this.prevState) + } + } + ] + } + + _init() { + this.star = '' + this.textCollection = { 'EnterUsername': '', 'EnterPassword': '' } + this.tag("Pwd").text.text = this.textCollection['EnterPassword'] + this.tag("UsernameText").text.text = this.textCollection['EnterUsername'] + } +} diff --git a/accelerator-home-ui/src/screens/FailAndOkScreen.js b/accelerator-home-ui/src/screens/FailAndOkScreen.js new file mode 100644 index 0000000..1ccf42a --- /dev/null +++ b/accelerator-home-ui/src/screens/FailAndOkScreen.js @@ -0,0 +1,123 @@ +/** + * If not stated otherwise in this file or this component's LICENSE + * file the following copyright and licenses apply: + * + * Copyright 2020 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 { Language, Lightning, Router } from "@lightningjs/sdk"; +import { CONFIG } from '../Config/Config' + +const errorTitle = 'Error Title' +const errorMsg = 'Error Message' +export default class FailAndOkScreen extends Lightning.Component { + + constructor(...args) { + super(...args); + this.INFO = console.info; + this.LOG = console.log; + this.ERR = console.error; + this.WARN = console.warn; + } + + notify(args) { + this.LOG("notify args: " + JSON.stringify(args)) + if (args.title && args.msg) { + this.tag('FailScreen.Title').text.text = args.title + this.tag('FailScreen.Message').text.text = args.msg + } + } + + pageTransition() { + return 'left' + } + + _focus() { + this.LOG("FailAndOkScreen _focus() called, alpha before: " + this.alpha) + this.alpha = 1 + this.LOG("FailAndOkScreen _focus() alpha set to 1") + this.tag('FailScreen.OkButton').color = CONFIG.theme.hex + this.tag('FailScreen.OkButton.OkLabel').text.textColor = 0xFFFFFFFF + } + + _unfocus() { + this.alpha = 0 + this.tag('FailScreen.Title').text.text = errorTitle + this.tag('FailScreen.Message').text.text = errorMsg + this.tag('FailScreen.OkButton').color = 0xFFFFFFFF + this.tag('FailScreen.OkButton.OkLabel').text.textColor = 0xFF000000 + } + + static _template() { + return { + alpha: 0, + w: 1920, + h: 1080, + rect: true, + color: 0xcc000000, + FailScreen: { + x: 960, + y: 300, + Title: { + mountX: 0.5, + text: { + text: errorTitle, + fontFace: CONFIG.language.font, + fontSize: 40, + textColor: CONFIG.theme.hex, + }, + }, + BorderTop: { + x: 0, y: 75, w: 1558, h: 3, rect: true, mountX: 0.5, + }, + Message: { + x: 0, + y: 125, + mountX: 0.5, + text: { + text: errorMsg, + fontFace: CONFIG.language.font, + fontSize: 25, + }, + }, + OkButton: { + x: 0, y: 200, w: 200, mountX: 0.5, h: 50, rect: true, color: 0xFFFFFFFF, + OkLabel: { + x: 100, + y: 25, + mount: 0.5, + text: { + text: Language.translate("OK"), + fontFace: CONFIG.language.font, + fontSize: 25, + textColor: 0xFF000000, + }, + }, + }, + BorderBottom: { + x: 0, y: 300, w: 1558, h: 3, rect: true, mountX: 0.5, + }, + }, + }; + } + + _handleEnter() { + Router.focusPage() + } + + _handleBack() { + Router.focusPage() + } + +} diff --git a/accelerator-home-ui/src/screens/OtherSettingsScreens/FactoryResetConfirmationScreen.js b/accelerator-home-ui/src/screens/OtherSettingsScreens/FactoryResetConfirmationScreen.js index 730d7fb..fa94a09 100644 --- a/accelerator-home-ui/src/screens/OtherSettingsScreens/FactoryResetConfirmationScreen.js +++ b/accelerator-home-ui/src/screens/OtherSettingsScreens/FactoryResetConfirmationScreen.js @@ -211,7 +211,7 @@ export default class RebootConfirmationScreen extends Lightning.Component { try { localStorage.clear(); this.LOG("localStorage cleared successfully"); - } + } catch (err) { this.ERR("Error clearing localStorage: " + JSON.stringify(err)); } diff --git a/accelerator-home-ui/src/screens/OtherSettingsScreens/LanguageScreen.js b/accelerator-home-ui/src/screens/OtherSettingsScreens/LanguageScreen.js index ed150d5..322ca3f 100644 --- a/accelerator-home-ui/src/screens/OtherSettingsScreens/LanguageScreen.js +++ b/accelerator-home-ui/src/screens/OtherSettingsScreens/LanguageScreen.js @@ -20,16 +20,12 @@ import { Language, Lightning, Router } from '@lightningjs/sdk' import LanguageItem from '../../items/LanguageItem' import { availableLanguages, availableLanguageCodes, CONFIG } from '../../Config/Config' import AppApi from '../../api/AppApi'; -import RDKShellApis from '../../api/RDKShellApis'; import AlexaApi from '../../api/AlexaApi'; import thunderJS from 'ThunderJS'; -import { GLOBALS } from '../../Config/Config' -import FireBoltApi from '../../api/firebolt/FireBoltApi'; -import { Metrics } from '@firebolt-js/manage-sdk'; + const appApi = new AppApi() const thunder = thunderJS(CONFIG.thunderConfig) -const loader = 'Loader' export default class LanguageScreen extends Lightning.Component { @@ -77,11 +73,6 @@ export default class LanguageScreen extends Lightning.Component { } _active() { - if ("ResidentApp" !== GLOBALS.selfClientName) { - this.OnLanguageChangedfirebolt = FireBoltApi.get().localization.listen("languageChanged", value => { - this.LOG('language changed successfully' + JSON.stringify(value)) - }) - } this._Languages = this.tag('LanguageScreenContents.Languages') this._Languages.h = availableLanguages.length * 90 this._Languages.tag('List').h = availableLanguages.length * 90 @@ -94,17 +85,6 @@ export default class LanguageScreen extends Lightning.Component { item: item, } }) - appApi.deactivateResidentApp(loader) - RDKShellApis.moveToFront(GLOBALS.selfClientName) - RDKShellApis.setFocus(GLOBALS.selfClientName).then(result => { - this.LOG('LanguageScreen: ResidentApp moveToFront Success'); - RDKShellApis.getVisibility(GLOBALS.selfClientName).then(visible => { - if (!visible) RDKShellApis.setVisibility(GLOBALS.selfClientName, true); - }) - }).catch(err => { - this.ERR('LanguageScreen: Error' + JSON.stringify(err)); - Metrics.error(Metrics.ErrorType.OTHER, "AppLangugaeError", 'Thunder RDKShell setFocus Error' + JSON.stringify(err), false, null) - }); } _focus() { @@ -146,17 +126,8 @@ export default class LanguageScreen extends Lightning.Component { } }) } - if ("ResidentApp" === GLOBALS.selfClientName) { - appApi.setUILanguage(updatedLanguage) - } else { - FireBoltApi.get().localization.setlanguage(availableLanguages[this._Languages.tag('List').index]).then(res => this.LOG("sucess language set ::::" + JSON.stringify(res))) - } + appApi.setUILanguage(updatedLanguage) localStorage.setItem('Language',availableLanguages[this._Languages.tag('List').index]) - let path = location.pathname.split('index.html')[0] - let url = path.slice(-1) === '/' ? "static/loaderApp/index.html" : "/static/loaderApp/index.html" - let notification_url = location.origin + path + url - appApi.launchResident(notification_url, loader).catch(err => {this.ERR("error while launching loader url in resident app" + JSON.stringify(err)) }) - RDKShellApis.setVisibility(GLOBALS.selfClientName, false) location.reload(); } } diff --git a/accelerator-home-ui/src/screens/RcInformationScreen.js b/accelerator-home-ui/src/screens/RcInformationScreen.js index 03fe522..565303b 100644 --- a/accelerator-home-ui/src/screens/RcInformationScreen.js +++ b/accelerator-home-ui/src/screens/RcInformationScreen.js @@ -16,7 +16,7 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ -import { Lightning, Language, Router } from '@lightningjs/sdk' +import { Lightning, Language, Registry, Router } from '@lightningjs/sdk' import { COLORS } from './../colors/Colors' import { CONFIG } from '../Config/Config' import ThunderJS from 'ThunderJS' @@ -228,6 +228,8 @@ export default class RCInformationScreen extends Lightning.Component { } async _active() { + this.scanTrigger = null; + this.findRemoteTrigger = true; await RCApi.get().activate().catch(err => { this.ERR("RCInformationScreen error: " + JSON.stringify(err)) }); await RCApi.get().getNetStatus().then(result => { this.INFO("RCInformationScreen getNetStatus: " + JSON.stringify(result)) @@ -244,16 +246,21 @@ export default class RCInformationScreen extends Lightning.Component { this.tag("SwVersion.Value").text.text = `N/A` this.tag("BatteryPercent.Value").text.text = `N/A` this.tag("RCUName.Value").text.text = `N/A` - //RCApi.get().deactivate().catch(err=> { console.error("RCInformationScreen error:", err)}); + if (this.scanTrigger) { + Registry.clearTimeout(this.scanTrigger); + this.scanTrigger = null; + } + this.findRemoteTrigger = false; } onStatusCB(cbData) { // getStatus response has 'success' property; notification payload does not have that. + // this.LOG("RCInformationScreen onStatusCB cbData:" + JSON.stringify(cbData)); if ((cbData !== undefined) && ("success" in cbData ? cbData.success : true)) { let cbDatastatus if (Array.isArray(cbData.status)) { cbDatastatus = cbData.status[0] || {}; - } + } else if (cbData.status && typeof cbData.status === 'object') { cbDatastatus = cbData.status; } @@ -262,6 +269,11 @@ export default class RCInformationScreen extends Lightning.Component { let RemoteName = []; let connectedStatus = []; let MacAddress = []; let swVersion = []; let BatteryPercent = []; + if (this.scanTrigger) { + Registry.clearTimeout(this.scanTrigger); + this.scanTrigger = null; + } + cbDatastatus.remoteData.map(item => { RemoteName.push(item.name) }) @@ -282,14 +294,43 @@ export default class RCInformationScreen extends Lightning.Component { this.tag("SwVersion.Value").text.text = swVersion this.tag("BatteryPercent.Value").text.text = BatteryPercent this.tag("RCUName.Value").text.text = RemoteName + if (this.findRemoteTrigger) { + this.findRemoteTrigger = false; + RCApi.get().findMyRemote().catch(err => { + this.ERR("RCInformationScreen findMyRemote error: " + JSON.stringify(err)) + }); + } } else { - if(cbDatastatus.pairingState != "SEARCHING" && cbDatastatus.pairingState != "PAIRING" ) { - for(let i=0;i { - this.ERR("RCInformationScreen startPairing error: " + JSON.stringify(err)); - }); + if (cbDatastatus.pairingState === "IDLE" || cbDatastatus.pairingState === "FAILED") { + // after 2 seconds, initiate pairing flow if status is IDLE, as there is no paired device. + if (!this.scanTrigger) { + this.scanTrigger = Registry.setTimeout(() => { + this.scanTrigger = null; + RCApi.get().getNetStatus().then(result => { + let latestStatus = {}; + if (Array.isArray(result.status)) { + latestStatus = result.status[0] || {}; + } else if (result.status && typeof result.status === 'object') { + latestStatus = result.status; + } + + const latestHasRemoteData = Array.isArray(latestStatus.remoteData) && latestStatus.remoteData.length; + const latestInRetryState = latestStatus.pairingState === "IDLE" || latestStatus.pairingState === "FAILED"; + + if (!latestHasRemoteData && latestInRetryState) { + RCApi.get().startPairing().catch(err => { + this.ERR("RCInformationScreen startPairing error: " + JSON.stringify(err)); + }); + } + }).catch(err => { + this.ERR("RCInformationScreen getNetStatus before startPairing error: " + JSON.stringify(err)); + }); + }, 2000); + } + } else { + if (this.scanTrigger) { + Registry.clearTimeout(this.scanTrigger); + this.scanTrigger = null; } } } diff --git a/accelerator-home-ui/src/screens/SettingsScreen.js b/accelerator-home-ui/src/screens/SettingsScreen.js index 9659e8f..34b766f 100644 --- a/accelerator-home-ui/src/screens/SettingsScreen.js +++ b/accelerator-home-ui/src/screens/SettingsScreen.js @@ -79,9 +79,33 @@ export default class SettingsScreen extends Lightning.Component { src: Utils.asset('images/settings/Arrow.png'), }, }, - Bluetooth: { + ApplicationCatalogueLogin: { y: 90, type: SettingsMainItem, + Title: { + x: 10, + y: 45, + mountY: 0.5, + text: { + text: Language.translate('Connect to the Application Catalog'), + textColor: COLORS.titleColor, + fontFace: CONFIG.language.font, + fontSize: 25, + } + }, + Button: { + h: 45, + w: 45, + x: 1600, + mountX: 1, + y: 45, + mountY: 0.5, + src: Utils.asset('images/settings/Arrow.png'), + }, + }, + Bluetooth: { + y: 180, + type: SettingsMainItem, Title: { x: 10, y: 45, @@ -104,7 +128,7 @@ export default class SettingsScreen extends Lightning.Component { }, }, Video: { - y: 180, + y: 270, type: SettingsMainItem, Title: { x: 10, @@ -128,7 +152,7 @@ export default class SettingsScreen extends Lightning.Component { }, }, Audio: { - y: 270, + y: 360, type: SettingsMainItem, Title: { x: 10, @@ -152,7 +176,7 @@ export default class SettingsScreen extends Lightning.Component { }, }, OtherSettings: { - y: 360, + y: 450, type: SettingsMainItem, Title: { x: 10, @@ -177,7 +201,7 @@ export default class SettingsScreen extends Lightning.Component { }, NFRStatus: { - y: 450, + y: 540, type: SettingsMainItem, Title: { x: 10, @@ -204,7 +228,7 @@ export default class SettingsScreen extends Lightning.Component { DTVSettings: { alpha: 0.3, - y: 630, + y: 720, type: SettingsMainItem, Title: { x: 10, @@ -229,7 +253,7 @@ export default class SettingsScreen extends Lightning.Component { }, VoiceRemoteControl: { - y: 540, + y: 630, type: SettingsMainItem, Title: { x: 10, @@ -300,7 +324,7 @@ export default class SettingsScreen extends Lightning.Component { this.tag('NetworkConfiguration')._unfocus() } _handleDown() { - this._setState('Bluetooth') + this._setState('ApplicationCatalogueLogin') } _handleEnter() { if (!Router.isNavigating()) { @@ -308,6 +332,25 @@ export default class SettingsScreen extends Lightning.Component { } } }, + class ApplicationCatalogueLogin extends this { + $enter() { + this.tag('ApplicationCatalogueLogin')._focus() + } + $exit() { + this.tag('ApplicationCatalogueLogin')._unfocus() + } + _handleUp() { + this._setState('NetworkConfiguration') + } + _handleDown() { + this._setState('Bluetooth') + } + _handleEnter() { + if (!Router.isNavigating()) { + Router.navigate('settings/appcataloglogin') + } + } + }, class Bluetooth extends this { $enter() { this.tag('Bluetooth')._focus() @@ -316,7 +359,7 @@ export default class SettingsScreen extends Lightning.Component { this.tag('Bluetooth')._unfocus() } _handleUp() { - this._setState('NetworkConfiguration') + this._setState('ApplicationCatalogueLogin') } _handleDown() { this._setState('Video') @@ -444,7 +487,7 @@ export default class SettingsScreen extends Lightning.Component { this.tag('DTVSettings')._unfocus() } _handleUp() { - this._setState('NFRStatus') + this._setState('VoiceRemoteControl') } _handleEnter() { if (this.dtvPlugin) { diff --git a/accelerator-home-ui/src/screens/SplashScreens/BluetoothScreen.js b/accelerator-home-ui/src/screens/SplashScreens/BluetoothScreen.js index 847e366..bafc50e 100644 --- a/accelerator-home-ui/src/screens/SplashScreens/BluetoothScreen.js +++ b/accelerator-home-ui/src/screens/SplashScreens/BluetoothScreen.js @@ -35,6 +35,7 @@ export default class BluetoothScreen extends Lightning.Component { this.LOG = console.log; this.ERR = console.error; this.WARN = console.warn; + this.scanTrigger = null; } static _template() { @@ -135,10 +136,6 @@ export default class BluetoothScreen extends Lightning.Component { } } - _active() { - this.timeout = 30; - } - _PairingApis() { //bluetoothApi.btactivate().then(enableResult =>{ // console.log('1') @@ -169,23 +166,22 @@ export default class BluetoothScreen extends Lightning.Component { Router.navigate('splash/language') } }) - }) - .catch(err => { - this.ERR(`SplashBluetoothScreen cant stopscan device : ${JSON.stringify(err)}`) - }) - }) .catch(err => { - this.ERR("SplashBluetoothScreen cant stopscan device : " + JSON.stringify(err)) + this.ERR(`SplashBluetoothScreen can't stop scan device : ${JSON.stringify(err)}`) }) }) .catch(err => { - this.ERR("SplashBluetoothScreen cant getpaired device : " + JSON.stringify(err)) + this.ERR("SplashBluetoothScreen getpairedDevices failed : " + JSON.stringify(err)) }) }) .catch(err => { - this.ERR(`SplashBluetoothScreen Can't pair device : ${JSON.stringify(err)}`) + this.ERR("SplashBluetoothScreen getConnectedDevices failed : " + JSON.stringify(err)) }) + }) + .catch(err => { + this.ERR(`SplashBluetoothScreen Can't pair device : ${JSON.stringify(err)}`) + }) }) }) }) @@ -196,19 +192,24 @@ export default class BluetoothScreen extends Lightning.Component { } onStatusCB(cbData) { - //console.log("BluetoothScreen cbData:", JSON.stringify(cbData)); + //console.log("BluetoothScreen cbData:" + JSON.stringify(cbData)); // getStatus response has 'success' property; notification payload does not have that. if ((cbData !== undefined) && (cbData.hasOwnProperty("success") ? cbData.success : true)) { - let cbDatastatus + let cbDatastatus = {}; if (Array.isArray(cbData.status)) { cbDatastatus = cbData.status[0] || {}; - } + } else if (cbData.status && typeof cbData.status === 'object') { cbDatastatus = cbData.status; - } - if (cbDatastatus.remoteData.length) { + } + const remoteData = Array.isArray(cbDatastatus.remoteData) ? cbDatastatus.remoteData : []; + if (remoteData.length > 0) { //console.log("BluetoothScreen rcPairingApis RemoteData Length ", cbData.status.remoteData.length) - cbDatastatus.remoteData.map(item => { + if (this.scanTrigger) { + Registry.clearTimeout(this.scanTrigger); + this.scanTrigger = null; + } + remoteData.map(item => { this.tag('Info').text.text = `paired with device ${item.name}` // Do not clear this.RCTimeout if need to run this in background to reconnect on loss. // if (this.RCTimeout) { @@ -226,13 +227,35 @@ export default class BluetoothScreen extends Lightning.Component { } }) } else { - if(cbDatastatus.pairingState != "SEARCHING" && cbDatastatus.pairingState != "PAIRING" ) { - for(let i=0;i { - this.ERR("RCInformationScreen startPairing error: " + JSON.stringify(err)); - }); + if (cbDatastatus.pairingState === "IDLE" || cbDatastatus.pairingState === "FAILED") { + // after 2 seconds, initiate pairing flow if status is IDLE, as there is no paired device. + if (!this.scanTrigger) { + this.scanTrigger = Registry.setTimeout(() => { + this.scanTrigger = null; + RCApi.get().getNetStatus().then(result => { + let latestStatus = {}; + if (Array.isArray(result.status)) { + latestStatus = result.status[0] || {}; + } else if (result.status && typeof result.status === 'object') { + latestStatus = result.status; + } + + const latestHasRemoteData = Array.isArray(latestStatus.remoteData) && latestStatus.remoteData.length; + const latestInRetryState = latestStatus.pairingState === "IDLE" || latestStatus.pairingState === "FAILED"; + if (!latestHasRemoteData && latestInRetryState) { + RCApi.get().startPairing().catch(err => { + this.ERR("SplashBluetoothScreen startPairing error: " + JSON.stringify(err)); + }); + } + }).catch(err => { + this.ERR("SplashBluetoothScreen getNetStatus before startPairing error: " + JSON.stringify(err)); + }); + }, 2000); + } + } else { + if (this.scanTrigger) { + Registry.clearTimeout(this.scanTrigger); + this.scanTrigger = null; } } } @@ -279,7 +302,11 @@ export default class BluetoothScreen extends Lightning.Component { } _active() { + this.timeout = 30; this.initTimer() + if (typeof this.scanTrigger === 'undefined') { + this.scanTrigger = null; + } } pageTransition() { @@ -302,6 +329,10 @@ export default class BluetoothScreen extends Lightning.Component { if (this.RCTimeout) { Registry.clearTimeout(this.RCTimeout) } + if (this.scanTrigger) { + Registry.clearTimeout(this.scanTrigger) + this.scanTrigger = null; + } } static _states() { diff --git a/accelerator-home-ui/src/screens/SplashScreens/LanguageScreen.js b/accelerator-home-ui/src/screens/SplashScreens/LanguageScreen.js index 697d63b..f6cd056 100644 --- a/accelerator-home-ui/src/screens/SplashScreens/LanguageScreen.js +++ b/accelerator-home-ui/src/screens/SplashScreens/LanguageScreen.js @@ -17,16 +17,13 @@ * limitations under the License. **/ -import { Lightning, Router, Language, Storage } from '@lightningjs/sdk' -import { CONFIG, GLOBALS } from '../../Config/Config' +import { Lightning, Router, Language } from '@lightningjs/sdk' +import { CONFIG } from '../../Config/Config' import LanguageItem from '../../items/LanguageItem' import { availableLanguages, availableLanguageCodes } from '../../Config/Config' import AppApi from '../../api/AppApi' -import RDKShellApis from '../../api/RDKShellApis' -import FireBoltApi from '../../api/firebolt/FireBoltApi' const appApi = new AppApi() -const loader = 'Loader' export default class LanguageScreen extends Lightning.Component { constructor(...args) { @@ -122,17 +119,6 @@ export default class LanguageScreen extends Lightning.Component { } }) - appApi.deactivateResidentApp(loader) - RDKShellApis.moveToFront(GLOBALS.selfClientName) - RDKShellApis.setFocus(GLOBALS.selfClientName).then(result => { - this.LOG('LanguageScreen: ResidentApp moveToFront Success') - RDKShellApis.getVisibility(GLOBALS.selfClientName).then(visible => { - if (!visible) RDKShellApis.setVisibility(GLOBALS.selfClientName, true) - }) - }).catch(err => { - this.ERR('LanguageScreen: Error' + JSON.stringify(err)) - Metrics.error(Metrics.ErrorType.OTHER, "AppLangugaeError", 'Thunder RDKShell setFocus Error' + JSON.stringify(err), false, null) - }); } pageTransition() { @@ -148,11 +134,7 @@ export default class LanguageScreen extends Lightning.Component { } updateUILanguage(index) { - if ("ResidentApp" === GLOBALS.selfClientName) { - appApi.setUILanguage(availableLanguageCodes[availableLanguages[index]]) - } else { - FireBoltApi.get().localization.setlanguage(availableLanguages[index]).then(res => this.LOG("sucess language set ::::" + JSON.stringify(res))) - } + appApi.setUILanguage(availableLanguageCodes[availableLanguages[index]]) localStorage.setItem('Language',availableLanguages[index]) } @@ -181,12 +163,6 @@ export default class LanguageScreen extends Lightning.Component { const index = this._Languages.tag('List').index localStorage.setItem('LanguageSelectedIndex', index) this.updateUILanguage(index) - let path = location.pathname.split('index.html')[0] - let url = path.slice(-1) === '/' ? "static/loaderApp/index.html" : "/static/loaderApp/index.html" - let notification_url = location.origin + path + url - this.LOG("LanguageScreen notification_url: " + JSON.stringify(notification_url)) - appApi.launchResident(notification_url, loader).catch(err => { }) - RDKShellApis.setVisibility(GLOBALS.selfClientName, false) location.reload(); } } diff --git a/accelerator-home-ui/src/views/AppInfoPage.js b/accelerator-home-ui/src/views/AppInfoPage.js index 9070e46..50400d7 100644 --- a/accelerator-home-ui/src/views/AppInfoPage.js +++ b/accelerator-home-ui/src/views/AppInfoPage.js @@ -23,6 +23,7 @@ import { CONFIG, GLOBALS } from "../Config/Config"; import AppCard from "../items/AppCard"; import { getInstalledDACApps, startDACApp, uninstallDACApp } from "../api/DACApi"; import { filterExcludedApps } from "../helpers/DACAppPresentation"; +import UninstallConfirmation from "../overlays/UninstallConfirmation"; export default class AppInfoPage extends Lightning.Component { @@ -158,6 +159,13 @@ export default class AppInfoPage extends Lightning.Component { } } } + }, + + // Uninstall Confirmation Overlay + UninstallConfirmationOverlay: { + type: UninstallConfirmation, + visible: false, + zIndex: 10 } } } @@ -247,7 +255,7 @@ export default class AppInfoPage extends Lightning.Component { break; case 'uninstall': console.log("Uninstall app:", appInfo.name); - this._uninstallApp(appInfo); + this._showUninstallConfirmation(appInfo); break; default: console.log("Unknown action:", action); @@ -291,13 +299,73 @@ export default class AppInfoPage extends Lightning.Component { const result = await uninstallDACApp({ id: appInfo.id, version: appInfo.version, name: appInfo.name }, this); if (result) { console.log(`${appInfo.name} uninstalled successfully`); - // Refresh the list after uninstall - await this._fetchInstalledApps(); + return true; } else { console.error(`Failed to uninstall ${appInfo.name}`); + return false; } } catch (error) { console.error(`Error uninstalling ${appInfo.name}:`, error); + return false; + } + } + + /** + * Show the uninstall confirmation overlay + */ + _showUninstallConfirmation(appInfo) { + this.tag('UninstallConfirmationOverlay').appInfo = appInfo; + this.tag('UninstallConfirmationOverlay').visible = true; + this._setState('UninstallConfirmation'); + } + + /** + * Hide the uninstall confirmation overlay. + * Only handles overlay cleanup; callers are responsible for + * setting the correct page state afterward. + */ + _hideUninstallConfirmation() { + // Reset overlay to its initial state so that Uninstalling.$exit() runs, + // stopping the loader animation and restoring hidden UI elements. + this.tag('UninstallConfirmationOverlay')._setState('Confirm'); + this.tag('UninstallConfirmationOverlay').visible = false; + } + + /** + * Signal handler: user confirmed uninstall + */ + async $confirmUninstall(appInfo) { + console.log('Uninstall confirmed for:', appInfo.name); + this.tag('UninstallConfirmationOverlay').showUninstalling(); + const success = await this._uninstallApp(appInfo); + // Dismiss overlay first, then let _fetchInstalledApps -> _loadAppData + // be the single source of truth for the next page state. + this._hideUninstallConfirmation(); + if (success) { + // _loadAppData will set AppList or EmptyState based on refreshed data + await this._fetchInstalledApps(); + } else { + // Uninstall failed — data hasn't changed, restore list focus + this._setState('AppList'); + this.widgets.failok.notify({ + title: Language.translate('Uninstall Failed'), + msg: Language.translate('Failed to uninstall') + ` "${appInfo.name}". ` + Language.translate('Please try again later.'), + }); + Router.focusWidget('FailOk'); + } + } + + /** + * Signal handler: user cancelled uninstall + */ + $cancelUninstall() { + console.log('Uninstall cancelled'); + this._hideUninstallConfirmation(); + // Data hasn't changed — restore previous page state + if (this._appData.length > 0) { + this._setState('AppList'); + } else { + this._setState('EmptyState'); } } @@ -378,6 +446,17 @@ export default class AppInfoPage extends Lightning.Component { return false; } }, + class UninstallConfirmation extends this { + $enter() { + this.tag('UninstallConfirmationOverlay').visible = true; + } + _getFocused() { + return this.tag('UninstallConfirmationOverlay'); + } + $exit() { + this.tag('UninstallConfirmationOverlay').visible = false; + } + }, class EmptyState extends this { $enter() { this.tag('EmptyState.OkButton').color = CONFIG.theme.hex; @@ -393,6 +472,8 @@ export default class AppInfoPage extends Lightning.Component { return this; } _focus() { + // Re-fetch installed apps in case new apps were installed while away + this._fetchInstalledApps(); this.tag('EmptyState.OkButton').color = CONFIG.theme.hex; this.tag('EmptyState.OkButton.OkLabel').text.textColor = 0xFFFFFFFF; this.tag('EmptyState.OkButton.FocusBorder').alpha = 1; diff --git a/accelerator-home-ui/src/views/AppStore.js b/accelerator-home-ui/src/views/AppStore.js index 083d1d5..47608ec 100644 --- a/accelerator-home-ui/src/views/AppStore.js +++ b/accelerator-home-ui/src/views/AppStore.js @@ -3,6 +3,7 @@ import { Grid } from "@lightningjs/ui"; import { CONFIG } from "../Config/Config"; import AppCatalogItem from "../items/AppCatalogItem"; import { getAppCatalogInfo } from "../api/DACApi" +import { eventTarget, RefreshNeeded } from '../api/AppCatalog' export default class AppStore extends Lightning.Component { @@ -44,19 +45,39 @@ export default class AppStore extends Lightning.Component { } } - async _firstEnable() { + _firstEnable() { + this._onRefreshNeeded = () => { + this.LOG('RefreshNeeded event received - reloading catalog') + this._loadCatalog() + } + eventTarget.addEventListener(RefreshNeeded.eventName, this._onRefreshNeeded) + } + + async _loadCatalog() { let Catalog = [] try { Catalog = await getAppCatalogInfo() } catch (error) { this.ERR("Failed to get App Catalog Info:" + JSON.stringify(error)) } + if (!Array.isArray(Catalog) || Catalog.length === 0) { + this.LOG('No apps available in catalog') + return + } + Catalog.sort((a, b) => (a.name || '').localeCompare(b.name || '')) + this.tag('Catalog').clear() this.tag('Catalog').add(Catalog.map((element) => { return { h: AppCatalogItem.height + 90, w: AppCatalogItem.width, info: element } })); this._setState('Catalog') } + _detach() { + if (this._onRefreshNeeded) { + eventTarget.removeEventListener(RefreshNeeded.eventName, this._onRefreshNeeded) + } + } + _handleLeft() { @@ -75,9 +96,37 @@ export default class AppStore extends Lightning.Component { } _focus() { + this._loadCatalog() this._setState('Catalog') } + $showInstallError({ name, errorCode }) { + const appName = name || Language.translate('App') + const msg = Language.translate('Something went wrong while installing') + ` "${appName}". ` + Language.translate('Error code') + `: ${errorCode}` + this.widgets.failok.notify({ title: Language.translate('Installation Failed'), msg: msg }) + Router.focusWidget('FailOk') + } + + $showUninstallError({ name, error }) { + const appName = name || Language.translate('App') + let msg = Language.translate('Failed to uninstall') + ` "${appName}". ` + Language.translate('Please try again later.') + if (error) { + msg += ' ' + Language.translate('Error') + `: ${error}` + } + this.widgets.failok.notify({ title: Language.translate('Uninstall Failed'), msg: msg }) + Router.focusWidget('FailOk') + } + + $showLaunchError({ name, error }) { + const appName = name || Language.translate('App') + let msg = Language.translate('Something went wrong while launching') + ` "${appName}". ` + Language.translate('Please check the internet and remaining setup.') + if (error) { + msg += ' ' + Language.translate('Error') + `: ${error}` + } + this.widgets.failok.notify({ title: Language.translate('Launch Failed'), msg: msg }) + Router.focusWidget('FailOk') + } + static _states() { return [ class Catalog extends this { diff --git a/accelerator-home-ui/src/views/MainView.js b/accelerator-home-ui/src/views/MainView.js index 27c258a..7f4393f 100644 --- a/accelerator-home-ui/src/views/MainView.js +++ b/accelerator-home-ui/src/views/MainView.js @@ -32,6 +32,7 @@ import NetworkManager from '../api/NetworkManagerAPI.js' import { getAppCatalogInfo, getInstalledDACApps, startDACApp } from '../api/DACApi.js' import { filterExcludedApps } from '../helpers/DACAppPresentation.js' import AppController from '../AppController.js' +import { eventTarget, RefreshNeeded } from '../api/AppCatalog.js' /** Class for main view component in home UI */ export default class MainView extends Lightning.Component { @@ -458,6 +459,13 @@ export default class MainView extends Lightning.Component { } AppController.get().addPackageChangedListener(this._onPackageChanged) + // Refresh DAC apps row when app catalog authentication changes + this._onCatalogRefreshNeeded = () => { + this.LOG('RefreshNeeded event received - refreshing DAC apps row') + this.refreshSecondRow() + } + eventTarget.addEventListener(RefreshNeeded.eventName, this._onCatalogRefreshNeeded) + this.dacApps = dacCatalog this.fireAncestors("$mountEventConstructor", registerListener.bind(this)) @@ -469,6 +477,9 @@ export default class MainView extends Lightning.Component { _detach() { // Unsubscribe to avoid stale references to this MainView instance AppController.get().removePackageChangedListener(this._onPackageChanged) + if (this._onCatalogRefreshNeeded) { + eventTarget.removeEventListener(RefreshNeeded.eventName, this._onCatalogRefreshNeeded) + } } _firstActive() { @@ -718,8 +729,35 @@ export default class MainView extends Lightning.Component { } } $showNetworkError() { - this.widgets.fail.notify({ title: 'Network State', msg: 'Offline' }) - Router.focusWidget('Fail') + this.widgets.failok.notify({ title: Language.translate('No Internet'), msg: Language.translate('No internet connection. Please check your network and try again.') }) + Router.focusWidget('FailOk') + } + + $showInstallError({ name, errorCode }) { + const appName = name || Language.translate('App') + const msg = Language.translate('Something went wrong while installing') + ` "${appName}". ` + Language.translate('Error code') + `: ${errorCode}` + this.widgets.failok.notify({ title: Language.translate('Installation Failed'), msg: msg }) + Router.focusWidget('FailOk') + } + + $showUninstallError({ name, error }) { + const appName = name || Language.translate('App') + let msg = Language.translate('Failed to uninstall') + ` "${appName}". ` + Language.translate('Please try again later.') + if (error) { + msg += ' ' + Language.translate('Error') + `: ${error}` + } + this.widgets.failok.notify({ title: Language.translate('Uninstall Failed'), msg: msg }) + Router.focusWidget('FailOk') + } + + $showLaunchError({ name, error }) { + const appName = name || Language.translate('App') + let msg = Language.translate('Something went wrong while launching') + ` "${appName}". ` + Language.translate('Please check the internet and remaining setup.') + if (error) { + msg += ' ' + Language.translate('Error') + `: ${error}` + } + this.widgets.failok.notify({ title: Language.translate('Launch Failed'), msg: msg }) + Router.focusWidget('FailOk') } /** @@ -926,7 +964,8 @@ export default class MainView extends Lightning.Component { } else if (applicationType === 'DAC') { // Launch DAC app using startDACApp if (!GLOBALS.IsConnectedToInternet) { - this.fireAncestors('$showNetworkError') + console.log('No internet connection. Cannot launch DAC app.') + this.$showNetworkError() return } let dacApp = { @@ -937,7 +976,14 @@ export default class MainView extends Lightning.Component { url: uri } this.LOG('Launching DAC app from My Apps: ' + JSON.stringify(dacApp)) - startDACApp(dacApp) + try { + const launched = await startDACApp(dacApp) + if (!launched) { + this.$showLaunchError({ name: appData.displayName }) + } + } catch (err) { + this.$showLaunchError({ name: appData.displayName, error: err.message || err }) + } } } }, @@ -1031,14 +1077,10 @@ export default class MainView extends Lightning.Component { Router.focusWidget('Menu') } } - async _handleEnter() { + _handleEnter() { if (Router.isNavigating()) return; - this.LOG("MainView: internetConnectivity " + JSON.stringify(GLOBALS.IsConnectedToInternet)); - let params ={url: this.tag('TVShows').items[this.tag('TVShows').index].data.uri, - } - if (GLOBALS.IsConnectedToInternet) { - Router.navigate("player",params) - } + this.widgets.failok.notify({ title: Language.translate('Not Supported'), msg: Language.translate('VOD feature is not supported.') }) + Router.focusWidget('FailOk') } $exit() { this.tag('Text3').text.fontStyle = 'normal' diff --git a/accelerator-home-ui/static/images/RDKLogo.png b/accelerator-home-ui/static/images/RDKLogo.png index 566bb3e..d73e152 100644 Binary files a/accelerator-home-ui/static/images/RDKLogo.png and b/accelerator-home-ui/static/images/RDKLogo.png differ diff --git a/accelerator-home-ui/static/images/splash/RDKLogo.png b/accelerator-home-ui/static/images/splash/RDKLogo.png index 63669f8..045d614 100644 Binary files a/accelerator-home-ui/static/images/splash/RDKLogo.png and b/accelerator-home-ui/static/images/splash/RDKLogo.png differ diff --git a/bolt/package-configs/com.rdkcentral.refui.json b/bolt/package-configs/com.rdkcentral.refui.json index 6e1a209..0dc94e8 100644 --- a/bolt/package-configs/com.rdkcentral.refui.json +++ b/bolt/package-configs/com.rdkcentral.refui.json @@ -1,12 +1,12 @@ { "id": "com.rdkcentral.refui", - "version": "0.0.1", - "versionName": "develop", + "version": "6.0.3", + "versionName": "6.0.3", "name": "RDK Ref UI Home Screen", "packageType": "application", - "entryPoint": "file:///usr/share/refui/index.html", + "entryPoint": "--lightning --dev file:///usr/share/refui/index.html", "dependencies": { - "com.rdkcentral.wpe-develop": "0.0.1" + "com.rdkcentral.wpe": "0.2.0" }, "permissions": [ "urn:rdk:permission:home-app",