// Copyright (c) 2009-2026 SAP SE, All Rights Reserved

/**
 * @fileoverview Handles the lifecycle of all applications running in the unified shell.
 *
 * This module manages the full lifecycle of applications including:
 * - Application creation, initialization and destruction
 * - Navigation handling (before and after navigation events)
 * - Keep-alive application management and restoration
 * - Application container caching and reuse
 * - PostMessage handling for iframe-based applications
 * - ShellUIService management
 *
 * @version 1.147.1
 */
sap.ui.define([
    "sap/base/util/Deferred",
    "sap/ui/Device",
    "sap/ui/core/Element",
    "sap/ui/core/EventBus",
    "sap/ushell/appIntegration/KeepAliveApps",
    "sap/ushell/state/modules/BackNavigation",
    "sap/ushell/state/modules/ContentDensity",
    "sap/ushell/state/modules/Favicon",
    "sap/ushell/state/modules/HeaderLogo",
    "sap/ushell/appIntegration/contracts/EmbeddedUI5Handler",
    "sap/ushell/appIntegration/contracts/StatefulContainerV1Handler",
    "sap/ushell/appIntegration/contracts/StatefulContainerV2Handler",
    "sap/ushell/appIntegration/ApplicationContainerCache",
    "sap/ushell/appIntegration/ApplicationHandle",
    "sap/ushell/appIntegration/AppLifeCycle/KeepAliveMode",
    "sap/ushell/appIntegration/PostMessageHandler",
    "sap/ushell/appIntegration/PostMessageManager",
    "sap/ushell/services/PluginManager/PostMessagePluginInterface",
    "sap/ushell/appIntegration/IframeApplicationContainer",
    "sap/ushell/appIntegration/UI5ApplicationContainer",
    "sap/ushell/services/AppConfiguration",
    "sap/ushell/utils",
    "sap/ushell/utils/StagedLogger",
    "sap/ushell/Config",
    "sap/ushell/ApplicationType",
    "sap/ushell/utils/CallbackQueue",
    "sap/ushell/utils/UriParameters",
    "sap/base/Log",
    "sap/ushell/EventHub",
    "sap/ushell/utils/UrlParsing",
    "sap/ui/thirdparty/hasher",
    "sap/ushell/renderer/utils",
    "sap/m/library",
    "sap/ushell/AppInfoParameters",
    "sap/ushell/Container",
    "sap/ushell/resources",
    "sap/ushell/state/ShellModel",
    "sap/ushell/state/StateManager",
    "sap/ushell/state/KeepAlive",
    "sap/ushell/ui5service/ShellUIServiceFactory",
    "sap/ui/core/service/ServiceFactoryRegistry",
    "sap/ushell/library",
    "sap/ushell/navigation/RootIntent",
    "sap/ushell/renderer/RendererManagedComponents"
], (
    Deferred,
    Device,
    Element,
    EventBus,
    KeepAliveApps,
    BackNavigation,
    ContentDensity,
    Favicon,
    HeaderLogo,
    EmbeddedUI5Handler,
    StatefulContainerV1Handler,
    StatefulContainerV2Handler,
    ApplicationContainerCache,
    ApplicationHandle,
    KeepAliveMode,
    PostMessageHandler,
    PostMessageManager,
    PostMessagePluginInterface,
    IframeApplicationContainer,
    UI5ApplicationContainer,
    AppConfiguration,
    ushellUtils,
    StagedLogger,
    Config,
    ApplicationType,
    CallbackQueue,
    UriParameters,
    Log,
    EventHub,
    UrlParsing,
    hasher,
    RendererUtils,
    mobileLibrary,
    AppInfoParameters,
    Container,
    ushellResources,
    ShellModel,
    StateManager,
    KeepAlive,
    ShellUIServiceFactory,
    ServiceFactoryRegistry,
    ushellLibrary,
    RootIntent,
    RendererManagedComponents
) => {
    "use strict";

    // shortcut for sap.ushell.appIntegration.contracts.StatefulType
    const StatefulType = ushellLibrary.appIntegration.contracts.StatefulType;

    // shortcut for sap.ushell.state.StateManager.ShellMode
    const ShellMode = StateManager.ShellMode;

    // shortcut for sap.ushell.state.StateManager.LaunchpadState
    const LaunchpadState = StateManager.LaunchpadState;

    // shortcut for sap.ushell.state.StateManager.Operation
    const Operation = StateManager.Operation;

    // shortcut for sap.m.URLHelper
    const URLHelper = mobileLibrary.URLHelper;

    /**
     * @typedef {object} ResolvedHashFragment
     * Contains information on how to start an application.
     * Is created during {@link sap.ushell.services.NavTargetResolution.resolveHashFragment} and is enriched by
     * several components during the app lifecycle flow.
     *
     * @private
     */

    /**
     * @typedef {string} StorageAppId
     * Unique identifier for an application.
     * Is defined on the full hash and is used for caching and restoring applications.
     *
     * @private
     */

    /**
     * @typedef {object} StorageEntry
     * Entry of an application in the keep-alive storage containing all necessary information to restore the application.
     *
     * @private
     */

    /**
     * Manages the lifecycle of applications in the unified shell.
     *
     * @alias sap.ushell.appIntegration.AppLifeCycle
     * @class
     * @classdesc Handles application lifecycle management including creation, navigation,
     *   keep-alive state management, and destruction of applications. Manages both
     *   iframe-based and UI5 embedded applications.
     *
     * @private
     */
    function AppLifeCycle () {
        this._initInternals();
    }

    // Expose Enums
    AppLifeCycle.prototype.KeepAliveMode = KeepAliveMode;

    /**
     * Initializes the internal state of the AppLifeCycle.
     *
     * @since 1.146.0
     * @private
     */
    AppLifeCycle.prototype._initInternals = function () {
        // Dangling controls is a queue of requests to change shell elements attributes, requested by the application in the process of createContent before the actual application state was apply.
        this._bDisableHomeAppCache = false;
        this._bUseLegacyRestoreFlowForNextApp = false; // required for direct app start
        this._oCurrentApplication = {};
        this._oPreviousApplication = {};
        this._oGlobalShellUIService = null;
        this._oNavContainer = null;
        this._oInitDeferred = new Deferred();
        this.oApplicationStartQueue = new CallbackQueue();
        this.oApplicationStartQueue.push(() => this._oInitDeferred.promise);
        this.oApplicationNavigateQueue = new CallbackQueue();
        // stable hash to appId
        this._oStorageAppIdMap = {};
        // base appId to counter
        this._oStorageAppIdNextIndex = {};
        this._aDoables = [];
        this._bSkipErrorHandlingInNextOnAfterNavigate = false;

        this._iLogId = 0;
    };

    /**
     * Initializes the AppLifeCycle, setting up event handling, services, and application state management.
     * @param {object} oInNavContainer the navigation container.
     * @param {boolean} bInDisableHomeAppCache whether the home app cache is disabled.
     * @returns {Promise} after all initializations are done.
     *
     * @private
     */
    AppLifeCycle.prototype.init = function (oInNavContainer, bInDisableHomeAppCache) {
        const aInitPromises = [];
        if (Config.last("/core/shell/enablePersistantAppstateWhenSharing")) {
            Container.getServiceAsync("AppState").then((oAppStateService) => {
                const fnOldTriggerEmail = URLHelper.triggerEmail.bind(URLHelper);
                URLHelper.triggerEmail = function (sTo, sSubject, sBody, sCc, sBcc) {
                    const sFLPUrl = document.URL;
                    oAppStateService.setAppStateToPublic(sFLPUrl)
                        .done((sNewURL, sXStateKey, sIStateKey, sXStateKeyNew, sIStateKeyNew) => {
                            sSubject = sSubject && sXStateKey && sXStateKeyNew && sSubject.includes(sXStateKey) ? sSubject.replace(sXStateKey, sXStateKeyNew) : sSubject;
                            sSubject = sSubject && sIStateKey && sIStateKeyNew && sSubject.includes(sIStateKey) ? sSubject.replace(sIStateKey, sIStateKeyNew) : sSubject;
                            sBody = sBody && sXStateKey && sXStateKeyNew && sBody.includes(sXStateKey) ? sBody.replace(sXStateKey, sXStateKeyNew) : sBody;
                            sBody = sBody && sIStateKey && sIStateKeyNew && sBody.includes(sIStateKey) ? sBody.replace(sIStateKey, sIStateKeyNew) : sBody;
                            fnOldTriggerEmail(sTo, sSubject, sBody, sCc, sBcc);
                        })
                        .fail(fnOldTriggerEmail);
                };
            });
        }

        this._startPostMessageHandling();

        aInitPromises.push(ShellUIServiceFactory.init().then(() => {
            ServiceFactoryRegistry.register(
                "sap.ushell.ui5service.ShellUIService",
                ShellUIServiceFactory
            );
        }));
        aInitPromises.push(this._createGlobalShellUIService());

        this._oNavContainer = oInNavContainer;
        this._bDisableHomeAppCache = bInDisableHomeAppCache;

        // TODO add unsubscribe
        EventBus.getInstance().subscribe("sap.ushell", "getAppLifeCycle", this._onGetAppLifeCycle, this);

        this._addEvents();

        aInitPromises.push(ContentDensity.init());
        Favicon.init();
        HeaderLogo.init();
        ApplicationHandle.init(this, this._oNavContainer);

        this._oNavContainer.attachAfterNavigate(this.onAfterNavigate.bind(this));
        this._oNavContainer.attachBeforeNavigate(this.onBeforeNavigate.bind(this));

        return Promise.allSettled(aInitPromises).then(() => {
            this._oInitDeferred.resolve();
        });
    };

    /**
     * Deprecated event handler for "sap.ushell.getAppLifeCycle" event.
     * This event is private and should not be used by external code.
     * @param {object} oEvent the UI5 event object.
     * @param {string} sChannel the event bus channel.
     * @param {object} oData the send data.
     *
     * @private
     */
    AppLifeCycle.prototype._onGetAppLifeCycle = function (oEvent, sChannel, oData) {
        Log.error("the EventBus channel 'sap.ushell.getAppLifeCycle' is private and was discontinued.");
    };

    /**
     * Adds event handlers for application lifecycle events.
     *
     * @private
     */
    AppLifeCycle.prototype._addEvents = function () {
        this._aDoables.push(EventHub.on("setApplicationFullWidth").do((oData) => {
            this._setApplicationFullWidth(oData.bValue);
        }));

        this._aDoables.push(EventHub.on("reloadCurrentApp").do((oData) => {
            this._reloadCurrentApp(oData);
        }));
    };

    /**
     * Initializes PostMessage handling for communication with iframe-based applications.
     *
     * @private
     */
    AppLifeCycle.prototype._startPostMessageHandling = function () {
        PostMessageManager.init({
            getCurrentApplication: () => {
                return this.getCurrentApplication()?.container;
            },
            getAllApplications: () => {
                return ApplicationContainerCache.getAll();
            }
        });
        PostMessageHandler.register();
        this._waitForPluginsAndReplayPostMessages();

        PostMessagePluginInterface.init(
            async (sServiceRequest, oBody) => {
                const aMessageBodies = await PostMessageManager.sendRequestToAllApplications(sServiceRequest, oBody, true);
                return aMessageBodies[0]?.result;
            },
            PostMessageManager.setRequestHandler.bind(PostMessageManager),
            PostMessageManager.setDistributionPolicy.bind(PostMessageManager),
            true // bRunsInOuterShell
        );
    };

    /**
     * Waits for plugins to load and then replays any stored PostMessages.
     * @returns {Promise} A promise that resolves when plugins are loaded and messages replayed
     *
     * @private
     */
    AppLifeCycle.prototype._waitForPluginsAndReplayPostMessages = async function () {
        const PluginManager = await Container.getServiceAsync("PluginManager");
        const oPluginDeferred = PluginManager.getPluginLoadingPromise("RendererExtensions");

        await ushellUtils.promisify(oPluginDeferred);

        PostMessageManager.replayStoredMessages();
    };

    /**
     * Attaches a hash change handler to the shell navigation's hash changer
     * to listen for hash changes and notify applications accordingly.
     * @param {object} oShellNavigationInternal the shell navigation object.
     *
     * @private
     */
    AppLifeCycle.prototype.registerHandleHashChange = function (oShellNavigationInternal) {
        oShellNavigationInternal.hashChanger.attachEvent("hashChanged", (oHashChangeEvent) => {
            const oParameters = oHashChangeEvent.getParameters();

            if (this.getCurrentApplication()?.container?.getActive() && this.getCurrentApplication().container.getVisible()) {
                // always store latest hash in the current application container
                // with this we can restore the app with the correct hash
                this.getCurrentApplication().container.setCurrentHash(oParameters.fullHash);
            }

            // FIX for internal incident #1980317281 - In general, hash structure in FLP is splitted into 3 parts:
            // A - application identification & B - Application parameters & C - Internal application area
            // Now, when an IFrame changes its hash, it sends PostMessage up to the FLP. The FLP does 2 things: Change its URL
            // and send a PostMessage back to the IFrame. This fix, initiated in the PostMessageAPI.js, blocks only
            // the message back to the IFrame.
            if (hasher.disableBlueBoxHashChangeTrigger) {
                return;
            }

            if (oParameters && oShellNavigationInternal.hashChanger.isInnerAppNavigation(oParameters.newHash, oParameters.oldHash)) {
                PostMessageManager.sendRequestToAllApplications("sap.ushell.appRuntime.innerAppRouteChange", {
                    oHash: oParameters
                }, false);
            }

            PostMessageManager.sendRequestToAllApplications("sap.ushell.appRuntime.hashChange", {
                sHash: oParameters.fullHash
            }, false);
        });
    };

    /**
     * Returns the current active application.
     * Might be an empty object if no application is active.
     * Use {@link sap.ushell.appIntegration.AppLifeCycle#hasActiveApplication} to check if an active application is set.
     * @returns {object} Returns the current application.
     *
     * @private
     */
    AppLifeCycle.prototype.getCurrentApplication = function () {
        return this._oCurrentApplication;
    };

    /**
     * Checks whether there is an active application.
     * @returns {boolean} true if there is an active application, false otherwise.
     *
     * @since 1.145.0
     * @private
     */
    AppLifeCycle.prototype.hasActiveApplication = function () {
        return !!this.getCurrentApplication()?.appId;
    };

    /**
     * Updates the current application to use the legacy restore flow.
     * Instead of continuing the router is restarted when the application is restored.
     *
     * If no application is set yet the next application started will use the legacy restore flow.
     *
     * @since 1.138.0
     * @private
     */
    AppLifeCycle.prototype.useLegacyRestoreFlowForCurrentApplication = function () {
        this.oApplicationStartQueue.push(() => {
            // for direct app start the app creation and this call might be faster than the app life cycle flow
            if (!this.getCurrentApplication()?.appId) {
                this._bUseLegacyRestoreFlowForNextApp = true;
                return;
            }

            const sStorageAppId = this.getCurrentApplication().appId;

            const oStorageEntry = KeepAliveApps.get(sStorageAppId);
            if (oStorageEntry) {
                oStorageEntry.useLegacyRestoreFlow = true;
            }
        });
    };

    /**
     * Toggles the full width mode for the current application by adding or removing a specific CSS class.
     * @param {boolean} bIsFull whether the application should use the full width.
     *
     * @private
     */
    AppLifeCycle.prototype._setApplicationFullWidth = function (bIsFull) {
        const oCurrent = this.getCurrentApplication();

        // validate that we have a valid applicationContainer
        if (oCurrent.container) {
            oCurrent.container.toggleStyleClass("sapUShellApplicationContainerLimitedWidth", !bIsFull);
        }
    };

    /**
     * Returns the global ShellUIService instance.
     * @returns {sap.ushell.ui5service.ShellUIService} The global ShellUIService instance
     *
     * @private
     */
    AppLifeCycle.prototype.getShellUIService = function () {
        return this._oGlobalShellUIService;
    };

    /**
     * Resets the global ShellUIService instance, if it exists.
     * Should only be called when switching between applications.
     * @private
     */
    AppLifeCycle.prototype.resetGlobalShellUIService = function () {
        if (this._oGlobalShellUIService) {
            this._oGlobalShellUIService.resetService();
        }
    };

    /**
     * Creates the ShellUIService instance of the AppLifeCycle in application integration if not already created.
     *
     * @returns {Promise} A promise that resolves when the ShellUIService instance is created
     *
     * @since 1.127.0
     * @private
     */
    AppLifeCycle.prototype._createGlobalShellUIService = async function () {
        this._oGlobalShellUIService = await ShellUIServiceFactory.createInstanceInternal();
    };

    /**
     * Handles the before navigate event of the navigation container
     * to inform the current application about the upcoming navigation and allow it to prepare for it.
     * @param {sap.ui.base.Event} oEvent the UI5 event object.
     *
     * @private
     */
    AppLifeCycle.prototype.onBeforeNavigate = function (oEvent) {
        const sFromId = oEvent.getParameter("fromId") || "";
        const oFrom = oEvent.getParameter("from");
        const sToId = oEvent.getParameter("toId") || "";

        if (sToId === sFromId) {
            return; // no actual navigation happening; might be a navigation into the same container
        }

        if (sFromId && oFrom && oFrom.isA("sap.ushell.appIntegration.IframeApplicationContainer")) {
            const oApplicationContainer = oFrom;
            const sStorageAppId = oApplicationContainer.getCurrentAppId();

            const bSessionHandlingSupported = oApplicationContainer.getStatefulType() !== StatefulType.NotSupported;
            const bCached = !!KeepAliveApps.get(sStorageAppId);

            if (bCached || bSessionHandlingSupported) {
                oApplicationContainer.sendRequest("sap.ushell.sessionHandler.beforeApplicationHide", {}, false);
            }
        }
    };

    /**
     * Handles application lifecycle operations after navigation,
     * including cleanup of the source application and restoration of the target application.
     * @param {sap.ui.base.Event} oEvent the UI5 event object.
     *
     * @private
     */
    AppLifeCycle.prototype.onAfterNavigate = async function (oEvent) {
        const sFromId = oEvent.getParameter("fromId") || "";
        const oFrom = oEvent.getParameter("from");
        const sToId = oEvent.getParameter("toId") || "";
        const oTo = oEvent.getParameter("to");

        const oLogger = new StagedLogger({
            title: `Navigation: ${sFromId || "<none>"} → ${sToId || "<none>"}`,
            moduleName: "sap.ushell.appIntegration.AppLifeCycle.onAfterNavigate",
            stages: [
                "STAGE1: Cleanup source",
                "STAGE2: Restore target"
            ]
        });

        return this.oApplicationNavigateQueue.push(async () => {
            const bNavWithinSameContainer = sFromId === sToId;
            const bNavigationToFlpComponent = RendererManagedComponents.isManagedComponentInstance(oTo);

            // ========================= Stage 1 ================================
            const oStage1Logger = oLogger.getStage(1);

            // first cleanup
            if (sFromId && oFrom) {
                // ==== Navigation from app to [any] ====
                if (oFrom.isA("sap.ushell.appIntegration.ApplicationContainer")) {
                    const oApplicationContainer = oFrom;
                    const sCurrentAppId = oApplicationContainer.getCurrentAppId();
                    oStage1Logger.log(`Source: ApplicationContainer (appId: ${sCurrentAppId || "<none>"})`, "•");

                    try {
                        if (!bNavWithinSameContainer) {
                            await this._handleExitApplication(oApplicationContainer, bNavigationToFlpComponent);
                            oStage1Logger.log(`Exited: ${sCurrentAppId}`, "✓");
                        } else {
                            oStage1Logger.log("Navigation within same container - no cleanup needed", "•");
                        }
                    } catch (oError) {
                        Log.error(`Error during exit of application with id '${sCurrentAppId}'`, oError, "sap.ushell.appIntegration.AppLifeCycle");
                        oStage1Logger.log(`Failed to exit application with id '${sCurrentAppId}': ${oError.message}`, "✗");
                        await this.destroyApplication(sCurrentAppId, oApplicationContainer, true); // hard destroy
                    }

                    // ==== Navigation from renderer managed component to [any] ====
                } else {
                    const sFromControlId = sFromId;
                    const oFromControl = oFrom;
                    oStage1Logger.log(`Source: FLP component (${sFromControlId})`);

                    if (bNavWithinSameContainer) {
                        oStage1Logger.log("Same container navigation detected");
                        oLogger.end();
                        return;
                    }

                    // handle the case of appFinder
                    if (sToId.indexOf("Shell-appfinder-component") > 0) {
                        EventBus.getInstance().publish("sap.ushell", "appFinderAfterNavigate");
                        oStage1Logger.log("Published appFinderAfterNavigate event", "✓");
                    }

                    if (this._bDisableHomeAppCache) {
                        try {
                            this._removeApplicationContainerFromNavContainer(oFromControl);
                            oFromControl.destroy();
                            oStage1Logger.log(`Destroyed home component: ${sFromControlId} (cache disabled)`, "✓");
                        } catch (oError) {
                            Log.error(`Error when trying to destroy the home component: '${sFromControlId}'`, oError);
                            oStage1Logger.log(`Failed to destroy home component: ${oError.message}`, "✗");
                        }
                    } else {
                        oStage1Logger.log(`Home component kept in cache: ${sFromControlId}`, "•");
                    }
                }
            } else {
                oStage1Logger.log("No source for cleanup", "•");
            }

            // ========================= Stage 2 ================================
            const oStage2Logger = oLogger.getStage(2);

            // second restore state
            if (sToId && oTo) {
                // ==== Navigation from [any] to app ====
                if (oTo.isA("sap.ushell.appIntegration.ApplicationContainer")) {
                    const oApplicationContainer = oTo;
                    const sStorageAppId = oApplicationContainer.getCurrentAppId();

                    oStage2Logger.log(`Restoring application (appId: ${sStorageAppId || "<none>"})`, "•");

                    try {
                        const bIsFetchedFromCache = oApplicationContainer.getIsFetchedFromCache();
                        if (bIsFetchedFromCache && oApplicationContainer.isA("sap.ushell.appIntegration.IframeApplicationContainer")) {
                            oApplicationContainer.sendRequest("sap.ushell.sessionHandler.afterApplicationShow", {}, false);
                            oStage2Logger.log("Sent afterApplicationShow to iframe (reused container)", "✓");
                        }

                        if (KeepAliveApps.get(sStorageAppId)) {
                            await this._restoreApplication(sStorageAppId);
                            oStage2Logger.log(`Restored: ${sStorageAppId}`, "✓");
                        }
                    } catch (oError) {
                        Log.error(`Error during restore of application with id '${sStorageAppId}'`, oError, "sap.ushell.appIntegration.AppLifeCycle");
                        oStage2Logger.log(`Failed to restore application with id '${sStorageAppId}': ${oError.message}`, "✗");
                        await this.destroyApplication(sStorageAppId, oApplicationContainer, true); // hard destroy
                    }

                    // ==== Navigation from [any] to renderer managed component ====
                } else {
                    oStage2Logger.log(`Target: FLP component (${sToId})`, "•");
                    // navigation to home
                    if (bNavigationToFlpComponent) {
                        const aDestroyedIds = await this._destroyKeepAliveApps((oStorageEntry) => oStorageEntry.keepAliveMode === KeepAliveMode.Restricted);
                        await this._setApplicationContainerActive(undefined);
                        this._oCurrentApplication = {};

                        const aRestrictedIds = [];
                        KeepAliveApps.forEach((oStorageEntry) => {
                            if (oStorageEntry.keepAliveMode === KeepAliveMode.Restricted) {
                                aRestrictedIds.push(oStorageEntry.appId);
                            }
                        });

                        oStage2Logger.log(`restricted keep-alive apps destroyed (${aDestroyedIds.length}): [${aDestroyedIds.join(", ")}]`, "✓");
                    }

                    // Clear custom About Dialog parameters
                    AppInfoParameters.flush();
                    oStage2Logger.log("Flushed app info parameters", "✓");
                }
            } else {
                oStage2Logger.log("No target for restoration", "•");
            }

            oLogger.end();
        }, (oError) => {
            if (this._bSkipErrorHandlingInNextOnAfterNavigate) {
                Log.error("Recursive error during onAfterNavigate - skipping further handling", oError, "sap.ushell.appIntegration.AppLifeCycle");
                return;
            }

            // we cannot determine whether it failed with the cleanup of the previous app or with the restore of the new app
            // so we just do an emergency error handling and log the error
            EventBus.getInstance().publish("sap.ushell", "showErrorMessage", {
                details: {
                    info: `${oError.message}\n${oError.stack}`
                }
            });

            Log.error("Error during onAfterNavigate", oError, "sap.ushell.appIntegration.AppLifeCycle");
            oLogger.end();

            hasher.setHash("");

            this._bSkipErrorHandlingInNextOnAfterNavigate = true;
        }).then(() => {
            this._bSkipErrorHandlingInNextOnAfterNavigate = false;
        });
    };

    /**
     * Stores the application state for keep-alive functionality.
     *
     * @param {StorageAppId} sStorageAppId The storage app id
     * @returns {Promise} A promise that resolves when the app is stored
     *
     * @private
     */
    AppLifeCycle.prototype._storeApplication = async function (sStorageAppId) {
        const oStorageEntry = KeepAliveApps.get(sStorageAppId);

        if (oStorageEntry) {
            const oApplicationContainer = oStorageEntry.container;
            const bShouldBeKeptAlive = oApplicationContainer.getIsKeepAlive();
            if (!bShouldBeKeptAlive) {
                Log.error("Application should be kept alive but the flag is not set", null, "sap.ushell.appIntegration.AppLifeCycle");
                return;
            }

            if (oApplicationContainer.isA("sap.ushell.appIntegration.IframeApplicationContainer")) {
                oApplicationContainer.setDataHelpId("");

                if (oApplicationContainer.getStatefulType() === StatefulType.ContractV2) {
                    const bSupportsKeepAliveWithinSameFrame = StatefulContainerV2Handler.isStatefulContainerSupportingKeepAlive(oApplicationContainer);

                    if (bSupportsKeepAliveWithinSameFrame) {
                        const sActualAppFromId = oApplicationContainer.getCurrentAppId();
                        if (!sActualAppFromId) {
                            return; // stateful container is already stored
                        }

                        // return the container to the pool
                        this._setContainerReadyForReuse(oApplicationContainer);

                        await StatefulContainerV2Handler.storeAppWithinSameFrame(oApplicationContainer, sActualAppFromId);
                    }
                }

                await oApplicationContainer.sendRequest("sap.ushell.appRuntime.keepAliveAppHide", {}, false);
            } else if (oApplicationContainer.isA("sap.ushell.appIntegration.UI5ApplicationContainer")) {
                await EmbeddedUI5Handler.storeApp(oApplicationContainer, oStorageEntry);
            }

            // close storage entry
            oStorageEntry.stateStored = true;

            const oResolvedHashFragment = oApplicationContainer.getCurrentAppTargetResolution();
            this._publishAppClosedEvent(oResolvedHashFragment);
        }
    };

    /**
     * Should remove their foot print and prepare for possible reuse.
     * @param {StorageAppId} sStorageAppId the storage app id.
     * @param {sap.ushell.appIntegration.ApplicationContainer} oApplicationContainer the application container.
     * @param {boolean} [bHardDestroy=false] whether the application should be hard destroyed.
     * @returns {Promise} Resolves when the app is destroyed.
     *
     * @private
     */
    AppLifeCycle.prototype.destroyApplication = async function (sStorageAppId, oApplicationContainer, bHardDestroy) {
        // storageAppId can be omitted hard destroy
        if (typeof sStorageAppId !== "string" && bHardDestroy) {
            sStorageAppId = "dummy-id";
        }

        if (typeof sStorageAppId !== "string" || !oApplicationContainer) {
            return;
        }

        const oStorageEntry = KeepAliveApps.get(sStorageAppId);
        const bIsKeptAlive = !!oStorageEntry;

        if (!oApplicationContainer.getCurrentAppId() && !bHardDestroy && !bIsKeptAlive) {
            /*
             * No active app in the container, only destroy this app if it shall not be reused.
             *
             * When called multiple times for a stored app, the appruntime destroys  the current
             * app instead!
             */
            return;
        }

        const oResolvedHashFragment = oApplicationContainer.getCurrentAppTargetResolution();

        KeepAliveApps.removeById(sStorageAppId);
        KeepAlive.destroy(oStorageEntry);

        // ===============================================================================
        // try to reuse the iframe

        const bIsReusableContainer = oApplicationContainer.getStatefulType() !== StatefulType.NotSupported;
        if (!bHardDestroy && bIsReusableContainer) {
            oApplicationContainer.setDataHelpId("");

            // First return the container to the pool in case it was keep alive.
            // the "return to pool" step requires the current metadata and has to be done before the actual close
            this._setContainerReadyForReuse(oApplicationContainer);

            // Second close the app
            if (oApplicationContainer.getStatefulType() === StatefulType.ContractV2) {
                await StatefulContainerV2Handler.destroyApp(oApplicationContainer, sStorageAppId);
            } else if (oApplicationContainer.getStatefulType() === StatefulType.ContractV1) {
                await StatefulContainerV1Handler.destroyApp(oApplicationContainer, sStorageAppId);
            }

            this._publishAppClosedEvent(oResolvedHashFragment);
            return;
        }

        // ===============================================================================
        // the app or iframe cannot be reused (or should not be reused) so we destroy it

        // try to close the iframe gracefully
        if (oApplicationContainer.getStatefulType() === StatefulType.ContractV2) {
            await StatefulContainerV2Handler.destroyApp(oApplicationContainer, sStorageAppId);
        } else if (oApplicationContainer.getStatefulType() === StatefulType.ContractV1) {
            await StatefulContainerV1Handler.destroyApp(oApplicationContainer, sStorageAppId);
        }

        // remove all other related apps from the storage
        KeepAliveApps.removeByContainer(oApplicationContainer, (oStorageEntry) => {
            KeepAlive.destroy(oStorageEntry);
        });

        /**
         * If the application running in an iframe registered for "before close" event,
         * we first post it a message to prepare for closing (usually, the app will close
         * its session or release locks held on the server), and only when the iframe send a response
         * back that it finished processing the event, we will continue to destroy the app (iframe).
         * If the app in the iframe did not register to the event, we destroy the app immediately exactly
         * as it was done before.
         * Note that even if the response from the iframe is not successful, we still destroy the app
         * because the second app that we navigated to was already created so we can not stop
         * the actual navigation (this is the same behaviour that we had before).
         * This mechanism was added to solve the change made in Chrome to disallow Sync XHR on
         * browser close.
         */
        try {
            const oThenable = oApplicationContainer.sendBeforeAppCloseEvent();
            await ushellUtils.promisify(oThenable);
        } catch (oError) {
            Log.error(
                "FLP got a failed response from the iframe for the 'sap.ushell.services.CrossApplicationNavigation.beforeAppCloseEvent' message sent",
                oError,
                "sap.ushell.appIntegration.AppLifeCycle.js"
            );
        }

        ApplicationContainerCache.removeByContainer(oApplicationContainer);
        this._removeApplicationContainerFromNavContainer(oApplicationContainer);

        try {
            oApplicationContainer.destroy();
        } catch (oError) {
            Log.error(
                `Exception when trying to close application with id '${oApplicationContainer.getId()}'. This error must be fixed in order for FLP to operate properly.`,
                oError,
                "sap.ushell.appIntegration.AppLifeCycle"
            );
        }

        this._publishAppClosedEvent(oResolvedHashFragment);
    };

    /**
     * Restores the application state for a given storage app id.
     * @param {StorageAppId} sStorageAppId the storage id.
     *
     * @private
     */
    AppLifeCycle.prototype._restoreApplication = async function (sStorageAppId) {
        const oStorageEntry = KeepAliveApps.get(sStorageAppId);

        if (oStorageEntry?.stateStored) {
            // 1) Extensions
            BackNavigation.restore(oStorageEntry.service);

            // 2) Extension API
            ShellUIServiceFactory.restore(oStorageEntry);

            // 3) Application
            const oApplicationContainer = oStorageEntry.container;
            if (oApplicationContainer.isA("sap.ushell.appIntegration.IframeApplicationContainer")) {
                await oApplicationContainer.sendRequest("sap.ushell.appRuntime.keepAliveAppShow", {}, false);

                /**
                 * Check if only the app specific route has changed with the navigation
                 * This scenario can happen for a stored application which is targeted again with updated app-specific route.
                 * It is only relevant for stateful container not supporting keep-alive within the same frame.
                 * In this case the old application is restored but without information on the current hash.
                 */
                if (!StatefulContainerV2Handler.isStatefulContainerSupportingKeepAlive(oApplicationContainer)) {
                    const sCurrentHash = hasher.getHash();
                    const oCurrentParsedHash = UrlParsing.parseShellHash(sCurrentHash, true);
                    const oStoredParsedHash = UrlParsing.parseShellHash(oApplicationContainer.getCurrentHash(), true);
                    const oHashComparison = UrlParsing.compareParsedHashes(oStoredParsedHash, oCurrentParsedHash);

                    const bSameAppUpdatedRoute = oHashComparison.sameIntent && oHashComparison.sameParameters && !oHashComparison.sameAppSpecificRoute;
                    if (bSameAppUpdatedRoute) {
                        const oHashUpdate = {
                            oldHash: (oStoredParsedHash.appSpecificRoute || "").replace("&/", ""),
                            newHash: (oCurrentParsedHash.appSpecificRoute || "").replace("&/", ""),
                            fullHash: sCurrentHash
                        };
                        oApplicationContainer.sendRequest("sap.ushell.appRuntime.innerAppRouteChange", {
                            oHash: oHashUpdate
                        }, false);

                        oApplicationContainer.setCurrentHash(sCurrentHash);
                    }
                }
            } else if (oApplicationContainer.isA("sap.ushell.appIntegration.UI5ApplicationContainer")) {
                await EmbeddedUI5Handler.restoreAppAfterNavigate(oApplicationContainer, oStorageEntry);
            }
        }
    };

    /**
     * Handles the exit of an application during navigation.
     * Considers the navigation direction.
     *
     * @param {sap.ushell.appIntegration.ApplicationContainer} oOldApplicationContainer The old application container
     * @param {boolean} bNavigationToFlpComponent Whether the navigation target is a FLP component
     * @returns {Promise} Resolves when the app is closed
     *
     * @private
     */
    AppLifeCycle.prototype._handleExitApplication = async function (oOldApplicationContainer, bNavigationToFlpComponent) {
        if (!oOldApplicationContainer) {
            return;
        }

        const sOldStorageAppId = oOldApplicationContainer.getCurrentAppId();
        if (!sOldStorageAppId) {
            // no active app in the container
            return;
        }

        const bNavigationBackToApp = BackNavigation.isBackNavigation() && !bNavigationToFlpComponent;

        // Back Navigation Case
        if (bNavigationBackToApp) {
            await this.destroyApplication(sOldStorageAppId, oOldApplicationContainer);

            // Forward Navigation Case
            //      (and back navigation to FLP component)
        } else {
            if (oOldApplicationContainer.getIsKeepAlive()) {
                return this._storeApplication(sOldStorageAppId);
            }
            return this.destroyApplication(sOldStorageAppId, oOldApplicationContainer);
        }
    };

    /**
     * Destroys keep-alive applications that match the filter criteria.
     *
     * @param {function(StorageEntry): boolean} fnFilterApps The filter function that receives a storage entry
     * @param {boolean} [bHardDestroy=false] Whether the application should be hard destroyed
     * @returns {Promise<string[]>} Resolves when all matching keep-alive apps are closed
     *
     * @private
     */
    AppLifeCycle.prototype._destroyKeepAliveApps = async function (fnFilterApps, bHardDestroy) {
        const aFilteredApps = [];

        try {
            KeepAliveApps.forEach((oStorageEntry) => {
                if (fnFilterApps(oStorageEntry)) {
                    aFilteredApps.push(oStorageEntry);
                }
            });
            const aClosePromises = aFilteredApps.map((oFilteredApp) => {
                return this.destroyApplication(oFilteredApp.appId, oFilteredApp.container, bHardDestroy);
            });

            await Promise.allSettled(aClosePromises);
        } catch (oError) {
            Log.error("_destroyKeepAliveApps call failed", oError);
        }

        return aFilteredApps.map((oFilteredApp) => oFilteredApp.appId);
    };

    /**
     * Adds a storage entry for keep-alive application tracking.
     *
     * @param {StorageAppId} sStorageAppId The storage app id
     * @param {sap.ushell.appIntegration.ApplicationContainer} oApplicationContainer The application container
     * @param {ResolvedHashFragment} oResolvedHashFragment The resolved hash fragment object
     * @param {sap.ushell.services.URLParsing.DecomposedHash} oParsedHash The parsed shell hash
     * @param {sap.ushell.appIntegration.AppLifeCycle.KeepAliveMode} sKeepAliveMode The keep alive mode
     * @returns {StorageEntry} Returns the storage entry
     *
     * @private
     */
    AppLifeCycle.prototype._addStorageEntry = function (sStorageAppId, oApplicationContainer, oResolvedHashFragment, oParsedHash, sKeepAliveMode) {
        const oOldStorageEntry = KeepAliveApps.get(sStorageAppId);
        if (oOldStorageEntry) {
            return oOldStorageEntry;
        }

        const oNewStorageEntry = {
            service: {},
            shellHash: UrlParsing.constructShellHash(oParsedHash, true),
            appId: sStorageAppId,
            currentState: null, // current state is stored before close see: sap/ushell/state/CurrentState
            controlManager: null, // control manager state is stored before close see: sap/ushell/state/ControlManager
            container: oApplicationContainer,
            meta: undefined, // is set after _openApp in _initializeAppWithMetadata
            app: undefined, // is set in _openApp
            keepAliveMode: sKeepAliveMode,
            appTarget: oResolvedHashFragment,
            useLegacyRestoreFlow: this._bUseLegacyRestoreFlowForNextApp ?? false,
            stateStored: false
        };

        KeepAliveApps.set(sStorageAppId, oNewStorageEntry);

        return oNewStorageEntry;
    };

    /**
     * Stores the state of the current application
     *  - back navigation
     *  - title, hierarchy, relatedApps
     *  - currentState, controlManager
     * @returns {Promise} Resolves when the store is done.
     *
     * @since 1.128.0
     * @private
     */
    AppLifeCycle.prototype.storeAppExtensions = async function () {
        const oCurrentApp = this.getCurrentApplication();
        if (!oCurrentApp) {
            return;
        }
        const sStorageAppId = oCurrentApp.appId;
        const oStorageEntry = KeepAliveApps.get(sStorageAppId);

        if (!oStorageEntry) {
            // do not store for non-keep-alive apps
            return;
        }

        // back navigation
        BackNavigation.store(oStorageEntry.service);
        // currentState, controlManager
        KeepAlive.store(oStorageEntry);
        // About Dialog
        AppInfoParameters.store(oStorageEntry);
    };

    /**
     * Calculates the keep-alive mode for an application based on configuration.
     *
     * @param {StorageAppId} sStorageAppId The storage app id
     * @param {ResolvedHashFragment} oResolvedHashFragment The resolved hash fragment object
     * @param {sap.ushell.services.URLParsing.DecomposedHash} oParsedHash The parsed shell hash object
     * @param {sap.ushell.utils.StagedLogger.Stage} oLogger the logger instance.
     * @returns {sap.ushell.appIntegration.AppLifeCycle.KeepAliveMode} The keep alive mode
     *
     * @private
     */
    AppLifeCycle.prototype._calculateKeepAliveMode = function (sStorageAppId, oResolvedHashFragment, oParsedHash, oLogger) {
        const aKeepAliveModes = Object.values(KeepAliveMode);

        /**
        * @param {string} sMode the keep alive mode.
        * @param {string} sReason the reason for the decision.
        *
        * @since 1.145.0
        * @private
        */
        function logResult (sMode, sReason) {
            const sDisplayMode = sMode === "" ? "false" : sMode;
            oLogger.log(`Keep-alive: '${sDisplayMode}' - ${sReason}`, "•");
        }

        // generic intent currently can never be keep alive
        if (
            sStorageAppId.startsWith("application-Shell-startIntent")
            || sStorageAppId.startsWith("application-Shell-startGUI")
            || sStorageAppId.startsWith("application-Shell-startWDA")
        ) {
            logResult(KeepAliveMode.False, "Generic intent (startIntent/startGUI/startWDA)");
            return KeepAliveMode.False;
        }

        // Global override in query parameters
        let sKeepAlive = new URLSearchParams(window.location.search).get("sap-keep-alive");
        if (aKeepAliveModes.includes(sKeepAlive)) {
            logResult(sKeepAlive, "Set via global query parameter 'sap-keep-alive'");
            return sKeepAlive;
        }

        // App override in intent parameters
        sKeepAlive = oParsedHash.params?.["sap-keep-alive"]?.[0];
        if (aKeepAliveModes.includes(sKeepAlive)) {
            logResult(sKeepAlive, "Set via intent parameter 'sap-keep-alive'");
            return sKeepAlive;
        }

        // Magic override in resolved hash fragment
        if (oResolvedHashFragment.url) {
            sKeepAlive = UriParameters.fromURL(oResolvedHashFragment.url).get("sap-keep-alive");
            if (aKeepAliveModes.includes(sKeepAlive)) {
                logResult(sKeepAlive, "Set via app URL parameter 'sap-keep-alive'");
                return sKeepAlive;
            }
        }

        // if the app is a root intent, it should be kept alive (e.g. workzone advanced)
        // can be overridden by the above checks
        if (RootIntent.isEqual(oParsedHash)) {
            logResult(KeepAliveMode.True, "Root intent detected");
            return KeepAliveMode.True;
        }
        logResult(KeepAliveMode.False, "Default (no override found)");
        return KeepAliveMode.False;
    };

    /**
     * Calculates the application type from the resolved hash fragment.
     *
     * @param {ResolvedHashFragment} oResolvedHashFragment The resolved hash fragment object
     * @returns {sap.ushell.components.container.ApplicationType} The application type
     *
     * @private
     */
    AppLifeCycle.prototype._calculateAppType = function (oResolvedHashFragment) {
        if (oResolvedHashFragment.applicationType === "URL" && oResolvedHashFragment.additionalInformation?.startsWith?.("SAPUI5.Component=")) {
            return "SAPUI5";
        }
        return oResolvedHashFragment.applicationType;
    };

    /**
     * Reloads the current application.
     *
     * @param {object} oData The application container data
     * @param {string} oData.sAppContainerId The application container id
     * @param {string} oData.sCurrentHash The current hash to navigate to
     * @returns {Promise} Resolves when the app is reloaded
     *
     * @private
     */
    AppLifeCycle.prototype._reloadCurrentApp = async function (oData) {
        const oTargetApplicationContainer = ApplicationContainerCache.getById(oData.sAppContainerId);
        if (oTargetApplicationContainer) {
            await this.destroyApplication(oTargetApplicationContainer.getCurrentAppId(), oTargetApplicationContainer);
        }

        const ShellNavigationInternal = await Container.getServiceAsync("ShellNavigationInternal");
        try {
            ShellNavigationInternal.hashChanger.treatHashChanged(oData.sCurrentHash);
        } catch (oError) {
            Log.error("Error when trying to re-load the current displayed application", oError, "sap.ushell.services.AppLifeCycle");
        }
    };

    /**
     * Opens an application in an application container and sets it as the current application.
     *
     * @param {StorageAppId} sStorageAppId the id of the application.
     * @param {string} sBaseAppId the base application id.
     * @param {ResolvedHashFragment} oResolvedHashFragment the resolved hash fragment.
     * @param {sap.ushell.services.URLParsing.DecomposedHash} oParsedHash the parsed shell hash.
     * @param {boolean} bShouldBeCached Wether it should be cached.
     * @param {sap.ushell.utils.StagedLogger.Stage} oLogger the logger instance.
     * @returns {sap.ushell.appIntegration.ApplicationContainer} the application container.
     *
     * @private
     */
    AppLifeCycle.prototype._createOrReuseApplicationContainer = function (sStorageAppId, sBaseAppId, oResolvedHashFragment, oParsedHash, bShouldBeCached, oLogger) {
        function logResult (sResult, sReason) {
            oLogger.log(`Container is: '${sResult}' - ${sReason}`, "→");
        }

        let oApplicationContainer;

        const oStorageEntry = KeepAliveApps.get(sStorageAppId);
        // app was kept alive
        if (oStorageEntry && oStorageEntry.container) {
            logResult("kept alive", "Found keep-alive app in storage");
            oApplicationContainer = oStorageEntry.container;
        }

        // try to find a reusable container
        if (!oApplicationContainer) {
            const oStatefulApplicationContainer = ApplicationContainerCache.findFreeContainerByUrl(oResolvedHashFragment.url);

            const bIsReusableContainer = oStatefulApplicationContainer?.getStatefulType() !== StatefulType.NotSupported;
            if (bIsReusableContainer) {
                oApplicationContainer = oStatefulApplicationContainer;
            }
        }

        // create a new container if no reusable container was found
        if (!oApplicationContainer) {
            logResult("created", "No reusable container found");
            // Add new ShellUIService instance of the interface to the application container
            // this will only be used by iframe applications
            const oShellUIServiceInstance = this._oGlobalShellUIService.getInterface();

            const oCommonProperties = {
                dataHelpId: sBaseAppId,
                initialAppId: sStorageAppId,
                currentAppId: sStorageAppId,
                currentAppUrl: oResolvedHashFragment.url,
                currentHash: UrlParsing.constructShellHash(oParsedHash),
                shellUIService: oShellUIServiceInstance,
                currentAppTargetResolution: oResolvedHashFragment,
                applicationType: oResolvedHashFragment.applicationType,
                openWithPostByAppParam: oResolvedHashFragment.openWithPostByAppParam,
                reservedParameters: oResolvedHashFragment.reservedParameters,
                systemAlias: oResolvedHashFragment.systemAlias,
                targetNavigationMode: oResolvedHashFragment.targetNavigationMode,
                url: oResolvedHashFragment.url,
                frameworkId: oResolvedHashFragment.appCapabilities?.appFrameworkId
            };

            const bIsUI5AppWithoutIframe = oResolvedHashFragment.applicationType === "SAPUI5";

            if (bIsUI5AppWithoutIframe) {
                oApplicationContainer = new UI5ApplicationContainer({
                    ...oCommonProperties,
                    ui5ComponentId: oResolvedHashFragment.ui5ComponentId
                });
            } else {
                oApplicationContainer = new IframeApplicationContainer({
                    ...oCommonProperties
                });
            }
            this._oNavContainer.addPage(oApplicationContainer);
        } else {
            logResult("reused", "Found reusable stateful application container by URL");
            oApplicationContainer.setIsFetchedFromCache(true);
            oApplicationContainer.setDataHelpId(sBaseAppId);
        }

        ApplicationContainerCache.setContainerActive(oApplicationContainer);
        oApplicationContainer.setIsKeepAlive(bShouldBeCached);

        return oApplicationContainer;
    };

    /**
     * Removes an application container from the navigation container.
     *
     * @param {sap.ushell.appIntegration.ApplicationContainer} oApplicationContainer The application container
     *
     * @private
     */
    AppLifeCycle.prototype._removeApplicationContainerFromNavContainer = function (oApplicationContainer) {
        this._oNavContainer.removePage(oApplicationContainer.getId(), true);
    };

    /**
     * In the FLP, only one container at a time can be active. If we have
     * multiple ApplicationContainers, they may still be active in the
     * background, and still be able to send/receive postMessages (e.g.,
     * change the title while the user is on the FLP home).
     *
     * Also, we distinguish between visible containers and active
     * containers. As it is desirable that when a container is being opened
     * it starts setting the FLP title for example. It results in better
     * perceived performance.
     *
     * This method sets only one container as active and de-activates all
     * other application containers around.
     *
     * @param {sap.ushell.appIntegration.ApplicationContainer} oTargetApplicationContainer
     *   The application container to activate. Pass <code>null</code> in
     *   case no application container must be activated.
     *
     * @private
     */
    AppLifeCycle.prototype._setApplicationContainerActive = async function (oTargetApplicationContainer) {
        ApplicationContainerCache.forEach((oApplicationContainer) => {
            if (oApplicationContainer && oApplicationContainer !== oTargetApplicationContainer) {
                try {
                    Log.info(`Deactivating container ${oApplicationContainer.getId()}`);
                    oApplicationContainer.setActive(false);
                } catch {
                    /* empty */
                }
            }
        });

        if (oTargetApplicationContainer) {
            Log.info(`Activating container "${oTargetApplicationContainer.getId()}"`);
            oTargetApplicationContainer.setActive(true);
        }
    };

    /**
     * @param {StorageAppId} sStorageAppId the storage app id.
     * @param {sap.ushell.appIntegration.ApplicationContainer} oApplicationContainer the application container.
     * @param {ResolvedHashFragment} oResolvedHashFragment the resolved hash fragment.
     * @param {sap.ushell.services.URLParsing.DecomposedHash} oParsedHash the parsed shell hash.
     * @param {boolean} bWasKeptAlive whether the app should be kept alive.
     * @param {sap.ushell.utils.StagedLogger.Stage} oLogger the logger instance.
     * @returns {Promise} after the application was opened.
     *
     * @private
     */
    AppLifeCycle.prototype._openApp = async function (sStorageAppId, oApplicationContainer, oResolvedHashFragment, oParsedHash, bWasKeptAlive, oLogger) {
        // ===============================================================================
        // ApplicationContainer Reuse Scenario - e.g StatefulContainer or KeepAlive App

        if (oApplicationContainer.getIsFetchedFromCache()) {
            if (oApplicationContainer.getCurrentAppId() === sStorageAppId) {
                oLogger.log("Application is already running within the reuse container.", "→");
                return;
            }

            if (oApplicationContainer.getStatefulType() === StatefulType.ContractV2) {
                if (bWasKeptAlive && StatefulContainerV2Handler.isStatefulContainerSupportingKeepAlive(oApplicationContainer)) {
                    oLogger.log("Restoring keep alive StatefulContainerV2 app within the same frame.", "→");
                    return StatefulContainerV2Handler.restoreAppWithinSameFrame(
                        oApplicationContainer,
                        sStorageAppId,
                        oResolvedHashFragment
                    );
                }
                oLogger.log("Creating app in StatefulContainerV2", "→");
                const oResult = await StatefulContainerV2Handler.createApp(
                    oApplicationContainer,
                    sStorageAppId,
                    oResolvedHashFragment
                );

                if (oResult?.deletedKeepAliveId) {
                    /*
                     * The app was already destroyed within the iframe
                     * We _only_ need to destroy the remaining data in the outer shell
                     * Calling destroy on the iframe again will destroy the current app in the iframe
                     */
                    const oStorageEntryToDestroy = KeepAliveApps.get(oResult.deletedKeepAliveId);
                    if (oStorageEntryToDestroy) {
                        oLogger.log(`Destroying remaining keep-alive app data: ${oResult.deletedKeepAliveId}`, "✗");

                        KeepAliveApps.removeById(oResult.deletedKeepAliveId);
                        KeepAlive.destroy(oStorageEntryToDestroy);
                    }
                }
                return;
            }

            if (oApplicationContainer.getStatefulType() === StatefulType.ContractV1) {
                oLogger.log("Creating app in StatefulContainerV1", "→");
                return StatefulContainerV1Handler.createApp(
                    oApplicationContainer,
                    sStorageAppId,
                    oResolvedHashFragment
                );
            }

            // ApplicationContainer should be reused but does not supports StatefulContainer or already contains the app
            throw new Error("Unknown Application scenario. Cannot open app.");
        }

        // ===============================================================================
        // New ApplicationContainer Scenario

        if (oApplicationContainer.isA("sap.ushell.appIntegration.UI5ApplicationContainer")) {
            oLogger.log("Creating app in new sap.ushell.appIntegration.UI5ApplicationContainer", "→");
            // happens for direct app start scenario on the abap platform (sap/ushell_abap/bootstrap/BootTask)
            const bComponentLoadedInDirectAppStart = !!oResolvedHashFragment?.componentHandle;
            if (!bComponentLoadedInDirectAppStart) {
                AppConfiguration.setApplicationInInitMode();

                await EmbeddedUI5Handler.createApp(oApplicationContainer, oResolvedHashFragment, oParsedHash);
            }

            const oComponentHandle = oResolvedHashFragment.componentHandle;
            const oComponentInstance = oComponentHandle.getInstance();

            oApplicationContainer.setComponentHandle(oComponentHandle);

            this._oCurrentApplication.app = oComponentInstance;

            // The trampoline application subsequently gets destroyed after it's used to enable the redirection.
            const sNavigationRedirectHash = await EmbeddedUI5Handler.getNavigationRedirectHash(oApplicationContainer);
            if (typeof sNavigationRedirectHash === "string") {
                oLogger.log(`Trampoline application, navigation redirect detected: ${sNavigationRedirectHash}`, "→");
                await this.destroyApplication(sStorageAppId, oApplicationContainer);
                return sNavigationRedirectHash;
            }

            return;
        }

        /**
         * Initial ApplicationContainers for iframe applications.
         *
         * The openApp steps happens as part of the rendering.
         * The response cannot be awaited. Therefore, we just continue under the assumption that
         * the app will be opened successfully.
         */
        oLogger.log("Creating app in new sap.ushell.appIntegration.IframeApplicationContainer", "→");
    };

    /**
     * Announces that an application is opening for accessibility purposes.
     *
     * @param {ResolvedHashFragment} oResolvedHashFragment The resolved hash fragment object
     *
     * @private
     */
    AppLifeCycle.prototype._announceAppOpening = function (oResolvedHashFragment) {
        // Screen reader: "Loading Complete"
        window.setTimeout(() => {
            const oAccessibilityHelperLoadingComplete = document.getElementById("sapUshellLoadingAccessibilityHelper-loadingComplete");

            if (oAccessibilityHelperLoadingComplete) {
                oAccessibilityHelperLoadingComplete.setAttribute("aria-live", "polite");
                oAccessibilityHelperLoadingComplete.innerHTML = ushellResources.i18n.getText("loadingComplete");
                window.setTimeout(() => {
                    oAccessibilityHelperLoadingComplete.setAttribute("aria-live", "off");
                    oAccessibilityHelperLoadingComplete.innerHTML = "";
                }, 0);
            }
        }, 600);
    };

    /**
     * Publishes the appOpening event on the event bus.
     *
     * @param {ResolvedHashFragment} oResolvedHashFragment The resolved hash fragment object
     *
     * @private
     */
    AppLifeCycle.prototype.publishAppOpeningEvent = function (oResolvedHashFragment) {
        window.setTimeout(() => { // wrapped in setTimeout since "publish" is not async
            EventBus.getInstance().publish("launchpad", "appOpening", oResolvedHashFragment);
        }, 0);
    };

    /**
     * Publishes the appOpened event on the event bus and external channels.
     *
     * @param {ResolvedHashFragment} oResolvedHashFragment The resolved hash fragment object
     *
     * @private
     */
    AppLifeCycle.prototype.publishAppOpenedEvent = function (oResolvedHashFragment) {
        ushellUtils.setPerformanceMark("FLP.appOpened");

        window.setTimeout(() => { // wrapped in setTimeout since "publish" is not async
            EventBus.getInstance().publish("sap.ushell", "appOpened", oResolvedHashFragment);
        }, 0);

        // the former code leaked an *internal* data structure, making it part of a public API
        // restrict hte public api to the minimal set of precise documented properties which can be retained under
        // under future evolutions
        const oPublicEventData = this._publicEventDataFromResolutionResult(oResolvedHashFragment);
        // publish the event externally
        // Event is emitted internally (EventHub) _and_ externally (for compatibility reasons)
        EventHub.emit("AppRendered", oPublicEventData, true);
        RendererUtils.publishExternalEvent("appOpened", oPublicEventData);
    };

    /**
     * Publishes the appClosed event on the event bus and external channels.
     *
     * @param {ResolvedHashFragment} oResolvedHashFragment The resolved hash fragment object
     *
     * @private
     */
    AppLifeCycle.prototype._publishAppClosedEvent = function (oResolvedHashFragment) {
        window.setTimeout(() => { // wrapped in setTimeout since "publish" is not async
            EventBus.getInstance().publish("sap.ushell", "appClosed", oResolvedHashFragment);
        }, 0);

        // the former code leaked an *internal* data structure, making it part of a public API
        // restrict hte public api to the minimal set of precise documented properties which can be retained under
        // under future evolutions
        const oPublicEventData = this._publicEventDataFromResolutionResult(oResolvedHashFragment);
        // publish the event externally
        RendererUtils.publishExternalEvent("appClosed", oPublicEventData);
    };

    /**
     * Creates a new object Expose a minimal set of values to public external stakeholders
     * only expose what you can guarantee under any evolution of the unified shell on all platforms
     * @param {ResolvedHashFragment} oResolvedHashFragment an internal result of NavTargetResolutionInternal
     * @returns {object} an object exposing certain information to external stakeholders
     *
     * @private
     */
    AppLifeCycle.prototype._publicEventDataFromResolutionResult = function (oResolvedHashFragment) {
        const oPublicEventData = {};
        if (!oResolvedHashFragment) {
            return oResolvedHashFragment;
        }
        ["applicationType", "ui5ComponentName", "url", "additionalInformation", "text"].forEach((sProp) => {
            oPublicEventData[sProp] = oResolvedHashFragment[sProp];
        });
        Object.freeze(oPublicEventData);
        return oPublicEventData;
    };

    /**
     * Calculates whether the application should use the full width.
     *
     * @param {ResolvedHashFragment} oResolvedHashFragment the resolved hash fragment.
     * @param {object} oMetadata the metadata of the application.
     * @returns {boolean} Whether the application should use the width.
     *
     * @private
     */
    AppLifeCycle.prototype._isFullWidth = function (oResolvedHashFragment, oMetadata) {
        let bFullWidth;
        const sAppType = this._calculateAppType(oResolvedHashFragment);
        const bDefaultFullWidth = ApplicationType.getDefaultFullWidthSetting(sAppType);

        const bFullWidthCapability = oResolvedHashFragment.appCapabilities?.fullWidth;
        // Here there's a double check for the fullWidth - once as a type of boolean and one as a string.
        // This is because we found that developers configured this variable in the manifest also as a string,
        // so the checks of the oMetadata and the oResolvedHashFragment are to avoid regression with the use of this field.
        if (bDefaultFullWidth) {
            if (oResolvedHashFragment.fullWidth === false || oResolvedHashFragment.fullWidth === "false" ||
                oMetadata.fullWidth === false || oMetadata.fullWidth === "false" || bFullWidthCapability === false) {
                bFullWidth = false;
            }
        } else if (oResolvedHashFragment.fullWidth || oResolvedHashFragment.fullWidth === "true" ||
            oMetadata.fullWidth || oMetadata.fullWidth === "true") {
            bFullWidth = true;
        }

        if (bFullWidth === undefined) {
            bFullWidth = bDefaultFullWidth;
        }
        return bFullWidth;
    };

    /**
     * Calculates the base app id from the parsed shell hash.
     * This is stable for the same semantic object and action, regardless of parameters or app-specific route.
     * @param {sap.ushell.services.URLParsing.DecomposedHash} oParsedHash The parsed shell hash.
     * @returns {string} The base app id.
     *
     * @private
     */
    AppLifeCycle.prototype._getBaseAppId = function (oParsedHash) {
        const sBasicHash = UrlParsing.constructBasicHash(oParsedHash);
        const sBaseAppId = `application-${sBasicHash}`;

        return sBaseAppId;
    };

    /**
     * Calculates the storage app id from the parsed shell hash and base app id.
     * This is unique for each combination of semantic object, action, parameters excluding the app-specific route.
     * @param {sap.ushell.services.URLParsing.DecomposedHash} oParsedHash The parsed shell hash.
     * @param {string} sBaseAppId The base app id.
     * @returns {StorageAppId} The storage app id.
     *
     * @private
     */
    AppLifeCycle.prototype._getStorageAppId = function (oParsedHash, sBaseAppId) {
        const sStableHash = UrlParsing.constructStableHash(oParsedHash);

        if (this._oStorageAppIdMap[sStableHash]) {
            return this._oStorageAppIdMap[sStableHash];
        }

        let sStorageAppId;
        if (!this._oStorageAppIdNextIndex[sBaseAppId]) {
            this._oStorageAppIdNextIndex[sBaseAppId] = 0;
            sStorageAppId = sBaseAppId; // first time we use the app w/o counter
        }

        if (!sStorageAppId) {
            sStorageAppId = `${sBaseAppId}-${this._oStorageAppIdNextIndex[sBaseAppId]}`;
        }

        // now store the id for the stable hash
        this._oStorageAppIdMap[sStableHash] = sStorageAppId;
        this._oStorageAppIdNextIndex[sBaseAppId]++;

        return sStorageAppId;
    };

    /**
     * Starts an application by creating or reusing an application container.
     *
     * @param {ResolvedHashFragment} oResolvedHashFragment The resolved hash fragment
     * @param {sap.ushell.services.URLParsing.DecomposedHash} oParsedHash The parsed shell hash
     * @param {boolean} bNavigationFromHome Whether the navigation occurred from the home page
     * @param {sap.ushell.utils.StagedLogger} oLogger the logger instance.
     * @returns {Promise<sap.ushell.appIntegration.ApplicationHandle>} The application handle
     *
     * @private
     */
    AppLifeCycle.prototype._startApplication = async function (oResolvedHashFragment, oParsedHash, bNavigationFromHome, oLogger) {
        const sBaseAppId = this._getBaseAppId(oParsedHash);
        const sStorageAppId = this._getStorageAppId(oParsedHash, sBaseAppId);

        // ========================= Stage 1 ================================
        await this._cleanupBeforeNewApp(
            sStorageAppId,
            oResolvedHashFragment,
            oParsedHash,
            bNavigationFromHome,
            oLogger.getStage(1)
        );

        this._sendFocusBackToShell();
        this.publishAppOpeningEvent(oResolvedHashFragment);
        this._announceAppOpening(oResolvedHashFragment);

        const sKeepAliveMode = this._calculateKeepAliveMode(sStorageAppId, oResolvedHashFragment, oParsedHash, oLogger.getStage(1));
        const bShouldBeCached = sKeepAliveMode !== KeepAliveMode.False;

        // ========================= Stage 2 ================================
        const oApplicationContainer = this._createOrReuseApplicationContainer(sStorageAppId, sBaseAppId, oResolvedHashFragment, oParsedHash, bShouldBeCached, oLogger.getStage(2));

        // check after cleanup, but before creating the storage entry
        const bWasKeptAlive = !!KeepAliveApps.get(sStorageAppId);

        // store reference to last application
        this._oPreviousApplication = this._oCurrentApplication;

        // Set current application
        if (bShouldBeCached) {
            this._oCurrentApplication = this._addStorageEntry(
                sStorageAppId,
                oApplicationContainer,
                oResolvedHashFragment,
                oParsedHash,
                sKeepAliveMode
            );
        } else {
            // create application that is not persisted and not cached
            this._oCurrentApplication = {
                appId: sStorageAppId,
                container: oApplicationContainer,
                meta: undefined, // is set after _openApp in _initializeAppWithMetadata
                app: undefined // is set in _openApp
            };
        }
        // invalidate flag before next application start
        this._bUseLegacyRestoreFlowForNextApp = false;

        ushellUtils.setPerformanceMark("FLP - addAppContainer");

        // restore the app info parameters before activating the application container
        const oStorageEntry = KeepAliveApps.get(sStorageAppId);
        if (oStorageEntry) {
            AppInfoParameters.restore(oStorageEntry);
        } else {
            AppInfoParameters.flush();
        }

        oLogger.getStage(3).logLines(
            undefined,
            [
                `App info parameters ${oStorageEntry ? "restored" : "flushed"}`,
                `Cached: ${bShouldBeCached}`,
                `Was kept alive: ${bWasKeptAlive}`,
                `Application type: ${oResolvedHashFragment.applicationType}`
            ],
            "•"
        );

        // enable the PostMessageAPI for the application
        this._setApplicationContainerActive(oApplicationContainer);

        oLogger.getStage(4).log("Opening application", "→");
        const sNavigationRedirectHash = await this._openApp(
            sStorageAppId,
            oApplicationContainer,
            oResolvedHashFragment,
            oParsedHash,
            bWasKeptAlive,
            oLogger.getStage(4)
        );
        const oApplicationHandle = new ApplicationHandle(
            sStorageAppId,
            oResolvedHashFragment,
            oApplicationContainer,
            sNavigationRedirectHash
        );

        if (typeof sNavigationRedirectHash === "string") {
            // do not add the deleted application to the view port
            return oApplicationHandle;
        }

        // initialize now because the app is required for additional information
        this._initializeAppWithMetadata(oApplicationContainer, oResolvedHashFragment, oParsedHash);

        /*
         * finally allow the application container to be rendered
         * Prevent any earlier rendering because the previous steps
         * might alter the application container state, which can
         * and/or should affect the rendering.
         *
         * await next tick to prevent a rerendering of the application
         * container during the rendering phase of the NavContainer
         */
        oApplicationContainer.setReadyForRendering(true);
        ushellUtils.setPerformanceMark("FLP -- centerViewPort");

        this.publishAppOpenedEvent(oResolvedHashFragment);

        oLogger.getStage(5).log("Application successfully started", "✓");

        return oApplicationHandle;
    };

    /**
     * Starts an application with proper error handling and cleanup.
     *
     * @param {ResolvedHashFragment} oResolvedHashFragment The resolved hash fragment
     * @param {sap.ushell.services.URLParsing.DecomposedHash} oParsedHash The parsed shell hash
     * @param {boolean} bNavigationFromHome Whether the navigation occurred from the home page
     * @returns {Promise<sap.ushell.appIntegration.ApplicationHandle>} The application handle
     *
     * @private
     */
    AppLifeCycle.prototype.startApplication = async function (oResolvedHashFragment, oParsedHash, bNavigationFromHome) {
        return this.oApplicationStartQueue.push(async () => {
            // await any pending onAfterNavigate calls to finish before starting a new application.
            // This ensures the strict lifecycle order
            await this.oApplicationNavigateQueue.waitAllSettled();

            const oApplicationBefore = this._oCurrentApplication;
            this._oNavContainer.setBusy(true);

            const sIntent = `${oParsedHash.semanticObject}-${oParsedHash.action}`;
            const oLogger = new StagedLogger({
                title: `Starting Application: ${sIntent}`,
                moduleName: "sap.ushell.appIntegration.AppLifeCycle.startApplication",
                stages: [
                    "STAGE1: Cleanup & preparation",
                    "STAGE2: Container lifecycle",
                    "STAGE3: App initialization",
                    "STAGE4: Open application",
                    "STAGE5: Finalization"
                ]
            });

            try {
                const oApplicationHandle = await this._startApplication(
                    oResolvedHashFragment,
                    oParsedHash,
                    bNavigationFromHome,
                    oLogger
                );
                oLogger.end();

                return oApplicationHandle;
            } catch (oError) {
                oLogger.end();
                // cleanup the application container
                const bCurrentAppIsSet = !!this._oCurrentApplication?.appId;
                const bNewAppWasAlreadySet = bCurrentAppIsSet && this._oCurrentApplication.appId !== oApplicationBefore?.appId;

                if (bNewAppWasAlreadySet) {
                    const oApplicationContainer = this._oCurrentApplication.container;
                    if (oApplicationContainer) {
                        await this.destroyApplication(oApplicationContainer.getCurrentAppId(), oApplicationContainer, true); // force destroy
                    }

                    // restore the old application or stash it
                    const bApplicationBeforeWasDestroyed = oApplicationBefore?.container?.isDestroyed() ?? true;
                    if (bApplicationBeforeWasDestroyed) {
                        this._oCurrentApplication = {};
                    } else {
                        this._oCurrentApplication = oApplicationBefore;
                    }
                }

                this._oNavContainer.setBusy(false);

                throw oError;
            }
        });
    };

    /**
     * Handles the destruction of managed controls for non-keep-alive applications.
     *
     * This must happen BEFORE applying pending state changes because the ControlManager's
     * previous state becomes inaccessible after the state switch.
     * Component name checks in the cleanup routine prevent ID collisions.
     *
     * @since 1.147.0
     * @private
     */
    AppLifeCycle.prototype._cleanupBeforeStateSwitch = async function () {
        const bIsKeepAlive = !!KeepAliveApps.get(this._oPreviousApplication?.appId);
        if (!bIsKeepAlive) {
            StateManager.destroyManagedControls();
        }

        // else... keep-alive apps are managed via storage entries and can be destroyed even when
        // the current state of the ControlManager proceeded.
    };

    /**
     * Switches the view state when a new app is created.
     *
     * @param {sap.ushell.state.StateManager.LaunchpadState} sLaunchpadState The launchpad state
     * @param {StorageAppId} sToStorageAppId The application id that is taken from storage
     * @param {sap.ushell.components.container.ApplicationType} sApplicationType The app type
     * @param {boolean} bExplicitNavMode Whether there is an explicit navigation mode
     *
     * @private
     */
    AppLifeCycle.prototype.switchViewState = function (sLaunchpadState, sToStorageAppId, sApplicationType, bExplicitNavMode) {
        this._cleanupBeforeStateSwitch();

        const sShellMode = StateManager.getShellMode();
        // GUI Applications need a back button to work properly
        const oShellModeOverrides = {
            TR: {
                headerless: ShellMode.Minimal
            }
        };

        let sShellModeOverride;
        if (!bExplicitNavMode) {
            sShellModeOverride = oShellModeOverrides[sApplicationType]?.[sShellMode];
        }
        StateManager.switchState(sLaunchpadState, sShellModeOverride);

        // Restore state if it already exists
        const oToStorageEntry = KeepAliveApps.get(sToStorageAppId);
        if (oToStorageEntry) {
            KeepAlive.restore(oToStorageEntry);
        } else {
            KeepAlive.flush();
        }

        // Process Dangling UI elements and continue.
        StateManager.applyStalledChanges();
    };

    /**
     * Sends the focus back to the shell in desktop scenarios to prevent the focus from being lost after an app switch.
     * @private
     */
    AppLifeCycle.prototype._sendFocusBackToShell = function () {
        if (!Device.system.desktop) {
            return;
        }

        sap.ui.require(["sap/ushell/renderer/AccessKeysHandler"], (AccessKeysHandler) => {
            const sCurrentLaunchpadState = StateManager.getLaunchpadState();
            const bDefaultShellMode = StateManager.getShellMode() === ShellMode.Default;
            const oShellAppTitle = Element.getElementById("shellAppTitle");

            if (oShellAppTitle && sCurrentLaunchpadState === LaunchpadState.App && bDefaultShellMode) {
                const oShellAppTitleDomRef = oShellAppTitle.getFocusDomRef();
                if (oShellAppTitleDomRef) {
                    AccessKeysHandler.sendFocusBackToShell(oShellAppTitleDomRef.getAttribute("id"));
                }
            }
        });
    };

    /**
     * Initializes the application with its metadata and applies default styling.
     *
     * @param {sap.ushell.appIntegration.ApplicationContainer} oApplicationContainer The application container
     * @param {ResolvedHashFragment} oResolvedHashFragment The resolved hash fragment
     * @param {sap.ushell.services.URLParsing.DecomposedHash} oParsedHash The parsed shell hash
     *
     * @private
     */
    AppLifeCycle.prototype._initializeAppWithMetadata = function (oApplicationContainer, oResolvedHashFragment, oParsedHash) {
        const oMetadata = AppConfiguration.getMetadata(oResolvedHashFragment, oParsedHash);

        this._oCurrentApplication.meta = oMetadata;

        // ApplicationContainer is newly created
        if (!oApplicationContainer.getIsFetchedFromCache()) {
            oApplicationContainer.toggleStyleClass("sapUshellDefaultBackground", !oMetadata.hideLightBackground);

            const bFullWidth = this._isFullWidth(oResolvedHashFragment, oMetadata);
            if (!bFullWidth) {
                oApplicationContainer.addStyleClass("sapUShellApplicationContainerLimitedWidth");
            }
        }

        if (oApplicationContainer.isA("sap.ushell.appIntegration.UI5ApplicationContainer")) {
            EmbeddedUI5Handler.initializeMetadata(oApplicationContainer);
        }

        let sIcon = "sap-icon://folder";
        if (oMetadata && oMetadata.icon) {
            sIcon = oMetadata.icon;
        }
        StateManager.updateCurrentState("application.icon", Operation.Set, sIcon);
    };

    /**
     *  - prevent duplicated ids in the NavContainer.
     *  - prevent reuse of stateful containers for different servers.
     *  - destroy the keep alive app for certain navigation cases.
     *  - destroy the app if the iframe is invalid/unresponsive.
     *  - exit the current application if it is a stateful container.
     *
     * @param {StorageAppId} sStorageAppId the storage app id.
     * @param {ResolvedHashFragment} oResolvedHashFragment the resolved hash fragment object.
     * @param {sap.ushell.services.URLParsing.DecomposedHash} oParsedHash  the parsed shell hash.
     * @param {boolean} bNavigationFromHome whether the navigation occurred from the home page.
     * @param {sap.ushell.utils.StagedLogger.Stage} oLogger the logger.
     * @returns {Promise} after the application was destroyed.
     *
     * @private
     */
    AppLifeCycle.prototype._cleanupBeforeNewApp = async function (sStorageAppId, oResolvedHashFragment, oParsedHash, bNavigationFromHome, oLogger) {
        oLogger.log(
            [
                "Target application details:",
                `Storage ID: ${sStorageAppId}`,
                `URL: ${oResolvedHashFragment.url || "<none>"}`,
                `Navigation from home: ${bNavigationFromHome}`,
                `Has inner app route: ${!!oParsedHash.appSpecificRoute}`
            ].join("\n"),
            "•"
        );

        // ====== Exit current iframe app ======
        /*
         * for stateful containers, we need to exit/close the current application BEFORE any
         * navigation in the NavContainer
         */
        const oCurrentApplicationContainer = this.getCurrentApplication()?.container;
        const bIsReusableContainer = oCurrentApplicationContainer && oCurrentApplicationContainer.getStatefulType() !== StatefulType.NotSupported;
        if (bIsReusableContainer) {
            oLogger.log("Container is stateful: Exiting current application.", "→");
            await this._handleExitApplication(oCurrentApplicationContainer, false);
        }

        const oStorageEntry = KeepAliveApps.get(sStorageAppId);
        const bTargetAppWasKeptAlive = !!oStorageEntry;
        // the appId on the storage entry and the appId on the application container might be different.
        const oStoredApplicationContainer = oStorageEntry?.container;

        const bIsUI5AppWithoutIframe = oResolvedHashFragment.applicationType === "SAPUI5";

        // ====== Handle special navigation scenarios for keep alive apps ======
        if (bTargetAppWasKeptAlive) {
            const bNavigationWithInnerAppRoute = !!oParsedHash.appSpecificRoute;

            // Case: Navigating from homepage to a keepAlive application via deep link (bookmark).
            // In this case, although keepAlive is active we need to destroy the application and re-open it.
            if (bNavigationFromHome && bNavigationWithInnerAppRoute) {
                oLogger.log("Destroying keep-alive app due to bookmark navigation from home.", "→");
                Log.warning(`The keepAlive app '${sStorageAppId}' has to be destroyed because of a bookmark start.`);
                await this.destroyApplication(sStorageAppId, oStoredApplicationContainer);
            }

            // => we are navigating to the right keep alive app, so we keep it alive
        }

        // ====== Ensure that component name and id clashes are prevented ======
        if (bIsUI5AppWithoutIframe && !bTargetAppWasKeptAlive) {
            oLogger.log("Checking for component name/id clashes with existing applications.", "→");

            if (oCurrentApplicationContainer && oCurrentApplicationContainer.isA("sap.ushell.appIntegration.UI5ApplicationContainer")) {
                const bHasIdConflicts = EmbeddedUI5Handler.hasIdConflicts(
                    oResolvedHashFragment,
                    oCurrentApplicationContainer,
                    oLogger
                );
                if (bHasIdConflicts) {
                    Log.warning(`The app '${oCurrentApplicationContainer.getCurrentAppId()}' has to be destroyed because of id clashes.`);
                    await this.destroyApplication(oCurrentApplicationContainer.getCurrentAppId(), oCurrentApplicationContainer, true); // force destroy
                }
            }

            // close all keep alive apps with the same component name (except the current one)
            // otherwise we have clashes on the component name
            await this._destroyKeepAliveApps((oStorageEntry) => {
                if (oStorageEntry.appId === sStorageAppId) {
                    return false; // skip the current one
                }

                const bHasIdConflicts = EmbeddedUI5Handler.hasIdConflicts(
                    oResolvedHashFragment,
                    oStorageEntry.container,
                    oLogger
                );

                if (bHasIdConflicts) {
                    Log.warning(`The app '${oStorageEntry.container.getCurrentAppId()}' has to be destroyed because of id clashes.`);
                    return true;
                }

                return false;
            }, true); // force destroy
        }

        // ====== Cleanup invalid iframes ======
        // check all stateful containers whether they're still valid. if not, destroy them
        // before we try to create the new application within the invalid stateful container
        await ApplicationContainerCache.forEach(async (oPotentialApplicationContainer) => {
            if (oPotentialApplicationContainer.getStatefulType() !== StatefulType.ContractV2) {
                return;
            }

            try {
                oPotentialApplicationContainer.isValid();
                return;
            } catch (oError) {
                oLogger.log(`Destroying invalid stateful container: ${oPotentialApplicationContainer.getId()}`, "→");
                Log.warning(
                    `Destroying stateful container iframe due to unresponsiveness (${oPotentialApplicationContainer.getId()})`,
                    `reason: ${oError.message}`,
                    "sap.ushell.appIntegration.AppLifeCycle"
                );

                // not able to exit gracefully, destroy the container directly forcefully
                return this.destroyApplication(sStorageAppId, oPotentialApplicationContainer, true); // force destroy
            }
        });
    };

    /**
     * Only for testing
     * @param {sap.ui.core.Control} oNewNavContainer the new navigation container.
     *
     * @private
     */
    AppLifeCycle.prototype.setNavContainer = function (oNewNavContainer) {
        this._oNavContainer = oNewNavContainer;
    };

    /**
     * Prepares an application container for potential reuse.
     *
     * @param {sap.ushell.appIntegration.ApplicationContainer} oApplicationContainer The application container
     *
     * @private
     */
    AppLifeCycle.prototype._setContainerReadyForReuse = function (oApplicationContainer) {
        if (!oApplicationContainer) {
            return;
        }

        ApplicationContainerCache.setContainerReadyForReuse(oApplicationContainer, oApplicationContainer.getCurrentAppUrl());

        oApplicationContainer.setIsKeepAlive(false);
        oApplicationContainer.setCurrentAppUrl("");
        oApplicationContainer.setIsFetchedFromCache(false);
    };

    /**
     * Only for testing - resets the AppLifeCycle to its initial state.
     *
     * @private
     */
    AppLifeCycle.prototype.reset = function () {
        this._aDoables.forEach((oDoable) => {
            oDoable.off();
        });
        this._aDoables = [];

        KeepAliveApps.reset();
        ApplicationContainerCache.destroyAllContainers();
        PostMessageManager.reset();

        this._initInternals();
    };

    return new AppLifeCycle();
});
