diff --git a/app/src/main/java/org/linphone/ui/call/view/VuMeterView.kt b/app/src/main/java/org/linphone/ui/call/view/VuMeterView.kt
new file mode 100644
index 000000000..6aa39bf1f
--- /dev/null
+++ b/app/src/main/java/org/linphone/ui/call/view/VuMeterView.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (c) 2010-2025 Belledonne Communications SARL.
+ *
+ * This file is part of linphone-android
+ * (see https://www.linphone.org).
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.linphone.ui.call.view
+
+import android.content.Context
+import android.graphics.BitmapShader
+import android.graphics.Canvas
+import android.graphics.Matrix
+import android.graphics.Paint
+import android.graphics.Shader
+import android.util.AttributeSet
+import android.view.View
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.createBitmap
+import org.linphone.R
+
+class VuMeterView : View {
+ companion object {
+ private const val TAG = "[VuMeter View]"
+ }
+
+ private lateinit var paint: Paint
+ private lateinit var matrix: Matrix
+ private lateinit var vuMeterPaint: Paint
+
+ private val color = ContextCompat.getColor(context, R.color.vu_meter)
+
+ private var vuMeterPercentage: Float = 0f
+
+ constructor(context: Context?) : super(context) {
+ init()
+ }
+
+ constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
+ init()
+ }
+
+ constructor(context: Context?, attrs: AttributeSet?, defStyle: Int) : super(
+ context,
+ attrs,
+ defStyle
+ ) {
+ init()
+ }
+
+ private fun init() {
+ paint = Paint()
+ paint.isAntiAlias = true
+ matrix = Matrix()
+
+ vuMeterPaint = Paint()
+ vuMeterPaint.strokeWidth = 2f
+ vuMeterPaint.isAntiAlias = true
+ vuMeterPaint.setColor(color)
+ }
+
+ fun setVuMeterPercentage(percentage: Float) {
+ vuMeterPercentage = percentage
+ invalidate()
+ }
+
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ super.onSizeChanged(w, h, oldw, oldh)
+ createShader()
+ }
+
+ private fun createShader(): Shader {
+ val level = (height - height * vuMeterPercentage).toFloat()
+
+ val bitmap = createBitmap(width, height)
+ val canvas = Canvas(bitmap)
+ canvas.drawRect(0f, height.toFloat(), width.toFloat(), level, vuMeterPaint)
+
+ val shader = BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.CLAMP)
+ return shader
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ paint.setShader(createShader())
+ canvas.drawCircle(width / 2f, height / 2f, width / 2f, paint)
+ }
+}
diff --git a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt
index 891c3e75e..c76b43771 100644
--- a/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt
+++ b/app/src/main/java/org/linphone/ui/call/viewmodel/CurrentCallViewModel.kt
@@ -29,6 +29,9 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.linphone.LinphoneApplication.Companion.coreContext
@@ -69,6 +72,8 @@ class CurrentCallViewModel
constructor() : GenericViewModel() {
companion object {
private const val TAG = "[Current Call ViewModel]"
+ private const val VU_METER_MIN = -20f
+ private const val VU_METER_MAX = 4
}
val contact = MutableLiveData()
@@ -107,6 +112,8 @@ class CurrentCallViewModel
val isMicrophoneMuted = MutableLiveData()
+ val microphoneRecordingVolume = MutableLiveData()
+
val isSpeakerEnabled = MutableLiveData()
val isHeadsetEnabled = MutableLiveData()
@@ -541,6 +548,7 @@ class CurrentCallViewModel
operationInProgress.value = false
proximitySensorEnabled.value = false
videoUpdateInProgress.value = false
+ microphoneRecordingVolume.value = 0f
coreContext.postOnCoreThread { core ->
hideSipAddresses.postValue(corePreferences.hideSipAddresses)
@@ -1247,6 +1255,13 @@ class CurrentCallViewModel
} else {
Log.i("$TAG Failed to find an existing 1-1 conversation for current call")
}
+
+ microphoneVolumeVuMeterTickerFlow().onEach {
+ coreContext.postOnCoreThread {
+ val volumeDbm0 = currentCall.recordVolume
+ microphoneRecordingVolume.postValue(computeVuMeterValue(volumeDbm0))
+ }
+ }.launchIn(viewModelScope)
}
@WorkerThread
@@ -1519,4 +1534,17 @@ class CurrentCallViewModel
private fun showRecordingToast() {
showGreenToast(R.string.call_is_being_recorded, R.drawable.record_fill)
}
+
+ private fun microphoneVolumeVuMeterTickerFlow() = flow {
+ while (::currentCall.isInitialized) {
+ emit(Unit)
+ delay(50)
+ }
+ }
+
+ private fun computeVuMeterValue(volume: Float): Float {
+ if (volume < VU_METER_MIN) return 0f
+ if (volume > VU_METER_MAX) return 1f
+ return (volume - VU_METER_MIN) / (VU_METER_MAX - VU_METER_MIN)
+ }
}
diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt
index fff752750..aaa67004e 100644
--- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt
+++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt
@@ -69,6 +69,7 @@ import org.linphone.core.ConsolidatedPresence
import org.linphone.core.tools.Log
import org.linphone.ui.NotoSansFont
import org.linphone.ui.call.conference.model.ConferenceParticipantDeviceModel
+import org.linphone.ui.call.view.VuMeterView
import org.linphone.ui.call.view.RoundCornersTextureView
/**
@@ -501,6 +502,12 @@ fun setParticipantTextureView(
model.setTextureView(textureView)
}
+@UiThread
+@BindingAdapter("vuMeterPercentage")
+fun setVuMeterPercentage(view: VuMeterView, percentage: Float) {
+ view.setVuMeterPercentage(percentage)
+}
+
@UiThread
@BindingAdapter("onValueChanged")
fun AppCompatEditText.editTextSetting(lambda: () -> Unit) {
diff --git a/app/src/main/res/layout/call_actions_generic.xml b/app/src/main/res/layout/call_actions_generic.xml
index 3b854ad90..394b249c0 100644
--- a/app/src/main/res/layout/call_actions_generic.xml
+++ b/app/src/main/res/layout/call_actions_generic.xml
@@ -77,6 +77,28 @@
app:layout_constraintStart_toStartOf="@id/toggle_video"
app:layout_constraintEnd_toEndOf="@id/toggle_video"/>
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 10aaf2d1b..7b931aeaa 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -5,6 +5,7 @@
#FFFFFF
#99000000
#10000000
+ #3CFFFFFF
#191919
#303030