Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: handle timeout issues in splashscreen (VO-231) #1162

Merged
merged 2 commits into from
Feb 9, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -220,6 +223,7 @@ const Wrapper = () => {

useEffect(() => {
initFlagshipUIService()
setTimeoutForSplashScreen()
}, [])

return (
Expand Down
2 changes: 1 addition & 1 deletion src/app/theme/SplashScreenService.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
})
Expand Down
217 changes: 175 additions & 42 deletions src/app/theme/SplashScreenService.ts
Original file line number Diff line number Diff line change
@@ -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<string, NodeJS.Timeout | undefined> = {}

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<VisibilityStatus> => {
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<void> => {
devlog(`☁️ showSplashScreen called with ${bootsplashName ?? 'undefined'}`)
splashScreenLogger.debug(
`Attempting to show splash screen "${bootsplashName}"`
)

flagshipUIEventHandler.emit(
flagshipUIEvents.SET_COMPONENT_COLORS,
Expand All @@ -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 })
}

/**
Expand All @@ -72,28 +78,155 @@ 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<void> => {
// 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,
`Splashscreen`,
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<VisibilityStatus> => {
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<void> => {
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<string, boolean> = {}
Ldoppea marked this conversation as resolved.
Show resolved Hide resolved

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]) {
Ldoppea marked this conversation as resolved.
Show resolved Hide resolved
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
Ldoppea marked this conversation as resolved.
Show resolved Hide resolved
clearTimeout(autoHideTimers[splashScreen])
Ldoppea marked this conversation as resolved.
Show resolved Hide resolved
Reflect.deleteProperty(autoHideTimers, splashScreen)
}
})
}

AppState.addEventListener('change', nextAppState => {
if (nextAppState === 'active') {
resetTimersOnActive()
} else if (nextAppState === 'background') {
clearTimersOnBackground()
}
})
3 changes: 3 additions & 0 deletions src/libs/httpserver/indexGenerator.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import { shouldDisableGetIndex } from '/core/tools/env'
jest.mock('react-native', () => ({
Platform: {
OS: 'ios'
},
AppState: {
addEventListener: jest.fn()
}
}))

Expand Down
Loading