The best way of tracking location in background using react-native + Expo in 2020 - react-native

I want to create my own Endomono/Runtastic-like app using RN + expo (This app will be just for me, and I have android phone with pretty decent performance/battery life (Redmi note 7) so I don't worry about performance too much). I wanted to use all-in-one library for that, or just and library that allows me to execute some code each X seconds in background (and getAsyncLocation there). My point is just to send lat/lon data every X seconds to my backend HTTP django-rest-framework powered server.
I just spent whole day trying figure out any way to do that, I tried couple of libraries like this ones: react-native-background-geolocation, react-native-background-timer, react-native-background-job and few more. I followed step by step instalation guide, and I kept getting errors like: null is not an object (evaluating 'RNBackgroundTimer.setTimeout') .
I also tried this: I fixed some errors in this code (imports related), it seemed to work, but when I changed my GPS location using Fake GPS, and only one cast of didFocus functions appears in the console. Here's code:
import React from 'react';
import { EventEmitter } from 'fbemitter';
import { NavigationEvents } from 'react-navigation';
import { AppState, AsyncStorage, Platform, StyleSheet, Text, View, Button } from 'react-native';
import MapView from 'react-native-maps';
import * as Permissions from 'expo-permissions';
import * as Location from 'expo-location';
import * as TaskManager from 'expo-task-manager';
import { FontAwesome, MaterialIcons } from '#expo/vector-icons';
const STORAGE_KEY = 'expo-home-locations';
const LOCATION_UPDATES_TASK = 'location-updates';
const locationEventsEmitter = new EventEmitter();
export default class MapScreen extends React.Component {
static navigationOptions = {
title: 'Background location',
};
mapViewRef = React.createRef();
state = {
accuracy: 4,
isTracking: false,
showsBackgroundLocationIndicator: false,
savedLocations: [],
initialRegion: null,
error: null,
};
didFocus = async () => {
console.log("Hello")
let { status } = await Permissions.askAsync(Permissions.LOCATION);
if (status !== 'granted') {
AppState.addEventListener('change', this.handleAppStateChange);
this.setState({
error:
'Location permissions are required in order to use this feature. You can manually enable them at any time in the "Location Services" section of the Settings app.',
});
return;
} else {
this.setState({ error: null });
}
const { coords } = await Location.getCurrentPositionAsync();
console.log(coords)
const isTracking = await Location.hasStartedLocationUpdatesAsync(LOCATION_UPDATES_TASK);
const task = (await TaskManager.getRegisteredTasksAsync()).find(
({ taskName }) => taskName === LOCATION_UPDATES_TASK
);
const savedLocations = await getSavedLocations();
const accuracy = (task && task.options.accuracy) || this.state.accuracy;
this.eventSubscription = locationEventsEmitter.addListener('update', locations => {
this.setState({ savedLocations: locations });
});
if (!isTracking) {
alert('Click `Start tracking` to start getting location updates.');
}
this.setState({
accuracy,
isTracking,
savedLocations,
initialRegion: {
latitude: coords.latitude,
longitude: coords.longitude,
latitudeDelta: 0.004,
longitudeDelta: 0.002,
},
});
};
handleAppStateChange = nextAppState => {
if (nextAppState !== 'active') {
return;
}
if (this.state.initialRegion) {
AppState.removeEventListener('change', this.handleAppStateChange);
return;
}
this.didFocus();
};
componentWillUnmount() {
if (this.eventSubscription) {
this.eventSubscription.remove();
}
AppState.removeEventListener('change', this.handleAppStateChange);
}
async startLocationUpdates(accuracy = this.state.accuracy) {
await Location.startLocationUpdatesAsync(LOCATION_UPDATES_TASK, {
accuracy,
showsBackgroundLocationIndicator: this.state.showsBackgroundLocationIndicator,
});
if (!this.state.isTracking) {
alert(
'Now you can send app to the background, go somewhere and come back here! You can even terminate the app and it will be woken up when the new significant location change comes out.'
);
}
this.setState({ isTracking: true });
}
async stopLocationUpdates() {
await Location.stopLocationUpdatesAsync(LOCATION_UPDATES_TASK);
this.setState({ isTracking: false });
}
clearLocations = async () => {
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify([]));
this.setState({ savedLocations: [] });
};
toggleTracking = async () => {
await AsyncStorage.removeItem(STORAGE_KEY);
if (this.state.isTracking) {
await this.stopLocationUpdates();
} else {
await this.startLocationUpdates();
}
this.setState({ savedLocations: [] });
};
onAccuracyChange = () => {
const next = Location.Accuracy[this.state.accuracy + 1];
const accuracy = next ? Location.Accuracy[next] : Location.Accuracy.Lowest;
this.setState({ accuracy });
if (this.state.isTracking) {
// Restart background task with the new accuracy.
this.startLocationUpdates(accuracy);
}
};
toggleLocationIndicator = async () => {
const showsBackgroundLocationIndicator = !this.state.showsBackgroundLocationIndicator;
this.setState({ showsBackgroundLocationIndicator }, async () => {
if (this.state.isTracking) {
await this.startLocationUpdates();
}
});
};
onCenterMap = async () => {
const { coords } = await Location.getCurrentPositionAsync();
const mapView = this.mapViewRef.current;
if (mapView) {
mapView.animateToRegion({
latitude: coords.latitude,
longitude: coords.longitude,
latitudeDelta: 0.004,
longitudeDelta: 0.002,
});
}
};
renderPolyline() {
const { savedLocations } = this.state;
if (savedLocations.length === 0) {
return null;
}
return (
<MapView.Polyline
coordinates={savedLocations}
strokeWidth={3}
strokeColor={"black"}
/>
);
}
render() {
if (this.state.error) {
return <Text style={styles.errorText}>{this.state.error}</Text>;
}
if (!this.state.initialRegion) {
return <NavigationEvents onDidFocus={this.didFocus} />;
}
return (
<View style={styles.screen}>
<MapView
ref={this.mapViewRef}
style={styles.mapView}
initialRegion={this.state.initialRegion}
showsUserLocation>
{this.renderPolyline()}
</MapView>
<View style={styles.buttons} pointerEvents="box-none">
<View style={styles.topButtons}>
<View style={styles.buttonsColumn}>
{Platform.OS === 'android' ? null : (
<Button style={styles.button} onPress={this.toggleLocationIndicator} title="background/indicator">
<Text>{this.state.showsBackgroundLocationIndicator ? 'Hide' : 'Show'}</Text>
<Text> background </Text>
<FontAwesome name="location-arrow" size={20} color="white" />
<Text> indicator</Text>
</Button>
)}
</View>
<View style={styles.buttonsColumn}>
<Button style={styles.button} onPress={this.onCenterMap} title="my location">
<MaterialIcons name="my-location" size={20} color="white" />
</Button>
</View>
</View>
<View style={styles.bottomButtons}>
<Button style={styles.button} onPress={this.clearLocations} title="clear locations">
Clear locations
</Button>
<Button style={styles.button} onPress={this.toggleTracking} title="start-stop tracking">
{this.state.isTracking ? 'Stop tracking' : 'Start tracking'}
</Button>
</View>
</View>
</View>
);
}
}
async function getSavedLocations() {
try {
const item = await AsyncStorage.getItem(STORAGE_KEY);
return item ? JSON.parse(item) : [];
} catch (e) {
return [];
}
}
if (Platform.OS !== 'android') {
TaskManager.defineTask(LOCATION_UPDATES_TASK, async ({ data: { locations } }) => {
if (locations && locations.length > 0) {
const savedLocations = await getSavedLocations();
const newLocations = locations.map(({ coords }) => ({
latitude: coords.latitude,
longitude: coords.longitude,
}));
savedLocations.push(...newLocations);
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(savedLocations));
locationEventsEmitter.emit('update', savedLocations);
}
});
}
const styles = StyleSheet.create({
screen: {
flex: 1,
},
mapView: {
flex: 1,
},
buttons: {
flex: 1,
flexDirection: 'column',
justifyContent: 'space-between',
padding: 10,
position: 'absolute',
top: 0,
right: 0,
bottom: 0,
left: 0,
},
topButtons: {
flexDirection: 'row',
justifyContent: 'space-between',
},
bottomButtons: {
flexDirection: 'column',
alignItems: 'flex-end',
},
buttonsColumn: {
flexDirection: 'column',
alignItems: 'flex-start',
},
button: {
paddingVertical: 5,
paddingHorizontal: 10,
marginVertical: 5,
},
errorText: {
fontSize: 15,
color: 'rgba(0,0,0,0.7)',
margin: 20,
},
});
If you know any way to easily complete my target (of sending simple HTTP GET with location from background of Expo + RN app to my DRF backend) please let me know.

