diff --git a/customManifest.plugin.js b/customManifest.plugin.js index 46d8424..a8c858e 100644 --- a/customManifest.plugin.js +++ b/customManifest.plugin.js @@ -14,7 +14,7 @@ const withForegroundService = (config) => { mainApplication['service'].push({ $: { 'android:name': 'app.notifee.core.ForegroundService', - 'android:foregroundServiceType': 'microphone|mediaPlayback|connectedDevice', + 'android:foregroundServiceType': 'microphone|connectedDevice', 'tools:replace': 'android:foregroundServiceType', }, }); diff --git a/jest-setup.ts b/jest-setup.ts index 8291564..02f47a9 100644 --- a/jest-setup.ts +++ b/jest-setup.ts @@ -176,10 +176,16 @@ jest.mock('@notifee/react-native', () => { UNSPECIFIED: 'unspecified', }; + const AndroidForegroundServiceType = { + FOREGROUND_SERVICE_TYPE_MICROPHONE: 128, + FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE: 16, + }; + return { __esModule: true, default: mockNotifee, AndroidImportance, + AndroidForegroundServiceType, }; }); diff --git a/src/__tests__/no-self-mocking-suites.test.ts b/src/__tests__/no-self-mocking-suites.test.ts index 98cc807..7aefe70 100644 --- a/src/__tests__/no-self-mocking-suites.test.ts +++ b/src/__tests__/no-self-mocking-suites.test.ts @@ -67,7 +67,8 @@ const findSelfMock = (testFile: string): string | null => { const source = stripComments(fs.readFileSync(testFile, 'utf8')); // Match jest.mock('../') / jest.doMock("../"), with or without a factory. const selfMock = new RegExp(String.raw`jest\.(?:do)?[Mm]ock\(\s*['"\`]\.\./${subjectName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"\`]`); - return selfMock.test(source) ? path.relative(path.join(SRC, '..'), testFile) : null; + // Normalize to forward slashes so the known-debt list matches on Windows too. + return selfMock.test(source) ? path.relative(path.join(SRC, '..'), testFile).split(path.sep).join('/') : null; }; describe('test suites cover their real subject', () => { diff --git a/src/services/location.ts b/src/services/location.ts index 17ce5f2..56d6130 100644 --- a/src/services/location.ts +++ b/src/services/location.ts @@ -366,7 +366,16 @@ class LocationService { this.isRealtimeGeolocationEnabled = await loadRealtimeGeolocationState(); // Only request background permissions if the user has enabled background geolocation - const hasPermissions = await this.requestPermissions(this.isBackgroundGeolocationEnabled); + let hasPermissions: boolean; + try { + hasPermissions = await this.requestPermissions(this.isBackgroundGeolocationEnabled); + } catch (error) { + logger.error({ + message: 'Failed to request location permissions before starting updates', + context: { operation: 'startLocationUpdates', error }, + }); + throw error; + } if (!hasPermissions) { throw new Error('Location permissions not granted'); } diff --git a/src/stores/app/__tests__/livekit-store-room-switch.test.ts b/src/stores/app/__tests__/livekit-store-room-switch.test.ts index a42b360..4a5a984 100644 --- a/src/stores/app/__tests__/livekit-store-room-switch.test.ts +++ b/src/stores/app/__tests__/livekit-store-room-switch.test.ts @@ -24,7 +24,8 @@ jest.mock('@notifee/react-native', () => ({ stopForegroundService: jest.fn(), }, AndroidForegroundServiceType: { - FOREGROUND_SERVICE_TYPE_MICROPHONE: 1, + FOREGROUND_SERVICE_TYPE_MICROPHONE: 128, + FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE: 16, }, AndroidImportance: { DEFAULT: 3, diff --git a/src/stores/app/livekit-store.ts b/src/stores/app/livekit-store.ts index 3c05545..283818b 100644 --- a/src/stores/app/livekit-store.ts +++ b/src/stores/app/livekit-store.ts @@ -737,13 +737,25 @@ export const useLiveKitStore = create((set, get) => ({ // that triggers the already-registered handler. if (Platform.OS === 'android') { try { + // connectedDevice type is only legal when a bluetooth PTT handset is actually + // connected AND a runtime prerequisite (BLUETOOTH_CONNECT) is held — Android 14+ + // validates both at FGS start and throws SecurityException otherwise, blocking + // the service. Manifest FOREGROUND_SERVICE_CONNECTED_DEVICE alone is not enough. + let bluetoothDeviceActive = useBluetoothAudioStore.getState().connectedDevice !== null; + if (bluetoothDeviceActive) { + bluetoothDeviceActive = await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT); + } await notifee.displayNotification({ title: 'Active PTT Call', body: 'There is an active PTT call in progress.', android: { channelId: 'notif', asForegroundService: true, - foregroundServiceTypes: [AndroidForegroundServiceType.FOREGROUND_SERVICE_TYPE_MICROPHONE], + // microphone: keeps mic capture legal while backgrounded (Android 14+). + // Playback of remote audio needs no FGS type — any running FGS keeps the process alive. + foregroundServiceTypes: bluetoothDeviceActive + ? [AndroidForegroundServiceType.FOREGROUND_SERVICE_TYPE_MICROPHONE, AndroidForegroundServiceType.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE] + : [AndroidForegroundServiceType.FOREGROUND_SERVICE_TYPE_MICROPHONE], smallIcon: 'ic_launcher', }, });