From 33843de1cd587d8544c0613c925b84deb9e5d187 Mon Sep 17 00:00:00 2001 From: Antonin Cezard Date: Thu, 8 Feb 2024 15:17:10 +0100 Subject: [PATCH 1/2] fix: handle timeout issues in splashscreen Improve timeout management and add initial global timeout --- src/App.js | 6 +- src/app/theme/SplashScreenService.spec.ts | 2 +- src/app/theme/SplashScreenService.ts | 217 +++++++++++++++++---- src/libs/httpserver/indexGenerator.spec.js | 3 + 4 files changed, 184 insertions(+), 44 deletions(-) diff --git a/src/App.js b/src/App.js index 8ef51ae98..6f3a2969e 100644 --- a/src/App.js +++ b/src/App.js @@ -48,7 +48,10 @@ import { import { useOsReceiveApi } from '/app/view/OsReceive/hooks/useOsReceiveApi' import { LockScreenWrapper } from '/app/view/Lock/LockScreenWrapper' import { useSecureBackgroundSplashScreen } from '/hooks/useSplashScreen' -import { hideSplashScreen } from '/app/theme/SplashScreenService' +import { + hideSplashScreen, + setTimeoutForSplashScreen +} from '/app/theme/SplashScreenService' import { initFlagshipUIService } from '/app/view/FlagshipUI' import { useLauncherContext, @@ -220,6 +223,7 @@ const Wrapper = () => { useEffect(() => { initFlagshipUIService() + setTimeoutForSplashScreen() }, []) return ( diff --git a/src/app/theme/SplashScreenService.spec.ts b/src/app/theme/SplashScreenService.spec.ts index e8ab85109..ddd0b49dd 100644 --- a/src/app/theme/SplashScreenService.spec.ts +++ b/src/app/theme/SplashScreenService.spec.ts @@ -73,7 +73,7 @@ describe('SplashScreenService', () => { jest.advanceTimersByTime(newDuration) expect(RNBootSplash.hide).toHaveBeenLastCalledWith({ - bootsplashName: undefined, + bootsplashName: 'global', // Default bootsplash name when none is provided fade: true }) }) diff --git a/src/app/theme/SplashScreenService.ts b/src/app/theme/SplashScreenService.ts index ba00fbfbf..59fb1e90f 100644 --- a/src/app/theme/SplashScreenService.ts +++ b/src/app/theme/SplashScreenService.ts @@ -1,36 +1,53 @@ +import { AppState } from 'react-native' import RNBootSplash, { VisibilityStatus } from 'react-native-bootsplash' +import Minilog from 'cozy-minilog' + import { flagshipUIEventHandler, flagshipUIEvents } from '/app/view/FlagshipUI' -import { devlog } from '/core/tools/env' import { logToSentry } from '/libs/monitoring/Sentry' import config from '/app/theme/config.json' +const splashScreenLogger = Minilog('☁️ SplashScreen') + export const splashScreens = { LOCK_SCREEN: 'LOCK_SCREEN', - SECURE_BACKGROUND: 'secure_background' // this mirrors native declaration + SECURE_BACKGROUND: 'secure_background', // this mirrors native declaration + GLOBAL: 'global' } as const type SplashScreenEnumKeys = keyof typeof splashScreens export type SplashScreenEnum = (typeof splashScreens)[SplashScreenEnumKeys] -let autoHideTimer: NodeJS.Timeout | null = null +// Using a map to handle multiple timers +const autoHideTimers: Record = {} + let autoHideDuration = config.autoHideDuration // Default timeout duration in milliseconds export const setAutoHideDuration = (duration: number): void => { autoHideDuration = duration } +/** + * Retrieves the status of the splash screen ("visible" | "hidden" | "transitioning"). + * @returns A promise that resolves to the visibility status of the splash screen. + */ +export const getSplashScreenStatus = (): Promise => { + return RNBootSplash.getVisibilityStatus() +} + /** * Shows the splash screen with the specified bootsplash name. - * If no bootsplash name is provided, it will default to 'undefined'. + * If no bootsplash name is provided, it will default to a unique identifier 'global'. * * @param bootsplashName - The name of the bootsplash. * @returns A promise that resolves when the splash screen is shown. */ -export const showSplashScreen = ( - bootsplashName?: SplashScreenEnum +export const showSplashScreen = async ( + bootsplashName: SplashScreenEnum | undefined = splashScreens.GLOBAL ): Promise => { - devlog(`☁️ showSplashScreen called with ${bootsplashName ?? 'undefined'}`) + splashScreenLogger.debug( + `Attempting to show splash screen "${bootsplashName}"` + ) flagshipUIEventHandler.emit( flagshipUIEvents.SET_COMPONENT_COLORS, @@ -41,29 +58,18 @@ export const showSplashScreen = ( } ) - if (autoHideTimer) { - clearTimeout(autoHideTimer) - } + setTimeoutForSplashScreen(bootsplashName) - // Auto-hide the splash screen after a certain duration - // This mitigates the issue of the splash screen not being hidden for unforeseen reasons - if (bootsplashName !== splashScreens.SECURE_BACKGROUND) { - autoHideTimer = setTimeout(() => { - hideSplashScreen(bootsplashName).catch(error => { - devlog(`☁️ hideSplashScreen error:`, error) - }) - - logToSentry( - new Error( - `Splashscreen reached autoHideDuration with bootsplahName: ${ - bootsplashName ?? 'undefined' - } and autoHideDuration: ${autoHideDuration}` - ) - ) - }, autoHideDuration) + try { + await RNBootSplash.show({ fade: true, bootsplashName }) + return splashScreenLogger.info(`Splash screen shown "${bootsplashName}"`) + } catch (error) { + splashScreenLogger.error( + `Error showing splash screen: ${bootsplashName}`, + error + ) + logToSentry(error) } - - return RNBootSplash.show({ fade: true, bootsplashName }) } /** @@ -72,14 +78,12 @@ export const showSplashScreen = ( * @param bootsplashName - Optional name of the bootsplash. * @returns A promise that resolves when the splash screen is hidden. */ -export const hideSplashScreen = ( - bootsplashName?: SplashScreenEnum +export const hideSplashScreen = async ( + bootsplashName: SplashScreenEnum | undefined = splashScreens.GLOBAL ): Promise => { - // Clear the auto-hide timer as we don't want to hide the splash screen twice - if (autoHideTimer) { - clearTimeout(autoHideTimer) - autoHideTimer = null - } + splashScreenLogger.debug( + `Attempting to hide splash screen "${bootsplashName}"` + ) flagshipUIEventHandler.emit( flagshipUIEvents.SET_COMPONENT_COLORS, @@ -87,13 +91,142 @@ export const hideSplashScreen = ( undefined ) - return RNBootSplash.hide({ fade: true, bootsplashName }) + try { + await manageTimersAndHideSplashScreen(bootsplashName) + return splashScreenLogger.info(`Splash screen hidden "${bootsplashName}"`) + } catch (error) { + splashScreenLogger.error( + `Error hiding splash screen: ${bootsplashName}`, + error + ) + logToSentry(error) + } } -/** - * Retrieves the status of the splash screen ("visible" | "hidden" | "transitioning"). - * @returns A promise that resolves to the visibility status of the splash screen. - */ -export const getSplashScreenStatus = (): Promise => { - return RNBootSplash.getVisibilityStatus() +export const setTimeoutForSplashScreen = ( + bootsplashName: SplashScreenEnum | undefined = splashScreens.GLOBAL +): void => { + if (bootsplashName === splashScreens.SECURE_BACKGROUND) { + splashScreenLogger.info( + `Skipping timeout for secure background "${bootsplashName}"` + ) + return + } + + destroyTimer(bootsplashName) + + autoHideTimers[bootsplashName] = setTimeout(() => { + splashScreenLogger.warn( + `Auto-hide duration reached for splash screen "${bootsplashName}"` + ) + + logToSentry( + new Error( + `Splashscreen reached autoHideDuration with bootsplashName "${bootsplashName}"` + ) + ) + + void manageTimersAndHideSplashScreen(bootsplashName, true) + }, autoHideDuration) + + splashScreenLogger.debug( + `Setting timeout with ID "${JSON.stringify( + autoHideTimers[bootsplashName] + )}" for splash screen "${bootsplashName}"` + ) +} + +const manageTimersAndHideSplashScreen = async ( + bootsplashName: SplashScreenEnum | undefined = splashScreens.GLOBAL, + fromTimeout = false +): Promise => { + if (bootsplashName !== splashScreens.SECURE_BACKGROUND) + destroyTimer(bootsplashName, fromTimeout) + + try { + await RNBootSplash.hide({ fade: true, bootsplashName }) + } catch (error) { + splashScreenLogger.error( + `Error managing timers and hiding splash screen "${bootsplashName}"`, + error + ) + logToSentry(error) + } +} + +const destroyTimer = ( + bootsplashName: SplashScreenEnum, + fromTimeout = false +): void => { + const timer = autoHideTimers[bootsplashName] + + if (autoHideTimers[bootsplashName]) { + if (fromTimeout) { + splashScreenLogger.debug( + `Destroying existing timer with ID "${JSON.stringify( + timer + )}" for splash screen "${bootsplashName}" after auto-hide duration reached` + ) + } else { + splashScreenLogger.debug( + `Clearing existing timer with ID "${JSON.stringify( + timer + )}" for splash screen "${bootsplashName}" after manual hide` + ) + } + + clearTimeout(autoHideTimers[bootsplashName]) + Reflect.deleteProperty(autoHideTimers, bootsplashName) + } } + +let activeTimersAtBackground: Record = {} + +const resetTimersOnActive = (): void => { + splashScreenLogger.debug( + `App is becoming active. Evaluating timers to reset...` + ) + + const timersToReset = Object.keys(activeTimersAtBackground) + + if (timersToReset.length === 0) { + splashScreenLogger.info(`No splash screen timers to reset.`) + } else { + timersToReset.forEach(splashScreen => { + if (activeTimersAtBackground[splashScreen]) { + splashScreenLogger.info( + `Resetting timer for splash screen "${splashScreen}"` + ) + setTimeoutForSplashScreen(splashScreen as SplashScreenEnum) + } + }) + } + + // Clear the record once timers are reset + activeTimersAtBackground = {} +} + +const clearTimersOnBackground = (): void => { + splashScreenLogger.debug( + `App is going to background. Clearing active timers...` + ) + + Object.keys(autoHideTimers).forEach(splashScreen => { + if (autoHideTimers[splashScreen]) { + splashScreenLogger.info( + `Clearing timer for splash screen "${splashScreen}". Will reset on active.` + ) + activeTimersAtBackground[splashScreen] = true // Mark as active + clearTimeout(autoHideTimers[splashScreen]) + Reflect.deleteProperty(autoHideTimers, splashScreen) + } + }) +} + +AppState.addEventListener('change', nextAppState => { + if (nextAppState === 'active') { + resetTimersOnActive() + } else if (nextAppState === 'background') { + clearTimersOnBackground() + } +}) diff --git a/src/libs/httpserver/indexGenerator.spec.js b/src/libs/httpserver/indexGenerator.spec.js index 08fbd0b3e..dd8d84ade 100644 --- a/src/libs/httpserver/indexGenerator.spec.js +++ b/src/libs/httpserver/indexGenerator.spec.js @@ -17,6 +17,9 @@ import { shouldDisableGetIndex } from '/core/tools/env' jest.mock('react-native', () => ({ Platform: { OS: 'ios' + }, + AppState: { + addEventListener: jest.fn() } })) From 137135e9c0eda592406c4d236b58bc239b68c581 Mon Sep 17 00:00:00 2001 From: Antonin Cezard Date: Fri, 9 Feb 2024 14:52:17 +0100 Subject: [PATCH 2/2] refactor: improve readability in splashscreen Typing and runtime logic --- src/app/theme/SplashScreenService.ts | 44 ++++++++++++++-------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/app/theme/SplashScreenService.ts b/src/app/theme/SplashScreenService.ts index 59fb1e90f..00c109bdd 100644 --- a/src/app/theme/SplashScreenService.ts +++ b/src/app/theme/SplashScreenService.ts @@ -19,7 +19,10 @@ type SplashScreenEnumKeys = keyof typeof splashScreens export type SplashScreenEnum = (typeof splashScreens)[SplashScreenEnumKeys] // Using a map to handle multiple timers -const autoHideTimers: Record = {} +const autoHideTimers = {} as Record< + SplashScreenEnum | string, + NodeJS.Timeout | undefined +> let autoHideDuration = config.autoHideDuration // Default timeout duration in milliseconds @@ -180,30 +183,26 @@ const destroyTimer = ( } } -let activeTimersAtBackground: Record = {} +let activeTimersAtBackground: SplashScreenEnum[] = [] const resetTimersOnActive = (): void => { splashScreenLogger.debug( `App is becoming active. Evaluating timers to reset...` ) - const timersToReset = Object.keys(activeTimersAtBackground) - - if (timersToReset.length === 0) { + if (activeTimersAtBackground.length === 0) { splashScreenLogger.info(`No splash screen timers to reset.`) } else { - timersToReset.forEach(splashScreen => { - if (activeTimersAtBackground[splashScreen]) { - splashScreenLogger.info( - `Resetting timer for splash screen "${splashScreen}"` - ) - setTimeoutForSplashScreen(splashScreen as SplashScreenEnum) - } + activeTimersAtBackground.forEach(splashScreen => { + splashScreenLogger.info( + `Resetting timer for splash screen "${splashScreen}"` + ) + setTimeoutForSplashScreen(splashScreen) }) } // Clear the record once timers are reset - activeTimersAtBackground = {} + activeTimersAtBackground = [] } const clearTimersOnBackground = (): void => { @@ -211,15 +210,16 @@ const clearTimersOnBackground = (): void => { `App is going to background. Clearing active timers...` ) - Object.keys(autoHideTimers).forEach(splashScreen => { - if (autoHideTimers[splashScreen]) { - splashScreenLogger.info( - `Clearing timer for splash screen "${splashScreen}". Will reset on active.` - ) - activeTimersAtBackground[splashScreen] = true // Mark as active - clearTimeout(autoHideTimers[splashScreen]) - Reflect.deleteProperty(autoHideTimers, splashScreen) - } + const activeTimers = Object.keys(autoHideTimers) as SplashScreenEnum[] + + activeTimers.forEach(splashScreen => { + splashScreenLogger.info( + `Clearing timer for splash screen "${splashScreen}". Will reset on active.` + ) + + activeTimersAtBackground.push(splashScreen) + + destroyTimer(splashScreen) }) }