If you're using Expo you can simply use expo-task-manager and expo-location to get background location updates.
Here's a simplified version that I'm using (and it's working for sure on Android) on the App I'm currently developing:
import * as Location from 'expo-location';
import * as TaskManager from 'expo-task-manager';
import axios from 'axios';
const TASK_FETCH_LOCATION = 'TASK_FETCH_LOCATION';
// 1 define the task passing its name and a callback that will be called whenever the location changes
TaskManager.defineTask(TASK_FETCH_LOCATION, async ({ data: { locations }, error }) => {
if (error) {
console.error(error);
return;
}
const [location] = locations;
try {
const url = `https://<your-api-endpoint>`;
await axios.post(url, { location }); // you should use post instead of get to persist data on the backend
} catch (err) {
console.error(err);
}
});
// 2 start the task
Location.startLocationUpdatesAsync(TASK_FETCH_LOCATION, {
accuracy: Location.Accuracy.Highest,
distanceInterval: 1, // minimum change (in meters) betweens updates
deferredUpdatesInterval: 1000, // minimum interval (in milliseconds) between updates
// foregroundService is how you get the task to be updated as often as would be if the app was open
foregroundService: {
notificationTitle: 'Using your location',
notificationBody: 'To turn off, go back to the app and switch something off.',
},
});
// 3 when you're done, stop it
Location.hasStartedLocationUpdatesAsync(TASK_FETCH_LOCATION).then((value) => {
if (value) {
Location.stopLocationUpdatesAsync(TASK_FETCH_LOCATION);
}
});

It doesn't necessarily work with Expo, but if "eject" your project or start with the React Native CLI (via react-native init) then you could use an Android specific React Native "NativeModule" to accomplish your goal. I like using the react-native-location package, which has great support on iOS for background location updates, but on Android there is a bug currently. I put together an example project which has the necessary Android specific code inside a NativeModule you could use to start from:
https://github.com/andersryanc/ReactNative-LocationSample

Related

Issue sending & receiving streams between two clients in LiveKit's React Native SDK

I'm trying to build on the example app provided by livekit, so far I've implemented everything like the example app and I've been successful with connecting to a room on example website, I recieve audio from website, but I don't read the video stream, and I also can't send audio or video at all.
Steps to reproduce the behavior:
add the following to index.js
import { registerRootComponent } from "expo";
import { registerGlobals } from "livekit-react-native";
import App from "./App";
registerRootComponent(App);
registerGlobals();
Rendering the following component in App.tsx
import { Participant, Room, Track } from "livekit-client";
import {
useRoom,
useParticipant,
AudioSession,
VideoView,
} from "livekit-react-native";
import { useEffect, useState } from "react";
import { Text, ListRenderItem, StyleSheet, FlatList, View } from "react-native";
import { ParticipantView } from "./ParticipantView";
import { RoomControls } from "./RoomControls";
import type { TrackPublication } from "livekit-client";
const App = () => {
// Create a room state
const [, setIsConnected] = useState(false);
const [room] = useState(
() =>
new Room({
publishDefaults: { simulcast: false },
adaptiveStream: true,
})
);
// Get the participants from the room
const { participants } = useRoom(room);
const url = "[hard-coded-url]";
const token =
"[hard-coded-token";
useEffect(() => {
let connect = async () => {
// If you wish to configure audio, uncomment the following:
await AudioSession.configureAudio({
android: {
preferredOutputList: ["speaker"],
},
ios: {
defaultOutput: "speaker",
},
});
await AudioSession.startAudioSession();
await room.connect(url, token, {});
await room.localParticipant.setCameraEnabled(true);
await room.localParticipant.setMicrophoneEnabled(true);
await room.localParticipant.enableCameraAndMicrophone();
console.log("connected to ", url);
setIsConnected(true);
};
connect();
return () => {
room.disconnect();
AudioSession.stopAudioSession();
};
}, [url, token, room]);
// Setup views.
const stageView = participants.length > 0 && (
<ParticipantView participant={participants[0]} style={styles.stage} />
);
const renderParticipant: ListRenderItem<Participant> = ({ item }) => {
return (
<ParticipantView participant={item} style={styles.otherParticipantView} />
);
};
const otherParticipantsView = participants.length > 0 && (
<FlatList
data={participants}
renderItem={renderParticipant}
keyExtractor={(item) => item.sid}
horizontal={true}
style={styles.otherParticipantsList}
/>
);
const { cameraPublication, microphonePublication } = useParticipant(
room.localParticipant
);
return (
<View style={styles.container}>
{stageView}
{otherParticipantsView}
<RoomControls
micEnabled={isTrackEnabled(microphonePublication)}
setMicEnabled={(enabled: boolean) => {
room.localParticipant.setMicrophoneEnabled(enabled);
}}
cameraEnabled={isTrackEnabled(cameraPublication)}
setCameraEnabled={(enabled: boolean) => {
room.localParticipant.setCameraEnabled(enabled);
}}
onDisconnectClick={() => {
// navigation.pop();
console.log("disconnected");
}}
/>
</View>
);
};
function isTrackEnabled(pub?: TrackPublication): boolean {
return !(pub?.isMuted ?? true);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
stage: {
flex: 1,
width: "100%",
},
otherParticipantsList: {
width: "100%",
height: 150,
flexGrow: 0,
},
otherParticipantView: {
width: 150,
height: 150,
},
});
export default App;
the components used here are mostly the same as what's in the example, I've removed the screensharing logic and the messages
5. I run the app using an expo development build
6. it will log that it's connected, you'll be able to hear sound from the remote participant, but not see any video or send any sound.
7. if i try to add
await room.localParticipant.enableCameraAndMicrophone();
in the useEffect, I get the following error:
Possible Unhandled Promise Rejection (id: 0):
Error: Not implemented.
getSettings#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:103733:24
#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:120307:109
generatorResume#[native code]
asyncGeneratorStep#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:21908:26
_next#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:21927:29
#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:21932:14
tryCallTwo#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:26656:9
doResolve#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:26788:25
Promise#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:26675:14
#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:21924:25
#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:120173:52
generatorResume#[native code]
asyncGeneratorStep#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:21908:26
_next#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:21927:29
tryCallOne#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:26648:16
#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:26729:27
#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:27687:26
_callTimer#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:27602:17
_callReactNativeMicrotasksPass#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:27635:17
callReactNativeMicrotasks#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:27799:44
__callReactNativeMicrotasks#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:21006:46
#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:20806:45
__guard#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:20986:15
flushedQueue#http://192.168.1.150:8081/index.bundle?platform=ios&dev=true&hot=false:20805:21
flushedQueue#[native code]
Expected behavior
This should both receive & send video and audio streams between the two clients

