Skip to content

Commit 28096ad

Browse files
authored
fix: improve android wake lock and power manager handling RN-291 (#1990)
### 💡 Overview Follow up to #1971 Fixes the following issues on android: * Proximity stuff (turning off screen when kept closed to body for earpiece output) - should be done only for communicator mode * `getAudioDeviceStatus` method was somehow removed from callmanager native side. This PR adds it back. * Earpiece detection was using older APIs on newer android. This PR makes sure newer APIs are used. * Ringing calls started with earpiece. No need to do so. Changed to speaker now. * adds the necessary `android.permission.WAKE_LOCK` to the android library extra: comments to clarify impl in iOS ### 📝 Implementation notes 🎫 Ticket: https://linear.app/stream/issue/XYZ-123 📑 Docs: https://github.com/GetStream/docs-content/pull/<id>
1 parent 7803f2c commit 28096ad

File tree

6 files changed

+28
-40
lines changed

6 files changed

+28
-40
lines changed

packages/react-native-sdk/android/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33

44
<uses-permission android:name="android.permission.INTERNET" />
55
<uses-permission android:name="android.permission.DEVICE_POWER" />
6+
<uses-permission android:name="android.permission.WAKE_LOCK" />
67
</manifest>

packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/audio/AudioDeviceManager.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import com.streamvideo.reactnative.audio.utils.AudioManagerUtil
3333
import com.streamvideo.reactnative.audio.utils.AudioManagerUtil.Companion.getAvailableAudioDevices
3434
import com.streamvideo.reactnative.audio.utils.AudioSetupStoreUtil
3535
import com.streamvideo.reactnative.audio.utils.CallAudioRole
36+
import com.streamvideo.reactnative.callmanager.ProximityManager
3637
import com.streamvideo.reactnative.callmanager.StreamInCallManagerModule
3738
import com.streamvideo.reactnative.model.AudioDeviceEndpoint
3839
import com.streamvideo.reactnative.model.AudioDeviceEndpoint.Companion.EndpointType
@@ -70,12 +71,15 @@ class AudioDeviceManager(
7071

7172
/** Returns the currently selected audio device. */
7273
private var _selectedAudioDeviceEndpoint: AudioDeviceEndpoint? = null
73-
private var selectedAudioDeviceEndpoint: AudioDeviceEndpoint?
74+
var selectedAudioDeviceEndpoint: AudioDeviceEndpoint?
7475
get() = _selectedAudioDeviceEndpoint
7576
set(value) {
7677
_selectedAudioDeviceEndpoint = value
7778
// send an event to the frontend everytime this endpoint changes
7879
sendAudioStatusEvent()
80+
if (callAudioRole == CallAudioRole.Communicator) {
81+
proximityManager.update()
82+
}
7983
}
8084

8185
// Default audio device; speaker phone for video calls or earpiece for audio only phone calls
@@ -101,6 +105,8 @@ class AudioDeviceManager(
101105

102106
val bluetoothManager = BluetoothManager(mReactContext, this)
103107

108+
private val proximityManager by lazy { ProximityManager(mReactContext, this) }
109+
104110
init {
105111
// Note that we will immediately receive a call to onDevicesAdded with the list of
106112
// devices which are currently connected.
@@ -120,6 +126,7 @@ class AudioDeviceManager(
120126
bluetoothManager.start()
121127
mAudioManager.registerAudioDeviceCallback(this, null)
122128
updateAudioDeviceState()
129+
proximityManager.start()
123130
} else {
124131
// Audio routing is handled automatically by the system in normal media mode
125132
// and bluetooth microphones may not work on some devices.
@@ -141,6 +148,7 @@ class AudioDeviceManager(
141148
mAudioManager.setSpeakerphoneOn(false)
142149
}
143150
bluetoothManager.stop()
151+
proximityManager.stop()
144152
}
145153
audioSetupStoreUtil.restoreOriginalAudioSetup()
146154
audioFocusUtil.abandonFocus()
@@ -221,6 +229,7 @@ class AudioDeviceManager(
221229

222230
override fun close() {
223231
mAudioManager.unregisterAudioDeviceCallback(this)
232+
proximityManager.onDestroy()
224233
}
225234

226235
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>?) {
@@ -522,7 +531,7 @@ class AudioDeviceManager(
522531
}
523532
}
524533

525-
private fun audioStatusMap(): WritableMap {
534+
fun audioStatusMap(): WritableMap {
526535
val endpoint = this.selectedAudioDeviceEndpoint
527536
val availableEndpoints = Arguments.fromList(getCurrentDeviceEndpoints().map { it.name })
528537

packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/callmanager/ProximityManager.kt

Lines changed: 4 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import android.media.AudioDeviceInfo
88
import android.media.AudioManager
99
import android.os.PowerManager
1010
import android.util.Log
11+
import com.streamvideo.reactnative.audio.AudioDeviceManager
1112

1213
/**
1314
* Encapsulates Android proximity sensor handling for in-call UX.
@@ -20,6 +21,7 @@ import android.util.Log
2021
*/
2122
class ProximityManager(
2223
private val context: Context,
24+
private val audioDeviceManager: AudioDeviceManager,
2325
) {
2426

2527
companion object {
@@ -67,10 +69,7 @@ class ProximityManager(
6769
}
6870
try {
6971
powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager
70-
// Obtain PROXIMITY_SCREEN_OFF_WAKE_LOCK via reflection to avoid compile-time dependency
71-
val field = PowerManager::class.java.getField("PROXIMITY_SCREEN_OFF_WAKE_LOCK")
72-
val level = field.getInt(null)
73-
proximityWakeLock = powerManager?.newWakeLock(level, "$TAG:Proximity")
72+
proximityWakeLock = powerManager?.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, "$TAG:Proximity")
7473
} catch (t: Throwable) {
7574
Log.w(TAG, "Proximity wakelock init failed (may be unsupported on this device)", t)
7675
proximityWakeLock = null
@@ -156,28 +155,6 @@ class ProximityManager(
156155
}
157156

158157
private fun isOnEarpiece(): Boolean {
159-
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
160-
// If speakerphone is on, not earpiece
161-
if (audioManager.isSpeakerphoneOn) return false
162-
163-
// Check if Bluetooth SCO/A2DP or wired headset is connected
164-
var hasBt = false
165-
var hasWired = false
166-
val outputs = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
167-
outputs.forEach { dev ->
168-
val type = dev.type
169-
if (type == AudioDeviceInfo.TYPE_BLUETOOTH_A2DP ||
170-
type == AudioDeviceInfo.TYPE_BLUETOOTH_SCO
171-
) {
172-
hasBt = true
173-
} else if (type == AudioDeviceInfo.TYPE_WIRED_HEADPHONES
174-
|| type == AudioDeviceInfo.TYPE_WIRED_HEADSET
175-
|| type == AudioDeviceInfo.TYPE_USB_HEADSET
176-
) {
177-
hasWired = true
178-
}
179-
}
180-
181-
return !hasBt && !hasWired
158+
return audioDeviceManager.selectedAudioDeviceEndpoint?.isEarpieceType() ?: false
182159
}
183160
}

packages/react-native-sdk/android/src/main/java/com/streamvideo/reactnative/callmanager/StreamInCallManagerModule.kt

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.streamvideo.reactnative.callmanager
33
import android.util.Log
44
import android.view.WindowManager
55
import com.facebook.react.bridge.LifecycleEventListener
6+
import com.facebook.react.bridge.Promise
67
import com.facebook.react.bridge.ReactApplicationContext
78
import com.facebook.react.bridge.ReactContextBaseJavaModule
89
import com.facebook.react.bridge.ReactMethod
@@ -20,7 +21,6 @@ class StreamInCallManagerModule(reactContext: ReactApplicationContext) :
2021
private var audioManagerActivated = false
2122

2223
private val mAudioDeviceManager = AudioDeviceManager(reactContext)
23-
private val proximityManager = ProximityManager(reactContext)
2424

2525
override fun getName(): String {
2626
return TAG
@@ -89,8 +89,6 @@ class StreamInCallManagerModule(reactContext: ReactApplicationContext) :
8989
mAudioDeviceManager.start(it)
9090
setKeepScreenOn(true)
9191
audioManagerActivated = true
92-
// Initialize and evaluate proximity monitoring via controller
93-
proximityManager.start()
9492
}
9593
}
9694
}
@@ -103,8 +101,6 @@ class StreamInCallManagerModule(reactContext: ReactApplicationContext) :
103101
Log.d(TAG, "stop() mAudioDeviceManager")
104102
mAudioDeviceManager.stop()
105103
setMicrophoneMute(false)
106-
// Disable proximity monitoring via controller and clear keep-screen-on
107-
proximityManager.stop()
108104
setKeepScreenOn(false)
109105
audioManagerActivated = false
110106
}
@@ -133,15 +129,18 @@ class StreamInCallManagerModule(reactContext: ReactApplicationContext) :
133129
return
134130
}
135131
mAudioDeviceManager.setSpeakerphoneOn(enable)
136-
// Re-evaluate proximity monitoring when route may change
137-
this.proximityManager.update()
138132
}
139133

140134
@ReactMethod
141135
fun setMicrophoneMute(enable: Boolean) {
142136
mAudioDeviceManager.setMicrophoneMute(enable)
143137
}
144138

139+
@ReactMethod
140+
fun getAudioDeviceStatus(promise: Promise) {
141+
promise.resolve(mAudioDeviceManager.audioStatusMap())
142+
}
143+
145144
@ReactMethod
146145
fun logAudioState() {
147146
WebRtcAudioUtils.logAudioState(
@@ -160,8 +159,6 @@ class StreamInCallManagerModule(reactContext: ReactApplicationContext) :
160159
mAudioDeviceManager.switchDeviceFromDeviceName(
161160
endpointDeviceName
162161
)
163-
// Re-evaluate proximity monitoring when endpoint changes
164-
this.proximityManager.update()
165162
}
166163

167164
@ReactMethod

packages/react-native-sdk/ios/StreamInCallManager.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,11 @@ class StreamInCallManager: RCTEventEmitter {
7979
options: session.categoryOptions
8080
)
8181
configureAudioSession()
82-
// Enable wake lock to prevent the screen from dimming/locking during a call
82+
8383
DispatchQueue.main.async {
84+
// Enable wake lock to prevent the screen from dimming/locking during a call
8485
UIApplication.shared.isIdleTimerDisabled = true
86+
// Register for audio route changes to turn off screen when earpiece is connected
8587
self.registerAudioRouteObserver()
8688
self.updateProximityMonitoring()
8789
self.log("Wake lock enabled (idle timer disabled)")
@@ -120,8 +122,10 @@ class StreamInCallManager: RCTEventEmitter {
120122
}
121123
// Disable wake lock and proximity when call manager stops so the device can sleep again
122124
DispatchQueue.main.async {
125+
// Disable proximity monitoring to disable earpiece detection
123126
self.setProximityMonitoringEnabled(false)
124127
self.unregisterAudioRouteObserver()
128+
// Disable wake lock to allow the screen to dim/lock again
125129
UIApplication.shared.isIdleTimerDisabled = false
126130
self.log("Wake lock disabled (idle timer enabled)")
127131
}

sample-apps/react-native/dogfood/src/navigators/Call.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ const CallLeaveOnUnmount = ({ call }: { call: StreamCallType }) => {
4646
useEffect(() => {
4747
callManager.start({
4848
audioRole: 'communicator',
49-
deviceEndpointType: 'earpiece',
49+
deviceEndpointType: 'speaker',
5050
});
5151
return () => {
5252
callManager.stop();

0 commit comments

Comments
 (0)