diff --git a/App.tsx b/App.tsx
index 125fe1b..53c4e1f 100644
--- a/App.tsx
+++ b/App.tsx
@@ -2,116 +2,406 @@
* Sample React Native App
* https://github.com/facebook/react-native
*
+ * Generated with the TypeScript template
+ * https://github.com/react-native-community/react-native-template-typescript
+ *
* @format
*/
import React from 'react';
-import type {PropsWithChildren} from 'react';
import {
SafeAreaView,
ScrollView,
StatusBar,
StyleSheet,
Text,
- useColorScheme,
View,
+ Switch,
+ Button,
+ Alert,
+ PermissionsAndroid,
+ // ToastAndroid,
+ NativeModules,
} from 'react-native';
-import {
- Colors,
- DebugInstructions,
- Header,
- LearnMoreLinks,
- ReloadInstructions,
-} from 'react-native/Libraries/NewAppScreen';
+import BackgroundFetch from 'react-native-background-fetch';
+import Geolocation, {
+ GeolocationError,
+ GeolocationResponse,
+} from '@react-native-community/geolocation';
-type SectionProps = PropsWithChildren<{
- title: string;
-}>;
+const {ForegroundHeadlessModule} = NativeModules;
-function Section({children, title}: SectionProps): React.JSX.Element {
- const isDarkMode = useColorScheme() === 'dark';
- return (
-
-
- {title}
-
-
- {children}
-
-
- );
+const Colors = {
+ gold: '#fedd1e',
+ black: '#000',
+ white: '#fff',
+ lightGrey: '#ccc',
+ blue: '#337AB7',
+ brick: '#973920',
+};
+
+/// Util class for handling fetch-event peristence in AsyncStorage.
+import Event from './src/Event';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+
+Geolocation.setRNConfiguration({
+ skipPermissionRequests: true,
+ locationProvider: 'auto',
+});
+
+const requestPermissions = async () => {
+ try {
+ const granted = await PermissionsAndroid.request(
+ PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
+ {
+ title: 'Need permission to access location',
+ message:
+ 'Location access is needed ' + 'so we can track your location!',
+ buttonNegative: 'Cancel',
+ buttonPositive: 'OK',
+ },
+ );
+ const granted2 = await PermissionsAndroid.request(
+ PermissionsAndroid.PERMISSIONS.ACCESS_BACKGROUND_LOCATION,
+ {
+ title: 'Need permission to access location',
+ message:
+ 'Location access is needed ' + 'so we can track your location!',
+ buttonNegative: 'Cancel',
+ buttonPositive: 'OK',
+ },
+ );
+ if (
+ granted === PermissionsAndroid.RESULTS.GRANTED &&
+ granted2 === PermissionsAndroid.RESULTS.GRANTED
+ ) {
+ console.log('You can use the location');
+ } else {
+ console.log('Location permission denied');
+ }
+ } catch (err) {
+ console.warn(err);
+ }
+};
+
+function geoLocationPromise(): Promise {
+ return new Promise((resolve, reject) => {
+ Geolocation.getCurrentPosition(
+ info => {
+ console.log(info);
+ resolve(info);
+ },
+ error => {
+ console.log(error);
+ reject(error);
+ },
+ {
+ timeout: 20000,
+ maximumAge: 0,
+ // enableHighAccuracy: true,
+ },
+ );
+ });
}
-function App(): React.JSX.Element {
- const isDarkMode = useColorScheme() === 'dark';
+const App = () => {
+ const [enabled, setEnabled] = React.useState(false);
+ const [status, setStatus] = React.useState(-1);
+ const [events, setEvents] = React.useState([]);
- const backgroundStyle = {
- backgroundColor: isDarkMode ? Colors.darker : Colors.lighter,
+ React.useEffect(() => {
+ async function asyncTask() {
+ await requestPermissions();
+ initBackgroundFetch();
+ loadEvents();
+ }
+
+ asyncTask();
+ }, []);
+
+ /// Configure BackgroundFetch.
+ ///
+ const initBackgroundFetch = async () => {
+ const status: number = await BackgroundFetch.configure(
+ {
+ minimumFetchInterval: 15, // <-- minutes (15 is minimum allowed)
+ stopOnTerminate: false,
+ enableHeadless: true,
+ startOnBoot: false,
+ // Android options
+ forceAlarmManager: false, // <-- Set true to bypass JobScheduler.
+ requiredNetworkType: BackgroundFetch.NETWORK_TYPE_NONE, // Default
+ requiresCharging: false, // Default
+ requiresDeviceIdle: false, // Default
+ requiresBatteryNotLow: false, // Default
+ requiresStorageNotLow: false, // Default
+ },
+ async (taskId: string) => {
+ console.log('[BackgroundFetch] taskId', taskId);
+
+ if (taskId === 'react-native-background-fetch') {
+ // console.log('[BackgroundFetch] taskId', taskId);
+ const event = await Event.create(taskId, false);
+ // Update state.
+ setEvents(prev => [...prev, event]);
+ }
+
+ if (taskId === 'com.transistorsoft.customtask') {
+ // Geolocation.getCurrentPosition(
+ // async info => {
+ // console.log(info);
+ // const event = await Event.create(taskId, false, `lat-${info.coords.latitude};long-${info.coords.longitude}`);
+ // },
+ // err => console.log(err),
+ // {
+ // timeout: 10000,
+ // maximumAge: 10000,
+ // enableHighAccuracy: false,
+ // },
+ // );
+ try {
+ const locInfo = (await geoLocationPromise()) as GeolocationResponse;
+ const event = await Event.create(
+ taskId,
+ false,
+ `lat-${locInfo.coords.latitude};long-${locInfo.coords.longitude}`,
+ );
+ // Update state.
+ setEvents(prev => [...prev, event]);
+ } catch (error) {
+ const g = error as GeolocationError;
+ if (g.message) {
+ const event = await Event.create(taskId, false, `${g.message}`);
+ // Update state.
+ setEvents(prev => [...prev, event]);
+ } else {
+ const event = await Event.create(taskId, false);
+ // Update state.
+ setEvents(prev => [...prev, event]);
+ }
+ }
+ // console.log('Running [customtask] here!');
+ }
+
+ // Finish.
+ BackgroundFetch.finish(taskId);
+ },
+ (taskId: string) => {
+ // Oh No! Our task took too long to complete and the OS has signalled
+ // that this task must be finished immediately.
+ console.log('[Fetch] TIMEOUT taskId:', taskId);
+ BackgroundFetch.finish(taskId);
+ },
+ );
+ setStatus(status);
+ setEnabled(true);
};
- return (
-
-
-
-
-
-
- Edit App.tsx to change this
- screen and then come back to see your edits.
-
-
-
-
- Read the docs to discover what to do next:
-
-
+ /// Load persisted events from AsyncStorage.
+ ///
+ const loadEvents = () => {
+ Event.all()
+ .then(data => {
+ setEvents(data as Event[]);
+ })
+ .catch(error => {
+ Alert.alert('Error', 'Failed to load data from AsyncStorage: ' + error);
+ });
+ };
+
+ /// Toggle BackgroundFetch ON/OFF
+ ///
+ const onClickToggleEnabled = (value: boolean) => {
+ setEnabled(value);
+
+ if (value) {
+ BackgroundFetch.start();
+ } else {
+ BackgroundFetch.stop();
+ }
+ };
+
+ /// [Status] button handler.
+ ///
+ const onClickStatus = () => {
+ BackgroundFetch.status().then((status: number) => {
+ let statusConst = '';
+ switch (status) {
+ case BackgroundFetch.STATUS_AVAILABLE:
+ statusConst = 'STATUS_AVAILABLE';
+ break;
+ case BackgroundFetch.STATUS_DENIED:
+ statusConst = 'STATUS_DENIED';
+ break;
+ case BackgroundFetch.STATUS_RESTRICTED:
+ statusConst = 'STATUS_RESTRICTED';
+ break;
+ }
+ Alert.alert('BackgroundFetch.status()', `${statusConst} (${status})`);
+ });
+ };
+
+ /// [scheduleTask] button handler.
+ /// Schedules a custom-task to fire in 5000ms
+ ///
+ const onClickScheduleTask = () => {
+ const delay = 600000;
+ BackgroundFetch.scheduleTask({
+ taskId: 'com.transistorsoft.customtask',
+ stopOnTerminate: false,
+ enableHeadless: true,
+ delay: delay,
+ forceAlarmManager: false,
+ periodic: true,
+ })
+ .then(() => {
+ Alert.alert(
+ 'scheduleTask',
+ 'Scheduled task with delay: ' + delay + 'ms',
+ );
+ })
+ .catch(error => {
+ Alert.alert('scheduleTask ERROR', error);
+ });
+ };
+
+ /// Clear the Events list.
+ ///
+ const onClickClear = () => {
+ Event.destroyAll();
+ setEvents([]);
+ };
+
+ /// Fetch events renderer.
+ ///
+ const renderEvents = () => {
+ if (!events.length) {
+ return (
+
+ Waiting for BackgroundFetch events...
+
+ );
+ }
+ return events
+ .slice()
+ .reverse()
+ .map(event => (
+
+
+
+ {event.taskId} {event.isHeadless ? '[Headless]' : ''}
+
+
+ Remark - {event.location}
+ {event.timestamp}
-
+ ));
+ };
+
+ async function onLocPress(): Promise {
+ try {
+ // const locInfo = (await geoLocationPromise()) as GeolocationResponse;
+ // ToastAndroid.show(
+ // `lat-${locInfo.coords.latitude};long-${locInfo.coords.longitude}`,
+ // 5000,
+ // );
+ ForegroundHeadlessModule.startService();
+ console.log('Did it run?');
+ } catch (error) {
+ const g = error as GeolocationError;
+ if (g.message) {
+ console.log(g.message);
+ } else {
+ console.log(error);
+ }
+ }
+ }
+
+ function onLocStop() {
+ ForegroundHeadlessModule.stopService();
+ AsyncStorage.getItem('watchId', (err, result) => {
+ if (err) {
+ console.log('Couldnt get WatchID', err);
+ return;
+ }
+
+ console.log(result);
+
+ if (result) {
+ Geolocation.clearWatch(+result);
+ }
+ });
+ }
+
+ return (
+
+
+
+
+ BGFetch Demo
+
+
+
+
+
+ {renderEvents()}
+
+
+
+
+
+
+
+
+
);
-}
+};
const styles = StyleSheet.create({
- sectionContainer: {
- marginTop: 32,
- paddingHorizontal: 24,
+ container: {
+ flexDirection: 'column',
+ flex: 1,
},
- sectionTitle: {
+ title: {
fontSize: 24,
- fontWeight: '600',
+ flex: 1,
+ fontWeight: 'bold',
+ color: Colors.black,
},
- sectionDescription: {
- marginTop: 8,
- fontSize: 18,
- fontWeight: '400',
+ eventList: {
+ flex: 1,
+ backgroundColor: Colors.white,
},
- highlight: {
- fontWeight: '700',
+ event: {
+ padding: 10,
+ borderBottomWidth: 1,
+ borderColor: Colors.lightGrey,
+ },
+ taskId: {
+ color: Colors.blue,
+ fontSize: 16,
+ fontWeight: 'bold',
+ },
+ headless: {
+ fontWeight: 'bold',
+ },
+ remark: {
+ color: Colors.brick,
+ },
+ timestamp: {
+ color: Colors.black,
+ },
+ toolbar: {
+ height: 57,
+ flexDirection: 'row',
+ paddingLeft: 10,
+ paddingRight: 10,
+ alignItems: 'center',
+ backgroundColor: Colors.gold,
},
});
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index e189252..b116e9d 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,26 +1,49 @@
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+ android:roundIcon="@mipmap/ic_launcher_round"
+ android:supportsRtl="true"
+ android:theme="@style/AppTheme">
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
\ No newline at end of file
diff --git a/android/app/src/main/java/com/poclocationgather/ForegroundHeadlessModule.java b/android/app/src/main/java/com/poclocationgather/ForegroundHeadlessModule.java
new file mode 100644
index 0000000..5063ce6
--- /dev/null
+++ b/android/app/src/main/java/com/poclocationgather/ForegroundHeadlessModule.java
@@ -0,0 +1,44 @@
+package com.poclocationgather;
+
+import android.content.Intent;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.bridge.ReactContextBaseJavaModule;
+import com.facebook.react.bridge.ReactMethod;
+
+public class ForegroundHeadlessModule extends ReactContextBaseJavaModule {
+ private final ReactApplicationContext reactContext;
+
+ public ForegroundHeadlessModule(ReactApplicationContext reactContext) {
+ super(reactContext);
+ this.reactContext = reactContext;
+ }
+
+ @NonNull
+ @Override
+ public String getName() {
+ return "ForegroundHeadlessModule";
+ }
+
+ @ReactMethod
+ public void startService() {
+ Intent serviceIntent = new Intent(reactContext, ForegroundHeadlessService.class);
+ serviceIntent.setAction("START_FOREGROUND");
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ this.reactContext.startForegroundService(serviceIntent);
+ } else {
+ this.reactContext.startService(serviceIntent);
+ }
+ }
+
+ @ReactMethod
+ public void stopService() {
+ Intent serviceIntent = new Intent(reactContext, ForegroundHeadlessService.class);
+ serviceIntent.setAction("STOP_FOREGROUND");
+ this.reactContext.stopService(serviceIntent);
+ }
+}
diff --git a/android/app/src/main/java/com/poclocationgather/ForegroundHeadlessPackage.java b/android/app/src/main/java/com/poclocationgather/ForegroundHeadlessPackage.java
new file mode 100644
index 0000000..ce0b237
--- /dev/null
+++ b/android/app/src/main/java/com/poclocationgather/ForegroundHeadlessPackage.java
@@ -0,0 +1,29 @@
+package com.poclocationgather;
+
+import androidx.annotation.NonNull;
+
+import com.facebook.react.ReactPackage;
+import com.facebook.react.bridge.NativeModule;
+import com.facebook.react.bridge.ReactApplicationContext;
+import com.facebook.react.uimanager.ViewManager;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class ForegroundHeadlessPackage implements ReactPackage {
+
+ @NonNull
+ @Override
+ public List createNativeModules(@NonNull ReactApplicationContext reactApplicationContext) {
+ List modules = new ArrayList<>();
+ modules.add(new ForegroundHeadlessModule(reactApplicationContext));
+ return modules;
+ }
+
+ @NonNull
+ @Override
+ public List createViewManagers(@NonNull ReactApplicationContext reactApplicationContext) {
+ return Collections.emptyList();
+ }
+}
diff --git a/android/app/src/main/java/com/poclocationgather/ForegroundHeadlessService.java b/android/app/src/main/java/com/poclocationgather/ForegroundHeadlessService.java
new file mode 100644
index 0000000..654b694
--- /dev/null
+++ b/android/app/src/main/java/com/poclocationgather/ForegroundHeadlessService.java
@@ -0,0 +1,109 @@
+package com.poclocationgather;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+import android.os.Handler;
+import android.os.IBinder;
+import android.util.Log;
+
+import androidx.core.app.NotificationCompat;
+
+import com.facebook.react.HeadlessJsTaskService;
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.jstasks.HeadlessJsTaskConfig;
+
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.Nullable;
+
+public class ForegroundHeadlessService extends Service {
+
+ private static final String TAG = "ForegroundHeadlessService";
+ private static final String CHANNEL_ID = "location_channel";
+ private static final int NOTIFICATION_ID = 1001;
+
+ private Handler handler = new Handler();
+
+ private Runnable runnableCode = new Runnable() {
+ @Override
+ public void run() {
+ Context context = getApplicationContext();
+
+ // Start GeolocationHeadlessTask
+ Intent headlessTaskIntent = new Intent(context, GeolocationHeadlessTask.class);
+ context.startService(headlessTaskIntent);
+
+ // Acquire wake lock
+ HeadlessJsTaskService.acquireWakeLockNow(context);
+
+ // Schedule next execution
+ handler.postDelayed(this, 180000);
+ }
+ };
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ createNotificationChannel();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ this.handler.removeCallbacks(this.runnableCode); // Stop runnable execution
+ }
+
+ private void createNotificationChannel() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ NotificationChannel channel = new NotificationChannel(
+ CHANNEL_ID,
+ "POC Location Gather Service",
+ NotificationManager.IMPORTANCE_MIN
+ );
+ NotificationManager manager = getSystemService(NotificationManager.class);
+ manager.createNotificationChannel(channel);
+ }
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent != null && intent.getAction() != null && intent.getAction().equals("START_FOREGROUND")) {
+ Log.d(TAG, "Starting foreground headless service");
+ startForeground(NOTIFICATION_ID, createNotification());
+
+ // Start headlessJS Task that is going to access location
+// Intent headlessIntent = new Intent(getApplicationContext(), GeolocationHeadlessTask.class);
+// getApplicationContext().startService(headlessIntent);
+ this.handler.post(this.runnableCode);
+ } else if (intent != null && intent.getAction() != null && intent.getAction().equals("STOP_FOREGROUND")) {
+ Log.d(TAG, "Stopping foreground headless service");
+ stopForeground(true);
+ stopSelf();
+ }
+ return START_STICKY;
+ }
+
+ private Notification createNotification() {
+ Intent notificationIntent = new Intent(this, MainActivity.class);
+ PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE);
+
+ return new NotificationCompat.Builder(this, CHANNEL_ID)
+ .setContentTitle("Location Gather")
+ .setContentText("Background location is active")
+ .setSmallIcon(R.mipmap.ic_launcher)
+ .setContentIntent(pendingIntent)
+ .build();
+ }
+
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/com/poclocationgather/GeolocationHeadlessTask.java b/android/app/src/main/java/com/poclocationgather/GeolocationHeadlessTask.java
new file mode 100644
index 0000000..dd5f509
--- /dev/null
+++ b/android/app/src/main/java/com/poclocationgather/GeolocationHeadlessTask.java
@@ -0,0 +1,25 @@
+package com.poclocationgather;
+
+import android.content.Intent;
+import android.os.Bundle;
+
+import com.facebook.react.HeadlessJsTaskService;
+import com.facebook.react.bridge.Arguments;
+import com.facebook.react.jstasks.HeadlessJsTaskConfig;
+
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.Nullable;
+
+public class GeolocationHeadlessTask extends HeadlessJsTaskService {
+ @Override
+ protected @Nullable HeadlessJsTaskConfig getTaskConfig(Intent intent) {
+ Bundle extras = intent.getExtras();
+ return new HeadlessJsTaskConfig(
+ "GeolocationBackgroundTask",
+ extras != null ? Arguments.fromBundle(extras) : Arguments.createMap(),
+ TimeUnit.MINUTES.toMillis(5),
+ true
+ );
+ }
+}
diff --git a/android/app/src/main/java/com/poclocationgather/MainApplication.kt b/android/app/src/main/java/com/poclocationgather/MainApplication.kt
index 82992a4..011a537 100644
--- a/android/app/src/main/java/com/poclocationgather/MainApplication.kt
+++ b/android/app/src/main/java/com/poclocationgather/MainApplication.kt
@@ -19,7 +19,7 @@ class MainApplication : Application(), ReactApplication {
override fun getPackages(): List =
PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example:
- // add(MyReactNativePackage())
+ add(ForegroundHeadlessPackage())
}
override fun getJSMainModuleName(): String = "index"
diff --git a/android/build.gradle b/android/build.gradle
index a62d6da..0629672 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -18,4 +18,22 @@ buildscript {
}
}
+allprojects {
+ repositories {
+ mavenLocal()
+ maven {
+ // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
+ url("$rootDir/../node_modules/react-native/android")
+ }
+ maven {
+ // Android JSC is installed from npm
+ url("$rootDir/../node_modules/jsc-android/dist")
+ }
+ maven {
+ // react-native-background-fetch
+ url("${project(':react-native-background-fetch').projectDir}/libs")
+ }
+ }
+}
+
apply plugin: "com.facebook.react.rootproject"
diff --git a/index.js b/index.js
index a850d03..163cc31 100644
--- a/index.js
+++ b/index.js
@@ -6,4 +6,8 @@ import {AppRegistry} from 'react-native';
import App from './App';
import {name as appName} from './app.json';
+AppRegistry.registerHeadlessTask('GeolocationBackgroundTask', () =>
+ require('./src/task/watchPosition'),
+);
+
AppRegistry.registerComponent(appName, () => App);
diff --git a/package-lock.json b/package-lock.json
index bb32898..9c35ee2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,8 +8,11 @@
"name": "POCLocationGather",
"version": "0.0.1",
"dependencies": {
+ "@react-native-async-storage/async-storage": "^2.1.2",
+ "@react-native-community/geolocation": "^3.4.0",
"react": "18.3.1",
- "react-native": "0.77.0"
+ "react-native": "0.77.0",
+ "react-native-background-fetch": "^4.2.7"
},
"devDependencies": {
"@babel/core": "^7.25.2",
@@ -2763,6 +2766,18 @@
"node": ">= 8"
}
},
+ "node_modules/@react-native-async-storage/async-storage": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.1.2.tgz",
+ "integrity": "sha512-dvlNq4AlGWC+ehtH12p65+17V0Dx7IecOWl6WanF2ja38O1Dcjjvn7jVzkUHJ5oWkQBlyASurTPlTHgKXyYiow==",
+ "license": "MIT",
+ "dependencies": {
+ "merge-options": "^3.0.4"
+ },
+ "peerDependencies": {
+ "react-native": "^0.0.0-0 || >=0.65 <1.0"
+ }
+ },
"node_modules/@react-native-community/cli": {
"version": "15.0.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-15.0.1.tgz",
@@ -2996,6 +3011,19 @@
"node": ">=10"
}
},
+ "node_modules/@react-native-community/geolocation": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/@react-native-community/geolocation/-/geolocation-3.4.0.tgz",
+ "integrity": "sha512-bzZH89/cwmpkPMKKveoC72C4JH0yF4St5Ceg/ZM9pA1SqX9MlRIrIrrOGZ/+yi++xAvFDiYfihtn9TvXWU9/rA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/@react-native/assets-registry": {
"version": "0.77.0",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.77.0.tgz",
@@ -7497,6 +7525,15 @@
"node": ">=8"
}
},
+ "node_modules/is-plain-obj": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
+ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-plain-object": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
@@ -9281,6 +9318,18 @@
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
"license": "MIT"
},
+ "node_modules/merge-options": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz",
+ "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-plain-obj": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/merge-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
@@ -10699,6 +10748,12 @@
}
}
},
+ "node_modules/react-native-background-fetch": {
+ "version": "4.2.7",
+ "resolved": "https://registry.npmjs.org/react-native-background-fetch/-/react-native-background-fetch-4.2.7.tgz",
+ "integrity": "sha512-lR8MmQRjd7MV9KBSOsyxFRW7jeCjNOSvsT4PECwZv54iZZtxUkRjH8Kbr6SGiSPK1AmYM6TCdRBEt2I9FDZJEg==",
+ "license": "MIT"
+ },
"node_modules/react-native/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
diff --git a/package.json b/package.json
index 504209c..84593df 100644
--- a/package.json
+++ b/package.json
@@ -10,8 +10,11 @@
"test": "jest"
},
"dependencies": {
+ "@react-native-async-storage/async-storage": "^2.1.2",
+ "@react-native-community/geolocation": "^3.4.0",
"react": "18.3.1",
- "react-native": "0.77.0"
+ "react-native": "0.77.0",
+ "react-native-background-fetch": "^4.2.7"
},
"devDependencies": {
"@babel/core": "^7.25.2",
@@ -36,4 +39,4 @@
"engines": {
"node": ">=18"
}
-}
\ No newline at end of file
+}
diff --git a/src/Event.ts b/src/Event.ts
new file mode 100644
index 0000000..3fe4c95
--- /dev/null
+++ b/src/Event.ts
@@ -0,0 +1,82 @@
+import AsyncStorage from '@react-native-async-storage/async-storage';
+
+export default class Event {
+ taskId: string;
+ isHeadless: boolean;
+ timestamp: string;
+ location: string;
+ key: string;
+
+ static destroyAll() {
+ AsyncStorage.setItem('events', JSON.stringify([]));
+ }
+
+ static async create(taskId: string, isHeadless: boolean, loc?: string) {
+ const event = new Event(taskId, isHeadless, undefined, loc);
+
+ // Persist event into AsyncStorage.
+ AsyncStorage.getItem('events')
+ .then(json => {
+ const data = json === null ? [] : JSON.parse(json);
+ data.push(event.toJson());
+ AsyncStorage.setItem('events', JSON.stringify(data));
+ })
+ .catch(error => {
+ console.error('Event.create error: ', error);
+ });
+ return event;
+ }
+
+ static async all() {
+ return new Promise((resolve, reject) => {
+ AsyncStorage.getItem('events')
+ .then(json => {
+ const data = json === null ? [] : JSON.parse(json);
+ resolve(
+ data.map((record: any) => {
+ return new Event(
+ record.taskId,
+ record.isHeadless,
+ record.timestamp,
+ record.loc,
+ );
+ }),
+ );
+ })
+ .catch(error => {
+ console.error('Event.create error: ', error);
+ reject(error);
+ });
+ });
+ }
+
+ constructor(
+ taskId: string,
+ isHeadless: boolean,
+ timestamp?: string,
+ loc?: string,
+ ) {
+ if (!timestamp) {
+ const now: Date = new Date();
+ timestamp = now.toLocaleDateString() + ' ' + now.toLocaleTimeString();
+ }
+ if (!loc) {
+ loc = 'not available';
+ }
+
+ this.taskId = taskId;
+ this.isHeadless = isHeadless;
+ this.timestamp = timestamp;
+ this.location = loc;
+ this.key = `${this.taskId}-${this.timestamp}`;
+ }
+
+ toJson() {
+ return {
+ taskId: this.taskId,
+ timestamp: this.timestamp,
+ isHeadless: this.isHeadless,
+ loc: this.location,
+ };
+ }
+}
diff --git a/src/task/watchPosition.ts b/src/task/watchPosition.ts
new file mode 100644
index 0000000..85ace61
--- /dev/null
+++ b/src/task/watchPosition.ts
@@ -0,0 +1,29 @@
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import Geolocation from '@react-native-community/geolocation';
+
+module.exports = async () => {
+ console.log('Background Geolocation task has hopefully started');
+
+ Geolocation.setRNConfiguration({
+ skipPermissionRequests: false,
+ locationProvider: 'playServices',
+ });
+
+ const watchId = Geolocation.getCurrentPosition(
+ pos =>
+ console.log(
+ '[Background location]',
+ pos.coords.latitude,
+ '//',
+ pos.coords.longitude,
+ ),
+ err => console.log('Location Error while running in Background', err),
+ {
+ interval: 180000,
+ maximumAge: 0,
+ timeout: 20000,
+ },
+ );
+
+ await AsyncStorage.setItem('watchId', `${watchId}`);
+};