How to display a users location using React Native Background Geolocation?

I want to build a a geolocator app that can display the users location when the press a button using react native and Expo CLI. The code I have below will allow the app to track in the background and the foreground. I'm trying to get the react native map to display a pin or user location when they click the start background location button. The users location must be displayed on a map on a separate screen.
import React, { useEffect, useState } from "react"
import { StyleSheet, Text, View, Button } from "react-native"
import * as TaskManager from "expo-task-manager"
import * as Location from "expo-location"
const LOCATION_TASK_NAME = "LOCATION_TASK_NAME"
let foregroundSubscription = null
// Define the background task for location tracking
TaskManager.defineTask(LOCATION_TASK_NAME, async ({ data, error }) => {
if (error) {
console.error(error)
return
}
if (data) {
// Extract location coordinates from data
const { locations } = data
const location = locations[0]
if (location) {
console.log("Location in background", location.coords)
}
}
})
export default function App() {
// Define position state: {latitude: number, longitude: number}
const [position, setPosition] = useState(null)
// Request permissions right after starting the app
useEffect(() => {
const requestPermissions = async () => {
const foreground = await Location.requestForegroundPermissionsAsync()
if (foreground.granted) await Location.requestBackgroundPermissionsAsync()
}
requestPermissions()
}, [])
// Start location tracking in foreground
const startForegroundUpdate = async () => {
// Check if foreground permission is granted
const { granted } = await Location.getForegroundPermissionsAsync()
if (!granted) {
console.log("location tracking denied")
return
}
// Make sure that foreground location tracking is not running
foregroundSubscription?.remove()
// Start watching position in real-time
foregroundSubscription = await Location.watchPositionAsync(
{
// For better logs, we set the accuracy to the most sensitive option
accuracy: Location.Accuracy.BestForNavigation,
},
location => {
setPosition(location.coords)
}
)
}
// Stop location tracking in foreground
const stopForegroundUpdate = () => {
foregroundSubscription?.remove()
setPosition(null)
}
// Start location tracking in background
const startBackgroundUpdate = async () => {
// Don't track position if permission is not granted
const { granted } = await Location.getBackgroundPermissionsAsync()
if (!granted) {
console.log("location tracking denied")
return
}
// Make sure the task is defined otherwise do not start tracking
const isTaskDefined = await TaskManager.isTaskDefined(LOCATION_TASK_NAME)
if (!isTaskDefined) {
console.log("Task is not defined")
return
}
// Don't track if it is already running in background
const hasStarted = await Location.hasStartedLocationUpdatesAsync(
LOCATION_TASK_NAME
)
if (hasStarted) {
console.log("Already started")
return
}
await Location.startLocationUpdatesAsync(LOCATION_TASK_NAME, {
// For better logs, we set the accuracy to the most sensitive option
accuracy: Location.Accuracy.BestForNavigation,
// Make sure to enable this notification if you want to consistently track in the background
showsBackgroundLocationIndicator: true,
foregroundService: {
notificationTitle: "Location",
notificationBody: "Location tracking in background",
notificationColor: "#fff",
},
})
}
// Stop location tracking in background
const stopBackgroundUpdate = async () => {
const hasStarted = await Location.hasStartedLocationUpdatesAsync(
LOCATION_TASK_NAME
)
if (hasStarted) {
await Location.stopLocationUpdatesAsync(LOCATION_TASK_NAME)
console.log("Location tacking stopped")
}
}
return (
<View style={styles.container}>
<Text>Longitude: {position?.longitude}</Text>
<Text>Latitude: {position?.latitude}</Text>
<View style={styles.separator} />
<Button
onPress={startForegroundUpdate}
title="Start in foreground"
color="green"
/>
<View style={styles.separator} />
<Button
onPress={stopForegroundUpdate}
title="Stop in foreground"
color="red"
/>
<View style={styles.separator} />
<Button
onPress={startBackgroundUpdate}
title="Start in background"
color="green"
/>
<View style={styles.separator} />
<Button
onPress={stopBackgroundUpdate}
title="Stop in foreground"
color="red"
/>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
switchContainer: {
flexDirection: "row",
alignItems: "center",
},
button: {
marginTop: 15,
},
separator: {
marginVertical: 8,
},
})

