Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bugfix] Fix microphone loss during background voice calls on Android 14 #8914

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions library/ui-strings/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,8 @@

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

<string name="microphone_in_use_title">Microphone in use</string>

<!-- permissions Android M -->
<string name="permissions_rationale_popup_title">Information</string>
<!-- Note to translators: the translation MUST contain the string "${app_name}", which will be replaced by the application name -->
Expand Down
10 changes: 10 additions & 0 deletions vector/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@
<!-- Jitsi SDK is now API23+ -->
<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" />

<!-- For MicrophoneAccessService -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />

<!-- Adding CAMERA permission prevents Chromebooks to see the application on the PlayStore -->
<!-- Tell that the Camera is not mandatory to install the application -->
<uses-feature
Expand Down Expand Up @@ -395,6 +398,13 @@
android:foregroundServiceType="mediaProjection"
tools:targetApi="Q" />

<service
android:name=".features.call.audio.MicrophoneAccessService"
android:exported="false"
android:foregroundServiceType="microphone"
android:permission="android.permission.FOREGROUND_SERVICE_MICROPHONE">
</service>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you need to declare the permission as well. You can add around line 56:

    <!-- For MicrophoneAccessService -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />

It is working since this permission comes with the Jitsi library, but better to explicitely add it in our manifest.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did it and pushed, tell me if there is any more comments of improvement that need to be done :)

<!-- Receivers -->

<receiver
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.core.extensions.startForegroundCompat
import im.vector.app.features.call.CallArgs
import im.vector.app.features.call.VectorCallActivity
import im.vector.app.features.call.audio.MicrophoneAccessService
import im.vector.app.features.call.telecom.CallConnection
import im.vector.app.features.call.webrtc.WebRtcCall
import im.vector.app.features.call.webrtc.WebRtcCallManager
Expand Down Expand Up @@ -208,6 +209,9 @@ class CallAndroidService : VectorAndroidService() {
stopForegroundCompat()
mediaSession?.isActive = false
myStopSelf()

// Also stop the microphone service if it is running
stopService(Intent(this, MicrophoneAccessService::class.java))
}
val wasConnected = connectedCallIds.remove(callId)
if (!wasConnected && !terminatedCall.isOutgoing && !rejected && endCallReason != EndCallReason.ANSWERED_ELSEWHERE) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@

package im.vector.app.features.call

import android.Manifest
import android.app.Activity
import android.app.KeyguardManager
import android.app.PictureInPictureParams
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP
import android.content.pm.PackageManager
import android.graphics.Color
import android.media.projection.MediaProjection
import android.media.projection.MediaProjectionManager
Expand All @@ -40,6 +42,8 @@ import androidx.core.content.getSystemService
import androidx.core.util.Consumer
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ProcessLifecycleOwner
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Mavericks
import com.airbnb.mvrx.viewModel
Expand All @@ -57,6 +61,7 @@ import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL
import im.vector.app.core.utils.checkPermissions
import im.vector.app.core.utils.registerForPermissionsResult
import im.vector.app.databinding.ActivityCallBinding
import im.vector.app.features.call.audio.MicrophoneAccessService
import im.vector.app.features.call.dialpad.CallDialPadBottomSheet
import im.vector.app.features.call.dialpad.DialPadFragment
import im.vector.app.features.call.transfer.CallTransferActivity
Expand Down Expand Up @@ -245,6 +250,43 @@ class VectorCallActivity :
}
}

private fun startMicrophoneService() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)
== PackageManager.PERMISSION_GRANTED) {

// Only start the service if the app is in the foreground
if (isAppInForeground()) {
Timber.tag(loggerTag.value).v("Starting microphone foreground service")
val intent = Intent(this, MicrophoneAccessService::class.java)
ContextCompat.startForegroundService(this, intent)
} else {
Timber.tag(loggerTag.value).v("App is not in foreground; cannot start microphone service")
}
} else {
Timber.tag(loggerTag.value).v("Microphone permission not granted; cannot start service")
}
}

private fun isAppInForeground(): Boolean {
val appProcess = ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)
return appProcess
}
private fun stopMicrophoneService() {
Timber.tag(loggerTag.value).d("Stopping MicrophoneAccessService (if needed).")
val intent = Intent(this, MicrophoneAccessService::class.java)
stopService(intent)
}

override fun onPause() {
super.onPause()
startMicrophoneService()
}

override fun onResume() {
super.onResume()
stopMicrophoneService()
}

override fun onDestroy() {
detachRenderersIfNeeded()
turnScreenOffAndKeyguardOn()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package im.vector.app.features.call.audio

import android.content.Intent
import android.os.Binder
import android.os.IBinder
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.extensions.startForegroundCompat
import im.vector.app.core.services.VectorAndroidService
import im.vector.app.features.notifications.NotificationUtils
import javax.inject.Inject

@AndroidEntryPoint
class MicrophoneAccessService : VectorAndroidService() {

@Inject lateinit var notificationUtils: NotificationUtils
private val binder = LocalBinder()

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
showMicrophoneAccessNotification()

return START_STICKY
}

private fun showMicrophoneAccessNotification() {
val notificationId = System.currentTimeMillis().toInt()
val notification = notificationUtils.buildMicrophoneAccessNotification()
startForegroundCompat(notificationId, notification)
}

override fun onBind(intent: Intent?): IBinder {
return binder
}

inner class LocalBinder : Binder() {
fun getService(): MicrophoneAccessService = this@MicrophoneAccessService
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,19 @@ class NotificationUtils @Inject constructor(
.build()
}

/**
* Creates a notification indicating that the microphone is currently being accessed by the application.
*/
fun buildMicrophoneAccessNotification(): Notification {
return NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID)
.setContentTitle(stringProvider.getString(CommonStrings.microphone_in_use_title))
.setSmallIcon(R.drawable.ic_call_answer)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary))
.setCategory(NotificationCompat.CATEGORY_CALL)
.build()
}

/**
* Creates a notification that indicates the application is initializing.
*/
Expand Down
Loading