Skip to content

Commit c560226

Browse files
committed
RD-T39 Fixing permissions issue for voice calls.
1 parent 666cedb commit c560226

5 files changed

Lines changed: 189 additions & 158 deletions

File tree

app.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,11 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
7878
'android.permission.POST_NOTIFICATIONS',
7979
'android.permission.FOREGROUND_SERVICE',
8080
'android.permission.FOREGROUND_SERVICE_MICROPHONE',
81+
'android.permission.FOREGROUND_SERVICE_PHONE_CALL',
8182
'android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE',
8283
'android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK',
8384
'android.permission.READ_PHONE_STATE',
85+
'android.permission.READ_PHONE_NUMBERS',
8486
'android.permission.MANAGE_OWN_CALLS',
8587
],
8688
},

src/components/livekit/__tests__/livekit-bottom-sheet.test.tsx

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -531,12 +531,10 @@ describe('LiveKitBottomSheet', () => {
531531
});
532532

533533
it('should track analytics event when bottom sheet is opened', () => {
534-
const requestPermissions = jest.fn();
535534
mockUseLiveKitStore.mockReturnValue({
536535
...defaultLiveKitState,
537536
isBottomSheetVisible: true,
538537
availableRooms: mockAvailableRooms,
539-
requestPermissions,
540538
});
541539

542540
render(<LiveKitBottomSheet />);
@@ -552,7 +550,6 @@ describe('LiveKitBottomSheet', () => {
552550
isTalking: false,
553551
hasBluetoothMicrophone: false,
554552
hasBluetoothSpeaker: false,
555-
permissionsRequested: false,
556553
});
557554
});
558555

@@ -568,15 +565,13 @@ describe('LiveKitBottomSheet', () => {
568565
});
569566

570567
it('should track analytics event with connected state', () => {
571-
const requestPermissions = jest.fn();
572568
mockUseLiveKitStore.mockReturnValue({
573569
...defaultLiveKitState,
574570
isBottomSheetVisible: true,
575571
isConnected: true,
576572
currentRoomInfo: mockCurrentRoomInfo,
577573
availableRooms: mockAvailableRooms,
578574
isTalking: true,
579-
requestPermissions,
580575
});
581576

582577
render(<LiveKitBottomSheet />);
@@ -592,12 +587,10 @@ describe('LiveKitBottomSheet', () => {
592587
isTalking: true,
593588
hasBluetoothMicrophone: false,
594589
hasBluetoothSpeaker: false,
595-
permissionsRequested: false,
596590
});
597591
});
598592

599593
it('should track analytics event with bluetooth devices', () => {
600-
const requestPermissions = jest.fn();
601594
const bluetoothAudioDevices = {
602595
microphone: { id: 'bt-mic', name: 'Bluetooth Mic', type: 'bluetooth' as const, isAvailable: true },
603596
speaker: { id: 'bt-speaker', name: 'Bluetooth Speaker', type: 'bluetooth' as const, isAvailable: true },
@@ -607,7 +600,6 @@ describe('LiveKitBottomSheet', () => {
607600
...defaultLiveKitState,
608601
isBottomSheetVisible: true,
609602
availableRooms: mockAvailableRooms,
610-
requestPermissions,
611603
});
612604

613605
mockUseBluetoothAudioStore.mockReturnValue({
@@ -627,7 +619,6 @@ describe('LiveKitBottomSheet', () => {
627619
isTalking: false,
628620
hasBluetoothMicrophone: true,
629621
hasBluetoothSpeaker: true,
630-
permissionsRequested: false,
631622
});
632623
});
633624
});

src/components/livekit/livekit-bottom-sheet.tsx

Lines changed: 3 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export enum BottomSheetView {
2424
}
2525

2626
export const LiveKitBottomSheet = () => {
27-
const { isBottomSheetVisible, setIsBottomSheetVisible, availableRooms, fetchVoiceSettings, connectToRoom, disconnectFromRoom, currentRoomInfo, currentRoom, isConnected, isConnecting, isTalking, requestPermissions } =
27+
const { isBottomSheetVisible, setIsBottomSheetVisible, availableRooms, fetchVoiceSettings, connectToRoom, disconnectFromRoom, currentRoomInfo, currentRoom, isConnected, isConnecting, isTalking } =
2828
useLiveKitStore();
2929

3030
const { selectedAudioDevices } = useBluetoothAudioStore();
@@ -34,7 +34,6 @@ export const LiveKitBottomSheet = () => {
3434
const [currentView, setCurrentView] = useState<BottomSheetView>(BottomSheetView.ROOM_SELECT);
3535
const [previousView, setPreviousView] = useState<BottomSheetView | null>(null);
3636
const [isMuted, setIsMuted] = useState(true); // Default to muted
37-
const [permissionsRequested, setPermissionsRequested] = useState(false);
3837

3938
// Use ref to track if component is mounted to prevent state updates after unmount
4039
const isMountedRef = useRef(true);
@@ -60,7 +59,6 @@ export const LiveKitBottomSheet = () => {
6059
isTalking: isTalking,
6160
hasBluetoothMicrophone: selectedAudioDevices?.microphone?.type === 'bluetooth',
6261
hasBluetoothSpeaker: selectedAudioDevices?.speaker?.type === 'bluetooth',
63-
permissionsRequested: permissionsRequested,
6462
});
6563
}
6664
}, [
@@ -75,53 +73,10 @@ export const LiveKitBottomSheet = () => {
7573
isTalking,
7674
selectedAudioDevices?.microphone?.type,
7775
selectedAudioDevices?.speaker?.type,
78-
permissionsRequested,
7976
]);
8077