Geolocation clearWatch(watchId) does not stop location tracking (React Native)

I'm trying to create simple example of location tracker and I'm stuck with following case. My basic goal is to toggle location watch by pressing start/end button. I'm doing separation of concerns by implementing custom react hook which is then used in App component:
useWatchLocation.js
import {useEffect, useRef, useState} from "react"
import {PermissionsAndroid} from "react-native"
import Geolocation from "react-native-geolocation-service"
const watchCurrentLocation = async (successCallback, errorCallback) => {
if (!(await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION))) {
errorCallback("Permissions for location are not granted!")
}
return Geolocation.watchPosition(successCallback, errorCallback, {
timeout: 3000,
maximumAge: 500,
enableHighAccuracy: true,
distanceFilter: 0,
useSignificantChanges: false,
})
}
const stopWatchingLocation = (watchId) => {
Geolocation.clearWatch(watchId)
// Geolocation.stopObserving()
}
export default useWatchLocation = () => {
const [location, setLocation] = useState()
const [lastError, setLastError] = useState()
const [locationToggle, setLocationToggle] = useState(false)
const watchId = useRef(null)
const startLocationWatch = () => {
watchId.current = watchCurrentLocation(
(position) => {
setLocation(position)
},
(error) => {
setLastError(error)
}
)
}
const cancelLocationWatch = () => {
stopWatchingLocation(watchId.current)
setLocation(null)
setLastError(null)
}
const setLocationWatch = (flag) => {
setLocationToggle(flag)
}
// execution after render when locationToggle is changed
useEffect(() => {
if (locationToggle) {
startLocationWatch()
} else cancelLocationWatch()
return cancelLocationWatch()
}, [locationToggle])
// mount / unmount
useEffect(() => {
cancelLocationWatch()
}, [])
return { location, lastError, setLocationWatch }
}
App.js
import React from "react"
import {Button, Text, View} from "react-native"
import useWatchLocation from "./hooks/useWatchLocation"
export default App = () => {
const { location, lastError, setLocationWatch } = useWatchLocation()
return (
<View style={{ margin: 20 }}>
<View style={{ margin: 20, alignItems: "center" }}>
<Text>{location && `Time: ${new Date(location.timestamp).toLocaleTimeString()}`}</Text>
<Text>{location && `Latitude: ${location.coords.latitude}`}</Text>
<Text>{location && `Longitude: ${location.coords.longitude}`}</Text>
<Text>{lastError && `Error: ${lastError}`}</Text>
</View>
<View style={{ marginTop: 20, width: "100%", flexDirection: "row", justifyContent: "space-evenly" }}>
<Button onPress={() => {setLocationWatch(true)}} title="START" />
<Button onPress={() => {setLocationWatch(false)}} title="STOP" />
</View>
</View>
)
}
I have searched multiple examples which are online and code above should work. But the problem is when stop button is pressed location still keeps getting updated even though I invoke Geolocation.clearWatch(watchId).
I wrapped Geolocation calls to handle location permission and other possible debug stuff. It seems like watchId value that is saved using useRef hook inside useWatchLocation is invalid. My guess is based on attempting to call Geolocation.stopObserving() right after Geolocation.clearWatch(watchId). Subscription stops but I get warning:
Called stopObserving with existing subscriptions.
So I assume that original subscription was not cleared.
What am I missing/doing wrong?
EDIT: I figured out solution. But since isMounted pattern is generally considered antipattern: Does anyone have a better solution?
Ok, problem solved with isMounted pattern. isMounted.current is set at locationToggle effect to true and inside cancelLocationWatch to false:
const isMounted = useRef(null)
...
useEffect(() => {
if (locationToggle) {
isMounted.current = true // <--
startLocationWatch()
} else cancelLocationWatch()
return () => cancelLocationWatch()
}, [locationToggle])
...
const cancelLocationWatch = () => {
stopWatchingLocation(watchId.current)
setLocation(null)
setLastError(null)
isMounted.current = false // <--
}
And checked at mount / unmount effect, success and error callback:
const startLocationWatch = () => {
watchId.current = watchCurrentLocation(
(position) => {
if (isMounted.current) { // <--
setLocation(position)
}
},
(error) => {
if (isMounted.current) { // <--
setLastError(error)
}
}
)
}

