From 885755b179869235300a3b32fc7677ce4726b1fd Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Wed, 18 Feb 2026 19:22:06 -0800 Subject: [PATCH 1/5] RU-T47 Fixing PTT issue --- .eslintrc.js | 1 + package.json | 12 +- plugins/withInCallAudioModule.js | 48 +- .../call-detail-menu-analytics.test.tsx | 8 + .../call-detail-menu-integration.test.tsx | 9 + .../livekit/livekit-bottom-sheet.tsx | 33 +- ...oth-device-selection-bottom-sheet.test.tsx | 2 +- .../settings/audio-device-selection.tsx | 27 +- .../settings/bluetooth-device-item.tsx | 5 +- ...luetooth-device-selection-bottom-sheet.tsx | 32 +- src/components/sidebar/call-sidebar.tsx | 5 +- .../gps-coordinate-duplication-fix.test.tsx | 9 + .../status-gps-integration-working.test.tsx | 9 + .../__tests__/status-gps-integration.test.tsx | 9 + src/components/ui/bottom-sheet.tsx | 6 +- src/components/ui/html-renderer/index.web.tsx | 9 +- .../__tests__/useLiveKitCallStore.test.ts | 6 + .../livekit-call/store/useLiveKitCallStore.ts | 5 + src/lib/logging/index.tsx | 5 +- src/services/__tests__/audio.service.test.ts | 6 + .../__tests__/bluetooth-audio.service.test.ts | 49 ++ src/services/audio.service.ts | 8 +- src/services/bluetooth-audio.service.ts | 646 ++++++++++++++++-- src/services/callkeep.service.android.ts | 90 ++- src/stores/app/livekit-store.ts | 74 +- .../calls/__tests__/detail-store.test.ts | 32 +- src/translations/ar.json | 7 + src/translations/en.json | 7 + src/translations/es.json | 7 + yarn.lock | 77 +-- 30 files changed, 1012 insertions(+), 231 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 96a6bed9..a69187ca 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -44,6 +44,7 @@ module.exports = { files: ['src/translations/*.json'], extends: ['plugin:i18n-json/recommended'], rules: { + '@typescript-eslint/consistent-type-imports': 'off', 'i18n-json/valid-message-syntax': [ 2, { diff --git a/package.json b/package.json index c1e93dcc..6122de68 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "lint": "eslint src --ext .ts,.tsx --cache --cache-location node_modules/.cache/eslint", "type-check": "tsc --noemit", "lint:translations": "eslint ./src/translations/ --fix --ext .json ", - "test": "jest --coverage=true --coverageReporters=cobertura", + "test": "jest --runInBand --coverage=true --coverageReporters=cobertura", "check-all": "yarn run lint && yarn run type-check && yarn run lint:translations", "test:ci": "yarn run test --coverage", "test:watch": "yarn run test --watch", @@ -109,6 +109,7 @@ "app-icon-badge": "^0.1.2", "axios": "~1.12.0", "babel-plugin-module-resolver": "^5.0.2", + "babel-plugin-transform-import-meta": "^2.3.3", "buffer": "^6.0.3", "countly-sdk-react-native-bridge": "25.4.1", "date-fns": "^4.1.0", @@ -174,8 +175,7 @@ "react-query-kit": "~3.3.0", "tailwind-variants": "~0.2.1", "zod": "~3.23.8", - "zustand": "~4.5.5", - "babel-plugin-transform-import-meta": "^2.3.3" + "zustand": "~4.5.5" }, "devDependencies": { "@babel/core": "~7.26.0", @@ -191,8 +191,8 @@ "@types/mapbox-gl": "3.4.1", "@types/react": "~19.0.10", "@types/react-native-base64": "~0.2.2", - "@typescript-eslint/eslint-plugin": "~5.62.0", - "@typescript-eslint/parser": "~5.62.0", + "@typescript-eslint/eslint-plugin": "~7.18.0", + "@typescript-eslint/parser": "~7.18.0", "babel-jest": "~30.0.0", "concurrently": "9.2.1", "cross-env": "~7.0.3", @@ -226,7 +226,7 @@ "tailwindcss": "3.4.4", "ts-jest": "~29.1.2", "ts-node": "~10.9.2", - "typescript": "~5.8.3", + "typescript": "~5.5.4", "wait-on": "9.0.3" }, "repository": { diff --git a/plugins/withInCallAudioModule.js b/plugins/withInCallAudioModule.js index 5add14d4..51ad5a8d 100644 --- a/plugins/withInCallAudioModule.js +++ b/plugins/withInCallAudioModule.js @@ -12,7 +12,6 @@ import android.content.Context import android.media.AudioAttributes import android.media.AudioManager import android.media.SoundPool -import android.os.Build import android.util.Log import com.facebook.react.bridge.* @@ -101,6 +100,53 @@ class InCallAudioModule(reactContext: ReactApplicationContext) : ReactContextBas } } + @ReactMethod + fun setAudioRoute(route: String, promise: Promise) { + try { + val audioManager = reactApplicationContext.getSystemService(Context.AUDIO_SERVICE) as? AudioManager + if (audioManager == null) { + promise.reject("AUDIO_MANAGER_UNAVAILABLE", "AudioManager is not available") + return + } + + val normalizedRoute = route.lowercase() + audioManager.mode = AudioManager.MODE_IN_COMMUNICATION + + when (normalizedRoute) { + "bluetooth" -> { + audioManager.isSpeakerphoneOn = false + audioManager.isBluetoothScoOn = true + if (audioManager.isBluetoothScoAvailableOffCall) { + audioManager.startBluetoothSco() + } + } + + "speaker" -> { + audioManager.stopBluetoothSco() + audioManager.isBluetoothScoOn = false + audioManager.isSpeakerphoneOn = true + } + + "earpiece", "default" -> { + audioManager.stopBluetoothSco() + audioManager.isBluetoothScoOn = false + audioManager.isSpeakerphoneOn = false + } + + else -> { + promise.reject("INVALID_AUDIO_ROUTE", "Unsupported audio route: $route") + return + } + } + + Log.d(TAG, "Audio route set to: $normalizedRoute") + promise.resolve(true) + } catch (error: Exception) { + Log.e(TAG, "Failed to set audio route: $route", error) + promise.reject("SET_AUDIO_ROUTE_FAILED", error.message, error) + } + } + @ReactMethod fun cleanup() { soundPool?.release() diff --git a/src/components/calls/__tests__/call-detail-menu-analytics.test.tsx b/src/components/calls/__tests__/call-detail-menu-analytics.test.tsx index 575387fe..e76dbd05 100644 --- a/src/components/calls/__tests__/call-detail-menu-analytics.test.tsx +++ b/src/components/calls/__tests__/call-detail-menu-analytics.test.tsx @@ -6,6 +6,14 @@ import { useAnalytics } from '@/hooks/use-analytics'; // Mock dependencies jest.mock('@/hooks/use-analytics'); +jest.mock('@/lib/logging', () => ({ + logger: { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, +})); jest.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key, diff --git a/src/components/calls/__tests__/call-detail-menu-integration.test.tsx b/src/components/calls/__tests__/call-detail-menu-integration.test.tsx index 1f1d347b..0adbf937 100644 --- a/src/components/calls/__tests__/call-detail-menu-integration.test.tsx +++ b/src/components/calls/__tests__/call-detail-menu-integration.test.tsx @@ -65,6 +65,15 @@ jest.mock('@/components/ui/', () => ({ }, })); +jest.mock('@/lib/logging', () => ({ + logger: { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, +})); + describe('Call Detail Menu Integration Test', () => { const mockOnEditCall = jest.fn(); const mockOnCloseCall = jest.fn(); diff --git a/src/components/livekit/livekit-bottom-sheet.tsx b/src/components/livekit/livekit-bottom-sheet.tsx index 88e1fdc8..33a37adb 100644 --- a/src/components/livekit/livekit-bottom-sheet.tsx +++ b/src/components/livekit/livekit-bottom-sheet.tsx @@ -6,7 +6,6 @@ import { ScrollView, StyleSheet, TouchableOpacity, View } from 'react-native'; import { useAnalytics } from '@/hooks/use-analytics'; import { type DepartmentVoiceChannelResultData } from '@/models/v4/voice/departmentVoiceResultData'; -import { audioService } from '@/services/audio.service'; import { useBluetoothAudioStore } from '@/stores/app/bluetooth-audio-store'; import { Card } from '../../components/ui/card'; @@ -35,6 +34,9 @@ export const LiveKitBottomSheet = () => { const isConnected = useLiveKitStore((s) => s.isConnected); const isConnecting = useLiveKitStore((s) => s.isConnecting); const isTalking = useLiveKitStore((s) => s.isTalking); + const isMicrophoneEnabled = useLiveKitStore((s) => s.isMicrophoneEnabled); + const setMicrophoneEnabled = useLiveKitStore((s) => s.setMicrophoneEnabled); + const lastLocalMuteChangeTimestamp = useLiveKitStore((s) => s.lastLocalMuteChangeTimestamp); const selectedAudioDevices = useBluetoothAudioStore((s) => s.selectedAudioDevices); const { colorScheme } = useColorScheme(); @@ -77,11 +79,8 @@ export const LiveKitBottomSheet = () => { // Sync mute state with LiveKit room useEffect(() => { - if (currentRoom?.localParticipant) { - const micEnabled = currentRoom.localParticipant.isMicrophoneEnabled; - setIsMuted(!micEnabled); - } - }, [currentRoom?.localParticipant, currentRoom?.localParticipant?.isMicrophoneEnabled]); + setIsMuted(!isMicrophoneEnabled); + }, [isMicrophoneEnabled, lastLocalMuteChangeTimestamp]); useEffect(() => { // If we're showing the sheet, make sure we have the latest rooms @@ -135,25 +134,11 @@ export const LiveKitBottomSheet = () => { ); const handleMuteToggle = useCallback(async () => { - if (currentRoom?.localParticipant) { - const newMicEnabled = isMuted; // If currently muted, enable mic - try { - await currentRoom.localParticipant.setMicrophoneEnabled(newMicEnabled); - setIsMuted(!newMicEnabled); - - // Play appropriate sound based on mute state - if (newMicEnabled) { - // Mic is being unmuted - await audioService.playStartTransmittingSound(); - } else { - // Mic is being muted - await audioService.playStopTransmittingSound(); - } - } catch (error) { - console.error('Failed to toggle microphone:', error); - } + if (isConnected) { + const newMicEnabled = isMuted; + await setMicrophoneEnabled(newMicEnabled); } - }, [currentRoom, isMuted]); + }, [isConnected, isMuted, setMicrophoneEnabled]); const handleDisconnect = useCallback(() => { disconnectFromRoom(); diff --git a/src/components/settings/__tests__/bluetooth-device-selection-bottom-sheet.test.tsx b/src/components/settings/__tests__/bluetooth-device-selection-bottom-sheet.test.tsx index b3248a46..a91f01c2 100644 --- a/src/components/settings/__tests__/bluetooth-device-selection-bottom-sheet.test.tsx +++ b/src/components/settings/__tests__/bluetooth-device-selection-bottom-sheet.test.tsx @@ -338,7 +338,7 @@ describe('BluetoothDeviceSelectionBottomSheet', () => { render(); - expect(screen.getByText('bluetooth.bluetooth_disabled')).toBeTruthy(); + expect(screen.getByText('bluetooth.poweredOff')).toBeTruthy(); }); it('displays connection errors', () => { diff --git a/src/components/settings/audio-device-selection.tsx b/src/components/settings/audio-device-selection.tsx index 043c18f6..535aa85f 100644 --- a/src/components/settings/audio-device-selection.tsx +++ b/src/components/settings/audio-device-selection.tsx @@ -48,6 +48,25 @@ export const AudioDeviceSelection: React.FC = ({ show } }; + const getDeviceDisplayName = (device: AudioDeviceInfo) => { + const normalizedId = device.id.toLowerCase(); + const normalizedName = device.name.toLowerCase(); + + if (normalizedId === 'system-audio' || normalizedName === 'system audio' || normalizedName === 'system-audio' || normalizedName === 'system_audio') { + return t('settings.audio_device_selection.system_audio'); + } + + if (normalizedId === 'default-mic' || normalizedName === 'default microphone') { + return t('settings.audio_device_selection.default_microphone'); + } + + if (normalizedId === 'default-speaker' || normalizedName === 'default speaker') { + return t('settings.audio_device_selection.default_speaker'); + } + + return device.name; + }; + const renderDeviceItem = (device: AudioDeviceInfo, isSelected: boolean, onSelect: () => void, deviceType: 'microphone' | 'speaker') => { const deviceTypeLabel = getDeviceTypeLabel(device.type); const unavailableText = !device.isAvailable ? ` (${t('settings.audio_device_selection.unavailable')})` : ''; @@ -59,7 +78,7 @@ export const AudioDeviceSelection: React.FC = ({ show {renderDeviceIcon(device)} - {device.name} + {getDeviceDisplayName(device)} {deviceTypeLabel} {unavailableText} @@ -95,12 +114,14 @@ export const AudioDeviceSelection: React.FC = ({ show {t('settings.audio_device_selection.microphone')}: - {selectedAudioDevices.microphone?.name || t('settings.audio_device_selection.none_selected')} + + {selectedAudioDevices.microphone ? getDeviceDisplayName(selectedAudioDevices.microphone) : t('settings.audio_device_selection.none_selected')} + {t('settings.audio_device_selection.speaker')}: - {selectedAudioDevices.speaker?.name || t('settings.audio_device_selection.none_selected')} + {selectedAudioDevices.speaker ? getDeviceDisplayName(selectedAudioDevices.speaker) : t('settings.audio_device_selection.none_selected')} diff --git a/src/components/settings/bluetooth-device-item.tsx b/src/components/settings/bluetooth-device-item.tsx index e1aee960..6b0d5aa7 100644 --- a/src/components/settings/bluetooth-device-item.tsx +++ b/src/components/settings/bluetooth-device-item.tsx @@ -1,4 +1,4 @@ -import { BluetoothIcon, ChevronRightIcon } from 'lucide-react-native'; +import { ChevronRightIcon } from 'lucide-react-native'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -20,6 +20,9 @@ export const BluetoothDeviceItem = () => { const deviceDisplayName = React.useMemo(() => { if (preferredDevice) { + if (preferredDevice.id === 'system-audio') { + return t('bluetooth.system_audio'); + } return preferredDevice.name; } return t('bluetooth.no_device_selected'); diff --git a/src/components/settings/bluetooth-device-selection-bottom-sheet.tsx b/src/components/settings/bluetooth-device-selection-bottom-sheet.tsx index 84f9f8e6..ac50b684 100644 --- a/src/components/settings/bluetooth-device-selection-bottom-sheet.tsx +++ b/src/components/settings/bluetooth-device-selection-bottom-sheet.tsx @@ -37,14 +37,7 @@ export function BluetoothDeviceSelectionBottomSheet({ isOpen, onClose }: Bluetoo const connectionError = useBluetoothAudioStore((s) => s.connectionError); const [hasScanned, setHasScanned] = useState(false); const [connectingDeviceId, setConnectingDeviceId] = useState(null); - - // Start scanning when sheet opens - useEffect(() => { - if (isOpen && !hasScanned) { - startScan(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isOpen, hasScanned]); + const preferredDeviceDisplayName = preferredDevice?.id === 'system-audio' ? t('bluetooth.system_audio') : preferredDevice?.name; const startScan = React.useCallback(async () => { try { @@ -61,6 +54,13 @@ export function BluetoothDeviceSelectionBottomSheet({ isOpen, onClose }: Bluetoo } }, [t]); + // Start scanning when sheet opens + useEffect(() => { + if (isOpen && !hasScanned) { + startScan(); + } + }, [isOpen, hasScanned, startScan]); + const handleDeviceSelect = React.useCallback( async (device: BluetoothAudioDevice) => { try { @@ -222,7 +222,7 @@ export function BluetoothDeviceSelectionBottomSheet({ isOpen, onClose }: Bluetoo }, [isScanning, hasScanned, startScan, t]); return ( - + {t('bluetooth.select_device')} @@ -232,7 +232,7 @@ export function BluetoothDeviceSelectionBottomSheet({ isOpen, onClose }: Bluetoo {t('bluetooth.current_selection')} - {preferredDevice.name} + {preferredDeviceDisplayName}