Skip to content

Commit ab4291f

Browse files
authored
Merge pull request #8914 from amitshilo11/bugfix/microphone-access-loss-on-background-8881
[Bugfix] Fix microphone loss during background voice calls on Android 14
2 parents f189fa7 + 05be9d0 commit ab4291f

6 files changed

Lines changed: 124 additions & 0 deletions

File tree

library/ui-strings/src/main/res/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,8 @@
652652

653653
<string name="call_remove_jitsi_widget_progress">Ending call…</string>
654654

655+
<string name="microphone_in_use_title">Microphone in use</string>
656+
655657
<!-- permissions Android M -->
656658
<string name="permissions_rationale_popup_title">Information</string>
657659
<!-- Note to translators: the translation MUST contain the string "${app_name}", which will be replaced by the application name -->

vector/src/main/AndroidManifest.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@
5656
<!-- Jitsi SDK is now API23+ -->
5757
<uses-sdk tools:overrideLibrary="com.swmansion.gesturehandler,org.jitsi.meet.sdk,com.oney.WebRTCModule,com.learnium.RNDeviceInfo,com.reactnativecommunity.asyncstorage,com.ocetnik.timer,com.calendarevents,com.reactnativecommunity.netinfo,com.kevinresol.react_native_default_preference,com.rnimmersive,com.rnimmersivemode,com.corbt.keepawake,com.BV.LinearGradient,com.horcrux.svg,com.oblador.performance,com.reactnativecommunity.slider,com.brentvatne.react,com.reactnativecommunity.clipboard,com.swmansion.gesturehandler.react,org.linusu,org.reactnative.maskedview,com.reactnativepagerview,com.swmansion.reanimated,com.th3rdwave.safeareacontext,com.swmansion.rnscreens,org.devio.rn.splashscreen,com.reactnativecommunity.webview,org.wonday.orientation" />
5858

59+
<!-- For MicrophoneAccessService -->
60+
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
61+
5962
<!-- Adding CAMERA permission prevents Chromebooks to see the application on the PlayStore -->
6063
<!-- Tell that the Camera is not mandatory to install the application -->
6164
<uses-feature
@@ -397,6 +400,13 @@
397400
android:foregroundServiceType="mediaProjection"
398401
tools:targetApi="Q" />
399402

403+
<service
404+
android:name=".features.call.audio.MicrophoneAccessService"
405+
android:exported="false"
406+
android:foregroundServiceType="microphone"
407+
android:permission="android.permission.FOREGROUND_SERVICE_MICROPHONE">
408+
</service>
409+
400410
<!-- Receivers -->
401411

402412
<receiver

vector/src/main/java/im/vector/app/core/services/CallAndroidService.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import im.vector.app.core.extensions.singletonEntryPoint
2222
import im.vector.app.core.extensions.startForegroundCompat
2323
import im.vector.app.features.call.CallArgs
2424
import im.vector.app.features.call.VectorCallActivity
25+
import im.vector.app.features.call.audio.MicrophoneAccessService
2526
import im.vector.app.features.call.telecom.CallConnection
2627
import im.vector.app.features.call.webrtc.WebRtcCall
2728
import im.vector.app.features.call.webrtc.WebRtcCallManager
@@ -199,6 +200,9 @@ class CallAndroidService : VectorAndroidService() {
199200
stopForegroundCompat()
200201
mediaSession?.isActive = false
201202
myStopSelf()
203+
204+
// Also stop the microphone service if it is running
205+
stopService(Intent(this, MicrophoneAccessService::class.java))
202206
}
203207
val wasConnected = connectedCallIds.remove(callId)
204208
if (!wasConnected && !terminatedCall.isOutgoing && !rejected && endCallReason != EndCallReason.ANSWERED_ELSEWHERE) {

vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77

88
package im.vector.app.features.call
99

10+
import android.Manifest
1011
import android.app.Activity
1112
import android.app.KeyguardManager
1213
import android.app.PictureInPictureParams
1314
import android.content.Context
1415
import android.content.Intent
1516
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP
17+
import android.content.pm.PackageManager
1618
import android.graphics.Color
1719
import android.media.projection.MediaProjection
1820
import android.media.projection.MediaProjectionManager
@@ -31,6 +33,8 @@ import androidx.core.content.getSystemService
3133
import androidx.core.util.Consumer
3234
import androidx.core.view.isInvisible
3335
import androidx.core.view.isVisible
36+
import androidx.lifecycle.Lifecycle
37+
import androidx.lifecycle.ProcessLifecycleOwner
3438
import com.airbnb.mvrx.Fail
3539
import com.airbnb.mvrx.Mavericks
3640
import com.airbnb.mvrx.viewModel
@@ -48,6 +52,7 @@ import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL
4852
import im.vector.app.core.utils.checkPermissions
4953
import im.vector.app.core.utils.registerForPermissionsResult
5054
import im.vector.app.databinding.ActivityCallBinding
55+
import im.vector.app.features.call.audio.MicrophoneAccessService
5156
import im.vector.app.features.call.dialpad.CallDialPadBottomSheet
5257
import im.vector.app.features.call.dialpad.DialPadFragment
5358
import im.vector.app.features.call.transfer.CallTransferActivity
@@ -236,6 +241,43 @@ class VectorCallActivity :
236241
}
237242
}
238243