Uploading image to AWS S3

i am a beginner in react native. my problem is, i am trying to send image from camera to aws s3. this is my latest code.
import React from 'react';
import { StyleSheet, Text, View, TouchableOpacity, Button, Image } from 'react-native';
import ImagePicker from 'react-native-image-picker';
import { RNS3 } from 'react-native-aws3';
import fs from 'react-native-fs';
import Buffer from 'buffer';
import AWS from 'aws-sdk';
function uploadToS3(image){
const filename = "the_file.jpeg";
const options = {
keyPrefix: "uploads/",
bucket: "this is bucketname",
region: "this is region",
accessKey: "this is access key",
secretKey: "this is secret key",
successActionStatus: 201
}
try{
const s3 = new AWS.S3({accessKeyId: options.accessKey, secretAccessKey:options.secretKey, region:options.region});
var UploadURL;
const params = {Bucket: options.bucket, Key: options.keyPrefix+filename, ContentType: image.type};
s3.getSignedUrl('putObject', params, function (err, url) {
console.log('Your generated pre-signed URL is', url);
UploadURL = url;
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
console.log("ok: ", xhr.response);
alert("success");
} else {
console.log("no: " , xhr.response);
alert("no");
}
}
}
xhr.open('PUT', UploadURL)
xhr.setRequestHeader('Content-Type', image.type)
xhr.send({image})
});
} catch(error){
console.log("err : ",error)
}
}
class HomeScreen extends React.Component {
constructor(props) {
global.currentScreenIndex = 'HomeScreen';
super(props);
this.state = {
resourcePath: {},
requestStatus: {},
employees: {},
response: {},
};
}
cameraLaunch = (id) => {
console.log(id);
let options = {
title: 'Select Picture',
storageOptions: {
skipBackup: true,
path: 'images',
},
base64: true,
maxWidth: 400,
maxHeight: 400
};
ImagePicker.launchCamera(options, (res) => {
//console.log('Response = ', res);
if (res.didCancel) {
console.log('User cancelled image picker');
} else if (res.error) {
console.log('ImagePicker Error: ', res.error);
} else if (res.customButton) {
console.log('User tapped custom button: ', res.customButton);
alert(res.customButton);
} else {
let source = res;
this.setState({
resourcePath: source,
});
uploadToS3(res);
}
});
}
render(){
return (
<View style={{ flex: 1, alignItems: 'center', marginTop: 100 }}>
<Button
onPress={()=>this.cameraLaunch(1)}
title="OpenCamera"
color="#841584"
/>
<Text style={{ alignItems: 'center' }}>
{this.state.resourcePath.uri}
</Text>
<Image source={this.state.resourcePath} />
</View>
);
}
};
export default HomeScreen;
there is nothing wrong with the camera or presigned url generation. if run, there will be the_file.jpeg in "this is bucket name"/uploads/the_file.jpeg, but the problem is, its size is 0 byte. i have tried to send just image.data, but apparently it just make the_file.jpeg become a "txt file" with "jpeg" extension. please help.
ps : i am aware on how insecure this code.
It looks like you missed the purpose of presigned URLs which is to upload objects without the need of AWS credentials. In your example, you are initializing the S3 client using the AWS credential, in that case, you can simply use s3.upload function to upload your file.
I had the same issue and nothing helped. This is what I did.
Make sure you follow the amplify guide for setting up a app. amplify init, amplify add auth, amplify push, and then amplify add storage and then do this.
import Amplify, { Storage } from 'aws-amplify'
import config from './src/aws-exports'
// import awsconfig from './aws-exports';
// Might need to switch line 7 to awsconfig
Amplify.configure(config)
import { StatusBar } from 'expo-status-bar';
import React, { useState, useEffect } from 'react';
import { Button, Image, View, Platform, StyleSheet, Text, TextInput } from 'react-native';
import * as ImagePicker from 'expo-image-picker';
function App() {
const [image, setImage] = useState(null)
const [name, setName] = useState('Evan Erickson')
useEffect(() => {
(async () => {
if (Platform.OS !== 'web') {
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== 'granted') {
alert('Sorry, we need camera roll permissions to make this work!');
}
}
})();
}, []);
const pickImage = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
allowsEditing: true,
aspect: [4, 3],
quality: 1,
});
console.log(result)
async function pathToImageFile(data) {
try {
const response = await fetch(data);
const blob = await response.blob();
await Storage.put(`customers/${name}`, blob, {
contentType: 'image/jpeg', // contentType is optional
});
} catch (err) {
console.log('Error uploading file:', err);
}
}
// later
pathToImageFile(result.uri);
}
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Button title="Pick an image from camera roll" onPress={pickImage} />
{image && <Image source={{ uri: image }} style={{ width: 200, height: 200 }} />}
<Button title="Upload image" onPress={() => {alert(image)}} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
export default withAuthenticator(App)

