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

Add microphone service #1130

Merged
merged 2 commits into from
Nov 15, 2024
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
1 change: 1 addition & 0 deletions changelog.d/1126.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Correction du crash quand on reçoit ou émet un appel sur Tchap.
9 changes: 8 additions & 1 deletion library/external/realmfieldnameshelper/build.gradle
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

apply plugin: 'kotlin'
apply plugin: 'java'

sourceCompatibility = versions.sourceCompat
targetCompatibility = versions.sourceCompat

kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_17
}
}

dependencies {
implementation 'com.squareup:javapoet:1.13.0'
}
Expand All @@ -20,4 +28,3 @@ task sourcesJar(type: Jar, dependsOn: 'classes') {
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}

1 change: 1 addition & 0 deletions library/ui-strings/src/main/res/values-fr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2186,6 +2186,7 @@
<string name="audio_call_with_participant">Appel audio avec %s</string>
<string name="video_call_with_participant">Appel vidéo avec %s</string>
<string name="call_ringing">Appel en cours…</string>
<string name="microphone_in_use_title">Microphone en cours d’utilisation</string>
<string name="spaces">Espaces</string>
<string name="a11y_stop_voice_message">Arrêter l’enregistrement</string>
<string name="space_add_space_to_any_space_you_manage">Ajouter un espace à un espace que vous gérez.</string>
Expand Down
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
9 changes: 8 additions & 1 deletion vector/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@

<service
android:name=".core.services.CallAndroidService"
android:foregroundServiceType="microphone|phoneCall"
android:foregroundServiceType="phoneCall"
android:exported="false">
<!-- in order to get headset button events -->
<intent-filter>
Expand Down Expand Up @@ -404,6 +404,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>

<!-- 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 @@ -206,6 +207,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 @@ -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
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 @@ -34,6 +34,8 @@ import im.vector.app.features.call.vectorCallService
import im.vector.app.features.session.coroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.logger.LoggerTag
Expand Down Expand Up @@ -137,6 +139,7 @@ class WebRtcCallManager @Inject constructor(
private val rootEglBase by lazy { EglUtils.rootEglBase }

private var isInBackground: Boolean = true
private var syncStartedWhenInBackground: Boolean = false

override fun onResume(owner: LifecycleOwner) {
isInBackground = false
Expand Down Expand Up @@ -279,13 +282,15 @@ class WebRtcCallManager @Inject constructor(
peerConnectionFactory = null
audioManager.setMode(CallAudioManager.Mode.DEFAULT)
// did we start background sync? so we should stop it
if (isInBackground) {
if (syncStartedWhenInBackground) {
if (!unifiedPushHelper.isBackgroundSync()) {
Timber.tag(loggerTag.value).v("Sync started when in background, stop it")
currentSession?.syncService()?.stopAnyBackgroundSync()
} else {
// for fdroid we should not stop, it should continue syncing
// maybe we should restore default timeout/delay though?
}
syncStartedWhenInBackground = false
}
}
}
Expand Down Expand Up @@ -388,11 +393,20 @@ class WebRtcCallManager @Inject constructor(
if (isInBackground) {
if (!unifiedPushHelper.isBackgroundSync()) {
// only for push version as fdroid version is already doing it?
syncStartedWhenInBackground = true
currentSession?.syncService()?.startAutomaticBackgroundSync(30, 0)
} else {
// Maybe increase sync freq? but how to set back to default values?
}
}

// ensure the incoming call will not ring forever
sessionScope?.launch {
delay(2 * 60 * 1000 /* 2 minutes */)
if (mxCall.state is CallState.LocalRinging) {
onCallEnded(mxCall.callId, EndCallReason.INVITE_TIMEOUT, rejected = false)
}
}
}

override fun onCallAnswerReceived(callAnswerContent: CallAnswerContent) {
Expand Down
Loading