Working proof of concept to run location in the background

This commit is contained in:
Tanmaya Biswal 2025-03-31 10:41:57 +05:30
parent 550d211462
commit bb6c8b5ea4
13 changed files with 815 additions and 104 deletions

452
App.tsx
View File

@ -2,116 +2,406 @@
* Sample React Native App * Sample React Native App
* https://github.com/facebook/react-native * https://github.com/facebook/react-native
* *
* Generated with the TypeScript template
* https://github.com/react-native-community/react-native-template-typescript
*
* @format * @format
*/ */
import React from 'react'; import React from 'react';
import type {PropsWithChildren} from 'react';
import { import {
SafeAreaView, SafeAreaView,
ScrollView, ScrollView,
StatusBar, StatusBar,
StyleSheet, StyleSheet,
Text, Text,
useColorScheme,
View, View,
Switch,
Button,
Alert,
PermissionsAndroid,
// ToastAndroid,
NativeModules,
} from 'react-native'; } from 'react-native';
import { import BackgroundFetch from 'react-native-background-fetch';
Colors, import Geolocation, {
DebugInstructions, GeolocationError,
Header, GeolocationResponse,
LearnMoreLinks, } from '@react-native-community/geolocation';
ReloadInstructions,
} from 'react-native/Libraries/NewAppScreen';
type SectionProps = PropsWithChildren<{ const {ForegroundHeadlessModule} = NativeModules;
title: string;
}>;
function Section({children, title}: SectionProps): React.JSX.Element { const Colors = {
const isDarkMode = useColorScheme() === 'dark'; gold: '#fedd1e',
return ( black: '#000',
<View style={styles.sectionContainer}> white: '#fff',
<Text lightGrey: '#ccc',
style={[ blue: '#337AB7',
styles.sectionTitle, brick: '#973920',
{ };
color: isDarkMode ? Colors.white : Colors.black,
}, /// Util class for handling fetch-event peristence in AsyncStorage.
]}> import Event from './src/Event';
{title} import AsyncStorage from '@react-native-async-storage/async-storage';
</Text>
<Text Geolocation.setRNConfiguration({
style={[ skipPermissionRequests: true,
styles.sectionDescription, locationProvider: 'auto',
{ });
color: isDarkMode ? Colors.light : Colors.dark,
}, const requestPermissions = async () => {
]}> try {
{children} const granted = await PermissionsAndroid.request(
</Text> PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
</View> {
); 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<GeolocationResponse | GeolocationError> {
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 App = () => {
const isDarkMode = useColorScheme() === 'dark'; const [enabled, setEnabled] = React.useState(false);
const [status, setStatus] = React.useState(-1);
const [events, setEvents] = React.useState<Event[]>([]);
const backgroundStyle = { React.useEffect(() => {
backgroundColor: isDarkMode ? Colors.darker : Colors.lighter, 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 ( /// Load persisted events from AsyncStorage.
<SafeAreaView style={backgroundStyle}> ///
<StatusBar const loadEvents = () => {
barStyle={isDarkMode ? 'light-content' : 'dark-content'} Event.all()
backgroundColor={backgroundStyle.backgroundColor} .then(data => {
/> setEvents(data as Event[]);
<ScrollView })
contentInsetAdjustmentBehavior="automatic" .catch(error => {
style={backgroundStyle}> Alert.alert('Error', 'Failed to load data from AsyncStorage: ' + error);
<Header /> });
<View };
style={{
backgroundColor: isDarkMode ? Colors.black : Colors.white, /// Toggle BackgroundFetch ON/OFF
}}> ///
<Section title="Step One"> const onClickToggleEnabled = (value: boolean) => {
Edit <Text style={styles.highlight}>App.tsx</Text> to change this setEnabled(value);
screen and then come back to see your edits.
</Section> if (value) {
<Section title="See Your Changes"> BackgroundFetch.start();
<ReloadInstructions /> } else {
</Section> BackgroundFetch.stop();
<Section title="Debug"> }
<DebugInstructions /> };
</Section>
<Section title="Learn More"> /// [Status] button handler.
Read the docs to discover what to do next: ///
</Section> const onClickStatus = () => {
<LearnMoreLinks /> 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 (
<Text style={{padding: 10, fontSize: 16}}>
Waiting for BackgroundFetch events...
</Text>
);
}
return events
.slice()
.reverse()
.map(event => (
<View key={event.key} style={styles.event}>
<View style={{flexDirection: 'row'}}>
<Text style={styles.taskId}>
{event.taskId}&nbsp;{event.isHeadless ? '[Headless]' : ''}
</Text>
</View>
<Text style={styles.remark}>Remark - {event.location}</Text>
<Text style={styles.timestamp}>{event.timestamp}</Text>
</View> </View>
</ScrollView> ));
};
async function onLocPress(): Promise<void> {
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 (
<SafeAreaView style={{flex: 1, backgroundColor: Colors.gold}}>
<StatusBar barStyle={'light-content'}></StatusBar>
<View style={styles.container}>
<View style={styles.toolbar}>
<Text style={styles.title}>BGFetch Demo</Text>
<Button title="Loc" onPress={onLocPress} />
<Button title="Stop" onPress={onLocStop} />
<Switch value={enabled} onValueChange={onClickToggleEnabled} />
</View>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={styles.eventList}>
{renderEvents()}
</ScrollView>
<View style={styles.toolbar}>
<Button title={'status: ' + status} onPress={onClickStatus} />
<Text>&nbsp;</Text>
<Button title="scheduleTask" onPress={onClickScheduleTask} />
<View style={{flex: 1}} />
<Button title="clear" onPress={onClickClear} />
</View>
</View>
</SafeAreaView> </SafeAreaView>
); );
} };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
sectionContainer: { container: {
marginTop: 32, flexDirection: 'column',
paddingHorizontal: 24, flex: 1,
}, },
sectionTitle: { title: {
fontSize: 24, fontSize: 24,
fontWeight: '600', flex: 1,
fontWeight: 'bold',
color: Colors.black,
}, },
sectionDescription: { eventList: {
marginTop: 8, flex: 1,
fontSize: 18, backgroundColor: Colors.white,
fontWeight: '400',
}, },
highlight: { event: {
fontWeight: '700', 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,
}, },
}); });

View File

@ -1,26 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application <application
android:name=".MainApplication" android:name=".MainApplication"
android:label="@string/app_name" android:allowBackup="false"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:theme="@style/AppTheme"
android:supportsRtl="true">
<activity
android:name=".MainActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode" android:roundIcon="@mipmap/ic_launcher_round"
android:launchMode="singleTask" android:supportsRtl="true"
android:windowSoftInputMode="adjustResize" android:theme="@style/AppTheme">
android:exported="true">
<intent-filter> <!-- Background Services -->
<action android:name="android.intent.action.MAIN" /> <service
<category android:name="android.intent.category.LAUNCHER" /> android:name=".ForegroundHeadlessService"
</intent-filter> android:enabled="true"
</activity> android:foregroundServiceType="location"
android:exported="false" />
<!-- HeadlessJS Task Service -->
<service
android:name=".GeolocationHeadlessTask"
android:foregroundServiceType="location"
/>
<activity
android:name=".MainActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:exported="true"
android:label="@string/app_name"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application> </application>
</manifest>
</manifest>

View File

@ -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);
}
}

View File

@ -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<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactApplicationContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new ForegroundHeadlessModule(reactApplicationContext));
return modules;
}
@NonNull
@Override
public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactApplicationContext) {
return Collections.emptyList();
}
}

