-
Notifications
You must be signed in to change notification settings - Fork 24
Description
Is there an existing issue for this?
- I have searched the existing issues
Description of the bug
AudioGuidance.SILENT does not seem to disable turn-by-turn voice instructions when using NavigationView. Even when I explicitly set both the audioGuidance prop and setAudioGuidance to SILENT, the SDK still speaks navigation instructions.
I am trying to have visual navigation only (no TTS, no alerts), but voice guidance keeps playing.
React Native version
0.79.6
React version
19.0.0
Package version
^0.10.3
Native SDK versions
- I haven't changed the version of the native SDKs
React Native Doctor Output
N/A
Steps to reproduce
I’ve also tried:
Toggling between AudioGuidance.VOICE_ALERTS_AND_GUIDANCE and AudioGuidance.SILENT with a button and calling nav.setAudioGuidance(...) each time.
Only using the audioGuidance prop on NavigationView.
Only using nav.setAudioGuidance.
In all cases, when navigation starts the SDK still plays full voice instructions (e.g. “In 300 metres, turn left…”).
Expected vs Actual Behavior
Expected behavior
When audioGuidance={AudioGuidance.SILENT} is passed to NavigationView (and/or nav.setAudioGuidance(AudioGuidance.SILENT) is called), no audio output should be produced:
No turn-by-turn TTS
No alerts
Ideally no vibrations either
Essentially: a “silent mode” with visual guidance only.
Actual behavior
Turn-by-turn TTS still plays through the device speaker / Bluetooth.
Changing the audioGuidance value at runtime doesn’t seem to affect the behavior: voice guidance continues even when set to SILENT.
Code Sample
// TurnByTurnScreen.js — iOS + Android (JS)
// Requires: @googlemaps/react-native-navigation-sdk, expo-location
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { View, Pressable, Text, InteractionManager, TouchableOpacity, StyleSheet, Alert, Platform, Modal } from 'react-native';
import * as Location from 'expo-location';
import { Ionicons, MaterialIcons } from '@expo/vector-icons';
import {
NavigationView,
NavigationProvider,
useNavigation as useGoogleNav,
TravelMode,
// 🔥 AudioGuidance REMOVED
} from '@googlemaps/react-native-navigation-sdk';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const COLORS = {
ink: '#0b1f3a',
card: 'rgba(10,20,35,0.78)',
stroke: 'rgba(170,218,255,0.35)',
text: '#e6f2ff',
accent: '#aadaff',
lime: '#6ee36e',
};
const DISPLAY_OPTS = {
showTrafficLights: true,
showStopSigns: true,
showDestinationMarkers: true,
};
export default function TurnByTurnScreen(props) {
if (Platform.OS === 'ios') {
return (
<NavigationProvider>
<TurnByTurnInner {...props} />
</NavigationProvider>
);
}
return <TurnByTurnInner {...props} />;
}
function TurnByTurnInner({ route, navigation }) {
const insets = useSafeAreaInsets();
// --- ANDROID HOST LIFECYCLE GUARD ---
const [androidHostReady, setAndroidHostReady] = useState(Platform.OS !== 'android');
const [navMountKey, setNavMountKey] = useState(0);
// --- Google Navigation SDK handles ---
const { navigationController, addListeners, removeListeners } = useGoogleNav();
const mapCtrlRef = useRef(null);
const navUiCtrlRef = useRef(null);
// --- UI / state ---
const [started, setStarted] = useState(false);
const [gotNavCtrl, setGotNavCtrl] = useState(false);
const [hasFix, setHasFix] = useState(false);
const [trafficOn, setTrafficOn] = useState(true); // show same icon on iOS too
const [routePref, setRoutePref] = useState('FASTEST'); // FASTEST | SHORTEST
const [optionsOpen, setOptionsOpen] = useState(false);
const hasFixRef = useRef(false);
const lastFixRef = useRef({ lat: null, lng: null, bearing: 0 });
const NAV_TOP = Platform.select({ android: 10, ios: 6 });
const NAV_BOTTOM = Platform.select({ android: insets.bottom + 244, ios: insets.bottom + 210 });
// --- normalize incoming coordinates ---
const normAny = (o) => {
if (!o) return undefined;
if (Number.isFinite(+o?.lat) && Number.isFinite(+o?.lng)) return { lat: +o.lat, lng: +o.lng };
if (Number.isFinite(+o?.latitude) && Number.isFinite(+o?.longitude)) return { lat: +o.latitude, lng: +o.longitude };
return undefined;
};
const p = route?.params ?? {};
const _dest = normAny(p.dest) ?? normAny(p.destination);
const _pickup = normAny(p.pickup) ?? normAny(p.origin);
const _stops = Array.isArray(p.stops) ? p.stops.map(normAny).filter(Boolean) : [];
const _title = p.title ?? 'Destination';
const _afterPickup = !!p.afterPickup;
const isLatLng = (pt) =>
pt && Number.isFinite(pt.lat) && Number.isFinite(pt.lng) && !(pt.lat === 0 && pt.lng === 0);
const waypoints = useMemo(() => {
if (!_afterPickup) {
if (_pickup && isLatLng(_pickup)) return [{ title: 'Pickup', position: _pickup }];
if (_dest && isLatLng(_dest)) return [{ title: _title, position: _dest }];
return [];
}
const out = [];
_stops.forEach((s, i) => out.push({ title: `Stop ${i + 1}`, position: s }));
if (_dest && isLatLng(_dest)) out.push({ title: _title || 'Dropoff', position: _dest });
return out;
}, [_afterPickup, _pickup, _dest, _title, _stops]);
// --- Location perms upfront ---
useEffect(() => {
(async () => {
try {
const { status } = await Location.getForegroundPermissionsAsync();
if (status !== 'granted') {
const req = await Location.requestForegroundPermissionsAsync();
if (req.status !== 'granted') {
Alert.alert('Location required', 'Please allow location to start turn-by-turn.');
}
}
} catch {}
})();
}, []);
// 🔇 Init nav + aggressively mute guidance (no AudioGuidance enum)
useEffect(() => {
(async () => {
try {
if (!navigationController?.init) return;
if (Platform.OS === 'android' && !androidHostReady) return;
await navigationController.init();
try {
// Try all available mute flags
navigationController.setVoiceGuidanceMuted?.(true);
navigationController.setVoiceGuidanceEnabled?.(false);
navigationController.setTrafficPromptsEnabled?.(false);
} catch (e) {
console.log('[TurnByTurn] mute audio guidance error', e);
}
navigationController.setTrafficIncidentCardsEnabled?.(true);
navigationController.setReportIncidentButtonEnabled?.(true);
setGotNavCtrl(true);
} catch (e) {
Alert.alert('Navigation init failed', e?.message || String(e));
}
})();
}, [navigationController, androidHostReady]);
const reassertTripUI = () => {
try {
// UI controller (both platforms)
navUiCtrlRef.current?.setNavigationUIEnabled?.(true);
navUiCtrlRef.current?.setNavigationHeaderEnabled?.(true);
navUiCtrlRef.current?.setNavigationFooterEnabled?.(true);
navUiCtrlRef.current?.setTripProgressBarEnabled?.(true);
navUiCtrlRef.current?.setSpeedometerEnabled?.(true);
navUiCtrlRef.current?.setSpeedLimitIconEnabled?.(true);
// Map controller (mostly iOS)
mapCtrlRef.current?.setNavigationHeaderEnabled?.(true);
mapCtrlRef.current?.setNavigationFooterEnabled?.(true);
mapCtrlRef.current?.setNavigationTripProgressBarEnabled?.(true);
mapCtrlRef.current?.setShouldDisplaySpeedometer?.(true);
mapCtrlRef.current?.setShowsTrafficLights?.(true);
mapCtrlRef.current?.setShowsStopSigns?.(true);
} catch {}
};
// --- Listeners ---
useEffect(() => {
// ❌ Don’t attach listeners until the Android host is ready
if (Platform.OS === 'android' && !androidHostReady) return;
const callbacks = {
onLocationChanged: (ev) => {
const lat = ev?.location?.position?.lat ?? ev?.location?.lat ?? ev?.lat;
const lng = ev?.location?.position?.lng ?? ev?.location?.lng ?? ev?.lng;
const bearing =
ev?.location?.bearing ??
ev?.bearing ??
ev?.location?.heading ??
0;
if (Number.isFinite(lat) && Number.isFinite(lng)) {
lastFixRef.current = { lat, lng, bearing: Number.isFinite(bearing) ? bearing : 0 };
if (!hasFixRef.current) {
hasFixRef.current = true;
setHasFix(true);
}
}
},
onArrival: (evt) => {
if (evt?.isFinalDestination) {
try { navigationController.stopGuidance(); } catch {}
navigation?.goBack?.();
} else {
try {
navigationController.continueToNextDestination();
navigationController.startGuidance();
} catch {}
}
},
onRouteChanged: () => {
try {
navigationController.setDisplayOptions?.(DISPLAY_OPTS);
} catch {}
reassertTripUI();
},
};
addListeners(callbacks);
return () => removeListeners(callbacks);
}, [addListeners, removeListeners, navigation, navigationController, androidHostReady]);
useEffect(() => {
if (Platform.OS !== 'android') return;
const task = InteractionManager.runAfterInteractions(() => setAndroidHostReady(true));
return () => {
if (task?.cancel) task.cancel();
setAndroidHostReady(false);
};
}, []);
// --- Start guidance when ready ---
useEffect(() => {
if (!gotNavCtrl || !waypoints.length || started || !androidHostReady) return;
let cancelled = false;
const tryStart = async () => {
try {
const displayOptions = {
...DISPLAY_OPTS,
routeSelectionPreference: routePref,
shouldShowAlternativeRoutes: false,
};
await navigationController.setDestinations(
waypoints,
{ travelMode: TravelMode.DRIVING, avoidFerries: false, avoidTolls: false },
displayOptions,
{ ...DISPLAY_OPTS, routeSelectionPreference: routePref, shouldShowAlternativeRoutes: false }
);
navigationController.setReroutingEnabled?.(true);
navigationController.setTrafficPromptsEnabled?.(false);
await navigationController.startGuidance();
// ✅ Re-assert Mute Settings immediately after starting guidance
try {
navigationController.setVoiceGuidanceMuted?.(true);
navigationController.setVoiceGuidanceEnabled?.(false);
navigationController.setTrafficPromptsEnabled?.(false);
} catch (e) {
console.log('[TurnByTurn] re-assert mute audio guidance error', e);
}
try {
await navigationController.setDisplayOptions?.(DISPLAY_OPTS);
navigationController.setTrafficIncidentCardsEnabled?.(true);
} catch {}
if (!cancelled) {
setStarted(true);
healAndroidTraffic();
reassertTripUI();
setTimeout(reassertTripUI, 300);
}
} catch {
if (!cancelled) setTimeout(tryStart, 900);
}
};
tryStart();
return () => { cancelled = true; };
}, [gotNavCtrl, waypoints, started, routePref, navigationController, androidHostReady]);
// --- Map controller setup ---
const onMapViewControllerCreated = useCallback(async (evt) => {
const ctrl = evt?.nativeEvent?.mapViewController ?? evt;
mapCtrlRef.current = ctrl;
try {
ctrl?.setMyLocationEnabled?.(true);
ctrl?.setTrafficEnabled?.(true);
ctrl?.setTrafficIncidentsEnabled?.(true);
ctrl?.setBuildingsEnabled?.(true);
// --- iOS-only always-on UI (no-ops on Android) ---
ctrl?.setNavigationHeaderEnabled?.(true);
ctrl?.setNavigationFooterEnabled?.(true);
ctrl?.setNavigationTripProgressBarEnabled?.(true); // trip progress bar
ctrl?.setShouldDisplaySpeedometer?.(true); // speedometer control
ctrl?.setShowsTrafficLights?.(true); // traffic lights
ctrl?.setShowsStopSigns?.(true); // stop signs
setTrafficOn(true);
} catch {}
}, [_pickup, _dest]);
// --- Android traffic heal ---
const healAndroidTraffic = useCallback(() => {
if (Platform.OS !== 'android') return;
const ctrl = mapCtrlRef.current;
if (!ctrl?.setTrafficEnabled) return;
try {
ctrl.setTrafficEnabled(true);
ctrl.setTrafficIncidentsEnabled?.(true);
setTimeout(() => {
ctrl.setTrafficEnabled(false);
ctrl.setTrafficEnabled(true);
ctrl.setTrafficIncidentsEnabled?.(true);
}, 250);
setTimeout(() => {
ctrl.setTrafficEnabled(true);
ctrl.setTrafficIncidentsEnabled?.(true);
}, 1200);
} catch {}
}, []);
const onNavigationViewControllerCreated = useCallback((ui) => {
try {
navUiCtrlRef.current = ui;
ui?.setNavigationUIEnabled?.(true);
ui?.setRecenterButtonEnabled?.(true);
// --- Always-on core UI across platforms ---
ui?.setNavigationHeaderEnabled?.(true);
ui?.setNavigationFooterEnabled?.(true);
ui?.setTripProgressBarEnabled?.(true); // trip progress bar
ui?.setSpeedometerEnabled?.(true); // speedometer control
ui?.setSpeedLimitIconEnabled?.(true); // speed limit icon
ui?.showRouteOverview?.();
} catch {}
}, []);
const onNavigationControllerCreated = useCallback(() => {}, []);
const onNavigationUiControllerCreated = useCallback(() => {}, []);
// iOS recenter reliability shim: re-assert after NV is ready
const onNavigationReady = useCallback(async () => {
try {
// Wake recenter
navUiCtrlRef.current?.setRecenterButtonEnabled?.(false);
navUiCtrlRef.current?.setRecenterButtonEnabled?.(true);
reassertTripUI();
setTimeout(reassertTripUI, 200);
setTimeout(reassertTripUI, 800);
// Re-assert core UI (both platforms; no-ops where unsupported)
navUiCtrlRef.current?.setNavigationHeaderEnabled?.(true);
navUiCtrlRef.current?.setNavigationFooterEnabled?.(true);
navUiCtrlRef.current?.setTripProgressBarEnabled?.(true);
navUiCtrlRef.current?.setSpeedometerEnabled?.(true);
navUiCtrlRef.current?.setSpeedLimitIconEnabled?.(true);
// Ensure location toggle for recenter to function
mapCtrlRef.current?.setMyLocationEnabled?.(true);
} catch {}
}, []);
useEffect(() => {
return () => {
try { navigationController.stopGuidance(); } catch {}
// 🔐 extra safety so no stale refs can dispatch commands
mapCtrlRef.current = null;
navUiCtrlRef.current = null;
};
}, [navigationController]);
// --- Helpers / actions ---
const showOverview = () => { try { navUiCtrlRef.current?.showRouteOverview?.(); } catch {} };
const showAlternatives = async () => {
try {
await navigationController.setDestinations(
waypoints,
{ travelMode: TravelMode.DRIVING },
{ ...DISPLAY_OPTS, shouldShowAlternativeRoutes: true, routeSelectionPreference: routePref }
);
navUiCtrlRef.current?.showRouteOverview?.();
} catch {}
};
const applyRoutePref = async (pref) => {
try {
setRoutePref(pref);
await navigationController.setDestinations(
waypoints,
{ travelMode: TravelMode.DRIVING },
{ ...DISPLAY_OPTS, shouldShowAlternativeRoutes: false, routeSelectionPreference: pref }
);
navigationController.startGuidance?.();
healAndroidTraffic();
} catch {}
};
const toggleTraffic = () => {
const next = !trafficOn;
setTrafficOn(next);
try {
mapCtrlRef.current?.setTrafficEnabled?.(next);
mapCtrlRef.current?.setTrafficIncidentsEnabled?.(next);
} catch {}
};
const recenterNow = useCallback(async () => {
try {
// Prefer official camera-mode if available
if (navigationController?.setCameraMode) {
await navigationController.setCameraMode('FOLLOWING');
}
} catch {}
// Fallback: move camera to last known fix
try {
const fix = lastFixRef.current;
if (fix?.lat && fix?.lng && mapCtrlRef.current?.moveCamera) {
mapCtrlRef.current.moveCamera({
target: { lat: fix.lat, lng: fix.lng },
zoom: 18,
bearing: Number.isFinite(fix.bearing) ? fix.bearing : 0,
tilt: 45,
});
}
} catch {}
}, [navigationController]);
return (
<View style={styles.container}>
{/* Real fragment host: absolute, not collapsable. Only render when androidHostReady */}
{androidHostReady && (
<View
style={[StyleSheet.absoluteFill, { top: NAV_TOP, bottom: Platform.OS === 'android' ? NAV_BOTTOM - 2 : NAV_BOTTOM }]}
collapsable={false}
>
<NavigationView
style={StyleSheet.absoluteFill}
androidStylingOptions={{
primaryDayModeThemeColor: COLORS.ink,
headerDistanceValueTextColor: '#ffffff',
headerInstructionsFirstRowTextSize: '20',
}}
iOSStylingOptions={{
navigationHeaderPrimaryBackgroundColor: COLORS.ink,
navigationHeaderDistanceValueTextColor: '#ffffff',
}}
onMapViewControllerCreated={onMapViewControllerCreated}
onNavigationViewControllerCreated={onNavigationViewControllerCreated}
onNavigationControllerCreated={onNavigationControllerCreated}
onNavigationUiControllerCreated={onNavigationUiControllerCreated}
onNavigationReady={onNavigationReady}
/>
</View>
)}
{/* Right-side controls — pushed down below status bar on BOTH OS */}
<View
style={{
position: 'absolute',
right: 14,
top: insets.top + 20, // extra padding from the top
gap: 10,
}}
>
<Pressable onPress={showOverview} style={styles.fab}>
<Ionicons name="map-outline" size={20} color={COLORS.accent} />
</Pressable>
<Pressable onPress={showAlternatives} style={styles.fab}>
<Ionicons name="git-branch-outline" size={20} color={COLORS.accent} />
</Pressable>
{/* Traffic toggle (iOS + Android) */}
<Pressable onPress={toggleTraffic} style={[styles.fab, !trafficOn && styles.fabOff]}>
<MaterialIcons name="traffic" size={20} color={trafficOn ? COLORS.lime : '#8aa7c4'} />
</Pressable>
{/* Voice guidance button is already removed */}
{/* Options button (opens modal) — directly under traffic icon */}
<Pressable onPress={() => setOptionsOpen(true)} style={styles.fab}>
<Ionicons name="options-outline" size={20} color={COLORS.accent} />
</Pressable>
</View>
{/* MODAL with the items that used to be at the bottom */}
<Modal
visible={optionsOpen}
transparent
animationType="fade"
onRequestClose={() => setOptionsOpen(false)}
>
<View style={styles.modalBackdrop}>
<View style={styles.modalCard}>
<Text style={styles.modalTitle}>Navigation Options</Text>
{/* Route preference */}
<View style={styles.row}>
<TouchableOpacity onPress={() => applyRoutePref('FASTEST')} style={[styles.pill, routePref === 'FASTEST' && styles.pillOn]}>
<Text style={[styles.pillText, routePref === 'FASTEST' && styles.pillTextOn]}>Fastest</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => applyRoutePref('SHORTEST')} style={[styles.pill, routePref === 'SHORTEST' && styles.pillOn]}>
<Text style={[styles.pillText, routePref === 'SHORTEST' && styles.pillTextOn]}>Shortest</Text>
</TouchableOpacity>
</View>
{/* Quick report shortcuts */}
<Text style={styles.sectionLabel}>Quick report</Text>
<View style={styles.rowWrap}>
<TouchableOpacity onPress={() => Alert.alert('Report', 'Use the system report button on the map.')} style={styles.pill}><Text style={styles.pillText}>Police</Text></TouchableOpacity>
<TouchableOpacity onPress={() => Alert.alert('Report', 'Use the system report button on the map.')} style={styles.pill}><Text style={styles.pillText}>Speed trap</Text></TouchableOpacity>
<TouchableOpacity onPress={() => Alert.alert('Report', 'Use the system report button on the map.')} style={styles.pill}><Text style={styles.pillText}>Roadworks</Text></TouchableOpacity>
<TouchableOpacity onPress={() => Alert.alert('Report', 'Use the system report button on the map.')} style={styles.pill}><Text style={styles.pillText}>Roadblock</Text></TouchableOpacity>
</View>
<TouchableOpacity onPress={() => setOptionsOpen(false)} style={[styles.pill, { alignSelf: 'center', marginTop: 12 }]}>
<Text style={styles.pillText}>Close</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
{/* END MODAL */}
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#000' },
fab: {
backgroundColor: COLORS.card,
borderColor: COLORS.stroke,
borderWidth: 1,
padding: 10,
borderRadius: 14,
},
fabOff: { opacity: 0.7 },
// pills reused inside modal
pill: {
backgroundColor: '#1b2c45',
paddingVertical: 6,
paddingHorizontal: 10,
borderRadius: 12,
borderWidth: 1,
borderColor: COLORS.stroke,
marginRight: 8,
marginBottom: 8,
},
pillOn: {
backgroundColor: 'rgba(7, 194, 106, 0.25)',
borderColor: 'rgba(170,218,255,0.55)',
},
pillText: { color: COLORS.accent, fontWeight: '600', fontSize: 12 },
pillTextOn: { color: '#ffffff' },
// modal styles
modalBackdrop: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.45)',
justifyContent: 'flex-end',
},
modalCard: {
backgroundColor: '#0f2038',
borderTopLeftRadius: 18,
borderTopRightRadius: 18,
padding: 16,
borderTopWidth: 1,
borderColor: COLORS.stroke,
},
modalTitle: { color: COLORS.text, fontWeight: '700', fontSize: 16, marginBottom: 10 },
sectionLabel: { color: COLORS.accent, marginTop: 6, marginBottom: 6, fontSize: 12, fontWeight: '600' },
row: { flexDirection: 'row', alignItems: 'center', marginBottom: 8 },
rowWrap: { flexDirection: 'row', flexWrap: 'wrap' },
recenterBtn: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
backgroundColor: '#e6f2ff',
paddingVertical: 10,
paddingHorizontal: 14,
borderRadius: 10,
borderWidth: 1,
borderColor: COLORS.stroke,
},
recenterTxt: { color: COLORS.ink, fontWeight: '700', letterSpacing: 0.5 },
});Additional Context
Is there an additional step required to fully disable voice guidance?
Is AudioGuidance.SILENT currently expected to silence all TTS output, or only some subset of alerts?
If there is another supported way to render navigation with visuals only (no TTS), that would solve my use case.
Thank you