244+
private fun startMicrophoneService() {
245+
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
246+
== PackageManager.PERMISSION_GRANTED) {
247+
248+
// Only start the service if the app is in the foreground
249+
if (isAppInForeground()) {
250+
Timber.tag(loggerTag.value).v("Starting microphone foreground service")
251+
val intent = Intent(this, MicrophoneAccessService::class.java)
252+
ContextCompat.startForegroundService(this, intent)
253+
} else {
254+
Timber.tag(loggerTag.value).v("App is not in foreground; cannot start microphone service")
255+
}
256+
} else {
257+
Timber.tag(loggerTag.value).v("Microphone permission not granted; cannot start service")
258+
}
259+
}
260+
261+
private fun isAppInForeground(): Boolean {
262+
val appProcess = ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
263+
return appProcess
264+
}
265+
private fun stopMicrophoneService() {
266+
Timber.tag(loggerTag.value).d("Stopping MicrophoneAccessService (if needed).")
267+
val intent = Intent(this, MicrophoneAccessService::class.java)
268+
stopService(intent)
269+
}
270+
271+
override fun onPause() {
272+
super.onPause()
273+
startMicrophoneService()
274+
}
275+
276+
override fun onResume() {
277+
super.onResume()
278+
stopMicrophoneService()
279+
}
280+
239281
override fun onDestroy() {
240282
detachRenderersIfNeeded()
241283
turnScreenOffAndKeyguardOn()
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright (c) 2024 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package im.vector.app.features.call.audio
18+
19+
import android.content.Intent
20+
import android.os.Binder
21+
import android.os.IBinder
22+
import dagger.hilt.android.AndroidEntryPoint
23+
import im.vector.app.core.extensions.startForegroundCompat
24+
import im.vector.app.core.services.VectorAndroidService
25+
import im.vector.app.features.notifications.NotificationUtils
26+
import javax.inject.Inject
27+
28+
@AndroidEntryPoint
29+
class MicrophoneAccessService : VectorAndroidService() {
30+
31+
@Inject lateinit var notificationUtils: NotificationUtils
32+
private val binder = LocalBinder()
33+
34+
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
35+
showMicrophoneAccessNotification()
36+
37+
return START_STICKY
38+
}
39+
40+
private fun showMicrophoneAccessNotification() {
41+
val notificationId = System.currentTimeMillis().toInt()
42+
val notification = notificationUtils.buildMicrophoneAccessNotification()
43+
startForegroundCompat(notificationId, notification)
44+
}
45+
46+
override fun onBind(intent: Intent?): IBinder {
47+
return binder
48+
}
49+
50+
inner class LocalBinder : Binder() {
51+
fun getService(): MicrophoneAccessService = this@MicrophoneAccessService
52+
}
53+
}

vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,19 @@ class NotificationUtils @Inject constructor(
529529
.build()
530530
}
531531

532+
/**
533+
* Creates a notification indicating that the microphone is currently being accessed by the application.
534+
*/
535+
fun buildMicrophoneAccessNotification(): Notification {
536+
return NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID)
537+
.setContentTitle(stringProvider.getString(CommonStrings.microphone_in_use_title))
538+
.setSmallIcon(R.drawable.ic_call_answer)
539+
.setPriority(NotificationCompat.PRIORITY_LOW)
540+
.setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary))
541+
.setCategory(NotificationCompat.CATEGORY_CALL)
542+
.build()
543+
}
544+
532545
/**
533546
* Creates a notification that indicates the application is initializing.
534547
*/

0 commit comments

Comments
 (0)