81-
// Request permissions when the component becomes visible
82-
useEffect(() => {
83-
if (isBottomSheetVisible && !permissionsRequested && isMountedRef.current) {
84-
// Check if we're in a test environment
85-
const isTestEnvironment = process.env.NODE_ENV === 'test' || process.env.JEST_WORKER_ID !== undefined;
86-
87-
if (isTestEnvironment) {
88-
// In tests, handle permissions synchronously to avoid act warnings
89-
try {
90-
// Call requestPermissions but don't await it in tests
91-
const result = requestPermissions();
92-
// Only call .catch if the result is a promise
93-
if (result && typeof result.catch === 'function') {
94-
result.catch(() => {
95-
// Silently handle any errors in test environment
96-
});
97-
}
98-
setPermissionsRequested(true);
99-
} catch (error) {
100-
console.error('Failed to request permissions:', error);
101-
}
102-
} else {
103-
// In production, use the async approach with timeout
104-
const timeoutId = setTimeout(async () => {
105-
if (isMountedRef.current && !permissionsRequested) {
106-
try {
107-
await requestPermissions();
108-
if (isMountedRef.current) {
109-
setPermissionsRequested(true);
110-
}
111-
} catch (error) {
112-
if (isMountedRef.current) {
113-
console.error('Failed to request permissions:', error);
114-
}
115-
}
116-
}
117-
}, 0);
118-
119-
return () => {
120-
clearTimeout(timeoutId);
121-
};
122-
}
123-
}
124-
}, [isBottomSheetVisible, permissionsRequested, requestPermissions]);
78+
// Note: Permissions are now requested in connectToRoom when the user actually tries to join a voice call
79+
// This ensures permissions are granted before the Android foreground service starts
12580

12681
// Sync mute state with LiveKit room
12782
useEffect(() => {

src/components/ui/skeleton/index.tsx

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { VariantProps } from '@gluestack-ui/nativewind-utils';
2-
import React, { forwardRef } from 'react';
2+
import React, { forwardRef, useEffect, useRef } from 'react';
33
import { Animated, Easing, Platform, View } from 'react-native';
44

55
import { skeletonStyle, skeletonTextStyle } from './styles';
@@ -18,34 +18,48 @@ type ISkeletonTextProps = React.ComponentProps<typeof View> &
1818
};
1919

2020
const Skeleton = forwardRef<React.ElementRef<typeof View>, ISkeletonProps>(({ className, variant, children, startColor = 'bg-background-200', isLoaded = false, speed = 2, ...props }, ref) => {
21-
const pulseAnim = new Animated.Value(1);
21+
const pulseAnim = useRef(new Animated.Value(1)).current;
22+
const animRef = useRef<Animated.CompositeAnimation | null>(null);
2223
const customTimingFunction = Easing.bezier(0.4, 0, 0.6, 1);
2324
const fadeDuration = 0.6;
24-
const animationDuration = (fadeDuration * 10000) / speed; // Convert seconds to milliseconds
25+
const animationDuration = (fadeDuration * 10000) / speed;
2526

26-
const pulse = Animated.sequence([
27-
Animated.timing(pulseAnim, {
28-
toValue: 1, // Start with opacity 1
29-
duration: animationDuration / 2, // Third of the animation duration
30-
easing: customTimingFunction,
31-
useNativeDriver: Platform.OS !== 'web',
32-
}),
33-
Animated.timing(pulseAnim, {
34-
toValue: 0.75,
35-
duration: animationDuration / 2, // Third of the animation duration
36-
easing: customTimingFunction,
37-
useNativeDriver: Platform.OS !== 'web',
38-
}),
39-
Animated.timing(pulseAnim, {
40-
toValue: 1,
41-
duration: animationDuration / 2, // Third of the animation duration
42-
easing: customTimingFunction,
43-
useNativeDriver: Platform.OS !== 'web',
44-
}),
45-
]);
27+
useEffect(() => {
28+
if (!isLoaded) {
29+
const pulse = Animated.sequence([
30+
Animated.timing(pulseAnim, {
31+
toValue: 1,
32+
duration: animationDuration / 2,
33+
easing: customTimingFunction,
34+
useNativeDriver: Platform.OS !== 'web',
35+
}),
36+
Animated.timing(pulseAnim, {
37+
toValue: 0.75,
38+
duration: animationDuration / 2,
39+
easing: customTimingFunction,
40+
useNativeDriver: Platform.OS !== 'web',
41+
}),
42+
Animated.timing(pulseAnim, {
43+
toValue: 1,
44+
duration: animationDuration / 2,
45+
easing: customTimingFunction,
46+
useNativeDriver: Platform.OS !== 'web',
47+
}),
48+
]);
49+
animRef.current = Animated.loop(pulse);
50+
animRef.current.start();
51+
} else {
52+
animRef.current?.stop();
53+
animRef.current = null;
54+
}
55+
56+
return () => {
57+
animRef.current?.stop();
58+
animRef.current = null;
59+
};
60+
}, [isLoaded, animationDuration, pulseAnim, customTimingFunction]);
4661

4762
if (!isLoaded) {
48-
Animated.loop(pulse).start();
4963
return (
5064
<Animated.View
5165
style={{ opacity: pulseAnim }}
@@ -58,8 +72,6 @@ const Skeleton = forwardRef<React.ElementRef<typeof View>, ISkeletonProps>(({ cl
5872
/>
5973
);
6074
} else {
61-
Animated.loop(pulse).stop();
62-
6375
return children;
6476
}
6577
});

0 commit comments

Comments
 (0)