From 61fe57628faccd003d1e283c4f46740128d8a7cd Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 16 Apr 2024 11:48:34 +0200 Subject: [PATCH] Improved PiP --- .../compatibility/Api28Compatibility.kt | 20 +++++++++ .../compatibility/Api31Compatibility.kt | 12 +++++ .../linphone/compatibility/Compatibility.kt | 14 ++++++ .../java/org/linphone/ui/call/CallActivity.kt | 45 ++++++++++--------- .../org/linphone/utils/DataBindingUtils.kt | 19 ++++++++ .../call_active_conference_fragment.xml | 4 +- .../main/res/layout/call_active_fragment.xml | 6 ++- app/src/main/res/values/dimen.xml | 3 ++ 8 files changed, 97 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/org/linphone/compatibility/Api28Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api28Compatibility.kt index 8175b495b..5fd108926 100644 --- a/app/src/main/java/org/linphone/compatibility/Api28Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Api28Compatibility.kt @@ -19,11 +19,14 @@ */ package org.linphone.compatibility +import android.app.Activity import android.app.Notification +import android.app.PictureInPictureParams import android.app.Service import android.net.Uri import android.provider.MediaStore import org.linphone.core.tools.Log +import org.linphone.utils.AppUtils class Api28Compatibility { companion object { @@ -40,6 +43,23 @@ class Api28Compatibility { } } + fun enterPipMode(activity: Activity): Boolean { + val params = PictureInPictureParams.Builder() + .setAspectRatio(AppUtils.getPipRatio(activity)) + .build() + try { + if (!activity.enterPictureInPictureMode(params)) { + Log.e("$TAG Failed to enter PiP mode") + } else { + Log.i("$TAG Entered PiP mode") + return true + } + } catch (e: Exception) { + Log.e("$TAG Can't build PiP params: $e") + } + return false + } + fun getMediaCollectionUri(isImage: Boolean, isVideo: Boolean, isAudio: Boolean): Uri { return when { isImage -> { diff --git a/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt index aa0679abf..c4b93f314 100644 --- a/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Api31Compatibility.kt @@ -19,6 +19,8 @@ */ package org.linphone.compatibility +import android.app.Activity +import android.app.PictureInPictureParams import android.app.UiModeManager import android.content.Context import android.graphics.RenderEffect @@ -28,12 +30,22 @@ import android.view.View import androidx.annotation.RequiresApi import androidx.core.content.ContextCompat import org.linphone.core.tools.Log +import org.linphone.utils.AppUtils @RequiresApi(Build.VERSION_CODES.S) class Api31Compatibility { companion object { private const val TAG = "[API 31 Compatibility]" + fun enableAutoEnterPiP(activity: Activity, enable: Boolean) { + activity.setPictureInPictureParams( + PictureInPictureParams.Builder() + .setAspectRatio(AppUtils.getPipRatio(activity)) + .setAutoEnterEnabled(enable) + .build() + ) + } + fun setBlurRenderEffect(view: View) { val blurEffect = RenderEffect.createBlurEffect(16F, 16F, Shader.TileMode.MIRROR) view.setRenderEffect(blurEffect) diff --git a/app/src/main/java/org/linphone/compatibility/Compatibility.kt b/app/src/main/java/org/linphone/compatibility/Compatibility.kt index 385c0ee56..51e6e19a9 100644 --- a/app/src/main/java/org/linphone/compatibility/Compatibility.kt +++ b/app/src/main/java/org/linphone/compatibility/Compatibility.kt @@ -21,6 +21,7 @@ package org.linphone.compatibility import android.Manifest import android.annotation.SuppressLint +import android.app.Activity import android.app.Notification import android.app.Service import android.content.Context @@ -108,6 +109,19 @@ class Compatibility { return false } + fun enterPipMode(activity: Activity): Boolean { + if (Version.sdkStrictlyBelow(Version.API31_ANDROID_12)) { + return Api28Compatibility.enterPipMode(activity) + } + return activity.isInPictureInPictureMode + } + + fun enableAutoEnterPiP(activity: Activity, enable: Boolean) { + if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) { + Api31Compatibility.enableAutoEnterPiP(activity, enable) + } + } + fun forceDarkMode(context: Context) { Log.i("$TAG Forcing dark/night theme") if (Version.sdkAboveOrEqual(Version.API31_ANDROID_12)) { diff --git a/app/src/main/java/org/linphone/ui/call/CallActivity.kt b/app/src/main/java/org/linphone/ui/call/CallActivity.kt index 2b8d833b4..24d786b9b 100644 --- a/app/src/main/java/org/linphone/ui/call/CallActivity.kt +++ b/app/src/main/java/org/linphone/ui/call/CallActivity.kt @@ -20,7 +20,6 @@ package org.linphone.ui.call import android.Manifest -import android.app.PictureInPictureParams import android.content.Intent import android.content.pm.PackageManager import android.os.Bundle @@ -44,6 +43,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.linphone.LinphoneApplication.Companion.coreContext import org.linphone.R +import org.linphone.compatibility.Compatibility import org.linphone.core.tools.Log import org.linphone.databinding.CallActivityBinding import org.linphone.ui.GenericActivity @@ -57,7 +57,6 @@ import org.linphone.ui.call.model.AudioDeviceModel import org.linphone.ui.call.viewmodel.CallsViewModel import org.linphone.ui.call.viewmodel.CurrentCallViewModel import org.linphone.ui.call.viewmodel.SharedCallViewModel -import org.linphone.utils.AppUtils import org.linphone.utils.ToastUtils import org.linphone.utils.slideInToastFromTop import org.linphone.utils.slideInToastFromTopForDuration @@ -76,6 +75,8 @@ class CallActivity : GenericActivity() { private var bottomSheetDialog: BottomSheetDialogFragment? = null + private var isPipSupported = false + private val requestCameraPermissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted -> @@ -113,6 +114,11 @@ class CallActivity : GenericActivity() { } } + isPipSupported = packageManager.hasSystemFeature( + PackageManager.FEATURE_PICTURE_IN_PICTURE + ) + Log.i("$TAG Is PiP supported [$isPipSupported]") + sharedViewModel = run { ViewModelProvider(this)[SharedCallViewModel::class.java] } @@ -139,6 +145,13 @@ class CallActivity : GenericActivity() { } } + callViewModel.isVideoEnabled.observe(this) { enabled -> + if (isPipSupported) { + // Only enable PiP if video is enabled + Compatibility.enableAutoEnterPiP(this, enabled) + } + } + callViewModel.transferInProgressEvent.observe(this) { it.consume { showGreenToast( @@ -249,8 +262,8 @@ class CallActivity : GenericActivity() { super.onResume() val isInPipMode = isInPictureInPictureMode + Log.i("$TAG onResume: is in PiP mode? [$isInPipMode]") if (::callViewModel.isInitialized) { - Log.i("$TAG onResume: is in PiP mode? $isInPipMode") callViewModel.pipMode.value = isInPipMode } } @@ -287,25 +300,13 @@ class CallActivity : GenericActivity() { override fun onUserLeaveHint() { super.onUserLeaveHint() - if (::callViewModel.isInitialized && callViewModel.isVideoEnabled.value == true) { - Log.i("$TAG User leave hint, entering PiP mode") - val supportsPip = packageManager.hasSystemFeature( - PackageManager.FEATURE_PICTURE_IN_PICTURE - ) - Log.i("$TAG Is PiP supported: $supportsPip") - if (supportsPip) { - val params = PictureInPictureParams.Builder() - .setAspectRatio(AppUtils.getPipRatio(this)) - .build() - try { - if (!enterPictureInPictureMode(params)) { - Log.e("$TAG Failed to enter PiP mode") - callViewModel.pipMode.value = false - } else { - Log.i("$TAG Entered PiP mode") - } - } catch (e: Exception) { - Log.e("$TAG Can't build PiP params: $e") + if (::callViewModel.isInitialized) { + if (isPipSupported && callViewModel.isVideoEnabled.value == true) { + Log.i("$TAG User leave hint, entering PiP mode") + val pipMode = Compatibility.enterPipMode(this) + if (!pipMode) { + Log.e("$TAG Failed to enter PiP mode") + callViewModel.pipMode.value = false } } } diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index 5da4b134d..3b3a261bf 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -40,6 +40,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.AppCompatEditText import androidx.appcompat.widget.AppCompatTextView import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.doOnLayout @@ -495,6 +496,24 @@ fun setConstraintLayoutChildHorizontalBias(view: View, horizontalBias: Float) { view.layoutParams = params } +@BindingAdapter("layout_constraintHeight_max") +fun setConstraintLayoutHeightMax(view: View, dp: Float) { + val layout = view.parent as ConstraintLayout + val cs = ConstraintSet() + cs.clone(layout) + cs.constrainMaxHeight(view.id, dp.toInt()) + cs.applyTo(layout) +} + +@BindingAdapter("layout_constraintWidth_max") +fun setConstraintLayoutWidthMax(view: View, dp: Float) { + val layout = view.parent as ConstraintLayout + val cs = ConstraintSet() + cs.clone(layout) + cs.constrainMaxWidth(view.id, dp.toInt()) + cs.applyTo(layout) +} + @BindingAdapter("focusNextOnInput") fun focusNextOnInput(editText: EditText, enabled: Boolean) { if (!enabled) return diff --git a/app/src/main/res/layout/call_active_conference_fragment.xml b/app/src/main/res/layout/call_active_conference_fragment.xml index 0decc5c9f..d8a3e9eed 100644 --- a/app/src/main/res/layout/call_active_conference_fragment.xml +++ b/app/src/main/res/layout/call_active_conference_fragment.xml @@ -246,8 +246,8 @@ app:displayMode="black_bars" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintHeight_max="200dp" - app:layout_constraintWidth_max="200dp" /> + app:layout_constraintHeight_max="@{viewModel.pipMode ? @dimen/call_video_preview_pip_max_size : @dimen/call_video_preview_max_size, default=@dimen/call_video_preview_max_size}" + app:layout_constraintWidth_max="@{viewModel.pipMode ? @dimen/call_video_preview_pip_max_size : @dimen/call_video_preview_max_size, default=@dimen/call_video_preview_max_size}" /> + app:layout_constraintHeight_max="@{viewModel.pipMode ? @dimen/call_video_preview_pip_max_size : @dimen/call_video_preview_max_size, default=@dimen/call_video_preview_max_size}" + app:layout_constraintWidth_max="@{viewModel.pipMode ? @dimen/call_video_preview_pip_max_size : @dimen/call_video_preview_max_size, default=@dimen/call_video_preview_max_size}" /> 15dp 30dp + 200dp + 45dp + 100dp 120dp