Working proof of concept to run location in the background
This commit is contained in:
parent
550d211462
commit
bb6c8b5ea4
452
App.tsx
452
App.tsx
|
@ -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} {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> </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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
4
index.js
4
index.js
|
@ -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);
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}`);
|
||||||
|
};
|
Loading…
Reference in New Issue