View File

@ -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;
}
}

View File

@ -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
);
}
}

View File

@ -19,7 +19,7 @@ class MainApplication : Application(), ReactApplication {
override fun getPackages(): List<ReactPackage> = override fun getPackages(): List<ReactPackage> =
PackageList(this).packages.apply { PackageList(this).packages.apply {
// Packages that cannot be autolinked yet can be added manually here, for example: // Packages that cannot be autolinked yet can be added manually here, for example:
// add(MyReactNativePackage()) add(ForegroundHeadlessPackage())
} }
override fun getJSMainModuleName(): String = "index" override fun getJSMainModuleName(): String = "index"

View File

@ -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" apply plugin: "com.facebook.react.rootproject"

View File

@ -6,4 +6,8 @@ import {AppRegistry} from 'react-native';
import App from './App'; import App from './App';
import {name as appName} from './app.json'; import {name as appName} from './app.json';
AppRegistry.registerHeadlessTask('GeolocationBackgroundTask', () =>
require('./src/task/watchPosition'),
);
AppRegistry.registerComponent(appName, () => App); AppRegistry.registerComponent(appName, () => App);

57
package-lock.json generated
View File

@ -8,8 +8,11 @@
"name": "POCLocationGather", "name": "POCLocationGather",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@react-native-async-storage/async-storage": "^2.1.2",
"@react-native-community/geolocation": "^3.4.0",
"react": "18.3.1", "react": "18.3.1",
"react-native": "0.77.0" "react-native": "0.77.0",
"react-native-background-fetch": "^4.2.7"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",
@ -2763,6 +2766,18 @@
"node": ">= 8" "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": { "node_modules/@react-native-community/cli": {
"version": "15.0.1", "version": "15.0.1",
"resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-15.0.1.tgz", "resolved": "https://registry.npmjs.org/@react-native-community/cli/-/cli-15.0.1.tgz",
@ -2996,6 +3011,19 @@
"node": ">=10" "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": { "node_modules/@react-native/assets-registry": {
"version": "0.77.0", "version": "0.77.0",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.77.0.tgz", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.77.0.tgz",
@ -7497,6 +7525,15 @@
"node": ">=8" "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": { "node_modules/is-plain-object": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "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==", "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==",
"license": "MIT" "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": { "node_modules/merge-stream": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "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": { "node_modules/react-native/node_modules/ansi-styles": {
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",

View File

@ -10,8 +10,11 @@
"test": "jest" "test": "jest"
}, },
"dependencies": { "dependencies": {
"@react-native-async-storage/async-storage": "^2.1.2",
"@react-native-community/geolocation": "^3.4.0",
"react": "18.3.1", "react": "18.3.1",
"react-native": "0.77.0" "react-native": "0.77.0",
"react-native-background-fetch": "^4.2.7"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",
@ -36,4 +39,4 @@
"engines": { "engines": {
"node": ">=18" "node": ">=18"
} }
} }

82
src/Event.ts Normal file
View File

@ -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,
};
}
}

29
src/task/watchPosition.ts Normal file
View File

@ -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}`);
};