forked from expo/expo
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[home][ios] add diagnostics screen with background location demos (ex…
…po#3194) * [home] add diagnostics screen * [ios] fix home not having experienceUrl in constants * [home] make background location diagnostic screen look better * [ios] fix notification events not being send to home experience * [home] add diagnostic screen for geofencing * [home] subtle changes in background location diagnostic screen * [home] Only show diagnostics screen on iOS for now * [home] Verify permissions and handle the case where they are rejected
- Loading branch information
1 parent
150b218
commit 2715020
Showing
7 changed files
with
730 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,301 @@ | ||
import React from 'react'; | ||
import { EventEmitter } from 'fbemitter'; | ||
import { NavigationEvents } from 'react-navigation'; | ||
import { AppState, AsyncStorage, Platform, StyleSheet, Text, View } from 'react-native'; | ||
import { Location, MapView, Permissions, TaskManager } from 'expo'; | ||
import { FontAwesome, MaterialIcons } from '@expo/vector-icons'; | ||
|
||
import Button from '../components/PrimaryButton'; | ||
import Colors from '../constants/Colors'; | ||
|
||
const STORAGE_KEY = 'expo-home-locations'; | ||
const LOCATION_UPDATES_TASK = 'location-updates'; | ||
|
||
const locationEventsEmitter = new EventEmitter(); | ||
|
||
export default class BackgroundLocationScreen extends React.Component { | ||
static navigationOptions = { | ||
title: 'Background location', | ||
}; | ||
|
||
mapViewRef = React.createRef(); | ||
|
||
state = { | ||
accuracy: Location.Accuracy.High, | ||
isTracking: false, | ||
showsBackgroundLocationIndicator: false, | ||
savedLocations: [], | ||
initialRegion: null, | ||
error: null, | ||
}; | ||
|
||
didFocus = async () => { | ||
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(); | ||
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={Colors.tintColor} | ||
/> | ||
); | ||
} | ||
|
||
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}> | ||
<Text>{this.state.showsBackgroundLocationIndicator ? 'Hide' : 'Show'}</Text> | ||
<Text> background </Text> | ||
<FontAwesome name="location-arrow" size={20} color="white" /> | ||
<Text> indicator</Text> | ||
</Button> | ||
)} | ||
<Button style={styles.button} onPress={this.onAccuracyChange}> | ||
Accuracy: {Location.Accuracy[this.state.accuracy]} | ||
</Button> | ||
</View> | ||
<View style={styles.buttonsColumn}> | ||
<Button style={styles.button} onPress={this.onCenterMap}> | ||
<MaterialIcons name="my-location" size={20} color="white" /> | ||
</Button> | ||
</View> | ||
</View> | ||
|
||
<View style={styles.bottomButtons}> | ||
<Button style={styles.button} onPress={this.clearLocations}> | ||
Clear locations | ||
</Button> | ||
<Button style={styles.button} onPress={this.toggleTracking}> | ||
{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 []; | ||
} | ||
} | ||
|
||
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, | ||
}, | ||
}); |
Oops, something went wrong.