ReadableNativeMap cannot be cast to java.lang.String

I'm using Expo together with react-native-maps for a rather simple map component. It works fine on iOS, however on Android I get the following error:
abi30_0_0.com.facebook.react.bridge.ReadableNativeMap cannot be cast to java.lang.String
getString
ReadableNativeMap.java:168
showAlert
DialogModule.java:247
invoke
Method.java
invoke
JavaMethodWrapper.java:372
invoke
JavaModuleWrapper.java:160
run
NativeRunnable.java
handleCallback
Handler.java:790
dispatchMessage
Handler.java:99
dispatchMessage
MessageQueueThreadHandler.java:29
loop
Looper.java:164
run
MessageQueueThreadImpl.java:192
run
Thread.java:764
Here is my map component (sorry it's a bit longer):
import React from 'react';
import {
StyleSheet,
View,
Dimensions,
Alert,
TouchableOpacity,
Text,
Platform,
} from 'react-native';
import {
MapView,
Location,
Permissions,
Constants,
} from 'expo';
import { Ionicons } from '#expo/vector-icons';
import axios from 'axios';
import geolib from 'geolib';
import Polyline from '#mapbox/polyline';
import api from '../helpers/api';
import appConfig from '../app.json';
const { width, height } = Dimensions.get('window');
class MapScreen extends React.Component {
static navigationOptions = {
title: 'Map',
};
constructor(props) {
super(props);
this.state = {
coordinates: [],
focusedLocation: {
latitude: 0,
longitude: 0,
latitudeDelta: 0.0122,
longitudeDelta: width / height * 0.0122,
},
destinationReached: false,
isMapReady: false,
};
this.apikey = appConfig.expo.android.config.googleMaps.apiKey;
// bind this in constructor so state can be set in these methods
this.getLocation = this.getLocation.bind(this);
this.getDirections = this.getDirections.bind(this);
this.checkUserLocation = this.checkUserLocation.bind(this);
this.animateToCoordinates = this.animateToCoordinates.bind(this);
}
async componentDidMount() {
// ask the user for location permission
if (Platform.OS === 'android' && !Constants.isDevice) {
Alert.alert('Warning', 'This will not work on sketch in an android emulator. Try it on your device!');
return;
}
if (await !this.isPermissionGranted(Permissions.LOCATION)) {
Alert.alert('Permission', 'You need to enable location services');
return;
}
// get the current location of the user
// retrieve the destination location where the users shift will start
const [currentLocation, destinationLocation] = await Promise.all([
this.getLocation(),
this.getInterceptionCoords(),
]);
// retrieve a direction between these two points
this.getDirections(currentLocation, destinationLocation);
// monitor the current position of the user
this.watchid = await Location.watchPositionAsync({
enableHighAccuracy: true,
distanceInterval: 1,
}, this.checkUserLocation);
}
componentWillUnmount() {
if (this.watchid) {
this.watchid.remove();
}
}
/**
* retrieve current coordinates and move to them on the map
* assumes that location permission has already been granted
* #returns {Promise<{latitude: (number|*|string), longitude: (number|*|string)}>}
*/
async getLocation() {
// get current position if permission has been granted
const { coords } = await Location.getCurrentPositionAsync({
enableHighAccuracy: true,
});
// initalize map at current position
this.animateToCoordinates(coords);
this.setState(prevState => {
return {
focusedLocation: {
...prevState.focusedLocation,
latitude: coords.latitude,
longitude: coords.longitude,
},
};
});
return {
latitude: coords.latitude,
longitude: coords.longitude,
};
}
/**
* retrieves the coordinates of a route
* route: safety drivers position to the interception point
* #param startLoc
* #param destinationLoc
* #returns {Promise<*>}
*/
async getDirections(startLoc, destinationLoc) {
try {
const response = await axios({
method: 'GET',
url: 'https://maps.googleapis.com/maps/api/directions/json',
params: {
origin: Object.values(startLoc).join(','),
destination: Object.values(destinationLoc).join(','),
key: this.apikey,
},
responseType: 'json',
headers: {},
});
if (response.status !== 200) {
// this will execute the catch block
throw new Error('Fetching the coordinates of the interception point failed');
}
const { data } = response;
if (data.status !== 'OK') {
throw new Error('Determining a route between the two points failed');
}
const points = Polyline.decode(data.routes[0].overview_polyline.points);
const coordinates = points.map(point => {
return {
latitude: point[0],
longitude: point[1],
};
});
this.setState({ coordinates: coordinates });
return coordinates;
} catch (error) {
console.log(error);
Alert.alert('Network error', error);
return error;
}
}
/**
* get the coordinates of the interception point
* #returns {Promise<*>}
*/
async getInterceptionCoords() {
try {
const response = await api.get('/shifts/next');
if (response.status !== 200) {
// this will execute the catch block
throw new Error('Fetching the coordinates of the interception point failed');
}
const { data } = response;
return {
latitude: data.latStart,
longitude: data.longStart,
};
} catch (error) {
console.log(error);
Alert.alert('Network error', error);
return error;
}
}
checkUserLocation(location) {
const { coordinates } = this.state;
const { coords } = location;
if (Platform.OS === 'android') {
// follow the user location
// mapview component handles this for ios devices
this.animateToCoordinates(coords);
}
const destinationCoords = coordinates[coordinates.length - 1];
const distance = geolib.getDistance(coords, destinationCoords);
if (distance <= 20) {
// distance to destination is shorter than 20 metres
// show button so user can confirm arrival
this.setState({ destinationReached: true });
} else {
// remove arrival button in case the user moves away from the destination
this.setState({ destinationReached: false });
}
}
/**
* animate to specified coordinates on the map
* #param coords
*/
animateToCoordinates(coords) {
const { focusedLocation } = this.state;
const { latitude, longitude } = coords;
if (focusedLocation && latitude && longitude) {
this.map.animateToRegion({
...focusedLocation,
latitude: latitude,
longitude: longitude,
});
}
}
renderConfirmalButton() {
const { destinationReached } = this.state;
if (!destinationReached) {
return null;
}
return (
<View style={styles.confirmContainer}>
<TouchableOpacity
style={styles.confirmButton}
onPress={this.onArrivalConfirmed}
>
<View style={styles.drawerItem}>
<Ionicons
name="ios-checkmark-circle-outline"
size={30}
color="#ffffff"
style={styles.drawerItemIcon}
/>
<Text style={styles.buttonText}>Confirm Arrival</Text>
</View>
</TouchableOpacity>
</View>
);
}
isPermissionGranted = async permission => {
const { status } = await Permissions.askAsync(permission);
return (status === 'granted');
};
onArrivalConfirmed = () => {
Alert.alert('Confirmation', 'Arrival confirmed');
};
onMapReady = () => {
this.setState({ isMapReady: true });
};
render() {
const { coordinates, focusedLocation, isMapReady } = this.state;
return (
<View style={styles.container}>
<MapView
style={styles.map}
initialRegion={focusedLocation}
showsUserLocation
followsUserLocation={Platform.OS === 'ios'}
loadingEnabled
ref={map => { this.map = map; }}
onMapReady={() => this.onMapReady()}
>
<MapView.Polyline
coordinates={coordinates}
strokeWidth={3}
strokeColor="blue"
/>
{isMapReady && coordinates.length > 0 && (
<MapView.Marker
coordinate={coordinates[coordinates.length - 1]}
/>
)}
</MapView>
{this.renderConfirmalButton()}
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
},
map: {
width: width,
height: height,
},
confirmContainer: {
position: 'absolute',
left: 0,
bottom: 0,
height: 150,
width: '100%',
justifyContent: 'center',
},
confirmButton: {
paddingHorizontal: 30,
},
drawerItem: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
padding: 10,
backgroundColor: 'lightblue',
borderRadius: 15,
},
drawerItemIcon: {
marginRight: 10,
},
buttonText: {
color: '#ffffff',
fontSize: 22,
},
});
export default MapScreen;
Any help is greatly appreciated!