diff --git a/index.js b/index.js index 6e578f9..4586999 100644 --- a/index.js +++ b/index.js @@ -6,7 +6,6 @@ import "expo-splash-screen"; import BackgroundGeolocation from "react-native-background-geolocation"; import { Platform } from "react-native"; -import BackgroundFetch from "react-native-background-fetch"; import notifee from "@notifee/react-native"; import messaging from "@react-native-firebase/messaging"; @@ -56,62 +55,4 @@ const HeadlessTask = async (event) => { if (Platform.OS === "android") { BackgroundGeolocation.registerHeadlessTask(HeadlessTask); -} else if (Platform.OS === "ios") { - BackgroundGeolocation.onLocation(async () => { - await executeHeartbeatSync(); - }); - - BackgroundGeolocation.onMotionChange(async () => { - await executeHeartbeatSync(); - }); - - // Configure BackgroundFetch for iOS (iOS-specific configuration) - BackgroundFetch.configure( - { - minimumFetchInterval: 15, // Only valid option for iOS - gives best chance of execution - }, - // Event callback - async (taskId) => { - let syncResult = null; - - try { - // Execute the shared heartbeat logic and get result - syncResult = await executeHeartbeatSync(); - } catch (error) { - // silent error - } finally { - // CRITICAL: Always call finish with appropriate result - try { - if (taskId) { - let fetchResult; - - if (syncResult?.error || !syncResult?.syncSuccessful) { - // Task failed - fetchResult = BackgroundFetch.FETCH_RESULT_FAILED; - } else if ( - syncResult?.syncPerformed && - syncResult?.syncSuccessful - ) { - // Force sync was performed successfully - new data - fetchResult = BackgroundFetch.FETCH_RESULT_NEW_DATA; - } else { - // No sync was needed - no new data - fetchResult = BackgroundFetch.FETCH_RESULT_NO_DATA; - } - - BackgroundFetch.finish(taskId, fetchResult); - } - } catch (finishError) { - // silent error - } - } - }, - // Timeout callback (REQUIRED by BackgroundFetch API) - async (taskId) => { - // CRITICAL: Must call finish on timeout with FAILED result - BackgroundFetch.finish(taskId, BackgroundFetch.FETCH_RESULT_FAILED); - }, - ).catch(() => { - // silent error - }); } diff --git a/src/app/index.js b/src/app/index.js index a54a5c8..c7e819a 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -25,6 +25,8 @@ import { useUpdates } from "~/updates"; import Error from "~/components/Error"; import useTrackLocation from "~/hooks/useTrackLocation"; +import { initializeBackgroundFetch } from "~/services/backgroundFetch"; +import useMount from "~/hooks/useMount"; const appLogger = createLogger({ module: SYSTEM_SCOPES.APP, @@ -219,6 +221,23 @@ function AppContent() { useNetworkListener(); useTrackLocation(); + useMount(() => { + const setupBackgroundFetch = async () => { + try { + appLogger.info("Setting up BackgroundFetch"); + await initializeBackgroundFetch(); + appLogger.debug("BackgroundFetch setup completed"); + } catch (error) { + lifecycleLogger.error("BackgroundFetch setup failed", { + error: error?.message, + }); + errorHandler(error); + } + }; + + setupBackgroundFetch(); + }); + // Handle deep links after app is initialized with error handling useEffect(() => { let subscription; diff --git a/src/location/backgroundTask.js b/src/location/backgroundTask.js index 2ba859e..62ec416 100644 --- a/src/location/backgroundTask.js +++ b/src/location/backgroundTask.js @@ -4,8 +4,8 @@ import { STORAGE_KEYS } from "~/storage/storageKeys"; import { createLogger } from "~/lib/logger"; // Constants for persistence -const FORCE_SYNC_INTERVAL = 12 * 60 * 60 * 1000; -// const FORCE_SYNC_INTERVAL = 5 * 60 * 1000; // DEBUGGING +// const FORCE_SYNC_INTERVAL = 12 * 60 * 60 * 1000; +const FORCE_SYNC_INTERVAL = 1 * 60 * 1000; // DEBUGGING const geolocBgLogger = createLogger({ service: "background-task", @@ -73,21 +73,50 @@ export const executeHeartbeatSync = async () => { const lastSyncTime = await getLastSyncTime(); const now = Date.now(); const timeSinceLastSync = now - lastSyncTime; + if (timeSinceLastSync >= FORCE_SYNC_INTERVAL) { - geolocBgLogger.info("Forcing location sync"); + geolocBgLogger.info("Forcing location sync", { + timeSinceLastSync, + forceInterval: FORCE_SYNC_INTERVAL, + }); + try { - await Promise.race([ - async () => { - await executeSync(); - }, + const syncResult = await Promise.race([ + executeSync(), new Promise((_, reject) => setTimeout(() => reject(new Error("changePace timeout")), 10000), ), ]); await setLastSyncTime(now); + + geolocBgLogger.info("Force sync completed successfully", { + syncResult, + }); + + return syncResult; } catch (syncError) { - geolocBgLogger.error("Force sync failed", { error: syncError }); + geolocBgLogger.error("Force sync failed", { + error: syncError.message, + timeSinceLastSync, + }); + + return { + syncPerformed: true, + syncSuccessful: false, + error: syncError.message, + }; } + } else { + geolocBgLogger.debug("Sync not needed yet", { + timeSinceLastSync, + forceInterval: FORCE_SYNC_INTERVAL, + timeUntilNextSync: FORCE_SYNC_INTERVAL - timeSinceLastSync, + }); + + return { + syncPerformed: false, + syncSuccessful: true, + }; } }; diff --git a/src/services/backgroundFetch.js b/src/services/backgroundFetch.js new file mode 100644 index 0000000..803ff6b --- /dev/null +++ b/src/services/backgroundFetch.js @@ -0,0 +1,109 @@ +import { Platform } from "react-native"; +import BackgroundFetch from "react-native-background-fetch"; +import { createLogger } from "~/lib/logger"; +import { executeHeartbeatSync } from "~/location/backgroundTask"; + +const backgroundFetchLogger = createLogger({ + service: "background-fetch", + task: "service", +}); + +/** + * Initialize BackgroundFetch according to the documentation best practices. + * This should be called once when the root component mounts. + */ +export const initializeBackgroundFetch = async () => { + try { + backgroundFetchLogger.info("Initializing BackgroundFetch service"); + + // Configure BackgroundFetch for both platforms + const status = await BackgroundFetch.configure( + { + minimumFetchInterval: 15, // Only valid option - gives best chance of execution + }, + // Event callback - handles both default fetch events and custom scheduled tasks + async (taskId) => { + backgroundFetchLogger.info("BackgroundFetch event received", { + taskId, + }); + + let syncResult = null; + + try { + // Execute the shared heartbeat logic and get result + syncResult = await executeHeartbeatSync(); + backgroundFetchLogger.debug("Heartbeat sync completed", { + syncResult, + }); + } catch (error) { + backgroundFetchLogger.error("Heartbeat sync failed", { + error: error.message, + taskId, + }); + } finally { + // CRITICAL: Always call finish with appropriate result + try { + if (taskId) { + let fetchResult; + + if (syncResult?.error || !syncResult?.syncSuccessful) { + // Task failed + fetchResult = BackgroundFetch.FETCH_RESULT_FAILED; + } else if ( + syncResult?.syncPerformed && + syncResult?.syncSuccessful + ) { + // Force sync was performed successfully - new data + fetchResult = BackgroundFetch.FETCH_RESULT_NEW_DATA; + } else { + // No sync was needed - no new data + fetchResult = BackgroundFetch.FETCH_RESULT_NO_DATA; + } + + BackgroundFetch.finish(taskId, fetchResult); + backgroundFetchLogger.debug("BackgroundFetch task finished", { + taskId, + fetchResult, + }); + } + } catch (finishError) { + backgroundFetchLogger.error( + "Failed to finish BackgroundFetch task", + { + error: finishError.message, + taskId, + }, + ); + } + } + }, + // Timeout callback (REQUIRED by BackgroundFetch API) + async (taskId) => { + backgroundFetchLogger.warn("BackgroundFetch task timeout", { taskId }); + // CRITICAL: Must call finish on timeout with FAILED result + try { + BackgroundFetch.finish(taskId, BackgroundFetch.FETCH_RESULT_FAILED); + } catch (error) { + backgroundFetchLogger.error("Failed to finish timed out task", { + error: error.message, + taskId, + }); + } + }, + ); + + backgroundFetchLogger.info("BackgroundFetch configured successfully", { + status, + platform: Platform.OS, + }); + + return status; + } catch (error) { + backgroundFetchLogger.error("Failed to initialize BackgroundFetch", { + error: error.message, + stack: error.stack, + platform: Platform.OS, + }); + throw error; + } +};