diff --git a/app.config.ts b/app.config.ts index b353c33..86093ca 100644 --- a/app.config.ts +++ b/app.config.ts @@ -109,12 +109,14 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ 'android.permission.FOREGROUND_SERVICE', 'android.permission.FOREGROUND_SERVICE_MICROPHONE', 'android.permission.FOREGROUND_SERVICE_PHONE_CALL', - 'android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE', 'android.permission.READ_PHONE_STATE', 'android.permission.READ_PHONE_NUMBERS', 'android.permission.MANAGE_OWN_CALLS', ], - blockedPermissions: ['android.permission.READ_MEDIA_IMAGES', 'android.permission.READ_MEDIA_VIDEO'], + // FOREGROUND_SERVICE_CONNECTED_DEVICE is blocked, not merely absent: Bluetooth PTT handsets + // route through the microphone FGS session, so the type is unused, and Play rejects any + // declared foreground-service type whose use case cannot be demonstrated in the app. + blockedPermissions: ['android.permission.READ_MEDIA_IMAGES', 'android.permission.READ_MEDIA_VIDEO', 'android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE'], }, web: { favicon: './assets/favicon.png', @@ -142,8 +144,12 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ [ 'expo-secure-store', { - // Biometric-gated secure storage is not used; omit NSFaceIDUsageDescription. - faceIDPermission: false, + // Required even though biometric-gated storage is not used: expo-secure-store + // instantiates LAContext() unconditionally (SecureStoreModule.swift), so App Store + // static analysis flags a missing NSFaceIDUsageDescription with ITMS-90683 — the same + // way it flagged the omitted NSMotionUsageDescription. + faceIDPermission: + 'Resgrid Unit uses Face ID to unlock the securely stored credentials that keep you signed in to your department. For example, after your device locks, Face ID confirms it is you before the app restores your session and shows active calls.', }, ], 'expo-image', @@ -167,8 +173,12 @@ export default ({ config }: ConfigContext): ExpoConfig => ({ 'Resgrid Unit uses your location, including in the background, to keep your department dispatch map updated with your unit position (automatic vehicle location). For example, while you are en route to an emergency call, your unit location is periodically sent to dispatchers so they can track your arrival and coordinate resources, even when the app is not on screen.', locationAlwaysPermission: 'Resgrid Unit uses your location in the background to keep your department dispatch map updated with your unit position (automatic vehicle location). For example, while you are en route to an emergency call, your unit location is periodically sent to dispatchers so they can track your arrival and coordinate resources, even when the app is not on screen.', - // Motion activity APIs (getMotionActivityAsync) are not used; omit NSMotionUsageDescription. - motionUsagePermission: false, + // Required even though getMotionActivityAsync() is never called: expo-location links + // CoreMotion (MotionActivityPermissionRequester), and App Store static analysis rejects + // the binary with ITMS-90683 whenever the framework is referenced and the string is + // absent. Setting this to false previously caused that rejection. + motionUsagePermission: + 'Resgrid Unit uses motion data to improve the accuracy of your unit location on the department map. For example, while you are driving to a call, motion data helps distinguish travel from a stop so dispatchers see an accurate position and heading for your unit.', isIosBackgroundLocationEnabled: true, isAndroidBackgroundLocationEnabled: true, isAndroidForegroundServiceEnabled: true, diff --git a/customManifest.plugin.js b/customManifest.plugin.js index a8c858e..8af7e36 100644 --- a/customManifest.plugin.js +++ b/customManifest.plugin.js @@ -1,5 +1,7 @@ const { withAndroidManifest, AndroidConfig } = require('expo/config-plugins'); +const SERVICE_NAME = 'app.notifee.core.ForegroundService'; + const withForegroundService = (config) => { return withAndroidManifest(config, async (config) => { const manifest = config.modResults; @@ -11,13 +13,22 @@ const withForegroundService = (config) => { const mainApplication = AndroidConfig.Manifest.getMainApplicationOrThrow(manifest); mainApplication['service'] = mainApplication['service'] || []; - mainApplication['service'].push({ + + // Idempotent: a prebuild that reuses an existing android/ dir already has this service in + // the base manifest — and non-clean prebuilds have already accumulated duplicates there — so + // drop every copy before adding the canonical one. + const serviceEntry = { $: { - 'android:name': 'app.notifee.core.ForegroundService', - 'android:foregroundServiceType': 'microphone|connectedDevice', + 'android:name': SERVICE_NAME, + // microphone only. connectedDevice is intentionally absent — Bluetooth PTT handsets + // run on the same microphone session, and Play rejects foreground-service types whose + // use case cannot be demonstrated in the app. + 'android:foregroundServiceType': 'microphone', 'tools:replace': 'android:foregroundServiceType', }, - }); + }; + mainApplication['service'] = mainApplication['service'].filter((service) => service?.$?.['android:name'] !== SERVICE_NAME); + mainApplication['service'].push(serviceEntry); return config; }); }; diff --git a/src/stores/app/livekit-store.ts b/src/stores/app/livekit-store.ts index 283818b..0f6660d 100644 --- a/src/stores/app/livekit-store.ts +++ b/src/stores/app/livekit-store.ts @@ -737,25 +737,19 @@ 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, - // microphone: keeps mic capture legal while backgrounded (Android 14+). + // microphone only: 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], + // connectedDevice is deliberately NOT declared: Bluetooth PTT handsets route through + // the same mic session, so the type adds nothing and Play rejects FGS types whose use + // case cannot be demonstrated (Play policy: "Permission use is not declared or + // incorrectly declared"). + foregroundServiceTypes: [AndroidForegroundServiceType.FOREGROUND_SERVICE_TYPE_MICROPHONE], smallIcon: 'ic_launcher', }, });