diff --git a/src/location/emulatorService.js b/src/location/emulatorService.js
new file mode 100644
index 0000000..deea548
--- /dev/null
+++ b/src/location/emulatorService.js
@@ -0,0 +1,110 @@
+import BackgroundGeolocation from "react-native-background-geolocation";
+import AsyncStorage from "@react-native-async-storage/async-storage";
+import { createLogger } from "~/lib/logger";
+import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
+
+const EMULATOR_MODE_KEY = "emulator_mode_enabled";
+
+// Global variables
+let emulatorIntervalId = null;
+let isEmulatorModeEnabled = false;
+
+// Create a logger for the emulator service
+const emulatorLogger = createLogger({
+ module: BACKGROUND_SCOPES.GEOLOCATION,
+ feature: "emulator",
+});
+
+// Initialize emulator mode based on stored preference
+export const initEmulatorMode = async () => {
+ try {
+ const storedValue = await AsyncStorage.getItem(EMULATOR_MODE_KEY);
+ emulatorLogger.debug("Initializing emulator mode", { storedValue });
+
+ if (storedValue === "true") {
+ await enableEmulatorMode();
+ }
+ } catch (error) {
+ emulatorLogger.error("Failed to initialize emulator mode", {
+ error: error.message,
+ stack: error.stack,
+ });
+ }
+};
+
+// Enable emulator mode
+export const enableEmulatorMode = async () => {
+ emulatorLogger.info("Enabling emulator mode");
+
+ // Clear existing interval if any
+ if (emulatorIntervalId) {
+ clearInterval(emulatorIntervalId);
+ }
+
+ try {
+ // Call immediately once
+ await BackgroundGeolocation.changePace(true);
+ emulatorLogger.debug("Initial changePace call successful");
+
+ // Then set up interval
+ emulatorIntervalId = setInterval(
+ () => {
+ BackgroundGeolocation.changePace(true);
+ emulatorLogger.debug("Interval changePace call executed");
+ },
+ 30 * 60 * 1000,
+ ); // 30 minutes
+
+ isEmulatorModeEnabled = true;
+
+ // Persist the setting
+ await AsyncStorage.setItem(EMULATOR_MODE_KEY, "true");
+ emulatorLogger.debug("Emulator mode setting saved");
+ } catch (error) {
+ emulatorLogger.error("Failed to enable emulator mode", {
+ error: error.message,
+ stack: error.stack,
+ });
+ }
+};
+
+// Disable emulator mode
+export const disableEmulatorMode = async () => {
+ emulatorLogger.info("Disabling emulator mode");
+
+ if (emulatorIntervalId) {
+ clearInterval(emulatorIntervalId);
+ emulatorIntervalId = null;
+ }
+
+ isEmulatorModeEnabled = false;
+
+ // Persist the setting
+ try {
+ await AsyncStorage.setItem(EMULATOR_MODE_KEY, "false");
+ emulatorLogger.debug("Emulator mode setting saved");
+ } catch (error) {
+ emulatorLogger.error("Failed to save emulator mode setting", {
+ error: error.message,
+ stack: error.stack,
+ });
+ }
+};
+
+// Get current emulator mode state
+export const getEmulatorModeState = () => {
+ return isEmulatorModeEnabled;
+};
+
+// Toggle emulator mode
+export const toggleEmulatorMode = async (enabled) => {
+ emulatorLogger.info("Toggling emulator mode", { enabled });
+
+ if (enabled) {
+ await enableEmulatorMode();
+ } else {
+ await disableEmulatorMode();
+ }
+
+ return isEmulatorModeEnabled;
+};
diff --git a/src/location/trackLocation.js b/src/location/trackLocation.js
index 314b079..00d76f3 100644
--- a/src/location/trackLocation.js
+++ b/src/location/trackLocation.js
@@ -3,6 +3,7 @@ import { TRACK_MOVE } from "~/misc/devicePrefs";
import { createLogger } from "~/lib/logger";
import { BACKGROUND_SCOPES } from "~/lib/logger/scopes";
import jwtDecode from "jwt-decode";
+import { initEmulatorMode } from "./emulatorService";
import {
getAuthState,
@@ -271,4 +272,7 @@ export default async function trackLocation() {
// Check for pending records after a short delay to ensure everything is initialized
setTimeout(checkPendingRecords, 5000);
+
+ // Initialize emulator mode if previously enabled
+ initEmulatorMode();
}
diff --git a/src/scenes/Developer/index.js b/src/scenes/Developer/index.js
index 8499c95..1737f87 100644
--- a/src/scenes/Developer/index.js
+++ b/src/scenes/Developer/index.js
@@ -1,6 +1,7 @@
-import React, { useState } from "react";
+import React, { useState, useEffect } from "react";
import { View, StyleSheet, ScrollView } from "react-native";
import * as Sentry from "@sentry/react-native";
+import BackgroundGeolocation from "react-native-background-geolocation";
import {
Button,
Card,
@@ -12,6 +13,11 @@ import {
import { createStyles } from "~/theme";
import env, { setStaging } from "~/env";
import { authActions } from "~/stores";
+import {
+ getEmulatorModeState,
+ toggleEmulatorMode as toggleEmulatorModeService,
+ initEmulatorMode,
+} from "~/location/emulatorService";
const reset = async () => {
await authActions.logout();
@@ -29,7 +35,50 @@ const Section = ({ title, children }) => {
export default function Developer() {
const styles = useStyles();
+ const { colors } = useTheme();
const [isStaging, setIsStaging] = useState(env.IS_STAGING);
+ const [emulatorMode, setEmulatorMode] = useState(false);
+ const [syncStatus, setSyncStatus] = useState(null); // null, 'syncing', 'success', 'error'
+ const [syncResult, setSyncResult] = useState("");
+
+ // Initialize emulator mode when component mounts
+ useEffect(() => {
+ // Initialize the emulator service
+ initEmulatorMode();
+
+ // Set the initial state based on the global service
+ setEmulatorMode(getEmulatorModeState());
+ }, []);
+
+ // Handle toggling emulator mode
+ const handleEmulatorModeToggle = async (enabled) => {
+ const newState = await toggleEmulatorModeService(enabled);
+ setEmulatorMode(newState);
+ };
+
+ // Function to trigger geolocation sync
+ const triggerGeolocSync = async () => {
+ try {
+ setSyncStatus("syncing");
+ setSyncResult("");
+
+ // Get the count of pending records first
+ const count = await BackgroundGeolocation.getCount();
+
+ // Perform the sync
+ const records = await BackgroundGeolocation.sync();
+
+ const result = `Synced ${
+ records?.length || 0
+ } records (${count} pending)`;
+ setSyncResult(result);
+ setSyncStatus("success");
+ } catch (error) {
+ console.error("Geolocation sync failed:", error);
+ setSyncResult(`Sync failed: ${error.message}`);
+ setSyncStatus("error");
+ }
+ };
const triggerNullError = () => {
try {
// Wrap the null error in try-catch
@@ -101,6 +150,13 @@ export default function Developer() {
}}
/>
+
+ Emulator Mode
+
+
@@ -150,6 +206,35 @@ export default function Developer() {
+
+
+
+ {syncStatus && syncResult && (
+
+ {syncResult}
+
+ )}
+
+
+
+