Skip to content

[Bug]: AudioGuidance is still playing even when set SILENT #516

@lahamilton14

Description

@lahamilton14

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

Metadata

Metadata

Assignees

Labels

priority: p1Important issue which blocks shipping the next release. Will be fixed prior to next release.type: bugError or flaw in code with unintended results or allowing sub-optimal usage patterns.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions