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